@demlik/tea 0.14.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 (116) hide show
  1. package/README.md +30 -7
  2. package/dist/agent/index.d.ts +49 -36
  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-AQ6ZIMEA.js → chunk-6NG4V5SB.js} +89 -115
  9. package/dist/chunk-6NG4V5SB.js.map +1 -0
  10. package/dist/{chunk-HYDWG35Q.js → chunk-7MGXQZPW.js} +11 -9
  11. package/dist/chunk-7MGXQZPW.js.map +1 -0
  12. package/dist/{chunk-BLJ2JQSH.js → chunk-BCTEOVOU.js} +3 -3
  13. package/dist/{chunk-BLJ2JQSH.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-Z2L3LSNV.js → chunk-EHUDXAKC.js} +246 -210
  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-IVJP3GKJ.js +104 -0
  23. package/dist/chunk-IVJP3GKJ.js.map +1 -0
  24. package/dist/{chunk-F7FMAWLA.js → chunk-MVZS5FDM.js} +13 -6
  25. package/dist/chunk-MVZS5FDM.js.map +1 -0
  26. package/dist/{chunk-LYLTKJWM.js → chunk-MYNLY3HP.js} +9 -8
  27. package/dist/chunk-MYNLY3HP.js.map +1 -0
  28. package/dist/{chunk-I5MQMJDW.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-CTzMJ82M.d.ts → core-BbgtiXdg.d.ts} +231 -139
  35. package/dist/do/index.d.ts +37 -32
  36. package/dist/do/index.js +21 -14
  37. package/dist/do/index.js.map +1 -1
  38. package/dist/{durable-effects-B0-qGQDy.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 -70
  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-DJWIER_3.d.ts → index-7vV8xmsD.d.ts} +1 -1
  51. package/dist/{index-NjrT3fL2.d.ts → index-Bz5Rh9Ga.d.ts} +19 -96
  52. package/dist/index-CjFDUBeq.d.ts +207 -0
  53. package/dist/{index-6A7dB8f6.d.ts → index-CkFYA9Y6.d.ts} +14 -16
  54. package/dist/{index-B5BZ-_cT.d.ts → index-Cou86-GX.d.ts} +189 -108
  55. package/dist/{index-TvRb1KVf.d.ts → index-DU6JpQ8G.d.ts} +1 -1
  56. package/dist/{index-q0hXFeZ0.d.ts → index-Xufl55W8.d.ts} +286 -215
  57. package/dist/{index-4vZzljCD.d.ts → index-_dOR3xqA.d.ts} +57 -45
  58. package/dist/index.d.ts +181 -385
  59. package/dist/index.js +2 -1
  60. package/dist/jev/index.d.ts +147 -99
  61. package/dist/jev/index.js +329 -233
  62. package/dist/jev/index.js.map +1 -1
  63. package/dist/{machine-CdIcN2Xm.d.ts → machine-BQzTV8On.d.ts} +171 -280
  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 -33
  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-ClRZBhHQ.d.ts +53 -0
  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 -455
  91. package/dist/resilience/index.js +37 -483
  92. package/dist/resilience/index.js.map +1 -1
  93. package/dist/{runtime-types-LWDoi943.d.ts → runtime-types-CiVpC0HN.d.ts} +131 -388
  94. package/dist/testing/index.d.ts +165 -15
  95. package/dist/testing/index.js +104 -3
  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-DP3f5NIl.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-AQ6ZIMEA.js.map +0 -1
  104. package/dist/chunk-F2US5BW6.js +0 -72
  105. package/dist/chunk-F2US5BW6.js.map +0 -1
  106. package/dist/chunk-F7FMAWLA.js.map +0 -1
  107. package/dist/chunk-HUKZQX6U.js +0 -97
  108. package/dist/chunk-HUKZQX6U.js.map +0 -1
  109. package/dist/chunk-HYDWG35Q.js.map +0 -1
  110. package/dist/chunk-I5MQMJDW.js.map +0 -1
  111. package/dist/chunk-LYLTKJWM.js.map +0 -1
  112. package/dist/chunk-MYXFNWAJ.js.map +0 -1
  113. package/dist/chunk-RQULHGYH.js +0 -1804
  114. package/dist/chunk-RQULHGYH.js.map +0 -1
  115. package/dist/chunk-Z2L3LSNV.js.map +0 -1
  116. package/dist/index-DMCeMPMJ.d.ts +0 -272
