@demlik/tea 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +1 -1
  2. package/dist/agent/index.d.ts +5 -5
  3. package/dist/agent/index.js +10 -10
  4. package/dist/authed-call/index.d.ts +3 -3
  5. package/dist/authed-call/index.js +7 -7
  6. package/dist/await-terminal/index.d.ts +2 -2
  7. package/dist/await-terminal/index.js +3 -2
  8. package/dist/await-terminal/index.js.map +1 -1
  9. package/dist/batch-window/index.d.ts +4 -4
  10. package/dist/batch-window/index.js +3 -3
  11. package/dist/cache/index.d.ts +2 -2
  12. package/dist/cache/index.js +4 -4
  13. package/dist/{chunk-PUTHY7LR.js → chunk-2KBEIGYP.js} +3 -3
  14. package/dist/{chunk-PUTHY7LR.js.map → chunk-2KBEIGYP.js.map} +1 -1
  15. package/dist/{chunk-QKUWDC4P.js → chunk-2UGTDYOK.js} +3 -3
  16. package/dist/{chunk-QKUWDC4P.js.map → chunk-2UGTDYOK.js.map} +1 -1
  17. package/dist/{chunk-ZILCH3WS.js → chunk-66S2KEQQ.js} +4 -4
  18. package/dist/{chunk-ZILCH3WS.js.map → chunk-66S2KEQQ.js.map} +1 -1
  19. package/dist/{chunk-362SZ6CS.js → chunk-A7USKARI.js} +3 -3
  20. package/dist/{chunk-362SZ6CS.js.map → chunk-A7USKARI.js.map} +1 -1
  21. package/dist/{chunk-ZT3MGQB7.js → chunk-ASX7PL2K.js} +3 -3
  22. package/dist/{chunk-ZT3MGQB7.js.map → chunk-ASX7PL2K.js.map} +1 -1
  23. package/dist/{chunk-62QBHM3T.js → chunk-AZ3NHUPD.js} +3 -3
  24. package/dist/{chunk-62QBHM3T.js.map → chunk-AZ3NHUPD.js.map} +1 -1
  25. package/dist/{chunk-7FYQGDMF.js → chunk-AZZLE7XR.js} +3 -3
  26. package/dist/{chunk-7FYQGDMF.js.map → chunk-AZZLE7XR.js.map} +1 -1
  27. package/dist/{chunk-W2S4HK7J.js → chunk-EC6FO42Q.js} +6 -6
  28. package/dist/{chunk-W2S4HK7J.js.map → chunk-EC6FO42Q.js.map} +1 -1
  29. package/dist/{chunk-T6HDAPMR.js → chunk-ERYY5P6W.js} +3 -3
  30. package/dist/{chunk-T6HDAPMR.js.map → chunk-ERYY5P6W.js.map} +1 -1
  31. package/dist/{chunk-CIDS43FH.js → chunk-IVHQCVFX.js} +3 -3
  32. package/dist/{chunk-CIDS43FH.js.map → chunk-IVHQCVFX.js.map} +1 -1
  33. package/dist/{chunk-DBOJ2UC6.js → chunk-O7ED34JH.js} +4 -2
  34. package/dist/chunk-O7ED34JH.js.map +1 -0
  35. package/dist/{chunk-LFW66IIV.js → chunk-S6Y3RNX4.js} +4 -4
  36. package/dist/{chunk-LFW66IIV.js.map → chunk-S6Y3RNX4.js.map} +1 -1
  37. package/dist/{chunk-KEFMI24G.js → chunk-UV7VCTNC.js} +7 -7
  38. package/dist/{chunk-KEFMI24G.js.map → chunk-UV7VCTNC.js.map} +1 -1
  39. package/dist/{chunk-7UU5P2CX.js → chunk-WF46FABO.js} +3 -3
  40. package/dist/{chunk-7UU5P2CX.js.map → chunk-WF46FABO.js.map} +1 -1
  41. package/dist/{chunk-KWPB6BM6.js → chunk-XMHZ44QC.js} +4 -4
  42. package/dist/{chunk-KWPB6BM6.js.map → chunk-XMHZ44QC.js.map} +1 -1
  43. package/dist/{chunk-7A74NSG4.js → chunk-XOAP3LOT.js} +3 -3
  44. package/dist/{chunk-7A74NSG4.js.map → chunk-XOAP3LOT.js.map} +1 -1
  45. package/dist/{chunk-PBAYOJJO.js → chunk-ZH454CR5.js} +9 -3
  46. package/dist/chunk-ZH454CR5.js.map +1 -0
  47. package/dist/{core-DPYOyOJv.d.ts → core-CUN5HDKL.d.ts} +16 -1
  48. package/dist/deadline/index.d.ts +2 -2
  49. package/dist/deadline/index.js +3 -3
  50. package/dist/do/index.d.ts +9 -9
  51. package/dist/do/index.js +10 -10
  52. package/dist/do/index.js.map +1 -1
  53. package/dist/{durable-effects-B0YpoCHg.d.ts → durable-effects-CXMOi-sp.d.ts} +2 -2
  54. package/dist/extension/index.d.ts +2 -2
  55. package/dist/extension/index.js +3 -3
  56. package/dist/extension/react.js +3 -3
  57. package/dist/extension/subs/index.d.ts +2 -2
  58. package/dist/extension/test-utils.d.ts +1 -1
  59. package/dist/extension/test-utils.js.map +1 -1
  60. package/dist/fan-out/index.d.ts +2 -2
  61. package/dist/idempotent-intake/index.d.ts +2 -2
  62. package/dist/index.d.ts +4 -4
  63. package/dist/index.js +2 -2
  64. package/dist/llm-call/index.d.ts +3 -3
  65. package/dist/llm-call/index.js +7 -7
  66. package/dist/{machine-Q_Bsm7Vu.d.ts → machine-CoWpvp8x.d.ts} +3 -3
  67. package/dist/machine-viz/index.d.ts +1 -1
  68. package/dist/machine-viz/index.js +2 -2
  69. package/dist/mem/index.d.ts +2 -2
  70. package/dist/monitored-run/index.d.ts +4 -4
  71. package/dist/monitored-run/index.js +5 -5
  72. package/dist/node/index.d.ts +2 -2
  73. package/dist/paginated-walk/index.d.ts +3 -3
  74. package/dist/paginated-walk/index.js +7 -7
  75. package/dist/parity/index.d.ts +30 -2
  76. package/dist/parity/index.js +3 -3
  77. package/dist/parity/index.js.map +1 -1
  78. package/dist/pbt/arbitraries/index.d.ts +3 -3
  79. package/dist/pbt/arbitraries/index.js +1 -1
  80. package/dist/pbt/index.d.ts +1 -1
  81. package/dist/pbt/index.js +4 -4
  82. package/dist/pbt/runners/index.d.ts +1 -1
  83. package/dist/pbt/runners/index.js +3 -3
  84. package/dist/poller/index.d.ts +3 -3
  85. package/dist/poller/index.js +3 -3
  86. package/dist/prediction/index.d.ts +1 -1
  87. package/dist/prediction/index.js +2 -2
  88. package/dist/pure/index.d.ts +1 -1
  89. package/dist/pure/index.js +2 -2
  90. package/dist/react/index.d.ts +2 -2
  91. package/dist/react/index.js +2 -2
  92. package/dist/reconciler/index.d.ts +3 -3
  93. package/dist/reconciler/index.js +7 -7
  94. package/dist/recorder/index.d.ts +2 -2
  95. package/dist/resilient-call/index.d.ts +2 -2
  96. package/dist/resilient-call/index.js +6 -6
  97. package/dist/retry-to-success/index.js +2 -0
  98. package/dist/retry-to-success/index.js.map +1 -1
  99. package/dist/{runtime-types-uBuVvZqa.d.ts → runtime-types-BD3ZFfdw.d.ts} +2 -2
  100. package/dist/saga/index.d.ts +2 -2
  101. package/dist/snapshot/index.d.ts +1 -1
  102. package/dist/snapshot/index.js +3 -3
  103. package/dist/subs/index.d.ts +3 -3
  104. package/dist/subs/index.js +3 -3
  105. package/dist/testing/index.d.ts +2 -2
  106. package/dist/testing/index.js +2 -2
  107. package/dist/throttled-input/index.d.ts +2 -2
  108. package/dist/throttled-input/index.js +6 -6
  109. package/dist/token-refresh/index.d.ts +1 -1
  110. package/dist/token-refresh/index.js +3 -3
  111. package/dist/trace-replay/index.d.ts +2 -2
  112. package/dist/trace-replay/index.js +3 -3
  113. package/dist/{types-C6mXL9wA.d.ts → types-CjAkI83U.d.ts} +1 -1
  114. package/dist/with-deadline/index.d.ts +1 -1
  115. package/dist/with-deadline/index.js +2 -2
  116. package/dist/with-resilience/index.d.ts +2 -2
  117. package/dist/with-resilience/index.js +6 -6
  118. package/dist/with-telemetry/index.d.ts +1 -1
  119. package/dist/with-telemetry/index.js +2 -2
  120. package/dist/work-queue/adapter.d.ts +2 -2
  121. package/dist/work-queue/index.d.ts +3 -3
  122. package/dist/work-queue/index.js.map +1 -1
  123. package/dist/work-queue/ops.d.ts +2 -2
  124. package/dist/workflow/index.d.ts +6 -4
  125. package/dist/workflow/index.js +1 -1
  126. package/dist/workflow/index.js.map +1 -1
  127. package/package.json +12 -4
  128. package/dist/chunk-DBOJ2UC6.js.map +0 -1
  129. package/dist/chunk-PBAYOJJO.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { fromInterval } from './chunk-QKUWDC4P.js';
1
+ import { fromInterval } from './chunk-2UGTDYOK.js';
2
2
 
3
3
  // src/cache/index.ts
