@demlik/tea 0.1.0 → 0.1.2

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 (243) 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-27N2SPTN.js +222 -0
  18. package/dist/chunk-27N2SPTN.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-3XJWC7VW.js +61 -0
  24. package/dist/chunk-3XJWC7VW.js.map +1 -0
  25. package/dist/chunk-5DEQ5I3U.js +132 -0
  26. package/dist/chunk-5DEQ5I3U.js.map +1 -0
  27. package/dist/chunk-5VNWZADP.js +49 -0
  28. package/dist/chunk-5VNWZADP.js.map +1 -0
  29. package/dist/chunk-6RGBKODV.js +268 -0
  30. package/dist/chunk-6RGBKODV.js.map +1 -0
  31. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  32. package/dist/chunk-7FYQGDMF.js.map +1 -0
  33. package/dist/chunk-7W4LMPUW.js +186 -0
  34. package/dist/chunk-7W4LMPUW.js.map +1 -0
  35. package/dist/chunk-BYQUGDD3.js +16 -0
  36. package/dist/chunk-BYQUGDD3.js.map +1 -0
  37. package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
  38. package/dist/chunk-DILMKOZ7.js.map +1 -0
  39. package/dist/chunk-EHIX7NYZ.js +93 -0
  40. package/dist/chunk-EHIX7NYZ.js.map +1 -0
  41. package/dist/chunk-HCEVHFJ7.js +54 -0
  42. package/dist/chunk-HCEVHFJ7.js.map +1 -0
  43. package/dist/chunk-HJO6VNQI.js +15 -0
  44. package/dist/chunk-HJO6VNQI.js.map +1 -0
  45. package/dist/chunk-IN33Q4VJ.js +52 -0
  46. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  47. package/dist/chunk-J6527VRQ.js +56 -0
  48. package/dist/chunk-J6527VRQ.js.map +1 -0
  49. package/dist/chunk-JELSZN25.js +47 -0
  50. package/dist/chunk-JELSZN25.js.map +1 -0
  51. package/dist/chunk-K575PT4S.js +58 -0
  52. package/dist/chunk-K575PT4S.js.map +1 -0
  53. package/dist/chunk-K7Q23EQ4.js +20 -0
  54. package/dist/chunk-K7Q23EQ4.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-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
  58. package/dist/chunk-LP7VQBRG.js.map +1 -0
  59. package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
  60. package/dist/chunk-MMH7CW5K.js.map +1 -0
  61. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  62. package/dist/chunk-NSDFA6M6.js.map +1 -0
  63. package/dist/chunk-ON24RKSE.js +60 -0
  64. package/dist/chunk-ON24RKSE.js.map +1 -0
  65. package/dist/chunk-PS23H6KQ.js +87 -0
  66. package/dist/chunk-PS23H6KQ.js.map +1 -0
  67. package/dist/chunk-QLA76GIT.js +162 -0
  68. package/dist/chunk-QLA76GIT.js.map +1 -0
  69. package/dist/chunk-S3FQN4SL.js +506 -0
  70. package/dist/chunk-S3FQN4SL.js.map +1 -0
  71. package/dist/chunk-SC3FAFZN.js +53 -0
  72. package/dist/chunk-SC3FAFZN.js.map +1 -0
  73. package/dist/chunk-SINXE47Y.js +55 -0
  74. package/dist/chunk-SINXE47Y.js.map +1 -0
  75. package/dist/chunk-SQTCS44W.js +33 -0
  76. package/dist/chunk-SQTCS44W.js.map +1 -0
  77. package/dist/chunk-TGG26XMK.js +52 -0
  78. package/dist/chunk-TGG26XMK.js.map +1 -0
  79. package/dist/chunk-UV6UWGEO.js +147 -0
  80. package/dist/chunk-UV6UWGEO.js.map +1 -0
  81. package/dist/chunk-UYWUVEP6.js +158 -0
  82. package/dist/chunk-UYWUVEP6.js.map +1 -0
  83. package/dist/chunk-VLGARN6H.js +202 -0
  84. package/dist/chunk-VLGARN6H.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-BoB6t7dB.d.ts +188 -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.d.ts +6 -0
  92. package/dist/debounce/index.js +1 -54
  93. package/dist/debounce/index.js.map +1 -1
  94. package/dist/devtools/index.d.ts +1 -1
  95. package/dist/devtools/index.js +18 -6
  96. package/dist/devtools/index.js.map +1 -1
  97. package/dist/do/index.d.ts +1723 -92
  98. package/dist/do/index.js +673 -31
  99. package/dist/do/index.js.map +1 -1
  100. package/dist/durable-effects-CO9Uh971.d.ts +180 -0
  101. package/dist/extension/index.d.ts +31 -13
  102. package/dist/extension/index.js +5 -3
  103. package/dist/extension/index.js.map +1 -1
  104. package/dist/extension/react.js +8 -3
  105. package/dist/extension/react.js.map +1 -1
  106. package/dist/extension/subs/index.d.ts +2 -2
  107. package/dist/extension/subs/index.js.map +1 -1
  108. package/dist/extension/test-utils.js +11 -3
  109. package/dist/extension/test-utils.js.map +1 -1
  110. package/dist/fan-out/index.d.ts +239 -0
  111. package/dist/fan-out/index.js +5 -0
  112. package/dist/fan-out/index.js.map +1 -0
  113. package/dist/idempotency/adapter.d.ts +62 -0
  114. package/dist/idempotency/adapter.js +4 -0
  115. package/dist/idempotency/adapter.js.map +1 -0
  116. package/dist/idempotency/index.js +1 -56
  117. package/dist/idempotency/index.js.map +1 -1
  118. package/dist/idempotent-intake/index.d.ts +186 -0
  119. package/dist/idempotent-intake/index.js +7 -0
  120. package/dist/idempotent-intake/index.js.map +1 -0
  121. package/dist/index.d.ts +341 -191
  122. package/dist/index.js +2 -1
  123. package/dist/llm-call/index.d.ts +309 -0
  124. package/dist/llm-call/index.js +14 -0
  125. package/dist/llm-call/index.js.map +1 -0
  126. package/dist/machine-viz/index.d.ts +4 -4
  127. package/dist/machine-viz/index.js +5 -9
  128. package/dist/machine-viz/index.js.map +1 -1
  129. package/dist/mem/index.d.ts +1 -0
  130. package/dist/monitored-run/index.d.ts +253 -0
  131. package/dist/monitored-run/index.js +10 -0
  132. package/dist/monitored-run/index.js.map +1 -0
  133. package/dist/node/index.d.ts +5 -5
  134. package/dist/node/index.js.map +1 -1
  135. package/dist/paginated-walk/index.d.ts +234 -0
  136. package/dist/paginated-walk/index.js +15 -0
  137. package/dist/paginated-walk/index.js.map +1 -0
  138. package/dist/paginator/index.d.ts +271 -0
  139. package/dist/paginator/index.js +3 -0
  140. package/dist/paginator/index.js.map +1 -0
  141. package/dist/parity/index.d.ts +92 -0
  142. package/dist/parity/index.js +78 -0
  143. package/dist/parity/index.js.map +1 -0
  144. package/dist/pbt/arbitraries/index.d.ts +1 -1
  145. package/dist/pbt/arbitraries/index.js +1 -1
  146. package/dist/pbt/index.d.ts +15 -8
  147. package/dist/pbt/index.js +6 -11
  148. package/dist/pbt/index.js.map +1 -1
  149. package/dist/pbt/runners/index.d.ts +1 -1
  150. package/dist/pbt/runners/index.js +3 -2
  151. package/dist/poller/index.d.ts +288 -0
  152. package/dist/poller/index.js +79 -0
  153. package/dist/poller/index.js.map +1 -0
  154. package/dist/prediction/index.d.ts +130 -0
  155. package/dist/prediction/index.js +4 -0
  156. package/dist/prediction/index.js.map +1 -0
  157. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  158. package/dist/pure/index.d.ts +2 -0
  159. package/dist/pure/index.js +4 -0
  160. package/dist/pure/index.js.map +1 -0
  161. package/dist/raft/demo.d.ts +105 -0
  162. package/dist/raft/demo.js +172 -0
  163. package/dist/raft/demo.js.map +1 -0
  164. package/dist/raft/index.d.ts +548 -0
  165. package/dist/raft/index.js +7 -0
  166. package/dist/raft/index.js.map +1 -0
  167. package/dist/raft/sim.d.ts +222 -0
  168. package/dist/raft/sim.js +8 -0
  169. package/dist/raft/sim.js.map +1 -0
  170. package/dist/raft/timeline.d.ts +46 -0
  171. package/dist/raft/timeline.js +62 -0
  172. package/dist/raft/timeline.js.map +1 -0
  173. package/dist/rate-limit/index.js +1 -43
  174. package/dist/rate-limit/index.js.map +1 -1
  175. package/dist/react/index.d.ts +2 -1
  176. package/dist/react/index.js +43 -20
  177. package/dist/react/index.js.map +1 -1
  178. package/dist/reconciler/index.d.ts +192 -0
  179. package/dist/reconciler/index.js +166 -0
  180. package/dist/reconciler/index.js.map +1 -0
  181. package/dist/recorder/index.d.ts +7 -3
  182. package/dist/recorder/index.js +1 -155
  183. package/dist/recorder/index.js.map +1 -1
  184. package/dist/resilient-call/index.d.ts +294 -0
  185. package/dist/resilient-call/index.js +13 -0
  186. package/dist/resilient-call/index.js.map +1 -0
  187. package/dist/retry-backoff/index.d.ts +42 -5
  188. package/dist/retry-backoff/index.js +1 -35
  189. package/dist/retry-backoff/index.js.map +1 -1
  190. package/dist/retry-to-success/index.d.ts +109 -0
  191. package/dist/retry-to-success/index.js +35 -0
  192. package/dist/retry-to-success/index.js.map +1 -0
  193. package/dist/saga/index.d.ts +238 -0
  194. package/dist/saga/index.js +150 -0
  195. package/dist/saga/index.js.map +1 -0
  196. package/dist/snapshot/index.d.ts +334 -0
  197. package/dist/snapshot/index.js +5 -0
  198. package/dist/snapshot/index.js.map +1 -0
  199. package/dist/subs/index.d.ts +48 -10
  200. package/dist/subs/index.js +1 -1
  201. package/dist/testing/index.d.ts +107 -2
  202. package/dist/testing/index.js +204 -6
  203. package/dist/testing/index.js.map +1 -1
  204. package/dist/throttle/index.d.ts +9 -0
  205. package/dist/throttle/index.js +1 -52
  206. package/dist/throttle/index.js.map +1 -1
  207. package/dist/throttled-input/index.d.ts +380 -0
  208. package/dist/throttled-input/index.js +88 -0
  209. package/dist/throttled-input/index.js.map +1 -0
  210. package/dist/token-refresh/index.d.ts +215 -0
  211. package/dist/token-refresh/index.js +6 -0
  212. package/dist/token-refresh/index.js.map +1 -0
  213. package/dist/trace-replay/index.d.ts +11 -2
  214. package/dist/trace-replay/index.js +3 -55
  215. package/dist/trace-replay/index.js.map +1 -1
  216. package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
  217. package/dist/with-deadline/index.d.ts +196 -0
  218. package/dist/with-deadline/index.js +111 -0
  219. package/dist/with-deadline/index.js.map +1 -0
  220. package/dist/with-resilience/index.d.ts +205 -0
  221. package/dist/with-resilience/index.js +251 -0
  222. package/dist/with-resilience/index.js.map +1 -0
  223. package/dist/with-telemetry/index.d.ts +149 -0
  224. package/dist/with-telemetry/index.js +70 -0
  225. package/dist/with-telemetry/index.js.map +1 -0
  226. package/dist/work-queue/adapter.d.ts +82 -0
  227. package/dist/work-queue/adapter.js +4 -0
  228. package/dist/work-queue/adapter.js.map +1 -0
  229. package/dist/work-queue/index.d.ts +25 -11
  230. package/dist/work-queue/index.js +1 -46
  231. package/dist/work-queue/index.js.map +1 -1
  232. package/dist/work-queue/ops.d.ts +69 -0
  233. package/dist/work-queue/ops.js +3 -0
  234. package/dist/work-queue/ops.js.map +1 -0
  235. package/dist/workflow/index.d.ts +450 -0
  236. package/dist/workflow/index.js +225 -0
  237. package/dist/workflow/index.js.map +1 -0
  238. package/package.json +168 -25
  239. package/dist/chunk-CMG2KMWX.js.map +0 -1
  240. package/dist/chunk-PYBQREFU.js.map +0 -1
  241. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  242. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  243. package/dist/chunk-ZRUST6BK.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/with-deadline/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/with-deadline — auto-fail a wrapped machine at T+N, re-arm on\n * progress. A **pure function over machine data**: `withDeadline(base, config)`\n * takes any `Machine<S, M, C, U, Ctx>` and returns a NEW `Machine` over the\n * composed Model `{ base, $deadline }`. You `run()` / `replay()` the result\n * exactly like any other machine — there is no new runtime, no privileged\n * interception path.\n *\n * ## What it does\n *\n * The wrapper watches the base Msg stream. While the deadline is `armed`, a\n * RELATIVE timeout Sub counts down `config.ms`. Every base Msg the\n * `config.progress` predicate accepts (default: every base Msg) is treated as\n * activity: the wrapper bumps a numeric `seq`, which CHANGES the timer Sub's id,\n * so the substrate's reconcile pass retires the old countdown and starts a fresh\n * one — \"re-arm on progress\". If `config.ms` elapses with NO accepted progress,\n * the timeout Sub dispatches `$deadline:exceeded`; the merged `update` folds it\n * by flipping `phase` to `exceeded`, and from then on `subscriptions` returns no\n * timer (the machine has auto-failed). The expiry is a slice transition visible\n * in the Msg log — the auto-fail is never an off-ledger side effect.\n *\n * This is an OBSERVE-ONLY wrapper in the same sense as `withTelemetry`: it never\n * intercepts a base Cmd and never gates a base Msg. `state.base` after wrapping\n * is BYTE-IDENTICAL to running the base alone, so the shared\n * `assertWrapperFaithful` gate passes with the deadline bolted on.\n *\n * ## The 5 rules it satisfies (from the wrapper-tier contract)\n *\n * 1. **Named, serializable slice.** Wrapper state lives at `model.$deadline`,\n * a plain `{ phase, seq }` — JSON-serializable, never a closure. There is\n * NO absolute timestamp in the slice: the clock is never read in `update`.\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 * `$deadline:rearm` / `$deadline:expire` decision Cmd. No base Cmd ever\n * vanishes; nothing is gated.\n * 3. **Every decision is a Msg/Cmd.** Re-arm is a `$deadline:rearm` Cmd; the\n * auto-fail expiry is the `$deadline:exceeded` Msg the Sub delivers, folded\n * into the slice and echoed as a `$deadline:expire` Cmd. \"Why did this\n * fire\" is answerable from reducer + log alone.\n * 4. **Subs merge by id.** `subscriptions = [...base, ...deadlineSubs]`; the\n * timeout id is `$deadline:timeout:{seq}`, so a progress bump (seq+1)\n * yields a NEW id — the substrate retires the old timer and arms a fresh\n * one. When `exceeded`, no deadline sub is returned.\n * 5. **Clock out of update.** The countdown is a relative-timeout Sub; the\n * clock is read at subscribe time inside the Sub (`setTimeout`), NEVER in a\n * verb. The slice carries `seq`, not a deadline timestamp.\n *\n * ## Typical wiring\n *\n * const guarded = withDeadline(baseMachine, {\n * ms: 900_000, // auto-fail after 15 minutes of inactivity\n * progress: (msg) => msg.type !== \"heartbeat\", // heartbeats don't re-arm\n * });\n * const runtime = run(guarded, { ctx: baseCtx });\n */\n\nimport {\n applyCell,\n type Cmd,\n type Interpret,\n type Machine,\n msgKeysOf,\n type Reducer,\n type Sub,\n type Subscribe,\n subId,\n} from \"../index\";\nimport { fromTimeout } from \"../subs/from-timeout\";\n\n// ===========================================================================\n// The slice + the Sub + the Msg + the Cmds — the deadline vocabulary.\n// ===========================================================================\n\n/**\n * The wrapper's Model slice. `phase` is the lifecycle (`armed` while the\n * countdown runs, `exceeded` once the deadline fired); `seq` is the monotonic\n * re-arm counter that keys the timer Sub's id so each accepted progress Msg\n * retires the old timer and starts a fresh one.\n *\n * PURE and SERIALIZABLE: there is deliberately NO absolute timestamp here. A\n * deadline timestamp would force the merged `update` to read the wall clock to\n * compute it, which the conformance gate (property 2 — two replays under\n * distinct ambient clocks) is built to catch. Instead, the timer arms a fresh\n * full `ms` countdown per generation; no remaining-delay computation and no\n * clock read occur in the slice or `update` — the only clock is `setTimeout`\n * inside the Sub.\n */\nexport interface DeadlineSlice {\n /** `armed` while the countdown is live; `exceeded` once the deadline fired. */\n readonly phase: \"armed\" | \"exceeded\";\n /**\n * Monotonic re-arm counter. Starts at 0; each accepted progress Msg (while\n * armed) bumps it by one. Folds into the timer Sub's id, so a bump = a new id\n * = the substrate retires the old timer and arms a fresh `ms` countdown.\n */\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 `$deadline` 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 DeadlineModel<S> {\n readonly base: S;\n readonly $deadline: DeadlineSlice;\n}\n\n/**\n * The relative-timeout Sub the wrapper arms while `phase === \"armed\"`. `delayMs`\n * is the window `config.ms`; the id is `$deadline:timeout:{seq}`, so each\n * progress bump produces a distinct id and the reconcile pass churns the timer.\n * Same `delayMs`-on-the-Sub shape `fromTimeout` consumes.\n */\nexport type DeadlineTimeoutSub = Sub<\"$deadline:timeout\"> & {\n readonly delayMs: number;\n};\n\n/**\n * The Msg the timeout Sub dispatches when `config.ms` elapses with no accepted\n * progress. `seq` echoes the re-arm generation the timer was armed against, so a\n * stale timer's Msg (one whose id was already retired) is identifiable; the\n * merged `update` only honors it when it matches the current slice `seq`.\n */\nexport interface DeadlineExceededMsg {\n readonly type: \"$deadline:exceeded\";\n /** The re-arm generation this timer was armed against. */\n readonly seq: number;\n}\n\n/**\n * Construct the deadline-exceeded Msg. Exported so the subscribe cell and tests\n * share one constructor rather than two literals that can drift.\n */\nexport function deadlineExceededMsg(seq: number): DeadlineExceededMsg {\n return { type: \"$deadline:exceeded\", seq };\n}\n\n/**\n * The fire-and-forget decision Cmd the merged `update` appends on every\n * transition, so the wrapper's choice is visible in the replayed Cmd log\n * (rule 3) — `rearm` when an accepted progress Msg bumped `seq`, `expire` when\n * the deadline-exceeded Msg flipped the phase, `idle` otherwise (a non-progress\n * Msg, or a Msg after the machine already auto-failed). Plain data; its\n * interpret handler is a no-op (the decision is the Cmd's presence in the log,\n * not a side effect).\n */\nexport type DeadlineDecisionCmd = Cmd<\"$deadline:decision\"> & {\n readonly decision: \"rearm\" | \"expire\" | \"idle\";\n /** The slice `seq` AFTER this transition (the re-arm generation now armed). */\n readonly seq: number;\n};\n\n/**\n * A predicate over a Msg that decides whether it re-arms the deadline.\n *\n * The argument is `Readonly<M>` and the result is `boolean`: this is the\n * STRONGEST purity TS can express here. `Readonly<M>` removes the most common\n * impurity at compile time — the predicate cannot reassign the Msg's own\n * fields — and `boolean` pins the return so a side-effecting `(msg) => { … }`\n * with an implicit `undefined`/`void` return is rejected.\n *\n * What the type system CANNOT encode (TS has no effect tracking): observational\n * purity. A body that reads `Date.now()`, calls `Math.random()`, performs IO,\n * or closes over and mutates external state still type-checks. Those remain a\n * CONVENTION enforced by review, not the compiler — the predicate runs inside\n * the pure merged `update` (invariant 2), so it must be a pure function of the\n * Msg alone: no clock, no RNG, no IO, no captured mutable state.\n */\nexport type ProgressPredicate<M extends { type: string }> = (\n msg: Readonly<M>,\n) => boolean;\n\n/**\n * The deadline knob. `ms` is the inactivity window; `progress` is an OPTIONAL\n * predicate picking which base Msgs count as activity (default: EVERY base Msg\n * re-arms). The predicate runs INSIDE the pure merged `update`, so it must be a\n * pure function of the Msg — see {@link ProgressPredicate} for what the type\n * enforces vs. what stays a convention.\n */\nexport interface DeadlineConfig<M extends { type: string }> {\n /** The inactivity window in milliseconds. Auto-fail after `ms` with no progress. */\n readonly ms: number;\n /**\n * Predicate picking which base Msgs re-arm the timer. Default: every base Msg\n * counts as progress. Pure — a function of the Msg alone (see\n * {@link ProgressPredicate}).\n */\n readonly progress?: ProgressPredicate<M>;\n}\n\n// ===========================================================================\n// withDeadline — the wrapper.\n// ===========================================================================\n\n/**\n * Wrap `base` with an inactivity deadline. Returns a NEW `Machine` over the\n * composed Model `{ base, $deadline }`, with the base's Msgs/Cmds/Subs extended\n * by the wrapper's own (`$deadline:exceeded` Msg, `$deadline:decision` Cmd,\n * `$deadline:timeout` Sub).\n *\n * The composed machine:\n * - `init` rehydrates the base (honoring the `[loaded, []]` contract) and\n * seeds `$deadline: { phase: \"armed\", seq: 0 }`. Base init Cmds pass\n * through unchanged.\n * - `update` (Reducer form) runs the base reducer for base Msgs — base Cmds\n * pass through UNCHANGED — and on an accepted progress Msg bumps `seq`\n * (re-arm). The `$deadline:exceeded` Msg cell flips `phase` to `exceeded`.\n * Every transition appends a `$deadline:decision` marker Cmd.\n * - `subscriptions` is the base's, plus a `$deadline:timeout` Sub keyed on\n * `seq` while `phase === \"armed\"`.\n * - `subscribe` is the base's, plus the `$deadline:timeout` cell.\n * - `interpret` is the base's, plus a no-op `$deadline:decision` handler.\n *\n * @param base any `Machine<S, M, C, U, Ctx>`.\n * @param config the deadline knob (`ms` window + optional `progress` predicate).\n */\nexport function withDeadline<\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: DeadlineConfig<M>,\n): Machine<\n DeadlineModel<S>,\n M | DeadlineExceededMsg,\n C | DeadlineDecisionCmd,\n U | DeadlineTimeoutSub,\n Ctx\n> {\n const windowMs = config.ms;\n const isProgress = config.progress;\n\n // The merged update is a flat Reducer over the composed Model. The composed\n // Model is NOT discriminated on `.type`, so every (base Msg variant + the\n // wrapper's own `$deadline:exceeded`) gets exactly one cell.\n type Next = readonly [DeadlineModel<S>, readonly (C | DeadlineDecisionCmd)[]];\n const update = {} as Record<\n string,\n (state: DeadlineModel<S>, msg: M | DeadlineExceededMsg) => Next\n >;\n\n // Namespace guard for the Msg/update surface (mirrors the interpret +\n // subscribe guards below). The `$deadline:exceeded` cell is assigned into\n // `update` after this loop; if a base Msg.type already lives in the\n // `$deadline:` namespace, that assignment — or this loop — would SILENTLY\n // clobber / be clobbered by a base cell. The `$deadline:` Msg namespace is\n // the wrapper's: refuse to wrap a base that squats on it.\n // Base Msg keys enumerated via `msgKeysOf(base)` — keyed on the\n // authoritative `__form` tag, no structural re-derivation here (#275).\n const baseMsgKeys = msgKeysOf(base);\n for (const key of baseMsgKeys) {\n if (key.startsWith(\"$deadline:\")) {\n throw new Error(\n `withDeadline: the base machine declares a reserved \"${key}\" update ` +\n 'cell — the wrapper owns the \"$deadline:\" Msg namespace. ' +\n \"Rename the base Msg.\",\n );\n }\n }\n\n for (const key of baseMsgKeys) {\n update[key] = (state, msg) => {\n // 1) Run the base reducer — its result passes through UNCHANGED.\n const [nextBase, baseCmds] = applyCell<S, M, C>(\n base,\n state.base,\n msg as M,\n );\n // 2) Re-arm decision: a progress Msg WHILE armed bumps `seq` (new timer\n // id → fresh countdown). After auto-fail, or for a non-progress Msg,\n // the slice is unchanged and the decision is `idle`.\n const armed = state.$deadline.phase === \"armed\";\n const counts =\n armed && (isProgress === undefined || isProgress(msg as M));\n const $deadline: DeadlineSlice = counts\n ? { phase: \"armed\", seq: state.$deadline.seq + 1 }\n : state.$deadline;\n const next: DeadlineModel<S> = { base: nextBase, $deadline };\n // 3) Append the visible decision marker. Base Cmds come FIRST, unchanged.\n const decision: DeadlineDecisionCmd = {\n type: \"$deadline:decision\",\n decision: counts ? \"rearm\" : \"idle\",\n seq: $deadline.seq,\n };\n return [next, [...baseCmds, decision]];\n };\n }\n\n // The wrapper's own Msg cell. The timeout Sub delivers `$deadline:exceeded`\n // carrying the `seq` it was armed against. Honor it ONLY while armed and ONLY\n // when the `seq` matches the current generation — a stale timer's Msg (from a\n // generation already retired by a re-arm, or arriving after auto-fail) is a\n // no-op that still records a visible `idle` decision. The expiry flips `phase`\n // to `exceeded`; the slice transition IS the auto-fail, visible in the log.\n update[\"$deadline:exceeded\"] = (state, msg) => {\n const m = msg as DeadlineExceededMsg;\n const fires =\n state.$deadline.phase === \"armed\" && m.seq === state.$deadline.seq;\n const $deadline: DeadlineSlice = fires\n ? { phase: \"exceeded\", seq: state.$deadline.seq }\n : state.$deadline;\n const next: DeadlineModel<S> = { base: state.base, $deadline };\n const decision: DeadlineDecisionCmd = {\n type: \"$deadline:decision\",\n decision: fires ? \"expire\" : \"idle\",\n seq: $deadline.seq,\n };\n return [next, [decision]];\n };\n\n // The base's interpret, extended with the one no-op decision handler. The\n // `$deadline:decision` Cmd has no I/O — its purpose is log visibility (rule 3),\n // so the handler returns void and dispatches nothing. Namespace guard mirrors\n // withTelemetry: refuse to wrap a base that squats on the reserved Cmd key.\n const baseInterpret =\n (base as { interpret?: Interpret<M, C, Ctx> }).interpret ??\n ({} as Interpret<M, C, Ctx>);\n if (Object.hasOwn(baseInterpret as object, \"$deadline:decision\")) {\n throw new Error(\n 'withDeadline: the base machine declares a reserved \"$deadline:decision\" ' +\n 'interpret handler — the wrapper owns the \"$deadline:\" Cmd namespace. ' +\n \"Rename the base handler.\",\n );\n }\n const interpret = {\n ...(baseInterpret as Record<string, unknown>),\n \"$deadline:decision\": async (): Promise<void> => {},\n } as Interpret<M | DeadlineExceededMsg, C | DeadlineDecisionCmd, Ctx>;\n\n // Subs merge by id: the base's, plus the deadline countdown while armed. The\n // timeout id folds in `seq`, so each progress bump yields a NEW id and the\n // reconcile pass retires the old timer + arms a fresh `ms` countdown. When\n // exceeded, no deadline sub is returned (the machine has auto-failed).\n const baseSubscriptions = base.subscriptions;\n const subscriptions = (\n state: DeadlineModel<S>,\n ): readonly (U | DeadlineTimeoutSub)[] => {\n const baseSubs = baseSubscriptions ? baseSubscriptions(state.base) : [];\n if (state.$deadline.phase !== \"armed\") return baseSubs;\n const timeout: DeadlineTimeoutSub = {\n id: subId(`$deadline:timeout:${state.$deadline.seq}`),\n type: \"$deadline:timeout\",\n delayMs: windowMs,\n };\n return [...baseSubs, timeout];\n };\n\n // The base's subscribe cells, plus the timeout cell. The clock is read here —\n // at subscribe time, inside the Sub's `setTimeout` — NEVER in a verb. The\n // dispatched Msg carries the `seq` the timer was armed against so the merged\n // update can reject a stale generation.\n const baseSubscribe =\n (base as { subscribe?: Subscribe<M, U, Ctx> }).subscribe ??\n ({} as Subscribe<M, U, Ctx>);\n if (Object.hasOwn(baseSubscribe as object, \"$deadline:timeout\")) {\n throw new Error(\n 'withDeadline: the base machine declares a reserved \"$deadline:timeout\" ' +\n 'subscribe handler — the wrapper owns the \"$deadline:\" Sub namespace. ' +\n \"Rename the base handler.\",\n );\n }\n const timeoutCell = fromTimeout<DeadlineTimeoutSub, DeadlineExceededMsg>(\n (sub) => {\n // The sub id is `$deadline:timeout:{seq}`; recover the generation it was\n // armed against so a stale timer's Msg is identifiable in the reducer.\n const seq = Number(sub.id.slice(sub.id.lastIndexOf(\":\") + 1));\n return deadlineExceededMsg(seq);\n },\n );\n const subscribe = {\n ...(baseSubscribe as Record<string, unknown>),\n \"$deadline:timeout\": timeoutCell,\n } as Subscribe<M | DeadlineExceededMsg, U | DeadlineTimeoutSub, Ctx>;\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.\n if (loaded !== null) {\n return [loaded, []];\n }\n const [base0, cmds] = base.init(null, ctx);\n const model: DeadlineModel<S> = {\n base: base0,\n $deadline: { phase: \"armed\", seq: 0 },\n };\n // Base init Cmds pass through unchanged; no deadline Cmd on boot (boot is\n // not a base Msg transition, so it does not re-arm).\n return [model, cmds];\n },\n update: update as unknown as Reducer<\n DeadlineModel<S>,\n M | DeadlineExceededMsg,\n C | DeadlineDecisionCmd\n >,\n subscriptions,\n subscribe,\n interpret,\n } as Machine<\n DeadlineModel<S>,\n M | DeadlineExceededMsg,\n C | DeadlineDecisionCmd,\n U | DeadlineTimeoutSub,\n Ctx\n >;\n}\n"]}
