Uh oh!
There was an error while loading. Please reload this page.
feat(start-http): the HTTP runtime, and order-api migrated onto it - #14
Conversation
The kernel has carried `-http` as "routing, middleware, Result → HTTP status"
since it shipped. This spec rejects two thirds of that: routing and middleware
are solved by oRPC, Hono and Express, and the part `examples/order-api` proved
genuinely hard was the lifecycle — unit per request, flushing inside the unit,
and a drain that actually stops accepting. Two of today's review defects lived
in exactly that code.
Four decisions, each with alternatives weighed:
- Lifecycle only; the caller brings the handler. Result → status is a non-goal
for v1 — it is one `mapErrCases` in the caller's own code, and shipping a
mapping nobody asked for is how a lifecycle package becomes a framework.
- A request's unit closes when the RESPONSE completes, not when the handler
settles, turning the kernel's least-checkable contract into structure.
- The package mints `UnitMeta` with no knobs, so neither documented footgun —
a route template as `id`, a blank `x-request-id` winning — is reachable.
- `examples/order-api` migrates onto the package, so it gains a consumer inside
the gate.
Compatibility with oRPC, Hono and `@unthrown/orpc` is verified against the
shipped type definitions rather than assumed, and that check is what caught the
handler contract: oRPC resolves `{ matched: false }` without writing, so the
fallback must fire on any settle with the response still open, not only on
rejection. Self-review then narrowed the return type to `PromiseLike<unknown>`,
since a `void`-returning handler writing asynchronously would draw a premature
404 over a response still in flight.
`-amqp` and `-temporal` are deliberately out of scope; each gets its own cycle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Eleven tasks against the 2026-08-12 design, each ending in an independently testable deliverable and each following the repo's own TDD cycle: failing test, watch it fail, minimal implementation, watch it pass, commit. The order is chosen so every task can be reviewed on its own — bind, then the two bind-failure paths, then the permanent error listener, then unit-per-request, then the always-answer fallback, then the trace-id contract, then the drain. The example migration and the documentation land last, once the package they describe exists. Three risks are written into the plan with their fallbacks rather than left to be discovered: node's `fetch` may refuse to send a blank `x-request-id` (use the raw-socket fixture instead), `Module.forkScope` may return an `AsyncResult` rather than a promise (both satisfy `PromiseLike`, so match it rather than cast), and `peerDependencies: workspace:^` may not survive `strictPeerDependencies`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A pre-flight scan before execution found the plan mandating two things the review rubric treats as defects, both introduced while writing it. - Task 5 implemented `metaFor` complete with the blank-header guard, leaving Task 7 to "write the tests and confirm they pass". A test that never failed proves nothing. Task 5 now takes `x-request-id` verbatim and Task 7 owns the guard, driven by its own failing test. - Task 5's `answer` stub carried a `response` parameter it did not use, which a reviewer should flag as dead code. It now takes only the promise; Task 6 introduces the parameter alongside the fallback that needs it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 1 mandated `@btravstack/di` and `@btravstack/start` as devDependencies of a scaffold that imports neither, which fails `pnpm knip` and leaves the branch with a red commit — against the plan's own Global Constraints. Task 2 now adds them alongside the code that uses them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…import Task 1's scaffold left @btravstack/di and @btravstack/start as devDependencies of packages/start-http/package.json, but src/index.ts imports neither, so `pnpm knip` flagged both as unused and the gate went red. peerDependencies keeps both unchanged; Task 2 adds them back as devDependencies alongside the code that uses them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Serving` obliges Task 2 to write `drain` and `stop` the moment it builds one, but their tests cannot land until Tasks 3 and 8 — so 100% thresholds from Task 1 left six consecutive commits red, against the plan's own Global Constraints, and made a real coverage regression indistinguishable from the expected one. Task 8 turns them on once every path is reachable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 8 turns them back on once drain and stop are both fully exercised by tests, per the plan amendment in eca0fad.
Also adds the 'error' listener the occupied fixture's blocker server was missing since Task 3, carried over from that task's review.
Optional chaining on the last captured server let the test pass while asserting nothing if the node:http mock ever stopped intercepting. Move the lookup into a fixture (start-http) and a local helper (start's probes.spec.ts, same pre-existing shape) that assert.ok's the server exists, so the test body holds one deep assertion against a value that cannot be undefined.
metaFor took x-request-id verbatim, so an empty header beat the freshly minted trace id (traceId only falls back to meta.id when nullish, and "" is not) -- silently defeating the ambient record for every request from that caller, exactly as passing a route template would.
closeIdleConnections() only reaches connections idle at the instant the
drain starts; a connection with a request in flight survives it and node
keeps serving new requests down it for the whole drain window. Mark every
open response Connection: close (or end its socket on finish, if headers
are already on the wire) the moment the drain begins, so a busy connection
is retired too.
Also replaces the request handler's match({ errCases, defect }) with
recoverDefect: the work callback is typed AsyncResult<void, never>, so
errCases was a provably dead arm with no case to name. recoverDefect names
only the channel that can actually be present, and turning on the
package's 100%-lines/functions coverage gate is what surfaced it.
Turns on coverage thresholds (100% lines/functions) now that every path
the runtime has is reachable by a test, including the streamed-response
shape retire's headersSent branch exists for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>recoverDefect wraps a synchronous throw inside its own callback into a fresh, unaudited Defect, and that new Result was left unexamined by the trailing void. Wrap response.destroy() in a try/catch so the callback provably cannot throw, closing the gap without adding a third exception to the "no Result left unexamined" rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README.md already carried a [MIT](./LICENSE) footer, matching the sibling package, but Task 1 never created packages/start-http/LICENSE, so the link was dead. Copied verbatim from packages/start/LICENSE. Also tightens the Install section's peer-dependency sentence to match the kernel README's phrasing.
order-api hand-rolled the HTTP server lifecycle in orpc-runtime.ts; that
lifecycle now lives in the published @btravstack/start-http package, so
this example becomes a consumer of it inside the gate instead of a second
implementation. Only the per-request Module.forkScope, the oRPC router and
its Result -> ORPCError mapping remain the example's own.
OrderApiInfo's { port, prefix } is gone with it: the package publishes
{ port } only, and prefix is now a constant the example holds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>CLAUDE.md and README.md still described start-http as deferred, promising routing, middleware and Result -> HTTP status; two thirds of that is now explicitly out of scope. Corrects both, plus examples/README.md's stale Info teaching point (the sentence actually lives in examples/order-temporal/README.md) and a handful of drift the last few tasks left behind: orpcRuntime, an example test count, and two CLAUDE.md bullets pointing at examples/order-api/src/orpc-runtime.ts, a file Task 10 deleted. Adds the release changeset for the new package. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs-examples.test-d.ts carries its own copy of the ticker sample's comment, per CLAUDE.md's own doc-sync rule (CLAUDE.md, both READMEs, and this file together). The previous commit fixed the other two copies but missed this one, since it's prose pnpm typecheck cannot flag as drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ound The final whole-branch review of @btravstack/start-http found five documentation claims this branch itself made false (a stale "only non-empty needs" claim in two places, an example still described as containing a Runtime it no longer has, the new package missing from CLAUDE.md's Public surface and peer-dependency notes, and its README samples having no compile gate) plus an unused `PREFIX` export and one unguarded rethrow. - Narrow the "examples/ is the only place a non-empty `needs` meets a real module" claim in CLAUDE.md and examples/README.md: start-http's own test-fixtures.ts now exercises the same runtime-side path, so the claim left standing is that examples/ is the only place the gate is pinned by a type test. - Fix order-api's README and package.json description, which still described a Runtime the package no longer ships (deleted with orpc-runtime.ts). - Drop the now-unused `PREFIX` export from order-api's index.ts rather than wiring it into the client, which would drag handler.ts's server-side application graph into the client module. - Document @btravstack/start-http's public surface in CLAUDE.md, fix its peer-dependency list (start-http adds @btravstack/start), and pluralise the two "the published package" bullets now that there are two. - Record start-http's missing docs-examples.test-d.ts gate in Deferred, deliberately. - Guard http-runtime.ts's catch-arm `end(..., 500, ...)` behind its own try/catch, so a throw there can no longer reject the `void`-dropped `answer()` promise and trip the kernel's unhandledRejection handler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces @btravstack/start-http, an HTTP runtime package that implements the @btravstack/start runtime contract (bind, one unit per request, drain/stop semantics), and migrates examples/order-api from its bespoke transport onto this shared runtime. It also updates repo docs/specs/plans to reflect start-http as shipped and tightens one kernel test to avoid a vacuously-passing assertion.
Changes:
- Add
packages/start-httpwithhttpRuntime(...), docs, and a 100%-coverage vitest suite. - Migrate
examples/order-apito usehttpRuntime+ an extractedapiHandler, deleting the old hand-rolled runtime. - Update root/package/example documentation and type-checked docs samples to reflect the shipped HTTP runtime and revised responsibilities.
Reviewed changes
Copilot reviewed 30 out of 32 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates root README wording to reflect start-http shipping and handler-owned Result → HTTP mapping. |
| pnpm-lock.yaml | Adds workspace link entries for packages/start-http and examples/order-api dependency updates. |
| packages/start/src/probes.spec.ts | Prevents a vacuous pass by asserting the mocked server capture is non-empty before emitting events. |
| packages/start/src/docs-examples.test-d.ts | Keeps type-checked docs sample text in sync with updated README phrasing. |
| packages/start/README.md | Updates package README sample text consistent with the root README change. |
| packages/start/CLAUDE.md | Updates internal guidance to reference @btravstack/start-http instead of the old example runtime. |
| packages/start-http/vitest.config.ts | Adds package-local vitest config with enforced 100% line/function thresholds. |
| packages/start-http/tsconfig.json | Adds package tsconfig extending the shared base config. |
| packages/start-http/src/vitest.d.ts | Ensures @unthrown/vitest matcher types are available in the package. |
| packages/start-http/src/test-fixtures.ts | Adds vitest fixtures (server capture, keep-alive socket tooling, gate handlers) for runtime behavior tests. |
| packages/start-http/src/index.ts | Exposes httpRuntime and its public types. |
| packages/start-http/src/http-runtime.ts | Implements the HTTP runtime lifecycle, per-request unit binding to response completion, drain retirement behavior, and trace-id policy. |
| packages/start-http/src/http-runtime.spec.ts | Comprehensive runtime tests covering bind failures, post-bind errors, per-request unit lifecycle, 404/500 fallback, trace id adoption, and drain/keep-alive behavior. |
| packages/start-http/README.md | Documents scope, guarantees, non-goals, options, and behavior of @btravstack/start-http. |
| packages/start-http/package.json | Adds the new publishable package manifest, exports, scripts, and dependency metadata. |
| packages/start-http/LICENSE | Adds MIT license for the new package. |
| examples/README.md | Updates examples documentation to reflect order-api using start-http and the current gate exercise points. |
| examples/order-temporal/README.md | Updates cross-example Serving.info shape references to match HttpInfo. |
| examples/order-api/src/test-fixtures.ts | Rewires fixtures to start order-api via httpRuntime and removes transport-level keep-alive tooling moved into the package. |
| examples/order-api/src/orpc-runtime.ts | Deletes the previous bespoke HTTP/oRPC runtime implementation. |
| examples/order-api/src/needs-gate.test-d.ts | Updates needs-gate typing test to reference httpRuntime and the extracted handler. |
| examples/order-api/src/module.ts | Updates composition-root documentation to match the new httpRuntime needs declaration site. |
| examples/order-api/src/main.ts | Switches runtime boot to httpRuntime with explicit needs and the extracted apiHandler. |
| examples/order-api/src/index.ts | Updates exports to expose apiHandler/ApiNeeds and remove old runtime exports. |
| examples/order-api/src/handler.ts | Introduces apiHandler that performs per-request Module.forkScope + oRPC dispatch for use with httpRuntime. |
| examples/order-api/src/api.spec.ts | Renames and updates tests to reflect HttpInfo and removal of transport-specific tests migrated to start-http. |
| examples/order-api/README.md | Updates example docs to point to start-http for transport lifecycle concerns and focuses the example on handler/router concerns. |
| examples/order-api/package.json | Adds dependency on @btravstack/start-http and updates description accordingly. |
| docs/superpowers/specs/2026-08-12-start-http-design.md | Adds the design spec documenting scope/decisions/public surface for start-http. |
| docs/superpowers/plans/2026-08-12-start-http.md | Adds the implementation plan detailing tasks, constraints, and verification steps. |
| CLAUDE.md | Updates repo-level guidance to include start-http as shipped and adjusts related documentation/convention notes. |
| .changeset/start-http.md | Adds a changeset to release @btravstack/start-http as a minor version bump. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Uh oh!
There was an error while loading. Please reload this page.
Ships
@btravstack/start-http, the first of the three deferred runtime packages, and migratesexamples/order-apioff its hand-rolled transport onto it. Spec and plan are included as the first commits.The package owns a lifecycle, not a framework
CLAUDE.mdhad carried-httpas "routing, middleware,Result→ HTTP status" since the kernel shipped. Two thirds of that is now explicitly out of scope and will not ship. Routing and middleware are solved by oRPC, Hono and Express; the partexamples/order-apiproved genuinely hard was the lifecycle — and two of the defects found in this morning's review lived in exactly that code.Verified compatible against the shipped type definitions, not assumed: oRPC's
RPCHandler.handle, Hono'sgetRequestListener(notserve(), which owns its own server), and@unthrown/orpc, which sits inside a procedure and never sees the server.The central claim
A request's unit closes when the response completes, not when the handler settles. The kernel documents "flush the response inside the unit" as one of two contracts it cannot check — break it and an 8 MB body dies with
UND_ERR_SOCKET: other side closed. Tying the unit to the response'scloseevent makes that structural: writing late is impossible, because the unit is still open until the bytes are out.That creates a second obligation the package also takes on: every request produces exactly one completed response. A handler that resolves without writing gets a
404, one that rejects gets a500— otherwise the client hangs and the unit sits in flight until the drain abandons it. oRPC'smatched: falseis exactly that case.Three behaviours move out of the example and into the package, each one paid for already:
Connection: closeon responses open when the drain begins.closeIdleConnections()reaches only connections idle at that instant; measured on Node 22.19, a busy one survives and node keeps serving requests down it for the whole drain window.listenvalidates the port itself and throwsERR_SOCKET_BAD_PORT; uncaught it becomes aDefectand bypasses the declared error channel.'error'listener for life. Zero listeners turns a post-bindEMFILEinto an unhandled'error', which the kernel'suncaughtExceptionhandler escalates into killing the application.examples/order-apiLoses 274 lines of transport, keeps what it exists to teach: the oRPC router, the
Result→ORPCErrormapping, the per-requestModule.forkScope, the contract split. Itsneeds-gate.test-d.tsstill pinsstart's phantom rest-tuple gate — verified by stripping the@ts-expect-errorand confirming it still fails.Two tests moved into the package rather than being duplicated, and both closed a deferred item honestly: the permanent-
'error'-listener test (thevi.mocknow lives in the package's fixtures, where test convention 1 isn't at stake) and theheadersSentretire branch, which was recorded as unreachable through oRPC's router and is reachable here because the test supplies the handler.Also fixed
packages/start/src/probes.spec.tscarried a vacuous-capable assertion of the same shape —created?.emit(...)passes when the capture is empty. Merged this morning in #11; fixed here, and the guard was verified to fail loudly by deliberately breaking the mock.Gate
format,lint,typecheck,knip,buildgreen. 185 tests across 10 workspaces;packages/start-httpat 100% lines and functions, enforced. The single failure ispackages/start'sbinds 9000 when no probe port is given— environmental (EADDRINUSEon127.0.0.1:9000locally, passes in CI).Review trail, and what it caught
Eleven tasks, each implemented by a fresh agent and reviewed independently. Worth reading sceptically rather than as a warrant: nearly every Important finding was against the plan, not the implementations — devDependencies that failed
knip, coverage thresholds six tasks premature, arecoverDefectswap that reintroduced the gap it was meant to close, a brief pointing at the wrong file, and a branch declared unreachable that wasn't. That last would have shipped an untested behaviour — what happens to a streamed response when the drain begins — because the limitation was copied across a boundary where it didn't apply.One re-review cited
start-temporalas a published package to justify its verdict. It doesn't exist. That verdict was independently verified before being accepted.Deliberate, so they read as decisions rather than gaps
hostnamedefaults to0.0.0.0— the deployment target is a pod; a laptop should set127.0.0.1.AsyncResultcarrying anErrresolves, so it reaches404, not500. The package does not mapResult→ status.Serving.drain'ssignalis unused: HTTP has nothing to escalate to, unlike Temporal.packages/start-http/README.md's samples are compiled by nothing, unlike the kernel's — recorded inCLAUDE.md's deferred list rather than left silent.🤖 Generated with Claude Code