Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/drain-and-shutdown-fixes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@btravstack/start": minor
"@btravstack/start-core": minor
---

Seven shutdown-path fixes found by a full review of the kernel. Five change
Expand Down
2 changes: 1 addition & 1 deletion .changeset/drop-version-const.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@btravstack/start": minor
"@btravstack/start-core": minor
---

Remove the `VERSION` export.
Expand Down
4 changes: 2 additions & 2 deletions .changeset/initial-kernel.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@btravstack/start": minor
"@btravstack/start-core": minor
---

The application kernel: `start` boots a `@btravstack/di` module into a running
Expand Down Expand Up @@ -31,7 +31,7 @@ application scope on every path.
`70`) by setting `process.exitCode`.
- `currentUnit()` over an `AsyncLocalStorage` record carrying
`{ unitId, traceId, tenantId, deadline }` — data, never capabilities.
- A `@btravstack/start/testing` entry point with `testRuntime`,
- A `@btravstack/start-core/testing` entry point with `testRuntime`,
`createFakeClock` and `withApp`.
- **Every async API returns an `AsyncResult`, never a bare `Promise`** — the
infallible ones included, where `AsyncResult<T, never>` spells "async, and
Expand Down
2 changes: 1 addition & 1 deletion .changeset/start-amqp.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"@btravstack/start-amqp": minor
---

The AMQP consumer runtime for `@btravstack/start`.
The AMQP consumer runtime for `@btravstack/start-core`.

`amqpRuntime({ urls, contract, handlers, needs })` runs an `amqp-contract`
worker under the kernel's lifecycle: one unit per delivery, and a drain where
Expand Down
2 changes: 1 addition & 1 deletion .changeset/start-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"@btravstack/start-http": minor
---

The HTTP runtime for `@btravstack/start`.
The HTTP runtime for `@btravstack/start-core`.

