@demlik/tea 0.1.0 → 0.1.1

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 (238) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/index.d.ts +907 -0
  3. package/dist/agent/index.js +20 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/authed-call/index.d.ts +119 -0
  6. package/dist/authed-call/index.js +148 -0
  7. package/dist/authed-call/index.js.map +1 -0
  8. package/dist/await-terminal/index.d.ts +99 -0
  9. package/dist/await-terminal/index.js +75 -0
  10. package/dist/await-terminal/index.js.map +1 -0
  11. package/dist/batch-window/index.d.ts +259 -0
  12. package/dist/batch-window/index.js +43 -0
  13. package/dist/batch-window/index.js.map +1 -0
  14. package/dist/cache/index.d.ts +2 -2
  15. package/dist/cache/index.js +2 -50
  16. package/dist/cache/index.js.map +1 -1
  17. package/dist/{chunk-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
  18. package/dist/chunk-2WZHS4P2.js.map +1 -0
  19. package/dist/chunk-2YOE5PIS.js +45 -0
  20. package/dist/chunk-2YOE5PIS.js.map +1 -0
  21. package/dist/chunk-3545BOYL.js +65 -0
  22. package/dist/chunk-3545BOYL.js.map +1 -0
  23. package/dist/chunk-5VNWZADP.js +49 -0
  24. package/dist/chunk-5VNWZADP.js.map +1 -0
  25. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  26. package/dist/chunk-7FYQGDMF.js.map +1 -0
  27. package/dist/chunk-7VEG4KHB.js +202 -0
  28. package/dist/chunk-7VEG4KHB.js.map +1 -0
  29. package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
  30. package/dist/chunk-A56GNPI7.js.map +1 -0
  31. package/dist/chunk-BNJYE4AU.js +157 -0
  32. package/dist/chunk-BNJYE4AU.js.map +1 -0
  33. package/dist/chunk-BYQUGDD3.js +16 -0
  34. package/dist/chunk-BYQUGDD3.js.map +1 -0
  35. package/dist/chunk-D5EQDLDV.js +186 -0
  36. package/dist/chunk-D5EQDLDV.js.map +1 -0
  37. package/dist/chunk-EDGDR5XX.js +60 -0
  38. package/dist/chunk-EDGDR5XX.js.map +1 -0
  39. package/dist/chunk-EIZJTYKR.js +93 -0
  40. package/dist/chunk-EIZJTYKR.js.map +1 -0
  41. package/dist/chunk-EQRUAP2A.js +56 -0
  42. package/dist/chunk-EQRUAP2A.js.map +1 -0
  43. package/dist/chunk-FH5RUIIL.js +61 -0
  44. package/dist/chunk-FH5RUIIL.js.map +1 -0
  45. package/dist/chunk-HJO6VNQI.js +15 -0
  46. package/dist/chunk-HJO6VNQI.js.map +1 -0
  47. package/dist/chunk-IFBQJYGI.js +132 -0
  48. package/dist/chunk-IFBQJYGI.js.map +1 -0
  49. package/dist/chunk-IN33Q4VJ.js +52 -0
  50. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  51. package/dist/chunk-JELSZN25.js +47 -0
  52. package/dist/chunk-JELSZN25.js.map +1 -0
  53. package/dist/chunk-K575PT4S.js +58 -0
  54. package/dist/chunk-K575PT4S.js.map +1 -0
  55. package/dist/chunk-LEC4U6CX.js +91 -0
  56. package/dist/chunk-LEC4U6CX.js.map +1 -0
  57. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  58. package/dist/chunk-NSDFA6M6.js.map +1 -0
  59. package/dist/chunk-QGZW4W3E.js +20 -0
  60. package/dist/chunk-QGZW4W3E.js.map +1 -0
  61. package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
  62. package/dist/chunk-RGTPPF4U.js.map +1 -0
  63. package/dist/chunk-RVJFGRGK.js +222 -0
  64. package/dist/chunk-RVJFGRGK.js.map +1 -0
  65. package/dist/chunk-RVU3RBE6.js +31 -0
  66. package/dist/chunk-RVU3RBE6.js.map +1 -0
  67. package/dist/chunk-SC3FAFZN.js +53 -0
  68. package/dist/chunk-SC3FAFZN.js.map +1 -0
  69. package/dist/chunk-SINXE47Y.js +55 -0
  70. package/dist/chunk-SINXE47Y.js.map +1 -0
  71. package/dist/chunk-TAO4YOLO.js +87 -0
  72. package/dist/chunk-TAO4YOLO.js.map +1 -0
  73. package/dist/chunk-TC4HWABA.js +506 -0
  74. package/dist/chunk-TC4HWABA.js.map +1 -0
  75. package/dist/chunk-TGG26XMK.js +52 -0
  76. package/dist/chunk-TGG26XMK.js.map +1 -0
  77. package/dist/chunk-UV6UWGEO.js +147 -0
  78. package/dist/chunk-UV6UWGEO.js.map +1 -0
  79. package/dist/chunk-UYWUVEP6.js +158 -0
  80. package/dist/chunk-UYWUVEP6.js.map +1 -0
  81. package/dist/chunk-WUV3PSCZ.js +54 -0
  82. package/dist/chunk-WUV3PSCZ.js.map +1 -0
  83. package/dist/chunk-XRXKH7KA.js +268 -0
  84. package/dist/chunk-XRXKH7KA.js.map +1 -0
  85. package/dist/circuit-breaker/index.js +1 -51
  86. package/dist/circuit-breaker/index.js.map +1 -1
  87. package/dist/core-CymXZCeY.d.ts +178 -0
  88. package/dist/deadline/index.d.ts +24 -5
  89. package/dist/deadline/index.js +4 -18
  90. package/dist/deadline/index.js.map +1 -1
  91. package/dist/debounce/index.js +1 -54
  92. package/dist/debounce/index.js.map +1 -1
  93. package/dist/devtools/index.d.ts +1 -1
  94. package/dist/devtools/index.js +18 -6
  95. package/dist/devtools/index.js.map +1 -1
  96. package/dist/do/index.d.ts +1732 -92
  97. package/dist/do/index.js +677 -31
  98. package/dist/do/index.js.map +1 -1
  99. package/dist/durable-effects-2MDxDs45.d.ts +180 -0
  100. package/dist/extension/index.d.ts +31 -13
  101. package/dist/extension/index.js +5 -3
  102. package/dist/extension/index.js.map +1 -1
  103. package/dist/extension/react.js +8 -3
  104. package/dist/extension/react.js.map +1 -1
  105. package/dist/extension/subs/index.d.ts +2 -2
  106. package/dist/extension/subs/index.js.map +1 -1
  107. package/dist/extension/test-utils.js +11 -3
  108. package/dist/extension/test-utils.js.map +1 -1
  109. package/dist/fan-out/index.d.ts +239 -0
  110. package/dist/fan-out/index.js +5 -0
  111. package/dist/fan-out/index.js.map +1 -0
  112. package/dist/idempotency/adapter.d.ts +62 -0
  113. package/dist/idempotency/adapter.js +4 -0
  114. package/dist/idempotency/adapter.js.map +1 -0
  115. package/dist/idempotency/index.js +1 -56
  116. package/dist/idempotency/index.js.map +1 -1
  117. package/dist/idempotent-intake/index.d.ts +186 -0
  118. package/dist/idempotent-intake/index.js +7 -0
  119. package/dist/idempotent-intake/index.js.map +1 -0
  120. package/dist/index.d.ts +341 -191
  121. package/dist/index.js +2 -1
  122. package/dist/llm-call/index.d.ts +309 -0
  123. package/dist/llm-call/index.js +14 -0
  124. package/dist/llm-call/index.js.map +1 -0
  125. package/dist/machine-viz/index.d.ts +1 -1
  126. package/dist/machine-viz/index.js +1 -1
  127. package/dist/machine-viz/index.js.map +1 -1
  128. package/dist/mem/index.d.ts +1 -0
  129. package/dist/monitored-run/index.d.ts +253 -0
  130. package/dist/monitored-run/index.js +10 -0
  131. package/dist/monitored-run/index.js.map +1 -0
  132. package/dist/node/index.d.ts +5 -5
  133. package/dist/node/index.js.map +1 -1
  134. package/dist/paginated-walk/index.d.ts +234 -0
  135. package/dist/paginated-walk/index.js +15 -0
  136. package/dist/paginated-walk/index.js.map +1 -0
  137. package/dist/paginator/index.d.ts +271 -0
  138. package/dist/paginator/index.js +3 -0
  139. package/dist/paginator/index.js.map +1 -0
  140. package/dist/parity/index.d.ts +86 -0
  141. package/dist/parity/index.js +77 -0
  142. package/dist/parity/index.js.map +1 -0
  143. package/dist/pbt/arbitraries/index.d.ts +1 -1
  144. package/dist/pbt/arbitraries/index.js +1 -1
  145. package/dist/pbt/index.d.ts +1 -1
  146. package/dist/pbt/index.js +5 -4
  147. package/dist/pbt/index.js.map +1 -1
  148. package/dist/pbt/runners/index.d.ts +1 -1
  149. package/dist/pbt/runners/index.js +3 -2
  150. package/dist/poller/index.d.ts +288 -0
  151. package/dist/poller/index.js +79 -0
  152. package/dist/poller/index.js.map +1 -0
  153. package/dist/prediction/index.d.ts +109 -0
  154. package/dist/prediction/index.js +4 -0
  155. package/dist/prediction/index.js.map +1 -0
  156. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  157. package/dist/pure/index.d.ts +2 -0
  158. package/dist/pure/index.js +4 -0
  159. package/dist/pure/index.js.map +1 -0
  160. package/dist/raft/demo.d.ts +105 -0
  161. package/dist/raft/demo.js +172 -0
  162. package/dist/raft/demo.js.map +1 -0
  163. package/dist/raft/index.d.ts +548 -0
  164. package/dist/raft/index.js +7 -0
  165. package/dist/raft/index.js.map +1 -0
  166. package/dist/raft/sim.d.ts +222 -0
  167. package/dist/raft/sim.js +8 -0
  168. package/dist/raft/sim.js.map +1 -0
  169. package/dist/raft/timeline.d.ts +46 -0
  170. package/dist/raft/timeline.js +62 -0
  171. package/dist/raft/timeline.js.map +1 -0
  172. package/dist/rate-limit/index.js +1 -43
  173. package/dist/rate-limit/index.js.map +1 -1
  174. package/dist/react/index.d.ts +2 -1
  175. package/dist/react/index.js +39 -20
  176. package/dist/react/index.js.map +1 -1
  177. package/dist/reconciler/index.d.ts +192 -0
  178. package/dist/reconciler/index.js +166 -0
  179. package/dist/reconciler/index.js.map +1 -0
  180. package/dist/recorder/index.d.ts +7 -3
  181. package/dist/recorder/index.js +1 -155
  182. package/dist/recorder/index.js.map +1 -1
  183. package/dist/resilient-call/index.d.ts +251 -0
  184. package/dist/resilient-call/index.js +13 -0
  185. package/dist/resilient-call/index.js.map +1 -0
  186. package/dist/retry-backoff/index.d.ts +42 -5
  187. package/dist/retry-backoff/index.js +1 -35
  188. package/dist/retry-backoff/index.js.map +1 -1
  189. package/dist/saga/index.d.ts +225 -0
  190. package/dist/saga/index.js +150 -0
  191. package/dist/saga/index.js.map +1 -0
  192. package/dist/snapshot/index.d.ts +334 -0
  193. package/dist/snapshot/index.js +5 -0
  194. package/dist/snapshot/index.js.map +1 -0
  195. package/dist/subs/index.d.ts +48 -10
  196. package/dist/subs/index.js +1 -1
  197. package/dist/testing/index.d.ts +107 -2
  198. package/dist/testing/index.js +204 -6
  199. package/dist/testing/index.js.map +1 -1
  200. package/dist/throttle/index.js +1 -52
  201. package/dist/throttle/index.js.map +1 -1
  202. package/dist/throttled-input/index.d.ts +372 -0
  203. package/dist/throttled-input/index.js +88 -0
  204. package/dist/throttled-input/index.js.map +1 -0
  205. package/dist/token-refresh/index.d.ts +215 -0
  206. package/dist/token-refresh/index.js +6 -0
  207. package/dist/token-refresh/index.js.map +1 -0
  208. package/dist/trace-replay/index.d.ts +11 -2
  209. package/dist/trace-replay/index.js +3 -55
  210. package/dist/trace-replay/index.js.map +1 -1
  211. package/dist/{types-CwBC5wMn.d.ts → types-N9LZbdnr.d.ts} +1 -1
  212. package/dist/with-deadline/index.d.ts +196 -0
  213. package/dist/with-deadline/index.js +130 -0
  214. package/dist/with-deadline/index.js.map +1 -0
  215. package/dist/with-resilience/index.d.ts +205 -0
  216. package/dist/with-resilience/index.js +274 -0
  217. package/dist/with-resilience/index.js.map +1 -0
  218. package/dist/with-telemetry/index.d.ts +149 -0
  219. package/dist/with-telemetry/index.js +95 -0
  220. package/dist/with-telemetry/index.js.map +1 -0
  221. package/dist/work-queue/adapter.d.ts +82 -0
  222. package/dist/work-queue/adapter.js +4 -0
  223. package/dist/work-queue/adapter.js.map +1 -0
  224. package/dist/work-queue/index.d.ts +25 -11
  225. package/dist/work-queue/index.js +1 -46
  226. package/dist/work-queue/index.js.map +1 -1
  227. package/dist/work-queue/ops.d.ts +69 -0
  228. package/dist/work-queue/ops.js +3 -0
  229. package/dist/work-queue/ops.js.map +1 -0
  230. package/dist/workflow/index.d.ts +444 -0
  231. package/dist/workflow/index.js +225 -0
  232. package/dist/workflow/index.js.map +1 -0
  233. package/package.json +162 -25
  234. package/dist/chunk-CMG2KMWX.js.map +0 -1
  235. package/dist/chunk-PYBQREFU.js.map +0 -1
  236. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  237. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  238. package/dist/chunk-ZRUST6BK.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/with-resilience/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/with-resilience — the INTERCEPTING wrapper of the wrapper tier.\n *\n * `withResilience(base, config)` is a **pure function over machine data**: it\n * takes any `Machine<S, M, C, U, Ctx>`, a chosen TARGET base Cmd type, and a\n * resilience knob, and returns a NEW `Machine` over the composed Model\n * `{ base, $resilience }`. You `run()` / `replay()` the result exactly like any\n * other machine — there is no new runtime, no privileged interception path.\n *\n * This is the HARDEST wrapper. `withTelemetry` only OBSERVES; this one\n * INTERCEPTS. It bolts the resilient-call concern (cache → circuit-breaker →\n * rate-limit → exponential-backoff retry, with a deadline cap) onto a chosen\n * base Cmd WITHOUT the base authoring any of it. The base keeps emitting its\n * plain effect Cmd; the wrapper diverts each one through the resilience gate.\n *\n * ## Retag, never swallow (rule 2 — the whole danger)\n *\n * The merged `update` delegates to `base.update`, then PARTITIONS the base Cmds\n * by type:\n *\n * - A NON-target base Cmd passes through UNCHANGED — byte-identical, in the\n * same slot. The wrapper does not touch effects it was not asked to harden.\n * - A TARGET base Cmd is RETAGGED: it becomes the `input` payload of a\n * `$resilience:run` carrier Cmd (when the gate admits it), OR its gating is\n * RECORDED as a `$resilience:*` decision in the SAME transition (cache hit,\n * circuit-open fast-fail, rate-limited backoff). It is NEVER emitted raw,\n * and it NEVER vanishes without a `$resilience:*` Cmd recording where it\n * went. That recorded divergence is exactly what the conformance gate's\n * `intercepting` option verifies.\n *\n * The resilient-call result Msgs route back through the merged `update`:\n * `$resilience:ok` → `succeed` (close the breaker, fill the cache, reset the\n * retry run), `$resilience:err` → `fail` (trip the breaker, then back off or\n * settle), `$resilience:timer` → `onTimer` (a retry timer re-runs the gate; a\n * deadline timer settles the call failed). Every decision is a Msg/Cmd through\n * the reducer + log — \"why did this fire\" is answerable from the log alone.\n *\n * ## The slice + verbs are REUSED, not reinvented\n *\n * The `$resilience` slice IS resilient-call's `ResilientState<I, R>` (per-key\n * call / retry / circuit / bucket / cache), and the gate / backoff / breaker\n * logic IS its `attempt` / `succeed` / `fail` / `onTimer` verbs + `subs`. The\n * wrapper only ADAPTS the boundary: the verb's `input` is the ORIGINAL base\n * Cmd, the verb's `resilient_run` carrier is renamed to `$resilience:run`, and\n * the sub / timer ids are re-keyed into the `$resilience:` namespace (rule 4).\n *\n * ## The 5 rules it satisfies\n *\n * 1. **Named, serializable slice.** `model.$resilience` is resilient-call's\n * plain-data `ResilientState` — JSON-serializable (the deadline error is a\n * `{_tag,...}` sentinel, never a `new Error`), never a closure.\n * 2. **Retag-and-re-emit, never swallow.** See above — a target base Cmd is\n * always inside a `$resilience:run` inner payload or recorded by a\n * `$resilience:*` divergence Cmd in the same transition.\n * 3. **Every decision is a Msg/Cmd.** Gate / retry-schedule / fast-fail /\n * settle are all `$resilience:*` Cmds and Msgs through the merged `update`.\n * 4. **Subs merge by id.** Base subs ⧺ resilient-call retry/deadline timers,\n * re-keyed `$resilience:retry:{key}` / `$resilience:deadline:{key}`.\n * 5. **Clock out of update.** Backoff jitter RNG is injected at construction;\n * the only clock read is `Date.now()` at the interpret boundary (the\n * `$resilience:run` handler stamps the ok/err Msg `at`) and in the timer\n * Sub. The merged `update` is PURE — `at` arrives as Msg data.\n */\n\nimport { type DeadlineSub, subscribeDeadline } from \"../deadline\";\nimport type {\n Cmd,\n Interpret,\n Machine,\n Reducer,\n Sub,\n UpdateForm,\n} from \"../index\";\nimport { formOf, subId, tryInterpret } from \"../index\";\nimport { MsgType } from \"../protocol\";\nimport {\n createResilientCall,\n type ResilientConfig,\n type ResilientState,\n} from \"../resilient-call\";\n\n// ===========================================================================\n// Config — the target + the resilience knob + an optional key deriver.\n// ===========================================================================\n\n/**\n * The `withResilience` knob. Extends resilient-call's `ResilientConfig` (every\n * brick optional — omit a brick, omit its gate) with the two fields the wrapper\n * needs that the bare knob does not:\n *\n * - `target` — the `Cmd.type` discriminant of the ONE base Cmd this wrapper\n * hardens. Every other base Cmd passes through untouched.\n * - `keyOf` — OPTIONAL. Derives the per-call resilience `key` from the target\n * base Cmd, so distinct logical calls (distinct urls, distinct ids) get\n * independent retry / cache state. Pure — it runs inside the merged\n * `update`, so it must NOT read the clock / RNG. Defaults to a single\n * shared key (`target`), i.e. one logical call at a time.\n */\nexport interface ResilienceConfig<TC extends Cmd, TM extends { type: string }>\n extends ResilientConfig {\n /** The base `Cmd.type` to make resilient. */\n readonly target: TC[\"type\"];\n /** Optional per-call key deriver. Pure. Defaults to the target type itself. */\n readonly keyOf?: (cmd: TC) => string;\n /**\n * Extracts the real wall-clock time (ms) the TRIGGERING base Msg carries as\n * data — the time-as-data convention `resilient-call` expects on `attempt`.\n *\n * The merged `update` is PURE: it cannot read `Date.now()` (rule 5). The COLD\n * attempt's gate (the very first `attempt` for a target Cmd in a transition)\n * still needs a real instant — the breaker compares `at` against its recorded\n * `openedAtMs`, the cache against its write `at`, the bucket against its last\n * refill `at`, and the deadline cap anchors at `at + deadline.ms`. Fabricating\n * `at = 0` is the betrayal: a cold attempt after a real-time breaker trip then\n * computes cooldown = `0 - realTripTime < 0` forever, so the breaker NEVER\n * recovers, and a `0`-anchored deadline brick fires immediately.\n *\n * So the base Msg that drove this transition MUST carry its own wall time as\n * data, and `at` reads it off that Msg. The merged update passes\n * `at(currentBaseMsg)` into `rc.attempt` for every target Cmd that transition\n * emitted — real time enters the cold gate as Msg data, never `0`.\n *\n * REQUIRED whenever any time-sensitive brick (`circuit`, `rateLimit`, `cache`,\n * `deadline`) is configured — `withResilience` THROWS at construction if such\n * a brick is set and `at` is absent (errors are data: the cold gate needs real\n * time, refuse to build a breaker that can never recover). OPTIONAL when only\n * `retry` is configured: retry backoff reads its time off the result / timer\n * Msgs (`$resilience:err.at`, `$resilience:timer.atMs`), never the cold attempt\n * — so a `0` cold `at` is harmless there. Pure: it runs inside the merged\n * `update`, so it must NOT read the clock / RNG.\n */\n readonly at?: (msg: TM) => number;\n}\n\n// ===========================================================================\n// The slice + the wrapper Cmds/Msgs/Subs/Ports vocabulary.\n// ===========================================================================\n\n/**\n * The composed Model. The base machine's state nests under `base`; the wrapper\n * owns the NAMED `$resilience` slice — resilient-call's `ResilientState` keyed\n * by the per-call `key`, with `I = TC` (the original target base Cmd is the\n * port input) and `R = unknown` (the base interpret handler's result is opaque\n * to the wrapper — it only routes Ok/Err). Plain data → round-trips through\n * `JSON.parse(JSON.stringify(...))` iff the base state does.\n */\nexport interface ResilienceModel<S, TC extends Cmd> {\n readonly base: S;\n readonly $resilience: ResilientState<TC, unknown>;\n}\n\n/**\n * The carrier Cmd the wrapper emits when the gate ADMITS a target base Cmd. It\n * RETAGS the original base Cmd into `input` — the base Cmd is plain data, so no\n * closure (invariant 3). The interpret handler unwraps `input` and runs it\n * through the BASE interpret for that Cmd type. `key` indexes the resilient\n * call this run belongs to.\n */\nexport type ResilienceRunCmd<TC extends Cmd> = Cmd<\"$resilience:run\"> & {\n readonly key: string;\n /** The ORIGINAL base Cmd, retagged — never swallowed. */\n readonly input: TC;\n};\n\n/**\n * The success Msg the `$resilience:run` handler dispatches back when the base\n * interpret resolved OK. Routes to `succeed`. `at` is stamped at the interpret\n * boundary (the one permitted clock read).\n */\nexport type ResilienceOkMsg = {\n readonly type: \"$resilience:ok\";\n readonly key: string;\n readonly result: unknown;\n readonly at: number;\n};\n\n/**\n * The error Msg the `$resilience:run` handler dispatches back when the base\n * interpret threw / rejected. Routes to `fail` (trip the breaker, then back off\n * or settle). `at` is stamped at the interpret boundary.\n */\nexport type ResilienceErrMsg = {\n readonly type: \"$resilience:err\";\n readonly key: string;\n readonly error: unknown;\n readonly at: number;\n};\n\n/**\n * The retry / deadline timer Msg the `$resilience:timer` Sub dispatches when\n * the wall clock crosses the armed instant. Routes to `onTimer`. `id` is the\n * `$resilience:`-namespaced timer id; `atMs` is the fire instant (Msg data, the\n * clock that drove it lives in the Sub).\n */\nexport type ResilienceTimerMsg = {\n readonly type: \"$resilience:timer\";\n readonly id: string;\n readonly atMs: number;\n};\n\n/** Every Msg the wrapper adds to the base's Msg union. */\nexport type ResilienceMsg =\n | ResilienceOkMsg\n | ResilienceErrMsg\n | ResilienceTimerMsg;\n\n/** Every Cmd the wrapper adds to the base's Cmd union. */\nexport type ResilienceCmd<TC extends Cmd> = ResilienceRunCmd<TC>;\n\n/** The Sub the wrapper adds — a deadline-style timer in the `$resilience` family. */\nexport type ResilienceTimerSub = Sub<\"$resilience:timer\"> & {\n readonly atMs: number;\n};\n\n// ===========================================================================\n// Boundary re-keying — the resilient-call namespace ↔ the `$resilience` one.\n// ===========================================================================\n\n// resilient-call emits its carrier as `resilient_run` and keys its timer Subs\n// `resilient:retry:{key}` / `resilient:deadline:{key}`. The wrapper presents the\n// `$resilience:` namespace to the outside world (rule 4) while REUSING the\n// unmodified verbs underneath. These two pure string maps are the only seam:\n// rename the carrier on the way OUT, re-key the timer id on the way IN.\n\nconst RC_RETRY_PREFIX = \"resilient:retry:\";\nconst RC_DEADLINE_PREFIX = \"resilient:deadline:\";\nconst WX_RETRY_PREFIX = \"$resilience:retry:\";\nconst WX_DEADLINE_PREFIX = \"$resilience:deadline:\";\n\n/** resilient-call internal sub id → the `$resilience:`-namespaced public id. */\nfunction toWrapperTimerId(rcId: string): string {\n if (rcId.startsWith(RC_RETRY_PREFIX))\n return WX_RETRY_PREFIX + rcId.slice(RC_RETRY_PREFIX.length);\n if (rcId.startsWith(RC_DEADLINE_PREFIX))\n return WX_DEADLINE_PREFIX + rcId.slice(RC_DEADLINE_PREFIX.length);\n return rcId;\n}\n\n/** The `$resilience:`-namespaced timer id → the resilient-call internal id. */\nfunction toRcTimerId(wxId: string): string {\n if (wxId.startsWith(WX_RETRY_PREFIX))\n return RC_RETRY_PREFIX + wxId.slice(WX_RETRY_PREFIX.length);\n if (wxId.startsWith(WX_DEADLINE_PREFIX))\n return RC_DEADLINE_PREFIX + wxId.slice(WX_DEADLINE_PREFIX.length);\n return wxId;\n}\n\n// ===========================================================================\n// update-form dispatch — run the base reducer regardless of its shape.\n// (Mirrors withTelemetry's inline form-branching so the wrapper stays\n// standalone; the composed Model is never discriminated on `.type`.)\n// ===========================================================================\n\ntype BaseCellFn<S, M, C extends Cmd> = (\n state: S,\n msg: M,\n) => readonly [S, readonly C[]];\n\n/**\n * Invoke the base machine's `update` for `msg` against base `state`, returning\n * the base's `[nextBase, baseCmds]` UNCHANGED. Pure — routes to the base cell;\n * never reads the clock, allocates an Error, or mutates state.\n */\nfunction runBaseUpdate<S, M extends { type: string }, C extends Cmd>(\n update: object,\n // The base's update form, read once via `formOf(base)` — no local heuristic.\n form: UpdateForm,\n state: S,\n msg: M,\n): readonly [S, readonly C[]] {\n if (form === \"reducer\") {\n const record = update as unknown as Record<string, BaseCellFn<S, M, C>>;\n // biome-ignore lint/style/noNonNullAssertion: the base Reducer guarantees a cell for every dispatched Msg.type; `!` is required under noUncheckedIndexedAccess and cannot miss for a Msg the base accepts\n return record[msg.type]!(state, msg);\n }\n const table = update as unknown as Record<\n string,\n Record<string, BaseCellFn<S, M, C>>\n >;\n const stateKey = (state as unknown as { type: string }).type;\n // biome-ignore lint/style/noNonNullAssertion: the base Transitions table guarantees every (state.type × msg.type) cell exists; `!` is required under noUncheckedIndexedAccess and cannot miss at runtime\n return table[stateKey]![msg.type]!(state, msg);\n}\n\n/** Recover the base Msg.type set from either update form (see withTelemetry). */\nfunction baseMsgKeys(update: object, form: UpdateForm): readonly string[] {\n const keys = Object.keys(update);\n const firstKey = keys[0];\n if (firstKey === undefined) return [];\n if (form === \"reducer\") return keys;\n const firstValue = (update as Record<string, unknown>)[firstKey];\n return Object.keys(firstValue as object);\n}\n\n/**\n * Does `update` declare `key` as a Msg.type cell, in EITHER update form? The\n * reserved-namespace guard checks the correct level per form (the same\n * detection `baseMsgKeys` / `runBaseUpdate` use):\n *\n * - Reducer form (`{ [msgType]: cell }`, values are functions) — the\n * msg-type keys live at the TOP level, so `Object.hasOwn(update, key)`.\n * - Transitions form (`{ [stateType]: { [msgType]: cell } }`, values are\n * records) — the msg-type keys live in the INNER tables, one per state.\n * A reserved msg-type can hide in ANY inner table, so scan EVERY inner\n * table; a collision in even one inner cell would let a transitions-form\n * base squat on e.g. an inner `$resilience:ok` cell that the wrapper's\n * merged update would silently clobber. Top-level `Object.hasOwn` (the\n * old check) only sees state-type keys here and would MISS it entirely.\n */\nfunction updateDeclaresMsgKey(\n update: object,\n form: UpdateForm,\n key: string,\n): boolean {\n const keys = Object.keys(update);\n const firstKey = keys[0];\n if (firstKey === undefined) return false;\n if (form === \"reducer\") {\n // Reducer form — msg-type keys at the top level.\n return Object.hasOwn(update, key);\n }\n // Transitions form — msg-type keys live in each inner state-type table.\n for (const stateKey of keys) {\n const innerTable = (update as Record<string, unknown>)[stateKey];\n if (\n innerTable !== null &&\n typeof innerTable === \"object\" &&\n Object.hasOwn(innerTable, key)\n ) {\n return true;\n }\n }\n return false;\n}\n\n// ===========================================================================\n// withResilience — the wrapper.\n// ===========================================================================\n\n/**\n * Wrap `base` so its `config.target` Cmd is run through the resilient-call\n * concern (cache → circuit → rate-limit → retry, deadline-capped). Returns a\n * NEW `Machine` over the composed Model `{ base, $resilience }`, with the\n * base's Msgs/Cmds/Subs/Ctx extended by the wrapper's own.\n *\n * @param base any `Machine<S, M, C, U, Ctx>`.\n * @param config the resilience knob: `target`, the optional bricks, `keyOf`.\n * @param rng injected backoff jitter source. Pass a fixed `() => 0.5` in\n * tests to pin retry delays; defaults to `Math.random` (read only\n * at the verb boundary, never inside the merged `update`).\n */\nexport function withResilience<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n base: Machine<S, M, C, U, Ctx>,\n config: ResilienceConfig<C, M>,\n rng: () => number = Math.random,\n): Machine<\n ResilienceModel<S, C>,\n M | ResilienceMsg,\n C | ResilienceCmd<C>,\n U | ResilienceTimerSub,\n Ctx\n> {\n type WM = ResilienceModel<S, C>;\n type WMsg = M | ResilienceMsg;\n type WCmd = C | ResilienceCmd<C>;\n\n const { target } = config;\n const keyOf = config.keyOf ?? (() => target);\n\n // A time-sensitive brick gates the COLD attempt against real wall time read\n // off the triggering base Msg (`config.at`). Without `at` the cold gate would\n // have to fabricate `0` — and a cold attempt after a real-time breaker trip\n // then computes cooldown = `0 - realTripTime < 0` forever (the breaker NEVER\n // recovers), while a `0`-anchored deadline fires immediately. Refuse to build\n // such a machine: errors are data, surfaced at construction, never a silent\n // breaker that can't recover. Retry alone reads time off the result/timer\n // Msgs, not the cold attempt, so `at` stays optional there.\n const timeSensitiveBrick =\n config.circuit !== undefined ||\n config.rateLimit !== undefined ||\n config.cache !== undefined ||\n config.deadline !== undefined;\n if (timeSensitiveBrick && config.at === undefined) {\n throw new Error(\n \"withResilience: a time-sensitive brick (circuit / rateLimit / cache / \" +\n \"deadline) is configured but `config.at` is missing. The cold-attempt \" +\n \"gate needs the REAL wall time the triggering base Msg carries as data \" +\n \"(time-as-data) — without it the gate would fabricate `at = 0`, so a \" +\n \"cold attempt after a real-time breaker trip computes a negative \" +\n \"cooldown and the breaker NEVER recovers (and a deadline brick fires \" +\n \"immediately). Provide `at: (msg) => msg.<wallTimeField>`.\",\n );\n }\n // Read the triggering base Msg's wall time as data for the cold gate. When no\n // time-sensitive brick is configured `at` may be omitted; the cold attempt\n // then gates at `0`, which is harmless because retry backoff reads its time\n // off the result / timer Msgs, never the cold attempt.\n const atOf = config.at ?? (() => 0);\n\n // The REUSED knob: I = the original target base Cmd, R = the opaque base\n // interpret result. Its verbs / subs / gate are used unmodified; the wrapper\n // only adapts the boundary (carrier rename + timer-id re-keying).\n const rc = createResilientCall<C, unknown>(config, rng);\n\n // -------------------------------------------------------------------------\n // The merged update — a flat Reducer over the composed (non-discriminated)\n // Model. Cells: every base Msg.type (delegating to base.update + partition),\n // plus the three `$resilience:*` Msgs (delegating to the reused verbs).\n // -------------------------------------------------------------------------\n\n type Cell = (state: WM, msg: WMsg) => readonly [WM, readonly WCmd[]];\n const update: Record<string, Cell> = {};\n\n // The base's update form, read ONCE from the `__form` tag (see `formOf`) and\n // threaded into the per-form helpers — no structural re-derivation here.\n const baseForm = formOf(base);\n\n // --- Base-Msg cells: delegate, then PARTITION + retag the target Cmd. ----\n for (const key of baseMsgKeys(base.update, baseForm)) {\n update[key] = (state, msg) => {\n // 1) Run the base reducer. Its NON-target Cmds pass through unchanged.\n const [nextBase, baseCmds] = runBaseUpdate<S, M, C>(\n base.update,\n baseForm,\n state.base,\n msg as M,\n );\n\n // 2) Partition base Cmds: non-target pass straight through; each target\n // Cmd is routed through the resilient-call `attempt` gate, which either\n // emits a `resilient_run` carrier (→ renamed `$resilience:run`,\n // retagging the ORIGINAL base Cmd into `input`), or records the gate\n // decision in the slice with NO carrier (cache hit / circuit-open /\n // rate-limited backoff). Either way the target Cmd never escapes raw\n // and never vanishes off-ledger.\n let resilience = state.$resilience;\n const outCmds: WCmd[] = [];\n // The COLD attempt's gate time — the REAL wall time the triggering base\n // Msg carries as data (rule 5: the merged `update` stays pure; the clock\n // does not enter here, the Msg's own `at` data does). With this real\n // instant the breaker compares `at - openedAtMs` against its cooldown\n // (set by a prior real-time `$resilience:err`), so a cold attempt during\n // cooldown fast-fails and one AFTER cooldown is admitted — the breaker\n // RECOVERS. The cache compares `at` against its stored write `at`, the\n // bucket refills from `at`, and the deadline cap anchors at\n // `at + deadline.ms` — all against true wall time, never a fabricated\n // `0`. Every SUBSEQUENT gate run (`fail` → backoff, retry-timer\n // `onTimer` → re-gate) reads time off the result / timer Msgs the same\n // way. `atOf` defaults to `0` ONLY when no time-sensitive brick is\n // configured (construction throws otherwise), where retry backoff reads\n // its time off the result/timer Msgs and the cold `at` is never compared.\n const at = atOf(msg as M);\n for (const cmd of baseCmds) {\n if (cmd.type !== target) {\n outCmds.push(cmd);\n continue;\n }\n const callKey = keyOf(cmd);\n const [nextSlice, runCmds] = rc.attempt(resilience, callKey, cmd, at);\n resilience = nextSlice;\n // rc.attempt emits 0 or 1 `resilient_run` carriers. Rename each to the\n // `$resilience:run` namespace, preserving the retagged `input` (the\n // ORIGINAL base Cmd). Zero carriers = the gate recorded a divergence in\n // `nextSlice` (cache/circuit/backoff) — visible in the slice + log.\n for (const rcRun of runCmds) {\n outCmds.push({\n type: \"$resilience:run\",\n key: rcRun.key,\n input: rcRun.input,\n });\n }\n }\n\n const next: WM = { base: nextBase, $resilience: resilience };\n return [next, outCmds];\n };\n }\n\n // --- `$resilience:ok` → succeed ------------------------------------------\n update[\"$resilience:ok\"] = (state, msg) => {\n const m = msg as ResilienceOkMsg;\n const [slice, runCmds] = rc.succeed(state.$resilience, m.key, {\n type: MsgType.ResilientOk,\n key: m.key,\n result: m.result,\n at: m.at,\n });\n return [{ ...state, $resilience: slice }, renameRunCmds(runCmds)];\n };\n\n // --- `$resilience:err` → fail --------------------------------------------\n update[\"$resilience:err\"] = (state, msg) => {\n const m = msg as ResilienceErrMsg;\n const [slice, runCmds] = rc.fail(state.$resilience, m.key, {\n type: MsgType.ResilientErr,\n key: m.key,\n error: m.error,\n at: m.at,\n });\n return [{ ...state, $resilience: slice }, renameRunCmds(runCmds)];\n };\n\n // --- `$resilience:timer` → onTimer (retry re-runs gate / deadline settles) -\n update[\"$resilience:timer\"] = (state, msg) => {\n const m = msg as ResilienceTimerMsg;\n // Re-key the public `$resilience:`-id back to the resilient-call internal\n // id the `onTimer` verb matches against, then run the unmodified verb.\n const [slice, runCmds] = rc.onTimer(state.$resilience, {\n type: \"deadline_exceeded\",\n id: toRcTimerId(m.id),\n atMs: m.atMs,\n });\n return [{ ...state, $resilience: slice }, renameRunCmds(runCmds)];\n };\n\n // Rename any `resilient_run` carriers a verb produced (the retry re-attempt\n // path) into the `$resilience:run` namespace, preserving the retagged input.\n function renameRunCmds(\n runCmds: readonly { type: string; key: string; input: C }[],\n ): readonly WCmd[] {\n return runCmds.map((r) => ({\n type: \"$resilience:run\" as const,\n key: r.key,\n input: r.input,\n }));\n }\n\n // -------------------------------------------------------------------------\n // Subs — base subs ⧺ resilient-call timers, re-keyed into `$resilience:`.\n // -------------------------------------------------------------------------\n\n const baseSubscriptions = base.subscriptions;\n const baseSubscribe = (base as { subscribe?: unknown }).subscribe;\n\n function subscriptions(state: WM): readonly (U | ResilienceTimerSub)[] {\n const baseSubs = baseSubscriptions\n ? baseSubscriptions(state.base)\n : ([] as readonly U[]);\n // rc.subs returns DeadlineSubs keyed `resilient:retry:{k}` /\n // `resilient:deadline:{k}`. Re-key each into the `$resilience:` namespace\n // and re-type to the wrapper's `$resilience:timer` Sub family (rule 4).\n const rcSubs: readonly DeadlineSub[] = rc.subs(state.$resilience);\n const timerSubs: ResilienceTimerSub[] = rcSubs.map((s) => ({\n id: subId(toWrapperTimerId(s.id)),\n type: \"$resilience:timer\",\n atMs: s.atMs,\n }));\n return [...baseSubs, ...timerSubs];\n }\n\n // -------------------------------------------------------------------------\n // Interpret — base.interpret for non-target + the `$resilience:run` handler\n // that COMPOSES base.interpret for the retagged inner Cmd.\n // -------------------------------------------------------------------------\n\n const baseInterpret =\n (base as { interpret?: Interpret<M, C, Ctx> }).interpret ??\n ({} as Interpret<M, C, Ctx>);\n\n // Reserved-namespace guard (mirrors withTelemetry + the substrate's\n // definePort / SubId collision asserts). The wrapper OWNS four reserved keys:\n // the `$resilience:run` carrier Cmd, the `$resilience:ok` / `$resilience:err`\n // result Msgs, and the `$resilience:timer` Sub Msg. The merges below spread\n // the wrapper's cells over the base's update / interpret / subscribe maps; if\n // the base already declares ANY reserved key on ANY of those three surfaces,\n // the spread would SILENTLY clobber it (or the base's own key would shadow the\n // wrapper's). The `$resilience:` namespace is the wrapper's — refuse to wrap a\n // base that squats on any of the four across any surface.\n //\n // Ordered BEFORE the target-handler guard below: squatting a reserved key is a\n // target-INDEPENDENT structural defect of the base, so it is the more\n // fundamental rejection and must report first. Otherwise a base that BOTH\n // squats `$resilience:*` AND omits the target handler would surface the\n // narrower missing-target error and mask the namespace collision — exactly the\n // regression #111's construction-time hoist introduced (the hoisted target\n // check shadowed this guard for such a base), fixed by #112.\n //\n // `update` is checked PER FORM (the same Reducer-vs-Transitions detection the\n // wrapper already uses in `baseMsgKeys` / `runBaseUpdate`): a Reducer-form\n // base keys its msg-types at the TOP level, but a Transitions-form base keys\n // them in the INNER `{ [stateType]: { [msgType]: cell } }` tables — so a flat\n // top-level `Object.hasOwn` would MISS a reserved msg-type squatting inside an\n // inner table (e.g. an inner `$resilience:ok` cell) and let the merged update\n // silently clobber it. `interpret` and `subscribe` are always flat keyed-by\n // -type maps, so the direct `Object.hasOwn` is correct there.\n const RESERVED = [\n \"$resilience:run\",\n \"$resilience:ok\",\n \"$resilience:err\",\n \"$resilience:timer\",\n ] as const;\n const surfaces: readonly {\n readonly name: string;\n readonly declares: (reserved: string) => boolean;\n }[] = [\n {\n name: \"update\",\n declares: (reserved) =>\n updateDeclaresMsgKey(base.update as object, baseForm, reserved),\n },\n {\n name: \"interpret\",\n declares: (reserved) => Object.hasOwn(baseInterpret as object, reserved),\n },\n {\n name: \"subscribe\",\n declares: (reserved) =>\n Object.hasOwn((baseSubscribe as object | undefined) ?? {}, reserved),\n },\n ];\n for (const reserved of RESERVED) {\n for (const surface of surfaces) {\n if (surface.declares(reserved)) {\n throw new Error(\n `withResilience: the base machine declares a reserved \"${reserved}\" ` +\n `${surface.name} key — the wrapper owns the \"$resilience:\" ` +\n `namespace ($resilience:run / :ok / :err / :timer across update, ` +\n `interpret, and subscribe). Rename the base key.`,\n );\n }\n }\n }\n\n // Construction guard — the target Cmd MUST have a base interpret handler. This\n // is a property of the (base, config) pair, fully known here at construction:\n // the `$resilience:run` carrier delegates to `baseInterpret[target]`, so a\n // missing handler means the resilient effect has nothing to perform. Surface\n // it now — refuse to build a machine whose target can never run — rather than\n // lazily on the first effect (errors are data, surfaced at construction, like\n // the time-sensitive-`at` and reserved-namespace guards above). Ordered AFTER\n // the reserved-namespace guard so a base that squats `$resilience:*` is\n // rejected for the namespace collision first, not masked by this narrower\n // missing-target error (#112).\n if (!Object.hasOwn(baseInterpret as object, target)) {\n throw new Error(\n `withResilience: no base interpret handler for target Cmd \"${target}\" ` +\n `— the resilient effect has nothing to perform. The base machine must ` +\n `declare an interpret handler for its target Cmd type.`,\n );\n }\n\n // The `$resilience:run` handler delegates to the BASE interpret handler for\n // the retagged inner Cmd's type, wrapped via tryInterpret so Ok routes to\n // `$resilience:ok` and Err to `$resilience:err`. `Date.now()` is stamped here\n // — the ONE permitted clock read (interpret is impure; update is pure).\n const runHandler = tryInterpret<\n ResilienceRunCmd<C>,\n // The carried result is the base interpret handler's resolution, whose\n // contract is `Promise<M | void>` (a follow-up base Msg or nothing — see\n // `Interpret<M, C, Ctx>`). Carrying `M | void` here (not bare `unknown`)\n // keeps the base's follow-up Msg type at the seam; `ResilienceOkMsg.result`\n // then narrows from genuine looseness to \"a base Msg or void\".\n // biome-ignore lint/suspicious/noConfusingVoidType: mirrors `Interpret`'s own `Promise<M | void>` — the base handler resolves to a follow-up Msg OR nothing; `M | undefined` would reject no-return bodies\n M | void,\n ResilienceOkMsg | ResilienceErrMsg,\n unknown\n >(\n async (cmd, ctx) => {\n // Compose the base interpret: run the ORIGINAL base Cmd through the base's\n // own handler for its type. The base handler may itself return a follow-up\n // Msg or void; the resilience layer treats its RESOLUTION (resolve vs\n // throw) as the success/failure signal, and carries whatever it returned\n // as `result`. A throw → tryInterpret routes to `$resilience:err`.\n const inner = cmd.input;\n // The base handler's own type is `Interpret<M, C, Ctx>[type]`, i.e.\n // `(cmd, ctx & PortEmitter) => Promise<M | void>`. We read it off the\n // erased `Record` and re-narrow to that contract — the result type is\n // KNOWN (the base Msg union or void), so it is not widened to `unknown`.\n // biome-ignore lint/suspicious/noConfusingVoidType: the base handler contract is `Promise<M | void>` (follow-up Msg or no-return); narrowing to `undefined` would reject void-returning base handlers\n type BaseHandler = (c: C, ctx: Ctx) => Promise<M | void>;\n const handler = (baseInterpret as Record<string, unknown>)[inner.type] as\n | BaseHandler\n | undefined;\n if (handler === undefined) {\n // Unreachable in practice: the construction guard above already refused\n // to build this machine if `baseInterpret[target]` was missing, and the\n // retagged inner Cmd's type is always the target. Kept as defense in\n // depth on the erased-Record read — if it ever fires it routes to\n // `$resilience:err` (errors are data, never swallowed).\n throw new Error(\n `withResilience: no base interpret handler for target Cmd ` +\n `\"${inner.type}\" — the resilient effect has nothing to perform.`,\n );\n }\n return await handler(inner, ctx as unknown as Ctx);\n },\n (result, cmd): ResilienceOkMsg => ({\n type: \"$resilience:ok\",\n key: cmd.key,\n result,\n at: Date.now(),\n }),\n (error, cmd): ResilienceErrMsg => ({\n type: \"$resilience:err\",\n key: cmd.key,\n error,\n at: Date.now(),\n }),\n );\n\n const interpret = {\n ...(baseInterpret as Record<string, unknown>),\n \"$resilience:run\": runHandler,\n } as Interpret<WMsg, WCmd, Ctx>;\n\n // -------------------------------------------------------------------------\n // The `subscribe` map — base cells ⧺ the `$resilience:timer` cell, which\n // delegates the timer lifecycle to the deadline Sub primitive (re-keying the\n // dispatched Msg into the `$resilience:timer` shape). Clock lives here.\n // -------------------------------------------------------------------------\n\n const subscribeMap = {\n ...((baseSubscribe as Record<string, unknown> | undefined) ?? {}),\n \"$resilience:timer\": (\n sub: ResilienceTimerSub,\n ctx: Ctx,\n dispatch: (msg: WMsg) => void,\n ): (() => void) =>\n // Reuse the deadline subscribe cell wholesale: it arms a one-shot timer\n // for `max(0, atMs - Date.now())` and fires when the wall clock crosses\n // `atMs`. We hand it a `DeadlineSub` shape and translate its\n // `deadline_exceeded` Msg into the wrapper's `$resilience:timer` Msg.\n subscribeDeadline(\n { id: sub.id as DeadlineSub[\"id\"], type: \"deadline\", atMs: sub.atMs },\n ctx,\n (dl) =>\n dispatch({ type: \"$resilience:timer\", id: dl.id, atMs: dl.atMs }),\n ),\n };\n\n return {\n init: (loaded, ctx) => {\n // Rehydrate: `loaded !== null` MUST return `[loaded, []]` (no Cmds) — the\n // substrate's replay enforces it. Split the composed loaded Model into its\n // base slice and feed the base its own snapshot so the base rehydrate\n // contract is honored transitively.\n if (loaded !== null) {\n return [loaded, []];\n }\n const [base0, baseCmds] = base.init(null, ctx);\n // The base's OWN init Cmds: NON-target Cmds pass straight through; a\n // TARGET Cmd is RETAGGED (admitted) so it is observed, never raw — the\n // same retag-not-swallow contract the merged update enforces (rule 2). A\n // target Cmd that escaped init raw would bypass the gate AND the carrier,\n // so the resilience layer would never see the effect resolve. At a fresh\n // boot the breaker is closed and the bucket full, so the gate decision is\n // unambiguously ADMIT — but the Cmd must still travel inside a\n // `$resilience:run` carrier (the slice records its `running` phase). Boot\n // is the resilience epoch: the cold gate runs at `at = 0`, harmless here\n // because the breaker is closed (nothing to compare), the cache empty, and\n // the bucket full. Wall time enters once the effect resolves (the\n // `$resilience:ok` / `:err` Msgs carry `at`), exactly as in a transition.\n let resilience = rc.init();\n const initCmds: WCmd[] = [];\n for (const cmd of baseCmds) {\n if (cmd.type !== target) {\n initCmds.push(cmd);\n continue;\n }\n // The boot cold attempt gates at `at = 0` (boot is the resilience epoch:\n // no real Msg drove it, so there is no time-as-data to read). For the\n // circuit / rateLimit / cache bricks that is harmless at a fresh boot —\n // the breaker is closed (nothing to compare), the bucket full, the cache\n // empty, so the gate ADMITS unambiguously and the `0` is never compared\n // against a real instant. The DEADLINE brick is the exception: it anchors\n // the cap at `at + deadline.ms = 0 + ms`, so its `$resilience:deadline`\n // Sub arms at `atMs = ms` and `subscribeDeadline` fires after\n // `max(0, atMs - Date.now()) === 0` ms — i.e. IMMEDIATELY at boot,\n // settling the boot call failed before it can run. A `0`-anchored boot\n // deadline is never the author's intent; it is the same trap the\n // construction guard refuses for transitions (a fabricated `at` the\n // deadline cannot honor). Fail loud: a boot effect that needs a real\n // deadline belongs behind a post-run boot Msg (where a real `at` exists),\n // not an init Cmd. Mirrors the substrate guidance \"boot effects belong in\n // a boot Msg, not init Cmds.\"\n if (config.deadline !== undefined) {\n throw new Error(\n `withResilience: base.init emits the target Cmd \"${target}\" while a ` +\n `deadline brick is configured. The boot cold attempt has no real ` +\n `wall time (boot is gated at at=0), so the deadline would anchor at ` +\n `0 + ${config.deadline.ms}ms and its $resilience:deadline Sub would ` +\n `fire IMMEDIATELY (max(0, atMs - now) === 0), failing the boot call ` +\n `instantly. Move the boot effect out of base.init and into a boot ` +\n `Msg dispatched after run() (where the triggering Msg carries a real ` +\n `at via config.at), so the deadline anchors at a real instant. (Boot ` +\n `effects belong in a boot Msg, not init Cmds.)`,\n );\n }\n const callKey = keyOf(cmd);\n const [nextSlice, runCmds] = rc.attempt(resilience, callKey, cmd, 0);\n resilience = nextSlice;\n for (const rcRun of runCmds) {\n initCmds.push({\n type: \"$resilience:run\",\n key: rcRun.key,\n input: rcRun.input,\n });\n }\n }\n const model: WM = { base: base0, $resilience: resilience };\n return [model, initCmds];\n },\n update: update as unknown as Reducer<WM, WMsg, WCmd>,\n subscriptions,\n subscribe: subscribeMap as unknown as Machine<\n WM,\n WMsg,\n WCmd,\n U | ResilienceTimerSub,\n Ctx\n >[\"subscribe\"],\n interpret,\n } as Machine<WM, WMsg, WCmd, U | ResilienceTimerSub, Ctx>;\n}\n"]}
@@ -0,0 +1,149 @@
1
+ import { C as Cmd, S as Sub, M as Machine } from '../core-CymXZCeY.js';
2
+
3
+ /**
4
+ * @demlik/tea/with-telemetry — the pattern-setting wrapper of the wrapper tier.
5
+ *
6
+ * `withTelemetry(base, config)` is a **pure function over machine data**: it
7
+ * takes any `Machine<S, M, C, U, Ctx>` and returns a NEW `Machine` over the
8
+ * composed Model `{ base, $telemetry }`. You `run()` / `replay()` the result
9
+ * exactly like any other machine — there is no new runtime, no privileged
10
+ * interception path.
11
+ *
12
+ * This is the SAFEST wrapper and the one every future wrapper (`withDeadline`,
13
+ * `withResilience`) copies: it is **OBSERVE-ONLY**. It observes every base Msg
14
+ * + transition, increments a monotonic counter, and emits a fire-and-forget
15
+ * telemetry Cmd whose interpret handler hands the event to a sink Port. It must
16
+ * NOT transform base Cmds or gate base Msgs — base behavior is byte-identical
17
+ * with or without it. The moment a telemetry wrapper alters the Msg/Cmd stream
18
+ * instead of observing it, it has become saga middleware (the betrayal the
19
+ * research warned of).
20
+ *
21
+ * ## The 5 rules it satisfies (from the wrapper-tier contract)
22
+ *
23
+ * 1. **Named, serializable slice.** Wrapper state lives at `model.$telemetry`,
24
+ * a plain `{ seq: number }` — JSON-serializable, never a closure.
25
+ * 2. **Retag-and-re-emit, never swallow.** The merged `update` runs the base
26
+ * reducer, passes its Cmds through UNCHANGED, and APPENDS its own
27
+ * `$telemetry:emit` Cmd. No base Cmd ever vanishes; nothing is gated.
28
+ * 3. **Every decision is a Msg/Cmd.** Each telemetry emission is a
29
+ * `$telemetry:emit` Cmd through the merged `update`, so "why did this fire"
30
+ * is answerable from reducer + log alone, and `runtime.observe` sees it.
31
+ * 4. **Subs merge by id.** Telemetry has no timers — merged subs/subscribe are
32
+ * the base's, verbatim.
33
+ * 5. **Clock out of update.** The default event carries no wall-clock read.
34
+ * The optional `at` is stamped at the interpret boundary (the `clock`
35
+ * port), never inside the merged `update`.
36
+ *
37
+ * ## Where the clock lives
38
+ *
39
+ * Inside the merged `update`: nowhere. The reducer is pure — it reads `seq` from
40
+ * the slice and projects the event from `(msg, prev, next)`. The ONLY clock read
41
+ * is at the interpret boundary: the `$telemetry:emit` handler stamps `event.at`
42
+ * from the injected `clock` port (if supplied and the projector left `at`
43
+ * unset), exactly as `resilient-call` reads `Date.now()` only inside `handlers`.
44
+ *
45
+ * ## Typical wiring
46
+ *
47
+ * const observed = withTelemetry(baseMachine, {
48
+ * event: (msg, _prev, next) => ({ seq: next.$telemetry.seq, msgType: msg.type }),
49
+ * });
50
+ * const runtime = run(observed, {
51
+ * ctx: { ...baseCtx, telemetrySink: (e) => posthog.capture(e), clock: Date.now },
52
+ * });
53
+ */
54
+
55
+ /**
56
+ * The datum handed to the sink. The default projector emits `{ seq, msgType }`;
57
+ * a custom `event` projector may return any extra fields. `at` is OPTIONAL and
58
+ * plain data — it is stamped at the interpret boundary by the `clock` port, not
59
+ * inside the (pure, clock-free) merged `update`. Every field is JSON-round-trip
60
+ * serializable so a recorded telemetry stream stays durable.
61
+ */
62
+ interface TelemetryEvent {
63
+ /** Monotonic event counter — equals `$telemetry.seq` after the transition. */
64
+ readonly seq: number;
65
+ /** The `Msg.type` discriminant that drove this transition. */
66
+ readonly msgType: string;
67
+ /**
68
+ * Wall-clock instant the event was emitted, ms since epoch. Absent until the
69
+ * interpret boundary stamps it (the `clock` port). Optional because the
70
+ * merged `update` is clock-free — a verb must never read `Date.now()`.
71
+ */
72
+ readonly at?: number;
73
+ }
74
+ /**
75
+ * The wrapper's Model slice. A monotonic event counter — minimal but real, so
76
+ * the named-slice mechanism is exercised for the richer-slice wrappers that
77
+ * follow. `seq` equals the number of base Msgs folded so far: after replaying
78
+ * `N` msgs, `$telemetry.seq === N`. Plain data → serializes into a `Store<S>`.
79
+ */
80
+ interface TelemetrySlice {
81
+ readonly seq: number;
82
+ }
83
+ /**
84
+ * The composed Model. The base machine's state is nested under `base`; the
85
+ * wrapper's state lives in the NAMED `$telemetry` slice — never hidden in a
86
+ * closure. Both fields are plain data, so the whole composed Model round-trips
87
+ * through `JSON.parse(JSON.stringify(...))` iff the base state does.
88
+ */
89
+ interface TelemetryModel<S> {
90
+ readonly base: S;
91
+ readonly $telemetry: TelemetrySlice;
92
+ }
93
+ /**
94
+ * The fire-and-forget Cmd the merged `update` appends after every base
95
+ * transition. It carries the projected `event` as plain data (invariant 3 —
96
+ * no closure). The interpret handler hands `event` to the sink and dispatches
97
+ * NOTHING back, so there is no feedback loop.
98
+ */
99
+ type TelemetryEmitCmd = Cmd<"$telemetry:emit"> & {
100
+ readonly event: TelemetryEvent;
101
+ };
102
+ /**
103
+ * The side-effecting ports the wrapper adds to `Ctx`. The sink (PostHog / Sentry
104
+ * bridge) lives at the interpret boundary, never in a verb. `clock` is the
105
+ * optional wall-clock source used to stamp `event.at` at that same boundary —
106
+ * pass `Date.now` in production, a fixed `() => 1000` in tests; omit it to leave
107
+ * `at` unset.
108
+ */
109
+ interface TelemetryPorts {
110
+ /** The side-effecting sink. Fire-and-forget — its result is never awaited into a Msg. */
111
+ readonly telemetrySink: (event: TelemetryEvent) => void | Promise<void>;
112
+ /** Optional clock for stamping `event.at` at the interpret boundary. */
113
+ readonly clock?: () => number;
114
+ }
115
+ /**
116
+ * The telemetry knob. `event` is an OPTIONAL projector mapping
117
+ * `(msg, prev, next)` to a `TelemetryEvent`; omit it and the default emits
118
+ * `{ seq, msgType }` where `seq` is the post-transition counter. The projector
119
+ * runs INSIDE the pure merged `update`, so it must not read the clock — stamp
120
+ * `at` at the sink boundary via the `clock` port instead.
121
+ */
122
+ interface TelemetryConfig<S, M extends {
123
+ type: string;
124
+ }> {
125
+ readonly event?: (msg: M, prev: TelemetryModel<S>, next: TelemetryModel<S>) => TelemetryEvent;
126
+ }
127
+ /**
128
+ * Wrap `base` with observe-only telemetry. Returns a NEW `Machine` over the
129
+ * composed Model `{ base, $telemetry }`, with the base's Msgs/Cmds/Subs/Ctx
130
+ * extended by the wrapper's own.
131
+ *
132
+ * The composed machine:
133
+ * - `init` rehydrates the base (honoring the `[loaded, []]` contract) and
134
+ * seeds `$telemetry: { seq: 0 }`. Base init Cmds pass through unchanged.
135
+ * - `update` (Reducer form, keyed by every base `Msg.type`) runs the base
136
+ * reducer, increments `seq`, and appends a `$telemetry:emit` Cmd carrying
137
+ * the projected event. Base Cmds pass through UNCHANGED — the observe-only
138
+ * property.
139
+ * - `subscriptions` / `subscribe` are the base's, verbatim (no timers).
140
+ * - `interpret` is `{ ...base.interpret, "$telemetry:emit": sink-handler }`.
141
+ *
142
+ * @param base any `Machine<S, M, C, U, Ctx>`.
143
+ * @param config the telemetry knob (optional `event` projector).
144
+ */
145
+ declare function withTelemetry<S, M extends {
146
+ type: string;
147
+ }, C extends Cmd, U extends Sub, Ctx>(base: Machine<S, M, C, U, Ctx>, config?: TelemetryConfig<S, M>): Machine<TelemetryModel<S>, M, C | TelemetryEmitCmd, U, Ctx & TelemetryPorts>;
148
+
149
+ export { type TelemetryConfig, type TelemetryEmitCmd, type TelemetryEvent, type TelemetryModel, type TelemetryPorts, type TelemetrySlice, withTelemetry };
@@ -0,0 +1,95 @@
1
+ import '../chunk-2WZHS4P2.js';
2
+ import { formOf } from '../chunk-BNJYE4AU.js';
3
+
4
+ // src/with-telemetry/index.ts
5
+ function runBaseUpdate(update, form, state, msg) {
6
+ if (form === "reducer") {
7
+ const record = update;
8
+ return record[msg.type](state, msg);
9
+ }
10
+ const table = update;
11
+ const stateKey = state.type;
12
+ return table[stateKey][msg.type](state, msg);
13
+ }
14
+ function withTelemetry(base, config = {}) {
15
+ const project = config.event;
16
+ const baseForm = formOf(base);
17
+ const msgKeys = telemetryMsgKeys(base.update, baseForm);
18
+ const update = {};
19
+ for (const key of msgKeys) {
20
+ update[key] = (state, msg) => {
21
+ const [nextBase, baseCmds] = runBaseUpdate(
22
+ base.update,
23
+ baseForm,
24
+ state.base,
25
+ msg
26
+ );
27
+ const $telemetry = { seq: state.$telemetry.seq + 1 };
28
+ const next = { base: nextBase, $telemetry };
29
+ const event = project ? project(msg, state, next) : { seq: $telemetry.seq, msgType: msg.type };
30
+ const emit = { type: "$telemetry:emit", event };
31
+ return [next, [...baseCmds, emit]];
32
+ };
33
+ }
34
+ const baseInterpret = base.interpret ?? {};
35
+ if (Object.hasOwn(baseInterpret, "$telemetry:emit")) {
36
+ throw new Error(
37
+ 'withTelemetry: the base machine declares a reserved "$telemetry:emit" interpret handler \u2014 the wrapper owns the "$telemetry:" Cmd namespace. Rename the base handler.'
38
+ );
39
+ }
40
+ const interpret = {
41
+ ...baseInterpret,
42
+ // OBSERVE-ONLY at the boundary: the sink is fire-and-forget. The runtime
43
+ // `await`s every interpret handler in its dispatch loop, so awaiting the
44
+ // sink here would let a SLOW sink backpressure the wrapped machine (its
45
+ // next dispatch waits) and a FAILING sink crash dispatch (the throw would
46
+ // propagate out of the transition). Both violate the contract: telemetry
47
+ // must never affect base behavior OR latency. So we DO NOT await — we hand
48
+ // the event to the sink, normalize its result to a Promise, swallow any
49
+ // rejection/throw, and return immediately. The handler resolves in the same
50
+ // microtask the dispatch loop reaches it; the machine never waits on the
51
+ // sink, and a rejected sink can never become an unhandled rejection.
52
+ "$telemetry:emit": (cmd, ctx) => {
53
+ const event = cmd.event.at === void 0 && ctx.clock !== void 0 ? { ...cmd.event, at: ctx.clock() } : cmd.event;
54
+ try {
55
+ Promise.resolve(ctx.telemetrySink(event)).catch(() => {
56
+ });
57
+ } catch {
58
+ }
59
+ }
60
+ };
61
+ const baseSubscriptions = base.subscriptions;
62
+ const baseSubscribe = base.subscribe;
63
+ return {
64
+ init: (loaded, ctx) => {
65
+ if (loaded !== null) {
66
+ return [loaded, []];
67
+ }
68
+ const [base0, cmds] = base.init(null, ctx);
69
+ const model = { base: base0, $telemetry: { seq: 0 } };
70
+ return [model, cmds];
71
+ },
72
+ update,
73
+ // Subs merge to the base's, verbatim — telemetry owns no timers. The base
74
+ // subscribes against its own nested slice.
75
+ ...baseSubscriptions ? {
76
+ subscriptions: (state) => baseSubscriptions(state.base)
77
+ } : {},
78
+ ...baseSubscribe ? { subscribe: baseSubscribe } : {},
79
+ interpret
80
+ };
81
+ }
82
+ function telemetryMsgKeys(update, form) {
83
+ const keys = Object.keys(update);
84
+ const firstKey = keys[0];
85
+ if (firstKey === void 0) return [];
86
+ if (form === "reducer") {
87
+ return keys;
88
+ }
89
+ const firstValue = update[firstKey];
90
+ return Object.keys(firstValue);
91
+ }
92
+
93
+ export { withTelemetry };
94
+ //# sourceMappingURL=index.js.map
95
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/with-telemetry/index.ts"],"names":[],"mappings":";;;;AAsKA,SAAS,aAAA,CAKP,MAAA,EAEA,IAAA,EACA,KAAA,EACA,GAAA,EAC4B;AAC5B,EAAA,IAAI,SAAS,SAAA,EAAW;AAEtB,IAAA,MAAM,MAAA,GAAS,MAAA;AAEf,IAAA,OAAO,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA,CAAG,OAAO,GAAG,CAAA;AAAA,EACrC;AAEA,EAAA,MAAM,KAAA,GAAQ,MAAA;AAId,EAAA,MAAM,WAAY,KAAA,CAAsC,IAAA;AAExD,EAAA,OAAO,MAAM,QAAQ,CAAA,CAAG,IAAI,IAAI,CAAA,CAAG,OAAO,GAAG,CAAA;AAC/C;AAwBO,SAAS,aAAA,CAOd,IAAA,EACA,MAAA,GAAgC,EAAC,EAOjC;AACA,EAAA,MAAM,UAAU,MAAA,CAAO,KAAA;AAUvB,EAAA,MAAM,QAAA,GAAW,OAAO,IAAI,CAAA;AAC5B,EAAA,MAAM,OAAA,GAAU,gBAAA,CAAiB,IAAA,CAAK,MAAA,EAAQ,QAAQ,CAAA;AAEtD,EAAA,MAAM,SAAS,EAAC;AAOhB,EAAA,KAAA,MAAW,OAAO,OAAA,EAAS;AACzB,IAAA,MAAA,CAAO,GAAG,CAAA,GAAI,CAAC,KAAA,EAAO,GAAA,KAAQ;AAE5B,MAAA,MAAM,CAAC,QAAA,EAAU,QAAQ,CAAA,GAAI,aAAA;AAAA,QAC3B,IAAA,CAAK,MAAA;AAAA,QACL,QAAA;AAAA,QACA,KAAA,CAAM,IAAA;AAAA,QACN;AAAA,OACF;AAEA,MAAA,MAAM,aAA6B,EAAE,GAAA,EAAK,KAAA,CAAM,UAAA,CAAW,MAAM,CAAA,EAAE;AACnE,MAAA,MAAM,IAAA,GAA0B,EAAE,IAAA,EAAM,QAAA,EAAU,UAAA,EAAW;AAE7D,MAAA,MAAM,KAAA,GAAwB,OAAA,GAC1B,OAAA,CAAQ,GAAA,EAAK,KAAA,EAAO,IAAI,CAAA,GACxB,EAAE,GAAA,EAAK,UAAA,CAAW,GAAA,EAAK,OAAA,EAAS,IAAI,IAAA,EAAK;AAE7C,MAAA,MAAM,IAAA,GAAyB,EAAE,IAAA,EAAM,iBAAA,EAAmB,KAAA,EAAM;AAChE,MAAA,OAAO,CAAC,IAAA,EAAM,CAAC,GAAG,QAAA,EAAU,IAAI,CAAC,CAAA;AAAA,IACnC,CAAA;AAAA,EACF;AAQA,EAAA,MAAM,aAAA,GACH,IAAA,CAA8C,SAAA,IAC9C,EAAC;AAMJ,EAAA,IAAI,MAAA,CAAO,MAAA,CAAO,aAAA,EAAyB,iBAAiB,CAAA,EAAG;AAC7D,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAGF;AAAA,EACF;AACA,EAAA,MAAM,SAAA,GAAY;AAAA,IAChB,GAAI,aAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAWJ,iBAAA,EAAmB,CACjB,GAAA,EACA,GAAA,KACS;AACT,MAAA,MAAM,QACJ,GAAA,CAAI,KAAA,CAAM,EAAA,KAAO,MAAA,IAAa,IAAI,KAAA,KAAU,MAAA,GACxC,EAAE,GAAG,IAAI,KAAA,EAAO,EAAA,EAAI,IAAI,KAAA,EAAM,KAC9B,GAAA,CAAI,KAAA;AAIV,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,QAAQ,GAAA,CAAI,aAAA,CAAc,KAAK,CAAC,CAAA,CAAE,MAAM,MAAM;AAAA,QAAC,CAAC,CAAA;AAAA,MAC1D,CAAA,CAAA,MAAQ;AAAA,MAIR;AAAA,IACF;AAAA,GACF;AAGA,EAAA,MAAM,oBAAoB,IAAA,CAAK,aAAA;AAC/B,EAAA,MAAM,gBAAiB,IAAA,CAAiC,SAAA;AAExD,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,CAAC,MAAA,EAAQ,GAAA,KAAQ;AAKrB,MAAA,IAAI,WAAW,IAAA,EAAM;AACnB,QAAA,OAAO,CAAC,MAAA,EAAQ,EAAE,CAAA;AAAA,MACpB;AACA,MAAA,MAAM,CAAC,KAAA,EAAO,IAAI,IAAI,IAAA,CAAK,IAAA,CAAK,MAAM,GAAG,CAAA;AACzC,MAAA,MAAM,KAAA,GAA2B,EAAE,IAAA,EAAM,KAAA,EAAO,YAAY,EAAE,GAAA,EAAK,GAAE,EAAE;AAGvE,MAAA,OAAO,CAAC,OAAO,IAAI,CAAA;AAAA,IACrB,CAAA;AAAA,IACA,MAAA;AAAA;AAAA;AAAA,IAOA,GAAI,iBAAA,GACA;AAAA,MACE,aAAA,EAAe,CAAC,KAAA,KACd,iBAAA,CAAkB,MAAM,IAAI;AAAA,QAEhC,EAAC;AAAA,IACL,GAAI,aAAA,GAAgB,EAAE,SAAA,EAAW,aAAA,KAAkB,EAAC;AAAA,IACpD;AAAA,GACF;AAOF;AAQA,SAAS,gBAAA,CAAiB,QAAgB,IAAA,EAAqC;AAC7E,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA;AAC/B,EAAA,MAAM,QAAA,GAAW,KAAK,CAAC,CAAA;AACvB,EAAA,IAAI,QAAA,KAAa,MAAA,EAAW,OAAO,EAAC;AACpC,EAAA,IAAI,SAAS,SAAA,EAAW;AAEtB,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,UAAA,GAAc,OAAmC,QAAQ,CAAA;AAC/D,EAAA,OAAO,MAAA,CAAO,KAAK,UAAoB,CAAA;AACzC","file":"index.js","sourcesContent":["/**\n * @demlik/tea/with-telemetry — the pattern-setting wrapper of the wrapper tier.\n *\n * `withTelemetry(base, config)` is a **pure function over machine data**: it\n * takes any `Machine<S, M, C, U, Ctx>` and returns a NEW `Machine` over the\n * composed Model `{ base, $telemetry }`. You `run()` / `replay()` the result\n * exactly like any other machine — there is no new runtime, no privileged\n * interception path.\n *\n * This is the SAFEST wrapper and the one every future wrapper (`withDeadline`,\n * `withResilience`) copies: it is **OBSERVE-ONLY**. It observes every base Msg\n * + transition, increments a monotonic counter, and emits a fire-and-forget\n * telemetry Cmd whose interpret handler hands the event to a sink Port. It must\n * NOT transform base Cmds or gate base Msgs — base behavior is byte-identical\n * with or without it. The moment a telemetry wrapper alters the Msg/Cmd stream\n * instead of observing it, it has become saga middleware (the betrayal the\n * research warned of).\n *\n * ## The 5 rules it satisfies (from the wrapper-tier contract)\n *\n * 1. **Named, serializable slice.** Wrapper state lives at `model.$telemetry`,\n * a plain `{ seq: number }` — JSON-serializable, never a closure.\n * 2. **Retag-and-re-emit, never swallow.** The merged `update` runs the base\n * reducer, passes its Cmds through UNCHANGED, and APPENDS its own\n * `$telemetry:emit` Cmd. No base Cmd ever vanishes; nothing is gated.\n * 3. **Every decision is a Msg/Cmd.** Each telemetry emission is a\n * `$telemetry:emit` Cmd through the merged `update`, so \"why did this fire\"\n * is answerable from reducer + log alone, and `runtime.observe` sees it.\n * 4. **Subs merge by id.** Telemetry has no timers — merged subs/subscribe are\n * the base's, verbatim.\n * 5. **Clock out of update.** The default event carries no wall-clock read.\n * The optional `at` is stamped at the interpret boundary (the `clock`\n * port), never inside the merged `update`.\n *\n * ## Where the clock lives\n *\n * Inside the merged `update`: nowhere. The reducer is pure — it reads `seq` from\n * the slice and projects the event from `(msg, prev, next)`. The ONLY clock read\n * is at the interpret boundary: the `$telemetry:emit` handler stamps `event.at`\n * from the injected `clock` port (if supplied and the projector left `at`\n * unset), exactly as `resilient-call` reads `Date.now()` only inside `handlers`.\n *\n * ## Typical wiring\n *\n * const observed = withTelemetry(baseMachine, {\n * event: (msg, _prev, next) => ({ seq: next.$telemetry.seq, msgType: msg.type }),\n * });\n * const runtime = run(observed, {\n * ctx: { ...baseCtx, telemetrySink: (e) => posthog.capture(e), clock: Date.now },\n * });\n */\n\nimport type {\n Cmd,\n Interpret,\n Machine,\n Reducer,\n Sub,\n UpdateForm,\n} from \"../index\";\nimport { formOf } from \"../index\";\n\n// ===========================================================================\n// The event + the slice + the ports — the telemetry vocabulary.\n// ===========================================================================\n\n/**\n * The datum handed to the sink. The default projector emits `{ seq, msgType }`;\n * a custom `event` projector may return any extra fields. `at` is OPTIONAL and\n * plain data — it is stamped at the interpret boundary by the `clock` port, not\n * inside the (pure, clock-free) merged `update`. Every field is JSON-round-trip\n * serializable so a recorded telemetry stream stays durable.\n */\nexport interface TelemetryEvent {\n /** Monotonic event counter — equals `$telemetry.seq` after the transition. */\n readonly seq: number;\n /** The `Msg.type` discriminant that drove this transition. */\n readonly msgType: string;\n /**\n * Wall-clock instant the event was emitted, ms since epoch. Absent until the\n * interpret boundary stamps it (the `clock` port). Optional because the\n * merged `update` is clock-free — a verb must never read `Date.now()`.\n */\n readonly at?: number;\n}\n\n/**\n * The wrapper's Model slice. A monotonic event counter — minimal but real, so\n * the named-slice mechanism is exercised for the richer-slice wrappers that\n * follow. `seq` equals the number of base Msgs folded so far: after replaying\n * `N` msgs, `$telemetry.seq === N`. Plain data → serializes into a `Store<S>`.\n */\nexport interface TelemetrySlice {\n readonly seq: number;\n}\n\n/**\n * The composed Model. The base machine's state is nested under `base`; the\n * wrapper's state lives in the NAMED `$telemetry` slice — never hidden in a\n * closure. Both fields are plain data, so the whole composed Model round-trips\n * through `JSON.parse(JSON.stringify(...))` iff the base state does.\n */\nexport interface TelemetryModel<S> {\n readonly base: S;\n readonly $telemetry: TelemetrySlice;\n}\n\n/**\n * The fire-and-forget Cmd the merged `update` appends after every base\n * transition. It carries the projected `event` as plain data (invariant 3 —\n * no closure). The interpret handler hands `event` to the sink and dispatches\n * NOTHING back, so there is no feedback loop.\n */\nexport type TelemetryEmitCmd = Cmd<\"$telemetry:emit\"> & {\n readonly event: TelemetryEvent;\n};\n\n/**\n * The side-effecting ports the wrapper adds to `Ctx`. The sink (PostHog / Sentry\n * bridge) lives at the interpret boundary, never in a verb. `clock` is the\n * optional wall-clock source used to stamp `event.at` at that same boundary —\n * pass `Date.now` in production, a fixed `() => 1000` in tests; omit it to leave\n * `at` unset.\n */\nexport interface TelemetryPorts {\n /** The side-effecting sink. Fire-and-forget — its result is never awaited into a Msg. */\n readonly telemetrySink: (event: TelemetryEvent) => void | Promise<void>;\n /** Optional clock for stamping `event.at` at the interpret boundary. */\n readonly clock?: () => number;\n}\n\n/**\n * The telemetry knob. `event` is an OPTIONAL projector mapping\n * `(msg, prev, next)` to a `TelemetryEvent`; omit it and the default emits\n * `{ seq, msgType }` where `seq` is the post-transition counter. The projector\n * runs INSIDE the pure merged `update`, so it must not read the clock — stamp\n * `at` at the sink boundary via the `clock` port instead.\n */\nexport interface TelemetryConfig<S, M extends { type: string }> {\n readonly event?: (\n msg: M,\n prev: TelemetryModel<S>,\n next: TelemetryModel<S>,\n ) => TelemetryEvent;\n}\n\n// ===========================================================================\n// update-form dispatch — run the base reducer regardless of its shape.\n// ===========================================================================\n\n// The base `update` is either a Reducer (flat record keyed by Msg.type) or a\n// Transitions table (keyed by state.type then Msg.type). The merged machine is\n// always a Reducer over the NON-discriminated `TelemetryModel<S>`, so the merged\n// reducer must invoke the base's update through whichever shape it has. This\n// mirrors the form-branching `run` and `replay` do internally; we keep it inline\n// (not factored into a shared helper) so the wrapper stays standalone.\ntype BaseCellFn<S, M, C extends Cmd> = (\n state: S,\n msg: M,\n) => readonly [S, readonly C[]];\n\n/**\n * Invoke the base machine's `update` for `msg` against base `state`, returning\n * the base's `[nextBase, baseCmds]` UNCHANGED. Pure — it only routes to the\n * base cell; it never reads the clock, allocates an Error, or mutates state.\n */\nfunction runBaseUpdate<S, M extends { type: string }, C extends Cmd>(\n // Typed `object` (not `Machine[...][\"update\"]`) because the field is inspected\n // structurally and re-cast below; the precise union (Reducer | Transitions)\n // collapses under the substrate's conditional `update` type, so the wrapper\n // accepts the erased shape and dispatches on the runtime form.\n update: object,\n // The base's update form, read once via `formOf(base)` — no local heuristic.\n form: UpdateForm,\n state: S,\n msg: M,\n): readonly [S, readonly C[]] {\n if (form === \"reducer\") {\n // Reducer form: `update[msg.type](state, msg)`.\n const record = update as unknown as Record<string, BaseCellFn<S, M, C>>;\n // biome-ignore lint/style/noNonNullAssertion: the base machine's Reducer guarantees a cell for every dispatched Msg.type; `!` required under noUncheckedIndexedAccess and cannot miss for a Msg the base accepts\n return record[msg.type]!(state, msg);\n }\n // Transitions form: `update[state.type][msg.type](state, msg)`.\n const table = update as unknown as Record<\n string,\n Record<string, BaseCellFn<S, M, C>>\n >;\n const stateKey = (state as unknown as { type: string }).type;\n // biome-ignore lint/style/noNonNullAssertion: the base machine's Transitions table guarantees every (state.type × msg.type) cell exists; `!` required under noUncheckedIndexedAccess and cannot miss at runtime\n return table[stateKey]![msg.type]!(state, msg);\n}\n\n// ===========================================================================\n// withTelemetry — the wrapper.\n// ===========================================================================\n\n/**\n * Wrap `base` with observe-only telemetry. Returns a NEW `Machine` over the\n * composed Model `{ base, $telemetry }`, with the base's Msgs/Cmds/Subs/Ctx\n * extended by the wrapper's own.\n *\n * The composed machine:\n * - `init` rehydrates the base (honoring the `[loaded, []]` contract) and\n * seeds `$telemetry: { seq: 0 }`. Base init Cmds pass through unchanged.\n * - `update` (Reducer form, keyed by every base `Msg.type`) runs the base\n * reducer, increments `seq`, and appends a `$telemetry:emit` Cmd carrying\n * the projected event. Base Cmds pass through UNCHANGED — the observe-only\n * property.\n * - `subscriptions` / `subscribe` are the base's, verbatim (no timers).\n * - `interpret` is `{ ...base.interpret, \"$telemetry:emit\": sink-handler }`.\n *\n * @param base any `Machine<S, M, C, U, Ctx>`.\n * @param config the telemetry knob (optional `event` projector).\n */\nexport function withTelemetry<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n base: Machine<S, M, C, U, Ctx>,\n config: TelemetryConfig<S, M> = {},\n): Machine<\n TelemetryModel<S>,\n M,\n C | TelemetryEmitCmd,\n U,\n Ctx & TelemetryPorts\n> {\n const project = config.event;\n\n // The merged update is a flat Reducer over the composed Model. The composed\n // Model is NOT discriminated on `.type`, so the Transitions form is never an\n // option here — every base Msg variant gets exactly one cell. We build the\n // record by enumerating the base update's Msg keys: a Reducer's keys ARE the\n // Msg.type set; a Transitions table's inner keys are too (uniform across\n // phases), so we read the first phase's keys to recover them.\n // The base's update form, read ONCE from the `__form` tag (see `formOf`) and\n // threaded into the per-form helpers — no structural re-derivation here.\n const baseForm = formOf(base);\n const msgKeys = telemetryMsgKeys(base.update, baseForm);\n\n const update = {} as Record<\n string,\n (\n state: TelemetryModel<S>,\n msg: M,\n ) => readonly [TelemetryModel<S>, readonly (C | TelemetryEmitCmd)[]]\n >;\n for (const key of msgKeys) {\n update[key] = (state, msg) => {\n // 1) Run the base reducer — its result passes through UNCHANGED.\n const [nextBase, baseCmds] = runBaseUpdate<S, M, C>(\n base.update,\n baseForm,\n state.base,\n msg,\n );\n // 2) Advance the wrapper slice (monotonic counter). New object — no mutation.\n const $telemetry: TelemetrySlice = { seq: state.$telemetry.seq + 1 };\n const next: TelemetryModel<S> = { base: nextBase, $telemetry };\n // 3) Project the event (pure — no clock). Default: { seq, msgType }.\n const event: TelemetryEvent = project\n ? project(msg, state, next)\n : { seq: $telemetry.seq, msgType: msg.type };\n // 4) Append the fire-and-forget emit Cmd. Base Cmds come FIRST, unchanged.\n const emit: TelemetryEmitCmd = { type: \"$telemetry:emit\", event };\n return [next, [...baseCmds, emit]];\n };\n }\n\n // The base's interpret, extended with the one telemetry handler. The sink is\n // the ONLY side effect, and it lives here at the interpret boundary — never in\n // a verb. `clock` (if provided) stamps `event.at` here too, the one permitted\n // clock read. The handler returns void synchronously — it dispatches NOTHING\n // back (no feedback loop) AND it does NOT await the sink (no backpressure /\n // crash from a slow or failing sink). See the fire-and-forget note below.\n const baseInterpret =\n (base as { interpret?: Interpret<M, C, Ctx> }).interpret ??\n ({} as Interpret<M, C, Ctx>);\n // Namespace guard (mirrors the substrate's SubId / definePort collision\n // asserts). The spread below merges `$telemetry:emit` into the base's\n // interpret map; if the base already declares that key, the spread would\n // SILENTLY clobber the base handler. The `$telemetry:` Cmd namespace is the\n // wrapper's — refuse to wrap a base that squats on it instead of stealing it.\n if (Object.hasOwn(baseInterpret as object, \"$telemetry:emit\")) {\n throw new Error(\n 'withTelemetry: the base machine declares a reserved \"$telemetry:emit\" ' +\n 'interpret handler — the wrapper owns the \"$telemetry:\" Cmd namespace. ' +\n \"Rename the base handler.\",\n );\n }\n const interpret = {\n ...(baseInterpret as Record<string, unknown>),\n // OBSERVE-ONLY at the boundary: the sink is fire-and-forget. The runtime\n // `await`s every interpret handler in its dispatch loop, so awaiting the\n // sink here would let a SLOW sink backpressure the wrapped machine (its\n // next dispatch waits) and a FAILING sink crash dispatch (the throw would\n // propagate out of the transition). Both violate the contract: telemetry\n // must never affect base behavior OR latency. So we DO NOT await — we hand\n // the event to the sink, normalize its result to a Promise, swallow any\n // rejection/throw, and return immediately. The handler resolves in the same\n // microtask the dispatch loop reaches it; the machine never waits on the\n // sink, and a rejected sink can never become an unhandled rejection.\n \"$telemetry:emit\": (\n cmd: TelemetryEmitCmd,\n ctx: Ctx & TelemetryPorts,\n ): void => {\n const event =\n cmd.event.at === undefined && ctx.clock !== undefined\n ? { ...cmd.event, at: ctx.clock() }\n : cmd.event;\n // `Promise.resolve(fn())` covers BOTH failure shapes: a sync `throw` is\n // caught by `Promise.resolve` (the call is the argument, so a throw here\n // would escape — guard it), and an async rejection is caught by `.catch`.\n try {\n Promise.resolve(ctx.telemetrySink(event)).catch(() => {});\n } catch {\n // A sink that throws SYNCHRONOUSLY (before returning a Promise) is\n // swallowed here. Either way the slow/failing sink never reaches the\n // dispatch loop's `await`.\n }\n },\n } as Interpret<M, C | TelemetryEmitCmd, Ctx & TelemetryPorts>;\n\n // Capture once so the narrowing survives into the spread (no non-null assert).\n const baseSubscriptions = base.subscriptions;\n const baseSubscribe = (base as { subscribe?: unknown }).subscribe;\n\n return {\n init: (loaded, ctx) => {\n // Rehydrate path: `loaded !== null` MUST return `[loaded, []]` (no Cmds) —\n // the substrate's replay enforces this. We split the composed loaded Model\n // into its base slice and feed the base its own loaded snapshot, so the\n // base's rehydrate contract is honored transitively.\n if (loaded !== null) {\n return [loaded, []];\n }\n const [base0, cmds] = base.init(null, ctx);\n const model: TelemetryModel<S> = { base: base0, $telemetry: { seq: 0 } };\n // Base init Cmds pass through unchanged; no telemetry Cmd on boot (boot is\n // not a base Msg transition, so it does not advance `seq`).\n return [model, cmds];\n },\n update: update as unknown as Reducer<\n TelemetryModel<S>,\n M,\n C | TelemetryEmitCmd\n >,\n // Subs merge to the base's, verbatim — telemetry owns no timers. The base\n // subscribes against its own nested slice.\n ...(baseSubscriptions\n ? {\n subscriptions: (state: TelemetryModel<S>) =>\n baseSubscriptions(state.base),\n }\n : {}),\n ...(baseSubscribe ? { subscribe: baseSubscribe } : {}),\n interpret,\n } as Machine<\n TelemetryModel<S>,\n M,\n C | TelemetryEmitCmd,\n U,\n Ctx & TelemetryPorts\n >;\n}\n\n// Recover the base Msg.type set from either update form. A Reducer's own keys\n// ARE the Msg.type set. A Transitions table's keys are state.type; its INNER\n// keys are the Msg.type set (uniform across phases by the mapped-type\n// contract), so we read the first phase's inner keys. An empty update (`M` is\n// `never`) yields `[]` — the merged reducer then has no cells, which is correct\n// because no Msg can ever be dispatched.\nfunction telemetryMsgKeys(update: object, form: UpdateForm): readonly string[] {\n const keys = Object.keys(update);\n const firstKey = keys[0];\n if (firstKey === undefined) return [];\n if (form === \"reducer\") {\n // Reducer form — keys are Msg.type.\n return keys;\n }\n // Transitions form — keys are state.type; inner keys are Msg.type.\n const firstValue = (update as Record<string, unknown>)[firstKey];\n return Object.keys(firstValue as object);\n}\n"]}
@@ -0,0 +1,82 @@
1
+ import { QueueItem } from './index.js';
2
+ import '../index.js';
3
+ import '../core-CymXZCeY.js';
4
+
5
+ /**
6
+ * @demlik/tea/work-queue/adapter — the verb seam over the pure queue ops.
7
+ *
8
+ * The pure ops in `./ops` are the BLESSED transitions, but their names and
9
+ * argument tuples (`enqueueOp(queue, input, now, id)`, `claimNextOp(queue,
10
+ * now)`, …) and their `{ next, item }` / `{ next, changed }` result shapes are
11
+ * the STORE's vocabulary, not the consumer's. An L2 composition that owns a
12
+ * `QueueItem<I>[]` slice (`monitored-run`, `saga`, `idempotent-intake`,
13
+ * `fan-out`) used to import those ops by name and call them directly, so a
14
+ * change to the op surface — or to `QueueItem`'s fields — cascaded to every
15
+ * call site.
16
+ *
17
+ * `QueueAdapter<I>` is the insulating interface: four VERBS (`enqueue` /
18
+ * `claim` / `markDone` / `patch`) that consumers depend on instead of the raw
19
+ * ops. The verbs keep the ops' pure `(queue, …) => { next, … }` contract — they
20
+ * are pure array transforms, no I/O, no `Date.now` (the clock is injected as
21
+ * `now`, exactly as the ops require it) — so a consumer folding the adapter
22
+ * through its reducer stays inside invariant 2 (pure transitions).
23
+ *
24
+ * `queueAdapter<I>()` is the canonical implementation, a thin binding of the
25
+ * blessed ops. Because the verbs are the only surface consumers touch, a
26
+ * `QueueItem` shape change (a new field, a renamed status, a different op
27
+ * signature) is now absorbed HERE — re-bind the verb once — rather than at the
28
+ * 4 consumer call sites. The `patch` verb deliberately takes the same
29
+ * `(item) => item` closure `patchItemOp` does, so the one place a `QueueItem`
30
+ * literal is spread stays the consumer's patch closure, never a re-rolled
31
+ * record.
32
+ *
33
+ * This is an INTERNAL seam (no public-API port): consumers import the adapter
34
+ * from this subpath the same way they imported the ops. The ops remain
35
+ * exported for the in-process `createQueue` adapter (`./index`) and for any
36
+ * call site that genuinely wants the op directly.
37
+ */
38
+
39
+ /**
40
+ * The verb interface over a `QueueItem<I>[]` slice. Each verb mirrors a blessed
41
+ * op's pure contract but speaks the consumer's lifecycle vocabulary, so the
42
+ * consumer never names a `*Op` function or reaches into a `QueueItem` field it
43
+ * did not put there.
44
+ */
45
+ interface QueueAdapter<I> {
46
+ /** Append a new `pending` item stamped `now` under `id`. */
47
+ enqueue(queue: readonly QueueItem<I>[], input: I, now: number, id: string): {
48
+ next: QueueItem<I>[];
49
+ item: QueueItem<I>;
50
+ };
51
+ /**
52
+ * Claim the first `pending` item — flip it `running`, stamp `startedAt` to
53
+ * `now` — or `null` if nothing is pending.
54
+ */
55
+ claim(queue: readonly QueueItem<I>[], now: number): {
56
+ next: QueueItem<I>[];
57
+ claimed: QueueItem<I>;
58
+ } | null;
59
+ /** Drop the item `id` from the queue (completed runs leave the queue). */
60
+ markDone(queue: readonly QueueItem<I>[], id: string): {
61
+ next: QueueItem<I>[];
62
+ changed: boolean;
63
+ };
64
+ /**
65
+ * Single-item transition: replace the item `id` with `patch(item)`. The
66
+ * closure is the only place a `QueueItem` literal is constructed, so the
67
+ * field shape stays out of the consumer's hands except through this verb.
68
+ */
69
+ patch(queue: readonly QueueItem<I>[], id: string, patch: (item: QueueItem<I>) => QueueItem<I>): {
70
+ next: QueueItem<I>[];
71
+ changed: boolean;
72
+ };
73
+ }
74
+ /**
75
+ * The canonical `QueueAdapter<I>`, binding each verb to its blessed op. Pure
76
+ * and stateless — call once at module scope in a consumer and fold its verbs
77
+ * through the reducer. A `QueueItem` shape change is absorbed by re-binding a
78
+ * verb here, not at the consumers.
79
+ */
80
+ declare function queueAdapter<I>(): QueueAdapter<I>;
81
+
82
+ export { type QueueAdapter, queueAdapter };
@@ -0,0 +1,4 @@
1
+ export { queueAdapter } from '../chunk-HJO6VNQI.js';
2
+ import '../chunk-TGG26XMK.js';
3
+ //# sourceMappingURL=adapter.js.map
4
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"adapter.js"}
@@ -1,11 +1,18 @@
1
1
  import { Store } from '../index.js';
