@demlik/tea 0.20.0 → 0.22.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 (130) hide show
  1. package/README.md +11 -2
  2. package/dist/agent/index.d.ts +36 -23
  3. package/dist/agent/index.js +12 -10
  4. package/dist/{chunk-UHAQ4PI6.js → chunk-2A5XTKQQ.js} +205 -59
  5. package/dist/chunk-2A5XTKQQ.js.map +1 -0
  6. package/dist/{chunk-JAKM53JF.js → chunk-7K7SEYCH.js} +16 -15
  7. package/dist/chunk-7K7SEYCH.js.map +1 -0
  8. package/dist/{chunk-RKVUVALV.js → chunk-A3QPMX5M.js} +31 -9
  9. package/dist/chunk-A3QPMX5M.js.map +1 -0
  10. package/dist/{chunk-DVNXEB7V.js → chunk-BVFS3ICC.js} +3 -3
  11. package/dist/chunk-BVFS3ICC.js.map +1 -0
  12. package/dist/{chunk-FTCYY2DO.js → chunk-CPE5ZQXQ.js} +3 -3
  13. package/dist/{chunk-FTCYY2DO.js.map → chunk-CPE5ZQXQ.js.map} +1 -1
  14. package/dist/chunk-DEAPNMKO.js +11 -0
  15. package/dist/chunk-DEAPNMKO.js.map +1 -0
  16. package/dist/{chunk-XWIE5V7R.js → chunk-ETRNSJCR.js} +4 -4
  17. package/dist/{chunk-XWIE5V7R.js.map → chunk-ETRNSJCR.js.map} +1 -1
  18. package/dist/chunk-FXWDDNGD.js +98 -0
  19. package/dist/chunk-FXWDDNGD.js.map +1 -0
  20. package/dist/chunk-GFGOQIEC.js +10 -0
  21. package/dist/chunk-GFGOQIEC.js.map +1 -0
  22. package/dist/{chunk-2R62Q5UK.js → chunk-IAHEYPYJ.js} +3 -3
  23. package/dist/{chunk-2R62Q5UK.js.map → chunk-IAHEYPYJ.js.map} +1 -1
  24. package/dist/{chunk-NGYFBD5G.js → chunk-J5JNLXO7.js} +3 -3
  25. package/dist/{chunk-NGYFBD5G.js.map → chunk-J5JNLXO7.js.map} +1 -1
  26. package/dist/{chunk-72CN3VPY.js → chunk-NFOGYDRS.js} +5 -5
  27. package/dist/{chunk-72CN3VPY.js.map → chunk-NFOGYDRS.js.map} +1 -1
  28. package/dist/{chunk-A73IJ27X.js → chunk-R2YNDRGL.js} +3 -3
  29. package/dist/{chunk-A73IJ27X.js.map → chunk-R2YNDRGL.js.map} +1 -1
  30. package/dist/{chunk-5HVKDHY6.js → chunk-R5NXOPL2.js} +3 -3
  31. package/dist/{chunk-5HVKDHY6.js.map → chunk-R5NXOPL2.js.map} +1 -1
  32. package/dist/chunk-RMBZ7YH7.js +52 -0
  33. package/dist/chunk-RMBZ7YH7.js.map +1 -0
  34. package/dist/{chunk-AIXC33SE.js → chunk-SHXUSTHM.js} +62 -15
  35. package/dist/chunk-SHXUSTHM.js.map +1 -0
  36. package/dist/{chunk-TAKPC6SD.js → chunk-UGTC33VH.js} +4 -4
  37. package/dist/{chunk-TAKPC6SD.js.map → chunk-UGTC33VH.js.map} +1 -1
  38. package/dist/{chunk-EA47MI5Q.js → chunk-VH5MYJ2K.js} +4 -4
  39. package/dist/{chunk-EA47MI5Q.js.map → chunk-VH5MYJ2K.js.map} +1 -1
  40. package/dist/{chunk-2774I242.js → chunk-WZEDIUWR.js} +21 -20
  41. package/dist/chunk-WZEDIUWR.js.map +1 -0
  42. package/dist/{chunk-TGJLXZAU.js → chunk-ZRRET3RP.js} +11 -11
  43. package/dist/chunk-ZRRET3RP.js.map +1 -0
  44. package/dist/{core-CtvAyPpH.d.ts → core-DNDAEhO2.d.ts} +9 -12
  45. package/dist/devtools/index.d.ts +4 -4
  46. package/dist/do/index.d.ts +103 -38
  47. package/dist/do/index.js +40 -21
  48. package/dist/do/index.js.map +1 -1
  49. package/dist/{drive-loop-Dw1sGSsW.d.ts → drive-loop-DlOEv49c.d.ts} +2 -2
  50. package/dist/{durable-effects-Ump5lSdO.d.ts → durable-effects-wLWEdy9H.d.ts} +1 -1
  51. package/dist/effect/index.d.ts +121 -28
  52. package/dist/effect/index.js +45 -31
  53. package/dist/effect/index.js.map +1 -1
  54. package/dist/extension/index.d.ts +3 -3
  55. package/dist/extension/index.js +2 -2
  56. package/dist/extension/index.js.map +1 -1
  57. package/dist/flow/index.d.ts +17 -13
  58. package/dist/flow/index.js +15 -12
  59. package/dist/flow/index.js.map +1 -1
  60. package/dist/idempotency/index.d.ts +3 -3
  61. package/dist/idempotency/index.js +3 -3
  62. package/dist/{index-CrcoVfd_.d.ts → index-9P1PaXIV.d.ts} +2 -2
  63. package/dist/{index-D3NY3VCN.d.ts → index-BBLI2WWN.d.ts} +1 -1
  64. package/dist/{index-Dl71V7kt.d.ts → index-C1WhBZ6a.d.ts} +4 -5
  65. package/dist/{index-CKtEKGyp.d.ts → index-C92SgNWz.d.ts} +3 -3
  66. package/dist/{index-CZZ3HI-Z.d.ts → index-CAsUE_d1.d.ts} +4 -4
  67. package/dist/{index-EIgfl83J.d.ts → index-CO34esWT.d.ts} +2 -2
  68. package/dist/{index-Bk2hBUdK.d.ts → index-COeX9Bmz.d.ts} +11 -9
  69. package/dist/{index-CVU7SK3a.d.ts → index-Cg0QXnzf.d.ts} +2 -2
  70. package/dist/index.d.ts +9 -9
  71. package/dist/index.js +2 -2
  72. package/dist/jev/index.d.ts +15 -15
  73. package/dist/jev/index.js +5 -5
  74. package/dist/jev/index.js.map +1 -1
  75. package/dist/laws-DTtCDDlU.d.ts +135 -0
  76. package/dist/loop-DReKGV1X.d.ts +70 -0
  77. package/dist/{machine-Dtre2TmN.d.ts → machine-C0WhOAyB.d.ts} +7 -7
  78. package/dist/machine-viz/index.d.ts +1 -1
  79. package/dist/machine-viz/index.js +2 -2
  80. package/dist/mem/index.d.ts +4 -3
  81. package/dist/mem/index.js +2 -2
  82. package/dist/mem/index.js.map +1 -1
  83. package/dist/node/index.d.ts +57 -9
  84. package/dist/node/index.js +175 -68
  85. package/dist/node/index.js.map +1 -1
  86. package/dist/otel/index.d.ts +28 -12
  87. package/dist/otel/index.js +25 -21
  88. package/dist/otel/index.js.map +1 -1
  89. package/dist/paginate/index.d.ts +4 -4
  90. package/dist/paginate/index.js +5 -5
  91. package/dist/parity/index.d.ts +11 -15
  92. package/dist/parity/index.js +4 -9
  93. package/dist/parity/index.js.map +1 -1
  94. package/dist/pbt/index.d.ts +279 -67
  95. package/dist/pbt/index.js +299 -58
  96. package/dist/pbt/index.js.map +1 -1
  97. package/dist/persistence/index.d.ts +10 -12
  98. package/dist/persistence/index.js +4 -4
  99. package/dist/promise/index.d.ts +118 -21
  100. package/dist/promise/index.js +6 -4
  101. package/dist/react/index.d.ts +34 -21
  102. package/dist/react/index.js +271 -42
  103. package/dist/react/index.js.map +1 -1
  104. package/dist/resilience/index.d.ts +6 -6
  105. package/dist/resilience/index.js +5 -5
  106. package/dist/{run-fiBe7pOG.d.ts → run-Cnw4qXj2.d.ts} +105 -23
  107. package/dist/{runtime-types-DEluQgGO.d.ts → runtime-types-CP-ykwgf.d.ts} +157 -29
  108. package/dist/testing/effect/index.d.ts +162 -9
  109. package/dist/testing/effect/index.js +110 -10
  110. package/dist/testing/effect/index.js.map +1 -1
  111. package/dist/testing/index.d.ts +6 -8
  112. package/dist/testing/index.js +4 -24
  113. package/dist/testing/index.js.map +1 -1
  114. package/dist/testing/promise/index.d.ts +4 -4
  115. package/dist/testing/promise/index.js +5 -5
  116. package/dist/timing/index.d.ts +4 -4
  117. package/dist/timing/index.js +4 -4
  118. package/dist/{types-CXNzlbAg.d.ts → types-S53WDy_h.d.ts} +1 -1
  119. package/dist/work-queue/index.d.ts +4 -4
  120. package/dist/work-queue/index.js +2 -2
  121. package/package.json +19 -17
  122. package/dist/chunk-2774I242.js.map +0 -1
  123. package/dist/chunk-AIXC33SE.js.map +0 -1
  124. package/dist/chunk-DVNXEB7V.js.map +0 -1
  125. package/dist/chunk-HX3C6LDK.js +0 -34
  126. package/dist/chunk-HX3C6LDK.js.map +0 -1
  127. package/dist/chunk-JAKM53JF.js.map +0 -1
  128. package/dist/chunk-RKVUVALV.js.map +0 -1
  129. package/dist/chunk-TGJLXZAU.js.map +0 -1
  130. package/dist/chunk-UHAQ4PI6.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing/drive-loop.ts"],"names":[],"mappings":"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","file":"chunk-XWIE5V7R.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The round loop both `drive`s run — `@demlik/tea/testing/promise` and\n// `@demlik/tea/testing/effect` — said once, so the two cannot drift.\n//\n// `bindMachine` hands back the Cmds a fold emitted and stops there; performing\n// them is the caller's. So every consumer test that exercises a Cmd-emitting\n// machine hand-writes the same fixture: step the Msg, await the real interpret\n// handler over each emitted Cmd, feed the settle Msg back, repeat until quiet,\n// with a hand-picked guard bound in the loop header. `drive` is that fixture as\n// one call, and this file is its loop. It composes `bindMachine`'s synchronous\n// 2-arg `step` and adds NO second reducer path.\n//\n// What differs per engine is only how one Cmd is performed and whether Subs\n// run, so an engine hands this loop a `cellFor` lookup and, optionally, a\n// `subs` driver. Everything else — the rounds, the trace, the `maxRounds`\n// bound, the `Cmd.define` edge and its dispatch check (#304) — is here.\n//\n// **It returns the history, not just the endpoint.** `{ state, trace }`, where\n// `trace` is every Msg folded and every Cmd dispatched in order, lets a test\n// assert on the SEQUENCE as well as the settled state, and the same trace's\n// Msgs replayed through `replay` from the same initial state reproduce the\n// returned state exactly.\n//\n// **Exhausting `maxRounds` fails the drive.** A driver that quietly returns a\n// half-driven state lets a test assert green on a machine that never settled.\n//\n// Strengthens invariant 9 (the testing surface is named and small).\n// ---------------------------------------------------------------------------\n\nimport {\n type AnyCmdDef,\n type Cmd,\n type CmdContract,\n cmdContractOver,\n desiredSub,\n followUps,\n type Machine,\n type Sub,\n subEntriesOf,\n} from \"../pure/core\";\nimport type { CtxArg } from \"../runtime-types\";\nimport { bindMachine } from \"./bind-machine\";\n\n/**\n * The default round bound when `opts.maxRounds` is omitted: **100**.\n *\n * A round is one drain of the pending Msgs plus one drain of the Cmds those\n * folds emitted, so a settling machine costs a handful; 100 is loose enough\n * that a legitimate retry ladder finishes inside it and tight enough that a\n * genuine cycle stops rather than hangs.\n */\nexport const DEFAULT_MAX_ROUNDS = 100;\n\n/**\n * One entry of a driven run's history, in dispatch order.\n *\n * - `msg` — a Msg folded through the machine's reducer. The seed Msg is the\n * first entry of every trace; every later one is a settle Msg a handler\n * returned (or fired through its injected `dispatch`), or a Msg a running\n * Sub emitted.\n * - `cmd` — a Cmd handed to an interpret handler. It is recorded BEFORE the\n * handler runs, so a trace recovered from a failed handler names the Cmd\n * that failed as its last entry.\n */\nexport type DriveTraceEntry<M, C> =\n | { readonly kind: \"msg\"; readonly msg: M }\n | { readonly kind: \"cmd\"; readonly cmd: C };\n\n/** What a settled `drive` hands back. */\nexport interface DriveResult<S, M, C> {\n /** The state after the machine went quiet — no Msg pending, no Cmd unperformed. */\n readonly state: S;\n /**\n * Every Msg folded and every Cmd dispatched, in order. Filtering it to its\n * `msg` entries and replaying those through `replay` from the same initial\n * state reproduces {@link DriveResult.state}.\n */\n readonly trace: readonly DriveTraceEntry<M, C>[];\n}\n\n/**\n * `drive`'s `ctx` field. `CtxArg` widened by exactly one case: a machine\n * declaring `ctx: undefined` in its `types` block — the pure shape every\n * how-to on this package writes — may omit it too. `CtxArg` alone keeps `ctx`\n * REQUIRED there, because `{}` is not assignable to `undefined`, which would\n * make the common pure call site spell `{ ctx: undefined }` to say nothing.\n * A machine whose `Ctx` carries a field a handler reads still requires it.\n */\nexport type DriveCtxArg<Ctx> = [undefined] extends [Ctx]\n ? { readonly ctx?: Ctx }\n : CtxArg<Ctx>;\n\n/**\n * The options both `drive`s take. `ctx` is conditionally optional (see\n * {@link DriveCtxArg}).\n */\nexport type DriveOptions<Ctx> = DriveCtxArg<Ctx> & {\n /**\n * The round bound. Exceeding it fails the drive with\n * {@link DriveRoundsExceededError}; `drive` never returns a half-driven\n * state. Defaults to {@link DEFAULT_MAX_ROUNDS} (100).\n */\n readonly maxRounds?: number;\n /**\n * The clock that stamps `at` on a `Cmd.define`d Cmd's minted `_ok` / `_err`\n * Msg — the one `run` takes. Defaults to `Date.now`; pin it for a test that\n * asserts on `at`.\n */\n readonly clock?: () => number;\n};\n\n/**\n * Raised when a driven machine is still emitting work after `maxRounds` rounds.\n * Follows the tea error dialect (`override readonly name`) and carries a `_tag`\n * discriminant, so a test branches on it by `instanceof` or on the tag.\n *\n * It carries the round count it stopped at and the PARTIAL trace up to that\n * point, which is what a test needs to see the cycle — the bound alone says\n * only that one exists.\n */\nexport class DriveRoundsExceededError<M, C> extends Error {\n override readonly name = \"DriveRoundsExceededError\";\n readonly _tag = \"DriveRoundsExceededError\" as const;\n constructor(\n /** The bound that was exceeded. */\n public readonly maxRounds: number,\n /** The round the driver stopped at — always `maxRounds + 1`. */\n public readonly rounds: number,\n /** Every Msg folded and Cmd dispatched before the driver gave up. */\n public readonly trace: readonly DriveTraceEntry<M, C>[],\n ) {\n super(\n `@demlik/tea: drive did not settle within ${maxRounds} round(s) — the ` +\n `machine was still emitting work at round ${rounds}. The partial ` +\n `trace (${trace.length} entr${trace.length === 1 ? \"y\" : \"ies\"}) is ` +\n `on the error's \\`trace\\` field.`,\n );\n }\n}\n\n/**\n * Raised when a Cmd reaches `drive` with no handler for its `type` in the\n * handler record. The record's mapped type makes that a compile error at a\n * well-typed call site; this is the runtime arm for a handler record assembled\n * dynamically or cast.\n */\nexport class DriveNoHandlerError<M, C> extends Error {\n override readonly name = \"DriveNoHandlerError\";\n readonly _tag = \"DriveNoHandlerError\" as const;\n constructor(\n /** The Cmd `type` nothing in the handler record answered. */\n public readonly cmdType: string,\n /** Every Msg folded and Cmd dispatched before the miss. */\n public readonly trace: readonly DriveTraceEntry<M, C>[],\n ) {\n super(\n `@demlik/tea: drive found no interpret handler for Cmd type ` +\n `\"${cmdType}\" — the handlers record must answer every Cmd the ` +\n `machine emits.`,\n );\n }\n}\n\n/** The key `drive` hangs a partial trace off a failure under. */\nconst TRACE_KEY = \"driveTrace\";\n\n/**\n * Hang `trace` off `err` on a non-enumerable property, so {@link driveTraceOf}\n * reads it back without changing how the error prints or serializes. A\n * non-object failure carries nothing.\n */\nexport function annotateTrace(err: unknown, trace: readonly unknown[]): void {\n if (typeof err !== \"object\" || err === null) return;\n Object.defineProperty(err, TRACE_KEY, {\n value: [...trace],\n enumerable: false,\n configurable: true,\n writable: true,\n });\n}\n\n/**\n * Read back the partial trace `drive` attached to an error a handler failed\n * with. Returns `undefined` for anything `drive` did not annotate.\n *\n * A failing handler's error leaves `drive` UNSWALLOWED — same value, same\n * `instanceof`, same `_tag` — so a test that already branches on the handler's\n * own error type keeps doing so. The trace rides along on a non-enumerable\n * property so it is recoverable without changing how the error prints or\n * serializes.\n */\nexport function driveTraceOf<M, C>(\n err: unknown,\n): readonly DriveTraceEntry<M, C>[] | undefined {\n if (typeof err !== \"object\" || err === null) return undefined;\n const carried = (err as Record<string, unknown>)[TRACE_KEY];\n return Array.isArray(carried)\n ? (carried as readonly DriveTraceEntry<M, C>[])\n : undefined;\n}\n\n// === The loop ===\n\n/**\n * Perform one Cmd and resolve with the handler's raw return, before the\n * `Cmd.define` edge reads it. The `dispatch` it is handed is already checked\n * (#304).\n */\nexport type DriveCell<M, C> = (\n cmd: C,\n dispatch: (msg: M) => void,\n) => Promise<unknown>;\n\n/** The Subs a drive runs, as the loop sees them. */\nexport interface DriveSubs<S, M> {\n /**\n * Start every Sub `state` wants that is not running, and stop every running\n * one it no longer wants — the engine's reconcile, after each fold.\n */\n readonly reconcile: (state: S) => void;\n /**\n * Wait for the next Msgs a running Sub emits. Resolves with every Msg\n * buffered by then, or with `undefined` once no Sub is left running; rejects\n * when a Sub failed.\n */\n readonly next: () => Promise<readonly M[] | undefined>;\n /** Stop every Sub still running. */\n readonly close: () => Promise<void>;\n}\n\n/** What one engine hands the shared loop. */\nexport interface DriveEngine<S, M, C> {\n /**\n * The edge and dispatch check over the machine's `Cmd.define` list. The\n * engine builds it (see {@link driveContractOf}) so a handler that settles\n * through `cmdEdgeOf(ctx)` mints into the same record the check reads.\n */\n readonly contract: CmdContract;\n /** The cell that performs a Cmd of `type`, or `undefined` when none does. */\n readonly cellFor: (type: string) => DriveCell<M, C> | undefined;\n /** The Subs to run beside the Cmds; an engine that runs none omits it. */\n readonly subs?: DriveSubs<S, M>;\n}\n\n/** The contract both `drive`s hold a machine's Cmds to (ADR 0021). */\nexport function driveContractOf(\n machine: { readonly cmds?: readonly AnyCmdDef[] },\n clock: () => number,\n): CmdContract {\n return cmdContractOver(machine.cmds ?? [], clock);\n}\n\n/**\n * Drive `machine` from `initial` through `msg`, performing every emitted Cmd\n * through `engine.cellFor`, until the machine goes quiet.\n *\n * One round is: fold every pending Msg, then perform every Cmd those folds\n * emitted, in order. A Msg a cell returns or dispatches is pending for the next\n * round. When nothing is pending and Subs are running, the loop waits for the\n * next Msgs they emit; it settles once no Msg is pending, no Cmd is unperformed\n * and no Sub is left running.\n *\n * Every error that leaves the loop carries the partial trace (see\n * {@link driveTraceOf}).\n */\nexport async function driveLoop<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n initial: S,\n msg: M,\n ctx: Ctx,\n maxRounds: number,\n engine: DriveEngine<S, M, C>,\n): Promise<DriveResult<S, M, C>> {\n const bound = bindMachine(machine, ctx);\n const { contract, cellFor, subs } = engine;\n\n const trace: DriveTraceEntry<M, C>[] = [];\n let state = initial;\n let pending: M[] = [msg];\n let round = 0;\n\n try {\n subs?.reconcile(state);\n while (pending.length > 0) {\n round += 1;\n if (round > maxRounds) {\n throw new DriveRoundsExceededError<M, C>(maxRounds, round, trace);\n }\n\n // Drain the round's Msgs first, THEN its Cmds. Swapping the buffer before\n // the fold is what keeps a settle Msg out of the round that produced it.\n const folding = pending;\n pending = [];\n const emitted: C[] = [];\n for (const next of folding) {\n trace.push({ kind: \"msg\", msg: next });\n const [advanced, cmds] = bound.step(state, next);\n state = advanced;\n emitted.push(...cmds);\n subs?.reconcile(state);\n }\n\n for (const cmd of emitted) {\n trace.push({ kind: \"cmd\", cmd });\n const cell = cellFor(cmd.type);\n if (cell === undefined) {\n throw new DriveNoHandlerError<M, C>(cmd.type, trace);\n }\n // The dispatch half of ADR 0021, as the engine applies it (#304): a\n // `Cmd.define`d handler may not send a `_ok` / `_err` it built itself.\n // That Msg is dropped, never folded, and the refusal fails the drive\n // once the cell settles, so a handler that catches it cannot hide it.\n let refused: { readonly error: unknown } | undefined;\n const dispatch = (fired: M): void => {\n try {\n contract.checkDispatch(cmd, fired);\n } catch (error) {\n refused ??= { error };\n return;\n }\n pending.push(fired);\n };\n const settled = contract.settle(cmd, await cell(cmd, dispatch));\n if (refused !== undefined) throw refused.error;\n // A returned list lands in order, as `run`'s loop enqueues it.\n pending.push(...followUps<M>(settled));\n }\n\n if (pending.length === 0 && subs !== undefined) {\n pending = [...((await subs.next()) ?? [])];\n }\n }\n } catch (err) {\n // Unswallowed: the failure's own value is what leaves the loop, so a\n // test's `instanceof` / `_tag` branch still lands.\n annotateTrace(err, trace);\n throw err;\n } finally {\n await subs?.close();\n }\n\n return { state, trace };\n}\n\n/** The Subs `machine` wants at `state`, keyed by the id the engines derive. */\nexport function desiredSubs<S>(\n machine: { readonly subs?: ReadonlyArray<unknown> },\n state: S,\n): ReadonlyMap<string, Sub> {\n const desired = new Map<string, Sub>();\n for (const entry of subEntriesOf<S>(machine)) {\n const sub = desiredSub(entry, state);\n if (sub !== null) desired.set(sub.id, sub);\n }\n return desired;\n}\n"]}