@@ -1,161 +1,21 @@
1
- import { S as Sub, c as SubId } from './core-CTzMJ82M.js';
2
- import { S as SubscribeHandler } from './types-DP3f5NIl.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 = Sub<"deadline"> & {
69
+ type DeadlineSub<N extends string | undefined = undefined> = {
70
+ readonly id: SubId;
71
+ readonly type: "deadline";
212
72
  readonly atMs: number;
213
- } & DeadlineOpts;
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
@@ -221,10 +99,24 @@ type DeadlineSub = Sub<"deadline"> & {
221
99
  * they ask for what they need by name, never positionally, so a shape extension
222
100
  * never rewrites the ~13 `deadlineSub(id, atMs)` call sites.
223
101
  *
224
- * Empty today on purpose — the seam exists before the first extension does, so
225
- * the first field is a one-line additive change, not a 13-site migration.
102
+ * The first field to land here is {@link DeadlineOpts.name} — the knob name the
103
+ * dispatched Msg tag is derived from — and it landed exactly that way: one
104
+ * optional member, zero rewritten call sites.
226
105
  */
227
- type DeadlineOpts = {};
106
+ type DeadlineOpts<N extends string | undefined = undefined> = {
107
+ /**
108
+ * The knob this deadline belongs to. Present → the dispatched Msg's tag is
109
+ * `` `${name}_deadline` ``; absent → the tag stays the bare
110
+ * `"deadline_exceeded"` every machine wired before this field is already
111
+ * handling. `N` is the name as a TYPE as well as a value, so a consumer's
112
+ * cell key and the Msg type are derived from one place and cannot drift.
113
+ *
114
+ * Name a deadline when a machine arms more than one family of them. The Sub
115
+ * *id* was already scoped (`` `${name}:deadline:${key}` ``); this scopes the
116
+ * Msg the Sub dispatches, which is what a reducer cell keys off.
117
+ */
118
+ readonly name?: N;
119
+ };
228
120
  /**
229
121
  * The Msg the deadline dispatches when the wall clock crosses `atMs`. Exported
230
122
  * as a tagged shape so consumers can union it into their machine's Msg type and
@@ -238,25 +130,31 @@ type DeadlineOpts = {};
238
130
  * they prefer — `deadlineExceeded(...)` only fixes the wire shape, not the
239
131
  * consumer's local dialect.
240
132
  */
241
- type DeadlineExceeded = {
242
- readonly type: "deadline_exceeded";
133
+ type DeadlineExceeded<N extends string | undefined = undefined> = {
134
+ readonly type: DeadlineMsgType<N>;
243
135
  readonly id: string;
244
136
  readonly atMs: number;
245
137
  };
246
138
  /**
247
- * Build a deadline Sub literal. Pure data — no clock read, no timer; the timer
248
- * is armed later by the `subscribe` cell. `id` is branded via `subId(...)` so
139
+ * The dispatched Msg's tag, derived from the deadline's optional name. A named
140
+ * deadline speaks `` `${N}_deadline` ``; an unnamed one speaks the bare
141
+ * `"deadline_exceeded"` literal, which is what `DeadlineExceeded` with no type
142
+ * argument still means — so every machine wired before the name existed keeps
143
+ * the exact type it had.
144
+ */
145
+ type DeadlineMsgType<N extends string | undefined> = N extends string ? `${N}_deadline` : "deadline_exceeded";
146
+ /**
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
249
149
  * accidental raw-string drift fails at the type level (invariant 7).
250
150
  *
251
- * Return the result directly from `subscriptions(state)` while the deadline
252
- * should be armed; drop it (return `[]`, or stop returning this id) to disarm —
253
- * the reconcile pass calls the cleanup, clearing the pending timer before it
254
- * fires. That is the "cancel on state exit" lifecycle, identical to
255
- * `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.
256
155
  *
257
- * @param id Stable identity for this deadline. Same `id` across transitions =
258
- * same armed timer (no churn). Use distinct ids for distinct
259
- * 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.
260
158
  * @param atMs Absolute target in epoch milliseconds (e.g. `Date.now() + 900_000`
261
159
  * for a 15-minute guard, or a persisted `expiresAt`).
262
160
  * @param opts Optional additive fields folded onto the Sub literal (see
@@ -265,17 +163,16 @@ type DeadlineExceeded = {
265
163
  * optionality is the future-proofing seam — extending the shape
266
164
  * never breaks an existing `deadlineSub(id, atMs)` call site.
267
165
  */
268
- declare function deadlineSub(id: string, atMs: number, opts?: DeadlineOpts): DeadlineSub;
166
+ declare function deadlineSub<N extends string | undefined = undefined>(id: string, atMs: number, opts?: DeadlineOpts<N>): DeadlineSub<N>;
269
167
  /**
270
168
  * The host-plugged timer backing. Given the deadline's stable `id`, its
271
169
  * absolute `atMs`, and the Msg to fire, arm the platform-appropriate timer
272
170
  * (DO alarm registry, `setTimeout`, fake timer) and return a cleanup that
273
171
  * cancels the pending fire.
274
172
  *
275
- * `id` is the Sub's reconcile id — a registry-backed host (the DO alarm slot)
276
- * keys its entry on `id` so the cleanup deletes the EXACT entry the substrate
277
- * reconciled. A `setTimeout`-backed host ignores `id` (the closure holds the
278
- * 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).
279
176
  *
280
177
  * The host computes the gap itself: `atMs - Date.now()` is the REMAINING time.
281
178
  * On a host that just rehydrated after hibernation that gap is the shrunken
@@ -285,61 +182,235 @@ declare function deadlineSub(id: string, atMs: number, opts?: DeadlineOpts): Dea
285
182
  */
286
183
  type ArmTimer<M> = (id: SubId, atMs: number, msg: M, dispatch: (msg: M) => void) => () => void;
287
184
  /**
288
- * Build the `subscribe["deadline"]` cell from a host-plugged `armTimer`. This
289
- * module owns the Sub shape, the anchor, and the Msg; the HOST owns what backs
290
- * the timer. The returned handler reads `id` + `atMs` off the Sub, builds the
291
- * `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.
292
190
  *
293
191
  * Use it when `setTimeout` is the wrong backing — most concretely a Durable
294
192
  * Object, which hibernates and must register a `do_alarm` instead:
295
193
  *
296
- * subscribe: {
297
- * deadline: subscribeWith((id, atMs, msg, dispatch) =>
298
- * alarms.register(id, atMs, () => dispatch(msg)),
299
- * ),
300
- * }
194
+ * run(machine, {
195
+ * subscribe: {
196
+ * deadline: subscribeWith((id, atMs, msg, dispatch) =>
197
+ * alarms.register(id, atMs, () => dispatch(msg)),
198
+ * ),
199
+ * },
200
+ * })
301
201
  *
302
202
  * For the `setTimeout` default, use {@link subscribeDeadline} — it is exactly
303
203
  * `subscribeWith(setTimeoutArmTimer())`, so there is one deadline surface and
304
204
  * one anchor, not a second one per host.
305
205
  */
306
- declare function subscribeWith(armTimer: ArmTimer<DeadlineExceeded>): SubscribeHandler<DeadlineSub, DeadlineExceeded, 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;
307
207
  /**
308
208
  * The `setTimeout` timer backing — for node / browser / any host whose timer is
309
209
  * a plain `setTimeout`. Arms for the REMAINING time (`max(0, atMs - Date.now())`,
310
210
  * floored at 0 so an already-past deadline fires on the NEXT tick rather than
311
211
  * synchronously inside the reconcile pass), so a deadline re-derived after a
312
- * rehydrate fires at the original instant, not a fresh full window.
313
- *
314
- * Composes on `fromTimeout` rather than redrawing `setTimeout` /
315
- * `clearTimeout`: this package has ONE timer lifecycle, and the arm-timer seam
316
- * plugs into it instead of forking it. A hibernating host does NOT use this —
317
- * it plugs its own `armTimer` (a `do_alarm` registration) into
318
- * {@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}.
319
215
  */
320
- declare function setTimeoutArmTimer(): ArmTimer<DeadlineExceeded>;
216
+ declare function setTimeoutArmTimer<N extends string | undefined = undefined>(): ArmTimer<DeadlineExceeded<N>>;
321
217
  /**
322
- * The `subscribe["deadline"]` handler for the DEFAULT `setTimeout` backing. Arms a
323
- * one-shot timer for the remaining delay `max(0, atMs - Date.now())` and
324
- * dispatches `deadlineExceeded(...)` when it fires; returns a cleanup that
325
- * 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.
326
222
  *
327
223
  * It is `subscribeWith(setTimeoutArmTimer())` — the default backing named, not
328
224
  * a separate implementation, so the host-plugged path and the default path can
329
225
  * never disagree about the anchor.
330
226
  *
331
- * Assign directly to a `Subscribe` handler:
227
+ * run(machine, { subscribe: { deadline: subscribeDeadline } })
332
228
  *
333
- * subscribe: {
334
- * deadline: subscribeDeadline,
335
- * }
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.
336
232
  */
337
- declare const subscribeDeadline: SubscribeHandler<DeadlineSub, DeadlineExceeded, unknown>;
233
+ declare const subscribeDeadline: <N extends string | undefined = undefined>(sub: DeadlinesSub<N>, ctx: unknown, dispatch: (msg: DeadlineExceeded<N>) => void) => Dispose;
338
234
  /**
339
235
  * Construct the Msg the deadline dispatches. Exported so consumers can build /
340
- * 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
341
237
  * constructor rather than two literals that can drift).
342
238
  */
343
- declare function deadlineExceeded(id: string, atMs: number): DeadlineExceeded;
239
+ declare function deadlineExceeded<N extends string | undefined = undefined>(id: string, atMs: number, name?: N): DeadlineExceeded<N>;
240
+ /**
241
+ * The one place the named tag is spelled. `undefined` → the bare
242
+ * `"deadline_exceeded"` literal; a name → `` `${name}_deadline` ``. Exported so
243
+ * a consumer keying a reducer cell off a named deadline derives the key from
244
+ * the same function the Sub dispatches through, instead of re-spelling the
245
+ * template and drifting.
246
+ */
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>;
344
415
 
345
- 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 DeadlineOpts as f, cacheEvictMsg as g, cacheEvictionSub as h, cacheEvictionSubscribe as i, evictExpired as j, get as k, has as l, initCache as m, set as n, setTimeoutArmTimer as o, subscribeWith as p, remove as r, subscribeDeadline as s };
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 };