@demlik/tea 0.15.0 → 0.16.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 (115) hide show
  1. package/README.md +30 -7
  2. package/dist/agent/index.d.ts +49 -37
  3. package/dist/agent/index.js +12 -8
  4. package/dist/chunk-4LPY67E4.js +18 -0
  5. package/dist/chunk-4LPY67E4.js.map +1 -0
  6. package/dist/chunk-5XU6ZJO2.js +533 -0
  7. package/dist/chunk-5XU6ZJO2.js.map +1 -0
  8. package/dist/{chunk-AYS5N6WY.js → chunk-6NG4V5SB.js} +62 -172
  9. package/dist/chunk-6NG4V5SB.js.map +1 -0
  10. package/dist/{chunk-3B5X7SBB.js → chunk-7MGXQZPW.js} +11 -9
  11. package/dist/chunk-7MGXQZPW.js.map +1 -0
  12. package/dist/{chunk-V46HSVCK.js → chunk-BCTEOVOU.js} +3 -3
  13. package/dist/{chunk-V46HSVCK.js.map → chunk-BCTEOVOU.js.map} +1 -1
  14. package/dist/chunk-CH5KTOEZ.js +710 -0
  15. package/dist/chunk-CH5KTOEZ.js.map +1 -0
  16. package/dist/chunk-CIHSHLQX.js +64 -0
  17. package/dist/chunk-CIHSHLQX.js.map +1 -0
  18. package/dist/{chunk-TCLBCERH.js → chunk-EHUDXAKC.js} +238 -212
  19. package/dist/chunk-EHUDXAKC.js.map +1 -0
  20. package/dist/chunk-GOX2CYIM.js +380 -0
  21. package/dist/chunk-GOX2CYIM.js.map +1 -0
  22. package/dist/{chunk-22POWGFA.js → chunk-IVJP3GKJ.js} +46 -20
  23. package/dist/chunk-IVJP3GKJ.js.map +1 -0
  24. package/dist/{chunk-QPEVCJ5Q.js → chunk-MVZS5FDM.js} +13 -6
  25. package/dist/chunk-MVZS5FDM.js.map +1 -0
  26. package/dist/{chunk-VTQI2NHP.js → chunk-MYNLY3HP.js} +9 -8
  27. package/dist/chunk-MYNLY3HP.js.map +1 -0
  28. package/dist/{chunk-Q5CQTBFA.js → chunk-NSIAORRY.js} +20 -30
  29. package/dist/chunk-NSIAORRY.js.map +1 -0
  30. package/dist/{chunk-MYXFNWAJ.js → chunk-QNGYQMZB.js} +4 -4
  31. package/dist/chunk-QNGYQMZB.js.map +1 -0
  32. package/dist/chunk-WT5PBPLB.js +98 -0
  33. package/dist/chunk-WT5PBPLB.js.map +1 -0
  34. package/dist/{core-DnzqWy6u.d.ts → core-BbgtiXdg.d.ts} +231 -139
  35. package/dist/do/index.d.ts +37 -33
  36. package/dist/do/index.js +21 -14
  37. package/dist/do/index.js.map +1 -1
  38. package/dist/{durable-effects-CWzA-o2B.d.ts → durable-effects-7MTK_jnj.d.ts} +1 -1
  39. package/dist/effect/index.d.ts +151 -0
  40. package/dist/effect/index.js +98 -0
  41. package/dist/effect/index.js.map +1 -0
  42. package/dist/extension/index.d.ts +6 -7
  43. package/dist/extension/index.js +3 -2
  44. package/dist/extension/index.js.map +1 -1
  45. package/dist/flow/index.d.ts +68 -59
  46. package/dist/flow/index.js +32 -27
  47. package/dist/flow/index.js.map +1 -1
  48. package/dist/idempotency/index.d.ts +6 -7
  49. package/dist/idempotency/index.js +4 -2
  50. package/dist/{index-CSyMYDAB.d.ts → index-7vV8xmsD.d.ts} +1 -1
  51. package/dist/{index-KXqTRJyb.d.ts → index-Bz5Rh9Ga.d.ts} +19 -96
  52. package/dist/index-CjFDUBeq.d.ts +207 -0
  53. package/dist/{index-CuNO9m8i.d.ts → index-CkFYA9Y6.d.ts} +14 -16
  54. package/dist/{index-CA_7I5g9.d.ts → index-Cou86-GX.d.ts} +137 -246
  55. package/dist/{index-DrIRb_Ch.d.ts → index-DU6JpQ8G.d.ts} +1 -1
  56. package/dist/{index-HSYgG2Yt.d.ts → index-Xufl55W8.d.ts} +247 -211
  57. package/dist/{index-C2DwkjlP.d.ts → index-_dOR3xqA.d.ts} +57 -45
  58. package/dist/index.d.ts +121 -385
  59. package/dist/index.js +2 -1
  60. package/dist/jev/index.d.ts +133 -97
  61. package/dist/jev/index.js +205 -161
  62. package/dist/jev/index.js.map +1 -1
  63. package/dist/{machine-DGlPWCWI.d.ts → machine-BQzTV8On.d.ts} +171 -288
  64. package/dist/machine-viz/index.d.ts +5 -5
  65. package/dist/machine-viz/index.js +8 -4
  66. package/dist/machine-viz/index.js.map +1 -1
  67. package/dist/mem/index.d.ts +3 -4
  68. package/dist/mem/index.js +2 -1
  69. package/dist/mem/index.js.map +1 -1
  70. package/dist/node/index.d.ts +119 -78
  71. package/dist/node/index.js +52 -38
  72. package/dist/node/index.js.map +1 -1
  73. package/dist/paginate/index.d.ts +27 -22
  74. package/dist/paginate/index.js +6 -4
  75. package/dist/parity/index.d.ts +4 -5
  76. package/dist/parity/index.js +3 -2
  77. package/dist/parity/index.js.map +1 -1
  78. package/dist/pbt/index.d.ts +2 -2
  79. package/dist/pbt/index.js +2 -1
  80. package/dist/pbt/index.js.map +1 -1
  81. package/dist/persistence/index.d.ts +6 -7
  82. package/dist/persistence/index.js +5 -3
  83. package/dist/promise/index.d.ts +186 -0
  84. package/dist/promise/index.js +5 -0
  85. package/dist/promise/index.js.map +1 -0
  86. package/dist/{protocol-D8VbH-VH.d.ts → protocol-ClRZBhHQ.d.ts} +7 -8
  87. package/dist/react/index.d.ts +36 -17
  88. package/dist/react/index.js +27 -4
  89. package/dist/react/index.js.map +1 -1
  90. package/dist/resilience/index.d.ts +63 -456
  91. package/dist/resilience/index.js +37 -483
  92. package/dist/resilience/index.js.map +1 -1
  93. package/dist/{runtime-types-CMF13BBh.d.ts → runtime-types-CiVpC0HN.d.ts} +131 -388
  94. package/dist/testing/index.d.ts +22 -16
  95. package/dist/testing/index.js +15 -7
  96. package/dist/testing/index.js.map +1 -1
  97. package/dist/timing/index.d.ts +25 -22
  98. package/dist/timing/index.js +4 -3
  99. package/dist/timing/index.js.map +1 -1
  100. package/dist/{types-BseNPIhr.d.ts → types-LxZxGYda.d.ts} +2 -2
  101. package/dist/work-queue/index.d.ts +5 -6
  102. package/package.json +18 -5
  103. package/dist/chunk-22POWGFA.js.map +0 -1
  104. package/dist/chunk-3B5X7SBB.js.map +0 -1
  105. package/dist/chunk-66PY5WYF.js +0 -1816
  106. package/dist/chunk-66PY5WYF.js.map +0 -1
  107. package/dist/chunk-AYS5N6WY.js.map +0 -1
  108. package/dist/chunk-L3TND7LA.js +0 -97
  109. package/dist/chunk-L3TND7LA.js.map +0 -1
  110. package/dist/chunk-MYXFNWAJ.js.map +0 -1
  111. package/dist/chunk-Q5CQTBFA.js.map +0 -1
  112. package/dist/chunk-QPEVCJ5Q.js.map +0 -1
  113. package/dist/chunk-TCLBCERH.js.map +0 -1
  114. package/dist/chunk-VTQI2NHP.js.map +0 -1
  115. package/dist/index-ZCMhnVKN.d.ts +0 -272
