@demlik/tea 0.14.0 → 0.15.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.
- package/dist/agent/index.d.ts +14 -13
- package/dist/agent/index.js +6 -6
- package/dist/{chunk-F2US5BW6.js → chunk-22POWGFA.js} +17 -11
- package/dist/chunk-22POWGFA.js.map +1 -0
- package/dist/{chunk-HYDWG35Q.js → chunk-3B5X7SBB.js} +4 -4
- package/dist/{chunk-HYDWG35Q.js.map → chunk-3B5X7SBB.js.map} +1 -1
- package/dist/{chunk-RQULHGYH.js → chunk-66PY5WYF.js} +15 -3
- package/dist/{chunk-RQULHGYH.js.map → chunk-66PY5WYF.js.map} +1 -1
- package/dist/{chunk-AQ6ZIMEA.js → chunk-AYS5N6WY.js} +121 -37
- package/dist/chunk-AYS5N6WY.js.map +1 -0
- package/dist/{chunk-HUKZQX6U.js → chunk-L3TND7LA.js} +3 -3
- package/dist/{chunk-HUKZQX6U.js.map → chunk-L3TND7LA.js.map} +1 -1
- package/dist/{chunk-I5MQMJDW.js → chunk-Q5CQTBFA.js} +3 -3
- package/dist/{chunk-I5MQMJDW.js.map → chunk-Q5CQTBFA.js.map} +1 -1
- package/dist/{chunk-F7FMAWLA.js → chunk-QPEVCJ5Q.js} +3 -3
- package/dist/{chunk-F7FMAWLA.js.map → chunk-QPEVCJ5Q.js.map} +1 -1
- package/dist/{chunk-Z2L3LSNV.js → chunk-TCLBCERH.js} +17 -7
- package/dist/chunk-TCLBCERH.js.map +1 -0
- package/dist/{chunk-BLJ2JQSH.js → chunk-V46HSVCK.js} +3 -3
- package/dist/{chunk-BLJ2JQSH.js.map → chunk-V46HSVCK.js.map} +1 -1
- package/dist/{chunk-LYLTKJWM.js → chunk-VTQI2NHP.js} +3 -3
- package/dist/{chunk-LYLTKJWM.js.map → chunk-VTQI2NHP.js.map} +1 -1
- package/dist/{core-CTzMJ82M.d.ts → core-DnzqWy6u.d.ts} +1 -1
- package/dist/do/index.d.ts +13 -12
- package/dist/do/index.js +7 -7
- package/dist/{durable-effects-B0-qGQDy.d.ts → durable-effects-CWzA-o2B.d.ts} +1 -1
- package/dist/extension/index.d.ts +3 -3
- package/dist/extension/index.js +1 -1
- package/dist/flow/index.d.ts +15 -26
- package/dist/flow/index.js +8 -8
- package/dist/idempotency/index.d.ts +3 -3
- package/dist/idempotency/index.js +2 -2
- package/dist/{index-4vZzljCD.d.ts → index-C2DwkjlP.d.ts} +4 -4
- package/dist/{index-B5BZ-_cT.d.ts → index-CA_7I5g9.d.ts} +277 -87
- package/dist/{index-DJWIER_3.d.ts → index-CSyMYDAB.d.ts} +1 -1
- package/dist/{index-6A7dB8f6.d.ts → index-CuNO9m8i.d.ts} +2 -2
- package/dist/{index-TvRb1KVf.d.ts → index-DrIRb_Ch.d.ts} +1 -1
- package/dist/{index-q0hXFeZ0.d.ts → index-HSYgG2Yt.d.ts} +50 -15
- package/dist/{index-NjrT3fL2.d.ts → index-KXqTRJyb.d.ts} +4 -4
- package/dist/{index-DMCeMPMJ.d.ts → index-ZCMhnVKN.d.ts} +1 -1
- package/dist/index.d.ts +66 -6
- package/dist/index.js +1 -1
- package/dist/jev/index.d.ts +34 -22
- package/dist/jev/index.js +178 -126
- package/dist/jev/index.js.map +1 -1
- package/dist/{machine-CdIcN2Xm.d.ts → machine-DGlPWCWI.d.ts} +34 -26
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +1 -1
- package/dist/mem/index.d.ts +2 -2
- package/dist/mem/index.js +1 -1
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +1 -1
- package/dist/paginate/index.d.ts +6 -17
- package/dist/paginate/index.js +4 -4
- package/dist/parity/index.d.ts +3 -3
- package/dist/parity/index.js +2 -2
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +1 -1
- package/dist/persistence/index.d.ts +5 -5
- package/dist/persistence/index.js +3 -3
- package/dist/protocol-D8VbH-VH.d.ts +54 -0
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -1
- package/dist/resilience/index.d.ts +8 -7
- package/dist/resilience/index.js +5 -5
- package/dist/{runtime-types-LWDoi943.d.ts → runtime-types-CMF13BBh.d.ts} +1 -1
- package/dist/testing/index.d.ts +147 -3
- package/dist/testing/index.js +95 -2
- package/dist/testing/index.js.map +1 -1
- package/dist/timing/index.d.ts +4 -4
- package/dist/timing/index.js +3 -3
- package/dist/{types-DP3f5NIl.d.ts → types-BseNPIhr.d.ts} +1 -1
- package/dist/work-queue/index.d.ts +4 -4
- package/package.json +1 -1
- package/dist/chunk-AQ6ZIMEA.js.map +0 -1
- package/dist/chunk-F2US5BW6.js.map +0 -1
- package/dist/chunk-Z2L3LSNV.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/resilience/deadline/index.ts","../src/internal/resilience/cache/index.ts"],"names":[],"mappings":"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","file":"chunk-F2US5BW6.js","sourcesContent":["/**\n * internal/resilience/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock\n * deadline is reached.\n *\n * Generalizes the recurring \"auto-fail a machine at T+N\" / \"15-minute stale\n * guard\" pattern: a Sub whose `subscriptions(state)` returns it only while the\n * deadline should be armed, and which dispatches a single `DeadlineExceeded`\n * Msg when the wall clock crosses `atMs`.\n *\n * Difference from `fromTimeout` (relative — \"fire after N ms\"): a deadline is\n * an ABSOLUTE target. The delay is computed at subscribe time as\n * `max(0, atMs - Date.now())`. The consequence that matters: subscribing LATE —\n * e.g. after a `Store` rehydrate that resumes a machine whose deadline was set\n * before the page reloaded — still fires at the correct absolute moment,\n * because the remaining delay is recomputed from the current clock, not stored\n * in the Sub. A deadline already in the past at subscribe time yields delay `0`,\n * so `setTimeout(fn, 0)` fires on the NEXT tick (never synchronously inside the\n * reconcile pass — the substrate must finish wiring all subs before any Msg\n * lands).\n *\n * Composition over reinvention: the timer lifecycle is `fromTimeout`'s, not\n * redrawn here. `deadlineSub` produces the Sub literal carrying `atMs`; the\n * `subscribe` handler (`subscribeDeadline`) translates `atMs` → a relative delay\n * and delegates the `setTimeout` / `clearTimeout` to `fromTimeout`. The clock\n * read (`Date.now()`) lives only in the subscribe handler — never in a reducer\n * (invariant 2) — and is exercised in tests via vitest fake timers, matching\n * the no-injection convention of the `fromTimeout` / `fromInterval` exemplars.\n *\n * WHAT BACKS THE TIMER IS THE HOST'S CHOICE. `setTimeout` is the universal\n * default, but it is not universal: a Durable Object that hibernates has no\n * live `setTimeout` to wake it, so its deadline must be backed by a `do_alarm`\n * registered on the alarm registry; a test host wants a fake-timer schedule.\n * That backing is the `ArmTimer` seam — `subscribeWith(armTimer)` builds the\n * `subscribe.deadline` handler from a host-plugged timer, and `subscribeDeadline`\n * IS `subscribeWith(setTimeoutArmTimer())`. One deadline surface, three hosts;\n * there is no second Sub type and no second `atMs` anchor to keep in sync.\n *\n * The anchor is what makes the seam safe. `atMs` is ABSOLUTE, so whatever backs\n * it arms to a fixed wall-clock instant: the host computes the gap itself\n * (`atMs - Date.now()`), and on a host that just rehydrated after hibernation\n * that gap is the SHRUNKEN remainder — the deadline honours the original target\n * rather than resetting to a fresh full-length window.\n *\n * Strengthens invariant 4 (external time is a subscription — the deadline is a\n * `Sub` the runtime reconciles, never a `setTimeout` leaked into a reducer) and\n * invariant 7 (identity is explicit — the Sub carries a stable `SubId`, so the\n * reconcile pass leaves it running across transitions instead of churning it).\n */\n\nimport { type Sub, type SubId, subId } from \"../../../index\";\nimport { fromTimeout } from \"../../../subs/from-timeout\";\nimport type { SubscribeHandler } from \"../../../subs/types\";\n\n/**\n * The Sub variant a deadline produces. `atMs` is the absolute target — epoch\n * milliseconds (the `Date.now()` / `Date.parse(...)` scale), NOT a relative\n * delay. It rides on the Sub so the reconcile pass keys identity by `id`\n * alone: the same `id` across transitions means the same armed timer, even if\n * a later `subscriptions(state)` recomputes the literal.\n *\n * Lowercase `\"deadline\"` discriminant per the Sub naming convention (a source\n * noun that doubles as the SubId family); same shape family as the\n * `TimeoutSubData` the `fromTimeout` factory consumes.\n */\nexport type DeadlineSub = Sub<\"deadline\"> & {\n readonly atMs: number;\n} & DeadlineOpts;\n\n/**\n * Additive options the `deadlineSub` factory folds onto the Sub literal. This\n * is the extension seam (invariant 5 — composition by reduction): future fields\n * — `repeatMs` for a re-arming deadline, `jitterMs` for spread, etc. — land HERE\n * as optional members, and every existing call site keeps compiling because the\n * factory's third parameter is optional and the fields are `readonly` optionals\n * on `DeadlineSub`. Callers are insulated from the concrete `DeadlineSub` shape:\n * they ask for what they need by name, never positionally, so a shape extension\n * never rewrites the ~13 `deadlineSub(id, atMs)` call sites.\n *\n * Empty today on purpose — the seam exists before the first extension does, so\n * the first field is a one-line additive change, not a 13-site migration.\n */\n// biome-ignore lint/complexity/noBannedTypes: intentional empty extension seam — fields are added additively (see doc above).\nexport type DeadlineOpts = {};\n\n/**\n * The Msg the deadline dispatches when the wall clock crosses `atMs`. Exported\n * as a tagged shape so consumers can union it into their machine's Msg type and\n * handle it in a reducer cell. `id` lets a reducer disambiguate WHICH deadline\n * fired when a machine arms several at once; `atMs` echoes the target the timer\n * was scheduled against (useful for assertions / observability).\n *\n * Snake-shaped `\"deadline_exceeded\"` matches the tagged-Msg style this module's\n * orchestrator requested; the actor-in-name PascalCase convention\n * (`TimerFiredDeadline`) is the consumer's to apply at their own boundary if\n * they prefer — `deadlineExceeded(...)` only fixes the wire shape, not the\n * consumer's local dialect.\n */\nexport type DeadlineExceeded = {\n readonly type: \"deadline_exceeded\";\n readonly id: string;\n readonly atMs: number;\n};\n\n/**\n * Build a deadline Sub literal. Pure data — no clock read, no timer; the timer\n * is armed later by the `subscribe` cell. `id` is branded via `subId(...)` so\n * accidental raw-string drift fails at the type level (invariant 7).\n *\n * Return the result directly from `subscriptions(state)` while the deadline\n * should be armed; drop it (return `[]`, or stop returning this id) to disarm —\n * the reconcile pass calls the cleanup, clearing the pending timer before it\n * fires. That is the \"cancel on state exit\" lifecycle, identical to\n * `fromTimeout`'s.\n *\n * @param id Stable identity for this deadline. Same `id` across transitions =\n * same armed timer (no churn). Use distinct ids for distinct\n * deadlines on the same machine.\n * @param atMs Absolute target in epoch milliseconds (e.g. `Date.now() + 900_000`\n * for a 15-minute guard, or a persisted `expiresAt`).\n * @param opts Optional additive fields folded onto the Sub literal (see\n * {@link DeadlineOpts}). Omit it for the common case; pass it when a\n * future field (e.g. `repeatMs`) needs to ride on the deadline. The\n * optionality is the future-proofing seam — extending the shape\n * never breaks an existing `deadlineSub(id, atMs)` call site.\n */\nexport function deadlineSub(\n id: string,\n atMs: number,\n opts?: DeadlineOpts,\n): DeadlineSub {\n return { ...opts, id: subId(id), type: \"deadline\", atMs };\n}\n\n/**\n * The host-plugged timer backing. Given the deadline's stable `id`, its\n * absolute `atMs`, and the Msg to fire, arm the platform-appropriate timer\n * (DO alarm registry, `setTimeout`, fake timer) and return a cleanup that\n * cancels the pending fire.\n *\n * `id` is the Sub's reconcile id — a registry-backed host (the DO alarm slot)\n * keys its entry on `id` so the cleanup deletes the EXACT entry the substrate\n * reconciled. A `setTimeout`-backed host ignores `id` (the closure holds the\n * handle).\n *\n * The host computes the gap itself: `atMs - Date.now()` is the REMAINING time.\n * On a host that just rehydrated after hibernation that gap is the shrunken\n * remainder — the deadline is NOT reset to its full length. That recomputation\n * is the reason `atMs` rides on the Sub as an absolute instant rather than a\n * delay: the anchor is what every backing agrees on.\n */\nexport type ArmTimer<M> = (\n id: SubId,\n atMs: number,\n msg: M,\n dispatch: (msg: M) => void,\n) => () => void;\n\n/**\n * Build the `subscribe[\"deadline\"]` cell from a host-plugged `armTimer`. This\n * module owns the Sub shape, the anchor, and the Msg; the HOST owns what backs\n * the timer. The returned handler reads `id` + `atMs` off the Sub, builds the\n * `deadlineExceeded(...)` Msg, and hands all three to `armTimer`.\n *\n * Use it when `setTimeout` is the wrong backing — most concretely a Durable\n * Object, which hibernates and must register a `do_alarm` instead:\n *\n * subscribe: {\n * deadline: subscribeWith((id, atMs, msg, dispatch) =>\n * alarms.register(id, atMs, () => dispatch(msg)),\n * ),\n * }\n *\n * For the `setTimeout` default, use {@link subscribeDeadline} — it is exactly\n * `subscribeWith(setTimeoutArmTimer())`, so there is one deadline surface and\n * one anchor, not a second one per host.\n */\nexport function subscribeWith(\n armTimer: ArmTimer<DeadlineExceeded>,\n): SubscribeHandler<DeadlineSub, DeadlineExceeded, unknown> {\n return (sub, _ctx, dispatch) =>\n armTimer(sub.id, sub.atMs, deadlineExceeded(sub.id, sub.atMs), dispatch);\n}\n\n/**\n * The `setTimeout` timer backing — for node / browser / any host whose timer is\n * a plain `setTimeout`. Arms for the REMAINING time (`max(0, atMs - Date.now())`,\n * floored at 0 so an already-past deadline fires on the NEXT tick rather than\n * synchronously inside the reconcile pass), so a deadline re-derived after a\n * rehydrate fires at the original instant, not a fresh full window.\n *\n * Composes on `fromTimeout` rather than redrawing `setTimeout` /\n * `clearTimeout`: this package has ONE timer lifecycle, and the arm-timer seam\n * plugs into it instead of forking it. A hibernating host does NOT use this —\n * it plugs its own `armTimer` (a `do_alarm` registration) into\n * {@link subscribeWith}.\n */\nexport function setTimeoutArmTimer(): ArmTimer<DeadlineExceeded> {\n return (id, atMs, msg, dispatch) => {\n // Recompute the remaining delay from the CURRENT clock so a late subscribe\n // (post-rehydrate) still targets the correct absolute moment.\n const delayMs = Math.max(0, atMs - Date.now());\n return fromTimeout<Sub<\"deadline\"> & { delayMs: number }, DeadlineExceeded>(\n () => msg,\n )({ id, type: \"deadline\", delayMs }, undefined, dispatch);\n };\n}\n\n/**\n * The `subscribe[\"deadline\"]` handler for the DEFAULT `setTimeout` backing. Arms a\n * one-shot timer for the remaining delay `max(0, atMs - Date.now())` and\n * dispatches `deadlineExceeded(...)` when it fires; returns a cleanup that\n * clears the pending timer.\n *\n * It is `subscribeWith(setTimeoutArmTimer())` — the default backing named, not\n * a separate implementation, so the host-plugged path and the default path can\n * never disagree about the anchor.\n *\n * Assign directly to a `Subscribe` handler:\n *\n * subscribe: {\n * deadline: subscribeDeadline,\n * }\n */\nexport const subscribeDeadline: SubscribeHandler<\n DeadlineSub,\n DeadlineExceeded,\n unknown\n> = subscribeWith(setTimeoutArmTimer());\n\n/**\n * Construct the Msg the deadline dispatches. Exported so consumers can build /\n * assert the same shape (and so the subscribe cell and tests share one\n * constructor rather than two literals that can drift).\n */\nexport function deadlineExceeded(id: string, atMs: number): DeadlineExceeded {\n return { type: \"deadline_exceeded\", id, atMs };\n}\n","/**\n * internal/resilience/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.\n *\n * Same pure-state-ops shape as `../../idempotency/` and\n * `../../work-queue/ops.ts`: a state type plus pure transition functions. Host-\n * agnostic by construction — no `Date.now()`, no timers, no I/O inside the ops.\n * The caller injects the clock (`nowMs`) and folds the result through its own\n * reducer, keeping the store inside TEA's invariant 2 (transitions are pure:\n * same inputs → same outputs) so a consumer can exercise it with `replay`\n * without a fake clock.\n *\n * Every op returns a NEW cache value and never mutates its input (invariant 1 —\n * state is a value, not a place). `get` / `has` are read-shaped; `set` /\n * `remove` / `evictExpired` return a new cache.\n *\n * **Distinct from `/idempotency`.** Both are TTL-bounded key maps, but they\n * answer different questions and so carry different shapes:\n *\n * - `/idempotency` is DEDUPE-focused — \"have I already processed this key?\"\n * TTL is a single store-level policy (`ttlMs`), each entry stores the\n * `atMs` it was remembered at, and expiry is derived (`now - atMs >=\n * ttlMs`). It carries an `order` ledger + `capacity` because the use case\n * (webhook receivers, retried messages) needs bounded LRU eviction.\n * - `/cache` is RETRIEVAL-focused — \"what is the value for this key right\n * now?\" TTL is set PER ENTRY at write time: `set(key, value, nowMs,\n * ttlMs)` bakes the absolute `expiresAtMs = nowMs + ttlMs` into the entry.\n * Different keys can carry different lifetimes (a hot key cached for 1s, a\n * cold one for 1h) — the dimension idempotency's single store-level\n * `ttlMs` cannot express. No `order`, no `capacity`: a cache bounds itself\n * by TTL + periodic eviction, not by LRU count.\n *\n * The eviction Sub is the \"eviction is a Msg\" half: a `setInterval`-shaped Sub\n * (composed directly on `fromInterval`, from `src/subs/`) that periodically\n * dispatches a `cache_evict` Msg the consumer's reducer handles by calling\n * `evictExpired`. Time still never enters the reducer — the Sub fires the Msg,\n * the reducer reads `nowMs` off the Msg (the consumer stamps it at the Sub\n * boundary), and `evictExpired` does the pure drop.\n *\n * Internal to the package (`src/internal/resilience/`), not a published\n * subpath (#46); same one-shape-per-module rule as `work-queue`, `idempotency`,\n * and `rate-limit`.\n */\n\nimport type { Sub } from \"../../../index\";\nimport { fromInterval } from \"../../../subs\";\nimport type { SubscribeHandler } from \"../../../subs/types\";\n\n/**\n * A cached entry: the `value` and the absolute clock reading `expiresAtMs` it\n * expires at. Unlike `/idempotency`'s entry (which stores `atMs` and derives\n * expiry from a store-level `ttlMs`), the expiry instant is baked in at write\n * time — so per-entry TTLs are a first-class capability. Both fields are plain\n * data so the whole cache is trivially JSON-serializable into a consumer's\n * `Store<S>`.\n */\nexport interface CacheEntry<V> {\n readonly value: V;\n readonly expiresAtMs: number;\n}\n\n/**\n * The cache: a map of `key → entry`. No `order` ledger and no `capacity` (cf.\n * `/idempotency`) — a TTL cache bounds itself by expiry + periodic eviction,\n * not by LRU count. Each entry carries its own `expiresAtMs`, so the cache\n * needs no store-level TTL policy field.\n */\nexport interface TtlCache<V> {\n readonly entries: Readonly<Record<string, CacheEntry<V>>>;\n}\n\n/** Create an empty cache. */\nexport function initCache<V>(): TtlCache<V> {\n return { entries: {} };\n}\n\n/**\n * Whether `entry` is expired at `nowMs`. Half-open: an entry whose\n * `expiresAtMs` exactly equals `nowMs` is expired (`nowMs >= expiresAtMs`),\n * matching `/idempotency` and `/rate-limit`'s window-edge cutoff — a value\n * cached at `t` with a `ttlMs` lifetime is gone at exactly `t + ttlMs`.\n */\nfunction isExpired<V>(entry: CacheEntry<V>, nowMs: number): boolean {\n return nowMs >= entry.expiresAtMs;\n}\n\n/**\n * The cached `value` for `key` iff present AND unexpired at `nowMs`, else\n * `undefined`. An expired entry reads as absent even though it is physically\n * still in `entries` until the next `evictExpired` (or an overwriting `set`)\n * drops it. PURE — reads only, never mutates.\n */\nexport function get<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): V | undefined {\n const entry = cache.entries[key];\n if (entry === undefined || isExpired(entry, nowMs)) return undefined;\n return entry.value;\n}\n\n/**\n * True iff `key` is present AND unexpired at `nowMs`. The boolean sibling of\n * `get` for callers that only need presence, not the value. PURE.\n */\nexport function has<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): boolean {\n const entry = cache.entries[key];\n if (entry === undefined) return false;\n return !isExpired(entry, nowMs);\n}\n\n/**\n * Write `key → value` with an absolute expiry of `nowMs + ttlMs`. Re-`set`ting\n * an existing key overwrites both the value and the expiry — the TTL clock\n * restarts from the new `nowMs`. PURE — returns a new cache; the input is\n * untouched (a fresh shallow copy of `entries`).\n */\nexport function set<V>(\n cache: TtlCache<V>,\n key: string,\n value: V,\n nowMs: number,\n ttlMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n nextEntries[key] = { value, expiresAtMs: nowMs + ttlMs };\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Drop `key` regardless of expiry. PURE — returns a new cache; the input is\n * untouched. Removing an absent key returns an equivalent cache (the shallow\n * copy still happens, but no entry is dropped).\n */\nexport function remove<V>(cache: TtlCache<V>, key: string): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n delete nextEntries[key];\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Physically drop every entry expired at `nowMs` (`nowMs >= expiresAtMs`).\n * Returns a NEW cache with the survivors. This is the pure work the eviction\n * Sub's `cache_evict` Msg triggers — `get` / `has` already treat expired\n * entries as absent, so eviction is purely a memory-reclaim pass, not a\n * correctness one. PURE — the input cache is never mutated.\n */\nexport function evictExpired<V>(\n cache: TtlCache<V>,\n nowMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = {};\n for (const [key, entry] of Object.entries(cache.entries)) {\n if (isExpired(entry, nowMs)) continue;\n nextEntries[key] = entry;\n }\n return { ...cache, entries: nextEntries };\n}\n\n// ---------------------------------------------------------------------------\n// Eviction Sub — \"eviction is a Msg\".\n//\n// A periodic timer Sub that dispatches `cache_evict` so the consumer's reducer\n// can call `evictExpired`. It is `fromInterval` specialized to a fixed Msg\n// shape — the factory carries the interval ON the Sub (per the substrate's\n// reconcile-by-id contract) and dispatches a `cache_evict` Msg tagged with the\n// Sub's own `id`, so a consumer running several caches can route each tick to\n// the right cache by `msg.id`.\n//\n// Mirrors `fromInterval` exactly (factory + SubscribeHandler cell + exported\n// Msg constructor); it does NOT reimplement the timer lifecycle — it composes\n// directly on `fromInterval`, which owns `setInterval` / `clearInterval`. That\n// keeps the timer-handle bookkeeping (and its cleanup) in one place and this\n// module purely declarative.\n//\n// Strengthens invariant 4 (external time is a Sub, reconciled by id) and\n// invariant 9 (named, small Sub surface composed on the shared factory).\n// ---------------------------------------------------------------------------\n\n/**\n * The Msg the eviction Sub dispatches each tick. `id` is the Sub's own id so a\n * consumer with multiple caches can route the tick to the matching cache. The\n * reducer reads `Date.now()`-free time by stamping `nowMs` itself at the Sub\n * boundary if needed; the canonical reducer cell is\n * `cache_evict: (s) => [{ ...s, cache: evictExpired(s.cache, nowMs) }, []]`.\n */\nexport interface CacheEvictMsg {\n readonly type: \"cache_evict\";\n readonly id: string;\n}\n\n/** Construct a `cache_evict` Msg for the cache identified by `id`. */\nexport function cacheEvictMsg(id: string): CacheEvictMsg {\n return { type: \"cache_evict\", id };\n}\n\n/**\n * The Sub shape the eviction factory installs: a `setInterval`-shaped Sub\n * carrying its tick period (`intervalMs`) per the substrate's\n * reconcile-by-id-with-data-on-the-Sub contract. `type: \"cache\"` is the\n * lowercase source-noun Sub convention (canon §2.5 / naming-style).\n */\nexport type CacheEvictionSub = Sub<\"cache\"> & { readonly intervalMs: number };\n\n/**\n * Declare an eviction Sub for the cache identified by `id`, ticking every\n * `everyMs`. Drop the result into `subscriptions(state)`:\n *\n * subscriptions: (s) => [cacheEvictionSub(\"session-cache\", 30_000)]\n *\n * Same id ⇒ same running interval across transitions (no reconcile churn);\n * changing `everyMs` alone will NOT re-arm the timer (the substrate leaves a\n * same-id Sub running) — change the `id` too if the period must change. This\n * mirrors `fromInterval`'s data-on-the-Sub tradeoff exactly.\n */\nexport function cacheEvictionSub(\n id: string,\n everyMs: number,\n): CacheEvictionSub {\n return {\n id: id as CacheEvictionSub[\"id\"],\n type: \"cache\",\n intervalMs: everyMs,\n };\n}\n\n/**\n * The `subscribe` cell for the eviction Sub. Assign it to\n * `machine.subscribe.cache`:\n *\n * subscribe: { cache: cacheEvictionSubscribe }\n *\n * Composed directly on `fromInterval` — it owns the `setInterval` /\n * `clearInterval` lifecycle; this cell only pins the Msg the tick dispatches\n * (`cacheEvictMsg(sub.id)`). The cleanup `fromInterval` returns stops the\n * interval when the Sub is reconciled out (the cache's state exits the phase\n * that declared it).\n */\nexport const cacheEvictionSubscribe: SubscribeHandler<\n CacheEvictionSub,\n CacheEvictMsg,\n unknown\n> = fromInterval((sub) => cacheEvictMsg(sub.id));\n"]}
|