Skip to content

feat(examples): add the worker deployment and the examples index - #6

Merged
btravers merged 2 commits into
mainfrom
feat/examples-worker
Aug 11, 2026
Merged

feat(examples): add the worker deployment and the examples index#6
btravers merged 2 commits into
mainfrom
feat/examples-worker

Conversation

@btravers

Copy link
Copy Markdown
Contributor

Task 4 — the last of the clean-architecture example plan. A second runtime (an in-memory queue worker) over the same application, plus the examples index and docs.

What this proves

The kernel's headline claim is one process, one runtime: several runtime kinds exist, a process boots exactly one, and the same application module boots under any of them. order-api proved a runtime could be built. This proves the claim.

packages/start, order-domain, order-application and order-infrastructure are byte-unchanged — verified by diff stat, not asserted. The worker composes the same ApplicationModule + PersistenceModule and boots it under a different transport, and nothing in the layers below moved to accommodate it. RunUnit, UnitMeta and Serving.info fit a queue unmodified.

The same Result, two transports, the kernel involved in neither. This is the sharpest demonstration in the example set:

Outcome oRPC API Queue worker
Ok 200 with the order ack
DuplicateOrder ORPCError code CONFLICT dead-letter
InvalidQuantity ORPCError code INVALID_QUANTITY dead-letter
Defect INTERNAL_SERVER_ERROR bounded retry (3 attempts), then dead-letter

That last row is the interesting inversion: a 500 over HTTP is precisely the outcome worth redelivering on a queue. It gives the defect channel a runtime meaning rather than leaving it as "the thing that isn't an error".

Every arm is named — no P._, per this repo's no-catch-all-pattern rule.

The needs gate holds here too, verified in both directions by deleting the @ts-expect-error and confirming the UNSATISFIED RUNTIME NEEDS arity error, then restoring.

Also: each job is its own unit with a distinct trace id, and draining waits for the in-flight job ({ inFlightAtStart: 1, completed: 1, abandoned: 0 }).

Docs

