@demlik/tea 0.19.0 → 0.21.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 (120) hide show
  1. package/README.md +36 -20
  2. package/dist/agent/index.d.ts +48 -17
  3. package/dist/agent/index.js +10 -10
  4. package/dist/{chunk-IHUNKHDX.js → chunk-4I2LCQKB.js} +3 -3
  5. package/dist/{chunk-IHUNKHDX.js.map → chunk-4I2LCQKB.js.map} +1 -1
  6. package/dist/{chunk-XVIKSRCK.js → chunk-6NIQIUGR.js} +4 -4
  7. package/dist/{chunk-XVIKSRCK.js.map → chunk-6NIQIUGR.js.map} +1 -1
  8. package/dist/{chunk-JVKU2UQC.js → chunk-7Z2IDZ7R.js} +3 -3
  9. package/dist/{chunk-JVKU2UQC.js.map → chunk-7Z2IDZ7R.js.map} +1 -1
  10. package/dist/{chunk-H3MWNH7A.js → chunk-BFSZ4L6Y.js} +4 -4
  11. package/dist/{chunk-H3MWNH7A.js.map → chunk-BFSZ4L6Y.js.map} +1 -1
  12. package/dist/{chunk-H2ELKHI6.js → chunk-EB5OMOXX.js} +4 -4
  13. package/dist/chunk-EB5OMOXX.js.map +1 -0
  14. package/dist/{chunk-AIIXV4XB.js → chunk-GAVLND5C.js} +19 -4
  15. package/dist/chunk-GAVLND5C.js.map +1 -0
  16. package/dist/{chunk-I7E7AD2S.js → chunk-HU3Y2AZY.js} +4 -4
  17. package/dist/chunk-HU3Y2AZY.js.map +1 -0
  18. package/dist/{chunk-WTSOW2MD.js → chunk-MBQR3LFW.js} +5 -5
  19. package/dist/{chunk-WTSOW2MD.js.map → chunk-MBQR3LFW.js.map} +1 -1
  20. package/dist/{chunk-C3G2QFEZ.js → chunk-QIE4Y6BK.js} +4 -4
  21. package/dist/{chunk-C3G2QFEZ.js.map → chunk-QIE4Y6BK.js.map} +1 -1
  22. package/dist/{chunk-BRX3243I.js → chunk-TMYUDZLE.js} +3 -3
  23. package/dist/{chunk-BRX3243I.js.map → chunk-TMYUDZLE.js.map} +1 -1
  24. package/dist/{chunk-FNGZMVIT.js → chunk-VHKDUBXC.js} +3 -3
  25. package/dist/chunk-VHKDUBXC.js.map +1 -0
  26. package/dist/{chunk-AHW2CLPT.js → chunk-VM7PMFV5.js} +70 -47
  27. package/dist/chunk-VM7PMFV5.js.map +1 -0
  28. package/dist/{chunk-PCJA2HTZ.js → chunk-W6XWI6QE.js} +20 -20
  29. package/dist/chunk-W6XWI6QE.js.map +1 -0
  30. package/dist/{chunk-JYNYG5SF.js → chunk-YCJPLPQQ.js} +3 -3
  31. package/dist/chunk-YCJPLPQQ.js.map +1 -0
  32. package/dist/{chunk-YIMOFXST.js → chunk-YDF6P74H.js} +3 -3
  33. package/dist/chunk-YDF6P74H.js.map +1 -0
  34. package/dist/{chunk-JVE2LBFJ.js → chunk-Z7IL735U.js} +3 -3
  35. package/dist/{chunk-JVE2LBFJ.js.map → chunk-Z7IL735U.js.map} +1 -1
  36. package/dist/{chunk-PQSDUX6B.js → chunk-ZV4WGW74.js} +3 -3
  37. package/dist/{chunk-PQSDUX6B.js.map → chunk-ZV4WGW74.js.map} +1 -1
  38. package/dist/{core-Ce9ncpuv.d.ts → core-BKJW1Kmq.d.ts} +154 -2
  39. package/dist/devtools/index.d.ts +17 -0
  40. package/dist/devtools/index.js.map +1 -1
  41. package/dist/do/index.d.ts +17 -15
  42. package/dist/do/index.js +11 -11
  43. package/dist/do/index.js.map +1 -1
  44. package/dist/{drive-loop-CsUsW3Wo.d.ts → drive-loop-Dc6Z8Vbs.d.ts} +1 -1
  45. package/dist/{durable-effects-BmUrRA-h.d.ts → durable-effects-CgrLNccA.d.ts} +1 -1
  46. package/dist/effect/index.d.ts +191 -5
  47. package/dist/effect/index.js +71 -4
  48. package/dist/effect/index.js.map +1 -1
  49. package/dist/extension/index.d.ts +84 -3
  50. package/dist/extension/index.js +2 -2
  51. package/dist/extension/index.js.map +1 -1
  52. package/dist/flow/index.d.ts +33 -13
  53. package/dist/flow/index.js +13 -12
  54. package/dist/flow/index.js.map +1 -1
  55. package/dist/idempotency/index.d.ts +11 -3
  56. package/dist/idempotency/index.js +3 -3
  57. package/dist/{index-Ce5GYAuI.d.ts → index-1hit3dJ9.d.ts} +2 -2
  58. package/dist/{index-2kNxy6az.d.ts → index-B-VC4qjx.d.ts} +16 -4
  59. package/dist/{index-Bf9_bNql.d.ts → index-BjwRcjBi.d.ts} +2 -2
  60. package/dist/{index-Dy3379ve.d.ts → index-CYrUWi1c.d.ts} +4 -4
  61. package/dist/{index-DWdg7R1l.d.ts → index-Cuy1_xTh.d.ts} +5 -1
  62. package/dist/{index-D8sYVzCI.d.ts → index-DB8Yn0-K.d.ts} +6 -1
  63. package/dist/{index-e36uEyl4.d.ts → index-Diyo8WBO.d.ts} +2 -2
  64. package/dist/{index-B1Wzarkl.d.ts → index-Z6Uw7C9t.d.ts} +1 -1
  65. package/dist/index.d.ts +70 -9
  66. package/dist/index.js +2 -2
  67. package/dist/jev/index.d.ts +46 -34
  68. package/dist/jev/index.js +12 -8
  69. package/dist/jev/index.js.map +1 -1
  70. package/dist/{machine-CuojfZup.d.ts → machine-BuPTS1QH.d.ts} +11 -9
  71. package/dist/machine-viz/index.d.ts +2 -1
  72. package/dist/machine-viz/index.js +2 -2
  73. package/dist/machine-viz/index.js.map +1 -1
  74. package/dist/mem/index.d.ts +2 -2
  75. package/dist/mem/index.js +2 -2
  76. package/dist/node/index.d.ts +11 -2
  77. package/dist/node/index.js +2 -2
  78. package/dist/node/index.js.map +1 -1
  79. package/dist/otel/index.d.ts +10 -10
  80. package/dist/otel/index.js +10 -10
  81. package/dist/paginate/index.d.ts +5 -4
  82. package/dist/paginate/index.js +5 -5
  83. package/dist/parity/index.d.ts +3 -3
  84. package/dist/parity/index.js +3 -3
  85. package/dist/parity/index.js.map +1 -1
  86. package/dist/pbt/index.d.ts +1 -1
  87. package/dist/pbt/index.js +2 -2
  88. package/dist/persistence/index.d.ts +5 -5
  89. package/dist/persistence/index.js +4 -4
  90. package/dist/promise/index.d.ts +30 -10
  91. package/dist/promise/index.js +4 -4
  92. package/dist/react/index.d.ts +7 -6
  93. package/dist/react/index.js.map +1 -1
  94. package/dist/resilience/index.d.ts +7 -6
  95. package/dist/resilience/index.js +5 -5
  96. package/dist/resilience/index.js.map +1 -1
  97. package/dist/{index-BnsAqn0-.d.ts → run-D0sxbyno.d.ts} +23 -7
  98. package/dist/{runtime-types-BKg2CGMx.d.ts → runtime-types-BPKp2bGo.d.ts} +120 -9
  99. package/dist/testing/effect/index.d.ts +5 -5
  100. package/dist/testing/effect/index.js +8 -8
  101. package/dist/testing/index.d.ts +2 -2
  102. package/dist/testing/index.js +3 -3
  103. package/dist/testing/promise/index.d.ts +4 -4
  104. package/dist/testing/promise/index.js +5 -5
  105. package/dist/timing/index.d.ts +4 -4
  106. package/dist/timing/index.js +4 -4
  107. package/dist/types-BiXj7k_k.d.ts +9 -0
  108. package/dist/work-queue/index.d.ts +4 -4
  109. package/dist/work-queue/index.js +2 -2
  110. package/dist/work-queue/index.js.map +1 -1
  111. package/package.json +6 -3
  112. package/dist/chunk-AHW2CLPT.js.map +0 -1
  113. package/dist/chunk-AIIXV4XB.js.map +0 -1
  114. package/dist/chunk-FNGZMVIT.js.map +0 -1
  115. package/dist/chunk-H2ELKHI6.js.map +0 -1
  116. package/dist/chunk-I7E7AD2S.js.map +0 -1
  117. package/dist/chunk-JYNYG5SF.js.map +0 -1
  118. package/dist/chunk-PCJA2HTZ.js.map +0 -1
  119. package/dist/chunk-YIMOFXST.js.map +0 -1
  120. package/dist/types-SzNSHKlV.d.ts +0 -5
@@ -1,7 +1,7 @@
1
1
  import { idempotencyMemory } from './chunk-PR6TB3BL.js';
2
2
  import { undefinedOnly, unchecked } from './chunk-4LPY67E4.js';
3
3
  import { queueAdapter } from './chunk-TXN2KFHF.js';
4
- import { Cmd } from './chunk-AIIXV4XB.js';
4
+ import { Cmd } from './chunk-GAVLND5C.js';
5
5
 
6
6
  // src/internal/idempotency/idempotent-intake/index.ts
