Skip to content

feat(start-http): the HTTP runtime, and order-api migrated onto it - #14

Merged
btravers merged 23 commits into
mainfrom
docs/start-http-design
Aug 12, 2026
Merged

feat(start-http): the HTTP runtime, and order-api migrated onto it#14
btravers merged 23 commits into
mainfrom
docs/start-http-design

Conversation

@btravers

Copy link
Copy Markdown
Contributor

Ships @btravstack/start-http, the first of the three deferred runtime packages, and migrates examples/order-api off its hand-rolled transport onto it. Spec and plan are included as the first commits.

The package owns a lifecycle, not a framework

CLAUDE.md had carried -http as "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 part examples/order-api proved genuinely hard was the lifecycle — and two of the defects found in this morning's review lived in exactly that code.

start(AppModule,{runtime: httpRuntime({port: env.PORT,needs: [PlaceOrder,FindOrder,Logger],handler: (req,res,ctx)=>rpc.handle(req,res,{context: { ctx }}),}),});

Verified compatible against the shipped type definitions, not assumed: oRPC's RPCHandler.handle, Hono's getRequestListener (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's close event 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 a 500 — otherwise the client hangs and the unit sits in flight until the drain abandons it. oRPC's matched: false is exactly that case.

Three behaviours move out of the example and into the package, each one paid for already:

  • Connection: close on 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.
  • The bind's synchronous throw is caught.listen validates the port itself and throws ERR_SOCKET_BAD_PORT; uncaught it becomes a Defect and bypasses the declared error channel.
  • The server keeps an 'error' listener for life. Zero listeners turns a post-bind EMFILE into an unhandled 'error', which the kernel's uncaughtException handler escalates into killing the application.

examples/order-api

Loses 274 lines of transport, keeps what it exists to teach: the oRPC router, the ResultORPCError mapping, the per-request Module.forkScope, the contract split. Its needs-gate.test-d.ts still pins start's phantom rest-tuple gate — verified by stripping the @ts-expect-error and 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 (the vi.mock now lives in the package's fixtures, where test convention 1 isn't at stake) and the headersSent retire 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.ts carried 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, build green. 185 tests across 10 workspaces; packages/start-http at 100% lines and functions, enforced. The single failure is packages/start's binds 9000 when no probe port is given — environmental (EADDRINUSE on 127.0.0.1:9000 locally, 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, a recoverDefect swap 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-temporal as 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

  • hostname defaults to 0.0.0.0 — the deployment target is a pod; a laptop should set 127.0.0.1.
  • An AsyncResult carrying an Errresolves, so it reaches 404, not 500. The package does not map Result → status.
  • Serving.drain's signal is 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 in CLAUDE.md's deferred list rather than left silent.

🤖 Generated with Claude Code

Benoit Traversand others added 23 commits August 12, 2026 17:12
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>
CopilotAI lite review requested due to automatic review settings August 12, 2026 19:38

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-http with httpRuntime(...), docs, and a 100%-coverage vitest suite.
  • Migrate examples/order-api to use httpRuntime + an extracted apiHandler, 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
FileDescription
README.mdUpdates root README wording to reflect start-http shipping and handler-owned Result → HTTP mapping.
pnpm-lock.yamlAdds workspace link entries for packages/start-http and examples/order-api dependency updates.
packages/start/src/probes.spec.tsPrevents a vacuous pass by asserting the mocked server capture is non-empty before emitting events.
packages/start/src/docs-examples.test-d.tsKeeps type-checked docs sample text in sync with updated README phrasing.
packages/start/README.mdUpdates package README sample text consistent with the root README change.
packages/start/CLAUDE.mdUpdates internal guidance to reference @btravstack/start-http instead of the old example runtime.
packages/start-http/vitest.config.tsAdds package-local vitest config with enforced 100% line/function thresholds.
packages/start-http/tsconfig.jsonAdds package tsconfig extending the shared base config.
packages/start-http/src/vitest.d.tsEnsures @unthrown/vitest matcher types are available in the package.
packages/start-http/src/test-fixtures.tsAdds vitest fixtures (server capture, keep-alive socket tooling, gate handlers) for runtime behavior tests.
packages/start-http/src/index.tsExposes httpRuntime and its public types.
packages/start-http/src/http-runtime.tsImplements 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.tsComprehensive 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.mdDocuments scope, guarantees, non-goals, options, and behavior of @btravstack/start-http.
packages/start-http/package.jsonAdds the new publishable package manifest, exports, scripts, and dependency metadata.
packages/start-http/LICENSEAdds MIT license for the new package.
examples/README.mdUpdates examples documentation to reflect order-api using start-http and the current gate exercise points.
examples/order-temporal/README.mdUpdates cross-example Serving.info shape references to match HttpInfo.
examples/order-api/src/test-fixtures.tsRewires fixtures to start order-api via httpRuntime and removes transport-level keep-alive tooling moved into the package.
examples/order-api/src/orpc-runtime.tsDeletes the previous bespoke HTTP/oRPC runtime implementation.
examples/order-api/src/needs-gate.test-d.tsUpdates needs-gate typing test to reference httpRuntime and the extracted handler.
examples/order-api/src/module.tsUpdates composition-root documentation to match the new httpRuntime needs declaration site.
examples/order-api/src/main.tsSwitches runtime boot to httpRuntime with explicit needs and the extracted apiHandler.
examples/order-api/src/index.tsUpdates exports to expose apiHandler/ApiNeeds and remove old runtime exports.
examples/order-api/src/handler.tsIntroduces apiHandler that performs per-request Module.forkScope + oRPC dispatch for use with httpRuntime.
examples/order-api/src/api.spec.tsRenames and updates tests to reflect HttpInfo and removal of transport-specific tests migrated to start-http.
examples/order-api/README.mdUpdates example docs to point to start-http for transport lifecycle concerns and focuses the example on handler/router concerns.
examples/order-api/package.jsonAdds dependency on @btravstack/start-http and updates description accordingly.
docs/superpowers/specs/2026-08-12-start-http-design.mdAdds the design spec documenting scope/decisions/public surface for start-http.
docs/superpowers/plans/2026-08-12-start-http.mdAdds the implementation plan detailing tasks, constraints, and verification steps.
CLAUDE.mdUpdates repo-level guidance to include start-http as shipped and adjusts related documentation/convention notes.
.changeset/start-http.mdAdds 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.

@btravers
btravers merged commit c64d279 into mainAug 12, 2026
14 checks passed
@btravers
btravers deleted the docs/start-http-design branch August 12, 2026 21:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@btravers