@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
@@ -115,7 +115,7 @@ function createFanOut(config) {
115
115
  }
116
116
  return [launched, launchCmds];
117
117
  }
118
- function handlers(ports) {
118
+ function completion(ports) {
119
119
  return {
120
120
  /**
121
121
  * Emit the gathered done-results onto `ports.complete` iff `state` is a
@@ -138,10 +138,10 @@ function createFanOut(config) {
138
138
  itemOk,
139
139
  itemErr,
140
140
  isComplete,
141
- handlers
141
+ completion
142
142
  };
143
143
  }
144
144
 
145
145
  export { createFanOut, initFanOut, isComplete };
146
- //# sourceMappingURL=chunk-MYXFNWAJ.js.map
147
- //# sourceMappingURL=chunk-MYXFNWAJ.js.map
146
+ //# sourceMappingURL=chunk-QNGYQMZB.js.map
147
+ //# sourceMappingURL=chunk-QNGYQMZB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/flow/fan-out/index.ts"],"names":[],"mappings":"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","file":"chunk-QNGYQMZB.js","sourcesContent":["/**\n * internal/flow/fan-out — scatter-gather over a bounded-concurrency work list.\n *\n * The Level-2 combinator that fans a batch of items out across at most\n * `concurrency` in-flight effects, gathers each item's result (or error), and\n * — once every item has settled — fires an optional `join` over the collected\n * results. It is the \"map a list through an effect, but never run more than N\n * at once\" knob.\n *\n * The recipe (which bricks it composes):\n * - `../work-queue` — fan-out's tracked items ARE work-queue items. Each\n * item carries the same `{ id, status, input }` lifecycle vocabulary\n * (`pending → running → done | failed`). The slice this knob owns is four\n * partitions over that one lifecycle, so the work-queue's status taxonomy\n * is the substrate rather than a parallel invention. We reuse its\n * `QueueItem<I>` / `QueueItemStatus` TYPES and delegate the ledger\n * transitions to the `QueueAdapter` verbs (`../work-queue/adapter`):\n * `scatter` mints each pending record with `queue.enqueue`, and `launchUpTo`\n * flips `pending → running` with `queue.claim` (one positional claim per\n * launch). The lone exception is `settleOne`'s `running → done|failed`\n * flip — it must match \"first id-match that is still `running`\" to settle\n * exactly one of a DUPLICATE id, a status predicate the adapter verbs can't\n * express, so fan-out owns that one index (see `settleOne`). The async\n * `createQueue(store)` adapter is NOT used here because its `load → mutate\n * → save` reads the clock and lives at an I/O boundary, while a combinator's\n * verbs must be pure (invariant 2) — fan-out binds the pure `QueueAdapter`\n * to its in-Model ledger instead, the same route `monitored-run` /\n * `idempotent-intake` / `saga` take. The slice lives in the Model instead —\n * durable + replayable, the two non-negotiables every knob preserves.\n *\n * Typical wiring (combinator form — splice the hooks into your machine):\n *\n * const fan = createFanOut<Url, Report>({\n * concurrency: 4,\n * idOf: (url) => url, // stable, pure identity\n * of: (url) => ({ type: \"crawl\", url }), // the per-item effect, as data\n * join: (results) => ({ type: \"report_done\", results }),\n * });\n *\n * // in `update`:\n * start_crawl: (s, m) => { const [fanOut, cmds] = fan.scatter(s.fanOut, m.urls);\n * return [{ ...s, fanOut }, cmds]; },\n * crawl_ok: (s, m) => { const [fanOut, cmds] = fan.itemOk(s.fanOut, m.id, m.report);\n * return [{ ...s, fanOut }, cmds]; },\n * crawl_err: (s, m) => { const [fanOut, cmds] = fan.itemErr(s.fanOut, m.id, m.error);\n * return [{ ...s, fanOut }, cmds]; },\n *\n * Every verb is PURE: it returns a NEW slice plus the Cmds to perform (the\n * `of(item)` effects for newly-launched items, and at most one `join` Cmd when\n * the batch completes). The reducer never reads a clock or RNG — item identity\n * is supplied by the caller's `idOf` so `scatter` is deterministic, and the\n * launch effects are plain data the runtime performs (invariant 3).\n *\n * Fan-out does NOT own the interpret handler for a launched `of(item)` Cmd: `of`\n * already produces a fully caller-defined Cmd, so the consumer's own\n * `interpret` performs the effect and routes Ok/Err back to the `itemOk` /\n * `itemErr` verbs. `completion(ports)` is the one piece fan-out CAN pre-wire\n * generically: emitting the gathered batch onto a typed Port when the batch\n * completes, for consumers that observe completion out-of-band rather than\n * through the `join` Cmd.\n *\n * NOT a substrate primitive: it depends only on `../work-queue`'s pure types +\n * blessed ops and the core `Cmd` / `Port` types. Internal since #48 — not\n * published on any subpath; reached from inside the package as\n * `internal/flow/fan-out`.\n */\n\nimport { at } from \"../../../at\";\nimport type { Cmd, Port } from \"../../../index\";\nimport type { QueueItem, QueueItemStatus } from \"../../work-queue\";\nimport { queueAdapter } from \"../../work-queue/adapter\";\n\n/**\n * One settled-OK item: the original `input` and the `result` its effect\n * produced. Mirrors the spec slice's `done: {item, result}[]` — `item` is the\n * caller's input value (not the internal `QueueItem` envelope), so a consumer\n * reading `done` gets back exactly what it scattered.\n */\nexport interface FanOutDone<I, R> {\n /** The original scattered input value. */\n readonly item: I;\n /** The result the item's effect produced, routed in via `itemOk`. */\n readonly result: R;\n}\n\n/**\n * One settled-error item: the original `input` and the `error` its effect\n * surfaced. `error` is `unknown` — fan-out carries it forward verbatim (an\n * `Error`, a tagged result, a string) for the consumer's reducer to read,\n * never interpreting it (same discipline as `retry-backoff`'s `lastError`).\n */\nexport interface FanOutFailed<I> {\n /** The original scattered input value. */\n readonly item: I;\n /** The error the item's effect surfaced, routed in via `itemErr`. Carried, never interpreted. */\n readonly error: unknown;\n}\n\n/**\n * The slice this knob owns — four partitions over the work-queue item\n * lifecycle, plus the work-queue records the verbs thread the lifecycle\n * through.\n *\n * Why both the spec's four arrays AND `items`:\n * - The four arrays (`pending` / `running` / `done` / `failed`) are the\n * SPEC's public read surface — what a consumer's view renders and what\n * `isComplete` reads. They hold caller-facing values (`I`, or\n * `FanOutDone` / `FanOutFailed`), not the internal id-bearing envelope.\n * - `items` is the work-queue ledger: one `QueueItem<I>` per scattered item,\n * carrying the stable `id` (from `config.idOf`) that `itemOk` / `itemErr`\n * address and the `status` that decides launch-eligibility. It is the\n * single source of truth; the four arrays are a derived projection kept in\n * lockstep so consumers never re-scan `items` to render a partition.\n *\n * Every field is `readonly` and the whole slice is JSON-serializable (plain\n * arrays + the work-queue's plain `QueueItem` records), so it round-trips\n * through a consumer's `Store<S>` unchanged — durable across DO eviction.\n */\nexport interface FanOutState<I, R> {\n /** Items accepted but not yet launched (queue back-pressure beyond `concurrency`). */\n readonly pending: readonly I[];\n /** Items whose `of(item)` effect is in flight (size never exceeds `concurrency`). */\n readonly running: readonly I[];\n /** Items whose effect resolved OK, with the result it produced. */\n readonly done: readonly FanOutDone<I, R>[];\n /** Items whose effect failed, with the error it surfaced. */\n readonly failed: readonly FanOutFailed<I>[];\n /**\n * The work-queue ledger: one record per scattered item, keyed by the\n * caller's `idOf`. The single source of truth the four arrays project from.\n */\n readonly items: readonly QueueItem<I>[];\n /**\n * Index into `done` where the CURRENT wave's OK results begin. `done` is\n * append-only and accumulates across every wave (so a consumer reading\n * `done` sees the full history), but `join` must fold only the wave that\n * just drained — not the cumulative total. A wave opens when `scatter`\n * enqueues into a slice with nothing in flight; this marks `done.length` at\n * that moment so `join` receives exactly `done.slice(waveDoneFrom)`.\n */\n readonly waveDoneFrom: number;\n /**\n * Index into `failed` where the current wave's failures begin. Symmetric to\n * `waveDoneFrom` — the wave boundary over the failure partition. Carried for\n * a consumer that wants to read only the current wave's failures; `join`\n * itself folds only OK results.\n */\n readonly waveFailedFrom: number;\n /**\n * Whether the current wave has already fired its `join`. `join` fires at most\n * once per wave — set `true` at the completion edge so a stale settle that\n * re-reaches `isComplete` cannot re-fire it. A new `scatter` that opens a wave\n * resets it to `false`.\n */\n readonly joined: boolean;\n}\n\n/**\n * The knob. `concurrency` caps how many `of(item)` effects are in flight at\n * once; `idOf` mints a stable, PURE identity per item (so `scatter` is\n * deterministic and `itemOk` / `itemErr` can address an item by id); `of`\n * turns an item into the launch Cmd; `join` (optional) turns the gathered\n * results into a single completion Cmd fired exactly once when every item has\n * settled.\n *\n * Field optionality follows the resilient-call rule — omit `join`, omit the\n * completion Cmd (a consumer that observes `isComplete` itself, or wires the\n * `completion` Port, doesn't need one).\n */\nexport interface FanOutConfig<I, R, C extends Cmd, J extends Cmd> {\n /** Max in-flight `of(item)` effects. Clamped to `>= 1` (a 0/negative cap would deadlock). */\n readonly concurrency: number;\n /**\n * Stable, deterministic identity for an item. PURE — no clock, no RNG. The\n * id is what `itemOk` / `itemErr` address, so it must survive a round-trip\n * through the durable slice and match across the launch → settle gap. For\n * value-unique inputs (URLs, ids) this is often the identity function; for\n * structural inputs, a content hash or a caller-assigned key.\n */\n readonly idOf: (item: I) => string;\n /** The per-item effect, as data. Performed by the consumer's own `interpret`. */\n readonly of: (item: I) => C;\n /**\n * Optional: fold the gathered done-results into a single completion Cmd,\n * fired exactly once at the transition that empties `pending` + `running`.\n * Omit to drive completion off `isComplete` (or the `completion` Port) instead.\n */\n readonly join?: (results: readonly R[]) => J;\n}\n\n/**\n * Clamp the configured concurrency to a usable launch budget. A cap of 0 or\n * negative would launch nothing and the batch would deadlock with items stuck\n * `pending` forever; clamping to 1 degrades gracefully to fully-serial\n * fan-out (still correct, just no parallelism) rather than hanging.\n */\nfunction launchBudget(concurrency: number): number {\n return concurrency < 1 ? 1 : Math.floor(concurrency);\n}\n\n/** The starting slice: nothing scattered yet. */\nexport function initFanOut<I, R>(): FanOutState<I, R> {\n return {\n pending: [],\n running: [],\n done: [],\n failed: [],\n items: [],\n waveDoneFrom: 0,\n waveFailedFrom: 0,\n joined: false,\n };\n}\n\n/**\n * Whether every scattered item has settled (no `pending`, no `running`).\n * Derived — reads the slice, never mutates. The verbs use it to decide when to\n * fire `join`; consumers can read it directly to branch their own phase.\n *\n * An empty fan-out (nothing ever scattered) is NOT complete: completion is \"a\n * non-empty batch fully drained,\" so `isComplete(initFanOut())` is `false` and\n * a `scatter([])` of zero items also reads `false` (there was no batch to\n * complete). This keeps `join` from firing on a degenerate empty scatter.\n */\nexport function isComplete<I, R>(state: FanOutState<I, R>): boolean {\n if (state.items.length === 0) return false;\n return state.pending.length === 0 && state.running.length === 0;\n}\n\n/**\n * Count of items currently in flight. `running.length`, named for the launch\n * arithmetic below (and readable at a call site).\n */\nfunction inFlight<I, R>(state: FanOutState<I, R>): number {\n return state.running.length;\n}\n\n/**\n * Move up to `budget - inFlight` items from `pending` to `running`, emitting\n * `of(item)` for each launched item. PURE — returns the new slice and the\n * launch Cmds. The work-queue `items` ledger flips each launched item's status\n * `pending → running` via the `QueueAdapter`'s `claim`, in lockstep with the\n * projected arrays.\n *\n * Delegation, not re-rolling: each launch is one `queue.claim` call — the same\n * `pending → running` transition `src/internal/work-queue/` owns. `queue.claim`\n * flips the FIRST pending ledger record positionally, and fan-out's `pending`\n * array is appended in lockstep with the ledger, so the first `slots` pending\n * inputs map to the first `slots` pending records — claiming one-per-launch\n * settles exactly those. This also makes a DUPLICATE id correct: a positional\n * claim flips ONE record per launch, where a blanket id-set flip would promote\n * every pending record sharing the launched id (over-flipping the ledger past\n * `running.length`). The `at` stamp is `0` — fan-out's verbs are pure and never\n * read a clock; positional order, not the timestamp, drives every decision\n * (same convention as `scatter`'s `enqueuedAt: 0`).\n *\n * Shared by `scatter` (launch on enqueue) and `itemOk` / `itemErr` (launch the\n * next pending item as a slot frees). Factoring it here keeps the concurrency\n * invariant — `running.length <= budget` — in ONE place.\n */\nfunction launchUpTo<I, R, C extends Cmd>(\n state: FanOutState<I, R>,\n config: {\n concurrency: number;\n of: (item: I) => C;\n },\n): readonly [FanOutState<I, R>, readonly C[]] {\n const budget = launchBudget(config.concurrency);\n const slots = budget - inFlight(state);\n if (slots <= 0 || state.pending.length === 0) return [state, []];\n\n const toLaunch = state.pending.slice(0, slots);\n const stillPending = state.pending.slice(slots);\n\n // Flip the ledger records `pending → running` one launch at a time via the\n // `QueueAdapter` (positional first-pending claim per call).\n const queue = queueAdapter<I>();\n let items: readonly QueueItem<I>[] = state.items;\n for (let i = 0; i < toLaunch.length; i++) {\n const claimed = queue.claim(items, 0);\n if (claimed === null) break;\n items = claimed.next;\n }\n\n const next: FanOutState<I, R> = {\n ...state,\n pending: stillPending,\n running: [...state.running, ...toLaunch],\n items,\n };\n const cmds = toLaunch.map((item) => config.of(item));\n return [next, cmds];\n}\n\n/**\n * Settle EXACTLY ONE running entry for `id`: remove the first `running` item\n * whose `idOf` matches, and flip the first ledger record for `id` that is still\n * `running` to `status`. Returns the input that was settled plus the new\n * `running` / `items` arrays, or `undefined` if no running item carries that id\n * (a stale / unknown settle Msg → caller treats as a no-op rather than a crash).\n *\n * Why \"exactly one\": when two scattered items share an `idOf` (a duplicate id),\n * the ledger holds two `running` records under the same id and `running` holds\n * two inputs. A settle Msg carries one result, so it must close out exactly one\n * of them — not all matches. A blanket `filter((x) => idOf(x) !== id)` would\n * drop BOTH running entries while recording only ONE result, silently losing a\n * unit of work and corrupting the done/failed partition. find-index + splice\n * removes a single `running` entry; the matching `findIndex` over the ledger\n * flips a single record. The OTHER duplicate stays `running`, awaiting its own\n * settle Msg.\n *\n * This flip is NOT delegated to the `QueueAdapter`'s `patch`: the verb matches\n * the FIRST record by id, but the record fan-out must flip is \"first id-match\n * that is still `running`\" — and after a dup-id's first settle the first id-match\n * is the already-terminal record. The status predicate is load-bearing here and\n * the adapter verbs can't express it, so fan-out owns this one index. The flipped\n * record is still built the work-queue way (spread + new `status`), and there\n * is no `as` cast: `status` arrives already typed `QueueItemStatus`.\n */\nfunction settleOne<I, R>(\n state: FanOutState<I, R>,\n id: string,\n status: QueueItemStatus,\n idOf: (item: I) => string,\n):\n | { input: I; running: readonly I[]; items: readonly QueueItem<I>[] }\n | undefined {\n const runIdx = state.running.findIndex((item) => idOf(item) === id);\n // `at` collapses \"no running entry for this id\" into an `undefined` early-out,\n // narrowing `input` to `I` below without a non-null assertion (the pattern\n // `ops.ts` uses); `runIdx` stays around for the splice.\n const input = at(state.running, runIdx);\n if (input === undefined) return undefined;\n\n const running = [\n ...state.running.slice(0, runIdx),\n ...state.running.slice(runIdx + 1),\n ];\n\n const ledgerIdx = state.items.findIndex(\n (item) => item.id === id && item.status === \"running\",\n );\n // Same early-out via `at`: `ledger === undefined` narrows the record to\n // `QueueItem<I>` for the spread below without a non-null assertion.\n const ledger = at(state.items, ledgerIdx);\n const items =\n ledger === undefined\n ? state.items\n : [\n ...state.items.slice(0, ledgerIdx),\n { ...ledger, status },\n ...state.items.slice(ledgerIdx + 1),\n ];\n\n return { input, running, items };\n}\n\n/**\n * Optional Port for observing batch completion out-of-band. When a consumer\n * passes `completion(ports)` a `complete` Port, the runtime side can emit the\n * gathered results onto it the moment `isComplete` flips true — distinct from\n * the in-band `join` Cmd (which threads back through `update`). Use the Port\n * for \"tell the outside world the batch finished\" without folding a completion\n * signal into the consumer's Msg union.\n */\nexport interface FanOutPorts<R> {\n /** Fires once with every gathered done-result when the batch completes. */\n readonly complete: Port<readonly R[]>;\n}\n\n/**\n * Build the fan-out knob from `config`. Returns the uniform combinator\n * contract: `init` for the owned slice, the three settle verbs + the derived\n * `isComplete`, and `completion` for the optional out-of-band completion Port.\n *\n * No `subs` — fan-out runs no timers (the spec pins \"Subs: none\"). The\n * per-item effect's lifetime is owned by the consumer's `interpret`, and\n * completion is a pure function of the settle Msgs, so there is nothing\n * continuous to subscribe to.\n */\nexport function createFanOut<I, R, C extends Cmd = Cmd, J extends Cmd = Cmd>(\n config: FanOutConfig<I, R, C, J>,\n) {\n /**\n * Enqueue `items` and launch up to `concurrency` of them. PURE.\n *\n * Each item gets a ledger record (`status: \"pending\"`, its `idOf` id) and is\n * appended to `pending`; `launchUpTo` then promotes as many as the free\n * concurrency budget allows, emitting `of(item)` per launch. The returned\n * Cmds are exactly the launch effects — `scatter` never fires `join` (a batch\n * can't complete on the same transition it starts unless it launches zero\n * effects, which `isComplete`'s empty-batch rule already declines to treat as\n * completion).\n *\n * Each pending ledger record is minted by the `QueueAdapter`'s `enqueue` — the\n * same `pending` append `src/internal/work-queue/` owns — rather than re-rolled\n * inline, so the `status: \"pending\"` literal lives in exactly one place (no\n * `as QueueItemStatus` cast here).\n *\n * `enqueuedAt` is stamped `0` rather than read from a clock: fan-out's verbs\n * are pure, and fan-out does not use the timestamp for any decision (ordering\n * is positional via the arrays). A consumer that needs real enqueue times\n * threads them through its own Msg `at` and stores them alongside.\n */\n function scatter(\n state: FanOutState<I, R>,\n items: readonly I[],\n ): readonly [FanOutState<I, R>, readonly C[]] {\n // Append one pending ledger record per item via the `QueueAdapter`.\n // `queue.enqueue` returns `status: \"pending\"` already typed — no cast — and\n // the lifecycle's \"new work is pending\" rule stays in work-queue's hands.\n const queue = queueAdapter<I>();\n let ledger: readonly QueueItem<I>[] = state.items;\n for (const input of items) {\n ledger = queue.enqueue(ledger, input, 0, config.idOf(input)).next;\n }\n // A scatter OPENS A FRESH WAVE when nothing is in flight — either the very\n // first scatter, or a re-scatter after the previous wave fully drained. The\n // wave boundary advances to the current `done` / `failed` length so `join`\n // folds only THIS wave's results, never the cumulative history, and\n // `joined` resets so the new wave can fire its own `join` exactly once.\n //\n // A scatter that adds to a wave still in flight (pending or running\n // non-empty — e.g. enqueuing more work mid-batch) does NOT open a wave:\n // those items join the current wave and settle under its single `join`.\n const opensWave = state.pending.length === 0 && state.running.length === 0;\n\n const enqueued: FanOutState<I, R> = {\n ...state,\n pending: [...state.pending, ...items],\n items: ledger,\n ...(opensWave\n ? {\n waveDoneFrom: state.done.length,\n waveFailedFrom: state.failed.length,\n joined: false,\n }\n : {}),\n };\n return launchUpTo(enqueued, config);\n }\n\n /**\n * Record `id`'s effect as succeeded with `result`, then launch the next\n * pending item to backfill the freed slot. PURE.\n *\n * Moves the item out of `running` into `done` (carrying the original input +\n * its result), flips its ledger record `running → done`, and runs\n * `launchUpTo` to keep the pipeline full. If this transition empties both\n * `pending` and `running` AND a `join` is configured, the returned Cmds also\n * include the single `join(results)` Cmd — fired exactly once at the\n * completion edge.\n *\n * A settle for an unknown / already-settled `id` is a no-op (returns the\n * slice unchanged, no Cmds): the runtime's serial dispatch makes a true\n * double-settle impossible, but a stale Msg after a boot/replay is real, and\n * swallowing it is the safe direction (the result is already recorded).\n */\n function itemOk(\n state: FanOutState<I, R>,\n id: string,\n result: R,\n ): readonly [FanOutState<I, R>, readonly (C | J)[]] {\n const settled = settleOne(state, id, \"done\", config.idOf);\n if (settled === undefined) return [state, []];\n\n const next: FanOutState<I, R> = {\n ...state,\n running: settled.running,\n done: [...state.done, { item: settled.input, result }],\n items: settled.items,\n };\n return finishOrLaunch(next);\n }\n\n /**\n * Record `id`'s effect as failed with `error`, then launch the next pending\n * item to backfill the freed slot. PURE.\n *\n * Symmetric to `itemOk`: moves the item out of `running` into `failed`\n * (carrying the original input + the error), flips its ledger record\n * `running → failed`, backfills via `launchUpTo`, and fires `join` at the\n * completion edge if configured. A failed item still counts toward completion\n * — the batch is \"done\" when every item has settled, success or failure;\n * `join` receives only the OK results (failures are read off `failed`).\n *\n * Same unknown-`id` no-op contract as `itemOk`.\n */\n function itemErr(\n state: FanOutState<I, R>,\n id: string,\n error: unknown,\n ): readonly [FanOutState<I, R>, readonly (C | J)[]] {\n const settled = settleOne(state, id, \"failed\", config.idOf);\n if (settled === undefined) return [state, []];\n\n const next: FanOutState<I, R> = {\n ...state,\n running: settled.running,\n failed: [...state.failed, { item: settled.input, error }],\n items: settled.items,\n };\n return finishOrLaunch(next);\n }\n\n /**\n * Shared tail of `itemOk` / `itemErr`: backfill the freed slot, then — if the\n * CURRENT wave is now complete, a `join` is configured, and this wave has not\n * already joined — append the single completion Cmd and mark the wave joined.\n *\n * Two guards keep `join` firing exactly once per wave:\n * - `isComplete(next)` is the completion edge (the transition that drains\n * the last in-flight item; `launchUpTo` cannot re-fill from an empty\n * `pending`, so `running` stays empty and the edge is reached once).\n * - `!next.joined` makes it idempotent even if a stale settle re-reaches a\n * completed slice, and — critically — combined with the per-wave reset in\n * `scatter` it stops `join` re-firing across waves. Without the wave\n * boundary, `done` is append-only, so a SECOND wave draining would see\n * `isComplete` true and re-fold the WHOLE cumulative `done` (wave 1 +\n * wave 2). The fix folds only `done.slice(waveDoneFrom)` — exactly the\n * results this wave produced.\n */\n function finishOrLaunch(\n settled: FanOutState<I, R>,\n ): readonly [FanOutState<I, R>, readonly (C | J)[]] {\n const [launched, launchCmds] = launchUpTo(settled, config);\n if (config.join !== undefined && !launched.joined && isComplete(launched)) {\n const results = launched.done\n .slice(launched.waveDoneFrom)\n .map((d) => d.result);\n const next: FanOutState<I, R> = { ...launched, joined: true };\n return [next, [...launchCmds, config.join(results)]];\n }\n return [launched, launchCmds];\n }\n\n /**\n * The out-of-band completion splice. Returns an observer-shaped function the\n * consumer calls after every settle: when the batch has just completed it\n * emits the gathered results onto `ports.complete`. This is the Port-side\n * analogue of the `join` Cmd — use it when completion should leave the\n * runtime as a typed signal rather than re-enter `update`.\n *\n * Returns the emit-on-complete callback so the consumer wires it into its own\n * runtime `observe` (e.g. `runtime.observe((_, s) => emit(s.fanOut))`); fan-out\n * stays out of the dispatch loop, matching `historyTracker`'s discipline.\n */\n function completion(ports: FanOutPorts<R>) {\n return {\n /**\n * Emit the gathered done-results onto `ports.complete` iff `state` is a\n * just-completed batch. Idempotency is the caller's concern (call it once\n * per transition via `observe`); emitting to a Port with no subscribers\n * is a no-op at the runtime, so an extra call is harmless.\n */\n emitOnComplete(\n emit: <T>(port: Port<T>, value: T) => void,\n state: FanOutState<I, R>,\n ): void {\n if (!isComplete(state)) return;\n emit(\n ports.complete,\n state.done.slice(state.waveDoneFrom).map((d) => d.result),\n );\n },\n };\n }\n\n return {\n init: initFanOut<I, R>,\n scatter,\n itemOk,\n itemErr,\n isComplete,\n completion,\n };\n}\n"]}
@@ -0,0 +1,98 @@
1
+ import { startLoop, builtinExtensions, liveWork } from './chunk-5XU6ZJO2.js';
2
+ import { subEntriesOf, applyCell, DriveFailedError, DriveStalledError } from './chunk-CH5KTOEZ.js';
3
+
4
+ // src/promise/builtin-runners.ts
5
+ function timer(sub, _ctx, dispatch) {
6
+ const handle = setTimeout(() => dispatch(sub.deps.msg), sub.deps.ms);
7
+ return () => clearTimeout(handle);
8
+ }
9
+ var builtinRunners = { timer };
10
+
11
+ // src/promise/run.ts
12
+ function run(machine, opts) {
13
+ const interpretMap = opts.interpret ?? {};
14
+ const subscribeTable = opts.subscribe;
15
+ const builtins = builtinRunners;
16
+ const handle = startLoop({
17
+ init: machine.init,
18
+ reduce: (state, msg) => applyCell(machine, state, msg),
19
+ subs: subEntriesOf(machine),
20
+ runnerFor: (type) => subscribeTable?.[type] ?? builtins[type],
21
+ handlerFor: (type) => interpretMap[type],
22
+ store: opts.store,
23
+ // `ctx` is conditionally optional (see `CtxArg`); default the nullish case
24
+ // to `{}` so the handler ctx and `init(loaded, ctx)` get a value.
25
+ ctx: opts.ctx ?? {},
26
+ onError: opts.onError,
27
+ disposeTimeoutMs: opts.disposeTimeoutMs ?? 5e3,
28
+ idleCap: opts.__idleCap ?? 1e5,
29
+ extensions: builtinExtensions(machine, opts)
30
+ });
31
+ return handle;
32
+ }
33
+ function isStalled(runtime) {
34
+ const probe = runtime[liveWork];
35
+ if (probe === void 0) return false;
36
+ const live = probe();
37
+ return live.subs === 0 && live.cmds === 0;
38
+ }
39
+ async function driveToDone(handle, start, isTerminal, opts = {}) {
40
+ const failed = opts.failed ?? (() => false);
41
+ const settles = (state) => isTerminal(state) || failed(state);
42
+ let detach;
43
+ let unlisten;
44
+ try {
45
+ const runtime = await handle.ready;
46
+ const booted = runtime.getState();
47
+ const outcome = (state) => {
48
+ if (failed(state)) throw new DriveFailedError(state);
49
+ return state;
50
+ };
51
+ if (settles(booted)) return outcome(booted);
52
+ const terminal = new Promise((resolve) => {
53
+ detach = runtime.observe((_msg, state) => {
54
+ if (settles(state)) resolve(state);
55
+ });
56
+ });
57
+ let cancelFailed;
58
+ if (opts.signal !== void 0) {
59
+ const { signal, cancel } = opts;
60
+ let onCancelError;
61
+ cancelFailed = new Promise((_resolve, reject) => {
62
+ onCancelError = reject;
63
+ });
64
+ const onAbort = () => {
65
+ try {
66
+ runtime.dispatchOnce(cancel(runtime.getState())).catch(onCancelError);
67
+ } catch (error) {
68
+ onCancelError(error);
69
+ }
70
+ };
71
+ if (signal.aborted) {
72
+ onAbort();
73
+ return outcome(await Promise.race([terminal, cancelFailed]));
74
+ }
75
+ signal.addEventListener("abort", onAbort, { once: true });
76
+ unlisten = () => signal.removeEventListener("abort", onAbort);
77
+ }
78
+ const msg = typeof start === "function" ? start(booted) : start;
79
+ const started = runtime.dispatch(msg).then(() => {
80
+ const parked = runtime.getState();
81
+ if (!settles(parked) && isStalled(runtime)) {
82
+ throw new DriveStalledError(parked);
83
+ }
84
+ return terminal;
85
+ });
86
+ const racers = [terminal, started];
87
+ if (cancelFailed !== void 0) racers.push(cancelFailed);
88
+ return outcome(await Promise.race(racers));
89
+ } finally {
90
+ detach?.();
91
+ unlisten?.();
92
+ await handle.stop();
93
+ }
94
+ }
95
+
96
+ export { driveToDone, run };
97
+ //# sourceMappingURL=chunk-WT5PBPLB.js.map
98
+ //# sourceMappingURL=chunk-WT5PBPLB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/promise/builtin-runners.ts","../src/promise/run.ts"],"names":[],"mappings":"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","file":"chunk-WT5PBPLB.js","sourcesContent":["/**\n * The Promise engine's built-in sub runners (#270 R1.1). A machine declares a\n * built-in Sub by type alone — `{ type: \"timer\", deps: (s) => ({ ms, msg }) }`\n * — and `run` starts it with the runner here unless `subscribe` names one of\n * the same type, which replaces it (#270 R2.1).\n */\n\nimport type { BuiltinSubType, Dispose, TimerSub } from \"../pure/core\";\n\n/**\n * `timer`: dispatch `deps.msg` once, `deps.ms` after the Sub starts. The engine\n * restarts it when `ms` or `msg` changes and clears it when the Sub stops.\n */\nfunction timer<M>(\n sub: TimerSub<M>,\n _ctx: unknown,\n dispatch: (msg: M) => void,\n): Dispose {\n const handle = setTimeout(() => dispatch(sub.deps.msg), sub.deps.ms);\n return () => clearTimeout(handle);\n}\n\n/** Every built-in runner, keyed by the Sub type it runs. */\nexport const builtinRunners = { timer } as const satisfies Record<\n BuiltinSubType,\n unknown\n>;\n","/**\n * @demlik/tea runtime — `run`: boot a machine and drive its serial dispatch loop\n * on Promises; `driveToDone`: run one to its terminal State.\n *\n * `run` is assembly. The loop itself is `src/internal/engine/loop.ts` — a small\n * core with no special case for any built-in, shared with the Effect engine —\n * and every built-in is an extension of it (`src/internal/engine/builtins.ts`),\n * built from `run`'s options in the order both engines share.\n */\n\nimport { builtinExtensions } from \"../internal/engine/builtins\";\nimport {\n type LiveWorkProbe,\n type LoopHandler,\n type LoopRunner,\n liveWork,\n startLoop,\n} from \"../internal/engine/loop\";\nimport type { Interpret, Machine, RunHandlers, Sub } from \"../pure/core\";\nimport { applyCell, type Cmd, subEntriesOf } from \"../pure/core\";\nimport type {\n BootingRuntime,\n CtxArg,\n OnError,\n Store,\n Supervision,\n TelemetrySink,\n} from \"../runtime-types\";\nimport { DriveFailedError, DriveStalledError } from \"../runtime-types\";\nimport { builtinRunners } from \"./builtin-runners\";\n\n// === run ===\n//\n// Returns a `BootingRuntime<S, M>` synchronously; boot runs as the FIRST entry\n// on the serial dispatch tail, awaited implicitly by every public method.\n//\n// Save-then-effects ordering is structural: every transition mutates state,\n// awaits `store.save(newState)`, then reconciles subscriptions, then runs\n// `interpret` for emitted cmds, then fires external listeners. A throw in any\n// effect phase leaves the persisted state ahead of the host's belief about\n// what executed — the Railway discipline (`tryInterpret` in handlers) makes\n// that safe in practice.\nexport function run<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n E extends { type: string } = never,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n // `ctx` is a plain object (ADR 0020): tea does no dependency injection, so\n // a handler's services are whatever the host put on it. The Cmd handlers\n // arrive here, beside the machine and never on it (#251 R1.1). `NoInfer`\n // keeps them a check against the machine's `M` / `C` rather than a second\n // inference site that could widen either.\n opts: CtxArg<Ctx> &\n NoInfer<RunHandlers<M, C, U, Ctx>> & {\n store?: Store<S>;\n onError?: OnError;\n /**\n * The clock that stamps `at` on a `Cmd.define`d effect's settled Msg at\n * the interpret edge, and on each `telemetry` event. Defaults to\n * `Date.now`. Inject a fixed one for a deterministic run; a `replay` log\n * carries its own `at`s and never reads this.\n */\n clock?: () => number;\n /**\n * The SEMANTIC event projector. Maps one APPLIED transition `(msg, state)`\n * to zero-or-more public events of `E`; `[]` skips the transition. Maps the\n * machine's PRIVATE Msg vocabulary to NAMED events — the private names never\n * reach `on`'s `E` surface. Omit → `E = never` and `on` is uncallable. PURE.\n */\n events?: (msg: M, state: S) => readonly E[];\n /**\n * Declared policy for a reducer (`update`) throw. Always surfaced via\n * `onError` (`phase: \"reduce\"`); the strategy decides what the runtime does\n * next. Defaults to `\"stop\"`. See `Supervision`.\n */\n supervision?: Supervision<S, M>;\n /**\n * The run-terminality predicate — makes the run's outcome first-class. Fed to\n * `Runtime.result()` and `Runtime.done()`. PURE. Omit → never terminal.\n */\n terminal?: (state: S) => boolean;\n /**\n * A sink handed `{ seq, msgType, at }` for every applied transition —\n * the observe-only telemetry `withTelemetry` used to add by wrapping the\n * machine (#268). Fire-and-forget; see {@link TelemetrySink}.\n */\n telemetry?: TelemetrySink;\n /**\n * How long `stop()` waits for teardown work that returned a Promise (an\n * async `release` in `defineManagedResource`, an async Sub cleanup) before\n * giving up on it. Defaults to 5_000ms.\n *\n * `stop()` awaits those disposals so a host doing\n * `await runtime.stop(); env.evict()` cannot drop the isolate mid-release —\n * the leak the managed-resource battery exists to prevent, relocated to\n * shutdown. The bound is what keeps a release that never settles from\n * hanging the host: on expiry `stop()` reports a `DisposeTimeoutNotice`\n * (warn-only, like every `RuntimeDiscardNotice`) and resolves anyway,\n * because `stop()` resolving is a contract.\n */\n disposeTimeoutMs?: number;\n /**\n * Iteration cap for `idle()`'s quiescence wait. Defaults to 100_000. Test\n * seam only. Production code must not set it.\n *\n * @internal test-only\n */\n __idleCap?: number;\n },\n): BootingRuntime<S, M, E> {\n // `interpret` is optional when `C extends Cmd<never>`; default a missing map\n // to `{}` — a Cmd with no handler is skipped, invariant 6's forward progress\n // for a miswired consumer.\n const interpretMap = ((opts as { interpret?: Interpret<M, C, Ctx> })\n .interpret ?? {}) as Readonly<Record<string, LoopHandler<M> | undefined>>;\n\n // The runner for a Sub type: the one handed to `run` in `subscribe`, else\n // the engine's built-in of that name (#270 — a user entry overrides a\n // built-in). Looked up per start rather than merged once, so a handler table\n // that resolves its cells lazily (`/react` reads the latest render's) is\n // honoured.\n const subscribeTable = (opts as { subscribe?: unknown }).subscribe as\n | Readonly<Record<string, LoopRunner<Ctx, M> | undefined>>\n | undefined;\n const builtins = builtinRunners as Readonly<\n Record<string, LoopRunner<Ctx, M> | undefined>\n >;\n\n const handle = startLoop<S, M, C, Ctx>({\n init: machine.init,\n reduce: (state, msg) => applyCell<S, M, C>(machine, state, msg),\n subs: subEntriesOf<S>(machine),\n runnerFor: (type) => subscribeTable?.[type] ?? builtins[type],\n handlerFor: (type) => interpretMap[type],\n store: opts.store,\n // `ctx` is conditionally optional (see `CtxArg`); default the nullish case\n // to `{}` so the handler ctx and `init(loaded, ctx)` get a value.\n ctx: (opts.ctx ?? {}) as Ctx,\n onError: opts.onError,\n disposeTimeoutMs: opts.disposeTimeoutMs ?? 5_000,\n idleCap: opts.__idleCap ?? 100_000,\n extensions: builtinExtensions<S, M, C, E>(machine, opts),\n });\n return handle as unknown as BootingRuntime<S, M, E> & LiveWorkProbe;\n}\n\n/**\n * Is the runtime provably out of ways to transition on its own? True only when\n * the handle carries the `liveWork` read and it counts zero live Subs and zero\n * in-flight Cmds. A handle without the read (a wrapper that rebuilt the object\n * without spreading it) cannot be proven stalled, so the drive keeps waiting —\n * the pre-#68 contract, never a false stall.\n */\nfunction isStalled(runtime: object): boolean {\n const probe = (runtime as Partial<LiveWorkProbe>)[liveWork];\n if (probe === undefined) return false;\n const live = probe();\n return live.subs === 0 && live.cmds === 0;\n}\n\n/**\n * The outside-in stop, as a PAIR. A signal is only cancellation if something can\n * turn \"aborted\" into a transition this machine understands, and the kernel has\n * no built-in cancel Msg any more than it has a built-in terminal set — so the\n * `signal` and the `cancel` that reads it are one option, present together or\n * absent together. A discriminated union rather than two optional fields, on the\n * same reasoning as `AgentCompactionConfig`: a signal with no `cancel` is a stop\n * button wired to nothing, and that is a state worth making unrepresentable\n * rather than checking for.\n */\ntype DriveCancellation<S, M> =\n | {\n readonly signal?: undefined;\n readonly cancel?: undefined;\n }\n | {\n /**\n * Abort the run through this signal. On abort the drive dispatches\n * `cancel`'s Msg and resolves on the State that lands — it does NOT reject,\n * and a cancellation is never a `DriveFailedError`. Already aborted when\n * the drive is called → the cancel Msg goes in place of `start`, so the\n * run ends without the start's effects ever firing.\n */\n readonly signal: AbortSignal;\n /**\n * The Msg that settles this machine as cancelled, chosen off the current\n * State exactly as `start` is. PURE. It must land a State `isTerminal`\n * holds for — the drive resolves on the terminal predicate, not on the\n * abort — and that State should be DURABLE, or a reload resumes the run\n * its caller stopped.\n */\n readonly cancel: (state: S) => M;\n };\n\n/** Options for `driveToDone`. */\nexport type DriveToDoneOptions<\n S,\n M extends { type: string } = never,\n> = DriveCancellation<S, M> & {\n /**\n * Marks a terminal State as a FAILURE. A State this holds for ends the drive\n * like any terminal one — the runtime is stopped — but the drive REJECTS with\n * `DriveFailedError` carrying it instead of resolving. A failed State is\n * terminal by definition; it need not also satisfy `isTerminal`. Omit → the\n * drive never rejects on State, only on a runtime error.\n */\n readonly failed?: (state: S) => boolean;\n};\n\n/**\n * Drive a machine from `start` to its terminal State in one call, then tear the\n * runtime down. The one-shot shape every \"run this machine to done\" test and\n * caller boundary otherwise hand-wires as six steps — `await ready`, `observe`,\n * park a promise, `dispatch(start)`, `getState()`, `stop()` — with the observer\n * leak and the forgotten `stop` those six steps invite.\n *\n * Takes the handle `run()` returns (a `Runtime` extends it, so a booted one is\n * accepted too), awaits `ready`, dispatches `start`, and resolves with the first\n * State for which `isTerminal` holds. A machine that boots already terminal (a\n * rehydrated finished run) resolves on its boot State and `start` is never\n * dispatched — a finished run has nothing to set in motion. `start` may be a\n * function of the boot State instead of a Msg: a machine rehydrated MID-run\n * needs its resume Msg, not its start Msg, and only the boot State says which\n * (the agent's `agent_boot` vs `agent_start` is the case in point). The\n * observer is detached and `stop()` awaited on EVERY exit: resolve, `failed`,\n * a stall, a boot or dispatch throw, the quiescence cap.\n *\n * Rejections are typed, never silent:\n * - `DriveFailedError<S>` when `opts.failed` marks the final State — the State\n * rides on `error.state`.\n * - `DriveStalledError<S>` when `start`'s follow-up chain quiesces on a State\n * that is neither terminal nor `failed` and the runtime has no live Sub and\n * no Cmd in flight — nothing inside it can deliver another transition, so\n * waiting would be a hang with the runtime leaked. The stalled State rides\n * on `error.state`. A machine that CAN still move is not stalled: a live Sub\n * that delivers the terminal Msg after the dispatch\n * quiesces keeps the drive waiting, and it resolves on that State. What the\n * runtime cannot see — a dispatch from outside it after quiescence — does\n * not count as live; a machine that depends on one declares it as a Sub.\n * - `QuiescenceTimeoutError` when `start`'s follow-up chain never settles —\n * the SAME cap `dispatch` and `idle()` already reject on (invariant 6), not a\n * second clock. A livelocking machine surfaces as its own failure class.\n * - the boot error, or whatever `dispatch(start)` rejects with, otherwise.\n *\n * `isTerminal` is caller-supplied, exactly as `run()`'s `terminal` option is —\n * the kernel has no built-in terminal-set concept and this does not add one.\n *\n * Cancellation is the same story: `{ signal, cancel }` stops the drive from\n * outside, and it settles the run rather than escaping it. On abort the drive\n * dispatches `cancel`'s Msg and RESOLVES on the terminal State that transition\n * lands — no rejection, no `DriveFailedError`, no `AbortError`. That is the whole\n * point of routing a stop through the Model: the outcome is durable, so a killed\n * process resumes reading a run that ended instead of restarting one someone\n * stopped. A cancel that THROWS is the one abort that rejects: it lands\n * no State, so its error is what the drive has to report — and that holds\n * whether the `cancel` function throws synchronously or the fold it produced\n * throws later, since both leave the run with no State to settle on. A State the\n * caller's `failed` predicate marks rejects on an abort exit exactly as it does\n * anywhere else. In-flight effects are not recalled — a promise cannot be cancelled —\n * so they settle to their own end and `stop()` drains them as it always did;\n * keeping their results off the public channels is the machine's own business.\n * Omit the pair and every path here behaves exactly as it did before.\n *\n * @param handle the handle `run(machine, opts)` returned.\n * @param start the Msg that sets the run in motion, or a function choosing\n * it off the boot State. PURE.\n * @param isTerminal the terminal predicate over the machine's State. PURE.\n * @param opts an optional `failed` predicate and an optional\n * `{ signal, cancel }` pair (see {@link DriveToDoneOptions}).\n */\nexport async function driveToDone<\n S,\n M extends { type: string },\n E extends { type: string } = never,\n>(\n handle: BootingRuntime<S, M, E>,\n start: M | ((booted: S) => M),\n isTerminal: (state: S) => boolean,\n opts: DriveToDoneOptions<S, M> = {},\n): Promise<S> {\n const failed = opts.failed ?? (() => false);\n const settles = (state: S): boolean => isTerminal(state) || failed(state);\n let detach: (() => void) | undefined;\n let unlisten: (() => void) | undefined;\n try {\n const runtime = await handle.ready;\n // A run that rehydrated already terminal has nothing to start: resolve on\n // the boot State without dispatching, so `start`'s Cmds are never left in\n // flight for `stop()` to discard. Read BEFORE the signal so a run that\n // already ended keeps the outcome it ended on — an abort arriving after the\n // fact does not restate a finished run as cancelled.\n const booted = runtime.getState();\n // The one exit check every branch below shares: a State the caller's\n // `failed` predicate marks rejects, wherever the drive reached it.\n const outcome = (state: S): S => {\n if (failed(state)) throw new DriveFailedError(state);\n return state;\n };\n if (settles(booted)) return outcome(booted);\n // Attach BEFORE dispatching so a terminal transition landing inside the\n // start dispatch is caught.\n const terminal = new Promise<S>((resolve) => {\n detach = runtime.observe((_msg, state) => {\n if (settles(state)) resolve(state);\n });\n });\n // An abort mid-run enqueues the cancel Msg on the SAME serial tail every\n // other dispatch uses, so it lands between transitions rather than inside\n // one. Work already enqueued when the abort arrives still folds; work the\n // machine would have enqueued after it does not, because the cancelled State\n // is terminal and its verbs stop emitting. `dispatchOnce` (not `dispatch`)\n // because the cancel has no follow-up chain to drain. Its rejection is NOT\n // swallowed: only the cancel fold can land the terminal State on an abort,\n // so a cancel that throws leaves `terminal` with nothing to resolve it and\n // the drive would park forever (#170). `cancelFailed` carries that rejection\n // into whichever race the branch below awaits.\n let cancelFailed: Promise<never> | undefined;\n if (opts.signal !== undefined) {\n const { signal, cancel } = opts;\n let onCancelError!: (error: unknown) => void;\n cancelFailed = new Promise<never>((_resolve, reject) => {\n onCancelError = reject;\n });\n // The try covers the SYNCHRONOUS call site too, not just the dispatch it\n // returns: a caller-supplied `cancel` that throws before it ever produces\n // a Msg would otherwise escape this listener, and a listener's throw goes\n // nowhere — no caller frame to catch it, `terminal` still unresolved, the\n // drive parked exactly as #170 described (#174). Both throws are the same\n // fact — the cancel fold could not land a State — so both take the one\n // route out through `onCancelError`.\n const onAbort = () => {\n try {\n runtime.dispatchOnce(cancel(runtime.getState())).catch(onCancelError);\n } catch (error) {\n onCancelError(error);\n }\n };\n // Already aborted → cancel INSTEAD of starting, so `start`'s effects (a\n // model call, for the agent) never fire at all.\n if (signal.aborted) {\n onAbort();\n return outcome(await Promise.race([terminal, cancelFailed]));\n }\n signal.addEventListener(\"abort\", onAbort, { once: true });\n unlisten = () => signal.removeEventListener(\"abort\", onAbort);\n }\n // The race lets a dispatch rejection (reducer / interpret throw, the\n // quiescence cap) surface here instead of floating as an unhandled rejection\n // while the terminal await parks forever.\n const msg = typeof start === \"function\" ? start(booted) : start;\n const started = runtime.dispatch(msg).then(() => {\n // Quiesced. A settling State already went through the observer, so\n // `terminal` is resolved; otherwise the wait is legitimate only while the\n // runtime itself can still transition. With no live Sub and no Cmd in\n // flight nothing is coming, and the wait becomes the hang #68 names.\n const parked = runtime.getState();\n if (!settles(parked) && isStalled(runtime)) {\n throw new DriveStalledError(parked);\n }\n return terminal;\n });\n // `cancelFailed` rides the race for the mid-run abort: `started` cannot\n // carry the cancel's rejection, having usually resolved long before the\n // abort arrives.\n const racers: Promise<S>[] = [terminal, started];\n if (cancelFailed !== undefined) racers.push(cancelFailed);\n return outcome(await Promise.race(racers));\n } finally {\n detach?.();\n // The abort listener outlives this call unless it is removed: a signal a\n // caller reuses across runs would otherwise accumulate one dispatch per run\n // it has already finished.\n unlisten?.();\n // `stop()` resolves by contract, so awaiting it here cannot mask the error\n // a rejecting branch above is carrying.\n await handle.stop();\n }\n}\n"]}