7
7
  function intakeProcessDef() {
@@ -102,5 +102,5 @@ function createIntake(config) {
102
102
  }
103
103
 
104
104
  export { createIntake, intakeProcessDef, intakeReplayDef };
105
- //# sourceMappingURL=chunk-JYNYG5SF.js.map
106
- //# sourceMappingURL=chunk-JYNYG5SF.js.map
105
+ //# sourceMappingURL=chunk-YCJPLPQQ.js.map
106
+ //# sourceMappingURL=chunk-YCJPLPQQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/idempotency/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-YCJPLPQQ.js","sourcesContent":["/**\n * internal/idempotency/idempotent-intake — receive-once intake for webhooks / queue\n * messages: dedupe by key, enqueue the new ones, replay the cached result to\n * the duplicates. The \"process this payload exactly once even though it\n * arrives N times\" knob.\n *\n * **The recipe.** This is a Level-2 composition — it owns no primitive\n * queue/cache logic of its own, it *splices two L1 bricks into one Model slice*\n * and DELEGATES every transition to their VERB SEAMS — `IdempotencyMemory`\n * (`../idempotency/adapter`) and `QueueAdapter` (`../work-queue/adapter`) —\n * so the store ops and the `QueueItem` / `IdempotencyStore` shapes stay\n * insulated behind verbs the intake calls:\n *\n * - `../idempotency/adapter` — the dedupe-by-key + last-result cache. The\n * `seen` half of the slice is its `IdempotencyStore`, keyed by\n * `keyOf(payload)`. A key is recorded once `receive` calls `memory.remember`;\n * `complete` calls `memory.remember` again to swap its cached value from\n * `pending` to the finished `result`. The cache write is the brick's, never\n * re-rolled here.\n * - `../work-queue/adapter` — the pending-work list. The `queue` half of the\n * slice is its `QueueItem<P>[]`. Every status flip routes through the\n * adapter verbs: `receive` enqueues via `queue.enqueue`, `claimNext` stamps\n * the `running` claim via `queue.patch`, `boot` re-arms a dead worker's\n * claim via `queue.patch`. Intake owns only the SELECTION (which item,\n * gated on the done-cache reconcile below) — the array transition is the\n * brick's. The consumer drains it (claim → process → `complete`) at its own\n * pace.\n *\n * Both bricks are pure-state + pure-ops, host- and clock-agnostic by\n * construction, so the combined slice inherits TEA's two non-negotiables:\n *\n * - **Durable** — the slice is a plain serializable value (a record-backed\n * idempotency store + an array of queue items, no `Map`, no closures), so\n * it survives a Durable-Object eviction / reload and rehydrates whole.\n * - **Replayable** — every verb is a pure `(state, …) => [state, Cmd[]]`\n * transition; no `Date.now()`, no `Math.random()`, no I/O inside a verb.\n * `at` (the clock) and `id` (the queue-item id) are *injected* at the call\n * site, so `trace-replay` reconstructs each transition exactly.\n *\n * **The decision lives in a Cmd, not in a return shape.** `receive` cannot\n * \"return\" what it decided (the knob contract pins the shape to\n * `[State, Cmd[]]`), so the decision is *reified as a Cmd* the runtime\n * performs:\n *\n * - first sighting of a key → mark it `pending`, enqueue the payload, emit\n * `IntakeProcess` (go run the work).\n * - a key already `pending` → drop silently (the original is in flight);\n * emit nothing.\n * - a key already `done` → emit `IntakeReplay` carrying the cached\n * result so the duplicate caller gets the SAME response the first one\n * produced, with zero re-execution of the side effect.\n *\n * **Two crash-safety guarantees the verbs enforce (and the unit specs alone\n * missed — see the wired-machine test):**\n *\n * - *A slow worker is never double-enqueued.* The TTL clock starts at\n * `complete`, not at `receive`: a `pending` entry NEVER ages out, so a\n * duplicate of an in-flight key is always dropped no matter how long the\n * worker takes. Only a `done` entry expires (see `liveEntry`).\n * - *A complete-then-crash never re-runs.* `boot` and `claimNext` reconcile\n * the queue against the done cache: a queue item whose key already resolved\n * to `done` in `seen` is skipped (never reset to pending, never claimed),\n * so a worker that finished and cached its result before the DO evicted is\n * not re-executed on resume.\n *\n * Typical wiring (the combinator form — splice ~3 hooks into a host machine):\n *\n * const intake = createIntake<StripeEvent, ChargeResult>({\n * keyOf: (e) => e.id, // Stripe's idempotency key\n * ttlMs: 24 * 60 * 60 * 1000, // a replayed webhook 24h after it COMPLETED is re-run\n * });\n *\n * // in `init`:\n * intake: intake.init(),\n * // in `update`, on the inbound-webhook Msg:\n * const [next, cmds] = intake.receive(state.intake, payload, at, id);\n * // …route `IntakeProcess` to your worker, `IntakeReplay` to the responder.\n * // on the worker's done Msg:\n * const [next, _] = intake.complete(state.intake, key, result, at);\n * // once, after boot (DO reload), before resuming the drain:\n * const [next, _] = intake.boot(state.intake);\n *\n * Escape hatch: reach the raw L1 ops via `../idempotency` and `../../work-queue`\n * and thread them by hand.\n */\n\nimport { Cmd, type CmdOf } from \"../../../index\";\nimport { unchecked, undefinedOnly } from \"../../schema\";\nimport type { QueueItem } from \"../../work-queue\";\nimport { queueAdapter } from \"../../work-queue/adapter\";\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\n\n/**\n * A cached intake entry: either a key whose work is still in flight\n * (`pending`) or one whose work has finished and whose `result` is held for\n * replay to later duplicates (`done`). Discriminated on `phase` — `pending`\n * carries no payload, `done` carries the result `R`.\n *\n * Lives inside the `seen` half of the slice (`IdempotencyStore<IntakeEntry<R>>`).\n * It is the dedupe ledger's *value*; the ledger's *key* is `keyOf(payload)`.\n */\nexport type IntakeEntry<R> =\n | { readonly phase: \"pending\" }\n | { readonly phase: \"done\"; readonly result: R };\n\n/**\n * The intake knob's config — `keyOf` derives the idempotency key from a\n * payload, `ttlMs` (optional) ages a *completed* cached key out so a webhook\n * replayed long after the work finished is treated as new and re-processed.\n *\n * **The TTL clock starts at `complete`, not at `receive`.** A `pending` entry\n * (work still in flight) NEVER ages out, no matter how slow the worker is —\n * ageing a pending key would let the duplicate re-enqueue the in-flight\n * payload and double-process it. Only a `done` entry expires, measured from\n * the `at` passed to `complete` (the moment the result was cached). Inside the\n * window a duplicate replays the cached result; at/after the window it is\n * re-accepted and re-processed.\n *\n * Omit `ttlMs` for a cache whose `done` entries never expire by age (keys live\n * forever — fine for a bounded universe of ids, unbounded growth otherwise).\n *\n * The TTL is owned HERE, not delegated to the composed `IdempotencyStore`: the\n * store's uniform per-entry TTL cannot tell `pending` from `done`, so intake\n * keeps the store TTL-free and applies the phase-aware rule itself (see\n * `liveEntry`).\n */\nexport interface IntakeConfig<P> {\n /** Derive the idempotency key from a payload. Must be pure + deterministic. */\n readonly keyOf: (payload: P) => string;\n /**\n * Age a *completed* key out after this many ms, measured from the `at`\n * passed to `complete`. Pending keys never age out.\n */\n readonly ttlMs?: number;\n}\n\n/**\n * The Model slice this knob owns. `seen` is the dedupe + last-result cache\n * (the `../idempotency` brick); `queue` is the pending-work list (the\n * `../work-queue` brick). Both are plain serializable values — the whole slice\n * round-trips through `JSON` so it survives eviction and replays exactly.\n */\nexport interface IntakeState<P, R> {\n readonly seen: IdempotencyStore<IntakeEntry<R>>;\n readonly queue: readonly QueueItem<P>[];\n}\n\n// === Cmds the verbs reify ===\n//\n// The knob contract pins verbs to `(state, …) => [State, Cmd[]]`, so a verb's\n// *decision* (new vs. duplicate-pending vs. duplicate-done) cannot be a return\n// value — it is emitted as a Cmd the runtime's interpret handler performs.\n// Each Cmd is plain data (invariant 1): the payload / key / result it carries\n// are values, never closures.\n//\n// Both are `Cmd.define`d (ADR 0014). The payload `P` / result `R` are the\n// knob's own type parameters, so each def is minted per knob by a factory\n// (`createIntake` calls it once) rather than a module-scope constant — the\n// same shape `resilient-call`'s `runCmdDef<I, R>()` takes. The host performs\n// them and settles nothing back through intake, hence `ok: undefinedOnly` and an\n// empty `err` list.\n\n/** A genuinely-new payload was accepted and enqueued — go run the work. */\nexport function intakeProcessDef<P>() {\n return Cmd.define(\"intake:process\", {\n input: unchecked<{\n /** The dedupe key the payload was filed under. */\n readonly key: string;\n /** The enqueued queue-item id (the same `id` passed to `receive`). */\n readonly itemId: string;\n /** The original payload, for the worker to process. */\n readonly payload: P;\n }>(),\n ok: undefinedOnly,\n err: [],\n });\n}\n/**\n * The Cmd intake emits for a first-time request: process `payload` for the\n * queue item `itemId`.\n */\nexport type IntakeProcessCmd<P> = CmdOf<ReturnType<typeof intakeProcessDef<P>>>;\n\n/**\n * A duplicate of an already-completed key arrived — replay the cached result\n * to the caller instead of re-running the side effect.\n */\nexport function intakeReplayDef<R>() {\n return Cmd.define(\"intake:replay\", {\n input: unchecked<{\n /** The dedupe key whose cached result is being replayed. */\n readonly key: string;\n /** The result the ORIGINAL request produced, recalled from the cache. */\n readonly result: R;\n }>(),\n ok: undefinedOnly,\n err: [],\n });\n}\n/**\n * The Cmd intake emits for a duplicate of a completed request: hand back the\n * cached `result` instead of redoing the work.\n */\nexport type IntakeReplayCmd<R> = CmdOf<ReturnType<typeof intakeReplayDef<R>>>;\n\n/** The Cmd union the intake verbs emit. */\nexport type IntakeCmd<P, R> = IntakeProcessCmd<P> | IntakeReplayCmd<R>;\n\n/**\n * Build an idempotent-intake knob from `config`. Returns the uniform L2\n * contract — `init()` + the verbs (`receive`, `complete`, `boot`, `claimNext`).\n * No `subs` / `handlers`: intake owns no timers and does no I/O of its own (the\n * composed bricks are pure-ops; the worker that runs `IntakeProcess` and the\n * responder that runs `IntakeReplay` live in the host's interpret map).\n */\nexport function createIntake<P, R>(config: IntakeConfig<P>) {\n const { keyOf, ttlMs } = config;\n\n // The two store seams, bound once for this knob's `P`/`R`. The intake\n // delegates every queue status flip to `queue.*` and every dedupe-cache\n // transition to `memory.*`, so the `QueueItem<P>` / `IdempotencyStore` shapes\n // are insulated behind the verbs rather than reached by raw ops.\n const queue = queueAdapter<P>();\n const memory = idempotencyMemory<IntakeEntry<R>>();\n\n // The two Cmd constructors, minted once for this knob's `P`/`R`.\n const intakeProcess = intakeProcessDef<P>();\n const intakeReplay = intakeReplayDef<R>();\n\n /**\n * The empty slice: a TTL-free idempotency store (intake owns the TTL itself —\n * see `liveEntry`, because the store cannot tell `pending` from `done`) plus\n * an empty work queue. The rehydrate path is the host's `init(loaded)` — it\n * passes the persisted slice straight back, this is only the fresh-boot value.\n */\n function init(): IntakeState<P, R> {\n return {\n seen: memory.init(),\n queue: [],\n };\n }\n\n /**\n * The live cached entry for `key` at clock reading `at`, or `undefined` if\n * the key is absent or its `done` window has elapsed. This is intake's\n * phase-aware expiry rule, applied HERE rather than by the store's uniform\n * per-entry TTL:\n *\n * - **`pending`** — work is in flight; NEVER expires. Ageing a pending key\n * would let a slow worker's payload be re-enqueued and double-processed.\n * - **`done`** — expires `ttlMs` after the `atMs` the result was cached at\n * (the `at` passed to `complete`). Half-open, matching the store /\n * rate-limit window edge: `at - atMs >= ttlMs` reads as expired. No\n * `ttlMs` ⇒ a `done` entry never expires by age.\n *\n * PURE — reads `state.seen.entries` directly (no `Date.now`), never mutates.\n */\n function liveEntry(\n store: IdempotencyStore<IntakeEntry<R>>,\n key: string,\n at: number,\n ): IntakeEntry<R> | undefined {\n const cached = store.entries[key];\n if (cached === undefined) return undefined;\n const entry = cached.value;\n if (entry.phase === \"pending\") return entry; // in flight — never ages out\n if (ttlMs !== undefined && at - cached.atMs >= ttlMs) return undefined;\n return entry;\n }\n\n /**\n * Whether the queue item `it`'s key has already resolved to a live `done`\n * entry in `seen` (the work completed and its result is cached). Such an\n * item must NOT be re-armed by `boot` or claimed by `claimNext` — doing so\n * re-runs work whose result is already known (the complete-then-crash bug).\n *\n * `at = +Infinity` is the right reading for the reconcile check: a `done`\n * entry counts as done for reconciliation regardless of how far past its\n * replay TTL it is. (TTL governs whether a *duplicate receive* re-processes,\n * not whether a *queued, already-completed* item should re-run — that one\n * never should.)\n */\n function isAlreadyDone(state: IntakeState<P, R>, it: QueueItem<P>): boolean {\n return state.seen.entries[keyOf(it.input)]?.value.phase === \"done\";\n }\n\n /**\n * Receive a payload at clock reading `at`, filing it under the queue-item\n * id `id`. PURE — returns a NEW slice; the input is untouched.\n *\n * Three outcomes, each reified as a Cmd (see the Cmd union above):\n *\n * 1. **New key** (`liveEntry` is absent at `at`) — `remember` the key as\n * `pending`, `enqueue` the payload onto the work queue, and emit\n * `intake:process`. The only path that grows either half of the slice.\n * 2. **Duplicate, still pending** — the original is in flight. Drop\n * silently: the slice is returned unchanged and no Cmd is emitted, so\n * the duplicate neither re-enqueues nor double-processes. A pending\n * entry never ages out, so this holds no matter how slow the worker is.\n * 3. **Duplicate, already done (within TTL)** — emit `intake:replay`\n * carrying the cached `result` so the caller gets the original response.\n * The slice is unchanged (a replay touches neither cache nor queue). A\n * `done` key past its `ttlMs` window reads as absent and falls to (1).\n *\n * `id` is injected (work-queue's `genId` reads `Date.now()` / `crypto`, so\n * it cannot be called inside a pure verb). Generate it at the effect\n * boundary and thread it in; it becomes the enqueued item's id AND the\n * `itemId` on the emitted `intake:process` Cmd, so the worker can address the\n * exact queue item it must `complete`.\n */\n function receive(\n state: IntakeState<P, R>,\n payload: P,\n at: number,\n id: string,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const key = keyOf(payload);\n\n // Duplicate path — the key has a live entry at `at` (pending never ages;\n // done ages only past `ttlMs` measured from `complete`).\n const entry = liveEntry(state.seen, key, at);\n if (entry !== undefined) {\n if (entry.phase === \"done\") {\n // Replay the original result; touch nothing.\n return [state, [intakeReplay({ key, result: entry.result })]];\n }\n // phase === \"pending\": original is in flight — drop silently.\n return [state, []];\n }\n\n // New path — record the key as pending, enqueue the payload, process it.\n const seenNext = memory.remember(state.seen, key, { phase: \"pending\" }, at);\n const { next: queueNext, item } = queue.enqueue(\n state.queue,\n payload,\n at,\n id,\n );\n const nextState: IntakeState<P, R> = { seen: seenNext, queue: queueNext };\n return [nextState, [intakeProcess({ key, itemId: item.id, payload })]];\n }\n\n /**\n * Record the finished `result` for `key` at clock reading `at`. PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds (completion is\n * a pure cache write; the host marks the queue item done via the work-queue\n * adapter at the effect boundary).\n *\n * Swaps the key's cached entry from `pending` to `done` so every subsequent\n * `receive` of the same key replays this `result`. `remember` stamps the\n * entry's `atMs` to `at`, which is the moment intake's `done`-TTL clock\n * starts (see `liveEntry`): a completed key stays replayable for `ttlMs`\n * past completion, not past first receipt.\n *\n * `complete` does NOT remove the queue item — the queue is the host's drain\n * ledger, and which items leave it (and when) is the host's call via the\n * work-queue adapter. `complete` owns the cache half only.\n */\n function complete(\n state: IntakeState<P, R>,\n key: string,\n result: R,\n at: number,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const seenNext = memory.remember(\n state.seen,\n key,\n { phase: \"done\", result },\n at,\n );\n return [{ ...state, seen: seenNext }, []];\n }\n\n /**\n * Resume after a boot (Durable-Object reload / process restart). PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds.\n *\n * Resets every `running` queue item back to `pending` so the host's drain\n * re-claims it — EXCEPT an item whose key already resolved to `done` in\n * `seen`. That item's worker finished and cached its result before the crash;\n * re-arming it would re-run completed work (the complete-then-crash bug). It\n * is left as-is (and `claimNext` will likewise skip it), so the host drains\n * it via `markDone` without ever re-executing the side effect.\n *\n * A `running` item still `pending` in the cache is one a now-dead worker\n * claimed but never finished — that one IS reset, so it gets re-claimed.\n *\n * Returns the same reference when nothing changed (no running item needed\n * re-arming) so the host can skip a redundant save.\n *\n * Call once from a `boot` Msg the host dispatches after `run(...)` returns —\n * never from `init`'s rehydrate branch (that branch must be a pure\n * passthrough; see the substrate's `init` contract).\n */\n function boot(\n state: IntakeState<P, R>,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n // Selection lives HERE (the done-cache reconcile is intake's own rule); the\n // status FLIP is delegated to the `QueueAdapter`. A blanket reset would\n // re-arm completed-then-crashed items (it cannot see the done cache), so\n // intake picks the items and `queue.patch` performs each transition — the\n // same spread the work-queue lifecycle uses, not a re-rolled one.\n let items = state.queue;\n let changed = false;\n for (const it of state.queue) {\n if (it.status !== \"running\") continue;\n // Reconcile against the done cache: a completed-then-crashed item must\n // NOT be re-armed (it would re-run already-cached work).\n if (isAlreadyDone(state, it)) continue;\n const flip = queue.patch(items, it.id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n }));\n items = flip.next;\n changed = changed || flip.changed;\n }\n if (!changed) return [state, []];\n return [{ ...state, queue: items }, []];\n }\n\n /**\n * Claim the next pending queue item, flipping it `running` and stamping\n * `startedAt` to `at`. PURE — returns a NEW slice plus the claimed item, or\n * `null` if nothing claimable remains.\n *\n * \"Claimable\" is a pending item WHOSE KEY IS NOT ALREADY `done` in `seen`.\n * An item whose key completed before a crash (its `intake:process` ran, work\n * finished, result cached, but the item never left the queue) is skipped —\n * claiming it would re-run already-cached work. Such items are inert here;\n * the host drains them via `markDone`.\n *\n * This is the drain side of the intake, the mirror of `receive`'s enqueue.\n * It is a pure helper (not a verb that emits Cmds) because \"what to do with\n * the claimed item\" is the host's decision — typically: dispatch the worker,\n * await it, then call `complete`. Threading it through the slice keeps the\n * claim durable: the `running` stamp survives eviction, and `boot` re-arms\n * it if the worker died.\n */\n function claimNext(\n state: IntakeState<P, R>,\n at: number,\n ): {\n readonly state: IntakeState<P, R>;\n readonly claimed: QueueItem<P>;\n } | null {\n // Selection lives HERE (skip an item whose key already resolved `done`);\n // the status FLIP is delegated to the `QueueAdapter`. A blind `queue.claim`\n // would take the first PENDING item regardless of the done cache, which\n // would re-run already-cached work — so intake selects the eligible item and\n // `queue.patch` performs the running-stamp transition (the work-queue\n // lifecycle's own spread, not a re-rolled one).\n const target = state.queue.find(\n (it) => it.status === \"pending\" && !isAlreadyDone(state, it),\n );\n if (target === undefined) return null;\n // The patch closure is the single place the claimed item is materialized;\n // capture it here so the result needs no re-scan of the new queue.\n let claimed: QueueItem<P> = target;\n const { next } = queue.patch(state.queue, target.id, (item) => {\n claimed = { ...item, status: \"running\", startedAt: at };\n return claimed;\n });\n return { state: { ...state, queue: next }, claimed };\n }\n\n return { init, receive, complete, boot, claimNext };\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { undefinedOnly, unchecked } from './chunk-4LPY67E4.js';
2
- import { Cmd } from './chunk-AIIXV4XB.js';
2
+ import { Cmd } from './chunk-GAVLND5C.js';
3
3
 
4
4
  // src/internal/persistence/snapshot/index.ts
5
5
  var DEFAULT_SNAPSHOT_KEY = "@@snapshot";
@@ -60,5 +60,5 @@ function createSnapshot(config) {
60
60
  }
61
61
 
62
62
  export { createSnapshot, snapshotLoadDef, snapshotWriteDef };
63
- //# sourceMappingURL=chunk-YIMOFXST.js.map
64
- //# sourceMappingURL=chunk-YIMOFXST.js.map
63
+ //# sourceMappingURL=chunk-YDF6P74H.js.map
64
+ //# sourceMappingURL=chunk-YDF6P74H.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/persistence/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-YDF6P74H.js","sourcesContent":["/**\n * internal/persistence/snapshot — periodic state checkpoint to a host store\n * (R2/KV-shaped). Internal since #49 — not published on any subpath.\n *\n * The problem it solves: a long-running machine (a multi-stage run, a paginated\n * crawl, a saga) lives in TEA's in-memory `State` and is persisted as ONE blob\n * by the substrate's `Store<S>` on every transition. That blob is fine for\n * crash recovery of the *runtime* — but it is NOT a coarse, named checkpoint a\n * human (or a separate recovery job) can point a restart at. `snapshot` adds\n * the \"every N units of progress, write a durable checkpoint to R2/KV\"\n * discipline on top, so a crashed long run restarts from the last checkpoint\n * instead of from zero (the field-guide pain row).\n *\n * Shape: `createSnapshot(config)` hands back plain functions and two\n * `Cmd.define`d Cmds (`write`, `load`) you call from your own `update` (ADR\n * 0022). The state it owns is a Model slice (durable + replayable, the two\n * non-negotiables); the verbs are PURE decisions; the I/O (the actual `put` /\n * `get` against your store) lives only in the two handlers you write (ADR 0021).\n *\n * It is a LEAF module: it imports only the core substrate (`../index`, for\n * `Cmd`) 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 is the handler's business, never this module's — so it never\n * imports `@cloudflare/workers-types` and can be checkpointed to anything (R2,\n * KV, a DO sub-key, an in-memory map in tests).\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 * cmds: [snap.write, snap.load],\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_write_ok: (s, msg) => {\n * const [snapshot, cmds] = snap.confirm(s.snapshot, msg);\n * return [{ ...s, snapshot }, cmds];\n * },\n * snapshot_write_err: (s) => [s, []], // errors are data — decide here\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_load_ok: (s, msg) => {\n * if (msg.value === null) return [s, []]; // no checkpoint → keep fresh init\n * const [snapshot, cmds] = snap.boot(s.snapshot);\n * return [{ ...s, run: msg.value, snapshot }, cmds];\n * },\n * snapshot_load_err: (s) => [s, []], // errors are data — decide here\n *\n * // and where the machine runs, the two handlers you write:\n * run(machine, {\n * interpret: {\n * snapshot_write: async (cmd, { ok, err }) => {\n * try { await r2.put(cmd.key, cmd.payload); return ok(undefined); }\n * catch (cause) { return err({ _tag: \"snapshot_write_failed\", cause }); }\n * },\n * snapshot_load: async (cmd, { ok, err }) => {\n * try { return ok(await r2.get(cmd.key)); }\n * catch (cause) { return err({ _tag: \"snapshot_load_failed\", cause }); }\n * },\n * },\n * });\n */\n\nimport {\n Cmd,\n type CmdOf,\n type SettledErr,\n type SettledOk,\n type TaggedError,\n} from \"../../../index\";\nimport { unchecked, undefinedOnly } from \"../../schema\";\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// Cmds + Msgs — the write effect, the read effect, and their settled Msgs.\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.\n *\n * `payload` is `V` — the consumer's serializable checkpoint value (the run\n * state, the cursor, whatever the machine wants restartable). The verb takes it\n * as an argument so the snapshot slice never has to hold a copy of the\n * machine's domain state.\n *\n * `Cmd.define`d (ADR 0014, 0021), minted per `V` by a factory the way\n * `workflowActivityDef<A>()` is, since the payload type is the knob's own\n * parameter. The handler you write puts the payload in your store and returns\n * `ok(undefined)`, or `err({ _tag: \"snapshot_write_failed\", … })`; the engine\n * mints `snapshot_write_ok` / `snapshot_write_err`.\n */\nexport function snapshotWriteDef<V>() {\n return Cmd.define(\"snapshot_write\", {\n input: unchecked<{\n readonly key: string;\n readonly seq: number;\n readonly at: number;\n readonly payload: V;\n }>(),\n ok: undefinedOnly,\n err: [\"snapshot_write_failed\"],\n });\n}\n/**\n * The Cmd a snapshot decision emits to write a checkpoint: `payload` under\n * `key`, at sequence `seq`.\n */\nexport type SnapshotWriteCmd<V> = CmdOf<ReturnType<typeof snapshotWriteDef<V>>>;\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 you write reads the store and returns `ok(payload)` — `null` when\n * the store has no checkpoint under `key` — or\n * `err({ _tag: \"snapshot_load_failed\", … })`.\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 function snapshotLoadDef<V>() {\n return Cmd.define(\"snapshot_load\", {\n input: unchecked<{ readonly key: string }>(),\n ok: unchecked<V | null>(),\n err: [\"snapshot_load_failed\"],\n });\n}\n/** The Cmd `requestLoad` emits to read the checkpoint saved under `key`. */\nexport type SnapshotLoadCmd<V = unknown> = CmdOf<\n ReturnType<typeof snapshotLoadDef<V>>\n>;\n\n/**\n * The Msg the engine mints when a write lands. Fold it through\n * `confirm(state, msg)` to advance `lastSavedSeq` / `lastSavedAt`. It carries\n * the write Cmd, so `msg.cmd.seq` matches the emitting decision and a late\n * acknowledgement of an older write can never regress a newer confirmed one.\n */\nexport type SnapshotSavedMsg<V = unknown> = SettledOk<\n \"snapshot_write\",\n SnapshotWriteCmd<V>,\n undefined\n>;\n\n/**\n * The Msg the engine mints when a write fails. Errors are data (never\n * swallowed): the consumer's reducer decides whether a failed checkpoint is\n * fatal, retryable (compose `@demlik/tea/retry-backoff`), or merely logged.\n * `msg.cmd.seq` identifies which checkpoint failed.\n */\nexport type SnapshotFailedMsg<V = unknown> = SettledErr<\n \"snapshot_write\",\n SnapshotWriteCmd<V>,\n TaggedError<\"snapshot_write_failed\">\n>;\n\n/**\n * The Msg the engine mints when a read resolves. `value` is the recovered\n * checkpoint, or `null` when the store has no checkpoint under `key` (fresh\n * boot — never checkpointed, or a wiped store). `null` is NOT an error — \"no\n * checkpoint yet\" is a normal first-run state, distinct from \"the read failed\"\n * (`snapshot_load_err`).\n */\nexport type SnapshotLoadedMsg<V> = SettledOk<\n \"snapshot_load\",\n SnapshotLoadCmd<V>,\n V | null\n>;\n\n/**\n * The Msg the engine mints when a read fails. Errors are data: a failed\n * recovery read is a visible signal the consumer's reducer decides on\n * (fatal-abort, retry, or boot-fresh-and-log). Kept apart from a `null` load\n * so \"the store has no checkpoint\" can never masquerade as \"the read failed\".\n */\nexport type SnapshotLoadFailedMsg<V = unknown> = SettledErr<\n \"snapshot_load\",\n SnapshotLoadCmd<V>,\n TaggedError<\"snapshot_load_failed\">\n>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The combinator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The bundle `createSnapshot` returns: plain functions plus the two Cmd defs to\n * list in `cmds`. There are no subs: a checkpoint is driven by progress (a\n * `record` call from the consumer's reducer), not by a timer.\n */\nexport interface SnapshotKnob<V> {\n /** The `snapshot_write` Cmd def — list it in the machine's `cmds`. */\n readonly write: ReturnType<typeof snapshotWriteDef<V>>;\n /** The `snapshot_load` Cmd def — list it in the machine's `cmds`. */\n readonly load: ReturnType<typeof snapshotLoadDef<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 SnapshotWriteCmd<V>[]];\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 SnapshotWriteCmd<V>[]];\n /**\n * Record a confirmed durable write. PURE. Advances `lastSavedSeq` /\n * `lastSavedAt` to the acknowledged write's `seq` / `at`, but only forward:\n * an out-of-order acknowledgement of an OLDER `seq` (a slow write landing\n * after a newer one) is ignored, so the confirmed watermark is monotonic.\n * Fold the `snapshot_write_ok` 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_write_ok` cell.\n */\n confirm(\n state: SnapshotState,\n msg: SnapshotSavedMsg<V>,\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, so the cadence counter starts\n * fresh from there. `seq` / `lastSavedSeq` / `lastSavedAt` are preserved\n * (they describe what is durable, which a reload does not change). Call from\n * a `boot` Msg the host dispatches once after `run(...)`, never from `init`'s\n * rehydrate branch (invariant 2).\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — a resume emits nothing) so it drops straight into a\n * `snapshot_load_ok` cell.\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_load_ok` 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(...)` and folds `requestLoad` there.\n */\n requestLoad(\n state: SnapshotState,\n ): readonly [SnapshotState, readonly SnapshotLoadCmd<V>[]];\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 `write` / `load` are the Cmd defs whose handlers you write.\n */\nexport function createSnapshot<V>(config: SnapshotConfig): SnapshotKnob<V> {\n // Clamp the cadence to >= 1 at construction so `record`'s threshold check is\n // branchless and a misconfigured `0` / negative `every` checkpoints on every\n // progress unit instead of never.\n const every = config.every >= 1 ? config.every : 1;\n const key = config.key ?? DEFAULT_SNAPSHOT_KEY;\n const snapshotWrite = snapshotWriteDef<V>();\n const snapshotLoad = snapshotLoadDef<V>();\n\n /**\n * Emit the write Cmd for a checkpoint decision. Shared by `record`'s\n * cadence-hit branch and `force`. Bumps `seq`, resets `sinceLast`, returns the\n * new slice + the single `snapshot_write` Cmd. The input slice is never\n * mutated (invariant 1).\n */\n function emitWrite(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly SnapshotWriteCmd<V>[]] {\n const seq = state.seq + 1;\n const next: SnapshotState = { ...state, sinceLast: 0, seq };\n return [next, [snapshotWrite({ key, seq, at, payload })]];\n }\n\n return {\n write: snapshotWrite,\n load: snapshotLoad,\n\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 }, []];\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 const { seq, at } = msg.cmd;\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 && seq <= state.lastSavedSeq) {\n return [state, Cmd.none];\n }\n return [{ ...state, lastSavedSeq: seq, lastSavedAt: at }, Cmd.none];\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_load_ok` (the consumer's reducer), not here.\n return [state, [snapshotLoad({ key })]];\n },\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { StoreRefusedError, describeError, Refusal } from './chunk-AIIXV4XB.js';
1
+ import { StoreRefusedError, describeError, Refusal } from './chunk-GAVLND5C.js';
2
2
 
3
3
  // src/internal/engine/restore.ts
4
4
  async function restore(store) {
@@ -17,5 +17,5 @@ async function restore(store) {
17
17
  }
18
18
 
19
19
  export { restore };
20
- //# sourceMappingURL=chunk-JVE2LBFJ.js.map
21
- //# sourceMappingURL=chunk-JVE2LBFJ.js.map
20
+ //# sourceMappingURL=chunk-Z7IL735U.js.map
21
+ //# sourceMappingURL=chunk-Z7IL735U.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/engine/restore.ts"],"names":[],"mappings":";;;AAeA,eAAsB,QAAW,KAAA,EAAoC;AACnE,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,KAAA,CAAM,OAAA,CAAQ,MAAM,KAAA,CAAM,MAAM,CAAA;AAAA,EAC7C,SAAS,KAAA,EAAO;AACd,IAAA,IAAI,KAAA,YAAiB,mBAAmB,MAAM,KAAA;AAC9C,IAAA,MAAM,IAAI,iBAAA;AAAA,MACR,CAAA,mCAAA,EAAsC,aAAA,CAAc,KAAK,CAAC,CAAA,CAAA,CAAA;AAAA,MAC1D,EAAE,KAAA;AAAM,KACV;AAAA,EACF;AACA,EAAA,IAAI,oBAAoB,OAAA,EAAS,MAAM,IAAI,iBAAA,CAAkB,SAAS,MAAM,CAAA;AAC5E,EAAA,OAAO,QAAA;AACT","file":"chunk-JVE2LBFJ.js","sourcesContent":["import { describeError } from \"../../describe-error\";\nimport {\n type Migrated,\n Refusal,\n type Store,\n StoreRefusedError,\n} from \"../../runtime-types\";\n\n/**\n * Read a store's saved state through its `migrate`: the `S` it holds, or `null`\n * when nothing was saved. Throws {@link StoreRefusedError} when `migrate`\n * refuses, and when `load` or `migrate` throws (#316) — the caller then writes\n * nothing, so bytes it could not read are never overwritten. Boot and the work\n * queue both read a store through this.\n */\nexport async function restore<S>(store: Store<S>): Promise<S | null> {\n let migrated: Migrated<S>;\n try {\n migrated = store.migrate(await store.load());\n } catch (cause) {\n if (cause instanceof StoreRefusedError) throw cause;\n throw new StoreRefusedError(\n `the saved state could not be read (${describeError(cause)})`,\n { cause },\n );\n }\n if (migrated instanceof Refusal) throw new StoreRefusedError(migrated.reason);\n return migrated;\n}\n"]}
1
+ {"version":3,"sources":["../src/internal/engine/restore.ts"],"names":[],"mappings":";;;AAeA,eAAsB,QAAW,KAAA,EAAoC;AACnE,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,KAAA,CAAM,OAAA,CAAQ,MAAM,KAAA,CAAM,MAAM,CAAA;AAAA,EAC7C,SAAS,KAAA,EAAO;AACd,IAAA,IAAI,KAAA,YAAiB,mBAAmB,MAAM,KAAA;AAC9C,IAAA,MAAM,IAAI,iBAAA;AAAA,MACR,CAAA,mCAAA,EAAsC,aAAA,CAAc,KAAK,CAAC,CAAA,CAAA,CAAA;AAAA,MAC1D,EAAE,KAAA;AAAM,KACV;AAAA,EACF;AACA,EAAA,IAAI,oBAAoB,OAAA,EAAS,MAAM,IAAI,iBAAA,CAAkB,SAAS,MAAM,CAAA;AAC5E,EAAA,OAAO,QAAA;AACT","file":"chunk-Z7IL735U.js","sourcesContent":["import { describeError } from \"../../describe-error\";\nimport {\n type Migrated,\n Refusal,\n type Store,\n StoreRefusedError,\n} from \"../../runtime-types\";\n\n/**\n * Read a store's saved state through its `migrate`: the `S` it holds, or `null`\n * when nothing was saved. Throws {@link StoreRefusedError} when `migrate`\n * refuses, and when `load` or `migrate` throws (#316) — the caller then writes\n * nothing, so bytes it could not read are never overwritten. Boot and the work\n * queue both read a store through this.\n */\nexport async function restore<S>(store: Store<S>): Promise<S | null> {\n let migrated: Migrated<S>;\n try {\n migrated = store.migrate(await store.load());\n } catch (cause) {\n if (cause instanceof StoreRefusedError) throw cause;\n throw new StoreRefusedError(\n `the saved state could not be read (${describeError(cause)})`,\n { cause },\n );\n }\n if (migrated instanceof Refusal) throw new StoreRefusedError(migrated.reason);\n return migrated;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { subscribeDeadline, deadlineExceeded, deadlineSub } from './chunk-C3G2QFEZ.js';
1
+ import { subscribeDeadline, deadlineExceeded, deadlineSub } from './chunk-QIE4Y6BK.js';
2
2
 
3
3
  // src/internal/flow/batch-window/index.ts
4
4
  function initBatchWindow() {
@@ -42,5 +42,5 @@ function createBatchWindow(config) {
42
42
  }
43
43
 
44
44
  export { addItem, batchWindowExpired, createBatchWindow, initBatchWindow, onWindow, subsFor, subscribeBatchWindow, timerFor };
45
- //# sourceMappingURL=chunk-PQSDUX6B.js.map
46
- //# sourceMappingURL=chunk-PQSDUX6B.js.map
45
+ //# sourceMappingURL=chunk-ZV4WGW74.js.map
46
+ //# sourceMappingURL=chunk-ZV4WGW74.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/flow/batch-window/index.ts"],"names":[],"mappings":";;;AA0IO,SAAS,eAAA,GAAqC;AACnD,EAAA,OAAO,EAAE,MAAA,EAAQ,EAAC,EAAG,UAAU,IAAA,EAAK;AACtC;AAqBO,SAAS,kBAAA,CACd,IACA,IAAA,EACoB;AACpB,EAAA,OAAO,gBAAA,CAAiB,IAAI,IAAI,CAAA;AAClC;AAmEO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,IAAA,EACA,EAAA,EACyC;AACzC,EAAA,MAAM,MAAA,GAAS,CAAC,GAAG,KAAA,CAAM,QAAQ,IAAI,CAAA;AAGrC,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,EAAA;AAInC,EAAA,IAAI,OAAO,MAAA,IAAU,IAAA,CAAK,IAAI,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAA,EAAG;AACjD,IAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,CAAC,EAAE,MAAA,EAAQ,QAAA,EAAS,EAAG,EAAE,CAAA;AAClC;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,GAAA,EACyC;AAGzC,EAAA,IAAI,KAAA,CAAM,OAAO,MAAA,KAAW,CAAA,SAAU,CAAC,KAAA,EAAO,EAAE,CAAA;AAChD,EAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAC5D;AAqBO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACsB;AAK3B,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,EAAC;AACrC,EAAA,OAAO,CAAC,WAAA,CAAY,EAAA,EAAI,MAAM,QAAA,GAAW,MAAA,CAAO,KAAK,CAAC,CAAA;AACxD;AAgBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACiC;AACtC,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,IAAA;AACpC,EAAA,OAAO;AAAA,IACL,IAAI,MAAA,CAAO,KAAA;AAAA,IACX,KAAK,kBAAA,CAAmB,EAAA,EAAI,KAAA,CAAM,QAAA,GAAW,OAAO,KAAK;AAAA,GAC3D;AACF;AASO,IAAM,oBAAA,GAIG;AAYT,SAAS,kBACd,MAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,MAAM,eAAA,EAAmB;AAAA,IAC/B,GAAA,EAAK,CAAC,KAAA,EAAO,IAAA,EAAM,OAAO,OAAA,CAAQ,MAAA,EAAQ,KAAA,EAAO,IAAA,EAAM,EAAE,CAAA;AAAA,IACzD,UAAU,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,KAAS,CAAA;AAAA,IACnD,OAAO,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,OAAO,EAAE,CAAA;AAAA,IAChD,MAAM,CAAC,KAAA,EAAO,OAAO,OAAA,CAAQ,MAAA,EAAQ,OAAO,EAAE;AAAA,GAChD;AACF","file":"chunk-PQSDUX6B.js","sourcesContent":["/**\n * internal/flow/batch-window — coalesce a stream of items into size- or\n * time-bounded BATCHES, then flush each batch as a single Cmd.\n *\n * What this module actually DELEGATES to, vs. what it merely RE-EXPORTS:\n *\n * - `../deadline` — REAL delegation (the only one). The window timer's Msg\n * is `deadlineExceeded`, keyed on the window's identity. `timerFor` arms it\n * through the engine's built-in `timer` Sub: `maxMs` counted from the\n * transition that opened the window, so a machine needs no runner. The\n * absolute-instant form is still here too — `subsFor` lists the same\n * deadline for `deadlinesSub` + `subscribeDeadline`, which re-arms for the\n * REMAINING time after a `Store` rehydrate rather than a fresh `maxMs`.\n * - `../work-queue` — TYPE re-export only, NOT a sink this module calls. A\n * flush emits a Cmd; the CONSUMER's interpret handler is what enqueues onto a\n * `createQueue(store)`. This module never touches queue status — it has no\n * queue state and no status flip to delegate. It re-exports `EnqueueInput<I>`\n * purely so the consumer's flush Cmd and queue agree on the item shape; any\n * actual status flip belongs in the consumer and is delegated to\n * `../work-queue/ops` there, not here.\n * - `../debounce` — VALUE/TYPE re-export only, NOT wired into `update`. A\n * bursty source (keystrokes, scroll, websocket frames) can be `debounce`d at\n * the host BEFORE it ever becomes an `add` Msg, so the batch window only sees\n * settled items. `debounce` is a sink transformer OUTSIDE `update`;\n * batch-window is the durable, replayable half inside it. Re-exported for\n * one-import ergonomics — this module does not call it.\n *\n * Why this is a knob and not hand-wired logic: the managed state (the buffer +\n * the window-open instant) lives in the Model, so it survives DO eviction /\n * reload (durable) and every transition is still a Msg through `update`\n * (replayable). A closure holding a `setTimeout` and a mutable array would break\n * both — banned. `add` / `onWindow` are PURE: time arrives as an `at`\n * parameter, never `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const bw = createBatchWindow<LogLine, Msg>({\n * maxItems: 100,\n * maxMs: 5_000,\n * flush: (items) => ({ type: \"FlushBatch\", items }),\n * });\n *\n * // Model: { batch: BatchWindow<LogLine>, ... }\n * // init: (loaded) => [loaded ?? { batch: bw.init(), ... }, []]\n * // update cells:\n * // LogArrived: (s, m) => mapBatch(s, bw.add(s.batch, m.line, m.at))\n * // deadline_exceeded: (s, m) => mapBatch(s, bw.onWindow(s.batch, m.atMs))\n * // subs: [{ type: \"timer\", deps: (s: Model) => bw.timer(s.batch) }]\n * // at run: run(machine, { interpret }) // `timer` is built in\n *\n * where `mapBatch(s, [batch, cmds]) => [{ ...s, batch }, cmds]` lifts the slice\n * transition into the consumer's Model. The `FlushBatch` Cmd's interpret handler\n * is the consumer's — it is where the batch meets I/O (an `enqueue`, a network\n * POST, an R2 write).\n *\n * Internal since #48 — not published on any subpath; reached from inside the\n * package as `internal/flow/batch-window`. Same one-shape-per-package rule as `deadline`, `cache`, and\n * `work-queue`.\n */\n\nimport type { Cmd, Subscribe, TimerDeps } from \"../../../index\";\n\n// `../debounce` re-export (value + type) for one-import ergonomics — see file\n// header. This module does NOT call debounce; a consumer that wants to debounce\n// a bursty source BEFORE it becomes an `add` Msg reaches it from the same import\n// as the batch window.\nexport { type Debounced, debounce } from \"../../timing/debounce\";\n\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n type DeadlinesSub,\n deadlineExceeded,\n deadlineSub,\n subscribeDeadline,\n} from \"../../resilience/deadline\";\n\n// `../work-queue` TYPE re-export only (see file header). This module emits a\n// flush Cmd; the CONSUMER's interpret handler enqueues onto a\n// `createQueue(store)`. Re-export the caller-facing enqueue payload type so the\n// consumer's flush Cmd and queue agree on the item shape.\nexport type { EnqueueInput } from \"../../work-queue\";\n\n/**\n * Configuration for a batch window — the knob. Pure config, no mutable state\n * (the `BatchWindow<I>` slice carries that). One config object is shared across\n * every window of one logical stream (often a module-scope const).\n *\n * `flush` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps the\n * buffered items to the single Cmd the runtime performs when a window closes.\n * Keeping it a `(items) => Cmd` (not a `(items) => void`) is what keeps the\n * trigger replayable — the flush is a Cmd in the transition's output, never a\n * side effect inside the verb.\n */\nexport interface BatchWindowConfig<I, C extends Cmd> {\n /**\n * Size trigger. A batch flushes the instant its buffer reaches `maxItems`,\n * WITHOUT waiting for the time window — the classic \"send as soon as the\n * page is full\" cutoff. Must be `>= 1`; a config of `maxItems: 0` would flush\n * an empty batch on every `add`, which is never useful, so callers treat `1`\n * as the floor (the verb still behaves: with `maxItems <= 1` every item\n * flushes on arrival, a degenerate but legal \"no batching\" window).\n */\n readonly maxItems: number;\n /**\n * Time trigger, in milliseconds. A batch flushes no later than `openedAt +\n * maxMs` — `openedAt` being the `at` of the item that opened the window. This\n * is the latency ceiling: an item never waits longer than `maxMs` to ship,\n * even if the buffer never reaches `maxItems`.\n */\n readonly maxMs: number;\n /**\n * Map a flushed batch to the single Cmd the runtime performs. Called with the\n * buffered items in arrival order; the returned Cmd is what `add` / `onWindow`\n * emit when a window closes. PURE constructor — no I/O, no clock; the I/O\n * happens later in the consumer's interpret handler for the returned Cmd.\n */\n readonly flush: (items: readonly I[]) => C;\n}\n\n/**\n * The Model slice a batch window owns. `buffer` is the items accumulated for the\n * current open window; `openedAt` is the `at` of the item that OPENED it (the\n * anchor the time trigger measures against), or `null` when the window is\n * closed (buffer empty). The pair is the single source of truth for \"is a\n * window open, and when must it flush by?\" — both plain data so the whole slice\n * is trivially JSON-serializable into a consumer's `Store<S>`.\n *\n * Invariant the verbs preserve: `buffer.length === 0` IFF `openedAt === null`.\n * A non-empty buffer always has an `openedAt`; an empty buffer never does. The\n * window timer Sub keys its arm/disarm off exactly this invariant.\n */\nexport interface BatchWindow<I> {\n readonly buffer: readonly I[];\n readonly openedAt: number | null;\n}\n\n/** The starting slice: closed window, empty buffer. */\nexport function initBatchWindow<I>(): BatchWindow<I> {\n return { buffer: [], openedAt: null };\n}\n\n/**\n * The deadline a batch window's open timer lists: it IS `../deadline`'s\n * `DeadlineSub`, not a re-tagged copy. `atMs` is the absolute flush-by instant\n * (`openedAt + maxMs`). Aliasing the deadline type (rather than redeclaring an\n * identical shape) is what lets `subsFor` return `deadlineSub(...)` with no\n * cast — the value's static type already satisfies this name. Consumers running\n * several windows route each one by the deadline's `id`, not by a distinct type.\n */\nexport type BatchWindowSub = DeadlineSub;\n\n/** The Msg a closed-by-time window dispatches. Alias of `deadline`'s Msg. */\nexport type BatchWindowExpired = DeadlineExceeded;\n\n/**\n * Construct the window-expired Msg for the window identified by `id`, flushing\n * by `atMs`. Re-exported alias of `deadlineExceeded` so the runner and\n * the consumer's reducer share one constructor rather than two literals that\n * can drift. The consumer handles it in a reducer cell by calling `onWindow`.\n */\nexport function batchWindowExpired(\n id: string,\n atMs: number,\n): BatchWindowExpired {\n return deadlineExceeded(id, atMs);\n}\n\n/**\n * The bound knob returned by `createBatchWindow`. `init` + the two pure verbs +\n * `timer` + `subs`, all closing over one `BatchWindowConfig`. The verbs\n * return `readonly [BatchWindow<I>, readonly C[]]`: the next slice and the Cmds\n * to emit (one `flush` Cmd when a window closes, none otherwise).\n */\nexport interface BatchWindowKnob<I, C extends Cmd> {\n /** The starting slice — `initBatchWindow()`. */\n init(): BatchWindow<I>;\n /**\n * Buffer `item` (arrival-stamped `at`), flushing immediately if that brings\n * the buffer to `maxItems`. See `addItem`.\n */\n add(\n state: BatchWindow<I>,\n item: I,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * Flush whatever is buffered because the time window closed. See `onWindow`.\n */\n onWindow(\n state: BatchWindow<I>,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * The built-in `timer` Sub's deps while a window is open, `null` while it is\n * closed. See `timerFor`. `id` keys the Msg so several windows on one\n * machine route distinctly.\n */\n timer(\n state: BatchWindow<I>,\n id?: string,\n ): TimerDeps<BatchWindowExpired> | null;\n /**\n * The window timer as an absolute deadline, listed only while a window is\n * open — for `deadlinesSub` + `subscribeBatchWindow`. See `subsFor`.\n */\n subs(state: BatchWindow<I>, id?: string): readonly BatchWindowSub[];\n}\n\n/**\n * Buffer `item` and decide whether the size trigger fires. PURE.\n *\n * - Empty (closed) window → `item` OPENS it: `openedAt` becomes `at`, buffer\n * becomes `[item]`. This is the anchor the time window measures against.\n * - Open window → `item` appends; `openedAt` is left untouched (the window's\n * latency clock keeps running from the FIRST item, not this one).\n *\n * After buffering, if the buffer has reached `maxItems` the window flushes\n * immediately: emit `config.flush(buffer)` and RESET to the closed slice. The\n * size trigger pre-empts the time trigger — a full page ships now, the window\n * Sub for the (now closed) window disarms on the next reconcile because `subs`\n * returns `[]` for an empty buffer.\n *\n * `maxItems <= 1` is the degenerate \"no batching\" case: every item flushes on\n * arrival (buffer reaches 1 immediately). Legal and useful as a feature flag to\n * disable batching without restructuring the wiring.\n *\n * @param config the knob (maxItems / flush constructor)\n * @param state the current slice\n * @param item the item to buffer\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function addItem<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n item: I,\n at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n const buffer = [...state.buffer, item];\n // A closed window opens at THIS item's `at`; an already-open window keeps its\n // original `openedAt` (the latency clock runs from the first item).\n const openedAt = state.openedAt ?? at;\n\n // Size trigger: a full page ships now, regardless of the time window. The\n // floor of 1 means `maxItems <= 1` flushes every item on arrival.\n if (buffer.length >= Math.max(1, config.maxItems)) {\n return [initBatchWindow<I>(), [config.flush(buffer)]];\n }\n\n return [{ buffer, openedAt }, []];\n}\n\n/**\n * Flush the open window because its time bound was reached. PURE.\n *\n * - Open window (buffer non-empty) → emit `config.flush(buffer)` and RESET to\n * the closed slice.\n * - Closed window (buffer empty) → no-op: return the same slice and no Cmds.\n * This guards the race where the size trigger already flushed and reset the\n * window before the time Sub's Msg lands — a stale `onWindow` after a\n * size-flush must NOT emit an empty batch.\n *\n * `at` is accepted for symmetry with `add` and so the consumer's reducer cell\n * has a uniform `(state, at)` shape; the verb doesn't need it for the flush\n * decision (the buffer's emptiness is the whole signal), but taking it keeps a\n * future \"flush only if the window is genuinely past `openedAt + maxMs`\"\n * tightening a non-breaking change.\n *\n * @param config the knob (flush constructor)\n * @param state the current slice\n * @param at the firing instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function onWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n _at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n // Empty buffer = the size trigger already flushed (or nothing arrived). A\n // stale timer Msg must not ship an empty batch — return the slice untouched.\n if (state.buffer.length === 0) return [state, []];\n return [initBatchWindow<I>(), [config.flush(state.buffer)]];\n}\n\n/**\n * The window timer deadline, derived from the slice. PURE.\n *\n * Returns the single deadline targeting `openedAt + maxMs` while a window is\n * open (buffer non-empty), or `[]` while it is closed. Fed through\n * `deadlinesSub`, the engine arms the timer when the list gains the deadline\n * and disarms it (clearing the pending `setTimeout`) when the list loses it —\n * so a size-flush that empties the buffer auto-cancels the now-irrelevant time\n * trigger on the next transition.\n *\n * `id` defaults to `\"batch-window\"`; pass a distinct id per window when a\n * machine runs several so the dispatched `BatchWindowExpired` Msg routes to the\n * right one.\n *\n * @param config the knob (maxMs)\n * @param state the current slice\n * @param id stable identity for this window's timer (default\n * `\"batch-window\"`)\n */\nexport function subsFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): readonly BatchWindowSub[] {\n // Closed window → no timer. The invariant (empty buffer IFF openedAt null)\n // means either check suffices; we read `openedAt` so the `atMs` arithmetic\n // below is non-null without a `!`. `deadlineSub` already returns a\n // `DeadlineSub`, which IS `BatchWindowSub` — no cast.\n if (state.openedAt === null) return [];\n return [deadlineSub(id, state.openedAt + config.maxMs)];\n}\n\n/**\n * The built-in `timer` Sub's deps for a batch window. PURE.\n *\n * While a window is open it counts `maxMs` down from the transition that\n * opened it — the deps are fixed for the window's life (`openedAt` never moves\n * while items append), so the engine starts the countdown exactly once — and\n * fires `batchWindowExpired(id, openedAt + maxMs)`. A closed window is `null`,\n * so a size-flush that empties the buffer cancels the countdown on the next\n * transition.\n *\n * Relative by construction: a process that rehydrates an open window counts a\n * fresh `maxMs`. Reach for `subsFor` + `subscribeBatchWindow` when the flush\n * must land at the original absolute instant.\n */\nexport function timerFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): TimerDeps<BatchWindowExpired> | null {\n if (state.openedAt === null) return null;\n return {\n ms: config.maxMs,\n msg: batchWindowExpired(id, state.openedAt + config.maxMs),\n };\n}\n\n/**\n * The `deadline` runner for a batch window's absolute timer — the exact\n * `../deadline` runner. Arms a one-shot timer per listed deadline for\n * `max(0, atMs - now)` and dispatches `batchWindowExpired(...)`; returns a\n * cleanup that clears them. Hand it to `run` as `subscribe.deadline` when the\n * machine declares `deadlinesSub((s) => bw.subs(s.batch))`.\n */\nexport const subscribeBatchWindow: Subscribe<\n BatchWindowExpired,\n DeadlinesSub,\n unknown\n>[\"deadline\"] = subscribeDeadline;\n\n/**\n * Build a batch window knob from a config. Combinator form (the default L2\n * ergonomic): hand it the config, splice the ~4 returned hooks into your\n * machine. The pure verbs (`add` / `onWindow`) close over the config so the\n * consumer's reducer cells stay a uniform `(state, ...args)` shape; `timer`\n * arms the window timer.\n *\n * @typeParam I the item type buffered into each batch\n * @typeParam C the Cmd type `config.flush` returns\n */\nexport function createBatchWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n): BatchWindowKnob<I, C> {\n return {\n init: () => initBatchWindow<I>(),\n add: (state, item, at) => addItem(config, state, item, at),\n onWindow: (state, at) => onWindow(config, state, at),\n timer: (state, id) => timerFor(config, state, id),\n subs: (state, id) => subsFor(config, state, id),\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/internal/flow/batch-window/index.ts"],"names":[],"mappings":";;;AA0IO,SAAS,eAAA,GAAqC;AACnD,EAAA,OAAO,EAAE,MAAA,EAAQ,EAAC,EAAG,UAAU,IAAA,EAAK;AACtC;AAqBO,SAAS,kBAAA,CACd,IACA,IAAA,EACoB;AACpB,EAAA,OAAO,gBAAA,CAAiB,IAAI,IAAI,CAAA;AAClC;AAmEO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,IAAA,EACA,EAAA,EACyC;AACzC,EAAA,MAAM,MAAA,GAAS,CAAC,GAAG,KAAA,CAAM,QAAQ,IAAI,CAAA;AAGrC,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,EAAA;AAInC,EAAA,IAAI,OAAO,MAAA,IAAU,IAAA,CAAK,IAAI,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAA,EAAG;AACjD,IAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,CAAC,EAAE,MAAA,EAAQ,QAAA,EAAS,EAAG,EAAE,CAAA;AAClC;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,GAAA,EACyC;AAGzC,EAAA,IAAI,KAAA,CAAM,OAAO,MAAA,KAAW,CAAA,SAAU,CAAC,KAAA,EAAO,EAAE,CAAA;AAChD,EAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAC5D;AAqBO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACsB;AAK3B,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,EAAC;AACrC,EAAA,OAAO,CAAC,WAAA,CAAY,EAAA,EAAI,MAAM,QAAA,GAAW,MAAA,CAAO,KAAK,CAAC,CAAA;AACxD;AAgBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACiC;AACtC,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,IAAA;AACpC,EAAA,OAAO;AAAA,IACL,IAAI,MAAA,CAAO,KAAA;AAAA,IACX,KAAK,kBAAA,CAAmB,EAAA,EAAI,KAAA,CAAM,QAAA,GAAW,OAAO,KAAK;AAAA,GAC3D;AACF;AASO,IAAM,oBAAA,GAIG;AAYT,SAAS,kBACd,MAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,MAAM,eAAA,EAAmB;AAAA,IAC/B,GAAA,EAAK,CAAC,KAAA,EAAO,IAAA,EAAM,OAAO,OAAA,CAAQ,MAAA,EAAQ,KAAA,EAAO,IAAA,EAAM,EAAE,CAAA;AAAA,IACzD,UAAU,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,KAAS,CAAA;AAAA,IACnD,OAAO,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,OAAO,EAAE,CAAA;AAAA,IAChD,MAAM,CAAC,KAAA,EAAO,OAAO,OAAA,CAAQ,MAAA,EAAQ,OAAO,EAAE;AAAA,GAChD;AACF","file":"chunk-ZV4WGW74.js","sourcesContent":["/**\n * internal/flow/batch-window — coalesce a stream of items into size- or\n * time-bounded BATCHES, then flush each batch as a single Cmd.\n *\n * What this module actually DELEGATES to, vs. what it merely RE-EXPORTS:\n *\n * - `../deadline` — REAL delegation (the only one). The window timer's Msg\n * is `deadlineExceeded`, keyed on the window's identity. `timerFor` arms it\n * through the engine's built-in `timer` Sub: `maxMs` counted from the\n * transition that opened the window, so a machine needs no runner. The\n * absolute-instant form is still here too — `subsFor` lists the same\n * deadline for `deadlinesSub` + `subscribeDeadline`, which re-arms for the\n * REMAINING time after a `Store` rehydrate rather than a fresh `maxMs`.\n * - `../work-queue` — TYPE re-export only, NOT a sink this module calls. A\n * flush emits a Cmd; the CONSUMER's interpret handler is what enqueues onto a\n * `createQueue(store)`. This module never touches queue status — it has no\n * queue state and no status flip to delegate. It re-exports `EnqueueInput<I>`\n * purely so the consumer's flush Cmd and queue agree on the item shape; any\n * actual status flip belongs in the consumer and is delegated to\n * `../work-queue/ops` there, not here.\n * - `../debounce` — VALUE/TYPE re-export only, NOT wired into `update`. A\n * bursty source (keystrokes, scroll, websocket frames) can be `debounce`d at\n * the host BEFORE it ever becomes an `add` Msg, so the batch window only sees\n * settled items. `debounce` is a sink transformer OUTSIDE `update`;\n * batch-window is the durable, replayable half inside it. Re-exported for\n * one-import ergonomics — this module does not call it.\n *\n * Why this is a knob and not hand-wired logic: the managed state (the buffer +\n * the window-open instant) lives in the Model, so it survives DO eviction /\n * reload (durable) and every transition is still a Msg through `update`\n * (replayable). A closure holding a `setTimeout` and a mutable array would break\n * both — banned. `add` / `onWindow` are PURE: time arrives as an `at`\n * parameter, never `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const bw = createBatchWindow<LogLine, Msg>({\n * maxItems: 100,\n * maxMs: 5_000,\n * flush: (items) => ({ type: \"FlushBatch\", items }),\n * });\n *\n * // Model: { batch: BatchWindow<LogLine>, ... }\n * // init: (loaded) => [loaded ?? { batch: bw.init(), ... }, []]\n * // update cells:\n * // LogArrived: (s, m) => mapBatch(s, bw.add(s.batch, m.line, m.at))\n * // deadline_exceeded: (s, m) => mapBatch(s, bw.onWindow(s.batch, m.atMs))\n * // subs: [{ type: \"timer\", deps: (s: Model) => bw.timer(s.batch) }]\n * // at run: run(machine, { interpret }) // `timer` is built in\n *\n * where `mapBatch(s, [batch, cmds]) => [{ ...s, batch }, cmds]` lifts the slice\n * transition into the consumer's Model. The `FlushBatch` Cmd's interpret handler\n * is the consumer's — it is where the batch meets I/O (an `enqueue`, a network\n * POST, an R2 write).\n *\n * Internal since #48 — not published on any subpath; reached from inside the\n * package as `internal/flow/batch-window`. Same one-shape-per-package rule as `deadline`, `cache`, and\n * `work-queue`.\n */\n\nimport type { Cmd, Subscribe, TimerDeps } from \"../../../index\";\n\n// `../debounce` re-export (value + type) for one-import ergonomics — see file\n// header. This module does NOT call debounce; a consumer that wants to debounce\n// a bursty source BEFORE it becomes an `add` Msg reaches it from the same import\n// as the batch window.\nexport { type Debounced, debounce } from \"../../timing/debounce\";\n\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n type DeadlinesSub,\n deadlineExceeded,\n deadlineSub,\n subscribeDeadline,\n} from \"../../resilience/deadline\";\n\n// `../work-queue` TYPE re-export only (see file header). This module emits a\n// flush Cmd; the CONSUMER's interpret handler enqueues onto a\n// `createQueue(store)`. Re-export the caller-facing enqueue payload type so the\n// consumer's flush Cmd and queue agree on the item shape.\nexport type { EnqueueInput } from \"../../work-queue\";\n\n/**\n * Configuration for a batch window — the knob. Pure config, no mutable state\n * (the `BatchWindow<I>` slice carries that). One config object is shared across\n * every window of one logical stream (often a module-scope const).\n *\n * `flush` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps the\n * buffered items to the single Cmd the runtime performs when a window closes.\n * Keeping it a `(items) => Cmd` (not a `(items) => void`) is what keeps the\n * trigger replayable — the flush is a Cmd in the transition's output, never a\n * side effect inside the verb.\n */\nexport interface BatchWindowConfig<I, C extends Cmd> {\n /**\n * Size trigger. A batch flushes the instant its buffer reaches `maxItems`,\n * WITHOUT waiting for the time window — the classic \"send as soon as the\n * page is full\" cutoff. Must be `>= 1`; a config of `maxItems: 0` would flush\n * an empty batch on every `add`, which is never useful, so callers treat `1`\n * as the floor (the verb still behaves: with `maxItems <= 1` every item\n * flushes on arrival, a degenerate but legal \"no batching\" window).\n */\n readonly maxItems: number;\n /**\n * Time trigger, in milliseconds. A batch flushes no later than `openedAt +\n * maxMs` — `openedAt` being the `at` of the item that opened the window. This\n * is the latency ceiling: an item never waits longer than `maxMs` to ship,\n * even if the buffer never reaches `maxItems`.\n */\n readonly maxMs: number;\n /**\n * Map a flushed batch to the single Cmd the runtime performs. Called with the\n * buffered items in arrival order; the returned Cmd is what `add` / `onWindow`\n * emit when a window closes. PURE constructor — no I/O, no clock; the I/O\n * happens later in the consumer's interpret handler for the returned Cmd.\n */\n readonly flush: (items: readonly I[]) => C;\n}\n\n/**\n * The Model slice a batch window owns. `buffer` is the items accumulated for the\n * current open window; `openedAt` is the `at` of the item that OPENED it (the\n * anchor the time trigger measures against), or `null` when the window is\n * closed (buffer empty). The pair is the single source of truth for \"is a\n * window open, and when must it flush by?\" — both plain data so the whole slice\n * is trivially JSON-serializable into a consumer's `Store<S>`.\n *\n * Invariant the verbs preserve: `buffer.length === 0` IFF `openedAt === null`.\n * A non-empty buffer always has an `openedAt`; an empty buffer never does. The\n * window timer Sub keys its arm/disarm off exactly this invariant.\n */\nexport interface BatchWindow<I> {\n readonly buffer: readonly I[];\n readonly openedAt: number | null;\n}\n\n/** The starting slice: closed window, empty buffer. */\nexport function initBatchWindow<I>(): BatchWindow<I> {\n return { buffer: [], openedAt: null };\n}\n\n/**\n * The deadline a batch window's open timer lists: it IS `../deadline`'s\n * `DeadlineSub`, not a re-tagged copy. `atMs` is the absolute flush-by instant\n * (`openedAt + maxMs`). Aliasing the deadline type (rather than redeclaring an\n * identical shape) is what lets `subsFor` return `deadlineSub(...)` with no\n * cast — the value's static type already satisfies this name. Consumers running\n * several windows route each one by the deadline's `id`, not by a distinct type.\n */\nexport type BatchWindowSub = DeadlineSub;\n\n/** The Msg a closed-by-time window dispatches. Alias of `deadline`'s Msg. */\nexport type BatchWindowExpired = DeadlineExceeded;\n\n/**\n * Construct the window-expired Msg for the window identified by `id`, flushing\n * by `atMs`. Re-exported alias of `deadlineExceeded` so the runner and\n * the consumer's reducer share one constructor rather than two literals that\n * can drift. The consumer handles it in a reducer cell by calling `onWindow`.\n */\nexport function batchWindowExpired(\n id: string,\n atMs: number,\n): BatchWindowExpired {\n return deadlineExceeded(id, atMs);\n}\n\n/**\n * The bound knob returned by `createBatchWindow`. `init` + the two pure verbs +\n * `timer` + `subs`, all closing over one `BatchWindowConfig`. The verbs\n * return `readonly [BatchWindow<I>, readonly C[]]`: the next slice and the Cmds\n * to emit (one `flush` Cmd when a window closes, none otherwise).\n */\nexport interface BatchWindowKnob<I, C extends Cmd> {\n /** The starting slice — `initBatchWindow()`. */\n init(): BatchWindow<I>;\n /**\n * Buffer `item` (arrival-stamped `at`), flushing immediately if that brings\n * the buffer to `maxItems`. See `addItem`.\n */\n add(\n state: BatchWindow<I>,\n item: I,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * Flush whatever is buffered because the time window closed. See `onWindow`.\n */\n onWindow(\n state: BatchWindow<I>,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * The built-in `timer` Sub's deps while a window is open, `null` while it is\n * closed. See `timerFor`. `id` keys the Msg so several windows on one\n * machine route distinctly.\n */\n timer(\n state: BatchWindow<I>,\n id?: string,\n ): TimerDeps<BatchWindowExpired> | null;\n /**\n * The window timer as an absolute deadline, listed only while a window is\n * open — for `deadlinesSub` + `subscribeBatchWindow`. See `subsFor`.\n */\n subs(state: BatchWindow<I>, id?: string): readonly BatchWindowSub[];\n}\n\n/**\n * Buffer `item` and decide whether the size trigger fires. PURE.\n *\n * - Empty (closed) window → `item` OPENS it: `openedAt` becomes `at`, buffer\n * becomes `[item]`. This is the anchor the time window measures against.\n * - Open window → `item` appends; `openedAt` is left untouched (the window's\n * latency clock keeps running from the FIRST item, not this one).\n *\n * After buffering, if the buffer has reached `maxItems` the window flushes\n * immediately: emit `config.flush(buffer)` and RESET to the closed slice. The\n * size trigger pre-empts the time trigger — a full page ships now, the window\n * Sub for the (now closed) window disarms on the next reconcile because `subs`\n * returns `[]` for an empty buffer.\n *\n * `maxItems <= 1` is the degenerate \"no batching\" case: every item flushes on\n * arrival (buffer reaches 1 immediately). Legal and useful as a feature flag to\n * disable batching without restructuring the wiring.\n *\n * @param config the knob (maxItems / flush constructor)\n * @param state the current slice\n * @param item the item to buffer\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function addItem<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n item: I,\n at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n const buffer = [...state.buffer, item];\n // A closed window opens at THIS item's `at`; an already-open window keeps its\n // original `openedAt` (the latency clock runs from the first item).\n const openedAt = state.openedAt ?? at;\n\n // Size trigger: a full page ships now, regardless of the time window. The\n // floor of 1 means `maxItems <= 1` flushes every item on arrival.\n if (buffer.length >= Math.max(1, config.maxItems)) {\n return [initBatchWindow<I>(), [config.flush(buffer)]];\n }\n\n return [{ buffer, openedAt }, []];\n}\n\n/**\n * Flush the open window because its time bound was reached. PURE.\n *\n * - Open window (buffer non-empty) → emit `config.flush(buffer)` and RESET to\n * the closed slice.\n * - Closed window (buffer empty) → no-op: return the same slice and no Cmds.\n * This guards the race where the size trigger already flushed and reset the\n * window before the time Sub's Msg lands — a stale `onWindow` after a\n * size-flush must NOT emit an empty batch.\n *\n * `at` is accepted for symmetry with `add` and so the consumer's reducer cell\n * has a uniform `(state, at)` shape; the verb doesn't need it for the flush\n * decision (the buffer's emptiness is the whole signal), but taking it keeps a\n * future \"flush only if the window is genuinely past `openedAt + maxMs`\"\n * tightening a non-breaking change.\n *\n * @param config the knob (flush constructor)\n * @param state the current slice\n * @param at the firing instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function onWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n _at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n // Empty buffer = the size trigger already flushed (or nothing arrived). A\n // stale timer Msg must not ship an empty batch — return the slice untouched.\n if (state.buffer.length === 0) return [state, []];\n return [initBatchWindow<I>(), [config.flush(state.buffer)]];\n}\n\n/**\n * The window timer deadline, derived from the slice. PURE.\n *\n * Returns the single deadline targeting `openedAt + maxMs` while a window is\n * open (buffer non-empty), or `[]` while it is closed. Fed through\n * `deadlinesSub`, the engine arms the timer when the list gains the deadline\n * and disarms it (clearing the pending `setTimeout`) when the list loses it —\n * so a size-flush that empties the buffer auto-cancels the now-irrelevant time\n * trigger on the next transition.\n *\n * `id` defaults to `\"batch-window\"`; pass a distinct id per window when a\n * machine runs several so the dispatched `BatchWindowExpired` Msg routes to the\n * right one.\n *\n * @param config the knob (maxMs)\n * @param state the current slice\n * @param id stable identity for this window's timer (default\n * `\"batch-window\"`)\n */\nexport function subsFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): readonly BatchWindowSub[] {\n // Closed window → no timer. The invariant (empty buffer IFF openedAt null)\n // means either check suffices; we read `openedAt` so the `atMs` arithmetic\n // below is non-null without a `!`. `deadlineSub` already returns a\n // `DeadlineSub`, which IS `BatchWindowSub` — no cast.\n if (state.openedAt === null) return [];\n return [deadlineSub(id, state.openedAt + config.maxMs)];\n}\n\n/**\n * The built-in `timer` Sub's deps for a batch window. PURE.\n *\n * While a window is open it counts `maxMs` down from the transition that\n * opened it — the deps are fixed for the window's life (`openedAt` never moves\n * while items append), so the engine starts the countdown exactly once — and\n * fires `batchWindowExpired(id, openedAt + maxMs)`. A closed window is `null`,\n * so a size-flush that empties the buffer cancels the countdown on the next\n * transition.\n *\n * Relative by construction: a process that rehydrates an open window counts a\n * fresh `maxMs`. Reach for `subsFor` + `subscribeBatchWindow` when the flush\n * must land at the original absolute instant.\n */\nexport function timerFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): TimerDeps<BatchWindowExpired> | null {\n if (state.openedAt === null) return null;\n return {\n ms: config.maxMs,\n msg: batchWindowExpired(id, state.openedAt + config.maxMs),\n };\n}\n\n/**\n * The `deadline` runner for a batch window's absolute timer — the exact\n * `../deadline` runner. Arms a one-shot timer per listed deadline for\n * `max(0, atMs - now)` and dispatches `batchWindowExpired(...)`; returns a\n * cleanup that clears them. Hand it to `run` as `subscribe.deadline` when the\n * machine declares `deadlinesSub((s) => bw.subs(s.batch))`.\n */\nexport const subscribeBatchWindow: Subscribe<\n BatchWindowExpired,\n DeadlinesSub,\n unknown\n>[\"deadline\"] = subscribeDeadline;\n\n/**\n * Build a batch window knob from a config. Combinator form (the default L2\n * ergonomic): hand it the config, splice the ~4 returned hooks into your\n * machine. The pure verbs (`add` / `onWindow`) close over the config so the\n * consumer's reducer cells stay a uniform `(state, ...args)` shape; `timer`\n * arms the window timer.\n *\n * @typeParam I the item type buffered into each batch\n * @typeParam C the Cmd type `config.flush` returns\n */\nexport function createBatchWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n): BatchWindowKnob<I, C> {\n return {\n init: () => initBatchWindow<I>(),\n add: (state, item, at) => addItem(config, state, item, at),\n onWindow: (state, at) => onWindow(config, state, at),\n timer: (state, id) => timerFor(config, state, id),\n subs: (state, id) => subsFor(config, state, id),\n };\n}\n"]}
@@ -11,7 +11,7 @@ import { StandardSchemaV1 } from '@standard-schema/spec';
11
11
  * **Dependency direction (the actual decoupling):** this module imports
12
12
  * NOTHING from the runtime — no `run`/host/`Store`. The
13
13
  * runtime (`run`, the host, interpret, `Store`, subscribe — all in
14
- * `../index.ts`) imports *from* here; never the reverse. The root door
14
+ * `../index.ts`) imports *from* here; never the reverse. The root entry
15
15
  * re-exports this surface through `./index.ts`, and
16
16
  * `pure/import-graph.test.ts` is the regression fence asserting the pure
17
17
  * entrypoint's import graph never reaches `run`.
@@ -130,6 +130,11 @@ type CmdInput = {
130
130
  } & Record<string, unknown>;
131
131
  /** The value `Cmd.define("fetch", …)` builds: `{ type: "fetch", ...input }`. */
132
132
  type CmdValue<Name extends string, Input extends CmdInput, Ok, E extends Tagged> = Cmd<Name, Ok, E | MalformedResult> & Readonly<Input>;
133
+ /**
134
+ * The Msg the engine mints when a `Cmd.define`d Cmd's handler succeeds:
135
+ * `<name>_ok`, carrying the Cmd and its value. Handle it in `update` to fold
136
+ * the result into state.
137
+ */
133
138
  type SettledOk<Name extends string, C, Ok> = {
134
139
  readonly type: `${Name}_ok`;
135
140
  readonly cmd: C;
@@ -137,6 +142,11 @@ type SettledOk<Name extends string, C, Ok> = {
137
142
  /** Stamped by the runtime at the interpret edge (`run`'s `clock`). */
138
143
  readonly at: number;
139
144
  };
145
+ /**
146
+ * The Msg the engine mints when a `Cmd.define`d Cmd's handler fails:
147
+ * `<name>_err`, carrying the Cmd and the tagged error. Handle it in `update` to
148
+ * decide what a failure means.
149
+ */
140
150
  type SettledErr<Name extends string, C, E extends Tagged> = {
141
151
  readonly type: `${Name}_err`;
142
152
  readonly cmd: C;
@@ -200,15 +210,41 @@ declare function defineCmd<const Name extends string, Input extends CmdInput, Ok
200
210
  readonly ok: StandardSchemaV1<unknown, Ok>;
201
211
  readonly err: Tags;
202
212
  }): CmdDef<Name, Input, Ok, TaggedError<Tags[number]>>;
213
+ /**
214
+ * What an update cell returns: the `[nextState, cmds]` tuple, typed so that a
215
+ * Promise cannot stand in for it. An `async` cell fails to compile against it.
216
+ */
203
217
  type SyncReturn<S, C extends Cmd> = readonly [S, readonly C[]] & {
204
218
  readonly then?: never;
205
219
  };
220
+ /**
221
+ * Which of the two shapes a machine's `update` takes: a flat `reducer` keyed by
222
+ * Msg type, or a `transitions` table keyed by state type and then Msg type.
223
+ */
206
224
  type UpdateForm = "reducer" | "transitions";
225
+ /**
226
+ * Work out an `update` record's `UpdateForm` from its shape. For a machine, use
227
+ * `formOf`, which prefers the form `defineMachine` already recorded.
228
+ *
229
+ * A reducer's first own value is a function; a transitions table's is a record
230
+ * of functions. An empty record can never dispatch a Msg, so it reads as
231
+ * `reducer`.
232
+ */
207
233
  declare function detectUpdateForm(update: object): UpdateForm;
234
+ /**
235
+ * Read a machine's `UpdateForm`. Reach for it when code has to treat reducer
236
+ * and transitions machines differently. It prefers the form `defineMachine`
237
+ * recorded, and works the form out from the shape only for a machine that
238
+ * never went through `defineMachine`.
239
+ */
208
240
  declare function formOf(machine: {
209
241
  update: object;
210
242
  __form?: UpdateForm;
211
243
  }): UpdateForm;
244
+ /**
245
+ * Thrown when a Msg is dispatched to a state that has no update cell for it. It
246
+ * names the Msg type, the state, and the Msg types that state does accept.
247
+ */
212
248
  declare class NoCellError extends Error {
213
249
  readonly msgType: string;
214
250
  readonly stateName: string;
@@ -232,22 +268,39 @@ declare function acceptedTypes<S>(machine: {
232
268
  update: object;
233
269
  __form?: UpdateForm;
234
270
  }, state: S): readonly string[];
271
+ /**
272
+ * Run the one update cell that matches `state` and `msg` and return its
273
+ * `[nextState, cmds]`. Use it to step a machine by hand; it throws
274
+ * `NoCellError` when no cell matches.
275
+ */
235
276
  declare function applyCell<S, M extends {
236
277
  type: string;
237
278
  }, C extends Cmd>(machine: {
238
279
  update: object;
239
280
  __form?: UpdateForm;
240
281
  }, state: S, msg: M): readonly [S, readonly C[]];
282
+ /**
283
+ * `applyCell` with the development-mode checks that catch an impure cell: the
284
+ * input state is frozen and the result is asserted to be plain data.
285
+ */
241
286
  declare function applyCellChecked<S, M extends {
242
287
  type: string;
243
288
  }, C extends Cmd>(machine: {
244
289
  update: object;
245
290
  __form?: UpdateForm;
246
291
  }, state: S, msg: M): readonly [S, readonly C[]];
292
+ /**
293
+ * List every Msg type a machine has an update cell for, across all of its
294
+ * states.
295
+ */
247
296
  declare function msgKeysOf(machine: {
248
297
  update: object;
249
298
  __form?: UpdateForm;
250
299
  }): readonly string[];
300
+ /**
301
+ * What `describeMachine` returns: the Msg types a machine handles and, for a
302
+ * transitions machine, its states and the Msg types each one accepts.
303
+ */
251
304
  type MachineShape = {
252
305
  readonly form: "reducer";
253
306
  /** Every `Msg.type` the flat reducer has a cell for. */
@@ -261,14 +314,28 @@ type MachineShape = {
261
314
  /** `state.type` → the `Msg.type`s that state has a cell for. */
262
315
  readonly accepts: Readonly<Record<string, readonly string[]>>;
263
316
  };
317
+ /**
318
+ * Describe a machine's update table as data: its form, its Msg types, and the
319
+ * per-state accept sets of a transitions machine. Use it to build tooling, or
320
+ * to ask what a machine handles without dispatching to it.
321
+ */
264
322
  declare function describeMachine(machine: {
265
323
  update: object;
266
324
  __form?: UpdateForm;
267
325
  }): MachineShape;
326
+ /**
327
+ * List the Msg types a machine accepts in the state named `stateType`. A
328
+ * reducer machine accepts every one of its Msg types in every state, and a
329
+ * `stateType` with no row in a transitions table accepts none.
330
+ */
268
331
  declare function acceptsOf(machine: {
269
332
  update: object;
270
333
  __form?: UpdateForm;
271
334
  }, stateType: string): readonly string[];
335
+ /**
336
+ * The flat form of `update`: a record with one handler per Msg type, each
337
+ * returning `[nextState, cmds]`. Leaving a Msg type out is a compile error.
338
+ */
272
339
  type Reducer<S, M extends {
273
340
  type: string;
274
341
  }, C extends Cmd> = {
@@ -323,6 +390,11 @@ type Cmd<T extends string = string, Ok = unknown, E = unknown> = {
323
390
  /** Phantom — the `_tag` union this Cmd can settle with. Never assigned. */
324
391
  readonly __e?: E;
325
392
  };
393
+ /**
394
+ * The helpers for building Cmds: `Cmd.define` declares a typed Cmd constructor,
395
+ * and `Cmd.none`, `Cmd.batch`, `Cmd.when` and `Cmd.whenDefined` build the Cmd
396
+ * list an update cell returns.
397
+ */
326
398
  declare const Cmd: {
327
399
  /**
328
400
  * Declare a typed Cmd constructor (ADR 0014, 0021). Returns the builder —
@@ -407,6 +479,7 @@ declare const Cmd: {
407
479
  */
408
480
  readonly whenDefined: <T, const C extends Cmd>(value: T | undefined, build: (value: T) => C) => readonly C[];
409
481
  };
482
+ /** The branded string that identifies a running Sub. Build one with `subId`. */
410
483
  type SubId = string & {
411
484
  readonly __brand: "SubId";
412
485
  };
@@ -420,11 +493,29 @@ type SubId = string & {
420
493
  * Strengthens invariant 7 (identity is explicit).
421
494
  */
422
495
  declare function subId(s: string): SubId;
496
+ /**
497
+ * A running subscription as its runner sees it: its `type`, the `deps` value
498
+ * the machine's `subs` entry returned, and an `id` derived from both. Use it to
499
+ * declare a machine's Sub union and to type the `sub` argument of a runner.
500
+ *
501
+ * A machine never builds one. It declares `{ type, deps(state) }` in `subs`
502
+ * (see `DepKeyedSub`) and the engine derives the rest. A runner reads its data
503
+ * off `sub.deps`. `id` is `structuralHash({ type, deps })`, so it changes
504
+ * exactly when the type or the deps value does, which is when the engine
505
+ * restarts the runner.
506
+ */
423
507
  type Sub<T extends string = string, D = unknown> = {
424
508
  readonly id: SubId;
425
509
  readonly type: T;
426
510
  readonly deps: D;
427
511
  };
512
+ /**
513
+ * The cleanup function a Sub runner returns. The runner opens a resource and
514
+ * returns the function that closes it. The engine calls it when the Sub's
515
+ * `deps` go null (turned off), when they change (restarted: the old `Dispose`,
516
+ * then a fresh runner), and when the runtime stops. A returned Promise is
517
+ * awaited by `stop()`, with a bound.
518
+ */
428
519
  type Dispose = () => void | Promise<void>;
429
520
  /** The `deps` a `timer` Sub declares: fire `msg` once, `ms` after it starts. */
430
521
  type TimerDeps<M> = {
@@ -442,7 +533,16 @@ type BuiltinSub<M> = TimerSub<M>;
442
533
  * value. Same type and same deps → same id → the running Sub is left alone.
443
534
  */
444
535
  declare function subIdOf(type: string, deps: unknown): SubId;
536
+ /**
537
+ * Turn a plain-data value into a stable string that does not depend on object
538
+ * key order. Use it wherever two values must compare equal by content, as the
539
+ * engine does for Sub ids.
540
+ */
445
541
  declare function structuralHash(deps: unknown): string;
542
+ /**
543
+ * One entry of a machine's `subs`: a Sub `type` plus `deps(state)`, which
544
+ * returns the data the runner needs, or `null` to keep the Sub off.
545
+ */
446
546
  type DepKeyedSub<S, U extends Sub = Sub> = U extends Sub<infer T, infer D> ? {
447
547
  readonly type: T;
448
548
  /**
@@ -454,6 +554,11 @@ type DepKeyedSub<S, U extends Sub = Sub> = U extends Sub<infer T, infer D> ? {
454
554
  */
455
555
  readonly deps: (state: S) => D | null | undefined;
456
556
  } : never;
557
+ /**
558
+ * A machine's optional message filter: `ofState` names the identity this
559
+ * instance owns, and `ofMsg` the identity a Msg is addressed to. The engine
560
+ * drops a Msg addressed to a different identity before `update` runs.
561
+ */
457
562
  interface Identity<S, M> {
458
563
  /** The identity THIS instance owns. Pure (invariant 2); plain value. */
459
564
  readonly ofState: (state: S) => unknown;
@@ -464,6 +569,11 @@ interface Identity<S, M> {
464
569
  */
465
570
  readonly ofMsg: (msg: M) => unknown;
466
571
  }
572
+ /**
573
+ * A named, typed channel for values leaving the runtime. A Cmd handler sends on
574
+ * it with `ctx.emit(port, value)` and a host listens with
575
+ * `runtime.subscribePort`; create one with `definePort`.
576
+ */
467
577
  interface Port<T> {
468
578
  readonly __brand: "port";
469
579
  readonly name: string;
@@ -480,10 +590,18 @@ interface Port<T> {
480
590
  interface PortEmitter {
481
591
  emit<T>(port: Port<T>, value: T): void;
482
592
  }
593
+ /**
594
+ * The `Ctx` of a machine whose handlers read nothing from context. Use it in
595
+ * place of `unknown` to say so on purpose.
596
+ */
483
597
  type NoCtx = Readonly<Record<never, never>>;
484
598
  type HandlerCtx<Ctx> = (0 extends 1 & Ctx ? Ctx : [
485
599
  Ctx
486
600
  ] extends [void] ? unknown : Ctx) & PortEmitter;
601
+ /**
602
+ * The Cmd handler table an engine is handed: one async handler per Cmd type.
603
+ * Leaving a Cmd type out is a compile error.
604
+ */
487
605
  type Interpret<M extends {
488
606
  type: string;
489
607
  }, C extends Cmd, Ctx> = {
@@ -506,9 +624,28 @@ type OkOfCmd<C> = C extends {
506
624
  } ? Ok : unknown;
507
625
  /** The failures a `Cmd.define`d Cmd's handler may return: its declared tags. */
508
626
  type DeclaredErrorsOf<C> = Exclude<ErrorsOf<C>, MalformedResult>;
627
+ /**
628
+ * A Cmd handler that reports back through a `dispatch` limited to the Msgs it
629
+ * is allowed to send. Use it for work that outlives the handler call, and adapt
630
+ * it into an `Interpret` cell with `wrapDetached`.
631
+ *
632
+ * Such a handler hands its long-running work to `ctx.waitUntil(...)` and cannot
633
+ * return its terminal Msg inline, because awaiting it would deadlock the serial
634
+ * dispatch tail. So it returns `Promise<void>` and dispatches instead.
635
+ * `Allowed` is the Cmd's declared result-Msg set (e.g.
636
+ * `GraphFinished | GraphFailed`), and `dispatch` takes only those, so a wrong
637
+ * or mistyped terminal Msg fails to compile.
638
+ */
509
639
  type InterpretDetached<C extends Cmd, Allowed extends {
510
640
  type: string;
511
641
  }, Ctx> = (cmd: C, ctx: HandlerCtx<Ctx>, dispatch: (msg: Allowed) => void) => Promise<void>;
642
+ /**
643
+ * The Sub runner table an engine is handed: one runner per Sub type. Leaving a
644
+ * Sub type out is a compile error. Each runner receives the running Sub
645
+ * (`{ id, type, deps }`), the Ctx and a `dispatch`. It opens its resource,
646
+ * dispatches Msgs from it, and returns the `Dispose` the engine calls when the
647
+ * Sub stops.
648
+ */
512
649
  type Subscribe<M extends {
513
650
  type: string;
514
651
  }, U extends Sub, Ctx> = {
@@ -564,6 +701,10 @@ type Wired<S, M extends {
564
701
  }, C extends Cmd, U extends Sub, Ctx> = {
565
702
  readonly machine: Machine<S, M, C, U, Ctx>;
566
703
  } & RunHandlers<M, C, U, Ctx>;
704
+ /**
705
+ * A machine as plain data: `init`, `update`, and optionally `subs`, `cmds` and
706
+ * `identity`. Build one with `defineMachine` and hand it to an engine's `run`.
707
+ */
567
708
  type Machine<S, M extends {
568
709
  type: string;
569
710
  }, C extends Cmd, U extends Sub, Ctx> = {
@@ -643,6 +784,12 @@ type Machine<S, M extends {
643
784
  */
644
785
  readonly __form?: UpdateForm;
645
786
  };
787
+ /**
788
+ * Fold a machine's `update` over a list of Msgs from a starting state, and
789
+ * return the final state with every Cmd the cells emitted. `replay` and
790
+ * `foldMsgs` cover the common cases; use this when you need both the state and
791
+ * the Cmds from a state of your choosing.
792
+ */
646
793
  declare function foldUpdates<S, M extends {
647
794
  type: string;
648
795
  }, C extends Cmd>(machine: {
@@ -652,8 +799,13 @@ declare function foldUpdates<S, M extends {
652
799
  state: S;
653
800
  cmds: C[];
654
801
  };
802
+ /**
803
+ * Fold a machine's `update` over a list of Msgs from a base state and return
804
+ * only the final state. Use it for client-side prediction: re-apply
805
+ * unacknowledged inputs on top of a server snapshot without running any effect.
806
+ */
655
807
  declare function foldMsgs<S, M extends {
656
808
  type: string;
657
809
  }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, base: S, msgs: readonly M[]): S;
658
810
 
659
- export { applyCell as $, type AnyCmdDef as A, type BuiltinSub as B, Cmd as C, type Dispose as D, type ErrOf as E, type MachineShape as F, type NoCtx as G, type HandlerCtx as H, type Interpret as I, type OkOfCmd as J, OutcomeContractError as K, type OutcomeHelpers as L, type Machine as M, NoCellError as N, Outcome as O, type Port as P, type PortEmitter as Q, type RunHandlers as R, type Subscribe as S, type TimerDeps as T, type UpdateForm as U, type SubscribeArg as V, type Wired as W, type SyncReturn as X, UndeclaredFailureError as Y, acceptedTypes as Z, acceptsOf as _, type CmdOf as a, applyCellChecked as a0, describeMachine as a1, detectUpdateForm as a2, foldMsgs as a3, foldUpdates as a4, formOf as a5, msgKeysOf as a6, structuralHash as a7, subId as a8, subIdOf as a9, type CmdDef as b, type Sub as c, type SettledOk as d, type SettledErr as e, type TaggedError as f, type Reducer as g, type Settled as h, type Transitions as i, type InterpretDetached as j, type SubId as k, type DepKeyedSub as l, type TimerSub as m, type MalformedResult as n, type Tagged as o, type OkOf as p, type CmdValue as q, AsyncSchemaError as r, type BuiltinSubType as s, type CmdInput as t, type DeclaredErrorsOf as u, type ErrorsOf as v, type ExhaustiveTransitions as w, type Identity as x, type InterpretArg as y, type InterpretCell as z };
811
+ export { applyCell as $, type AnyCmdDef as A, type BuiltinSub as B, Cmd as C, type Dispose as D, type ErrorsOf as E, type InterpretCell as F, type MachineShape as G, type HandlerCtx as H, type Interpret as I, type NoCtx as J, OutcomeContractError as K, type OutcomeHelpers as L, type Machine as M, NoCellError as N, Outcome as O, type Port as P, type PortEmitter as Q, type RunHandlers as R, type Subscribe as S, type TimerDeps as T, type UpdateForm as U, type SubscribeArg as V, type Wired as W, type SyncReturn as X, UndeclaredFailureError as Y, acceptedTypes as Z, acceptsOf as _, type CmdOf as a, applyCellChecked as a0, describeMachine as a1, detectUpdateForm as a2, foldMsgs as a3, foldUpdates as a4, formOf as a5, msgKeysOf as a6, structuralHash as a7, subId as a8, subIdOf as a9, type CmdDef as b, type Sub as c, type SettledOk as d, type SettledErr as e, type TaggedError as f, type Reducer as g, type Settled as h, type Transitions as i, type InterpretDetached as j, type CmdInput as k, type SubId as l, type DepKeyedSub as m, type TimerSub as n, type MalformedResult as o, type OkOfCmd as p, type DeclaredErrorsOf as q, type BuiltinSubType as r, type Tagged as s, type OkOf as t, type ErrOf as u, type CmdValue as v, AsyncSchemaError as w, type ExhaustiveTransitions as x, type Identity as y, type InterpretArg as z };