@demlik/tea 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +49 -0
  2. package/dist/agent/index.d.ts +16 -770
  3. package/dist/agent/index.js +18 -16
  4. package/dist/authed-call/index.d.ts +3 -3
  5. package/dist/authed-call/index.js +22 -36
  6. package/dist/authed-call/index.js.map +1 -1
  7. package/dist/await-terminal/index.d.ts +2 -2
  8. package/dist/await-terminal/index.js +2 -2
  9. package/dist/batch-window/index.d.ts +3 -3
  10. package/dist/batch-window/index.js +3 -3
  11. package/dist/cache/index.d.ts +2 -2
  12. package/dist/cache/index.js +5 -2
  13. package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
  14. package/dist/chunk-25LBDCBC.js.map +1 -0
  15. package/dist/{chunk-IK4S5DJN.js → chunk-2AHAR7CX.js} +7 -14
  16. package/dist/chunk-2AHAR7CX.js.map +1 -0
  17. package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
  18. package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
  19. package/dist/{chunk-VFGKTGGC.js → chunk-5W7CNAHB.js} +191 -178
  20. package/dist/chunk-5W7CNAHB.js.map +1 -0
  21. package/dist/{chunk-6GIGQT6C.js → chunk-B5RAZVXW.js} +12 -11
  22. package/dist/chunk-B5RAZVXW.js.map +1 -0
  23. package/dist/{chunk-6VDCTR7N.js → chunk-D4NJBQAH.js} +258 -151
  24. package/dist/chunk-D4NJBQAH.js.map +1 -0
  25. package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
  26. package/dist/chunk-D736QUGF.js.map +1 -0
  27. package/dist/{chunk-EGWTT5XJ.js → chunk-ET6QQNSQ.js} +76 -95
  28. package/dist/chunk-ET6QQNSQ.js.map +1 -0
  29. package/dist/chunk-EWJP3YYP.js +21 -0
  30. package/dist/chunk-EWJP3YYP.js.map +1 -0
  31. package/dist/{chunk-M6UZT7HS.js → chunk-FCKP6N7Y.js} +6 -5
  32. package/dist/chunk-FCKP6N7Y.js.map +1 -0
  33. package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
  34. package/dist/chunk-FV3Z5MBE.js.map +1 -0
  35. package/dist/chunk-G5E74IJB.js +8 -0
  36. package/dist/chunk-G5E74IJB.js.map +1 -0
  37. package/dist/{chunk-G2U7DJHL.js → chunk-JFFVBEDI.js} +3 -3
  38. package/dist/{chunk-G2U7DJHL.js.map → chunk-JFFVBEDI.js.map} +1 -1
  39. package/dist/{chunk-FOGL3TC7.js → chunk-JXYYRKD6.js} +8 -10
  40. package/dist/chunk-JXYYRKD6.js.map +1 -0
  41. package/dist/{chunk-ZDRN3YCE.js → chunk-LJJV6ZD5.js} +17 -24
  42. package/dist/chunk-LJJV6ZD5.js.map +1 -0
  43. package/dist/chunk-LYLDCIPH.js +365 -0
  44. package/dist/chunk-LYLDCIPH.js.map +1 -0
  45. package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
  46. package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
  47. package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
  48. package/dist/chunk-MUUROGIH.js.map +1 -0
  49. package/dist/{chunk-DYRKDCJI.js → chunk-PBDYW2TU.js} +41 -7
  50. package/dist/chunk-PBDYW2TU.js.map +1 -0
  51. package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
  52. package/dist/chunk-Q2UM7BXU.js.map +1 -0
  53. package/dist/chunk-RMX4EOQK.js +26 -0
  54. package/dist/chunk-RMX4EOQK.js.map +1 -0
  55. package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
  56. package/dist/chunk-S2EGRPCM.js.map +1 -0
  57. package/dist/{chunk-5KVHID6A.js → chunk-SPWVPIFQ.js} +4 -4
  58. package/dist/{chunk-5KVHID6A.js.map → chunk-SPWVPIFQ.js.map} +1 -1
  59. package/dist/chunk-SSHDBOXF.js +71 -0
  60. package/dist/chunk-SSHDBOXF.js.map +1 -0
  61. package/dist/{chunk-6OUDPSSG.js → chunk-TWDHNIY6.js} +3 -3
  62. package/dist/{chunk-6OUDPSSG.js.map → chunk-TWDHNIY6.js.map} +1 -1
  63. package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
  64. package/dist/chunk-UF4MZSLT.js.map +1 -0
  65. package/dist/chunk-ZJ5AOYBH.js +8 -0
  66. package/dist/chunk-ZJ5AOYBH.js.map +1 -0
  67. package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
  68. package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
  69. package/dist/circuit-breaker/index.d.ts +1 -0
  70. package/dist/circuit-breaker/index.js +1 -1
  71. package/dist/{core-BmlJOvxM.d.ts → core-42kapW0n.d.ts} +96 -6
  72. package/dist/deadline/index.d.ts +77 -11
  73. package/dist/deadline/index.js +3 -3
  74. package/dist/devtools/index.js +8 -28
  75. package/dist/devtools/index.js.map +1 -1
  76. package/dist/do/index.d.ts +52 -24
  77. package/dist/do/index.js +94 -92
  78. package/dist/do/index.js.map +1 -1
  79. package/dist/{durable-effects-9LxFpmh3.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
  80. package/dist/extension/index.d.ts +25 -7
  81. package/dist/extension/index.js +4 -3
  82. package/dist/extension/index.js.map +1 -1
  83. package/dist/extension/react.js +4 -3
  84. package/dist/extension/react.js.map +1 -1
  85. package/dist/extension/subs/index.d.ts +2 -2
  86. package/dist/extension/subs/index.js +7 -10
  87. package/dist/extension/subs/index.js.map +1 -1
  88. package/dist/fan-out/index.d.ts +10 -10
  89. package/dist/fan-out/index.js +3 -3
  90. package/dist/idempotent-intake/index.d.ts +2 -2
  91. package/dist/idempotent-intake/index.js +3 -3
  92. package/dist/index.d.ts +50 -489
  93. package/dist/index.js +2 -2
  94. package/dist/llm-call/index.d.ts +3 -2
  95. package/dist/llm-call/index.js +12 -10
  96. package/dist/machine-B_BF4jij.d.ts +804 -0
  97. package/dist/machine-viz/index.d.ts +1 -1
  98. package/dist/machine-viz/index.js +2 -2
  99. package/dist/mem/index.d.ts +3 -2
  100. package/dist/mem/index.js.map +1 -1
  101. package/dist/monitored-run/index.d.ts +54 -30
  102. package/dist/monitored-run/index.js +7 -7
  103. package/dist/node/index.d.ts +3 -2
  104. package/dist/node/index.js +2 -3
  105. package/dist/node/index.js.map +1 -1
  106. package/dist/paginated-walk/index.d.ts +2 -2
  107. package/dist/paginated-walk/index.js +11 -10
  108. package/dist/parity/index.d.ts +6 -5
  109. package/dist/parity/index.js +5 -4
  110. package/dist/parity/index.js.map +1 -1
  111. package/dist/pbt/arbitraries/index.d.ts +1 -1
  112. package/dist/pbt/index.d.ts +1 -1
  113. package/dist/pbt/index.js +3 -3
  114. package/dist/pbt/index.js.map +1 -1
  115. package/dist/pbt/runners/index.d.ts +1 -1
  116. package/dist/pbt/runners/index.js +3 -3
  117. package/dist/poller/index.d.ts +59 -17
  118. package/dist/poller/index.js +56 -20
  119. package/dist/poller/index.js.map +1 -1
  120. package/dist/prediction/index.d.ts +1 -1
  121. package/dist/prediction/index.js +2 -2
  122. package/dist/pure/index.d.ts +1 -1
  123. package/dist/pure/index.js +2 -2
  124. package/dist/rate-limit/index.d.ts +2 -2
  125. package/dist/rate-limit/index.js +1 -1
  126. package/dist/react/index.d.ts +10 -3
  127. package/dist/react/index.js +2 -2
  128. package/dist/react/index.js.map +1 -1
  129. package/dist/reconciler/index.d.ts +2 -2
  130. package/dist/reconciler/index.js +13 -15
  131. package/dist/reconciler/index.js.map +1 -1
  132. package/dist/recorder/index.d.ts +20 -20
  133. package/dist/recorder/index.js +1 -1
  134. package/dist/resilient-call/index.d.ts +31 -6
  135. package/dist/resilient-call/index.js +10 -9
  136. package/dist/retry-backoff/index.d.ts +191 -14
  137. package/dist/retry-backoff/index.js +1 -1
  138. package/dist/retry-to-success/index.js +1 -1
  139. package/dist/runtime-types-CukV8kMX.d.ts +406 -0
  140. package/dist/saga/index.d.ts +3 -2
  141. package/dist/saga/index.js +2 -2
  142. package/dist/saga/index.js.map +1 -1
  143. package/dist/snapshot/index.d.ts +1 -1
  144. package/dist/snapshot/index.js +3 -3
  145. package/dist/subs/index.d.ts +372 -4
  146. package/dist/subs/index.js +4 -1
  147. package/dist/testing/index.d.ts +2 -2
  148. package/dist/testing/index.js +2 -2
  149. package/dist/throttled-input/index.d.ts +2 -2
  150. package/dist/throttled-input/index.js +14 -13
  151. package/dist/throttled-input/index.js.map +1 -1
  152. package/dist/token-refresh/index.d.ts +1 -1
  153. package/dist/token-refresh/index.js +3 -3
  154. package/dist/trace-replay/index.d.ts +12 -7
  155. package/dist/trace-replay/index.js +4 -3
  156. package/dist/{types-BoUWgwSm.d.ts → types-KbL3W1mj.d.ts} +1 -1
  157. package/dist/with-deadline/index.d.ts +1 -1
  158. package/dist/with-deadline/index.js +2 -2
  159. package/dist/with-resilience/index.d.ts +21 -2
  160. package/dist/with-resilience/index.js +12 -23
  161. package/dist/with-resilience/index.js.map +1 -1
  162. package/dist/with-telemetry/index.d.ts +1 -1
  163. package/dist/with-telemetry/index.js +2 -2
  164. package/dist/work-queue/adapter.d.ts +2 -2
  165. package/dist/work-queue/adapter.js +2 -2
  166. package/dist/work-queue/index.d.ts +3 -2
  167. package/dist/work-queue/index.js +1 -1
  168. package/dist/work-queue/index.js.map +1 -1
  169. package/dist/work-queue/ops.d.ts +2 -2
  170. package/dist/work-queue/ops.js +1 -1
  171. package/dist/workflow/index.d.ts +51 -19
  172. package/dist/workflow/index.js +71 -52
  173. package/dist/workflow/index.js.map +1 -1
  174. package/package.json +4 -1
  175. package/dist/chunk-2YOE5PIS.js.map +0 -1
  176. package/dist/chunk-3545BOYL.js.map +0 -1
  177. package/dist/chunk-5VNWZADP.js.map +0 -1
  178. package/dist/chunk-6GIGQT6C.js.map +0 -1
  179. package/dist/chunk-6VDCTR7N.js.map +0 -1
  180. package/dist/chunk-DYRKDCJI.js.map +0 -1
  181. package/dist/chunk-EGWTT5XJ.js.map +0 -1
  182. package/dist/chunk-EMJTPHI7.js +0 -91
  183. package/dist/chunk-EMJTPHI7.js.map +0 -1
  184. package/dist/chunk-FOGL3TC7.js.map +0 -1
  185. package/dist/chunk-IK4S5DJN.js.map +0 -1
  186. package/dist/chunk-M6UZT7HS.js.map +0 -1
  187. package/dist/chunk-NSDFA6M6.js +0 -198
  188. package/dist/chunk-NSDFA6M6.js.map +0 -1
  189. package/dist/chunk-SC3FAFZN.js.map +0 -1
  190. package/dist/chunk-SKAMZKTE.js +0 -20
  191. package/dist/chunk-SKAMZKTE.js.map +0 -1
  192. package/dist/chunk-TGG26XMK.js.map +0 -1
  193. package/dist/chunk-UV6UWGEO.js.map +0 -1
  194. package/dist/chunk-UYWUVEP6.js.map +0 -1
  195. package/dist/chunk-VFGKTGGC.js.map +0 -1
  196. package/dist/chunk-ZDRN3YCE.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { C as Cmd, I as Interpret } from '../core-BmlJOvxM.js';
1
+ import { C as Cmd, c as Interpret } from '../core-42kapW0n.js';
2
2
  import { M as MsgType } from '../protocol-B5YmJr0d.js';
3
3
 
4
4
  /**
@@ -1,6 +1,6 @@
1
- export { createTokenRefresh, initTokenRefresh, refreshTokenCmd, tokenRefreshFailedMsg, tokenRefreshedMsg } from '../chunk-6OUDPSSG.js';
1
+ export { createTokenRefresh, initTokenRefresh, refreshTokenCmd, tokenRefreshFailedMsg, tokenRefreshedMsg } from '../chunk-TWDHNIY6.js';
2
2
  import '../chunk-JELSZN25.js';
3
- import '../chunk-6VDCTR7N.js';
4
- import '../chunk-DYRKDCJI.js';
3
+ import '../chunk-D4NJBQAH.js';
4
+ import '../chunk-PBDYW2TU.js';
5
5
  //# sourceMappingURL=index.js.map
6
6
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
- import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
1
+ import { C as Cmd, S as Sub, M as Machine } from '../core-42kapW0n.js';
2
2
  import { Trace } from '../recorder/index.js';
3
- import '../index.js';
3
+ import '../runtime-types-CukV8kMX.js';
4
4
 
5
5
  /**
6
6
  * The outcome of {@link replayTrace}.
@@ -50,11 +50,16 @@ declare function replayTrace<S, M extends {
50
50
  type: string;
51
51
  }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, trace: Trace<S, M>, ctx: Ctx): ReplayResult<S>;
52
52
  /**
53
- * Order-insensitive structural deep-equality — the single comparator this
54
- * package owns, exposed so consumers (e.g. `@demlik/tea/parity`'s `diff`)
55
- * reuse it rather than adding a second deep-compare. `true` iff `a` and `b`
56
- * are structurally equal under {@link firstDivergence}'s walk (object keys
57
- * compared order-insensitively, arrays index-by-index).
53
+ * Order-insensitive structural deep-equality — the comparator the
54
+ * record/replay lane owns, exposed so consumers (e.g. `@demlik/tea/parity`'s
55
+ * `parityEqual`) reuse it rather than adding a fourth deep-compare. It is one
56
+ * of three structural walkers in the package, divergent on purpose:
57
+ * this `deepEqual` returns a boolean verdict (order-insensitive, for
58
+ * replay/parity equality), `devtools`'s `diffState` returns a change list (for
59
+ * a human-readable diff view), and `testing`'s `__deepEqual` is the assertion
60
+ * helpers' internal compare — same shape, three jobs, not one to collapse.
61
+ * `true` iff `a` and `b` are structurally equal under {@link firstDivergence}'s
62
+ * walk (object keys compared order-insensitively, arrays index-by-index).
58
63
  */
59
64
  declare function deepEqual(a: unknown, b: unknown): boolean;
60
65
 
@@ -1,5 +1,6 @@
1
- export { deepEqual, replayTrace } from '../chunk-IK4S5DJN.js';
2
- import '../chunk-6VDCTR7N.js';
3
- import '../chunk-DYRKDCJI.js';
1
+ export { deepEqual, replayTrace } from '../chunk-2AHAR7CX.js';
2
+ import '../chunk-EWJP3YYP.js';
3
+ import '../chunk-D4NJBQAH.js';
4
+ import '../chunk-PBDYW2TU.js';
4
5
  //# sourceMappingURL=index.js.map
5
6
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import { S as Sub } from './core-BmlJOvxM.js';
1
+ import { S as Sub } from './core-42kapW0n.js';
2
2
 
3
3
  type SubscribeHandler<S extends Sub, M, Ctx> = (sub: S, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
4
4
 
@@ -1,4 +1,4 @@
1
- import { S as Sub, C as Cmd, M as Machine } from '../core-BmlJOvxM.js';
1
+ import { S as Sub, C as Cmd, M as Machine } from '../core-42kapW0n.js';
2
2
 
3
3
  /**
4
4
  * @demlik/tea/with-deadline — auto-fail a wrapped machine at T+N, re-arm on
@@ -1,6 +1,6 @@
1
1
  import { fromTimeout } from '../chunk-2YFLOCWX.js';
2
- import '../chunk-6VDCTR7N.js';
3
- import { msgKeysOf, applyCell, subId } from '../chunk-DYRKDCJI.js';
2
+ import '../chunk-D4NJBQAH.js';
3
+ import { msgKeysOf, applyCell, subId } from '../chunk-PBDYW2TU.js';
4
4
 
5
5
  // src/with-deadline/index.ts
6
6
  function deadlineExceededMsg(seq) {
@@ -1,7 +1,7 @@
1
- import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
1
+ import { C as Cmd, S as Sub, M as Machine } from '../core-42kapW0n.js';
2
2
  import { ResilientConfig, ResilientState } from '../resilient-call/index.js';
3
3
  import '../cache/index.js';
4
- import '../types-BoUWgwSm.js';
4
+ import '../types-KbL3W1mj.js';
5
5
  import '../circuit-breaker/index.js';
6
6
  import '../deadline/index.js';
7
7
  import '../protocol-B5YmJr0d.js';
@@ -9,6 +9,8 @@ import '../rate-limit/index.js';
9
9
  import '../retry-backoff/index.js';
10
10
 
11
11
  /**
12
+ * @packageDocumentation
13
+ *
12
14
  * @demlik/tea/with-resilience — the INTERCEPTING wrapper of the wrapper tier.
13
15
  *
14
16
  * `withResilience(base, config)` is a **pure function over machine data**: it
@@ -70,6 +72,23 @@ import '../retry-backoff/index.js';
70
72
  * the only clock read is `Date.now()` at the interpret boundary (the
71
73
  * `$resilience:run` handler stamps the ok/err Msg `at`) and in the timer
72
74
  * Sub. The merged `update` is PURE — `at` arrives as Msg data.
75
+ *
76
+ * ## The retry bound may be a DURATION, not only a count
77
+ *
78
+ * `config.retry` is resilient-call's `AnyRetryPolicy`: a count (`RetryPolicy`),
79
+ * a wall-clock outage budget (`DurationRetryPolicy`, `maxElapsedMs`), or an
80
+ * explicit `unbounded: true`. Nothing extra is wired for the duration bound and
81
+ * nothing extra is required of the base machine — rule 5 already made the clock
82
+ * arrive as DATA on exactly the Msgs the streak is measured from:
83
+ * `$resilience:err.at` (stamped by the wrapper's own `$resilience:run` handler
84
+ * at the interpret boundary) and `$resilience:timer.atMs` (the Sub's fire
85
+ * instant). Those are the two instants `recordFailure` / `shouldRetry` are fed,
86
+ * so an outage budget is measured against true wall time even when
87
+ * `config.at` is absent — the cold gate's `at` never records a failure.
88
+ *
89
+ * `config.at` therefore stays REQUIRED-when-time-sensitive-brick and OPTIONAL
90
+ * otherwise, unchanged: a duration-bounded retry does NOT make it required,
91
+ * because the cold attempt is not a failure observation.
73
92
  */
74
93
 
75
94
  /**
@@ -1,14 +1,15 @@
1
- import { createResilientCall } from '../chunk-6GIGQT6C.js';
1
+ import { createResilientCall } from '../chunk-B5RAZVXW.js';
2
2
  import { MsgType } from '../chunk-JELSZN25.js';
3
- import { subscribeDeadline } from '../chunk-SKAMZKTE.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';
3
+ import { subscribeDeadline } from '../chunk-RMX4EOQK.js';
4
+ import '../chunk-Q2UM7BXU.js';
5
+ import '../chunk-S2EGRPCM.js';
6
+ import '../chunk-UF4MZSLT.js';
7
+ import '../chunk-2KNLKQVN.js';
8
+ import '../chunk-LYLDCIPH.js';
9
9
  import '../chunk-2YFLOCWX.js';
10
- import { tryInterpret } from '../chunk-6VDCTR7N.js';
11
- import { msgKeysOf, applyCell, subId, formOf } from '../chunk-DYRKDCJI.js';
10
+ import '../chunk-ZJ5AOYBH.js';
11
+ import { tryInterpret } from '../chunk-D4NJBQAH.js';
12
+ import { msgKeysOf, applyCell, subId, formOf } from '../chunk-PBDYW2TU.js';
12
13
 
13
14
  // src/with-resilience/index.ts
14
15
  var RC_RETRY_PREFIX = "resilient:retry:";
@@ -70,13 +71,7 @@ function withResilience(base, config, rng = Math.random) {
70
71
  const callKey = keyOf(cmd);
71
72
  const [nextSlice, runCmds] = rc.attempt(resilience, callKey, cmd, at);
72
73
  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
- }
74
+ outCmds.push(...renameRunCmds(runCmds));
80
75
  }
81
76
  const next = { base: nextBase, $resilience: resilience };
82
77
  return [next, outCmds];
@@ -228,13 +223,7 @@ function withResilience(base, config, rng = Math.random) {
228
223
  const callKey = keyOf(cmd);
229
224
  const [nextSlice, runCmds] = rc.attempt(resilience, callKey, cmd, 0);
230
225
  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
- }
226
+ initCmds.push(...renameRunCmds(runCmds));
238
227
  }
239
228
  const model = { base: base0, $resilience: resilience };
240
229
  return [model, initCmds];
@@ -1 +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"]}
1
+ {"version":3,"sources":["../../src/with-resilience/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @packageDocumentation\n *\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 * ## The retry bound may be a DURATION, not only a count\n *\n * `config.retry` is resilient-call's `AnyRetryPolicy`: a count (`RetryPolicy`),\n * a wall-clock outage budget (`DurationRetryPolicy`, `maxElapsedMs`), or an\n * explicit `unbounded: true`. Nothing extra is wired for the duration bound and\n * nothing extra is required of the base machine — rule 5 already made the clock\n * arrive as DATA on exactly the Msgs the streak is measured from:\n * `$resilience:err.at` (stamped by the wrapper's own `$resilience:run` handler\n * at the interpret boundary) and `$resilience:timer.atMs` (the Sub's fire\n * instant). Those are the two instants `recordFailure` / `shouldRetry` are fed,\n * so an outage budget is measured against true wall time even when\n * `config.at` is absent — the cold gate's `at` never records a failure.\n *\n * `config.at` therefore stays REQUIRED-when-time-sensitive-brick and OPTIONAL\n * otherwise, unchanged: a duration-bounded retry does NOT make it required,\n * because the cold attempt is not a failure observation.\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. `renameRunCmds`\n // renames each into the `$resilience:run` namespace, preserving the\n // retagged `input` (the ORIGINAL base Cmd). Zero carriers = the gate\n // recorded a divergence in `nextSlice` (cache/circuit/backoff) —\n // visible in the slice + log.\n outCmds.push(...renameRunCmds(runCmds));\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 // Same `$resilience:run` retag as the update loop — one helper.\n initCmds.push(...renameRunCmds(runCmds));\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"]}
@@ -1,4 +1,4 @@
1
- import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
1
+ import { C as Cmd, S as Sub, M as Machine } from '../core-42kapW0n.js';
2
2
 
3
3
  /**
4
4
  * @demlik/tea/with-telemetry — the pattern-setting wrapper of the wrapper tier.
@@ -1,5 +1,5 @@
1
- import '../chunk-6VDCTR7N.js';
2
- import { msgKeysOf, applyCell } from '../chunk-DYRKDCJI.js';
1
+ import '../chunk-D4NJBQAH.js';
2
+ import { msgKeysOf, applyCell } from '../chunk-PBDYW2TU.js';
3
3
 
4
4
  // src/with-telemetry/index.ts
5
5
  function withTelemetry(base, config = {}) {
@@ -1,6 +1,6 @@
1
1
  import { QueueItem } from './index.js';
2
- import '../index.js';
3
- import '../core-BmlJOvxM.js';
2
+ import '../runtime-types-CukV8kMX.js';
3
+ import '../core-42kapW0n.js';
4
4
 
5
5
  /**
6
6
  * @demlik/tea/work-queue/adapter — the verb seam over the pure queue ops.
@@ -1,4 +1,4 @@
1
- export { queueAdapter } from '../chunk-HJO6VNQI.js';
2
- import '../chunk-TGG26XMK.js';
1
+ export { queueAdapter } from '../chunk-MU6MCIAC.js';
2
+ import '../chunk-FV3Z5MBE.js';
3
3
  //# sourceMappingURL=adapter.js.map
4
4
  //# sourceMappingURL=adapter.js.map
@@ -1,7 +1,8 @@
1
- import { Store } from '../index.js';
2
- import '../core-BmlJOvxM.js';
1
+ import { S as Store } from '../runtime-types-CukV8kMX.js';
2
+ import '../core-42kapW0n.js';
3
3
 
4
4
  /**
5
+ * @packageDocumentation
5
6
  * @demlik/tea/work-queue — substrate-agnostic work-queue lifecycle on `Store<S>`.
6
7
  *
7
8
  * Two public surfaces, one shape:
@@ -1,4 +1,4 @@
1
- import { enqueueOp, claimNextOp, markDoneOp, patchItemOp, resetRunningOp, removeOp } from '../chunk-TGG26XMK.js';
1
+ import { enqueueOp, claimNextOp, markDoneOp, patchItemOp, resetRunningOp, removeOp } from '../chunk-FV3Z5MBE.js';
2
2
 
3
3
  // src/work-queue/index.ts
4
4
  function genId() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/work-queue/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/work-queue — substrate-agnostic work-queue lifecycle on `Store<S>`.\n *\n * Two public surfaces, one shape:\n * - `createQueue(store)` (this file) is the in-process adapter — it binds the\n * pure ops to an injected `Store` and runs `load → mutate → save`.\n * - `@demlik/tea/work-queue/ops` (`./ops`) is the BLESSED, importable surface\n * of those same pure ops. L2 compositions that own their own Model slice\n * (`monitored-run`, `idempotent-intake`, …) import the ops directly and\n * delegate status flips to them instead of re-rolling the transitions.\n * Both routes call the identical ops, so the lifecycle rules live in exactly\n * one place. This mirrors `@demlik/tea/do`'s one-shape-per-package rule.\n *\n * The package is generic in the input payload `I` (audit-specific fields go\n * here in apps/extension). `output` is a free-form side-channel correlation\n * token (a backend id, URL, trace id) — typed as `string` because that's its\n * only honest type. Consumers that need structured per-item data persist it in\n * their own store and put the foreign-key string in `output`.\n */\n\nimport type { Store } from \"../index\";\nimport {\n claimNextOp,\n enqueueOp,\n markDoneOp,\n patchItemOp,\n removeOp,\n resetRunningOp,\n} from \"./ops\";\n\n/**\n * Generate a queue-item id at the adapter boundary. Reads `Date.now()` and\n * `Math.random()` — impure helpers belong in the adapter (`index.ts`),\n * never in `ops.ts` (which is purely-functional by contract). Uses native\n * UUID when available, falls back to timestamp + random for environments\n * without `crypto.randomUUID`.\n */\nfunction genId(): string {\n return (\n globalThis.crypto?.randomUUID?.() ??\n `q_${Date.now()}_${Math.random().toString(36).slice(2)}`\n );\n}\n\nexport type QueueItemStatus =\n | \"pending\"\n | \"running\"\n | \"done\"\n | \"failed\"\n | \"cancelled\";\n\n/**\n * A single queue entry. Every field is `readonly`: a persisted slice is\n * immutable, so a transition returns a NEW item (the ops always spread —\n * `{ ...item, status: … }`) rather than mutating in place. This type-enforces\n * the durability invariant the consumers already honor by convention, and\n * keeps a slice round-trippable through JSON unchanged across a reload.\n */\nexport interface QueueItem<I> {\n readonly id: string;\n readonly status: QueueItemStatus;\n readonly input: I;\n readonly enqueuedAt: number;\n readonly startedAt?: number;\n readonly finishedAt?: number;\n readonly output?: string;\n readonly error?: string;\n}\n\n/**\n * Caller-facing enqueue payload. Just `I` today — kept named so future\n * variants (priority, deferUntil, …) can grow without breaking callers.\n */\nexport type EnqueueInput<I> = I;\n\n/**\n * Adapter that binds the pure ops to an injected `Store<QueueItem<I>[]>`.\n *\n * `load → mutate → save` is the only durability discipline this package\n * imposes. The store decides what \"save\" means (chrome.storage, KV, DO\n * storage, in-memory) — that decision belongs at the caller.\n *\n * Ops that don't change the queue (e.g. `markDone` on an unknown id) skip the\n * save call so the underlying store doesn't observe a write churn.\n */\nexport function createQueue<I>(store: Store<QueueItem<I>[]>) {\n // `Store<S>.load()` returns `unknown` (invariant 8 boundary). Route\n // through `store.migrate(raw)` to get the typed value before any\n // queue operation reads it — same wiring the substrate's `run()` uses at\n // boot. The work queue is a substrate-agnostic primitive on top of\n // `Store<S>`, so it honors the same contract.\n async function loadParsed(): Promise<QueueItem<I>[] | null> {\n const raw = await store.load();\n return store.migrate(raw);\n }\n\n async function list(): Promise<QueueItem<I>[]> {\n return (await loadParsed()) ?? [];\n }\n\n async function enqueue(input: EnqueueInput<I>): Promise<QueueItem<I>> {\n const queue = await list();\n const { next, item } = enqueueOp<I>(queue, input, Date.now(), genId());\n await store.save(next);\n return item;\n }\n\n async function claimNext(): Promise<QueueItem<I> | null> {\n const queue = await list();\n const result = claimNextOp<I>(queue, Date.now());\n if (!result) return null;\n await store.save(result.next);\n return result.claimed;\n }\n\n // `markDone` ignores the optional second arg by design: completed runs leave\n // the queue, and the consumer is expected to persist any output (auditId,\n // etc.) wherever those completed records belong. Kept in the signature for\n // call-site readability and forward-compat with `bindOutput`.\n async function markDone(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = markDoneOp<I>(queue, id);\n if (changed) await store.save(next);\n }\n\n async function markFailed(id: string, error: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"failed\",\n finishedAt: Date.now(),\n error,\n }));\n if (changed) await store.save(next);\n }\n\n async function markCancelled(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"cancelled\",\n finishedAt: Date.now(),\n }));\n if (changed) await store.save(next);\n }\n\n async function resetRunningToPending(): Promise<void> {\n const queue = await list();\n const { next, changed } = resetRunningOp<I>(queue);\n if (changed) await store.save(next);\n }\n\n async function removeItem(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = removeOp<I>(queue, id);\n if (changed) await store.save(next);\n }\n\n async function retryItem(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n finishedAt: undefined,\n error: undefined,\n output: undefined,\n }));\n if (changed) await store.save(next);\n }\n\n // `bindOutput` attaches a side-channel correlation token (string) to a\n // running item without changing its status. The queue can't reach into\n // `I` to mutate it (the substrate is domain-agnostic), so callers that\n // need to thread a backend id / URL / trace id through the lifecycle\n // route it here. Consumers needing structured payloads should persist\n // them in their own store and put the foreign-key string in `output`.\n async function bindOutput(id: string, value: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n output: value,\n }));\n if (changed) await store.save(next);\n }\n\n return {\n enqueue,\n claimNext,\n markDone,\n markFailed,\n markCancelled,\n resetRunningToPending,\n removeItem,\n retryItem,\n bindOutput,\n list,\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/work-queue/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @packageDocumentation\n * @demlik/tea/work-queue — substrate-agnostic work-queue lifecycle on `Store<S>`.\n *\n * Two public surfaces, one shape:\n * - `createQueue(store)` (this file) is the in-process adapter — it binds the\n * pure ops to an injected `Store` and runs `load → mutate → save`.\n * - `@demlik/tea/work-queue/ops` (`./ops`) is the BLESSED, importable surface\n * of those same pure ops. L2 compositions that own their own Model slice\n * (`monitored-run`, `idempotent-intake`, …) import the ops directly and\n * delegate status flips to them instead of re-rolling the transitions.\n * Both routes call the identical ops, so the lifecycle rules live in exactly\n * one place. This mirrors `@demlik/tea/do`'s one-shape-per-package rule.\n *\n * The package is generic in the input payload `I` (audit-specific fields go\n * here in apps/extension). `output` is a free-form side-channel correlation\n * token (a backend id, URL, trace id) — typed as `string` because that's its\n * only honest type. Consumers that need structured per-item data persist it in\n * their own store and put the foreign-key string in `output`.\n */\n\nimport type { Store } from \"../index\";\nimport {\n claimNextOp,\n enqueueOp,\n markDoneOp,\n patchItemOp,\n removeOp,\n resetRunningOp,\n} from \"./ops\";\n\n/**\n * Generate a queue-item id at the adapter boundary. Reads `Date.now()` and\n * `Math.random()` — impure helpers belong in the adapter (`index.ts`),\n * never in `ops.ts` (which is purely-functional by contract). Uses native\n * UUID when available, falls back to timestamp + random for environments\n * without `crypto.randomUUID`.\n */\nfunction genId(): string {\n return (\n globalThis.crypto?.randomUUID?.() ??\n `q_${Date.now()}_${Math.random().toString(36).slice(2)}`\n );\n}\n\nexport type QueueItemStatus =\n | \"pending\"\n | \"running\"\n | \"done\"\n | \"failed\"\n | \"cancelled\";\n\n/**\n * A single queue entry. Every field is `readonly`: a persisted slice is\n * immutable, so a transition returns a NEW item (the ops always spread —\n * `{ ...item, status: … }`) rather than mutating in place. This type-enforces\n * the durability invariant the consumers already honor by convention, and\n * keeps a slice round-trippable through JSON unchanged across a reload.\n */\nexport interface QueueItem<I> {\n readonly id: string;\n readonly status: QueueItemStatus;\n readonly input: I;\n readonly enqueuedAt: number;\n readonly startedAt?: number;\n readonly finishedAt?: number;\n readonly output?: string;\n readonly error?: string;\n}\n\n/**\n * Caller-facing enqueue payload. Just `I` today — kept named so future\n * variants (priority, deferUntil, …) can grow without breaking callers.\n */\nexport type EnqueueInput<I> = I;\n\n/**\n * Adapter that binds the pure ops to an injected `Store<QueueItem<I>[]>`.\n *\n * `load → mutate → save` is the only durability discipline this package\n * imposes. The store decides what \"save\" means (chrome.storage, KV, DO\n * storage, in-memory) — that decision belongs at the caller.\n *\n * Ops that don't change the queue (e.g. `markDone` on an unknown id) skip the\n * save call so the underlying store doesn't observe a write churn.\n */\nexport function createQueue<I>(store: Store<QueueItem<I>[]>) {\n // `Store<S>.load()` returns `unknown` (invariant 8 boundary). Route\n // through `store.migrate(raw)` to get the typed value before any\n // queue operation reads it — same wiring the substrate's `run()` uses at\n // boot. The work queue is a substrate-agnostic primitive on top of\n // `Store<S>`, so it honors the same contract.\n async function loadParsed(): Promise<QueueItem<I>[] | null> {\n const raw = await store.load();\n return store.migrate(raw);\n }\n\n async function list(): Promise<QueueItem<I>[]> {\n return (await loadParsed()) ?? [];\n }\n\n async function enqueue(input: EnqueueInput<I>): Promise<QueueItem<I>> {\n const queue = await list();\n const { next, item } = enqueueOp<I>(queue, input, Date.now(), genId());\n await store.save(next);\n return item;\n }\n\n async function claimNext(): Promise<QueueItem<I> | null> {\n const queue = await list();\n const result = claimNextOp<I>(queue, Date.now());\n if (!result) return null;\n await store.save(result.next);\n return result.claimed;\n }\n\n // `markDone` ignores the optional second arg by design: completed runs leave\n // the queue, and the consumer is expected to persist any output (auditId,\n // etc.) wherever those completed records belong. Kept in the signature for\n // call-site readability and forward-compat with `bindOutput`.\n async function markDone(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = markDoneOp<I>(queue, id);\n if (changed) await store.save(next);\n }\n\n async function markFailed(id: string, error: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"failed\",\n finishedAt: Date.now(),\n error,\n }));\n if (changed) await store.save(next);\n }\n\n async function markCancelled(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"cancelled\",\n finishedAt: Date.now(),\n }));\n if (changed) await store.save(next);\n }\n\n async function resetRunningToPending(): Promise<void> {\n const queue = await list();\n const { next, changed } = resetRunningOp<I>(queue);\n if (changed) await store.save(next);\n }\n\n async function removeItem(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = removeOp<I>(queue, id);\n if (changed) await store.save(next);\n }\n\n async function retryItem(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n finishedAt: undefined,\n error: undefined,\n output: undefined,\n }));\n if (changed) await store.save(next);\n }\n\n // `bindOutput` attaches a side-channel correlation token (string) to a\n // running item without changing its status. The queue can't reach into\n // `I` to mutate it (the substrate is domain-agnostic), so callers that\n // need to thread a backend id / URL / trace id through the lifecycle\n // route it here. Consumers needing structured payloads should persist\n // them in their own store and put the foreign-key string in `output`.\n async function bindOutput(id: string, value: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n output: value,\n }));\n if (changed) await store.save(next);\n }\n\n return {\n enqueue,\n claimNext,\n markDone,\n markFailed,\n markCancelled,\n resetRunningToPending,\n removeItem,\n retryItem,\n bindOutput,\n list,\n };\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { QueueItem } from './index.js';
2
- import '../index.js';
3
- import '../core-BmlJOvxM.js';
2
+ import '../runtime-types-CukV8kMX.js';
3
+ import '../core-42kapW0n.js';
4
4
 
5
5
  /**
6
6
  * Pure queue-lifecycle ops over `QueueItem<I>[]` — the BLESSED delegation
@@ -1,3 +1,3 @@
1
- export { claimNextOp, enqueueOp, markDoneOp, patchItemOp, removeOp, resetRunningOp } from '../chunk-TGG26XMK.js';
1
+ export { claimNextOp, enqueueOp, markDoneOp, patchItemOp, removeOp, resetRunningOp } from '../chunk-FV3Z5MBE.js';
2
2
  //# sourceMappingURL=ops.js.map
3
3
  //# sourceMappingURL=ops.js.map