`httpRuntime({ port, needs, handler })` owns an HTTP server's lifecycle and
nothing else: it binds (publishing the real port on `Serving.info`, so
Expand Down
2 changes: 1 addition & 1 deletion .changeset/start-temporal.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"@btravstack/start-temporal": minor
---

The Temporal worker runtime for `@btravstack/start`.
The Temporal worker runtime for `@btravstack/start-core`.

`temporalRuntime({ connection, taskQueue, workflows, activities, needs })` runs a
Temporal worker under the kernel's lifecycle: one unit per activity attempt, and
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
# pnpm 11 needs `node:sqlite`, which Node 20 does not have. That floor is
# declared, not proven.
node-versions: '["", "22.19", "24", "26"]'
# The reusable default appends `--coverage`; packages/start already runs
# The reusable default appends `--coverage`; packages/start-core already runs
# `vitest run --coverage` (with 100% thresholds), and the root script is
# `turbo run test`, which would take the extra flags as its own.
test-command: "pnpm test"
32 changes: 16 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ reasoning behind them. Keep it in sync with the code as the package evolves

## What this is

`@btravstack/start` — the application kernel. It boots a
`@btravstack/start-core` — the application kernel. It boots a
[`@btravstack/di`](https://github.com/btravstack/di) module into a running
process with one runtime, drains in-flight work on SIGTERM, and closes the
application scope on every path. It owns three things — the lifecycle state
Expand Down Expand Up @@ -174,11 +174,11 @@ hook). User-facing changes need a changeset.

## Kernel internals

Two sections live in `packages/start/CLAUDE.md`, which loads only when you work
Two sections live in `packages/start-core/CLAUDE.md`, which loads only when you work
under that directory: **Load-bearing runtime invariants (tests must guard
these)** — each invariant with the test that guards it — and **Internal design
(don't break these)**. Read them before changing anything in
`packages/start/src/`, and update them in the same commit as the code.
`packages/start-core/src/`, and update them in the same commit as the code.

## A known footgun: `start` without `runMain` exits 0 after a crash

Expand Down Expand Up @@ -233,11 +233,11 @@ started elsewhere.

## Public surface

`packages/start/src/index.ts` is the one place the API is decided. `testing.ts`
is a second entry point (`@btravstack/start/testing`), kept out of the main one
`packages/start-core/src/index.ts` is the one place the API is decided. `testing.ts`
is a second entry point (`@btravstack/start-core/testing`), kept out of the main one
so a production bundle never pulls the fakes in.

### `@btravstack/start`
### `@btravstack/start-core`

- **`start(module, options)` → `RunningApp<E>`** — the entry point. Takes a
`Module<X, E, Scope>` (not `Module<X, E, never>`: `Needs` is covariant on
Expand Down Expand Up @@ -330,7 +330,7 @@ runtimes, and no `recoverFailure`-style channel-moving helper. Swapping an
adapter is composing a different module, which di already documents and the type
checker already verifies.

### `@btravstack/start/testing`
### `@btravstack/start-core/testing`

- **`testRuntime(name?)`** — an in-memory `Runtime<never, TestRuntimeInfo>` plus
`started()`, `untilStarted()` (an `AsyncResult<void, never>`), `accepting()`,
Expand Down Expand Up @@ -363,7 +363,7 @@ checker already verifies.
Their public surfaces live in `packages/start-http/CLAUDE.md`,
`packages/start-temporal/CLAUDE.md` and `packages/start-amqp/CLAUDE.md`, which
load only when you work under those directories — the same split
`packages/start/CLAUDE.md` already uses for the kernel's internals. Read the
`packages/start-core/CLAUDE.md` already uses for the kernel's internals. Read the
one you are changing before you change it, and update it in the same commit as
the code.

Expand All @@ -373,7 +373,7 @@ the code.
eleven workspaces run under the same six commands as the kernel — 93 specs
plus five `needs-gate.test-d.ts` files and four `layering.test-d.ts` ones —
so an example that stops compiling, stops linting or stops passing fails CI
exactly as `packages/start` would. Four of the five needs-gate files pin
exactly as `packages/start-core` would. Four of the five needs-gate files pin
**`start`'s** runtime-needs gate (`order-api`, `order-worker`,
`order-temporal`, `order-amqp`); the fifth, `order-application`'s, pins
**di's** `UNSATISFIED DEPENDENCIES` gate on `Module.scoped`. They are
Expand Down Expand Up @@ -467,7 +467,7 @@ namespace }` back off `Serving.info`. The Worker's lifecycle, the unit per
- **Runtime dependencies: none.** `unthrown` and `@btravstack/di` are **peer**
dependencies of `start` — the dual-copy hazard is real for both (di's port
identity and unthrown's `isResult` each compare across copies). `start-http`
peers on both of those plus `@btravstack/start` itself, for the same reason.
peers on both of those plus `@btravstack/start-core` itself, for the same reason.
`node:` builtins only otherwise. Do not add a dependency.
- `declarationMap: false` on all four published packages — the published
tarball has no `src/`, so maps would be dead ends.
Expand Down Expand Up @@ -513,7 +513,7 @@ namespace }` back off `Serving.info`. The Worker's lifecycle, the unit per
a plausible "simplification" (the `teardownErrors` aliasing, the `ready()`
latch, the monotonic `completed`), which is what the surviving comments are.
- Conventional commits (`feat:`, `fix:`, `docs:`, `test:`, `chore:`).
- Coverage thresholds are 100% lines/functions on `packages/start`, with
- Coverage thresholds are 100% lines/functions on `packages/start-core`, with
`testing.ts` excluded (it is a re-export barrel).
- Test mechanics: `@unthrown/vitest`'s matchers are registered via `setupFiles`
(`toBeOk`, `toBeOkWith`, `toBeErrTagged`, …). Timing is asserted through
Expand All @@ -524,8 +524,8 @@ namespace }` back off `Serving.info`. The Worker's lifecycle, the unit per
- Documentation drifts silently, and a sibling repo has already shipped a
falsehood this way. When the public surface changes, update **this** file,
both READMEs **and** `docs-examples.test-d.ts` in the same commit — and when
the change is to `packages/start/src/` internals or the invariants guarding
them, `packages/start/CLAUDE.md` too — and for a runtime package, its own:
the change is to `packages/start-core/src/` internals or the invariants guarding
them, `packages/start-core/CLAUDE.md` too — and for a runtime package, its own:
`packages/start-http/CLAUDE.md`, `packages/start-temporal/CLAUDE.md` or
`packages/start-amqp/CLAUDE.md`, whichever is where that package's public
surface lives. There are **five** `CLAUDE.md` files; naming the wrong one is
Expand All @@ -536,7 +536,7 @@ namespace }` back off `Serving.info`. The Worker's lifecycle, the unit per
Five rules, each with the reason it exists — binding at two different scopes,
which is a decision rather than an accident.

