From d5be140ddd995e542e271ed9a0242b2f67daa97b Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Fri, 21 Aug 2026 23:29:29 +0200 Subject: [PATCH 1/2] refactor(core)!: a runtime resolves ports, it does not need them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two different `needs` in one framework was one too many. di's Module has one — what a composition root supplies it — and the kernel's Runtime had another, meaning the ports it reads back out of the built application context. They never appear in the same object, which is what made the collision easy to misread rather than harmless. The type parameter follows the field: Runtime, RuntimeHost, RunUnit, RuntimeResolvesOf. So does start's gate sentence, now "UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export". Every shipped runtime declares resolves: [], so an application that composes a starter 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, which is now said where the field is declared. --- .changeset/runtime-resolves.md | 40 ++++++++++++++++++++ CLAUDE.md | 6 +-- docs/examples/order-api.md | 4 +- docs/explanation/compile-time-wiring.md | 12 +++--- docs/explanation/design-decisions.md | 4 +- docs/explanation/one-process-one-runtime.md | 2 +- docs/explanation/starters.md | 10 ++--- docs/explanation/the-kernel-maps-nothing.md | 2 +- docs/explanation/why-start.md | 2 +- docs/how-to/open-a-per-request-scope.md | 4 +- docs/how-to/write-a-runtime.md | 18 ++++----- docs/index.md | 2 +- docs/reference/amqp.md | 2 +- docs/reference/core/exit-codes.md | 2 +- docs/reference/core/runtime.md | 42 ++++++++++----------- docs/reference/core/start.md | 18 ++++----- docs/reference/di/entry-points.md | 2 +- docs/reference/glossary.md | 4 +- docs/reference/http.md | 2 +- docs/reference/testing.md | 18 ++++----- docs/typedoc.core.json | 2 +- packages/amqp/CLAUDE.md | 4 +- packages/amqp/src/amqp-runtime.ts | 2 +- packages/core/CLAUDE.md | 34 ++++++++--------- packages/core/README.md | 2 +- packages/core/src/docs-examples.test-d.ts | 16 ++++---- packages/core/src/runtime.ts | 35 ++++++++++------- packages/core/src/start.spec.ts | 2 +- packages/core/src/start.test-d.ts | 8 ++-- packages/core/src/start.ts | 20 +++++----- packages/http/CLAUDE.md | 4 +- packages/http/src/http-runtime.ts | 2 +- packages/observability/src/test-fixtures.ts | 2 +- packages/temporal/CLAUDE.md | 4 +- packages/temporal/src/temporal-runtime.ts | 2 +- packages/testing/src/test-runtime.ts | 2 +- 36 files changed, 192 insertions(+), 145 deletions(-) create mode 100644 .changeset/runtime-resolves.md 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..9185c5a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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..9e26ccab 100644 --- a/docs/explanation/starters.md +++ b/docs/explanation/starters.md @@ -148,16 +148,16 @@ error at the record. ## Why `needs` disappeared -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,8 +165,8 @@ 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 +(`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 need 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..5c8069e2 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 runtime's `resolves` disappeared 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..09da4358 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 `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`. | -`Needs` is parameterised by port **classes** (`AnyPort`) but hands out -`Context>`, because di parameterises `Context` by +`Resolves` 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. -## `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/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/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..14f63437 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>`, + (`Needs 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,17 @@ 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 missing need 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 +425,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 +456,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/runtime.ts b/packages/core/src/runtime.ts index 341a4ed4..084112b7 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -36,12 +36,12 @@ export class RuntimeStartFailed extends TaggedError("RuntimeStartFailed")<{ * (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` 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>; }; /** @@ -116,6 +122,7 @@ export type RuntimeInstance = InstanceType>; /** 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..cd876b21 100644 --- a/packages/core/src/start.test-d.ts +++ b/packages/core/src/start.test-d.ts @@ -27,13 +27,13 @@ class NeedsClock extends RuntimePort> {} const needsGreeting: Runtime = { name: "needs-greeting", - needs: [Greeting], + resolves: [Greeting], start: () => OkAsync({ ...serving, info: { port: 8080 } }), }; const needsClock: Runtime = { name: "needs-clock", - needs: [Clock], + resolves: [Clock], start: () => OkAsync(serving), }; @@ -61,7 +61,7 @@ start(Unsatisfied); // 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` @@ -121,7 +121,7 @@ class NeedsSpan extends RuntimePort> {} const needsSpan: Runtime = { name: "needs-span", - needs: [Span], + resolves: [Span], start: () => OkAsync(serving), }; diff --git a/packages/core/src/start.ts b/packages/core/src/start.ts index f86f60d8..7fff532d 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"; @@ -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 // 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..0501077b 100644 --- a/packages/observability/src/test-fixtures.ts +++ b/packages/observability/src/test-fixtures.ts @@ -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; From 6a36151c57f988b53a27d90a0625f3fa615fa7c7 Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sat, 22 Aug 2026 00:17:06 +0200 Subject: [PATCH 2/2] fix(docs): the rename left the old word in the prose beside it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweep renamed the identifiers and the gate sentences and stopped there, so a dozen comments and headings still said `needs` about the runtime — including two `@ts-expect-error` directives naming an arm that no longer exists, which is the worst kind of stale: a negative that still passes while documenting the wrong gate. Every runtime-sense `needs` is now `resolves`: start.test-d's directives and runtime names, runtime.ts's own remaining `Needs` mentions, observability's fixture docstring, the three examples' gate files and README, starters.md's heading, and the reference tables. di's `Needs` channel keeps its name everywhere it appears, which is the distinction the rename existed to draw. Audited mechanically afterwards rather than by eye: every surviving `needs` near the word "runtime" is either di's channel, a unit module's, or the English verb. --- CLAUDE.md | 2 +- docs/explanation/starters.md | 4 ++-- docs/explanation/the-kernel-maps-nothing.md | 2 +- docs/reference/core/runtime.md | 14 ++++++------- docs/reference/temporal.md | 2 +- examples/README.md | 8 ++++---- .../src/needs-gate.test-d.ts | 5 +++-- .../src/needs-gate.test-d.ts | 6 +++--- packages/core/CLAUDE.md | 5 +++-- packages/core/src/run-main.ts | 2 +- packages/core/src/runtime.ts | 6 +++--- packages/core/src/start.test-d.ts | 20 +++++++++---------- packages/core/src/start.ts | 8 ++++---- packages/observability/src/test-fixtures.ts | 2 +- 14 files changed, 44 insertions(+), 42 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9185c5a5..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 diff --git a/docs/explanation/starters.md b/docs/explanation/starters.md index 9e26ccab..54019588 100644 --- a/docs/explanation/starters.md +++ b/docs/explanation/starters.md @@ -146,7 +146,7 @@ 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 `resolves` field, and `start`'s gate checks it against the module's exports. **No shipped starter uses it any more.** Each @@ -167,7 +167,7 @@ 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.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 need them. +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 5c8069e2..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 `resolves` 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/reference/core/runtime.md b/docs/reference/core/runtime.md index 09da4358..6e17ae55 100644 --- a/docs/reference/core/runtime.md +++ b/docs/reference/core/runtime.md @@ -22,16 +22,16 @@ type Runtime = { }; ``` -| 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 `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`. | `Resolves` 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. +port **instance** types. `InstanceType` is `never`, so a runtime that +resolves nothing gets a context it can read nothing from. ## `RuntimeHost` 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/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/core/CLAUDE.md b/packages/core/CLAUDE.md index 14f63437..bbd2d15b 100644 --- a/packages/core/CLAUDE.md +++ b/packages/core/CLAUDE.md @@ -123,7 +123,7 @@ never>`: `Needs` is covariant on `Module`, so this accepts a needs-free - **`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 @@ -351,7 +351,8 @@ Type-level invariants live in `start.test-d.ts` and are checked by 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 PORTS — …`. + `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. 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 084112b7..e0128f29 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -35,7 +35,7 @@ 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>`, 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. @@ -111,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"); @@ -119,7 +119,7 @@ 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 RuntimeResolvesOf = diff --git a/packages/core/src/start.test-d.ts b/packages/core/src/start.test-d.ts index cd876b21..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", + name: "resolves-greeting", resolves: [Greeting], start: () => OkAsync({ ...serving, info: { port: 8080 } }), }; const needsClock: Runtime = { - name: "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,7 +54,7 @@ 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 @@ -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,7 +120,7 @@ const GreetingSpanUnit = Module("GreetingSpanUnit")({ class NeedsSpan extends RuntimePort> {} const needsSpan: Runtime = { - name: "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 7fff532d..3b015b1b 100644 --- a/packages/core/src/start.ts +++ b/packages/core/src/start.ts @@ -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 @@ -404,7 +404,7 @@ export const start = ( 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 `Resolves` are diff --git a/packages/observability/src/test-fixtures.ts b/packages/observability/src/test-fixtures.ts index 0501077b..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> {}