1
+ {"version":3,"sources":["../src/testing/drive-loop.ts"],"names":[],"mappings":"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","file":"chunk-ETRNSJCR.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The round loop both `drive`s run — `@demlik/tea/testing/promise` and\n// `@demlik/tea/testing/effect` — said once, so the two cannot drift.\n//\n// `bindMachine` hands back the Cmds a fold emitted and stops there; performing\n// them is the caller's. So every consumer test that exercises a Cmd-emitting\n// machine hand-writes the same fixture: step the Msg, await the real interpret\n// handler over each emitted Cmd, feed the settle Msg back, repeat until quiet,\n// with a hand-picked guard bound in the loop header. `drive` is that fixture as\n// one call, and this file is its loop. It composes `bindMachine`'s synchronous\n// 2-arg `step` and adds NO second reducer path.\n//\n// What differs per engine is only how one Cmd is performed and whether Subs\n// run, so an engine hands this loop a `cellFor` lookup and, optionally, a\n// `subs` driver. Everything else — the rounds, the trace, the `maxRounds`\n// bound, the `Cmd.define` edge and its dispatch check (#304) — is here.\n//\n// **It returns the history, not just the endpoint.** `{ state, trace }`, where\n// `trace` is every Msg folded and every Cmd dispatched in order, lets a test\n// assert on the SEQUENCE as well as the settled state, and the same trace's\n// Msgs replayed through `replay` from the same initial state reproduce the\n// returned state exactly.\n//\n// **Exhausting `maxRounds` fails the drive.** A driver that quietly returns a\n// half-driven state lets a test assert green on a machine that never settled.\n//\n// Strengthens invariant 9 (the testing surface is named and small).\n// ---------------------------------------------------------------------------\n\nimport {\n type AnyCmdDef,\n type Cmd,\n type CmdContract,\n cmdContractOver,\n desiredSub,\n followUps,\n type Machine,\n type Sub,\n subEntriesOf,\n} from \"../pure/core\";\nimport type { CtxArg } from \"../runtime-types\";\nimport { bindMachine } from \"./bind-machine\";\n\n/**\n * The default round bound when `opts.maxRounds` is omitted: **100**.\n *\n * A round is one drain of the pending Msgs plus one drain of the Cmds those\n * folds emitted, so a settling machine costs a handful; 100 is loose enough\n * that a legitimate retry ladder finishes inside it and tight enough that a\n * genuine cycle stops rather than hangs.\n */\nexport const DEFAULT_MAX_ROUNDS = 100;\n\n/**\n * One entry of a driven run's history, in dispatch order.\n *\n * - `msg` — a Msg folded through the machine's reducer. The seed Msg is the\n * first entry of every trace; every later one is a settle Msg a handler\n * returned (or fired through its injected `dispatch`), or a Msg a running\n * Sub emitted.\n * - `cmd` — a Cmd handed to an interpret handler. It is recorded BEFORE the\n * handler runs, so a trace recovered from a failed handler names the Cmd\n * that failed as its last entry.\n */\nexport type DriveTraceEntry<M, C> =\n | { readonly kind: \"msg\"; readonly msg: M }\n | { readonly kind: \"cmd\"; readonly cmd: C };\n\n/** What a settled `drive` hands back. */\nexport interface DriveResult<S, M, C> {\n /** The state after the machine went quiet — no Msg pending, no Cmd unperformed. */\n readonly state: S;\n /**\n * Every Msg folded and every Cmd dispatched, in order. Filtering it to its\n * `msg` entries and replaying those through `replay` from the same initial\n * state reproduces {@link DriveResult.state}.\n */\n readonly trace: readonly DriveTraceEntry<M, C>[];\n}\n\n/**\n * `drive`'s `ctx` field. `CtxArg` widened by exactly one case: a machine\n * declaring `ctx: undefined` in its `types` block — the pure shape every\n * how-to on this package writes — may omit it too. `CtxArg` alone keeps `ctx`\n * REQUIRED there, because `{}` is not assignable to `undefined`, which would\n * make the common pure call site spell `{ ctx: undefined }` to say nothing.\n * A machine whose `Ctx` carries a field a handler reads still requires it.\n */\nexport type DriveCtxArg<Ctx> = [undefined] extends [Ctx]\n ? { readonly ctx?: Ctx }\n : CtxArg<Ctx>;\n\n/**\n * The options both `drive`s take. `ctx` is conditionally optional (see\n * {@link DriveCtxArg}).\n */\nexport type DriveOptions<Ctx> = DriveCtxArg<Ctx> & {\n /**\n * The round bound. Exceeding it fails the drive with\n * {@link DriveRoundsExceededError}; `drive` never returns a half-driven\n * state. Defaults to {@link DEFAULT_MAX_ROUNDS} (100).\n */\n readonly maxRounds?: number;\n /**\n * The clock that stamps `at` on a `Cmd.define`d Cmd's minted `_ok` / `_err`\n * Msg — the one `run` takes. Defaults to `Date.now`; pin it for a test that\n * asserts on `at`.\n */\n readonly clock?: () => number;\n};\n\n/**\n * Raised when a driven machine is still emitting work after `maxRounds` rounds.\n * Follows the tea error dialect (`override readonly name`) and carries a `_tag`\n * discriminant, so a test branches on it by `instanceof` or on the tag.\n *\n * It carries the round count it stopped at and the PARTIAL trace up to that\n * point, which is what a test needs to see the cycle — the bound alone says\n * only that one exists.\n */\nexport class DriveRoundsExceededError<M, C> extends Error {\n override readonly name = \"DriveRoundsExceededError\";\n readonly _tag = \"DriveRoundsExceededError\" as const;\n constructor(\n /** The bound that was exceeded. */\n public readonly maxRounds: number,\n /** The round the driver stopped at — always `maxRounds + 1`. */\n public readonly rounds: number,\n /** Every Msg folded and Cmd dispatched before the driver gave up. */\n public readonly trace: readonly DriveTraceEntry<M, C>[],\n ) {\n super(\n `@demlik/tea: drive did not settle within ${maxRounds} round(s) — the ` +\n `machine was still emitting work at round ${rounds}. The partial ` +\n `trace (${trace.length} entr${trace.length === 1 ? \"y\" : \"ies\"}) is ` +\n `on the error's \\`trace\\` field.`,\n );\n }\n}\n\n/**\n * Raised when a Cmd reaches `drive` with no handler for its `type` in the\n * handler record. The record's mapped type makes that a compile error at a\n * well-typed call site; this is the runtime arm for a handler record assembled\n * dynamically or cast.\n */\nexport class DriveNoHandlerError<M, C> extends Error {\n override readonly name = \"DriveNoHandlerError\";\n readonly _tag = \"DriveNoHandlerError\" as const;\n constructor(\n /** The Cmd `type` nothing in the handler record answered. */\n public readonly cmdType: string,\n /** Every Msg folded and Cmd dispatched before the miss. */\n public readonly trace: readonly DriveTraceEntry<M, C>[],\n ) {\n super(\n `@demlik/tea: drive found no interpret handler for Cmd type ` +\n `\"${cmdType}\" — the handlers record must answer every Cmd the ` +\n `machine emits.`,\n );\n }\n}\n\n/** The key `drive` hangs a partial trace off a failure under. */\nconst TRACE_KEY = \"driveTrace\";\n\n/**\n * Hang `trace` off `err` on a non-enumerable property, so {@link driveTraceOf}\n * reads it back without changing how the error prints or serializes. A\n * non-object failure carries nothing.\n */\nexport function annotateTrace(err: unknown, trace: readonly unknown[]): void {\n if (typeof err !== \"object\" || err === null) return;\n Object.defineProperty(err, TRACE_KEY, {\n value: [...trace],\n enumerable: false,\n configurable: true,\n writable: true,\n });\n}\n\n/**\n * Read back the partial trace `drive` attached to an error a handler failed\n * with. Returns `undefined` for anything `drive` did not annotate.\n *\n * A failing handler's error leaves `drive` UNSWALLOWED — same value, same\n * `instanceof`, same `_tag` — so a test that already branches on the handler's\n * own error type keeps doing so. The trace rides along on a non-enumerable\n * property so it is recoverable without changing how the error prints or\n * serializes.\n */\nexport function driveTraceOf<M, C>(\n err: unknown,\n): readonly DriveTraceEntry<M, C>[] | undefined {\n if (typeof err !== \"object\" || err === null) return undefined;\n const carried = (err as Record<string, unknown>)[TRACE_KEY];\n return Array.isArray(carried)\n ? (carried as readonly DriveTraceEntry<M, C>[])\n : undefined;\n}\n\n// === The loop ===\n\n/**\n * Perform one Cmd and resolve with the handler's raw return, before the\n * `Cmd.define` edge reads it. The `dispatch` it is handed is already checked\n * (#304).\n */\nexport type DriveCell<M, C> = (\n cmd: C,\n dispatch: (msg: M) => void,\n) => Promise<unknown>;\n\n/** The Subs a drive runs, as the loop sees them. */\nexport interface DriveSubs<S, M> {\n /**\n * Start every Sub `state` wants that is not running, and stop every running\n * one it no longer wants — the engine's reconcile, after each fold.\n */\n readonly reconcile: (state: S) => void;\n /**\n * Wait for the next Msgs a running Sub emits. Resolves with every Msg\n * buffered by then, or with `undefined` once no Sub is left running; rejects\n * when a Sub failed.\n */\n readonly next: () => Promise<readonly M[] | undefined>;\n /** Stop every Sub still running. */\n readonly close: () => Promise<void>;\n}\n\n/** What one engine hands the shared loop. */\nexport interface DriveEngine<S, M, C> {\n /**\n * The edge and dispatch check over the machine's `Cmd.define` list. The\n * engine builds it (see {@link driveContractOf}) so a handler that settles\n * through `cmdEdgeOf(ctx)` mints into the same record the check reads.\n */\n readonly contract: CmdContract;\n /** The cell that performs a Cmd of `type`, or `undefined` when none does. */\n readonly cellFor: (type: string) => DriveCell<M, C> | undefined;\n /** The Subs to run beside the Cmds; an engine that runs none omits it. */\n readonly subs?: DriveSubs<S, M>;\n}\n\n/** The contract both `drive`s hold a machine's Cmds to (ADR 0021). */\nexport function driveContractOf(\n machine: { readonly cmds?: readonly AnyCmdDef[] },\n clock: () => number,\n): CmdContract {\n return cmdContractOver(machine.cmds ?? [], clock);\n}\n\n/**\n * Drive `machine` from `initial` through `msg`, performing every emitted Cmd\n * through `engine.cellFor`, until the machine goes quiet.\n *\n * One round is: fold every pending Msg, then perform every Cmd those folds\n * emitted, in order. A Msg a cell returns or dispatches is pending for the next\n * round. When nothing is pending and Subs are running, the loop waits for the\n * next Msgs they emit; it settles once no Msg is pending, no Cmd is unperformed\n * and no Sub is left running.\n *\n * Every error that leaves the loop carries the partial trace (see\n * {@link driveTraceOf}).\n */\nexport async function driveLoop<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n initial: S,\n msg: M,\n ctx: Ctx,\n maxRounds: number,\n engine: DriveEngine<S, M, C>,\n): Promise<DriveResult<S, M, C>> {\n const bound = bindMachine(machine, ctx);\n const { contract, cellFor, subs } = engine;\n\n const trace: DriveTraceEntry<M, C>[] = [];\n let state = initial;\n let pending: M[] = [msg];\n let round = 0;\n\n try {\n subs?.reconcile(state);\n while (pending.length > 0) {\n round += 1;\n if (round > maxRounds) {\n throw new DriveRoundsExceededError<M, C>(maxRounds, round, trace);\n }\n\n // Drain the round's Msgs first, THEN its Cmds. Swapping the buffer before\n // the fold is what keeps a settle Msg out of the round that produced it.\n const folding = pending;\n pending = [];\n const emitted: C[] = [];\n for (const next of folding) {\n trace.push({ kind: \"msg\", msg: next });\n const [advanced, cmds] = bound.step(state, next);\n state = advanced;\n emitted.push(...cmds);\n subs?.reconcile(state);\n }\n\n for (const cmd of emitted) {\n trace.push({ kind: \"cmd\", cmd });\n const cell = cellFor(cmd.type);\n if (cell === undefined) {\n throw new DriveNoHandlerError<M, C>(cmd.type, trace);\n }\n // The dispatch half of ADR 0021, as the engine applies it (#304): a\n // `Cmd.define`d handler may not send a `_ok` / `_err` it built itself.\n // That Msg is dropped, never folded, and the refusal fails the drive\n // once the cell settles, so a handler that catches it cannot hide it.\n let refused: { readonly error: unknown } | undefined;\n const dispatch = (fired: M): void => {\n try {\n contract.checkDispatch(cmd, fired);\n } catch (error) {\n refused ??= { error };\n return;\n }\n pending.push(fired);\n };\n const settled = contract.settle(cmd, await cell(cmd, dispatch));\n if (refused !== undefined) throw refused.error;\n // A returned list lands in order, as `run`'s loop enqueues it.\n pending.push(...followUps<M>(settled));\n }\n\n if (pending.length === 0 && subs !== undefined) {\n pending = [...((await subs.next()) ?? [])];\n }\n }\n } catch (err) {\n // Unswallowed: the failure's own value is what leaves the loop, so a\n // test's `instanceof` / `_tag` branch still lands.\n annotateTrace(err, trace);\n throw err;\n } finally {\n await subs?.close();\n }\n\n return { state, trace };\n}\n\n/** The Subs `machine` wants at `state`, keyed by the id the engines derive. */\nexport function desiredSubs<S>(\n machine: { readonly subs?: ReadonlyArray<unknown> },\n state: S,\n): ReadonlyMap<string, Sub> {\n const desired = new Map<string, Sub>();\n for (const entry of subEntriesOf<S>(machine)) {\n const sub = desiredSub(entry, state);\n if (sub !== null) desired.set(sub.id, sub);\n }\n return desired;\n}\n"]}
@@ -0,0 +1,98 @@
1
+ import { replay, applyCell, acceptedTypes } from './chunk-A3QPMX5M.js';
2
+
3
+ // src/pbt/laws.ts
4
+ function verdict(answer, report) {
5
+ if (answer === true || answer === null) return null;
6
+ const reason = answer === false || answer === "" ? null : answer;
7
+ return { report: report(), reason };
8
+ }
9
+ function lawTerminates(fold, terminal) {
10
+ if (terminal(fold.finalState, fold.initCmds)) return null;
11
+ return {
12
+ report: `final state did not match terminal predicate. Final: ${JSON.stringify(fold.finalState)}`,
13
+ reason: null
14
+ };
15
+ }
16
+ function lawInvariant(fold, invariant) {
17
+ for (const step of fold.steps) {
18
+ const broken = verdict(
19
+ invariant(step, fold.initCmds),
20
+ () => `failed at step msg=${JSON.stringify(step.msg)} prev=${JSON.stringify(step.prev)} next=${JSON.stringify(step.next)} cmds=${JSON.stringify(step.cmds)}`
21
+ );
22
+ if (broken !== null) return broken;
23
+ }
24
+ return null;
25
+ }
26
+ function lawTrace(fold, predicate) {
27
+ return verdict(
28
+ predicate(fold.steps, fold.finalState, fold.initCmds),
29
+ () => `predicate failed on trace length=${fold.steps.length}, final=${JSON.stringify(fold.finalState)}`
30
+ );
31
+ }
32
+
33
+ // src/pbt/runners/replay-fold.ts
34
+ function foldEvents(machine, ctx, loaded, msgs) {
35
+ const { state: initialState, cmds: initCmds } = replay(machine, {
36
+ msgs: [],
37
+ ctx,
38
+ loaded: loaded ?? null
39
+ });
40
+ const states = [initialState];
41
+ const steps = [];
42
+ let current = initialState;
43
+ for (const msg of msgs) {
44
+ const [next, emitted] = applyCell(machine, current, msg);
45
+ steps.push({ prev: current, msg, next, cmds: [...emitted] });
46
+ states.push(next);
47
+ current = next;
48
+ }
49
+ return { states, steps, finalState: current, initCmds };
50
+ }
51
+
52
+ // src/pbt/arbitraries/guided-fold.ts
53
+ var MAX_PICK = 5039;
54
+ function resolveGuided(machine, ctx, loaded, answerers, events) {
55
+ const index = new Map(answerers.map(({ cmdType }, at) => [cmdType, at]));
56
+ const read = (event, waiting2) => {
57
+ if (event.kind === "outside") return { msg: event.msg, answers: null };
58
+ if (waiting2.length === 0) {
59
+ return event.otherwise === void 0 ? null : { msg: event.otherwise, answers: null };
60
+ }
61
+ const at = event.pick % waiting2.length;
62
+ const cmd = waiting2[at];
63
+ const cell = index.get(cmd.type);
64
+ const answerer = answerers[cell];
65
+ return { msg: answerer.make(cmd, event.values[cell]), answers: at };
66
+ };
67
+ const answerable = (cmds) => cmds.filter((cmd) => index.has(cmd.type));
68
+ const msgs = [];
69
+ const waiting = [];
70
+ let state;
71
+ try {
72
+ const boot = replay(machine, { msgs: [], ctx, loaded });
73
+ state = boot.state;
74
+ waiting.push(...answerable(boot.cmds));
75
+ } catch {
76
+ return msgs;
77
+ }
78
+ for (const event of events) {
79
+ const meant = read(event, waiting);
80
+ if (meant === null) continue;
81
+ const { msg, answers } = meant;
82
+ if (!acceptedTypes(machine, state).includes(msg.type)) continue;
83
+ if (answers !== null) waiting.splice(answers, 1);
84
+ msgs.push(msg);
85
+ try {
86
+ const [next, sent] = applyCell(machine, state, msg);
87
+ state = next;
88
+ waiting.push(...answerable(sent));
89
+ } catch {
90
+ return msgs;
91
+ }
92
+ }
93
+ return msgs;
94
+ }
95
+
96
+ export { MAX_PICK, foldEvents, lawInvariant, lawTerminates, lawTrace, resolveGuided };
97
+ //# sourceMappingURL=chunk-FXWDDNGD.js.map
98
+ //# sourceMappingURL=chunk-FXWDDNGD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pbt/laws.ts","../src/pbt/runners/replay-fold.ts","../src/pbt/arbitraries/guided-fold.ts"],"names":["waiting"],"mappings":"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","file":"chunk-FXWDDNGD.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The laws, as plain check functions over one fold (#749).\n//\n// A law reads what `foldEvents` handed back and answers `null` when it holds, or\n// a `LawBreak` saying what broke. It draws nothing and throws nothing of its\n// own, so any checker can run it: fast-check through `propertyTerminates`,\n// `propertyInvariant` and `propertyTrace`, which call these, or Effect's\n// `Arbitrary.checkEffect`, through `@demlik/tea/testing/effect`.\n//\n// This file imports neither `fast-check` nor `effect`, and\n// `src/entry-points.import-graph.test.ts` keeps it that way, so both entries\n// export it. The ruling: https://github.com/kamp-us/demlik/issues/749#issuecomment-6564369809\n// ---------------------------------------------------------------------------\n\nimport type { Fold, Step } from \"./runners/replay-fold\";\n\n/**\n * What a broken law says. `report` names the fold that broke it. `reason` is the\n * string the check returned to say why, or `null` when it returned `false`.\n *\n * @experimental\n */\nexport interface LawBreak {\n readonly report: string;\n readonly reason: string | null;\n}\n\n/**\n * Read what a check answered: `true` and `null` hold, `false` and any string\n * break. An empty string breaks with nothing to say, so it reads as `false`.\n */\nfunction verdict(\n answer: boolean | string | null,\n report: () => string,\n): LawBreak | null {\n if (answer === true || answer === null) return null;\n const reason = answer === false || answer === \"\" ? null : answer;\n return { report: report(), reason };\n}\n\n/**\n * The law `propertyTerminates` runs: the fold's final state is one `terminal`\n * accepts. `terminal` also gets the Cmds `init` sent.\n *\n * @example\n * ```ts\n * import type { Machine } from \"@demlik/tea\";\n * import { foldEvents, lawTerminates } from \"@demlik/tea/pbt\";\n *\n * type State = { type: \"idle\" } | { type: \"done\" };\n * type Msg = { type: \"finish\" };\n * declare const machine: Machine<State, Msg, never, never, object>;\n *\n * const fold = foldEvents(machine, {}, null, [{ type: \"finish\" }]);\n * lawTerminates(fold, (s) => s.type === \"done\"); // null when it holds\n * ```\n *\n * @experimental\n */\nexport function lawTerminates<S, M, C>(\n fold: Fold<S, M, C>,\n terminal: (state: NoInfer<S>, initCmds: readonly NoInfer<C>[]) => boolean,\n): LawBreak | null {\n if (terminal(fold.finalState, fold.initCmds)) return null;\n return {\n report:\n `final state did not match terminal predicate. ` +\n `Final: ${JSON.stringify(fold.finalState)}`,\n reason: null,\n };\n}\n\n/**\n * The law `propertyInvariant` runs: `invariant` holds on every step of the fold.\n * Return `true` or `null` to accept, `false` or a string saying why to break. It\n * stops at the first step that breaks, and the report names that step. The second\n * argument is the Cmds `init` sent.\n *\n * @example\n * ```ts\n * import type { Machine } from \"@demlik/tea\";\n * import { foldEvents, lawInvariant } from \"@demlik/tea/pbt\";\n *\n * type State = { readonly n: number };\n * type Msg = { type: \"inc\" };\n * declare const machine: Machine<State, Msg, never, never, object>;\n *\n * const fold = foldEvents(machine, {}, null, [{ type: \"inc\" }]);\n * lawInvariant(fold, ({ prev, next }) => next.n > prev.n || \"n did not grow\");\n * ```\n *\n * @experimental\n */\nexport function lawInvariant<S, M, C>(\n fold: Fold<S, M, C>,\n invariant: (\n step: Step<NoInfer<S>, NoInfer<M>, NoInfer<C>>,\n initCmds: readonly NoInfer<C>[],\n ) => boolean | string | null,\n): LawBreak | null {\n for (const step of fold.steps) {\n const broken = verdict(\n invariant(step, fold.initCmds),\n () =>\n `failed at step msg=${JSON.stringify(step.msg)} ` +\n `prev=${JSON.stringify(step.prev)} next=${JSON.stringify(step.next)} ` +\n `cmds=${JSON.stringify(step.cmds)}`,\n );\n if (broken !== null) return broken;\n }\n return null;\n}\n\n/**\n * The law `propertyTrace` runs: `predicate` holds on the fold's steps and final\n * state together. Return `true` or `null` to accept, `false` or a string saying\n * why to break. The last argument is the Cmds `init` sent.\n *\n * @example\n * ```ts\n * import type { Machine } from \"@demlik/tea\";\n * import { foldEvents, lawTrace } from \"@demlik/tea/pbt\";\n *\n * type State = { type: \"open\" } | { type: \"done\" };\n * type Msg = { type: \"close\" };\n * declare const machine: Machine<State, Msg, never, never, object>;\n *\n * const fold = foldEvents(machine, {}, null, [{ type: \"close\" }]);\n * lawTrace(fold, (steps, final) => final.type !== \"done\" || steps.length > 0);\n * ```\n *\n * @experimental\n */\nexport function lawTrace<S, M, C>(\n fold: Fold<S, M, C>,\n predicate: (\n steps: readonly Step<NoInfer<S>, NoInfer<M>, NoInfer<C>>[],\n finalState: NoInfer<S>,\n initCmds: readonly NoInfer<C>[],\n ) => boolean | string | null,\n): LawBreak | null {\n return verdict(\n predicate(fold.steps, fold.finalState, fold.initCmds),\n () =>\n `predicate failed on trace length=${fold.steps.length}, ` +\n `final=${JSON.stringify(fold.finalState)}`,\n );\n}\n","// ---------------------------------------------------------------------------\n// `foldEvents` — pure deterministic fold over a Msg sequence.\n//\n// PBT needs the per-step (prev/msg/next/cmds) structure to assert\n// step-shaped invariants. The substrate's `replay(machine, opts)` returns\n// only final state + cumulative cmds — insufficient. We use `replay` for\n// the boot step (calls `machine.init(loaded, ctx)` faithfully) then walk\n// msgs ourselves through `applyCell`, the substrate's own cell dispatch.\n//\n// Why walk cells directly instead of looping `replay` once per msg with\n// `loaded: prevState`? Because `init(loaded)` runs every call, and on\n// machines with non-trivial init transforms (e.g. the audit machine's\n// `auditing → attach_debugger` resume cell) that would either (a) re-fire\n// the transform on every step, or (b) require fragile cmd-subtraction to\n// isolate this step's cmds. Calling the cells directly keeps the fold pure\n// and the cmds per-step exactly.\n//\n// Dispatches through `applyCell` — the single primitive `run`/`replay` use —\n// so form classification agrees with production by construction (#275).\n//\n// `foldEvents` does NOT call `interpret`, does NOT touch a Store, does NOT\n// start Subs. The reducer surface is the contract.\n// ---------------------------------------------------------------------------\n\nimport {\n applyCell,\n type Cmd,\n type Machine,\n replay,\n type Sub,\n} from \"../../index\";\n\n/**\n * One unit in a fold trace — the four pieces of data a property\n * invariant typically asserts on. `prev` is the state before the msg\n * applied; `next` is the state after; `cmds` is the cmds emitted by this\n * single step (NOT the running total).\n */\nexport interface Step<S, M, C> {\n readonly prev: S;\n readonly msg: M;\n readonly next: S;\n readonly cmds: readonly C[];\n}\n\n/**\n * What {@link foldEvents} hands back: every state in order, one {@link Step} per\n * Msg, the final state, and the Cmds `init` sent. A law reads one of these.\n */\nexport interface Fold<S, M, C> {\n readonly states: readonly S[];\n readonly steps: readonly Step<S, M, C>[];\n readonly finalState: S;\n readonly initCmds: readonly C[];\n}\n\n/**\n * Fold a Msg sequence through a machine's pure reducer, returning the\n * per-step trace, every state in order, the final state, and the Cmds `init`\n * sent. Initial state comes from `replay(machine, { msgs: [], ctx, loaded })`\n * — i.e. `machine.init(loaded ?? null, ctx)` faithfully. Init's emitted cmds\n * are NOT included in any `Step` (they're the boot moment, not a Msg): they\n * come back as `initCmds`, on a fresh start and on a loaded state alike.\n *\n * @param machine The machine under test.\n * @param ctx The Ctx — typically `stubCtxThrowingProxy<Ctx>()` when\n * init is pure. Pass a real ctx if init reads from it.\n * @param loaded Optional pre-init snapshot (the `loaded` arg to\n * `machine.init`). Defaults to `null` (fresh boot).\n * @param msgs The Msg sequence to fold.\n *\n * @example\n * ```ts\n * import type { Machine } from \"@demlik/tea\";\n * import { foldEvents } from \"@demlik/tea/pbt\";\n *\n * type State = { type: \"idle\" } | { type: \"counting\"; n: number } | { type: \"stopped\" };\n * type Msg = { type: \"start\" } | { type: \"tick\" } | { type: \"stop\" };\n * interface Ctx { readonly tabId: number }\n * declare const machine: Machine<State, Msg, never, never, Ctx>;\n * declare const ctx: Ctx;\n *\n * const { states, steps, finalState } = foldEvents(machine, ctx, null, [\n * { type: \"start\" }, { type: \"tick\" }, { type: \"stop\" },\n * ]);\n * // steps[1] = { prev: { type: \"counting\", n: 0 }, msg: { type: \"tick\" }, ... }\n * ```\n */\nexport function foldEvents<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n loaded: NoInfer<S> | null,\n msgs: readonly NoInfer<M>[],\n): Fold<S, M, C> {\n // Boot through `replay` so `init` runs faithfully and we honor the\n // substrate's loaded-snapshot contract (init may transform the loaded\n // value before the first Msg lands — e.g. the audit machine flips\n // `debuggerAttached: false` on resume). With no msgs, its `cmds` are init's.\n const { state: initialState, cmds: initCmds } = replay(machine, {\n msgs: [],\n ctx,\n loaded: loaded ?? null,\n });\n\n // Per-msg dispatch goes through `applyCell` — the same primitive `run` and\n // `replay` use — so this runner classifies the update form identically to\n // production (`formOf` honors the `__form` tag; #275).\n const states: S[] = [initialState];\n const steps: Step<S, M, C>[] = [];\n let current = initialState;\n\n for (const msg of msgs) {\n const [next, emitted] = applyCell<S, M, C>(machine, current, msg);\n steps.push({ prev: current, msg, next, cmds: [...emitted] });\n states.push(next);\n current = next;\n }\n\n return { states, steps, finalState: current, initCmds };\n}\n","// ---------------------------------------------------------------------------\n// The guided fold: turn blind events into a Msg list in which every answer\n// answers a Cmd that was sent and not yet answered.\n//\n// Any generator can draw the blind events. `arbGuidedSequence` draws them with\n// fast-check, and `@demlik/tea/testing/effect` draws them with Effect's\n// `Arbitrary`, so this file imports neither: one rule, two generators.\n// The route: https://github.com/kamp-us/demlik/issues/745#issuecomment-6552209764\n// ---------------------------------------------------------------------------\n\nimport {\n acceptedTypes,\n applyCell,\n type Cmd,\n type Machine,\n replay,\n type Sub,\n} from \"../../index\";\n\n/**\n * One drawn event. It means nothing until the fold reads it. An answer event\n * may carry an `otherwise` Msg, which stands in for the answer when nothing is\n * waiting, so the event is not dropped.\n */\nexport type BlindEvent<M> =\n | { readonly kind: \"outside\"; readonly msg: M }\n | {\n readonly kind: \"answer\";\n readonly pick: number;\n readonly values: readonly unknown[];\n readonly otherwise?: M;\n };\n\n/**\n * Largest blind pick, read modulo the Cmds waiting. 5040 divides evenly by every\n * count from 1 to 10, so no Cmd is favoured among that many.\n */\nexport const MAX_PICK = 5039;\n\n/**\n * How one Cmd type's answer is made from a drawn value. An answer event holds\n * one value per answerer, in the order of the list the fold is given.\n */\nexport interface Answerer<C extends Cmd, M> {\n readonly cmdType: string;\n readonly make: (cmd: C, value: unknown) => M;\n}\n\n/**\n * Fold blind events through the machine into a Msg list. An answer event's pick\n * is read modulo the Cmds waiting at that point and names one of them; with\n * nothing waiting the answer is dropped, and the event's `otherwise` Msg, when it\n * carries one, goes in its place. An event whose Msg the state has no cell for is\n * dropped too, and a refused answer leaves its Cmd waiting.\n *\n * A boot or step that throws ends the list there, so a runner's own fold meets\n * the same throw and reports it as that list's failure.\n */\nexport function resolveGuided<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n loaded: NoInfer<S> | null,\n answerers: readonly Answerer<C, M>[],\n events: readonly BlindEvent<M>[],\n): readonly M[] {\n const index = new Map(answerers.map(({ cmdType }, at) => [cmdType, at]));\n\n /**\n * What an event means now: an answer to the waiting Cmd at `answers`, an outside\n * Msg (`answers` is `null`), or `null` when it is dropped.\n */\n const read = (\n event: BlindEvent<M>,\n waiting: readonly C[],\n ): { readonly msg: M; readonly answers: number | null } | null => {\n if (event.kind === \"outside\") return { msg: event.msg, answers: null };\n if (waiting.length === 0) {\n return event.otherwise === undefined\n ? null\n : { msg: event.otherwise, answers: null };\n }\n const at = event.pick % waiting.length;\n const cmd = waiting[at] as C;\n const cell = index.get(cmd.type) as number;\n const answerer = answerers[cell] as Answerer<C, M>;\n return { msg: answerer.make(cmd, event.values[cell]), answers: at };\n };\n const answerable = (cmds: readonly C[]): readonly C[] =>\n cmds.filter((cmd) => index.has(cmd.type));\n\n const msgs: M[] = [];\n const waiting: C[] = [];\n let state: S;\n try {\n const boot = replay(machine, { msgs: [], ctx, loaded });\n state = boot.state;\n waiting.push(...answerable(boot.cmds));\n } catch {\n return msgs; // The runner's fold boots too, and fails on this same throw.\n }\n for (const event of events) {\n const meant = read(event, waiting);\n if (meant === null) continue;\n const { msg, answers } = meant;\n // A refused answer takes nothing off `waiting`: its Cmd still has no answer.\n if (!acceptedTypes(machine, state).includes(msg.type)) continue;\n if (answers !== null) waiting.splice(answers, 1);\n msgs.push(msg);\n try {\n const [next, sent] = applyCell<S, M, C>(machine, state, msg);\n state = next;\n waiting.push(...answerable(sent));\n } catch {\n return msgs; // `msg` is last, so the runner's fold fails on this same throw.\n }\n }\n return msgs;\n}\n"]}
@@ -0,0 +1,10 @@
1
+ // src/internal/engine/builtin-runners.ts
2
+ function timer(sub, _ctx, dispatch) {
3
+ const handle = setTimeout(() => dispatch(sub.deps.msg), sub.deps.ms);
4
+ return () => clearTimeout(handle);
5
+ }
6
+ var builtinRunners = { timer };
7
+
8
+ export { builtinRunners };
9
+ //# sourceMappingURL=chunk-GFGOQIEC.js.map
10
+ //# sourceMappingURL=chunk-GFGOQIEC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/engine/builtin-runners.ts"],"names":[],"mappings":";AAcA,SAAS,KAAA,CACP,GAAA,EACA,IAAA,EACA,QAAA,EACS;AACT,EAAA,MAAM,MAAA,GAAS,UAAA,CAAW,MAAM,QAAA,CAAS,GAAA,CAAI,KAAK,GAAG,CAAA,EAAG,GAAA,CAAI,IAAA,CAAK,EAAE,CAAA;AACnE,EAAA,OAAO,MAAM,aAAa,MAAM,CAAA;AAClC;AAGO,IAAM,cAAA,GAAiB,EAAE,KAAA","file":"chunk-GFGOQIEC.js","sourcesContent":["/**\n * The built-in Sub runners in their plain shape, the Promise engine's (#270\n * R1.1). A machine declares a built-in Sub by type alone — `{ type: \"timer\", deps:\n * (s) => ({ ms, msg }) }` — and `run` starts it with the runner here unless\n * `subscribe` names one of the same type, which replaces it (#270 R2.1). `/react`\n * reads them too, because a hidden component's built-in Sub has to stop as well.\n */\n\nimport type { BuiltinSubType, Dispose, TimerSub } from \"../../pure/core\";\n\n/**\n * `timer`: dispatch `deps.msg` once, `deps.ms` after the Sub starts. The engine\n * restarts it when `ms` or `msg` changes and clears it when the Sub stops.\n */\nfunction timer<M>(\n sub: TimerSub<M>,\n _ctx: unknown,\n dispatch: (msg: M) => void,\n): Dispose {\n const handle = setTimeout(() => dispatch(sub.deps.msg), sub.deps.ms);\n return () => clearTimeout(handle);\n}\n\n/** Every built-in runner, keyed by the Sub type it runs. */\nexport const builtinRunners = { timer } as const satisfies Record<\n BuiltinSubType,\n unknown\n>;\n"]}
@@ -1,5 +1,5 @@
1
1
  import { undefinedOnly, unchecked } from './chunk-4LPY67E4.js';