@@ -0,0 +1,205 @@
1
+ import { C as Cmd, S as Sub, M as Machine } from '../core-BoB6t7dB.js';
2
+ import { ResilientConfig, ResilientState } from '../resilient-call/index.js';
3
+ import '../cache/index.js';
4
+ import '../types-B-8bcAp8.js';
5
+ import '../circuit-breaker/index.js';
6
+ import '../deadline/index.js';
7
+ import '../protocol-B5YmJr0d.js';
8
+ import '../rate-limit/index.js';
9
+ import '../retry-backoff/index.js';
10
+
11
+ /**
12
+ * @demlik/tea/with-resilience — the INTERCEPTING wrapper of the wrapper tier.
13
+ *
14
+ * `withResilience(base, config)` is a **pure function over machine data**: it
15
+ * takes any `Machine<S, M, C, U, Ctx>`, a chosen TARGET base Cmd type, and a
16
+ * resilience knob, and returns a NEW `Machine` over the composed Model
17
+ * `{ base, $resilience }`. You `run()` / `replay()` the result exactly like any
18
+ * other machine — there is no new runtime, no privileged interception path.
19
+ *
20
+ * This is the HARDEST wrapper. `withTelemetry` only OBSERVES; this one
21
+ * INTERCEPTS. It bolts the resilient-call concern (cache → circuit-breaker →
22
+ * rate-limit → exponential-backoff retry, with a deadline cap) onto a chosen
23
+ * base Cmd WITHOUT the base authoring any of it. The base keeps emitting its
24
+ * plain effect Cmd; the wrapper diverts each one through the resilience gate.
25
+ *
26
+ * ## Retag, never swallow (rule 2 — the whole danger)
27
+ *
28
+ * The merged `update` delegates to `base.update`, then PARTITIONS the base Cmds
29
+ * by type:
30
+ *
31
+ * - A NON-target base Cmd passes through UNCHANGED — byte-identical, in the
32
+ * same slot. The wrapper does not touch effects it was not asked to harden.
33
+ * - A TARGET base Cmd is RETAGGED: it becomes the `input` payload of a
34
+ * `$resilience:run` carrier Cmd (when the gate admits it), OR its gating is
35
+ * RECORDED as a `$resilience:*` decision in the SAME transition (cache hit,
36
+ * circuit-open fast-fail, rate-limited backoff). It is NEVER emitted raw,
37
+ * and it NEVER vanishes without a `$resilience:*` Cmd recording where it
38
+ * went. That recorded divergence is exactly what the conformance gate's
39
+ * `intercepting` option verifies.
40
+ *
41
+ * The resilient-call result Msgs route back through the merged `update`:
42
+ * `$resilience:ok` → `succeed` (close the breaker, fill the cache, reset the
43
+ * retry run), `$resilience:err` → `fail` (trip the breaker, then back off or
44
+ * settle), `$resilience:timer` → `onTimer` (a retry timer re-runs the gate; a
45
+ * deadline timer settles the call failed). Every decision is a Msg/Cmd through
46
+ * the reducer + log — "why did this fire" is answerable from the log alone.
47
+ *
48
+ * ## The slice + verbs are REUSED, not reinvented
49
+ *
50
+ * The `$resilience` slice IS resilient-call's `ResilientState<I, R>` (per-key
51
+ * call / retry / circuit / bucket / cache), and the gate / backoff / breaker
52
+ * logic IS its `attempt` / `succeed` / `fail` / `onTimer` verbs + `subs`. The
53
+ * wrapper only ADAPTS the boundary: the verb's `input` is the ORIGINAL base
54
+ * Cmd, the verb's `resilient_run` carrier is renamed to `$resilience:run`, and
55
+ * the sub / timer ids are re-keyed into the `$resilience:` namespace (rule 4).
56
+ *
57
+ * ## The 5 rules it satisfies
58
+ *
59
+ * 1. **Named, serializable slice.** `model.$resilience` is resilient-call's
60
+ * plain-data `ResilientState` — JSON-serializable (the deadline error is a
61
+ * `{_tag,...}` sentinel, never a `new Error`), never a closure.
62
+ * 2. **Retag-and-re-emit, never swallow.** See above — a target base Cmd is
63
+ * always inside a `$resilience:run` inner payload or recorded by a
64
+ * `$resilience:*` divergence Cmd in the same transition.
65
+ * 3. **Every decision is a Msg/Cmd.** Gate / retry-schedule / fast-fail /
66
+ * settle are all `$resilience:*` Cmds and Msgs through the merged `update`.
67
+ * 4. **Subs merge by id.** Base subs ⧺ resilient-call retry/deadline timers,
68
+ * re-keyed `$resilience:retry:{key}` / `$resilience:deadline:{key}`.
69
+ * 5. **Clock out of update.** Backoff jitter RNG is injected at construction;
70
+ * the only clock read is `Date.now()` at the interpret boundary (the
71
+ * `$resilience:run` handler stamps the ok/err Msg `at`) and in the timer
72
+ * Sub. The merged `update` is PURE — `at` arrives as Msg data.
73
+ */
74
+
75
+ /**
76
+ * The `withResilience` knob. Extends resilient-call's `ResilientConfig` (every
77
+ * brick optional — omit a brick, omit its gate) with the two fields the wrapper
78
+ * needs that the bare knob does not:
79
+ *
80
+ * - `target` — the `Cmd.type` discriminant of the ONE base Cmd this wrapper
81
+ * hardens. Every other base Cmd passes through untouched.
82
+ * - `keyOf` — OPTIONAL. Derives the per-call resilience `key` from the target
83
+ * base Cmd, so distinct logical calls (distinct urls, distinct ids) get
84
+ * independent retry / cache state. Pure — it runs inside the merged
85
+ * `update`, so it must NOT read the clock / RNG. Defaults to a single
86
+ * shared key (`target`), i.e. one logical call at a time.
87
+ */
88
+ interface ResilienceConfig<TC extends Cmd, TM extends {
89
+ type: string;
90
+ }> extends ResilientConfig {
91
+ /** The base `Cmd.type` to make resilient. */
92
+ readonly target: TC["type"];
93
+ /** Optional per-call key deriver. Pure. Defaults to the target type itself. */
94
+ readonly keyOf?: (cmd: TC) => string;
95
+ /**
96
+ * Extracts the real wall-clock time (ms) the TRIGGERING base Msg carries as
97
+ * data — the time-as-data convention `resilient-call` expects on `attempt`.
98
+ *
99
+ * The merged `update` is PURE: it cannot read `Date.now()` (rule 5). The COLD
100
+ * attempt's gate (the very first `attempt` for a target Cmd in a transition)
101
+ * still needs a real instant — the breaker compares `at` against its recorded
102
+ * `openedAtMs`, the cache against its write `at`, the bucket against its last
103
+ * refill `at`, and the deadline cap anchors at `at + deadline.ms`. Fabricating
104
+ * `at = 0` is the betrayal: a cold attempt after a real-time breaker trip then
105
+ * computes cooldown = `0 - realTripTime < 0` forever, so the breaker NEVER
106
+ * recovers, and a `0`-anchored deadline brick fires immediately.
107
+ *
108
+ * So the base Msg that drove this transition MUST carry its own wall time as
109
+ * data, and `at` reads it off that Msg. The merged update passes
110
+ * `at(currentBaseMsg)` into `rc.attempt` for every target Cmd that transition
111
+ * emitted — real time enters the cold gate as Msg data, never `0`.
112
+ *
113
+ * REQUIRED whenever any time-sensitive brick (`circuit`, `rateLimit`, `cache`,
114
+ * `deadline`) is configured — `withResilience` THROWS at construction if such
115
+ * a brick is set and `at` is absent (errors are data: the cold gate needs real
116
+ * time, refuse to build a breaker that can never recover). OPTIONAL when only
117
+ * `retry` is configured: retry backoff reads its time off the result / timer
118
+ * Msgs (`$resilience:err.at`, `$resilience:timer.atMs`), never the cold attempt
119
+ * — so a `0` cold `at` is harmless there. Pure: it runs inside the merged
120
+ * `update`, so it must NOT read the clock / RNG.
121
+ */
122
+ readonly at?: (msg: TM) => number;
123
+ }
124
+ /**
125
+ * The composed Model. The base machine's state nests under `base`; the wrapper
126
+ * owns the NAMED `$resilience` slice — resilient-call's `ResilientState` keyed
127
+ * by the per-call `key`, with `I = TC` (the original target base Cmd is the
128
+ * port input) and `R = unknown` (the base interpret handler's result is opaque
129
+ * to the wrapper — it only routes Ok/Err). Plain data → round-trips through
130
+ * `JSON.parse(JSON.stringify(...))` iff the base state does.
131
+ */
132
+ interface ResilienceModel<S, TC extends Cmd> {
133
+ readonly base: S;
134
+ readonly $resilience: ResilientState<TC, unknown>;
135
+ }
136
+ /**
137
+ * The carrier Cmd the wrapper emits when the gate ADMITS a target base Cmd. It
138
+ * RETAGS the original base Cmd into `input` — the base Cmd is plain data, so no
139
+ * closure (invariant 3). The interpret handler unwraps `input` and runs it
140
+ * through the BASE interpret for that Cmd type. `key` indexes the resilient
141
+ * call this run belongs to.
142
+ */
143
+ type ResilienceRunCmd<TC extends Cmd> = Cmd<"$resilience:run"> & {
144
+ readonly key: string;
145
+ /** The ORIGINAL base Cmd, retagged — never swallowed. */
146
+ readonly input: TC;
147
+ };
148
+ /**
149
+ * The success Msg the `$resilience:run` handler dispatches back when the base
150
+ * interpret resolved OK. Routes to `succeed`. `at` is stamped at the interpret
151
+ * boundary (the one permitted clock read).
152
+ */
153
+ type ResilienceOkMsg = {
154
+ readonly type: "$resilience:ok";
155
+ readonly key: string;
156
+ readonly result: unknown;
157
+ readonly at: number;
158
+ };
159
+ /**
160
+ * The error Msg the `$resilience:run` handler dispatches back when the base
161
+ * interpret threw / rejected. Routes to `fail` (trip the breaker, then back off
162
+ * or settle). `at` is stamped at the interpret boundary.
163
+ */
164
+ type ResilienceErrMsg = {
165
+ readonly type: "$resilience:err";
166
+ readonly key: string;
167
+ readonly error: unknown;
168
+ readonly at: number;
169
+ };
170
+ /**
171
+ * The retry / deadline timer Msg the `$resilience:timer` Sub dispatches when
172
+ * the wall clock crosses the armed instant. Routes to `onTimer`. `id` is the
173
+ * `$resilience:`-namespaced timer id; `atMs` is the fire instant (Msg data, the
174
+ * clock that drove it lives in the Sub).
175
+ */
176
+ type ResilienceTimerMsg = {
177
+ readonly type: "$resilience:timer";
178
+ readonly id: string;
179
+ readonly atMs: number;
180
+ };
181
+ /** Every Msg the wrapper adds to the base's Msg union. */
182
+ type ResilienceMsg = ResilienceOkMsg | ResilienceErrMsg | ResilienceTimerMsg;
183
+ /** Every Cmd the wrapper adds to the base's Cmd union. */
184
+ type ResilienceCmd<TC extends Cmd> = ResilienceRunCmd<TC>;
185
+ /** The Sub the wrapper adds — a deadline-style timer in the `$resilience` family. */
186
+ type ResilienceTimerSub = Sub<"$resilience:timer"> & {
187
+ readonly atMs: number;
188
+ };
189
+ /**
190
+ * Wrap `base` so its `config.target` Cmd is run through the resilient-call
191
+ * concern (cache → circuit → rate-limit → retry, deadline-capped). Returns a
192
+ * NEW `Machine` over the composed Model `{ base, $resilience }`, with the
193
+ * base's Msgs/Cmds/Subs/Ctx extended by the wrapper's own.
194
+ *
195
+ * @param base any `Machine<S, M, C, U, Ctx>`.
196
+ * @param config the resilience knob: `target`, the optional bricks, `keyOf`.
197
+ * @param rng injected backoff jitter source. Pass a fixed `() => 0.5` in
198
+ * tests to pin retry delays; defaults to `Math.random` (read only
199
+ * at the verb boundary, never inside the merged `update`).
200
+ */
201
+ declare function withResilience<S, M extends {
202
+ type: string;
203
+ }, C extends Cmd, U extends Sub, Ctx>(base: Machine<S, M, C, U, Ctx>, config: ResilienceConfig<C, M>, rng?: () => number): Machine<ResilienceModel<S, C>, M | ResilienceMsg, C | ResilienceCmd<C>, U | ResilienceTimerSub, Ctx>;
204
+
205
+ export { type ResilienceCmd, type ResilienceConfig, type ResilienceErrMsg, type ResilienceModel, type ResilienceMsg, type ResilienceOkMsg, type ResilienceRunCmd, type ResilienceTimerMsg, type ResilienceTimerSub, withResilience };
@@ -0,0 +1,251 @@
1
+ import { createResilientCall } from '../chunk-27N2SPTN.js';
2
+ import { MsgType } from '../chunk-JELSZN25.js';
3
+ import { subscribeDeadline } from '../chunk-K7Q23EQ4.js';
4
+ import '../chunk-5VNWZADP.js';
5
+ import '../chunk-2YOE5PIS.js';
6
+ import '../chunk-SC3FAFZN.js';
7
+ import '../chunk-IN33Q4VJ.js';
8
+ import '../chunk-NSDFA6M6.js';
9
+ import '../chunk-2YFLOCWX.js';
10
+ import { tryInterpret } from '../chunk-MMH7CW5K.js';
11
+ import { msgKeysOf, applyCell, subId, formOf } from '../chunk-QLA76GIT.js';
12
+
13
+ // src/with-resilience/index.ts
14
+ var RC_RETRY_PREFIX = "resilient:retry:";
15
+ var RC_DEADLINE_PREFIX = "resilient:deadline:";
16
+ var WX_RETRY_PREFIX = "$resilience:retry:";
17
+ var WX_DEADLINE_PREFIX = "$resilience:deadline:";
18
+ function toWrapperTimerId(rcId) {
19
+ if (rcId.startsWith(RC_RETRY_PREFIX))
20
+ return WX_RETRY_PREFIX + rcId.slice(RC_RETRY_PREFIX.length);
21
+ if (rcId.startsWith(RC_DEADLINE_PREFIX))
22
+ return WX_DEADLINE_PREFIX + rcId.slice(RC_DEADLINE_PREFIX.length);
23
+ return rcId;
24
+ }
25
+ function toRcTimerId(wxId) {
26
+ if (wxId.startsWith(WX_RETRY_PREFIX))
27
+ return RC_RETRY_PREFIX + wxId.slice(WX_RETRY_PREFIX.length);
28
+ if (wxId.startsWith(WX_DEADLINE_PREFIX))
29
+ return RC_DEADLINE_PREFIX + wxId.slice(WX_DEADLINE_PREFIX.length);
30
+ return wxId;
31
+ }
32
+ function updateDeclaresMsgKey(machine, key) {
33
+ if (formOf(machine) === "reducer") {
34
+ return Object.hasOwn(machine.update, key);
35
+ }
36
+ for (const innerTable of Object.values(machine.update)) {
37
+ if (innerTable !== null && (typeof innerTable === "object" || typeof innerTable === "function") && Object.hasOwn(innerTable, key)) {
38
+ return true;
39
+ }
40
+ }
41
+ return false;
42
+ }
43
+ function withResilience(base, config, rng = Math.random) {
44
+ const { target } = config;
45
+ const keyOf = config.keyOf ?? (() => target);
46
+ const timeSensitiveBrick = config.circuit !== void 0 || config.rateLimit !== void 0 || config.cache !== void 0 || config.deadline !== void 0;
47
+ if (timeSensitiveBrick && config.at === void 0) {
48
+ throw new Error(
49
+ "withResilience: a time-sensitive brick (circuit / rateLimit / cache / deadline) is configured but `config.at` is missing. The cold-attempt gate needs the REAL wall time the triggering base Msg carries as data (time-as-data) \u2014 without it the gate would fabricate `at = 0`, so a cold attempt after a real-time breaker trip computes a negative cooldown and the breaker NEVER recovers (and a deadline brick fires immediately). Provide `at: (msg) => msg.<wallTimeField>`."
50
+ );
51
+ }
52
+ const atOf = config.at ?? (() => 0);
53
+ const rc = createResilientCall(config, rng);
54
+ const update = {};
55
+ for (const key of msgKeysOf(base)) {
56
+ update[key] = (state, msg) => {
57
+ const [nextBase, baseCmds] = applyCell(
58
+ base,
59
+ state.base,
60
+ msg
61
+ );
62
+ let resilience = state.$resilience;
63
+ const outCmds = [];
64
+ const at = atOf(msg);
65
+ for (const cmd of baseCmds) {
66
+ if (cmd.type !== target) {
67
+ outCmds.push(cmd);
68
+ continue;
69
+ }
70
+ const callKey = keyOf(cmd);
71
+ const [nextSlice, runCmds] = rc.attempt(resilience, callKey, cmd, at);
72
+ resilience = nextSlice;
73
+ for (const rcRun of runCmds) {
74
+ outCmds.push({
75
+ type: "$resilience:run",
76
+ key: rcRun.key,
77
+ input: rcRun.input
78
+ });
79
+ }
80
+ }
81
+ const next = { base: nextBase, $resilience: resilience };
82
+ return [next, outCmds];
83
+ };
84
+ }
85
+ update["$resilience:ok"] = (state, msg) => {
86
+ const m = msg;
87
+ const [slice, runCmds] = rc.succeed(state.$resilience, m.key, {
88
+ type: MsgType.ResilientOk,
89
+ key: m.key,
90
+ result: m.result,
91
+ at: m.at
92
+ });
93
+ return [{ ...state, $resilience: slice }, renameRunCmds(runCmds)];
94
+ };
95
+ update["$resilience:err"] = (state, msg) => {
96
+ const m = msg;
97
+ const [slice, runCmds] = rc.fail(state.$resilience, m.key, {
98
+ type: MsgType.ResilientErr,
99
+ key: m.key,
100
+ error: m.error,
101
+ at: m.at
102
+ });
103
+ return [{ ...state, $resilience: slice }, renameRunCmds(runCmds)];
104
+ };
105
+ update["$resilience:timer"] = (state, msg) => {
106
+ const m = msg;
107
+ const [slice, runCmds] = rc.onTimer(state.$resilience, {
108
+ type: "deadline_exceeded",
109
+ id: toRcTimerId(m.id),
110
+ atMs: m.atMs
111
+ });
112
+ return [{ ...state, $resilience: slice }, renameRunCmds(runCmds)];
113
+ };
114
+ function renameRunCmds(runCmds) {
115
+ return runCmds.map((r) => ({
116
+ type: "$resilience:run",
117
+ key: r.key,
118
+ input: r.input
119
+ }));
120
+ }
121
+ const baseSubscriptions = base.subscriptions;
122
+ const baseSubscribe = base.subscribe;
123
+ function subscriptions(state) {
124
+ const baseSubs = baseSubscriptions ? baseSubscriptions(state.base) : [];
125
+ const rcSubs = rc.subs(state.$resilience);
126
+ const timerSubs = rcSubs.map((s) => ({
127
+ id: subId(toWrapperTimerId(s.id)),
128
+ type: "$resilience:timer",
129
+ atMs: s.atMs
130
+ }));
131
+ return [...baseSubs, ...timerSubs];
132
+ }
133
+ const baseInterpret = base.interpret ?? {};
134
+ const RESERVED = [
135
+ "$resilience:run",
136
+ "$resilience:ok",
137
+ "$resilience:err",
138
+ "$resilience:timer"
139
+ ];
140
+ const surfaces = [
141
+ {
142
+ name: "update",
143
+ declares: (reserved) => updateDeclaresMsgKey(base, reserved)
144
+ },
145
+ {
146
+ name: "interpret",
147
+ declares: (reserved) => Object.hasOwn(baseInterpret, reserved)
148
+ },
149
+ {
150
+ name: "subscribe",
151
+ declares: (reserved) => Object.hasOwn(baseSubscribe ?? {}, reserved)
152
+ }
153
+ ];
154
+ for (const reserved of RESERVED) {
155
+ for (const surface of surfaces) {
156
+ if (surface.declares(reserved)) {
157
+ throw new Error(
158
+ `withResilience: the base machine declares a reserved "${reserved}" ${surface.name} key \u2014 the wrapper owns the "$resilience:" namespace ($resilience:run / :ok / :err / :timer across update, interpret, and subscribe). Rename the base key.`
159
+ );
160
+ }
161
+ }
162
+ }
163
+ if (!Object.hasOwn(baseInterpret, target)) {
164
+ throw new Error(
165
+ `withResilience: no base interpret handler for target Cmd "${target}" \u2014 the resilient effect has nothing to perform. The base machine must declare an interpret handler for its target Cmd type.`
166
+ );
167
+ }
168
+ const runHandler = tryInterpret(
169
+ async (cmd, ctx) => {
170
+ const inner = cmd.input;
171
+ const handler = baseInterpret[inner.type];
172
+ if (handler === void 0) {
173
+ throw new Error(
174
+ `withResilience: no base interpret handler for target Cmd "${inner.type}" \u2014 the resilient effect has nothing to perform.`
175
+ );
176
+ }
177
+ return await handler(inner, ctx);
178
+ },
179
+ (result, cmd) => ({
180
+ type: "$resilience:ok",
181
+ key: cmd.key,
182
+ result,
183
+ at: Date.now()
184
+ }),
185
+ (error, cmd) => ({
186
+ type: "$resilience:err",
187
+ key: cmd.key,
188
+ error,
189
+ at: Date.now()
190
+ })
191
+ );
192
+ const interpret = {
193
+ ...baseInterpret,
194
+ "$resilience:run": runHandler
195
+ };
196
+ const subscribeMap = {
197
+ ...baseSubscribe ?? {},
198
+ "$resilience:timer": (sub, ctx, dispatch) => (
199
+ // Reuse the deadline subscribe cell wholesale: it arms a one-shot timer
200
+ // for `max(0, atMs - Date.now())` and fires when the wall clock crosses
201
+ // `atMs`. We hand it a `DeadlineSub` shape and translate its
202
+ // `deadline_exceeded` Msg into the wrapper's `$resilience:timer` Msg.
203
+ subscribeDeadline(
204
+ { id: sub.id, type: "deadline", atMs: sub.atMs },
205
+ ctx,
206
+ (dl) => dispatch({ type: "$resilience:timer", id: dl.id, atMs: dl.atMs })
207
+ )
208
+ )
209
+ };
210
+ return {
211
+ init: (loaded, ctx) => {
212
+ if (loaded !== null) {
213
+ return [loaded, []];
214
+ }
215
+ const [base0, baseCmds] = base.init(null, ctx);
216
+ let resilience = rc.init();
217
+ const initCmds = [];
218
+ for (const cmd of baseCmds) {
219
+ if (cmd.type !== target) {
220
+ initCmds.push(cmd);
221
+ continue;
222
+ }
223
+ if (config.deadline !== void 0) {
224
+ throw new Error(
225
+ `withResilience: base.init emits the target Cmd "${target}" while a deadline brick is configured. The boot cold attempt has no real wall time (boot is gated at at=0), so the deadline would anchor at 0 + ${config.deadline.ms}ms and its $resilience:deadline Sub would fire IMMEDIATELY (max(0, atMs - now) === 0), failing the boot call instantly. Move the boot effect out of base.init and into a boot Msg dispatched after run() (where the triggering Msg carries a real at via config.at), so the deadline anchors at a real instant. (Boot effects belong in a boot Msg, not init Cmds.)`
226
+ );
227
+ }
228
+ const callKey = keyOf(cmd);
229
+ const [nextSlice, runCmds] = rc.attempt(resilience, callKey, cmd, 0);
230
+ resilience = nextSlice;
231
+ for (const rcRun of runCmds) {
232
+ initCmds.push({
233
+ type: "$resilience:run",
234
+ key: rcRun.key,
235
+ input: rcRun.input
236
+ });
237
+ }
238
+ }
239
+ const model = { base: base0, $resilience: resilience };
240
+ return [model, initCmds];
241
+ },
242
+ update,
243
+ subscriptions,
244
+ subscribe: subscribeMap,
245
+ interpret
246
+ };
247
+ }
248
+
249
+ export { withResilience };
250
+ //# sourceMappingURL=index.js.map
251
+ //# sourceMappingURL=index.js.map
@@ -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 { applyCell, formOf, msgKeysOf, 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 * Does the base machine declare `key` as a Msg.type cell, in EITHER update\n * form? The reserved-namespace guard checks the correct level per form (keyed\n * on `formOf`, the same classification `applyCell` / `msgKeysOf` 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 machine: { update: object; __form?: UpdateForm },\n key: string,\n): boolean {\n if (formOf(machine) === \"reducer\") {\n // Reducer form — msg-type keys at the top level.\n return Object.hasOwn(machine.update, key);\n }\n // Transitions form — msg-type keys live in each inner state-type table. An\n // inner table can be a callable object (the shape `__form` disambiguates),\n // so accept \"function\" alongside \"object\" here.\n for (const innerTable of Object.values(machine.update)) {\n if (\n innerTable !== null &&\n (typeof innerTable === \"object\" || typeof innerTable === \"function\") &&\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 // --- Base-Msg cells: delegate, then PARTITION + retag the target Cmd. ----\n // Base Msg keys enumerated via `msgKeysOf(base)` — keyed on the\n // authoritative `__form` tag, no structural re-derivation here (#275).\n for (const key of msgKeysOf(base)) {\n update[key] = (state, msg) => {\n // 1) Run the base reducer. Its NON-target Cmds pass through unchanged.\n const [nextBase, baseCmds] = applyCell<S, M, C>(\n base,\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 `formOf` classification the wrapper\n // already uses via `msgKeysOf` / `applyCell`): 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) => updateDeclaresMsgKey(base, 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"]}