@demlik/tea 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +30 -7
  2. package/dist/agent/index.d.ts +49 -36
  3. package/dist/agent/index.js +12 -8
  4. package/dist/chunk-4LPY67E4.js +18 -0
  5. package/dist/chunk-4LPY67E4.js.map +1 -0
  6. package/dist/chunk-5XU6ZJO2.js +533 -0
  7. package/dist/chunk-5XU6ZJO2.js.map +1 -0
  8. package/dist/{chunk-AQ6ZIMEA.js → chunk-6NG4V5SB.js} +89 -115
  9. package/dist/chunk-6NG4V5SB.js.map +1 -0
  10. package/dist/{chunk-HYDWG35Q.js → chunk-7MGXQZPW.js} +11 -9
  11. package/dist/chunk-7MGXQZPW.js.map +1 -0
  12. package/dist/{chunk-BLJ2JQSH.js → chunk-BCTEOVOU.js} +3 -3
  13. package/dist/{chunk-BLJ2JQSH.js.map → chunk-BCTEOVOU.js.map} +1 -1
  14. package/dist/chunk-CH5KTOEZ.js +710 -0
  15. package/dist/chunk-CH5KTOEZ.js.map +1 -0
  16. package/dist/chunk-CIHSHLQX.js +64 -0
  17. package/dist/chunk-CIHSHLQX.js.map +1 -0
  18. package/dist/{chunk-Z2L3LSNV.js → chunk-EHUDXAKC.js} +246 -210
  19. package/dist/chunk-EHUDXAKC.js.map +1 -0
  20. package/dist/chunk-GOX2CYIM.js +380 -0
  21. package/dist/chunk-GOX2CYIM.js.map +1 -0
  22. package/dist/chunk-IVJP3GKJ.js +104 -0
  23. package/dist/chunk-IVJP3GKJ.js.map +1 -0
  24. package/dist/{chunk-F7FMAWLA.js → chunk-MVZS5FDM.js} +13 -6
  25. package/dist/chunk-MVZS5FDM.js.map +1 -0
  26. package/dist/{chunk-LYLTKJWM.js → chunk-MYNLY3HP.js} +9 -8
  27. package/dist/chunk-MYNLY3HP.js.map +1 -0
  28. package/dist/{chunk-I5MQMJDW.js → chunk-NSIAORRY.js} +20 -30
  29. package/dist/chunk-NSIAORRY.js.map +1 -0
  30. package/dist/{chunk-MYXFNWAJ.js → chunk-QNGYQMZB.js} +4 -4
  31. package/dist/chunk-QNGYQMZB.js.map +1 -0
  32. package/dist/chunk-WT5PBPLB.js +98 -0
  33. package/dist/chunk-WT5PBPLB.js.map +1 -0
  34. package/dist/{core-CTzMJ82M.d.ts → core-BbgtiXdg.d.ts} +231 -139
  35. package/dist/do/index.d.ts +37 -32
  36. package/dist/do/index.js +21 -14
  37. package/dist/do/index.js.map +1 -1
  38. package/dist/{durable-effects-B0-qGQDy.d.ts → durable-effects-7MTK_jnj.d.ts} +1 -1
  39. package/dist/effect/index.d.ts +151 -0
  40. package/dist/effect/index.js +98 -0
  41. package/dist/effect/index.js.map +1 -0
  42. package/dist/extension/index.d.ts +6 -7
  43. package/dist/extension/index.js +3 -2
  44. package/dist/extension/index.js.map +1 -1
  45. package/dist/flow/index.d.ts +68 -70
  46. package/dist/flow/index.js +32 -27
  47. package/dist/flow/index.js.map +1 -1
  48. package/dist/idempotency/index.d.ts +6 -7
  49. package/dist/idempotency/index.js +4 -2
  50. package/dist/{index-DJWIER_3.d.ts → index-7vV8xmsD.d.ts} +1 -1
  51. package/dist/{index-NjrT3fL2.d.ts → index-Bz5Rh9Ga.d.ts} +19 -96
  52. package/dist/index-CjFDUBeq.d.ts +207 -0
  53. package/dist/{index-6A7dB8f6.d.ts → index-CkFYA9Y6.d.ts} +14 -16
  54. package/dist/{index-B5BZ-_cT.d.ts → index-Cou86-GX.d.ts} +189 -108
  55. package/dist/{index-TvRb1KVf.d.ts → index-DU6JpQ8G.d.ts} +1 -1
  56. package/dist/{index-q0hXFeZ0.d.ts → index-Xufl55W8.d.ts} +286 -215
  57. package/dist/{index-4vZzljCD.d.ts → index-_dOR3xqA.d.ts} +57 -45
  58. package/dist/index.d.ts +181 -385
  59. package/dist/index.js +2 -1
  60. package/dist/jev/index.d.ts +147 -99
  61. package/dist/jev/index.js +329 -233
  62. package/dist/jev/index.js.map +1 -1
  63. package/dist/{machine-CdIcN2Xm.d.ts → machine-BQzTV8On.d.ts} +171 -280
  64. package/dist/machine-viz/index.d.ts +5 -5
  65. package/dist/machine-viz/index.js +8 -4
  66. package/dist/machine-viz/index.js.map +1 -1
  67. package/dist/mem/index.d.ts +3 -4
  68. package/dist/mem/index.js +2 -1
  69. package/dist/mem/index.js.map +1 -1
  70. package/dist/node/index.d.ts +119 -78
  71. package/dist/node/index.js +52 -38
  72. package/dist/node/index.js.map +1 -1
  73. package/dist/paginate/index.d.ts +27 -33
  74. package/dist/paginate/index.js +6 -4
  75. package/dist/parity/index.d.ts +4 -5
  76. package/dist/parity/index.js +3 -2
  77. package/dist/parity/index.js.map +1 -1
  78. package/dist/pbt/index.d.ts +2 -2
  79. package/dist/pbt/index.js +2 -1
  80. package/dist/pbt/index.js.map +1 -1
  81. package/dist/persistence/index.d.ts +6 -7
  82. package/dist/persistence/index.js +5 -3
  83. package/dist/promise/index.d.ts +186 -0
  84. package/dist/promise/index.js +5 -0
  85. package/dist/promise/index.js.map +1 -0
  86. package/dist/protocol-ClRZBhHQ.d.ts +53 -0
  87. package/dist/react/index.d.ts +36 -17
  88. package/dist/react/index.js +27 -4
  89. package/dist/react/index.js.map +1 -1
  90. package/dist/resilience/index.d.ts +63 -455
  91. package/dist/resilience/index.js +37 -483
  92. package/dist/resilience/index.js.map +1 -1
  93. package/dist/{runtime-types-LWDoi943.d.ts → runtime-types-CiVpC0HN.d.ts} +131 -388
  94. package/dist/testing/index.d.ts +165 -15
  95. package/dist/testing/index.js +104 -3
  96. package/dist/testing/index.js.map +1 -1
  97. package/dist/timing/index.d.ts +25 -22
  98. package/dist/timing/index.js +4 -3
  99. package/dist/timing/index.js.map +1 -1
  100. package/dist/{types-DP3f5NIl.d.ts → types-LxZxGYda.d.ts} +2 -2
  101. package/dist/work-queue/index.d.ts +5 -6
  102. package/package.json +18 -5
  103. package/dist/chunk-AQ6ZIMEA.js.map +0 -1
  104. package/dist/chunk-F2US5BW6.js +0 -72
  105. package/dist/chunk-F2US5BW6.js.map +0 -1
  106. package/dist/chunk-F7FMAWLA.js.map +0 -1
  107. package/dist/chunk-HUKZQX6U.js +0 -97
  108. package/dist/chunk-HUKZQX6U.js.map +0 -1
  109. package/dist/chunk-HYDWG35Q.js.map +0 -1
  110. package/dist/chunk-I5MQMJDW.js.map +0 -1
  111. package/dist/chunk-LYLTKJWM.js.map +0 -1
  112. package/dist/chunk-MYXFNWAJ.js.map +0 -1
  113. package/dist/chunk-RQULHGYH.js +0 -1804
  114. package/dist/chunk-RQULHGYH.js.map +0 -1
  115. package/dist/chunk-Z2L3LSNV.js.map +0 -1
  116. package/dist/index-DMCeMPMJ.d.ts +0 -272