2
+ import '../core-CymXZCeY.js';
2
3
 
3
4
  /**
4
5
  * @demlik/tea/work-queue — substrate-agnostic work-queue lifecycle on `Store<S>`.
5
6
  *
6
- * The pure ops live in `./ops` and are NOT exported. Tests and consumers reach
7
- * them only through `createQueue(store)`. This mirrors `@demlik/tea/do`'s
8
- * one-shape-per-package rule: a single adapter is the public surface.
7
+ * Two public surfaces, one shape:
8
+ * - `createQueue(store)` (this file) is the in-process adapter — it binds the
9
+ * pure ops to an injected `Store` and runs `load → mutate → save`.
10
+ * - `@demlik/tea/work-queue/ops` (`./ops`) is the BLESSED, importable surface
11
+ * of those same pure ops. L2 compositions that own their own Model slice
12
+ * (`monitored-run`, `idempotent-intake`, …) import the ops directly and
13
+ * delegate status flips to them instead of re-rolling the transitions.
14
+ * Both routes call the identical ops, so the lifecycle rules live in exactly
15
+ * one place. This mirrors `@demlik/tea/do`'s one-shape-per-package rule.
9
16
  *
10
17
  * The package is generic in the input payload `I` (audit-specific fields go
11
18
  * here in apps/extension). `output` is a free-form side-channel correlation
@@ -15,15 +22,22 @@ import { Store } from '../index.js';
15
22
  */
