@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.
- package/README.md +30 -7
- package/dist/agent/index.d.ts +49 -36
- package/dist/agent/index.js +12 -8
- package/dist/chunk-4LPY67E4.js +18 -0
- package/dist/chunk-4LPY67E4.js.map +1 -0
- package/dist/chunk-5XU6ZJO2.js +533 -0
- package/dist/chunk-5XU6ZJO2.js.map +1 -0
- package/dist/{chunk-AQ6ZIMEA.js → chunk-6NG4V5SB.js} +89 -115
- package/dist/chunk-6NG4V5SB.js.map +1 -0
- package/dist/{chunk-HYDWG35Q.js → chunk-7MGXQZPW.js} +11 -9
- package/dist/chunk-7MGXQZPW.js.map +1 -0
- package/dist/{chunk-BLJ2JQSH.js → chunk-BCTEOVOU.js} +3 -3
- package/dist/{chunk-BLJ2JQSH.js.map → chunk-BCTEOVOU.js.map} +1 -1
- package/dist/chunk-CH5KTOEZ.js +710 -0
- package/dist/chunk-CH5KTOEZ.js.map +1 -0
- package/dist/chunk-CIHSHLQX.js +64 -0
- package/dist/chunk-CIHSHLQX.js.map +1 -0
- package/dist/{chunk-Z2L3LSNV.js → chunk-EHUDXAKC.js} +246 -210
- package/dist/chunk-EHUDXAKC.js.map +1 -0
- package/dist/chunk-GOX2CYIM.js +380 -0
- package/dist/chunk-GOX2CYIM.js.map +1 -0
- package/dist/chunk-IVJP3GKJ.js +104 -0
- package/dist/chunk-IVJP3GKJ.js.map +1 -0
- package/dist/{chunk-F7FMAWLA.js → chunk-MVZS5FDM.js} +13 -6
- package/dist/chunk-MVZS5FDM.js.map +1 -0
- package/dist/{chunk-LYLTKJWM.js → chunk-MYNLY3HP.js} +9 -8
- package/dist/chunk-MYNLY3HP.js.map +1 -0
- package/dist/{chunk-I5MQMJDW.js → chunk-NSIAORRY.js} +20 -30
- package/dist/chunk-NSIAORRY.js.map +1 -0
- package/dist/{chunk-MYXFNWAJ.js → chunk-QNGYQMZB.js} +4 -4
- package/dist/chunk-QNGYQMZB.js.map +1 -0
- package/dist/chunk-WT5PBPLB.js +98 -0
- package/dist/chunk-WT5PBPLB.js.map +1 -0
- package/dist/{core-CTzMJ82M.d.ts → core-BbgtiXdg.d.ts} +231 -139
- package/dist/do/index.d.ts +37 -32
- package/dist/do/index.js +21 -14
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-B0-qGQDy.d.ts → durable-effects-7MTK_jnj.d.ts} +1 -1
- package/dist/effect/index.d.ts +151 -0
- package/dist/effect/index.js +98 -0
- package/dist/effect/index.js.map +1 -0
- package/dist/extension/index.d.ts +6 -7
- package/dist/extension/index.js +3 -2
- package/dist/extension/index.js.map +1 -1
- package/dist/flow/index.d.ts +68 -70
- package/dist/flow/index.js +32 -27
- package/dist/flow/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +6 -7
- package/dist/idempotency/index.js +4 -2
- package/dist/{index-DJWIER_3.d.ts → index-7vV8xmsD.d.ts} +1 -1
- package/dist/{index-NjrT3fL2.d.ts → index-Bz5Rh9Ga.d.ts} +19 -96
- package/dist/index-CjFDUBeq.d.ts +207 -0
- package/dist/{index-6A7dB8f6.d.ts → index-CkFYA9Y6.d.ts} +14 -16
- package/dist/{index-B5BZ-_cT.d.ts → index-Cou86-GX.d.ts} +189 -108
- package/dist/{index-TvRb1KVf.d.ts → index-DU6JpQ8G.d.ts} +1 -1
- package/dist/{index-q0hXFeZ0.d.ts → index-Xufl55W8.d.ts} +286 -215
- package/dist/{index-4vZzljCD.d.ts → index-_dOR3xqA.d.ts} +57 -45
- package/dist/index.d.ts +181 -385
- package/dist/index.js +2 -1
- package/dist/jev/index.d.ts +147 -99
- package/dist/jev/index.js +329 -233
- package/dist/jev/index.js.map +1 -1
- package/dist/{machine-CdIcN2Xm.d.ts → machine-BQzTV8On.d.ts} +171 -280
- package/dist/machine-viz/index.d.ts +5 -5
- package/dist/machine-viz/index.js +8 -4
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +3 -4
- package/dist/mem/index.js +2 -1
- package/dist/mem/index.js.map +1 -1
- package/dist/node/index.d.ts +119 -78
- package/dist/node/index.js +52 -38
- package/dist/node/index.js.map +1 -1
- package/dist/paginate/index.d.ts +27 -33
- package/dist/paginate/index.js +6 -4
- package/dist/parity/index.d.ts +4 -5
- package/dist/parity/index.js +3 -2
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/index.d.ts +2 -2
- package/dist/pbt/index.js +2 -1
- package/dist/pbt/index.js.map +1 -1
- package/dist/persistence/index.d.ts +6 -7
- package/dist/persistence/index.js +5 -3
- package/dist/promise/index.d.ts +186 -0
- package/dist/promise/index.js +5 -0
- package/dist/promise/index.js.map +1 -0
- package/dist/protocol-ClRZBhHQ.d.ts +53 -0
- package/dist/react/index.d.ts +36 -17
- package/dist/react/index.js +27 -4
- package/dist/react/index.js.map +1 -1
- package/dist/resilience/index.d.ts +63 -455
- package/dist/resilience/index.js +37 -483
- package/dist/resilience/index.js.map +1 -1
- package/dist/{runtime-types-LWDoi943.d.ts → runtime-types-CiVpC0HN.d.ts} +131 -388
- package/dist/testing/index.d.ts +165 -15
- package/dist/testing/index.js +104 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/timing/index.d.ts +25 -22
- package/dist/timing/index.js +4 -3
- package/dist/timing/index.js.map +1 -1
- package/dist/{types-DP3f5NIl.d.ts → types-LxZxGYda.d.ts} +2 -2
- package/dist/work-queue/index.d.ts +5 -6
- package/package.json +18 -5
- package/dist/chunk-AQ6ZIMEA.js.map +0 -1
- package/dist/chunk-F2US5BW6.js +0 -72
- package/dist/chunk-F2US5BW6.js.map +0 -1
- package/dist/chunk-F7FMAWLA.js.map +0 -1
- package/dist/chunk-HUKZQX6U.js +0 -97
- package/dist/chunk-HUKZQX6U.js.map +0 -1
- package/dist/chunk-HYDWG35Q.js.map +0 -1
- package/dist/chunk-I5MQMJDW.js.map +0 -1
- package/dist/chunk-LYLTKJWM.js.map +0 -1
- package/dist/chunk-MYXFNWAJ.js.map +0 -1
- package/dist/chunk-RQULHGYH.js +0 -1804
- package/dist/chunk-RQULHGYH.js.map +0 -1
- package/dist/chunk-Z2L3LSNV.js.map +0 -1
- package/dist/index-DMCeMPMJ.d.ts +0 -272
|
@@ -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"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/flow/batch-window/index.ts"],"names":[],"mappings":";;;AA2IO,SAAS,eAAA,GAAqC;AACnD,EAAA,OAAO,EAAE,MAAA,EAAQ,EAAC,EAAG,UAAU,IAAA,EAAK;AACtC;AAqBO,SAAS,kBAAA,CACd,IACA,IAAA,EACoB;AACpB,EAAA,OAAO,gBAAA,CAAiB,IAAI,IAAI,CAAA;AAClC;AAkEO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,IAAA,EACA,EAAA,EACyC;AACzC,EAAA,MAAM,MAAA,GAAS,CAAC,GAAG,KAAA,CAAM,QAAQ,IAAI,CAAA;AAGrC,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,EAAA;AAInC,EAAA,IAAI,OAAO,MAAA,IAAU,IAAA,CAAK,IAAI,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAA,EAAG;AACjD,IAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,CAAC,EAAE,MAAA,EAAQ,QAAA,EAAS,EAAG,EAAE,CAAA;AAClC;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,GAAA,EACyC;AAGzC,EAAA,IAAI,KAAA,CAAM,OAAO,MAAA,KAAW,CAAA,SAAU,CAAC,KAAA,EAAO,EAAE,CAAA;AAChD,EAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAC5D;AAqBO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACsB;AAK3B,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,EAAC;AACrC,EAAA,OAAO,CAAC,WAAA,CAAY,EAAA,EAAI,MAAM,QAAA,GAAW,MAAA,CAAO,KAAK,CAAC,CAAA;AACxD;AAQO,IAAM,oBAAA,GAIT;AAYG,SAAS,kBACd,MAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,MAAM,eAAA,EAAmB;AAAA,IAC/B,GAAA,EAAK,CAAC,KAAA,EAAO,IAAA,EAAM,OAAO,OAAA,CAAQ,MAAA,EAAQ,KAAA,EAAO,IAAA,EAAM,EAAE,CAAA;AAAA,IACzD,UAAU,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,KAAS,CAAA;AAAA,IACnD,MAAM,CAAC,KAAA,EAAO,OAAO,OAAA,CAAQ,MAAA,EAAQ,OAAO,EAAE,CAAA;AAAA,IAC9C,QAAA,EAAU,OAAO,EAAE,QAAA,EAAU,oBAAA,EAAqB;AAAA,GACpD;AACF","file":"chunk-F7FMAWLA.js","sourcesContent":["/**\n * internal/flow/batch-window — coalesce a stream of items into size- or\n * time-bounded BATCHES, then flush each batch as a single Cmd.\n *\n * What this module actually DELEGATES to, vs. what it merely RE-EXPORTS:\n *\n * - `../deadline` — REAL delegation (the only one). The window timer IS a\n * `deadline` Sub: `subsFor` returns `deadlineSub` keyed on the window's\n * identity, the subscribe cell is `subscribeDeadline` verbatim, and the\n * expiry Msg is `deadlineExceeded`. Nothing about the timer is redrawn here.\n * \"Fire at an absolute instant, even across a rehydrate\" is exactly\n * `deadline`'s absolute-target lifecycle: a late subscribe (post-`Store`\n * rehydrate) recomputes the remaining delay from the current clock and still\n * fires at the correct moment.\n * - `../work-queue` — TYPE re-export only, NOT a sink this module calls. A\n * flush emits a Cmd; the CONSUMER's interpret handler is what enqueues onto a\n * `createQueue(store)`. This module never touches queue status — it has no\n * queue state and no status flip to delegate. It re-exports `EnqueueInput<I>`\n * purely so the consumer's flush Cmd and queue agree on the item shape; any\n * actual status flip belongs in the consumer and is delegated to\n * `../work-queue/ops` there, not here.\n * - `../debounce` — VALUE/TYPE re-export only, NOT wired into `update`. A\n * bursty source (keystrokes, scroll, websocket frames) can be `debounce`d at\n * the host BEFORE it ever becomes an `add` Msg, so the batch window only sees\n * settled items. `debounce` is a sink transformer OUTSIDE `update`;\n * batch-window is the durable, replayable half inside it. Re-exported for\n * one-import ergonomics — this module does not call it.\n *\n * Why this is a knob and not hand-wired logic: the managed state (the buffer +\n * the window-open instant) lives in the Model, so it survives DO eviction /\n * reload (durable) and every transition is still a Msg through `update`\n * (replayable). A closure holding a `setTimeout` and a mutable array would break\n * both — banned. `add` / `onWindow` are PURE: time arrives as an `at`\n * parameter, never `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const bw = createBatchWindow<LogLine, Msg>({\n * maxItems: 100,\n * maxMs: 5_000,\n * flush: (items) => ({ type: \"FlushBatch\", items }),\n * });\n *\n * // Model: { batch: BatchWindow<LogLine>, ... }\n * // init: (loaded) => [loaded ?? { batch: bw.init(), ... }, []]\n * // update cells:\n * // LogArrived: (s, m) => mapBatch(s, bw.add(s.batch, m.line, m.at))\n * // WindowFired: (s, m) => mapBatch(s, bw.onWindow(s.batch, m.atMs))\n * // subscriptions: (s) => bw.subs(s.batch)\n * // subscribe: { ...bw.handlers() }\n *\n * where `mapBatch(s, [batch, cmds]) => [{ ...s, batch }, cmds]` lifts the slice\n * transition into the consumer's Model. The `FlushBatch` Cmd's interpret handler\n * is the consumer's — it is where the batch meets I/O (an `enqueue`, a network\n * POST, an R2 write).\n *\n * Internal since #48 — not published on any subpath; reached from inside the\n * package as `internal/flow/batch-window`. Same one-shape-per-package rule as `deadline`, `cache`, and\n * `work-queue`.\n */\n\nimport type { Cmd } from \"../../../index\";\n\n// `../debounce` re-export (value + type) for one-import ergonomics — see file\n// header. This module does NOT call debounce; a consumer that wants to debounce\n// a bursty source BEFORE it becomes an `add` Msg reaches it from the same import\n// as the batch window.\nexport { type Debounced, debounce } from \"../../timing/debounce\";\n\nimport type { SubscribeHandler } from \"../../../subs/types\";\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineExceeded,\n deadlineSub,\n subscribeDeadline,\n} from \"../../resilience/deadline\";\n\n// `../work-queue` TYPE re-export only (see file header). This module emits a\n// flush Cmd; the CONSUMER's interpret handler enqueues onto a\n// `createQueue(store)`. Re-export the caller-facing enqueue payload type so the\n// consumer's flush Cmd and queue agree on the item shape.\nexport type { EnqueueInput } from \"../../work-queue\";\n\n/**\n * Configuration for a batch window — the knob. Pure config, no mutable state\n * (the `BatchWindow<I>` slice carries that). One config object is shared across\n * every window of one logical stream (often a module-scope const).\n *\n * `flush` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps the\n * buffered items to the single Cmd the runtime performs when a window closes.\n * Keeping it a `(items) => Cmd` (not a `(items) => void`) is what keeps the\n * trigger replayable — the flush is a Cmd in the transition's output, never a\n * side effect inside the verb.\n */\nexport interface BatchWindowConfig<I, C extends Cmd> {\n /**\n * Size trigger. A batch flushes the instant its buffer reaches `maxItems`,\n * WITHOUT waiting for the time window — the classic \"send as soon as the\n * page is full\" cutoff. Must be `>= 1`; a config of `maxItems: 0` would flush\n * an empty batch on every `add`, which is never useful, so callers treat `1`\n * as the floor (the verb still behaves: with `maxItems <= 1` every item\n * flushes on arrival, a degenerate but legal \"no batching\" window).\n */\n readonly maxItems: number;\n /**\n * Time trigger, in milliseconds. A batch flushes no later than `openedAt +\n * maxMs` — `openedAt` being the `at` of the item that opened the window. This\n * is the latency ceiling: an item never waits longer than `maxMs` to ship,\n * even if the buffer never reaches `maxItems`.\n */\n readonly maxMs: number;\n /**\n * Map a flushed batch to the single Cmd the runtime performs. Called with the\n * buffered items in arrival order; the returned Cmd is what `add` / `onWindow`\n * emit when a window closes. PURE constructor — no I/O, no clock; the I/O\n * happens later in the consumer's interpret handler for the returned Cmd.\n */\n readonly flush: (items: readonly I[]) => C;\n}\n\n/**\n * The Model slice a batch window owns. `buffer` is the items accumulated for the\n * current open window; `openedAt` is the `at` of the item that OPENED it (the\n * anchor the time trigger measures against), or `null` when the window is\n * closed (buffer empty). The pair is the single source of truth for \"is a\n * window open, and when must it flush by?\" — both plain data so the whole slice\n * is trivially JSON-serializable into a consumer's `Store<S>`.\n *\n * Invariant the verbs preserve: `buffer.length === 0` IFF `openedAt === null`.\n * A non-empty buffer always has an `openedAt`; an empty buffer never does. The\n * window timer Sub keys its arm/disarm off exactly this invariant.\n */\nexport interface BatchWindow<I> {\n readonly buffer: readonly I[];\n readonly openedAt: number | null;\n}\n\n/** The starting slice: closed window, empty buffer. */\nexport function initBatchWindow<I>(): BatchWindow<I> {\n return { buffer: [], openedAt: null };\n}\n\n/**\n * The Sub a batch window's open timer produces: it IS `../deadline`'s\n * `DeadlineSub`, not a re-tagged copy. `atMs` is the absolute flush-by instant\n * (`openedAt + maxMs`). Aliasing the deadline type (rather than redeclaring an\n * identical shape) is what lets `subsFor` return `deadlineSub(...)` with no\n * cast — the value's static type already satisfies this name. Consumers running\n * several windows route each one by the Sub's `id`, not by a distinct type.\n */\nexport type BatchWindowSub = DeadlineSub;\n\n/** The Msg a closed-by-time window dispatches. Alias of `deadline`'s Msg. */\nexport type BatchWindowExpired = DeadlineExceeded;\n\n/**\n * Construct the window-expired Msg for the window identified by `id`, flushing\n * by `atMs`. Re-exported alias of `deadlineExceeded` so the subscribe cell and\n * the consumer's reducer share one constructor rather than two literals that\n * can drift. The consumer handles it in a reducer cell by calling `onWindow`.\n */\nexport function batchWindowExpired(\n id: string,\n atMs: number,\n): BatchWindowExpired {\n return deadlineExceeded(id, atMs);\n}\n\n/**\n * The bound knob returned by `createBatchWindow`. `init` + the two pure verbs +\n * `subs` + `handlers`, all closing over one `BatchWindowConfig`. The verbs\n * return `readonly [BatchWindow<I>, readonly C[]]`: the next slice and the Cmds\n * to emit (one `flush` Cmd when a window closes, none otherwise).\n */\nexport interface BatchWindowKnob<I, C extends Cmd> {\n /** The starting slice — `initBatchWindow()`. */\n init(): BatchWindow<I>;\n /**\n * Buffer `item` (arrival-stamped `at`), flushing immediately if that brings\n * the buffer to `maxItems`. See `addItem`.\n */\n add(\n state: BatchWindow<I>,\n item: I,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * Flush whatever is buffered because the time window closed. See `onWindow`.\n */\n onWindow(\n state: BatchWindow<I>,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * The window timer Sub, armed only while a window is open. See `subsFor`.\n * `id` keys the deadline so several windows on one machine route distinctly.\n */\n subs(state: BatchWindow<I>, id?: string): readonly BatchWindowSub[];\n /**\n * The `subscribe` cells this knob splices into `machine.subscribe`. One cell,\n * keyed `\"deadline\"`, delegating to `../deadline`'s subscribe handler. Spread\n * into the consumer's subscribe map: `subscribe: { ...bw.handlers() }`.\n */\n handlers(): {\n deadline: SubscribeHandler<BatchWindowSub, BatchWindowExpired, unknown>;\n };\n}\n\n/**\n * Buffer `item` and decide whether the size trigger fires. PURE.\n *\n * - Empty (closed) window → `item` OPENS it: `openedAt` becomes `at`, buffer\n * becomes `[item]`. This is the anchor the time window measures against.\n * - Open window → `item` appends; `openedAt` is left untouched (the window's\n * latency clock keeps running from the FIRST item, not this one).\n *\n * After buffering, if the buffer has reached `maxItems` the window flushes\n * immediately: emit `config.flush(buffer)` and RESET to the closed slice. The\n * size trigger pre-empts the time trigger — a full page ships now, the window\n * Sub for the (now closed) window disarms on the next reconcile because `subs`\n * returns `[]` for an empty buffer.\n *\n * `maxItems <= 1` is the degenerate \"no batching\" case: every item flushes on\n * arrival (buffer reaches 1 immediately). Legal and useful as a feature flag to\n * disable batching without restructuring the wiring.\n *\n * @param config the knob (maxItems / flush constructor)\n * @param state the current slice\n * @param item the item to buffer\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function addItem<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n item: I,\n at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n const buffer = [...state.buffer, item];\n // A closed window opens at THIS item's `at`; an already-open window keeps its\n // original `openedAt` (the latency clock runs from the first item).\n const openedAt = state.openedAt ?? at;\n\n // Size trigger: a full page ships now, regardless of the time window. The\n // floor of 1 means `maxItems <= 1` flushes every item on arrival.\n if (buffer.length >= Math.max(1, config.maxItems)) {\n return [initBatchWindow<I>(), [config.flush(buffer)]];\n }\n\n return [{ buffer, openedAt }, []];\n}\n\n/**\n * Flush the open window because its time bound was reached. PURE.\n *\n * - Open window (buffer non-empty) → emit `config.flush(buffer)` and RESET to\n * the closed slice.\n * - Closed window (buffer empty) → no-op: return the same slice and no Cmds.\n * This guards the race where the size trigger already flushed and reset the\n * window before the time Sub's Msg lands — a stale `onWindow` after a\n * size-flush must NOT emit an empty batch.\n *\n * `at` is accepted for symmetry with `add` and so the consumer's reducer cell\n * has a uniform `(state, at)` shape; the verb doesn't need it for the flush\n * decision (the buffer's emptiness is the whole signal), but taking it keeps a\n * future \"flush only if the window is genuinely past `openedAt + maxMs`\"\n * tightening a non-breaking change.\n *\n * @param config the knob (flush constructor)\n * @param state the current slice\n * @param at the firing instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function onWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n _at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n // Empty buffer = the size trigger already flushed (or nothing arrived). A\n // stale timer Msg must not ship an empty batch — return the slice untouched.\n if (state.buffer.length === 0) return [state, []];\n return [initBatchWindow<I>(), [config.flush(state.buffer)]];\n}\n\n/**\n * The window timer Sub, derived from the slice. PURE.\n *\n * Returns the single `deadline` Sub targeting `openedAt + maxMs` while a window\n * is open (buffer non-empty), or `[]` while it is closed. The substrate's\n * reconcile pass arms the timer when the array gains the Sub and disarms it\n * (clearing the pending `setTimeout`) when the array loses it — so a size-flush\n * that empties the buffer auto-cancels the now-irrelevant time trigger on the\n * next transition.\n *\n * `id` defaults to `\"batch-window\"`; pass a distinct id per window when a\n * machine runs several so the dispatched `BatchWindowExpired` Msg (and its\n * reconcile identity) routes to the right one.\n *\n * @param config the knob (maxMs)\n * @param state the current slice\n * @param id stable identity for this window's timer (default\n * `\"batch-window\"`)\n */\nexport function subsFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): readonly BatchWindowSub[] {\n // Closed window → no timer. The invariant (empty buffer IFF openedAt null)\n // means either check suffices; we read `openedAt` so the `atMs` arithmetic\n // below is non-null without a `!`. `deadlineSub` already returns a\n // `DeadlineSub`, which IS `BatchWindowSub` — no cast.\n if (state.openedAt === null) return [];\n return [deadlineSub(id, state.openedAt + config.maxMs)];\n}\n\n/**\n * The `subscribe[\"deadline\"]` cell for a batch window's timer — the exact\n * `../deadline` handler. Arms a one-shot timer for `max(0, atMs - now)` and\n * dispatches `batchWindowExpired(...)`; returns a cleanup that clears it. The\n * consumer assigns it via the knob's `handlers()`.\n */\nexport const subscribeBatchWindow: SubscribeHandler<\n BatchWindowSub,\n BatchWindowExpired,\n unknown\n> = subscribeDeadline;\n\n/**\n * Build a batch window knob from a config. Combinator form (the default L2\n * ergonomic): hand it the config, splice the ~4 returned hooks into your\n * machine. The pure verbs (`add` / `onWindow`) close over the config so the\n * consumer's reducer cells stay a uniform `(state, ...args)` shape; `subs` /\n * `handlers` pre-wire the window timer.\n *\n * @typeParam I the item type buffered into each batch\n * @typeParam C the Cmd type `config.flush` returns\n */\nexport function createBatchWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n): BatchWindowKnob<I, C> {\n return {\n init: () => initBatchWindow<I>(),\n add: (state, item, at) => addItem(config, state, item, at),\n onWindow: (state, at) => onWindow(config, state, at),\n subs: (state, id) => subsFor(config, state, id),\n handlers: () => ({ deadline: subscribeBatchWindow }),\n };\n}\n"]}
|
package/dist/chunk-HUKZQX6U.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { Cmd, tryInterpret } from './chunk-RQULHGYH.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
|
|
4
|
-
var DEFAULT_SNAPSHOT_KEY = "@@snapshot";
|
|
5
|
-
function snapshotWriteDef() {
|
|
6
|
-
return Cmd.define("snapshot_write", {
|
|
7
|
-
input: z.custom(),
|
|
8
|
-
ok: z.void(),
|
|
9
|
-
err: []
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
var snapshotLoad = Cmd.define("snapshot_load", {
|
|
13
|
-
input: z.object({ key: z.string() }),
|
|
14
|
-
ok: z.void(),
|
|
15
|
-
err: []
|
|
16
|
-
});
|
|
17
|
-
function snapshotSaved(seq, at) {
|
|
18
|
-
return { type: "snapshot_saved", seq, at };
|
|
19
|
-
}
|
|
20
|
-
function snapshotFailed(seq, error) {
|
|
21
|
-
return { type: "snapshot_failed", seq, error };
|
|
22
|
-
}
|
|
23
|
-
function snapshotLoaded(payload) {
|
|
24
|
-
return { type: "snapshot_loaded", payload };
|
|
25
|
-
}
|
|
26
|
-
function snapshotLoadFailed(error) {
|
|
27
|
-
return { type: "snapshot_load_failed", error };
|
|
28
|
-
}
|
|
29
|
-
function createSnapshot(config) {
|
|
30
|
-
const every = config.every >= 1 ? config.every : 1;
|
|
31
|
-
const key = config.key ?? DEFAULT_SNAPSHOT_KEY;
|
|
32
|
-
const snapshotWrite = snapshotWriteDef();
|
|
33
|
-
function emitWrite(state, payload, at) {
|
|
34
|
-
const seq = state.seq + 1;
|
|
35
|
-
const next = { ...state, sinceLast: 0, seq };
|
|
36
|
-
return [next, [snapshotWrite({ key, seq, at, payload })]];
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
init() {
|
|
40
|
-
return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };
|
|
41
|
-
},
|
|
42
|
-
record(state, payload, at) {
|
|
43
|
-
const sinceLast = state.sinceLast + 1;
|
|
44
|
-
if (sinceLast < every) {
|
|
45
|
-
return [{ ...state, sinceLast }, Cmd.none];
|
|
46
|
-
}
|
|
47
|
-
return emitWrite(state, payload, at);
|
|
48
|
-
},
|
|
49
|
-
force(state, payload, at) {
|
|
50
|
-
return emitWrite(state, payload, at);
|
|
51
|
-
},
|
|
52
|
-
confirm(state, msg) {
|
|
53
|
-
if (state.lastSavedSeq !== null && msg.seq <= state.lastSavedSeq) {
|
|
54
|
-
return [state, Cmd.none];
|
|
55
|
-
}
|
|
56
|
-
return [
|
|
57
|
-
{ ...state, lastSavedSeq: msg.seq, lastSavedAt: msg.at },
|
|
58
|
-
Cmd.none
|
|
59
|
-
];
|
|
60
|
-
},
|
|
61
|
-
boot(state) {
|
|
62
|
-
return [{ ...state, sinceLast: 0 }, Cmd.none];
|
|
63
|
-
},
|
|
64
|
-
requestLoad(state) {
|
|
65
|
-
return [state, [snapshotLoad({ key })]];
|
|
66
|
-
},
|
|
67
|
-
handlers(ports) {
|
|
68
|
-
const { store } = ports;
|
|
69
|
-
return {
|
|
70
|
-
// The write effect. Performs the `put`; on success the saved `seq`/`at`
|
|
71
|
-
// ride the `snapshot_saved` Msg back into `confirm`; on failure the
|
|
72
|
-
// error rides `snapshot_failed`. Railway via the core's `tryInterpret`
|
|
73
|
-
// (matching every other module's handler) — the handler always resolves
|
|
74
|
-
// with a Msg, never rejects into the runtime.
|
|
75
|
-
snapshot_write: tryInterpret(
|
|
76
|
-
(cmd) => store.put(cmd.key, cmd.payload),
|
|
77
|
-
(_value, cmd) => snapshotSaved(cmd.seq, cmd.at),
|
|
78
|
-
(error, cmd) => snapshotFailed(cmd.seq, error)
|
|
79
|
-
),
|
|
80
|
-
// The read effect — the recovery half. Performs the `get`; on success
|
|
81
|
-
// the recovered payload (or `null` when no checkpoint exists) rides the
|
|
82
|
-
// `snapshot_loaded` Msg the consumer folds into `init`'s loaded branch;
|
|
83
|
-
// on failure the error rides `snapshot_load_failed`. Same Railway
|
|
84
|
-
// discipline via `tryInterpret` — the read never rejects into the runtime.
|
|
85
|
-
snapshot_load: tryInterpret(
|
|
86
|
-
(cmd) => store.get(cmd.key),
|
|
87
|
-
(payload) => snapshotLoaded(payload),
|
|
88
|
-
(error) => snapshotLoadFailed(error)
|
|
89
|
-
)
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export { createSnapshot, snapshotFailed, snapshotLoad, snapshotLoadFailed, snapshotLoaded, snapshotSaved, snapshotWriteDef };
|
|
96
|
-
//# sourceMappingURL=chunk-HUKZQX6U.js.map
|
|
97
|
-
//# sourceMappingURL=chunk-HUKZQX6U.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/persistence/snapshot/index.ts"],"names":[],"mappings":";;;AAwGA,IAAM,oBAAA,GAAuB,YAAA;AA2FtB,SAAS,gBAAA,GAAsB;AACpC,EAAA,OAAO,GAAA,CAAI,OAAO,gBAAA,EAAkB;AAAA,IAClC,KAAA,EAAO,EAAE,MAAA,EAKN;AAAA,IACH,EAAA,EAAI,EAAE,IAAA,EAAK;AAAA,IACX,KAAK;AAAC,GACP,CAAA;AACH;AAuCO,IAAM,YAAA,GAAe,GAAA,CAAI,MAAA,CAAO,eAAA,EAAiB;AAAA,EACtD,KAAA,EAAO,EAAE,MAAA,CAAO,EAAE,KAAK,CAAA,CAAE,MAAA,IAAU,CAAA;AAAA,EACnC,EAAA,EAAI,EAAE,IAAA,EAAK;AAAA,EACX,KAAK;AACP,CAAC;AAiCM,SAAS,aAAA,CAAc,KAAa,EAAA,EAA8B;AACvE,EAAA,OAAO,EAAE,IAAA,EAAM,gBAAA,EAAkB,GAAA,EAAK,EAAA,EAAG;AAC3C;AAGO,SAAS,cAAA,CAAe,KAAa,KAAA,EAAmC;AAC7E,EAAA,OAAO,EAAE,IAAA,EAAM,iBAAA,EAAmB,GAAA,EAAK,KAAA,EAAM;AAC/C;AAGO,SAAS,eAAkB,OAAA,EAAyC;AACzE,EAAA,OAAO,EAAE,IAAA,EAAM,iBAAA,EAAmB,OAAA,EAAQ;AAC5C;AAGO,SAAS,mBAAmB,KAAA,EAAuC;AACxE,EAAA,OAAO,EAAE,IAAA,EAAM,sBAAA,EAAwB,KAAA,EAAM;AAC/C;AAmHO,SAAS,eAAkB,MAAA,EAAyC;AAIzE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,IAAS,CAAA,GAAI,OAAO,KAAA,GAAQ,CAAA;AACjD,EAAA,MAAM,GAAA,GAAM,OAAO,GAAA,IAAO,oBAAA;AAC1B,EAAA,MAAM,gBAAgB,gBAAA,EAAoB;AAQ1C,EAAA,SAAS,SAAA,CACP,KAAA,EACA,OAAA,EACA,EAAA,EAC0C;AAC1C,IAAA,MAAM,GAAA,GAAM,MAAM,GAAA,GAAM,CAAA;AACxB,IAAA,MAAM,OAAsB,EAAE,GAAG,KAAA,EAAO,SAAA,EAAW,GAAG,GAAA,EAAI;AAC1D,IAAA,OAAO,CAAC,IAAA,EAAM,CAAC,aAAA,CAAc,EAAE,GAAA,EAAK,GAAA,EAAK,EAAA,EAAI,OAAA,EAAS,CAAC,CAAC,CAAA;AAAA,EAC1D;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,GAAsB;AACpB,MAAA,OAAO,EAAE,WAAW,CAAA,EAAG,GAAA,EAAK,GAAG,YAAA,EAAc,IAAA,EAAM,aAAa,IAAA,EAAK;AAAA,IACvE,CAAA;AAAA,IAEA,MAAA,CAAO,KAAA,EAAO,OAAA,EAAS,EAAA,EAAI;AACzB,MAAA,MAAM,SAAA,GAAY,MAAM,SAAA,GAAY,CAAA;AAEpC,MAAA,IAAI,YAAY,KAAA,EAAO;AACrB,QAAA,OAAO,CAAC,EAAE,GAAG,OAAO,SAAA,EAAU,EAAG,IAAI,IAAI,CAAA;AAAA,MAC3C;AAEA,MAAA,OAAO,SAAA,CAAU,KAAA,EAAO,OAAA,EAAS,EAAE,CAAA;AAAA,IACrC,CAAA;AAAA,IAEA,KAAA,CAAM,KAAA,EAAO,OAAA,EAAS,EAAA,EAAI;AACxB,MAAA,OAAO,SAAA,CAAU,KAAA,EAAO,OAAA,EAAS,EAAE,CAAA;AAAA,IACrC,CAAA;AAAA,IAEA,OAAA,CAAQ,OAAO,GAAA,EAAK;AAGlB,MAAA,IAAI,MAAM,YAAA,KAAiB,IAAA,IAAQ,GAAA,CAAI,GAAA,IAAO,MAAM,YAAA,EAAc;AAChE,QAAA,OAAO,CAAC,KAAA,EAAO,GAAA,CAAI,IAAI,CAAA;AAAA,MACzB;AACA,MAAA,OAAO;AAAA,QACL,EAAE,GAAG,KAAA,EAAO,YAAA,EAAc,IAAI,GAAA,EAAK,WAAA,EAAa,IAAI,EAAA,EAAG;AAAA,QACvD,GAAA,CAAI;AAAA,OACN;AAAA,IACF,CAAA;AAAA,IAEA,KAAK,KAAA,EAAO;AACV,MAAA,OAAO,CAAC,EAAE,GAAG,KAAA,EAAO,WAAW,CAAA,EAAE,EAAG,IAAI,IAAI,CAAA;AAAA,IAC9C,CAAA;AAAA,IAEA,YAAY,KAAA,EAAO;AAIjB,MAAA,OAAO,CAAC,OAAO,CAAC,YAAA,CAAa,EAAE,GAAA,EAAK,CAAC,CAAC,CAAA;AAAA,IACxC,CAAA;AAAA,IAEA,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,EAAE,OAAM,GAAI,KAAA;AAClB,MAAA,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAML,cAAA,EAAgB,YAAA;AAAA,UAMd,CAAC,GAAA,KAAQ,KAAA,CAAM,IAAI,GAAA,CAAI,GAAA,EAAK,IAAI,OAAO,CAAA;AAAA,UACvC,CAAC,MAAA,EAAQ,GAAA,KAAQ,cAAc,GAAA,CAAI,GAAA,EAAK,IAAI,EAAE,CAAA;AAAA,UAC9C,CAAC,KAAA,EAAO,GAAA,KAAQ,cAAA,CAAe,GAAA,CAAI,KAAK,KAAK;AAAA,SAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMA,aAAA,EAAe,YAAA;AAAA,UAMb,CAAC,GAAA,KAAQ,KAAA,CAAM,GAAA,CAAI,IAAI,GAAG,CAAA;AAAA,UAC1B,CAAC,OAAA,KAAY,cAAA,CAAe,OAAO,CAAA;AAAA,UACnC,CAAC,KAAA,KAAU,kBAAA,CAAmB,KAAK;AAAA;AACrC,OACF;AAAA,IACF;AAAA,GACF;AACF","file":"chunk-HUKZQX6U.js","sourcesContent":["/**\n * internal/persistence/snapshot — periodic state checkpoint to a host store\n * (R2/KV-shaped). Internal since #49 — not published on any subpath.\n *\n * The problem it solves: a long-running machine (a multi-stage run, a paginated\n * crawl, a saga) lives in TEA's in-memory `State` and is persisted as ONE blob\n * by the substrate's `Store<S>` on every transition. That blob is fine for\n * crash recovery of the *runtime* — but it is NOT a coarse, named checkpoint a\n * human (or a separate recovery job) can point a restart at. `snapshot` adds\n * the \"every N units of progress, write a durable checkpoint to R2/KV\"\n * discipline on top, so a crashed long run restarts from the last checkpoint\n * instead of from zero (the field-guide pain row).\n *\n * Shape: this is the same knob contract every L2 composition follows —\n * `createSnapshot(config)` hands back a `{ init, <verbs>, handlers }` bundle you\n * splice into your machine. The state it owns is a Model slice (durable +\n * replayable, the two non-negotiables); the verbs are PURE decisions; the I/O\n * (the actual `put` to the store) lives only in `handlers`.\n *\n * It is a LEAF module: it imports only the core substrate (`../index`, for\n * `Cmd` / `Interpret`) and no sibling subpath. (It does NOT reuse\n * `@demlik/tea/retry-backoff` — that is an even-leafier utility that depends on\n * nothing at all, not even the core; the kinship here is \"leaf with injected\n * time\", not a shared dependency.)\n * Nothing here reads the clock or the RNG on its own behalf — time enters as an\n * `at`/`now` parameter at the verb boundary and the store I/O is realized only\n * when the runtime performs the emitted `Cmd`. That keeps every verb\n * deterministic (invariant 2) and the checkpoint write a `Cmd` the runtime\n * interprets (invariant 3).\n *\n * The Store port is deliberately ABSTRACT — `{ get, put }`, the lowest common\n * shape of R2 and KV — so this module never imports `@cloudflare/workers-types`\n * and can be checkpointed to anything (R2, KV, a DO sub-key, an in-memory map in\n * tests). The host binds the concrete adapter at `handlers({ store })`.\n *\n * Typical wiring:\n *\n * const snap = createSnapshot<RunState>({ every: 25, key: \"run/checkpoint\" });\n *\n * // machine state:\n * type State = { run: RunState; snapshot: SnapshotState };\n * init: (loaded) => loaded ? [loaded, []] : [{ run: initRun(), snapshot: snap.init() }, []],\n *\n * // in `update`, on each progress Msg, fold the snapshot slice:\n * progress: (s, msg) => {\n * const run = advance(s.run, msg);\n * const [snapshot, cmds] = snap.record(s.snapshot, run, msg.at);\n * return [{ ...s, run, snapshot }, cmds];\n * },\n * // the write completed → durable ack folds back through update:\n * snapshot_saved: (s, msg) => {\n * const [snapshot, cmds] = snap.confirm(s.snapshot, msg);\n * return [{ ...s, snapshot }, cmds];\n * },\n *\n * // RECOVERY — the half that makes a checkpoint a restart point, not just a\n * // backup. A `boot` Msg the host dispatches once after `run(...)` asks the\n * // store for the durable checkpoint; the answer folds the recovered payload\n * // back into the run slice:\n * boot: (s) => snap.requestLoad(s), // emits snapshot_load Cmd\n * snapshot_loaded: (s, msg) => {\n * if (msg.payload === null) return [s, []]; // no checkpoint → keep fresh init\n * const [snapshot, cmds] = snap.boot(s.snapshot);\n * return [{ ...s, run: msg.payload, snapshot }, cmds];\n * },\n * snapshot_load_failed: (s) => [s, []], // errors are data — decide here\n *\n * // splice the I/O — both effect cells (write + load):\n * interpret: { ...snap.handlers({ store: r2Adapter }) },\n */\n\nimport { z } from \"zod\";\nimport { Cmd, type CmdOf, type Interpret, tryInterpret } from \"../../../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config — the knob.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot knob.\n *\n * `every` is the cadence: a checkpoint is written once `every` units of\n * progress have accumulated since the last one (see `record`). It is the\n * `snapshotEvery` field `monitored-run` threads through to this module.\n *\n * `key` is the store key the checkpoint is written under. A single machine\n * keeps a single rolling checkpoint, so the key is fixed at config time rather\n * than passed per write — the latest checkpoint overwrites the previous one\n * (R2/KV are last-write-wins on a key). Defaults to `\"@@snapshot\"`, mirroring\n * `doStore`'s `@@state` default-key convention.\n */\nexport interface SnapshotConfig {\n /**\n * Write a checkpoint once this many progress units have accumulated since the\n * last write. `every: 1` checkpoints on every `record`; a value `<= 0` is\n * treated as `1` (every progress unit triggers a write) — a non-positive\n * cadence has no other sensible meaning, and clamping keeps `record` from\n * never checkpointing on a misconfigured `0`.\n */\n readonly every: number;\n /** Store key the rolling checkpoint is written under. Defaults to `\"@@snapshot\"`. */\n readonly key?: string;\n}\n\nconst DEFAULT_SNAPSHOT_KEY = \"@@snapshot\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Slice — the Model field this knob owns.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot bookkeeping slice. Plain data — JSON-serializable so it rides\n * inside the consumer's `Store<S>` blob and survives DO eviction / reload (the\n * \"durable\" non-negotiable). It carries NO payload: the thing being\n * checkpointed lives elsewhere in the machine's state; this slice only tracks\n * *when* to checkpoint and *what has been acknowledged durable*.\n *\n * - `sinceLast` — progress units accumulated since the last checkpoint\n * DECISION (reset to 0 the moment `record` decides to write, not when the\n * write lands). The cadence counter.\n * - `seq` — monotonic checkpoint sequence. Bumped each time a write is\n * emitted, so the write Cmd and its later acknowledgement can be matched\n * even if several are in flight. The \"version\" of the latest emitted\n * checkpoint.\n * - `lastSavedSeq` — the highest `seq` whose write the host has confirmed\n * durable (via `confirm`). `null` until the first confirmed write. The gap\n * `seq - lastSavedSeq` is the \"writes emitted but not yet acknowledged\"\n * depth — observable for backpressure / health.\n * - `lastSavedAt` — the `at` of the most recent confirmed write (epoch ms),\n * `null` until the first. Carried for observability, never compared inside\n * a verb (time is injected, never read here).\n */\nexport interface SnapshotState {\n readonly sinceLast: number;\n readonly seq: number;\n readonly lastSavedSeq: number | null;\n readonly lastSavedAt: number | null;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Store port — abstract R2/KV-shaped checkpoint sink.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The abstract checkpoint sink — the intersection of R2's and KV's surface,\n * narrowed to exactly the two methods a rolling checkpoint needs. Deliberately\n * NOT the substrate's `Store<S>` (`{ load, save, migrate }`): that is the\n * runtime's whole-state persistence boundary keyed at a fixed cell, whereas a\n * checkpoint store is keyed, value-typed, and host-supplied.\n *\n * - `put(key, value)` — write the checkpoint. Last-write-wins on `key`.\n * - `get(key)` — read the latest checkpoint back, or `null` if none. Used by\n * a recovery path to seed `init`'s `loaded` from the durable checkpoint\n * when the runtime's own `Store<S>` blob is gone.\n *\n * No `@cloudflare/workers-types` import — an R2 bucket, a KV namespace, a DO\n * sub-key, or an in-memory `Map` all satisfy this shape with a 3-line adapter\n * at the host.\n */\nexport interface SnapshotStore<V> {\n get(key: string): Promise<V | null>;\n put(key: string, value: V): Promise<void>;\n}\n\n/**\n * The ports `handlers` binds. Just the store today — named so a future port\n * (a metrics sink, a clock) can grow without breaking the `handlers(ports)`\n * call shape.\n */\nexport interface SnapshotPorts<V> {\n readonly store: SnapshotStore<V>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmd + Msg — the write effect and its durable acknowledgement.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The checkpoint-write Cmd a `record` / `force` decision emits. Plain data\n * (invariant 1 — Cmds are data, not closures): it carries the `payload` to\n * write, the `key` to write it under, the `seq` it represents, and the `at` it\n * was decided. The handler performs the `put` and routes the result back to a\n * `confirm` / fail Msg.\n *\n * `payload` is `V` — the consumer's serializable checkpoint value (the run\n * state, the cursor, whatever the machine wants restartable). The verb takes it\n * as an argument so the snapshot slice never has to hold a copy of the\n * machine's domain state.\n *\n * `Cmd.define`d (ADR 0014), minted per `V` by a factory the way\n * `workflowActivityDef<A>()` is, since the payload type is the knob's own\n * parameter. The handler answers through this module's OWN Msgs\n * (`snapshot_saved` / `snapshot_failed`), never the minted `_ok` / `_err`,\n * hence `ok: z.void()` and an empty `err` list.\n */\nexport function snapshotWriteDef<V>() {\n return Cmd.define(\"snapshot_write\", {\n input: z.custom<{\n readonly key: string;\n readonly seq: number;\n readonly at: number;\n readonly payload: V;\n }>(),\n ok: z.void(),\n err: [],\n });\n}\nexport type SnapshotWriteCmd<V> = CmdOf<ReturnType<typeof snapshotWriteDef<V>>>;\n\n/**\n * The Msg the write handler dispatches on a SUCCESSFUL `put`. Fold it back\n * through `confirm(state, msg)` to advance `lastSavedSeq` / `lastSavedAt`. The\n * `seq` matches the emitting Cmd so a late acknowledgement of an older write\n * can never regress a newer confirmed one.\n */\nexport type SnapshotSavedMsg = {\n readonly type: \"snapshot_saved\";\n readonly seq: number;\n readonly at: number;\n};\n\n/**\n * The Msg the write handler dispatches on a FAILED `put`. Errors are data\n * (never swallowed): the consumer's reducer decides whether a failed checkpoint\n * is fatal, retryable (compose `@demlik/tea/retry-backoff`), or merely logged.\n * `seq` identifies which checkpoint failed; `error` is carried, never\n * interpreted here.\n */\nexport type SnapshotFailedMsg = {\n readonly type: \"snapshot_failed\";\n readonly seq: number;\n readonly error: unknown;\n};\n\n/**\n * The checkpoint-READ Cmd a `requestLoad` decision emits — the recovery half of\n * the module. Plain data (invariant 1): it carries only the `key` to read. The\n * handler performs `store.get(key)` and routes the result back to a\n * `snapshot_loaded` (Ok, payload-or-null) / `snapshot_load_failed` (Err) Msg.\n *\n * Distinct from `snapshot_write`: a write carries a payload OUT to the store; a\n * load asks the store to hand a payload BACK. The consumer dispatches the\n * triggering Msg once on resume (a `boot` Msg, never `init`'s rehydrate branch —\n * invariant 2), folds the recovered payload into its run slice, then continues.\n */\nexport const snapshotLoad = Cmd.define(\"snapshot_load\", {\n input: z.object({ key: z.string() }),\n ok: z.void(),\n err: [],\n});\nexport type SnapshotLoadCmd = CmdOf<typeof snapshotLoad>;\n\n/**\n * The Msg the load handler dispatches when `store.get` RESOLVES. `payload` is\n * the recovered checkpoint value, or `null` when the store has no checkpoint\n * under `key` (fresh boot — never checkpointed, or a wiped store). The consumer\n * folds this into `init`'s loaded branch: `payload === null` → keep the fresh\n * `init` state; non-null → seed the run slice from the durable checkpoint and\n * `boot` the snapshot slice's cadence counter.\n *\n * A `null` payload is NOT an error — \"no checkpoint yet\" is a normal first-run\n * state, distinct from \"the read failed\" (`snapshot_load_failed`).\n */\nexport type SnapshotLoadedMsg<V> = {\n readonly type: \"snapshot_loaded\";\n readonly payload: V | null;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` REJECTS. Errors are data\n * (never swallowed — the field-guide rule): a failed recovery read is a visible\n * signal the consumer's reducer decides on (fatal-abort, retry via\n * `@demlik/tea/retry-backoff`, or boot-fresh-and-log). `error` is carried, never\n * interpreted here. Kept separate from `snapshot_loaded`'s `null` so \"the store\n * has no checkpoint\" can never masquerade as \"the read failed\".\n */\nexport type SnapshotLoadFailedMsg = {\n readonly type: \"snapshot_load_failed\";\n readonly error: unknown;\n};\n\n/** Construct a `snapshot_saved` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotSaved(seq: number, at: number): SnapshotSavedMsg {\n return { type: \"snapshot_saved\", seq, at };\n}\n\n/** Construct a `snapshot_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotFailed(seq: number, error: unknown): SnapshotFailedMsg {\n return { type: \"snapshot_failed\", seq, error };\n}\n\n/** Construct a `snapshot_loaded` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoaded<V>(payload: V | null): SnapshotLoadedMsg<V> {\n return { type: \"snapshot_loaded\", payload };\n}\n\n/** Construct a `snapshot_load_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoadFailed(error: unknown): SnapshotLoadFailedMsg {\n return { type: \"snapshot_load_failed\", error };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The combinator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The bundle `createSnapshot` returns — the uniform knob contract. `subs` is\n * omitted: a checkpoint is driven by progress (a `record` call from the\n * consumer's reducer), not by a timer, so this module has no subscriptions.\n */\nexport interface SnapshotKnob<V> {\n /** The initial slice — zero progress, no checkpoint emitted or confirmed yet. */\n init(): SnapshotState;\n /**\n * Account one unit of progress. PURE. Increments `sinceLast`; once the\n * accumulated count reaches the configured cadence, DECIDES to checkpoint:\n * resets `sinceLast` to 0, bumps `seq`, and emits a single `snapshot_write`\n * Cmd carrying `payload` + `key` + the new `seq` + `at`. Below the cadence it\n * returns the bumped slice and NO Cmd.\n *\n * `payload` is the checkpoint value to persist (the consumer's domain state);\n * `at` is the caller-stamped time the progress occurred (epoch ms) — injected,\n * never read from the clock here.\n */\n record(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Force a checkpoint regardless of the cadence counter. PURE. Always bumps\n * `seq`, resets `sinceLast`, and emits the write Cmd. Use on a phase boundary\n * the cadence would otherwise miss — e.g. \"the run just finished, checkpoint\n * the terminal state now\" — so the last durable checkpoint is the final state,\n * not whatever the modulo happened to land on.\n */\n force(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Record a confirmed durable write. PURE. Advances `lastSavedSeq` /\n * `lastSavedAt` to the acknowledged `seq` / `at`, but only forward: an\n * out-of-order acknowledgement of an OLDER `seq` (a slow write landing after a\n * newer one) is ignored, so the confirmed watermark is monotonic. Fold the\n * `snapshot_saved` Msg here.\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — an ack emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_saved` cell without the caller\n * re-wrapping a bare State.\n */\n confirm(\n state: SnapshotState,\n msg: SnapshotSavedMsg,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Resume after a reload. PURE. Resets `sinceLast` to 0 because any\n * un-checkpointed progress accumulated before the crash is moot — recovery\n * resumes from the last DURABLE checkpoint (`get` from the store), so the\n * cadence counter starts fresh from there. `seq` / `lastSavedSeq` /\n * `lastSavedAt` are preserved (they describe what is durable, which a reload\n * does not change). Call from a `boot` Msg the host dispatches once after\n * `run(...)`, never from `init`'s rehydrate branch (invariant 2).\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — a resume emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_loaded` cell without the caller\n * re-wrapping a bare State.\n */\n boot(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Ask the store for the durable checkpoint. PURE. Emits a single\n * `snapshot_load` Cmd carrying the configured `key`; returns the slice\n * UNCHANGED (a read decides nothing about the cadence bookkeeping — only the\n * later `snapshot_loaded` fold, via `boot`, touches the slice). This is the\n * entry point of the recovery loop: the consumer dispatches a `boot` Msg once\n * after `run(...)`, folds `requestLoad` here, and the emitted Cmd's handler\n * reads `store.get(key)` and dispatches `snapshot_loaded` / `snapshot_load_failed`.\n *\n * Returns `readonly [SnapshotState, readonly Cmd[]]` (the reducer-cell shape)\n * so it drops straight into a `boot: (s) => snap.requestLoad(s)` cell.\n */\n requestLoad(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * The `interpret` map for this knob's two effect Cmds:\n *\n * - `snapshot_write` — binds the store and performs the `put`, routing\n * Ok → `snapshot_saved` / Err → `snapshot_failed`.\n * - `snapshot_load` — binds the store and performs the `get`, routing\n * Ok → `snapshot_loaded` (payload-or-null) / Err → `snapshot_load_failed`.\n *\n * Railway throughout — neither effect throws into the runtime; the outcome is\n * always a Msg. Spread into your machine's `interpret`.\n */\n handlers(\n ports: SnapshotPorts<V>,\n ): Interpret<\n | SnapshotSavedMsg\n | SnapshotFailedMsg\n | SnapshotLoadedMsg<V>\n | SnapshotLoadFailedMsg,\n SnapshotWriteCmd<V> | SnapshotLoadCmd,\n unknown\n >;\n}\n\n/**\n * Create a snapshot knob from a `SnapshotConfig`. The returned bundle is spliced\n * into a consumer machine: `init()` seeds the slice, `record` / `force` are\n * folded in reducer cells, `confirm` / `boot` handle the acknowledgement +\n * resume Msgs, and `handlers({ store })` supplies the write `interpret` cell.\n */\nexport function createSnapshot<V>(config: SnapshotConfig): SnapshotKnob<V> {\n // Clamp the cadence to >= 1 at construction so `record`'s threshold check is\n // branchless and a misconfigured `0` / negative `every` checkpoints on every\n // progress unit instead of never.\n const every = config.every >= 1 ? config.every : 1;\n const key = config.key ?? DEFAULT_SNAPSHOT_KEY;\n const snapshotWrite = snapshotWriteDef<V>();\n\n /**\n * Emit the write Cmd for a checkpoint decision. Shared by `record`'s\n * cadence-hit branch and `force`. Bumps `seq`, resets `sinceLast`, returns the\n * new slice + the single `snapshot_write` Cmd. The input slice is never\n * mutated (invariant 1).\n */\n function emitWrite(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]] {\n const seq = state.seq + 1;\n const next: SnapshotState = { ...state, sinceLast: 0, seq };\n return [next, [snapshotWrite({ key, seq, at, payload })]];\n }\n\n return {\n init(): SnapshotState {\n return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };\n },\n\n record(state, payload, at) {\n const sinceLast = state.sinceLast + 1;\n // Below the cadence: accumulate, emit nothing.\n if (sinceLast < every) {\n return [{ ...state, sinceLast }, Cmd.none];\n }\n // Cadence reached: checkpoint. `emitWrite` resets the counter + bumps seq.\n return emitWrite(state, payload, at);\n },\n\n force(state, payload, at) {\n return emitWrite(state, payload, at);\n },\n\n confirm(state, msg) {\n // Forward-only watermark: ignore a stale ack of an older seq so a slow\n // write landing after a newer one can't regress `lastSavedSeq`.\n if (state.lastSavedSeq !== null && msg.seq <= state.lastSavedSeq) {\n return [state, Cmd.none];\n }\n return [\n { ...state, lastSavedSeq: msg.seq, lastSavedAt: msg.at },\n Cmd.none,\n ];\n },\n\n boot(state) {\n return [{ ...state, sinceLast: 0 }, Cmd.none];\n },\n\n requestLoad(state) {\n // A read decides nothing about the cadence bookkeeping — return the slice\n // unchanged and emit the single read Cmd. The recovered payload folds back\n // through `snapshot_loaded` (the consumer's reducer), not here.\n return [state, [snapshotLoad({ key })]];\n },\n\n handlers(ports) {\n const { store } = ports;\n return {\n // The write effect. Performs the `put`; on success the saved `seq`/`at`\n // ride the `snapshot_saved` Msg back into `confirm`; on failure the\n // error rides `snapshot_failed`. Railway via the core's `tryInterpret`\n // (matching every other module's handler) — the handler always resolves\n // with a Msg, never rejects into the runtime.\n snapshot_write: tryInterpret<\n SnapshotWriteCmd<V>,\n void,\n SnapshotSavedMsg | SnapshotFailedMsg,\n unknown\n >(\n (cmd) => store.put(cmd.key, cmd.payload),\n (_value, cmd) => snapshotSaved(cmd.seq, cmd.at),\n (error, cmd) => snapshotFailed(cmd.seq, error),\n ),\n // The read effect — the recovery half. Performs the `get`; on success\n // the recovered payload (or `null` when no checkpoint exists) rides the\n // `snapshot_loaded` Msg the consumer folds into `init`'s loaded branch;\n // on failure the error rides `snapshot_load_failed`. Same Railway\n // discipline via `tryInterpret` — the read never rejects into the runtime.\n snapshot_load: tryInterpret<\n SnapshotLoadCmd,\n V | null,\n SnapshotLoadedMsg<V> | SnapshotLoadFailedMsg,\n unknown\n >(\n (cmd) => store.get(cmd.key),\n (payload) => snapshotLoaded(payload),\n (error) => snapshotLoadFailed(error),\n ),\n };\n },\n };\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/flow/monitored-run/index.ts"],"names":[],"mappings":"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","file":"chunk-HYDWG35Q.js","sourcesContent":["/**\n * internal/flow/monitored-run — a long-running operation that is BOTH staged and\n * watched: an ordered stage pipeline whose POSITION survives eviction, wrapped\n * by a no-progress safety deadline and (optionally) a periodic durable\n * checkpoint.\n *\n * This is the merge of two field-guide concerns the audit-agent machine\n * hand-wired together (`seed/audit-agent-machine`): the OUTER pipeline position\n * (`RunStage` — planning → testing → summarizing → reporting, replayed on cold\n * wake by re-emitting the one outstanding effect) and the no-progress safety\n * alarm (`subs.ts`, keyed on `progressSeq` so every progress event retires the\n * old alarm and arms a fresh one). `monitored-run` lifts both into one config-\n * driven knob with the uniform L2 contract `resilient-call` established:\n * `init()`, the verbs (`start` / `advance` / `progress` / `onDeadline` /\n * `boot`), `subs(state)`, and `handlers(ports)`.\n *\n * - It is **staged, optionally**. Omit `stages` and the run is single-shot:\n * `start` enters `running`, `advance` finishes it to `done`. Provide\n * `stages` and it becomes an ordered pipeline — each `advance` retires the\n * current stage and claims the next, finishing to `done` only when the last\n * stage is retired. The pipeline POSITION lives in the Model (a\n * `work-queue` of stage items), so an eviction mid-pipeline resumes at the\n * same stage via `boot` — the audit machine's \"pipeline position survives\n * eviction\" property, generalized.\n * - It is **watched**. While `running`, a no-progress deadline is armed at\n * `lastProgressAt + deadlineMs` and keyed on `progressSeq`. Every\n * `progress` / `advance` bumps the seq, so the substrate retires the old\n * alarm and arms a new one — a self-rearming watchdog. If the alarm fires\n * (`onDeadline`) the run goes `failed` with a deadline reason. Omit\n * `deadlineMs` → no watchdog (the `stale` phase is never reachable).\n * - It **checkpoints, optionally**. Provide `snapshotEvery` and every\n * progress unit is accounted to a `../snapshot` slice; once the cadence is\n * reached a checkpoint Cmd is emitted (interpreted by the spliced snapshot\n * `handlers`), so a crashed run restarts from the last durable checkpoint\n * rather than from zero. Omit it → no checkpointing.\n *\n * ## Why the run lifecycle is INLINE (run-tracker not extracted)\n *\n * The phase union (`idle` / `running` / `stale` / `done` / `failed` / `cancelled`, a\n * discriminated union carrying only each phase's own data) and its bookkeeping\n * (`runId`, `startedAt`, `lastProgressAt`, `progressSeq`) live directly on this\n * slice rather than behind a `run-tracker` sub-knob.\n * Extraction earns its keep when two independent axes vary; here the lifecycle\n * has exactly one anchoring axis (this run), so a separate module would be a\n * premature seam. The two genuinely independent axes — the safety DEADLINE and\n * the periodic SNAPSHOT — ARE extracted (`../deadline`, `../snapshot`), and the\n * stage list is a `../work-queue` slice (`QueueItem<Stage>[]`) whose every\n * status flip is DELEGATED to the blessed `../work-queue/ops` (`enqueueOp` /\n * `claimNextOp` / `markDoneOp`) — never re-rolled here. The lifecycle is the\n * glue between them.\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is plain data (the stage queue is a\n * JSON-serializable `QueueItem<Stage>[]`, the snapshot slice is plain\n * numbers), so it survives DO eviction / reload.\n * - **Replayable** — every transition is a pure verb returning new state +\n * Cmds; nothing here reads the clock or RNG. Time arrives as an `at` / `atMs`\n * parameter; the only clock read is `Date.now()` inside the snapshot\n * `handlers` port (the effect boundary), exactly as `../snapshot` stamps it.\n *\n * ## Typical wiring\n *\n * const run = createMonitoredRun<MyStage, MyState>({\n * stages: [\"plan\", \"test\", \"report\"],\n * deadlineMs: 10 * 60_000,\n * snapshotEvery: 25,\n * });\n *\n * init: (loaded) => loaded ? [loaded, []] : [{ run: run.init() }, []],\n * update: {\n * begin: (s, m) => lift(s, run.start(s.run, m.runId, m.at)),\n * advance: (s, m) => lift(s, run.advance(s.run, checkpoint(s), m.result, m.at)),\n * progress: (s, m) => lift(s, run.progress(s.run, checkpoint(s), m.at)),\n * run_deadline: (s, m) => lift(s, run.onDeadline(s.run, m)),\n * boot: (s, m) => lift(s, run.boot(s.run, m.at)),\n * },\n * subscriptions: (s) => run.subs(s.run),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: run.handlers({ store: r2 }),\n */\n\nimport type { Cmd, Interpret } from \"../../../index\";\nimport {\n createSnapshot,\n type SnapshotFailedMsg,\n type SnapshotLoadCmd,\n type SnapshotLoadedMsg,\n type SnapshotLoadFailedMsg,\n type SnapshotSavedMsg,\n type SnapshotState,\n type SnapshotStore,\n type SnapshotWriteCmd,\n} from \"../../persistence/snapshot\";\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n subscribeDeadline,\n} from \"../../resilience/deadline\";\nimport type { QueueItem } from \"../../work-queue\";\nimport { queueAdapter } from \"../../work-queue/adapter\";\n\n// ===========================================================================\n// Config — the knob. Every field optional; omit a field → omit its gate.\n// ===========================================================================\n\n/**\n * The monitored-run knob. EVERY field is optional:\n * - omit `stages` → single-shot run (no pipeline; one `advance` finishes).\n * - omit `deadlineMs` → no no-progress watchdog (the run is never auto-failed).\n * - omit `snapshotEvery`→ no periodic checkpoint (no `snapshot_write` Cmds).\n *\n * An empty config `{}` is a valid single-shot, unwatched, uncheckpointed run.\n *\n * `Stage` is the consumer's stage identifier — any JSON-serializable value\n * (a string id, a `{ kind, ... }` record). It rides through the stage queue\n * unchanged so the consumer's reducer can branch on the current stage to emit\n * that stage's outstanding effect (the audit machine's `outstandingEffect`).\n */\nexport interface MonitoredRunConfig<Stage> {\n /**\n * The ordered stages of the pipeline. Position survives eviction (lives in\n * the Model's stage queue). Omit for a single-shot run. An empty array is\n * treated as single-shot too — there is nothing to pipeline through.\n */\n readonly stages?: readonly Stage[];\n /**\n * No-progress watchdog budget, in ms. The safety deadline is armed at\n * `lastProgressAt + deadlineMs` and re-armed on every progress event. Omit\n * for no watchdog.\n */\n readonly deadlineMs?: number;\n /**\n * Checkpoint cadence — write a durable snapshot once this many progress\n * units have accumulated. Threaded straight into `../snapshot`'s `every`.\n * Omit for no checkpointing.\n */\n readonly snapshotEvery?: number;\n /**\n * Store key the rolling checkpoint is written under. Forwarded to\n * `../snapshot`'s `key`; defaults to that module's `@@snapshot`. Ignored\n * when `snapshotEvery` is omitted.\n */\n readonly snapshotKey?: string;\n}\n\n// ===========================================================================\n// Slice — the Model field this knob owns. Run lifecycle INLINE + composed bricks.\n// ===========================================================================\n\n/**\n * Why a run terminated as `failed`. Discriminated so each reason carries only\n * its own data (pattern 11):\n * - `deadline` — the no-progress watchdog fired (`onDeadline`).\n * - `stage` — the consumer reported a stage failure via `advance(..., fail)`.\n *\n * `stale` is a SEPARATE phase (not a fail reason): a run is `stale` only by an\n * explicit consumer signal that progress has wedged WITHOUT the deadline having\n * fired — a soft, recoverable observation. The deadline firing is a hard\n * terminal (`failed { reason: \"deadline\" }`).\n *\n * The `stage` variant carries the FAILING STAGE VALUE (the `Stage` itself), not\n * a positional index: done stages leave the queue (`markDoneOp` filters them),\n * so a residual index drifts as the pipeline advances. The stage value is the\n * stable, meaningful identifier — `undefined` for a single-shot run (no stage).\n */\nexport type RunFailure<Stage> =\n | { readonly reason: \"deadline\"; readonly at: number }\n | {\n readonly reason: \"stage\";\n readonly stage: Stage | undefined;\n readonly error: unknown;\n };\n\n/**\n * The run bookkeeping every phase carries in common, split out so each arm of\n * the `MonitoredRunState` discriminated union names only the data it ADDS\n * (`runId` on started phases, `failure` on `failed`) — the same discipline the\n * sibling `PaginatorState` uses for its shared `seen` / `pages` counters. Plain\n * data end to end so the whole slice survives DO eviction / reload:\n *\n * - `stepStates` — the stage pipeline as a `work-queue` of stage items.\n * Empty for a single-shot run. The ONE `running` item is\n * the current stage; `done` items are retired stages\n * (they leave the queue via `markDoneOp`, so the queue\n * shrinks as the pipeline advances and is empty at\n * `done`). POSITION survives eviction because this list\n * is durable.\n * - `startedAt` — `at` of `start`. Observability; never compared in a verb.\n * - `lastProgressAt` — `at` of the most recent progress event; the watchdog\n * base. Re-armed by `progress` / `advance`.\n * - `progressSeq` — monotonic progress counter; keys the safety alarm Sub\n * id so every bump retires the old alarm and arms a new\n * one (the audit subs' `safety:{progressSeq}` discipline).\n * - `snapshot` — the `../snapshot` slice (cadence counter + durable\n * watermark). Always present (uniform shape); the gate\n * is simply never driven when `snapshotEvery` is omitted.\n */\ninterface RunCore<Stage> {\n readonly stepStates: readonly QueueItem<Stage>[];\n readonly startedAt: number;\n readonly lastProgressAt: number;\n readonly progressSeq: number;\n readonly snapshot: SnapshotState;\n}\n\n/**\n * The slice — a discriminated union on `phase` so each phase carries ONLY its\n * own data and impossible combinations are unrepresentable (pattern 11), the\n * same idiom the sibling `CircuitState` / `PaginatorState` / `RunFailure` follow:\n *\n * - `idle` — created but never started. Carries no `runId` (identity is\n * host-minted at `start`) and no `failure`: \"not started\" is its\n * OWN phase, not a `running` run wearing an empty-string `runId`\n * sentinel. `start` is the only transition out, so no verb has to\n * guard \"has this actually started?\" — `phase === \"idle\"` says it.\n * - `running` — the run is live. Carries the host-minted `runId` and the\n * `RunCore` pipeline position + watchdog bookkeeping.\n * - `stale` — a soft \"no progress observed\" mark the consumer can set and\n * clear; still resumable (a `progress` clears it back to\n * `running`). Distinct from a fired deadline, which is terminal.\n * - `done` — every stage retired (or the single-shot `advance` landed).\n * - `failed` — terminal failure; the typed `failure` reason rides ONLY this\n * arm, so it is present iff `phase === \"failed\"` BY CONSTRUCTION —\n * there is no nullable side field to keep in sync.\n * - `cancelled` — the consumer stopped the run from outside (`cancel`). Its own\n * phase rather than a `RunFailure` reason, on the same argument\n * that keeps `stale` a phase: a caller who asked for the stop did\n * not suffer a failure, and every \"did this fail?\" reader would\n * otherwise have to except one reason. Terminal, so it re-enters\n * no verb and arms no watchdog.\n *\n * `runId` is the host-minted run identity, stamped at `start` (the machine never\n * mints — see the audit seed's \"run identity is host-minted\"). It rides every\n * started phase, but not `idle`. `cancelled` is the one phase reachable from BOTH\n * sides of that line, so its `runId` is `string | null`: `null` says the run was\n * cancelled before it ever started and there was no identity to mint, which is a\n * different fact from a cancelled live run and stays a different value.\n */\nexport type MonitoredRunState<Stage> =\n | (RunCore<Stage> & { readonly phase: \"idle\" })\n | (RunCore<Stage> & { readonly phase: \"running\"; readonly runId: string })\n | (RunCore<Stage> & { readonly phase: \"stale\"; readonly runId: string })\n | (RunCore<Stage> & { readonly phase: \"done\"; readonly runId: string })\n | (RunCore<Stage> & {\n readonly phase: \"failed\";\n readonly runId: string;\n readonly failure: RunFailure<Stage>;\n })\n | (RunCore<Stage> & {\n readonly phase: \"cancelled\";\n readonly runId: string | null;\n readonly at: number;\n });\n\n/**\n * The live (still-timed) phases — `running` or `stale`. The verbs narrow to this\n * before touching the watchdog bookkeeping (`runId` / `progressSeq`), so a\n * settled (`done` / `failed` / `cancelled`) or never-started (`idle`) slice can never be bumped\n * — the narrow replaces the old `runId === \"\"` born-live guard.\n */\ntype LiveRun<Stage> = Extract<\n MonitoredRunState<Stage>,\n { readonly phase: \"running\" | \"stale\" }\n>;\n\n/**\n * The ENDED phases — a run that finished (`done`) or was stopped from outside\n * (`cancelled`). Every arm here was started or explicitly stopped, so `idle` is\n * out and `runId` is present on the whole union: a consumer holding one of these\n * reads the run identity without a `phase` guard the value can never fail.\n *\n * It is the resolved half of a drive: a promise that resolves only on an ended\n * run should say so in its type rather than resolve `MonitoredRunState` and\n * leave every reader to re-prove the `idle` arm impossible (#155). `cancelled`\n * keeps its `string | null` `runId` unchanged — a run cancelled before it ever\n * started genuinely has no identity, and that stays a different value here.\n */\nexport type EndedRun<Stage> = Extract<\n MonitoredRunState<Stage>,\n { readonly phase: \"done\" | \"cancelled\" }\n>;\n\n/**\n * The outcome a consumer reports to `advance`: the current stage either\n * succeeded (retire it, claim the next) or failed (terminate the run). Plain\n * data — the consumer parses its stage result at its own boundary and hands a\n * settled outcome here.\n */\nexport type StageResult =\n | { readonly kind: \"ok\" }\n | { readonly kind: \"fail\"; readonly error: unknown };\n\n// ===========================================================================\n// Cmd + Msg — re-exported from the composed bricks so consumers wire one import.\n// ===========================================================================\n\n/** The checkpoint-write Cmd, generic over the consumer's checkpoint value `V`. */\nexport type MonitoredRunCmd<V> = SnapshotWriteCmd<V>;\n\n/** The deadline Msg the safety alarm dispatches when the watchdog fires. */\nexport type MonitoredRunTimerMsg = DeadlineExceeded;\n\n/** Ports the consumer supplies to `handlers` — just the checkpoint store. */\nexport interface MonitoredRunPorts<V> {\n /** Where periodic checkpoints are written (R2 / KV / DO sub-key / Map). */\n readonly store: SnapshotStore<V>;\n}\n\n// ===========================================================================\n// Sub-id family. The safety alarm is keyed on `progressSeq` so a progress\n// event retires the old alarm and arms a new one (single-shot, re-armed by id\n// bump — the audit subs discipline).\n// ===========================================================================\n\nfunction safetyTimerId(runId: string, progressSeq: number): string {\n return `monitored:safety:${runId}:${progressSeq}`;\n}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a monitored-run knob from `config`. `Stage` is the consumer's stage\n * identifier type; `V` is the consumer's checkpoint value type (what\n * `snapshotEvery` checkpoints). Returns the uniform L2 knob contract.\n *\n * No `rng` parameter: unlike `resilient-call`, nothing here jitters — the only\n * external inputs are time (an `at` / `atMs` parameter) and the consumer's\n * stage outcomes. The clock is read only inside the snapshot `handlers` port.\n */\nexport function createMonitoredRun<Stage, V = unknown>(\n config: MonitoredRunConfig<Stage> = {},\n) {\n const stages = config.stages ?? [];\n const isPipeline = stages.length > 0;\n\n // The stage-queue seam, bound once. Every stage status flip delegates to the\n // `QueueAdapter` verbs (`wq.enqueue` / `wq.claim` / `wq.markDone`), so the\n // `QueueItem<Stage>` shape stays insulated behind them.\n const wq = queueAdapter<Stage>();\n\n // The snapshot sub-knob is built only when `snapshotEvery` is configured;\n // absence is `null` so the progress gate short-circuits to \"no checkpoint\".\n // When present, the slice still carries a default SnapshotState so the shape\n // is uniform across configs — the gate simply never records into it.\n const snap =\n config.snapshotEvery !== undefined\n ? createSnapshot<V>({\n every: config.snapshotEvery,\n ...(config.snapshotKey !== undefined\n ? { key: config.snapshotKey }\n : {}),\n })\n : null;\n\n // The snapshot knob to actually drive: `snap` when configured, else a single\n // inert default (`every: 1`) bound ONCE here. Its `record` is never invoked\n // (the progress gate short-circuits when `snap` is null), so the cadence value\n // is immaterial — one shared stateless knob of pure functions stands in for\n // the disabled-checkpoint case across init/boot/handlers.\n const activeSnap = snap ?? createSnapshot<V>({ every: 1 });\n\n /** The starting slice for a never-started run: the `idle` phase, no `runId`. */\n function init(): MonitoredRunState<Stage> {\n return {\n phase: \"idle\",\n stepStates: [],\n startedAt: 0,\n lastProgressAt: 0,\n progressSeq: 0,\n // A default snapshot slice even without the brick → uniform shape. When\n // `snap` is null the inert `activeSnap` default supplies the slice; its\n // `record` is never called, so the cadence value is immaterial.\n snapshot: activeSnap.init(),\n };\n }\n\n /**\n * Seed the stage queue from the configured stages: every stage enqueued\n * `pending`, then the first claimed `running`. `at` stamps the claim. PURE —\n * the work-queue ops are pure array transforms; ids are positional (`stage:i`)\n * so they are deterministic and replayable (no `crypto`, no `Math.random`).\n */\n function seedStages(at: number): readonly QueueItem<Stage>[] {\n let queue: readonly QueueItem<Stage>[] = [];\n stages.forEach((stage, i) => {\n queue = wq.enqueue(queue, stage, at, `stage:${i}`).next;\n });\n // Claim the first stage as the running position. A non-empty pipeline always\n // has a pending head here, so the claim succeeds.\n const claimed = wq.claim(queue, at);\n return claimed ? claimed.next : queue;\n }\n\n /** The current (running) stage item, or `undefined` for a single-shot run. */\n function currentStage(\n s: MonitoredRunState<Stage>,\n ): QueueItem<Stage> | undefined {\n return s.stepStates.find((i) => i.status === \"running\");\n }\n\n // === Verb: start =========================================================\n\n /**\n * Start (or restart) the run for `runId` at time `at`. Seeds the stage queue\n * (if a pipeline) and enters `running` with the watchdog clock primed. Stamps\n * `runId` from the host — the machine never mints (invariant: identity is\n * host-minted, parsed at the boundary). PURE.\n */\n function start(\n s: MonitoredRunState<Stage>,\n runId: string,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n // Build the `running` arm explicitly rather than spreading `...s`: a restart\n // from `failed` must NOT carry the old `failure` field forward (the `running`\n // arm has none), and a start from `idle` has no `runId` to preserve. Only the\n // snapshot slice threads through.\n const next: MonitoredRunState<Stage> = {\n phase: \"running\",\n runId,\n stepStates: isPipeline ? seedStages(at) : [],\n startedAt: at,\n lastProgressAt: at,\n progressSeq: 0,\n snapshot: s.snapshot,\n };\n return [next, []];\n }\n\n /**\n * Bump the watchdog clock: set `lastProgressAt`/`progressSeq` so the safety\n * alarm Sub id changes and the substrate re-arms a fresh deadline. Shared by\n * `progress` and `advance`. PURE.\n */\n function bumpProgress(s: LiveRun<Stage>, at: number): LiveRun<Stage> {\n return { ...s, lastProgressAt: at, progressSeq: s.progressSeq + 1 };\n }\n\n /**\n * Account one progress unit to the snapshot slice (when the brick is\n * configured), returning the new slice + any checkpoint Cmd. `payload` is the\n * consumer's checkpoint value to persist. PURE — `snap.record` is pure, the\n * write is realized only when the runtime performs the emitted Cmd.\n */\n function recordSnapshot(\n s: MonitoredRunState<Stage>,\n payload: V,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (snap === null) return [s, []];\n const [snapshot, cmds] = snap.record(s.snapshot, payload, at);\n // `snap.record` only ever emits `SnapshotWriteCmd<V>` (its sole Cmd), but\n // its signature widens to `readonly Cmd[]`. Narrow back to this knob's\n // precise Cmd union so a consumer's host Cmd type can stay exact.\n return [{ ...s, snapshot }, cmds as readonly MonitoredRunCmd<V>[]];\n }\n\n // === Verb: progress ======================================================\n\n /**\n * Record forward progress: bump the watchdog (re-arming the safety alarm),\n * clear any `stale` mark back to `running`, and account a snapshot unit\n * (emitting a checkpoint Cmd on a cadence hit). A no-op unless the run is live\n * (`running` / `stale`) — a late progress event on a settled (`done` /\n * `failed`) or never-started (`idle`) run is ignored. PURE.\n */\n function progress(\n s: MonitoredRunState<Stage>,\n payload: V,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase !== \"running\" && s.phase !== \"stale\") return [s, []];\n const bumped: MonitoredRunState<Stage> = {\n ...bumpProgress(s, at),\n phase: \"running\",\n };\n return recordSnapshot(bumped, payload, at);\n }\n\n // === Verb: markStale =====================================================\n\n /**\n * Mark the run `stale` — a soft \"no progress observed\" signal the consumer\n * raises WITHOUT the deadline having fired (e.g. an upstream heartbeat gap).\n * Recoverable: the next `progress` flips it back to `running`. A no-op unless\n * the run is currently `running` (a settled run cannot go stale). Does NOT\n * touch `progressSeq` — going stale is not progress, so the armed deadline\n * keeps counting toward the hard terminal. PURE.\n */\n function markStale(\n s: MonitoredRunState<Stage>,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase !== \"running\") return [s, []];\n return [{ ...s, phase: \"stale\" }, []];\n }\n\n // === Verb: cancel ========================================================\n\n /**\n * Stop the run from outside and settle it `cancelled` at `at`. The durable half\n * of an `AbortSignal`: the outcome is a phase in the slice, so a reload reads a\n * run that ended rather than one to resume.\n *\n * Legal from `idle` too, not just the live phases — a signal already aborted\n * when the run is asked for has to end it BEFORE `start`, and leaving it `idle`\n * would let the next boot start the very run the caller stopped. That is the\n * one arm where `runId` is `null`, built explicitly rather than spread so no\n * stale `failure` or absent `runId` rides along.\n *\n * A no-op on a settled run (`done` / `failed` / `cancelled`): a terminal outcome\n * is already the answer, and an abort landing after it must not overwrite it.\n * That also makes a second abort idempotent. Emits no Cmd — cancelling issues no\n * effect, and the in-flight ones it cannot recall are the consumer's to drain.\n * PURE.\n */\n function cancel(\n s: MonitoredRunState<Stage>,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase !== \"idle\" && s.phase !== \"running\" && s.phase !== \"stale\") {\n return [s, []];\n }\n const next: MonitoredRunState<Stage> = {\n phase: \"cancelled\",\n runId: s.phase === \"idle\" ? null : s.runId,\n at,\n stepStates: s.stepStates,\n startedAt: s.startedAt,\n lastProgressAt: s.lastProgressAt,\n progressSeq: s.progressSeq,\n snapshot: s.snapshot,\n };\n return [next, []];\n }\n\n // === Verb: advance =======================================================\n\n /**\n * Advance the pipeline on a stage outcome:\n *\n * - `result.kind === \"fail\"` → terminate `failed { reason: \"stage\" }`,\n * carrying the failing stage index + error. No further stages run.\n * - `result.kind === \"ok\"`:\n * - single-shot run → finish to `done` (and force a final checkpoint\n * when checkpointing, so the last durable snapshot is the terminal\n * state).\n * - pipeline → retire the current stage (`markDoneOp`) and claim the\n * next (`claimNextOp`). If none remain, finish to `done`. The new\n * position survives eviction.\n *\n * An advance is itself a progress event, so it bumps the watchdog (re-arming\n * the alarm at the new stage) and accounts a snapshot unit. A no-op on a\n * settled (`done` / `failed` / `cancelled`) or never-started (`idle`) run. PURE — `at` is the\n * only clock, ids are positional.\n */\n function advance(\n s: MonitoredRunState<Stage>,\n payload: V,\n result: StageResult,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase !== \"running\" && s.phase !== \"stale\") return [s, []];\n\n // Stage failure → hard terminal. Carry the failing stage VALUE (stable),\n // `undefined` for a single-shot run with no stage.\n if (result.kind === \"fail\") {\n const failed: MonitoredRunState<Stage> = {\n ...s,\n phase: \"failed\",\n failure: {\n reason: \"stage\",\n stage: currentStage(s)?.input,\n error: result.error,\n },\n };\n return [failed, []];\n }\n\n // Success: bump the watchdog first (an advance is progress), then move the\n // pipeline position.\n const bumped = bumpProgress(s, at);\n\n // Single-shot: one advance finishes the run.\n if (!isPipeline) {\n const done: MonitoredRunState<Stage> = { ...bumped, phase: \"done\" };\n // Force a terminal checkpoint so the last durable snapshot is `done`,\n // not whatever the cadence happened to land on.\n return finishWithSnapshot(done, payload, at);\n }\n\n // Pipeline: retire the running stage, claim the next.\n const current = currentStage(s);\n const afterDone =\n current !== undefined\n ? wq.markDone(s.stepStates, current.id).next\n : s.stepStates;\n const claimed = wq.claim(afterDone, at);\n\n if (claimed === null) {\n // No more stages — the pipeline is complete.\n const done: MonitoredRunState<Stage> = {\n ...bumped,\n phase: \"done\",\n stepStates: afterDone,\n };\n return finishWithSnapshot(done, payload, at);\n }\n\n // Advance to the next stage — POSITION now points at the claimed item.\n const moved: MonitoredRunState<Stage> = {\n ...bumped,\n stepStates: claimed.next,\n };\n return recordSnapshot(moved, payload, at);\n }\n\n /**\n * Finish helper: force a terminal checkpoint when checkpointing is enabled\n * (so the durable snapshot is the terminal state), else just settle. Shared\n * by single-shot finish and pipeline completion. PURE.\n */\n function finishWithSnapshot(\n s: MonitoredRunState<Stage>,\n payload: V,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (snap === null) return [s, []];\n const [snapshot, cmds] = snap.force(s.snapshot, payload, at);\n // Same narrowing as `recordSnapshot`: `snap.force` emits only the write Cmd.\n return [{ ...s, snapshot }, cmds as readonly MonitoredRunCmd<V>[]];\n }\n\n // === Verb: onDeadline ====================================================\n\n /**\n * The safety alarm fired. Two outcomes by Sub-id match:\n *\n * - the alarm matches the CURRENT `progressSeq` (no progress since it was\n * armed) → the run is wedged → terminate `failed { reason: \"deadline\" }`.\n * - the alarm matches an OLDER seq → a stale fire racing a progress event\n * that already re-armed the alarm → no-op (the substrate retired this id;\n * tolerate a fire still in flight defensively).\n *\n * A no-op on a settled run (its alarm was reconciled away; tolerate a stale\n * fire) and on a NEVER-STARTED (`idle`) run (`subs` armed nothing for it, so\n * any alarm reaching it is a rogue fire that must not un-start it). PURE —\n * `msg.atMs` stamps the failure.\n */\n function onDeadline(\n s: MonitoredRunState<Stage>,\n msg: MonitoredRunTimerMsg,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n // Only a live (`running` / `stale`) run is watched. A settled (`done` /\n // `failed`) run's alarm was reconciled away, and a never-started (`idle`) run\n // — which has no host-minted `runId` and armed nothing — must not be\n // un-started by a born-live/rogue fire. The phase narrow IS the guard the old\n // `runId === \"\"` sentinel stood in for.\n if (s.phase !== \"running\" && s.phase !== \"stale\") return [s, []];\n // Only the alarm armed against the current progressSeq is live. A bumped\n // seq means progress re-armed it; an older fire is stale.\n if (msg.id !== safetyTimerId(s.runId, s.progressSeq)) return [s, []];\n const failed: MonitoredRunState<Stage> = {\n ...s,\n phase: \"failed\",\n failure: { reason: \"deadline\", at: msg.atMs },\n };\n return [failed, []];\n }\n\n // === Verb: boot ==========================================================\n\n /**\n * Resume after a reload (cold wake). Re-seeds the watchdog clock to `at` so\n * the safety alarm re-arms from NOW (the pre-crash `lastProgressAt` would\n * immediately fire a deadline that already elapsed during downtime — a cold\n * wake is not a no-progress wedge). Bumps `progressSeq` so the alarm gets a\n * fresh id, and resets the snapshot cadence counter (un-checkpointed\n * pre-crash progress is moot — recovery resumes from the last durable\n * checkpoint). The pipeline POSITION (`stepStates`) is preserved untouched —\n * that is the whole point of staging: resume at the same stage.\n *\n * A no-op on a settled (`done` / `failed` / `cancelled`) or never-started (`idle`) run. Emits\n * NO Cmd — re-emitting the current stage's outstanding effect is the CONSUMER's\n * job (it knows the per-stage Cmd), the audit machine's `outstandingEffect(stage)`\n * pattern. PURE.\n */\n function boot(\n s: MonitoredRunState<Stage>,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase !== \"running\" && s.phase !== \"stale\") return [s, []];\n // `snap.boot` returns the reducer-cell shape `[SnapshotState, Cmd[]]`\n // (uniform across every sibling knob); a boot emits no Cmd, so take only the\n // slice. Assigning the tuple itself would put an ARRAY where a SnapshotState\n // belongs — a type error AND a non-round-trippable slice.\n const [bootedSnapshot] = activeSnap.boot(s.snapshot);\n const reseeded: MonitoredRunState<Stage> = {\n ...s,\n phase: \"running\",\n lastProgressAt: at,\n progressSeq: s.progressSeq + 1,\n snapshot: bootedSnapshot,\n // Any stage left `running` mid-flight stays the running position; the\n // queue is already correct (a single in-flight stage). resetRunningOp is\n // NOT applied — unlike a work pool, a monitored run has exactly one\n // in-flight stage and resuming it (not re-queuing it) is the correct\n // cold-wake behavior.\n };\n return [reseeded, []];\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions: the no-progress safety deadline, armed only while\n * the run is live (`running` / `stale`) AND a `deadlineMs` is configured. The\n * Sub id is keyed on `progressSeq`, so every progress event retires the old\n * alarm and the substrate arms a fresh one at the new `lastProgressAt +\n * deadlineMs` — a self-rearming watchdog with no manual `clearTimeout`. A\n * settled run desires no subs. Wire `subscribe: { deadline: subscribeDeadline }`.\n *\n * A NEVER-STARTED slice (`init()`) is its OWN `idle` phase, not a `running` run\n * with an empty `runId`. The `phase !== \"running\" && phase !== \"stale\"` narrow\n * therefore excludes it structurally — no separate `runId === \"\"` sentinel gate\n * is needed. Were `idle` armed, it would fire a deadline at `0 + deadlineMs`\n * (already in the past) and auto-fail a run that never began — the \"born live\"\n * defect. `start` is the only transition out of `idle`, so the watchdog exists\n * exactly once a real run is in flight.\n */\n function subs(s: MonitoredRunState<Stage>): readonly DeadlineSub[] {\n if (config.deadlineMs === undefined) return [];\n if (s.phase !== \"running\" && s.phase !== \"stale\") return [];\n return [\n deadlineSub(\n safetyTimerId(s.runId, s.progressSeq),\n s.lastProgressAt + config.deadlineMs,\n ),\n ];\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handler for the checkpoint write. Delegates wholesale to\n * `../snapshot`'s `handlers` (which performs the `put` and routes Ok →\n * `snapshot_saved` / Err → `snapshot_failed` via Railway — the write never\n * rejects into the runtime). When checkpointing is disabled (`snapshotEvery`\n * omitted), no `snapshot_write` Cmd is ever emitted, so the returned map is an\n * inert no-op handler — kept for a uniform `handlers(ports)` call shape.\n *\n * Fold the `snapshot_saved` Msg back through this knob's `confirmSnapshot`\n * verb to advance the durable watermark.\n */\n function handlers(\n ports: MonitoredRunPorts<V>,\n ): Interpret<\n | SnapshotSavedMsg\n | SnapshotFailedMsg\n | SnapshotLoadedMsg<V>\n | SnapshotLoadFailedMsg,\n SnapshotWriteCmd<V> | SnapshotLoadCmd,\n unknown\n > {\n return activeSnap.handlers({ store: ports.store });\n }\n\n /**\n * Fold a confirmed checkpoint write into the slice — advances the snapshot\n * watermark (forward-only). A no-op pass-through when checkpointing is\n * disabled. PURE. Fold the `snapshot_saved` Msg here.\n */\n function confirmSnapshot(\n s: MonitoredRunState<Stage>,\n msg: SnapshotSavedMsg,\n ): MonitoredRunState<Stage> {\n if (snap === null) return s;\n // `snap.confirm` returns the reducer-cell shape `[SnapshotState, Cmd[]]`\n // (an ack emits nothing). Take only the slice — folding the whole tuple into\n // `snapshot` would store an ARRAY where a SnapshotState belongs (a type error\n // and a non-round-trippable slice).\n const [confirmed] = snap.confirm(s.snapshot, msg);\n return { ...s, snapshot: confirmed };\n }\n\n return {\n init,\n start,\n progress,\n markStale,\n cancel,\n advance,\n onDeadline,\n boot,\n confirmSnapshot,\n subs,\n handlers,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the\n * slice lives at `state.run`. Convenience for the common single-slice host;\n * hosts with a differently-named field spread by hand. PURE — a thin record\n * rebuild, no clock / RNG.\n */\nexport function liftRun<\n S extends { run: MonitoredRunState<Stage> },\n Stage,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [MonitoredRunState<Stage>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, run: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives so consumers (and tests) wire one\n * import: `subscribeDeadline` is the `subscribe` cell, `deadlineSub` builds the\n * Sub literal this knob's `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type {\n DeadlineSub,\n DeadlineExceeded,\n SnapshotSavedMsg,\n SnapshotWriteCmd,\n};\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/paginate/paginator/index.ts","../src/internal/paginate/paginated-walk/index.ts"],"names":["start","drain","resume"],"mappings":"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","file":"chunk-I5MQMJDW.js","sourcesContent":["/**\n * internal/paginate/paginator — the cursor/offset/page-token walk loop as pure\n * state + ops.\n *\n * Same shape as `../../../retry-backoff`, `../../resilience/circuit-breaker`,\n * and `../../resilience/rate-limit`: a state type plus pure transition functions,\n * host-agnostic by construction. Nothing here reads the clock, the RNG, or any\n * I/O on its own behalf — the consumer fetches a page out-of-band and folds the\n * *result* back in through `recordPage`. That keeps every function inside TEA's\n * invariant 2 (transitions are pure: same inputs → same outputs) so a consumer\n * can thread the walk through `update` and exercise it with `replay` / the pbt\n * fold without a fake network.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). All fields are `readonly` and plain data, so\n * the whole walk is trivially JSON-serializable into a consumer's `Store<S>`\n * and survives a Durable-Object eviction / reload mid-walk: the cursor lives in\n * the Model, not a closure (the durability + replay non-negotiables every TEA\n * knob preserves).\n *\n * ## The cursor is opaque, and the consumer advances it\n *\n * A paginated API hands back a *next cursor* however it likes: an offset\n * (`?offset=40`), a page token (`?pageToken=ey…`), a \"next\" URL, or `null` at\n * the end. This module is generic in `Cursor` and never inspects it — it only\n * carries it forward. The consumer (who alone knows the API's shape) reads the\n * next cursor off the just-fetched page and passes it to `recordPage`:\n *\n * const next = page.nextPageToken ?? null; // consumer-specific extraction\n * const walk = recordPage(state.walk, page.items.length, next);\n *\n * Passing `next: null` (or a missing `undefined` cursor — the op compares with\n * loose `== null`) means \"the API said there is no next page\" → the walk\n * finishes (`done`). Passing a `Cursor` means \"fetch this one next\" → the walk\n * either keeps `fetching` or `pauses` for backpressure (below). Decoupling the\n * extraction from the op is the same discipline `circuit-breaker` uses for the\n * clock: the impure / domain-specific bit (`nextCursor(page)`) lives at the\n * call site; the op stays a pure fold over the extracted value.\n *\n * ## The phase machine\n *\n * Four phases, each a discriminated-union arm carrying ONLY the data that phase\n * needs so impossible states are unrepresentable (pattern 11):\n *\n * - **idle** — created, not started. No cursor is armed yet; `start` arms the\n * first one. Carries the running `seen` / `pages` counters (both 0 here, but\n * present on every phase so a consumer reads them uniformly).\n *\n * - **fetching** — a page request for `cursor` is outstanding. The walk sits\n * here while the consumer's fetch is in flight; `recordPage` resolves it.\n * Carries the `cursor` being fetched so a post-eviction resume re-issues the\n * SAME request (idempotent retry) rather than skipping a page.\n *\n * - **paused** — backpressure tripped. The API still has more pages (a non-null\n * next cursor), but `seen` has reached `highWaterMark` and the consumer has\n * not drained yet. Carries the `cursor` to resume from. No fetch is\n * outstanding — the loop is deliberately idle until `resume`. This is the\n * valve that stops a fast producer (the API) from outrunning a slow consumer\n * (whatever processes each page) and blowing memory.\n *\n * - **done** — the API returned a null next cursor; the walk is exhausted. A\n * terminal phase: `start` / `recordPage` / `resume` are all no-ops on it\n * (idempotent completion — a late duplicate result can't un-finish a walk).\n *\n * ### Transition table\n *\n * Rows are the current phase; columns are the op. `→` denotes the next phase;\n * `self` means unchanged (a no-op for that (phase × op) pair). `next` is the\n * cursor the consumer extracted from the page; `hwm` is `highWaterMark`.\n *\n * | from \\ op | start | recordPage(count, next) | resume |\n * | ----------- | -------------- | -------------------------------------------------------------------- | ------------------------------ |\n * | idle | → fetching(c0) | self *(¹)* | self |\n * | fetching | self | seen=max(0,seen+count), pages+=1; next==null → done; seen≥hwm → paused(next); else → fetching(next) | self |\n * | paused | self | self *(¹)* | seen<hwm → fetching(cursor) *(²)* |\n * | done | self | self | self |\n *\n * *(¹)* `recordPage` only makes sense while `fetching` (a page was outstanding).\n * On any other phase it is a no-op: a result that arrives when no fetch was in\n * flight (a late duplicate, a double-dispatch) must not advance the cursor or\n * double-count `seen`. The walk is a coordination aid, not a correctness gate —\n * it absorbs the stray rather than corrupting the count.\n *\n * *(²)* `resume` re-arms the paused cursor ONLY once `seen` has dropped below\n * `highWaterMark` (the consumer drained, lowering `seen` via `drain`). Calling\n * `resume` while still over the mark is a no-op — the valve stays shut until\n * there is real headroom, so a consumer that resumes optimistically can't\n * defeat the backpressure. Resuming a walk that isn't `paused` is likewise a\n * no-op.\n *\n * ## Backpressure\n *\n * `seen` is the count of items the API has produced that the consumer has not\n * yet acknowledged as processed. `recordPage` adds each page's item count to it;\n * `drain(n)` subtracts what the consumer finished. When `seen` reaches\n * `highWaterMark`, `recordPage` parks the walk in `paused` instead of issuing\n * the next fetch — the producer stops until `drain` + `resume` re-open the\n * valve. A `highWaterMark` of `0` (or any non-positive value) disables\n * backpressure: the comparison `seen >= hwm` is satisfied vacuously only when\n * `hwm <= 0` would pause immediately, so we treat `hwm <= 0` as \"unbounded\" and\n * never pause. The walk then runs flat-out, `fetching → fetching` until `done`.\n *\n * Internal since #47 — not published on any subpath; reached from inside the\n * package as `internal/paginate/paginator`. Same one-shape-per-package rule as `work-queue`, `retry-backoff`,\n * `circuit-breaker`, and `rate-limit`. It is the L1 escape hatch the\n * `paginated-walk` composition builds its walk loop on.\n */\n\n/**\n * Paginator policy — pure configuration, no mutable state. One policy is shared\n * across the whole walk (often a module-scope const). `PaginatorState` carries\n * the cursor + counters; this carries the knobs they're compared against.\n *\n * Generic in `Cursor` so `firstCursor` matches whatever the API's cursor shape\n * is — an offset `number`, a page-token `string`, a `{ after: string }` record,\n * etc. The module never inspects the cursor; the type just threads through.\n */\nexport interface PaginatorPolicy<Cursor> {\n /**\n * The cursor the walk fetches first, armed by `start`. For an offset API this\n * is typically `0`; for a token API, `null`-or-empty-meaning-\"first page\" is\n * the API's call, so pass whatever sentinel the API treats as page one.\n */\n readonly firstCursor: Cursor;\n /**\n * Backpressure threshold: the number of un-drained items at which the walk\n * pauses instead of fetching the next page. `recordPage` parks in `paused`\n * once `seen >= highWaterMark`; `drain` + `resume` re-open the valve.\n *\n * A non-positive value (`<= 0`) disables backpressure — the walk never pauses\n * and runs `fetching → fetching` until the API exhausts. Use that when the\n * consumer processes each page synchronously inside `recordPage` and there is\n * nothing to back up.\n */\n readonly highWaterMark: number;\n}\n\n/**\n * Sensible defaults: start from a numeric offset of `0`, pause after 1000\n * un-drained items. Tuned for an offset-paginated REST API whose consumer\n * processes pages asynchronously; override per call site (especially\n * `firstCursor`, which is API-specific) rather than mutating this shared const.\n *\n * Typed as `PaginatorPolicy<number>` because the only honest default cursor is\n * the numeric offset `0`; a token-based API supplies its own `firstCursor` and\n * its own `PaginatorPolicy<string>`.\n */\nexport const defaultPaginatorPolicy: PaginatorPolicy<number> = {\n firstCursor: 0,\n highWaterMark: 1000,\n};\n\n/**\n * The walk's phase. A discriminated union on `phase` so each phase carries ONLY\n * its own data and impossible combinations are unrepresentable (pattern 11):\n * there is no `done` state that still holds a pending cursor, no `idle` state\n * mid-fetch. Every field is `readonly` so the whole value is trivially\n * JSON-serializable into a consumer's `Store<S>` and safe to thread through a\n * pure reducer / a Durable-Object reload.\n *\n * Two counters ride on every phase so a consumer reads them uniformly without\n * narrowing first:\n * - `seen` — items produced by the API but not yet drained (backpressure gauge).\n * - `pages` — total pages recorded so far (progress / observability).\n *\n * - `idle` — not started; `start` arms `firstCursor`.\n * - `fetching` — a page request for `cursor` is outstanding.\n * - `paused` — backpressure tripped; `cursor` is the page to resume from.\n * - `done` — API exhausted (null next cursor); terminal.\n */\nexport type PaginatorState<Cursor> =\n | { readonly phase: \"idle\"; readonly seen: number; readonly pages: number }\n | {\n readonly phase: \"fetching\";\n readonly cursor: Cursor;\n readonly seen: number;\n readonly pages: number;\n }\n | {\n readonly phase: \"paused\";\n readonly cursor: Cursor;\n readonly seen: number;\n readonly pages: number;\n }\n | { readonly phase: \"done\"; readonly seen: number; readonly pages: number };\n\n/** The starting state: idle, nothing seen, no pages recorded. */\nexport function initPaginator<Cursor>(): PaginatorState<Cursor> {\n return { phase: \"idle\", seen: 0, pages: 0 };\n}\n\n/**\n * Arm the first fetch. PURE — returns a new state; the input is never mutated.\n *\n * - `idle` → `fetching(firstCursor)`. This is the only phase `start` acts on.\n * - any other phase → unchanged. Re-arming a walk that has already begun (or\n * finished) would skip back to page one and double-count; a stray `start`\n * after launch is absorbed, not honored. Use `initPaginator()` to begin a\n * genuinely fresh walk.\n */\nexport function start<Cursor>(\n state: PaginatorState<Cursor>,\n policy: PaginatorPolicy<Cursor>,\n): PaginatorState<Cursor> {\n if (state.phase !== \"idle\") return state;\n return {\n phase: \"fetching\",\n cursor: policy.firstCursor,\n seen: state.seen,\n pages: state.pages,\n };\n}\n\n/**\n * Record the result of the outstanding fetch and decide what comes next. PURE —\n * returns a new state; the input is never mutated.\n *\n * `count` is how many items the just-fetched page yielded (added to `seen` for\n * backpressure accounting). `next` is the cursor the consumer extracted from the\n * page — a `Cursor` to fetch next, or `null`/`undefined` (a missing optional\n * cursor field) if the API signalled the end.\n *\n * - `fetching` → `seen = max(0, seen + count)`, `pages += 1`, then branch on\n * `next`:\n * - `next == null` → `done` (API exhausted; loose-null so a\n * missing/`undefined` cursor also finishes).\n * - `seen >= highWaterMark` → `paused(next)` (backpressure; `hwm <= 0`\n * disables this branch — see policy docs).\n * - otherwise → `fetching(next)` (keep walking).\n * - any other phase → unchanged. A result with no fetch outstanding (a late\n * duplicate, a double-dispatch) must not advance the cursor or double-count;\n * it is absorbed. See the module header's note ¹.\n *\n * `count` is floored into the gauge: a non-finite `count` (`NaN`/`Infinity`) is\n * treated as `0`, and the additive sum is clamped at `0`. A record can only hold\n * or raise `seen`; only `drain` lowers it. This keeps a stray negative or\n * non-finite count from poisoning the `seen >= highWaterMark` comparison and\n * silently defeating backpressure.\n *\n * The new `seen` total — not the pre-page value — is what the backpressure check\n * reads, so a single page large enough to cross the mark pauses immediately\n * rather than fetching one more first.\n */\nexport function recordPage<Cursor>(\n state: PaginatorState<Cursor>,\n count: number,\n next: Cursor | null | undefined,\n policy: PaginatorPolicy<Cursor>,\n): PaginatorState<Cursor> {\n if (state.phase !== \"fetching\") return state;\n // A page's item count must be a finite, non-negative addition to `seen`. A\n // negative count would drive `seen` down (a record can never lower the gauge —\n // only `drain` does) and a non-finite count (`NaN` / `Infinity`) would poison\n // every later `seen >= highWaterMark` comparison, permanently defeating\n // backpressure (NaN compares false, so the valve never trips). Floor the\n // CONTRIBUTION (not just the sum) at 0: clamp a non-finite count to 0\n // (mirroring retry-backoff's `Number.isNaN` discipline at backoffDelay ~:119)\n // and a negative count to 0. Flooring the contribution — rather than the sum —\n // is what makes a record strictly monotonic: `Math.max(0, seen + count)` only\n // catches a count that drags the *sum* below zero, so a small negative count\n // (e.g. `seen=10, count=-3 → 7`) would still silently lower the gauge and\n // break \"only `drain` lowers `seen`\". Clamping `count` first guarantees the\n // additive step can only hold or raise the gauge.\n const safeCount = Number.isFinite(count) ? Math.max(0, count) : 0;\n const seen = state.seen + safeCount;\n const pages = state.pages + 1;\n // Loose `== null` so a *missing* next cursor (`undefined`, common for an\n // optional API field the consumer extracted as `page.nextPageToken`) finishes\n // the walk exactly like an explicit `null`. Strict `=== null` would route an\n // `undefined` cursor into the fetching/paused branch and carry a cursor-less\n // (malformed) state forward.\n if (next == null) {\n return { phase: \"done\", seen, pages };\n }\n // `hwm <= 0` disables backpressure entirely (unbounded walk); only a positive\n // mark can park the loop. `seen >= hwm` is the half-open edge — reaching the\n // mark exactly pauses, matching the `>=` window edges `rate-limit` /\n // `circuit-breaker` use.\n if (policy.highWaterMark > 0 && seen >= policy.highWaterMark) {\n return { phase: \"paused\", cursor: next, seen, pages };\n }\n return { phase: \"fetching\", cursor: next, seen, pages };\n}\n\n/**\n * Acknowledge that the consumer finished processing `n` items, lowering the\n * backpressure gauge. PURE — returns a new state; the input is never mutated.\n *\n * Subtracts `n` from `seen`, floored at 0 (a consumer that over-acks, or acks a\n * page recorded before a reload, can never drive `seen` negative). `n <= 0` is a\n * no-op on the count. `drain` does NOT change the phase — it only frees\n * headroom; `resume` is the separate, explicit decision to re-issue a fetch once\n * the headroom exists. Splitting \"I drained\" from \"I want to continue\" lets a\n * consumer drain in several steps and resume once.\n *\n * Works on every phase: a consumer may drain while still `fetching` (the gauge\n * just drops, no pause was reached) or while `paused` (freeing room for a later\n * `resume`). On `done` the gauge can still be drained for accurate final\n * accounting, though it no longer gates anything.\n */\nexport function drain<Cursor>(\n state: PaginatorState<Cursor>,\n n: number,\n): PaginatorState<Cursor> {\n if (n <= 0) return state;\n const seen = Math.max(0, state.seen - n);\n if (seen === state.seen) return state;\n return { ...state, seen };\n}\n\n/**\n * Re-open the backpressure valve after a `drain`. PURE — returns a new state;\n * the input is never mutated.\n *\n * - `paused` AND `seen < highWaterMark` → `fetching(cursor)`: the consumer has\n * drained below the mark, so re-issue the parked fetch.\n * - `paused` but `seen >= highWaterMark` → unchanged. There is still no\n * headroom; the valve stays shut until a `drain` lowers `seen`. A consumer\n * that resumes optimistically can't defeat the backpressure. (When `hwm <=\n * 0` backpressure is disabled, so the walk never reaches `paused` and this\n * branch is unreachable — but the guard reads `< hwm` consistently, and a\n * non-positive `hwm` makes `seen < hwm` false for any non-negative `seen`,\n * correctly refusing to resume a state that should never have paused.)\n * - any other phase → unchanged. Resuming a walk that isn't paused (idle,\n * fetching, done) is a no-op.\n *\n * See the module header's note ².\n */\nexport function resume<Cursor>(\n state: PaginatorState<Cursor>,\n policy: PaginatorPolicy<Cursor>,\n): PaginatorState<Cursor> {\n if (state.phase !== \"paused\") return state;\n if (state.seen < policy.highWaterMark) {\n return {\n phase: \"fetching\",\n cursor: state.cursor,\n seen: state.seen,\n pages: state.pages,\n };\n }\n return state;\n}\n\n/**\n * Whether the walk has finished (the API returned a null next cursor). PURE,\n * read-only. A derived predicate so a consumer's reducer can branch\n * (`if (isDone(walk)) return [doneState, [joinCmd]]`) without re-checking the\n * `phase` discriminant by hand at every call site.\n */\nexport function isDone<Cursor>(state: PaginatorState<Cursor>): boolean {\n return state.phase === \"done\";\n}\n","/**\n * internal/paginate/paginated-walk — traverse a paginated API / sitemap end to end\n * WITHOUT a fake clock, without 429s, and resumable across a Durable-Object\n * eviction. The L2 composition that bolts `../resilient-call`'s resilience onto\n * `../paginator`'s cursor walk: each page is fetched as ONE keyed resilient\n * call (retry + backoff + rate-limit gate), and the cursor advances only once a\n * page settles `ok`.\n *\n * ## The recipe (the two siblings it composes)\n *\n * - **`../paginator`** owns the walk loop: the opaque `Cursor`, the `seen` /\n * `pages` counters, and the `idle → fetching → … → done` phase machine.\n * `paginated-walk` never re-implements cursor advancement — it folds each\n * settled page through `recordPage`, letting the paginator decide\n * `done` / `fetching(next)` / `paused`. Backpressure is a TWO-way valve:\n * `recordPage` parks the walk in `paused` once `seen >= highWaterMark`, and\n * the `drain` / `resume` verbs (delegating to the paginator's own\n * `drain` / `resume`) lower the gauge and re-open the parked fetch — without\n * them `highWaterMark` would be a one-way valve that strands a paused walk.\n * - **`../resilient-call`** owns the page fetch: the single `resilient_run`\n * effect per page is gated through that knob's cache → circuit → rate-limit\n * → retry pipeline. A transient page failure backs off and re-issues the\n * SAME cursor (idempotent retry); the walk does not advance until the page\n * genuinely settles.\n *\n * Those are the only two siblings this module calls into. The resilience knobs\n * (`retry` / `circuit` / `rateLimit` / `deadline`) are NOT separate bricks\n * `paginated-walk` composes — they belong to `resilient-call`, which owns the\n * rate-limit gate internally. This module merely passes them through to\n * `createResilientCall` and re-uses two config TYPES at the boundary:\n * `RateLimitConfig` (re-exported by `resilient-call`) and `RetryPolicy` (from\n * `../retry-backoff`). Pass a `rateLimit` and `resilient-call`'s token bucket\n * throttles page fetches (\"don't 429 the upstream\"); omit it and the walk runs\n * as fast as the upstream answers.\n *\n * Because the page-fetch resilience IS a `resilient-call` slice, this knob\n * **inherits that knob's contract verbatim**: the slice is a flat record, every\n * verb returns `readonly [State, Cmd[]]`, time is always an `at` parameter, and\n * the jitter RNG is injected once at `createPaginatedWalk(config, rng)` — never\n * read inside a verb (per the spec, `paginated-walk` *extends* `resilient-call`).\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is plain data: the paginator phase + counters, and\n * the resilient-call brick states (all JSON-serializable). The cursor lives\n * in the Model, not a closure, so a walk survives eviction mid-traversal and\n * resumes from the exact page it was fetching.\n * - **Replayable** — every transition is a verb returning new state + Cmds.\n * `nextCursor` / `onPage` are pure config functions applied at the verb\n * boundary; the Cmds they yield are plain data. `replay` reconstructs the\n * whole walk exactly.\n *\n * ## Where the clock / RNG live\n *\n * Inside the verbs: nowhere. `at` arrives on `start` / `pageOk` / `pageErr` /\n * `onTimer` and is threaded straight into the underlying resilient-call gate (so\n * a rate-limited or retried page measures backoff from that `at`). The jitter\n * RNG is injected at construction. The only clock read is `Date.now()` inside\n * the resilient-call `handlers` port (the effect boundary), inherited unchanged.\n *\n * ## The fixed fetch key\n *\n * A walk fetches one page at a time, so the resilient-call layer tracks exactly\n * one logical call under the constant key `PAGE_KEY`. The cursor being fetched\n * is the resilient call's `input`, so a post-eviction resume re-issues the same\n * page request. (`resilient-call` is keyed to support N concurrent calls; a walk\n * deliberately uses a single key — the paginator is the sequencer.)\n *\n * ## Typical wiring\n *\n * const walk = createPaginatedWalk<PageToken, ApiPage>({\n * firstPage: null, // the API's \"page one\" sentinel\n * nextCursor: (page) => page.nextToken ?? null,\n * onPage: (page) => [{ type: \"index_rows\", rows: page.items }],\n * rateLimit: { capacity: 10, refillPerSec: 5 },\n * retry: defaultRetryPolicy,\n * });\n *\n * // in the machine:\n * init: () => [{ walk: walk.init() }, []],\n * update: {\n * start: (s, m) => lift(s, walk.start(s.walk, m.at)),\n * page_ok: (s, m) => lift(s, walk.pageOk(s.walk, m.result, m.at)),\n * page_err: (s, m) => lift(s, walk.pageErr(s.walk, m.error, m.at)),\n * retry_due:(s, m) => lift(s, walk.onTimer(s.walk, m)),\n * drained: (s, m) => lift(s, walk.drain(s.walk, m.n)),\n * resume: (s, m) => lift(s, walk.resume(s.walk, m.at)),\n * },\n * subscriptions: (s) => walk.subs(s.walk),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: walk.handlers({ run: (cursor) => api.fetchPage(cursor) }),\n */\n\nimport type { Cmd } from \"../../../index\";\nimport { MsgType } from \"../../../protocol\";\nimport type { RetryPolicy } from \"../../../retry-backoff\";\nimport {\n type CircuitConfig,\n createResilientCall,\n type DeadlineConfig,\n type DeadlineSub,\n deadlineSub,\n type FailMsg,\n type RateLimitConfig,\n type ResilientPorts,\n type ResilientState,\n type ResilientTimerMsg,\n type RunCmd,\n type SucceedMsg,\n subscribeDeadline,\n} from \"../../resilience/resilient-call\";\nimport {\n drain as drainWalk,\n initPaginator,\n isDone,\n type PaginatorPolicy,\n type PaginatorState,\n recordPage,\n resume as resumeWalk,\n start as startWalk,\n} from \"../paginator\";\n\n// ===========================================================================\n// Config — the knob. `firstPage` / `nextCursor` / `onPage` describe the walk;\n// the resilience bricks are inherited from resilient-call (every one optional —\n// omit a brick, omit its gate).\n// ===========================================================================\n\n/**\n * The paginated-walk knob. Generic in `Cursor` (the opaque page handle the\n * upstream API hands back — offset, page token, \"next\" URL, …) and `Page` (the\n * shape your fetch port resolves with).\n *\n * `nextCursor` and `onPage` are PURE config functions applied at the verb\n * boundary, never stored in the slice or in a Cmd (invariant 3 — Cmds are\n * plain data, not closures). `nextCursor(page)` extracts the API's next-page\n * handle (or `null` at the end); `onPage(page)` yields the Cmds the consumer\n * wants emitted for each successfully fetched page (index rows, write to R2,\n * dispatch a downstream Msg — whatever; an empty array is fine).\n *\n * The resilience bricks (`retry` / `circuit` / `rateLimit` / `deadline`) are\n * the SAME optional knobs `resilient-call` exposes — paginated-walk extends it.\n * `rateLimit` is the brick that keeps the walk from 429-ing the upstream.\n */\nexport interface PaginatedWalkConfig<Cursor, Page, EmittedCmd extends Cmd> {\n /**\n * The cursor the walk fetches first. For an offset API this is typically `0`;\n * for a token API, whatever sentinel the API treats as \"page one\" (often\n * `null` or `\"\"`). Threaded into `paginator`'s `firstCursor`.\n */\n readonly firstPage: Cursor;\n /**\n * Extract the next cursor from a just-fetched page. Return a `Cursor` to keep\n * walking, or `null` when the API signalled the end (the walk finishes). PURE\n * + consumer-specific (only the consumer knows the API's pagination shape).\n */\n readonly nextCursor: (page: Page) => Cursor | null;\n /**\n * The Cmds to emit for each successfully fetched page — the consumer's work\n * per page (index, persist, fan out, …). Returns plain-data Cmds; `[]` is a\n * valid \"I process the page synchronously elsewhere\" answer. PURE.\n */\n readonly onPage: (page: Page) => readonly EmittedCmd[];\n /**\n * How many items a page contributes to the paginator's backpressure gauge\n * (`seen`). Defaults to `1` per page (count pages, not items) when omitted; a\n * consumer that wants per-item backpressure returns `page.items.length`. PURE.\n */\n readonly pageSize?: (page: Page) => number;\n /**\n * Backpressure threshold handed to the paginator. `seen >= highWaterMark`\n * parks the walk in `paused` instead of fetching the next page. Non-positive\n * (the default `0`) disables backpressure — the walk runs flat out until the\n * API exhausts. See `../paginator`'s `highWaterMark` docs.\n */\n readonly highWaterMark?: number;\n\n // --- resilience bricks, inherited from resilient-call (all optional) ---\n /** Exponential-backoff retry policy for a transient page-fetch failure. */\n readonly retry?: RetryPolicy;\n /** Circuit breaker around the page fetch (one upstream → one breaker). */\n readonly circuit?: CircuitConfig;\n /** Token-bucket rate limit — the \"don't 429 the upstream\" knob. */\n readonly rateLimit?: RateLimitConfig;\n /** Overall wall-clock deadline per individual page fetch. */\n readonly deadline?: DeadlineConfig;\n}\n\n// ===========================================================================\n// Slice — the Model field this knob owns. The paginator walk state + the\n// resilient-call slice for the page fetch. Both are flat plain data.\n// ===========================================================================\n\n/**\n * The slice. `walk` is the cursor/phase/counters from `../paginator`;\n * `resilience` is the `../resilient-call` slice that gates the page fetch.\n * Every field is JSON-serializable, so the whole walk survives a reload.\n *\n * The two move in lockstep: `walk` decides WHICH cursor to fetch next, while\n * `resilience` decides HOW that fetch is attempted (gated, retried, backed\n * off). `pageOk` advances `walk`; `pageErr` defers to `resilience` and leaves\n * `walk` parked on the same cursor until the page truly settles.\n */\nexport interface PaginatedWalkState<Cursor, Page> {\n readonly walk: PaginatorState<Cursor>;\n readonly resilience: ResilientState<Cursor, Page>;\n}\n\n// ===========================================================================\n// Cmds + Msgs the knob speaks.\n// ===========================================================================\n\n/**\n * The page-fetch effect this knob emits: the inherited `resilient_run` Cmd from\n * resilient-call, whose `input` is the `Cursor` to fetch and whose `key` is the\n * fixed `PAGE_KEY`. The consumer's `handlers(ports)` interprets it.\n */\nexport type FetchPageCmd<Cursor> = RunCmd<Cursor>;\n\n/** Page-settled Msgs the `handlers` port dispatches back (inherited verbatim). */\nexport type PageOkMsg<Page> = SucceedMsg<Page>;\nexport type PageErrMsg = FailMsg;\n\n/** The retry / deadline timer Msg — inherited from resilient-call. */\nexport type PaginatedWalkTimerMsg = ResilientTimerMsg;\n\n/**\n * Ports the consumer supplies to `handlers`. `run(cursor)` fetches ONE page for\n * the given cursor; throwing routes to `pageErr` (and thus the resilience\n * backoff). Same shape as resilient-call's port, specialized to the page fetch.\n */\nexport interface PaginatedWalkPorts<Cursor, Page>\n extends ResilientPorts<Cursor, Page> {}\n\n// ===========================================================================\n// The fixed key. A walk fetches one page at a time → exactly one logical\n// resilient call. The paginator is the sequencer; resilient-call's keying is\n// pinned to this single key.\n// ===========================================================================\n\n/** The single resilient-call key every page fetch runs under. */\nexport const PAGE_KEY = \"page\";\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a paginated-walk knob from `config`. `rng` is injected for the\n * underlying retry jitter — pass a fixed `() => 0` in tests to pin backoff;\n * defaults to `Math.random` (read at the verb boundary inside resilient-call,\n * never in a `paginated-walk` verb). Inherited straight from resilient-call.\n *\n * Returns the uniform L2 knob contract: `init()`, the verbs `start` / `pageOk`\n * / `pageErr` / `onTimer`, `subs(state)`, and `handlers(ports)`. `Cursor` is\n * the page handle, `Page` the fetch result, `EmittedCmd` the consumer's\n * per-page Cmds plus the inherited `FetchPageCmd`.\n */\nexport function createPaginatedWalk<Cursor, Page, EmittedCmd extends Cmd = Cmd>(\n config: PaginatedWalkConfig<Cursor, Page, EmittedCmd>,\n rng: () => number = Math.random,\n) {\n // The page fetch is a resilient call keyed by Cursor input → Page result.\n // We feed it only the resilience bricks; the walk-loop knobs are ours.\n const rc = createResilientCall<Cursor, Page>(\n {\n retry: config.retry,\n circuit: config.circuit,\n rateLimit: config.rateLimit,\n deadline: config.deadline,\n },\n rng,\n );\n\n // The paginator policy: our firstPage is the paginator's firstCursor; the\n // high-water mark gates backpressure (0 / omitted → unbounded).\n const paginatorPolicy: PaginatorPolicy<Cursor> = {\n firstCursor: config.firstPage,\n highWaterMark: config.highWaterMark ?? 0,\n };\n\n // The combined Cmd type the verbs can emit: the inherited page-fetch effect\n // plus whatever `onPage` yields.\n type OutCmd = FetchPageCmd<Cursor> | EmittedCmd;\n\n /** Replace the resilience slice, leaving `walk` untouched. */\n function withResilience(\n s: PaginatedWalkState<Cursor, Page>,\n resilience: ResilientState<Cursor, Page>,\n ): PaginatedWalkState<Cursor, Page> {\n return { ...s, resilience };\n }\n\n /** The starting slice: an idle paginator + a fresh resilient-call slice. */\n function init(): PaginatedWalkState<Cursor, Page> {\n return {\n walk: initPaginator<Cursor>(),\n resilience: rc.init(),\n };\n }\n\n /**\n * Issue a resilient page fetch for `cursor` at `at`. The cursor is the\n * resilient call's `input` (so a retry / resume re-fetches the same page) and\n * `PAGE_KEY` is the fixed key. Returns the advanced resilience slice + the\n * `resilient_run` Cmd (or none, if the gate cached / fast-failed / backed off).\n * PURE — `at` is the only clock. Shared by `start` and `pageOk`'s advance.\n */\n function fetch(\n s: PaginatedWalkState<Cursor, Page>,\n cursor: Cursor,\n at: number,\n ): readonly [\n PaginatedWalkState<Cursor, Page>,\n readonly FetchPageCmd<Cursor>[],\n ] {\n const [resilience, cmds] = rc.attempt(s.resilience, PAGE_KEY, cursor, at);\n return [withResilience(s, resilience), cmds];\n }\n\n /**\n * Issue the page fetch for a freshly-armed paginator transition, else a pure\n * no-op. `walk` is the result of a paginator verb (`startWalk` / `resumeWalk`)\n * applied to `s.walk`; this states the arm-and-fetch contract ONCE for the two\n * verbs that share it (`start`, `resume`).\n *\n * A paginator verb is a pure no-op on any phase it does not act on — it returns\n * the SAME state object. Reference identity (not phase) is the honest \"did the\n * verb arm a fresh fetch?\" check: a walk already `fetching` would also satisfy\n * `phase === \"fetching\"` yet must NOT re-issue a fetch (page-one skip /\n * double-fetch). So we proceed ONLY when the verb produced a fresh state. The\n * `phase === \"fetching\"` narrow that follows is therefore always true here\n * (both verbs only ever produce a `fetching` arm), and also narrows `.cursor`\n * for the type checker. PURE — `at` threads into the fetch gate.\n */\n function armAndFetch(\n s: PaginatedWalkState<Cursor, Page>,\n walk: PaginatorState<Cursor>,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n if (walk === s.walk || walk.phase !== \"fetching\") {\n return [s, []];\n }\n return fetch({ ...s, walk }, walk.cursor, at);\n }\n\n // === Verb: start =========================================================\n\n /**\n * Begin the walk: arm the paginator's first cursor and issue the first page\n * fetch (gated through resilience). On any non-`idle` paginator phase this is\n * a no-op — re-starting a walk in flight would skip back to page one and\n * double-count, so a stray `start` after launch is absorbed (the paginator's\n * own `start` guard). PURE — `at` threads into the fetch gate.\n */\n function start(\n s: PaginatedWalkState<Cursor, Page>,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // `startWalk` is a pure no-op on any non-`idle` phase (same reference); a\n // fresh `fetching(firstCursor)` arm → issue the first page fetch.\n return armAndFetch(s, startWalk(s.walk, paginatorPolicy), at);\n }\n\n // === Verb: pageOk ========================================================\n\n /**\n * Record a successfully fetched `page` and decide what comes next. PURE —\n * `at` is the cache / next-fetch clock.\n *\n * 1. Settle the resilient call OK (closes the breaker, fills the cache,\n * resets the page-fetch retry counter) — inherited from resilient-call.\n * 2. Fold the page through the paginator: add `pageSize(page)` to `seen`,\n * bump `pages`, and branch on `nextCursor(page)`:\n * - `null` → the API is exhausted → paginator goes `done`.\n * - a cursor → advance, then either fetch it (backpressure room) or\n * park in `paused` (high-water mark reached).\n * 3. Emit the consumer's `onPage(page)` Cmds either way, PLUS the next\n * page-fetch effect when the paginator stays `fetching`.\n *\n * A `pageOk` that arrives when the paginator is not `fetching` (a late\n * duplicate, a double-dispatch) is absorbed by `recordPage` — the cursor does\n * not advance and `seen` is not double-counted; only the resilient settle +\n * `onPage` Cmds happen. The walk is a coordination aid, not a correctness\n * gate.\n */\n function pageOk(\n s: PaginatedWalkState<Cursor, Page>,\n page: Page,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // 1) Settle the underlying resilient call as a success.\n const [resilience] = rc.succeed(s.resilience, PAGE_KEY, {\n type: MsgType.ResilientOk,\n key: PAGE_KEY,\n result: page,\n at,\n });\n\n // 2) Advance the paginator with the page's contribution + the next cursor.\n const count = config.pageSize ? config.pageSize(page) : 1;\n const next = config.nextCursor(page);\n const walk = recordPage(s.walk, count, next, paginatorPolicy);\n\n const advanced: PaginatedWalkState<Cursor, Page> = { walk, resilience };\n\n // 3) The consumer's per-page work, always emitted on a successful page.\n const pageCmds: readonly EmittedCmd[] = config.onPage(page);\n\n // If the paginator stayed `fetching`, issue the next page fetch (gated).\n if (walk.phase === \"fetching\") {\n const [withFetch, fetchCmds] = fetch(advanced, walk.cursor, at);\n return [withFetch, [...pageCmds, ...fetchCmds]];\n }\n\n // `done` (exhausted) or `paused` (backpressure) → no further fetch now.\n return [advanced, pageCmds];\n }\n\n // === Verb: pageErr =======================================================\n\n /**\n * Record a failed page fetch and back off via resilience. PURE — `at` stamps\n * the breaker trip + the retry-delay base.\n *\n * Defers to the resilient-call `fail` verb: it trips the breaker and either\n * schedules a retry (paginator stays parked on the SAME cursor — no advance)\n * or settles the resilient call `failed` once retries are exhausted. The\n * paginator is deliberately untouched: a failed fetch must NOT advance the\n * cursor or skip a page. When the retry timer later fires, `onTimer` re-issues\n * the same page fetch.\n *\n * GUARDED by the paginator phase, exactly as `pageOk` is guarded by\n * `recordPage`'s `fetching`-only check. A `pageErr` only makes sense while a\n * page fetch is outstanding (`walk.phase === \"fetching\"`). A stray `pageErr`\n * that arrives after the walk has settled — `done` (exhausted), `paused`\n * (backpressure), or `idle` (not started) — has NO outstanding fetch to fail.\n * Without this guard it would still call `rc.fail`, which trips the SHARED\n * circuit breaker (dinging a healthy upstream) and overwrites the settled\n * `PAGE_KEY` resilient slot with `failed` (clobbering the succeeded result a\n * resume would otherwise observe). So a stray `pageErr` is absorbed as a pure\n * no-op — identity preserved — matching `pageOk`'s \"the walk is a coordination\n * aid, not a correctness gate\" stance.\n */\n function pageErr(\n s: PaginatedWalkState<Cursor, Page>,\n error: unknown,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // Only a `fetching` paginator has an outstanding page fetch to fail. On any\n // other phase the fail would corrupt shared resilience state for a fetch\n // that is not in flight — absorb it as a pure no-op.\n if (s.walk.phase !== \"fetching\") {\n return [s, []];\n }\n const [resilience, cmds] = rc.fail(s.resilience, PAGE_KEY, {\n type: MsgType.ResilientErr,\n key: PAGE_KEY,\n error,\n at,\n });\n return [withResilience(s, resilience), cmds];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A retry / deadline timer fired. Defers to resilient-call's `onTimer`: a\n * retry timer re-runs the gate for the page-fetch's remembered cursor (re-\n * issuing the SAME page); a deadline timer settles the resilient call failed.\n * The paginator stays parked on its cursor throughout — only `pageOk` advances\n * it. A stale fire is a no-op (inherited). PURE.\n *\n * GUARDED by the paginator phase, for the same reason as `pageErr`. A timer is\n * only meaningful while a page fetch is outstanding (`walk.phase ===\n * \"fetching\"`). A stray timer that fires after the walk has settled (`done` /\n * `paused` / `idle`) would otherwise re-enter `rc.onTimer` — re-issuing a fetch\n * the paginator no longer wants (a deadline timer could even overwrite the\n * settled `PAGE_KEY` slot with a deadline `failed`). On any non-`fetching`\n * phase the timer is absorbed as a pure no-op.\n */\n function onTimer(\n s: PaginatedWalkState<Cursor, Page>,\n msg: PaginatedWalkTimerMsg,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n if (s.walk.phase !== \"fetching\") {\n return [s, []];\n }\n const [resilience, cmds] = rc.onTimer(s.resilience, msg);\n if (resilience === s.resilience) {\n // Inherited no-op identity → keep our slice identity too (pure no-op).\n return [s, []];\n }\n return [withResilience(s, resilience), cmds];\n }\n\n // === Verb: drain =========================================================\n\n /**\n * Acknowledge that the consumer finished processing `n` items, lowering the\n * paginator's backpressure gauge (`seen`). PURE — no clock. Delegates to\n * `paginator.drain`: subtracts `n` (floored at 0), leaves the phase untouched.\n *\n * The OTHER half of the backpressure valve `highWaterMark` opens: `recordPage`\n * parks the walk in `paused` once `seen >= highWaterMark`, and `drain` is how a\n * consumer lowers `seen` back below the mark so `resume` can re-open the fetch.\n * Without it `highWaterMark` would be a one-way valve — a walk could pause but\n * never continue. Emits no Cmds: draining only frees headroom; `resume` is the\n * separate, explicit \"continue now\" decision.\n */\n function drain(\n s: PaginatedWalkState<Cursor, Page>,\n n: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n const walk = drainWalk(s.walk, n);\n if (walk === s.walk) {\n // Pure no-op identity (n <= 0, or seen already at floor).\n return [s, []];\n }\n return [{ ...s, walk }, []];\n }\n\n // === Verb: resume ========================================================\n\n /**\n * Re-open the backpressure valve after a `drain`: if the walk is `paused` and\n * `seen` has dropped below `highWaterMark`, re-arm the parked cursor and issue\n * its page fetch (gated through resilience). PURE — `at` threads into the fetch\n * gate. Delegates the phase decision to `paginator.resume`:\n *\n * - `paused` AND `seen < highWaterMark` → `fetching(cursor)` → emit the fetch.\n * - `paused` but still over the mark → no-op (the valve stays shut until a\n * further `drain` frees real headroom).\n * - any other phase → no-op (nothing to resume).\n *\n * Together with `drain` this completes the valve: `recordPage` shuts it\n * (`paused`), `drain` lowers the gauge, `resume` re-opens it and fires the next\n * page fetch — the same `fetch` helper `start` / `pageOk` use, so a resumed\n * fetch is gated identically (cache / circuit / rate-limit / retry).\n */\n function resume(\n s: PaginatedWalkState<Cursor, Page>,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // `resumeWalk` is a pure no-op (same reference) on any phase but `paused`, or\n // when `seen` is still at/over the mark. Only a fresh `fetching` arm means\n // the valve actually re-opened — issue the page fetch only then.\n return armAndFetch(s, resumeWalk(s.walk, paginatorPolicy), at);\n }\n\n // === Derived predicates ==================================================\n\n /**\n * Whether the walk has finished — the API returned a null next cursor and the\n * paginator is `done`. PURE, read-only. Lets a consumer's reducer fire a\n * \"walk complete\" Cmd (`if (walk.isDone(s.walk)) …`) without re-checking the\n * paginator phase by hand.\n */\n function isComplete(s: PaginatedWalkState<Cursor, Page>): boolean {\n return isDone(s.walk);\n }\n\n /**\n * The terminal page-fetch failure that stranded the walk, or `undefined` if no\n * page has terminally failed. PURE, read-only. Errors are data: a page that\n * exhausts its retries (or trips the deadline) settles the `PAGE_KEY` resilient\n * slot to `failed` while the paginator stays parked on its cursor — the walk is\n * stuck and will never advance on its own. Without a way to read that phase the\n * failure is unobservable: the walk simply stops emitting fetches with no\n * signal. `failure(s)` surfaces the settled error (the same plain-data sentinel\n * resilient-call records) so a consumer can fire a \"walk dead\" Cmd, alert, or\n * re-`start` a fresh walk.\n */\n function failure(s: PaginatedWalkState<Cursor, Page>): unknown {\n const call = s.resilience.calls[PAGE_KEY];\n return call?.phase === \"failed\" ? call.error : undefined;\n }\n\n /**\n * Whether the walk is STUCK: a page fetch has terminally `failed` yet the\n * paginator is still parked on a cursor (not `done`), so no further fetch will\n * ever be issued without intervention. PURE, read-only.\n *\n * This is the dead-walk predicate the consumer polls (`if (walk.isStuck(s)) …`)\n * to distinguish a healthy finish (`isComplete`) from a silent death. A walk is\n * stuck precisely when the resilient slot settled `failed` AND the paginator\n * has NOT reached `done` — a terminal failure on the final page that also\n * finished the walk is `isComplete`, not stuck. Pairs with `failure(s)`, which\n * hands back the error that stuck it.\n */\n function isStuck(s: PaginatedWalkState<Cursor, Page>): boolean {\n return s.resilience.calls[PAGE_KEY]?.phase === \"failed\" && !isDone(s.walk);\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions: exactly the resilient-call subs for the page\n * fetch — a retry timer while the fetch is `waiting_retry`, and (with the\n * `deadline` brick) a per-fetch deadline timer while it is active. Reconciled\n * by id, so a phase change cancels the matching timer automatically. Wire\n * `subscribe: { deadline: subscribeDeadline }` (re-exported below).\n */\n function subs(s: PaginatedWalkState<Cursor, Page>): readonly DeadlineSub[] {\n return rc.subs(s.resilience);\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handler for the page-fetch effect. Inherited from\n * resilient-call (`resilient_run`): wraps the consumer's `run(cursor)` port\n * via `tryInterpret` (Railway) — success → `resilient_ok` (handled by\n * `pageOk`), failure → `resilient_err` (handled by `pageErr`), each stamped\n * with `Date.now()` at the effect boundary (the ONE permitted clock read).\n *\n * interpret: walk.handlers({ run: (cursor) => api.fetchPage(cursor) })\n *\n * The consumer maps the inherited Msg types to their `pageOk` / `pageErr`\n * reducer cells (the wire shape is `resilient_ok` / `resilient_err`).\n */\n function handlers(ports: PaginatedWalkPorts<Cursor, Page>) {\n return rc.handlers(ports);\n }\n\n return {\n init,\n start,\n pageOk,\n pageErr,\n onTimer,\n drain,\n resume,\n isComplete,\n failure,\n isStuck,\n subs,\n handlers,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice\n * lives at `state.walk`. A convenience for the common single-slice host;\n * consumers with a differently-named field spread by hand. Pure — a thin record\n * rebuild, no clock / RNG.\n */\nexport function liftWalk<\n S extends { walk: PaginatedWalkState<Cursor, Page> },\n Cursor,\n Page,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [PaginatedWalkState<Cursor, Page>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, walk: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives (inherited from resilient-call) so\n * consumers wire one import: `subscribeDeadline` is the `subscribe` cell,\n * `deadlineSub` builds the Sub literal this knob's `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub };\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/idempotency/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-LYLTKJWM.js","sourcesContent":["/**\n * internal/idempotency/idempotent-intake — receive-once intake for webhooks / queue\n * messages: dedupe by key, enqueue the new ones, replay the cached result to\n * the duplicates. The \"process this payload exactly once even though it\n * arrives N times\" knob.\n *\n * **The recipe.** This is a Level-2 composition — it owns no primitive\n * queue/cache logic of its own, it *splices two L1 bricks into one Model slice*\n * and DELEGATES every transition to their VERB SEAMS — `IdempotencyMemory`\n * (`../idempotency/adapter`) and `QueueAdapter` (`../work-queue/adapter`) —\n * so the store ops and the `QueueItem` / `IdempotencyStore` shapes stay\n * insulated behind verbs the intake calls:\n *\n * - `../idempotency/adapter` — the dedupe-by-key + last-result cache. The\n * `seen` half of the slice is its `IdempotencyStore`, keyed by\n * `keyOf(payload)`. A key is recorded once `receive` calls `memory.remember`;\n * `complete` calls `memory.remember` again to swap its cached value from\n * `pending` to the finished `result`. The cache write is the brick's, never\n * re-rolled here.\n * - `../work-queue/adapter` — the pending-work list. The `queue` half of the\n * slice is its `QueueItem<P>[]`. Every status flip routes through the\n * adapter verbs: `receive` enqueues via `queue.enqueue`, `claimNext` stamps\n * the `running` claim via `queue.patch`, `boot` re-arms a dead worker's\n * claim via `queue.patch`. Intake owns only the SELECTION (which item,\n * gated on the done-cache reconcile below) — the array transition is the\n * brick's. The consumer drains it (claim → process → `complete`) at its own\n * pace.\n *\n * Both bricks are pure-state + pure-ops, host- and clock-agnostic by\n * construction, so the combined slice inherits TEA's two non-negotiables:\n *\n * - **Durable** — the slice is a plain serializable value (a record-backed\n * idempotency store + an array of queue items, no `Map`, no closures), so\n * it survives a Durable-Object eviction / reload and rehydrates whole.\n * - **Replayable** — every verb is a pure `(state, …) => [state, Cmd[]]`\n * transition; no `Date.now()`, no `Math.random()`, no I/O inside a verb.\n * `at` (the clock) and `id` (the queue-item id) are *injected* at the call\n * site, so `trace-replay` reconstructs each transition exactly.\n *\n * **The decision lives in a Cmd, not in a return shape.** `receive` cannot\n * \"return\" what it decided (the knob contract pins the shape to\n * `[State, Cmd[]]`), so the decision is *reified as a Cmd* the runtime\n * performs:\n *\n * - first sighting of a key → mark it `pending`, enqueue the payload, emit\n * `IntakeProcess` (go run the work).\n * - a key already `pending` → drop silently (the original is in flight);\n * emit nothing.\n * - a key already `done` → emit `IntakeReplay` carrying the cached\n * result so the duplicate caller gets the SAME response the first one\n * produced, with zero re-execution of the side effect.\n *\n * **Two crash-safety guarantees the verbs enforce (and the unit specs alone\n * missed — see the wired-machine test):**\n *\n * - *A slow worker is never double-enqueued.* The TTL clock starts at\n * `complete`, not at `receive`: a `pending` entry NEVER ages out, so a\n * duplicate of an in-flight key is always dropped no matter how long the\n * worker takes. Only a `done` entry expires (see `liveEntry`).\n * - *A complete-then-crash never re-runs.* `boot` and `claimNext` reconcile\n * the queue against the done cache: a queue item whose key already resolved\n * to `done` in `seen` is skipped (never reset to pending, never claimed),\n * so a worker that finished and cached its result before the DO evicted is\n * not re-executed on resume.\n *\n * Typical wiring (the combinator form — splice ~3 hooks into a host machine):\n *\n * const intake = createIntake<StripeEvent, ChargeResult>({\n * keyOf: (e) => e.id, // Stripe's idempotency key\n * ttlMs: 24 * 60 * 60 * 1000, // a replayed webhook 24h after it COMPLETED is re-run\n * });\n *\n * // in `init`:\n * intake: intake.init(),\n * // in `update`, on the inbound-webhook Msg:\n * const [next, cmds] = intake.receive(state.intake, payload, at, id);\n * // …route `IntakeProcess` to your worker, `IntakeReplay` to the responder.\n * // on the worker's done Msg:\n * const [next, _] = intake.complete(state.intake, key, result, at);\n * // once, after boot (DO reload), before resuming the drain:\n * const [next, _] = intake.boot(state.intake);\n *\n * Escape hatch: reach the raw L1 ops via `../idempotency` and `../../work-queue`\n * and thread them by hand.\n */\n\nimport { z } from \"zod\";\nimport { Cmd, type CmdOf } from \"../../../index\";\nimport type { QueueItem } from \"../../work-queue\";\nimport { queueAdapter } from \"../../work-queue/adapter\";\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\n\n/**\n * A cached intake entry: either a key whose work is still in flight\n * (`pending`) or one whose work has finished and whose `result` is held for\n * replay to later duplicates (`done`). Discriminated on `phase` — `pending`\n * carries no payload, `done` carries the result `R`.\n *\n * Lives inside the `seen` half of the slice (`IdempotencyStore<IntakeEntry<R>>`).\n * It is the dedupe ledger's *value*; the ledger's *key* is `keyOf(payload)`.\n */\nexport type IntakeEntry<R> =\n | { readonly phase: \"pending\" }\n | { readonly phase: \"done\"; readonly result: R };\n\n/**\n * The intake knob's config — `keyOf` derives the idempotency key from a\n * payload, `ttlMs` (optional) ages a *completed* cached key out so a webhook\n * replayed long after the work finished is treated as new and re-processed.\n *\n * **The TTL clock starts at `complete`, not at `receive`.** A `pending` entry\n * (work still in flight) NEVER ages out, no matter how slow the worker is —\n * ageing a pending key would let the duplicate re-enqueue the in-flight\n * payload and double-process it. Only a `done` entry expires, measured from\n * the `at` passed to `complete` (the moment the result was cached). Inside the\n * window a duplicate replays the cached result; at/after the window it is\n * re-accepted and re-processed.\n *\n * Omit `ttlMs` for a cache whose `done` entries never expire by age (keys live\n * forever — fine for a bounded universe of ids, unbounded growth otherwise).\n *\n * The TTL is owned HERE, not delegated to the composed `IdempotencyStore`: the\n * store's uniform per-entry TTL cannot tell `pending` from `done`, so intake\n * keeps the store TTL-free and applies the phase-aware rule itself (see\n * `liveEntry`).\n */\nexport interface IntakeConfig<P> {\n /** Derive the idempotency key from a payload. Must be pure + deterministic. */\n readonly keyOf: (payload: P) => string;\n /**\n * Age a *completed* key out after this many ms, measured from the `at`\n * passed to `complete`. Pending keys never age out.\n */\n readonly ttlMs?: number;\n}\n\n/**\n * The Model slice this knob owns. `seen` is the dedupe + last-result cache\n * (the `../idempotency` brick); `queue` is the pending-work list (the\n * `../work-queue` brick). Both are plain serializable values — the whole slice\n * round-trips through `JSON` so it survives eviction and replays exactly.\n */\nexport interface IntakeState<P, R> {\n readonly seen: IdempotencyStore<IntakeEntry<R>>;\n readonly queue: readonly QueueItem<P>[];\n}\n\n// === Cmds the verbs reify ===\n//\n// The knob contract pins verbs to `(state, …) => [State, Cmd[]]`, so a verb's\n// *decision* (new vs. duplicate-pending vs. duplicate-done) cannot be a return\n// value — it is emitted as a Cmd the runtime's interpret handler performs.\n// Each Cmd is plain data (invariant 1): the payload / key / result it carries\n// are values, never closures.\n//\n// Both are `Cmd.define`d (ADR 0014). The payload `P` / result `R` are the\n// knob's own type parameters, so each def is minted per knob by a factory\n// (`createIntake` calls it once) rather than a module-scope constant — the\n// same shape `resilient-call`'s `runCmdDef<I, R>()` takes. The host performs\n// them and settles nothing back through intake, hence `ok: z.void()` and an\n// empty `err` list.\n\n/** A genuinely-new payload was accepted and enqueued — go run the work. */\nexport function intakeProcessDef<P>() {\n return Cmd.define(\"intake:process\", {\n input: z.custom<{\n /** The dedupe key the payload was filed under. */\n readonly key: string;\n /** The enqueued queue-item id (the same `id` passed to `receive`). */\n readonly itemId: string;\n /** The original payload, for the worker to process. */\n readonly payload: P;\n }>(),\n ok: z.void(),\n err: [],\n });\n}\nexport type IntakeProcessCmd<P> = CmdOf<ReturnType<typeof intakeProcessDef<P>>>;\n\n/**\n * A duplicate of an already-completed key arrived — replay the cached result\n * to the caller instead of re-running the side effect.\n */\nexport function intakeReplayDef<R>() {\n return Cmd.define(\"intake:replay\", {\n input: z.custom<{\n /** The dedupe key whose cached result is being replayed. */\n readonly key: string;\n /** The result the ORIGINAL request produced, recalled from the cache. */\n readonly result: R;\n }>(),\n ok: z.void(),\n err: [],\n });\n}\nexport type IntakeReplayCmd<R> = CmdOf<ReturnType<typeof intakeReplayDef<R>>>;\n\n/** The Cmd union the intake verbs emit. */\nexport type IntakeCmd<P, R> = IntakeProcessCmd<P> | IntakeReplayCmd<R>;\n\n/**\n * Build an idempotent-intake knob from `config`. Returns the uniform L2\n * contract — `init()` + the verbs (`receive`, `complete`, `boot`, `claimNext`).\n * No `subs` / `handlers`: intake owns no timers and does no I/O of its own (the\n * composed bricks are pure-ops; the worker that runs `IntakeProcess` and the\n * responder that runs `IntakeReplay` live in the host's interpret map).\n */\nexport function createIntake<P, R>(config: IntakeConfig<P>) {\n const { keyOf, ttlMs } = config;\n\n // The two store seams, bound once for this knob's `P`/`R`. The intake\n // delegates every queue status flip to `queue.*` and every dedupe-cache\n // transition to `memory.*`, so the `QueueItem<P>` / `IdempotencyStore` shapes\n // are insulated behind the verbs rather than reached by raw ops.\n const queue = queueAdapter<P>();\n const memory = idempotencyMemory<IntakeEntry<R>>();\n\n // The two Cmd constructors, minted once for this knob's `P`/`R`.\n const intakeProcess = intakeProcessDef<P>();\n const intakeReplay = intakeReplayDef<R>();\n\n /**\n * The empty slice: a TTL-free idempotency store (intake owns the TTL itself —\n * see `liveEntry`, because the store cannot tell `pending` from `done`) plus\n * an empty work queue. The rehydrate path is the host's `init(loaded)` — it\n * passes the persisted slice straight back, this is only the fresh-boot value.\n */\n function init(): IntakeState<P, R> {\n return {\n seen: memory.init(),\n queue: [],\n };\n }\n\n /**\n * The live cached entry for `key` at clock reading `at`, or `undefined` if\n * the key is absent or its `done` window has elapsed. This is intake's\n * phase-aware expiry rule, applied HERE rather than by the store's uniform\n * per-entry TTL:\n *\n * - **`pending`** — work is in flight; NEVER expires. Ageing a pending key\n * would let a slow worker's payload be re-enqueued and double-processed.\n * - **`done`** — expires `ttlMs` after the `atMs` the result was cached at\n * (the `at` passed to `complete`). Half-open, matching the store /\n * rate-limit window edge: `at - atMs >= ttlMs` reads as expired. No\n * `ttlMs` ⇒ a `done` entry never expires by age.\n *\n * PURE — reads `state.seen.entries` directly (no `Date.now`), never mutates.\n */\n function liveEntry(\n store: IdempotencyStore<IntakeEntry<R>>,\n key: string,\n at: number,\n ): IntakeEntry<R> | undefined {\n const cached = store.entries[key];\n if (cached === undefined) return undefined;\n const entry = cached.value;\n if (entry.phase === \"pending\") return entry; // in flight — never ages out\n if (ttlMs !== undefined && at - cached.atMs >= ttlMs) return undefined;\n return entry;\n }\n\n /**\n * Whether the queue item `it`'s key has already resolved to a live `done`\n * entry in `seen` (the work completed and its result is cached). Such an\n * item must NOT be re-armed by `boot` or claimed by `claimNext` — doing so\n * re-runs work whose result is already known (the complete-then-crash bug).\n *\n * `at = +Infinity` is the right reading for the reconcile check: a `done`\n * entry counts as done for reconciliation regardless of how far past its\n * replay TTL it is. (TTL governs whether a *duplicate receive* re-processes,\n * not whether a *queued, already-completed* item should re-run — that one\n * never should.)\n */\n function isAlreadyDone(state: IntakeState<P, R>, it: QueueItem<P>): boolean {\n return state.seen.entries[keyOf(it.input)]?.value.phase === \"done\";\n }\n\n /**\n * Receive a payload at clock reading `at`, filing it under the queue-item\n * id `id`. PURE — returns a NEW slice; the input is untouched.\n *\n * Three outcomes, each reified as a Cmd (see the Cmd union above):\n *\n * 1. **New key** (`liveEntry` is absent at `at`) — `remember` the key as\n * `pending`, `enqueue` the payload onto the work queue, and emit\n * `intake:process`. The only path that grows either half of the slice.\n * 2. **Duplicate, still pending** — the original is in flight. Drop\n * silently: the slice is returned unchanged and no Cmd is emitted, so\n * the duplicate neither re-enqueues nor double-processes. A pending\n * entry never ages out, so this holds no matter how slow the worker is.\n * 3. **Duplicate, already done (within TTL)** — emit `intake:replay`\n * carrying the cached `result` so the caller gets the original response.\n * The slice is unchanged (a replay touches neither cache nor queue). A\n * `done` key past its `ttlMs` window reads as absent and falls to (1).\n *\n * `id` is injected (work-queue's `genId` reads `Date.now()` / `crypto`, so\n * it cannot be called inside a pure verb). Generate it at the effect\n * boundary and thread it in; it becomes the enqueued item's id AND the\n * `itemId` on the emitted `intake:process` Cmd, so the worker can address the\n * exact queue item it must `complete`.\n */\n function receive(\n state: IntakeState<P, R>,\n payload: P,\n at: number,\n id: string,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const key = keyOf(payload);\n\n // Duplicate path — the key has a live entry at `at` (pending never ages;\n // done ages only past `ttlMs` measured from `complete`).\n const entry = liveEntry(state.seen, key, at);\n if (entry !== undefined) {\n if (entry.phase === \"done\") {\n // Replay the original result; touch nothing.\n return [state, [intakeReplay({ key, result: entry.result })]];\n }\n // phase === \"pending\": original is in flight — drop silently.\n return [state, []];\n }\n\n // New path — record the key as pending, enqueue the payload, process it.\n const seenNext = memory.remember(state.seen, key, { phase: \"pending\" }, at);\n const { next: queueNext, item } = queue.enqueue(\n state.queue,\n payload,\n at,\n id,\n );\n const nextState: IntakeState<P, R> = { seen: seenNext, queue: queueNext };\n return [nextState, [intakeProcess({ key, itemId: item.id, payload })]];\n }\n\n /**\n * Record the finished `result` for `key` at clock reading `at`. PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds (completion is\n * a pure cache write; the host marks the queue item done via the work-queue\n * adapter at the effect boundary).\n *\n * Swaps the key's cached entry from `pending` to `done` so every subsequent\n * `receive` of the same key replays this `result`. `remember` stamps the\n * entry's `atMs` to `at`, which is the moment intake's `done`-TTL clock\n * starts (see `liveEntry`): a completed key stays replayable for `ttlMs`\n * past completion, not past first receipt.\n *\n * `complete` does NOT remove the queue item — the queue is the host's drain\n * ledger, and which items leave it (and when) is the host's call via the\n * work-queue adapter. `complete` owns the cache half only.\n */\n function complete(\n state: IntakeState<P, R>,\n key: string,\n result: R,\n at: number,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const seenNext = memory.remember(\n state.seen,\n key,\n { phase: \"done\", result },\n at,\n );\n return [{ ...state, seen: seenNext }, []];\n }\n\n /**\n * Resume after a boot (Durable-Object reload / process restart). PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds.\n *\n * Resets every `running` queue item back to `pending` so the host's drain\n * re-claims it — EXCEPT an item whose key already resolved to `done` in\n * `seen`. That item's worker finished and cached its result before the crash;\n * re-arming it would re-run completed work (the complete-then-crash bug). It\n * is left as-is (and `claimNext` will likewise skip it), so the host drains\n * it via `markDone` without ever re-executing the side effect.\n *\n * A `running` item still `pending` in the cache is one a now-dead worker\n * claimed but never finished — that one IS reset, so it gets re-claimed.\n *\n * Returns the same reference when nothing changed (no running item needed\n * re-arming) so the host can skip a redundant save.\n *\n * Call once from a `boot` Msg the host dispatches after `run(...)` returns —\n * never from `init`'s rehydrate branch (that branch must be a pure\n * passthrough; see the substrate's `init` contract).\n */\n function boot(\n state: IntakeState<P, R>,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n // Selection lives HERE (the done-cache reconcile is intake's own rule); the\n // status FLIP is delegated to the `QueueAdapter`. A blanket reset would\n // re-arm completed-then-crashed items (it cannot see the done cache), so\n // intake picks the items and `queue.patch` performs each transition — the\n // same spread the work-queue lifecycle uses, not a re-rolled one.\n let items = state.queue;\n let changed = false;\n for (const it of state.queue) {\n if (it.status !== \"running\") continue;\n // Reconcile against the done cache: a completed-then-crashed item must\n // NOT be re-armed (it would re-run already-cached work).\n if (isAlreadyDone(state, it)) continue;\n const flip = queue.patch(items, it.id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n }));\n items = flip.next;\n changed = changed || flip.changed;\n }\n if (!changed) return [state, []];\n return [{ ...state, queue: items }, []];\n }\n\n /**\n * Claim the next pending queue item, flipping it `running` and stamping\n * `startedAt` to `at`. PURE — returns a NEW slice plus the claimed item, or\n * `null` if nothing claimable remains.\n *\n * \"Claimable\" is a pending item WHOSE KEY IS NOT ALREADY `done` in `seen`.\n * An item whose key completed before a crash (its `intake:process` ran, work\n * finished, result cached, but the item never left the queue) is skipped —\n * claiming it would re-run already-cached work. Such items are inert here;\n * the host drains them via `markDone`.\n *\n * This is the drain side of the intake, the mirror of `receive`'s enqueue.\n * It is a pure helper (not a verb that emits Cmds) because \"what to do with\n * the claimed item\" is the host's decision — typically: dispatch the worker,\n * await it, then call `complete`. Threading it through the slice keeps the\n * claim durable: the `running` stamp survives eviction, and `boot` re-arms\n * it if the worker died.\n */\n function claimNext(\n state: IntakeState<P, R>,\n at: number,\n ): {\n readonly state: IntakeState<P, R>;\n readonly claimed: QueueItem<P>;\n } | null {\n // Selection lives HERE (skip an item whose key already resolved `done`);\n // the status FLIP is delegated to the `QueueAdapter`. A blind `queue.claim`\n // would take the first PENDING item regardless of the done cache, which\n // would re-run already-cached work — so intake selects the eligible item and\n // `queue.patch` performs the running-stamp transition (the work-queue\n // lifecycle's own spread, not a re-rolled one).\n const target = state.queue.find(\n (it) => it.status === \"pending\" && !isAlreadyDone(state, it),\n );\n if (target === undefined) return null;\n // The patch closure is the single place the claimed item is materialized;\n // capture it here so the result needs no re-scan of the new queue.\n let claimed: QueueItem<P> = target;\n const { next } = queue.patch(state.queue, target.id, (item) => {\n claimed = { ...item, status: \"running\", startedAt: at };\n return claimed;\n });\n return { state: { ...state, queue: next }, claimed };\n }\n\n return { init, receive, complete, boot, claimNext };\n}\n"]}
|