package/dist/index.d.ts CHANGED
@@ -1,10 +1,73 @@
1
- import { B as BootingRuntime, a as ScopedCtxArg, S as Store, O as OnError, b as Supervision, R as Runtime } from './runtime-types-LWDoi943.js';
2
- export { c as Branded, C as CtxArg, D as DepToken, d as DepsOf, e as DispatchDiscardedError, f as DispatchSettle, g as DisposeTimeoutNotice, h as DriveFailedError, i as DriveStalledError, j as FencedRead, F as FencedStore, k as FoldRefusal, I as IdentityDropNotice, M as MachineTypes, l as OnReleaseError, P as PortNameCollisionError, m as ProvideFailedError, n as Provided, o as ProvidedCtx, p as Provider, q as ProviderCycleError, Q as QuiescenceTimeoutError, r as RuntimeDiscardNotice, s as RuntimeDiscardedError, t as RuntimeErrorContext, u as RuntimeErrorPhase, v as RuntimeRef, w as Schema, x as Scope, y as StoreConflictError, z as SubIdCollisionError, A as SupervisionStrategy, U as UnknownProviderError, _ as __resetPortRegistry, E as absurd, G as asReducer, H as defineMachine, J as definePort, K as dep, L as isFencedStore, N as isProvided, T as layer, V as noop, W as provide, X as replay, Y as schemaMigrate, Z as settle, $ as tryApplyCell, a0 as tryFoldMsgs, a1 as tryInterpret, a2 as value, a3 as wrapDetached } from './runtime-types-LWDoi943.js';
3
- import { C as Cmd, S as Sub, M as Machine, R as RequiredCtx, P as Port, D as Dispose, o as DepKeyedSub, c as SubId } from './core-CTzMJ82M.js';
4
- export { A as AnyCmdDef, b as CmdDef, p as CmdInput, a as CmdOf, i as CmdValue, E as ErrOf, q as ErrorsOf, r as ExhaustiveTransitions, s as Identity, I as Interpret, h as InterpretDetached, t as MachineShape, l as MalformedResult, N as NoCellError, k as NoCtx, O as OkOf, g as PortEmitter, d as Reducer, m as Requirements, f as RequirementsOf, e as Settled, u as SettledErr, v as SettledOk, w as Subscribe, x as SyncReturn, j as Tagged, n as TaggedError, T as Transitions, U as UpdateForm, y as acceptedTypes, z as acceptsOf, B as applyCell, F as applyCellChecked, G as describeMachine, H as detectUpdateForm, J as foldMsgs, K as foldUpdates, L as formOf, Q as msgKeysOf, V as structuralHash, W as subId } from './core-CTzMJ82M.js';
5
- import { S as SubscribeHandler } from './types-DP3f5NIl.js';
6
- import 'better-result';
7
- import 'zod';
1
+ import { B as BootingRuntime, R as Runtime } from './runtime-types-CiVpC0HN.js';
2
+ export { b as BootedRunHandle, d as Branded, C as CtxArg, D as DispatchDiscardedError, e as DispatchSettle, f as DisposeTimeoutNotice, g as DriveFailedError, h as DriveStalledError, E as EngineRun, i as FencedRead, F as FencedStore, j as FoldRefusal, I as IdentityDropNotice, M as MachineTypes, O as OnError, P as PortNameCollisionError, Q as QuiescenceTimeoutError, c as RunHandle, k as RunOptions, l as RuntimeDiscardNotice, m as RuntimeDiscardedError, n as RuntimeErrorContext, o as RuntimeErrorPhase, p as RuntimeRef, q as Schema, S as Store, r as StoreConflictError, a as Supervision, s as SupervisionStrategy, t as TelemetryEvent, T as TelemetrySink, _ as __resetPortRegistry, u as absurd, v as asReducer, w as defineMachine, x as definePort, y as isFencedStore, z as noop, A as replay, G as schemaMigrate, H as tryApplyCell, J as tryFoldMsgs, K as tryInterpret, L as wrapDetached } from './runtime-types-CiVpC0HN.js';
3
+ import { C as Cmd, S as Sub, M as Machine, P as Port, o as DepKeyedSub, h as Dispose } from './core-BbgtiXdg.js';
4
+ export { A as AnyCmdDef, w as AsyncSchemaError, a as BuiltinSub, B as BuiltinSubType, b as CmdDef, x as CmdInput, c as CmdOf, v as CmdValue, D as DeclaredErrorsOf, u as ErrOf, E as ErrorsOf, y as ExhaustiveTransitions, z as Identity, p as Interpret, F as InterpretArg, G as InterpretCell, I as InterpretDetached, H as MachineShape, r as MalformedResult, N as NoCellError, J as NoCtx, t as OkOf, O as OkOfCmd, l as Outcome, K as OutcomeContractError, L as OutcomeHelpers, m as PortEmitter, i as Reducer, R as RunHandlers, j as Settled, f as SettledErr, e as SettledOk, n as SubId, d as Subscribe, Q as SubscribeArg, V as SyncReturn, s as Tagged, g as TaggedError, T as TimerDeps, q as TimerSub, k as Transitions, X as UndeclaredFailureError, U as UpdateForm, W as Wired, Y as acceptedTypes, Z as acceptsOf, _ as applyCell, $ as applyCellChecked, a0 as describeMachine, a1 as detectUpdateForm, a2 as foldMsgs, a3 as foldUpdates, a4 as formOf, a5 as msgKeysOf, a6 as structuralHash, a7 as subId, a8 as subIdOf } from './core-BbgtiXdg.js';
5
+ import { S as SubscribeHandler } from './types-LxZxGYda.js';
6
+ import '@standard-schema/spec';
7
+
8
+ /**
9
+ * @packageDocumentation
10
+ * The composition seam — the layer between a battery and a door.
11
+ *
12
+ * A battery is pure state plus verbs over that state. A door holds several
13
+ * batteries side by side in one Model. Everything between those two facts was,
14
+ * until now, hand-written at every consumer: the one-key record rebuild that
15
+ * threads a verb's `[slice, cmds]` result back into the host state, and the
16
+ * statement order that decides which slice wins a derived read.
17
+ *
18
+ * Two helpers cover it, and neither is an optics kit. {@link liftSlice} is the
19
+ * record rebuild with the key checked by the type system; {@link readInOrder}
20
+ * turns a read's precedence from statement order into a value you can name,
21
+ * export and assert on. ADR 0001 (no off-the-shelf resilience) and ADR 0014
22
+ * (Effect's types, never its runtime values) between them settle why this is
23
+ * forty lines here rather than a dependency.
24
+ *
25
+ * `docs/how-to/compose-two-battery-slices.md` wires both end to end over a
26
+ * two-battery door, and its `ts` blocks are compiled and run as
27
+ * `src/docs/how-to/compose-slices.test.ts`.
28
+ *
29
+ * Both are pure: no clock, no RNG, no storage, nothing captured. A `liftSlice`
30
+ * result is a fresh record and a Cmd array, so ADR 0014's journal/hash promise
31
+ * over a reducer's return value is untouched.
32
+ */
33
+ /**
34
+ * Lift a battery verb's result into the host state that carries its slice.
35
+ *
36
+ * `liftSlice("resilience", state, knob.attempt(state.resilience, …))` is the
37
+ * `{ ...state, resilience: slice }` every consumer writes by hand, with the key
38
+ * checked: `key` must be a key of `S`, and the slice the verb returned must be
39
+ * assignable to the type that key carries — so naming the wrong field, or
40
+ * threading one battery's slice into another's key, is a compile error rather
41
+ * than a state shape nothing reads correctly.
42
+ *
43
+ * PURE — a fresh record and the verb's own Cmds, unchanged.
44
+ */
45
+ declare function liftSlice<S, K extends keyof S, C>(key: K, state: S, [slice, cmds]: readonly [S[K], readonly C[]]): readonly [S, readonly C[]];
46
+ /**
47
+ * One step of a composed read: the slice it consults, under the name that slice
48
+ * goes by. `read` returns `undefined` for "this slice has nothing to say",
49
+ * which is what hands the read on to the next step.
50
+ */
51
+ interface ReadStep<In, Out> {
52
+ /** The slice this step consults — `"cache"`, `"failed"`, `"inFlight"`. */
53
+ readonly name: string;
54
+ /** What this slice says about `input`, or `undefined` to defer. */
55
+ readonly read: (input: In) => Out | undefined;
56
+ }
57
+ /**
58
+ * Run a composed read through `order` and return the first answer any step
59
+ * gives, or `absent` when every one defers.
60
+ *
61
+ * The point is that `order` is a VALUE. A derived read over three battery
62
+ * slices otherwise states its precedence as the order of three `if` statements,
63
+ * where it is invisible to the type system, unnameable in a test and one
64
+ * innocent-looking reshuffle away from being wrong — which is exactly the
65
+ * defect that escaped `classify-batch`'s per-battery test suites. Written as
66
+ * data, the precedence can be exported, printed and asserted on directly.
67
+ *
68
+ * PURE — it evaluates `read` in order and stores nothing.
69
+ */
70
+ declare function readInOrder<In, Out>(input: In, order: readonly ReadStep<In, Out>[], absent: Out): Out;
8
71
 
