@demlik/tea 0.2.0 → 0.3.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-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
  16. package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
  17. package/dist/{chunk-6VDCTR7N.js → chunk-BP2R7TPP.js} +203 -141
  18. package/dist/chunk-BP2R7TPP.js.map +1 -0
  19. package/dist/chunk-C6QC273X.js +71 -0
  20. package/dist/chunk-C6QC273X.js.map +1 -0
  21. package/dist/{chunk-ZDRN3YCE.js → chunk-CAXMQJJ3.js} +17 -24
  22. package/dist/chunk-CAXMQJJ3.js.map +1 -0
  23. package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
  24. package/dist/chunk-D736QUGF.js.map +1 -0
  25. package/dist/chunk-EWJP3YYP.js +21 -0
  26. package/dist/chunk-EWJP3YYP.js.map +1 -0
  27. package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
  28. package/dist/chunk-FV3Z5MBE.js.map +1 -0
  29. package/dist/chunk-G5E74IJB.js +8 -0
  30. package/dist/chunk-G5E74IJB.js.map +1 -0
  31. package/dist/{chunk-6GIGQT6C.js → chunk-IXBKFQH5.js} +12 -11
  32. package/dist/chunk-IXBKFQH5.js.map +1 -0
  33. package/dist/{chunk-G2U7DJHL.js → chunk-JFFVBEDI.js} +3 -3
  34. package/dist/{chunk-G2U7DJHL.js.map → chunk-JFFVBEDI.js.map} +1 -1
  35. package/dist/chunk-LYLDCIPH.js +365 -0
  36. package/dist/chunk-LYLDCIPH.js.map +1 -0
  37. package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
  38. package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
  39. package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
  40. package/dist/chunk-MUUROGIH.js.map +1 -0
  41. package/dist/{chunk-5KVHID6A.js → chunk-MVY5DAMF.js} +4 -4
  42. package/dist/{chunk-5KVHID6A.js.map → chunk-MVY5DAMF.js.map} +1 -1
  43. package/dist/{chunk-EGWTT5XJ.js → chunk-NPERUUUE.js} +76 -95
  44. package/dist/chunk-NPERUUUE.js.map +1 -0
  45. package/dist/{chunk-DYRKDCJI.js → chunk-PBDYW2TU.js} +41 -7
  46. package/dist/chunk-PBDYW2TU.js.map +1 -0
  47. package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
  48. package/dist/chunk-Q2UM7BXU.js.map +1 -0
  49. package/dist/chunk-RMX4EOQK.js +26 -0
  50. package/dist/chunk-RMX4EOQK.js.map +1 -0
  51. package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
  52. package/dist/chunk-S2EGRPCM.js.map +1 -0
  53. package/dist/{chunk-IK4S5DJN.js → chunk-S4O26D7B.js} +7 -14
  54. package/dist/chunk-S4O26D7B.js.map +1 -0
  55. package/dist/{chunk-6OUDPSSG.js → chunk-SK5JGUY7.js} +3 -3
  56. package/dist/{chunk-6OUDPSSG.js.map → chunk-SK5JGUY7.js.map} +1 -1
  57. package/dist/{chunk-FOGL3TC7.js → chunk-TPGASTFF.js} +8 -10
  58. package/dist/chunk-TPGASTFF.js.map +1 -0
  59. package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
  60. package/dist/chunk-UF4MZSLT.js.map +1 -0
  61. package/dist/{chunk-M6UZT7HS.js → chunk-VBUCHVCV.js} +6 -5
  62. package/dist/chunk-VBUCHVCV.js.map +1 -0
  63. package/dist/{chunk-VFGKTGGC.js → chunk-WGGAMW7Y.js} +191 -178
  64. package/dist/chunk-WGGAMW7Y.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 +3 -2
  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-gsog27b6.d.ts +337 -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,20 +1,22 @@
1
- export { COMPACTION_PURPOSE, agentBootMsg, agentEvents, agentTurnSchema, compactionSummarySchema, createAgent, isAgentTurn, isCompactionSummary, liftAgent, mergeInterpret, status } from '../chunk-VFGKTGGC.js';
2
- import '../chunk-ZDRN3YCE.js';
3
- import '../chunk-M6UZT7HS.js';
4
- import '../chunk-5KVHID6A.js';
5
- import '../chunk-6GIGQT6C.js';
1
+ export { COMPACTION_PURPOSE, agentBootMsg, agentEvents, agentTurnSchema, compactionSummarySchema, createAgent, isAgentTurn, isCompactionSummary, liftAgent, mergeInterpret, status } from '../chunk-WGGAMW7Y.js';
2
+ import '../chunk-CAXMQJJ3.js';
3
+ import '../chunk-VBUCHVCV.js';
4
+ import '../chunk-MVY5DAMF.js';
5
+ import '../chunk-IXBKFQH5.js';
6
6
  import '../chunk-JELSZN25.js';
7
- import '../chunk-UV6UWGEO.js';
8
- export { deadlineSub, subscribeDeadline } from '../chunk-SKAMZKTE.js';
9
- import '../chunk-5VNWZADP.js';
10
- import '../chunk-2YOE5PIS.js';
11
- import '../chunk-SC3FAFZN.js';
12
- import '../chunk-IN33Q4VJ.js';
13
- import '../chunk-HJO6VNQI.js';
14
- import '../chunk-TGG26XMK.js';
15
- import '../chunk-NSDFA6M6.js';
7
+ import '../chunk-D736QUGF.js';
8
+ export { deadlineSub, subscribeDeadline } from '../chunk-RMX4EOQK.js';
9
+ import '../chunk-Q2UM7BXU.js';
10
+ import '../chunk-S2EGRPCM.js';
11
+ import '../chunk-UF4MZSLT.js';
12
+ import '../chunk-2KNLKQVN.js';
13
+ import '../chunk-MU6MCIAC.js';
14
+ import '../chunk-FV3Z5MBE.js';
15
+ import '../chunk-LYLDCIPH.js';
16
16
  import '../chunk-2YFLOCWX.js';
17
- import '../chunk-6VDCTR7N.js';
18
- import '../chunk-DYRKDCJI.js';
17
+ import '../chunk-ZJ5AOYBH.js';
18
+ import '../chunk-G5E74IJB.js';
19
+ import '../chunk-BP2R7TPP.js';
20
+ import '../chunk-PBDYW2TU.js';
19
21
  //# sourceMappingURL=index.js.map
20
22
  //# sourceMappingURL=index.js.map
@@ -1,11 +1,11 @@
1
- import { c as PortEmitter, C as Cmd } from '../core-BmlJOvxM.js';
1
+ import { b as PortEmitter, C as Cmd } from '../core-42kapW0n.js';
2
2
  import { M as MsgType } from '../protocol-B5YmJr0d.js';
3
3
  import { ResilientConfig, ResilientState, RunCmd, ResilientPorts, SucceedMsg, FailMsg, ResilientTimerMsg } from '../resilient-call/index.js';
4
4
  import { TokenRefreshConfig, Token, TokenState, RefreshTokenCmd, TokenRefreshMsg } from '../token-refresh/index.js';
5
5
  import { DeadlineSub } from '../deadline/index.js';
6
6
  export { DeadlineExceeded, deadlineSub, subscribeDeadline } from '../deadline/index.js';
7
7
  import '../cache/index.js';
8
- import '../types-BoUWgwSm.js';
8
+ import '../types-KbL3W1mj.js';
9
9
  import '../circuit-breaker/index.js';
10
10
  import '../rate-limit/index.js';
11
11
  import '../retry-backoff/index.js';
@@ -100,7 +100,7 @@ declare function createAuthedCall<I, R>(config: AuthedConfig, rng?: () => number
100
100
  needsRefresh: (s: AuthedState<I, R>, at: number) => boolean;
101
101
  subs: (s: AuthedState<I, R>) => readonly DeadlineSub[];
102
102
  handlers: (ports: AuthedPorts<I, R>) => {
103
- refresh_token: (cmd: RefreshTokenCmd, ctx: PortEmitter) => Promise<void | TokenRefreshMsg>;
103
+ refresh_token: (cmd: RefreshTokenCmd, ctx: PortEmitter, dispatch?: ((msg: TokenRefreshMsg) => void) | undefined) => Promise<void | TokenRefreshMsg>;
104
104
  resilient_run: (cmd: RunCmd<I>, ctx: Readonly<Record<never, never>>) => Promise<FailMsg | SucceedMsg<R>>;
105
105
  };
106
106
  };
@@ -1,15 +1,16 @@
1
- import { createTokenRefresh, refreshTokenCmd } from '../chunk-6OUDPSSG.js';
2
- import { createResilientCall } from '../chunk-6GIGQT6C.js';
1
+ import { createTokenRefresh, refreshTokenCmd } from '../chunk-SK5JGUY7.js';
2
+ import { createResilientCall } from '../chunk-IXBKFQH5.js';
3
3
  import '../chunk-JELSZN25.js';
4
- export { deadlineSub, subscribeDeadline } from '../chunk-SKAMZKTE.js';
5
- import '../chunk-5VNWZADP.js';
6
- import '../chunk-2YOE5PIS.js';
7
- import '../chunk-SC3FAFZN.js';
8
- import '../chunk-IN33Q4VJ.js';
9
- import '../chunk-NSDFA6M6.js';
4
+ export { deadlineSub, subscribeDeadline } from '../chunk-RMX4EOQK.js';
5
+ import '../chunk-Q2UM7BXU.js';
6
+ import '../chunk-S2EGRPCM.js';
7
+ import '../chunk-UF4MZSLT.js';
8
+ import '../chunk-2KNLKQVN.js';
9
+ import '../chunk-LYLDCIPH.js';
10
10
  import '../chunk-2YFLOCWX.js';
11
- import '../chunk-6VDCTR7N.js';
12
- import '../chunk-DYRKDCJI.js';
11
+ import '../chunk-ZJ5AOYBH.js';
12
+ import '../chunk-BP2R7TPP.js';
13
+ import { without } from '../chunk-PBDYW2TU.js';
13
14
 
14
15
  // src/authed-call/index.ts