**Rules 4 and 5 are substantive and bind everywhere**, `packages/start`
**Rules 4 and 5 are substantive and bind everywhere**, `packages/start-core`
included. They are what stops an assertion silently declining to run: a
conditional or optional-chained `expect` skips without failing the test, and a
scatter of shallow assertions hides which one is load-bearing. That shape was
Expand Down Expand Up @@ -666,14 +666,14 @@ A sixth rule is about production code that tests keep honest:
for it; the `Module.forkScope` call lands when the first runtime needs a
per-request transaction.
- **A `docs-examples.test-d.ts` for `start-http`, `start-temporal` and
`start-amqp`.** `packages/start`'s exists precisely so its two READMEs
`start-amqp`.** `packages/start-core`'s exists precisely so its two READMEs
cannot drift from `runtime.ts` / `drain-report.ts` without failing `pnpm
typecheck`; the three runtime packages' README samples have no such gate
and are compiled by nothing. Deliberately not built — three packages' worth
of samples still did not justify the harness. Add it the next time one of
those samples is found to have drifted, the same way this gap itself was
found.
- ~~Bringing `packages/start`'s 13 spec files under the Test conventions.~~
- ~~Bringing `packages/start-core`'s 13 spec files under the Test conventions.~~
**Closed by decision, not by doing it.** An audit of the 93 tests found the
substantive rules (4 and 5) already kept — one conditional assertion, since
deleted, and zero optional-chained ones — so the sweep would have been
Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ boot a [`@btravstack/di`](https://github.com/btravstack/di) module into a runnin
process, and stop it again without losing work.**

[![CI](https://github.com/btravstack/start/actions/workflows/ci.yml/badge.svg)](https://github.com/btravstack/start/actions/workflows/ci.yml)
[![npm version](https://img.shields.io/npm/v/%40btravstack%2Fstart.svg?logo=npm)](https://www.npmjs.com/package/@btravstack/start)
[![npm version](https://img.shields.io/npm/v/%40btravstack%2Fstart-core.svg?logo=npm)](https://www.npmjs.com/package/@btravstack/start-core)
[![TypeScript](https://img.shields.io/badge/TypeScript-7.0-blue?logo=typescript)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Expand All @@ -28,7 +28,7 @@ never calls `process.exit`.
## Install

```sh
pnpm add @btravstack/start @btravstack/di unthrown
pnpm add @btravstack/start-core @btravstack/di unthrown
```

`@btravstack/di` and `unthrown` are **peer dependencies** — install all three.
Expand All @@ -38,7 +38,12 @@ The kernel itself has no runtime dependencies beyond `node:` builtins.

```ts
import { Module, Port, Provider } from "@btravstack/di";
import { runMain, start, type Runtime, type Serving } from "@btravstack/start";
import {
runMain,
start,
type Runtime,
type Serving,
} from "@btravstack/start-core";
import { Ok, OkAsync } from "unthrown";

class Greeter extends Port("Greeter")<{
Expand Down Expand Up @@ -99,7 +104,7 @@ await runMain(start(AppModule, { runtime: ticker }));
`start` call, not a boot-time crash.

Every code sample on this page is compiled by
[`packages/start/src/docs-examples.test-d.ts`](./packages/start/src/docs-examples.test-d.ts),
[`packages/start-core/src/docs-examples.test-d.ts`](./packages/start-core/src/docs-examples.test-d.ts),
so a sample that stops compiling fails the build.

## What it is not
Expand Down Expand Up @@ -502,7 +507,7 @@ swallowed: a broken reporter must not take the process down mid-shutdown.

## Testing

`@btravstack/start/testing` ships the deterministic half of the lifecycle.
`@btravstack/start-core/testing` ships the deterministic half of the lifecycle.

```ts
const drainTest = async (): Promise<void> => {
Expand Down Expand Up @@ -576,13 +581,13 @@ lines done well.

## Documentation

See [`packages/start`](./packages/start) for the package README,
See [`packages/start-core`](./packages/start-core) for the package README,
[`examples/`](./examples) for an eleven-package clean-architecture application
booted under four different runtimes, and [`CLAUDE.md`](./CLAUDE.md) for the
authoritative spec: the theses, the public surface and the conventions. The
load-bearing invariants with the test that guards each, and the internal design
notes, live in
[`packages/start/CLAUDE.md`](./packages/start/CLAUDE.md).
[`packages/start-core/CLAUDE.md`](./packages/start-core/CLAUDE.md).

## License

Expand Down
4 changes: 2 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Eleven small packages that are **one application booted four ways**: a clean
architecture split across four layers, deployed once as an oRPC API, once as a
queue worker, once as a Temporal worker and once as an AMQP consumer, with each
transport's contract in a package of its own — and, at the same time,
exercising `@btravstack/start` end to end from a consumer's own workspace,
exercising `@btravstack/start-core` end to end from a consumer's own workspace,
`workspace:*` and all.

| Package | Layer | Shows |
Expand Down Expand Up @@ -65,7 +65,7 @@ booting the lot. The api and temporal contracts sat inside
`order-api/src/contract.ts` and `order-temporal/src/contract.ts` before being
extracted, so no client could take one without the others until then;
`order-amqp-contract` started as its own package from the outset, the same
shape without the detour. None of the three depends on `@btravstack/start`, on
shape without the detour. None of the three depends on `@btravstack/start-core`, on
`@btravstack/di`, or on any other example — the transports depend on **them**.

The rule is enforced by the compiler rather than by review: each contract
Expand Down
2 changes: 1 addition & 1 deletion examples/order-amqp-contract/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `@btravstack/start` example: the order AMQP contract
# `@btravstack/start-core` example: the order AMQP contract

The AMQP contract — one exchange, one queue with a dead-letter exchange and a
retry policy, one message, one publisher, one consumer — in a package of its
Expand Down
2 changes: 1 addition & 1 deletion examples/order-amqp/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `@btravstack/start` example: the order AMQP worker
# `@btravstack/start-core` example: the order AMQP worker

The fourth deployment. The same application, the same persistence, the same
composition — driven by a real message broker instead of an HTTP server, an
Expand Down
2 changes: 1 addition & 1 deletion examples/order-amqp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"dependencies": {
"@amqp-contract/worker": "catalog:",
"@btravstack/di": "catalog:",
"@btravstack/start": "workspace:*",
"@btravstack/start-amqp": "workspace:*",
"@btravstack/start-core": "workspace:*",
"@btravstack/start-example-order-amqp-contract": "workspace:*",
"@btravstack/start-example-order-application": "workspace:*",
"@btravstack/start-example-order-config": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion examples/order-amqp/src/amqp-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { declareHandler, NonRetryableError, RetryableError } from "@amqp-contract/worker";
import type { Runtime } from "@btravstack/start";
import {
amqpRuntime,
messageUnits,
type AmqpInfo,
type MessageUnitContext,
} from "@btravstack/start-amqp";
import type { Runtime } from "@btravstack/start-core";
import type { OrderContract } from "@btravstack/start-example-order-amqp-contract";
import { Logger, PlaceOrder } from "@btravstack/start-example-order-application";
import { ErrAsync, P } from "unthrown";
Expand Down
2 changes: 1 addition & 1 deletion examples/order-amqp/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { runMain, start } from "@btravstack/start";
import { runMain, start } from "@btravstack/start-core";
import { orderContract } from "@btravstack/start-example-order-amqp-contract";
import { P } from "unthrown";

Expand Down
2 changes: 1 addition & 1 deletion examples/order-amqp/src/needs-gate.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* `start(...)` call site.
*/
import { Module } from "@btravstack/di";
import { start } from "@btravstack/start";
import { start } from "@btravstack/start-core";
import { orderContract } from "@btravstack/start-example-order-amqp-contract";
import {
ApplicationModule,
Expand Down
2 changes: 1 addition & 1 deletion examples/order-amqp/src/test-fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { it as amqpIt } from "@amqp-contract/testing";
import type { AmqpTestFixtures } from "@amqp-contract/testing/extension";
import { Module, Port, Provider, type Scope, type ServiceOf } from "@btravstack/di";
import { start, type RunningApp } from "@btravstack/start";
import type { AmqpInfo } from "@btravstack/start-amqp";
import { start, type RunningApp } from "@btravstack/start-core";
import { orderContract } from "@btravstack/start-example-order-amqp-contract";
import {
ApplicationModule,
Expand Down
2 changes: 1 addition & 1 deletion examples/order-api-contract/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `@btravstack/start` example: the order API contract
# `@btravstack/start-core` example: the order API contract

The oRPC contract — the wire shapes and the declared error codes — in a package
of its own, depending on `@orpc/contract` and nothing else.
Expand Down
2 changes: 1 addition & 1 deletion examples/order-api/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `@btravstack/start` example: the order API layer
# `@btravstack/start-core` example: the order API layer

The transport. A router implementing
[`order-api-contract`](../order-api-contract), served over `node:http` under
Expand Down
2 changes: 1 addition & 1 deletion examples/order-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"dependencies": {
"@btravstack/di": "catalog:",
"@btravstack/start": "workspace:*",
"@btravstack/start-core": "workspace:*",
"@btravstack/start-example-order-api-contract": "workspace:*",
"@btravstack/start-example-order-application": "workspace:*",
"@btravstack/start-example-order-config": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion examples/order-api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { runMain, start } from "@btravstack/start";
import { runMain, start } from "@btravstack/start-core";
import { FindOrder, Logger, PlaceOrder } from "@btravstack/start-example-order-application";
import { httpRuntime } from "@btravstack/start-http";
import { P } from "unthrown";
Expand Down
2 changes: 1 addition & 1 deletion examples/order-api/src/needs-gate.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* *classes* while di parameterises `Context` by port *instances*.
*/
import { Module } from "@btravstack/di";
import { start } from "@btravstack/start";
import { start } from "@btravstack/start-core";
import {
ApplicationModule,
FindOrder,
Expand Down
2 changes: 1 addition & 1 deletion examples/order-api/src/test-fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";

import { Module, Port, Provider, type Scope, type ServiceOf } from "@btravstack/di";
import { start, type RunningApp } from "@btravstack/start";
import { start, type RunningApp } from "@btravstack/start-core";
import {
ApplicationModule,
FindOrder,
Expand Down
6 changes: 3 additions & 3 deletions examples/order-application/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `@btravstack/start` example: the order application layer
# `@btravstack/start-core` example: the order application layer

The use cases, and the ports they need to run. This layer turns the domain's
rules into operations — "place an order", "find an order" — and declares, as
Expand Down Expand Up @@ -61,10 +61,10 @@ the log line — with no Prisma, no HTTP and no kernel booted.

## The single kernel touchpoint

`src/logger.ts` imports exactly one thing from `@btravstack/start`:
`src/logger.ts` imports exactly one thing from `@btravstack/start-core`:

```ts
import { currentUnit } from "@btravstack/start";
import { currentUnit } from "@btravstack/start-core";
```

One `Logger` is constructed per scope, but the kernel opens a _unit_ per request
Expand Down
Loading
Loading