9
72
  interface HistoryTracker<S, M extends {
10
73
  type: string;
@@ -181,177 +244,6 @@ declare function reconcile<S, M extends {
181
244
  type: string;
182
245
  }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, authoritativeState: S, lastAppliedSeq: Ack | Seq, pending: readonly SeqTagged<M>[]): S;
183
246
 
184
- /**
185
- * @demlik/tea runtime — `run`: boot + the serial dispatch loop. The pure helpers
186
- * that compose against a machine without running it (`replay`, `tryInterpret`)
187
- * live in `./runtime-types`.
188
- */
189
-
190
- declare function run<S, M extends {
191
- type: string;
192
- }, C extends Cmd, U extends Sub, Ctx, E extends {
193
- type: string;
194
- } = never>(machine: Machine<S, M, C, U, Ctx>, opts: ScopedCtxArg<Ctx & RequiredCtx<C>> & {
195
- store?: Store<S>;
196
- onError?: OnError;
197
- /**
198
- * The clock that stamps `at` on a `Cmd.define`d effect's settled Msg at
199
- * the interpret edge. Defaults to `Date.now`. Inject a fixed one for a
200
- * deterministic run; a `replay` log carries its own `at`s and never reads
201
- * this.
202
- */
203
- clock?: () => number;
204
- /**
205
- * The SEMANTIC event projector. Maps one APPLIED transition `(msg, state)`
206
- * to zero-or-more public events of `E`; `[]` skips the transition. Maps the
207
- * machine's PRIVATE Msg vocabulary to NAMED events — the private names never
208
- * reach `on`'s `E` surface. Omit → `E = never` and `on` is uncallable. PURE.
209
- */
210
- events?: (msg: M, state: S) => readonly E[];
211
- /**
212
- * Declared policy for a reducer (`update`) throw. Always surfaced via
213
- * `onError` (`phase: "reduce"`); the strategy decides what the runtime does
214
- * next. Defaults to `"stop"`. See `Supervision`.
215
- */
216
- supervision?: Supervision<S, M>;
217
- /**
218
- * The run-terminality predicate — makes the run's outcome first-class. Fed to
219
- * `Runtime.result()` and `Runtime.done()`. PURE. Omit → never terminal.
220
- */
221
- terminal?: (state: S) => boolean;
222
- /**
223
- * How long `stop()` waits for teardown work that returned a Promise (an
224
- * async `release` in `defineManagedResource`, an async Sub cleanup) before
225
- * giving up on it. Defaults to 5_000ms.
226
- *
227
- * `stop()` awaits those disposals so a host doing
228
- * `await runtime.stop(); env.evict()` cannot drop the isolate mid-release —
229
- * the leak the managed-resource battery exists to prevent, relocated to
230
- * shutdown. The bound is what keeps a release that never settles from
231
- * hanging the host: on expiry `stop()` reports a `DisposeTimeoutNotice`
232
- * (warn-only, like every `RuntimeDiscardNotice`) and resolves anyway,
233
- * because `stop()` resolving is a contract.
234
- */
235
- disposeTimeoutMs?: number;
236
- /**
237
- * Iteration cap for `idle()`'s quiescence wait. Defaults to 100_000. Test
238
- * seam only. Production code must not set it.
239
- *
240
- * @internal test-only
241
- */
242
- __idleCap?: number;
243
- }): BootingRuntime<S, M, E>;
244
- /**
245
- * The outside-in stop, as a PAIR. A signal is only cancellation if something can
246
- * turn "aborted" into a transition this machine understands, and the kernel has
247
- * no built-in cancel Msg any more than it has a built-in terminal set — so the
248
- * `signal` and the `cancel` that reads it are one option, present together or
249
- * absent together. A discriminated union rather than two optional fields, on the
250
- * same reasoning as `AgentCompactionConfig`: a signal with no `cancel` is a stop
251
- * button wired to nothing, and that is a state worth making unrepresentable
252
- * rather than checking for.
253
- */
254
- type DriveCancellation<S, M> = {
255
- readonly signal?: undefined;
256
- readonly cancel?: undefined;
257
- } | {
258
- /**
259
- * Abort the run through this signal. On abort the drive dispatches
260
- * `cancel`'s Msg and resolves on the State that lands — it does NOT reject,
261
- * and a cancellation is never a `DriveFailedError`. Already aborted when
262
- * the drive is called → the cancel Msg goes in place of `start`, so the
263
- * run ends without the start's effects ever firing.
264
- */
265
- readonly signal: AbortSignal;
266
- /**
267
- * The Msg that settles this machine as cancelled, chosen off the current
268
- * State exactly as `start` is. PURE. It must land a State `isTerminal`
269
- * holds for — the drive resolves on the terminal predicate, not on the
270
- * abort — and that State should be DURABLE, or a reload resumes the run
271
- * its caller stopped.
272
- */
273
- readonly cancel: (state: S) => M;
274
- };
275
- /** Options for `driveToDone`. */
276
- type DriveToDoneOptions<S, M extends {
277
- type: string;
278
- } = never> = DriveCancellation<S, M> & {
279
- /**
280
- * Marks a terminal State as a FAILURE. A State this holds for ends the drive
281
- * like any terminal one — the runtime is stopped — but the drive REJECTS with
282
- * `DriveFailedError` carrying it instead of resolving. A failed State is
283
- * terminal by definition; it need not also satisfy `isTerminal`. Omit → the
284
- * drive never rejects on State, only on a runtime error.
285
- */
286
- readonly failed?: (state: S) => boolean;
287
- };
288
- /**
289
- * Drive a machine from `start` to its terminal State in one call, then tear the
290
- * runtime down. The one-shot shape every "run this machine to done" test and
291
- * caller boundary otherwise hand-wires as six steps — `await ready`, `observe`,
292
- * park a promise, `dispatch(start)`, `getState()`, `stop()` — with the observer
293
- * leak and the forgotten `stop` those six steps invite.
294
- *
295
- * Takes the handle `run()` returns (a `Runtime` extends it, so a booted one is
296
- * accepted too), awaits `ready`, dispatches `start`, and resolves with the first
297
- * State for which `isTerminal` holds. A machine that boots already terminal (a
298
- * rehydrated finished run) resolves on its boot State and `start` is never
299
- * dispatched — a finished run has nothing to set in motion. `start` may be a
300
- * function of the boot State instead of a Msg: a machine rehydrated MID-run
301
- * needs its resume Msg, not its start Msg, and only the boot State says which
302
- * (the agent's `agent_boot` vs `agent_start` is the case in point). The
303
- * observer is detached and `stop()` awaited on EVERY exit: resolve, `failed`,
304
- * a stall, a boot or dispatch throw, the quiescence cap.
305
- *
306
- * Rejections are typed, never silent:
307
- * - `DriveFailedError<S>` when `opts.failed` marks the final State — the State
308
- * rides on `error.state`.
309
- * - `DriveStalledError<S>` when `start`'s follow-up chain quiesces on a State
310
- * that is neither terminal nor `failed` and the runtime has no live Sub and
311
- * no Cmd in flight — nothing inside it can deliver another transition, so
312
- * waiting would be a hang with the runtime leaked. The stalled State rides
313
- * on `error.state`. A machine that CAN still move is not stalled: a live Sub
314
- * (manual or dep-keyed) that delivers the terminal Msg after the dispatch
315
- * quiesces keeps the drive waiting, and it resolves on that State. What the
316
- * runtime cannot see — a dispatch from outside it after quiescence — does
317
- * not count as live; a machine that depends on one declares it as a Sub.
318
- * - `QuiescenceTimeoutError` when `start`'s follow-up chain never settles —
319
- * the SAME cap `dispatch` and `idle()` already reject on (invariant 6), not a
320
- * second clock. A livelocking machine surfaces as its own failure class.
321
- * - the boot error, or whatever `dispatch(start)` rejects with, otherwise.
322
- *
323
- * `isTerminal` is caller-supplied, exactly as `run()`'s `terminal` option is —
324
- * the kernel has no built-in terminal-set concept and this does not add one.
325
- *
326
- * Cancellation is the same story: `{ signal, cancel }` stops the drive from
327
- * outside, and it settles the run rather than escaping it. On abort the drive
328
- * dispatches `cancel`'s Msg and RESOLVES on the terminal State that transition
329
- * lands — no rejection, no `DriveFailedError`, no `AbortError`. That is the whole
330
- * point of routing a stop through the Model: the outcome is durable, so a killed
331
- * process resumes reading a run that ended instead of restarting one someone
332
- * stopped. A cancel that THROWS is the one abort that rejects: it lands
333
- * no State, so its error is what the drive has to report — and that holds
334
- * whether the `cancel` function throws synchronously or the fold it produced
335
- * throws later, since both leave the run with no State to settle on. A State the
336
- * caller's `failed` predicate marks rejects on an abort exit exactly as it does
337
- * anywhere else. In-flight effects are not recalled — a promise cannot be cancelled —
338
- * so they settle to their own end and `stop()` drains them as it always did;
339
- * keeping their results off the public channels is the machine's own business.
340
- * Omit the pair and every path here behaves exactly as it did before.
341
- *
342
- * @param handle the handle `run(machine, opts)` returned.
343
- * @param start the Msg that sets the run in motion, or a function choosing
344
- * it off the boot State. PURE.
345
- * @param isTerminal the terminal predicate over the machine's State. PURE.
346
- * @param opts an optional `failed` predicate and an optional
347
- * `{ signal, cancel }` pair (see {@link DriveToDoneOptions}).
348
- */
349
- declare function driveToDone<S, M extends {
350
- type: string;
351
- }, E extends {
352
- type: string;
353
- } = never>(handle: BootingRuntime<S, M, E>, start: M | ((booted: S) => M), isTerminal: (state: S) => boolean, opts?: DriveToDoneOptions<S, M>): Promise<S>;
354
-
355
247
  /**
356
248
  * The imperative listener target, expressed as the `add`/`remove` pair the
357
249
  * substrate pairs into a reconciled resource. Both halves receive the SAME
@@ -385,15 +277,16 @@ interface ListenerTarget<Args extends readonly unknown[], S extends Sub, Ctx> {
385
277
  * `Args`, `S`, `Ctx` are fixed when the target is defined — they describe the
386
278
  * one listener topology this factory owns (the platform arg tuple, which Sub
387
279
  * shape it filters on, what context resolves it). `M` is supplied at the
388
- * subscribe cell because the same listener feeds different Msgs in different
280
+ * runner because the same listener feeds different Msgs in different
389
281
  * machines. Pinning `S` here (rather than at the `msgFn` step) lets
390
- * `add`/`remove` read the concrete Sub's fields — `sub.alarmName`,
391
- * `sub.channel` — with NO cast at the substrate edge: the narrowing happens in
392
- * author-land where the Sub shape is in scope.
282
+ * `add`/`remove` read the concrete Sub's deps — `sub.deps.alarmName`,
283
+ * `sub.deps.channel` — with NO cast at the substrate edge: the narrowing
284
+ * happens in author-land where the Sub shape is in scope.
393
285
  *
394
286
  * The returned function is a `SubscribeHandler` factory in the exact shape of
395
287
  * every other Sub factory in `src/subs/`: supply the `msgFn` projection, get a
396
- * `(sub, ctx, dispatch) => cleanup` handler assignable to `machine.subscribe`.
288
+ * `(sub, ctx, dispatch) => cleanup` runner for the `subscribe` table `run`
289
+ * takes.
397
290
  * The cleanup the author NEVER writes is `() => remove(listener, sub, ctx)`,
398
291
  * with `listener` the identical reference passed to `add`.
399
292
  */