@@ -1,161 +1,21 @@
1
- import { S as Sub, c as SubId } from './core-DnzqWy6u.js';
2
- import { S as SubscribeHandler } from './types-BseNPIhr.js';
3
-
4
- /**
5
- * internal/resilience/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.
6
- *
7
- * Same pure-state-ops shape as `../../idempotency/` and
8
- * `../../work-queue/ops.ts`: a state type plus pure transition functions. Host-
9
- * agnostic by construction — no `Date.now()`, no timers, no I/O inside the ops.
10
- * The caller injects the clock (`nowMs`) and folds the result through its own
11
- * reducer, keeping the store inside TEA's invariant 2 (transitions are pure:
12
- * same inputs → same outputs) so a consumer can exercise it with `replay`
13
- * without a fake clock.
14
- *
15
- * Every op returns a NEW cache value and never mutates its input (invariant 1 —
16
- * state is a value, not a place). `get` / `has` are read-shaped; `set` /
17
- * `remove` / `evictExpired` return a new cache.
18
- *
19
- * **Distinct from `/idempotency`.** Both are TTL-bounded key maps, but they
20
- * answer different questions and so carry different shapes:
21
- *
22
- * - `/idempotency` is DEDUPE-focused — "have I already processed this key?"
23
- * TTL is a single store-level policy (`ttlMs`), each entry stores the
24
- * `atMs` it was remembered at, and expiry is derived (`now - atMs >=
25
- * ttlMs`). It carries an `order` ledger + `capacity` because the use case
26
- * (webhook receivers, retried messages) needs bounded LRU eviction.
27
- * - `/cache` is RETRIEVAL-focused — "what is the value for this key right
28
- * now?" TTL is set PER ENTRY at write time: `set(key, value, nowMs,
29
- * ttlMs)` bakes the absolute `expiresAtMs = nowMs + ttlMs` into the entry.
30
- * Different keys can carry different lifetimes (a hot key cached for 1s, a
31
- * cold one for 1h) — the dimension idempotency's single store-level
32
- * `ttlMs` cannot express. No `order`, no `capacity`: a cache bounds itself
33
- * by TTL + periodic eviction, not by LRU count.
34
- *
35
- * The eviction Sub is the "eviction is a Msg" half: a `setInterval`-shaped Sub
36
- * (composed directly on `fromInterval`, from `src/subs/`) that periodically
37
- * dispatches a `cache_evict` Msg the consumer's reducer handles by calling
38
- * `evictExpired`. Time still never enters the reducer — the Sub fires the Msg,
39
- * the reducer reads `nowMs` off the Msg (the consumer stamps it at the Sub
40
- * boundary), and `evictExpired` does the pure drop.
41
- *
42
- * Internal to the package (`src/internal/resilience/`), not a published
43
- * subpath (#46); same one-shape-per-module rule as `work-queue`, `idempotency`,
44
- * and `rate-limit`.
45
- */
46
-
47
- /**
48
- * A cached entry: the `value` and the absolute clock reading `expiresAtMs` it
49
- * expires at. Unlike `/idempotency`'s entry (which stores `atMs` and derives
50
- * expiry from a store-level `ttlMs`), the expiry instant is baked in at write
51
- * time — so per-entry TTLs are a first-class capability. Both fields are plain
52
- * data so the whole cache is trivially JSON-serializable into a consumer's
53
- * `Store<S>`.
54
- */
55
- interface CacheEntry<V> {
56
- readonly value: V;
57
- readonly expiresAtMs: number;
58
- }
59
- /**
60
- * The cache: a map of `key → entry`. No `order` ledger and no `capacity` (cf.
61
- * `/idempotency`) — a TTL cache bounds itself by expiry + periodic eviction,
62
- * not by LRU count. Each entry carries its own `expiresAtMs`, so the cache
63
- * needs no store-level TTL policy field.
64
- */
65
- interface TtlCache<V> {
66
- readonly entries: Readonly<Record<string, CacheEntry<V>>>;
67
- }
68
- /** Create an empty cache. */
69
- declare function initCache<V>(): TtlCache<V>;
70
- /**
71
- * The cached `value` for `key` iff present AND unexpired at `nowMs`, else
72
- * `undefined`. An expired entry reads as absent even though it is physically
73
- * still in `entries` until the next `evictExpired` (or an overwriting `set`)
74
- * drops it. PURE — reads only, never mutates.
75
- */
76
- declare function get<V>(cache: TtlCache<V>, key: string, nowMs: number): V | undefined;
77
- /**
78
- * True iff `key` is present AND unexpired at `nowMs`. The boolean sibling of
79
- * `get` for callers that only need presence, not the value. PURE.
80
- */
81
- declare function has<V>(cache: TtlCache<V>, key: string, nowMs: number): boolean;
82
- /**
83
- * Write `key → value` with an absolute expiry of `nowMs + ttlMs`. Re-`set`ting
84
- * an existing key overwrites both the value and the expiry — the TTL clock
85
- * restarts from the new `nowMs`. PURE — returns a new cache; the input is
86
- * untouched (a fresh shallow copy of `entries`).
87
- */
88
- declare function set<V>(cache: TtlCache<V>, key: string, value: V, nowMs: number, ttlMs: number): TtlCache<V>;
89
- /**
90
- * Drop `key` regardless of expiry. PURE — returns a new cache; the input is
91
- * untouched. Removing an absent key returns an equivalent cache (the shallow
92
- * copy still happens, but no entry is dropped).
93
- */
94
- declare function remove<V>(cache: TtlCache<V>, key: string): TtlCache<V>;
95
- /**
96
- * Physically drop every entry expired at `nowMs` (`nowMs >= expiresAtMs`).
97
- * Returns a NEW cache with the survivors. This is the pure work the eviction
98
- * Sub's `cache_evict` Msg triggers — `get` / `has` already treat expired
99
- * entries as absent, so eviction is purely a memory-reclaim pass, not a
100
- * correctness one. PURE — the input cache is never mutated.
101
- */
102
- declare function evictExpired<V>(cache: TtlCache<V>, nowMs: number): TtlCache<V>;
103
- /**
104
- * The Msg the eviction Sub dispatches each tick. `id` is the Sub's own id so a
105
- * consumer with multiple caches can route the tick to the matching cache. The
106
- * reducer reads `Date.now()`-free time by stamping `nowMs` itself at the Sub
107
- * boundary if needed; the canonical reducer cell is
108
- * `cache_evict: (s) => [{ ...s, cache: evictExpired(s.cache, nowMs) }, []]`.
109
- */
110
- interface CacheEvictMsg {
111
- readonly type: "cache_evict";
112
- readonly id: string;
113
- }
114
- /** Construct a `cache_evict` Msg for the cache identified by `id`. */
115
- declare function cacheEvictMsg(id: string): CacheEvictMsg;
116
- /**
117
- * The Sub shape the eviction factory installs: a `setInterval`-shaped Sub
118
- * carrying its tick period (`intervalMs`) per the substrate's
119
- * reconcile-by-id-with-data-on-the-Sub contract. `type: "cache"` is the
120
- * lowercase source-noun Sub convention (canon §2.5 / naming-style).
121
- */
122
- type CacheEvictionSub = Sub<"cache"> & {
123
- readonly intervalMs: number;
124
- };
125
- /**
126
- * Declare an eviction Sub for the cache identified by `id`, ticking every
127
- * `everyMs`. Drop the result into `subscriptions(state)`:
128
- *
129
- * subscriptions: (s) => [cacheEvictionSub("session-cache", 30_000)]
130
- *
131
- * Same id ⇒ same running interval across transitions (no reconcile churn);
132
- * changing `everyMs` alone will NOT re-arm the timer (the substrate leaves a
133
- * same-id Sub running) — change the `id` too if the period must change. This
134
- * mirrors `fromInterval`'s data-on-the-Sub tradeoff exactly.
135
- */
136
- declare function cacheEvictionSub(id: string, everyMs: number): CacheEvictionSub;
137
- /**
138
- * The `subscribe` cell for the eviction Sub. Assign it to
139
- * `machine.subscribe.cache`:
140
- *
141
- * subscribe: { cache: cacheEvictionSubscribe }
142
- *
143
- * Composed directly on `fromInterval` — it owns the `setInterval` /
144
- * `clearInterval` lifecycle; this cell only pins the Msg the tick dispatches
145
- * (`cacheEvictMsg(sub.id)`). The cleanup `fromInterval` returns stops the
146
- * interval when the Sub is reconciled out (the cache's state exits the phase
147
- * that declared it).
148
- */
149
- declare const cacheEvictionSubscribe: SubscribeHandler<CacheEvictionSub, CacheEvictMsg, unknown>;
1
+ import { n as SubId, S as Sub, o as DepKeyedSub, h as Dispose, T as TimerDeps } from './core-BbgtiXdg.js';
2
+ import { S as SubscribeHandler } from './types-LxZxGYda.js';
150
3
 