examples/README.md (following btravstack/di's shape — the layering, the dependency direction, and that these are the only runtimes in the repo with non-empty needs), a link from the root README, and CLAUDE.md recording that examples/ is part of the gate, that the Prisma client is generated at test time, and that oRPC is pinned to a beta because latest is the 1.x line.

Verification

150 tests (93 kernel + 57 examples), up from 139. Full six-command gate green, no Docker.

One process note, disclosed rather than buried: spec and implementation were written together, so the suite was never seen red. In place of that, the two assertions this task turns on were mutation-tested — routing the DuplicateOrder arm to ack, and collapsing the per-job id to a constant — and both fail loudly. Outputs are in the task report.

The same ApplicationModule + PersistenceModule composition, booted under an
in-memory queue worker instead of the oRPC server — with no change to
order-application, order-infrastructure or the kernel. The same DuplicateOrder
that is a typed CONFLICT over HTTP is a dead-letter here, and a Defect that is
an INTERNAL_SERVER_ERROR over HTTP is worth another delivery on a queue.

queueWorkerRuntime declares a non-empty needs of its own ([PlaceOrder, Logger],
two of the three the module exports), pinned in both directions by
needs-gate.test-d.ts. A delivery is the unit and the message id is the traceId,
since a retried message is two units; the disposition is applied inside the
unit, because acking a message and flushing a response are the same obligation.

Adds examples/README.md as the index — the layering, the dependency direction,
and the two runtimes being the only ones in the repo with a non-empty needs —
plus links from the root README, and CLAUDE.md records that examples/ is part
of the gate, that the Prisma client is generated at test time, and why oRPC is
pinned to an exact beta.
Copilot AI lite review requested due to automatic review settings August 11, 2026 22:49

Copilot AI 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

Adds a second example deployment (order-worker) that boots the existing clean-architecture order application under an in-memory queue worker runtime, plus an examples index and cross-links so the repo documentation points at the “one application, two runtimes” proof.

Changes:

  • Introduce examples/order-worker as a new workspace: queue broker abstraction, queue runtime, env parsing, composition root, and test suite (including needs-gate type tests).
  • Add examples/README.md as an index explaining the five example packages, layering, and the “same Result, two transports” mapping.
  • Update root docs/spec and repo tooling to reference and include the new workspace (README, CLAUDE, knip, lockfile).

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
README.md Links to examples/ and updates documentation section to reference the examples set.
pnpm-lock.yaml Adds lock entries for the new examples/order-worker workspace dependencies.
knip.json Registers examples/order-worker entrypoint for Knip workspace analysis.
examples/README.md New examples index explaining package roles, layering, and the two deployments.
examples/order-worker/vitest.config.ts New Vitest config for the worker example package.
examples/order-worker/tsconfig.test-d.json New TS config for type-level tests (*.test-d.ts) in worker package.
examples/order-worker/tsconfig.json New TS config for the worker package (incl. allowImportingTsExtensions).
examples/order-worker/src/vitest.d.ts Adds @unthrown/vitest type registration for tests.
examples/order-worker/src/test-fixtures.ts Provides Vitest fixtures for starting apps, queue access, and specialized test graphs.
examples/order-worker/src/queue.ts Implements an in-memory queue/broker abstraction with publish/claim/requeue/settle.
examples/order-worker/src/queue-runtime.ts Implements the queue worker Runtime and maps Result → ack/retry/dead-letter.
examples/order-worker/src/queue-runtime.spec.ts End-to-end runtime behavior specs (ack, dead-letter, retry, info publishing, draining).
examples/order-worker/src/needs-gate.test-d.ts Type-level tests pinning the runtime needs gate behavior at start(...).
examples/order-worker/src/module.ts Defines OrderWorkerModule composition root for the worker deployment.
examples/order-worker/src/main.ts Worker process entry: validate env, start, and runMain.
examples/order-worker/src/index.ts Package public exports barrel for worker example.
examples/order-worker/src/env.ts Env schema and parsing as a Result value (no throwing parse).
examples/order-worker/src/env.spec.ts Env parsing tests (defaults, parsing, malformed values, range validation).
examples/order-worker/README.md Package README documenting purpose, mapping, unit/trace semantics, and running commands.
examples/order-worker/package.json New workspace package definition and scripts (test/typecheck/test:types).
examples/order-api/README.md Cross-links to order-worker and updates test count line.
CLAUDE.md Updates authoritative spec to include examples/ as part of the gate and documents example-related conventions.
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.

Comment thread examples/order-worker/src/queue.ts Outdated
`publish`'s docstring claimed a settlement always arrives "because the
worker's attempt budget is finite". The budget bounds the redeliveries of a
job a worker has *claimed*; nothing bounds the wait for a claim. With no
worker running — or one that stopped or drained with jobs still queued — the
promise never resolves and an awaited `publish` hangs.

Say so, rather than inventing broker behaviour to satisfy the old sentence:
settling still-pending messages on shutdown is the opposite of what a durable
queue does, it cannot be honoured by the AMQP channel this example promises
swaps in unchanged, and it would still not cover the no-worker case.

`Serving.drain` gets the same treatment: it stops *claiming*, so a message not
yet claimed stays in the queue unsettled and the drain does not wait for it.

Two specs pin both halves, racing a publish against one macrotask turn via the
new `withinATurn` fixture — a bound, since the delivery path holds no timer —
so the legitimate never-settles state fails in a millisecond instead of timing
out. 150 tests -> 152.
@btravers
btravers merged commit 974cc7b into main Aug 11, 2026
13 checks passed
@btravers
btravers deleted the feat/examples-worker branch August 11, 2026 23:39
btravers added a commit that referenced this pull request Aug 12, 2026
…to 0

The previous commit's bare `z.coerce.number()` dropped a guard that was earning
its keep. `Number("")` is `0`, and a port's `min` is `0` so that an ephemeral
bind stays expressible — so `PORT=` silently bound an ephemeral port instead of
being reported. That is a regression, not a documented quirk, and an example
that ships a footgun teaches the footgun.

A non-empty trimmed string in front of the coercion closes it while keeping the
simplification the reviewer asked for:

  z.string().trim().min(1)
    .pipe(z.coerce.number<string>().int().min(min).max(max))
    .default(fallback)

Still no regex and no hand-rolled `.transform(Number)`. The `<string>` type
argument is required because `z.coerce.number()`'s input is `unknown`, which
`.pipe` will not accept from a `string`. `.default(...)` still applies only when
the variable is genuinely absent, so reject-to-default is unchanged too.

All seven cases are now pinned by a test in each of the three env specs: absent,
"", whitespace, "abc", "3.5", a valid value, out of range. `order-api`'s original
`{ PORT: "abc", PROBE_PORT: "" }` fixture is restored and passes unchanged, and
the notes describing the hole are gone from the three env.ts files, the README
and CLAUDE.md #6 — which states the rule plainly again.
Sign up for free to 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