@@ -422,26 +315,26 @@ interface MinimalWebSocket {
422
315
  }
423
316
 
424
317
  type BroadcastSubData = {
425
- channelName: string;
318
+ readonly channelName: string;
426
319
  };
427
- declare function fromBroadcastChannel<S extends Sub & BroadcastSubData, M>(msgFn: (event: MinimalMessageEvent, sub: S) => M | null): SubscribeHandler<S, M, unknown>;
320
+ declare function fromBroadcastChannel<S extends Sub<string, BroadcastSubData>, M>(msgFn: (event: MinimalMessageEvent, sub: S) => M | null): SubscribeHandler<S, M, unknown>;
428
321
 
429
322
  type EventSourceSubData = {
430
- url: string;
323
+ readonly url: string;
431
324
  };
432
325
  interface EventSourceFactoryOpts<S, M> {
433
326
  onMessage: (data: string, sub: S) => M | null;
434
327
  onError?: (event: MinimalEvent, sub: S) => M | null;
435
328
  onOpen?: (sub: S) => M | null;
436
329
  }
437
- declare function fromEventSource<S extends Sub & EventSourceSubData, M>(opts: EventSourceFactoryOpts<S, M>): SubscribeHandler<S, M, unknown>;
330
+ declare function fromEventSource<S extends Sub<string, EventSourceSubData>, M>(opts: EventSourceFactoryOpts<S, M>): SubscribeHandler<S, M, unknown>;
438
331
 
