@demlik/tea 0.15.0 → 0.17.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 -37
- 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-4NTCPTNQ.js +64 -0
- package/dist/chunk-4NTCPTNQ.js.map +1 -0
- package/dist/{chunk-3B5X7SBB.js → chunk-4PXNZ5UY.js} +11 -9
- package/dist/chunk-4PXNZ5UY.js.map +1 -0
- package/dist/{chunk-AYS5N6WY.js → chunk-6XBEYZME.js} +62 -172
- package/dist/chunk-6XBEYZME.js.map +1 -0
- package/dist/chunk-C4U5D5HP.js +380 -0
- package/dist/chunk-C4U5D5HP.js.map +1 -0
- package/dist/{chunk-QPEVCJ5Q.js → chunk-DFUEAA6L.js} +13 -6
- package/dist/chunk-DFUEAA6L.js.map +1 -0
- package/dist/{chunk-22POWGFA.js → chunk-GWWCCSDH.js} +46 -20
- package/dist/chunk-GWWCCSDH.js.map +1 -0
- package/dist/chunk-K4ETBNKS.js +733 -0
- package/dist/chunk-K4ETBNKS.js.map +1 -0
- package/dist/chunk-KWNCM6XM.js +539 -0
- package/dist/chunk-KWNCM6XM.js.map +1 -0
- package/dist/{chunk-V46HSVCK.js → chunk-LYVOGOBC.js} +3 -3
- package/dist/{chunk-V46HSVCK.js.map → chunk-LYVOGOBC.js.map} +1 -1
- package/dist/{chunk-VTQI2NHP.js → chunk-OPH6Y4UT.js} +9 -8
- package/dist/chunk-OPH6Y4UT.js.map +1 -0
- package/dist/{chunk-Q5CQTBFA.js → chunk-OYR7ENKU.js} +20 -30
- package/dist/chunk-OYR7ENKU.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-QWIPH4YH.js +98 -0
- package/dist/chunk-QWIPH4YH.js.map +1 -0
- package/dist/{chunk-TCLBCERH.js → chunk-XBZQT2TD.js} +238 -212
- package/dist/chunk-XBZQT2TD.js.map +1 -0
- package/dist/{core-DnzqWy6u.d.ts → core-rDbOaJTN.d.ts} +236 -140
- package/dist/do/index.d.ts +37 -33
- package/dist/do/index.js +21 -14
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-CWzA-o2B.d.ts → durable-effects-CNaw97Be.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 -59
- 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-KXqTRJyb.d.ts → index-BOrsbdAf.d.ts} +19 -96
- package/dist/{index-DrIRb_Ch.d.ts → index-B_BxplWi.d.ts} +1 -1
- package/dist/index-CWmsXNdX.d.ts +207 -0
- package/dist/{index-C2DwkjlP.d.ts → index-Cberb5Rr.d.ts} +57 -45
- package/dist/{index-CSyMYDAB.d.ts → index-CcrKLtse.d.ts} +1 -1
- package/dist/{index-CuNO9m8i.d.ts → index-Cx0F2M34.d.ts} +14 -16
- package/dist/{index-HSYgG2Yt.d.ts → index-DaX_P35Q.d.ts} +247 -211
- package/dist/{index-CA_7I5g9.d.ts → index-q1PoukOZ.d.ts} +137 -246
- package/dist/index.d.ts +121 -385
- package/dist/index.js +2 -1
- package/dist/jev/index.d.ts +133 -97
- package/dist/jev/index.js +205 -161
- package/dist/jev/index.js.map +1 -1
- package/dist/{machine-DGlPWCWI.d.ts → machine-BcfCFMYF.d.ts} +171 -288
- 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 -22
- 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 +10 -12
- 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-D8VbH-VH.d.ts → protocol-ClRZBhHQ.d.ts} +7 -8
- 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 -456
- package/dist/resilience/index.js +37 -483
- package/dist/resilience/index.js.map +1 -1
- package/dist/{runtime-types-CMF13BBh.d.ts → runtime-types-B62Ucnxr.d.ts} +132 -389
- package/dist/testing/index.d.ts +33 -113
- package/dist/testing/index.js +39 -181
- 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-BseNPIhr.d.ts → types-DPm5YCDQ.d.ts} +2 -2
- package/dist/work-queue/index.d.ts +5 -6
- package/package.json +20 -7
- package/dist/chunk-22POWGFA.js.map +0 -1
- package/dist/chunk-3B5X7SBB.js.map +0 -1
- package/dist/chunk-66PY5WYF.js +0 -1816
- package/dist/chunk-66PY5WYF.js.map +0 -1
- package/dist/chunk-AYS5N6WY.js.map +0 -1
- package/dist/chunk-L3TND7LA.js +0 -97
- package/dist/chunk-L3TND7LA.js.map +0 -1
- package/dist/chunk-MYXFNWAJ.js.map +0 -1
- package/dist/chunk-Q5CQTBFA.js.map +0 -1
- package/dist/chunk-QPEVCJ5Q.js.map +0 -1
- package/dist/chunk-TCLBCERH.js.map +0 -1
- package/dist/chunk-VTQI2NHP.js.map +0 -1
- package/dist/index-ZCMhnVKN.d.ts +0 -272
package/dist/timing/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/internal/timing/throttle/index.ts","../../src/internal/timing/throttled-input/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * internal/timing/throttle — a timer-based call transformer that caps invocation\n * to at most once per `ms` window.\n *\n * Sibling of `../debounce` — the two share a SYMMETRIC surface\n * (`Throttled<A>` / `Debounced<A>`, both callable + `.cancel()` + `.flush()`,\n * both `{ leading?, trailing? }`) so they read as a pair. They are kept in\n * separate files with NO cross-import: each is a self-contained timer, and the\n * coalesce-vs-drop policies differ enough that sharing code would hide the\n * distinction. The contrast IS the lesson:\n *\n * - **debounce** waits for quiet — fires once AFTER a burst settles.\n * - **throttle** enforces a rate — fires AT MOST once per `ms` while the\n * burst is ongoing, dropping the calls in between.\n *\n * Layering with `../throttled-input`: `throttle` and `debounce` are\n * the two host-boundary PRIMITIVES — two distinct algorithms\n * (at-most-once-per-window vs collapse-a-burst) behind one signature shape.\n * `throttled-input` is NOT a third primitive: it is a TEA machine composed\n * OVER them (the same semantics, reimplemented as pure, durable Model state)\n * that re-exports both. Wrapping a raw host event → these primitives; durable, replayable\n * gating inside a machine → `throttled-input` (see its module doc for the full\n * layering map).\n *\n * WHAT THIS IS NOT — and why it lives outside `update`:\n *\n * - NOT a reducer / Transitions cell. Reducers are PURE (invariant 2 — same\n * inputs → same outputs, no `Date.now`, no timers). A throttle holds a live\n * `setTimeout` handle and a window cursor; folding it into `update` would\n * smuggle a timer into the pure core.\n * - NOT a Sub. A Sub is a continuous SOURCE of Msgs the substrate reconciles\n * by id every transition (invariant 4). A throttle is a SINK transformer:\n * it wraps a `void`-returning handler (the canonical one is\n * `Runtime.dispatch`) so a high-frequency HOST event is rate-limited BEFORE\n * it ever becomes a Msg.\n *\n * It is a higher-order function at the host boundary (pattern 14 — TEA owns\n * state, JS owns timing). The host attaches the wrapped function to a\n * high-frequency event source (`mousemove`, `scroll`, `pointermove`, a game\n * loop) and the substrate downstream only ever sees the rate-limited Msg:\n *\n * const onMoved = throttle(\n * (e: PointerEvent) =>\n * runtime.dispatch({ type: \"UserMovedCursor\", x: e.clientX, y: e.clientY }),\n * 16, // ~60fps cap\n * );\n * window.addEventListener(\"pointermove\", onMoved);\n * // cleanup: onMoved.cancel(); window.removeEventListener(\"pointermove\", onMoved);\n *\n * Internal since #47 — not published on any subpath; reached from inside the\n * package as `internal/timing/throttle`. Same one-shape-per-package rule as\n * `rate-limit`, `deadline`, and `work-queue`.\n *\n * Uses the universal `setTimeout` / `clearTimeout` timer idiom (same discipline\n * as `subs/from-timeout.ts`): exactly one live handle at a time, the window\n * timer clears itself on expiry, `cancel` / `flush` clear and null it so a\n * stale callback can never run.\n */\n\n/**\n * A throttled wrapper around `fn`. Callable with the SAME argument tuple as\n * `fn` — `(...args: A) => void` — plus two control methods that share names and\n * shapes with `Debounced<A>` so the two modules read as a symmetric pair.\n */\nexport interface Throttled<A extends readonly unknown[]> {\n /** Invoke `fn` if the window is open, else record args for the trailing fire. */\n (...args: A): void;\n /**\n * Drop any pending trailing fire WITHOUT invoking `fn`, and close the active\n * window. Clears the timer, forgets the captured trailing args, and resets\n * the cursor so the next call fires immediately on the leading edge (if\n * `leading` is enabled). Idempotent.\n *\n * The host-cleanup partner of `removeEventListener`: cancel the pending fire\n * when the component unmounts / the listener detaches, so a queued\n * `dispatch` can't land after teardown.\n */\n cancel(): void;\n /**\n * Fire any pending trailing call IMMEDIATELY with its latest captured args,\n * then close the window. No-op when nothing is pending. Use to force the last\n * dropped call out early — e.g. flush the final cursor position when a drag\n * ends, so the rest state isn't a stale mid-drag sample.\n */\n flush(): void;\n}\n\n/**\n * Wrap `fn` so it runs AT MOST once per `ms` window, no matter how often it is\n * called.\n *\n * Leading + trailing (the default): the first call fires `fn` IMMEDIATELY (the\n * leading edge) and opens a `ms` window. Calls during the window are dropped\n * from immediate firing but their args are remembered; when the window closes,\n * `fn` fires ONCE more with the LATEST dropped args (the trailing edge) and a\n * new window opens. A call after the window has already closed leads a fresh\n * window immediately. Spacing every call at least `ms` apart therefore fires\n * each one on its own leading edge.\n *\n * @param fn The handler to rate-limit. Typically `(...) => runtime.dispatch(...)`,\n * but any `(...args: A) => void` works. The return value is\n * ignored — throttle is a fire transformer, not a value pipe.\n * @param ms Window length in milliseconds. `fn` fires at most once per `ms`.\n * @param opts.leading Fire on the FIRST call of a window (the leading edge),\n * immediately. Default `true`.\n * @param opts.trailing Fire once at window END with the latest args dropped\n * during the window. Default `true`.\n *\n * Edge combinations (matching lodash's settled semantics, the de-facto\n * standard the ecosystem rediscovered):\n *\n * - `{ leading: true, trailing: true }` (default): immediate fire + a\n * trailing fire per window while calls keep arriving. The trailing fire is\n * SUPPRESSED when no extra call arrived during the window (a lone call\n * already fired on the leading edge — no stale trailing double-fire).\n * - `{ leading: true, trailing: false }`: only the immediate fire; everything\n * in the window is dropped with no catch-up at the end.\n * - `{ leading: false, trailing: true }`: no immediate fire; the FIRST call\n * opens a window and `fn` fires at the window's end with the latest args.\n * Steady \"sample every `ms`\" behavior with no instant response.\n * - `{ leading: false, trailing: false }`: never fires. Degenerate but legal;\n * we don't throw — the caller asked for a no-op transformer.\n *\n * WHY a default of leading + trailing both `true`: a throttle's job is \"respond\n * now, then keep responding at a steady rate.\" Leading gives the instant\n * response (the first `mousemove` updates immediately); trailing guarantees the\n * FINAL sample of a window isn't lost (the cursor ends where the user actually\n * left it, not one window-width behind). Dropping either loses one of those two\n * guarantees, so both default on.\n */\nexport function throttle<A extends readonly unknown[]>(\n fn: (...args: A) => void,\n ms: number,\n opts?: { leading?: boolean; trailing?: boolean },\n): Throttled<A> {\n const leading = opts?.leading ?? true;\n const trailing = opts?.trailing ?? true;\n\n // Exactly one live handle at a time (the from-timeout discipline). `null`\n // when no window is open — the single source of truth for \"is a window\n // active?\". The window timer arms on a fire and clears itself on expiry; the\n // call path never stacks overlapping timers.\n let handle: ReturnType<typeof setTimeout> | null = null;\n // The latest args seen DURING an open window, held for the trailing fire.\n // Overwritten by every dropped call (the trailing edge fires with the LAST\n // args), cleared the instant we fire / cancel. `null` (not `undefined`) is\n // the empty sentinel because `A` may itself contain `undefined` elements — a\n // present-but-empty tuple is still a pending call, distinct from \"nothing\n // pending\".\n let pendingArgs: A | null = null;\n\n function clearHandle(): void {\n if (handle !== null) {\n clearTimeout(handle);\n handle = null;\n }\n }\n\n // Window expiry: the `ms` window just closed. If a call arrived during the\n // window (`pendingArgs !== null`) and trailing is enabled, fire it now and\n // open a FRESH window so the rate cap continues across a sustained burst.\n // If nothing was dropped, let the window close (handle → null) so the next\n // call leads a new window immediately.\n function onWindowExpired(): void {\n if (trailing && pendingArgs !== null) {\n const args = pendingArgs;\n pendingArgs = null;\n // Re-arm BEFORE firing so a re-entrant call from inside `fn` sees an open\n // window (and is correctly dropped/queued) rather than leading a second\n // window — keeps the \"at most once per `ms`\" invariant under re-entry.\n handle = setTimeout(onWindowExpired, ms);\n fn(...args);\n } else {\n // No trailing fire owed — close the window.\n handle = null;\n }\n }\n\n const throttled = (...args: A): void => {\n if (handle === null) {\n // Window is closed → this call leads a new window.\n if (leading) {\n fn(...args);\n } else if (trailing) {\n // No leading fire, but trailing wants the first call's args so the\n // window's end has something to fire. Without this, a leading:false\n // throttle would swallow a lone call entirely.\n pendingArgs = args;\n }\n handle = setTimeout(onWindowExpired, ms);\n } else if (trailing) {\n // Window is open → drop this call from immediate firing, but remember its\n // args as the latest candidate for the trailing edge.\n pendingArgs = args;\n }\n // Window open + trailing disabled → pure drop, nothing to remember.\n };\n\n throttled.cancel = (): void => {\n clearHandle();\n pendingArgs = null;\n };\n\n throttled.flush = (): void => {\n // Force the pending trailing fire out now and close the window. Mirrors\n // `onWindowExpired`'s fire branch but is caller-driven, so it tears the\n // timer down rather than re-arming.\n if (pendingArgs !== null) {\n const args = pendingArgs;\n pendingArgs = null;\n clearHandle();\n fn(...args);\n } else {\n // Nothing pending — still close a lingering window so a post-flush call\n // leads cleanly.\n clearHandle();\n }\n };\n\n return throttled;\n}\n","/**\n * internal/timing/throttled-input — gate a high-frequency input stream into a\n * SETTLED, RATE-CAPPED, optionally DEDUPED sequence of emits, as a TEA knob: one\n * config object + a few pre-wired hooks you spread into your machine.\n *\n * Layering (the throttle-family map): `../throttle` and `../debounce` are the\n * two host-boundary PRIMITIVES — two distinct algorithms\n * (at-most-once-per-window vs collapse-a-burst) behind one signature shape.\n * THIS module is the third name but NOT a third primitive: a TEA machine\n * composed OVER them that re-exports both. Three exports, one layering — not\n * duplication. The reuse list below spells out composition vs reimplemented\n * semantic per sibling.\n *\n * What it reuses, and HOW (composition vs. reimplemented semantic — the two are\n * not the same, so this list keeps them apart):\n *\n * - `../cache` — COMPOSED directly. The DEDUPE / memoize layer is `../cache`'s\n * `TtlCache` plus its `get` / `set` / `initCache` ops, called at runtime —\n * not reinvented. While `cacheTtlMs` is set, an input whose `cacheKey` is\n * already cached and unexpired is served from the cache and emits NOTHING\n * (the downstream effect ran once for that key within its TTL); a fresh emit\n * writes the value into the cache so the next identical input within\n * `cacheTtlMs` is suppressed. The optional eviction Sub is the consumer's to\n * wire if they want memory reclaim (this knob's correctness needs only TTL\n * reads).\n * - `../deadline` — COMPOSED directly. The settle timer IS a `../deadline`\n * Sub: `subsFor` builds it via `deadlineSub`, and the subscribe cell +\n * Msg constructor are RE-EXPORTS of `../deadline`'s own (`subscribeDeadline`\n * → `subscribeThrottledInput`, `deadlineExceeded` → `throttledInputSettled`).\n * The timer lifecycle stays owned by `../deadline`; this knob redraws none\n * of it.\n * - `../throttle` / `../debounce` — TYPE + SEMANTIC reuse only, NOT composed.\n * Those siblings are host-boundary SINK transformers that hold a live\n * `setTimeout`; a `setTimeout` in a closure is exactly what a durable,\n * replayable knob must NOT hold. So their CODE is never called here. Instead\n * the RATE-cap semantic (\"at most once per `ms`\", leading-edge) and the\n * SETTLE semantic (\"wait until quiet\", trailing-edge) are REIMPLEMENTED as\n * pure Model state: `lastAt` is the durable rate cursor (half-open cutoff\n * `at - lastAt >= throttleMs`), and `pending` + the `../deadline` Sub realize\n * the debounce window inside `update`. The `throttle` / `debounce`\n * transformers themselves are RE-EXPORTED (the only reuse of their actual\n * code) so a consumer can pre-throttle / pre-coalesce a genuinely bursty\n * HOST event at the boundary BEFORE it ever becomes an `input` Msg — a\n * separate, optional escape hatch, not part of this knob's gates.\n *\n * \"Omit a brick, omit its gate\" (the resilient-call config rule): every gate\n * field is optional. Omit `throttleMs` → no rate cap (every input passes the\n * rate gate). Omit `debounceMs` → no settle wait (an input that clears the rate\n * gate emits IMMEDIATELY rather than being held). Omit `cacheTtlMs` → no cache\n * slice at all (the `cache` field is absent; no dedupe). A config with all three\n * omitted is a pass-through: `input` always emits.\n *\n * Why this is a knob and not hand-wired logic: the managed state (`lastAt`,\n * `pending`, `cache`) lives in the Model, so it survives DO eviction / reload\n * (durable) and every transition is still a Msg through `update` (replayable). A\n * closure holding a `setTimeout` and a mutable \"last value\" would break both —\n * banned. `input` / `onFlush` are PURE: time arrives as an `at` parameter, never\n * `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const search = createThrottledInput<string, Msg>({\n * debounceMs: 200, // wait for the user to stop typing\n * throttleMs: 1_000, // but never fire more than 1/s during a long burst\n * cacheTtlMs: 30_000, // skip a query repeated within 30s\n * cacheKey: (q) => q,\n * emit: (q) => ({ type: \"RunSearch\", query: q }),\n * });\n *\n * // Model: { search: ThrottledInput<string>, ... }\n * // init: (loaded) => [loaded ?? { search: search.init(), ... }, []]\n * // update cells:\n * // QueryTyped: (s, m) => mapInput(s, search.input(s.search, m.value, m.at))\n * // SettleFired: (s, m) => mapInput(s, search.onFlush(s.search, m.atMs))\n * // subscriptions: (s) => search.subs(s.search)\n * // subscribe: { ...search.handlers() }\n *\n * where `mapInput(s, [slice, cmds]) => [{ ...s, search: slice }, cmds]` lifts the\n * slice transition into the consumer's Model. The `RunSearch` Cmd's interpret\n * handler is the consumer's — it is where the settled input meets I/O.\n *\n * NOT a substrate primitive: it depends only on sibling subpaths (`../throttle`,\n * `../debounce`, `../cache`, `../deadline`) and the core `Cmd` / `Sub` types.\n * Internal since #47 — reached from inside the package as\n * `internal/timing/throttled-input`; the L1 escape hatch is those siblings\n * threaded by hand. Same one-shape-per-package rule as `batch-window` and `poller`.\n */\n\nimport type { Cmd, Sub } from \"../../../index\";\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../../resilience/cache\";\nimport { type DeadlineSub, deadlineSub } from \"../../resilience/deadline\";\n\n// `../throttle` + `../debounce` — host-boundary pre-transformers (see file\n// header). Re-exported so a consumer that wants to throttle/debounce a bursty\n// HOST source BEFORE it becomes an `input` Msg reaches them from the same\n// import as the durable gate.\nexport { type Debounced, debounce } from \"../debounce\";\nexport { type Throttled, throttle } from \"../throttle\";\n\n/**\n * The gate fields every throttled-input config shares, regardless of whether\n * dedupe is on. The two dedupe variants below extend this with mutually\n * exclusive cache fields.\n *\n * `emit` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps a settled\n * value to the single Cmd the runtime performs when the gates let a value\n * through. Keeping it `(value) => Cmd` (not `(value) => void`) is what keeps the\n * trigger replayable — the emit is a Cmd in the transition's output, never a\n * side effect inside the verb (mirrors `batch-window`'s `flush`).\n */\ninterface ThrottledInputGates<V, C extends Cmd> {\n /**\n * Rate cap, in milliseconds. An input that lands within `throttleMs` of the\n * last EMIT (`at - lastAt < throttleMs`) is dropped from immediate firing —\n * the leading-edge \"at most once per `throttleMs`\" rate-cap rule, applied to\n * emits. The half-open cutoff (`>=` passes) matches `../throttle` and\n * `../cache`'s window-edge convention. Omit for no rate cap.\n *\n * Interacts with `debounceMs`: when both are set, a rate-blocked input is\n * still HELD as `pending` (so the settle timer eventually trails it out at a\n * legal moment) rather than silently dropped — the trailing-edge guarantee\n * that the FINAL sample of a window is never lost.\n */\n readonly throttleMs?: number;\n /**\n * Settle wait, in milliseconds. An input that clears the rate gate is not\n * emitted immediately; it is held as `pending` and a debounce deadline is\n * armed at `pendingAt + debounceMs`. A fresh input re-arms (slides) the\n * window. `onFlush` emits the pending value when the window finally expires.\n * Omit for no settle wait — a passing input emits on arrival.\n */\n readonly debounceMs?: number;\n /**\n * Map a settled value to the single Cmd the runtime performs. Called with the\n * value the gates let through; the returned Cmd is what `input` / `onFlush`\n * emit. PURE constructor — no I/O, no clock; the I/O happens later in the\n * consumer's interpret handler for the returned Cmd.\n */\n readonly emit: (value: V) => C;\n}\n\n/**\n * Configuration for a throttled input — the knob. Pure config, no mutable state\n * (the `ThrottledInput<V>` slice carries that). One config object is shared\n * across every input of one logical stream (often a module-scope const).\n *\n * A DISCRIMINATED UNION on the presence of dedupe, so the type system makes a\n * cache-key collision UNREPRESENTABLE (\"omit a brick, omit its gate\"):\n *\n * - **No dedupe** (`ThrottledInputNoCache`): `cacheTtlMs` absent ⇒ no cache\n * slice, no dedupe. `cacheKey` is statically forbidden here (`never`) — a\n * key with no cache to feed is dead config, so the type rejects it rather\n * than silently ignoring it.\n * - **Dedupe** (`ThrottledInputWithCache`): `cacheTtlMs` set ⇒ `cacheKey` is\n * REQUIRED. There is no `String(value)` default to fall back to, so a\n * structured value can NEVER collapse two distinct values to one slot (the\n * `\"[object Object]\"` collision). The consumer must state the identity that\n * keys the cache; a primitive stream still trivially passes `(v) => v` /\n * `String`, but it does so EXPLICITLY.\n */\nexport type ThrottledInputConfig<V, C extends Cmd> =\n | ThrottledInputNoCache<V, C>\n | ThrottledInputWithCache<V, C>;\n\n/**\n * The no-dedupe config variant: gates only, no cache. `cacheTtlMs` is absent\n * and `cacheKey` is statically `never` so a stray key (which would feed no\n * cache) is a type error, not a silently-ignored field.\n */\nexport interface ThrottledInputNoCache<V, C extends Cmd>\n extends ThrottledInputGates<V, C> {\n readonly cacheTtlMs?: undefined;\n readonly cacheKey?: never;\n}\n\n/**\n * The dedupe config variant: a per-entry TTL plus a REQUIRED `cacheKey`. An\n * input whose `cacheKey` is already cached and unexpired emits NOTHING (the\n * downstream effect ran once for that key within its TTL); a fresh emit writes\n * the value into the cache.\n *\n * `cacheKey` is required (no `String(value)` default) precisely so a structured\n * value cannot silently collide — the consumer names the identity, the cache\n * keys on exactly that.\n */\nexport interface ThrottledInputWithCache<V, C extends Cmd>\n extends ThrottledInputGates<V, C> {\n /**\n * Dedupe / memoize TTL, in milliseconds. An input whose `cacheKey` is already\n * cached and unexpired emits NOTHING; a fresh emit writes the value into the\n * cache so the next identical input within `cacheTtlMs` is suppressed.\n */\n readonly cacheTtlMs: number;\n /**\n * Key a value for the dedupe cache. REQUIRED whenever `cacheTtlMs` is set:\n * there is no `String(value)` fallback, so a structured value's identity is\n * always the consumer's explicit choice and two distinct values can never\n * collapse to one slot. A primitive stream passes `(v) => v` / `String`.\n */\n readonly cacheKey: (value: V) => string;\n}\n\n/**\n * The Model slice a throttled input owns — its visible slice (the knob\n * principle: managed state lives in the Model, never a closure, so it is durable\n * and replayable).\n *\n * - `lastAt` — the `at` of the most recent EMIT, or `null` before the first\n * emit. The `../throttle` rate cap measures against it: an input\n * within `throttleMs` of `lastAt` is rate-blocked. Persisted so a\n * resumed gate keeps its rate cursor.\n * - `pending` — the value HELD for the settle (debounce) window, paired with the\n * `at` it was held (the anchor the deadline targets), or `null`\n * when nothing is waiting. The single source of truth for \"is a\n * settle window open?\" — the debounce timer Sub arms iff this is\n * non-null.\n * - `cache` — the `../cache` TTL store backing dedupe, present IFF\n * `cacheTtlMs` is configured. Absent (the field omitted) for a\n * gate with no dedupe — no per-op cost, nothing to serialize.\n */\nexport interface ThrottledInput<V> {\n readonly lastAt: number | null;\n readonly pending: PendingInput<V> | null;\n readonly cache?: TtlCache<V>;\n}\n\n/**\n * A value held for the settle window: the `value` itself and the `at` it was\n * held (the anchor the debounce deadline targets, `at + debounceMs`). A distinct\n * type (not a bare `V`) because `V` may itself be `null` — a present-but-null\n * value is still a real pending input, distinct from \"nothing pending\", exactly\n * the `null`-sentinel discipline `../throttle` / `../debounce` use for their\n * `pendingArgs`.\n */\nexport interface PendingInput<V> {\n readonly value: V;\n readonly at: number;\n}\n\n/**\n * The Sub variant a throttled input's settle timer produces — a `../deadline`\n * Sub (so the window fires at the correct ABSOLUTE moment even after a late\n * subscribe / rehydrate, and so a consumer running several gates routes each by\n * `id`). `atMs` is the absolute settle-by instant (`pendingAt + debounceMs`).\n */\nexport type ThrottledInputSub = DeadlineSub;\n\n/**\n * The stable Sub id the gate arms its settle deadline under. A single gate arms\n * exactly one settle timer at a time, so one id suffices; a consumer running\n * several gates on one machine namespaces them by composing the knob under\n * distinct slices with distinct ids (the `subs(state, id)` parameter).\n */\nconst DEFAULT_SUB_ID = \"throttled-input:settle\";\n\n/**\n * The starting slice: no prior emit, nothing pending. The `cache` field is\n * seeded only when `cacheTtlMs` is configured (see `createThrottledInput`); the\n * free `initThrottledInput` leaves it absent (the no-dedupe shape).\n */\nexport function initThrottledInput<V>(): ThrottledInput<V> {\n return { lastAt: null, pending: null };\n}\n\n/**\n * Resolve a value's cache key under a DEDUPE config. PURE. Takes the narrowed\n * `ThrottledInputWithCache` (not the union) because `cacheKey` is only present —\n * and only meaningful — when dedupe is on; there is no `String(value)` default\n * to fall back to, so a structured value's identity is always the consumer's\n * explicit choice and two distinct values can never collapse to one slot.\n */\nfunction keyOf<V, C extends Cmd>(\n config: ThrottledInputWithCache<V, C>,\n value: V,\n): string {\n return config.cacheKey(value);\n}\n\n/**\n * Narrow `config` to its dedupe variant iff a cache slice is actually wired —\n * `cacheTtlMs` set AND a `cache` store present. Returns the narrowed config\n * (carrying the required `cacheKey`) paired with the resolved `cache` store, or\n * `null` when no dedupe is configured. The single place the union is\n * discriminated, so `isCacheHit` / `commitEmit` share one narrowing — and reuse\n * the resolved `cache` — rather than re-deriving the predicate and re-narrowing\n * `state.cache`.\n */\nfunction cacheConfig<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n): {\n readonly config: ThrottledInputWithCache<V, C>;\n readonly cache: TtlCache<V>;\n} | null {\n return config.cacheTtlMs !== undefined && state.cache !== undefined\n ? { config, cache: state.cache }\n : null;\n}\n\n/**\n * Whether `value` is a cache HIT under `config` at `at` — present and unexpired.\n * PURE — reads only. Always `false` when `cacheTtlMs` is unset or no `cache`\n * slice exists (no dedupe configured), so the dedupe gate is a no-op for an\n * unconfigured gate.\n */\nfunction isCacheHit<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): boolean {\n const cached = cacheConfig(config, state);\n if (cached === null) return false;\n return cacheGet(cached.cache, keyOf(cached.config, value), at) !== undefined;\n}\n\n/**\n * Emit `value` at clock `at`: bump the rate cursor, refresh the dedupe cache,\n * and clear any pending hold. PURE. The single place an emit's bookkeeping\n * lives, shared by `input` (immediate emit) and `onFlush` (trailing emit) so the\n * two paths can never drift on what an emit updates.\n *\n * - `lastAt` advances to `at` — the next input is rate-measured against this\n * emit, the `../throttle` window cursor.\n * - `cache` (when configured) records `value` under its key with `cacheTtlMs`\n * from `at` — so the next identical input within the TTL is suppressed.\n * - `pending` clears — the value just went out; nothing is waiting.\n */\nfunction commitEmit<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n const cached = cacheConfig(config, state);\n const cache =\n cached !== null\n ? cacheSet(\n cached.cache,\n keyOf(cached.config, value),\n value,\n at,\n cached.config.cacheTtlMs,\n )\n : state.cache;\n const next: ThrottledInput<V> = {\n ...state,\n lastAt: at,\n pending: null,\n ...(cache !== undefined ? { cache } : {}),\n };\n return [next, [config.emit(value)]];\n}\n\n/**\n * Feed a new input `value` arriving at clock `at` through the gates. PURE.\n *\n * The gate order is DEDUPE → RATE → SETTLE (each skipped when its config field\n * is omitted), the same cache-first-then-gate order `resilient-call` uses (a hit\n * short-circuits before the expensive path):\n *\n * 1. **Dedupe** (`cacheTtlMs` set): if `value`'s key is cached and unexpired,\n * suppress — return the slice UNCHANGED (no emit, no pending, no re-arm).\n * The downstream effect already ran for this key within its TTL.\n * 2. **Rate** (`throttleMs` set): if `at` is within `throttleMs` of the last\n * emit (`at - lastAt < throttleMs`), this input is rate-blocked.\n * - With `debounceMs` set → HOLD it as `pending` (the trailing edge: the\n * settle timer will emit it at a legal moment, so the final sample is\n * never lost). The deadline `subs` arms targets `at + debounceMs`.\n * - Without `debounceMs` → DROP it (pure rate cap, no catch-up).\n * 3. **Settle** (`debounceMs` set): the input cleared the rate gate. HOLD it\n * as `pending` and let the debounce window slide; `onFlush` emits it once\n * the window expires. A fresh input before expiry overwrites `pending` (the\n * trailing edge fires with the LAST value, the `../debounce` default).\n * 4. **Pass-through**: no `debounceMs`, cleared the rate gate → EMIT now.\n *\n * @param config the knob (the three optional gates + `emit`)\n * @param state the current slice\n * @param value the input value\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function input<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n // 1. Dedupe gate — a cached, unexpired key short-circuits everything.\n if (isCacheHit(config, state, value, at)) {\n return [state, []];\n }\n\n // 2. Rate gate — half-open cutoff (`>=` passes). `lastAt === null` (no prior\n // emit) always passes: the first input is never rate-blocked. Shared with\n // `onFlush` via `rateBlockedAt` so the arrival and trailing-edge paths agree\n // on the window.\n if (rateBlockedAt(config, state, at)) {\n // With a settle window, a rate-blocked input is HELD (trailing edge) so it\n // emits at the next legal moment; without one, it is a pure drop.\n if (config.debounceMs !== undefined) {\n return [{ ...state, pending: { value, at } }, []];\n }\n return [state, []];\n }\n\n // 3. Settle gate — passed the rate gate; if a debounce window is configured,\n // hold the value and (re)arm the settle deadline rather than emit now.\n if (config.debounceMs !== undefined) {\n return [{ ...state, pending: { value, at } }, []];\n }\n\n // 4. Pass-through — no settle wait, cleared the rate gate: emit immediately.\n return commitEmit(config, state, value, at);\n}\n\n/**\n * Whether emitting at clock `at` would breach the rate cap — `at` lands within\n * `throttleMs` of the last emit (`at - lastAt < throttleMs`). PURE — reads only.\n * Always `false` when `throttleMs` is unset (no rate cap) or before the first\n * emit (`lastAt === null`), so an unconfigured / never-fired gate never holds on\n * the rate gate. The single rate-gate predicate, shared by `input` (drop / hold\n * on arrival) and `onFlush` (re-hold a still-early trailing edge) so the two\n * paths can never drift on what \"within the rate window\" means.\n */\nfunction rateBlockedAt<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n at: number,\n): boolean {\n return (\n config.throttleMs !== undefined &&\n state.lastAt !== null &&\n at - state.lastAt < config.throttleMs\n );\n}\n\n/**\n * Emit the pending value because the settle (debounce) window closed. PURE.\n *\n * - Pending present, rate gate CLEAR → emit `config.emit(pending.value)`,\n * advance the rate cursor (`lastAt = at`), refresh the dedupe cache, and clear\n * `pending`. The trailing edge of `../debounce` realized as a Model transition.\n * - Pending present, rate gate STILL BLOCKED → re-HOLD: return the slice with\n * `pending` intact and NO Cmds. A settle Msg can fire at `pending.at +\n * debounceMs`, which may still be inside the rate window (`lastAt +\n * throttleMs`) — emitting then would breach the headline \"at most once per\n * `throttleMs`\" cap. Re-holding keeps the value waiting; `subs` re-arms the\n * deadline at `max(pending.at + debounceMs, lastAt + throttleMs)` so the next\n * timer fires at a LEGAL moment and the trailing edge is deferred, never lost.\n * - Pending absent → no-op: return the same slice and no Cmds. Guards the race\n * where an `input` already emitted (or a dedupe hit cleared interest) before\n * the settle timer's Msg lands — a stale `onFlush` must NOT emit. Symmetric\n * with `batch-window`'s closed-window guard.\n *\n * `at` is the firing instant the settle Msg carried (the caller stamps it; the\n * verb never reads the clock). It becomes the new rate cursor and the cache\n * write's `nowMs`, so a settled emit rate-blocks subsequent inputs from its\n * actual fire moment, not the moment the value was first held.\n *\n * @param config the knob (`throttleMs`, `emit`, cache config)\n * @param state the current slice\n * @param at the settle-timer firing instant (epoch ms)\n */\nexport function onFlush<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n // No pending value = an immediate emit already fired, or a dedupe hit cleared\n // interest. A stale timer Msg must not emit — return the slice untouched.\n if (state.pending === null) return [state, []];\n // Rate gate re-check: a settle Msg can land before the rate window opens (its\n // naive deadline is pending.at + debounceMs, which `subs` lifts to a legal\n // target — but a stale / early timer can still arrive). Emitting now would\n // breach \"at most once per throttleMs\", so re-hold and let `subs` re-arm at\n // the legal moment. The value stays pending; nothing is dropped.\n if (rateBlockedAt(config, state, at)) return [state, []];\n return commitEmit(config, state, state.pending.value, at);\n}\n\n/**\n * The settle timer Sub, derived from the slice. PURE.\n *\n * Returns the single `../deadline` Sub while a value is held AND a settle window\n * is configured, or `[]` otherwise. The deadline target is the LATER of two\n * constraints (so the timer never fires before BOTH are satisfied):\n *\n * - `pending.at + debounceMs` — the settle (debounce) deadline: the value has\n * stopped sliding, the burst has gone quiet.\n * - `lastAt + throttleMs` — the rate cap: emitting earlier would breach \"at\n * most once per `throttleMs`\". Only applies once a prior emit exists\n * (`lastAt !== null`) and a rate cap is configured (`throttleMs` set).\n *\n * `max(settleAt, rateAt)` is the earliest LEGAL trailing-edge moment. Anchoring\n * the timer here — rather than the naive `pending.at + debounceMs` — is what\n * keeps the rate cap intact across the trailing edge: a value held while still\n * inside the rate window arms its timer at `lastAt + throttleMs`, so `onFlush`\n * fires at a moment the rate gate already permits. (`onFlush` re-checks the gate\n * anyway and re-holds if an early/stale timer slips through, so the cap holds\n * even if a timer fires ahead of this target.)\n *\n * The substrate's reconcile pass arms the timer when the array gains the Sub and\n * disarms it (clearing the pending `setTimeout`) when the array loses it — so an\n * `onFlush` (or a dedupe hit) that clears `pending` auto-cancels the now-stale\n * settle timer on the next transition, and a sliding input that re-stamps\n * `pending.at` re-arms a fresh absolute target.\n *\n * `debounceMs` undefined ⇒ no settle window ⇒ `[]` even with a `pending` value\n * (an unconfigured settle never holds, but the guard keeps `subs` total).\n *\n * @param config the knob (`debounceMs`, `throttleMs`)\n * @param state the current slice\n * @param id stable identity for this gate's settle timer (default\n * `\"throttled-input:settle\"`); pass a distinct id per gate when a\n * machine runs several so the dispatched Msg routes distinctly.\n */\nexport function subsFor<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n id = DEFAULT_SUB_ID,\n): readonly ThrottledInputSub[] {\n if (config.debounceMs === undefined || state.pending === null) return [];\n const settleAt = state.pending.at + config.debounceMs;\n // The rate floor: the earliest moment the rate cap permits an emit. Absent\n // when no rate cap is configured or no prior emit set a cursor — then the\n // settle deadline alone governs.\n const rateAt =\n config.throttleMs !== undefined && state.lastAt !== null\n ? state.lastAt + config.throttleMs\n : settleAt;\n return [deadlineSub(id, Math.max(settleAt, rateAt))];\n}\n\n/**\n * The bound knob returned by `createThrottledInput`. `init` + the two pure verbs\n * + `subs`, all closing over one `ThrottledInputConfig`. The verbs return\n * `readonly [ThrottledInput<V>, readonly C[]]`: the next slice and the Cmds to\n * emit (one `emit` Cmd when a value passes, none while held / dropped / deduped).\n *\n * Pure-ops + subs module: there is no `handlers(ports)` cell. The settle timer's\n * subscribe cell IS `../deadline`'s `subscribeDeadline` (re-exported below) —\n * the consumer wires it into `subscribe` directly; the knob redraws no timer\n * lifecycle of its own. (Compare `poller`, identical: it too reuses\n * `../deadline`'s cell and ships no `handlers`.)\n */\nexport interface ThrottledInputKnob<V, C extends Cmd> {\n /** Seed the Model slice (with a cache iff `cacheTtlMs` is configured). */\n init(): ThrottledInput<V>;\n /**\n * Feed a new input through the dedupe → rate → settle gates. See `input`.\n */\n input(\n state: ThrottledInput<V>,\n value: V,\n at: number,\n ): readonly [ThrottledInput<V>, readonly C[]];\n /**\n * Emit the held value because the settle window closed. See `onFlush`.\n */\n onFlush(\n state: ThrottledInput<V>,\n at: number,\n ): readonly [ThrottledInput<V>, readonly C[]];\n /**\n * The settle timer Sub, armed only while a value is held. See `subsFor`.\n * `id` keys the deadline so several gates on one machine route distinctly.\n */\n subs(state: ThrottledInput<V>, id?: string): readonly Sub[];\n}\n\n/**\n * Build a throttled-input knob from `config`. Combinator form (the default L2\n * ergonomic): hand it the config, splice the returned hooks into your machine.\n * The pure verbs (`input` / `onFlush`) close over the config so the consumer's\n * reducer cells stay a uniform `(state, ...args)` shape; `subs` pre-wires the\n * settle timer.\n *\n * `init()` seeds a `cache` slice IFF `cacheTtlMs` is configured — an unconfigured\n * gate carries no cache field (nothing to serialize, no dedupe). Pure factory —\n * no clock read, no timer; everything happens when the consumer calls the\n * returned verbs / `subs` with an injected `at`.\n *\n * @typeParam V the input value type\n * @typeParam C the Cmd type `config.emit` returns\n */\nexport function createThrottledInput<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n): ThrottledInputKnob<V, C> {\n function init(): ThrottledInput<V> {\n const base = initThrottledInput<V>();\n // Seed the cache only when dedupe is configured — an unconfigured gate\n // carries no cache field at all (the no-dedupe shape).\n return config.cacheTtlMs !== undefined\n ? { ...base, cache: initCache<V>() }\n : base;\n }\n\n return {\n init,\n input: (state, value, at) => input(config, state, value, at),\n onFlush: (state, at) => onFlush(config, state, at),\n subs: (state, id) => subsFor(config, state, id),\n };\n}\n\n/**\n * The settle-timer wiring, RE-EXPORTED from `../deadline` (not redrawn) so the\n * timer lifecycle stays owned there. Consolidated into one `export ... from`:\n *\n * - `subscribeThrottledInput` (= `../deadline`'s `subscribeDeadline`) — the\n * `subscribe[\"deadline\"]` cell. Arms a one-shot timer for `max(0, atMs -\n * now)`, dispatches the deadline Msg, returns a cleanup that clears it. The\n * consumer assigns it: `subscribe: { deadline: subscribeThrottledInput }`.\n * - `throttledInputSettled` (= `../deadline`'s `deadlineExceeded`) +\n * `ThrottledInputSettled` (= `DeadlineExceeded`) — the settle-timer Msg\n * constructor and type the consumer handles via `onFlush`. Sharing one\n * constructor with the subscribe cell keeps the two from drifting; the\n * reducer reads `msg.atMs` as the `at` it threads into `onFlush`.\n */\nexport {\n type DeadlineExceeded as ThrottledInputSettled,\n deadlineExceeded as throttledInputSettled,\n subscribeDeadline as subscribeThrottledInput,\n} from \"../../resilience/deadline\";\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/internal/timing/throttle/index.ts","../../src/internal/timing/throttled-input/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * internal/timing/throttle — a timer-based call transformer that caps invocation\n * to at most once per `ms` window.\n *\n * Sibling of `../debounce` — the two share a SYMMETRIC surface\n * (`Throttled<A>` / `Debounced<A>`, both callable + `.cancel()` + `.flush()`,\n * both `{ leading?, trailing? }`) so they read as a pair. They are kept in\n * separate files with NO cross-import: each is a self-contained timer, and the\n * coalesce-vs-drop policies differ enough that sharing code would hide the\n * distinction. The contrast IS the lesson:\n *\n * - **debounce** waits for quiet — fires once AFTER a burst settles.\n * - **throttle** enforces a rate — fires AT MOST once per `ms` while the\n * burst is ongoing, dropping the calls in between.\n *\n * Layering with `../throttled-input`: `throttle` and `debounce` are\n * the two host-boundary PRIMITIVES — two distinct algorithms\n * (at-most-once-per-window vs collapse-a-burst) behind one signature shape.\n * `throttled-input` is NOT a third primitive: it is a TEA machine composed\n * OVER them (the same semantics, reimplemented as pure, durable Model state)\n * that re-exports both. Wrapping a raw host event → these primitives; durable, replayable\n * gating inside a machine → `throttled-input` (see its module doc for the full\n * layering map).\n *\n * WHAT THIS IS NOT — and why it lives outside `update`:\n *\n * - NOT a reducer / Transitions cell. Reducers are PURE (invariant 2 — same\n * inputs → same outputs, no `Date.now`, no timers). A throttle holds a live\n * `setTimeout` handle and a window cursor; folding it into `update` would\n * smuggle a timer into the pure core.\n * - NOT a Sub. A Sub is a continuous SOURCE of Msgs the substrate reconciles\n * by id every transition (invariant 4). A throttle is a SINK transformer:\n * it wraps a `void`-returning handler (the canonical one is\n * `Runtime.dispatch`) so a high-frequency HOST event is rate-limited BEFORE\n * it ever becomes a Msg.\n *\n * It is a higher-order function at the host boundary (pattern 14 — TEA owns\n * state, JS owns timing). The host attaches the wrapped function to a\n * high-frequency event source (`mousemove`, `scroll`, `pointermove`, a game\n * loop) and the substrate downstream only ever sees the rate-limited Msg:\n *\n * const onMoved = throttle(\n * (e: PointerEvent) =>\n * runtime.dispatch({ type: \"UserMovedCursor\", x: e.clientX, y: e.clientY }),\n * 16, // ~60fps cap\n * );\n * window.addEventListener(\"pointermove\", onMoved);\n * // cleanup: onMoved.cancel(); window.removeEventListener(\"pointermove\", onMoved);\n *\n * Internal since #47 — not published on any subpath; reached from inside the\n * package as `internal/timing/throttle`. Same one-shape-per-package rule as\n * `rate-limit`, `deadline`, and `work-queue`.\n *\n * Uses the universal `setTimeout` / `clearTimeout` timer idiom (same discipline\n * as `subs/from-timeout.ts`): exactly one live handle at a time, the window\n * timer clears itself on expiry, `cancel` / `flush` clear and null it so a\n * stale callback can never run.\n */\n\n/**\n * A throttled wrapper around `fn`. Callable with the SAME argument tuple as\n * `fn` — `(...args: A) => void` — plus two control methods that share names and\n * shapes with `Debounced<A>` so the two modules read as a symmetric pair.\n */\nexport interface Throttled<A extends readonly unknown[]> {\n /** Invoke `fn` if the window is open, else record args for the trailing fire. */\n (...args: A): void;\n /**\n * Drop any pending trailing fire WITHOUT invoking `fn`, and close the active\n * window. Clears the timer, forgets the captured trailing args, and resets\n * the cursor so the next call fires immediately on the leading edge (if\n * `leading` is enabled). Idempotent.\n *\n * The host-cleanup partner of `removeEventListener`: cancel the pending fire\n * when the component unmounts / the listener detaches, so a queued\n * `dispatch` can't land after teardown.\n */\n cancel(): void;\n /**\n * Fire any pending trailing call IMMEDIATELY with its latest captured args,\n * then close the window. No-op when nothing is pending. Use to force the last\n * dropped call out early — e.g. flush the final cursor position when a drag\n * ends, so the rest state isn't a stale mid-drag sample.\n */\n flush(): void;\n}\n\n/**\n * Wrap `fn` so it runs AT MOST once per `ms` window, no matter how often it is\n * called.\n *\n * Leading + trailing (the default): the first call fires `fn` IMMEDIATELY (the\n * leading edge) and opens a `ms` window. Calls during the window are dropped\n * from immediate firing but their args are remembered; when the window closes,\n * `fn` fires ONCE more with the LATEST dropped args (the trailing edge) and a\n * new window opens. A call after the window has already closed leads a fresh\n * window immediately. Spacing every call at least `ms` apart therefore fires\n * each one on its own leading edge.\n *\n * @param fn The handler to rate-limit. Typically `(...) => runtime.dispatch(...)`,\n * but any `(...args: A) => void` works. The return value is\n * ignored — throttle is a fire transformer, not a value pipe.\n * @param ms Window length in milliseconds. `fn` fires at most once per `ms`.\n * @param opts.leading Fire on the FIRST call of a window (the leading edge),\n * immediately. Default `true`.\n * @param opts.trailing Fire once at window END with the latest args dropped\n * during the window. Default `true`.\n *\n * Edge combinations (matching lodash's settled semantics, the de-facto\n * standard the ecosystem rediscovered):\n *\n * - `{ leading: true, trailing: true }` (default): immediate fire + a\n * trailing fire per window while calls keep arriving. The trailing fire is\n * SUPPRESSED when no extra call arrived during the window (a lone call\n * already fired on the leading edge — no stale trailing double-fire).\n * - `{ leading: true, trailing: false }`: only the immediate fire; everything\n * in the window is dropped with no catch-up at the end.\n * - `{ leading: false, trailing: true }`: no immediate fire; the FIRST call\n * opens a window and `fn` fires at the window's end with the latest args.\n * Steady \"sample every `ms`\" behavior with no instant response.\n * - `{ leading: false, trailing: false }`: never fires. Degenerate but legal;\n * we don't throw — the caller asked for a no-op transformer.\n *\n * WHY a default of leading + trailing both `true`: a throttle's job is \"respond\n * now, then keep responding at a steady rate.\" Leading gives the instant\n * response (the first `mousemove` updates immediately); trailing guarantees the\n * FINAL sample of a window isn't lost (the cursor ends where the user actually\n * left it, not one window-width behind). Dropping either loses one of those two\n * guarantees, so both default on.\n */\nexport function throttle<A extends readonly unknown[]>(\n fn: (...args: A) => void,\n ms: number,\n opts?: { leading?: boolean; trailing?: boolean },\n): Throttled<A> {\n const leading = opts?.leading ?? true;\n const trailing = opts?.trailing ?? true;\n\n // Exactly one live handle at a time (the from-timeout discipline). `null`\n // when no window is open — the single source of truth for \"is a window\n // active?\". The window timer arms on a fire and clears itself on expiry; the\n // call path never stacks overlapping timers.\n let handle: ReturnType<typeof setTimeout> | null = null;\n // The latest args seen DURING an open window, held for the trailing fire.\n // Overwritten by every dropped call (the trailing edge fires with the LAST\n // args), cleared the instant we fire / cancel. `null` (not `undefined`) is\n // the empty sentinel because `A` may itself contain `undefined` elements — a\n // present-but-empty tuple is still a pending call, distinct from \"nothing\n // pending\".\n let pendingArgs: A | null = null;\n\n function clearHandle(): void {\n if (handle !== null) {\n clearTimeout(handle);\n handle = null;\n }\n }\n\n // Window expiry: the `ms` window just closed. If a call arrived during the\n // window (`pendingArgs !== null`) and trailing is enabled, fire it now and\n // open a FRESH window so the rate cap continues across a sustained burst.\n // If nothing was dropped, let the window close (handle → null) so the next\n // call leads a new window immediately.\n function onWindowExpired(): void {\n if (trailing && pendingArgs !== null) {\n const args = pendingArgs;\n pendingArgs = null;\n // Re-arm BEFORE firing so a re-entrant call from inside `fn` sees an open\n // window (and is correctly dropped/queued) rather than leading a second\n // window — keeps the \"at most once per `ms`\" invariant under re-entry.\n handle = setTimeout(onWindowExpired, ms);\n fn(...args);\n } else {\n // No trailing fire owed — close the window.\n handle = null;\n }\n }\n\n const throttled = (...args: A): void => {\n if (handle === null) {\n // Window is closed → this call leads a new window.\n if (leading) {\n fn(...args);\n } else if (trailing) {\n // No leading fire, but trailing wants the first call's args so the\n // window's end has something to fire. Without this, a leading:false\n // throttle would swallow a lone call entirely.\n pendingArgs = args;\n }\n handle = setTimeout(onWindowExpired, ms);\n } else if (trailing) {\n // Window is open → drop this call from immediate firing, but remember its\n // args as the latest candidate for the trailing edge.\n pendingArgs = args;\n }\n // Window open + trailing disabled → pure drop, nothing to remember.\n };\n\n throttled.cancel = (): void => {\n clearHandle();\n pendingArgs = null;\n };\n\n throttled.flush = (): void => {\n // Force the pending trailing fire out now and close the window. Mirrors\n // `onWindowExpired`'s fire branch but is caller-driven, so it tears the\n // timer down rather than re-arming.\n if (pendingArgs !== null) {\n const args = pendingArgs;\n pendingArgs = null;\n clearHandle();\n fn(...args);\n } else {\n // Nothing pending — still close a lingering window so a post-flush call\n // leads cleanly.\n clearHandle();\n }\n };\n\n return throttled;\n}\n","/**\n * internal/timing/throttled-input — gate a high-frequency input stream into a\n * SETTLED, RATE-CAPPED, optionally DEDUPED sequence of emits, as a TEA knob: one\n * config object + a few pre-wired hooks you spread into your machine.\n *\n * Layering (the throttle-family map): `../throttle` and `../debounce` are the\n * two host-boundary PRIMITIVES — two distinct algorithms\n * (at-most-once-per-window vs collapse-a-burst) behind one signature shape.\n * THIS module is the third name but NOT a third primitive: a TEA machine\n * composed OVER them that re-exports both. Three exports, one layering — not\n * duplication. The reuse list below spells out composition vs reimplemented\n * semantic per sibling.\n *\n * What it reuses, and HOW (composition vs. reimplemented semantic — the two are\n * not the same, so this list keeps them apart):\n *\n * - `../cache` — COMPOSED directly. The DEDUPE / memoize layer is `../cache`'s\n * `TtlCache` plus its `get` / `set` / `initCache` ops, called at runtime —\n * not reinvented. While `cacheTtlMs` is set, an input whose `cacheKey` is\n * already cached and unexpired is served from the cache and emits NOTHING\n * (the downstream effect ran once for that key within its TTL); a fresh emit\n * writes the value into the cache so the next identical input within\n * `cacheTtlMs` is suppressed. The optional eviction Sub is the consumer's to\n * wire if they want memory reclaim (this knob's correctness needs only TTL\n * reads).\n * - `../deadline` — COMPOSED directly. The settle timer IS a `../deadline`\n * deadline: `subsFor` builds it via `deadlineSub`, and the runner +\n * Msg constructor are RE-EXPORTS of `../deadline`'s own (`subscribeDeadline`\n * → `subscribeThrottledInput`, `deadlineExceeded` → `throttledInputSettled`).\n * The timer lifecycle stays owned by `../deadline`; this knob redraws none\n * of it.\n * - `../throttle` / `../debounce` — TYPE + SEMANTIC reuse only, NOT composed.\n * Those siblings are host-boundary SINK transformers that hold a live\n * `setTimeout`; a `setTimeout` in a closure is exactly what a durable,\n * replayable knob must NOT hold. So their CODE is never called here. Instead\n * the RATE-cap semantic (\"at most once per `ms`\", leading-edge) and the\n * SETTLE semantic (\"wait until quiet\", trailing-edge) are REIMPLEMENTED as\n * pure Model state: `lastAt` is the durable rate cursor (half-open cutoff\n * `at - lastAt >= throttleMs`), and `pending` + the `../deadline` timer realize\n * the debounce window inside `update`. The `throttle` / `debounce`\n * transformers themselves are RE-EXPORTED (the only reuse of their actual\n * code) so a consumer can pre-throttle / pre-coalesce a genuinely bursty\n * HOST event at the boundary BEFORE it ever becomes an `input` Msg — a\n * separate, optional escape hatch, not part of this knob's gates.\n *\n * \"Omit a brick, omit its gate\" (the resilient-call config rule): every gate\n * field is optional. Omit `throttleMs` → no rate cap (every input passes the\n * rate gate). Omit `debounceMs` → no settle wait (an input that clears the rate\n * gate emits IMMEDIATELY rather than being held). Omit `cacheTtlMs` → no cache\n * slice at all (the `cache` field is absent; no dedupe). A config with all three\n * omitted is a pass-through: `input` always emits.\n *\n * Why this is a knob and not hand-wired logic: the managed state (`lastAt`,\n * `pending`, `cache`) lives in the Model, so it survives DO eviction / reload\n * (durable) and every transition is still a Msg through `update` (replayable). A\n * closure holding a `setTimeout` and a mutable \"last value\" would break both —\n * banned. `input` / `onFlush` are PURE: time arrives as an `at` parameter, never\n * `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const search = createThrottledInput<string, Msg>({\n * debounceMs: 200, // wait for the user to stop typing\n * throttleMs: 1_000, // but never fire more than 1/s during a long burst\n * cacheTtlMs: 30_000, // skip a query repeated within 30s\n * cacheKey: (q) => q,\n * emit: (q) => ({ type: \"RunSearch\", query: q }),\n * });\n *\n * // Model: { search: ThrottledInput<string>, ... }\n * // init: (loaded) => [loaded ?? { search: search.init(), ... }, []]\n * // update cells:\n * // QueryTyped: (s, m) => mapInput(s, search.input(s.search, m.value, m.at))\n * // SettleFired: (s, m) => mapInput(s, search.onFlush(s.search, m.atMs))\n * // types.sub: {} as DeadlinesSub\n * // subs: [deadlinesSub((s: Model) => search.subs(s.search))]\n * // at run: run(machine, { interpret, subscribe: { deadline: subscribeThrottledInput } })\n *\n * where `mapInput(s, [slice, cmds]) => [{ ...s, search: slice }, cmds]` lifts the\n * slice transition into the consumer's Model. The `RunSearch` Cmd's interpret\n * handler is the consumer's — it is where the settled input meets I/O.\n *\n * NOT a substrate primitive: it depends only on sibling subpaths (`../throttle`,\n * `../debounce`, `../cache`, `../deadline`) and the core `Cmd` type.\n * Internal since #47 — reached from inside the package as\n * `internal/timing/throttled-input`; the L1 escape hatch is those siblings\n * threaded by hand. Same one-shape-per-package rule as `batch-window` and `poller`.\n */\n\nimport type { Cmd } from \"../../../index\";\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../../resilience/cache\";\nimport { type DeadlineSub, deadlineSub } from \"../../resilience/deadline\";\n\n// `../throttle` + `../debounce` — host-boundary pre-transformers (see file\n// header). Re-exported so a consumer that wants to throttle/debounce a bursty\n// HOST source BEFORE it becomes an `input` Msg reaches them from the same\n// import as the durable gate.\nexport { type Debounced, debounce } from \"../debounce\";\nexport { type Throttled, throttle } from \"../throttle\";\n\n/**\n * The gate fields every throttled-input config shares, regardless of whether\n * dedupe is on. The two dedupe variants below extend this with mutually\n * exclusive cache fields.\n *\n * `emit` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps a settled\n * value to the single Cmd the runtime performs when the gates let a value\n * through. Keeping it `(value) => Cmd` (not `(value) => void`) is what keeps the\n * trigger replayable — the emit is a Cmd in the transition's output, never a\n * side effect inside the verb (mirrors `batch-window`'s `flush`).\n */\ninterface ThrottledInputGates<V, C extends Cmd> {\n /**\n * Rate cap, in milliseconds. An input that lands within `throttleMs` of the\n * last EMIT (`at - lastAt < throttleMs`) is dropped from immediate firing —\n * the leading-edge \"at most once per `throttleMs`\" rate-cap rule, applied to\n * emits. The half-open cutoff (`>=` passes) matches `../throttle` and\n * `../cache`'s window-edge convention. Omit for no rate cap.\n *\n * Interacts with `debounceMs`: when both are set, a rate-blocked input is\n * still HELD as `pending` (so the settle timer eventually trails it out at a\n * legal moment) rather than silently dropped — the trailing-edge guarantee\n * that the FINAL sample of a window is never lost.\n */\n readonly throttleMs?: number;\n /**\n * Settle wait, in milliseconds. An input that clears the rate gate is not\n * emitted immediately; it is held as `pending` and a debounce deadline is\n * armed at `pendingAt + debounceMs`. A fresh input re-arms (slides) the\n * window. `onFlush` emits the pending value when the window finally expires.\n * Omit for no settle wait — a passing input emits on arrival.\n */\n readonly debounceMs?: number;\n /**\n * Map a settled value to the single Cmd the runtime performs. Called with the\n * value the gates let through; the returned Cmd is what `input` / `onFlush`\n * emit. PURE constructor — no I/O, no clock; the I/O happens later in the\n * consumer's interpret handler for the returned Cmd.\n */\n readonly emit: (value: V) => C;\n}\n\n/**\n * Configuration for a throttled input — the knob. Pure config, no mutable state\n * (the `ThrottledInput<V>` slice carries that). One config object is shared\n * across every input of one logical stream (often a module-scope const).\n *\n * A DISCRIMINATED UNION on the presence of dedupe, so the type system makes a\n * cache-key collision UNREPRESENTABLE (\"omit a brick, omit its gate\"):\n *\n * - **No dedupe** (`ThrottledInputNoCache`): `cacheTtlMs` absent ⇒ no cache\n * slice, no dedupe. `cacheKey` is statically forbidden here (`never`) — a\n * key with no cache to feed is dead config, so the type rejects it rather\n * than silently ignoring it.\n * - **Dedupe** (`ThrottledInputWithCache`): `cacheTtlMs` set ⇒ `cacheKey` is\n * REQUIRED. There is no `String(value)` default to fall back to, so a\n * structured value can NEVER collapse two distinct values to one slot (the\n * `\"[object Object]\"` collision). The consumer must state the identity that\n * keys the cache; a primitive stream still trivially passes `(v) => v` /\n * `String`, but it does so EXPLICITLY.\n */\nexport type ThrottledInputConfig<V, C extends Cmd> =\n | ThrottledInputNoCache<V, C>\n | ThrottledInputWithCache<V, C>;\n\n/**\n * The no-dedupe config variant: gates only, no cache. `cacheTtlMs` is absent\n * and `cacheKey` is statically `never` so a stray key (which would feed no\n * cache) is a type error, not a silently-ignored field.\n */\nexport interface ThrottledInputNoCache<V, C extends Cmd>\n extends ThrottledInputGates<V, C> {\n readonly cacheTtlMs?: undefined;\n readonly cacheKey?: never;\n}\n\n/**\n * The dedupe config variant: a per-entry TTL plus a REQUIRED `cacheKey`. An\n * input whose `cacheKey` is already cached and unexpired emits NOTHING (the\n * downstream effect ran once for that key within its TTL); a fresh emit writes\n * the value into the cache.\n *\n * `cacheKey` is required (no `String(value)` default) precisely so a structured\n * value cannot silently collide — the consumer names the identity, the cache\n * keys on exactly that.\n */\nexport interface ThrottledInputWithCache<V, C extends Cmd>\n extends ThrottledInputGates<V, C> {\n /**\n * Dedupe / memoize TTL, in milliseconds. An input whose `cacheKey` is already\n * cached and unexpired emits NOTHING; a fresh emit writes the value into the\n * cache so the next identical input within `cacheTtlMs` is suppressed.\n */\n readonly cacheTtlMs: number;\n /**\n * Key a value for the dedupe cache. REQUIRED whenever `cacheTtlMs` is set:\n * there is no `String(value)` fallback, so a structured value's identity is\n * always the consumer's explicit choice and two distinct values can never\n * collapse to one slot. A primitive stream passes `(v) => v` / `String`.\n */\n readonly cacheKey: (value: V) => string;\n}\n\n/**\n * The Model slice a throttled input owns — its visible slice (the knob\n * principle: managed state lives in the Model, never a closure, so it is durable\n * and replayable).\n *\n * - `lastAt` — the `at` of the most recent EMIT, or `null` before the first\n * emit. The `../throttle` rate cap measures against it: an input\n * within `throttleMs` of `lastAt` is rate-blocked. Persisted so a\n * resumed gate keeps its rate cursor.\n * - `pending` — the value HELD for the settle (debounce) window, paired with the\n * `at` it was held (the anchor the deadline targets), or `null`\n * when nothing is waiting. The single source of truth for \"is a\n * settle window open?\" — the debounce timer Sub arms iff this is\n * non-null.\n * - `cache` — the `../cache` TTL store backing dedupe, present IFF\n * `cacheTtlMs` is configured. Absent (the field omitted) for a\n * gate with no dedupe — no per-op cost, nothing to serialize.\n */\nexport interface ThrottledInput<V> {\n readonly lastAt: number | null;\n readonly pending: PendingInput<V> | null;\n readonly cache?: TtlCache<V>;\n}\n\n/**\n * A value held for the settle window: the `value` itself and the `at` it was\n * held (the anchor the debounce deadline targets, `at + debounceMs`). A distinct\n * type (not a bare `V`) because `V` may itself be `null` — a present-but-null\n * value is still a real pending input, distinct from \"nothing pending\", exactly\n * the `null`-sentinel discipline `../throttle` / `../debounce` use for their\n * `pendingArgs`.\n */\nexport interface PendingInput<V> {\n readonly value: V;\n readonly at: number;\n}\n\n/**\n * The deadline a throttled input's settle timer lists — a `../deadline` entry\n * (so the window fires at the correct ABSOLUTE moment even after a late\n * subscribe / rehydrate, and so a consumer running several gates routes each by\n * `id`). `atMs` is the absolute settle-by instant (`pendingAt + debounceMs`).\n */\nexport type ThrottledInputSub = DeadlineSub;\n\n/**\n * The stable id the gate arms its settle deadline under. A single gate arms\n * exactly one settle timer at a time, so one id suffices; a consumer running\n * several gates on one machine namespaces them by composing the knob under\n * distinct slices with distinct ids (the `subs(state, id)` parameter).\n */\nconst DEFAULT_SUB_ID = \"throttled-input:settle\";\n\n/**\n * The starting slice: no prior emit, nothing pending. The `cache` field is\n * seeded only when `cacheTtlMs` is configured (see `createThrottledInput`); the\n * free `initThrottledInput` leaves it absent (the no-dedupe shape).\n */\nexport function initThrottledInput<V>(): ThrottledInput<V> {\n return { lastAt: null, pending: null };\n}\n\n/**\n * Resolve a value's cache key under a DEDUPE config. PURE. Takes the narrowed\n * `ThrottledInputWithCache` (not the union) because `cacheKey` is only present —\n * and only meaningful — when dedupe is on; there is no `String(value)` default\n * to fall back to, so a structured value's identity is always the consumer's\n * explicit choice and two distinct values can never collapse to one slot.\n */\nfunction keyOf<V, C extends Cmd>(\n config: ThrottledInputWithCache<V, C>,\n value: V,\n): string {\n return config.cacheKey(value);\n}\n\n/**\n * Narrow `config` to its dedupe variant iff a cache slice is actually wired —\n * `cacheTtlMs` set AND a `cache` store present. Returns the narrowed config\n * (carrying the required `cacheKey`) paired with the resolved `cache` store, or\n * `null` when no dedupe is configured. The single place the union is\n * discriminated, so `isCacheHit` / `commitEmit` share one narrowing — and reuse\n * the resolved `cache` — rather than re-deriving the predicate and re-narrowing\n * `state.cache`.\n */\nfunction cacheConfig<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n): {\n readonly config: ThrottledInputWithCache<V, C>;\n readonly cache: TtlCache<V>;\n} | null {\n return config.cacheTtlMs !== undefined && state.cache !== undefined\n ? { config, cache: state.cache }\n : null;\n}\n\n/**\n * Whether `value` is a cache HIT under `config` at `at` — present and unexpired.\n * PURE — reads only. Always `false` when `cacheTtlMs` is unset or no `cache`\n * slice exists (no dedupe configured), so the dedupe gate is a no-op for an\n * unconfigured gate.\n */\nfunction isCacheHit<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): boolean {\n const cached = cacheConfig(config, state);\n if (cached === null) return false;\n return cacheGet(cached.cache, keyOf(cached.config, value), at) !== undefined;\n}\n\n/**\n * Emit `value` at clock `at`: bump the rate cursor, refresh the dedupe cache,\n * and clear any pending hold. PURE. The single place an emit's bookkeeping\n * lives, shared by `input` (immediate emit) and `onFlush` (trailing emit) so the\n * two paths can never drift on what an emit updates.\n *\n * - `lastAt` advances to `at` — the next input is rate-measured against this\n * emit, the `../throttle` window cursor.\n * - `cache` (when configured) records `value` under its key with `cacheTtlMs`\n * from `at` — so the next identical input within the TTL is suppressed.\n * - `pending` clears — the value just went out; nothing is waiting.\n */\nfunction commitEmit<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n const cached = cacheConfig(config, state);\n const cache =\n cached !== null\n ? cacheSet(\n cached.cache,\n keyOf(cached.config, value),\n value,\n at,\n cached.config.cacheTtlMs,\n )\n : state.cache;\n const next: ThrottledInput<V> = {\n ...state,\n lastAt: at,\n pending: null,\n ...(cache !== undefined ? { cache } : {}),\n };\n return [next, [config.emit(value)]];\n}\n\n/**\n * Feed a new input `value` arriving at clock `at` through the gates. PURE.\n *\n * The gate order is DEDUPE → RATE → SETTLE (each skipped when its config field\n * is omitted), the same cache-first-then-gate order `resilient-call` uses (a hit\n * short-circuits before the expensive path):\n *\n * 1. **Dedupe** (`cacheTtlMs` set): if `value`'s key is cached and unexpired,\n * suppress — return the slice UNCHANGED (no emit, no pending, no re-arm).\n * The downstream effect already ran for this key within its TTL.\n * 2. **Rate** (`throttleMs` set): if `at` is within `throttleMs` of the last\n * emit (`at - lastAt < throttleMs`), this input is rate-blocked.\n * - With `debounceMs` set → HOLD it as `pending` (the trailing edge: the\n * settle timer will emit it at a legal moment, so the final sample is\n * never lost). The deadline `subs` arms targets `at + debounceMs`.\n * - Without `debounceMs` → DROP it (pure rate cap, no catch-up).\n * 3. **Settle** (`debounceMs` set): the input cleared the rate gate. HOLD it\n * as `pending` and let the debounce window slide; `onFlush` emits it once\n * the window expires. A fresh input before expiry overwrites `pending` (the\n * trailing edge fires with the LAST value, the `../debounce` default).\n * 4. **Pass-through**: no `debounceMs`, cleared the rate gate → EMIT now.\n *\n * @param config the knob (the three optional gates + `emit`)\n * @param state the current slice\n * @param value the input value\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function input<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n // 1. Dedupe gate — a cached, unexpired key short-circuits everything.\n if (isCacheHit(config, state, value, at)) {\n return [state, []];\n }\n\n // 2. Rate gate — half-open cutoff (`>=` passes). `lastAt === null` (no prior\n // emit) always passes: the first input is never rate-blocked. Shared with\n // `onFlush` via `rateBlockedAt` so the arrival and trailing-edge paths agree\n // on the window.\n if (rateBlockedAt(config, state, at)) {\n // With a settle window, a rate-blocked input is HELD (trailing edge) so it\n // emits at the next legal moment; without one, it is a pure drop.\n if (config.debounceMs !== undefined) {\n return [{ ...state, pending: { value, at } }, []];\n }\n return [state, []];\n }\n\n // 3. Settle gate — passed the rate gate; if a debounce window is configured,\n // hold the value and (re)arm the settle deadline rather than emit now.\n if (config.debounceMs !== undefined) {\n return [{ ...state, pending: { value, at } }, []];\n }\n\n // 4. Pass-through — no settle wait, cleared the rate gate: emit immediately.\n return commitEmit(config, state, value, at);\n}\n\n/**\n * Whether emitting at clock `at` would breach the rate cap — `at` lands within\n * `throttleMs` of the last emit (`at - lastAt < throttleMs`). PURE — reads only.\n * Always `false` when `throttleMs` is unset (no rate cap) or before the first\n * emit (`lastAt === null`), so an unconfigured / never-fired gate never holds on\n * the rate gate. The single rate-gate predicate, shared by `input` (drop / hold\n * on arrival) and `onFlush` (re-hold a still-early trailing edge) so the two\n * paths can never drift on what \"within the rate window\" means.\n */\nfunction rateBlockedAt<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n at: number,\n): boolean {\n return (\n config.throttleMs !== undefined &&\n state.lastAt !== null &&\n at - state.lastAt < config.throttleMs\n );\n}\n\n/**\n * Emit the pending value because the settle (debounce) window closed. PURE.\n *\n * - Pending present, rate gate CLEAR → emit `config.emit(pending.value)`,\n * advance the rate cursor (`lastAt = at`), refresh the dedupe cache, and clear\n * `pending`. The trailing edge of `../debounce` realized as a Model transition.\n * - Pending present, rate gate STILL BLOCKED → re-HOLD: return the slice with\n * `pending` intact and NO Cmds. A settle Msg can fire at `pending.at +\n * debounceMs`, which may still be inside the rate window (`lastAt +\n * throttleMs`) — emitting then would breach the headline \"at most once per\n * `throttleMs`\" cap. Re-holding keeps the value waiting; `subs` re-arms the\n * deadline at `max(pending.at + debounceMs, lastAt + throttleMs)` so the next\n * timer fires at a LEGAL moment and the trailing edge is deferred, never lost.\n * - Pending absent → no-op: return the same slice and no Cmds. Guards the race\n * where an `input` already emitted (or a dedupe hit cleared interest) before\n * the settle timer's Msg lands — a stale `onFlush` must NOT emit. Symmetric\n * with `batch-window`'s closed-window guard.\n *\n * `at` is the firing instant the settle Msg carried (the caller stamps it; the\n * verb never reads the clock). It becomes the new rate cursor and the cache\n * write's `nowMs`, so a settled emit rate-blocks subsequent inputs from its\n * actual fire moment, not the moment the value was first held.\n *\n * @param config the knob (`throttleMs`, `emit`, cache config)\n * @param state the current slice\n * @param at the settle-timer firing instant (epoch ms)\n */\nexport function onFlush<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n // No pending value = an immediate emit already fired, or a dedupe hit cleared\n // interest. A stale timer Msg must not emit — return the slice untouched.\n if (state.pending === null) return [state, []];\n // Rate gate re-check: a settle Msg can land before the rate window opens (its\n // naive deadline is pending.at + debounceMs, which `subs` lifts to a legal\n // target — but a stale / early timer can still arrive). Emitting now would\n // breach \"at most once per throttleMs\", so re-hold and let `subs` re-arm at\n // the legal moment. The value stays pending; nothing is dropped.\n if (rateBlockedAt(config, state, at)) return [state, []];\n return commitEmit(config, state, state.pending.value, at);\n}\n\n/**\n * The settle timer deadline, derived from the slice. PURE.\n *\n * Returns the single `../deadline` entry while a value is held AND a settle window\n * is configured, or `[]` otherwise. The deadline target is the LATER of two\n * constraints (so the timer never fires before BOTH are satisfied):\n *\n * - `pending.at + debounceMs` — the settle (debounce) deadline: the value has\n * stopped sliding, the burst has gone quiet.\n * - `lastAt + throttleMs` — the rate cap: emitting earlier would breach \"at\n * most once per `throttleMs`\". Only applies once a prior emit exists\n * (`lastAt !== null`) and a rate cap is configured (`throttleMs` set).\n *\n * `max(settleAt, rateAt)` is the earliest LEGAL trailing-edge moment. Anchoring\n * the timer here — rather than the naive `pending.at + debounceMs` — is what\n * keeps the rate cap intact across the trailing edge: a value held while still\n * inside the rate window arms its timer at `lastAt + throttleMs`, so `onFlush`\n * fires at a moment the rate gate already permits. (`onFlush` re-checks the gate\n * anyway and re-holds if an early/stale timer slips through, so the cap holds\n * even if a timer fires ahead of this target.)\n *\n * Fed through `deadlinesSub`, the engine arms the timer when the list gains the\n * deadline and disarms it (clearing the pending `setTimeout`) when the list\n * loses it — so an\n * `onFlush` (or a dedupe hit) that clears `pending` auto-cancels the now-stale\n * settle timer on the next transition, and a sliding input that re-stamps\n * `pending.at` re-arms a fresh absolute target.\n *\n * `debounceMs` undefined ⇒ no settle window ⇒ `[]` even with a `pending` value\n * (an unconfigured settle never holds, but the guard keeps `subs` total).\n *\n * @param config the knob (`debounceMs`, `throttleMs`)\n * @param state the current slice\n * @param id stable identity for this gate's settle timer (default\n * `\"throttled-input:settle\"`); pass a distinct id per gate when a\n * machine runs several so the dispatched Msg routes distinctly.\n */\nexport function subsFor<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n id = DEFAULT_SUB_ID,\n): readonly ThrottledInputSub[] {\n if (config.debounceMs === undefined || state.pending === null) return [];\n const settleAt = state.pending.at + config.debounceMs;\n // The rate floor: the earliest moment the rate cap permits an emit. Absent\n // when no rate cap is configured or no prior emit set a cursor — then the\n // settle deadline alone governs.\n const rateAt =\n config.throttleMs !== undefined && state.lastAt !== null\n ? state.lastAt + config.throttleMs\n : settleAt;\n return [deadlineSub(id, Math.max(settleAt, rateAt))];\n}\n\n/**\n * The bound knob returned by `createThrottledInput`. `init` + the two pure verbs\n * + `subs`, all closing over one `ThrottledInputConfig`. The verbs return\n * `readonly [ThrottledInput<V>, readonly C[]]`: the next slice and the Cmds to\n * emit (one `emit` Cmd when a value passes, none while held / dropped / deduped).\n *\n * Pure-ops + subs module: there is no `handlers(ports)` cell. The settle timer's\n * runner IS `../deadline`'s `subscribeDeadline` (re-exported below) — the\n * consumer passes it to `run` as `subscribe.deadline`; the knob redraws no timer\n * lifecycle of its own. (Compare `poller`, identical: it too reuses\n * `../deadline`'s runner and ships no `handlers`.)\n */\nexport interface ThrottledInputKnob<V, C extends Cmd> {\n /** Seed the Model slice (with a cache iff `cacheTtlMs` is configured). */\n init(): ThrottledInput<V>;\n /**\n * Feed a new input through the dedupe → rate → settle gates. See `input`.\n */\n input(\n state: ThrottledInput<V>,\n value: V,\n at: number,\n ): readonly [ThrottledInput<V>, readonly C[]];\n /**\n * Emit the held value because the settle window closed. See `onFlush`.\n */\n onFlush(\n state: ThrottledInput<V>,\n at: number,\n ): readonly [ThrottledInput<V>, readonly C[]];\n /**\n * The settle timer deadline, listed only while a value is held. See\n * `subsFor`. `id` keys the deadline so several gates on one machine route\n * distinctly.\n */\n subs(state: ThrottledInput<V>, id?: string): readonly ThrottledInputSub[];\n}\n\n/**\n * Build a throttled-input knob from `config`. Combinator form (the default L2\n * ergonomic): hand it the config, splice the returned hooks into your machine.\n * The pure verbs (`input` / `onFlush`) close over the config so the consumer's\n * reducer cells stay a uniform `(state, ...args)` shape; `subs` pre-wires the\n * settle timer.\n *\n * `init()` seeds a `cache` slice IFF `cacheTtlMs` is configured — an unconfigured\n * gate carries no cache field (nothing to serialize, no dedupe). Pure factory —\n * no clock read, no timer; everything happens when the consumer calls the\n * returned verbs / `subs` with an injected `at`.\n *\n * @typeParam V the input value type\n * @typeParam C the Cmd type `config.emit` returns\n */\nexport function createThrottledInput<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n): ThrottledInputKnob<V, C> {\n function init(): ThrottledInput<V> {\n const base = initThrottledInput<V>();\n // Seed the cache only when dedupe is configured — an unconfigured gate\n // carries no cache field at all (the no-dedupe shape).\n return config.cacheTtlMs !== undefined\n ? { ...base, cache: initCache<V>() }\n : base;\n }\n\n return {\n init,\n input: (state, value, at) => input(config, state, value, at),\n onFlush: (state, at) => onFlush(config, state, at),\n subs: (state, id) => subsFor(config, state, id),\n };\n}\n\n/**\n * The settle-timer wiring, RE-EXPORTED from `../deadline` (not redrawn) so the\n * timer lifecycle stays owned there. Consolidated into one `export ... from`:\n *\n * - `subscribeThrottledInput` (= `../deadline`'s `subscribeDeadline`) — the\n * `deadline` runner. Arms a one-shot timer per listed deadline for\n * `max(0, atMs - now)`, dispatches the deadline Msg, returns a cleanup that\n * clears them. The consumer passes it to `run`:\n * `subscribe: { deadline: subscribeThrottledInput }`.\n * - `throttledInputSettled` (= `../deadline`'s `deadlineExceeded`) +\n * `ThrottledInputSettled` (= `DeadlineExceeded`) — the settle-timer Msg\n * constructor and type the consumer handles via `onFlush`. Sharing one\n * constructor with the runner keeps the two from drifting; the\n * reducer reads `msg.atMs` as the `at` it threads into `onFlush`.\n */\nexport {\n type DeadlineExceeded as ThrottledInputSettled,\n deadlineExceeded as throttledInputSettled,\n subscribeDeadline as subscribeThrottledInput,\n} from \"../../resilience/deadline\";\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as Sub } from './core-
|
|
1
|
+
import { S as Sub, h as Dispose } from './core-rDbOaJTN.js';
|
|
2
2
|
|
|
3
|
-
type SubscribeHandler<S extends Sub, M, Ctx> = (sub: S, ctx: Ctx, dispatch: (msg: M) => void) =>
|
|
3
|
+
type SubscribeHandler<S extends Sub, M, Ctx> = (sub: S, ctx: Ctx, dispatch: (msg: M) => void) => Dispose;
|
|
4
4
|
|
|
5
5
|
export type { SubscribeHandler as S };
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { Q as QueueItem } from '../index-
|
|
2
|
-
export { E as EnqueueInput, a as QueueItemStatus, c as createQueue } from '../index-
|
|
3
|
-
import '../runtime-types-
|
|
4
|
-
import '
|
|
5
|
-
import '
|
|
6
|
-
import 'zod';
|
|
1
|
+
import { Q as QueueItem } from '../index-CcrKLtse.js';
|
|
2
|
+
export { E as EnqueueInput, a as QueueItemStatus, c as createQueue } from '../index-CcrKLtse.js';
|
|
3
|
+
import '../runtime-types-B62Ucnxr.js';
|
|
4
|
+
import '../core-rDbOaJTN.js';
|
|
5
|
+
import '@standard-schema/spec';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* internal/work-queue/adapter — the verb seam over the pure queue ops.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demlik/tea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "TEA-faithful state machine substrate — one package, every host adapter. Core + subs + testing, plus React, Durable Object, Chrome extension, Node, in-memory, devtools, and property-testing modules as subpaths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tea",
|
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
32
|
"import": "./dist/index.js"
|
|
33
33
|
},
|
|
34
|
+
"./promise": {
|
|
35
|
+
"types": "./dist/promise/index.d.ts",
|
|
36
|
+
"import": "./dist/promise/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./effect": {
|
|
39
|
+
"types": "./dist/effect/index.d.ts",
|
|
40
|
+
"import": "./dist/effect/index.js"
|
|
41
|
+
},
|
|
34
42
|
"./do": {
|
|
35
43
|
"types": "./dist/do/index.d.ts",
|
|
36
44
|
"import": "./dist/do/index.js"
|
|
@@ -115,17 +123,20 @@
|
|
|
115
123
|
"./package.json": "./package.json"
|
|
116
124
|
},
|
|
117
125
|
"dependencies": {
|
|
118
|
-
"
|
|
119
|
-
"zod": "^4.5.4"
|
|
126
|
+
"@standard-schema/spec": "^1.1.0"
|
|
120
127
|
},
|
|
121
128
|
"peerDependencies": {
|
|
129
|
+
"effect": "^4.0.0-rc.0",
|
|
122
130
|
"fast-check": "^3 || ^4",
|
|
123
131
|
"react": "^18 || ^19",
|
|
124
132
|
"react-dom": "^18 || ^19",
|
|
125
|
-
"vitest": "^2 || ^3",
|
|
133
|
+
"vitest": "^2 || ^3 || ^4 || ^5",
|
|
126
134
|
"ws": "^8"
|
|
127
135
|
},
|
|
128
136
|
"peerDependenciesMeta": {
|
|
137
|
+
"effect": {
|
|
138
|
+
"optional": true
|
|
139
|
+
},
|
|
129
140
|
"fast-check": {
|
|
130
141
|
"optional": true
|
|
131
142
|
},
|
|
@@ -147,13 +158,14 @@
|
|
|
147
158
|
"@biomejs/biome": "2.3.14",
|
|
148
159
|
"@changesets/cli": "^2.27.11",
|
|
149
160
|
"@cloudflare/workers-types": "4.20260312.1",
|
|
150
|
-
"@kampus/fabrika-cli": "0.
|
|
161
|
+
"@kampus/fabrika-cli": "0.7.1",
|
|
151
162
|
"@types/chrome": "^0.1.42",
|
|
152
163
|
"@types/node": "^22",
|
|
153
164
|
"@types/react": "19.0.8",
|
|
154
165
|
"@types/react-dom": "19.0.3",
|
|
155
166
|
"@types/ws": "8.5.14",
|
|
156
167
|
"ai": "^7.0.93",
|
|
168
|
+
"effect": "4.0.0-rc.117",
|
|
157
169
|
"fast-check": "^3.23.0",
|
|
158
170
|
"happy-dom": "^20.9.0",
|
|
159
171
|
"react": "19.0.0",
|
|
@@ -162,8 +174,9 @@
|
|
|
162
174
|
"typedoc": "0.28.20",
|
|
163
175
|
"typescript": "5.7.3",
|
|
164
176
|
"vite": "^8.2.1",
|
|
165
|
-
"vitest": "~
|
|
166
|
-
"ws": "8.18.1"
|
|
177
|
+
"vitest": "~5.0.1",
|
|
178
|
+
"ws": "8.18.1",
|
|
179
|
+
"zod": "^4.5.4"
|
|
167
180
|
},
|
|
168
181
|
"publishConfig": {
|
|
169
182
|
"access": "public"
|
|
@@ -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-22POWGFA.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<N extends string | undefined = undefined> =\n Sub<\"deadline\"> & {\n readonly atMs: number;\n } & DeadlineOpts<N>;\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 * The first field to land here is {@link DeadlineOpts.name} — the knob name the\n * dispatched Msg tag is derived from — and it landed exactly that way: one\n * optional member, zero rewritten call sites.\n */\nexport type DeadlineOpts<N extends string | undefined = undefined> = {\n /**\n * The knob this deadline belongs to. Present → the dispatched Msg's tag is\n * `` `${name}_deadline` ``; absent → the tag stays the bare\n * `\"deadline_exceeded\"` every machine wired before this field is already\n * handling. `N` is the name as a TYPE as well as a value, so a consumer's\n * cell key and the Msg type are derived from one place and cannot drift.\n *\n * Name a deadline when a machine arms more than one family of them. The Sub\n * *id* was already scoped (`` `${name}:deadline:${key}` ``); this scopes the\n * Msg the Sub dispatches, which is what a reducer cell keys off.\n */\n readonly name?: N;\n};\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 extends string | undefined = undefined> = {\n readonly type: DeadlineMsgType<N>;\n readonly id: string;\n readonly atMs: number;\n};\n\n/**\n * The dispatched Msg's tag, derived from the deadline's optional name. A named\n * deadline speaks `` `${N}_deadline` ``; an unnamed one speaks the bare\n * `\"deadline_exceeded\"` literal, which is what `DeadlineExceeded` with no type\n * argument still means — so every machine wired before the name existed keeps\n * the exact type it had.\n */\nexport type DeadlineMsgType<N extends string | undefined> = N extends string\n ? `${N}_deadline`\n : \"deadline_exceeded\";\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 extends string | undefined = undefined>(\n id: string,\n atMs: number,\n opts?: DeadlineOpts<N>,\n): DeadlineSub<N> {\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 extends string | undefined = undefined>(\n armTimer: ArmTimer<DeadlineExceeded<N>>,\n): SubscribeHandler<DeadlineSub<N>, DeadlineExceeded<N>, unknown> {\n return (sub, _ctx, dispatch) =>\n armTimer(\n sub.id,\n sub.atMs,\n deadlineExceeded(sub.id, sub.atMs, sub.name),\n dispatch,\n );\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<\n N extends string | undefined = undefined,\n>(): ArmTimer<DeadlineExceeded<N>> {\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<\n Sub<\"deadline\"> & { delayMs: number },\n DeadlineExceeded<N>\n >(() => msg)({ 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 *\n * Generic in the deadline's name so ONE handler serves every knob in a machine:\n * the tag it dispatches is read off the Sub's own `name`, so a machine mounting\n * a named knob beside an unnamed one wires this same cell once and each Sub\n * still dispatches its own tag.\n */\nexport const subscribeDeadline: <N extends string | undefined = undefined>(\n sub: DeadlineSub<N>,\n ctx: unknown,\n dispatch: (msg: DeadlineExceeded<N>) => void,\n) => () => void = <N extends string | undefined>(\n sub: DeadlineSub<N>,\n ctx: unknown,\n dispatch: (msg: DeadlineExceeded<N>) => void,\n) => subscribeWith<N>(setTimeoutArmTimer<N>())(sub, ctx, dispatch);\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<N extends string | undefined = undefined>(\n id: string,\n atMs: number,\n name?: N,\n): DeadlineExceeded<N> {\n return { type: deadlineMsgType(name), id, atMs };\n}\n\n/**\n * The one place the named tag is spelled. `undefined` → the bare\n * `\"deadline_exceeded\"` literal; a name → `` `${name}_deadline` ``. Exported so\n * a consumer keying a reducer cell off a named deadline derives the key from\n * the same function the Sub dispatches through, instead of re-spelling the\n * template and drifting.\n */\nexport function deadlineMsgType<N extends string | undefined>(\n name?: N,\n): DeadlineMsgType<N> {\n return (\n name === undefined ? \"deadline_exceeded\" : `${name}_deadline`\n ) as DeadlineMsgType<N>;\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/monitored-run/index.ts"],"names":[],"mappings":";;;;;AA4TA,SAAS,aAAA,CAAc,OAAe,WAAA,EAA6B;AACjE,EAAA,OAAO,CAAA,iBAAA,EAAoB,KAAK,CAAA,CAAA,EAAI,WAAW,CAAA,CAAA;AACjD;AAeO,SAAS,kBAAA,CACd,MAAA,GAAoC,EAAC,EACrC;AACA,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,EAAC;AACjC,EAAA,MAAM,UAAA,GAAa,OAAO,MAAA,GAAS,CAAA;AAKnC,EAAA,MAAM,KAAK,YAAA,EAAoB;AAM/B,EAAA,MAAM,IAAA,GACJ,MAAA,CAAO,aAAA,KAAkB,MAAA,GACrB,cAAA,CAAkB;AAAA,IAChB,OAAO,MAAA,CAAO,aAAA;AAAA,IACd,GAAI,OAAO,WAAA,KAAgB,MAAA,GACvB,EAAE,GAAA,EAAK,MAAA,CAAO,WAAA,EAAY,GAC1B;AAAC,GACN,CAAA,GACD,IAAA;AAON,EAAA,MAAM,aAAa,IAAA,IAAQ,cAAA,CAAkB,EAAE,KAAA,EAAO,GAAG,CAAA;AAGzD,EAAA,SAAS,IAAA,GAAiC;AACxC,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,MAAA;AAAA,MACP,YAAY,EAAC;AAAA,MACb,SAAA,EAAW,CAAA;AAAA,MACX,cAAA,EAAgB,CAAA;AAAA,MAChB,WAAA,EAAa,CAAA;AAAA;AAAA;AAAA;AAAA,MAIb,QAAA,EAAU,WAAW,IAAA;AAAK,KAC5B;AAAA,EACF;AAQA,EAAA,SAAS,WAAW,EAAA,EAAyC;AAC3D,IAAA,IAAI,QAAqC,EAAC;AAC1C,IAAA,MAAA,CAAO,OAAA,CAAQ,CAAC,KAAA,EAAO,CAAA,KAAM;AAC3B,MAAA,KAAA,GAAQ,EAAA,CAAG,QAAQ,KAAA,EAAO,KAAA,EAAO,IAAI,CAAA,MAAA,EAAS,CAAC,EAAE,CAAA,CAAE,IAAA;AAAA,IACrD,CAAC,CAAA;AAGD,IAAA,MAAM,OAAA,GAAU,EAAA,CAAG,KAAA,CAAM,KAAA,EAAO,EAAE,CAAA;AAClC,IAAA,OAAO,OAAA,GAAU,QAAQ,IAAA,GAAO,KAAA;AAAA,EAClC;AAGA,EAAA,SAAS,aACP,CAAA,EAC8B;AAC9B,IAAA,OAAO,EAAE,UAAA,CAAW,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,WAAW,SAAS,CAAA;AAAA,EACxD;AAUA,EAAA,SAAS,KAAA,CACP,CAAA,EACA,KAAA,EACA,EAAA,EACoE;AAKpE,IAAA,MAAM,IAAA,GAAiC;AAAA,MACrC,KAAA,EAAO,SAAA;AAAA,MACP,KAAA;AAAA,MACA,UAAA,EAAY,UAAA,GAAa,UAAA,CAAW,EAAE,IAAI,EAAC;AAAA,MAC3C,SAAA,EAAW,EAAA;AAAA,MACX,cAAA,EAAgB,EAAA;AAAA,MAChB,WAAA,EAAa,CAAA;AAAA,MACb,UAAU,CAAA,CAAE;AAAA,KACd;AACA,IAAA,OAAO,CAAC,IAAA,EAAM,EAAE,CAAA;AAAA,EAClB;AAOA,EAAA,SAAS,YAAA,CAAa,GAAmB,EAAA,EAA4B;AACnE,IAAA,OAAO,EAAE,GAAG,CAAA,EAAG,cAAA,EAAgB,IAAI,WAAA,EAAa,CAAA,CAAE,cAAc,CAAA,EAAE;AAAA,EACpE;AAQA,EAAA,SAAS,cAAA,CACP,CAAA,EACA,OAAA,EACA,EAAA,EACoE;AACpE,IAAA,IAAI,SAAS,IAAA,EAAM,OAAO,CAAC,CAAA,EAAG,EAAE,CAAA;AAChC,IAAA,MAAM,CAAC,UAAU,IAAI,CAAA,GAAI,KAAK,MAAA,CAAO,CAAA,CAAE,QAAA,EAAU,OAAA,EAAS,EAAE,CAAA;AAI5D,IAAA,OAAO,CAAC,EAAE,GAAG,CAAA,EAAG,QAAA,IAAY,IAAqC,CAAA;AAAA,EACnE;AAWA,EAAA,SAAS,QAAA,CACP,CAAA,EACA,OAAA,EACA,EAAA,EACoE;AACpE,IAAA,IAAI,CAAA,CAAE,KAAA,KAAU,SAAA,IAAa,CAAA,CAAE,KAAA,KAAU,SAAS,OAAO,CAAC,CAAA,EAAG,EAAE,CAAA;AAC/D,IAAA,MAAM,MAAA,GAAmC;AAAA,MACvC,GAAG,YAAA,CAAa,CAAA,EAAG,EAAE,CAAA;AAAA,MACrB,KAAA,EAAO;AAAA,KACT;AACA,IAAA,OAAO,cAAA,CAAe,MAAA,EAAQ,OAAA,EAAS,EAAE,CAAA;AAAA,EAC3C;AAYA,EAAA,SAAS,UACP,CAAA,EACoE;AACpE,IAAA,IAAI,EAAE,KAAA,KAAU,SAAA,SAAkB,CAAC,CAAA,EAAG,EAAE,CAAA;AACxC,IAAA,OAAO,CAAC,EAAE,GAAG,CAAA,EAAG,OAAO,OAAA,EAAQ,EAAG,EAAE,CAAA;AAAA,EACtC;AAqBA,EAAA,SAAS,MAAA,CACP,GACA,EAAA,EACoE;AACpE,IAAA,IAAI,CAAA,CAAE,UAAU,MAAA,IAAU,CAAA,CAAE,UAAU,SAAA,IAAa,CAAA,CAAE,UAAU,OAAA,EAAS;AACtE,MAAA,OAAO,CAAC,CAAA,EAAG,EAAE,CAAA;AAAA,IACf;AACA,IAAA,MAAM,IAAA,GAAiC;AAAA,MACrC,KAAA,EAAO,WAAA;AAAA,MACP,KAAA,EAAO,CAAA,CAAE,KAAA,KAAU,MAAA,GAAS,OAAO,CAAA,CAAE,KAAA;AAAA,MACrC,EAAA;AAAA,MACA,YAAY,CAAA,CAAE,UAAA;AAAA,MACd,WAAW,CAAA,CAAE,SAAA;AAAA,MACb,gBAAgB,CAAA,CAAE,cAAA;AAAA,MAClB,aAAa,CAAA,CAAE,WAAA;AAAA,MACf,UAAU,CAAA,CAAE;AAAA,KACd;AACA,IAAA,OAAO,CAAC,IAAA,EAAM,EAAE,CAAA;AAAA,EAClB;AAsBA,EAAA,SAAS,OAAA,CACP,CAAA,EACA,OAAA,EACA,MAAA,EACA,EAAA,EACoE;AACpE,IAAA,IAAI,CAAA,CAAE,KAAA,KAAU,SAAA,IAAa,CAAA,CAAE,KAAA,KAAU,SAAS,OAAO,CAAC,CAAA,EAAG,EAAE,CAAA;AAI/D,IAAA,IAAI,MAAA,CAAO,SAAS,MAAA,EAAQ;AAC1B,MAAA,MAAM,MAAA,GAAmC;AAAA,QACvC,GAAG,CAAA;AAAA,QACH,KAAA,EAAO,QAAA;AAAA,QACP,OAAA,EAAS;AAAA,UACP,MAAA,EAAQ,OAAA;AAAA,UACR,KAAA,EAAO,YAAA,CAAa,CAAC,CAAA,EAAG,KAAA;AAAA,UACxB,OAAO,MAAA,CAAO;AAAA;AAChB,OACF;AACA,MAAA,OAAO,CAAC,MAAA,EAAQ,EAAE,CAAA;AAAA,IACpB;AAIA,IAAA,MAAM,MAAA,GAAS,YAAA,CAAa,CAAA,EAAG,EAAE,CAAA;AAGjC,IAAA,IAAI,CAAC,UAAA,EAAY;AACf,MAAA,MAAM,IAAA,GAAiC,EAAE,GAAG,MAAA,EAAQ,OAAO,MAAA,EAAO;AAGlE,MAAA,OAAO,kBAAA,CAAmB,IAAA,EAAM,OAAA,EAAS,EAAE,CAAA;AAAA,IAC7C;AAGA,IAAA,MAAM,OAAA,GAAU,aAAa,CAAC,CAAA;AAC9B,IAAA,MAAM,SAAA,GACJ,OAAA,KAAY,MAAA,GACR,EAAA,CAAG,QAAA,CAAS,CAAA,CAAE,UAAA,EAAY,OAAA,CAAQ,EAAE,CAAA,CAAE,IAAA,GACtC,CAAA,CAAE,UAAA;AACR,IAAA,MAAM,OAAA,GAAU,EAAA,CAAG,KAAA,CAAM,SAAA,EAAW,EAAE,CAAA;AAEtC,IAAA,IAAI,YAAY,IAAA,EAAM;AAEpB,MAAA,MAAM,IAAA,GAAiC;AAAA,QACrC,GAAG,MAAA;AAAA,QACH,KAAA,EAAO,MAAA;AAAA,QACP,UAAA,EAAY;AAAA,OACd;AACA,MAAA,OAAO,kBAAA,CAAmB,IAAA,EAAM,OAAA,EAAS,EAAE,CAAA;AAAA,IAC7C;AAGA,IAAA,MAAM,KAAA,GAAkC;AAAA,MACtC,GAAG,MAAA;AAAA,MACH,YAAY,OAAA,CAAQ;AAAA,KACtB;AACA,IAAA,OAAO,cAAA,CAAe,KAAA,EAAO,OAAA,EAAS,EAAE,CAAA;AAAA,EAC1C;AAOA,EAAA,SAAS,kBAAA,CACP,CAAA,EACA,OAAA,EACA,EAAA,EACoE;AACpE,IAAA,IAAI,SAAS,IAAA,EAAM,OAAO,CAAC,CAAA,EAAG,EAAE,CAAA;AAChC,IAAA,MAAM,CAAC,UAAU,IAAI,CAAA,GAAI,KAAK,KAAA,CAAM,CAAA,CAAE,QAAA,EAAU,OAAA,EAAS,EAAE,CAAA;AAE3D,IAAA,OAAO,CAAC,EAAE,GAAG,CAAA,EAAG,QAAA,IAAY,IAAqC,CAAA;AAAA,EACnE;AAkBA,EAAA,SAAS,UAAA,CACP,GACA,GAAA,EACoE;AAMpE,IAAA,IAAI,CAAA,CAAE,KAAA,KAAU,SAAA,IAAa,CAAA,CAAE,KAAA,KAAU,SAAS,OAAO,CAAC,CAAA,EAAG,EAAE,CAAA;AAG/D,IAAA,IAAI,GAAA,CAAI,EAAA,KAAO,aAAA,CAAc,CAAA,CAAE,KAAA,EAAO,CAAA,CAAE,WAAW,CAAA,EAAG,OAAO,CAAC,CAAA,EAAG,EAAE,CAAA;AACnE,IAAA,MAAM,MAAA,GAAmC;AAAA,MACvC,GAAG,CAAA;AAAA,MACH,KAAA,EAAO,QAAA;AAAA,MACP,SAAS,EAAE,MAAA,EAAQ,UAAA,EAAY,EAAA,EAAI,IAAI,IAAA;AAAK,KAC9C;AACA,IAAA,OAAO,CAAC,MAAA,EAAQ,EAAE,CAAA;AAAA,EACpB;AAmBA,EAAA,SAAS,IAAA,CACP,GACA,EAAA,EACoE;AACpE,IAAA,IAAI,CAAA,CAAE,KAAA,KAAU,SAAA,IAAa,CAAA,CAAE,KAAA,KAAU,SAAS,OAAO,CAAC,CAAA,EAAG,EAAE,CAAA;AAK/D,IAAA,MAAM,CAAC,cAAc,CAAA,GAAI,UAAA,CAAW,IAAA,CAAK,EAAE,QAAQ,CAAA;AACnD,IAAA,MAAM,QAAA,GAAqC;AAAA,MACzC,GAAG,CAAA;AAAA,MACH,KAAA,EAAO,SAAA;AAAA,MACP,cAAA,EAAgB,EAAA;AAAA,MAChB,WAAA,EAAa,EAAE,WAAA,GAAc,CAAA;AAAA,MAC7B,QAAA,EAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAMZ;AACA,IAAA,OAAO,CAAC,QAAA,EAAU,EAAE,CAAA;AAAA,EACtB;AAoBA,EAAA,SAAS,KAAK,CAAA,EAAqD;AACjE,IAAA,IAAI,MAAA,CAAO,UAAA,KAAe,MAAA,EAAW,OAAO,EAAC;AAC7C,IAAA,IAAI,EAAE,KAAA,KAAU,SAAA,IAAa,EAAE,KAAA,KAAU,OAAA,SAAgB,EAAC;AAC1D,IAAA,OAAO;AAAA,MACL,WAAA;AAAA,QACE,aAAA,CAAc,CAAA,CAAE,KAAA,EAAO,CAAA,CAAE,WAAW,CAAA;AAAA,QACpC,CAAA,CAAE,iBAAiB,MAAA,CAAO;AAAA;AAC5B,KACF;AAAA,EACF;AAeA,EAAA,SAAS,SACP,KAAA,EAQA;AACA,IAAA,OAAO,WAAW,QAAA,CAAS,EAAE,KAAA,EAAO,KAAA,CAAM,OAAO,CAAA;AAAA,EACnD;AAOA,EAAA,SAAS,eAAA,CACP,GACA,GAAA,EAC0B;AAC1B,IAAA,IAAI,IAAA,KAAS,MAAM,OAAO,CAAA;AAK1B,IAAA,MAAM,CAAC,SAAS,CAAA,GAAI,KAAK,OAAA,CAAQ,CAAA,CAAE,UAAU,GAAG,CAAA;AAChD,IAAA,OAAO,EAAE,GAAG,CAAA,EAAG,QAAA,EAAU,SAAA,EAAU;AAAA,EACrC;AAEA,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,KAAA;AAAA,IACA,QAAA;AAAA,IACA,SAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,IAAA;AAAA,IACA,eAAA;AAAA,IACA,IAAA;AAAA,IACA;AAAA,GACF;AACF;AAQO,SAAS,OAAA,CAKd,KAAA,EACA,CAAC,KAAA,EAAO,IAAI,CAAA,EACgB;AAC5B,EAAA,OAAO,CAAC,EAAE,GAAG,OAAO,GAAA,EAAK,KAAA,IAAS,IAAI,CAAA;AACxC","file":"chunk-3B5X7SBB.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"]}
|