4
4
  function initCache() {
@@ -48,5 +48,5 @@ function cacheEvictionSub(id, everyMs) {
48
48
  var cacheEvictionSubscribe = fromInterval((sub) => cacheEvictMsg(sub.id));
49
49
 
50
50
  export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, evictExpired, get, has, initCache, remove, set };
51
- //# sourceMappingURL=chunk-7UU5P2CX.js.map
52
- //# sourceMappingURL=chunk-7UU5P2CX.js.map
51
+ //# sourceMappingURL=chunk-WF46FABO.js.map
52
+ //# sourceMappingURL=chunk-WF46FABO.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cache/index.ts"],"names":[],"mappings":";;;AAuEO,SAAS,SAAA,GAA4B;AAC1C,EAAA,OAAO,EAAE,OAAA,EAAS,EAAC,EAAE;AACvB;AAQA,SAAS,SAAA,CAAa,OAAsB,KAAA,EAAwB;AAClE,EAAA,OAAO,SAAS,KAAA,CAAM,WAAA;AACxB;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,UAAU,MAAA,IAAa,SAAA,CAAU,KAAA,EAAO,KAAK,GAAG,OAAO,MAAA;AAC3D,EAAA,OAAO,KAAA,CAAM,KAAA;AACf;AAMO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACS;AACT,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,CAAC,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA;AAChC;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACA,OACA,KAAA,EACa;AACb,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,WAAA,CAAY,GAAG,CAAA,GAAI,EAAE,KAAA,EAAO,WAAA,EAAa,QAAQ,KAAA,EAAM;AACvD,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAOO,SAAS,MAAA,CAAU,OAAoB,GAAA,EAA0B;AACtE,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,OAAO,YAAY,GAAG,CAAA;AACtB,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AASO,SAAS,YAAA,CACd,OACA,KAAA,EACa;AACb,EAAA,MAAM,cAA6C,EAAC;AACpD,EAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA,EAAG;AACxD,IAAA,IAAI,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA,EAAG;AAC7B,IAAA,WAAA,CAAY,GAAG,CAAA,GAAI,KAAA;AAAA,EACrB;AACA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAmCO,SAAS,cAAc,EAAA,EAA2B;AACvD,EAAA,OAAO,EAAE,IAAA,EAAM,aAAA,EAAe,EAAA,EAAG;AACnC;AAqBO,SAAS,gBAAA,CACd,IACA,OAAA,EACkB;AAClB,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,IAAA,EAAM,OAAA;AAAA,IACN,UAAA,EAAY;AAAA,GACd;AACF;AAcO,IAAM,yBAIT,YAAA,CAAa,CAAC,QAAQ,aAAA,CAAc,GAAA,CAAI,EAAE,CAAC","file":"chunk-7UU5P2CX.js","sourcesContent":["/**\n * @demlik/tea/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.\n *\n * Same pure-state-ops shape as `@demlik/tea/idempotency` and\n * `@demlik/tea/work-queue`: 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 `@demlik/tea/subs`' `fromInterval`) 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 * NOT exported from the package root — reached via the `@demlik/tea/cache`\n * subpath, same one-shape-per-package rule as `work-queue`, `idempotency`, and\n * `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
+ {"version":3,"sources":["../src/cache/index.ts"],"names":[],"mappings":";;;AAuEO,SAAS,SAAA,GAA4B;AAC1C,EAAA,OAAO,EAAE,OAAA,EAAS,EAAC,EAAE;AACvB;AAQA,SAAS,SAAA,CAAa,OAAsB,KAAA,EAAwB;AAClE,EAAA,OAAO,SAAS,KAAA,CAAM,WAAA;AACxB;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,UAAU,MAAA,IAAa,SAAA,CAAU,KAAA,EAAO,KAAK,GAAG,OAAO,MAAA;AAC3D,EAAA,OAAO,KAAA,CAAM,KAAA;AACf;AAMO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACS;AACT,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,CAAC,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA;AAChC;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACA,OACA,KAAA,EACa;AACb,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,WAAA,CAAY,GAAG,CAAA,GAAI,EAAE,KAAA,EAAO,WAAA,EAAa,QAAQ,KAAA,EAAM;AACvD,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAOO,SAAS,MAAA,CAAU,OAAoB,GAAA,EAA0B;AACtE,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,OAAO,YAAY,GAAG,CAAA;AACtB,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AASO,SAAS,YAAA,CACd,OACA,KAAA,EACa;AACb,EAAA,MAAM,cAA6C,EAAC;AACpD,EAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA,EAAG;AACxD,IAAA,IAAI,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA,EAAG;AAC7B,IAAA,WAAA,CAAY,GAAG,CAAA,GAAI,KAAA;AAAA,EACrB;AACA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAmCO,SAAS,cAAc,EAAA,EAA2B;AACvD,EAAA,OAAO,EAAE,IAAA,EAAM,aAAA,EAAe,EAAA,EAAG;AACnC;AAqBO,SAAS,gBAAA,CACd,IACA,OAAA,EACkB;AAClB,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,IAAA,EAAM,OAAA;AAAA,IACN,UAAA,EAAY;AAAA,GACd;AACF;AAcO,IAAM,yBAIT,YAAA,CAAa,CAAC,QAAQ,aAAA,CAAc,GAAA,CAAI,EAAE,CAAC","file":"chunk-WF46FABO.js","sourcesContent":["/**\n * @demlik/tea/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.\n *\n * Same pure-state-ops shape as `@demlik/tea/idempotency` and\n * `@demlik/tea/work-queue`: 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 `@demlik/tea/subs`' `fromInterval`) 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 * NOT exported from the package root — reached via the `@demlik/tea/cache`\n * subpath, same one-shape-per-package rule as `work-queue`, `idempotency`, and\n * `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,5 +1,5 @@
1
- import { tryInterpret } from './chunk-PBAYOJJO.js';
2
- import { Cmd } from './chunk-DBOJ2UC6.js';
1
+ import { tryInterpret } from './chunk-ZH454CR5.js';
2
+ import { Cmd } from './chunk-O7ED34JH.js';
3
3
 
4
4
  // src/snapshot/index.ts
5
5
  var DEFAULT_SNAPSHOT_KEY = "@@snapshot";
@@ -89,5 +89,5 @@ function createSnapshot(config) {
89
89
  }
90
90
 
91
91
  export { createSnapshot, snapshotFailed, snapshotLoadFailed, snapshotLoaded, snapshotSaved };
92
- //# sourceMappingURL=chunk-KWPB6BM6.js.map
93
- //# sourceMappingURL=chunk-KWPB6BM6.js.map
92
+ //# sourceMappingURL=chunk-XMHZ44QC.js.map
93
+ //# sourceMappingURL=chunk-XMHZ44QC.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-KWPB6BM6.js","sourcesContent":["/**\n * @demlik/tea/snapshot — periodic state checkpoint to a host store (R2/KV-shaped).\n *\n * The problem it solves: a long-running machine (a multi-stage run, a paginated\n * crawl, a saga) lives in TEA's in-memory `State` and is persisted as ONE blob\n * by the substrate's `Store<S>` on every transition. That blob is fine for\n * crash recovery of the *runtime* — but it is NOT a coarse, named checkpoint a\n * human (or a separate recovery job) can point a restart at. `snapshot` adds\n * the \"every N units of progress, write a durable checkpoint to R2/KV\"\n * discipline on top, so a crashed long run restarts from the last checkpoint\n * instead of from zero (the field-guide pain row).\n *\n * Shape: this is the same knob contract every L2 composition follows —\n * `createSnapshot(config)` hands back a `{ init, <verbs>, handlers }` bundle you\n * splice into your machine. The state it owns is a Model slice (durable +\n * replayable, the two non-negotiables); the verbs are PURE decisions; the I/O\n * (the actual `put` to the store) lives only in `handlers`.\n *\n * It is a LEAF module: it imports only the core substrate (`../index`, for\n * `Cmd` / `Interpret`) and no sibling subpath. (It does NOT reuse\n * `@demlik/tea/retry-backoff` — that is an even-leafier utility that depends on\n * nothing at all, not even the core; the kinship here is \"leaf with injected\n * time\", not a shared dependency.)\n * Nothing here reads the clock or the RNG on its own behalf — time enters as an\n * `at`/`now` parameter at the verb boundary and the store I/O is realized only\n * when the runtime performs the emitted `Cmd`. That keeps every verb\n * deterministic (invariant 2) and the checkpoint write a `Cmd` the runtime\n * interprets (invariant 3).\n *\n * The Store port is deliberately ABSTRACT — `{ get, put }`, the lowest common\n * shape of R2 and KV — so this module never imports `@cloudflare/workers-types`\n * and can be checkpointed to anything (R2, KV, a DO sub-key, an in-memory map in\n * tests). The host binds the concrete adapter at `handlers({ store })`.\n *\n * Typical wiring:\n *\n * const snap = createSnapshot<RunState>({ every: 25, key: \"run/checkpoint\" });\n *\n * // machine state:\n * type State = { run: RunState; snapshot: SnapshotState };\n * init: (loaded) => loaded ? [loaded, []] : [{ run: initRun(), snapshot: snap.init() }, []],\n *\n * // in `update`, on each progress Msg, fold the snapshot slice:\n * progress: (s, msg) => {\n * const run = advance(s.run, msg);\n * const [snapshot, cmds] = snap.record(s.snapshot, run, msg.at);\n * return [{ ...s, run, snapshot }, cmds];\n * },\n * // the write completed → durable ack folds back through update:\n * snapshot_saved: (s, msg) => {\n * const [snapshot, cmds] = snap.confirm(s.snapshot, msg);\n * return [{ ...s, snapshot }, cmds];\n * },\n *\n * // RECOVERY — the half that makes a checkpoint a restart point, not just a\n * // backup. A `boot` Msg the host dispatches once after `run(...)` asks the\n * // store for the durable checkpoint; the answer folds the recovered payload\n * // back into the run slice:\n * boot: (s) => snap.requestLoad(s), // emits snapshot_load Cmd\n * snapshot_loaded: (s, msg) => {\n * if (msg.payload === null) return [s, []]; // no checkpoint → keep fresh init\n * const [snapshot, cmds] = snap.boot(s.snapshot);\n * return [{ ...s, run: msg.payload, snapshot }, cmds];\n * },\n * snapshot_load_failed: (s) => [s, []], // errors are data — decide here\n *\n * // splice the I/O — both effect cells (write + load):\n * interpret: { ...snap.handlers({ store: r2Adapter }) },\n */\n\nimport { Cmd, type Interpret, tryInterpret } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config — the knob.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot knob.\n *\n * `every` is the cadence: a checkpoint is written once `every` units of\n * progress have accumulated since the last one (see `record`). It is the\n * `snapshotEvery` field `monitored-run` threads through to this module.\n *\n * `key` is the store key the checkpoint is written under. A single machine\n * keeps a single rolling checkpoint, so the key is fixed at config time rather\n * than passed per write — the latest checkpoint overwrites the previous one\n * (R2/KV are last-write-wins on a key). Defaults to `\"@@snapshot\"`, mirroring\n * `doStore`'s `@@state` default-key convention.\n */\nexport interface SnapshotConfig {\n /**\n * Write a checkpoint once this many progress units have accumulated since the\n * last write. `every: 1` checkpoints on every `record`; a value `<= 0` is\n * treated as `1` (every progress unit triggers a write) — a non-positive\n * cadence has no other sensible meaning, and clamping keeps `record` from\n * never checkpointing on a misconfigured `0`.\n */\n readonly every: number;\n /** Store key the rolling checkpoint is written under. Defaults to `\"@@snapshot\"`. */\n readonly key?: string;\n}\n\nconst DEFAULT_SNAPSHOT_KEY = \"@@snapshot\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Slice — the Model field this knob owns.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot bookkeeping slice. Plain data — JSON-serializable so it rides\n * inside the consumer's `Store<S>` blob and survives DO eviction / reload (the\n * \"durable\" non-negotiable). It carries NO payload: the thing being\n * checkpointed lives elsewhere in the machine's state; this slice only tracks\n * *when* to checkpoint and *what has been acknowledged durable*.\n *\n * - `sinceLast` — progress units accumulated since the last checkpoint\n * DECISION (reset to 0 the moment `record` decides to write, not when the\n * write lands). The cadence counter.\n * - `seq` — monotonic checkpoint sequence. Bumped each time a write is\n * emitted, so the write Cmd and its later acknowledgement can be matched\n * even if several are in flight. The \"version\" of the latest emitted\n * checkpoint.\n * - `lastSavedSeq` — the highest `seq` whose write the host has confirmed\n * durable (via `confirm`). `null` until the first confirmed write. The gap\n * `seq - lastSavedSeq` is the \"writes emitted but not yet acknowledged\"\n * depth — observable for backpressure / health.\n * - `lastSavedAt` — the `at` of the most recent confirmed write (epoch ms),\n * `null` until the first. Carried for observability, never compared inside\n * a verb (time is injected, never read here).\n */\nexport interface SnapshotState {\n readonly sinceLast: number;\n readonly seq: number;\n readonly lastSavedSeq: number | null;\n readonly lastSavedAt: number | null;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Store port — abstract R2/KV-shaped checkpoint sink.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The abstract checkpoint sink — the intersection of R2's and KV's surface,\n * narrowed to exactly the two methods a rolling checkpoint needs. Deliberately\n * NOT the substrate's `Store<S>` (`{ load, save, migrate }`): that is the\n * runtime's whole-state persistence boundary keyed at a fixed cell, whereas a\n * checkpoint store is keyed, value-typed, and host-supplied.\n *\n * - `put(key, value)` — write the checkpoint. Last-write-wins on `key`.\n * - `get(key)` — read the latest checkpoint back, or `null` if none. Used by\n * a recovery path to seed `init`'s `loaded` from the durable checkpoint\n * when the runtime's own `Store<S>` blob is gone.\n *\n * No `@cloudflare/workers-types` import — an R2 bucket, a KV namespace, a DO\n * sub-key, or an in-memory `Map` all satisfy this shape with a 3-line adapter\n * at the host.\n */\nexport interface SnapshotStore<V> {\n get(key: string): Promise<V | null>;\n put(key: string, value: V): Promise<void>;\n}\n\n/**\n * The ports `handlers` binds. Just the store today — named so a future port\n * (a metrics sink, a clock) can grow without breaking the `handlers(ports)`\n * call shape.\n */\nexport interface SnapshotPorts<V> {\n readonly store: SnapshotStore<V>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmd + Msg — the write effect and its durable acknowledgement.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The checkpoint-write Cmd a `record` / `force` decision emits. Plain data\n * (invariant 1 — Cmds are data, not closures): it carries the `payload` to\n * write, the `key` to write it under, the `seq` it represents, and the `at` it\n * was decided. The handler performs the `put` and routes the result back to a\n * `confirm` / fail Msg.\n *\n * `payload` is `V` — the consumer's serializable checkpoint value (the run\n * state, the cursor, whatever the machine wants restartable). The verb takes it\n * as an argument so the snapshot slice never has to hold a copy of the\n * machine's domain state.\n */\nexport type SnapshotWriteCmd<V> = Cmd<\"snapshot_write\"> & {\n readonly key: string;\n readonly seq: number;\n readonly at: number;\n readonly payload: V;\n};\n\n/**\n * The Msg the write handler dispatches on a SUCCESSFUL `put`. Fold it back\n * through `confirm(state, msg)` to advance `lastSavedSeq` / `lastSavedAt`. The\n * `seq` matches the emitting Cmd so a late acknowledgement of an older write\n * can never regress a newer confirmed one.\n */\nexport type SnapshotSavedMsg = {\n readonly type: \"snapshot_saved\";\n readonly seq: number;\n readonly at: number;\n};\n\n/**\n * The Msg the write handler dispatches on a FAILED `put`. Errors are data\n * (never swallowed): the consumer's reducer decides whether a failed checkpoint\n * is fatal, retryable (compose `@demlik/tea/retry-backoff`), or merely logged.\n * `seq` identifies which checkpoint failed; `error` is carried, never\n * interpreted here.\n */\nexport type SnapshotFailedMsg = {\n readonly type: \"snapshot_failed\";\n readonly seq: number;\n readonly error: unknown;\n};\n\n/**\n * The checkpoint-READ Cmd a `requestLoad` decision emits — the recovery half of\n * the module. Plain data (invariant 1): it carries only the `key` to read. The\n * handler performs `store.get(key)` and routes the result back to a\n * `snapshot_loaded` (Ok, payload-or-null) / `snapshot_load_failed` (Err) Msg.\n *\n * Distinct from `snapshot_write`: a write carries a payload OUT to the store; a\n * load asks the store to hand a payload BACK. The consumer dispatches the\n * triggering Msg once on resume (a `boot` Msg, never `init`'s rehydrate branch —\n * invariant 2), folds the recovered payload into its run slice, then continues.\n */\nexport type SnapshotLoadCmd = Cmd<\"snapshot_load\"> & {\n readonly key: string;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` RESOLVES. `payload` is\n * the recovered checkpoint value, or `null` when the store has no checkpoint\n * under `key` (fresh boot — never checkpointed, or a wiped store). The consumer\n * folds this into `init`'s loaded branch: `payload === null` → keep the fresh\n * `init` state; non-null → seed the run slice from the durable checkpoint and\n * `boot` the snapshot slice's cadence counter.\n *\n * A `null` payload is NOT an error — \"no checkpoint yet\" is a normal first-run\n * state, distinct from \"the read failed\" (`snapshot_load_failed`).\n */\nexport type SnapshotLoadedMsg<V> = {\n readonly type: \"snapshot_loaded\";\n readonly payload: V | null;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` REJECTS. Errors are data\n * (never swallowed — the field-guide rule): a failed recovery read is a visible\n * signal the consumer's reducer decides on (fatal-abort, retry via\n * `@demlik/tea/retry-backoff`, or boot-fresh-and-log). `error` is carried, never\n * interpreted here. Kept separate from `snapshot_loaded`'s `null` so \"the store\n * has no checkpoint\" can never masquerade as \"the read failed\".\n */\nexport type SnapshotLoadFailedMsg = {\n readonly type: \"snapshot_load_failed\";\n readonly error: unknown;\n};\n\n/** Construct a `snapshot_saved` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotSaved(seq: number, at: number): SnapshotSavedMsg {\n return { type: \"snapshot_saved\", seq, at };\n}\n\n/** Construct a `snapshot_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotFailed(seq: number, error: unknown): SnapshotFailedMsg {\n return { type: \"snapshot_failed\", seq, error };\n}\n\n/** Construct a `snapshot_loaded` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoaded<V>(payload: V | null): SnapshotLoadedMsg<V> {\n return { type: \"snapshot_loaded\", payload };\n}\n\n/** Construct a `snapshot_load_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoadFailed(error: unknown): SnapshotLoadFailedMsg {\n return { type: \"snapshot_load_failed\", error };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The combinator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The bundle `createSnapshot` returns — the uniform knob contract. `subs` is\n * omitted: a checkpoint is driven by progress (a `record` call from the\n * consumer's reducer), not by a timer, so this module has no subscriptions.\n */\nexport interface SnapshotKnob<V> {\n /** The initial slice — zero progress, no checkpoint emitted or confirmed yet. */\n init(): SnapshotState;\n /**\n * Account one unit of progress. PURE. Increments `sinceLast`; once the\n * accumulated count reaches the configured cadence, DECIDES to checkpoint:\n * resets `sinceLast` to 0, bumps `seq`, and emits a single `snapshot_write`\n * Cmd carrying `payload` + `key` + the new `seq` + `at`. Below the cadence it\n * returns the bumped slice and NO Cmd.\n *\n * `payload` is the checkpoint value to persist (the consumer's domain state);\n * `at` is the caller-stamped time the progress occurred (epoch ms) — injected,\n * never read from the clock here.\n */\n record(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Force a checkpoint regardless of the cadence counter. PURE. Always bumps\n * `seq`, resets `sinceLast`, and emits the write Cmd. Use on a phase boundary\n * the cadence would otherwise miss — e.g. \"the run just finished, checkpoint\n * the terminal state now\" — so the last durable checkpoint is the final state,\n * not whatever the modulo happened to land on.\n */\n force(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Record a confirmed durable write. PURE. Advances `lastSavedSeq` /\n * `lastSavedAt` to the acknowledged `seq` / `at`, but only forward: an\n * out-of-order acknowledgement of an OLDER `seq` (a slow write landing after a\n * newer one) is ignored, so the confirmed watermark is monotonic. Fold the\n * `snapshot_saved` Msg here.\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — an ack emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_saved` cell without the caller\n * re-wrapping a bare State.\n */\n confirm(\n state: SnapshotState,\n msg: SnapshotSavedMsg,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Resume after a reload. PURE. Resets `sinceLast` to 0 because any\n * un-checkpointed progress accumulated before the crash is moot — recovery\n * resumes from the last DURABLE checkpoint (`get` from the store), so the\n * cadence counter starts fresh from there. `seq` / `lastSavedSeq` /\n * `lastSavedAt` are preserved (they describe what is durable, which a reload\n * does not change). Call from a `boot` Msg the host dispatches once after\n * `run(...)`, never from `init`'s rehydrate branch (invariant 2).\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — a resume emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_loaded` cell without the caller\n * re-wrapping a bare State.\n */\n boot(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Ask the store for the durable checkpoint. PURE. Emits a single\n * `snapshot_load` Cmd carrying the configured `key`; returns the slice\n * UNCHANGED (a read decides nothing about the cadence bookkeeping — only the\n * later `snapshot_loaded` fold, via `boot`, touches the slice). This is the\n * entry point of the recovery loop: the consumer dispatches a `boot` Msg once\n * after `run(...)`, folds `requestLoad` here, and the emitted Cmd's handler\n * reads `store.get(key)` and dispatches `snapshot_loaded` / `snapshot_load_failed`.\n *\n * Returns `readonly [SnapshotState, readonly Cmd[]]` (the reducer-cell shape)\n * so it drops straight into a `boot: (s) => snap.requestLoad(s)` cell.\n */\n requestLoad(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * The `interpret` map for this knob's two effect Cmds:\n *\n * - `snapshot_write` — binds the store and performs the `put`, routing\n * Ok → `snapshot_saved` / Err → `snapshot_failed`.\n * - `snapshot_load` — binds the store and performs the `get`, routing\n * Ok → `snapshot_loaded` (payload-or-null) / Err → `snapshot_load_failed`.\n *\n * Railway throughout — neither effect throws into the runtime; the outcome is\n * always a Msg. Spread into your machine's `interpret`.\n */\n handlers(\n ports: SnapshotPorts<V>,\n ): Interpret<\n | SnapshotSavedMsg\n | SnapshotFailedMsg\n | SnapshotLoadedMsg<V>\n | SnapshotLoadFailedMsg,\n SnapshotWriteCmd<V> | SnapshotLoadCmd,\n unknown\n >;\n}\n\n/**\n * Create a snapshot knob from a `SnapshotConfig`. The returned bundle is spliced\n * into a consumer machine: `init()` seeds the slice, `record` / `force` are\n * folded in reducer cells, `confirm` / `boot` handle the acknowledgement +\n * resume Msgs, and `handlers({ store })` supplies the write `interpret` cell.\n */\nexport function createSnapshot<V>(config: SnapshotConfig): SnapshotKnob<V> {\n // Clamp the cadence to >= 1 at construction so `record`'s threshold check is\n // branchless and a misconfigured `0` / negative `every` checkpoints on every\n // progress unit instead of never.\n const every = config.every >= 1 ? config.every : 1;\n const key = config.key ?? DEFAULT_SNAPSHOT_KEY;\n\n /**\n * Emit the write Cmd for a checkpoint decision. Shared by `record`'s\n * cadence-hit branch and `force`. Bumps `seq`, resets `sinceLast`, returns the\n * new slice + the single `snapshot_write` Cmd. The input slice is never\n * mutated (invariant 1).\n */\n function emitWrite(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]] {\n const seq = state.seq + 1;\n const next: SnapshotState = { ...state, sinceLast: 0, seq };\n const cmd: SnapshotWriteCmd<V> = {\n type: \"snapshot_write\",\n key,\n seq,\n at,\n payload,\n };\n return [next, [cmd]];\n }\n\n return {\n init(): SnapshotState {\n return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };\n },\n\n record(state, payload, at) {\n const sinceLast = state.sinceLast + 1;\n // Below the cadence: accumulate, emit nothing.\n if (sinceLast < every) {\n return [{ ...state, sinceLast }, Cmd.none];\n }\n // Cadence reached: checkpoint. `emitWrite` resets the counter + bumps seq.\n return emitWrite(state, payload, at);\n },\n\n force(state, payload, at) {\n return emitWrite(state, payload, at);\n },\n\n confirm(state, msg) {\n // Forward-only watermark: ignore a stale ack of an older seq so a slow\n // write landing after a newer one can't regress `lastSavedSeq`.\n if (state.lastSavedSeq !== null && msg.seq <= state.lastSavedSeq) {\n return [state, Cmd.none];\n }\n return [\n { ...state, lastSavedSeq: msg.seq, lastSavedAt: msg.at },\n Cmd.none,\n ];\n },\n\n boot(state) {\n return [{ ...state, sinceLast: 0 }, Cmd.none];\n },\n\n requestLoad(state) {\n // A read decides nothing about the cadence bookkeeping — return the slice\n // unchanged and emit the single read Cmd. The recovered payload folds back\n // through `snapshot_loaded` (the consumer's reducer), not here.\n const cmd: SnapshotLoadCmd = { type: \"snapshot_load\", key };\n return [state, [cmd]];\n },\n\n handlers(ports) {\n const { store } = ports;\n return {\n // The write effect. Performs the `put`; on success the saved `seq`/`at`\n // ride the `snapshot_saved` Msg back into `confirm`; on failure the\n // error rides `snapshot_failed`. Railway via the core's `tryInterpret`\n // (matching every other module's handler) — the handler always resolves\n // with a Msg, never rejects into the runtime.\n snapshot_write: tryInterpret<\n SnapshotWriteCmd<V>,\n void,\n SnapshotSavedMsg | SnapshotFailedMsg,\n unknown\n >(\n (cmd) => store.put(cmd.key, cmd.payload),\n (_value, cmd) => snapshotSaved(cmd.seq, cmd.at),\n (error, cmd) => snapshotFailed(cmd.seq, error),\n ),\n // The read effect — the recovery half. Performs the `get`; on success\n // the recovered payload (or `null` when no checkpoint exists) rides the\n // `snapshot_loaded` Msg the consumer folds into `init`'s loaded branch;\n // on failure the error rides `snapshot_load_failed`. Same Railway\n // discipline via `tryInterpret` — the read never rejects into the runtime.\n snapshot_load: tryInterpret<\n SnapshotLoadCmd,\n V | null,\n SnapshotLoadedMsg<V> | SnapshotLoadFailedMsg,\n unknown\n >(\n (cmd) => store.get(cmd.key),\n (payload) => snapshotLoaded(payload),\n (error) => snapshotLoadFailed(error),\n ),\n };\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-XMHZ44QC.js","sourcesContent":["/**\n * @demlik/tea/snapshot — periodic state checkpoint to a host store (R2/KV-shaped).\n *\n * The problem it solves: a long-running machine (a multi-stage run, a paginated\n * crawl, a saga) lives in TEA's in-memory `State` and is persisted as ONE blob\n * by the substrate's `Store<S>` on every transition. That blob is fine for\n * crash recovery of the *runtime* — but it is NOT a coarse, named checkpoint a\n * human (or a separate recovery job) can point a restart at. `snapshot` adds\n * the \"every N units of progress, write a durable checkpoint to R2/KV\"\n * discipline on top, so a crashed long run restarts from the last checkpoint\n * instead of from zero (the field-guide pain row).\n *\n * Shape: this is the same knob contract every L2 composition follows —\n * `createSnapshot(config)` hands back a `{ init, <verbs>, handlers }` bundle you\n * splice into your machine. The state it owns is a Model slice (durable +\n * replayable, the two non-negotiables); the verbs are PURE decisions; the I/O\n * (the actual `put` to the store) lives only in `handlers`.\n *\n * It is a LEAF module: it imports only the core substrate (`../index`, for\n * `Cmd` / `Interpret`) and no sibling subpath. (It does NOT reuse\n * `@demlik/tea/retry-backoff` — that is an even-leafier utility that depends on\n * nothing at all, not even the core; the kinship here is \"leaf with injected\n * time\", not a shared dependency.)\n * Nothing here reads the clock or the RNG on its own behalf — time enters as an\n * `at`/`now` parameter at the verb boundary and the store I/O is realized only\n * when the runtime performs the emitted `Cmd`. That keeps every verb\n * deterministic (invariant 2) and the checkpoint write a `Cmd` the runtime\n * interprets (invariant 3).\n *\n * The Store port is deliberately ABSTRACT — `{ get, put }`, the lowest common\n * shape of R2 and KV — so this module never imports `@cloudflare/workers-types`\n * and can be checkpointed to anything (R2, KV, a DO sub-key, an in-memory map in\n * tests). The host binds the concrete adapter at `handlers({ store })`.\n *\n * Typical wiring:\n *\n * const snap = createSnapshot<RunState>({ every: 25, key: \"run/checkpoint\" });\n *\n * // machine state:\n * type State = { run: RunState; snapshot: SnapshotState };\n * init: (loaded) => loaded ? [loaded, []] : [{ run: initRun(), snapshot: snap.init() }, []],\n *\n * // in `update`, on each progress Msg, fold the snapshot slice:\n * progress: (s, msg) => {\n * const run = advance(s.run, msg);\n * const [snapshot, cmds] = snap.record(s.snapshot, run, msg.at);\n * return [{ ...s, run, snapshot }, cmds];\n * },\n * // the write completed → durable ack folds back through update:\n * snapshot_saved: (s, msg) => {\n * const [snapshot, cmds] = snap.confirm(s.snapshot, msg);\n * return [{ ...s, snapshot }, cmds];\n * },\n *\n * // RECOVERY — the half that makes a checkpoint a restart point, not just a\n * // backup. A `boot` Msg the host dispatches once after `run(...)` asks the\n * // store for the durable checkpoint; the answer folds the recovered payload\n * // back into the run slice:\n * boot: (s) => snap.requestLoad(s), // emits snapshot_load Cmd\n * snapshot_loaded: (s, msg) => {\n * if (msg.payload === null) return [s, []]; // no checkpoint → keep fresh init\n * const [snapshot, cmds] = snap.boot(s.snapshot);\n * return [{ ...s, run: msg.payload, snapshot }, cmds];\n * },\n * snapshot_load_failed: (s) => [s, []], // errors are data — decide here\n *\n * // splice the I/O — both effect cells (write + load):\n * interpret: { ...snap.handlers({ store: r2Adapter }) },\n */\n\nimport { Cmd, type Interpret, tryInterpret } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config — the knob.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot knob.\n *\n * `every` is the cadence: a checkpoint is written once `every` units of\n * progress have accumulated since the last one (see `record`). It is the\n * `snapshotEvery` field `monitored-run` threads through to this module.\n *\n * `key` is the store key the checkpoint is written under. A single machine\n * keeps a single rolling checkpoint, so the key is fixed at config time rather\n * than passed per write — the latest checkpoint overwrites the previous one\n * (R2/KV are last-write-wins on a key). Defaults to `\"@@snapshot\"`, mirroring\n * `doStore`'s `@@state` default-key convention.\n */\nexport interface SnapshotConfig {\n /**\n * Write a checkpoint once this many progress units have accumulated since the\n * last write. `every: 1` checkpoints on every `record`; a value `<= 0` is\n * treated as `1` (every progress unit triggers a write) — a non-positive\n * cadence has no other sensible meaning, and clamping keeps `record` from\n * never checkpointing on a misconfigured `0`.\n */\n readonly every: number;\n /** Store key the rolling checkpoint is written under. Defaults to `\"@@snapshot\"`. */\n readonly key?: string;\n}\n\nconst DEFAULT_SNAPSHOT_KEY = \"@@snapshot\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Slice — the Model field this knob owns.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot bookkeeping slice. Plain data — JSON-serializable so it rides\n * inside the consumer's `Store<S>` blob and survives DO eviction / reload (the\n * \"durable\" non-negotiable). It carries NO payload: the thing being\n * checkpointed lives elsewhere in the machine's state; this slice only tracks\n * *when* to checkpoint and *what has been acknowledged durable*.\n *\n * - `sinceLast` — progress units accumulated since the last checkpoint\n * DECISION (reset to 0 the moment `record` decides to write, not when the\n * write lands). The cadence counter.\n * - `seq` — monotonic checkpoint sequence. Bumped each time a write is\n * emitted, so the write Cmd and its later acknowledgement can be matched\n * even if several are in flight. The \"version\" of the latest emitted\n * checkpoint.\n * - `lastSavedSeq` — the highest `seq` whose write the host has confirmed\n * durable (via `confirm`). `null` until the first confirmed write. The gap\n * `seq - lastSavedSeq` is the \"writes emitted but not yet acknowledged\"\n * depth — observable for backpressure / health.\n * - `lastSavedAt` — the `at` of the most recent confirmed write (epoch ms),\n * `null` until the first. Carried for observability, never compared inside\n * a verb (time is injected, never read here).\n */\nexport interface SnapshotState {\n readonly sinceLast: number;\n readonly seq: number;\n readonly lastSavedSeq: number | null;\n readonly lastSavedAt: number | null;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Store port — abstract R2/KV-shaped checkpoint sink.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The abstract checkpoint sink — the intersection of R2's and KV's surface,\n * narrowed to exactly the two methods a rolling checkpoint needs. Deliberately\n * NOT the substrate's `Store<S>` (`{ load, save, migrate }`): that is the\n * runtime's whole-state persistence boundary keyed at a fixed cell, whereas a\n * checkpoint store is keyed, value-typed, and host-supplied.\n *\n * - `put(key, value)` — write the checkpoint. Last-write-wins on `key`.\n * - `get(key)` — read the latest checkpoint back, or `null` if none. Used by\n * a recovery path to seed `init`'s `loaded` from the durable checkpoint\n * when the runtime's own `Store<S>` blob is gone.\n *\n * No `@cloudflare/workers-types` import — an R2 bucket, a KV namespace, a DO\n * sub-key, or an in-memory `Map` all satisfy this shape with a 3-line adapter\n * at the host.\n */\nexport interface SnapshotStore<V> {\n get(key: string): Promise<V | null>;\n put(key: string, value: V): Promise<void>;\n}\n\n/**\n * The ports `handlers` binds. Just the store today — named so a future port\n * (a metrics sink, a clock) can grow without breaking the `handlers(ports)`\n * call shape.\n */\nexport interface SnapshotPorts<V> {\n readonly store: SnapshotStore<V>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmd + Msg — the write effect and its durable acknowledgement.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The checkpoint-write Cmd a `record` / `force` decision emits. Plain data\n * (invariant 1 — Cmds are data, not closures): it carries the `payload` to\n * write, the `key` to write it under, the `seq` it represents, and the `at` it\n * was decided. The handler performs the `put` and routes the result back to a\n * `confirm` / fail Msg.\n *\n * `payload` is `V` — the consumer's serializable checkpoint value (the run\n * state, the cursor, whatever the machine wants restartable). The verb takes it\n * as an argument so the snapshot slice never has to hold a copy of the\n * machine's domain state.\n */\nexport type SnapshotWriteCmd<V> = Cmd<\"snapshot_write\"> & {\n readonly key: string;\n readonly seq: number;\n readonly at: number;\n readonly payload: V;\n};\n\n/**\n * The Msg the write handler dispatches on a SUCCESSFUL `put`. Fold it back\n * through `confirm(state, msg)` to advance `lastSavedSeq` / `lastSavedAt`. The\n * `seq` matches the emitting Cmd so a late acknowledgement of an older write\n * can never regress a newer confirmed one.\n */\nexport type SnapshotSavedMsg = {\n readonly type: \"snapshot_saved\";\n readonly seq: number;\n readonly at: number;\n};\n\n/**\n * The Msg the write handler dispatches on a FAILED `put`. Errors are data\n * (never swallowed): the consumer's reducer decides whether a failed checkpoint\n * is fatal, retryable (compose `@demlik/tea/retry-backoff`), or merely logged.\n * `seq` identifies which checkpoint failed; `error` is carried, never\n * interpreted here.\n */\nexport type SnapshotFailedMsg = {\n readonly type: \"snapshot_failed\";\n readonly seq: number;\n readonly error: unknown;\n};\n\n/**\n * The checkpoint-READ Cmd a `requestLoad` decision emits — the recovery half of\n * the module. Plain data (invariant 1): it carries only the `key` to read. The\n * handler performs `store.get(key)` and routes the result back to a\n * `snapshot_loaded` (Ok, payload-or-null) / `snapshot_load_failed` (Err) Msg.\n *\n * Distinct from `snapshot_write`: a write carries a payload OUT to the store; a\n * load asks the store to hand a payload BACK. The consumer dispatches the\n * triggering Msg once on resume (a `boot` Msg, never `init`'s rehydrate branch —\n * invariant 2), folds the recovered payload into its run slice, then continues.\n */\nexport type SnapshotLoadCmd = Cmd<\"snapshot_load\"> & {\n readonly key: string;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` RESOLVES. `payload` is\n * the recovered checkpoint value, or `null` when the store has no checkpoint\n * under `key` (fresh boot — never checkpointed, or a wiped store). The consumer\n * folds this into `init`'s loaded branch: `payload === null` → keep the fresh\n * `init` state; non-null → seed the run slice from the durable checkpoint and\n * `boot` the snapshot slice's cadence counter.\n *\n * A `null` payload is NOT an error — \"no checkpoint yet\" is a normal first-run\n * state, distinct from \"the read failed\" (`snapshot_load_failed`).\n */\nexport type SnapshotLoadedMsg<V> = {\n readonly type: \"snapshot_loaded\";\n readonly payload: V | null;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` REJECTS. Errors are data\n * (never swallowed — the field-guide rule): a failed recovery read is a visible\n * signal the consumer's reducer decides on (fatal-abort, retry via\n * `@demlik/tea/retry-backoff`, or boot-fresh-and-log). `error` is carried, never\n * interpreted here. Kept separate from `snapshot_loaded`'s `null` so \"the store\n * has no checkpoint\" can never masquerade as \"the read failed\".\n */\nexport type SnapshotLoadFailedMsg = {\n readonly type: \"snapshot_load_failed\";\n readonly error: unknown;\n};\n\n/** Construct a `snapshot_saved` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotSaved(seq: number, at: number): SnapshotSavedMsg {\n return { type: \"snapshot_saved\", seq, at };\n}\n\n/** Construct a `snapshot_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotFailed(seq: number, error: unknown): SnapshotFailedMsg {\n return { type: \"snapshot_failed\", seq, error };\n}\n\n/** Construct a `snapshot_loaded` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoaded<V>(payload: V | null): SnapshotLoadedMsg<V> {\n return { type: \"snapshot_loaded\", payload };\n}\n\n/** Construct a `snapshot_load_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoadFailed(error: unknown): SnapshotLoadFailedMsg {\n return { type: \"snapshot_load_failed\", error };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The combinator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The bundle `createSnapshot` returns — the uniform knob contract. `subs` is\n * omitted: a checkpoint is driven by progress (a `record` call from the\n * consumer's reducer), not by a timer, so this module has no subscriptions.\n */\nexport interface SnapshotKnob<V> {\n /** The initial slice — zero progress, no checkpoint emitted or confirmed yet. */\n init(): SnapshotState;\n /**\n * Account one unit of progress. PURE. Increments `sinceLast`; once the\n * accumulated count reaches the configured cadence, DECIDES to checkpoint:\n * resets `sinceLast` to 0, bumps `seq`, and emits a single `snapshot_write`\n * Cmd carrying `payload` + `key` + the new `seq` + `at`. Below the cadence it\n * returns the bumped slice and NO Cmd.\n *\n * `payload` is the checkpoint value to persist (the consumer's domain state);\n * `at` is the caller-stamped time the progress occurred (epoch ms) — injected,\n * never read from the clock here.\n */\n record(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Force a checkpoint regardless of the cadence counter. PURE. Always bumps\n * `seq`, resets `sinceLast`, and emits the write Cmd. Use on a phase boundary\n * the cadence would otherwise miss — e.g. \"the run just finished, checkpoint\n * the terminal state now\" — so the last durable checkpoint is the final state,\n * not whatever the modulo happened to land on.\n */\n force(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Record a confirmed durable write. PURE. Advances `lastSavedSeq` /\n * `lastSavedAt` to the acknowledged `seq` / `at`, but only forward: an\n * out-of-order acknowledgement of an OLDER `seq` (a slow write landing after a\n * newer one) is ignored, so the confirmed watermark is monotonic. Fold the\n * `snapshot_saved` Msg here.\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — an ack emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_saved` cell without the caller\n * re-wrapping a bare State.\n */\n confirm(\n state: SnapshotState,\n msg: SnapshotSavedMsg,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Resume after a reload. PURE. Resets `sinceLast` to 0 because any\n * un-checkpointed progress accumulated before the crash is moot — recovery\n * resumes from the last DURABLE checkpoint (`get` from the store), so the\n * cadence counter starts fresh from there. `seq` / `lastSavedSeq` /\n * `lastSavedAt` are preserved (they describe what is durable, which a reload\n * does not change). Call from a `boot` Msg the host dispatches once after\n * `run(...)`, never from `init`'s rehydrate branch (invariant 2).\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — a resume emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_loaded` cell without the caller\n * re-wrapping a bare State.\n */\n boot(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Ask the store for the durable checkpoint. PURE. Emits a single\n * `snapshot_load` Cmd carrying the configured `key`; returns the slice\n * UNCHANGED (a read decides nothing about the cadence bookkeeping — only the\n * later `snapshot_loaded` fold, via `boot`, touches the slice). This is the\n * entry point of the recovery loop: the consumer dispatches a `boot` Msg once\n * after `run(...)`, folds `requestLoad` here, and the emitted Cmd's handler\n * reads `store.get(key)` and dispatches `snapshot_loaded` / `snapshot_load_failed`.\n *\n * Returns `readonly [SnapshotState, readonly Cmd[]]` (the reducer-cell shape)\n * so it drops straight into a `boot: (s) => snap.requestLoad(s)` cell.\n */\n requestLoad(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * The `interpret` map for this knob's two effect Cmds:\n *\n * - `snapshot_write` — binds the store and performs the `put`, routing\n * Ok → `snapshot_saved` / Err → `snapshot_failed`.\n * - `snapshot_load` — binds the store and performs the `get`, routing\n * Ok → `snapshot_loaded` (payload-or-null) / Err → `snapshot_load_failed`.\n *\n * Railway throughout — neither effect throws into the runtime; the outcome is\n * always a Msg. Spread into your machine's `interpret`.\n */\n handlers(\n ports: SnapshotPorts<V>,\n ): Interpret<\n | SnapshotSavedMsg\n | SnapshotFailedMsg\n | SnapshotLoadedMsg<V>\n | SnapshotLoadFailedMsg,\n SnapshotWriteCmd<V> | SnapshotLoadCmd,\n unknown\n >;\n}\n\n/**\n * Create a snapshot knob from a `SnapshotConfig`. The returned bundle is spliced\n * into a consumer machine: `init()` seeds the slice, `record` / `force` are\n * folded in reducer cells, `confirm` / `boot` handle the acknowledgement +\n * resume Msgs, and `handlers({ store })` supplies the write `interpret` cell.\n */\nexport function createSnapshot<V>(config: SnapshotConfig): SnapshotKnob<V> {\n // Clamp the cadence to >= 1 at construction so `record`'s threshold check is\n // branchless and a misconfigured `0` / negative `every` checkpoints on every\n // progress unit instead of never.\n const every = config.every >= 1 ? config.every : 1;\n const key = config.key ?? DEFAULT_SNAPSHOT_KEY;\n\n /**\n * Emit the write Cmd for a checkpoint decision. Shared by `record`'s\n * cadence-hit branch and `force`. Bumps `seq`, resets `sinceLast`, returns the\n * new slice + the single `snapshot_write` Cmd. The input slice is never\n * mutated (invariant 1).\n */\n function emitWrite(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]] {\n const seq = state.seq + 1;\n const next: SnapshotState = { ...state, sinceLast: 0, seq };\n const cmd: SnapshotWriteCmd<V> = {\n type: \"snapshot_write\",\n key,\n seq,\n at,\n payload,\n };\n return [next, [cmd]];\n }\n\n return {\n init(): SnapshotState {\n return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };\n },\n\n record(state, payload, at) {\n const sinceLast = state.sinceLast + 1;\n // Below the cadence: accumulate, emit nothing.\n if (sinceLast < every) {\n return [{ ...state, sinceLast }, Cmd.none];\n }\n // Cadence reached: checkpoint. `emitWrite` resets the counter + bumps seq.\n return emitWrite(state, payload, at);\n },\n\n force(state, payload, at) {\n return emitWrite(state, payload, at);\n },\n\n confirm(state, msg) {\n // Forward-only watermark: ignore a stale ack of an older seq so a slow\n // write landing after a newer one can't regress `lastSavedSeq`.\n if (state.lastSavedSeq !== null && msg.seq <= state.lastSavedSeq) {\n return [state, Cmd.none];\n }\n return [\n { ...state, lastSavedSeq: msg.seq, lastSavedAt: msg.at },\n Cmd.none,\n ];\n },\n\n boot(state) {\n return [{ ...state, sinceLast: 0 }, Cmd.none];\n },\n\n requestLoad(state) {\n // A read decides nothing about the cadence bookkeeping — return the slice\n // unchanged and emit the single read Cmd. The recovered payload folds back\n // through `snapshot_loaded` (the consumer's reducer), not here.\n const cmd: SnapshotLoadCmd = { type: \"snapshot_load\", key };\n return [state, [cmd]];\n },\n\n handlers(ports) {\n const { store } = ports;\n return {\n // The write effect. Performs the `put`; on success the saved `seq`/`at`\n // ride the `snapshot_saved` Msg back into `confirm`; on failure the\n // error rides `snapshot_failed`. Railway via the core's `tryInterpret`\n // (matching every other module's handler) — the handler always resolves\n // with a Msg, never rejects into the runtime.\n snapshot_write: tryInterpret<\n SnapshotWriteCmd<V>,\n void,\n SnapshotSavedMsg | SnapshotFailedMsg,\n unknown\n >(\n (cmd) => store.put(cmd.key, cmd.payload),\n (_value, cmd) => snapshotSaved(cmd.seq, cmd.at),\n (error, cmd) => snapshotFailed(cmd.seq, error),\n ),\n // The read effect — the recovery half. Performs the `get`; on success\n // the recovered payload (or `null` when no checkpoint exists) rides the\n // `snapshot_loaded` Msg the consumer folds into `init`'s loaded branch;\n // on failure the error rides `snapshot_load_failed`. Same Railway\n // discipline via `tryInterpret` — the read never rejects into the runtime.\n snapshot_load: tryInterpret<\n SnapshotLoadCmd,\n V | null,\n SnapshotLoadedMsg<V> | SnapshotLoadFailedMsg,\n unknown\n >(\n (cmd) => store.get(cmd.key),\n (payload) => snapshotLoaded(payload),\n (error) => snapshotLoadFailed(error),\n ),\n };\n },\n };\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { initPaginator, start, recordPage, drain, resume, isDone } from './chunk-SINXE47Y.js';
2
- import { createResilientCall } from './chunk-W2S4HK7J.js';
2
+ import { createResilientCall } from './chunk-EC6FO42Q.js';
3
3
  import { MsgType } from './chunk-JELSZN25.js';
4
4
 
5
5
  // src/paginated-walk/index.ts
@@ -126,5 +126,5 @@ function liftWalk(state, [slice, cmds]) {
126
126
  }
127
127
 
128
128
  export { PAGE_KEY, createPaginatedWalk, liftWalk };
129
- //# sourceMappingURL=chunk-7A74NSG4.js.map
130
- //# sourceMappingURL=chunk-7A74NSG4.js.map
129
+ //# sourceMappingURL=chunk-XOAP3LOT.js.map
130
+ //# sourceMappingURL=chunk-XOAP3LOT.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/paginated-walk/index.ts"],"names":["start","drain","resume"],"mappings":"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","file":"chunk-7A74NSG4.js","sourcesContent":["/**\n * @demlik/tea/paginated-walk — traverse a paginated API / sitemap end to end\n * WITHOUT a fake clock, without 429s, and resumable across a Durable-Object\n * eviction. The L2 composition that bolts `../resilient-call`'s resilience onto\n * `../paginator`'s cursor walk: each page is fetched as ONE keyed resilient\n * call (retry + backoff + rate-limit gate), and the cursor advances only once a\n * page settles `ok`.\n *\n * ## The recipe (the two siblings it composes)\n *\n * - **`../paginator`** owns the walk loop: the opaque `Cursor`, the `seen` /\n * `pages` counters, and the `idle → fetching → … → done` phase machine.\n * `paginated-walk` never re-implements cursor advancement — it folds each\n * settled page through `recordPage`, letting the paginator decide\n * `done` / `fetching(next)` / `paused`. Backpressure is a TWO-way valve:\n * `recordPage` parks the walk in `paused` once `seen >= highWaterMark`, and\n * the `drain` / `resume` verbs (delegating to the paginator's own\n * `drain` / `resume`) lower the gauge and re-open the parked fetch — without\n * them `highWaterMark` would be a one-way valve that strands a paused walk.\n * - **`../resilient-call`** owns the page fetch: the single `resilient_run`\n * effect per page is gated through that knob's cache → circuit → rate-limit\n * → retry pipeline. A transient page failure backs off and re-issues the\n * SAME cursor (idempotent retry); the walk does not advance until the page\n * genuinely settles.\n *\n * Those are the only two siblings this module calls into. The resilience knobs\n * (`retry` / `circuit` / `rateLimit` / `deadline`) are NOT separate bricks\n * `paginated-walk` composes — they belong to `resilient-call`, which owns the\n * rate-limit gate internally. This module merely passes them through to\n * `createResilientCall` and re-uses two config TYPES at the boundary:\n * `RateLimitConfig` (re-exported by `resilient-call`) and `RetryPolicy` (from\n * `../retry-backoff`). Pass a `rateLimit` and `resilient-call`'s token bucket\n * throttles page fetches (\"don't 429 the upstream\"); omit it and the walk runs\n * as fast as the upstream answers.\n *\n * Because the page-fetch resilience IS a `resilient-call` slice, this knob\n * **inherits that knob's contract verbatim**: the slice is a flat record, every\n * verb returns `readonly [State, Cmd[]]`, time is always an `at` parameter, and\n * the jitter RNG is injected once at `createPaginatedWalk(config, rng)` — never\n * read inside a verb (per the spec, `paginated-walk` *extends* `resilient-call`).\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is plain data: the paginator phase + counters, and\n * the resilient-call brick states (all JSON-serializable). The cursor lives\n * in the Model, not a closure, so a walk survives eviction mid-traversal and\n * resumes from the exact page it was fetching.\n * - **Replayable** — every transition is a verb returning new state + Cmds.\n * `nextCursor` / `onPage` are pure config functions applied at the verb\n * boundary; the Cmds they yield are plain data. `replay` reconstructs the\n * whole walk exactly.\n *\n * ## Where the clock / RNG live\n *\n * Inside the verbs: nowhere. `at` arrives on `start` / `pageOk` / `pageErr` /\n * `onTimer` and is threaded straight into the underlying resilient-call gate (so\n * a rate-limited or retried page measures backoff from that `at`). The jitter\n * RNG is injected at construction. The only clock read is `Date.now()` inside\n * the resilient-call `handlers` port (the effect boundary), inherited unchanged.\n *\n * ## The fixed fetch key\n *\n * A walk fetches one page at a time, so the resilient-call layer tracks exactly\n * one logical call under the constant key `PAGE_KEY`. The cursor being fetched\n * is the resilient call's `input`, so a post-eviction resume re-issues the same\n * page request. (`resilient-call` is keyed to support N concurrent calls; a walk\n * deliberately uses a single key — the paginator is the sequencer.)\n *\n * ## Typical wiring\n *\n * const walk = createPaginatedWalk<PageToken, ApiPage>({\n * firstPage: null, // the API's \"page one\" sentinel\n * nextCursor: (page) => page.nextToken ?? null,\n * onPage: (page) => [{ type: \"index_rows\", rows: page.items }],\n * rateLimit: { capacity: 10, refillPerSec: 5 },\n * retry: defaultRetryPolicy,\n * });\n *\n * // in the machine:\n * init: () => [{ walk: walk.init() }, []],\n * update: {\n * start: (s, m) => lift(s, walk.start(s.walk, m.at)),\n * page_ok: (s, m) => lift(s, walk.pageOk(s.walk, m.result, m.at)),\n * page_err: (s, m) => lift(s, walk.pageErr(s.walk, m.error, m.at)),\n * retry_due:(s, m) => lift(s, walk.onTimer(s.walk, m)),\n * drained: (s, m) => lift(s, walk.drain(s.walk, m.n)),\n * resume: (s, m) => lift(s, walk.resume(s.walk, m.at)),\n * },\n * subscriptions: (s) => walk.subs(s.walk),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: walk.handlers({ run: (cursor) => api.fetchPage(cursor) }),\n */\n\nimport type { Cmd } from \"../index\";\nimport {\n drain as drainWalk,\n initPaginator,\n isDone,\n type PaginatorPolicy,\n type PaginatorState,\n recordPage,\n resume as resumeWalk,\n start as startWalk,\n} from \"../paginator\";\nimport { MsgType } from \"../protocol\";\nimport {\n type CircuitConfig,\n createResilientCall,\n type DeadlineConfig,\n type DeadlineSub,\n deadlineSub,\n type FailMsg,\n type RateLimitConfig,\n type ResilientPorts,\n type ResilientState,\n type ResilientTimerMsg,\n type RunCmd,\n type SucceedMsg,\n subscribeDeadline,\n} from \"../resilient-call\";\nimport type { RetryPolicy } from \"../retry-backoff\";\n\n// ===========================================================================\n// Config — the knob. `firstPage` / `nextCursor` / `onPage` describe the walk;\n// the resilience bricks are inherited from resilient-call (every one optional —\n// omit a brick, omit its gate).\n// ===========================================================================\n\n/**\n * The paginated-walk knob. Generic in `Cursor` (the opaque page handle the\n * upstream API hands back — offset, page token, \"next\" URL, …) and `Page` (the\n * shape your fetch port resolves with).\n *\n * `nextCursor` and `onPage` are PURE config functions applied at the verb\n * boundary, never stored in the slice or in a Cmd (invariant 3 — Cmds are\n * plain data, not closures). `nextCursor(page)` extracts the API's next-page\n * handle (or `null` at the end); `onPage(page)` yields the Cmds the consumer\n * wants emitted for each successfully fetched page (index rows, write to R2,\n * dispatch a downstream Msg — whatever; an empty array is fine).\n *\n * The resilience bricks (`retry` / `circuit` / `rateLimit` / `deadline`) are\n * the SAME optional knobs `resilient-call` exposes — paginated-walk extends it.\n * `rateLimit` is the brick that keeps the walk from 429-ing the upstream.\n */\nexport interface PaginatedWalkConfig<Cursor, Page, EmittedCmd extends Cmd> {\n /**\n * The cursor the walk fetches first. For an offset API this is typically `0`;\n * for a token API, whatever sentinel the API treats as \"page one\" (often\n * `null` or `\"\"`). Threaded into `paginator`'s `firstCursor`.\n */\n readonly firstPage: Cursor;\n /**\n * Extract the next cursor from a just-fetched page. Return a `Cursor` to keep\n * walking, or `null` when the API signalled the end (the walk finishes). PURE\n * + consumer-specific (only the consumer knows the API's pagination shape).\n */\n readonly nextCursor: (page: Page) => Cursor | null;\n /**\n * The Cmds to emit for each successfully fetched page — the consumer's work\n * per page (index, persist, fan out, …). Returns plain-data Cmds; `[]` is a\n * valid \"I process the page synchronously elsewhere\" answer. PURE.\n */\n readonly onPage: (page: Page) => readonly EmittedCmd[];\n /**\n * How many items a page contributes to the paginator's backpressure gauge\n * (`seen`). Defaults to `1` per page (count pages, not items) when omitted; a\n * consumer that wants per-item backpressure returns `page.items.length`. PURE.\n */\n readonly pageSize?: (page: Page) => number;\n /**\n * Backpressure threshold handed to the paginator. `seen >= highWaterMark`\n * parks the walk in `paused` instead of fetching the next page. Non-positive\n * (the default `0`) disables backpressure — the walk runs flat out until the\n * API exhausts. See `../paginator`'s `highWaterMark` docs.\n */\n readonly highWaterMark?: number;\n\n // --- resilience bricks, inherited from resilient-call (all optional) ---\n /** Exponential-backoff retry policy for a transient page-fetch failure. */\n readonly retry?: RetryPolicy;\n /** Circuit breaker around the page fetch (one upstream → one breaker). */\n readonly circuit?: CircuitConfig;\n /** Token-bucket rate limit — the \"don't 429 the upstream\" knob. */\n readonly rateLimit?: RateLimitConfig;\n /** Overall wall-clock deadline per individual page fetch. */\n readonly deadline?: DeadlineConfig;\n}\n\n// ===========================================================================\n// Slice — the Model field this knob owns. The paginator walk state + the\n// resilient-call slice for the page fetch. Both are flat plain data.\n// ===========================================================================\n\n/**\n * The slice. `walk` is the cursor/phase/counters from `../paginator`;\n * `resilience` is the `../resilient-call` slice that gates the page fetch.\n * Every field is JSON-serializable, so the whole walk survives a reload.\n *\n * The two move in lockstep: `walk` decides WHICH cursor to fetch next, while\n * `resilience` decides HOW that fetch is attempted (gated, retried, backed\n * off). `pageOk` advances `walk`; `pageErr` defers to `resilience` and leaves\n * `walk` parked on the same cursor until the page truly settles.\n */\nexport interface PaginatedWalkState<Cursor, Page> {\n readonly walk: PaginatorState<Cursor>;\n readonly resilience: ResilientState<Cursor, Page>;\n}\n\n// ===========================================================================\n// Cmds + Msgs the knob speaks.\n// ===========================================================================\n\n/**\n * The page-fetch effect this knob emits: the inherited `resilient_run` Cmd from\n * resilient-call, whose `input` is the `Cursor` to fetch and whose `key` is the\n * fixed `PAGE_KEY`. The consumer's `handlers(ports)` interprets it.\n */\nexport type FetchPageCmd<Cursor> = RunCmd<Cursor>;\n\n/** Page-settled Msgs the `handlers` port dispatches back (inherited verbatim). */\nexport type PageOkMsg<Page> = SucceedMsg<Page>;\nexport type PageErrMsg = FailMsg;\n\n/** The retry / deadline timer Msg — inherited from resilient-call. */\nexport type PaginatedWalkTimerMsg = ResilientTimerMsg;\n\n/**\n * Ports the consumer supplies to `handlers`. `run(cursor)` fetches ONE page for\n * the given cursor; throwing routes to `pageErr` (and thus the resilience\n * backoff). Same shape as resilient-call's port, specialized to the page fetch.\n */\nexport interface PaginatedWalkPorts<Cursor, Page>\n extends ResilientPorts<Cursor, Page> {}\n\n// ===========================================================================\n// The fixed key. A walk fetches one page at a time → exactly one logical\n// resilient call. The paginator is the sequencer; resilient-call's keying is\n// pinned to this single key.\n// ===========================================================================\n\n/** The single resilient-call key every page fetch runs under. */\nexport const PAGE_KEY = \"page\";\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a paginated-walk knob from `config`. `rng` is injected for the\n * underlying retry jitter — pass a fixed `() => 0` in tests to pin backoff;\n * defaults to `Math.random` (read at the verb boundary inside resilient-call,\n * never in a `paginated-walk` verb). Inherited straight from resilient-call.\n *\n * Returns the uniform L2 knob contract: `init()`, the verbs `start` / `pageOk`\n * / `pageErr` / `onTimer`, `subs(state)`, and `handlers(ports)`. `Cursor` is\n * the page handle, `Page` the fetch result, `EmittedCmd` the consumer's\n * per-page Cmds plus the inherited `FetchPageCmd`.\n */\nexport function createPaginatedWalk<Cursor, Page, EmittedCmd extends Cmd = Cmd>(\n config: PaginatedWalkConfig<Cursor, Page, EmittedCmd>,\n rng: () => number = Math.random,\n) {\n // The page fetch is a resilient call keyed by Cursor input → Page result.\n // We feed it only the resilience bricks; the walk-loop knobs are ours.\n const rc = createResilientCall<Cursor, Page>(\n {\n retry: config.retry,\n circuit: config.circuit,\n rateLimit: config.rateLimit,\n deadline: config.deadline,\n },\n rng,\n );\n\n // The paginator policy: our firstPage is the paginator's firstCursor; the\n // high-water mark gates backpressure (0 / omitted → unbounded).\n const paginatorPolicy: PaginatorPolicy<Cursor> = {\n firstCursor: config.firstPage,\n highWaterMark: config.highWaterMark ?? 0,\n };\n\n // The combined Cmd type the verbs can emit: the inherited page-fetch effect\n // plus whatever `onPage` yields.\n type OutCmd = FetchPageCmd<Cursor> | EmittedCmd;\n\n /** Replace the resilience slice, leaving `walk` untouched. */\n function withResilience(\n s: PaginatedWalkState<Cursor, Page>,\n resilience: ResilientState<Cursor, Page>,\n ): PaginatedWalkState<Cursor, Page> {\n return { ...s, resilience };\n }\n\n /** The starting slice: an idle paginator + a fresh resilient-call slice. */\n function init(): PaginatedWalkState<Cursor, Page> {\n return {\n walk: initPaginator<Cursor>(),\n resilience: rc.init(),\n };\n }\n\n /**\n * Issue a resilient page fetch for `cursor` at `at`. The cursor is the\n * resilient call's `input` (so a retry / resume re-fetches the same page) and\n * `PAGE_KEY` is the fixed key. Returns the advanced resilience slice + the\n * `resilient_run` Cmd (or none, if the gate cached / fast-failed / backed off).\n * PURE — `at` is the only clock. Shared by `start` and `pageOk`'s advance.\n */\n function fetch(\n s: PaginatedWalkState<Cursor, Page>,\n cursor: Cursor,\n at: number,\n ): readonly [\n PaginatedWalkState<Cursor, Page>,\n readonly FetchPageCmd<Cursor>[],\n ] {\n const [resilience, cmds] = rc.attempt(s.resilience, PAGE_KEY, cursor, at);\n return [withResilience(s, resilience), cmds];\n }\n\n /**\n * Issue the page fetch for a freshly-armed paginator transition, else a pure\n * no-op. `walk` is the result of a paginator verb (`startWalk` / `resumeWalk`)\n * applied to `s.walk`; this states the arm-and-fetch contract ONCE for the two\n * verbs that share it (`start`, `resume`).\n *\n * A paginator verb is a pure no-op on any phase it does not act on — it returns\n * the SAME state object. Reference identity (not phase) is the honest \"did the\n * verb arm a fresh fetch?\" check: a walk already `fetching` would also satisfy\n * `phase === \"fetching\"` yet must NOT re-issue a fetch (page-one skip /\n * double-fetch). So we proceed ONLY when the verb produced a fresh state. The\n * `phase === \"fetching\"` narrow that follows is therefore always true here\n * (both verbs only ever produce a `fetching` arm), and also narrows `.cursor`\n * for the type checker. PURE — `at` threads into the fetch gate.\n */\n function armAndFetch(\n s: PaginatedWalkState<Cursor, Page>,\n walk: PaginatorState<Cursor>,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n if (walk === s.walk || walk.phase !== \"fetching\") {\n return [s, []];\n }\n return fetch({ ...s, walk }, walk.cursor, at);\n }\n\n // === Verb: start =========================================================\n\n /**\n * Begin the walk: arm the paginator's first cursor and issue the first page\n * fetch (gated through resilience). On any non-`idle` paginator phase this is\n * a no-op — re-starting a walk in flight would skip back to page one and\n * double-count, so a stray `start` after launch is absorbed (the paginator's\n * own `start` guard). PURE — `at` threads into the fetch gate.\n */\n function start(\n s: PaginatedWalkState<Cursor, Page>,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // `startWalk` is a pure no-op on any non-`idle` phase (same reference); a\n // fresh `fetching(firstCursor)` arm → issue the first page fetch.\n return armAndFetch(s, startWalk(s.walk, paginatorPolicy), at);\n }\n\n // === Verb: pageOk ========================================================\n\n /**\n * Record a successfully fetched `page` and decide what comes next. PURE —\n * `at` is the cache / next-fetch clock.\n *\n * 1. Settle the resilient call OK (closes the breaker, fills the cache,\n * resets the page-fetch retry counter) — inherited from resilient-call.\n * 2. Fold the page through the paginator: add `pageSize(page)` to `seen`,\n * bump `pages`, and branch on `nextCursor(page)`:\n * - `null` → the API is exhausted → paginator goes `done`.\n * - a cursor → advance, then either fetch it (backpressure room) or\n * park in `paused` (high-water mark reached).\n * 3. Emit the consumer's `onPage(page)` Cmds either way, PLUS the next\n * page-fetch effect when the paginator stays `fetching`.\n *\n * A `pageOk` that arrives when the paginator is not `fetching` (a late\n * duplicate, a double-dispatch) is absorbed by `recordPage` — the cursor does\n * not advance and `seen` is not double-counted; only the resilient settle +\n * `onPage` Cmds happen. The walk is a coordination aid, not a correctness\n * gate.\n */\n function pageOk(\n s: PaginatedWalkState<Cursor, Page>,\n page: Page,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // 1) Settle the underlying resilient call as a success.\n const [resilience] = rc.succeed(s.resilience, PAGE_KEY, {\n type: MsgType.ResilientOk,\n key: PAGE_KEY,\n result: page,\n at,\n });\n\n // 2) Advance the paginator with the page's contribution + the next cursor.\n const count = config.pageSize ? config.pageSize(page) : 1;\n const next = config.nextCursor(page);\n const walk = recordPage(s.walk, count, next, paginatorPolicy);\n\n const advanced: PaginatedWalkState<Cursor, Page> = { walk, resilience };\n\n // 3) The consumer's per-page work, always emitted on a successful page.\n const pageCmds: readonly EmittedCmd[] = config.onPage(page);\n\n // If the paginator stayed `fetching`, issue the next page fetch (gated).\n if (walk.phase === \"fetching\") {\n const [withFetch, fetchCmds] = fetch(advanced, walk.cursor, at);\n return [withFetch, [...pageCmds, ...fetchCmds]];\n }\n\n // `done` (exhausted) or `paused` (backpressure) → no further fetch now.\n return [advanced, pageCmds];\n }\n\n // === Verb: pageErr =======================================================\n\n /**\n * Record a failed page fetch and back off via resilience. PURE — `at` stamps\n * the breaker trip + the retry-delay base.\n *\n * Defers to the resilient-call `fail` verb: it trips the breaker and either\n * schedules a retry (paginator stays parked on the SAME cursor — no advance)\n * or settles the resilient call `failed` once retries are exhausted. The\n * paginator is deliberately untouched: a failed fetch must NOT advance the\n * cursor or skip a page. When the retry timer later fires, `onTimer` re-issues\n * the same page fetch.\n *\n * GUARDED by the paginator phase, exactly as `pageOk` is guarded by\n * `recordPage`'s `fetching`-only check. A `pageErr` only makes sense while a\n * page fetch is outstanding (`walk.phase === \"fetching\"`). A stray `pageErr`\n * that arrives after the walk has settled — `done` (exhausted), `paused`\n * (backpressure), or `idle` (not started) — has NO outstanding fetch to fail.\n * Without this guard it would still call `rc.fail`, which trips the SHARED\n * circuit breaker (dinging a healthy upstream) and overwrites the settled\n * `PAGE_KEY` resilient slot with `failed` (clobbering the succeeded result a\n * resume would otherwise observe). So a stray `pageErr` is absorbed as a pure\n * no-op — identity preserved — matching `pageOk`'s \"the walk is a coordination\n * aid, not a correctness gate\" stance.\n */\n function pageErr(\n s: PaginatedWalkState<Cursor, Page>,\n error: unknown,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // Only a `fetching` paginator has an outstanding page fetch to fail. On any\n // other phase the fail would corrupt shared resilience state for a fetch\n // that is not in flight — absorb it as a pure no-op.\n if (s.walk.phase !== \"fetching\") {\n return [s, []];\n }\n const [resilience, cmds] = rc.fail(s.resilience, PAGE_KEY, {\n type: MsgType.ResilientErr,\n key: PAGE_KEY,\n error,\n at,\n });\n return [withResilience(s, resilience), cmds];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A retry / deadline timer fired. Defers to resilient-call's `onTimer`: a\n * retry timer re-runs the gate for the page-fetch's remembered cursor (re-\n * issuing the SAME page); a deadline timer settles the resilient call failed.\n * The paginator stays parked on its cursor throughout — only `pageOk` advances\n * it. A stale fire is a no-op (inherited). PURE.\n *\n * GUARDED by the paginator phase, for the same reason as `pageErr`. A timer is\n * only meaningful while a page fetch is outstanding (`walk.phase ===\n * \"fetching\"`). A stray timer that fires after the walk has settled (`done` /\n * `paused` / `idle`) would otherwise re-enter `rc.onTimer` — re-issuing a fetch\n * the paginator no longer wants (a deadline timer could even overwrite the\n * settled `PAGE_KEY` slot with a deadline `failed`). On any non-`fetching`\n * phase the timer is absorbed as a pure no-op.\n */\n function onTimer(\n s: PaginatedWalkState<Cursor, Page>,\n msg: PaginatedWalkTimerMsg,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n if (s.walk.phase !== \"fetching\") {\n return [s, []];\n }\n const [resilience, cmds] = rc.onTimer(s.resilience, msg);\n if (resilience === s.resilience) {\n // Inherited no-op identity → keep our slice identity too (pure no-op).\n return [s, []];\n }\n return [withResilience(s, resilience), cmds];\n }\n\n // === Verb: drain =========================================================\n\n /**\n * Acknowledge that the consumer finished processing `n` items, lowering the\n * paginator's backpressure gauge (`seen`). PURE — no clock. Delegates to\n * `paginator.drain`: subtracts `n` (floored at 0), leaves the phase untouched.\n *\n * The OTHER half of the backpressure valve `highWaterMark` opens: `recordPage`\n * parks the walk in `paused` once `seen >= highWaterMark`, and `drain` is how a\n * consumer lowers `seen` back below the mark so `resume` can re-open the fetch.\n * Without it `highWaterMark` would be a one-way valve — a walk could pause but\n * never continue. Emits no Cmds: draining only frees headroom; `resume` is the\n * separate, explicit \"continue now\" decision.\n */\n function drain(\n s: PaginatedWalkState<Cursor, Page>,\n n: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n const walk = drainWalk(s.walk, n);\n if (walk === s.walk) {\n // Pure no-op identity (n <= 0, or seen already at floor).\n return [s, []];\n }\n return [{ ...s, walk }, []];\n }\n\n // === Verb: resume ========================================================\n\n /**\n * Re-open the backpressure valve after a `drain`: if the walk is `paused` and\n * `seen` has dropped below `highWaterMark`, re-arm the parked cursor and issue\n * its page fetch (gated through resilience). PURE — `at` threads into the fetch\n * gate. Delegates the phase decision to `paginator.resume`:\n *\n * - `paused` AND `seen < highWaterMark` → `fetching(cursor)` → emit the fetch.\n * - `paused` but still over the mark → no-op (the valve stays shut until a\n * further `drain` frees real headroom).\n * - any other phase → no-op (nothing to resume).\n *\n * Together with `drain` this completes the valve: `recordPage` shuts it\n * (`paused`), `drain` lowers the gauge, `resume` re-opens it and fires the next\n * page fetch — the same `fetch` helper `start` / `pageOk` use, so a resumed\n * fetch is gated identically (cache / circuit / rate-limit / retry).\n */\n function resume(\n s: PaginatedWalkState<Cursor, Page>,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // `resumeWalk` is a pure no-op (same reference) on any phase but `paused`, or\n // when `seen` is still at/over the mark. Only a fresh `fetching` arm means\n // the valve actually re-opened — issue the page fetch only then.\n return armAndFetch(s, resumeWalk(s.walk, paginatorPolicy), at);\n }\n\n // === Derived predicates ==================================================\n\n /**\n * Whether the walk has finished — the API returned a null next cursor and the\n * paginator is `done`. PURE, read-only. Lets a consumer's reducer fire a\n * \"walk complete\" Cmd (`if (walk.isDone(s.walk)) …`) without re-checking the\n * paginator phase by hand.\n */\n function isComplete(s: PaginatedWalkState<Cursor, Page>): boolean {\n return isDone(s.walk);\n }\n\n /**\n * The terminal page-fetch failure that stranded the walk, or `undefined` if no\n * page has terminally failed. PURE, read-only. Errors are data: a page that\n * exhausts its retries (or trips the deadline) settles the `PAGE_KEY` resilient\n * slot to `failed` while the paginator stays parked on its cursor — the walk is\n * stuck and will never advance on its own. Without a way to read that phase the\n * failure is unobservable: the walk simply stops emitting fetches with no\n * signal. `failure(s)` surfaces the settled error (the same plain-data sentinel\n * resilient-call records) so a consumer can fire a \"walk dead\" Cmd, alert, or\n * re-`start` a fresh walk.\n */\n function failure(s: PaginatedWalkState<Cursor, Page>): unknown {\n const call = s.resilience.calls[PAGE_KEY];\n return call?.phase === \"failed\" ? call.error : undefined;\n }\n\n /**\n * Whether the walk is STUCK: a page fetch has terminally `failed` yet the\n * paginator is still parked on a cursor (not `done`), so no further fetch will\n * ever be issued without intervention. PURE, read-only.\n *\n * This is the dead-walk predicate the consumer polls (`if (walk.isStuck(s)) …`)\n * to distinguish a healthy finish (`isComplete`) from a silent death. A walk is\n * stuck precisely when the resilient slot settled `failed` AND the paginator\n * has NOT reached `done` — a terminal failure on the final page that also\n * finished the walk is `isComplete`, not stuck. Pairs with `failure(s)`, which\n * hands back the error that stuck it.\n */\n function isStuck(s: PaginatedWalkState<Cursor, Page>): boolean {\n return s.resilience.calls[PAGE_KEY]?.phase === \"failed\" && !isDone(s.walk);\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions: exactly the resilient-call subs for the page\n * fetch — a retry timer while the fetch is `waiting_retry`, and (with the\n * `deadline` brick) a per-fetch deadline timer while it is active. Reconciled\n * by id, so a phase change cancels the matching timer automatically. Wire\n * `subscribe: { deadline: subscribeDeadline }` (re-exported below).\n */\n function subs(s: PaginatedWalkState<Cursor, Page>): readonly DeadlineSub[] {\n return rc.subs(s.resilience);\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handler for the page-fetch effect. Inherited from\n * resilient-call (`resilient_run`): wraps the consumer's `run(cursor)` port\n * via `tryInterpret` (Railway) — success → `resilient_ok` (handled by\n * `pageOk`), failure → `resilient_err` (handled by `pageErr`), each stamped\n * with `Date.now()` at the effect boundary (the ONE permitted clock read).\n *\n * interpret: walk.handlers({ run: (cursor) => api.fetchPage(cursor) })\n *\n * The consumer maps the inherited Msg types to their `pageOk` / `pageErr`\n * reducer cells (the wire shape is `resilient_ok` / `resilient_err`).\n */\n function handlers(ports: PaginatedWalkPorts<Cursor, Page>) {\n return rc.handlers(ports);\n }\n\n return {\n init,\n start,\n pageOk,\n pageErr,\n onTimer,\n drain,\n resume,\n isComplete,\n failure,\n isStuck,\n subs,\n handlers,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice\n * lives at `state.walk`. A convenience for the common single-slice host;\n * consumers with a differently-named field spread by hand. Pure — a thin record\n * rebuild, no clock / RNG.\n */\nexport function liftWalk<\n S extends { walk: PaginatedWalkState<Cursor, Page> },\n Cursor,\n Page,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [PaginatedWalkState<Cursor, Page>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, walk: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives (inherited from resilient-call) so\n * consumers wire one import: `subscribeDeadline` is the `subscribe` cell,\n * `deadlineSub` builds the Sub literal this knob's `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub };\n"]}
1
+ {"version":3,"sources":["../src/paginated-walk/index.ts"],"names":["start","drain","resume"],"mappings":"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","file":"chunk-XOAP3LOT.js","sourcesContent":["/**\n * @demlik/tea/paginated-walk — traverse a paginated API / sitemap end to end\n * WITHOUT a fake clock, without 429s, and resumable across a Durable-Object\n * eviction. The L2 composition that bolts `../resilient-call`'s resilience onto\n * `../paginator`'s cursor walk: each page is fetched as ONE keyed resilient\n * call (retry + backoff + rate-limit gate), and the cursor advances only once a\n * page settles `ok`.\n *\n * ## The recipe (the two siblings it composes)\n *\n * - **`../paginator`** owns the walk loop: the opaque `Cursor`, the `seen` /\n * `pages` counters, and the `idle → fetching → … → done` phase machine.\n * `paginated-walk` never re-implements cursor advancement — it folds each\n * settled page through `recordPage`, letting the paginator decide\n * `done` / `fetching(next)` / `paused`. Backpressure is a TWO-way valve:\n * `recordPage` parks the walk in `paused` once `seen >= highWaterMark`, and\n * the `drain` / `resume` verbs (delegating to the paginator's own\n * `drain` / `resume`) lower the gauge and re-open the parked fetch — without\n * them `highWaterMark` would be a one-way valve that strands a paused walk.\n * - **`../resilient-call`** owns the page fetch: the single `resilient_run`\n * effect per page is gated through that knob's cache → circuit → rate-limit\n * → retry pipeline. A transient page failure backs off and re-issues the\n * SAME cursor (idempotent retry); the walk does not advance until the page\n * genuinely settles.\n *\n * Those are the only two siblings this module calls into. The resilience knobs\n * (`retry` / `circuit` / `rateLimit` / `deadline`) are NOT separate bricks\n * `paginated-walk` composes — they belong to `resilient-call`, which owns the\n * rate-limit gate internally. This module merely passes them through to\n * `createResilientCall` and re-uses two config TYPES at the boundary:\n * `RateLimitConfig` (re-exported by `resilient-call`) and `RetryPolicy` (from\n * `../retry-backoff`). Pass a `rateLimit` and `resilient-call`'s token bucket\n * throttles page fetches (\"don't 429 the upstream\"); omit it and the walk runs\n * as fast as the upstream answers.\n *\n * Because the page-fetch resilience IS a `resilient-call` slice, this knob\n * **inherits that knob's contract verbatim**: the slice is a flat record, every\n * verb returns `readonly [State, Cmd[]]`, time is always an `at` parameter, and\n * the jitter RNG is injected once at `createPaginatedWalk(config, rng)` — never\n * read inside a verb (per the spec, `paginated-walk` *extends* `resilient-call`).\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is plain data: the paginator phase + counters, and\n * the resilient-call brick states (all JSON-serializable). The cursor lives\n * in the Model, not a closure, so a walk survives eviction mid-traversal and\n * resumes from the exact page it was fetching.\n * - **Replayable** — every transition is a verb returning new state + Cmds.\n * `nextCursor` / `onPage` are pure config functions applied at the verb\n * boundary; the Cmds they yield are plain data. `replay` reconstructs the\n * whole walk exactly.\n *\n * ## Where the clock / RNG live\n *\n * Inside the verbs: nowhere. `at` arrives on `start` / `pageOk` / `pageErr` /\n * `onTimer` and is threaded straight into the underlying resilient-call gate (so\n * a rate-limited or retried page measures backoff from that `at`). The jitter\n * RNG is injected at construction. The only clock read is `Date.now()` inside\n * the resilient-call `handlers` port (the effect boundary), inherited unchanged.\n *\n * ## The fixed fetch key\n *\n * A walk fetches one page at a time, so the resilient-call layer tracks exactly\n * one logical call under the constant key `PAGE_KEY`. The cursor being fetched\n * is the resilient call's `input`, so a post-eviction resume re-issues the same\n * page request. (`resilient-call` is keyed to support N concurrent calls; a walk\n * deliberately uses a single key — the paginator is the sequencer.)\n *\n * ## Typical wiring\n *\n * const walk = createPaginatedWalk<PageToken, ApiPage>({\n * firstPage: null, // the API's \"page one\" sentinel\n * nextCursor: (page) => page.nextToken ?? null,\n * onPage: (page) => [{ type: \"index_rows\", rows: page.items }],\n * rateLimit: { capacity: 10, refillPerSec: 5 },\n * retry: defaultRetryPolicy,\n * });\n *\n * // in the machine:\n * init: () => [{ walk: walk.init() }, []],\n * update: {\n * start: (s, m) => lift(s, walk.start(s.walk, m.at)),\n * page_ok: (s, m) => lift(s, walk.pageOk(s.walk, m.result, m.at)),\n * page_err: (s, m) => lift(s, walk.pageErr(s.walk, m.error, m.at)),\n * retry_due:(s, m) => lift(s, walk.onTimer(s.walk, m)),\n * drained: (s, m) => lift(s, walk.drain(s.walk, m.n)),\n * resume: (s, m) => lift(s, walk.resume(s.walk, m.at)),\n * },\n * subscriptions: (s) => walk.subs(s.walk),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: walk.handlers({ run: (cursor) => api.fetchPage(cursor) }),\n */\n\nimport type { Cmd } from \"../index\";\nimport {\n drain as drainWalk,\n initPaginator,\n isDone,\n type PaginatorPolicy,\n type PaginatorState,\n recordPage,\n resume as resumeWalk,\n start as startWalk,\n} from \"../paginator\";\nimport { MsgType } from \"../protocol\";\nimport {\n type CircuitConfig,\n createResilientCall,\n type DeadlineConfig,\n type DeadlineSub,\n deadlineSub,\n type FailMsg,\n type RateLimitConfig,\n type ResilientPorts,\n type ResilientState,\n type ResilientTimerMsg,\n type RunCmd,\n type SucceedMsg,\n subscribeDeadline,\n} from \"../resilient-call\";\nimport type { RetryPolicy } from \"../retry-backoff\";\n\n// ===========================================================================\n// Config — the knob. `firstPage` / `nextCursor` / `onPage` describe the walk;\n// the resilience bricks are inherited from resilient-call (every one optional —\n// omit a brick, omit its gate).\n// ===========================================================================\n\n/**\n * The paginated-walk knob. Generic in `Cursor` (the opaque page handle the\n * upstream API hands back — offset, page token, \"next\" URL, …) and `Page` (the\n * shape your fetch port resolves with).\n *\n * `nextCursor` and `onPage` are PURE config functions applied at the verb\n * boundary, never stored in the slice or in a Cmd (invariant 3 — Cmds are\n * plain data, not closures). `nextCursor(page)` extracts the API's next-page\n * handle (or `null` at the end); `onPage(page)` yields the Cmds the consumer\n * wants emitted for each successfully fetched page (index rows, write to R2,\n * dispatch a downstream Msg — whatever; an empty array is fine).\n *\n * The resilience bricks (`retry` / `circuit` / `rateLimit` / `deadline`) are\n * the SAME optional knobs `resilient-call` exposes — paginated-walk extends it.\n * `rateLimit` is the brick that keeps the walk from 429-ing the upstream.\n */\nexport interface PaginatedWalkConfig<Cursor, Page, EmittedCmd extends Cmd> {\n /**\n * The cursor the walk fetches first. For an offset API this is typically `0`;\n * for a token API, whatever sentinel the API treats as \"page one\" (often\n * `null` or `\"\"`). Threaded into `paginator`'s `firstCursor`.\n */\n readonly firstPage: Cursor;\n /**\n * Extract the next cursor from a just-fetched page. Return a `Cursor` to keep\n * walking, or `null` when the API signalled the end (the walk finishes). PURE\n * + consumer-specific (only the consumer knows the API's pagination shape).\n */\n readonly nextCursor: (page: Page) => Cursor | null;\n /**\n * The Cmds to emit for each successfully fetched page — the consumer's work\n * per page (index, persist, fan out, …). Returns plain-data Cmds; `[]` is a\n * valid \"I process the page synchronously elsewhere\" answer. PURE.\n */\n readonly onPage: (page: Page) => readonly EmittedCmd[];\n /**\n * How many items a page contributes to the paginator's backpressure gauge\n * (`seen`). Defaults to `1` per page (count pages, not items) when omitted; a\n * consumer that wants per-item backpressure returns `page.items.length`. PURE.\n */\n readonly pageSize?: (page: Page) => number;\n /**\n * Backpressure threshold handed to the paginator. `seen >= highWaterMark`\n * parks the walk in `paused` instead of fetching the next page. Non-positive\n * (the default `0`) disables backpressure — the walk runs flat out until the\n * API exhausts. See `../paginator`'s `highWaterMark` docs.\n */\n readonly highWaterMark?: number;\n\n // --- resilience bricks, inherited from resilient-call (all optional) ---\n /** Exponential-backoff retry policy for a transient page-fetch failure. */\n readonly retry?: RetryPolicy;\n /** Circuit breaker around the page fetch (one upstream → one breaker). */\n readonly circuit?: CircuitConfig;\n /** Token-bucket rate limit — the \"don't 429 the upstream\" knob. */\n readonly rateLimit?: RateLimitConfig;\n /** Overall wall-clock deadline per individual page fetch. */\n readonly deadline?: DeadlineConfig;\n}\n\n// ===========================================================================\n// Slice — the Model field this knob owns. The paginator walk state + the\n// resilient-call slice for the page fetch. Both are flat plain data.\n// ===========================================================================\n\n/**\n * The slice. `walk` is the cursor/phase/counters from `../paginator`;\n * `resilience` is the `../resilient-call` slice that gates the page fetch.\n * Every field is JSON-serializable, so the whole walk survives a reload.\n *\n * The two move in lockstep: `walk` decides WHICH cursor to fetch next, while\n * `resilience` decides HOW that fetch is attempted (gated, retried, backed\n * off). `pageOk` advances `walk`; `pageErr` defers to `resilience` and leaves\n * `walk` parked on the same cursor until the page truly settles.\n */\nexport interface PaginatedWalkState<Cursor, Page> {\n readonly walk: PaginatorState<Cursor>;\n readonly resilience: ResilientState<Cursor, Page>;\n}\n\n// ===========================================================================\n// Cmds + Msgs the knob speaks.\n// ===========================================================================\n\n/**\n * The page-fetch effect this knob emits: the inherited `resilient_run` Cmd from\n * resilient-call, whose `input` is the `Cursor` to fetch and whose `key` is the\n * fixed `PAGE_KEY`. The consumer's `handlers(ports)` interprets it.\n */\nexport type FetchPageCmd<Cursor> = RunCmd<Cursor>;\n\n/** Page-settled Msgs the `handlers` port dispatches back (inherited verbatim). */\nexport type PageOkMsg<Page> = SucceedMsg<Page>;\nexport type PageErrMsg = FailMsg;\n\n/** The retry / deadline timer Msg — inherited from resilient-call. */\nexport type PaginatedWalkTimerMsg = ResilientTimerMsg;\n\n/**\n * Ports the consumer supplies to `handlers`. `run(cursor)` fetches ONE page for\n * the given cursor; throwing routes to `pageErr` (and thus the resilience\n * backoff). Same shape as resilient-call's port, specialized to the page fetch.\n */\nexport interface PaginatedWalkPorts<Cursor, Page>\n extends ResilientPorts<Cursor, Page> {}\n\n// ===========================================================================\n// The fixed key. A walk fetches one page at a time → exactly one logical\n// resilient call. The paginator is the sequencer; resilient-call's keying is\n// pinned to this single key.\n// ===========================================================================\n\n/** The single resilient-call key every page fetch runs under. */\nexport const PAGE_KEY = \"page\";\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a paginated-walk knob from `config`. `rng` is injected for the\n * underlying retry jitter — pass a fixed `() => 0` in tests to pin backoff;\n * defaults to `Math.random` (read at the verb boundary inside resilient-call,\n * never in a `paginated-walk` verb). Inherited straight from resilient-call.\n *\n * Returns the uniform L2 knob contract: `init()`, the verbs `start` / `pageOk`\n * / `pageErr` / `onTimer`, `subs(state)`, and `handlers(ports)`. `Cursor` is\n * the page handle, `Page` the fetch result, `EmittedCmd` the consumer's\n * per-page Cmds plus the inherited `FetchPageCmd`.\n */\nexport function createPaginatedWalk<Cursor, Page, EmittedCmd extends Cmd = Cmd>(\n config: PaginatedWalkConfig<Cursor, Page, EmittedCmd>,\n rng: () => number = Math.random,\n) {\n // The page fetch is a resilient call keyed by Cursor input → Page result.\n // We feed it only the resilience bricks; the walk-loop knobs are ours.\n const rc = createResilientCall<Cursor, Page>(\n {\n retry: config.retry,\n circuit: config.circuit,\n rateLimit: config.rateLimit,\n deadline: config.deadline,\n },\n rng,\n );\n\n // The paginator policy: our firstPage is the paginator's firstCursor; the\n // high-water mark gates backpressure (0 / omitted → unbounded).\n const paginatorPolicy: PaginatorPolicy<Cursor> = {\n firstCursor: config.firstPage,\n highWaterMark: config.highWaterMark ?? 0,\n };\n\n // The combined Cmd type the verbs can emit: the inherited page-fetch effect\n // plus whatever `onPage` yields.\n type OutCmd = FetchPageCmd<Cursor> | EmittedCmd;\n\n /** Replace the resilience slice, leaving `walk` untouched. */\n function withResilience(\n s: PaginatedWalkState<Cursor, Page>,\n resilience: ResilientState<Cursor, Page>,\n ): PaginatedWalkState<Cursor, Page> {\n return { ...s, resilience };\n }\n\n /** The starting slice: an idle paginator + a fresh resilient-call slice. */\n function init(): PaginatedWalkState<Cursor, Page> {\n return {\n walk: initPaginator<Cursor>(),\n resilience: rc.init(),\n };\n }\n\n /**\n * Issue a resilient page fetch for `cursor` at `at`. The cursor is the\n * resilient call's `input` (so a retry / resume re-fetches the same page) and\n * `PAGE_KEY` is the fixed key. Returns the advanced resilience slice + the\n * `resilient_run` Cmd (or none, if the gate cached / fast-failed / backed off).\n * PURE — `at` is the only clock. Shared by `start` and `pageOk`'s advance.\n */\n function fetch(\n s: PaginatedWalkState<Cursor, Page>,\n cursor: Cursor,\n at: number,\n ): readonly [\n PaginatedWalkState<Cursor, Page>,\n readonly FetchPageCmd<Cursor>[],\n ] {\n const [resilience, cmds] = rc.attempt(s.resilience, PAGE_KEY, cursor, at);\n return [withResilience(s, resilience), cmds];\n }\n\n /**\n * Issue the page fetch for a freshly-armed paginator transition, else a pure\n * no-op. `walk` is the result of a paginator verb (`startWalk` / `resumeWalk`)\n * applied to `s.walk`; this states the arm-and-fetch contract ONCE for the two\n * verbs that share it (`start`, `resume`).\n *\n * A paginator verb is a pure no-op on any phase it does not act on — it returns\n * the SAME state object. Reference identity (not phase) is the honest \"did the\n * verb arm a fresh fetch?\" check: a walk already `fetching` would also satisfy\n * `phase === \"fetching\"` yet must NOT re-issue a fetch (page-one skip /\n * double-fetch). So we proceed ONLY when the verb produced a fresh state. The\n * `phase === \"fetching\"` narrow that follows is therefore always true here\n * (both verbs only ever produce a `fetching` arm), and also narrows `.cursor`\n * for the type checker. PURE — `at` threads into the fetch gate.\n */\n function armAndFetch(\n s: PaginatedWalkState<Cursor, Page>,\n walk: PaginatorState<Cursor>,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n if (walk === s.walk || walk.phase !== \"fetching\") {\n return [s, []];\n }\n return fetch({ ...s, walk }, walk.cursor, at);\n }\n\n // === Verb: start =========================================================\n\n /**\n * Begin the walk: arm the paginator's first cursor and issue the first page\n * fetch (gated through resilience). On any non-`idle` paginator phase this is\n * a no-op — re-starting a walk in flight would skip back to page one and\n * double-count, so a stray `start` after launch is absorbed (the paginator's\n * own `start` guard). PURE — `at` threads into the fetch gate.\n */\n function start(\n s: PaginatedWalkState<Cursor, Page>,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // `startWalk` is a pure no-op on any non-`idle` phase (same reference); a\n // fresh `fetching(firstCursor)` arm → issue the first page fetch.\n return armAndFetch(s, startWalk(s.walk, paginatorPolicy), at);\n }\n\n // === Verb: pageOk ========================================================\n\n /**\n * Record a successfully fetched `page` and decide what comes next. PURE —\n * `at` is the cache / next-fetch clock.\n *\n * 1. Settle the resilient call OK (closes the breaker, fills the cache,\n * resets the page-fetch retry counter) — inherited from resilient-call.\n * 2. Fold the page through the paginator: add `pageSize(page)` to `seen`,\n * bump `pages`, and branch on `nextCursor(page)`:\n * - `null` → the API is exhausted → paginator goes `done`.\n * - a cursor → advance, then either fetch it (backpressure room) or\n * park in `paused` (high-water mark reached).\n * 3. Emit the consumer's `onPage(page)` Cmds either way, PLUS the next\n * page-fetch effect when the paginator stays `fetching`.\n *\n * A `pageOk` that arrives when the paginator is not `fetching` (a late\n * duplicate, a double-dispatch) is absorbed by `recordPage` — the cursor does\n * not advance and `seen` is not double-counted; only the resilient settle +\n * `onPage` Cmds happen. The walk is a coordination aid, not a correctness\n * gate.\n */\n function pageOk(\n s: PaginatedWalkState<Cursor, Page>,\n page: Page,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // 1) Settle the underlying resilient call as a success.\n const [resilience] = rc.succeed(s.resilience, PAGE_KEY, {\n type: MsgType.ResilientOk,\n key: PAGE_KEY,\n result: page,\n at,\n });\n\n // 2) Advance the paginator with the page's contribution + the next cursor.\n const count = config.pageSize ? config.pageSize(page) : 1;\n const next = config.nextCursor(page);\n const walk = recordPage(s.walk, count, next, paginatorPolicy);\n\n const advanced: PaginatedWalkState<Cursor, Page> = { walk, resilience };\n\n // 3) The consumer's per-page work, always emitted on a successful page.\n const pageCmds: readonly EmittedCmd[] = config.onPage(page);\n\n // If the paginator stayed `fetching`, issue the next page fetch (gated).\n if (walk.phase === \"fetching\") {\n const [withFetch, fetchCmds] = fetch(advanced, walk.cursor, at);\n return [withFetch, [...pageCmds, ...fetchCmds]];\n }\n\n // `done` (exhausted) or `paused` (backpressure) → no further fetch now.\n return [advanced, pageCmds];\n }\n\n // === Verb: pageErr =======================================================\n\n /**\n * Record a failed page fetch and back off via resilience. PURE — `at` stamps\n * the breaker trip + the retry-delay base.\n *\n * Defers to the resilient-call `fail` verb: it trips the breaker and either\n * schedules a retry (paginator stays parked on the SAME cursor — no advance)\n * or settles the resilient call `failed` once retries are exhausted. The\n * paginator is deliberately untouched: a failed fetch must NOT advance the\n * cursor or skip a page. When the retry timer later fires, `onTimer` re-issues\n * the same page fetch.\n *\n * GUARDED by the paginator phase, exactly as `pageOk` is guarded by\n * `recordPage`'s `fetching`-only check. A `pageErr` only makes sense while a\n * page fetch is outstanding (`walk.phase === \"fetching\"`). A stray `pageErr`\n * that arrives after the walk has settled — `done` (exhausted), `paused`\n * (backpressure), or `idle` (not started) — has NO outstanding fetch to fail.\n * Without this guard it would still call `rc.fail`, which trips the SHARED\n * circuit breaker (dinging a healthy upstream) and overwrites the settled\n * `PAGE_KEY` resilient slot with `failed` (clobbering the succeeded result a\n * resume would otherwise observe). So a stray `pageErr` is absorbed as a pure\n * no-op — identity preserved — matching `pageOk`'s \"the walk is a coordination\n * aid, not a correctness gate\" stance.\n */\n function pageErr(\n s: PaginatedWalkState<Cursor, Page>,\n error: unknown,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // Only a `fetching` paginator has an outstanding page fetch to fail. On any\n // other phase the fail would corrupt shared resilience state for a fetch\n // that is not in flight — absorb it as a pure no-op.\n if (s.walk.phase !== \"fetching\") {\n return [s, []];\n }\n const [resilience, cmds] = rc.fail(s.resilience, PAGE_KEY, {\n type: MsgType.ResilientErr,\n key: PAGE_KEY,\n error,\n at,\n });\n return [withResilience(s, resilience), cmds];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A retry / deadline timer fired. Defers to resilient-call's `onTimer`: a\n * retry timer re-runs the gate for the page-fetch's remembered cursor (re-\n * issuing the SAME page); a deadline timer settles the resilient call failed.\n * The paginator stays parked on its cursor throughout — only `pageOk` advances\n * it. A stale fire is a no-op (inherited). PURE.\n *\n * GUARDED by the paginator phase, for the same reason as `pageErr`. A timer is\n * only meaningful while a page fetch is outstanding (`walk.phase ===\n * \"fetching\"`). A stray timer that fires after the walk has settled (`done` /\n * `paused` / `idle`) would otherwise re-enter `rc.onTimer` — re-issuing a fetch\n * the paginator no longer wants (a deadline timer could even overwrite the\n * settled `PAGE_KEY` slot with a deadline `failed`). On any non-`fetching`\n * phase the timer is absorbed as a pure no-op.\n */\n function onTimer(\n s: PaginatedWalkState<Cursor, Page>,\n msg: PaginatedWalkTimerMsg,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n if (s.walk.phase !== \"fetching\") {\n return [s, []];\n }\n const [resilience, cmds] = rc.onTimer(s.resilience, msg);\n if (resilience === s.resilience) {\n // Inherited no-op identity → keep our slice identity too (pure no-op).\n return [s, []];\n }\n return [withResilience(s, resilience), cmds];\n }\n\n // === Verb: drain =========================================================\n\n /**\n * Acknowledge that the consumer finished processing `n` items, lowering the\n * paginator's backpressure gauge (`seen`). PURE — no clock. Delegates to\n * `paginator.drain`: subtracts `n` (floored at 0), leaves the phase untouched.\n *\n * The OTHER half of the backpressure valve `highWaterMark` opens: `recordPage`\n * parks the walk in `paused` once `seen >= highWaterMark`, and `drain` is how a\n * consumer lowers `seen` back below the mark so `resume` can re-open the fetch.\n * Without it `highWaterMark` would be a one-way valve — a walk could pause but\n * never continue. Emits no Cmds: draining only frees headroom; `resume` is the\n * separate, explicit \"continue now\" decision.\n */\n function drain(\n s: PaginatedWalkState<Cursor, Page>,\n n: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n const walk = drainWalk(s.walk, n);\n if (walk === s.walk) {\n // Pure no-op identity (n <= 0, or seen already at floor).\n return [s, []];\n }\n return [{ ...s, walk }, []];\n }\n\n // === Verb: resume ========================================================\n\n /**\n * Re-open the backpressure valve after a `drain`: if the walk is `paused` and\n * `seen` has dropped below `highWaterMark`, re-arm the parked cursor and issue\n * its page fetch (gated through resilience). PURE — `at` threads into the fetch\n * gate. Delegates the phase decision to `paginator.resume`:\n *\n * - `paused` AND `seen < highWaterMark` → `fetching(cursor)` → emit the fetch.\n * - `paused` but still over the mark → no-op (the valve stays shut until a\n * further `drain` frees real headroom).\n * - any other phase → no-op (nothing to resume).\n *\n * Together with `drain` this completes the valve: `recordPage` shuts it\n * (`paused`), `drain` lowers the gauge, `resume` re-opens it and fires the next\n * page fetch — the same `fetch` helper `start` / `pageOk` use, so a resumed\n * fetch is gated identically (cache / circuit / rate-limit / retry).\n */\n function resume(\n s: PaginatedWalkState<Cursor, Page>,\n at: number,\n ): readonly [PaginatedWalkState<Cursor, Page>, readonly OutCmd[]] {\n // `resumeWalk` is a pure no-op (same reference) on any phase but `paused`, or\n // when `seen` is still at/over the mark. Only a fresh `fetching` arm means\n // the valve actually re-opened — issue the page fetch only then.\n return armAndFetch(s, resumeWalk(s.walk, paginatorPolicy), at);\n }\n\n // === Derived predicates ==================================================\n\n /**\n * Whether the walk has finished — the API returned a null next cursor and the\n * paginator is `done`. PURE, read-only. Lets a consumer's reducer fire a\n * \"walk complete\" Cmd (`if (walk.isDone(s.walk)) …`) without re-checking the\n * paginator phase by hand.\n */\n function isComplete(s: PaginatedWalkState<Cursor, Page>): boolean {\n return isDone(s.walk);\n }\n\n /**\n * The terminal page-fetch failure that stranded the walk, or `undefined` if no\n * page has terminally failed. PURE, read-only. Errors are data: a page that\n * exhausts its retries (or trips the deadline) settles the `PAGE_KEY` resilient\n * slot to `failed` while the paginator stays parked on its cursor — the walk is\n * stuck and will never advance on its own. Without a way to read that phase the\n * failure is unobservable: the walk simply stops emitting fetches with no\n * signal. `failure(s)` surfaces the settled error (the same plain-data sentinel\n * resilient-call records) so a consumer can fire a \"walk dead\" Cmd, alert, or\n * re-`start` a fresh walk.\n */\n function failure(s: PaginatedWalkState<Cursor, Page>): unknown {\n const call = s.resilience.calls[PAGE_KEY];\n return call?.phase === \"failed\" ? call.error : undefined;\n }\n\n /**\n * Whether the walk is STUCK: a page fetch has terminally `failed` yet the\n * paginator is still parked on a cursor (not `done`), so no further fetch will\n * ever be issued without intervention. PURE, read-only.\n *\n * This is the dead-walk predicate the consumer polls (`if (walk.isStuck(s)) …`)\n * to distinguish a healthy finish (`isComplete`) from a silent death. A walk is\n * stuck precisely when the resilient slot settled `failed` AND the paginator\n * has NOT reached `done` — a terminal failure on the final page that also\n * finished the walk is `isComplete`, not stuck. Pairs with `failure(s)`, which\n * hands back the error that stuck it.\n */\n function isStuck(s: PaginatedWalkState<Cursor, Page>): boolean {\n return s.resilience.calls[PAGE_KEY]?.phase === \"failed\" && !isDone(s.walk);\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions: exactly the resilient-call subs for the page\n * fetch — a retry timer while the fetch is `waiting_retry`, and (with the\n * `deadline` brick) a per-fetch deadline timer while it is active. Reconciled\n * by id, so a phase change cancels the matching timer automatically. Wire\n * `subscribe: { deadline: subscribeDeadline }` (re-exported below).\n */\n function subs(s: PaginatedWalkState<Cursor, Page>): readonly DeadlineSub[] {\n return rc.subs(s.resilience);\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handler for the page-fetch effect. Inherited from\n * resilient-call (`resilient_run`): wraps the consumer's `run(cursor)` port\n * via `tryInterpret` (Railway) — success → `resilient_ok` (handled by\n * `pageOk`), failure → `resilient_err` (handled by `pageErr`), each stamped\n * with `Date.now()` at the effect boundary (the ONE permitted clock read).\n *\n * interpret: walk.handlers({ run: (cursor) => api.fetchPage(cursor) })\n *\n * The consumer maps the inherited Msg types to their `pageOk` / `pageErr`\n * reducer cells (the wire shape is `resilient_ok` / `resilient_err`).\n */\n function handlers(ports: PaginatedWalkPorts<Cursor, Page>) {\n return rc.handlers(ports);\n }\n\n return {\n init,\n start,\n pageOk,\n pageErr,\n onTimer,\n drain,\n resume,\n isComplete,\n failure,\n isStuck,\n subs,\n handlers,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice\n * lives at `state.walk`. A convenience for the common single-slice host;\n * consumers with a differently-named field spread by hand. Pure — a thin record\n * rebuild, no clock / RNG.\n */\nexport function liftWalk<\n S extends { walk: PaginatedWalkState<Cursor, Page> },\n Cursor,\n Page,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [PaginatedWalkState<Cursor, Page>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, walk: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives (inherited from resilient-call) so\n * consumers wire one import: `subscribeDeadline` is the `subscribe` cell,\n * `deadlineSub` builds the Sub literal this knob's `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub };\n"]}
@@ -1,4 +1,4 @@
1
- import { detectUpdateForm, foldUpdates, depsInactive, structuralHash, applyCellChecked } from './chunk-DBOJ2UC6.js';
1
+ import { detectUpdateForm, foldUpdates, depsInactive, structuralHash, applyCellChecked } from './chunk-O7ED34JH.js';
2
2
  import { Result } from 'better-result';
3
3
 
4
4
  // src/observability.ts
@@ -62,6 +62,7 @@ var QuiescenceTimeoutError = class extends Error {
62
62
  );
63
63
  this.iterations = iterations;
64
64
  }
65
+ iterations;
65
66
  name = "QuiescenceTimeoutError";
66
67
  _tag = "QuiescenceTimeoutError";
67
68
  };
@@ -74,6 +75,7 @@ var RuntimeDiscardedError = class extends RuntimeDiscardNotice {
74
75
  );
75
76
  this.pendingCmds = pendingCmds;
76
77
  }