439
332
  declare function fromEventTarget<S extends Sub, M>(getTarget: () => MinimalEventTarget, eventName: string, msgFn: (event: MinimalEvent, sub: S) => M | null): SubscribeHandler<S, M, unknown>;
440
333
 
441
334
  type IntervalSubData = {
442
- intervalMs: number;
335
+ readonly intervalMs: number;
443
336
  };
444
- declare function fromInterval<S extends Sub & IntervalSubData, M>(msgFn: (sub: S) => M): SubscribeHandler<S, M, unknown>;
337
+ declare function fromInterval<S extends Sub<string, IntervalSubData>, M>(msgFn: (sub: S) => M): SubscribeHandler<S, M, unknown>;
445
338
 
446
339
  declare function fromPort<S extends Sub, M, Ctx, T, RS = unknown, RM extends {
447
340
  type: string;
@@ -450,7 +343,7 @@ declare function fromPort<S extends Sub, M, Ctx, T, RS = unknown, RM extends {
450
343
  }>(selectRuntime: (ctx: Ctx) => Runtime<RS, RM>, port: Port<T>, msgFn: (value: T, sub: S) => M | null): SubscribeHandler<S, M, Ctx>;
451
344
 
452
345
  type WebSocketSubData = {
453
- wsUrl: string;
346
+ readonly wsUrl: string;
454
347
  };
455
348
  interface WebSocketFactoryOpts<S, M> {
456
349
  onMessage: (data: unknown, sub: S) => M | null;
@@ -458,7 +351,7 @@ interface WebSocketFactoryOpts<S, M> {
458
351
  onError?: (event: MinimalEvent, sub: S) => M | null;
459
352
  onClose?: (code: number, reason: string, sub: S) => M | null;
460
353
  }
461
- declare function fromWebSocket<S extends Sub & WebSocketSubData, M>(opts: WebSocketFactoryOpts<S, M>): SubscribeHandler<S, M, unknown>;
354
+ declare function fromWebSocket<S extends Sub<string, WebSocketSubData>, M>(opts: WebSocketFactoryOpts<S, M>): SubscribeHandler<S, M, unknown>;
462
355
 
463
356
  /** Cancels a scheduled reconnect timer (the inverse of `schedule`). */
464
357
  type CancelTimer = () => void;
@@ -482,97 +375,37 @@ interface ReconnectingWebSocketFactoryOpts<S, M> {
482
375
  /** Inject the timer (defaults to `setTimeout`/`clearTimeout`); the test seam. */
483
376
  schedule?: (fn: () => void, ms: number) => CancelTimer;
484
377
  }
485
- declare function fromReconnectingWebSocket<S extends Sub & WebSocketSubData, M>(opts: ReconnectingWebSocketFactoryOpts<S, M>): SubscribeHandler<S, M, unknown>;
378
+ declare function fromReconnectingWebSocket<S extends Sub<string, WebSocketSubData>, M>(opts: ReconnectingWebSocketFactoryOpts<S, M>): SubscribeHandler<S, M, unknown>;
486
379
 
487
380
  type TimeoutSubData = {
488
- delayMs: number;
381
+ readonly delayMs: number;
489
382
  };
490
- declare function fromTimeout<S extends Sub & TimeoutSubData, M>(msgFn: (sub: S) => M): SubscribeHandler<S, M, unknown>;
383
+ declare function fromTimeout<S extends Sub<string, TimeoutSubData>, M>(msgFn: (sub: S) => M): SubscribeHandler<S, M, unknown>;
491
384
 
492
385
  /**
493
- * The Sub the battery builds. The consumer's `Sub` union includes
494
- * `ManagedResourceSub<TKey>` directly — the substrate reconciles by `id` and
495
- * `type` like any other Sub.
496
- */
497
- interface ManagedResourceSub<TKey> extends Sub<"managed_resource"> {
498
- /** The lifetime key. Stable while the resource should be held; a change
499
- * retires the old resource (release) and acquires a fresh one. */
500
- readonly key: TKey;
501
- }
502
- /**
503
- * A battery paired with a state-gate. Self-contained: it knows its `name`
504
- * (for the unique-name assert), how to derive its live Sub from a state
505
- * (`subFor`), how to handle its own `subscribe`, and whether it `owns` a given
506
- * Sub id.
507
- *
508
- * Generic in the consumer state `S`, the shared lifetime key `TKey`, and the
509
- * shared `Ctx`. `Handle` is the ONLY thing erased — each battery's handle type
510
- * differs (a checkpoint saver vs. a tool-call bridge) but never crosses this
511
- * interface, so a `combineManagedResources` array holds entries of
512
- * heterogeneous handle types with no cast. The sub type stays
513
- * `ManagedResourceSub<TKey>` end to end, so the substrate's narrowed Sub flows
514
- * straight in.
515
- */
516
- interface GatedManagedResource<S, TKey, Ctx> {
517
- /** The owning battery's `name`. The combinator asserts these are unique. */
518
- readonly name: string;
519
- /**
520
- * Derive this resource's live Sub for a given state: the battery's
521
- * `.sub(key)` when `when(state)` returns a key, or `null` when it returns
522
- * `null` (the resource should be torn down in that state).
523
- */
524
- readonly subFor: (state: S) => ManagedResourceSub<TKey> | null;
525
- /**
526
- * This battery's own `.subscribe`. The combinator routes an incoming
527
- * `managed_resource` Sub to the entry whose `owns(sub)` is true, then calls
528
- * this.
529
- */
530
- readonly subscribe: (sub: ManagedResourceSub<TKey>, ctx: Ctx, dispatch: (msg: never) => void) => Dispose;
531
- /**
532
- * Does this entry own the given Sub? `sub.id === subIdFor(sub.key)` encodes
533
- * the battery `name`, so a match is unambiguous.
534
- */
535
- readonly owns: (sub: ManagedResourceSub<TKey>) => boolean;
536
- }
537
- /**
538
- * What `combineManagedResources` returns: the single managed-resource
539
- * `subscribe` handler for the machine's `subscribe` record, and a `subs(state)`
540
- * builder for `subscriptions(state)`.
386
+ * The running Sub of a managed resource named `N`: its `deps` is the lifetime
387
+ * key. The consumer's Sub union (`types.sub`) includes it directly.
541
388
  */
542
- interface CombinedManagedResources<S, TKey, Ctx> {
543
- /**
544
- * Map every entry through its `when(state)`, drop the `null`s, and build the
545
- * live Subs. Spread into `subscriptions(state)`. The derived list and the
546
- * `subscribe` routing share one source — the entries — so they never drift.
547
- */
548
- readonly subs: (state: S) => ManagedResourceSub<TKey>[];
549
- /**
550
- * The SINGLE `subscribe.managed_resource` handler. Routes an incoming Sub to
551
- * the owning battery by matching `sub.id === battery.subIdFor(sub.key)` (the
552
- * id encodes the battery `name`). Throws loudly on an unrouted id — a wiring
553
- * bug, never a silent no-op resource.
554
- */
555
- readonly subscribe: (sub: ManagedResourceSub<TKey>, ctx: Ctx, dispatch: (msg: never) => void) => Dispose;
556
- }
557
- interface DefineManagedResourceOpts<TKey, Handle, Ctx> {
389
+ type ManagedResourceSub<N extends string, TKey> = Sub<N, TKey>;
390
+ interface DefineManagedResourceOpts<N extends string, TKey, Handle, Ctx> {
558
391
  /**
559
- * A short, stable name. The Sub id is `managed:${name}:${stringify(key)}`,
560
- * so two managed resources in one machine never collide and neither
561
- * collides with a `fromTransport` Sub keyed on the same identity.
392
+ * A short, stable name — and the resource's Sub `type`. The machine's
393
+ * `subscribe` table holds this battery's runner under it, so it must be
394
+ * unique among the machine's Sub types.
562
395
  */
563
- readonly name: string;
396
+ readonly name: N;
564
397
  /**
565
- * Build the resource. Runs when the Sub appears in `subscriptions(state)`.
398
+ * Build the resource. Runs when the resource's `deps` turns non-null.
566
399
  * The returned Handle is the ONLY thing `release` receives — capture
567
- * everything teardown needs into it. If `acquire` throws, the substrate's
568
- * reconcile does NOT register the Sub and `release` is never called (no
569
- * dangling half-built resource).
400
+ * everything teardown needs into it. If `acquire` throws, the engine does
401
+ * NOT register the Sub and `release` is never called (no dangling
402
+ * half-built resource).
570
403
  */
571
404
  readonly acquire: (key: TKey, ctx: Ctx) => Handle;
572
405
  /**
573
- * Tear the resource down. Runs when the Sub leaves the desired set (phase
574
- * exited) or when its key changes. MANDATORY — the guaranteed teardown is
575
- * the reason this battery exists.
406
+ * Tear the resource down. Runs when the key goes null (phase exited) or
407
+ * changes. MANDATORY — the guaranteed teardown is the reason this battery
408
+ * exists.
576
409
  *
577
410
  * May be sync or async, and an async one is really awaited: the promise is
578
411
  * returned to the substrate, which tracks it and drains it inside `stop()`
@@ -586,78 +419,44 @@ interface DefineManagedResourceOpts<TKey, Handle, Ctx> {
586
419
  readonly release: (handle: Handle) => void | Promise<void>;
587
420
  }
588
421
  /**
589
- * What the battery returns: a `.sub(key)` builder for `subscriptions`, the
590
- * `.subscribe` handler for the machine's `subscribe` record, a `.get(key)`
591
- * accessor so Cmd handlers can reach the live Handle while the resource is
592
- * held, and a `.subIdFor(key)` for tests.
422
+ * What the battery returns: a `.depKeyed(when)` entry for the machine's
423
+ * `subs`, the `.subscribe` runner for the `subscribe` table handed to `run`,
424
+ * and a `.get(key)` accessor so Cmd handlers can reach the live Handle while
425
+ * the resource is held.
593
426
  */
594
- interface ManagedResourceBattery<TKey, Handle, Ctx> {
427
+ interface ManagedResourceBattery<N extends string, TKey, Handle, Ctx> {
428
+ /** The battery's Sub type — its `name`. */
429
+ readonly type: N;
595
430
  /**
596
- * Build the Sub for `subscriptions(state)`. The consumer calls this from
597
- * inside their `subscriptions` cell for the phases where the resource
598
- * should be live.
431
+ * The `subs` entry. `when(state)` returns the resource's KEY when it should
432
+ * be held, or `null` when it should be torn down. The key is the Sub's
433
+ * `deps`, so the engine derives the id from it: an unchanged key leaves the
434
+ * resource alone, a changed key releases the old one and acquires anew.
435
+ * Plain JSON-compatible data only — the id hash throws on anything else.
599
436
  */
600
- readonly sub: (key: TKey) => ManagedResourceSub<TKey>;
437
+ readonly depKeyed: <S>(when: (state: S) => TKey | null) => DepKeyedSub<S, ManagedResourceSub<N, TKey>>;
601
438
  /**
602
- * Pair this battery with a state-gate, producing a self-contained entry for
603
- * `combineManagedResources`. `when(state)` returns the resource's KEY when
604
- * it should be live in that state, or `null` when it should be torn down.
605
- *
606
- * Folding "is it active?" and "what key?" into ONE declaration at the
607
- * battery's own definition site is the point: a central `subscriptions(state)`
608
- * list can silently forget to gate a new phase, but a per-resource gate
609
- * travels with the resource. The combinator derives BOTH the active-sub list
610
- * and the subscribe routing from these entries, so the two can never disagree.
611
- *
612
- * Generic in the consumer's state `S` (each `.gated` call binds its own `S`).
613
- * The returned entry erases this battery's `Handle` — `combineManagedResources`
614
- * holds entries of heterogeneous handle types with no cast — while keeping
615
- * `TKey` and `Ctx` so the substrate's narrowed `ManagedResourceSub<TKey>`
616
- * flows straight into the entry's `subscribe`.
439
+ * The runner for this battery's Sub type — hand it to `run` as
440
+ * `subscribe: { [battery.type]: battery.subscribe }`. Runs `acquire` on
441
+ * start, holds the Handle in the battery's handle table, and returns a
442
+ * cleanup that runs `release` (and forgets the Handle) on stop.
617
443
  */
618
- readonly gated: <S>(when: (state: S) => TKey | null) => GatedManagedResource<S, TKey, Ctx>;
444
+ readonly subscribe: (sub: ManagedResourceSub<N, TKey>, ctx: Ctx, dispatch: (msg: never) => void) => Dispose;
619
445
  /**
620
- * Dep-keyed entry for the machine's `subs` array. `when(state)` returns the
621
- * resource's KEY when it should be held, or `null` when it should be torn
622
- * down — the exact gate `.gated` takes, now producing a `DepKeyedSub` the
623
- * kernel folds directly instead of an entry the `combineManagedResources`
624
- * router multiplexes.
625
- *
626
- * This is the simplification the dep-keyed kernel unlocks: two managed
627
- * resources used to COLLIDE on the single `subscribe.managed_resource` key
628
- * (both subs share `type: "managed_resource"`), forcing
629
- * `combineManagedResources` to route by SubId. A dep-keyed Sub has its OWN
630
- * reconcile slot (its array position), so there is no shared `type` to route
631
- * — each resource is one `subs` entry, no combinator, no router. `acquire` /
632
- * `release` still ride the kernel's dispose-on-change / dispose-on-null
633
- * reconcile, now via the dep-keyed source's `Dispose` instead of the
634
- * `subscribe` cell's cleanup.
635
- */
636
- readonly depKeyed: <S>(when: (state: S) => TKey | null) => DepKeyedSub<S, never, Ctx>;
637
- /**
638
- * The `subscribe.managed_resource` handler. Drop straight into the
639
- * machine's `subscribe` record. Runs `acquire` on install, holds the
640
- * Handle in the battery's handle table, and returns a cleanup that runs
641
- * `release` (and forgets the Handle) on reconcile-out.
642
- */
643
- readonly subscribe: (sub: ManagedResourceSub<TKey>, ctx: Ctx, dispatch: (msg: never) => void) => Dispose;
644
- /**
645
- * Accessor for the live Handle, keyed on `key`. Returns `undefined` when
646
- * the resource is not currently held (no active sub for that key) — the
647
- * caller decides whether that is an error. A Cmd handler wires its
648
- * `ctx.getX(key)` through this, so the handler reads the SAME owner the
446
+ * Accessor for the live Handle, keyed on `key` (the author's key, never the
447
+ * derived Sub id). Returns `undefined` when the resource is not currently
448
+ * held — the caller decides whether that is an error. A Cmd handler wires
449
+ * its `ctx.getX(key)` through this, so the handler reads the SAME owner the
649
450
  * reconciler holds, never a hand-built duplicate.
650
451
  */
651
452
  readonly get: (key: TKey) => Handle | undefined;
652
- /** Pre-derived subId for `(key)` — handy for tests. */
653
- readonly subIdFor: (key: TKey) => SubId;
654
453
  }
655
454
  /**
656
455
  * Build the battery. One call per managed resource at the machine's host
657
456
  * file.
658
457
  *
659
458
  * @example
660
- * const checkpointStore = defineManagedResource<RunId, CheckpointSaver, Ctx>({
459
+ * const checkpointStore = defineManagedResource<"checkpoint", RunId, CheckpointSaver, Ctx>({
661
460
  * name: "checkpoint",
662
461
  * acquire: (runId, ctx) =>
663
462
  * new CheckpointSaver({ storage: ctx.storage, bucket: ctx.bucket, runId }),
@@ -665,9 +464,15 @@ interface ManagedResourceBattery<TKey, Handle, Ctx> {
665
464
  * });
666
465
  *
667
466
  * // In the machine:
668
- * subscriptions: (state) =>
669
- * state.type === "running" ? [checkpointStore.sub(state.runId)] : [],
670
- * subscribe: { managed_resource: checkpointStore.subscribe },
467
+ * types: { …, sub: {} as ManagedResourceSub<"checkpoint", RunId> },
468
+ * subs: [
469
+ * checkpointStore.depKeyed((s: State) =>
470
+ * s.type === "running" ? s.runId : null,
471
+ * ),
472
+ * ],
473
+ *
474
+ * // At run:
475
+ * run(machine, { interpret, subscribe: { checkpoint: checkpointStore.subscribe } });
671
476
  *
672
477
  * // In interpret (read the live handle the reconciler holds):
673
478
  * start_graph: async (cmd, ctx) => {
@@ -675,8 +480,7 @@ interface ManagedResourceBattery<TKey, Handle, Ctx> {
675
480
  * ...
676
481
  * }
677
482
  */
678
- declare function defineManagedResource<TKey, Handle, Ctx>(opts: DefineManagedResourceOpts<TKey, Handle, Ctx>): ManagedResourceBattery<TKey, Handle, Ctx>;
679
- declare function combineManagedResources<S, TKey, Ctx>(entries: readonly GatedManagedResource<S, TKey, Ctx>[]): CombinedManagedResources<S, TKey, Ctx>;
483
+ declare function defineManagedResource<N extends string, TKey, Handle, Ctx>(opts: DefineManagedResourceOpts<N, TKey, Handle, Ctx>): ManagedResourceBattery<N, TKey, Handle, Ctx>;
680
484
 
681
485
  /**
682
486
  * Duplex transport. The host (DO, worker thread, in-process bus) constructs
@@ -688,8 +492,8 @@ interface Transport {
688
492
  send(data: string): void;
689
493
  /**
690
494
  * Subscribe to inbound frames. Returns a cleanup that removes the listener.
691
- * The battery wires this in its Sub's subscribe handler; cleanup runs when
692
- * the phase exits (the substrate's reconcileSubs).
495
+ * The battery wires this in its runner; cleanup runs when the seam's key
496
+ * goes null or changes.
693
497
  */
694
498
  onMessage(listener: (data: string) => void): () => void;
695
499
  /**
@@ -702,26 +506,22 @@ interface Transport {
702
506
  }
703
507
  /**
704
508
  * Factory the consumer wires to a platform-specific transport. The battery
705
- * passes the runtime-derived key (`runId`, or whatever identity is on the
706
- * phase) so per-run transports can be addressed.
509
+ * passes the seam's key (`runId`, or whatever identity is on the phase) so
510
+ * per-run transports can be addressed.
707
511
  */
708
512
  type TransportFactory<K> = (key: K) => Transport;
709
513
  /**
710
- * The Sub the battery builds. The consumer's `Sub` union includes
711
- * `TransportSub<TKey>` directly — no wrapping needed, the substrate
712
- * reconciles by `id` and `type` like any other Sub.
514
+ * The running Sub of a seam named `N`: its `deps` is the seam key. The
515
+ * consumer's Sub union (`types.sub`) includes it directly.
713
516
  */
714
- interface TransportSub<TKey> extends Sub<"transport"> {
715
- /** The phase / run identity. Stable for the lifetime of the seam. */
716
- readonly key: TKey;
717
- }
718
- interface FromTransportOpts<TKey, Inbound, Outbound, M, Ctx> {
517
+ type TransportSub<N extends string, TKey> = Sub<N, TKey>;
518
+ interface FromTransportOpts<N extends string, TKey, Inbound, Outbound, M, Ctx> {
719
519
  /**
720
- * A short, stable name for this seam. The Sub id is derived from
721
- * `${name}:${stringify(key)}` so two seams in the same machine (e.g.
722
- * `hands` + `worker`) never collide.
520
+ * A short, stable name for this seam — and its Sub `type`. The machine's
521
+ * `subscribe` table holds the seam's runner under it, so it must be unique
522
+ * among the machine's Sub types.
723
523
  */
724
- readonly name: string;
524
+ readonly name: N;
725
525
  /**
726
526
  * Build the transport. The battery owns the lifetime; the factory is the
727
527
  * platform-specific constructor (`new WebSocket(...)` adapter on a DO,
@@ -753,53 +553,42 @@ interface FromTransportOpts<TKey, Inbound, Outbound, M, Ctx> {
753
553
  readonly serializeOutbound: (outbound: Outbound) => string;
754
554
  }
755
555
  /**
756
- * What the battery returns: a Sub builder (for `subscriptions`), the Sub's
757
- * `subscribe` handler (for `subscribe.transport`), and a `send(key, outbound)`
758
- * helper the consumer's Cmd handler calls. Plus a `subIdFor` factory the
759
- * consumer can use when keying related Subs (e.g. a deadline) to the same seam
760
- * identity.
556
+ * What the battery returns: a `.depKeyed(when)` entry for the machine's
557
+ * `subs`, the `.subscribe` runner for the `subscribe` table handed to `run`,
558
+ * and a `send(key, outbound)` helper the consumer's Cmd handler calls.
761
559
  */
762
- interface TransportBattery<TKey, Outbound, M, Ctx> {
560
+ interface TransportBattery<N extends string, TKey, Outbound, M, Ctx> {
561
+ /** The seam's Sub type — its `name`. */
562
+ readonly type: N;
763
563
  /**
764
- * Build the Sub for `subscriptions(state)`. The consumer calls this from
765
- * inside their `subscriptions` cell when the seam should be open.
564
+ * The `subs` entry. `when(state)` returns the seam's KEY when it should be
565
+ * open, or `null` when it should be torn down. The key is the Sub's `deps`,
566
+ * so the engine derives the id from it: an unchanged key leaves the seam
567
+ * open, a changed key closes it and opens a fresh one. Plain
568
+ * JSON-compatible data only — the id hash throws on anything else.
766
569
  */
767
- readonly sub: (key: TKey) => TransportSub<TKey>;
570
+ readonly depKeyed: <S>(when: (state: S) => TKey | null) => DepKeyedSub<S, TransportSub<N, TKey>>;
768
571
  /**
769
- * The `subscribe.transport` handler. Drop straight into the machine's
770
- * `subscribe` record — the battery wires inbound + close in one shot.
572
+ * The runner for this seam's Sub type — hand it to `run` as
573
+ * `subscribe: { [seam.type]: seam.subscribe }`. Wires inbound + close in
574
+ * one shot and publishes the live transport to the outbound handle table.
771
575
  */
772
- readonly subscribe: (sub: TransportSub<TKey>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
576
+ readonly subscribe: (sub: TransportSub<N, TKey>, ctx: Ctx, dispatch: (msg: M) => void) => Dispose;
773
577
  /**
774
- * Outbound send. The Cmd handler calls this; the battery's handle table
775
- * (built when subscribe ran) routes to the live transport. If the seam
776
- * is closed (no active sub), the send is dropped honestly (logged, not
777
- * thrown — the reducer already moved past caring, same shape as Rule 2
778
- * fire-and-forget Cmds).
578
+ * Outbound send, addressed by the seam's key (never the derived Sub id,
579
+ * which the caller cannot know). The Cmd handler calls this; the battery's
580
+ * handle table (built when the runner started) routes to the live
581
+ * transport. If the seam is closed (no running Sub), the send is dropped
582
+ * honestly (logged, not thrown — the reducer already moved past caring,
583
+ * same shape as Rule 2 fire-and-forget Cmds).
779
584
  */
780
585
  readonly send: (key: TKey, outbound: Outbound) => void;
781
- /** Pre-derived subId for `(key)` — handy for deadline Subs keyed to the same seam. */
782
- readonly subIdFor: (key: TKey) => SubId;
783
- /**
784
- * Pair this seam with a state-gate, producing a dep-keyed Sub for the
785
- * machine's `subs` array. `when(state)` returns the seam's KEY when it
786
- * should be open, or `null` when it should be torn down. The kernel derives
787
- * the id from `structuralHash(key)` and reconciles the seam's lifetime — so
788
- * the author neither lists this Sub in a central `subscriptions(state)` nor
789
- * writes its id. Replaces `subscriptions: (s) => active(s) ? [hands.sub(...)] : []`
790
- * + `subscribe: { transport: hands.subscribe }` with one `subs` entry.
791
- *
792
- * The `source` closes over the battery's own `subscribe` (open inbound +
793
- * close, wire the handle table) using the same `ctx` the kernel passes every
794
- * Sub source.
795
- */
796
- readonly depKeyed: <S>(when: (state: S) => TKey | null) => DepKeyedSub<S, M, Ctx>;
797
586
  }
798
587
  /**
799
588
  * Build the battery. One call per seam at the machine's host file.
800
589
  *
801
590
  * @example
802
- * const handsSeam = fromTransport<RunId, HandsInbound, HandsOutbound, Msg, Ctx>({
591
+ * const handsSeam = fromTransport<"hands", RunId, HandsInbound, HandsOutbound, Msg, Ctx>({
803
592
  * name: "hands",
804
593
  * openTransport: (runId, ctx) => ctx.openHandsWs(runId),
805
594
  * parseInbound: (raw) => parseHandsInbound(JSON.parse(raw)),
@@ -809,12 +598,19 @@ interface TransportBattery<TKey, Outbound, M, Ctx> {
809
598
  * });
810
599
  *
811
600
  * // In the machine:
812
- * subscriptions: (state) => state.type === "auditing" ? [handsSeam.sub(state.runId)] : [],
813
- * subscribe: { transport: handsSeam.subscribe },
601
+ * types: { …, sub: {} as TransportSub<"hands", RunId> },
602
+ * subs: [
603
+ * handsSeam.depKeyed((s: State) =>
604
+ * s.type === "auditing" ? s.runId : null,
605
+ * ),
606
+ * ],
607
+ *
608
+ * // At run:
609
+ * run(machine, { interpret, subscribe: { hands: handsSeam.subscribe } });
814
610
  *
815
611
  * // In interpret:
816
612
  * send_hands: async (cmd) => { handsSeam.send(cmd.runId, cmd.outbound); },
817
613
  */
818
- declare function fromTransport<TKey, Inbound, Outbound, M, Ctx>(opts: FromTransportOpts<TKey, Inbound, Outbound, M, Ctx>): TransportBattery<TKey, Outbound, M, Ctx>;
614
+ declare function fromTransport<N extends string, TKey, Inbound, Outbound, M, Ctx>(opts: FromTransportOpts<N, TKey, Inbound, Outbound, M, Ctx>): TransportBattery<N, TKey, Outbound, M, Ctx>;
819
615
 
820
- export { type Ack, type AckPartition, BootingRuntime, type CancelTimer, Cmd, type CombinedManagedResources, type DefineManagedResourceOpts, DepKeyedSub, Dispose, type DriveToDoneOptions, type EventSourceFactoryOpts, type FromTransportOpts, type GatedManagedResource, type HistoryTracker, type ListenerTarget, Machine, type ManagedResourceBattery, type ManagedResourceSub, NO_ACK, OnError, Port, type ReconnectingWebSocketFactoryOpts, RequiredCtx, Runtime, ScopedCtxArg, type Seq, type SeqTagged, Store, Sub, SubId, SubscribeHandler, Supervision, type Transport, type TransportBattery, type TransportFactory, type TransportSub, type WebSocketFactoryOpts, type WebSocketSubData, ack, combineManagedResources, defineListener, defineManagedResource, driveToDone, fromBroadcastChannel, fromEventSource, fromEventTarget, fromInterval, fromPort, fromReconnectingWebSocket, fromTimeout, fromTransport, fromWebSocket, historyTracker, initAck, nextSeq, partitionByAck, reconcile, run, tagSeq };
616
+ export { type Ack, type AckPartition, BootingRuntime, type CancelTimer, Cmd, type DefineManagedResourceOpts, DepKeyedSub, Dispose, type EventSourceFactoryOpts, type FromTransportOpts, type HistoryTracker, type ListenerTarget, Machine, type ManagedResourceBattery, type ManagedResourceSub, NO_ACK, Port, type ReadStep, type ReconnectingWebSocketFactoryOpts, Runtime, type Seq, type SeqTagged, Sub, SubscribeHandler, type Transport, type TransportBattery, type TransportFactory, type TransportSub, type WebSocketFactoryOpts, type WebSocketSubData, ack, defineListener, defineManagedResource, fromBroadcastChannel, fromEventSource, fromEventTarget, fromInterval, fromPort, fromReconnectingWebSocket, fromTimeout, fromTransport, fromWebSocket, historyTracker, initAck, liftSlice, nextSeq, partitionByAck, readInOrder, reconcile, tagSeq };