diff --git a/.changeset/runtime-resolves.md b/.changeset/runtime-resolves.md new file mode 100644 index 00000000..fb7dd4a1 --- /dev/null +++ b/.changeset/runtime-resolves.md @@ -0,0 +1,40 @@ +--- +"@btravstack/contract": minor +"@btravstack/di": minor +"@btravstack/config": minor +"@btravstack/core": minor +"@btravstack/testing": minor +"@btravstack/observability": minor +"@btravstack/http": minor +"@btravstack/temporal": minor +"@btravstack/amqp": minor +--- + +`Runtime.needs` is `Runtime.resolves` + +Two different `needs` in one framework was one too many. di's `Module` has a +`needs` — what a composition root supplies it — and the kernel's `Runtime` had +one too, meaning something else entirely: the ports the runtime reads back out +of the built application context. They never appear in the same object, which +is exactly why the collision was easy to miss and easy to misread. + +```ts +const runtime: Runtime = { + name: "ticker", + resolves: [Clock], + start: (host) => OkAsync(serving), +}; +``` + +The type parameter is `Resolves` rather than `Needs` throughout — +`Runtime`, `RuntimeHost`, `RunUnit` — and +`start`'s gate sentence follows: +`"UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export"`. + +Every shipped runtime declares `resolves: []`, so an application that composes +`http()` / `temporal()` / `amqp()` and never writes a runtime by hand is +unaffected. A **hand-rolled** runtime renames one field. + +The array is still never read at run time — it exists so `Resolves` is +inferable from the value, and `start`'s gate checks it against the module's +exports. diff --git a/CLAUDE.md b/CLAUDE.md index f141b86b..2942383c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -444,7 +444,7 @@ type checker already verifies. exactly as `packages/core` would. Three of the four needs-gate files pin **`start`'s** gate (`order-api`, `order-temporal-worker`, `order-amqp-worker` — its `NO RUNTIME` arm, since no starter's runtime - declares a `needs` any more; `order-api`'s also pins the `unit` halves) and + resolves anything any more; `order-api`'s also pins the `unit` halves) and the **unmet need** on the starter's port (a composition importing `http()` / `temporal({ contract, workflows })` / `amqp({ contract })` without providing the router / activities / handlers carries the starter's port in `Needs`, and @@ -462,8 +462,8 @@ type checker already verifies. **Four** mechanisms, easy to conflate — and only two print a name. Do not call the second "di's `UNSATISFIED DEPENDENCIES` gate": an earlier revision of this file did, and it is wrong in both halves. `start`'s - `UNSATISFIED RUNTIME NEEDS` arm is pinned only by `packages/core`'s own - `start.test-d.ts`, since every shipped runtime declares `needs: []`. + `UNSATISFIED RUNTIME PORTS` arm is pinned only by `packages/core`'s own + `start.test-d.ts`, since every shipped runtime declares `resolves: []`. `examples/` is not the only place the gate is pinned by a **type test**: `packages/amqp/src/amqp-runtime.test-d.ts` pins the handlers-port half of `amqp`'s own gate, and its sibling `packages/amqp/src/handler.test-d.ts` pins @@ -782,7 +782,7 @@ CustomersSlice, observability()], exports: [Logger] })`** is the whole request scope write to, and `Logger` is in `exports` because `RequestModule` reads it out of the application scope. `RequestModule` rides `StartOptions.unit` so - the per-request fork is the kernel's. There is no `runtime`, `needs`, + the per-request fork is the kernel's. There is no `runtime`, `resolves`, `handler`, `port` or env-reading to spell anywhere. It is also the **one** `main.ts` that is not a single line: it passes `onEvent: kernelEvents(createLogger(jsonSink()))` so the kernel's nine events diff --git a/docs/examples/order-api.md b/docs/examples/order-api.md index e83f0e29..d410e4ae 100644 --- a/docs/examples/order-api.md +++ b/docs/examples/order-api.md @@ -565,8 +565,8 @@ and the diagnostic names the port: `Type '"HttpRouter"' is not assignable to type '"@di/Scope"'`. It is **not** di's `UNSATISFIED DEPENDENCIES` arity gate, which guards `Module.build` and `Module.scoped`; conflating the two is easy and the distinction is the point of -having both pinned here. There is no `UNSATISFIED RUNTIME NEEDS` arm, because -the shipped runtime declares no needs. +having both pinned here. There is no `UNSATISFIED RUNTIME PORTS` arm, because +the shipped runtime resolves nothing. ```ts // @ts-expect-error — UNSATISFIED UNIT NEEDS: the module does not export Logger for RequestModule to read. diff --git a/docs/explanation/compile-time-wiring.md b/docs/explanation/compile-time-wiring.md index 76f9dd9e..02e5aca0 100644 --- a/docs/explanation/compile-time-wiring.md +++ b/docs/explanation/compile-time-wiring.md @@ -164,11 +164,11 @@ the `module` parameter** — `unknown`, and invisible, when the gate is satisfie a sentence otherwise. `start`, `runMain` and `@btravstack/testing`'s `Boot` all carry it: -| Arm | Fires when | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `NO RUNTIME` | The module exports no port declared over `RuntimePort`. A process boots exactly one runtime, and it is a service of the module — a root that forgets `HttpModule`/`http(...)` is refused here. | -| `UNSATISFIED RUNTIME NEEDS` | The runtime's declared `needs` are not among the module's exports — the **module's alone**, never the unit module's, because `RuntimeHost.ctx` is the application context and a unit-only port does not exist at startup. No shipped starter declares any today. | -| `UNSATISFIED UNIT NEEDS` | With `StartOptions.unit`, the unit module's needs are not covered by the module's exports, `Scope` or `Env` — `forkScope`'s gate, stated at `start`'s call site, where the parent is actually known. | +| Arm | Fires when | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NO RUNTIME` | The module exports no port declared over `RuntimePort`. A process boots exactly one runtime, and it is a service of the module — a root that forgets `HttpModule`/`http(...)` is refused here. | +| `UNSATISFIED RUNTIME PORTS` | The runtime's declared `resolves` are not among the module's exports — the **module's alone**, never the unit module's, because `RuntimeHost.ctx` is the application context and a unit-only port does not exist at startup. No shipped starter declares any today. | +| `UNSATISFIED UNIT NEEDS` | With `StartOptions.unit`, the unit module's needs are not covered by the module's exports, `Scope` or `Env` — `forkScope`'s gate, stated at `start`'s call site, where the parent is actually known. | ```ts const Application = Module("Application")({ @@ -281,7 +281,7 @@ ends on, which is the payload of the whole message. | di's own gate (a conditional rest tuple) | `Module.scoped`/`build`/`forkScope` | `Expected 3 arguments, but got 1.` — [the whole message](#the-gate-an-arity-error), unchanged | same — no task on this branch touched it | | An unmet need at `start` (plain assignability) | a starter's own port, e.g. `AmqpHandlers` | `'"AmqpHandlers"' is not assignable to type '"@di/Scope"'` | same — this was always the best diagnostic in the repo; the thirteen corrections were to the documentation calling it di's gate, not to the gate | | `start`'s `StartGate` — `NO RUNTIME` | [the Greeter example above](#the-kernels-own-gate) | `Expected 4 arguments, but got 1.` | ends on `"NO RUNTIME — the module exports no port declared over RuntimePort"` — [full example above](#the-kernels-own-gate) | -| `start`'s `StartGate` — `UNSATISFIED RUNTIME NEEDS` | a runtime's `needs` uncovered by the module's exports | `Expected 4 arguments, but got 1.` | ends on `"UNSATISFIED RUNTIME NEEDS — the runtime needs a port the module does not export"` | +| `start`'s `StartGate` — `UNSATISFIED RUNTIME PORTS` | a runtime's `resolves` uncovered by the module's exports | `Expected 4 arguments, but got 1.` | ends on `"UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export"` | | `start`'s `StartGate` — `UNSATISFIED UNIT NEEDS` | a unit module's needs uncovered | `Expected 4 arguments, but got 2.` | ends on `"UNSATISFIED UNIT NEEDS — the unit module needs a port the module does not export"` | | amqp's/temporal's composer — `UNCOVERED HANDLERS`/`UNCOVERED ACTIVITIES` | `AmqpHandlers(contract)([...])` / `TemporalActivities(contract)([...])` missing a key | ends on `'"UNCOVERED HANDLERS"'` / `'"UNCOVERED ACTIVITIES"'` | ends on `'"UNCOVERED HANDLERS — the contract declares a consumer this array does not cover"'` / the `ACTIVITIES` twin; the missing key prints too, as a separate diagnostic on the trailing element, once the array is as long as the marker tuple (measured: `'"orderAudit"'`, `'"fulfillOrder"'`) | | http's keyed router — `UNDECLARED KEY` | `HttpRouter(contract)(controllers)` with a key the contract does not declare | ends on `'never'` | ends on `'"UNDECLARED KEY — the contract declares no fragment under billing"'` — the key is named too, straight from the mapped type's own `K` | diff --git a/docs/explanation/design-decisions.md b/docs/explanation/design-decisions.md index e9c799a8..f59c814a 100644 --- a/docs/explanation/design-decisions.md +++ b/docs/explanation/design-decisions.md @@ -27,8 +27,8 @@ with it a runtime constructed outside the graph that reaches back into it. `start`, `runMain` and `@btravstack/testing`'s `Boot` all intersect `StartGate` onto their `module` parameter — `unknown`, and -invisible, when the module exports a runtime whose needs its exports cover; one -of three sentences (`NO RUNTIME — …`, `UNSATISFIED RUNTIME NEEDS — …`, +invisible, when the module exports a runtime whose ports its exports cover; one +of three sentences (`NO RUNTIME — …`, `UNSATISFIED RUNTIME PORTS — …`, `UNSATISFIED UNIT NEEDS — …`) otherwise. It rides the parameter so that the sentence **prints**: an argument that fails a parameter type makes TypeScript name that type, where the trailing rest tuple this used to be failed as an diff --git a/docs/explanation/one-process-one-runtime.md b/docs/explanation/one-process-one-runtime.md index aaa5f300..0fe68148 100644 --- a/docs/explanation/one-process-one-runtime.md +++ b/docs/explanation/one-process-one-runtime.md @@ -117,5 +117,5 @@ never does arithmetic on time. - The three beats the drain runs, and why each is where it is: [Draining, in three beats](/explanation/draining-in-three-beats). -- What a starter is, and why the shipped runtimes have no `needs`: +- What a starter is, and why the shipped runtimes resolve nothing: [Starters](/explanation/starters). diff --git a/docs/explanation/starters.md b/docs/explanation/starters.md index 5f2d84eb..54019588 100644 --- a/docs/explanation/starters.md +++ b/docs/explanation/starters.md @@ -146,18 +146,18 @@ application never writes `implement`, `os.…`, `declareHandler` or `declareActivitiesHandler`, and a typo'd or missing procedure is a compile error at the record. -## Why `needs` disappeared +## Why the starters resolve nothing -The kernel's `Runtime` has a `needs` field, and `start`'s gate checks it +The kernel's `Runtime` has a `resolves` field, and `start`'s gate checks it against the module's exports. **No shipped starter uses it any more.** Each takes the application's router / activities / handlers as a port its runtime provider _depends on_ through di — `Provider(HttpRuntime)({ config: HttpConfig, handler: HttpHandler }, …)` where `HttpHandler` is built from the router port, -`Provider(AmqpRuntime)({ config: AmqpConfig, handlers: AmqpHandlersPort }, …)` — so their `Needs` is `never` and `RuntimeHost.ctx` +`Provider(AmqpRuntime)({ config: AmqpConfig, handlers: AmqpHandlersPort }, …)` — so their `Resolves` is `never` and `RuntimeHost.ctx` goes unread. The reason is not tidiness. A port's service type is fixed at declaration, so -a runtime with application-specific `needs` — "I need whatever this +a runtime with application-specific `resolves` — "I read whatever this application's router port is" — could not ship its port: `HttpRuntime` has to be one class in `@btravstack/http`, and its type cannot mention a port only the application knows. Making the router a dependency of the runtime's @@ -165,9 +165,9 @@ provider moves that knowledge to where it exists — the composition root that provides the router — and the `Needs` channel checks it there: a root that imports `http()` without providing the router carries an unmet need `start` refuses, naming the port -(`Type 'HttpRouterPort' is not assignable to type 'Env | Scope'`). The kernel keeps `Runtime.needs`, `RunUnit`'s typed `ctx` and the -`UNSATISFIED RUNTIME NEEDS` arm as the general contract for a hand-rolled -runtime; the starters simply do not need them. +(`Type 'HttpRouterPort' is not assignable to type 'Env | Scope'`). The kernel keeps `Runtime.resolves`, `RunUnit`'s typed `ctx` and the +`UNSATISFIED RUNTIME PORTS` arm as the general contract for a hand-rolled +runtime; the starters simply do not use them. ## What a starter does not do diff --git a/docs/explanation/the-kernel-maps-nothing.md b/docs/explanation/the-kernel-maps-nothing.md index 73ff5555..2f970de9 100644 --- a/docs/explanation/the-kernel-maps-nothing.md +++ b/docs/explanation/the-kernel-maps-nothing.md @@ -149,7 +149,7 @@ the id for whoever wants it. ## Where to go next -- The starters, and why the runtime's `needs` disappeared once the router, +- The starters, and why the shipped runtimes resolve nothing once the router, activities and handlers became ports: [Starters](/explanation/starters). - The record a handler runs inside, and who may read it: [Ambient data, injected capabilities](/explanation/ambient-vs-context). diff --git a/docs/explanation/why-start.md b/docs/explanation/why-start.md index 9283916d..efaa26a0 100644 --- a/docs/explanation/why-start.md +++ b/docs/explanation/why-start.md @@ -25,7 +25,7 @@ holding it. `di` answers "does this composition hold together?" before the process exists: a missing provider, a private port reached across a module boundary, a -runtime whose needs the root does not export — each is a compile error at the +runtime whose ports the root does not export — each is a compile error at the call site (see [Compile errors, not surprises](/explanation/compile-time-wiring)). By the time `start` sees a module there is nothing left to discover about it. diff --git a/docs/how-to/open-a-per-request-scope.md b/docs/how-to/open-a-per-request-scope.md index 7090b8b7..b65765cb 100644 --- a/docs/how-to/open-a-per-request-scope.md +++ b/docs/how-to/open-a-per-request-scope.md @@ -155,8 +155,8 @@ OrderPersistenceModule, observability()], exports: [Logger] })`. A unit-provided port exists only while a unit is open, and reaches a runtime through `host.run`'s work callback alone. `host.ctx.get(RequestSpan)` at runtime startup type-checks against nothing and would be a defect, so the -gate rejects a runtime whose `needs` name a unit-only port: `UNSATISFIED -RUNTIME NEEDS` is checked against the module's exports **only**, never the +gate rejects a runtime whose `resolves` names a unit-only port: `UNSATISFIED +RUNTIME PORTS` is checked against the module's exports **only**, never the unit's. Resolve at start what the application module itself exports. ::: diff --git a/docs/how-to/write-a-runtime.md b/docs/how-to/write-a-runtime.md index cd4f769a..c692ea7f 100644 --- a/docs/how-to/write-a-runtime.md +++ b/docs/how-to/write-a-runtime.md @@ -26,18 +26,18 @@ class Ticker extends RuntimePort> {} `RuntimePort` is `Port("Runtime")` left generic: every runtime port shares one id at runtime — a process boots exactly one — while each carries its own -`Needs` and `Info` in the type. `Runtime` says this runtime +`Resolves` and `Info` in the type. `Runtime` says this runtime resolves `Greeter` from the application context; `start` refuses, at compile time, a module that exports the port without exporting `Greeter`. ## Step 2 — implement `Runtime` ```ts -type Runtime = { +type Runtime = { readonly name: string; - readonly needs: readonly Needs[]; + readonly resolves: readonly Resolves[]; readonly start: ( - host: RuntimeHost, + host: RuntimeHost, ) => AsyncResult, RuntimeStartFailed>; }; ``` @@ -54,7 +54,7 @@ import type { Runtime, Serving } from "@btravstack/core"; const ticker: Runtime = { name: "ticker", - needs: [Greeter], + resolves: [Greeter], start: (host) => { const timer = setInterval(() => { // Every piece of work goes through `host.run`: that is what makes it @@ -109,7 +109,7 @@ type TcpInfo = { readonly port: number }; const tcpish: Runtime = { name: "tcpish", - needs: [Greeter], + resolves: [Greeter], start: () => fromExecutor, RuntimeStartFailed>((settle) => { const server = createServer(); @@ -157,7 +157,7 @@ The composition root is what differs between an `api`, a `worker` and a `Ticker` from `exports` and `runMain` refuses the module against `"NO RUNTIME — the module exports no port declared over RuntimePort"`; drop `Greeter` and it refuses it against -`"UNSATISFIED RUNTIME NEEDS — the runtime needs a port the module does not export"`. +`"UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export"`. Either way the sentence is the error's **last** line; the first names the two `Module<…>` types. @@ -229,9 +229,9 @@ per-request acquire leaves the unit open for the process lifetime. The three shipped runtimes go one step further and take what the application supplies — a router, activities, handlers — as a **port their runtime provider -depends on**, so their `needs` is `never` and `host.ctx` goes unread. Do the +depends on**, so their `resolves` is `never` and `host.ctx` goes unread. Do the same once your runtime has application-specific inputs: a port's service type -is fixed at declaration, so a runtime with `needs: [OrderRouter]` cannot ship +is fixed at declaration, so a runtime with `resolves: [OrderRouter]` cannot ship its port; a provider that depends on `OrderRouter` can. See [Starters](/explanation/starters). diff --git a/docs/index.md b/docs/index.md index 5a654d47..f63f1287 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,7 +22,7 @@ features: - title: One process, one runtime details: An API, a Temporal worker and an AMQP consumer are three processes booting the same module under a different composition root. The runtime is a service of the module, and a graph holds exactly one. - title: Wiring proven at compile time - details: A module that forgets a provider, a runtime whose needs are not exported, a root with no runtime — each is a compile error at the call site, before anything runs. That is @btravstack/di, and start builds on it. + details: A module that forgets a provider, a runtime whose ports are not exported, a root with no runtime — each is a compile error at the call site, before anything runs. That is @btravstack/di, and start builds on it. - title: A drain that survives Kubernetes details: SIGTERM flips readiness, waits for endpoint removal to catch up, then stops accepting and gives in-flight work a deadline. Whatever is still open is reported abandoned, not lost silently. - title: Nothing throws diff --git a/docs/reference/amqp.md b/docs/reference/amqp.md index e6ee7b29..1a6c98f9 100644 --- a/docs/reference/amqp.md +++ b/docs/reference/amqp.md @@ -274,7 +274,7 @@ A blank value is a `ConfigInvalid` — `startFailed` and exit `78` under ## `AmqpRuntime` and `AmqpInfo` Declared over the kernel's `RuntimePort` with service -`Runtime` — no needs. Its `start` calls +`Runtime` — it resolves nothing. Its `start` calls `TypedAmqpWorker.create({ contract, handlers, middleware, urls: [url], … })`. `create` reports a connection failure on the **defect** channel with a `TechnicalError` cause; the starter recovers that into diff --git a/docs/reference/core/exit-codes.md b/docs/reference/core/exit-codes.md index 044feea8..8368f93d 100644 --- a/docs/reference/core/exit-codes.md +++ b/docs/reference/core/exit-codes.md @@ -25,7 +25,7 @@ const runMain: ( `Result` into a code. It carries the same phantom marker as `start`, intersected onto `module` (see [The gate](/reference/core/start#the-gate-startgate-x-unitneeds)), so -`NO RUNTIME — …`, `UNSATISFIED RUNTIME NEEDS — …` and +`NO RUNTIME — …`, `UNSATISFIED RUNTIME PORTS — …` and `UNSATISFIED UNIT NEEDS — …` are printed at this call site too. | Parameter | Default | Semantics | diff --git a/docs/reference/core/runtime.md b/docs/reference/core/runtime.md index 899c867b..6e17ae55 100644 --- a/docs/reference/core/runtime.md +++ b/docs/reference/core/runtime.md @@ -10,35 +10,35 @@ description: Runtime, RuntimeHost, RunUnit, Serving, RuntimePort and RuntimeStar > [Write a runtime](/how-to/write-a-runtime); for why the kernel maps nothing, > see [The kernel maps nothing](/explanation/the-kernel-maps-nothing). -## `Runtime` +## `Runtime` ```ts -type Runtime = { +type Runtime = { readonly name: string; - readonly needs: readonly Needs[]; + readonly resolves: readonly Resolves[]; readonly start: ( - host: RuntimeHost, + host: RuntimeHost, ) => AsyncResult, RuntimeStartFailed>; }; ``` -| Member | Semantics | -| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | Reported on the `serving` event. | -| `needs` | The port **classes** the runtime resolves from `host.ctx`. `start`'s gate checks them against the module's exports at the call site. Every shipped starter declares `needs: []` — what its handlers need is its provider's business, through di — so this is the general contract, used by `testRuntime` and hand-rolled runtimes. | -| `start` | Called once, after the graph is built. `Ok(serving)` moves the phase to `serving`; `Err(RuntimeStartFailed)` is a startup failure the kernel reports through `exited`. | +| Member | Semantics | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | Reported on the `serving` event. | +| `resolves` | The port **classes** the runtime resolves from `host.ctx`. `start`'s gate checks them against the module's exports at the call site. Every shipped starter declares `resolves: []` — what its handlers read is its provider's business, through di — so this is the general contract, used by `testRuntime` and hand-rolled runtimes. | +| `start` | Called once, after the graph is built. `Ok(serving)` moves the phase to `serving`; `Err(RuntimeStartFailed)` is a startup failure the kernel reports through `exited`. | -`Needs` is parameterised by port **classes** (`AnyPort`) but hands out -`Context>`, because di parameterises `Context` by -port **instance** types. `InstanceType` is `never`, so a needs-free -runtime gets a context it can read nothing from. +`Resolves` is parameterised by port **classes** (`AnyPort`) but hands out +`Context>`, because di parameterises `Context` by +port **instance** types. `InstanceType` is `never`, so a runtime that +resolves nothing gets a context it can read nothing from. -## `RuntimeHost` +## `RuntimeHost` ```ts -type RuntimeHost = { - readonly ctx: Context>; - readonly run: RunUnit; +type RuntimeHost = { + readonly ctx: Context>; + readonly run: RunUnit; }; ``` @@ -46,13 +46,13 @@ type RuntimeHost = { `StartOptions.unit` module's. `run` is the kernel's unit registry, closed over that context. -## `RunUnit` +## `RunUnit` ```ts -type RunUnit = ( +type RunUnit = ( meta: UnitMeta, work: ( - ctx: Context>, + ctx: Context>, signal: AbortSignal, ) => ReturnType>, ) => AsyncResult; @@ -98,7 +98,7 @@ class HttpRuntime extends RuntimePort> {} `RuntimePort` is the one port the kernel resolves its runtime from. Left generic on purpose: every runtime port is **one id** at runtime — a process -boots exactly one — while each carries its own `Needs`/`Info` in the type. A +boots exactly one — while each carries its own `Resolves`/`Info` in the type. A runtime package declares its own class over it and ships a module providing it. `RuntimeInfoOf` reads the `Info` back out of a module's exports; it is the only helper type of that family the package exports. @@ -214,7 +214,7 @@ class Greeter extends Port("Greeter")<{ const ticker: Runtime = { name: "ticker", - needs: [Greeter], + resolves: [Greeter], start: (host) => { const timer = setInterval(() => { // Every piece of work goes through `host.run`: that is what makes it @@ -250,4 +250,4 @@ const TickerModule = Module("Ticker")({ A composition root that imports `TickerModule` must also export `Greeter`, or `start` refuses the module against -`"UNSATISFIED RUNTIME NEEDS — the runtime needs a port the module does not export"`. +`"UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export"`. diff --git a/docs/reference/core/start.md b/docs/reference/core/start.md index 688acf6e..c14522ae 100644 --- a/docs/reference/core/start.md +++ b/docs/reference/core/start.md @@ -87,18 +87,18 @@ type StartGate = [Extract] extends [ never, ] ? "NO RUNTIME — the module exports no port declared over RuntimePort" - : [InstanceType>] extends [X] + : [InstanceType>] extends [X] ? [Exclude] extends [never] ? unknown : "UNSATISFIED UNIT NEEDS — the unit module needs a port the module does not export" - : "UNSATISFIED RUNTIME NEEDS — the runtime needs a port the module does not export"; + : "UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export"; ``` -| Arm | Fires when | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `NO RUNTIME` | `X` contains no port declared over `RuntimePort`. Every starter's module sugar exports one; a hand-rolled root must export its runtime port. | -| `UNSATISFIED RUNTIME NEEDS` | The runtime's declared `needs` are not all among the module's exports — the **module's alone**, never the unit module's, because `RuntimeHost.ctx` is the application context. | -| `UNSATISFIED UNIT NEEDS` | The `unit` module's needs are not covered by the module's exports, `Scope` or `Env` — `Module.forkScope`'s gate, stated where the parent is actually known. | +| Arm | Fires when | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NO RUNTIME` | `X` contains no port declared over `RuntimePort`. Every starter's module sugar exports one; a hand-rolled root must export its runtime port. | +| `UNSATISFIED RUNTIME PORTS` | The runtime's declared `resolves` are not all among the module's exports — the **module's alone**, never the unit module's, because `RuntimeHost.ctx` is the application context. | +| `UNSATISFIED UNIT NEEDS` | The `unit` module's needs are not covered by the module's exports, `Scope` or `Env` — `Module.forkScope`'s gate, stated where the parent is actually known. | `runMain`, and `@btravstack/testing`'s `Boot`, carry the same marker. @@ -126,7 +126,7 @@ TypeScript escape. Spelling phantom arguments out by hand went with the tuple. `RuntimePort` is `Port("Runtime")`, exported **generic** — no fixed service — so a runtime package declares its own concrete port over it and every runtime -port is one id at runtime while each carries its own `Needs`/`Info` in the +port is one id at runtime while each carries its own `Resolves`/`Info` in the type. `RuntimeInfoOf` reads the `Info` back out of a module's exports, which is how `RunningApp>` types `runtimeInfo()`. @@ -139,7 +139,7 @@ type HttpInfo = { readonly port: number }; const httpish: Runtime = { name: "httpish", - needs: [], + resolves: [], start: () => OkAsync({ drain: () => OkAsync(), diff --git a/docs/reference/di/entry-points.md b/docs/reference/di/entry-points.md index 6d8c1296..7b3135ae 100644 --- a/docs/reference/di/entry-points.md +++ b/docs/reference/di/entry-points.md @@ -186,7 +186,7 @@ same instance. `Module.scoped` — so the application scope is opened as the process boots and closed on every exit path, with what its finalisers report surfacing as `ExitReport.teardownErrors`. It adds a gate of its own on top of di's -(`NO RUNTIME`, `UNSATISFIED RUNTIME NEEDS`, `UNSATISFIED UNIT NEEDS`), and +(`NO RUNTIME`, `UNSATISFIED RUNTIME PORTS`, `UNSATISFIED UNIT NEEDS`), and `RunningApp` rather than a `Context` is what it hands back: the runtime, not the caller, is what reads the built context. diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index bdcbe6d1..d0f58623 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -58,7 +58,7 @@ every unit. See [Open a per-request scope](/how-to/open-a-per-request-scope). **gate** — A phantom type that is inert when a composition is sound and refuses the call otherwise. The two shipped here are not the same shape. `start`'s is `StartGate`, a marker **intersected onto `module`** — `unknown` when sound, one -of three sentences (`NO RUNTIME — …`, `UNSATISFIED RUNTIME NEEDS — …`, +of three sentences (`NO RUNTIME — …`, `UNSATISFIED RUNTIME PORTS — …`, `UNSATISFIED UNIT NEEDS — …`) otherwise, and the sentence prints in the error. di's on `Module.scoped` is a conditional **rest tuple** labelled `UNSATISFIED DEPENDENCIES`, so it fails on arity — `Expected 3 arguments, but @@ -94,7 +94,7 @@ Kubernetes' eventually-consistent endpoint removal needs. See **provider** — `Provider(port)(deps, arm)` — how a port's service is built: `value`, `sync`, `make`, `class` or `acquire`/`release`. See [Providers](/reference/di/providers). -**runtime** — The service behind a port declared over `RuntimePort`: `{ name, needs, +**runtime** — The service behind a port declared over `RuntimePort`: `{ name, resolves, start }`, where `start` returns a `Serving`. A process boots exactly one. See [The Runtime contract](/reference/core/runtime) and [One process, one runtime](/explanation/one-process-one-runtime). diff --git a/docs/reference/http.md b/docs/reference/http.md index 470e7228..b109e522 100644 --- a/docs/reference/http.md +++ b/docs/reference/http.md @@ -533,7 +533,7 @@ under `runMain`. Anything in the graph may depend on `HttpConfig`. ## `HttpRuntime` and `HttpInfo` `HttpRuntime` is declared over the kernel's `RuntimePort` with service -`Runtime`: the runtime declares **no needs** — the router is +`Runtime`: the runtime **resolves nothing** — the router is a port its provider depends on — so `RuntimeHost.ctx` goes unread. Once listening it publishes `HttpInfo`, `{ port }`, on `Serving.info`; with `PORT=0` that is the only way to learn the port that was actually bound. diff --git a/docs/reference/temporal.md b/docs/reference/temporal.md index b789a9fd..ab82513a 100644 --- a/docs/reference/temporal.md +++ b/docs/reference/temporal.md @@ -367,7 +367,7 @@ still the finaliser's to surface as a `teardownError`. ## `TemporalRuntime` and `TemporalInfo` Declared over the kernel's `RuntimePort` with service -`Runtime` — no needs. Its `start` calls +`Runtime` — it resolves nothing. Its `start` calls `declareActivitiesHandler` **inside** the qualified chain (a contract it cannot satisfy — an undeclared implementation, a declared one missing — throws there, and that throw becomes `Err(RuntimeStartFailed({ runtime: diff --git a/docs/reference/testing.md b/docs/reference/testing.md index 61b2ee89..272e49ab 100644 --- a/docs/reference/testing.md +++ b/docs/reference/testing.md @@ -210,15 +210,15 @@ type SubmittedUnit = { }; ``` -| Member | Semantics | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name`, `needs`, `start` | The `Runtime` half: `needs` is `[]`; `start` records `host.run`, starts accepting, publishes `{ name }` on `Serving.info` and answers `Ok(serving)`. | -| `module` | A `Module` providing **this** runtime on `TestRuntimePort` — the shape a runtime package ships, sized for a test. Import it next to the module under test and export `TestRuntimePort`, and `start` finds it. It provides this very object: a wrapper built by spreading (`{ ...runtime, start }`) still carries a module that boots the inner one. | -| `started()` | `true` once the kernel has called `start`. | -| `untilStarted()` | Resolves the first time the kernel calls `start`. What a test awaits before `submit()`, since `start` itself is only called once the graph is built. | -| `accepting()` | `true` between `start` and the first of `drain` / `stop`. Lets a test observe **when** the kernel told the runtime to stop accepting, which the drain's ordering turns on. | -| `serving()` | The `Serving` handed to the kernel. **Throws** if the runtime was never started — a loud fixture misuse, not a modeled outcome. | -| `submit()` | Opens a unit through the kernel's `run` with `{ kind: "test", id: "" }` (`n` counts up from `1`, so ids stay unique). Returns a `SubmittedUnit`. **Throws** when not accepting. | +| Member | Semantics | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name`, `resolves`, `start` | The `Runtime` half: `resolves` is `[]`; `start` records `host.run`, starts accepting, publishes `{ name }` on `Serving.info` and answers `Ok(serving)`. | +| `module` | A `Module` providing **this** runtime on `TestRuntimePort` — the shape a runtime package ships, sized for a test. Import it next to the module under test and export `TestRuntimePort`, and `start` finds it. It provides this very object: a wrapper built by spreading (`{ ...runtime, start }`) still carries a module that boots the inner one. | +| `started()` | `true` once the kernel has called `start`. | +| `untilStarted()` | Resolves the first time the kernel calls `start`. What a test awaits before `submit()`, since `start` itself is only called once the graph is built. | +| `accepting()` | `true` between `start` and the first of `drain` / `stop`. Lets a test observe **when** the kernel told the runtime to stop accepting, which the drain's ordering turns on. | +| `serving()` | The `Serving` handed to the kernel. **Throws** if the runtime was never started — a loud fixture misuse, not a modeled outcome. | +| `submit()` | Opens a unit through the kernel's `run` with `{ kind: "test", id: "" }` (`n` counts up from `1`, so ids stay unique). Returns a `SubmittedUnit`. **Throws** when not accepting. | `SubmittedUnit` is how a test holds a unit open across a drain: `settle` is the unit's own outcome, `result` is what the kernel hands back for it, and diff --git a/docs/typedoc.core.json b/docs/typedoc.core.json index a1ea8e23..3db2fb9c 100644 --- a/docs/typedoc.core.json +++ b/docs/typedoc.core.json @@ -4,5 +4,5 @@ "entryPoints": ["../packages/core/src/index.ts"], "tsconfig": "../packages/core/tsconfig.json", "out": "api/core", - "intentionallyNotExported": ["RuntimeInstance", "RuntimeNeedsOf", "RuntimeOf"] + "intentionallyNotExported": ["RuntimeInstance", "RuntimeResolvesOf", "RuntimeOf"] } diff --git a/examples/README.md b/examples/README.md index 700d2b65..7fd2f106 100644 --- a/examples/README.md +++ b/examples/README.md @@ -164,7 +164,7 @@ tag is not unique per attempt (see [`@btravstack/amqp`'s README](../packages/amqp/README.md) for why), where a queue job id or a task token already is. -## What each runtime needs, and how the gate sees it +## What each runtime resolves, and how the gate sees it A runtime is a service the composition root exports, on a port each starter ships over the kernel's `RuntimePort` (`HttpRuntime`, `TemporalRuntime`, @@ -187,8 +187,8 @@ contract key instead, and the starter composes an **array** rather than a record — the same exactness (every key covered, two slices claiming one key is di's duplicate-provider defect at build) reached a different way. No starter -declares a `needs` any more — all three runtimes are `Runtime` -— so `start`'s `UNSATISFIED RUNTIME NEEDS` arm is exercised only by the +runtime resolves anything any more — all three are `Runtime` +— so `start`'s `UNSATISFIED RUNTIME PORTS` arm is exercised only by the kernel's own type test now; what the examples pin is the other two gates. Pinned in `order-api/src/needs-gate.test-d.ts`, @@ -319,7 +319,7 @@ a spec swaps the default stdout sink for a recorder — so what a handler said comes back as a `Line`, and the assertions read `attributes.orderId` and `unit.traceId` as fields. -Where a guarantee is compile-time only — an unmet port, a runtime's `needs` — +Where a guarantee is compile-time only — an unmet port, a runtime's `resolves` — the assertion is a `@ts-expect-error` in a `*.test-d.ts` file, checked by `tsc` rather than executed. diff --git a/examples/order-amqp-worker/src/needs-gate.test-d.ts b/examples/order-amqp-worker/src/needs-gate.test-d.ts index f837443a..a4901d73 100644 --- a/examples/order-amqp-worker/src/needs-gate.test-d.ts +++ b/examples/order-amqp-worker/src/needs-gate.test-d.ts @@ -7,8 +7,9 @@ import { AmqpModule, AmqpRuntime, amqp } from "@btravstack/amqp"; * parameter. Type-checked by this package's `test:types` script, * never executed. * - * There is no UNSATISFIED RUNTIME NEEDS negative any more: the runtime has - * none. What used to be its needs — the handlers, and what they read — is now + * There is no UNSATISFIED RUNTIME PORTS negative any more: the runtime + * resolves nothing. What it used to resolve — the handlers, and what they + * read — is now * the starter's own handlers port, which the starter DEPENDS on, so a * composition without a provider for it is the needs channel, not the marker * and not di's `UNSATISFIED DEPENDENCIES` arity gate: the module's needs diff --git a/examples/order-temporal-worker/src/needs-gate.test-d.ts b/examples/order-temporal-worker/src/needs-gate.test-d.ts index dea68822..6a88d8ff 100644 --- a/examples/order-temporal-worker/src/needs-gate.test-d.ts +++ b/examples/order-temporal-worker/src/needs-gate.test-d.ts @@ -3,10 +3,10 @@ import { start } from "@btravstack/core"; /** * The compile-time half of the orchestration deployment: `start` resolves its * runtime from the `TemporalRuntime` port `TemporalModule` exports, and that - * runtime needs nothing from the application context — its activities reach + * runtime resolves nothing from the application context — its activities reach * it as a port the starter depends on through di. So there is no - * `UNSATISFIED RUNTIME NEEDS` arm to pin here, as there was when the runtime - * declared five `needs` of its own; what replaces it is the needs channel — + * `UNSATISFIED RUNTIME PORTS` arm to pin here, as there was when the runtime + * resolved five ports of its own; what replaces it is di's needs channel — * refused by `start`'s `Module` parameter, which names the * port, and NOT di's `UNSATISFIED DEPENDENCIES` arity gate. * diff --git a/packages/amqp/CLAUDE.md b/packages/amqp/CLAUDE.md index 6afeb501..d3279733 100644 --- a/packages/amqp/CLAUDE.md +++ b/packages/amqp/CLAUDE.md @@ -158,7 +158,7 @@ K]`, which always names the marker — printed as the **bare string**, the - **`amqp(options)` → `Module>`** — the starter, the same shape as `@btravstack/http`'s `http()`. It provides the runtime on **`AmqpRuntime`** (`extends RuntimePort>` — the runtime has **no** needs) and the broker on +AmqpInfo>>` — the runtime resolves **nothing**) and the broker on **`AmqpConfig`** (`{ url }`, bound from `AMQP_URL`, default `amqp://127.0.0.1:5672`), and it **needs** its handlers port, typed for `contract`, which the application provides. The composition root imports @@ -169,7 +169,7 @@ AmqpInfo>>` — the runtime has **no** needs) and the broker on the diagnostic ends on `Type '"AmqpHandlers"' is not assignable to type '"@di/Scope"'` and names the port (`examples/order-amqp-worker/src/needs-gate.test-d.ts` pins that - diagnostic, since `start`'s own gate has no `UNSATISFIED RUNTIME NEEDS` arm + diagnostic, since `start`'s own gate has no `UNSATISFIED RUNTIME PORTS` arm to fire any more). `AmqpOptions` — `contract: TContract` (`TContract` bounded by `Parameters[0]["contract"]`, never imported diff --git a/packages/amqp/src/amqp-runtime.ts b/packages/amqp/src/amqp-runtime.ts index 53a67387..8582b195 100644 --- a/packages/amqp/src/amqp-runtime.ts +++ b/packages/amqp/src/amqp-runtime.ts @@ -123,7 +123,7 @@ export const amqp = ( { sync: ({ config: bound, handlers }): Runtime => ({ name: "amqp", - needs: [], + resolves: [], start: (host) => createWorker(host, bound, options, handlers), }), }, diff --git a/packages/core/CLAUDE.md b/packages/core/CLAUDE.md index d6750200..bbd2d15b 100644 --- a/packages/core/CLAUDE.md +++ b/packages/core/CLAUDE.md @@ -33,7 +33,7 @@ never>`: `Needs` is covariant on `Module`, so this accepts a needs-free drives what it finds. The kernel is DI initialisation and lifecycle, nothing else. The `module` parameter is intersected with the phantom marker `StartGate`: `NO RUNTIME` when the module exports no runtime - port, `UNSATISFIED RUNTIME NEEDS` when the runtime's declared needs are not + port, `UNSATISFIED RUNTIME PORTS` when what the runtime resolves is not among the module's exports (the module's alone — a unit-only port exists only while a unit is open, and `RuntimeHost.ctx` is the application context), `UNSATISFIED UNIT NEEDS` for the fork's own direction — all three at the @@ -45,22 +45,22 @@ never>`: `Needs` is covariant on `Module`, so this accepts a needs-free service): a runtime package declares its own concrete port over it — `class HttpRuntime extends RuntimePort> {}` — so every runtime is one id at runtime while each carries its own - `Needs`/`Info` in the type. `RuntimeOf` / `RuntimeNeedsOf` / + `Resolves`/`Info` in the type. `RuntimeOf` / `RuntimeResolvesOf` / `RuntimeInfoOf` read those back out of a module's exports (only `RuntimeInfoOf` is exported — the other two are the gate's internals); `RuntimeInstance` is the shared instance type (`InstanceType>`, internal too). Every runtime package ships its port and a starter — `HttpRuntime`/`http()`, `TemporalRuntime`/`temporal()`, `AmqpRuntime`/`amqp()` — and none of them - has `needs` any more: each takes the application's router / activities / + has a `resolves` any more: each takes the application's router / activities / handlers as a **port its runtime provider depends on** through di — the starter's own fixed port (`HttpRouterPort`, `TemporalActivitiesPort`, `AmqpHandlersPort`, one id each; the temporal and amqp ones typed per contract at the type level, the same generic-value move `RuntimePort` itself makes), which the application provides and never names — so their - `Needs` is `never` and `RuntimeHost.ctx` goes unread by every shipped - runtime. The kernel keeps `Runtime.needs`, `RunUnit`'s typed `ctx` and the - `UNSATISFIED RUNTIME NEEDS` arm as the general contract (`testRuntime` and a + `Resolves` is `never` and `RuntimeHost.ctx` goes unread by every shipped + runtime. The kernel keeps `Runtime.resolves`, `RunUnit`'s typed `ctx` and the + `UNSATISFIED RUNTIME PORTS` arm as the general contract (`testRuntime` and a hand-rolled runtime still use it), but no starter does. A port's service type is fixed at declaration, which is why a runtime with application-specific needs could not ship its port — the reason the needs went, not a constraint @@ -84,7 +84,7 @@ never>`: `Needs` is covariant on `Module`, so this accepts a needs-free built — after an `await` when a unit provider is async — so a runtime that subscribes to an event from inside its work must check it has not already fired (see contract 3 above). The gate checks - both directions at the call site: runtime `needs` may draw on `UnitX`, and + both directions at the call site: runtime `resolves` may draw on `UnitX`, and `UnitNeeds` must be covered by the module's exports or `Scope`. One caveat: `RuntimeHost.ctx` is the **application** context, so a unit-provided port exists only inside unit work — resolving one at runtime startup is a @@ -120,10 +120,10 @@ never>`: `Needs` is covariant on `Module`, so this accepts a needs-free `inFlightAtStart` if in-flight work spawned more, which is honest reporting, not a bug), `abandoned` (units still open at the deadline; **the field the exit code keys on**). -- **`Runtime` / `RuntimeHost` / `RunUnit` / +- **`Runtime` / `RuntimeHost` / `RunUnit` / `Serving`** — the runtime contract (the _service_ behind a runtime port). All parameterised by port **classes** - (`Needs extends AnyPort`) but handing out `Context>`, + (`Resolves extends AnyPort`) but handing out `Context>`, because di parameterises `Context` by port **instance** types. `Serving.drain(signal)` returns `AsyncResult` — **not** a `DrainReport`: only the kernel can see the unit registry, so the kernel owns @@ -347,17 +347,18 @@ Beyond the nine: Type-level invariants live in `start.test-d.ts` and are checked by `pnpm typecheck`: -- **The module must export a runtime, and that runtime's declared `needs` are +- **The module must export a runtime, and that runtime's declared `resolves` are checked against the module's exports at the `start` call site** (the phantom marker `StartGate`, intersected onto `module`). A composition with no port declared over `RuntimePort` among its exports fails to match - `NO RUNTIME — …`; a missing need fails to match `UNSATISFIED RUNTIME NEEDS — …`. + `NO RUNTIME — …`; a port the module does not export fails to match + `UNSATISFIED RUNTIME PORTS — …`. Each arm's sentence is pinned by an `expectTypeOf>` in `start.test-d.ts` — `@ts-expect-error` accepts any error, so the sentence a reader is shown is asserted there or nowhere. - `InstanceType` is `never`, so a needs-free runtime works against any + `InstanceType` is `never`, so a runtime resolving nothing works against any module. `Needs` and `Info` are not type parameters of `start` any more: they - are read off `X` (`RuntimeNeedsOf`, `RuntimeInfoOf` — `ServiceOf` of + are read off `X` (`RuntimeResolvesOf`, `RuntimeInfoOf` — `ServiceOf` of `Extract`, all in `runtime.ts`; only `RuntimeInfoOf` is exported from the package, the rest are the gate's internals), which is what lets `RunningApp>` type `runtimeInfo()` from the module @@ -425,7 +426,7 @@ ConfigInvalid })` rather than widening `exited`'s error union for every - **The needs check is a phantom marker intersected onto `module`, not a trailing rest tuple.** - `module: Module & ([InstanceType>] extends [X] ? unknown : "UNSATISFIED RUNTIME NEEDS — …")` + `module: Module & ([InstanceType>] extends [X] ? unknown : "UNSATISFIED RUNTIME PORTS — …")` (preceded by the `NO RUNTIME` arm on `Extract`) — against the module's exports alone, never the `unit` module's: a unit-only port exists only while a unit is open, and `RuntimeHost.ctx` is the @@ -456,9 +457,9 @@ ConfigInvalid })` rather than widening `exited`'s error union for every subclass it. - **`Context`'s contravariance is what makes the check free.** An - application context whose exports cover the runtime's needs is assignable to - `Context>` with no work. The - `ctx as unknown as Context>` inside `start`'s `use` + application context whose exports cover what the runtime resolves is assignable to + `Context>` with no work. The + `ctx as unknown as Context>` inside `start`'s `use` callback is needed only because the gate proves `InstanceType extends X` at the **call site**, and that proof is not visible to the checker inside a body where `X` and `Needs` are still unresolved type parameters. diff --git a/packages/core/README.md b/packages/core/README.md index 53e91b89..3f65daac 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -56,7 +56,7 @@ const AppModule = Module("App")({ // sample doesn't need. const ticker: Runtime = { name: "ticker", - needs: [Greeter], + resolves: [Greeter], start: (host) => { const timer = setInterval(() => { // Every piece of work goes through `host.run`: that is what makes it diff --git a/packages/core/src/docs-examples.test-d.ts b/packages/core/src/docs-examples.test-d.ts index 1544aa50..367c3ef6 100644 --- a/packages/core/src/docs-examples.test-d.ts +++ b/packages/core/src/docs-examples.test-d.ts @@ -55,7 +55,7 @@ const AppModule = Module("App")({ // sample doesn't need. const ticker: Runtime = { name: "ticker", - needs: [Greeter], + resolves: [Greeter], start: (host) => { const timer = setInterval(() => { // Every piece of work goes through `host.run`: that is what makes it @@ -179,15 +179,15 @@ type ReadmeServing = { readonly info?: Info; }; -type ReadmeRuntime = { +type ReadmeRuntime = { readonly name: string; - readonly needs: readonly Needs[]; - readonly start: (host: RuntimeHost) => AsyncResult, RuntimeStartFailed>; + readonly resolves: readonly Resolves[]; + readonly start: (host: RuntimeHost) => AsyncResult, RuntimeStartFailed>; }; -type ReadmeRuntimeHost = { - readonly ctx: Context>; - readonly run: RunUnit; +type ReadmeRuntimeHost = { + readonly ctx: Context>; + readonly run: RunUnit; }; type ReadmeDrainReport = { @@ -210,7 +210,7 @@ type HttpInfo = { readonly port: number }; const httpish: Runtime = { name: "httpish", - needs: [Greeter], + resolves: [Greeter], start: () => OkAsync({ drain: () => OkAsync(), diff --git a/packages/core/src/run-main.ts b/packages/core/src/run-main.ts index 5c44aeef..8ab5aa4c 100644 --- a/packages/core/src/run-main.ts +++ b/packages/core/src/run-main.ts @@ -111,7 +111,7 @@ export const awaitExit = async ( // top-level `await runMain(...)` in an entry point is the intended shape. export const runMain = async ( // The same phantom gate `start` carries, for the same reason: it makes the - // runtime's declared needs a compile-time check at *this* call site. + // runtime's declared `resolves` a compile-time check at *this* call site. module: Module & StartGate, options: StartOptions = {}, exit: (code: number) => void = (code) => { diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 341a4ed4..e0128f29 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -35,13 +35,13 @@ export class RuntimeStartFailed extends TaggedError("RuntimeStartFailed")<{ * inside `host.run`. A runtime that subscribes to an event from inside `work` * (a response's `'close'`) must first check whether it has already fired. */ -// `Context>`, not `Context`: di parameterises -// `Context` by port *instance* types, while a runtime declares its needs -// as port *classes* (`AnyPort` is `abstract new () => AnyPortInstance`). -// `InstanceType` is `never`, so a needs-free runtime is unaffected. -export type RunUnit = ( +// `Context>`, not `Context`: di parameterises +// `Context` by port *instance* types, while a runtime declares what it +// resolves as port *classes* (`AnyPort` is `abstract new () => AnyPortInstance`). +// `InstanceType` is `never`, so a runtime resolving nothing is unaffected. +export type RunUnit = ( meta: UnitMeta, - work: (ctx: Context>, signal: AbortSignal) => ReturnType>, + work: (ctx: Context>, signal: AbortSignal) => ReturnType>, ) => AsyncResult; /** @@ -56,14 +56,14 @@ export type RunUnit = ( * unit unless a `traceId` is supplied (see {@link UnitMeta}). * * `ctx` is the **application** context, and `start`'s gate checks a - * runtime's `needs` against the application module's exports only — a port a + * runtime's `resolves` against the application module's exports only — a port a * `StartOptions.unit` module provides exists only while a unit is open, and a - * runtime naming it as a need is rejected at the call site rather than left + * runtime naming it there is rejected at the call site rather than left * to `ctx.get(...)` throwing at startup. */ -export type RuntimeHost = { - readonly ctx: Context>; - readonly run: RunUnit; +export type RuntimeHost = { + readonly ctx: Context>; + readonly run: RunUnit; }; /** @@ -87,10 +87,16 @@ export type Serving = { readonly info?: Info; }; -export type Runtime = { +export type Runtime = { readonly name: string; - readonly needs: readonly Needs[]; - readonly start: (host: RuntimeHost) => AsyncResult, RuntimeStartFailed>; + // `resolves`, not `needs`: di's `Module` has a `needs` of its own and the two + // are different obligations — a module's is what a composition root supplies + // it, this is what the runtime reads back out of the built application + // context. The array is never read at run time; it exists so `Resolves` is + // inferable from the value, and `start`'s gate checks it against the + // module's exports. + readonly resolves: readonly Resolves[]; + readonly start: (host: RuntimeHost) => AsyncResult, RuntimeStartFailed>; }; /** @@ -105,7 +111,7 @@ export type Runtime = { * * Left generic on purpose (`Port("Runtime")` without a fixed service): every * runtime port is one id at runtime — a process boots exactly one — while each - * carries its own `Needs`/`Info` in the type, which is what `start`'s gate and + * carries its own `Resolves`/`Info` in the type, which is what `start`'s gate and * `RunningApp.runtimeInfo()` read back out of the module's exports. */ export const RuntimePort = Port("Runtime"); @@ -113,9 +119,10 @@ export const RuntimePort = Port("Runtime"); /** The instance type every runtime port shares — `Extract` this from a module's exports to find its runtime. */ export type RuntimeInstance = InstanceType>; -/** The `Runtime` a module exports, or `never` when it exports none. */ +/** The `Runtime` a module exports, or `never` when it exports none. */ export type RuntimeOf = ServiceOf>; -export type RuntimeNeedsOf = RuntimeOf extends Runtime ? Needs : never; +export type RuntimeResolvesOf = + RuntimeOf extends Runtime ? Resolves : never; export type RuntimeInfoOf = RuntimeOf extends Runtime ? Info : never; diff --git a/packages/core/src/start.spec.ts b/packages/core/src/start.spec.ts index 7b70cbed..021b5296 100644 --- a/packages/core/src/start.spec.ts +++ b/packages/core/src/start.spec.ts @@ -291,7 +291,7 @@ describe("runtimeInfo", () => { // `Serving.info`, which is the whole point of the default. const silent: Runtime = { name: "silent", - needs: [], + resolves: [], start: () => OkAsync({ drain: () => OkAsync(), stop: () => OkAsync() }), }; const app = start(runtimeModule(silent), { signals: false, probes: false }); diff --git a/packages/core/src/start.test-d.ts b/packages/core/src/start.test-d.ts index 97605c2b..9f8b3a3c 100644 --- a/packages/core/src/start.test-d.ts +++ b/packages/core/src/start.test-d.ts @@ -20,24 +20,24 @@ const serving: Serving = { }; // A runtime is a service on a port declared over `RuntimePort`; the port -// carries the runtime's `Needs` and `Info`, and the module exporting it is +// carries the runtime's `Resolves` and `Info`, and the module exporting it is // what `start` reads both back from. class NeedsGreeting extends RuntimePort> {} class NeedsClock extends RuntimePort> {} const needsGreeting: Runtime = { - name: "needs-greeting", - needs: [Greeting], + name: "resolves-greeting", + resolves: [Greeting], start: () => OkAsync({ ...serving, info: { port: 8080 } }), }; const needsClock: Runtime = { - name: "needs-clock", - needs: [Clock], + name: "resolves-clock", + resolves: [Clock], start: () => OkAsync(serving), }; -// The gate is satisfied: the module exports the port the runtime needs — and +// The gate is satisfied: the module exports the port the runtime resolves — and // `Info` is read off the module, not passed in. const Satisfied = Module("Satisfied")({ imports: [AppModule], @@ -54,14 +54,14 @@ const Unsatisfied = Module("Unsatisfied")({ provides: [Provider(NeedsClock)({ value: needsClock })], exports: [Greeting, NeedsClock], }); -// @ts-expect-error -- UNSATISFIED RUNTIME NEEDS: the runtime needs `Clock`, which the module does not export +// @ts-expect-error -- UNSATISFIED RUNTIME PORTS: the runtime resolves `Clock`, which the module does not export start(Unsatisfied); // WHICH arm rejected it, pinned: the directive above accepts ANY error, so the // sentence a reader is actually shown is asserted here or nowhere. expectTypeOf< StartGate ->().toEqualTypeOf<"UNSATISFIED RUNTIME NEEDS — the runtime needs a port the module does not export">(); +>().toEqualTypeOf<"UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export">(); // The other way the gate bites: a module that exports no runtime port at all. // @ts-expect-error -- NO RUNTIME: `AppModule` exports no port declared over `RuntimePort` @@ -70,12 +70,12 @@ expectTypeOf< StartGate >().toEqualTypeOf<"NO RUNTIME — the module exports no port declared over RuntimePort">(); -// A needs-free runtime works against any module: `InstanceType` is +// A runtime that resolves nothing works against any module: `InstanceType` is // `never`, and `[never] extends [X]` holds for every `X`. `testRuntime` ships // its own module, so nothing else needs composing. expectTypeOf(start(testRuntime().module)).toEqualTypeOf>(); -// The unit half of the gate, isolated: the runtime's needs are satisfied, so +// The unit half of the gate, isolated: what the runtime resolves is satisfied, so // only the unit module's unmet `Clock` can be what rejects the call. class Span extends Port("GateSpan")<{ readonly note: string }> {} @@ -99,7 +99,7 @@ expectTypeOf< StartGate >().toEqualTypeOf<"UNSATISFIED UNIT NEEDS — the unit module needs a port the module does not export">(); -// A runtime may NOT draw a need from the unit module's exports: `Span` exists +// A runtime may NOT resolve a port from the unit module's exports: `Span` exists // only while a unit is open, and `RuntimeHost.ctx` is the application context // — so a runtime that names it is rejected here rather than left to `ctx.get` // throwing at startup. @@ -120,8 +120,8 @@ const GreetingSpanUnit = Module("GreetingSpanUnit")({ class NeedsSpan extends RuntimePort> {} const needsSpan: Runtime = { - name: "needs-span", - needs: [Span], + name: "resolves-span", + resolves: [Span], start: () => OkAsync(serving), }; @@ -130,5 +130,5 @@ const SpanApp = Module("SpanApp")({ provides: [Provider(NeedsSpan)({ value: needsSpan })], exports: [Greeting, NeedsSpan], }); -// @ts-expect-error -- UNSATISFIED RUNTIME NEEDS: `Span` is a unit-only port, not among `SpanApp`'s exports +// @ts-expect-error -- UNSATISFIED RUNTIME PORTS: `Span` is a unit-only port, not among `SpanApp`'s exports start(SpanApp, { unit: GreetingSpanUnit }); diff --git a/packages/core/src/start.ts b/packages/core/src/start.ts index f86f60d8..3b015b1b 100644 --- a/packages/core/src/start.ts +++ b/packages/core/src/start.ts @@ -23,7 +23,7 @@ import { type Runtime, type RuntimeInfoOf, type RuntimeInstance, - type RuntimeNeedsOf, + type RuntimeResolvesOf, type Serving, } from "./runtime.js"; import { createUnitRegistry } from "./units.js"; @@ -125,8 +125,8 @@ export type RunningApp = { /** * The phantom marker `start`, `runMain` and `Boot` all intersect onto their * `module` parameter: `unknown` — and invisible — when the module exports a - * runtime and its exports cover that runtime's declared needs, a sentence - * otherwise, so a missing runtime or an unmet need fails to typecheck at the + * runtime and its exports cover what that runtime resolves, a sentence + * otherwise, so a missing runtime or an unresolvable port fails to typecheck at the * call site. * * It rides the `module` parameter rather than a trailing rest tuple because a @@ -143,7 +143,7 @@ export type RunningApp = { * With a `unit` module in play it also checks the fork's own direction: the * unit module's needs must be covered by the module's exports, `Scope` or * `Env` — `forkScope`'s gate stated at `start`'s call site, where the parent - * is actually known. A runtime's needs are checked against the module's + * is actually known. What a runtime resolves is checked against the module's * exports ONLY, never the unit's: `RuntimeHost.ctx` is the application * context, and a runtime that resolved a unit-only port at start would find * nothing there — so the gate rejects it rather than letting it type-check @@ -151,11 +151,11 @@ export type RunningApp = { */ export type StartGate = [Extract] extends [never] ? "NO RUNTIME — the module exports no port declared over RuntimePort" - : [InstanceType>] extends [X] + : [InstanceType>] extends [X] ? [Exclude] extends [never] ? unknown : "UNSATISFIED UNIT NEEDS — the unit module needs a port the module does not export" - : "UNSATISFIED RUNTIME NEEDS — the runtime needs a port the module does not export"; + : "UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export"; // `Module`, not `Module`: `Needs` sits in // covariant position on `Module`, so this accepts a module with no needs at @@ -170,7 +170,7 @@ export const start = ( options: StartOptions = {}, ): RunningApp> => { type Info = RuntimeInfoOf; - type Needs = RuntimeNeedsOf; + type Resolves = RuntimeResolvesOf; const clock = options.clock ?? systemClock; const env = options.env ?? process.env; const emit = safeSink(options.onEvent ?? stderrSink); @@ -400,16 +400,16 @@ export const start = ( // where the checker cannot see it. const runtime = (ctx as unknown as Context).get( RuntimePort as unknown as abstract new () => RuntimeInstance, - ) as Runtime; + ) as Runtime; runtimeName = runtime.name; // `Context` is contravariant, so an application context whose - // exports cover the runtime's needs is assignable here. The assertion is + // exports cover what the runtime resolves is assignable here. The assertion is // needed only because the `StartGate` intersected onto `module` proves - // `InstanceType extends X` at the *call site*, and that proof is - // not visible to the checker inside this body, where `X` and `Needs` are + // `InstanceType extends X` at the *call site*, and that proof is + // not visible to the checker inside this body, where `X` and `Resolves` are // still unresolved type parameters. - const runtimeCtx = ctx as unknown as Context>; + const runtimeCtx = ctx as unknown as Context>; // The registry counts and aborts; it knows nothing about contexts. The // An ANNOTATION, not an assertion: a future divergence between this @@ -418,7 +418,7 @@ export const start = ( // ambient record and the unit is not counted closed until the scope // is (`unit-module.spec.ts` guards both halves). const unit = options.unit; - const run: RunUnit = (meta, work) => + const run: RunUnit = (meta, work) => registry.run(meta, (signal) => { if (unit === undefined) return work(runtimeCtx, signal); @@ -437,7 +437,7 @@ export const start = ( unit, (forked) => fromSafePromise( - (async () => await work(forked as Context>, signal))(), + (async () => await work(forked as Context>, signal))(), ).flatMap((result) => result), { onTeardownError: (port, cause) => emit({ type: "teardownError", port, cause }) }, ) as ReturnType; diff --git a/packages/http/CLAUDE.md b/packages/http/CLAUDE.md index 07d07527..958f4d81 100644 --- a/packages/http/CLAUDE.md +++ b/packages/http/CLAUDE.md @@ -351,7 +351,7 @@ InstanceType> & { readonly port: PortClassOf them, oRPC's context stays empty), built by `HttpRouter(contract)(deps, arm)`. The starter provides `Runtime` on the **`HttpRuntime`** port (a class over - core's `RuntimePort`, **no `needs`**), which the composition root imports + core's `RuntimePort`, **an empty `resolves`**), which the composition root imports next to the application and exports so `start` finds it, and **`HttpConfig`** (`{ port, hostname }`) bound through `Config.provider` from `PORT` (default `3000`) and `HOST` (default `0.0.0.0` — a pod, not a laptop) in the kernel's @@ -416,7 +416,7 @@ plugins })`: CORS, body limits, compression, CSRF are transport policy oRPC `Type '"HttpRouter"' is not assignable to type '"@di/Scope"'`. Neither is di's `UNSATISFIED DEPENDENCIES` arity gate. `examples/order-api/src/needs-gate.test-d.ts` pins both, plus the - `StartOptions.unit` halves. There is no `UNSATISFIED RUNTIME NEEDS` case for + `StartOptions.unit` halves. There is no `UNSATISFIED RUNTIME PORTS` case for this runtime any more: it declares none. - **What it decides.** A procedure under `prefix` answers with its output or the `ORPCError` the router's `.result()` triage mapped its `Result` to diff --git a/packages/http/src/http-runtime.ts b/packages/http/src/http-runtime.ts index 6747bb15..785bb47a 100644 --- a/packages/http/src/http-runtime.ts +++ b/packages/http/src/http-runtime.ts @@ -82,7 +82,7 @@ const httpRuntime = ( securityHeaders: HttpOptions["securityHeaders"], ): Runtime => ({ name: "http", - needs: [], + resolves: [], start: (host) => listen(host, config, handler, securityHeaders), }); diff --git a/packages/observability/src/test-fixtures.ts b/packages/observability/src/test-fixtures.ts index c2cf9a1b..f5946fe5 100644 --- a/packages/observability/src/test-fixtures.ts +++ b/packages/observability/src/test-fixtures.ts @@ -54,7 +54,7 @@ export class UnitSpan extends Port("ObservabilityFixtureUnitSpan")<{ readonly op * No shipped runtime sets `UnitMeta.tenantId` — it is there for a * multi-tenant deployment to supply — so a hand-rolled one is the only way to * prove the logger carries it, and it doubles as the smallest example of a - * runtime declaring a `need`. + * runtime declaring what it `resolves`. */ class TenantRuntime extends RuntimePort> {} @@ -64,7 +64,7 @@ const tenantRuntimeModule = (tenantId: string) => Provider(TenantRuntime)({ value: { name: "tenant", - needs: [Logger], + resolves: [Logger], start: (host) => { void host.run({ kind: "tenanted", id: "unit-1", tenantId }, (ctx) => { ctx.get(Logger).info("inside a tenant's unit"); diff --git a/packages/temporal/CLAUDE.md b/packages/temporal/CLAUDE.md index 7f53075b..0ce7729d 100644 --- a/packages/temporal/CLAUDE.md +++ b/packages/temporal/CLAUDE.md @@ -147,7 +147,7 @@ TemporalConnection, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf>`** — the starter, the same shape as `@btravstack/http`'s `http()`. It provides `Runtime` on the **`TemporalRuntime`** port (a class over core's `RuntimePort`, the package's own now that the - runtime has no needs), **`TemporalConfig`** (`{ address, namespace }`) bound + runtime resolves nothing), **`TemporalConfig`** (`{ address, namespace }`) bound through `Config.provider` from `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and `TEMPORAL_NAMESPACE` (default `default`) in the kernel's `Env`, and **`TemporalConnection`** (a `NativeConnection`) as a **resourceful** provider @@ -180,7 +180,7 @@ ActivitiesPortOf }, { sync })` — ends on `Type '"TemporalActivities"' is not assignable to type '"@di/Scope"'` (the `examples/order-temporal-worker` `needs-gate.test-d.ts` pins that; there is - no `UNSATISFIED RUNTIME NEEDS` arm any more, the runtime needs nothing). + no `UNSATISFIED RUNTIME PORTS` arm any more, the runtime resolves nothing). `TemporalActivitiesPort as ActivitiesPortOf` (here and in `TemporalActivities`) is the only cast the record meets: the port is one generic value, the type it carries for `C` is what `sync` reads `impls` diff --git a/packages/temporal/src/temporal-runtime.ts b/packages/temporal/src/temporal-runtime.ts index bbec402d..97d4743a 100644 --- a/packages/temporal/src/temporal-runtime.ts +++ b/packages/temporal/src/temporal-runtime.ts @@ -213,7 +213,7 @@ export const temporal = ( activities: impls, }): Runtime => ({ name: "temporal", - needs: [], + resolves: [], start: (host) => createWorker(host, connection, bound, impls, options), }), }, diff --git a/packages/testing/src/test-runtime.ts b/packages/testing/src/test-runtime.ts index 0da4e949..5ebf4a8f 100644 --- a/packages/testing/src/test-runtime.ts +++ b/packages/testing/src/test-runtime.ts @@ -80,7 +80,7 @@ export const testRuntime = (name = "test"): TestRuntime => { provides: [Provider(TestRuntimePort)({ sync: () => runtime })], exports: [TestRuntimePort], }), - needs: [], + resolves: [], start: (host: RuntimeHost) => { run = host.run; accepting = true;