15
16
  function createAuthedCall(config, rng = Math.random) {
@@ -26,27 +27,22 @@ function createAuthedCall(config, rng = Math.random) {
26
27
  function liftResilient(s, [resilience, cmds]) {
27
28
  return [{ ...s, resilience }, cmds];
28
29
  }
29
- function without(rec, key) {
30
- const { [key]: _dropped, ...rest } = rec;
31
- return rest;
32
- }
33
- function attempt(s, key, input, at) {
34
- const cleared = {
30
+ function clearAuthKey(s, key) {
31
+ return {
35
32
  ...s,
36
33
  authRetry: without(s.authRetry, key),
37
34
  pendingAuthRetry: without(s.pendingAuthRetry, key)
38
35
  };
36
+ }
37
+ function attempt(s, key, input, at) {
38
+ const cleared = clearAuthKey(s, key);
39
39
  return liftResilient(
40
40
  cleared,
41
41
  rc.attempt(cleared.resilience, key, input, at)
42
42
  );
43
43
  }
44
44
  function succeed(s, key, msg) {
45
- const settled = {
46
- ...s,
47
- authRetry: without(s.authRetry, key),
48
- pendingAuthRetry: without(s.pendingAuthRetry, key)
49
- };
45
+ const settled = clearAuthKey(s, key);
50
46
  return liftResilient(settled, rc.succeed(settled.resilience, key, msg));
51
47
  }
52
48
  function fail(s, key, msg) {
@@ -61,15 +57,7 @@ function createAuthedCall(config, rng = Math.random) {
61
57
  if (spent >= 1 || input === void 0) {
62
58
  const error = { _tag: "unauthorized", key };
63
59
  const [settled] = rc.settleFailed(s.resilience, key, error);
64
- return [
65
- {
66
- ...s,
67
- resilience: settled,
68
- authRetry: without(s.authRetry, key),
69
- pendingAuthRetry: without(s.pendingAuthRetry, key)
70
- },
71
- []
72
- ];
60
+ return [{ ...clearAuthKey(s, key), resilience: settled }, []];
73
61
  }
74
62
  return [
75
63
  {
@@ -102,17 +90,15 @@ function createAuthedCall(config, rng = Math.random) {
102
90
  return [next, cmds];
103
91
  }
104
92
  function settleAuthIfDone(s, onlyKey, resilience) {
105
- let authRetry = s.authRetry;
106
- let pendingAuthRetry = s.pendingAuthRetry;
107
- const keys = onlyKey !== void 0 ? [onlyKey] : Object.keys(authRetry);
93
+ const keys = onlyKey !== void 0 ? [onlyKey] : Object.keys(s.authRetry);
94
+ let next = s;
108
95
  for (const key of keys) {
109
96
  const phase = resilience.calls[key]?.phase;
110
97
  if (phase === "succeeded" || phase === "failed") {
111
- authRetry = without(authRetry, key);
112
- pendingAuthRetry = without(pendingAuthRetry, key);
98
+ next = clearAuthKey(next, key);
113
99
  }
114
100
  }
115
- return { ...s, authRetry, pendingAuthRetry };
101
+ return next;
116
102
  }
117
103
  function subs(s) {
118
104
  return rc.subs(s.resilience);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/authed-call/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/authed-call — resilient-call with one extra dimension: a bearer\n * credential that must be minted before a call goes out and re-minted when the\n * server says it is no longer good (a 401). It is composition #10 in the specs:\n * *extends resilient-call; needs the L1 `token-refresh` brick*.\n *\n * The recipe — two existing bricks, stacked, nothing hand-wired:\n *\n * - `../resilient-call` owns ALL the call resilience: cache → circuit-breaker\n * → rate-limit → exponential-backoff retry, plus the per-call deadline. This\n * knob does not re-implement a single line of it. The `attempt` / `succeed`\n * / `fail` / `onTimer` verbs here are thin delegations that thread the\n * composed slice's `resilience` field through the resilient-call verb of the\n * same name. Whatever resilience config a consumer passes flows straight\n * down — every `ResilientConfig` field is still optional, still \"omit a\n * brick → omit its gate\".\n *\n * - `../token-refresh` owns the credential lifecycle: the `{ token, stale }`\n * slice and the decisions about *when* a token must be refreshed (expiry\n * skew, or a reactive 401). This knob adds the one verb resilient-call has\n * no concept of — `on401` — and routes the refresh I/O through token-\n * refresh's `refresh_token` handler.\n *\n * So the slice is exactly *resilient-call's slice + token-refresh's slice*:\n *\n * { resilience: ResilientState<I,R>, auth: TokenState }\n *\n * and the verb set is *resilient-call's four verbs + `on401`*, precisely the\n * spec's \"resilient-call's + `on401(s)` (refresh → retry once)\".\n *\n * ## The 401 dance — \"refresh → retry once\"\n *\n * A 401 is special: it is not a transient failure (backing off and retrying the\n * SAME credential just earns another 401), and it is not a terminal failure (a\n * fresh token usually fixes it). So `on401(s, key, at)` does three things:\n *\n * 1. marks the held token `stale` (via token-refresh's `on401`) and emits a\n * `refresh_token` Cmd to mint a new one;\n * 2. parks the in-flight call in a per-key `authRetry` budget so that, *once*,\n * the refresh landing re-issues that call with the new credential;\n * 3. caps that to a SINGLE auth-retry per call. A second 401 on the same call\n * after we already refreshed and retried does NOT loop — it settles the\n * call `failed` (errors are data: the failure is recorded, never swallowed\n * or spun on forever). The budget resets on the next fresh `attempt`.\n *\n * When the refresh resolves, the consumer folds `refreshed` into the auth slice\n * and calls `onRefreshed(s, at)`, which re-attempts every parked call through\n * the resilient gate with the new token in hand.\n *\n * ## The two non-negotiables (canon — inherited from both bricks)\n *\n * - **Durable** — the slice is `ResilientState` (plain data) + `TokenState`\n * (plain data) + a plain `Record<string, number>` retry budget. All JSON-\n * serializable, so it survives a Durable Object eviction / reload.\n * - **Replayable** — every verb returns `readonly [State, Cmd[]]` and never\n * reads the wall clock or RNG. Time arrives as an `at` parameter; the RNG\n * for retry jitter is injected once at `createAuthedCall(config, rng)` and\n * handed straight to the resilient-call knob. The only clock / network reads\n * live in `handlers` (the effect boundary) — resilient-call's `run` port and\n * token-refresh's `refresh` port.\n *\n * ## Typical wiring\n *\n * const ac = createAuthedCall<string, Resp>({\n * refresh: () => sdk.mintToken(),\n * retry: defaultRetryPolicy,\n * circuit: { threshold: 5, cooldownMs: 30_000 },\n * deadline: { ms: 5_000 },\n * skewMs: 30_000,\n * });\n *\n * // in the machine:\n * init: () => [ac.init(), []],\n * update: {\n * fetch: (s, m) => ac.attempt(s, m.key, m.input, m.at),\n * call_ok: (s, m) => ac.succeed(s, m.key, m),\n * call_err:(s, m) => ac.fail(s, m.key, m),\n * got_401: (s, m) => ac.on401(s, m.key, m.at),\n * token_refreshed: (s, m) => ac.onRefreshed(ac.installToken(s, m.token), m.at),\n * retry_due: (s, m) => ac.onTimer(s, m),\n * },\n * subscriptions: (s) => ac.subs(s),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: ac.handlers({ run: ctx.call, refresh: () => sdk.mintToken() }),\n */\n\nimport type { Cmd } from \"../index\";\nimport type { MsgType } from \"../protocol\";\nimport {\n createResilientCall,\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n type FailMsg,\n type ResilientConfig,\n type ResilientPorts,\n type ResilientState,\n type ResilientTimerMsg,\n type RunCmd,\n type SucceedMsg,\n subscribeDeadline,\n} from \"../resilient-call\";\nimport {\n createTokenRefresh,\n type RefreshTokenCmd,\n refreshTokenCmd,\n type Token,\n type TokenRefreshConfig,\n type TokenRefreshMsg,\n type TokenRefreshPorts,\n type TokenState,\n} from \"../token-refresh\";\n\n// ===========================================================================\n// Config — the knob. The resilient-call config PLUS the two token-refresh\n// fields. Every field optional except `refresh` (the credential is the whole\n// point: without a mint port there is nothing to authenticate with).\n// ===========================================================================\n\n/**\n * The authed-call knob. It is `ResilientConfig` widened by the token dimension:\n *\n * - `refresh` — the ONLY required field: the port that mints a fresh `Token`.\n * Identical to token-refresh's `TokenRefreshPorts.refresh`; it is hoisted\n * onto the config (not the ports) so the consumer spells the credential\n * source once, the same way `rng` is curried onto the factory.\n * - `skewMs` — token-refresh's refresh-ahead skew (optional, defaults to 0).\n * - every `ResilientConfig` field (`retry` / `circuit` / `rateLimit` /\n * `cache` / `deadline`) — passed straight through to the resilient-call\n * knob, each still optional, each still \"omit a brick → omit its gate\".\n */\nexport interface AuthedConfig extends ResilientConfig, TokenRefreshConfig {\n /** Mint a fresh credential. The single DI seam the auth dimension adds. */\n readonly refresh: () => Promise<Token>;\n}\n\n// ===========================================================================\n// Slice — resilient-call's slice + token-refresh's slice + the auth-retry\n// budget. A flat record, every field plain data.\n// ===========================================================================\n\n/**\n * The composed slice. Three plain-data fields, no closures:\n *\n * - `resilience` — resilient-call's entire slice (circuit, bucket, retry,\n * cache, calls). Owned and mutated only via resilient-call's verbs.\n * - `auth` — token-refresh's `{ token, stale }` slice. Owned and mutated only\n * via token-refresh's verbs.\n * - `authRetry` — per-key count of auth-retries (401 → refresh → retry) spent\n * on a call. Caps the dance at one retry per call so a credential the server\n * keeps rejecting cannot spin forever. A key absent from the record has\n * spent zero. Cleared when a call settles or a fresh `attempt` starts.\n *\n * `pendingAuthRetry` carries the inputs of calls parked waiting for the refresh\n * to land, keyed so `onRefreshed` can re-issue exactly those calls. Plain data —\n * `input` is the same payload the call was issued with.\n */\nexport interface AuthedState<I, R> {\n readonly resilience: ResilientState<I, R>;\n readonly auth: TokenState;\n readonly authRetry: Readonly<Record<string, number>>;\n readonly pendingAuthRetry: Readonly<Record<string, I>>;\n}\n\n// ===========================================================================\n// Cmds + Msgs. The union of both bricks' Cmds/Msgs plus a 401 trigger Msg.\n// ===========================================================================\n\n/** The two effects this knob emits: resilient-call's run, token-refresh's mint. */\nexport type AuthedCmd<I> = RunCmd<I> | RefreshTokenCmd;\n\n/**\n * Every Msg the knob's verbs fold. Resilient-call's settle + timer Msgs, token-\n * refresh's refresh-result Msgs, and the 401 trigger the consumer dispatches\n * when a guarded call comes back unauthorized.\n */\nexport type Unauthorized = {\n readonly type: typeof MsgType.Unauthorized;\n readonly key: string;\n readonly at: number;\n};\n\n/**\n * The plain-data error a 401 we cannot fix settles with. A `{_tag, ...}`\n * sentinel — NEVER a `new Error(...)` — so the slice stays JSON-serializable\n * (an Error object round-trips to `{}` under JSON and carries a stack trace,\n * both of which break the slice's durability invariant once it is persisted\n * into a `Store<S>` and reloaded). Mirrors resilient-call's `DeadlineExceededError`\n * sentinel discipline. `key` echoes the call that was rejected so a consumer can\n * attribute the failure.\n */\nexport type UnauthorizedError = {\n readonly _tag: \"unauthorized\";\n readonly key: string;\n};\n\n/** Ports `handlers` needs — resilient-call's run port AND token-refresh's refresh port. */\nexport interface AuthedPorts<I, R> extends ResilientPorts<I, R> {\n /** Mint a fresh credential. Mirrors `AuthedConfig.refresh` for symmetry at the splice site. */\n readonly refresh: () => Promise<Token>;\n}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build an authed-call knob from `config`. `rng` is injected for retry jitter\n * and threaded straight into the underlying resilient-call knob — pass a fixed\n * `() => 0.5` in tests to pin backoff; defaults to `Math.random` (read only at\n * the verb boundary inside resilient-call, never in a brick op).\n *\n * Returns the uniform L2 knob contract, widened by the auth verbs (`on401`,\n * `onRefreshed`, `installToken`). `I` is the port input type, `R` the result.\n */\nexport function createAuthedCall<I, R>(\n config: AuthedConfig,\n rng: () => number = Math.random,\n) {\n // Delegate ALL call resilience to the root composition. The resilient config\n // is exactly `config` minus the two auth-only fields — but since\n // `ResilientConfig` only reads the brick fields it knows, passing the wider\n // `config` is harmless. We pass it directly so future resilient bricks flow\n // through without a re-spread.\n const rc = createResilientCall<I, R>(config, rng);\n // Delegate the credential lifecycle to the L1 token brick.\n const tr = createTokenRefresh({ skewMs: config.skewMs });\n\n /** The starting slice — both bricks' inits plus empty auth-retry bookkeeping. */\n function init(): AuthedState<I, R> {\n return {\n resilience: rc.init(),\n auth: tr.init(),\n authRetry: {},\n pendingAuthRetry: {},\n };\n }\n\n // Rebuild the host record after a resilient-call verb, preserving the auth\n // dimension. The single seam where a `[ResilientState, Cmd[]]` becomes a\n // `[AuthedState, Cmd[]]`.\n function liftResilient(\n s: AuthedState<I, R>,\n [resilience, cmds]: readonly [ResilientState<I, R>, readonly RunCmd<I>[]],\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n return [{ ...s, resilience }, cmds];\n }\n\n // Drop a key from a `Record` immutably.\n function without<V>(\n rec: Readonly<Record<string, V>>,\n key: string,\n ): Readonly<Record<string, V>> {\n const { [key]: _dropped, ...rest } = rec;\n return rest;\n }\n\n // === Verb: attempt =======================================================\n\n /**\n * Start (or restart) the call for `key`. Delegates the gate entirely to\n * resilient-call. A fresh attempt resets this key's auth-retry budget (a new\n * logical call gets its own single 401 retry) and drops any stale parked\n * input. PURE — `at` is the only clock.\n *\n * Note this knob does NOT proactively refresh on expiry inside `attempt`: the\n * expiry-driven refresh is the consumer's call-boundary concern (token-refresh\n * exposes `ensureFresh` for exactly that, re-exported below). `attempt` owns\n * the *reactive* path — it issues the call, and a 401 coming back triggers the\n * refresh. Keeping the two seams separate means a consumer who wants only\n * reactive auth (no proactive refresh) wires nothing extra.\n */\n function attempt(\n s: AuthedState<I, R>,\n key: string,\n input: I,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const cleared: AuthedState<I, R> = {\n ...s,\n authRetry: without(s.authRetry, key),\n pendingAuthRetry: without(s.pendingAuthRetry, key),\n };\n return liftResilient(\n cleared,\n rc.attempt(cleared.resilience, key, input, at),\n );\n }\n\n // === Verb: succeed =======================================================\n\n /**\n * Record a success for `key`: delegate to resilient-call (closes the breaker,\n * fills the cache, resets retry) and clear this key's auth-retry bookkeeping —\n * the call settled, so its 401 budget and any parked input are forgotten.\n * PURE.\n */\n function succeed(\n s: AuthedState<I, R>,\n key: string,\n msg: SucceedMsg<R>,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const settled: AuthedState<I, R> = {\n ...s,\n authRetry: without(s.authRetry, key),\n pendingAuthRetry: without(s.pendingAuthRetry, key),\n };\n return liftResilient(settled, rc.succeed(settled.resilience, key, msg));\n }\n\n // === Verb: fail ==========================================================\n\n /**\n * Record a NON-auth failure for `key` (a 5xx, a network drop — anything that\n * is not a 401). Delegate to resilient-call's `fail`, which trips the breaker\n * and backs off / settles per the retry policy. The auth dimension is\n * untouched: a generic failure says nothing about the credential. PURE.\n *\n * A 401 is NOT routed here — the consumer dispatches `unauthorized` and the\n * machine calls `on401` instead. Routing a 401 through `fail` would burn the\n * resilient retry budget on a credential that a plain re-issue cannot fix.\n */\n function fail(\n s: AuthedState<I, R>,\n key: string,\n msg: FailMsg,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n // If the resilient verb settles this call, forget its auth bookkeeping too.\n const [resilience, cmds] = rc.fail(s.resilience, key, msg);\n const next = settleAuthIfDone({ ...s, resilience }, key, resilience);\n return [next, cmds];\n }\n\n // === Verb: on401 =========================================================\n\n /**\n * The auth dimension's one verb: the server rejected `key`'s call as\n * unauthorized. \"Refresh → retry once\":\n *\n * - First 401 on this call → mark the token stale, emit a `refresh_token`\n * Cmd, and PARK the call's input so the refresh landing re-issues it. The\n * resilient call phase is left as-is (still `running`, or `waiting_retry`\n * if a transient failure had already backed it off); the in-flight\n * resilient effect is abandoned in favour of the post-refresh re-issue.\n * - Second 401 (the budget is already spent) → settle the call `failed`\n * with the unauthorized error. No second refresh, no loop. Errors are\n * data: the failure is recorded on the resilient slice, visible to the\n * consumer.\n *\n * A 401 is NOT a downstream-health signal — a credential the server rejected\n * says nothing about the backend's health. So the terminal path routes through\n * resilient-call's `settleFailed` (which settles `failed` WITHOUT tripping the\n * shared breaker or advancing this key's retry counter), never `fail` (which\n * would punish a healthy target and burn the retry budget on a request that a\n * plain re-issue cannot fix).\n *\n * PURE — `at` is carried for symmetry with the other verbs (the refresh Cmd\n * itself carries no time; the refresh port stamps its own result Msg).\n */\n function on401(\n s: AuthedState<I, R>,\n key: string,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n void at; // carried for symmetry; the async refresh's result Msg stamps the clock.\n const spent = s.authRetry[key] ?? 0;\n const call = s.resilience.calls[key];\n // A 401 can arrive while the call is `running` OR after a transient failure\n // backed it off to `waiting_retry`; both hold the input needed to re-issue.\n // Reading only `running` would drop a 401 that races a backoff — the call\n // would never refresh.\n const input =\n call?.phase === \"running\" || call?.phase === \"waiting_retry\"\n ? call.input\n : undefined;\n\n // Budget exhausted (already refreshed + retried once), or nothing to\n // re-issue: settle the call failed with the unauthorized error. Through\n // `settleFailed`, NOT `fail` — a 401 is not a breaker / retry signal.\n if (spent >= 1 || input === undefined) {\n const error: UnauthorizedError = { _tag: \"unauthorized\", key };\n const [settled] = rc.settleFailed(s.resilience, key, error);\n return [\n {\n ...s,\n resilience: settled,\n authRetry: without(s.authRetry, key),\n pendingAuthRetry: without(s.pendingAuthRetry, key),\n },\n [],\n ];\n }\n\n // First 401: mark stale, park the call, ask for a fresh token.\n return [\n {\n ...s,\n auth: tr.on401(s.auth),\n authRetry: { ...s.authRetry, [key]: spent + 1 },\n pendingAuthRetry: { ...s.pendingAuthRetry, [key]: input },\n },\n [refreshTokenCmd()],\n ];\n }\n\n // === Verb: installToken ==================================================\n\n /**\n * Fold a freshly minted `token` into the auth slice (delegates to token-\n * refresh's `refreshed`: installs the token, clears `stale`). PURE. Call this\n * from the `token_refreshed` reducer cell, then chain `onRefreshed` to re-fire\n * the parked calls. Split from `onRefreshed` so a consumer can install a token\n * without auto-retrying (e.g. a proactive expiry refresh with nothing parked).\n */\n function installToken(s: AuthedState<I, R>, token: Token): AuthedState<I, R> {\n return { ...s, auth: tr.refreshed(s.auth, token) };\n }\n\n // === Verb: onRefreshed ===================================================\n\n /**\n * Re-issue every call parked by a 401 now that a fresh token has landed. Runs\n * each parked input back through resilient-call's `attempt` gate (so the\n * circuit / rate-limit / cache are all re-checked against the new `at`),\n * clears the parked set, and accumulates the run Cmds. The auth-retry COUNT is\n * preserved (each re-issued call has already spent one auth-retry, so a second\n * 401 on it settles failed). PURE — `at` is the gate's clock.\n *\n * No parked calls → a no-op that returns the slice unchanged by reference, so\n * a proactive (expiry) refresh with nothing in flight costs nothing.\n */\n function onRefreshed(\n s: AuthedState<I, R>,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const parked = Object.entries(s.pendingAuthRetry);\n if (parked.length === 0) return [s, []];\n\n let resilience = s.resilience;\n const cmds: AuthedCmd<I>[] = [];\n for (const [key, input] of parked) {\n const [next, runCmds] = rc.attempt(resilience, key, input, at);\n resilience = next;\n cmds.push(...runCmds);\n }\n return [{ ...s, resilience, pendingAuthRetry: {} }, cmds];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A retry / deadline timer fired — delegate straight to resilient-call. If the\n * delegated verb settled the call, forget its auth bookkeeping. PURE.\n */\n function onTimer(\n s: AuthedState<I, R>,\n msg: ResilientTimerMsg,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const [resilience, cmds] = rc.onTimer(s.resilience, msg);\n const next = settleAuthIfDone({ ...s, resilience }, undefined, resilience);\n return [next, cmds];\n }\n\n // --- helpers --------------------------------------------------------------\n\n // After a resilient verb, drop auth bookkeeping for any key the resilient\n // slice now reports as settled (succeeded / failed). `onlyKey` narrows the\n // scan to one key when the verb targeted one (fail); `undefined` scans all\n // (onTimer can settle a deadline on any key).\n function settleAuthIfDone(\n s: AuthedState<I, R>,\n onlyKey: string | undefined,\n resilience: ResilientState<I, R>,\n ): AuthedState<I, R> {\n let authRetry = s.authRetry;\n let pendingAuthRetry = s.pendingAuthRetry;\n const keys = onlyKey !== undefined ? [onlyKey] : Object.keys(authRetry);\n for (const key of keys) {\n const phase = resilience.calls[key]?.phase;\n if (phase === \"succeeded\" || phase === \"failed\") {\n authRetry = without(authRetry, key);\n pendingAuthRetry = without(pendingAuthRetry, key);\n }\n }\n return { ...s, authRetry, pendingAuthRetry };\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions — exactly resilient-call's (retry + deadline timers\n * keyed per call). The auth dimension arms NO timers: a refresh is a one-shot\n * Cmd, not a recurring sub, and the parked-call re-issue is driven by the\n * `token_refreshed` Msg, not a timer. Wire `subscribe: { deadline:\n * subscribeDeadline }`.\n */\n function subs(s: AuthedState<I, R>): readonly DeadlineSub[] {\n return rc.subs(s.resilience);\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handlers: resilient-call's `resilient_run` (wraps the\n * `run` port) AND token-refresh's `refresh_token` (wraps the `refresh` port).\n * Both route Ok/Err through `tryInterpret`, stamping `Date.now()` at the\n * effect boundary — the only clock reads in the whole module. Assign to the\n * machine's `interpret`:\n *\n * interpret: ac.handlers({ run: ctx.call, refresh: () => sdk.mintToken() })\n */\n function handlers(ports: AuthedPorts<I, R>) {\n const trPorts: TokenRefreshPorts = { refresh: ports.refresh };\n return {\n ...rc.handlers({ run: ports.run }),\n ...tr.handlers(trPorts),\n };\n }\n\n return {\n init,\n attempt,\n succeed,\n fail,\n on401,\n installToken,\n onRefreshed,\n onTimer,\n /** token-refresh's call-boundary verb, re-exposed for proactive expiry refresh. */\n needsRefresh: (s: AuthedState<I, R>, at: number) =>\n tr.needsRefresh(s.auth, at),\n subs,\n handlers,\n };\n}\n\n/** The shape `createAuthedCall` returns — useful for typing a held knob. */\nexport type AuthedCall<I, R> = ReturnType<typeof createAuthedCall<I, R>>;\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice\n * lives at `state.authed`. A convenience for the common single-slice host;\n * consumers with a differently-named field spread by hand. Pure — a thin record\n * rebuild, no clock / RNG.\n */\nexport function liftAuthed<\n S extends { authed: AuthedState<I, R> },\n I,\n R,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [AuthedState<I, R>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, authed: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives (this knob's `subs` emits `DeadlineSub`s,\n * inherited from resilient-call) and the token-refresh Msg constructors a\n * consumer wires into their reducer, so the whole composition imports from one\n * subpath.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type {\n DeadlineSub,\n DeadlineExceeded,\n ResilientState,\n TokenState,\n Token,\n TokenRefreshMsg,\n SucceedMsg,\n FailMsg,\n ResilientTimerMsg,\n RunCmd,\n};\n"]}
1
+ {"version":3,"sources":["../../src/authed-call/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/authed-call — resilient-call with one extra dimension: a bearer\n * credential that must be minted before a call goes out and re-minted when the\n * server says it is no longer good (a 401). It is composition #10 in the specs:\n * *extends resilient-call; needs the L1 `token-refresh` brick*.\n *\n * The recipe — two existing bricks, stacked, nothing hand-wired:\n *\n * - `../resilient-call` owns ALL the call resilience: cache → circuit-breaker\n * → rate-limit → exponential-backoff retry, plus the per-call deadline. This\n * knob does not re-implement a single line of it. The `attempt` / `succeed`\n * / `fail` / `onTimer` verbs here are thin delegations that thread the\n * composed slice's `resilience` field through the resilient-call verb of the\n * same name. Whatever resilience config a consumer passes flows straight\n * down — every `ResilientConfig` field is still optional, still \"omit a\n * brick → omit its gate\".\n *\n * - `../token-refresh` owns the credential lifecycle: the `{ token, stale }`\n * slice and the decisions about *when* a token must be refreshed (expiry\n * skew, or a reactive 401). This knob adds the one verb resilient-call has\n * no concept of — `on401` — and routes the refresh I/O through token-\n * refresh's `refresh_token` handler.\n *\n * So the slice is exactly *resilient-call's slice + token-refresh's slice*:\n *\n * { resilience: ResilientState<I,R>, auth: TokenState }\n *\n * and the verb set is *resilient-call's four verbs + `on401`*, precisely the\n * spec's \"resilient-call's + `on401(s)` (refresh → retry once)\".\n *\n * ## The 401 dance — \"refresh → retry once\"\n *\n * A 401 is special: it is not a transient failure (backing off and retrying the\n * SAME credential just earns another 401), and it is not a terminal failure (a\n * fresh token usually fixes it). So `on401(s, key, at)` does three things:\n *\n * 1. marks the held token `stale` (via token-refresh's `on401`) and emits a\n * `refresh_token` Cmd to mint a new one;\n * 2. parks the in-flight call in a per-key `authRetry` budget so that, *once*,\n * the refresh landing re-issues that call with the new credential;\n * 3. caps that to a SINGLE auth-retry per call. A second 401 on the same call\n * after we already refreshed and retried does NOT loop — it settles the\n * call `failed` (errors are data: the failure is recorded, never swallowed\n * or spun on forever). The budget resets on the next fresh `attempt`.\n *\n * When the refresh resolves, the consumer folds `refreshed` into the auth slice\n * and calls `onRefreshed(s, at)`, which re-attempts every parked call through\n * the resilient gate with the new token in hand.\n *\n * ## The two non-negotiables (canon — inherited from both bricks)\n *\n * - **Durable** — the slice is `ResilientState` (plain data) + `TokenState`\n * (plain data) + a plain `Record<string, number>` retry budget. All JSON-\n * serializable, so it survives a Durable Object eviction / reload.\n * - **Replayable** — every verb returns `readonly [State, Cmd[]]` and never\n * reads the wall clock or RNG. Time arrives as an `at` parameter; the RNG\n * for retry jitter is injected once at `createAuthedCall(config, rng)` and\n * handed straight to the resilient-call knob. The only clock / network reads\n * live in `handlers` (the effect boundary) — resilient-call's `run` port and\n * token-refresh's `refresh` port.\n *\n * ## Typical wiring\n *\n * const ac = createAuthedCall<string, Resp>({\n * refresh: () => sdk.mintToken(),\n * retry: defaultRetryPolicy,\n * circuit: { threshold: 5, cooldownMs: 30_000 },\n * deadline: { ms: 5_000 },\n * skewMs: 30_000,\n * });\n *\n * // in the machine:\n * init: () => [ac.init(), []],\n * update: {\n * fetch: (s, m) => ac.attempt(s, m.key, m.input, m.at),\n * call_ok: (s, m) => ac.succeed(s, m.key, m),\n * call_err:(s, m) => ac.fail(s, m.key, m),\n * got_401: (s, m) => ac.on401(s, m.key, m.at),\n * token_refreshed: (s, m) => ac.onRefreshed(ac.installToken(s, m.token), m.at),\n * retry_due: (s, m) => ac.onTimer(s, m),\n * },\n * subscriptions: (s) => ac.subs(s),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: ac.handlers({ run: ctx.call, refresh: () => sdk.mintToken() }),\n */\n\nimport type { Cmd } from \"../index\";\nimport type { MsgType } from \"../protocol\";\nimport { without } from \"../pure/core\";\nimport {\n createResilientCall,\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n type FailMsg,\n type ResilientConfig,\n type ResilientPorts,\n type ResilientState,\n type ResilientTimerMsg,\n type RunCmd,\n type SucceedMsg,\n subscribeDeadline,\n} from \"../resilient-call\";\nimport {\n createTokenRefresh,\n type RefreshTokenCmd,\n refreshTokenCmd,\n type Token,\n type TokenRefreshConfig,\n type TokenRefreshMsg,\n type TokenRefreshPorts,\n type TokenState,\n} from \"../token-refresh\";\n\n// ===========================================================================\n// Config — the knob. The resilient-call config PLUS the two token-refresh\n// fields. Every field optional except `refresh` (the credential is the whole\n// point: without a mint port there is nothing to authenticate with).\n// ===========================================================================\n\n/**\n * The authed-call knob. It is `ResilientConfig` widened by the token dimension:\n *\n * - `refresh` — the ONLY required field: the port that mints a fresh `Token`.\n * Identical to token-refresh's `TokenRefreshPorts.refresh`; it is hoisted\n * onto the config (not the ports) so the consumer spells the credential\n * source once, the same way `rng` is curried onto the factory.\n * - `skewMs` — token-refresh's refresh-ahead skew (optional, defaults to 0).\n * - every `ResilientConfig` field (`retry` / `circuit` / `rateLimit` /\n * `cache` / `deadline`) — passed straight through to the resilient-call\n * knob, each still optional, each still \"omit a brick → omit its gate\".\n */\nexport interface AuthedConfig extends ResilientConfig, TokenRefreshConfig {\n /** Mint a fresh credential. The single DI seam the auth dimension adds. */\n readonly refresh: () => Promise<Token>;\n}\n\n// ===========================================================================\n// Slice — resilient-call's slice + token-refresh's slice + the auth-retry\n// budget. A flat record, every field plain data.\n// ===========================================================================\n\n/**\n * The composed slice. Three plain-data fields, no closures:\n *\n * - `resilience` — resilient-call's entire slice (circuit, bucket, retry,\n * cache, calls). Owned and mutated only via resilient-call's verbs.\n * - `auth` — token-refresh's `{ token, stale }` slice. Owned and mutated only\n * via token-refresh's verbs.\n * - `authRetry` — per-key count of auth-retries (401 → refresh → retry) spent\n * on a call. Caps the dance at one retry per call so a credential the server\n * keeps rejecting cannot spin forever. A key absent from the record has\n * spent zero. Cleared when a call settles or a fresh `attempt` starts.\n *\n * `pendingAuthRetry` carries the inputs of calls parked waiting for the refresh\n * to land, keyed so `onRefreshed` can re-issue exactly those calls. Plain data —\n * `input` is the same payload the call was issued with.\n */\nexport interface AuthedState<I, R> {\n readonly resilience: ResilientState<I, R>;\n readonly auth: TokenState;\n readonly authRetry: Readonly<Record<string, number>>;\n readonly pendingAuthRetry: Readonly<Record<string, I>>;\n}\n\n// ===========================================================================\n// Cmds + Msgs. The union of both bricks' Cmds/Msgs plus a 401 trigger Msg.\n// ===========================================================================\n\n/** The two effects this knob emits: resilient-call's run, token-refresh's mint. */\nexport type AuthedCmd<I> = RunCmd<I> | RefreshTokenCmd;\n\n/**\n * Every Msg the knob's verbs fold. Resilient-call's settle + timer Msgs, token-\n * refresh's refresh-result Msgs, and the 401 trigger the consumer dispatches\n * when a guarded call comes back unauthorized.\n */\nexport type Unauthorized = {\n readonly type: typeof MsgType.Unauthorized;\n readonly key: string;\n readonly at: number;\n};\n\n/**\n * The plain-data error a 401 we cannot fix settles with. A `{_tag, ...}`\n * sentinel — NEVER a `new Error(...)` — so the slice stays JSON-serializable\n * (an Error object round-trips to `{}` under JSON and carries a stack trace,\n * both of which break the slice's durability invariant once it is persisted\n * into a `Store<S>` and reloaded). Mirrors resilient-call's `DeadlineExceededError`\n * sentinel discipline. `key` echoes the call that was rejected so a consumer can\n * attribute the failure.\n */\nexport type UnauthorizedError = {\n readonly _tag: \"unauthorized\";\n readonly key: string;\n};\n\n/** Ports `handlers` needs — resilient-call's run port AND token-refresh's refresh port. */\nexport interface AuthedPorts<I, R> extends ResilientPorts<I, R> {\n /** Mint a fresh credential. Mirrors `AuthedConfig.refresh` for symmetry at the splice site. */\n readonly refresh: () => Promise<Token>;\n}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build an authed-call knob from `config`. `rng` is injected for retry jitter\n * and threaded straight into the underlying resilient-call knob — pass a fixed\n * `() => 0.5` in tests to pin backoff; defaults to `Math.random` (read only at\n * the verb boundary inside resilient-call, never in a brick op).\n *\n * Returns the uniform L2 knob contract, widened by the auth verbs (`on401`,\n * `onRefreshed`, `installToken`). `I` is the port input type, `R` the result.\n */\nexport function createAuthedCall<I, R>(\n config: AuthedConfig,\n rng: () => number = Math.random,\n) {\n // Delegate ALL call resilience to the root composition. The resilient config\n // is exactly `config` minus the two auth-only fields — but since\n // `ResilientConfig` only reads the brick fields it knows, passing the wider\n // `config` is harmless. We pass it directly so future resilient bricks flow\n // through without a re-spread.\n const rc = createResilientCall<I, R>(config, rng);\n // Delegate the credential lifecycle to the L1 token brick.\n const tr = createTokenRefresh({ skewMs: config.skewMs });\n\n /** The starting slice — both bricks' inits plus empty auth-retry bookkeeping. */\n function init(): AuthedState<I, R> {\n return {\n resilience: rc.init(),\n auth: tr.init(),\n authRetry: {},\n pendingAuthRetry: {},\n };\n }\n\n // Rebuild the host record after a resilient-call verb, preserving the auth\n // dimension. The single seam where a `[ResilientState, Cmd[]]` becomes a\n // `[AuthedState, Cmd[]]`.\n function liftResilient(\n s: AuthedState<I, R>,\n [resilience, cmds]: readonly [ResilientState<I, R>, readonly RunCmd<I>[]],\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n return [{ ...s, resilience }, cmds];\n }\n\n // Forget a key's auth bookkeeping: the 401 budget (`authRetry`) and any\n // parked input (`pendingAuthRetry`) that a settled or restarted call must\n // clear TOGETHER. These two records form a coupled invariant — a call that\n // settles, restarts, or gives up on its 401 dance drops both — so the joint\n // clear lives in one place rather than being open-coded at each site.\n function clearAuthKey(s: AuthedState<I, R>, key: string): AuthedState<I, R> {\n return {\n ...s,\n authRetry: without(s.authRetry, key),\n pendingAuthRetry: without(s.pendingAuthRetry, key),\n };\n }\n\n // === Verb: attempt =======================================================\n\n /**\n * Start (or restart) the call for `key`. Delegates the gate entirely to\n * resilient-call. A fresh attempt resets this key's auth-retry budget (a new\n * logical call gets its own single 401 retry) and drops any stale parked\n * input. PURE — `at` is the only clock.\n *\n * Note this knob does NOT proactively refresh on expiry inside `attempt`: the\n * expiry-driven refresh is the consumer's call-boundary concern (token-refresh\n * exposes `ensureFresh` for exactly that, re-exported below). `attempt` owns\n * the *reactive* path — it issues the call, and a 401 coming back triggers the\n * refresh. Keeping the two seams separate means a consumer who wants only\n * reactive auth (no proactive refresh) wires nothing extra.\n */\n function attempt(\n s: AuthedState<I, R>,\n key: string,\n input: I,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const cleared = clearAuthKey(s, key);\n return liftResilient(\n cleared,\n rc.attempt(cleared.resilience, key, input, at),\n );\n }\n\n // === Verb: succeed =======================================================\n\n /**\n * Record a success for `key`: delegate to resilient-call (closes the breaker,\n * fills the cache, resets retry) and clear this key's auth-retry bookkeeping —\n * the call settled, so its 401 budget and any parked input are forgotten.\n * PURE.\n */\n function succeed(\n s: AuthedState<I, R>,\n key: string,\n msg: SucceedMsg<R>,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const settled = clearAuthKey(s, key);\n return liftResilient(settled, rc.succeed(settled.resilience, key, msg));\n }\n\n // === Verb: fail ==========================================================\n\n /**\n * Record a NON-auth failure for `key` (a 5xx, a network drop — anything that\n * is not a 401). Delegate to resilient-call's `fail`, which trips the breaker\n * and backs off / settles per the retry policy. The auth dimension is\n * untouched: a generic failure says nothing about the credential. PURE.\n *\n * A 401 is NOT routed here — the consumer dispatches `unauthorized` and the\n * machine calls `on401` instead. Routing a 401 through `fail` would burn the\n * resilient retry budget on a credential that a plain re-issue cannot fix.\n */\n function fail(\n s: AuthedState<I, R>,\n key: string,\n msg: FailMsg,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n // If the resilient verb settles this call, forget its auth bookkeeping too.\n const [resilience, cmds] = rc.fail(s.resilience, key, msg);\n const next = settleAuthIfDone({ ...s, resilience }, key, resilience);\n return [next, cmds];\n }\n\n // === Verb: on401 =========================================================\n\n /**\n * The auth dimension's one verb: the server rejected `key`'s call as\n * unauthorized. \"Refresh → retry once\":\n *\n * - First 401 on this call → mark the token stale, emit a `refresh_token`\n * Cmd, and PARK the call's input so the refresh landing re-issues it. The\n * resilient call phase is left as-is (still `running`, or `waiting_retry`\n * if a transient failure had already backed it off); the in-flight\n * resilient effect is abandoned in favour of the post-refresh re-issue.\n * - Second 401 (the budget is already spent) → settle the call `failed`\n * with the unauthorized error. No second refresh, no loop. Errors are\n * data: the failure is recorded on the resilient slice, visible to the\n * consumer.\n *\n * A 401 is NOT a downstream-health signal — a credential the server rejected\n * says nothing about the backend's health. So the terminal path routes through\n * resilient-call's `settleFailed` (which settles `failed` WITHOUT tripping the\n * shared breaker or advancing this key's retry counter), never `fail` (which\n * would punish a healthy target and burn the retry budget on a request that a\n * plain re-issue cannot fix).\n *\n * PURE — `at` is carried for symmetry with the other verbs (the refresh Cmd\n * itself carries no time; the refresh port stamps its own result Msg).\n */\n function on401(\n s: AuthedState<I, R>,\n key: string,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n void at; // carried for symmetry; the async refresh's result Msg stamps the clock.\n const spent = s.authRetry[key] ?? 0;\n const call = s.resilience.calls[key];\n // A 401 can arrive while the call is `running` OR after a transient failure\n // backed it off to `waiting_retry`; both hold the input needed to re-issue.\n // Reading only `running` would drop a 401 that races a backoff — the call\n // would never refresh.\n const input =\n call?.phase === \"running\" || call?.phase === \"waiting_retry\"\n ? call.input\n : undefined;\n\n // Budget exhausted (already refreshed + retried once), or nothing to\n // re-issue: settle the call failed with the unauthorized error. Through\n // `settleFailed`, NOT `fail` — a 401 is not a breaker / retry signal.\n if (spent >= 1 || input === undefined) {\n const error: UnauthorizedError = { _tag: \"unauthorized\", key };\n const [settled] = rc.settleFailed(s.resilience, key, error);\n return [{ ...clearAuthKey(s, key), resilience: settled }, []];\n }\n\n // First 401: mark stale, park the call, ask for a fresh token.\n return [\n {\n ...s,\n auth: tr.on401(s.auth),\n authRetry: { ...s.authRetry, [key]: spent + 1 },\n pendingAuthRetry: { ...s.pendingAuthRetry, [key]: input },\n },\n [refreshTokenCmd()],\n ];\n }\n\n // === Verb: installToken ==================================================\n\n /**\n * Fold a freshly minted `token` into the auth slice (delegates to token-\n * refresh's `refreshed`: installs the token, clears `stale`). PURE. Call this\n * from the `token_refreshed` reducer cell, then chain `onRefreshed` to re-fire\n * the parked calls. Split from `onRefreshed` so a consumer can install a token\n * without auto-retrying (e.g. a proactive expiry refresh with nothing parked).\n */\n function installToken(s: AuthedState<I, R>, token: Token): AuthedState<I, R> {\n return { ...s, auth: tr.refreshed(s.auth, token) };\n }\n\n // === Verb: onRefreshed ===================================================\n\n /**\n * Re-issue every call parked by a 401 now that a fresh token has landed. Runs\n * each parked input back through resilient-call's `attempt` gate (so the\n * circuit / rate-limit / cache are all re-checked against the new `at`),\n * clears the parked set, and accumulates the run Cmds. The auth-retry COUNT is\n * preserved (each re-issued call has already spent one auth-retry, so a second\n * 401 on it settles failed). PURE — `at` is the gate's clock.\n *\n * No parked calls → a no-op that returns the slice unchanged by reference, so\n * a proactive (expiry) refresh with nothing in flight costs nothing.\n */\n function onRefreshed(\n s: AuthedState<I, R>,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const parked = Object.entries(s.pendingAuthRetry);\n if (parked.length === 0) return [s, []];\n\n let resilience = s.resilience;\n const cmds: AuthedCmd<I>[] = [];\n for (const [key, input] of parked) {\n const [next, runCmds] = rc.attempt(resilience, key, input, at);\n resilience = next;\n cmds.push(...runCmds);\n }\n return [{ ...s, resilience, pendingAuthRetry: {} }, cmds];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A retry / deadline timer fired — delegate straight to resilient-call. If the\n * delegated verb settled the call, forget its auth bookkeeping. PURE.\n */\n function onTimer(\n s: AuthedState<I, R>,\n msg: ResilientTimerMsg,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const [resilience, cmds] = rc.onTimer(s.resilience, msg);\n const next = settleAuthIfDone({ ...s, resilience }, undefined, resilience);\n return [next, cmds];\n }\n\n // --- helpers --------------------------------------------------------------\n\n // After a resilient verb, drop auth bookkeeping for any key the resilient\n // slice now reports as settled (succeeded / failed). `onlyKey` narrows the\n // scan to one key when the verb targeted one (fail); `undefined` scans all\n // (onTimer can settle a deadline on any key).\n function settleAuthIfDone(\n s: AuthedState<I, R>,\n onlyKey: string | undefined,\n resilience: ResilientState<I, R>,\n ): AuthedState<I, R> {\n const keys = onlyKey !== undefined ? [onlyKey] : Object.keys(s.authRetry);\n let next = s;\n for (const key of keys) {\n const phase = resilience.calls[key]?.phase;\n if (phase === \"succeeded\" || phase === \"failed\") {\n next = clearAuthKey(next, key);\n }\n }\n return next;\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions — exactly resilient-call's (retry + deadline timers\n * keyed per call). The auth dimension arms NO timers: a refresh is a one-shot\n * Cmd, not a recurring sub, and the parked-call re-issue is driven by the\n * `token_refreshed` Msg, not a timer. Wire `subscribe: { deadline:\n * subscribeDeadline }`.\n */\n function subs(s: AuthedState<I, R>): readonly DeadlineSub[] {\n return rc.subs(s.resilience);\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handlers: resilient-call's `resilient_run` (wraps the\n * `run` port) AND token-refresh's `refresh_token` (wraps the `refresh` port).\n * Both route Ok/Err through `tryInterpret`, stamping `Date.now()` at the\n * effect boundary — the only clock reads in the whole module. Assign to the\n * machine's `interpret`:\n *\n * interpret: ac.handlers({ run: ctx.call, refresh: () => sdk.mintToken() })\n */\n function handlers(ports: AuthedPorts<I, R>) {\n const trPorts: TokenRefreshPorts = { refresh: ports.refresh };\n return {\n ...rc.handlers({ run: ports.run }),\n ...tr.handlers(trPorts),\n };\n }\n\n return {\n init,\n attempt,\n succeed,\n fail,\n on401,\n installToken,\n onRefreshed,\n onTimer,\n /** token-refresh's call-boundary verb, re-exposed for proactive expiry refresh. */\n needsRefresh: (s: AuthedState<I, R>, at: number) =>\n tr.needsRefresh(s.auth, at),\n subs,\n handlers,\n };\n}\n\n/** The shape `createAuthedCall` returns — useful for typing a held knob. */\nexport type AuthedCall<I, R> = ReturnType<typeof createAuthedCall<I, R>>;\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice\n * lives at `state.authed`. A convenience for the common single-slice host;\n * consumers with a differently-named field spread by hand. Pure — a thin record\n * rebuild, no clock / RNG.\n */\nexport function liftAuthed<\n S extends { authed: AuthedState<I, R> },\n I,\n R,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [AuthedState<I, R>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, authed: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives (this knob's `subs` emits `DeadlineSub`s,\n * inherited from resilient-call) and the token-refresh Msg constructors a\n * consumer wires into their reducer, so the whole composition imports from one\n * subpath.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type {\n DeadlineSub,\n DeadlineExceeded,\n ResilientState,\n TokenState,\n Token,\n TokenRefreshMsg,\n SucceedMsg,\n FailMsg,\n ResilientTimerMsg,\n RunCmd,\n};\n"]}
@@ -1,5 +1,5 @@
1
- import { Runtime, CtxArg } from '../index.js';
2
- import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
1
+ import { R as Runtime, C as CtxArg } from '../runtime-types-gsog27b6.js';
2
+ import { C as Cmd, S as Sub, M as Machine } from '../core-42kapW0n.js';
3
3
 
4
4
  /**
5
5
  * @demlik/tea/await-terminal — run a ONE-SHOT / decision-procedure machine to a
@@ -1,5 +1,5 @@
1
- import { run } from '../chunk-6VDCTR7N.js';
2
- import '../chunk-DYRKDCJI.js';
1
+ import { run } from '../chunk-BP2R7TPP.js';
2
+ import '../chunk-PBDYW2TU.js';
3
3
 
4
4
  // src/await-terminal/index.ts
5
5
  var TerminalTimeoutError = class extends Error {
@@ -1,9 +1,9 @@
1
- import { C as Cmd } from '../core-BmlJOvxM.js';
1
+ import { C as Cmd } from '../core-42kapW0n.js';
2
2
  export { Debounced, debounce } from '../debounce/index.js';
3
3
  import { DeadlineSub, DeadlineExceeded } from '../deadline/index.js';
4
- import { S as SubscribeHandler } from '../types-BoUWgwSm.js';
4
+ import { S as SubscribeHandler } from '../types-KbL3W1mj.js';
5
5
  export { EnqueueInput } from '../work-queue/index.js';
6
- import '../index.js';
6
+ import '../runtime-types-gsog27b6.js';
7
7
 
8
8
  /**
9
9
  * @demlik/tea/batch-window — coalesce a stream of items into size- or
@@ -1,8 +1,8 @@
1
1
  export { debounce } from '../chunk-J6527VRQ.js';
2
- import { subscribeDeadline, deadlineExceeded, deadlineSub } from '../chunk-SKAMZKTE.js';
2
+ import { subscribeDeadline, deadlineExceeded, deadlineSub } from '../chunk-RMX4EOQK.js';
3
3
  import '../chunk-2YFLOCWX.js';
4
- import '../chunk-6VDCTR7N.js';
5
- import '../chunk-DYRKDCJI.js';
4
+ import '../chunk-BP2R7TPP.js';
5
+ import '../chunk-PBDYW2TU.js';
6
6
 
7
7
  // src/batch-window/index.ts
8
8
  function initBatchWindow() {
@@ -1,5 +1,5 @@
1
- import { S as Sub } from '../core-BmlJOvxM.js';
2
- import { S as SubscribeHandler } from '../types-BoUWgwSm.js';
1
+ import { S as Sub } from '../core-42kapW0n.js';
2
+ import { S as SubscribeHandler } from '../types-KbL3W1mj.js';
3
3
 
4
4
  /**
5
5
  * @demlik/tea/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.
@@ -1,5 +1,8 @@
1
- export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, evictExpired, get, has, initCache, remove, set } from '../chunk-IN33Q4VJ.js';
2
- import '../chunk-NSDFA6M6.js';
1
+ export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, evictExpired, get, has, initCache, remove, set } from '../chunk-2KNLKQVN.js';
2
+ import '../chunk-LYLDCIPH.js';
3
3
  import '../chunk-2YFLOCWX.js';
4
+ import '../chunk-ZJ5AOYBH.js';
5
+ import '../chunk-BP2R7TPP.js';
6
+ import '../chunk-PBDYW2TU.js';
4
7
  //# sourceMappingURL=index.js.map
5
8
  //# sourceMappingURL=index.js.map
@@ -27,10 +27,11 @@ function isOwed(ledger, id) {
27
27
  return ledger.has(id);
28
28
  }
29
29
  function pendingEffectsLedger(restore) {
30
- let mirror = restore?.events ? foldLedger(restore.events) : emptyLedger();
30
+ const events = restore?.events ? [...restore.events] : void 0;
31
+ let mirror = events ? foldLedger(events) : emptyLedger();
31
32
  let counter = restore?.lastId ?? 0;
32
- if (restore?.lastId === void 0 && restore?.events) {
33
- for (const e of restore.events) if (e.id > counter) counter = e.id;
33
+ if (restore?.lastId === void 0 && events) {
34
+ for (const e of events) if (e.id > counter) counter = e.id;
34
35
  } else if (restore?.lastId === void 0) {
35
36
  for (const id of mirror.keys()) if (id > counter) counter = id;
36
37
  }
@@ -61,5 +62,5 @@ function pendingEffectsLedger(restore) {
61
62
  }
62
63
 
63
64
  export { applyEffectEvent, emptyLedger, foldLedger, isOwed, pendingEffectsLedger, survivingEffects };
64
- //# sourceMappingURL=chunk-3545BOYL.js.map
65
- //# sourceMappingURL=chunk-3545BOYL.js.map
65
+ //# sourceMappingURL=chunk-25LBDCBC.js.map
66
+ //# sourceMappingURL=chunk-25LBDCBC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/do/durable-effects.ts"],"names":[],"mappings":"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","file":"chunk-25LBDCBC.js","sourcesContent":["/**\n * @demlik/tea/do — the durable pending-effects ledger (substrate primitive #1).\n *\n * Durable *state* and durable *effects* are different problems. An\n * event-sourced actor rebuilds its STATE by folding the event log\n * (`doEventSourcedStore`, sibling file). But an outbound side effect — a tool\n * round-trip, a message to a sibling grain — is fire-and-forget: if the actor\n * interprets the `Cmd` inside an activation that hibernates mid-flight, the\n * effect is lost, yet nothing in the replayed state records that it is still\n * OWED. This is the one real tax of the event-sourced-virtual-actor target\n * (ADR 0003 primitive #1): \"I owe this effect\" must itself be part of the\n * folded state.\n *\n * Akka's classic `AtLeastOnceDelivery` solves it with a ledger of unconfirmed\n * deliveries kept AS PART OF the persisted state: each `deliver` adds an entry\n * keyed by a monotonic `deliveryId`; each `confirmDelivery` removes it. On\n * recovery the ledger is rebuilt from the SAME events, and any surviving entry\n * is re-sent. At-least-once ⇒ a confirmed-but-not-yet-persisted delivery WILL\n * be re-sent after a crash, so the receiver must deduplicate by id.\n *\n * ## The load-bearing rule (do not violate)\n *\n * The ledger is a PURE FOLD over the same kind of event stream the actor\n * already replays — it is **NOT** a separately-persisted side table:\n *\n * - `effect_owed` — `{ id, effect }` ADDS an entry to the ledger.\n * - `effect_confirmed`— `{ id }` REMOVES the entry.\n *\n * `foldLedger(events)` is `events.reduce(applyEffectEvent, empty())` — the same\n * fold shape `replay` / `foldEvents` / the recorder use. Persist these two\n * events into the SAME log `doEventSourcedStore` already appends (they are just\n * Msg variants), and the ledger is rebuilt for free on the next activation:\n * `foldLedger(log)` after eviction equals `foldLedger(log)` never-evicted, by\n * construction (a pure fold over identical input).\n *\n * On activation (replay completion), `survivingEffects(ledger)` returns the\n * owed-but-unconfirmed entries to re-fire, ordered by their monotonic id. The\n * re-fire is a SIDE EFFECT DERIVED from the surviving ledger — never stored\n * separately, never fired during replay (recovery.md: add to the ledger during\n * recovery; let recovery-END re-emit the survivors).\n *\n * Anti-patterns this file refuses (mirroring durable-effects.md):\n * - A side-table ledger persisted apart from the events. The events ARE the\n * ledger; the Map is rebuilt by folding them.\n * - Re-firing every historical effect during replay. Only `survivingEffects`\n * (post-fold) re-emits.\n * - Assuming the receiver gets each effect exactly once. At-least-once: dedup\n * by `id` at the receiver (`isOwed` here is the sender's view; the receiver\n * owns its own dedup).\n */\n\nimport type { Cmd } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The two events the ledger folds. These are ordinary Msg variants — persist\n// them into the same event log `doEventSourcedStore` appends, and the ledger\n// rebuilds itself on the next activation with no extra storage.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Monotonic, gap-free delivery id — the single correlation + dedup key. */\nexport type DeliveryId = number;\n\n/**\n * An effect is now OWED: it has been decided but its delivery is not yet\n * confirmed. Adds `{ id → effect }` to the ledger when folded.\n *\n * Persist this BEFORE interpreting the effect (durable-effects.md rule:\n * \"persist the intent before deliver\"). A crash between persist and deliver is\n * fine — the surviving entry re-fires on activation. A crash before the persist\n * means the effect was never owed, so nothing is lost that the actor promised.\n */\nexport interface EffectOwed<E> extends Cmd<\"effect_owed\"> {\n readonly id: DeliveryId;\n readonly effect: E;\n}\n\n/**\n * An owed effect has been CONFIRMED delivered. Removes its entry from the\n * ledger when folded. Persist this BEFORE acting on the confirmation\n * (\"persist the confirmation before confirmDelivery\").\n */\nexport interface EffectConfirmed extends Cmd<\"effect_confirmed\"> {\n readonly id: DeliveryId;\n}\n\n/** The event union the ledger folds over. `E` is the consumer's effect type. */\nexport type EffectLedgerEvent<E> = EffectOwed<E> | EffectConfirmed;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The ledger itself — a pure value. Insertion-ordered by monotonic id, so\n// `survivingEffects` re-emits in owed-order without a sort.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The pending-effects ledger: owed-but-unconfirmed effects keyed by their\n * monotonic delivery id. A pure value reconstructed by `foldLedger` — it is\n * never persisted on its own; the events are.\n *\n * `ReadonlyMap` preserves insertion order, which (because ids are appended\n * monotonically) is exactly id order — so iteration yields the survivors\n * oldest-first with no sort.\n */\nexport type PendingEffectsLedger<E> = ReadonlyMap<DeliveryId, E>;\n\n/** A single surviving entry to re-emit: its id and the effect it owes. */\nexport interface OwedEffect<E> {\n readonly id: DeliveryId;\n readonly effect: E;\n}\n\n/** The empty ledger — the fold's seed (a fresh actor owes nothing). */\nexport function emptyLedger<E>(): PendingEffectsLedger<E> {\n return new Map<DeliveryId, E>();\n}\n\n/**\n * Apply ONE ledger event — the reducer at the heart of the fold.\n *\n * - `effect_owed` → add `{ id → effect }`.\n * - `effect_confirmed` → remove `id` (no-op if already absent: a duplicate\n * confirm, or a confirm for an id evicted with the actor — at-least-once\n * tolerates both).\n *\n * Pure: returns a NEW ledger, never mutates the input — so folding the same\n * events twice (live and post-eviction) yields equal results, and a caller may\n * keep the prior ledger value.\n */\nexport function applyEffectEvent<E>(\n ledger: PendingEffectsLedger<E>,\n event: EffectLedgerEvent<E>,\n): PendingEffectsLedger<E> {\n const next = new Map(ledger);\n switch (event.type) {\n case \"effect_owed\":\n next.set(event.id, event.effect);\n return next;\n case \"effect_confirmed\":\n next.delete(event.id);\n return next;\n }\n}\n\n/**\n * THE FOLD. Rebuild the ledger from an event stream — the same fold shape\n * `replay` uses for state. After a simulated eviction this is called over the\n * persisted log and yields a ledger byte-for-byte equal to the never-evicted\n * one, because a pure fold over identical input is identical.\n *\n * This is the function that proves \"no side table\": the ledger is `events`\n * reduced, nothing more.\n */\nexport function foldLedger<E>(\n events: Iterable<EffectLedgerEvent<E>>,\n): PendingEffectsLedger<E> {\n let ledger = emptyLedger<E>();\n for (const event of events) ledger = applyEffectEvent(ledger, event);\n return ledger;\n}\n\n/**\n * RE-EMIT ON ACTIVATION. Given the rebuilt ledger, the owed-but-unconfirmed\n * effects to re-fire — ordered oldest-first by monotonic id. On a fresh /\n * fully-confirmed actor this is empty (nothing re-emitted).\n *\n * Idempotency is the RECEIVER's job: it must dedup by `id`, because\n * at-least-once means a re-emitted already-handled id WILL arrive again\n * (e.g. an effect confirmed in the activation that hibernated before the\n * `effect_confirmed` event was persisted). Re-firing the same id is therefore\n * a no-op AT THE RECEIVER, not here.\n */\nexport function survivingEffects<E>(\n ledger: PendingEffectsLedger<E>,\n): readonly OwedEffect<E>[] {\n const out: OwedEffect<E>[] = [];\n for (const [id, effect] of ledger) out.push({ id, effect });\n return out;\n}\n\n/** Is this id still owed (unconfirmed) in the ledger? Sender-side view. */\nexport function isOwed<E>(\n ledger: PendingEffectsLedger<E>,\n id: DeliveryId,\n): boolean {\n return ledger.has(id);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// pendingEffectsLedger — the stateful, monotonic-id-issuing recorder.\n//\n// The pure `foldLedger` is the contract; this is the ergonomic driver a host\n// uses live. It owns the monotonic `deliveryId` (gap-free, shared across all\n// destinations — durable-effects.md), emits the `effect_owed` / `effect_confirmed`\n// events for the caller to PERSIST, and keeps an in-memory mirror folded from\n// those same events so `survivingEffects` is available without a re-read.\n//\n// Mirrors `deferredGateway`'s factory shape: no clock / RNG captured; pure\n// bookkeeping. The caller is responsible for persisting each returned event\n// into the log (so the ledger survives eviction) and for actually interpreting\n// the effect after the owed event is durable.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * A live, monotonic-id-issuing ledger recorder.\n *\n * - `owe(effect)` — allocate the next `deliveryId`, return the\n * `effect_owed` event to PERSIST plus the allocated `id`. Folding the event\n * into the recorder's mirror is done for you; the caller persists it.\n * - `confirm(id)` — return the `effect_confirmed` event to PERSIST, and `true`\n * iff the id was owed (a duplicate confirm returns `false` — the boolean is\n * itself a dedup signal, exactly Akka `confirmDelivery`).\n * - `surviving()` — the current owed-but-unconfirmed effects (for re-emit on\n * activation).\n * - `ledger()` — the current folded ledger value.\n * - `lastId()` — the highest id issued (so a host can seed the counter from a\n * rebuilt ledger after eviction; see `restore`).\n */\nexport interface PendingEffectsRecorder<E> {\n owe(effect: E): { id: DeliveryId; event: EffectOwed<E> };\n confirm(id: DeliveryId): { confirmed: boolean; event: EffectConfirmed };\n surviving(): readonly OwedEffect<E>[];\n ledger(): PendingEffectsLedger<E>;\n lastId(): DeliveryId;\n}\n\n/**\n * Build a live recorder. `restore` rehydrates it after an eviction: pass the\n * events read back from the log (or the rebuilt ledger's survivors) so the\n * monotonic counter resumes WITHOUT gaps and the mirror reflects the surviving\n * owed set. Omit `restore` for a fresh actor.\n *\n * Counter seeding rule: the next id is `max(seen ids) + 1`. Restoring only the\n * survivors is sufficient for correctness of re-emit, but to keep ids gap-free\n * across confirmed-and-pruned entries the host should pass `lastId` explicitly\n * when it has it (e.g. persisted alongside the snapshot). When only survivors\n * are known, the counter resumes from the highest surviving id — a confirmed id\n * higher than every survivor could in principle be reissued, so a host that\n * needs strict global monotonicity persists `lastId`.\n */\nexport function pendingEffectsLedger<E>(restore?: {\n readonly events?: Iterable<EffectLedgerEvent<E>>;\n readonly lastId?: DeliveryId;\n}): PendingEffectsRecorder<E> {\n // Materialize the restore events ONCE — `restore.events` is only an\n // `Iterable`, so a single-use generator would be exhausted by `foldLedger`\n // and yield nothing on a second walk (seeding the counter to 0 and reissuing\n // colliding ids). Folding and counter-seeding both read this array.\n const events = restore?.events ? [...restore.events] : undefined;\n let mirror: PendingEffectsLedger<E> = events\n ? foldLedger(events)\n : emptyLedger<E>();\n\n // Seed the monotonic counter. Prefer an explicit `lastId` (gap-free across\n // pruned ids); else the highest id observed in the restore events; else the\n // highest surviving id; else 0 (fresh).\n let counter = restore?.lastId ?? 0;\n if (restore?.lastId === undefined && events) {\n for (const e of events) if (e.id > counter) counter = e.id;\n } else if (restore?.lastId === undefined) {\n for (const id of mirror.keys()) if (id > counter) counter = id;\n }\n\n return {\n owe(effect) {\n counter += 1;\n const id = counter;\n const event: EffectOwed<E> = { type: \"effect_owed\", id, effect };\n mirror = applyEffectEvent(mirror, event);\n return { id, event };\n },\n confirm(id) {\n const confirmed = mirror.has(id);\n const event: EffectConfirmed = { type: \"effect_confirmed\", id };\n mirror = applyEffectEvent(mirror, event);\n return { confirmed, event };\n },\n surviving() {\n return survivingEffects(mirror);\n },\n ledger() {\n return mirror;\n },\n lastId() {\n return counter;\n },\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { fromInterval } from './chunk-NSDFA6M6.js';
1
+ import { fromInterval } from './chunk-LYLDCIPH.js';
2
2
 
3
3
  // src/cache/index.ts
4
4
  function initCache() {
@@ -48,5 +48,5 @@ function cacheEvictionSub(id, everyMs) {
48
48
  var cacheEvictionSubscribe = fromInterval((sub) => cacheEvictMsg(sub.id));
49
49
 
50
50
  export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, evictExpired, get, has, initCache, remove, set };
51
- //# sourceMappingURL=chunk-IN33Q4VJ.js.map
52
- //# sourceMappingURL=chunk-IN33Q4VJ.js.map
51
+ //# sourceMappingURL=chunk-2KNLKQVN.js.map
52
+ //# sourceMappingURL=chunk-2KNLKQVN.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cache/index.ts"],"names":[],"mappings":";;;AAuEO,SAAS,SAAA,GAA4B;AAC1C,EAAA,OAAO,EAAE,OAAA,EAAS,EAAC,EAAE;AACvB;AAQA,SAAS,SAAA,CAAa,OAAsB,KAAA,EAAwB;AAClE,EAAA,OAAO,SAAS,KAAA,CAAM,WAAA;AACxB;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,UAAU,MAAA,IAAa,SAAA,CAAU,KAAA,EAAO,KAAK,GAAG,OAAO,MAAA;AAC3D,EAAA,OAAO,KAAA,CAAM,KAAA;AACf;AAMO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACS;AACT,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,CAAC,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA;AAChC;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACA,OACA,KAAA,EACa;AACb,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,WAAA,CAAY,GAAG,CAAA,GAAI,EAAE,KAAA,EAAO,WAAA,EAAa,QAAQ,KAAA,EAAM;AACvD,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAOO,SAAS,MAAA,CAAU,OAAoB,GAAA,EAA0B;AACtE,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,OAAO,YAAY,GAAG,CAAA;AACtB,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AASO,SAAS,YAAA,CACd,OACA,KAAA,EACa;AACb,EAAA,MAAM,cAA6C,EAAC;AACpD,EAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA,EAAG;AACxD,IAAA,IAAI,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA,EAAG;AAC7B,IAAA,WAAA,CAAY,GAAG,CAAA,GAAI,KAAA;AAAA,EACrB;AACA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAmCO,SAAS,cAAc,EAAA,EAA2B;AACvD,EAAA,OAAO,EAAE,IAAA,EAAM,aAAA,EAAe,EAAA,EAAG;AACnC;AAqBO,SAAS,gBAAA,CACd,IACA,OAAA,EACkB;AAClB,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,IAAA,EAAM,OAAA;AAAA,IACN,UAAA,EAAY;AAAA,GACd;AACF;AAcO,IAAM,yBAIT,YAAA,CAAa,CAAC,QAAQ,aAAA,CAAc,GAAA,CAAI,EAAE,CAAC","file":"chunk-IN33Q4VJ.js","sourcesContent":["/**\n * @demlik/tea/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.\n *\n * Same pure-state-ops shape as `@demlik/tea/idempotency` and\n * `@demlik/tea/work-queue`: a state type plus pure transition functions. Host-\n * agnostic by construction — no `Date.now()`, no timers, no I/O inside the ops.\n * The caller injects the clock (`nowMs`) and folds the result through its own\n * reducer, keeping the store inside TEA's invariant 2 (transitions are pure:\n * same inputs → same outputs) so a consumer can exercise it with `replay`\n * without a fake clock.\n *\n * Every op returns a NEW cache value and never mutates its input (invariant 1 —\n * state is a value, not a place). `get` / `has` are read-shaped; `set` /\n * `remove` / `evictExpired` return a new cache.\n *\n * **Distinct from `/idempotency`.** Both are TTL-bounded key maps, but they\n * answer different questions and so carry different shapes:\n *\n * - `/idempotency` is DEDUPE-focused — \"have I already processed this key?\"\n * TTL is a single store-level policy (`ttlMs`), each entry stores the\n * `atMs` it was remembered at, and expiry is derived (`now - atMs >=\n * ttlMs`). It carries an `order` ledger + `capacity` because the use case\n * (webhook receivers, retried messages) needs bounded LRU eviction.\n * - `/cache` is RETRIEVAL-focused — \"what is the value for this key right\n * now?\" TTL is set PER ENTRY at write time: `set(key, value, nowMs,\n * ttlMs)` bakes the absolute `expiresAtMs = nowMs + ttlMs` into the entry.\n * Different keys can carry different lifetimes (a hot key cached for 1s, a\n * cold one for 1h) — the dimension idempotency's single store-level\n * `ttlMs` cannot express. No `order`, no `capacity`: a cache bounds itself\n * by TTL + periodic eviction, not by LRU count.\n *\n * The eviction Sub is the \"eviction is a Msg\" half: a `setInterval`-shaped Sub\n * (composed directly on `@demlik/tea/subs`' `fromInterval`) that periodically\n * dispatches a `cache_evict` Msg the consumer's reducer handles by calling\n * `evictExpired`. Time still never enters the reducer — the Sub fires the Msg,\n * the reducer reads `nowMs` off the Msg (the consumer stamps it at the Sub\n * boundary), and `evictExpired` does the pure drop.\n *\n * NOT exported from the package root — reached via the `@demlik/tea/cache`\n * subpath, same one-shape-per-package rule as `work-queue`, `idempotency`, and\n * `rate-limit`.\n */\n\nimport type { Sub } from \"../index\";\nimport { fromInterval } from \"../subs\";\nimport type { SubscribeHandler } from \"../subs/types\";\n\n/**\n * A cached entry: the `value` and the absolute clock reading `expiresAtMs` it\n * expires at. Unlike `/idempotency`'s entry (which stores `atMs` and derives\n * expiry from a store-level `ttlMs`), the expiry instant is baked in at write\n * time — so per-entry TTLs are a first-class capability. Both fields are plain\n * data so the whole cache is trivially JSON-serializable into a consumer's\n * `Store<S>`.\n */\nexport interface CacheEntry<V> {\n readonly value: V;\n readonly expiresAtMs: number;\n}\n\n/**\n * The cache: a map of `key → entry`. No `order` ledger and no `capacity` (cf.\n * `/idempotency`) — a TTL cache bounds itself by expiry + periodic eviction,\n * not by LRU count. Each entry carries its own `expiresAtMs`, so the cache\n * needs no store-level TTL policy field.\n */\nexport interface TtlCache<V> {\n readonly entries: Readonly<Record<string, CacheEntry<V>>>;\n}\n\n/** Create an empty cache. */\nexport function initCache<V>(): TtlCache<V> {\n return { entries: {} };\n}\n\n/**\n * Whether `entry` is expired at `nowMs`. Half-open: an entry whose\n * `expiresAtMs` exactly equals `nowMs` is expired (`nowMs >= expiresAtMs`),\n * matching `/idempotency` and `/rate-limit`'s window-edge cutoff — a value\n * cached at `t` with a `ttlMs` lifetime is gone at exactly `t + ttlMs`.\n */\nfunction isExpired<V>(entry: CacheEntry<V>, nowMs: number): boolean {\n return nowMs >= entry.expiresAtMs;\n}\n\n/**\n * The cached `value` for `key` iff present AND unexpired at `nowMs`, else\n * `undefined`. An expired entry reads as absent even though it is physically\n * still in `entries` until the next `evictExpired` (or an overwriting `set`)\n * drops it. PURE — reads only, never mutates.\n */\nexport function get<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): V | undefined {\n const entry = cache.entries[key];\n if (entry === undefined || isExpired(entry, nowMs)) return undefined;\n return entry.value;\n}\n\n/**\n * True iff `key` is present AND unexpired at `nowMs`. The boolean sibling of\n * `get` for callers that only need presence, not the value. PURE.\n */\nexport function has<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): boolean {\n const entry = cache.entries[key];\n if (entry === undefined) return false;\n return !isExpired(entry, nowMs);\n}\n\n/**\n * Write `key → value` with an absolute expiry of `nowMs + ttlMs`. Re-`set`ting\n * an existing key overwrites both the value and the expiry — the TTL clock\n * restarts from the new `nowMs`. PURE — returns a new cache; the input is\n * untouched (a fresh shallow copy of `entries`).\n */\nexport function set<V>(\n cache: TtlCache<V>,\n key: string,\n value: V,\n nowMs: number,\n ttlMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n nextEntries[key] = { value, expiresAtMs: nowMs + ttlMs };\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Drop `key` regardless of expiry. PURE — returns a new cache; the input is\n * untouched. Removing an absent key returns an equivalent cache (the shallow\n * copy still happens, but no entry is dropped).\n */\nexport function remove<V>(cache: TtlCache<V>, key: string): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n delete nextEntries[key];\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Physically drop every entry expired at `nowMs` (`nowMs >= expiresAtMs`).\n * Returns a NEW cache with the survivors. This is the pure work the eviction\n * Sub's `cache_evict` Msg triggers — `get` / `has` already treat expired\n * entries as absent, so eviction is purely a memory-reclaim pass, not a\n * correctness one. PURE — the input cache is never mutated.\n */\nexport function evictExpired<V>(\n cache: TtlCache<V>,\n nowMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = {};\n for (const [key, entry] of Object.entries(cache.entries)) {\n if (isExpired(entry, nowMs)) continue;\n nextEntries[key] = entry;\n }\n return { ...cache, entries: nextEntries };\n}\n\n// ---------------------------------------------------------------------------\n// Eviction Sub — \"eviction is a Msg\".\n//\n// A periodic timer Sub that dispatches `cache_evict` so the consumer's reducer\n// can call `evictExpired`. It is `fromInterval` specialized to a fixed Msg\n// shape — the factory carries the interval ON the Sub (per the substrate's\n// reconcile-by-id contract) and dispatches a `cache_evict` Msg tagged with the\n// Sub's own `id`, so a consumer running several caches can route each tick to\n// the right cache by `msg.id`.\n//\n// Mirrors `fromInterval` exactly (factory + SubscribeHandler cell + exported\n// Msg constructor); it does NOT reimplement the timer lifecycle — it composes\n// directly on `fromInterval`, which owns `setInterval` / `clearInterval`. That\n// keeps the timer-handle bookkeeping (and its cleanup) in one place and this\n// module purely declarative.\n//\n// Strengthens invariant 4 (external time is a Sub, reconciled by id) and\n// invariant 9 (named, small Sub surface composed on the shared factory).\n// ---------------------------------------------------------------------------\n\n/**\n * The Msg the eviction Sub dispatches each tick. `id` is the Sub's own id so a\n * consumer with multiple caches can route the tick to the matching cache. The\n * reducer reads `Date.now()`-free time by stamping `nowMs` itself at the Sub\n * boundary if needed; the canonical reducer cell is\n * `cache_evict: (s) => [{ ...s, cache: evictExpired(s.cache, nowMs) }, []]`.\n */\nexport interface CacheEvictMsg {\n readonly type: \"cache_evict\";\n readonly id: string;\n}\n\n/** Construct a `cache_evict` Msg for the cache identified by `id`. */\nexport function cacheEvictMsg(id: string): CacheEvictMsg {\n return { type: \"cache_evict\", id };\n}\n\n/**\n * The Sub shape the eviction factory installs: a `setInterval`-shaped Sub\n * carrying its tick period (`intervalMs`) per the substrate's\n * reconcile-by-id-with-data-on-the-Sub contract. `type: \"cache\"` is the\n * lowercase source-noun Sub convention (canon §2.5 / naming-style).\n */\nexport type CacheEvictionSub = Sub<\"cache\"> & { readonly intervalMs: number };\n\n/**\n * Declare an eviction Sub for the cache identified by `id`, ticking every\n * `everyMs`. Drop the result into `subscriptions(state)`:\n *\n * subscriptions: (s) => [cacheEvictionSub(\"session-cache\", 30_000)]\n *\n * Same id ⇒ same running interval across transitions (no reconcile churn);\n * changing `everyMs` alone will NOT re-arm the timer (the substrate leaves a\n * same-id Sub running) — change the `id` too if the period must change. This\n * mirrors `fromInterval`'s data-on-the-Sub tradeoff exactly.\n */\nexport function cacheEvictionSub(\n id: string,\n everyMs: number,\n): CacheEvictionSub {\n return {\n id: id as CacheEvictionSub[\"id\"],\n type: \"cache\",\n intervalMs: everyMs,\n };\n}\n\n/**\n * The `subscribe` cell for the eviction Sub. Assign it to\n * `machine.subscribe.cache`:\n *\n * subscribe: { cache: cacheEvictionSubscribe }\n *\n * Composed directly on `fromInterval` — it owns the `setInterval` /\n * `clearInterval` lifecycle; this cell only pins the Msg the tick dispatches\n * (`cacheEvictMsg(sub.id)`). The cleanup `fromInterval` returns stops the\n * interval when the Sub is reconciled out (the cache's state exits the phase\n * that declared it).\n */\nexport const cacheEvictionSubscribe: SubscribeHandler<\n CacheEvictionSub,\n CacheEvictMsg,\n unknown\n> = fromInterval((sub) => cacheEvictMsg(sub.id));\n"]}
1
+ {"version":3,"sources":["../src/cache/index.ts"],"names":[],"mappings":";;;AAuEO,SAAS,SAAA,GAA4B;AAC1C,EAAA,OAAO,EAAE,OAAA,EAAS,EAAC,EAAE;AACvB;AAQA,SAAS,SAAA,CAAa,OAAsB,KAAA,EAAwB;AAClE,EAAA,OAAO,SAAS,KAAA,CAAM,WAAA;AACxB;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,UAAU,MAAA,IAAa,SAAA,CAAU,KAAA,EAAO,KAAK,GAAG,OAAO,MAAA;AAC3D,EAAA,OAAO,KAAA,CAAM,KAAA;AACf;AAMO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACS;AACT,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,CAAC,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA;AAChC;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACA,OACA,KAAA,EACa;AACb,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,WAAA,CAAY,GAAG,CAAA,GAAI,EAAE,KAAA,EAAO,WAAA,EAAa,QAAQ,KAAA,EAAM;AACvD,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAOO,SAAS,MAAA,CAAU,OAAoB,GAAA,EAA0B;AACtE,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,OAAO,YAAY,GAAG,CAAA;AACtB,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AASO,SAAS,YAAA,CACd,OACA,KAAA,EACa;AACb,EAAA,MAAM,cAA6C,EAAC;AACpD,EAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA,EAAG;AACxD,IAAA,IAAI,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA,EAAG;AAC7B,IAAA,WAAA,CAAY,GAAG,CAAA,GAAI,KAAA;AAAA,EACrB;AACA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAmCO,SAAS,cAAc,EAAA,EAA2B;AACvD,EAAA,OAAO,EAAE,IAAA,EAAM,aAAA,EAAe,EAAA,EAAG;AACnC;AAqBO,SAAS,gBAAA,CACd,IACA,OAAA,EACkB;AAClB,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,IAAA,EAAM,OAAA;AAAA,IACN,UAAA,EAAY;AAAA,GACd;AACF;AAcO,IAAM,yBAIT,YAAA,CAAa,CAAC,QAAQ,aAAA,CAAc,GAAA,CAAI,EAAE,CAAC","file":"chunk-2KNLKQVN.js","sourcesContent":["/**\n * @demlik/tea/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.\n *\n * Same pure-state-ops shape as `@demlik/tea/idempotency` and\n * `@demlik/tea/work-queue`: a state type plus pure transition functions. Host-\n * agnostic by construction — no `Date.now()`, no timers, no I/O inside the ops.\n * The caller injects the clock (`nowMs`) and folds the result through its own\n * reducer, keeping the store inside TEA's invariant 2 (transitions are pure:\n * same inputs → same outputs) so a consumer can exercise it with `replay`\n * without a fake clock.\n *\n * Every op returns a NEW cache value and never mutates its input (invariant 1 —\n * state is a value, not a place). `get` / `has` are read-shaped; `set` /\n * `remove` / `evictExpired` return a new cache.\n *\n * **Distinct from `/idempotency`.** Both are TTL-bounded key maps, but they\n * answer different questions and so carry different shapes:\n *\n * - `/idempotency` is DEDUPE-focused — \"have I already processed this key?\"\n * TTL is a single store-level policy (`ttlMs`), each entry stores the\n * `atMs` it was remembered at, and expiry is derived (`now - atMs >=\n * ttlMs`). It carries an `order` ledger + `capacity` because the use case\n * (webhook receivers, retried messages) needs bounded LRU eviction.\n * - `/cache` is RETRIEVAL-focused — \"what is the value for this key right\n * now?\" TTL is set PER ENTRY at write time: `set(key, value, nowMs,\n * ttlMs)` bakes the absolute `expiresAtMs = nowMs + ttlMs` into the entry.\n * Different keys can carry different lifetimes (a hot key cached for 1s, a\n * cold one for 1h) — the dimension idempotency's single store-level\n * `ttlMs` cannot express. No `order`, no `capacity`: a cache bounds itself\n * by TTL + periodic eviction, not by LRU count.\n *\n * The eviction Sub is the \"eviction is a Msg\" half: a `setInterval`-shaped Sub\n * (composed directly on `@demlik/tea/subs`' `fromInterval`) that periodically\n * dispatches a `cache_evict` Msg the consumer's reducer handles by calling\n * `evictExpired`. Time still never enters the reducer — the Sub fires the Msg,\n * the reducer reads `nowMs` off the Msg (the consumer stamps it at the Sub\n * boundary), and `evictExpired` does the pure drop.\n *\n * NOT exported from the package root — reached via the `@demlik/tea/cache`\n * subpath, same one-shape-per-package rule as `work-queue`, `idempotency`, and\n * `rate-limit`.\n */\n\nimport type { Sub } from \"../index\";\nimport { fromInterval } from \"../subs\";\nimport type { SubscribeHandler } from \"../subs/types\";\n\n/**\n * A cached entry: the `value` and the absolute clock reading `expiresAtMs` it\n * expires at. Unlike `/idempotency`'s entry (which stores `atMs` and derives\n * expiry from a store-level `ttlMs`), the expiry instant is baked in at write\n * time — so per-entry TTLs are a first-class capability. Both fields are plain\n * data so the whole cache is trivially JSON-serializable into a consumer's\n * `Store<S>`.\n */\nexport interface CacheEntry<V> {\n readonly value: V;\n readonly expiresAtMs: number;\n}\n\n/**\n * The cache: a map of `key → entry`. No `order` ledger and no `capacity` (cf.\n * `/idempotency`) — a TTL cache bounds itself by expiry + periodic eviction,\n * not by LRU count. Each entry carries its own `expiresAtMs`, so the cache\n * needs no store-level TTL policy field.\n */\nexport interface TtlCache<V> {\n readonly entries: Readonly<Record<string, CacheEntry<V>>>;\n}\n\n/** Create an empty cache. */\nexport function initCache<V>(): TtlCache<V> {\n return { entries: {} };\n}\n\n/**\n * Whether `entry` is expired at `nowMs`. Half-open: an entry whose\n * `expiresAtMs` exactly equals `nowMs` is expired (`nowMs >= expiresAtMs`),\n * matching `/idempotency` and `/rate-limit`'s window-edge cutoff — a value\n * cached at `t` with a `ttlMs` lifetime is gone at exactly `t + ttlMs`.\n */\nfunction isExpired<V>(entry: CacheEntry<V>, nowMs: number): boolean {\n return nowMs >= entry.expiresAtMs;\n}\n\n/**\n * The cached `value` for `key` iff present AND unexpired at `nowMs`, else\n * `undefined`. An expired entry reads as absent even though it is physically\n * still in `entries` until the next `evictExpired` (or an overwriting `set`)\n * drops it. PURE — reads only, never mutates.\n */\nexport function get<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): V | undefined {\n const entry = cache.entries[key];\n if (entry === undefined || isExpired(entry, nowMs)) return undefined;\n return entry.value;\n}\n\n/**\n * True iff `key` is present AND unexpired at `nowMs`. The boolean sibling of\n * `get` for callers that only need presence, not the value. PURE.\n */\nexport function has<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): boolean {\n const entry = cache.entries[key];\n if (entry === undefined) return false;\n return !isExpired(entry, nowMs);\n}\n\n/**\n * Write `key → value` with an absolute expiry of `nowMs + ttlMs`. Re-`set`ting\n * an existing key overwrites both the value and the expiry — the TTL clock\n * restarts from the new `nowMs`. PURE — returns a new cache; the input is\n * untouched (a fresh shallow copy of `entries`).\n */\nexport function set<V>(\n cache: TtlCache<V>,\n key: string,\n value: V,\n nowMs: number,\n ttlMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n nextEntries[key] = { value, expiresAtMs: nowMs + ttlMs };\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Drop `key` regardless of expiry. PURE — returns a new cache; the input is\n * untouched. Removing an absent key returns an equivalent cache (the shallow\n * copy still happens, but no entry is dropped).\n */\nexport function remove<V>(cache: TtlCache<V>, key: string): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n delete nextEntries[key];\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Physically drop every entry expired at `nowMs` (`nowMs >= expiresAtMs`).\n * Returns a NEW cache with the survivors. This is the pure work the eviction\n * Sub's `cache_evict` Msg triggers — `get` / `has` already treat expired\n * entries as absent, so eviction is purely a memory-reclaim pass, not a\n * correctness one. PURE — the input cache is never mutated.\n */\nexport function evictExpired<V>(\n cache: TtlCache<V>,\n nowMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = {};\n for (const [key, entry] of Object.entries(cache.entries)) {\n if (isExpired(entry, nowMs)) continue;\n nextEntries[key] = entry;\n }\n return { ...cache, entries: nextEntries };\n}\n\n// ---------------------------------------------------------------------------\n// Eviction Sub — \"eviction is a Msg\".\n//\n// A periodic timer Sub that dispatches `cache_evict` so the consumer's reducer\n// can call `evictExpired`. It is `fromInterval` specialized to a fixed Msg\n// shape — the factory carries the interval ON the Sub (per the substrate's\n// reconcile-by-id contract) and dispatches a `cache_evict` Msg tagged with the\n// Sub's own `id`, so a consumer running several caches can route each tick to\n// the right cache by `msg.id`.\n//\n// Mirrors `fromInterval` exactly (factory + SubscribeHandler cell + exported\n// Msg constructor); it does NOT reimplement the timer lifecycle — it composes\n// directly on `fromInterval`, which owns `setInterval` / `clearInterval`. That\n// keeps the timer-handle bookkeeping (and its cleanup) in one place and this\n// module purely declarative.\n//\n// Strengthens invariant 4 (external time is a Sub, reconciled by id) and\n// invariant 9 (named, small Sub surface composed on the shared factory).\n// ---------------------------------------------------------------------------\n\n/**\n * The Msg the eviction Sub dispatches each tick. `id` is the Sub's own id so a\n * consumer with multiple caches can route the tick to the matching cache. The\n * reducer reads `Date.now()`-free time by stamping `nowMs` itself at the Sub\n * boundary if needed; the canonical reducer cell is\n * `cache_evict: (s) => [{ ...s, cache: evictExpired(s.cache, nowMs) }, []]`.\n */\nexport interface CacheEvictMsg {\n readonly type: \"cache_evict\";\n readonly id: string;\n}\n\n/** Construct a `cache_evict` Msg for the cache identified by `id`. */\nexport function cacheEvictMsg(id: string): CacheEvictMsg {\n return { type: \"cache_evict\", id };\n}\n\n/**\n * The Sub shape the eviction factory installs: a `setInterval`-shaped Sub\n * carrying its tick period (`intervalMs`) per the substrate's\n * reconcile-by-id-with-data-on-the-Sub contract. `type: \"cache\"` is the\n * lowercase source-noun Sub convention (canon §2.5 / naming-style).\n */\nexport type CacheEvictionSub = Sub<\"cache\"> & { readonly intervalMs: number };\n\n/**\n * Declare an eviction Sub for the cache identified by `id`, ticking every\n * `everyMs`. Drop the result into `subscriptions(state)`:\n *\n * subscriptions: (s) => [cacheEvictionSub(\"session-cache\", 30_000)]\n *\n * Same id ⇒ same running interval across transitions (no reconcile churn);\n * changing `everyMs` alone will NOT re-arm the timer (the substrate leaves a\n * same-id Sub running) — change the `id` too if the period must change. This\n * mirrors `fromInterval`'s data-on-the-Sub tradeoff exactly.\n */\nexport function cacheEvictionSub(\n id: string,\n everyMs: number,\n): CacheEvictionSub {\n return {\n id: id as CacheEvictionSub[\"id\"],\n type: \"cache\",\n intervalMs: everyMs,\n };\n}\n\n/**\n * The `subscribe` cell for the eviction Sub. Assign it to\n * `machine.subscribe.cache`:\n *\n * subscribe: { cache: cacheEvictionSubscribe }\n *\n * Composed directly on `fromInterval` — it owns the `setInterval` /\n * `clearInterval` lifecycle; this cell only pins the Msg the tick dispatches\n * (`cacheEvictMsg(sub.id)`). The cleanup `fromInterval` returns stops the\n * interval when the Sub is reconciled out (the cache's state exits the phase\n * that declared it).\n */\nexport const cacheEvictionSubscribe: SubscribeHandler<\n CacheEvictionSub,\n CacheEvictMsg,\n unknown\n> = fromInterval((sub) => cacheEvictMsg(sub.id));\n"]}