78
+ pendingCmds;
77
79
  name = "RuntimeDiscardedError";
78
80
  _tag = "RuntimeDiscardedError";
79
81
  };
@@ -84,6 +86,7 @@ var IdentityDropNotice = class extends RuntimeDiscardNotice {
84
86
  );
85
87
  this.msgType = msgType;
86
88
  }
89
+ msgType;
87
90
  name = "IdentityDropNotice";
88
91
  _tag = "IdentityDropNotice";
89
92
  };
@@ -95,6 +98,8 @@ var DisposeTimeoutNotice = class extends RuntimeDiscardNotice {
95
98
  this.pendingDisposals = pendingDisposals;
96
99
  this.timeoutMs = timeoutMs;
97
100
  }
101
+ pendingDisposals;
102
+ timeoutMs;
98
103
  name = "DisposeTimeoutNotice";
99
104
  _tag = "DisposeTimeoutNotice";
100
105
  };
@@ -105,6 +110,7 @@ var DispatchDiscardedError = class extends RuntimeDiscardNotice {
105
110
  );
106
111
  this.msgType = msgType;
107
112
  }
113
+ msgType;
108
114
  name = "DispatchDiscardedError";
109
115
  _tag = "DispatchDiscardedError";
110
116
  };
@@ -656,5 +662,5 @@ function run(machine, opts) {
656
662
  }
657
663
 
658
664
  export { DispatchDiscardedError, DisposeTimeoutNotice, IdentityDropNotice, PortNameCollisionError, QuiescenceTimeoutError, RuntimeDiscardNotice, RuntimeDiscardedError, SubIdCollisionError, __resetPortRegistry, absurd, asReducer, defineMachine, definePort, historyTracker, noop, replay, run, schemaMigrate, tryInterpret, wrapDetached };
659
- //# sourceMappingURL=chunk-PBAYOJJO.js.map
660
- //# sourceMappingURL=chunk-PBAYOJJO.js.map
665
+ //# sourceMappingURL=chunk-ZH454CR5.js.map
666
+ //# sourceMappingURL=chunk-ZH454CR5.js.map