151
4
  /**
152
5
  * internal/resilience/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock
153
6
  * deadline is reached.
154
7
  *
155
8
  * Generalizes the recurring "auto-fail a machine at T+N" / "15-minute stale
156
- * guard" pattern: a Sub whose `subscriptions(state)` returns it only while the
157
- * deadline should be armed, and which dispatches a single `DeadlineExceeded`
158
- * Msg when the wall clock crosses `atMs`.
9
+ * guard" pattern: a deadline a battery lists only while it should be armed,
10
+ * which dispatches a single `DeadlineExceeded` Msg when the wall clock crosses
11
+ * `atMs`.
12
+ *
13
+ * A battery's `subs(slice)` returns its deadlines as a LIST (`DeadlineSub[]`,
14
+ * one per retry timer, say). A machine declares that list as ONE Sub of type
15
+ * `"deadline"` whose `deps` is the list (`deadlinesSub(select)`), and the
16
+ * `deadline` runner arms every deadline in it. A change to the list restarts
17
+ * the runner, which re-arms each deadline for its REMAINING time — `atMs` is
18
+ * absolute, so a restart never moves a deadline.
159
19
  *
160
20
  * Difference from `fromTimeout` (relative — "fire after N ms"): a deadline is
161
21
  * an ABSOLUTE target. The delay is computed at subscribe time as
@@ -168,10 +28,9 @@ declare const cacheEvictionSubscribe: SubscribeHandler<CacheEvictionSub, CacheEv
168
28
  * reconcile pass — the substrate must finish wiring all subs before any Msg
169
29
  * lands).
170
30
  *
171
- * Composition over reinvention: the timer lifecycle is `fromTimeout`'s, not
172
- * redrawn here. `deadlineSub` produces the Sub literal carrying `atMs`; the
173
- * `subscribe` handler (`subscribeDeadline`) translates `atMs` → a relative delay
174
- * and delegates the `setTimeout` / `clearTimeout` to `fromTimeout`. The clock
31
+ * `deadlineSub` produces one deadline literal carrying `atMs`; the runner
32
+ * (`subscribeDeadline`) translates each `atMs` → a relative delay and arms a
33
+ * `setTimeout` / `clearTimeout` pair per deadline. The clock
175
34
  * read (`Date.now()`) lives only in the subscribe handler — never in a reducer
176
35
  * (invariant 2) — and is exercised in tests via vitest fake timers, matching
177
36
  * the no-injection convention of the `fromTimeout` / `fromInterval` exemplars.
@@ -198,19 +57,38 @@ declare const cacheEvictionSubscribe: SubscribeHandler<CacheEvictionSub, CacheEv
198
57
  */