2
- import { Cmd } from './chunk-RKVUVALV.js';
2
+ import { Cmd } from './chunk-A3QPMX5M.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-2R62Q5UK.js.map
64
- //# sourceMappingURL=chunk-2R62Q5UK.js.map
63
+ //# sourceMappingURL=chunk-IAHEYPYJ.js.map
64
+ //# sourceMappingURL=chunk-IAHEYPYJ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/persistence/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-2R62Q5UK.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
+ {"version":3,"sources":["../src/internal/persistence/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-IAHEYPYJ.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. A pure restored `init` may return\n * resume State and Cmds, including a load Cmd from `requestLoad`. A host `boot`\n * Msg after `run(...)` is an optional protocol; Subs are already open then.\n * Fold the recovered payload into the run slice, then continue.\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). A pure\n * restored `init` may use this to return resume State and Cmds. A host `boot`\n * Msg after `run(...)` is an optional protocol; Subs are already open then.\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: call from pure restored `init` to return\n * resume State and the load Cmd, or from an optional host `boot` Msg after\n * `run(...)`, when Subs are already open.\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,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-RKVUVALV.js';
4
+ import { Cmd } from './chunk-A3QPMX5M.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-NGYFBD5G.js.map
106
- //# sourceMappingURL=chunk-NGYFBD5G.js.map
105
+ //# sourceMappingURL=chunk-J5JNLXO7.js.map
106
+ //# sourceMappingURL=chunk-J5JNLXO7.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/idempotency/idempotent-intake/index.ts"],"names":[],"mappings":";;;;;;AAoKO,SAAS,gBAAA,GAAsB;AACpC,EAAA,OAAO,GAAA,CAAI,OAAO,gBAAA,EAAkB;AAAA,IAClC,OAAO,SAAA,EAOJ;AAAA,IACH,EAAA,EAAI,aAAA;AAAA,IACJ,KAAK;AAAC,GACP,CAAA;AACH;AAWO,SAAS,eAAA,GAAqB;AACnC,EAAA,OAAO,GAAA,CAAI,OAAO,eAAA,EAAiB;AAAA,IACjC,OAAO,SAAA,EAKJ;AAAA,IACH,EAAA,EAAI,aAAA;AAAA,IACJ,KAAK;AAAC,GACP,CAAA;AACH;AAiBO,SAAS,aAAmB,MAAA,EAAyB;AAC1D,EAAA,MAAM,EAAE,KAAA,EAAO,KAAA,EAAM,GAAI,MAAA;AAMzB,EAAA,MAAM,QAAQ,YAAA,EAAgB;AAC9B,EAAA,MAAM,SAAS,iBAAA,EAAkC;AAGjD,EAAA,MAAM,gBAAgB,gBAAA,EAAoB;AAC1C,EAAA,MAAM,eAAe,eAAA,EAAmB;AAQxC,EAAA,SAAS,IAAA,GAA0B;AACjC,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,OAAO,IAAA,EAAK;AAAA,MAClB,OAAO;AAAC,KACV;AAAA,EACF;AAiBA,EAAA,SAAS,SAAA,CACP,KAAA,EACA,GAAA,EACA,EAAA,EAC4B;AAC5B,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAChC,IAAA,IAAI,MAAA,KAAW,QAAW,OAAO,MAAA;AACjC,IAAA,MAAM,QAAQ,MAAA,CAAO,KAAA;AACrB,IAAA,IAAI,KAAA,CAAM,KAAA,KAAU,SAAA,EAAW,OAAO,KAAA;AACtC,IAAA,IAAI,UAAU,MAAA,IAAa,EAAA,GAAK,MAAA,CAAO,IAAA,IAAQ,OAAO,OAAO,MAAA;AAC7D,IAAA,OAAO,KAAA;AAAA,EACT;AAcA,EAAA,SAAS,aAAA,CAAc,OAA0B,EAAA,EAA2B;AAC1E,IAAA,OAAO,KAAA,CAAM,KAAK,OAAA,CAAQ,KAAA,CAAM,GAAG,KAAK,CAAC,CAAA,EAAG,KAAA,CAAM,KAAA,KAAU,MAAA;AAAA,EAC9D;AA0BA,EAAA,SAAS,OAAA,CACP,KAAA,EACA,OAAA,EACA,EAAA,EACA,EAAA,EAC0D;AAC1D,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,CAAA;AAIzB,IAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,KAAA,CAAM,IAAA,EAAM,KAAK,EAAE,CAAA;AAC3C,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,IAAI,KAAA,CAAM,UAAU,MAAA,EAAQ;AAE1B,QAAA,OAAO,CAAC,KAAA,EAAO,CAAC,YAAA,CAAa,EAAE,GAAA,EAAK,MAAA,EAAQ,KAAA,CAAM,MAAA,EAAQ,CAAC,CAAC,CAAA;AAAA,MAC9D;AAEA,MAAA,OAAO,CAAC,KAAA,EAAO,EAAE,CAAA;AAAA,IACnB;AAGA,IAAA,MAAM,QAAA,GAAW,MAAA,CAAO,QAAA,CAAS,KAAA,CAAM,IAAA,EAAM,KAAK,EAAE,KAAA,EAAO,SAAA,EAAU,EAAG,EAAE,CAAA;AAC1E,IAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,KAAS,KAAA,CAAM,OAAA;AAAA,MACtC,KAAA,CAAM,KAAA;AAAA,MACN,OAAA;AAAA,MACA,EAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,MAAM,SAAA,GAA+B,EAAE,IAAA,EAAM,QAAA,EAAU,OAAO,SAAA,EAAU;AACxE,IAAA,OAAO,CAAC,SAAA,EAAW,CAAC,aAAA,CAAc,EAAE,GAAA,EAAK,MAAA,EAAQ,IAAA,CAAK,EAAA,EAAI,OAAA,EAAS,CAAC,CAAC,CAAA;AAAA,EACvE;AAkBA,EAAA,SAAS,QAAA,CACP,KAAA,EACA,GAAA,EACA,MAAA,EACA,EAAA,EAC0D;AAC1D,IAAA,MAAM,WAAW,MAAA,CAAO,QAAA;AAAA,MACtB,KAAA,CAAM,IAAA;AAAA,MACN,GAAA;AAAA,MACA,EAAE,KAAA,EAAO,MAAA,EAAQ,MAAA,EAAO;AAAA,MACxB;AAAA,KACF;AACA,IAAA,OAAO,CAAC,EAAE,GAAG,KAAA,EAAO,MAAM,QAAA,EAAS,EAAG,EAAE,CAAA;AAAA,EAC1C;AAuBA,EAAA,SAAS,KACP,KAAA,EAC0D;AAM1D,IAAA,IAAI,QAAQ,KAAA,CAAM,KAAA;AAClB,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,KAAA,MAAW,EAAA,IAAM,MAAM,KAAA,EAAO;AAC5B,MAAA,IAAI,EAAA,CAAG,WAAW,SAAA,EAAW;AAG7B,MAAA,IAAI,aAAA,CAAc,KAAA,EAAO,EAAE,CAAA,EAAG;AAC9B,MAAA,MAAM,OAAO,KAAA,CAAM,KAAA,CAAM,OAAO,EAAA,CAAG,EAAA,EAAI,CAAC,IAAA,MAAU;AAAA,QAChD,GAAG,IAAA;AAAA,QACH,MAAA,EAAQ,SAAA;AAAA,QACR,SAAA,EAAW;AAAA,OACb,CAAE,CAAA;AACF,MAAA,KAAA,GAAQ,IAAA,CAAK,IAAA;AACb,MAAA,OAAA,GAAU,WAAW,IAAA,CAAK,OAAA;AAAA,IAC5B;AACA,IAAA,IAAI,CAAC,OAAA,EAAS,OAAO,CAAC,KAAA,EAAO,EAAE,CAAA;AAC/B,IAAA,OAAO,CAAC,EAAE,GAAG,KAAA,EAAO,OAAO,KAAA,EAAM,EAAG,EAAE,CAAA;AAAA,EACxC;AAoBA,EAAA,SAAS,SAAA,CACP,OACA,EAAA,EAIO;AAOP,IAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,IAAA;AAAA,MACzB,CAAC,OAAO,EAAA,CAAG,MAAA,KAAW,aAAa,CAAC,aAAA,CAAc,OAAO,EAAE;AAAA,KAC7D;AACA,IAAA,IAAI,MAAA,KAAW,QAAW,OAAO,IAAA;AAGjC,IAAA,IAAI,OAAA,GAAwB,MAAA;AAC5B,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,KAAA,CAAM,KAAA,CAAM,MAAM,KAAA,EAAO,MAAA,CAAO,EAAA,EAAI,CAAC,IAAA,KAAS;AAC7D,MAAA,OAAA,GAAU,EAAE,GAAG,IAAA,EAAM,MAAA,EAAQ,SAAA,EAAW,WAAW,EAAA,EAAG;AACtD,MAAA,OAAO,OAAA;AAAA,IACT,CAAC,CAAA;AACD,IAAA,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,KAAA,EAAO,IAAA,IAAQ,OAAA,EAAQ;AAAA,EACrD;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,OAAA,EAAS,QAAA,EAAU,MAAM,SAAA,EAAU;AACpD","file":"chunk-NGYFBD5G.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
+ {"version":3,"sources":["../src/internal/idempotency/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-J5JNLXO7.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 during restored init, returning its State and Cmds together,\n * or from a deliberate host boot Msg after `ready`. Choose one resume path.\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,8 +1,8 @@
1
- import { initCache, deadlineSub, nextTimer, get, set } from './chunk-TAKPC6SD.js';
1
+ import { initCache, deadlineSub, nextTimer, get, set } from './chunk-UGTC33VH.js';
2
2
  import { asRng, recordFailure, shouldRetry, nextDelayMs, initRetry } from './chunk-VBDU4JL6.js';
3
3
  import { unchecked } from './chunk-4LPY67E4.js';
4
- import { liftSlice } from './chunk-DVNXEB7V.js';
5
- import { Cmd, without } from './chunk-RKVUVALV.js';
4
+ import { liftSlice } from './chunk-BVFS3ICC.js';
5
+ import { Cmd, without } from './chunk-A3QPMX5M.js';
6
6
 
7
7
  // src/internal/resilience/circuit-breaker/index.ts
8
8
  var defaultCircuitPolicy = {
@@ -391,5 +391,5 @@ function liftResilience(state, result) {
391
391
  }
392
392
 
393
393
  export { DEFAULT_RESILIENT_NAME, canPass, createResilientCall, defaultCircuitPolicy, initBucket, initCircuit, initWindow, liftResilience, onFailure, onSuccess, record, refill, remaining, runCmdDef, tryConsume };
394
- //# sourceMappingURL=chunk-72CN3VPY.js.map
395
- //# sourceMappingURL=chunk-72CN3VPY.js.map
394
+ //# sourceMappingURL=chunk-NFOGYDRS.js.map
395
+ //# sourceMappingURL=chunk-NFOGYDRS.js.map