@demlik/tea 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/index.d.ts +907 -0
  3. package/dist/agent/index.js +20 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/authed-call/index.d.ts +119 -0
  6. package/dist/authed-call/index.js +148 -0
  7. package/dist/authed-call/index.js.map +1 -0
  8. package/dist/await-terminal/index.d.ts +99 -0
  9. package/dist/await-terminal/index.js +75 -0
  10. package/dist/await-terminal/index.js.map +1 -0
  11. package/dist/batch-window/index.d.ts +259 -0
  12. package/dist/batch-window/index.js +43 -0
  13. package/dist/batch-window/index.js.map +1 -0
  14. package/dist/cache/index.d.ts +2 -2
  15. package/dist/cache/index.js +2 -50
  16. package/dist/cache/index.js.map +1 -1
  17. package/dist/{chunk-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
  18. package/dist/chunk-2WZHS4P2.js.map +1 -0
  19. package/dist/chunk-2YOE5PIS.js +45 -0
  20. package/dist/chunk-2YOE5PIS.js.map +1 -0
  21. package/dist/chunk-3545BOYL.js +65 -0
  22. package/dist/chunk-3545BOYL.js.map +1 -0
  23. package/dist/chunk-5VNWZADP.js +49 -0
  24. package/dist/chunk-5VNWZADP.js.map +1 -0
  25. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  26. package/dist/chunk-7FYQGDMF.js.map +1 -0
  27. package/dist/chunk-7VEG4KHB.js +202 -0
  28. package/dist/chunk-7VEG4KHB.js.map +1 -0
  29. package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
  30. package/dist/chunk-A56GNPI7.js.map +1 -0
  31. package/dist/chunk-BNJYE4AU.js +157 -0
  32. package/dist/chunk-BNJYE4AU.js.map +1 -0
  33. package/dist/chunk-BYQUGDD3.js +16 -0
  34. package/dist/chunk-BYQUGDD3.js.map +1 -0
  35. package/dist/chunk-D5EQDLDV.js +186 -0
  36. package/dist/chunk-D5EQDLDV.js.map +1 -0
  37. package/dist/chunk-EDGDR5XX.js +60 -0
  38. package/dist/chunk-EDGDR5XX.js.map +1 -0
  39. package/dist/chunk-EIZJTYKR.js +93 -0
  40. package/dist/chunk-EIZJTYKR.js.map +1 -0
  41. package/dist/chunk-EQRUAP2A.js +56 -0
  42. package/dist/chunk-EQRUAP2A.js.map +1 -0
  43. package/dist/chunk-FH5RUIIL.js +61 -0
  44. package/dist/chunk-FH5RUIIL.js.map +1 -0
  45. package/dist/chunk-HJO6VNQI.js +15 -0
  46. package/dist/chunk-HJO6VNQI.js.map +1 -0
  47. package/dist/chunk-IFBQJYGI.js +132 -0
  48. package/dist/chunk-IFBQJYGI.js.map +1 -0
  49. package/dist/chunk-IN33Q4VJ.js +52 -0
  50. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  51. package/dist/chunk-JELSZN25.js +47 -0
  52. package/dist/chunk-JELSZN25.js.map +1 -0
  53. package/dist/chunk-K575PT4S.js +58 -0
  54. package/dist/chunk-K575PT4S.js.map +1 -0
  55. package/dist/chunk-LEC4U6CX.js +91 -0
  56. package/dist/chunk-LEC4U6CX.js.map +1 -0
  57. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  58. package/dist/chunk-NSDFA6M6.js.map +1 -0
  59. package/dist/chunk-QGZW4W3E.js +20 -0
  60. package/dist/chunk-QGZW4W3E.js.map +1 -0
  61. package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
  62. package/dist/chunk-RGTPPF4U.js.map +1 -0
  63. package/dist/chunk-RVJFGRGK.js +222 -0
  64. package/dist/chunk-RVJFGRGK.js.map +1 -0
  65. package/dist/chunk-RVU3RBE6.js +31 -0
  66. package/dist/chunk-RVU3RBE6.js.map +1 -0
  67. package/dist/chunk-SC3FAFZN.js +53 -0
  68. package/dist/chunk-SC3FAFZN.js.map +1 -0
  69. package/dist/chunk-SINXE47Y.js +55 -0
  70. package/dist/chunk-SINXE47Y.js.map +1 -0
  71. package/dist/chunk-TAO4YOLO.js +87 -0
  72. package/dist/chunk-TAO4YOLO.js.map +1 -0
  73. package/dist/chunk-TC4HWABA.js +506 -0
  74. package/dist/chunk-TC4HWABA.js.map +1 -0
  75. package/dist/chunk-TGG26XMK.js +52 -0
  76. package/dist/chunk-TGG26XMK.js.map +1 -0
  77. package/dist/chunk-UV6UWGEO.js +147 -0
  78. package/dist/chunk-UV6UWGEO.js.map +1 -0
  79. package/dist/chunk-UYWUVEP6.js +158 -0
  80. package/dist/chunk-UYWUVEP6.js.map +1 -0
  81. package/dist/chunk-WUV3PSCZ.js +54 -0
  82. package/dist/chunk-WUV3PSCZ.js.map +1 -0
  83. package/dist/chunk-XRXKH7KA.js +268 -0
  84. package/dist/chunk-XRXKH7KA.js.map +1 -0
  85. package/dist/circuit-breaker/index.js +1 -51
  86. package/dist/circuit-breaker/index.js.map +1 -1
  87. package/dist/core-CymXZCeY.d.ts +178 -0
  88. package/dist/deadline/index.d.ts +24 -5
  89. package/dist/deadline/index.js +4 -18
  90. package/dist/deadline/index.js.map +1 -1
  91. package/dist/debounce/index.js +1 -54
  92. package/dist/debounce/index.js.map +1 -1
  93. package/dist/devtools/index.d.ts +1 -1
  94. package/dist/devtools/index.js +18 -6
  95. package/dist/devtools/index.js.map +1 -1
  96. package/dist/do/index.d.ts +1732 -92
  97. package/dist/do/index.js +677 -31
  98. package/dist/do/index.js.map +1 -1
  99. package/dist/durable-effects-2MDxDs45.d.ts +180 -0
  100. package/dist/extension/index.d.ts +31 -13
  101. package/dist/extension/index.js +5 -3
  102. package/dist/extension/index.js.map +1 -1
  103. package/dist/extension/react.js +8 -3
  104. package/dist/extension/react.js.map +1 -1
  105. package/dist/extension/subs/index.d.ts +2 -2
  106. package/dist/extension/subs/index.js.map +1 -1
  107. package/dist/extension/test-utils.js +11 -3
  108. package/dist/extension/test-utils.js.map +1 -1
  109. package/dist/fan-out/index.d.ts +239 -0
  110. package/dist/fan-out/index.js +5 -0
  111. package/dist/fan-out/index.js.map +1 -0
  112. package/dist/idempotency/adapter.d.ts +62 -0
  113. package/dist/idempotency/adapter.js +4 -0
  114. package/dist/idempotency/adapter.js.map +1 -0
  115. package/dist/idempotency/index.js +1 -56
  116. package/dist/idempotency/index.js.map +1 -1
  117. package/dist/idempotent-intake/index.d.ts +186 -0
  118. package/dist/idempotent-intake/index.js +7 -0
  119. package/dist/idempotent-intake/index.js.map +1 -0
  120. package/dist/index.d.ts +341 -191
  121. package/dist/index.js +2 -1
  122. package/dist/llm-call/index.d.ts +309 -0
  123. package/dist/llm-call/index.js +14 -0
  124. package/dist/llm-call/index.js.map +1 -0
  125. package/dist/machine-viz/index.d.ts +1 -1
  126. package/dist/machine-viz/index.js +1 -1
  127. package/dist/machine-viz/index.js.map +1 -1
  128. package/dist/mem/index.d.ts +1 -0
  129. package/dist/monitored-run/index.d.ts +253 -0
  130. package/dist/monitored-run/index.js +10 -0
  131. package/dist/monitored-run/index.js.map +1 -0
  132. package/dist/node/index.d.ts +5 -5
  133. package/dist/node/index.js.map +1 -1
  134. package/dist/paginated-walk/index.d.ts +234 -0
  135. package/dist/paginated-walk/index.js +15 -0
  136. package/dist/paginated-walk/index.js.map +1 -0
  137. package/dist/paginator/index.d.ts +271 -0
  138. package/dist/paginator/index.js +3 -0
  139. package/dist/paginator/index.js.map +1 -0
  140. package/dist/parity/index.d.ts +86 -0
  141. package/dist/parity/index.js +77 -0
  142. package/dist/parity/index.js.map +1 -0
  143. package/dist/pbt/arbitraries/index.d.ts +1 -1
  144. package/dist/pbt/arbitraries/index.js +1 -1
  145. package/dist/pbt/index.d.ts +1 -1
  146. package/dist/pbt/index.js +5 -4
  147. package/dist/pbt/index.js.map +1 -1
  148. package/dist/pbt/runners/index.d.ts +1 -1
  149. package/dist/pbt/runners/index.js +3 -2
  150. package/dist/poller/index.d.ts +288 -0
  151. package/dist/poller/index.js +79 -0
  152. package/dist/poller/index.js.map +1 -0
  153. package/dist/prediction/index.d.ts +109 -0
  154. package/dist/prediction/index.js +4 -0
  155. package/dist/prediction/index.js.map +1 -0
  156. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  157. package/dist/pure/index.d.ts +2 -0
  158. package/dist/pure/index.js +4 -0
  159. package/dist/pure/index.js.map +1 -0
  160. package/dist/raft/demo.d.ts +105 -0
  161. package/dist/raft/demo.js +172 -0
  162. package/dist/raft/demo.js.map +1 -0
  163. package/dist/raft/index.d.ts +548 -0
  164. package/dist/raft/index.js +7 -0
  165. package/dist/raft/index.js.map +1 -0
  166. package/dist/raft/sim.d.ts +222 -0
  167. package/dist/raft/sim.js +8 -0
  168. package/dist/raft/sim.js.map +1 -0
  169. package/dist/raft/timeline.d.ts +46 -0
  170. package/dist/raft/timeline.js +62 -0
  171. package/dist/raft/timeline.js.map +1 -0
  172. package/dist/rate-limit/index.js +1 -43
  173. package/dist/rate-limit/index.js.map +1 -1
  174. package/dist/react/index.d.ts +2 -1
  175. package/dist/react/index.js +39 -20
  176. package/dist/react/index.js.map +1 -1
  177. package/dist/reconciler/index.d.ts +192 -0
  178. package/dist/reconciler/index.js +166 -0
  179. package/dist/reconciler/index.js.map +1 -0
  180. package/dist/recorder/index.d.ts +7 -3
  181. package/dist/recorder/index.js +1 -155
  182. package/dist/recorder/index.js.map +1 -1
  183. package/dist/resilient-call/index.d.ts +251 -0
  184. package/dist/resilient-call/index.js +13 -0
  185. package/dist/resilient-call/index.js.map +1 -0
  186. package/dist/retry-backoff/index.d.ts +42 -5
  187. package/dist/retry-backoff/index.js +1 -35
  188. package/dist/retry-backoff/index.js.map +1 -1
  189. package/dist/saga/index.d.ts +225 -0
  190. package/dist/saga/index.js +150 -0
  191. package/dist/saga/index.js.map +1 -0
  192. package/dist/snapshot/index.d.ts +334 -0
  193. package/dist/snapshot/index.js +5 -0
  194. package/dist/snapshot/index.js.map +1 -0
  195. package/dist/subs/index.d.ts +48 -10
  196. package/dist/subs/index.js +1 -1
  197. package/dist/testing/index.d.ts +107 -2
  198. package/dist/testing/index.js +204 -6
  199. package/dist/testing/index.js.map +1 -1
  200. package/dist/throttle/index.js +1 -52
  201. package/dist/throttle/index.js.map +1 -1
  202. package/dist/throttled-input/index.d.ts +372 -0
  203. package/dist/throttled-input/index.js +88 -0
  204. package/dist/throttled-input/index.js.map +1 -0
  205. package/dist/token-refresh/index.d.ts +215 -0
  206. package/dist/token-refresh/index.js +6 -0
  207. package/dist/token-refresh/index.js.map +1 -0
  208. package/dist/trace-replay/index.d.ts +11 -2
  209. package/dist/trace-replay/index.js +3 -55
  210. package/dist/trace-replay/index.js.map +1 -1
  211. package/dist/{types-CwBC5wMn.d.ts → types-N9LZbdnr.d.ts} +1 -1
  212. package/dist/with-deadline/index.d.ts +196 -0
  213. package/dist/with-deadline/index.js +130 -0
  214. package/dist/with-deadline/index.js.map +1 -0
  215. package/dist/with-resilience/index.d.ts +205 -0
  216. package/dist/with-resilience/index.js +274 -0
  217. package/dist/with-resilience/index.js.map +1 -0
  218. package/dist/with-telemetry/index.d.ts +149 -0
  219. package/dist/with-telemetry/index.js +95 -0
  220. package/dist/with-telemetry/index.js.map +1 -0
  221. package/dist/work-queue/adapter.d.ts +82 -0
  222. package/dist/work-queue/adapter.js +4 -0
  223. package/dist/work-queue/adapter.js.map +1 -0
  224. package/dist/work-queue/index.d.ts +25 -11
  225. package/dist/work-queue/index.js +1 -46
  226. package/dist/work-queue/index.js.map +1 -1
  227. package/dist/work-queue/ops.d.ts +69 -0
  228. package/dist/work-queue/ops.js +3 -0
  229. package/dist/work-queue/ops.js.map +1 -0
  230. package/dist/workflow/index.d.ts +444 -0
  231. package/dist/workflow/index.js +225 -0
  232. package/dist/workflow/index.js.map +1 -0
  233. package/package.json +162 -25
  234. package/dist/chunk-CMG2KMWX.js.map +0 -1
  235. package/dist/chunk-PYBQREFU.js.map +0 -1
  236. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  237. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  238. package/dist/chunk-ZRUST6BK.js.map +0 -1
@@ -0,0 +1,215 @@
1
+ import { C as Cmd, I as Interpret } from '../core-CymXZCeY.js';
2
+ import { M as MsgType } from '../protocol-B5YmJr0d.js';
3
+
4
+ /**
5
+ * @demlik/tea/token-refresh — a credential's lifecycle as pure state + verbs,
6
+ * with the actual token fetch deferred to a single injected port.
7
+ *
8
+ * This is the L1 brick `authed-call` (#10 in the composition specs) stacks on:
9
+ * a token slice `{ token, stale }` plus pure transitions that decide *when* a
10
+ * credential must be re-minted, and one `handlers(ports)` splice that performs
11
+ * the re-mint. It follows the uniform knob contract every composition wears —
12
+ * `createTokenRefresh(config) → { init, <verbs>, handlers }` — so a consumer
13
+ * spreads ~3 hooks into their machine instead of hand-wiring the refresh dance.
14
+ *
15
+ * It is a LEAF module in the same mold as `@demlik/tea/retry-backoff`: it
16
+ * depends only on the core (`../index`), never on a sibling brick. The two
17
+ * non-negotiables of every knob hold here by construction:
18
+ *
19
+ * - **Durable** — the token slice is plain JSON-serializable data, so it
20
+ * survives a Durable Object eviction / page reload. A rehydrated machine
21
+ * resumes knowing exactly which token it holds and whether it is stale.
22
+ * - **Replayable** — every transition is a pure verb returning
23
+ * `readonly [State, Cmd[]]`. No verb reads the wall clock or performs I/O,
24
+ * so `replay` reconstructs the slice exactly. Time enters ONLY as an `at`
25
+ * parameter the caller stamps; the network fetch enters ONLY at the
26
+ * `handlers` boundary. This keeps the substrate's purity invariants intact
27
+ * (invariant 2: pure transitions; invariant 4: I/O is an effect, not a
28
+ * reducer body).
29
+ *
30
+ * ## The staleness model — two independent reasons to refresh
31
+ *
32
+ * A token becomes unusable for two unrelated reasons, and the slice tracks both
33
+ * without conflating them:
34
+ *
35
+ * 1. **Expiry** — the token has a known `expiresAt` (epoch ms). It is "near
36
+ * expiry" once `at >= expiresAt - skewMs`. The `skewMs` skew refreshes
37
+ * *ahead* of the hard deadline so an in-flight call never races a token
38
+ * that expires mid-request. This is the proactive, time-driven reason and
39
+ * is computed purely from the injected `at` — no clock in the verb.
40
+ *
41
+ * 2. **Server rejection (401)** — the server rejected a request the client
42
+ * believed was still valid (clock skew, a server-side revocation, an early
43
+ * rotation). `on401` flips a `stale` flag regardless of `expiresAt`. This
44
+ * is the reactive reason: the client's belief about validity was wrong, so
45
+ * the next `needsRefresh` returns true even though the clock says the
46
+ * token is fine.
47
+ *
48
+ * `needsRefresh` is the union of the two (plus "no token at all"). One source
49
+ * of truth for "should I fetch a fresh credential before the next call?", read
50
+ * by the consumer's reducer at the call boundary.
51
+ *
52
+ * Typical wiring (inside a consumer machine):
53
+ *
54
+ * const tr = createTokenRefresh({ skewMs: 30_000 });
55
+ *
56
+ * // in init:
57
+ * init: (loaded) => [loaded ?? { auth: tr.init() }, Cmd.none],
58
+ *
59
+ * // in a reducer cell that is about to make an authed call:
60
+ * before_call: (s, msg) => {
61
+ * const [auth, cmds] = tr.ensureFresh(s.auth, msg.at);
62
+ * return [{ ...s, auth }, cmds]; // emits a refresh Cmd iff a refresh is due
63
+ * },
64
+ *
65
+ * // on a 401 from a guarded call:
66
+ * got_401: (s) => [{ ...s, auth: tr.on401(s.auth) }, []],
67
+ *
68
+ * // when the refresh port resolves:
69
+ * token_refreshed: (s, msg) => [{ ...s, auth: tr.refreshed(s.auth, msg.token) }, []],
70
+ *
71
+ * // splice the I/O:
72
+ * interpret: { ...tr.handlers({ refresh: () => mySdk.mintToken() }) },
73
+ *
74
+ * NOT a substrate primitive: from `../index` it reuses only the `Cmd` /
75
+ * `Interpret` types and the `tryInterpret` Railway helper (a runtime function,
76
+ * not a type) that every effectful brick uses — and no sibling brick. It is a
77
+ * leaf utility consumers reach via the `@demlik/tea/token-refresh` subpath.
78
+ */
79
+
80
+ /**
81
+ * A minted credential: the opaque `value` to send on the wire, and the absolute
82
+ * `expiresAt` (epoch milliseconds — the `Date.now()` scale) the issuer stamped
83
+ * it with. Both fields are plain data so a `Token` is trivially serializable
84
+ * into a consumer's `Store<S>`.
85
+ *
86
+ * `value` is `string` because that is the universal shape of a bearer token /
87
+ * API key / session cookie on the wire. A consumer whose refresh port yields a
88
+ * richer object can JSON-encode it into `value`, or keep this module's slice as
89
+ * the "is it fresh?" gate and hold the richer object elsewhere in their Model.
90
+ */
91
+ interface Token {
92
+ /** The opaque credential to present on each request (bearer token, API key). */
93
+ readonly value: string;
94
+ /** Absolute expiry instant, epoch milliseconds. The `Date.now()` scale. */
95
+ readonly expiresAt: number;
96
+ }
97
+ /**
98
+ * Pure configuration — the knob's only field. Carries no mutable state;
99
+ * `TokenState` does. One config is shared across all the refreshes of one
100
+ * logical credential (often a module-scope const).
101
+ */
102
+ interface TokenRefreshConfig {
103
+ /**
104
+ * Refresh-ahead skew, in milliseconds. A token is treated as needing a
105
+ * refresh once `at >= expiresAt - skewMs`, i.e. `skewMs` BEFORE its hard
106
+ * expiry. This buys headroom so a call that starts just under the wire never
107
+ * races a token that lapses mid-flight, and absorbs small client/server clock
108
+ * disagreement. `0` means "refresh exactly at expiry"; a negative value is
109
+ * clamped to `0` (a skew that pushes the trigger past expiry is meaningless).
110
+ *
111
+ * @default 0
112
+ */
113
+ readonly skewMs?: number;
114
+ }
115
+ /**
116
+ * The token slice this knob owns — the Model field a consumer spreads in via
117
+ * `init()`. A two-field record rather than a phase union because the two
118
+ * dimensions are orthogonal: a token can be present-and-fresh, present-but-stale
119
+ * (a 401 came back), absent, or absent-and-stale (cleared after a failed
120
+ * refresh). Every combination is meaningful, so neither field implies the
121
+ * other's value and a discriminated union would only manufacture impossible
122
+ * arms.
123
+ *
124
+ * - `token` — the currently held credential, or `null` when none has been
125
+ * minted yet (fresh boot) or the last refresh failed.
126
+ * - `stale` — set by `on401` when the server rejected a request the client
127
+ * believed valid. Forces the next `needsRefresh` to true regardless of
128
+ * `expiresAt`; cleared by `refreshed` once a new credential lands.
129
+ *
130
+ * Both fields `readonly` so the whole slice threads safely through a pure
131
+ * reducer and serializes into a `Store<S>`.
132
+ */
133
+ interface TokenState {
134
+ /** The held credential, or `null` if none is available (boot / failed refresh). */
135
+ readonly token: Token | null;
136
+ /** Whether a 401 has marked the held token unusable ahead of its `expiresAt`. */
137
+ readonly stale: boolean;
138
+ }
139
+ /**
140
+ * The starting slice: no token held, not stale. The first `needsRefresh` after
141
+ * this returns `true` (no token), so a consumer's first guarded call mints a
142
+ * credential before going out.
143
+ */
144
+ declare function initTokenRefresh(): TokenState;
145
+ /**
146
+ * The Cmd this knob emits to ask the runtime to mint a fresh token. Plain data
147
+ * (no closure — invariant 1) carrying nothing: the refresh port takes no input,
148
+ * and the slice the result installs is read off the follow-up Msg, not this
149
+ * Cmd. The consumer unions this into their machine's Cmd type and lets
150
+ * `handlers` interpret it.
151
+ */
152
+ interface RefreshTokenCmd extends Cmd<"refresh_token"> {
153
+ }
154
+ /** Construct a `refresh_token` Cmd. */
155
+ declare function refreshTokenCmd(): RefreshTokenCmd;
156
+ /**
157
+ * The Msg the refresh port dispatches on success — carries the freshly minted
158
+ * `Token`. The consumer unions this into their Msg type and handles it by
159
+ * folding `refreshed(state, msg.token)` into the slice.
160
+ */
161
+ interface TokenRefreshedMsg {
162
+ readonly type: typeof MsgType.TokenRefreshed;
163
+ readonly token: Token;
164
+ }
165
+ /** Construct a `token_refreshed` Msg carrying the new token. */
166
+ declare function tokenRefreshedMsg(token: Token): TokenRefreshedMsg;
167
+ /**
168
+ * The Msg the refresh port dispatches on failure — carries the rejection so the
169
+ * consumer's reducer can decide whether to back off, surface an error, or give
170
+ * up (errors are data: it is dispatched, never swallowed). The slice itself is
171
+ * left untouched by a failed refresh — the held token (if any) stays held and
172
+ * stale stays set, so the consumer can retry; clearing it is the consumer's
173
+ * policy, expressed in their reducer.
174
+ */
175
+ interface TokenRefreshFailedMsg {
176
+ readonly type: typeof MsgType.TokenRefreshFailed;
177
+ readonly error: unknown;
178
+ }
179
+ /** Construct a `token_refresh_failed` Msg carrying the rejection. */
180
+ declare function tokenRefreshFailedMsg(error: unknown): TokenRefreshFailedMsg;
181
+ /** The two Msgs the refresh port can produce. Union for a consumer's Msg type. */
182
+ type TokenRefreshMsg = TokenRefreshedMsg | TokenRefreshFailedMsg;
183
+ /** The ports this knob's `handlers` splice needs — the single refresh DI seam. */
184
+ interface TokenRefreshPorts {
185
+ /**
186
+ * Mint a fresh credential. Called ONLY from the `refresh_token` interpret
187
+ * handler — never from a verb — so the network I/O and any clock the issuer
188
+ * reads stay at the effect boundary. May reject; the rejection is routed to
189
+ * `token_refresh_failed` (errors are data), never thrown into the runtime.
190
+ */
191
+ readonly refresh: () => Promise<Token>;
192
+ }
193
+ /**
194
+ * The knob: hand it a config, get back the slice initializer, the pure verbs,
195
+ * and the `handlers` splice. Mirrors every L2 composition's uniform contract so
196
+ * the call sites read identically across bricks.
197
+ *
198
+ * @example
199
+ * const tr = createTokenRefresh({ skewMs: 30_000 });
200
+ * const s0 = tr.init();
201
+ * const [s1, cmds] = tr.ensureFresh(s0, Date.now()); // cmds = [refreshTokenCmd()]
202
+ */
203
+ declare function createTokenRefresh(config?: TokenRefreshConfig): {
204
+ /** The slice initializer — spread into the consumer's `init`. */
205
+ init: typeof initTokenRefresh;
206
+ needsRefresh: (state: TokenState, at: number) => boolean;
207
+ on401: (state: TokenState) => TokenState;
208
+ refreshed: (state: TokenState, token: Token) => TokenState;
209
+ ensureFresh: (state: TokenState, at: number) => readonly [TokenState, readonly Cmd[]];
210
+ handlers: (ports: TokenRefreshPorts) => Interpret<TokenRefreshMsg, RefreshTokenCmd, unknown>;
211
+ };
212
+ /** The shape `createTokenRefresh` returns — useful for typing a held knob. */
213
+ type TokenRefresh = ReturnType<typeof createTokenRefresh>;
214
+
215
+ export { type RefreshTokenCmd, type Token, type TokenRefresh, type TokenRefreshConfig, type TokenRefreshFailedMsg, type TokenRefreshMsg, type TokenRefreshPorts, type TokenRefreshedMsg, type TokenState, createTokenRefresh, initTokenRefresh, refreshTokenCmd, tokenRefreshFailedMsg, tokenRefreshedMsg };
@@ -0,0 +1,6 @@
1
+ export { createTokenRefresh, initTokenRefresh, refreshTokenCmd, tokenRefreshFailedMsg, tokenRefreshedMsg } from '../chunk-FH5RUIIL.js';
2
+ import '../chunk-JELSZN25.js';
3
+ import '../chunk-2WZHS4P2.js';
4
+ import '../chunk-BNJYE4AU.js';
5
+ //# sourceMappingURL=index.js.map
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -1,5 +1,6 @@
1
- import { Cmd, Sub, Machine } from '../index.js';
1
+ import { C as Cmd, S as Sub, M as Machine } from '../core-CymXZCeY.js';
2
2
  import { Trace } from '../recorder/index.js';
3
+ import '../index.js';
3
4
 
4
5
  /**
5
6
  * The outcome of {@link replayTrace}.
@@ -48,5 +49,13 @@ interface ReplayResult<S> {
48
49
  declare function replayTrace<S, M extends {
49
50
  type: string;
50
51
  }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, trace: Trace<S, M>, ctx: Ctx): ReplayResult<S>;
52
+ /**
53
+ * Order-insensitive structural deep-equality — the single comparator this
54
+ * package owns, exposed so consumers (e.g. `@demlik/tea/parity`'s `diff`)
55
+ * reuse it rather than adding a second deep-compare. `true` iff `a` and `b`
56
+ * are structurally equal under {@link firstDivergence}'s walk (object keys
57
+ * compared order-insensitively, arrays index-by-index).
58
+ */
59
+ declare function deepEqual(a: unknown, b: unknown): boolean;
51
60
 
52
- export { type ReplayResult, replayTrace };
61
+ export { type ReplayResult, deepEqual, replayTrace };
@@ -1,57 +1,5 @@
1
- import { replay } from '../chunk-CMG2KMWX.js';
2
-
3
- // src/trace-replay/index.ts
4
- function replayTrace(machine, trace, ctx) {
5
- const result = replay(machine, {
6
- msgs: trace.msgs,
7
- ctx,
8
- loaded: trace.loaded
9
- });
10
- const actual = result.state;
11
- const expected = trace.finalState;
12
- const divergence = firstDivergence(expected, actual, "state");
13
- if (divergence === null) {
14
- return { matches: true, expected, actual };
15
- }
16
- return { matches: false, expected, actual, divergence };
17
- }
18
- function firstDivergence(expected, actual, path) {
19
- if (Object.is(expected, actual)) return null;
20
- const te = typeOf(expected);
21
- const ta = typeOf(actual);
22
- if (te !== ta) return { path, expected, actual };
23
- if (te === "array") {
24
- const ae = expected;
25
- const aa = actual;
26
- if (ae.length !== aa.length) {
27
- return { path, expected, actual };
28
- }
29
- for (let i = 0; i < ae.length; i++) {
30
- const d = firstDivergence(ae[i], aa[i], `${path}[${i}]`);
31
- if (d !== null) return d;
32
- }
33
- return null;
34
- }
35
- if (te === "object") {
36
- const oe = expected;
37
- const oa = actual;
38
- const keys = [.../* @__PURE__ */ new Set([...Object.keys(oe), ...Object.keys(oa)])].sort();
39
- for (const key of keys) {
40
- const d = firstDivergence(oe[key], oa[key], `${path}.${key}`);
41
- if (d !== null) return d;
42
- }
43
- return null;
44
- }
45
- return { path, expected, actual };
46
- }
47
- function typeOf(v) {
48
- if (v === null) return "null";
49
- if (v === void 0) return "undefined";
50
- if (Array.isArray(v)) return "array";
51
- if (typeof v === "object") return "object";
52
- return "primitive";
53
- }
54
-
55
- export { replayTrace };
1
+ export { deepEqual, replayTrace } from '../chunk-EDGDR5XX.js';
2
+ import '../chunk-2WZHS4P2.js';
3
+ import '../chunk-BNJYE4AU.js';
56
4
  //# sourceMappingURL=index.js.map
57
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/trace-replay/index.ts"],"names":[],"mappings":";;;AAoEO,SAAS,WAAA,CAOd,OAAA,EACA,KAAA,EACA,GAAA,EACiB;AACjB,EAAA,MAAM,MAAA,GAAS,OAAO,OAAA,EAAS;AAAA,IAC7B,MAAM,KAAA,CAAM,IAAA;AAAA,IACZ,GAAA;AAAA,IACA,QAAQ,KAAA,CAAM;AAAA,GACf,CAAA;AACD,EAAA,MAAM,SAAS,MAAA,CAAO,KAAA;AACtB,EAAA,MAAM,WAAW,KAAA,CAAM,UAAA;AAEvB,EAAA,MAAM,UAAA,GAAa,eAAA,CAAgB,QAAA,EAAU,MAAA,EAAQ,OAAO,CAAA;AAC5D,EAAA,IAAI,eAAe,IAAA,EAAM;AACvB,IAAA,OAAO,EAAE,OAAA,EAAS,IAAA,EAAM,QAAA,EAAU,MAAA,EAAO;AAAA,EAC3C;AACA,EAAA,OAAO,EAAE,OAAA,EAAS,KAAA,EAAO,QAAA,EAAU,QAAQ,UAAA,EAAW;AACxD;AAsBA,SAAS,eAAA,CACP,QAAA,EACA,MAAA,EACA,IAAA,EACY;AAIZ,EAAA,IAAI,MAAA,CAAO,EAAA,CAAG,QAAA,EAAU,MAAM,GAAG,OAAO,IAAA;AAExC,EAAA,MAAM,EAAA,GAAK,OAAO,QAAQ,CAAA;AAC1B,EAAA,MAAM,EAAA,GAAK,OAAO,MAAM,CAAA;AAGxB,EAAA,IAAI,OAAO,EAAA,EAAI,OAAO,EAAE,IAAA,EAAM,UAAU,MAAA,EAAO;AAE/C,EAAA,IAAI,OAAO,OAAA,EAAS;AAClB,IAAA,MAAM,EAAA,GAAK,QAAA;AACX,IAAA,MAAM,EAAA,GAAK,MAAA;AACX,IAAA,IAAI,EAAA,CAAG,MAAA,KAAW,EAAA,CAAG,MAAA,EAAQ;AAG3B,MAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,MAAA,EAAO;AAAA,IAClC;AACA,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,EAAA,CAAG,QAAQ,CAAA,EAAA,EAAK;AAClC,MAAA,MAAM,CAAA,GAAI,eAAA,CAAgB,EAAA,CAAG,CAAC,CAAA,EAAG,EAAA,CAAG,CAAC,CAAA,EAAG,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA,CAAG,CAAA;AACvD,MAAA,IAAI,CAAA,KAAM,MAAM,OAAO,CAAA;AAAA,IACzB;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,OAAO,QAAA,EAAU;AACnB,IAAA,MAAM,EAAA,GAAK,QAAA;AACX,IAAA,MAAM,EAAA,GAAK,MAAA;AAIX,IAAA,MAAM,OAAO,CAAC,uBAAO,GAAA,CAAI,CAAC,GAAG,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA,EAAG,GAAG,OAAO,IAAA,CAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAA,EAAK;AACzE,IAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,MAAA,MAAM,CAAA,GAAI,eAAA,CAAgB,EAAA,CAAG,GAAG,CAAA,EAAG,EAAA,CAAG,GAAG,CAAA,EAAG,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,GAAG,CAAA,CAAE,CAAA;AAC5D,MAAA,IAAI,CAAA,KAAM,MAAM,OAAO,CAAA;AAAA,IACzB;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAIA,EAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,MAAA,EAAO;AAClC;AAGA,SAAS,OACP,CAAA,EACyD;AACzD,EAAA,IAAI,CAAA,KAAM,MAAM,OAAO,MAAA;AACvB,EAAA,IAAI,CAAA,KAAM,QAAW,OAAO,WAAA;AAC5B,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA,EAAG,OAAO,OAAA;AAC7B,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,EAAU,OAAO,QAAA;AAClC,EAAA,OAAO,WAAA;AACT","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// trace-replay — replay a recorded Trace against a machine and diff the state.\n//\n// This is the back half of the record/replay feature. `../recorder` captures\n// a run (`loaded` + `msgs` + `finalState`); this module feeds that input\n// through the core `replay(machine, { msgs, ctx, loaded })` fold and asks:\n//\n// does the current reducer still produce the SAME final state for this\n// recorded input?\n//\n// Equal ⇒ the reducer is behavior-compatible with the recording. Diverged ⇒\n// the reducer changed for that input, and the divergence path\n// (`state.queue[2].status`) points at exactly the cell that drifted. This is\n// the regression / production-repro tool: record a prod incident, replay it\n// against the fixed reducer locally, confirm the fix changes the state at the\n// path you expected — or catch an unintended drift before it ships.\n//\n// Standalone by design. It imports ONLY the `Trace` type from `../recorder`\n// and the `replay` engine from `../index`. The deep-equal + first-divergence\n// path finder is self-contained (no external diff lib) — a richer pretty-diff\n// lands in `../devtools` later; trace-replay stays dependency-free so it can\n// run anywhere a machine runs (CI, a Worker, a Node script).\n// ---------------------------------------------------------------------------\n\nimport { type Cmd, type Machine, replay, type Sub } from \"../index\";\nimport type { Trace } from \"../recorder\";\n\n/**\n * The outcome of {@link replayTrace}.\n *\n * @typeParam S - the machine's State type.\n */\nexport interface ReplayResult<S> {\n /** `true` iff the recomputed final state deep-equals the trace's `finalState`. */\n matches: boolean;\n /** The trace's recorded `finalState` — what the run originally produced. */\n expected: S;\n /** The state the current reducer produced for the same input. */\n actual: S;\n /**\n * Present iff `matches` is `false`. The FIRST point of divergence found by\n * a deterministic pre-order walk:\n * - `path` — a dotted/indexed access path from the state root, e.g.\n * `state.queue[2].status` or `state` for a root-level\n * scalar mismatch.\n * - `expected` — the recorded value at that path.\n * - `actual` — the recomputed value at that path.\n */\n divergence?: { path: string; expected: unknown; actual: unknown };\n}\n\n/**\n * Replay a recorded {@link Trace} against `machine` and diff the recomputed\n * final state against the recorded one.\n *\n * Runs the core pure fold `replay(machine, { msgs: trace.msgs, ctx,\n * loaded: trace.loaded })` — init + update only, never `interpret`, never a\n * `Store`, never a live subscription (see `replay` in ../index). The `ctx` is\n * supplied fresh by the caller: the trace is the INPUT (loaded + msgs); ctx is\n * the ENVIRONMENT the reducer runs against now.\n *\n * @param machine - the machine to replay against. Mutating its reducer (the\n * whole point of a regression check) is what makes a recorded\n * trace diverge.\n * @param trace - a Trace from `../recorder` (live `dump()` or a re-hydrated\n * `parseJSONL(...)`).\n * @param ctx - the runtime ctx, supplied fresh at replay time.\n */\nexport function replayTrace<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n trace: Trace<S, M>,\n ctx: Ctx,\n): ReplayResult<S> {\n const result = replay(machine, {\n msgs: trace.msgs,\n ctx,\n loaded: trace.loaded,\n });\n const actual = result.state;\n const expected = trace.finalState;\n\n const divergence = firstDivergence(expected, actual, \"state\");\n if (divergence === null) {\n return { matches: true, expected, actual };\n }\n return { matches: false, expected, actual, divergence };\n}\n\n// === self-contained deep-equal + first-divergence path finder ===\n//\n// One walk does both jobs: it returns the FIRST point where `expected` and\n// `actual` differ (as a path + the two values), or `null` when they are\n// deeply equal. \"First\" is defined by a deterministic pre-order traversal:\n// the node itself, then its children in a stable key order (array index\n// order; object keys sorted so the result does not depend on insertion\n// order — two states with the same data but different key order are equal,\n// and the reported path for a real mismatch is reproducible run-to-run).\n//\n// Scope: TEA state is plain JSON-shaped data (the substrate's __DEV__ guards\n// reject closures in Cmds, and states are conventionally serializable). So\n// the walk handles: primitives, null/undefined, arrays, and plain objects.\n// It treats other object kinds (Date, Map, Set, class instances) by reference\n// identity + a structural fallback via Object.entries — sufficient for the\n// JSON-shaped states this tool targets; a richer differ lives in ../devtools.\n\n/** A divergence record, or `null` when the two values are deeply equal. */\ntype Divergence = { path: string; expected: unknown; actual: unknown } | null;\n\nfunction firstDivergence(\n expected: unknown,\n actual: unknown,\n path: string,\n): Divergence {\n // Identity / primitive fast path. Handles same-reference, and equal\n // primitives. `Object.is` so NaN === NaN and +0 ≠ -0 (matches structural\n // equality intent better than ===).\n if (Object.is(expected, actual)) return null;\n\n const te = typeOf(expected);\n const ta = typeOf(actual);\n // Different kinds at this node (e.g. object vs array, number vs string,\n // present vs null) — this node IS the divergence.\n if (te !== ta) return { path, expected, actual };\n\n if (te === \"array\") {\n const ae = expected as unknown[];\n const aa = actual as unknown[];\n if (ae.length !== aa.length) {\n // Length mismatch reported at the array node itself — the shape differs,\n // which is more informative than an arbitrary element index.\n return { path, expected, actual };\n }\n for (let i = 0; i < ae.length; i++) {\n const d = firstDivergence(ae[i], aa[i], `${path}[${i}]`);\n if (d !== null) return d;\n }\n return null;\n }\n\n if (te === \"object\") {\n const oe = expected as Record<string, unknown>;\n const oa = actual as Record<string, unknown>;\n // Union of keys, sorted for deterministic traversal. A key present in one\n // but absent in the other diverges at that child path (its value is\n // `undefined` on the missing side).\n const keys = [...new Set([...Object.keys(oe), ...Object.keys(oa)])].sort();\n for (const key of keys) {\n const d = firstDivergence(oe[key], oa[key], `${path}.${key}`);\n if (d !== null) return d;\n }\n return null;\n }\n\n // Primitives (string/number/boolean/bigint/symbol/function) that failed the\n // Object.is fast path, or null/undefined that differ — this node diverges.\n return { path, expected, actual };\n}\n\n/** Coarse type tag used by the divergence walk to decide how to recurse. */\nfunction typeOf(\n v: unknown,\n): \"null\" | \"undefined\" | \"array\" | \"object\" | \"primitive\" {\n if (v === null) return \"null\";\n if (v === undefined) return \"undefined\";\n if (Array.isArray(v)) return \"array\";\n if (typeof v === \"object\") return \"object\";\n return \"primitive\";\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -1,4 +1,4 @@
1
- import { Sub } from './index.js';
1
+ import { S as Sub } from './core-CymXZCeY.js';
2
2
 
3
3
  type SubscribeHandler<S extends Sub, M, Ctx> = (sub: S, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
4
4
 
@@ -0,0 +1,196 @@
1
+ import { S as Sub, C as Cmd, M as Machine } from '../core-CymXZCeY.js';
2
+
3
+ /**
4
+ * @demlik/tea/with-deadline — auto-fail a wrapped machine at T+N, re-arm on
5
+ * progress. A **pure function over machine data**: `withDeadline(base, config)`
6
+ * takes any `Machine<S, M, C, U, Ctx>` and returns a NEW `Machine` over the
7
+ * composed Model `{ base, $deadline }`. You `run()` / `replay()` the result
8
+ * exactly like any other machine — there is no new runtime, no privileged
9
+ * interception path.
10
+ *
11
+ * ## What it does
12
+ *
13
+ * The wrapper watches the base Msg stream. While the deadline is `armed`, a
14
+ * RELATIVE timeout Sub counts down `config.ms`. Every base Msg the
15
+ * `config.progress` predicate accepts (default: every base Msg) is treated as
16
+ * activity: the wrapper bumps a numeric `seq`, which CHANGES the timer Sub's id,
17
+ * so the substrate's reconcile pass retires the old countdown and starts a fresh
18
+ * one — "re-arm on progress". If `config.ms` elapses with NO accepted progress,
19
+ * the timeout Sub dispatches `$deadline:exceeded`; the merged `update` folds it
20
+ * by flipping `phase` to `exceeded`, and from then on `subscriptions` returns no
21
+ * timer (the machine has auto-failed). The expiry is a slice transition visible
22
+ * in the Msg log — the auto-fail is never an off-ledger side effect.
23
+ *
24
+ * This is an OBSERVE-ONLY wrapper in the same sense as `withTelemetry`: it never
25
+ * intercepts a base Cmd and never gates a base Msg. `state.base` after wrapping
26
+ * is BYTE-IDENTICAL to running the base alone, so the shared
27
+ * `assertWrapperFaithful` gate passes with the deadline bolted on.
28
+ *
29
+ * ## The 5 rules it satisfies (from the wrapper-tier contract)
30
+ *
31
+ * 1. **Named, serializable slice.** Wrapper state lives at `model.$deadline`,
32
+ * a plain `{ phase, seq }` — JSON-serializable, never a closure. There is
33
+ * NO absolute timestamp in the slice: the clock is never read in `update`.
34
+ * 2. **Retag-and-re-emit, never swallow.** The merged `update` runs the base
35
+ * reducer, passes its Cmds through UNCHANGED, and APPENDS its own
36
+ * `$deadline:rearm` / `$deadline:expire` decision Cmd. No base Cmd ever
37
+ * vanishes; nothing is gated.
38
+ * 3. **Every decision is a Msg/Cmd.** Re-arm is a `$deadline:rearm` Cmd; the
39
+ * auto-fail expiry is the `$deadline:exceeded` Msg the Sub delivers, folded
40
+ * into the slice and echoed as a `$deadline:expire` Cmd. "Why did this
41
+ * fire" is answerable from reducer + log alone.
42
+ * 4. **Subs merge by id.** `subscriptions = [...base, ...deadlineSubs]`; the
43
+ * timeout id is `$deadline:timeout:{seq}`, so a progress bump (seq+1)
44
+ * yields a NEW id — the substrate retires the old timer and arms a fresh
45
+ * one. When `exceeded`, no deadline sub is returned.
46
+ * 5. **Clock out of update.** The countdown is a relative-timeout Sub; the
47
+ * clock is read at subscribe time inside the Sub (`setTimeout`), NEVER in a
48
+ * verb. The slice carries `seq`, not a deadline timestamp.
49
+ *
50
+ * ## Typical wiring
51
+ *
52
+ * const guarded = withDeadline(baseMachine, {
53
+ * ms: 900_000, // auto-fail after 15 minutes of inactivity
54
+ * progress: (msg) => msg.type !== "heartbeat", // heartbeats don't re-arm
55
+ * });
56
+ * const runtime = run(guarded, { ctx: baseCtx });
57
+ */
58
+
59
+ /**
60
+ * The wrapper's Model slice. `phase` is the lifecycle (`armed` while the
61
+ * countdown runs, `exceeded` once the deadline fired); `seq` is the monotonic
62
+ * re-arm counter that keys the timer Sub's id so each accepted progress Msg
63
+ * retires the old timer and starts a fresh one.
64
+ *
65
+ * PURE and SERIALIZABLE: there is deliberately NO absolute timestamp here. A
66
+ * deadline timestamp would force the merged `update` to read the wall clock to
67
+ * compute it, which the conformance gate (property 2 — two replays under
68
+ * distinct ambient clocks) is built to catch. Instead, the timer arms a fresh
69
+ * full `ms` countdown per generation; no remaining-delay computation and no
70
+ * clock read occur in the slice or `update` — the only clock is `setTimeout`
71
+ * inside the Sub.
72
+ */
73
+ interface DeadlineSlice {
74
+ /** `armed` while the countdown is live; `exceeded` once the deadline fired. */
75
+ readonly phase: "armed" | "exceeded";
76
+ /**
77
+ * Monotonic re-arm counter. Starts at 0; each accepted progress Msg (while
78
+ * armed) bumps it by one. Folds into the timer Sub's id, so a bump = a new id
79
+ * = the substrate retires the old timer and arms a fresh `ms` countdown.
80
+ */
81
+ readonly seq: number;
82
+ }
83
+ /**
84
+ * The composed Model. The base machine's state is nested under `base`; the
85
+ * wrapper's state lives in the NAMED `$deadline` slice — never hidden in a
86
+ * closure. Both fields are plain data, so the whole composed Model round-trips
87
+ * through `JSON.parse(JSON.stringify(...))` iff the base state does.
88
+ */
89
+ interface DeadlineModel<S> {
90
+ readonly base: S;
91
+ readonly $deadline: DeadlineSlice;
92
+ }
93
+ /**
94
+ * The relative-timeout Sub the wrapper arms while `phase === "armed"`. `delayMs`
95
+ * is the window `config.ms`; the id is `$deadline:timeout:{seq}`, so each
96
+ * progress bump produces a distinct id and the reconcile pass churns the timer.
97
+ * Same `delayMs`-on-the-Sub shape `fromTimeout` consumes.
98
+ */
99
+ type DeadlineTimeoutSub = Sub<"$deadline:timeout"> & {
100
+ readonly delayMs: number;
101
+ };
102
+ /**
103
+ * The Msg the timeout Sub dispatches when `config.ms` elapses with no accepted
104
+ * progress. `seq` echoes the re-arm generation the timer was armed against, so a
105
+ * stale timer's Msg (one whose id was already retired) is identifiable; the
106
+ * merged `update` only honors it when it matches the current slice `seq`.
107
+ */
108
+ interface DeadlineExceededMsg {
109
+ readonly type: "$deadline:exceeded";
110
+ /** The re-arm generation this timer was armed against. */
111
+ readonly seq: number;
112
+ }
113
+ /**
114
+ * Construct the deadline-exceeded Msg. Exported so the subscribe cell and tests
115
+ * share one constructor rather than two literals that can drift.
116
+ */
117
+ declare function deadlineExceededMsg(seq: number): DeadlineExceededMsg;
118
+ /**
119
+ * The fire-and-forget decision Cmd the merged `update` appends on every
120
+ * transition, so the wrapper's choice is visible in the replayed Cmd log
121
+ * (rule 3) — `rearm` when an accepted progress Msg bumped `seq`, `expire` when
122
+ * the deadline-exceeded Msg flipped the phase, `idle` otherwise (a non-progress
123
+ * Msg, or a Msg after the machine already auto-failed). Plain data; its
124
+ * interpret handler is a no-op (the decision is the Cmd's presence in the log,
125
+ * not a side effect).
126
+ */
127
+ type DeadlineDecisionCmd = Cmd<"$deadline:decision"> & {
128
+ readonly decision: "rearm" | "expire" | "idle";
129
+ /** The slice `seq` AFTER this transition (the re-arm generation now armed). */
130
+ readonly seq: number;
131
+ };
132
+ /**
133
+ * A predicate over a Msg that decides whether it re-arms the deadline.
134
+ *
135
+ * The argument is `Readonly<M>` and the result is `boolean`: this is the
136
+ * STRONGEST purity TS can express here. `Readonly<M>` removes the most common
137
+ * impurity at compile time — the predicate cannot reassign the Msg's own
138
+ * fields — and `boolean` pins the return so a side-effecting `(msg) => { … }`
139
+ * with an implicit `undefined`/`void` return is rejected.
140
+ *
141
+ * What the type system CANNOT encode (TS has no effect tracking): observational
142
+ * purity. A body that reads `Date.now()`, calls `Math.random()`, performs IO,
143
+ * or closes over and mutates external state still type-checks. Those remain a
144
+ * CONVENTION enforced by review, not the compiler — the predicate runs inside
145
+ * the pure merged `update` (invariant 2), so it must be a pure function of the
146
+ * Msg alone: no clock, no RNG, no IO, no captured mutable state.
147
+ */
148
+ type ProgressPredicate<M extends {
149
+ type: string;
150
+ }> = (msg: Readonly<M>) => boolean;
151
+ /**
152
+ * The deadline knob. `ms` is the inactivity window; `progress` is an OPTIONAL
153
+ * predicate picking which base Msgs count as activity (default: EVERY base Msg
154
+ * re-arms). The predicate runs INSIDE the pure merged `update`, so it must be a
155
+ * pure function of the Msg — see {@link ProgressPredicate} for what the type
156
+ * enforces vs. what stays a convention.
157
+ */
158
+ interface DeadlineConfig<M extends {
159
+ type: string;
160
+ }> {
161
+ /** The inactivity window in milliseconds. Auto-fail after `ms` with no progress. */
162
+ readonly ms: number;
163
+ /**
164
+ * Predicate picking which base Msgs re-arm the timer. Default: every base Msg
165
+ * counts as progress. Pure — a function of the Msg alone (see
166
+ * {@link ProgressPredicate}).
167
+ */
168
+ readonly progress?: ProgressPredicate<M>;
169
+ }
170
+ /**
171
+ * Wrap `base` with an inactivity deadline. Returns a NEW `Machine` over the
172
+ * composed Model `{ base, $deadline }`, with the base's Msgs/Cmds/Subs extended
173
+ * by the wrapper's own (`$deadline:exceeded` Msg, `$deadline:decision` Cmd,
174
+ * `$deadline:timeout` Sub).
175
+ *
176
+ * The composed machine:
177
+ * - `init` rehydrates the base (honoring the `[loaded, []]` contract) and
178
+ * seeds `$deadline: { phase: "armed", seq: 0 }`. Base init Cmds pass
179
+ * through unchanged.
180
+ * - `update` (Reducer form) runs the base reducer for base Msgs — base Cmds
181
+ * pass through UNCHANGED — and on an accepted progress Msg bumps `seq`
182
+ * (re-arm). The `$deadline:exceeded` Msg cell flips `phase` to `exceeded`.
183
+ * Every transition appends a `$deadline:decision` marker Cmd.
184
+ * - `subscriptions` is the base's, plus a `$deadline:timeout` Sub keyed on
185
+ * `seq` while `phase === "armed"`.
186
+ * - `subscribe` is the base's, plus the `$deadline:timeout` cell.
187
+ * - `interpret` is the base's, plus a no-op `$deadline:decision` handler.
188
+ *
189
+ * @param base any `Machine<S, M, C, U, Ctx>`.
190
+ * @param config the deadline knob (`ms` window + optional `progress` predicate).
191
+ */
192
+ declare function withDeadline<S, M extends {
193
+ type: string;
194
+ }, C extends Cmd, U extends Sub, Ctx>(base: Machine<S, M, C, U, Ctx>, config: DeadlineConfig<M>): Machine<DeadlineModel<S>, M | DeadlineExceededMsg, C | DeadlineDecisionCmd, U | DeadlineTimeoutSub, Ctx>;
195
+
196
+ export { type DeadlineConfig, type DeadlineDecisionCmd, type DeadlineExceededMsg, type DeadlineModel, type DeadlineSlice, type DeadlineTimeoutSub, type ProgressPredicate, deadlineExceededMsg, withDeadline };
@@ -0,0 +1,130 @@
1
+ import { fromTimeout } from '../chunk-2YFLOCWX.js';
2
+ import '../chunk-2WZHS4P2.js';
3
+ import { formOf, subId } from '../chunk-BNJYE4AU.js';
4
+
5
+ // src/with-deadline/index.ts
6
+ function deadlineExceededMsg(seq) {
7
+ return { type: "$deadline:exceeded", seq };
8
+ }
9
+ function runBaseUpdate(update, form, state, msg) {
10
+ if (form === "reducer") {
11
+ const record = update;
12
+ return record[msg.type](state, msg);
13
+ }
14
+ const table = update;
15
+ const stateKey = state.type;
16
+ return table[stateKey][msg.type](state, msg);
17
+ }
18
+ function deadlineMsgKeys(update, form) {
19
+ const keys = Object.keys(update);
20
+ const firstKey = keys[0];
21
+ if (firstKey === void 0) return [];
22
+ if (form === "reducer") return keys;
23
+ const firstValue = update[firstKey];
24
+ return Object.keys(firstValue);
25
+ }
26
+ function withDeadline(base, config) {
27
+ const windowMs = config.ms;
28
+ const isProgress = config.progress;
29
+ const update = {};
30
+ const baseForm = formOf(base);
31
+ const baseMsgKeys = deadlineMsgKeys(base.update, baseForm);
32
+ for (const key of baseMsgKeys) {
33
+ if (key.startsWith("$deadline:")) {
34
+ throw new Error(
35
+ `withDeadline: the base machine declares a reserved "${key}" update cell \u2014 the wrapper owns the "$deadline:" Msg namespace. Rename the base Msg.`
36
+ );
37
+ }
38
+ }
39
+ for (const key of baseMsgKeys) {
40
+ update[key] = (state, msg) => {
41
+ const [nextBase, baseCmds] = runBaseUpdate(
42
+ base.update,
43
+ baseForm,
44
+ state.base,
45
+ msg
46
+ );
47
+ const armed = state.$deadline.phase === "armed";
48
+ const counts = armed && (isProgress === void 0 || isProgress(msg));
49
+ const $deadline = counts ? { phase: "armed", seq: state.$deadline.seq + 1 } : state.$deadline;
50
+ const next = { base: nextBase, $deadline };
51
+ const decision = {
52
+ type: "$deadline:decision",
53
+ decision: counts ? "rearm" : "idle",
54
+ seq: $deadline.seq
55
+ };
56
+ return [next, [...baseCmds, decision]];
57
+ };
58
+ }
59
+ update["$deadline:exceeded"] = (state, msg) => {
60
+ const m = msg;
61
+ const fires = state.$deadline.phase === "armed" && m.seq === state.$deadline.seq;
62
+ const $deadline = fires ? { phase: "exceeded", seq: state.$deadline.seq } : state.$deadline;
63
+ const next = { base: state.base, $deadline };
64
+ const decision = {
65
+ type: "$deadline:decision",
66
+ decision: fires ? "expire" : "idle",
67
+ seq: $deadline.seq
68
+ };
69
+ return [next, [decision]];
70
+ };
71
+ const baseInterpret = base.interpret ?? {};
72
+ if (Object.hasOwn(baseInterpret, "$deadline:decision")) {
73
+ throw new Error(
74
+ 'withDeadline: the base machine declares a reserved "$deadline:decision" interpret handler \u2014 the wrapper owns the "$deadline:" Cmd namespace. Rename the base handler.'
75
+ );
76
+ }
77
+ const interpret = {
78
+ ...baseInterpret,
79
+ "$deadline:decision": async () => {
80
+ }
81
+ };
82
+ const baseSubscriptions = base.subscriptions;
83
+ const subscriptions = (state) => {
84
+ const baseSubs = baseSubscriptions ? baseSubscriptions(state.base) : [];
85
+ if (state.$deadline.phase !== "armed") return baseSubs;
86
+ const timeout = {
87
+ id: subId(`$deadline:timeout:${state.$deadline.seq}`),
88
+ type: "$deadline:timeout",
89
+ delayMs: windowMs
90
+ };
91
+ return [...baseSubs, timeout];
92
+ };
93
+ const baseSubscribe = base.subscribe ?? {};
94
+ if (Object.hasOwn(baseSubscribe, "$deadline:timeout")) {
95
+ throw new Error(
96
+ 'withDeadline: the base machine declares a reserved "$deadline:timeout" subscribe handler \u2014 the wrapper owns the "$deadline:" Sub namespace. Rename the base handler.'
97
+ );
98
+ }
99
+ const timeoutCell = fromTimeout(
100
+ (sub) => {
101
+ const seq = Number(sub.id.slice(sub.id.lastIndexOf(":") + 1));
102
+ return deadlineExceededMsg(seq);
103
+ }
104
+ );
105
+ const subscribe = {
106
+ ...baseSubscribe,
107
+ "$deadline:timeout": timeoutCell
108
+ };
109
+ return {
110
+ init: (loaded, ctx) => {
111
+ if (loaded !== null) {
112
+ return [loaded, []];
113
+ }
114
+ const [base0, cmds] = base.init(null, ctx);
115
+ const model = {
116
+ base: base0,
117
+ $deadline: { phase: "armed", seq: 0 }
118
+ };
119
+ return [model, cmds];
120
+ },
121
+ update,
122
+ subscriptions,
123
+ subscribe,
124
+ interpret
125
+ };
126
+ }
127
+
128
+ export { deadlineExceededMsg, withDeadline };
129
+ //# sourceMappingURL=index.js.map
130
+ //# sourceMappingURL=index.js.map