199
58
 
200
59
  /**
201
- * The Sub variant a deadline produces. `atMs` is the absolute target — epoch
60
+ * One deadline, as a battery lists it. `atMs` is the absolute target — epoch
202
61
  * milliseconds (the `Date.now()` / `Date.parse(...)` scale), NOT a relative
203
- * delay. It rides on the Sub so the reconcile pass keys identity by `id`
204
- * alone: the same `id` across transitions means the same armed timer, even if
205
- * a later `subscriptions(state)` recomputes the literal.
62
+ * delay. `id` names the deadline inside its list: it rides on the dispatched
63
+ * Msg so a reducer can tell WHICH deadline fired, and a registry-backed timer
64
+ * (a DO alarm slot) keys its entry on it.
206
65
  *
207
- * Lowercase `"deadline"` discriminant per the Sub naming convention (a source
208
- * noun that doubles as the SubId family); same shape family as the
209
- * `TimeoutSubData` the `fromTimeout` factory consumes.
66
+ * It is plain data — it becomes part of the `deadline` Sub's `deps`, which the
67
+ * engine hashes — and is not itself a running Sub.
210
68
  */
211
- type DeadlineSub<N extends string | undefined = undefined> = Sub<"deadline"> & {
69
+ type DeadlineSub<N extends string | undefined = undefined> = {
70
+ readonly id: SubId;
71
+ readonly type: "deadline";
212
72
  readonly atMs: number;
213
73
  } & DeadlineOpts<N>;
74
+ /**
75
+ * The running `"deadline"` Sub: its `deps` is the non-empty list of deadlines
76
+ * to arm. Name it in `types.sub` for a machine that arms deadlines.
77
+ */
78
+ type DeadlinesSub<N extends string | undefined = undefined> = Sub<"deadline", readonly DeadlineSub<N>[]>;
79
+ /**
80
+ * The `deps` of a `deadline` Sub: the list, or `null` when it is empty — an
81
+ * empty list is a Sub with nothing to arm, and an off Sub is not a live one
82
+ * (`driveToDone` reads live Subs to tell a waiting machine from a stalled one).
83
+ */
84
+ declare function deadlines<N extends string | undefined = undefined>(list: readonly DeadlineSub<N>[]): readonly DeadlineSub<N>[] | null;
85
+ /**
86
+ * The `subs` entry that arms whatever deadlines `select` lists at a state:
87
+ *
88
+ * subs: [deadlinesSub((s: State) => rc.subs(s.resilience))],
89
+ * // run(machine, { subscribe: { deadline: subscribeDeadline } })
90
+ */
91
+ declare function deadlinesSub<S, N extends string | undefined = undefined>(select: (state: S) => readonly DeadlineSub<N>[]): DepKeyedSub<S, DeadlinesSub<N>>;
214
92
  /**
215
93
  * Additive options the `deadlineSub` factory folds onto the Sub literal. This
216
94
  * is the extension seam (invariant 5 — composition by reduction): future fields
@@ -266,19 +144,17 @@ type DeadlineExceeded<N extends string | undefined = undefined> = {
266
144
  */
267
145
  type DeadlineMsgType<N extends string | undefined> = N extends string ? `${N}_deadline` : "deadline_exceeded";
268
146
  /**
269
- * Build a deadline Sub literal. Pure data — no clock read, no timer; the timer
270
- * is armed later by the `subscribe` cell. `id` is branded via `subId(...)` so
147
+ * Build a deadline literal. Pure data — no clock read, no timer; the timer
148
+ * is armed later by the `deadline` runner. `id` is branded via `subId(...)` so
271
149
  * accidental raw-string drift fails at the type level (invariant 7).
272
150
  *
273
- * Return the result directly from `subscriptions(state)` while the deadline
274
- * should be armed; drop it (return `[]`, or stop returning this id) to disarm —
275
- * the reconcile pass calls the cleanup, clearing the pending timer before it
276
- * fires. That is the "cancel on state exit" lifecycle, identical to
277
- * `fromTimeout`'s.
151
+ * List it from a battery's `subs(slice)` while the deadline should be armed;
152
+ * drop it to disarm — the engine restarts the `deadline` Sub, whose cleanup
153
+ * clears the pending timer before it fires. That is the "cancel on state exit"
154
+ * lifecycle.
278
155
  *
279
- * @param id Stable identity for this deadline. Same `id` across transitions =
280
- * same armed timer (no churn). Use distinct ids for distinct
281
- * deadlines on the same machine.
156
+ * @param id Identity for this deadline, echoed on the Msg it fires. Use
157
+ * distinct ids for distinct deadlines on the same machine.
282
158
  * @param atMs Absolute target in epoch milliseconds (e.g. `Date.now() + 900_000`
283
159
  * for a 15-minute guard, or a persisted `expiresAt`).
284
160
  * @param opts Optional additive fields folded onto the Sub literal (see
@@ -294,10 +170,9 @@ declare function deadlineSub<N extends string | undefined = undefined>(id: strin
294
170
  * (DO alarm registry, `setTimeout`, fake timer) and return a cleanup that
295
171
  * cancels the pending fire.
296
172
  *
297
- * `id` is the Sub's reconcile id — a registry-backed host (the DO alarm slot)
298
- * keys its entry on `id` so the cleanup deletes the EXACT entry the substrate
299
- * reconciled. A `setTimeout`-backed host ignores `id` (the closure holds the
300
- * handle).
173
+ * `id` is the deadline's own id — a registry-backed host (the DO alarm slot)
174
+ * keys its entry on `id` so the cleanup deletes the EXACT entry it armed. A
175
+ * `setTimeout`-backed host ignores `id` (the closure holds the handle).
301
176
  *
302
177
  * The host computes the gap itself: `atMs - Date.now()` is the REMAINING time.
303
178
  * On a host that just rehydrated after hibernation that gap is the shrunken
@@ -307,64 +182,58 @@ declare function deadlineSub<N extends string | undefined = undefined>(id: strin
307
182
  */
308
183
  type ArmTimer<M> = (id: SubId, atMs: number, msg: M, dispatch: (msg: M) => void) => () => void;
309
184
  /**
310
- * Build the `subscribe["deadline"]` cell from a host-plugged `armTimer`. This
311
- * module owns the Sub shape, the anchor, and the Msg; the HOST owns what backs
312
- * the timer. The returned handler reads `id` + `atMs` off the Sub, builds the
313
- * `deadlineExceeded(...)` Msg, and hands all three to `armTimer`.
185
+ * Build the `deadline` runner from a host-plugged `armTimer`. This module owns
186
+ * the deadline shape, the anchor, and the Msg; the HOST owns what backs the
187
+ * timer. The runner arms every deadline in the Sub's `deps` list — reading
188
+ * `id` + `atMs`, building the `deadlineExceeded(...)` Msg and handing all three
189
+ * to `armTimer` — and its cleanup cancels them all.
314
190
  *
315
191
  * Use it when `setTimeout` is the wrong backing — most concretely a Durable
316
192
  * Object, which hibernates and must register a `do_alarm` instead:
317
193
  *
318
- * subscribe: {
319
- * deadline: subscribeWith((id, atMs, msg, dispatch) =>
320
- * alarms.register(id, atMs, () => dispatch(msg)),
321
- * ),
322
- * }
194
+ * run(machine, {
195
+ * subscribe: {
196
+ * deadline: subscribeWith((id, atMs, msg, dispatch) =>
197
+ * alarms.register(id, atMs, () => dispatch(msg)),
198
+ * ),
199
+ * },
200
+ * })
323
201
  *
324
202
  * For the `setTimeout` default, use {@link subscribeDeadline} — it is exactly
325
203
  * `subscribeWith(setTimeoutArmTimer())`, so there is one deadline surface and
326
204
  * one anchor, not a second one per host.
327
205
  */
328
- declare function subscribeWith<N extends string | undefined = undefined>(armTimer: ArmTimer<DeadlineExceeded<N>>): SubscribeHandler<DeadlineSub<N>, DeadlineExceeded<N>, unknown>;
206
+ declare function subscribeWith<N extends string | undefined = undefined>(armTimer: ArmTimer<DeadlineExceeded<N>>): (sub: DeadlinesSub<N>, ctx: unknown, dispatch: (msg: DeadlineExceeded<N>) => void) => Dispose;
329
207
  /**
330
208
  * The `setTimeout` timer backing — for node / browser / any host whose timer is
331
209
  * a plain `setTimeout`. Arms for the REMAINING time (`max(0, atMs - Date.now())`,
332
210
  * floored at 0 so an already-past deadline fires on the NEXT tick rather than
333
211
  * synchronously inside the reconcile pass), so a deadline re-derived after a
334
- * rehydrate fires at the original instant, not a fresh full window.
335
- *
336
- * Composes on `fromTimeout` rather than redrawing `setTimeout` /
337
- * `clearTimeout`: this package has ONE timer lifecycle, and the arm-timer seam
338
- * plugs into it instead of forking it. A hibernating host does NOT use this —
339
- * it plugs its own `armTimer` (a `do_alarm` registration) into
340
- * {@link subscribeWith}.
212
+ * rehydrate — or re-armed by a restart — fires at the original instant, not a
213
+ * fresh full window. A hibernating host does NOT use this — it plugs its own
214
+ * `armTimer` (a `do_alarm` registration) into {@link subscribeWith}.
341
215
  */
342
216
  declare function setTimeoutArmTimer<N extends string | undefined = undefined>(): ArmTimer<DeadlineExceeded<N>>;
343
217
  /**
344
- * The `subscribe["deadline"]` handler for the DEFAULT `setTimeout` backing. Arms a
345
- * one-shot timer for the remaining delay `max(0, atMs - Date.now())` and
346
- * dispatches `deadlineExceeded(...)` when it fires; returns a cleanup that
347
- * clears the pending timer.
218
+ * The `deadline` runner for the DEFAULT `setTimeout` backing. Arms a one-shot
219
+ * timer per listed deadline for its remaining delay `max(0, atMs - Date.now())`
220
+ * and dispatches `deadlineExceeded(...)` when each fires; the cleanup clears
221
+ * every pending timer.
348
222
  *
349
223
  * It is `subscribeWith(setTimeoutArmTimer())` — the default backing named, not
350
224
  * a separate implementation, so the host-plugged path and the default path can
351
225
  * never disagree about the anchor.
352
226
  *
353
- * Assign directly to a `Subscribe` handler:
354
- *
355
- * subscribe: {
356
- * deadline: subscribeDeadline,
357
- * }
227
+ * run(machine, { subscribe: { deadline: subscribeDeadline } })
358
228
  *
359
- * Generic in the deadline's name so ONE handler serves every knob in a machine:
360
- * the tag it dispatches is read off the Sub's own `name`, so a machine mounting
361
- * a named knob beside an unnamed one wires this same cell once and each Sub
362
- * still dispatches its own tag.
229
+ * Generic in the deadline's name so ONE runner serves every knob in a machine:
230
+ * the tag it dispatches is read off each deadline's own `name`, so a machine
231
+ * listing a named knob's deadlines beside an unnamed one's wires this once.
363
232
  */
364
- declare const subscribeDeadline: <N extends string | undefined = undefined>(sub: DeadlineSub<N>, ctx: unknown, dispatch: (msg: DeadlineExceeded<N>) => void) => () => void;
233
+ declare const subscribeDeadline: <N extends string | undefined = undefined>(sub: DeadlinesSub<N>, ctx: unknown, dispatch: (msg: DeadlineExceeded<N>) => void) => Dispose;
365
234
  /**
366
235
  * Construct the Msg the deadline dispatches. Exported so consumers can build /
367
- * assert the same shape (and so the subscribe cell and tests share one
236
+ * assert the same shape (and so the runner and tests share one
368
237
  * constructor rather than two literals that can drift).
369
238
  */
370
239
  declare function deadlineExceeded<N extends string | undefined = undefined>(id: string, atMs: number, name?: N): DeadlineExceeded<N>;
@@ -376,5 +245,172 @@ declare function deadlineExceeded<N extends string | undefined = undefined>(id:
376
245
  * template and drifting.
377
246
  */
378
247
  declare function deadlineMsgType<N extends string | undefined>(name?: N): DeadlineMsgType<N>;
248
+ /**
249
+ * The built-in `timer` Sub's deps for the SOONEST deadline in `list`, counted
250
+ * from `nowMs`; `null` when the list is empty (no timer armed).
251
+ *
252
+ * This is how a battery arms a whole deadline list through the engine's
253
+ * built-in `timer` instead of a `deadline` runner: a machine declares ONE
254
+ * `{ type: "timer", deps: (s) => nextTimer(list(s), clock(s)) }` entry. The
255
+ * timer fires the soonest deadline's `DeadlineExceeded` Msg; the transition it
256
+ * causes drops that deadline from the list, and the next soonest is armed.
257
+ *
258
+ * `nowMs` is the instant the list was last computed against — the battery's
259
+ * own record of its latest transition, never a clock read (invariant 2). The
260
+ * countdown is `atMs - nowMs`, clamped at `0`, and because `ms` is part of the
261
+ * deps, every new `nowMs` restarts the countdown from that instant. PURE.
262
+ */
263
+ declare function nextTimer<N extends string | undefined = undefined>(list: readonly DeadlineSub<N>[], nowMs: number): TimerDeps<DeadlineExceeded<N>> | null;
264
+
265
+ /**
266
+ * internal/resilience/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.
267
+ *
268
+ * Same pure-state-ops shape as `../../idempotency/` and
269
+ * `../../work-queue/ops.ts`: a state type plus pure transition functions. Host-
270
+ * agnostic by construction — no `Date.now()`, no timers, no I/O inside the ops.
271
+ * The caller injects the clock (`nowMs`) and folds the result through its own
272
+ * reducer, keeping the store inside TEA's invariant 2 (transitions are pure:
273
+ * same inputs → same outputs) so a consumer can exercise it with `replay`
274
+ * without a fake clock.
275
+ *
276
+ * Every op returns a NEW cache value and never mutates its input (invariant 1 —
277
+ * state is a value, not a place). `get` / `has` are read-shaped; `set` /
278
+ * `remove` / `evictExpired` return a new cache.
279
+ *
280
+ * **Distinct from `/idempotency`.** Both are TTL-bounded key maps, but they
281
+ * answer different questions and so carry different shapes:
282
+ *
283
+ * - `/idempotency` is DEDUPE-focused — "have I already processed this key?"
284
+ * TTL is a single store-level policy (`ttlMs`), each entry stores the
285
+ * `atMs` it was remembered at, and expiry is derived (`now - atMs >=
286
+ * ttlMs`). It carries an `order` ledger + `capacity` because the use case
287
+ * (webhook receivers, retried messages) needs bounded LRU eviction.
288
+ * - `/cache` is RETRIEVAL-focused — "what is the value for this key right
289
+ * now?" TTL is set PER ENTRY at write time: `set(key, value, nowMs,
290
+ * ttlMs)` bakes the absolute `expiresAtMs = nowMs + ttlMs` into the entry.
291
+ * Different keys can carry different lifetimes (a hot key cached for 1s, a
292
+ * cold one for 1h) — the dimension idempotency's single store-level
293
+ * `ttlMs` cannot express. No `order`, no `capacity`: a cache bounds itself
294
+ * by TTL + periodic eviction, not by LRU count.
295
+ *
296
+ * The eviction Sub is the "eviction is a Msg" half: a `setInterval`-shaped Sub
297
+ * (composed directly on `fromInterval`, from `src/subs/`) that periodically
298
+ * dispatches a `cache_evict` Msg the consumer's reducer handles by calling
299
+ * `evictExpired`. Time still never enters the reducer — the Sub fires the Msg,
300
+ * the reducer reads `nowMs` off the Msg (the consumer stamps it at the Sub
301
+ * boundary), and `evictExpired` does the pure drop.
302
+ *
303
+ * Internal to the package (`src/internal/resilience/`), not a published
304
+ * subpath (#46); same one-shape-per-module rule as `work-queue`, `idempotency`,
305
+ * and `rate-limit`.
306
+ */
307
+
308
+ /**
309
+ * A cached entry: the `value` and the absolute clock reading `expiresAtMs` it
310
+ * expires at. Unlike `/idempotency`'s entry (which stores `atMs` and derives
311
+ * expiry from a store-level `ttlMs`), the expiry instant is baked in at write
312
+ * time — so per-entry TTLs are a first-class capability. Both fields are plain
313
+ * data so the whole cache is trivially JSON-serializable into a consumer's
314
+ * `Store<S>`.
315
+ */
316
+ interface CacheEntry<V> {
317
+ readonly value: V;
318
+ readonly expiresAtMs: number;
319
+ }
320
+ /**
321
+ * The cache: a map of `key → entry`. No `order` ledger and no `capacity` (cf.
322
+ * `/idempotency`) — a TTL cache bounds itself by expiry + periodic eviction,
323
+ * not by LRU count. Each entry carries its own `expiresAtMs`, so the cache
324
+ * needs no store-level TTL policy field.
325
+ */
326
+ interface TtlCache<V> {
327
+ readonly entries: Readonly<Record<string, CacheEntry<V>>>;
328
+ }
329
+ /** Create an empty cache. */
330
+ declare function initCache<V>(): TtlCache<V>;
331
+ /**
332
+ * The cached `value` for `key` iff present AND unexpired at `nowMs`, else
333
+ * `undefined`. An expired entry reads as absent even though it is physically
334
+ * still in `entries` until the next `evictExpired` (or an overwriting `set`)
335
+ * drops it. PURE — reads only, never mutates.
336
+ */
337
+ declare function get<V>(cache: TtlCache<V>, key: string, nowMs: number): V | undefined;
338
+ /**
339
+ * True iff `key` is present AND unexpired at `nowMs`. The boolean sibling of
340
+ * `get` for callers that only need presence, not the value. PURE.
341
+ */
342
+ declare function has<V>(cache: TtlCache<V>, key: string, nowMs: number): boolean;
343
+ /**
344
+ * Write `key → value` with an absolute expiry of `nowMs + ttlMs`. Re-`set`ting
345
+ * an existing key overwrites both the value and the expiry — the TTL clock
346
+ * restarts from the new `nowMs`. PURE — returns a new cache; the input is
347
+ * untouched (a fresh shallow copy of `entries`).
348
+ */
349
+ declare function set<V>(cache: TtlCache<V>, key: string, value: V, nowMs: number, ttlMs: number): TtlCache<V>;
350
+ /**
351
+ * Drop `key` regardless of expiry. PURE — returns a new cache; the input is
352
+ * untouched. Removing an absent key returns an equivalent cache (the shallow
353
+ * copy still happens, but no entry is dropped).
354
+ */
355
+ declare function remove<V>(cache: TtlCache<V>, key: string): TtlCache<V>;
356
+ /**
357
+ * Physically drop every entry expired at `nowMs` (`nowMs >= expiresAtMs`).
358
+ * Returns a NEW cache with the survivors. This is the pure work the eviction
359
+ * Sub's `cache_evict` Msg triggers — `get` / `has` already treat expired
360
+ * entries as absent, so eviction is purely a memory-reclaim pass, not a
361
+ * correctness one. PURE — the input cache is never mutated.
362
+ */
363
+ declare function evictExpired<V>(cache: TtlCache<V>, nowMs: number): TtlCache<V>;
364
+ /**
365
+ * The Msg the eviction Sub dispatches each tick. `id` is the cache's name so a
366
+ * consumer with multiple caches can route the tick to the matching cache. The
367
+ * reducer reads `Date.now()`-free time by stamping `nowMs` itself at the Sub
368
+ * boundary if needed; the canonical reducer cell is
369
+ * `cache_evict: (s) => [{ ...s, cache: evictExpired(s.cache, nowMs) }, []]`.
370
+ */
371
+ interface CacheEvictMsg {
372
+ readonly type: "cache_evict";
373
+ readonly id: string;
374
+ }
375
+ /** Construct a `cache_evict` Msg for the cache identified by `id`. */
376
+ declare function cacheEvictMsg(id: string): CacheEvictMsg;
377
+ /**
378
+ * The `deps` of an eviction Sub: which cache it ticks for (`name`, echoed as
379
+ * the Msg's `id`) and the tick period (`intervalMs`, which `fromInterval`
380
+ * reads).
381
+ */
382
+ type CacheEvictionDeps = {
383
+ readonly name: string;
384
+ readonly intervalMs: number;
385
+ };
386
+ /**
387
+ * The running eviction Sub: a `setInterval`-shaped Sub whose `deps` carry the
388
+ * cache's name and tick period. `type: "cache"` is the lowercase source-noun
389
+ * Sub convention (canon §2.5 / naming-style).
390
+ */
391
+ type CacheEvictionSub = Sub<"cache", CacheEvictionDeps>;
392
+ /**
393
+ * The `subs` entry for an eviction tick on the cache named `name`, every
394
+ * `everyMs`. It is on for the machine's life:
395
+ *
396
+ * subs: [cacheEvictionSub("session-cache", 30_000)],
397
+ * // run(machine, { subscribe: { cache: cacheEvictionSubscribe } })
398
+ *
399
+ * The engine keys the Sub on its `deps`, so a machine that declares the same
400
+ * name and period keeps one running interval, and a changed period restarts
401
+ * it at the new one.
402
+ */
403
+ declare function cacheEvictionSub(name: string, everyMs: number): DepKeyedSub<unknown, CacheEvictionSub>;
404
+ /**
405
+ * The `cache` runner for the eviction Sub. Hand it to `run`:
406
+ *
407
+ * run(machine, { subscribe: { cache: cacheEvictionSubscribe } })
408
+ *
409
+ * Composed directly on `fromInterval` — it owns the `setInterval` /
410
+ * `clearInterval` lifecycle; this runner only pins the Msg the tick dispatches
411
+ * (`cacheEvictMsg(sub.deps.name)`). The cleanup `fromInterval` returns stops
412
+ * the interval when the Sub stops.
413
+ */
414
+ declare const cacheEvictionSubscribe: SubscribeHandler<CacheEvictionSub, CacheEvictMsg, unknown>;
379
415
 
380
- export { type ArmTimer as A, type CacheEvictionSub as C, type DeadlineExceeded as D, type TtlCache as T, type DeadlineSub as a, deadlineSub as b, type CacheEvictMsg as c, deadlineExceeded as d, type CacheEntry as e, type DeadlineMsgType as f, type DeadlineOpts as g, cacheEvictMsg as h, cacheEvictionSub as i, cacheEvictionSubscribe as j, deadlineMsgType as k, evictExpired as l, get as m, has as n, initCache as o, set as p, setTimeoutArmTimer as q, remove as r, subscribeDeadline as s, subscribeWith as t };
416
+ export { type ArmTimer as A, type CacheEvictionSub as C, type DeadlineExceeded as D, type TtlCache as T, type DeadlineSub as a, type DeadlinesSub as b, deadlineSub as c, deadlineExceeded as d, deadlinesSub as e, type CacheEvictMsg as f, type CacheEntry as g, type CacheEvictionDeps as h, type DeadlineMsgType as i, type DeadlineOpts as j, cacheEvictMsg as k, cacheEvictionSub as l, cacheEvictionSubscribe as m, deadlineMsgType as n, deadlines as o, evictExpired as p, get as q, has as r, subscribeDeadline as s, initCache as t, nextTimer as u, remove as v, set as w, setTimeoutArmTimer as x, subscribeWith as y };