@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
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/internal/flow/fan-out/index.ts"],"names":[],"mappings":"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","file":"chunk-MYXFNWAJ.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 * `handlers(ports)` is the I/O splice: it wires the runtime side that actually\n * runs each launched `of(item)` Cmd. Because `of` already produces a fully\n * caller-defined Cmd, fan-out does NOT own the interpret handler for it — the\n * consumer's own `interpret` performs the effect and routes Ok/Err back to the\n * `itemOk` / `itemErr` verbs (the same shape resilient-fetch uses with\n * `tryInterpret`). `handlers` therefore exposes the one piece fan-out CAN\n * pre-wire generically: emitting the gathered batch onto a typed Port when the\n * batch completes, for consumers that observe completion out-of-band rather\n * than 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 * `handlers` 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 `handlers` 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 `handlers(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 `handlers` 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 handlers(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 handlers,\n };\n}\n"]}