16
23
 
17
24
  type QueueItemStatus = "pending" | "running" | "done" | "failed" | "cancelled";
25
+ /**
26
+ * A single queue entry. Every field is `readonly`: a persisted slice is
27
+ * immutable, so a transition returns a NEW item (the ops always spread —
28
+ * `{ ...item, status: … }`) rather than mutating in place. This type-enforces
29
+ * the durability invariant the consumers already honor by convention, and
30
+ * keeps a slice round-trippable through JSON unchanged across a reload.
31
+ */
18
32
  interface QueueItem<I> {
19
- id: string;
20
- status: QueueItemStatus;
21
- input: I;
22
- enqueuedAt: number;
23
- startedAt?: number;
24
- finishedAt?: number;
25
- output?: string;
26
- error?: string;
33
+ readonly id: string;
34
+ readonly status: QueueItemStatus;
35
+ readonly input: I;
36
+ readonly enqueuedAt: number;
37
+ readonly startedAt?: number;
38
+ readonly finishedAt?: number;
39
+ readonly output?: string;
40
+ readonly error?: string;
27
41
  }
28
42
  /**
29
43
  * Caller-facing enqueue payload. Just `I` today — kept named so future
@@ -1,49 +1,4 @@
1
- // src/work-queue/ops.ts
2
- function enqueueOp(queue, input, now, id) {
3
- const item = {
4
- id,
5
- status: "pending",
6
- input,
7
- enqueuedAt: now
8
- };
9
- return { next: [...queue, item], item };
10
- }
11
- function claimNextOp(queue, now) {
12
- const idx = queue.findIndex((i) => i.status === "pending");
13
- if (idx === -1) return null;
14
- const claimed = {
15
- ...queue[idx],
16
- status: "running",
17
- startedAt: now
18
- };
19
- const next = queue.slice();
20
- next[idx] = claimed;
21
- return { next, claimed };
22
- }
23
- function markDoneOp(queue, id) {
24
- const next = queue.filter((i) => i.id !== id);
25
- return { next, changed: next.length !== queue.length };
26
- }
27
- function patchItemOp(queue, id, patch) {
28
- const idx = queue.findIndex((i) => i.id === id);
29
- if (idx === -1) return { next: queue.slice(), changed: false };
30
- const next = queue.slice();
31
- next[idx] = patch(queue[idx]);
32
- return { next, changed: true };
33
- }
34
- function resetRunningOp(queue) {
35
- let changed = false;
36
- const next = queue.map((i) => {
37
- if (i.status !== "running") return i;
38
- changed = true;
39
- return { ...i, status: "pending", startedAt: void 0 };
40
- });
41
- return { next, changed };
42
- }
43
- function removeOp(queue, id) {
44
- const next = queue.filter((i) => i.id !== id);
45
- return { next, changed: next.length !== queue.length };
46
- }
1
+ import { enqueueOp, claimNextOp, markDoneOp, patchItemOp, resetRunningOp, removeOp } from '../chunk-TGG26XMK.js';
47
2
 
48
3
  // src/work-queue/index.ts
49
4
  function genId() {