@demlik/tea 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/agent/index.d.ts +16 -770
- package/dist/agent/index.js +18 -16
- package/dist/authed-call/index.d.ts +3 -3
- package/dist/authed-call/index.js +22 -36
- package/dist/authed-call/index.js.map +1 -1
- package/dist/await-terminal/index.d.ts +2 -2
- package/dist/await-terminal/index.js +2 -2
- package/dist/batch-window/index.d.ts +3 -3
- package/dist/batch-window/index.js +3 -3
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +5 -2
- package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
- package/dist/chunk-25LBDCBC.js.map +1 -0
- package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
- package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
- package/dist/{chunk-MMH7CW5K.js → chunk-BP2R7TPP.js} +216 -157
- package/dist/chunk-BP2R7TPP.js.map +1 -0
- package/dist/chunk-C6QC273X.js +71 -0
- package/dist/chunk-C6QC273X.js.map +1 -0
- package/dist/{chunk-7W4LMPUW.js → chunk-CAXMQJJ3.js} +17 -24
- package/dist/chunk-CAXMQJJ3.js.map +1 -0
- package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
- package/dist/chunk-D736QUGF.js.map +1 -0
- package/dist/chunk-EWJP3YYP.js +21 -0
- package/dist/chunk-EWJP3YYP.js.map +1 -0
- package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
- package/dist/chunk-FV3Z5MBE.js.map +1 -0
- package/dist/chunk-G5E74IJB.js +8 -0
- package/dist/chunk-G5E74IJB.js.map +1 -0
- package/dist/{chunk-27N2SPTN.js → chunk-IXBKFQH5.js} +12 -11
- package/dist/chunk-IXBKFQH5.js.map +1 -0
- package/dist/{chunk-SQTCS44W.js → chunk-JFFVBEDI.js} +3 -3
- package/dist/{chunk-SQTCS44W.js.map → chunk-JFFVBEDI.js.map} +1 -1
- package/dist/chunk-LYLDCIPH.js +365 -0
- package/dist/chunk-LYLDCIPH.js.map +1 -0
- package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
- package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
- package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
- package/dist/chunk-MUUROGIH.js.map +1 -0
- package/dist/{chunk-EHIX7NYZ.js → chunk-MVY5DAMF.js} +4 -4
- package/dist/{chunk-EHIX7NYZ.js.map → chunk-MVY5DAMF.js.map} +1 -1
- package/dist/{chunk-LP7VQBRG.js → chunk-NPERUUUE.js} +76 -95
- package/dist/chunk-NPERUUUE.js.map +1 -0
- package/dist/{chunk-QLA76GIT.js → chunk-PBDYW2TU.js} +68 -9
- package/dist/chunk-PBDYW2TU.js.map +1 -0
- package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
- package/dist/chunk-Q2UM7BXU.js.map +1 -0
- package/dist/chunk-RMX4EOQK.js +26 -0
- package/dist/chunk-RMX4EOQK.js.map +1 -0
- package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
- package/dist/chunk-S2EGRPCM.js.map +1 -0
- package/dist/{chunk-ON24RKSE.js → chunk-S4O26D7B.js} +7 -14
- package/dist/chunk-S4O26D7B.js.map +1 -0
- package/dist/{chunk-3XJWC7VW.js → chunk-SK5JGUY7.js} +3 -3
- package/dist/{chunk-3XJWC7VW.js.map → chunk-SK5JGUY7.js.map} +1 -1
- package/dist/{chunk-5DEQ5I3U.js → chunk-TPGASTFF.js} +8 -10
- package/dist/chunk-TPGASTFF.js.map +1 -0
- package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
- package/dist/chunk-UF4MZSLT.js.map +1 -0
- package/dist/{chunk-PS23H6KQ.js → chunk-VBUCHVCV.js} +6 -5
- package/dist/chunk-VBUCHVCV.js.map +1 -0
- package/dist/{chunk-S3FQN4SL.js → chunk-WGGAMW7Y.js} +191 -178
- package/dist/chunk-WGGAMW7Y.js.map +1 -0
- package/dist/chunk-ZJ5AOYBH.js +8 -0
- package/dist/chunk-ZJ5AOYBH.js.map +1 -0
- package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
- package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
- package/dist/circuit-breaker/index.d.ts +1 -0
- package/dist/circuit-breaker/index.js +1 -1
- package/dist/{core-BoB6t7dB.d.ts → core-42kapW0n.d.ts} +105 -6
- package/dist/deadline/index.d.ts +77 -11
- package/dist/deadline/index.js +3 -3
- package/dist/devtools/index.js +8 -28
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +483 -339
- package/dist/do/index.js +244 -214
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-CO9Uh971.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
- package/dist/extension/index.d.ts +25 -7
- package/dist/extension/index.js +4 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +4 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js +7 -10
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/fan-out/index.d.ts +10 -10
- package/dist/fan-out/index.js +3 -3
- package/dist/idempotent-intake/index.d.ts +2 -2
- package/dist/idempotent-intake/index.js +3 -3
- package/dist/index.d.ts +50 -472
- package/dist/index.js +2 -2
- package/dist/llm-call/index.d.ts +3 -2
- package/dist/llm-call/index.js +12 -10
- package/dist/machine-B_BF4jij.d.ts +804 -0
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/mem/index.d.ts +3 -2
- package/dist/mem/index.js.map +1 -1
- package/dist/monitored-run/index.d.ts +54 -30
- package/dist/monitored-run/index.js +7 -7
- package/dist/node/index.d.ts +3 -2
- package/dist/node/index.js +2 -3
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +2 -2
- package/dist/paginated-walk/index.js +11 -10
- package/dist/parity/index.d.ts +6 -5
- package/dist/parity/index.js +5 -4
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +3 -3
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -3
- package/dist/poller/index.d.ts +59 -17
- package/dist/poller/index.js +56 -20
- package/dist/poller/index.js.map +1 -1
- package/dist/prediction/index.d.ts +1 -1
- package/dist/prediction/index.js +2 -2
- package/dist/pure/index.d.ts +1 -1
- package/dist/pure/index.js +2 -2
- package/dist/rate-limit/index.d.ts +2 -2
- package/dist/rate-limit/index.js +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +2 -2
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +2 -2
- package/dist/reconciler/index.js +13 -15
- package/dist/reconciler/index.js.map +1 -1
- package/dist/recorder/index.d.ts +20 -20
- package/dist/recorder/index.js +1 -1
- package/dist/resilient-call/index.d.ts +31 -6
- package/dist/resilient-call/index.js +10 -9
- package/dist/retry-backoff/index.d.ts +191 -14
- package/dist/retry-backoff/index.js +1 -1
- package/dist/retry-to-success/index.js +1 -1
- package/dist/runtime-types-gsog27b6.d.ts +337 -0
- package/dist/saga/index.d.ts +3 -2
- package/dist/saga/index.js +2 -2
- package/dist/saga/index.js.map +1 -1
- package/dist/snapshot/index.d.ts +1 -1
- package/dist/snapshot/index.js +3 -3
- package/dist/subs/index.d.ts +372 -4
- package/dist/subs/index.js +4 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/throttled-input/index.d.ts +2 -2
- package/dist/throttled-input/index.js +14 -13
- package/dist/throttled-input/index.js.map +1 -1
- package/dist/token-refresh/index.d.ts +1 -1
- package/dist/token-refresh/index.js +3 -3
- package/dist/trace-replay/index.d.ts +12 -7
- package/dist/trace-replay/index.js +4 -3
- package/dist/{types-B-8bcAp8.d.ts → types-KbL3W1mj.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +1 -1
- package/dist/with-deadline/index.js +2 -2
- package/dist/with-resilience/index.d.ts +21 -2
- package/dist/with-resilience/index.js +12 -23
- package/dist/with-resilience/index.js.map +1 -1
- package/dist/with-telemetry/index.d.ts +1 -1
- package/dist/with-telemetry/index.js +2 -2
- package/dist/work-queue/adapter.d.ts +2 -2
- package/dist/work-queue/adapter.js +2 -2
- package/dist/work-queue/index.d.ts +3 -2
- package/dist/work-queue/index.js +1 -1
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +2 -2
- package/dist/work-queue/ops.js +1 -1
- package/dist/workflow/index.d.ts +51 -19
- package/dist/workflow/index.js +71 -52
- package/dist/workflow/index.js.map +1 -1
- package/package.json +4 -19
- package/dist/chunk-27N2SPTN.js.map +0 -1
- package/dist/chunk-2YOE5PIS.js.map +0 -1
- package/dist/chunk-3545BOYL.js.map +0 -1
- package/dist/chunk-5DEQ5I3U.js.map +0 -1
- package/dist/chunk-5VNWZADP.js.map +0 -1
- package/dist/chunk-6RGBKODV.js +0 -268
- package/dist/chunk-6RGBKODV.js.map +0 -1
- package/dist/chunk-7W4LMPUW.js.map +0 -1
- package/dist/chunk-DILMKOZ7.js +0 -91
- package/dist/chunk-DILMKOZ7.js.map +0 -1
- package/dist/chunk-K7Q23EQ4.js +0 -20
- package/dist/chunk-K7Q23EQ4.js.map +0 -1
- package/dist/chunk-LP7VQBRG.js.map +0 -1
- package/dist/chunk-MMH7CW5K.js.map +0 -1
- package/dist/chunk-NSDFA6M6.js +0 -198
- package/dist/chunk-NSDFA6M6.js.map +0 -1
- package/dist/chunk-ON24RKSE.js.map +0 -1
- package/dist/chunk-PS23H6KQ.js.map +0 -1
- package/dist/chunk-QLA76GIT.js.map +0 -1
- package/dist/chunk-S3FQN4SL.js.map +0 -1
- package/dist/chunk-SC3FAFZN.js.map +0 -1
- package/dist/chunk-TGG26XMK.js.map +0 -1
- package/dist/chunk-UV6UWGEO.js.map +0 -1
- package/dist/chunk-UYWUVEP6.js.map +0 -1
- package/dist/chunk-VLGARN6H.js +0 -202
- package/dist/chunk-VLGARN6H.js.map +0 -1
- package/dist/raft/demo.d.ts +0 -105
- package/dist/raft/demo.js +0 -172
- package/dist/raft/demo.js.map +0 -1
- package/dist/raft/index.d.ts +0 -548
- package/dist/raft/index.js +0 -7
- package/dist/raft/index.js.map +0 -1
- package/dist/raft/sim.d.ts +0 -222
- package/dist/raft/sim.js +0 -8
- package/dist/raft/sim.js.map +0 -1
- package/dist/raft/timeline.d.ts +0 -46
- package/dist/raft/timeline.js +0 -62
- package/dist/raft/timeline.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/workflow/index.ts"],"names":["id"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/workflow — the durable-workflow runtime core (#124, the first\n * Phase-1 slice of the Temporal-style durable-workflow engine, epic #118).\n *\n * A workflow is a long-running, multi-step transaction whose every step (an\n * **activity**) is a side effect that may evict the actor mid-flight. The\n * substrate already solves \"a state survives eviction\" (the event-sourced\n * store) and \"an owed effect survives eviction\" (the `do/` pending-effects\n * ledger, #67). This module composes those two into the third primitive: a\n * **pure TEA reducer over a workflow's progress**, where each step is dispatched\n * as a durable owed effect and the next step is decided only after the previous\n * one's result is folded back in. That is exactly Temporal's contract — a\n * deterministic workflow function whose activities are the only impurity, so a\n * replay of the event log re-decides every step identically.\n *\n * The mapping onto TEA, mirroring `../raft`:\n *\n * - **Reducer** — {@link createWorkflow}'s verbs (`init`, `onActivityOk`,\n * `onActivityErr`) are the transition function over {@link WorkflowState}.\n * PURE: they read no clock and no RNG. Wall time, randomness, retries — all\n * impurity lives in the consumer's injected *interpret cell* (the thing that\n * actually performs the activity `Cmd` and dispatches its result `Msg`).\n * A workflow driven by a recorded result schedule re-decides bit-for-bit —\n * the headline replay-determinism payoff (acceptance criterion 4).\n * - **Cmd** — the activity dispatch is an {@link ActivityCmd}, recorded on the\n * #67 ledger as an `effect_owed` BEFORE it is performed and `effect_confirmed`\n * on its result. This module emits the Cmd literals; it performs NO side\n * effects. The owed/confirmed events are persisted into the same log the\n * reducer folds, so the ledger (and thus \"which activity is still owed\")\n * rebuilds for free on cold wake — re-emit-on-wake is idempotent by the\n * ledger's monotonic delivery id (acceptance criterion 3).\n * - **Msg** — an activity result (`activity_ok` / `activity_err`) is a `Msg`\n * the consumer routes back into a verb. The verb folds the result, records\n * the completed step, and advances to the next activity (or to `completed`\n * when the sequence is exhausted).\n *\n * **Invalid states unrepresentable.** The in-flight activity (`current`, with\n * its delivery id) lives ONLY on the `running` variant of {@link WorkflowState}.\n * A `completed` workflow carries its final output and no current activity; a\n * `failed` one carries its failure and no current activity. So \"the current\n * activity exists only while running\" is a type-level guarantee, not a runtime\n * convention (acceptance criterion 1).\n *\n * **Boundary with `@demlik/tea/saga` — the compensation test.** Both drive an\n * ordered multi-step transaction with reverse-order compensation (#125 here);\n * the line between them is whether every step carries a true inverse. Here\n * {@link WorkflowStep.compensation} is OPTIONAL — an irreversible step (a sent\n * email) is simply SKIPPED during the unwind, and the run still reaches a\n * terminal state. Saga's `SagaStep<D, U>` REQUIRES both `do` and `undo` — a\n * step without its inverse is unrepresentable, so full reversibility is a\n * type-level guarantee. Pick by the test: **every step must be reversible →\n * saga; some steps are irreversible but the run must finish → workflow.**\n * Cross to saga when you want the type system to enforce that every step can\n * be undone; stay here when the transaction includes steps that can't be.\n *\n * This module is the pure reducer only — like `../raft/index.ts` is the pure\n * Raft reducer with the DO grain deferred to its `./do` sibling (here #126).\n *\n * Generic over three opaque payloads, like raft's command generic: the activity\n * descriptor `A` (what a step asks to do — the consumer's interpret cell knows\n * how to perform it), the activity result `R`, and the failure `F`. The reducer\n * never inspects them; it only sequences them.\n *\n * NOT a substrate primitive: it depends only on the core `Cmd` type and the\n * sibling `../do` durable-effects ledger. Consumers reach it via the\n * `@demlik/tea/workflow` subpath.\n */\n\nimport {\n type DeliveryId,\n type EffectConfirmed,\n type EffectLedgerEvent,\n type EffectOwed,\n emptyLedger,\n foldLedger,\n type PendingEffectsLedger,\n pendingEffectsLedger,\n survivingEffects,\n} from \"../do/durable-effects\";\nimport type { Cmd } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Steps — the static definition of a workflow.\n//\n// A workflow is defined by an ordered sequence of steps. A step is an activity\n// descriptor: a stable `name` (for readability + correlation in the event log)\n// plus the opaque `activity` payload the consumer's interpret cell performs.\n// Steps are inert data; the reducer walks them in order.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * One step of a workflow: a named activity descriptor. `A` is the opaque\n * payload the consumer's interpret cell knows how to perform (an HTTP call, a\n * sibling-grain message, a tool round-trip). The reducer never inspects\n * `activity` — it only sequences the steps and correlates results.\n *\n * A step may declare a **compensation** — the inverse activity that undoes the\n * forward one (cancel the reservation, refund the charge). Compensation is the\n * `{ do, undo }` pair of `@demlik/tea/saga` mapped onto the workflow: the step's\n * `activity` is the `do`, its `compensation` is the `undo`. The pair is the unit\n * of reversibility — a step that completed (its forward activity succeeded) is\n * exactly a step whose `compensation` must run, in reverse order, if a LATER\n * step fails (#125). A step WITHOUT a compensation contributes nothing to the\n * unwind: it is skipped during the reverse walk (an irreversible step — a sent\n * email — that the saga model treats as a no-op `undo`).\n */\nexport interface WorkflowStep<A> {\n /** Stable, human-readable step name — appears in the completed-step record. */\n readonly name: string;\n /** The opaque activity this step performs. Interpreted by the consumer. */\n readonly activity: A;\n /**\n * The opaque compensating (inverse) activity, performed in reverse order on a\n * downstream failure (#125). Optional: a step with no compensation is skipped\n * during the unwind. Interpreted by the consumer's interpret cell, exactly\n * like {@link activity}.\n */\n readonly compensation?: A;\n}\n\n/**\n * A completed step: the step that ran plus the result its activity produced.\n * The `running` state accumulates these in execution order, so the full\n * forward history is available to the consumer (and to #125's compensation,\n * which will walk it in reverse).\n */\nexport interface CompletedStep<A, R> {\n readonly step: WorkflowStep<A>;\n readonly result: R;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The in-flight activity — the bridge between the reducer and the #67 ledger.\n//\n// While `running`, exactly one activity is in flight. It carries the step it is\n// performing AND the monotonic `deliveryId` the #67 ledger issued for it. The\n// delivery id is the single correlation key: an `activity_ok`/`activity_err`\n// Msg names the id it answers, the reducer matches it against `current.id`, and\n// the ledger confirms that same id. Re-emit-on-wake re-fires the SAME id, so a\n// duplicate result is a no-op (idempotent by delivery id, acceptance crit. 3).\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** The activity currently in flight on a `running` workflow. */\nexport interface InFlightActivity<A> {\n /** The 0-based index of this step in the workflow's step sequence. */\n readonly index: number;\n /** The step whose activity is in flight. */\n readonly step: WorkflowStep<A>;\n /** The #67 ledger delivery id this activity was owed under (the dedup key). */\n readonly id: DeliveryId;\n}\n\n/**\n * The compensation currently in flight on a `compensating` workflow (#125). The\n * mirror of {@link InFlightActivity} for the reverse walk: exactly one\n * compensation is owed at a time, on the SAME #67 ledger, with its own monotonic\n * delivery id. `index` is the index (into `completed`/`steps`) of the step being\n * undone — the unwind walks these from high to low (strict reverse order).\n */\nexport interface InFlightCompensation<A> {\n /** The 0-based index (into the step sequence) of the step being compensated. */\n readonly index: number;\n /** The step whose compensation is in flight. */\n readonly step: WorkflowStep<A>;\n /** The #67 ledger delivery id this compensation was owed under (the dedup key). */\n readonly id: DeliveryId;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// WorkflowState — the discriminated union. Invalid states unrepresentable.\n//\n// `status` is the discriminant. The in-flight `current` activity exists ONLY on\n// `running` and `compensating`; `completed` carries the final `output`;\n// `failed` / `failed_compensated` / `compensation_failed` carry the failure.\n// There is no representable state with a current activity that is neither\n// running nor compensating, nor a terminal state still holding one\n// (acceptance crit. 1).\n//\n// The failure path (#125), mirroring `@demlik/tea/saga`'s phases:\n//\n// running ──activity_err with completed steps──▶ compensating\n// running ──activity_err with NO completed steps──▶ failed (empty rollback)\n// compensating ──all compensations confirmed──▶ failed_compensated\n// compensating ──a compensation itself fails──▶ compensation_failed\n//\n// `compensating` is the analogue of saga's `compensating`; `failed_compensated`\n// of saga's `aborted` (fully unwound); `compensation_failed` of saga's\n// `compensation_failed` (an `undo` bounced, partially unwound — visible state,\n// never a workflow wedged in `compensating` forever).\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * A workflow in progress. Carries the ordered `completed` steps (forward\n * history), the `steps` it is sequencing, and the `current` in-flight activity\n * (with its ledger delivery id). `completed.length === current.index` always\n * holds — the current activity is the next step after the last completed one.\n */\nexport interface RunningWorkflow<A, R> {\n readonly status: \"running\";\n /** The full, static step sequence this workflow runs. */\n readonly steps: readonly WorkflowStep<A>[];\n /** Steps that have produced a result, in execution order. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The single activity in flight right now. */\n readonly current: InFlightActivity<A>;\n}\n\n/**\n * A workflow that ran every step to completion. `output` is the result of the\n * final step — the workflow's overall output. No activity is in flight.\n */\nexport interface CompletedWorkflow<A, R> {\n readonly status: \"completed\";\n readonly steps: readonly WorkflowStep<A>[];\n /** Every step, in execution order, with its result. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The final step's result — the workflow's output. */\n readonly output: R;\n}\n\n/**\n * A workflow that failed on an activity with NOTHING to compensate — the\n * forward failure happened with zero completed steps (the first activity\n * failed). `failedStep` is the step whose activity reported the failure;\n * `failure` is the opaque failure payload. `completed` is empty by construction:\n * a failure WITH completed steps pivots into {@link CompensatingWorkflow}\n * instead, so `failed` is precisely the empty-rollback terminal (the analogue\n * of saga's `aborted` reached straight from the first step). No activity is in\n * flight; this is terminal (#125 acceptance criterion 3).\n */\nexport interface FailedWorkflow<A, R, F> {\n readonly status: \"failed\";\n readonly steps: readonly WorkflowStep<A>[];\n /** Empty by construction — a failure with completed steps compensates instead. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The step whose activity failed. */\n readonly failedStep: WorkflowStep<A>;\n /** The opaque failure the activity reported. */\n readonly failure: F;\n}\n\n/**\n * A workflow unwinding after a forward failure (#125): the compensations of the\n * `completed` steps are being emitted in STRICT REVERSE order, one at a time, on\n * the same #67 ledger. The analogue of saga's `compensating` phase.\n *\n * `current` is the single compensation in flight (the mirror of `running`'s\n * in-flight activity). `compensated` records, in unwind order, the completed\n * steps whose compensation has confirmed — the reverse-walk's progress log, so\n * a cold wake resumes mid-rollback exactly. `failedStep`/`failure` carry the\n * forward failure that triggered the unwind (verbatim, never interpreted),\n * preserved for the consumer to read and to fold onto the terminal state.\n *\n * Invariant: the unwind walks `completed` from its tail toward index 0, skipping\n * steps with no compensation, so `current.index` is strictly below the index of\n * every entry in `compensated`.\n */\nexport interface CompensatingWorkflow<A, R, F> {\n readonly status: \"compensating\";\n readonly steps: readonly WorkflowStep<A>[];\n /** The completed forward steps, in execution order — the history being unwound. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The step whose forward activity failed, triggering the unwind. */\n readonly failedStep: WorkflowStep<A>;\n /** The opaque forward failure that triggered the unwind. Carried, never interpreted. */\n readonly failure: F;\n /** The single compensation in flight right now. */\n readonly current: InFlightCompensation<A>;\n /** Completed steps whose compensation has confirmed, in unwind (reverse) order. */\n readonly compensated: readonly CompletedStep<A, R>[];\n}\n\n/**\n * A workflow that failed forward and then fully unwound: every completed step's\n * compensation confirmed, in reverse order. Terminal. The analogue of saga's\n * `aborted` (fully unwound) — named `failed_compensated` to align with #125's\n * vocabulary. No activity or compensation is in flight.\n */\nexport interface FailedCompensatedWorkflow<A, R, F> {\n readonly status: \"failed_compensated\";\n readonly steps: readonly WorkflowStep<A>[];\n /** The forward steps that completed (and were each compensated), in execution order. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The step whose forward activity failed, triggering the (now-finished) unwind. */\n readonly failedStep: WorkflowStep<A>;\n /** The opaque forward failure that triggered compensation. Carried, never interpreted. */\n readonly failure: F;\n}\n\n/**\n * A workflow whose ROLLBACK itself failed: a compensation activity reported a\n * failure mid-unwind. Terminal, PARTIALLY unwound — the analogue of saga's\n * `compensation_failed`. A failed compensation is a real-world fact (a refund\n * that bounced, a hold that won't release) that must become visible terminal\n * state, never a workflow wedged in `compensating` forever.\n *\n * `compensated` is left intact (the steps already rolled back); `uncompensated`\n * is the step whose compensation bounced PLUS the steps still owed a\n * compensation behind it — exactly what the consumer must reconcile by hand.\n * `compensationFailure` is the rollback failure (distinct from `failure`, the\n * forward failure that started the unwind), carried verbatim.\n */\nexport interface CompensationFailedWorkflow<A, R, F> {\n readonly status: \"compensation_failed\";\n readonly steps: readonly WorkflowStep<A>[];\n /** The full completed-forward history, in execution order. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The step whose forward activity failed, triggering the unwind. */\n readonly failedStep: WorkflowStep<A>;\n /** The opaque forward failure that triggered compensation. Carried, never interpreted. */\n readonly failure: F;\n /** Completed steps successfully compensated before the rollback broke, in unwind order. */\n readonly compensated: readonly CompletedStep<A, R>[];\n /** The step whose compensation bounced. Its forward effect stands un-reversed. */\n readonly failedCompensationStep: WorkflowStep<A>;\n /** The opaque rollback failure (distinct from {@link failure}). Carried, never interpreted. */\n readonly compensationFailure: F;\n}\n\n/**\n * The workflow's state — a discriminated union on `status`. The current\n * in-flight activity is reachable ONLY through `running`, and the in-flight\n * compensation ONLY through `compensating`, making \"an in-flight effect exists\n * only while the workflow is actively driving one\" unrepresentable to violate.\n */\nexport type WorkflowState<A, R, F> =\n | RunningWorkflow<A, R>\n | CompletedWorkflow<A, R>\n | FailedWorkflow<A, R, F>\n | CompensatingWorkflow<A, R, F>\n | FailedCompensatedWorkflow<A, R, F>\n | CompensationFailedWorkflow<A, R, F>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmds — the activity dispatch. Recorded on the #67 ledger as an owed effect.\n//\n// `ActivityCmd` is the single Cmd this module emits: \"perform the activity for\n// step `index`, and the result you dispatch back must carry delivery id `id`\".\n// The consumer's interpret cell performs `activity` and dispatches an\n// `ActivityOk`/`ActivityErr` Msg carrying the SAME id. The owed/confirmed\n// ledger events ride alongside (see {@link WorkflowStep0}/the step tuple).\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Dispatch the in-flight activity. Routed through the consumer's `interpret`\n * over an injected activity port. The reducer emits this literal; it performs\n * no side effect. The result MUST be dispatched back as a Msg carrying `id`.\n */\nexport interface ActivityCmd<A> extends Cmd<\"workflow_activity\"> {\n /** The 0-based step index this activity belongs to. */\n readonly index: number;\n /** The #67 delivery id the result Msg must echo (the dedup key). */\n readonly id: DeliveryId;\n /** The opaque activity to perform. */\n readonly activity: A;\n}\n\n/**\n * Dispatch a compensation (#125). Same shape + ledger discipline as\n * {@link ActivityCmd} — a compensation is just another durable owed effect on\n * the same #67 ledger, with its own delivery id — but the consumer's interpret\n * cell performs the inverse `compensation` payload, and the result is dispatched\n * back as a {@link CompensationOk}/{@link CompensationErr} carrying the SAME id.\n * Distinguished by `type` so the consumer routes forward vs. inverse work to the\n * right port, and the re-emit-on-wake stays type-correct.\n */\nexport interface CompensationCmd<A> extends Cmd<\"workflow_compensation\"> {\n /** The 0-based step index whose compensation this is. */\n readonly index: number;\n /** The #67 delivery id the result Msg must echo (the dedup key). */\n readonly id: DeliveryId;\n /** The opaque compensating (inverse) activity to perform. */\n readonly compensation: A;\n}\n\n/** The Cmd union this module emits: forward activity dispatches AND (#125)\n * reverse compensation dispatches. The owed/confirmed ledger events are not\n * `WorkflowCmd`s — they are Msg variants persisted into the event log (see\n * {@link EffectLedgerEvent}); the {@link WorkflowStep0} tuple carries them out\n * alongside the dispatch Cmd so the host persists owed-before-dispatch. Both\n * Cmd kinds are owed effects on the same ledger, so `survivingActivities`\n * re-emits either kind on cold wake. */\nexport type WorkflowCmd<A> = ActivityCmd<A> | CompensationCmd<A>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Msgs — activity results the consumer routes back into the reducer.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** An activity succeeded: `id` echoes the {@link ActivityCmd} it answers; the\n * reducer matches it against `current.id`, records the completed step, and\n * advances. */\nexport interface ActivityOk<R> {\n readonly type: \"activity_ok\";\n readonly id: DeliveryId;\n readonly result: R;\n}\n\n/** An activity failed (retries already exhausted by the consumer's interpret\n * cell — this module does not retry). `id` echoes the {@link ActivityCmd};\n * the reducer begins compensation (or settles `failed` when nothing has\n * completed). */\nexport interface ActivityErr<F> {\n readonly type: \"activity_err\";\n readonly id: DeliveryId;\n readonly failure: F;\n}\n\n/** A compensation succeeded (#125): the inverse activity took. `id` echoes the\n * {@link CompensationCmd} it answers; the reducer matches it against the\n * in-flight compensation's id, records the step as compensated, and either\n * emits the next compensation (continuing in reverse) or settles\n * `failed_compensated` when the unwind is complete. A compensation produces no\n * result the workflow carries — its only signal is \"the undo took\". */\nexport interface CompensationOk {\n readonly type: \"compensation_ok\";\n readonly id: DeliveryId;\n}\n\n/** A compensation itself failed (#125): the inverse activity bounced (a refund\n * that won't go through). `id` echoes the {@link CompensationCmd}; the reducer\n * halts the rollback at the terminal `compensation_failed` state — never wedged\n * in `compensating` forever. */\nexport interface CompensationErr<F> {\n readonly type: \"compensation_err\";\n readonly id: DeliveryId;\n readonly failure: F;\n}\n\n/** The Msg union the reducer folds: forward activity results AND (#125) reverse\n * compensation results. */\nexport type WorkflowMsg<R, F> =\n | ActivityOk<R>\n | ActivityErr<F>\n | CompensationOk\n | CompensationErr<F>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The step tuple — the reducer's output shape.\n//\n// A verb returns the next `WorkflowState` PLUS the side-effect intents the host\n// must enact, in the ORDER they must be enacted (owed-before-dispatch):\n//\n// - `ledger` — the `effect_owed` / `effect_confirmed` events to PERSIST into\n// the same event log the reducer folds. A `running`-emitting verb owes the\n// new activity (and a verb that folded a prior result confirms it). Persist\n// these BEFORE the dispatch Cmd (the #67 \"persist the intent before deliver\"\n// rule) so an eviction between persist and dispatch re-fires on wake.\n// - `cmds` — the activity dispatch Cmd(s). Empty on a terminal transition.\n//\n// This is the same `readonly [state, cmds]` discipline as raft's `RaftStep`,\n// extended with the ledger-events channel because activities ARE owed effects.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** A workflow reducer step: the next state, the ledger events to persist\n * (owed-before-dispatch), and the activity Cmds to dispatch. */\nexport interface WorkflowStep0<A, R, F> {\n readonly state: WorkflowState<A, R, F>;\n /** Ledger events to persist into the event log, in order, BEFORE `cmds`. */\n readonly ledger: readonly EffectLedgerEvent<WorkflowCmd<A>>[];\n /** Activity dispatch Cmds, after the ledger events are durable. */\n readonly cmds: readonly WorkflowCmd<A>[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// createWorkflow — the factory. Returns the hook bag (init/verbs), mirroring\n// how `createRaftNode` / `createAgent` shape their knobs. The factory owns the\n// #67 monotonic-id recorder so every dispatched activity gets a gap-free\n// delivery id; the recorder is rehydrated from the persisted ledger events on\n// cold wake (see `restore`) so ids resume without gaps.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** The hook bag returned by {@link createWorkflow}. Spread its verbs into a\n * machine; `init(steps)` seeds a fresh workflow and emits the first activity,\n * the verbs fold results and advance, and `survivingActivities` re-emits the\n * owed-but-unconfirmed dispatch on cold wake (idempotent by delivery id). */\nexport interface Workflow<A, R, F> {\n /**\n * Seed a fresh workflow over `steps` and dispatch its first activity.\n *\n * - Empty `steps` ⇒ a workflow with nothing to do completes only when it has\n * an output; with no steps there is no output, so an empty sequence is\n * rejected as a misuse (a workflow must have at least one step). The reducer\n * never reaches `completed` without a final result to carry.\n * - Otherwise → `running` with `current` = step 0, owed on the ledger.\n */\n init(steps: readonly WorkflowStep<A>[]): WorkflowStep0<A, R, F>;\n\n /**\n * Fold an activity success. If `msg.id` does not match the in-flight\n * activity's id (a stale/duplicate result from re-emit-on-wake, or a result\n * for an already-advanced step), the state is returned UNCHANGED with no\n * effects — idempotent by delivery id (acceptance criterion 3). Otherwise:\n * record the completed step, confirm the owed activity on the ledger, and\n * either advance to the next step (owing its activity) or transition to\n * `completed` carrying the final result.\n *\n * Only meaningful on `running`; a result arriving for a terminal workflow is\n * a no-op (the workflow is done — at-least-once tolerates the late echo).\n */\n onActivityOk(\n state: WorkflowState<A, R, F>,\n msg: ActivityOk<R>,\n ): WorkflowStep0<A, R, F>;\n\n /**\n * Fold an activity failure (#125). Same id-match idempotency guard as\n * {@link onActivityOk}. On a match: confirm the owed activity on the ledger,\n * then PIVOT into compensation — walk the completed-step history in strict\n * reverse and owe the first declared compensation as a durable effect on the\n * same ledger, transitioning to `compensating`. If NO completed step declares\n * a compensation (the empty-rollback edge — the first activity failed, or\n * every completed step is irreversible), settle `failed` directly with\n * nothing to unwind.\n */\n onActivityErr(\n state: WorkflowState<A, R, F>,\n msg: ActivityErr<F>,\n ): WorkflowStep0<A, R, F>;\n\n /**\n * Fold a compensation success (#125). Same id-match idempotency guard against\n * the in-flight compensation. On a match: confirm the owed compensation,\n * record the step as compensated, and continue the reverse walk — owe the\n * next declared compensation strictly below the step just undone, or settle\n * `failed_compensated` when the unwind is complete. Only meaningful on\n * `compensating`; a result for a terminal/forward workflow is a no-op.\n */\n onCompensationOk(\n state: WorkflowState<A, R, F>,\n msg: CompensationOk,\n ): WorkflowStep0<A, R, F>;\n\n /**\n * Fold a compensation failure (#125). Same id-match idempotency guard. On a\n * match: confirm the owed compensation and HALT the rollback at the terminal\n * `compensation_failed` state — a bounced compensation is visible terminal\n * state to reconcile by hand, never a workflow wedged in `compensating`\n * forever. Mirrors `@demlik/tea/saga`'s `undoErr`.\n */\n onCompensationErr(\n state: WorkflowState<A, R, F>,\n msg: CompensationErr<F>,\n ): WorkflowStep0<A, R, F>;\n\n /**\n * The owed-but-unconfirmed dispatch(es) to re-emit on activation — a forward\n * activity OR (#125) a compensation, rebuilt by folding the persisted ledger\n * events. On a fresh / fully-settled workflow this is empty. The re-fired Cmd\n * carries the SAME delivery id, so a duplicate result is a no-op at the\n * reducer (the id-match guard). This is the cold-wake re-emit that makes both\n * activities AND compensations exactly-once-observable despite the\n * at-least-once transport (acceptance criterion 2 & 3) — including a\n * compensation owed mid-rollback when the actor was evicted.\n */\n survivingActivities(\n ledgerEvents: Iterable<EffectLedgerEvent<WorkflowCmd<A>>>,\n ): readonly WorkflowCmd<A>[];\n}\n\n/**\n * Build a workflow hook bag. The factory captures NO clock and NO RNG — pure\n * bookkeeping over the injected #67 recorder, exactly like\n * `pendingEffectsLedger`. `restore` rehydrates the monotonic delivery-id\n * counter from the persisted ledger events on cold wake, so re-dispatched\n * activities resume their ids without gaps.\n *\n * @param restore - the persisted ledger events (read back from the log) and/or\n * the last issued delivery id, so the id counter resumes gap-free. Omit for a\n * fresh workflow.\n */\nexport function createWorkflow<A, R, F>(restore?: {\n readonly events?: Iterable<EffectLedgerEvent<WorkflowCmd<A>>>;\n readonly lastId?: DeliveryId;\n}): Workflow<A, R, F> {\n // The #67 monotonic-id recorder. It owns the gap-free `deliveryId` and folds\n // an in-memory mirror of the ledger from the same owed/confirmed events the\n // host persists. Reused verbatim — activities AND compensations ARE owed\n // effects on this one ledger (do NOT reinvent the ledger).\n const recorder = pendingEffectsLedger<WorkflowCmd<A>>(restore);\n\n /**\n * Owe + dispatch the activity for `step` at `index`: allocate its delivery\n * id, build the dispatch Cmd, and emit BOTH the `effect_owed` ledger event\n * (to persist first) and the dispatch Cmd. The owed effect IS the dispatch\n * Cmd, so a cold wake re-emits the identical dispatch by folding the ledger.\n *\n * Returns the running state's `current` plus the owed event + dispatch Cmd.\n */\n /** Index into a step sequence with a bounds-check narrowing away `undefined`\n * (`noUncheckedIndexedAccess`). Every call site has already proven the index\n * in range — this converts that proof into the non-optional type, throwing\n * only on a logic error that should be unreachable. */\n function stepAt(\n steps: readonly WorkflowStep<A>[],\n index: number,\n ): WorkflowStep<A> {\n const step = steps[index];\n if (step === undefined) {\n throw new Error(\n `createWorkflow: step index ${index} out of range (length ` +\n `${steps.length}) — a reducer bounds proof was violated.`,\n );\n }\n return step;\n }\n\n /**\n * Predict the next delivery id, build the FINAL dispatch Cmd with it via\n * `build`, owe that exact Cmd on the #67 ledger, and assert the issued id\n * equals the prediction.\n *\n * The dispatch Cmd needs the delivery id, and the ledger event carries that\n * Cmd as its `effect` payload — so the id and the Cmd are mutually dependent.\n * Resolve the cycle by predicting the next id (`lastId() + 1`, the recorder's\n * gap-free monotonic rule), building the final Cmd, then handing that exact\n * Cmd to `recorder.owe`. The recorder issues the predicted id and folds the\n * final Cmd into its mirror — so the mirror, the persisted `effect_owed`\n * event, and the dispatch Cmd all carry one identical id and payload (no\n * placeholder, no rebuild). Shared by the forward owe ({@link oweActivity})\n * and the reverse owe ({@link oweCompensation}) so both halves of the\n * transaction allocate ids on the SAME monotonic ledger.\n */\n function oweOnLedger<C extends WorkflowCmd<A>>(\n build: (id: DeliveryId) => C,\n ): {\n readonly id: DeliveryId;\n readonly owed: EffectOwed<WorkflowCmd<A>>;\n readonly cmd: C;\n } {\n const id = (recorder.lastId() + 1) as DeliveryId;\n const cmd = build(id);\n const { id: issued, event } = recorder.owe(cmd);\n // Defensive: the prediction must equal the issued id, or the ledger mirror\n // and the dispatch Cmd would diverge. This holds by the recorder's gap-free\n // monotonic contract; assert it rather than silently carry a mismatch.\n if (issued !== id) {\n throw new Error(\n `createWorkflow: delivery-id prediction (${id}) diverged from the ` +\n `recorder's issued id (${issued}). The #67 recorder's monotonic ` +\n \"counter is not gap-free as assumed — this is a substrate invariant \" +\n \"violation, not a recoverable condition.\",\n );\n }\n return { id, owed: event, cmd };\n }\n\n function oweActivity(\n step: WorkflowStep<A>,\n index: number,\n ): {\n readonly current: InFlightActivity<A>;\n readonly owed: EffectOwed<WorkflowCmd<A>>;\n readonly cmd: ActivityCmd<A>;\n } {\n const { id, owed, cmd } = oweOnLedger<ActivityCmd<A>>((id) => ({\n type: \"workflow_activity\",\n index,\n id,\n activity: step.activity,\n }));\n const current: InFlightActivity<A> = { index, step, id };\n return { current, owed, cmd };\n }\n\n /**\n * Owe + dispatch the COMPENSATION for the completed step at `index` (#125):\n * the reverse-walk mirror of {@link oweActivity}. The compensation is a\n * durable owed effect on the SAME #67 ledger, so a cold wake mid-rollback\n * re-emits the identical compensation dispatch by folding the ledger\n * (acceptance criterion 2). `step.compensation` is required to be present —\n * the caller (the reverse walk) only owes compensations for steps that\n * declared one, skipping the rest.\n */\n function oweCompensation(\n step: WorkflowStep<A>,\n compensation: A,\n index: number,\n ): {\n readonly current: InFlightCompensation<A>;\n readonly owed: EffectOwed<WorkflowCmd<A>>;\n readonly cmd: CompensationCmd<A>;\n } {\n const { id, owed, cmd } = oweOnLedger<CompensationCmd<A>>((id) => ({\n type: \"workflow_compensation\",\n index,\n id,\n compensation,\n }));\n const current: InFlightCompensation<A> = { index, step, id };\n return { current, owed, cmd };\n }\n\n /**\n * Begin (or continue) the reverse walk from `completed`, owing the next\n * compensation strictly BELOW `belowIndex` (exclusive) that declares one. The\n * single source of truth for \"what to undo next\", shared by the failure pivot\n * ({@link Workflow.onActivityErr}) and each `compensation_ok` step\n * ({@link Workflow.onCompensationOk}). Steps with no compensation are SKIPPED\n * (they contribute nothing to the unwind — saga's no-op `undo`).\n *\n * Returns the next compensation to owe (its in-flight record + ledger event +\n * dispatch Cmd), or `null` when no compensable step remains below `belowIndex`\n * — the unwind is complete and the caller settles `failed_compensated`.\n */\n function oweNextCompensation(\n completed: readonly CompletedStep<A, R>[],\n belowIndex: number,\n ): {\n readonly current: InFlightCompensation<A>;\n readonly owed: EffectOwed<WorkflowCmd<A>>;\n readonly cmd: CompensationCmd<A>;\n } | null {\n // Walk the completed history from just below `belowIndex` toward 0 (strict\n // reverse order), stopping at the first step that declares a compensation.\n for (let i = belowIndex - 1; i >= 0; i--) {\n const entry = completed[i];\n // `completed[i]` is the i-th completed step; `i` is also its step index\n // (completed is in execution order, contiguous from 0 up to the failure).\n if (entry !== undefined && entry.step.compensation !== undefined) {\n return oweCompensation(entry.step, entry.step.compensation, i);\n }\n }\n return null;\n }\n\n return {\n init(steps) {\n if (steps.length === 0) {\n throw new Error(\n \"createWorkflow.init: a workflow must have at least one step. An \" +\n \"empty step sequence has no final result to carry into `completed`, \" +\n \"so it is unrepresentable as a completed workflow — reject it at the \" +\n \"boundary rather than admit a resultless terminal state.\",\n );\n }\n const { current, owed, cmd } = oweActivity(stepAt(steps, 0), 0);\n const state: RunningWorkflow<A, R> = {\n status: \"running\",\n steps,\n completed: [],\n current,\n };\n return { state, ledger: [owed], cmds: [cmd] };\n },\n\n onActivityOk(state, msg) {\n // A result for a terminal workflow, or one whose id does not match the\n // in-flight activity, is a no-op — idempotent by delivery id. This is the\n // guard that makes re-emit-on-wake safe: a re-fired (already-folded)\n // activity's result arrives with an id that is no longer `current.id`.\n if (state.status !== \"running\" || msg.id !== state.current.id) {\n return { state, ledger: [], cmds: [] };\n }\n\n // Record the completed step and confirm the owed activity on the ledger.\n const confirmed: EffectConfirmed = confirm(state.current.id);\n const completedStep: CompletedStep<A, R> = {\n step: state.current.step,\n result: msg.result,\n };\n const completed = [...state.completed, completedStep];\n\n const nextIndex = state.current.index + 1;\n if (nextIndex >= state.steps.length) {\n // The final step's result is the workflow's output → `completed`.\n const done: CompletedWorkflow<A, R> = {\n status: \"completed\",\n steps: state.steps,\n completed,\n output: msg.result,\n };\n return { state: done, ledger: [confirmed], cmds: [] };\n }\n\n // Advance: owe + dispatch the next activity.\n const { current, owed, cmd } = oweActivity(\n stepAt(state.steps, nextIndex),\n nextIndex,\n );\n const running: RunningWorkflow<A, R> = {\n status: \"running\",\n steps: state.steps,\n completed,\n current,\n };\n // Persist the confirm of the prior activity AND the owe of the next, in\n // that order, before dispatching the next. (Confirm-then-owe keeps the\n // ledger holding at most one in-flight activity per workflow.)\n return { state: running, ledger: [confirmed, owed], cmds: [cmd] };\n },\n\n onActivityErr(state, msg) {\n if (state.status !== \"running\" || msg.id !== state.current.id) {\n return { state, ledger: [], cmds: [] };\n }\n const confirmed: EffectConfirmed = confirm(state.current.id);\n\n // #125: the forward failure PIVOTS into compensation. Mirroring saga's\n // `stepErr`: walk the completed history in strict reverse, owing the\n // first declared compensation as a durable effect on this SAME ledger.\n // `state.completed` is preserved precisely so this reverse walk has its\n // history.\n const next = oweNextCompensation(state.completed, state.completed.length);\n\n if (next === null) {\n // Empty rollback: no completed step declares a compensation (the first\n // activity failed, OR every completed step is irreversible). The\n // analogue of saga aborting straight from the first step — settle\n // `failed` directly, nothing to unwind (acceptance criterion 3).\n const failed: FailedWorkflow<A, R, F> = {\n status: \"failed\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.current.step,\n failure: msg.failure,\n };\n return { state: failed, ledger: [confirmed], cmds: [] };\n }\n\n // Pivot to `compensating`, emitting the first compensation. Confirm the\n // failed forward activity AND owe the first compensation, in that order,\n // before dispatching it (the same confirm-then-owe ledger discipline as\n // the forward advance — at most one in-flight effect per workflow).\n const compensating: CompensatingWorkflow<A, R, F> = {\n status: \"compensating\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.current.step,\n failure: msg.failure,\n current: next.current,\n compensated: [],\n };\n return {\n state: compensating,\n ledger: [confirmed, next.owed],\n cmds: [next.cmd],\n };\n },\n\n onCompensationOk(state, msg) {\n // A compensation result for a non-compensating workflow, or one whose id\n // does not match the in-flight compensation, is a no-op — idempotent by\n // delivery id, the same re-emit-on-wake guard as the forward verbs.\n if (state.status !== \"compensating\" || msg.id !== state.current.id) {\n return { state, ledger: [], cmds: [] };\n }\n\n // Confirm the owed compensation and record the step as compensated.\n const confirmed: EffectConfirmed = confirm(state.current.id);\n const undoneStep = state.completed[state.current.index];\n if (undoneStep === undefined) {\n throw new Error(\n `createWorkflow.onCompensationOk: compensation index ` +\n `${state.current.index} has no completed step — a reverse-walk ` +\n \"bounds proof was violated.\",\n );\n }\n const compensated = [...state.compensated, undoneStep];\n\n // Continue the reverse walk strictly below the step just undone, skipping\n // steps with no compensation.\n const next = oweNextCompensation(state.completed, state.current.index);\n\n if (next === null) {\n // Fully unwound → `failed_compensated` (saga's `aborted`).\n const settled: FailedCompensatedWorkflow<A, R, F> = {\n status: \"failed_compensated\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.failedStep,\n failure: state.failure,\n };\n return { state: settled, ledger: [confirmed], cmds: [] };\n }\n\n const compensating: CompensatingWorkflow<A, R, F> = {\n status: \"compensating\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.failedStep,\n failure: state.failure,\n current: next.current,\n compensated,\n };\n return {\n state: compensating,\n ledger: [confirmed, next.owed],\n cmds: [next.cmd],\n };\n },\n\n onCompensationErr(state, msg) {\n // Same id-match idempotency guard as the other verbs.\n if (state.status !== \"compensating\" || msg.id !== state.current.id) {\n return { state, ledger: [], cmds: [] };\n }\n // Confirm the owed compensation (the dispatch is no longer owed — it was\n // delivered and answered, just with a failure), then HALT the rollback at\n // the terminal `compensation_failed` state. Mirroring saga's `undoErr`: a\n // bounced compensation is a real-world fact the consumer must reconcile by\n // hand, never a workflow wedged in `compensating` forever. `compensated`\n // (already rolled back) is left intact for the consumer to read.\n const confirmed: EffectConfirmed = confirm(state.current.id);\n const halted: CompensationFailedWorkflow<A, R, F> = {\n status: \"compensation_failed\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.failedStep,\n failure: state.failure,\n compensated: state.compensated,\n failedCompensationStep: state.current.step,\n compensationFailure: msg.failure,\n };\n return { state: halted, ledger: [confirmed], cmds: [] };\n },\n\n survivingActivities(ledgerEvents) {\n // Rebuild the ledger by folding the persisted events (NOT a side table),\n // then the surviving owed effects ARE the dispatch Cmds to re-fire — each\n // owed effect's payload is its dispatch Cmd (a forward activity OR a\n // compensation), carrying the same id.\n const ledger: PendingEffectsLedger<WorkflowCmd<A>> =\n foldLedger(ledgerEvents);\n return survivingEffects(ledger).map((owed) => owed.effect);\n },\n };\n\n /** Confirm an owed activity (or compensation) on the recorder's ledger,\n * returning the `effect_confirmed` event to persist. The boolean (was-owed)\n * is not needed by the reducer — the id-match guard above already gates on\n * the in-flight effect — so it is discarded here. */\n function confirm(id: DeliveryId): EffectConfirmed {\n return recorder.confirm(id).event;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// foldWorkflow — the pure replay fold. The determinism contract (acceptance\n// criterion 4) in one function: fold a workflow's full event log — its `init`\n// steps plus the ordered activity-result Msgs — back into a `WorkflowState`,\n// reading no clock and no RNG. Two folds of the same log produce byte-identical\n// state, because every decision is a pure function of (state, msg) and the\n// delivery ids are reissued in the same order from the same fresh recorder.\n//\n// This is the workflow-level twin of `foldLedger`: the host's `replay()` over\n// the event-sourced store re-runs exactly this fold to rebuild a workflow on\n// cold wake. Kept here (not only in the host, #126) so the determinism contract\n// is provable against the pure core alone.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Replay a workflow from its event log: seed over `steps`, then fold each\n * activity-result `Msg` in order. Returns the final {@link WorkflowState}.\n *\n * Determinism: a freshly-constructed {@link createWorkflow} reissues delivery\n * ids monotonically from 0, and the reducer reads no ambient state — so two\n * calls with the same `steps` + `msgs` return deeply-equal states (and equal\n * `JSON.stringify`). This is the byte-identity property #124 pins.\n *\n * Note this folds ONLY the result Msgs; the `init` dispatch + per-step\n * owe/confirm ledger events are an internal effect channel reconstructed by the\n * fresh recorder, not part of the replayed Msg log — exactly as the activity\n * results are the only thing the consumer records and replays.\n */\nexport function foldWorkflow<A, R, F>(\n steps: readonly WorkflowStep<A>[],\n msgs: Iterable<WorkflowMsg<R, F>>,\n): WorkflowState<A, R, F> {\n const wf = createWorkflow<A, R, F>();\n let { state } = wf.init(steps);\n for (const msg of msgs) {\n // Route each result Msg to its verb. The forward results (`activity_*`) and\n // the reverse compensation results (`compensation_*`, #125) are folded by\n // the same fresh recorder, so a replay across a failure+compensation\n // sequence re-issues every delivery id in identical order — byte-identity\n // holds end to end (acceptance criterion 4).\n let step: WorkflowStep0<A, R, F>;\n switch (msg.type) {\n case \"activity_ok\":\n step = wf.onActivityOk(state, msg);\n break;\n case \"activity_err\":\n step = wf.onActivityErr(state, msg);\n break;\n case \"compensation_ok\":\n step = wf.onCompensationOk(state, msg);\n break;\n case \"compensation_err\":\n step = wf.onCompensationErr(state, msg);\n break;\n }\n state = step.state;\n }\n return state;\n}\n\n// Re-export the durable-effects types a consumer needs to persist + replay a\n// workflow's ledger (the owed/confirmed events, the delivery id, the empty\n// seed) without reaching into the `../do` subpath directly.\nexport {\n type DeliveryId,\n type EffectConfirmed,\n type EffectLedgerEvent,\n type EffectOwed,\n emptyLedger,\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/workflow/route.ts","../../src/workflow/index.ts"],"names":["id"],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// routeWorkflowMsg — the ONE WorkflowMsg → verb routing table.\n//\n// Two call sites fold a `WorkflowMsg` into a workflow's next step: the pure\n// replay `foldWorkflow` (a `switch`) and the durable grain's `defineMachine`\n// update map in `do.ts` (one cell per Msg tag). Both encode the SAME mapping\n// (`activity_ok` → `onActivityOk`, `activity_err` → `onActivityErr`,\n// `compensation_ok` → `onCompensationOk`, `compensation_err` →\n// `onCompensationErr`). Left duplicated, adding a fifth Msg variant meant\n// editing the routing decision in two places. This dispatcher owns the table\n// once; each call site delegates. The `switch` is exhaustive over the\n// `WorkflowMsg` union, so a new variant is a compile error here until routed.\n// ---------------------------------------------------------------------------\n\nimport type {\n Workflow,\n WorkflowMsg,\n WorkflowReducerStep,\n WorkflowState,\n} from \"./index\";\n\nexport function routeWorkflowMsg<A, R, F>(\n wf: Workflow<A, R, F>,\n state: WorkflowState<A, R, F>,\n msg: WorkflowMsg<R, F>,\n): WorkflowReducerStep<A, R, F> {\n switch (msg.type) {\n case \"activity_ok\":\n return wf.onActivityOk(state, msg);\n case \"activity_err\":\n return wf.onActivityErr(state, msg);\n case \"compensation_ok\":\n return wf.onCompensationOk(state, msg);\n case \"compensation_err\":\n return wf.onCompensationErr(state, msg);\n }\n}\n","/**\n * @packageDocumentation\n * @demlik/tea/workflow — the durable-workflow runtime core (#124, the first\n * Phase-1 slice of the Temporal-style durable-workflow engine, epic #118).\n *\n * A workflow is a long-running, multi-step transaction whose every step (an\n * **activity**) is a side effect that may evict the actor mid-flight. The\n * substrate already solves \"a state survives eviction\" (the event-sourced\n * store) and \"an owed effect survives eviction\" (the `do/` pending-effects\n * ledger, #67). This module composes those two into the third primitive: a\n * **pure TEA reducer over a workflow's progress**, where each step is dispatched\n * as a durable owed effect and the next step is decided only after the previous\n * one's result is folded back in. That is exactly Temporal's contract — a\n * deterministic workflow function whose activities are the only impurity, so a\n * replay of the event log re-decides every step identically.\n *\n * The mapping onto TEA, mirroring `../raft`:\n *\n * - **Reducer** — {@link createWorkflow}'s verbs (`init`, `onActivityOk`,\n * `onActivityErr`) are the transition function over {@link WorkflowState}.\n * PURE: they read no clock and no RNG. Wall time, randomness, retries — all\n * impurity lives in the consumer's injected *interpret cell* (the thing that\n * actually performs the activity `Cmd` and dispatches its result `Msg`).\n * A workflow driven by a recorded result schedule re-decides bit-for-bit —\n * the headline replay-determinism payoff (acceptance criterion 4).\n * - **Cmd** — the activity dispatch is an {@link ActivityCmd}, recorded on the\n * #67 ledger as an `effect_owed` BEFORE it is performed and `effect_confirmed`\n * on its result. This module emits the Cmd literals; it performs NO side\n * effects. The owed/confirmed events are persisted into the same log the\n * reducer folds, so the ledger (and thus \"which activity is still owed\")\n * rebuilds for free on cold wake — re-emit-on-wake is idempotent by the\n * ledger's monotonic delivery id (acceptance criterion 3).\n * - **Msg** — an activity result (`activity_ok` / `activity_err`) is a `Msg`\n * the consumer routes back into a verb. The verb folds the result, records\n * the completed step, and advances to the next activity (or to `completed`\n * when the sequence is exhausted).\n *\n * **Invalid states unrepresentable.** The in-flight activity (`current`, with\n * its delivery id) lives ONLY on the `running` variant of {@link WorkflowState}.\n * A `completed` workflow carries its final output and no current activity; a\n * `failed` one carries its failure and no current activity. So \"the current\n * activity exists only while running\" is a type-level guarantee, not a runtime\n * convention (acceptance criterion 1).\n *\n * **Boundary with `@demlik/tea/saga` — the compensation test.** Both drive an\n * ordered multi-step transaction with reverse-order compensation (#125 here);\n * the line between them is whether every step carries a true inverse. Here\n * {@link WorkflowStep.compensation} is OPTIONAL — an irreversible step (a sent\n * email) is simply SKIPPED during the unwind, and the run still reaches a\n * terminal state. Saga's `SagaStep<D, U>` REQUIRES both `do` and `undo` — a\n * step without its inverse is unrepresentable, so full reversibility is a\n * type-level guarantee. Pick by the test: **every step must be reversible →\n * saga; some steps are irreversible but the run must finish → workflow.**\n * Cross to saga when you want the type system to enforce that every step can\n * be undone; stay here when the transaction includes steps that can't be.\n *\n * This module is the pure reducer only — like `../raft/index.ts` is the pure\n * Raft reducer with the DO grain deferred to its `./do` sibling (here #126).\n *\n * Generic over three opaque payloads, like raft's command generic: the activity\n * descriptor `A` (what a step asks to do — the consumer's interpret cell knows\n * how to perform it), the activity result `R`, and the failure `F`. The reducer\n * never inspects them; it only sequences them.\n *\n * NOT a substrate primitive: it depends only on the core `Cmd` type and the\n * sibling `../do` durable-effects ledger. Consumers reach it via the\n * `@demlik/tea/workflow` subpath.\n */\n\nimport {\n type DeliveryId,\n type EffectConfirmed,\n type EffectLedgerEvent,\n type EffectOwed,\n emptyLedger,\n foldLedger,\n type PendingEffectsLedger,\n pendingEffectsLedger,\n survivingEffects,\n} from \"../do/durable-effects\";\nimport type { Cmd } from \"../index\";\nimport { routeWorkflowMsg } from \"./route\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Steps — the static definition of a workflow.\n//\n// A workflow is defined by an ordered sequence of steps. A step is an activity\n// descriptor: a stable `name` (for readability + correlation in the event log)\n// plus the opaque `activity` payload the consumer's interpret cell performs.\n// Steps are inert data; the reducer walks them in order.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * One step of a workflow: a named activity descriptor. `A` is the opaque\n * payload the consumer's interpret cell knows how to perform (an HTTP call, a\n * sibling-grain message, a tool round-trip). The reducer never inspects\n * `activity` — it only sequences the steps and correlates results.\n *\n * A step may declare a **compensation** — the inverse activity that undoes the\n * forward one (cancel the reservation, refund the charge). Compensation is the\n * `{ do, undo }` pair of `@demlik/tea/saga` mapped onto the workflow: the step's\n * `activity` is the `do`, its `compensation` is the `undo`. The pair is the unit\n * of reversibility — a step that completed (its forward activity succeeded) is\n * exactly a step whose `compensation` must run, in reverse order, if a LATER\n * step fails (#125). A step WITHOUT a compensation contributes nothing to the\n * unwind: it is skipped during the reverse walk (an irreversible step — a sent\n * email — that the saga model treats as a no-op `undo`).\n */\nexport interface WorkflowStep<A> {\n /** Stable, human-readable step name — appears in the completed-step record. */\n readonly name: string;\n /** The opaque activity this step performs. Interpreted by the consumer. */\n readonly activity: A;\n /**\n * The opaque compensating (inverse) activity, performed in reverse order on a\n * downstream failure (#125). Optional: a step with no compensation is skipped\n * during the unwind. Interpreted by the consumer's interpret cell, exactly\n * like {@link activity}.\n */\n readonly compensation?: A;\n}\n\n/**\n * A workflow's step sequence at construction — a NON-EMPTY tuple. A workflow\n * with nothing to do reaches `completed` only by carrying a final result, and an\n * empty sequence produces none, so \"≥ 1 step\" is a precondition. Encoding it as\n * `readonly [WorkflowStep<A>, ...WorkflowStep<A>[]]` makes the empty case a\n * COMPILE error at every construction boundary (`init`, `foldWorkflow`,\n * `workflowGrain`) — parse-don't-validate, so the reducer never needs a runtime\n * throw to reject a resultless workflow.\n */\nexport type WorkflowSteps<A> = readonly [WorkflowStep<A>, ...WorkflowStep<A>[]];\n\n/**\n * A completed step: the step that ran plus the result its activity produced.\n * The `running` state accumulates these in execution order, so the full\n * forward history is available to the consumer (and to #125's compensation,\n * which will walk it in reverse).\n */\nexport interface CompletedStep<A, R> {\n readonly step: WorkflowStep<A>;\n readonly result: R;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The in-flight activity — the bridge between the reducer and the #67 ledger.\n//\n// While `running`, exactly one activity is in flight. It carries the step it is\n// performing AND the monotonic `deliveryId` the #67 ledger issued for it. The\n// delivery id is the single correlation key: an `activity_ok`/`activity_err`\n// Msg names the id it answers, the reducer matches it against `current.id`, and\n// the ledger confirms that same id. Re-emit-on-wake re-fires the SAME id, so a\n// duplicate result is a no-op (idempotent by delivery id, acceptance crit. 3).\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** The activity currently in flight on a `running` workflow. */\nexport interface InFlightActivity<A> {\n /** The 0-based index of this step in the workflow's step sequence. */\n readonly index: number;\n /** The step whose activity is in flight. */\n readonly step: WorkflowStep<A>;\n /** The #67 ledger delivery id this activity was owed under (the dedup key). */\n readonly id: DeliveryId;\n}\n\n/**\n * The compensation currently in flight on a `compensating` workflow (#125). The\n * mirror of {@link InFlightActivity} for the reverse walk: exactly one\n * compensation is owed at a time, on the SAME #67 ledger, with its own monotonic\n * delivery id. `index` is the index (into `completed`/`steps`) of the step being\n * undone — the unwind walks these from high to low (strict reverse order).\n */\nexport interface InFlightCompensation<A> {\n /** The 0-based index (into the step sequence) of the step being compensated. */\n readonly index: number;\n /** The step whose compensation is in flight. */\n readonly step: WorkflowStep<A>;\n /** The #67 ledger delivery id this compensation was owed under (the dedup key). */\n readonly id: DeliveryId;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// WorkflowState — the discriminated union. Invalid states unrepresentable.\n//\n// `status` is the discriminant. The in-flight `current` activity exists ONLY on\n// `running` and `compensating`; `completed` carries the final `output`;\n// `failed` / `failed_compensated` / `compensation_failed` carry the failure.\n// There is no representable state with a current activity that is neither\n// running nor compensating, nor a terminal state still holding one\n// (acceptance crit. 1).\n//\n// The failure path (#125), mirroring `@demlik/tea/saga`'s phases:\n//\n// running ──activity_err with completed steps──▶ compensating\n// running ──activity_err with NO completed steps──▶ failed (empty rollback)\n// compensating ──all compensations confirmed──▶ failed_compensated\n// compensating ──a compensation itself fails──▶ compensation_failed\n//\n// `compensating` is the analogue of saga's `compensating`; `failed_compensated`\n// of saga's `aborted` (fully unwound); `compensation_failed` of saga's\n// `compensation_failed` (an `undo` bounced, partially unwound — visible state,\n// never a workflow wedged in `compensating` forever).\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * A workflow in progress. Carries the ordered `completed` steps (forward\n * history), the `steps` it is sequencing, and the `current` in-flight activity\n * (with its ledger delivery id). `completed.length === current.index` always\n * holds — the current activity is the next step after the last completed one.\n */\nexport interface RunningWorkflow<A, R> {\n readonly status: \"running\";\n /** The full, static step sequence this workflow runs. */\n readonly steps: readonly WorkflowStep<A>[];\n /** Steps that have produced a result, in execution order. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The single activity in flight right now. */\n readonly current: InFlightActivity<A>;\n}\n\n/**\n * A workflow that ran every step to completion. `output` is the result of the\n * final step — the workflow's overall output. No activity is in flight.\n */\nexport interface CompletedWorkflow<A, R> {\n readonly status: \"completed\";\n readonly steps: readonly WorkflowStep<A>[];\n /** Every step, in execution order, with its result. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The final step's result — the workflow's output. */\n readonly output: R;\n}\n\n/**\n * A workflow that failed on an activity with NOTHING to compensate — the\n * forward failure happened with zero completed steps (the first activity\n * failed). `failedStep` is the step whose activity reported the failure;\n * `failure` is the opaque failure payload. `completed` is empty by construction:\n * a failure WITH completed steps pivots into {@link CompensatingWorkflow}\n * instead, so `failed` is precisely the empty-rollback terminal (the analogue\n * of saga's `aborted` reached straight from the first step). No activity is in\n * flight; this is terminal (#125 acceptance criterion 3).\n */\nexport interface FailedWorkflow<A, R, F> {\n readonly status: \"failed\";\n readonly steps: readonly WorkflowStep<A>[];\n /** Empty by construction — a failure with completed steps compensates instead. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The step whose activity failed. */\n readonly failedStep: WorkflowStep<A>;\n /** The opaque failure the activity reported. */\n readonly failure: F;\n}\n\n/**\n * A workflow unwinding after a forward failure (#125): the compensations of the\n * `completed` steps are being emitted in STRICT REVERSE order, one at a time, on\n * the same #67 ledger. The analogue of saga's `compensating` phase.\n *\n * `current` is the single compensation in flight (the mirror of `running`'s\n * in-flight activity). `compensated` records, in unwind order, the completed\n * steps whose compensation has confirmed — the reverse-walk's progress log, so\n * a cold wake resumes mid-rollback exactly. `failedStep`/`failure` carry the\n * forward failure that triggered the unwind (verbatim, never interpreted),\n * preserved for the consumer to read and to fold onto the terminal state.\n *\n * Invariant: the unwind walks `completed` from its tail toward index 0, skipping\n * steps with no compensation, so `current.index` is strictly below the index of\n * every entry in `compensated`.\n */\nexport interface CompensatingWorkflow<A, R, F> {\n readonly status: \"compensating\";\n readonly steps: readonly WorkflowStep<A>[];\n /** The completed forward steps, in execution order — the history being unwound. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The step whose forward activity failed, triggering the unwind. */\n readonly failedStep: WorkflowStep<A>;\n /** The opaque forward failure that triggered the unwind. Carried, never interpreted. */\n readonly failure: F;\n /** The single compensation in flight right now. */\n readonly current: InFlightCompensation<A>;\n /** Completed steps whose compensation has confirmed, in unwind (reverse) order. */\n readonly compensated: readonly CompletedStep<A, R>[];\n}\n\n/**\n * A workflow that failed forward and then fully unwound: every completed step's\n * compensation confirmed, in reverse order. Terminal. The analogue of saga's\n * `aborted` (fully unwound) — named `failed_compensated` to align with #125's\n * vocabulary. No activity or compensation is in flight.\n */\nexport interface FailedCompensatedWorkflow<A, R, F> {\n readonly status: \"failed_compensated\";\n readonly steps: readonly WorkflowStep<A>[];\n /** The forward steps that completed (and were each compensated), in execution order. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The step whose forward activity failed, triggering the (now-finished) unwind. */\n readonly failedStep: WorkflowStep<A>;\n /** The opaque forward failure that triggered compensation. Carried, never interpreted. */\n readonly failure: F;\n}\n\n/**\n * A workflow whose ROLLBACK itself failed: a compensation activity reported a\n * failure mid-unwind. Terminal, PARTIALLY unwound — the analogue of saga's\n * `compensation_failed`. A failed compensation is a real-world fact (a refund\n * that bounced, a hold that won't release) that must become visible terminal\n * state, never a workflow wedged in `compensating` forever.\n *\n * `compensated` is left intact (the steps already rolled back); `uncompensated`\n * is the step whose compensation bounced PLUS the steps still owed a\n * compensation behind it — exactly what the consumer must reconcile by hand.\n * `compensationFailure` is the rollback failure (distinct from `failure`, the\n * forward failure that started the unwind), carried verbatim.\n */\nexport interface CompensationFailedWorkflow<A, R, F> {\n readonly status: \"compensation_failed\";\n readonly steps: readonly WorkflowStep<A>[];\n /** The full completed-forward history, in execution order. */\n readonly completed: readonly CompletedStep<A, R>[];\n /** The step whose forward activity failed, triggering the unwind. */\n readonly failedStep: WorkflowStep<A>;\n /** The opaque forward failure that triggered compensation. Carried, never interpreted. */\n readonly failure: F;\n /** Completed steps successfully compensated before the rollback broke, in unwind order. */\n readonly compensated: readonly CompletedStep<A, R>[];\n /** The step whose compensation bounced. Its forward effect stands un-reversed. */\n readonly failedCompensationStep: WorkflowStep<A>;\n /** The opaque rollback failure (distinct from {@link failure}). Carried, never interpreted. */\n readonly compensationFailure: F;\n}\n\n/**\n * The workflow's state — a discriminated union on `status`. The current\n * in-flight activity is reachable ONLY through `running`, and the in-flight\n * compensation ONLY through `compensating`, making \"an in-flight effect exists\n * only while the workflow is actively driving one\" unrepresentable to violate.\n */\nexport type WorkflowState<A, R, F> =\n | RunningWorkflow<A, R>\n | CompletedWorkflow<A, R>\n | FailedWorkflow<A, R, F>\n | CompensatingWorkflow<A, R, F>\n | FailedCompensatedWorkflow<A, R, F>\n | CompensationFailedWorkflow<A, R, F>;\n\n/** Every `status` discriminant of the {@link WorkflowState} union. */\nexport type WorkflowStatus = WorkflowState<unknown, unknown, unknown>[\"status\"];\n\n/**\n * The runtime accept-set of every {@link WorkflowStatus} — the single source of\n * truth the boundary snapshot parse keys off (see `do.ts`). Built from an\n * exhaustive `Record<WorkflowStatus, true>`, so adding a variant to the union\n * above without listing it here is a COMPILE error: the set can never silently\n * drift from the union again (the #312 regression, where the #125 compensation\n * statuses were missing and evicted-mid-rollback grains re-seeded fresh).\n */\nexport const WORKFLOW_STATUSES: ReadonlySet<string> = new Set(\n Object.keys({\n running: true,\n completed: true,\n failed: true,\n compensating: true,\n failed_compensated: true,\n compensation_failed: true,\n } satisfies Record<WorkflowStatus, true>),\n);\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmds — the activity dispatch. Recorded on the #67 ledger as an owed effect.\n//\n// `ActivityCmd` is the single Cmd this module emits: \"perform the activity for\n// step `index`, and the result you dispatch back must carry delivery id `id`\".\n// The consumer's interpret cell performs `activity` and dispatches an\n// `ActivityOk`/`ActivityErr` Msg carrying the SAME id. The owed/confirmed\n// ledger events ride alongside (see {@link WorkflowReducerStep}/the step tuple).\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Dispatch the in-flight activity. Routed through the consumer's `interpret`\n * over an injected activity port. The reducer emits this literal; it performs\n * no side effect. The result MUST be dispatched back as a Msg carrying `id`.\n */\nexport interface ActivityCmd<A> extends Cmd<\"workflow_activity\"> {\n /** The 0-based step index this activity belongs to. */\n readonly index: number;\n /** The #67 delivery id the result Msg must echo (the dedup key). */\n readonly id: DeliveryId;\n /** The opaque activity to perform. */\n readonly activity: A;\n}\n\n/**\n * Dispatch a compensation (#125). Same shape + ledger discipline as\n * {@link ActivityCmd} — a compensation is just another durable owed effect on\n * the same #67 ledger, with its own delivery id — but the consumer's interpret\n * cell performs the inverse `compensation` payload, and the result is dispatched\n * back as a {@link CompensationOk}/{@link CompensationErr} carrying the SAME id.\n * Distinguished by `type` so the consumer routes forward vs. inverse work to the\n * right port, and the re-emit-on-wake stays type-correct.\n */\nexport interface CompensationCmd<A> extends Cmd<\"workflow_compensation\"> {\n /** The 0-based step index whose compensation this is. */\n readonly index: number;\n /** The #67 delivery id the result Msg must echo (the dedup key). */\n readonly id: DeliveryId;\n /** The opaque compensating (inverse) activity to perform. */\n readonly compensation: A;\n}\n\n/** The Cmd union this module emits: forward activity dispatches AND (#125)\n * reverse compensation dispatches. The owed/confirmed ledger events are not\n * `WorkflowCmd`s — they are Msg variants persisted into the event log (see\n * {@link EffectLedgerEvent}); the {@link WorkflowReducerStep} tuple carries them out\n * alongside the dispatch Cmd so the host persists owed-before-dispatch. Both\n * Cmd kinds are owed effects on the same ledger, so `survivingActivities`\n * re-emits either kind on cold wake. */\nexport type WorkflowCmd<A> = ActivityCmd<A> | CompensationCmd<A>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Msgs — activity results the consumer routes back into the reducer.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** An activity succeeded: `id` echoes the {@link ActivityCmd} it answers; the\n * reducer matches it against `current.id`, records the completed step, and\n * advances. */\nexport interface ActivityOk<R> {\n readonly type: \"activity_ok\";\n readonly id: DeliveryId;\n readonly result: R;\n}\n\n/** An activity failed (retries already exhausted by the consumer's interpret\n * cell — this module does not retry). `id` echoes the {@link ActivityCmd};\n * the reducer begins compensation (or settles `failed` when nothing has\n * completed). */\nexport interface ActivityErr<F> {\n readonly type: \"activity_err\";\n readonly id: DeliveryId;\n readonly failure: F;\n}\n\n/** A compensation succeeded (#125): the inverse activity took. `id` echoes the\n * {@link CompensationCmd} it answers; the reducer matches it against the\n * in-flight compensation's id, records the step as compensated, and either\n * emits the next compensation (continuing in reverse) or settles\n * `failed_compensated` when the unwind is complete. A compensation produces no\n * result the workflow carries — its only signal is \"the undo took\". */\nexport interface CompensationOk {\n readonly type: \"compensation_ok\";\n readonly id: DeliveryId;\n}\n\n/** A compensation itself failed (#125): the inverse activity bounced (a refund\n * that won't go through). `id` echoes the {@link CompensationCmd}; the reducer\n * halts the rollback at the terminal `compensation_failed` state — never wedged\n * in `compensating` forever. */\nexport interface CompensationErr<F> {\n readonly type: \"compensation_err\";\n readonly id: DeliveryId;\n readonly failure: F;\n}\n\n/** The Msg union the reducer folds: forward activity results AND (#125) reverse\n * compensation results. */\nexport type WorkflowMsg<R, F> =\n | ActivityOk<R>\n | ActivityErr<F>\n | CompensationOk\n | CompensationErr<F>;\n\n/** Every `type` discriminant tag of the {@link WorkflowMsg} union. */\nexport type WorkflowMsgType = WorkflowMsg<unknown, unknown>[\"type\"];\n\n/**\n * The runtime accept-set of every {@link WorkflowMsgType} — the single source of\n * truth the boundary replay parse keys off (see `do.ts`). Built from an\n * exhaustive `Record<WorkflowMsgType, true>`, so adding a Msg variant to the\n * union above without listing it here is a COMPILE error: the set can never\n * silently drift from the union again (the #312 regression, where the #125\n * compensation result tags were missing and dropped from replay).\n */\nexport const WORKFLOW_MSG_TYPES: ReadonlySet<string> = new Set(\n Object.keys({\n activity_ok: true,\n activity_err: true,\n compensation_ok: true,\n compensation_err: true,\n } satisfies Record<WorkflowMsgType, true>),\n);\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The step tuple — the reducer's output shape.\n//\n// A verb returns the next `WorkflowState` PLUS the side-effect intents the host\n// must enact, in the ORDER they must be enacted (owed-before-dispatch):\n//\n// - `ledger` — the `effect_owed` / `effect_confirmed` events to PERSIST into\n// the same event log the reducer folds. A `running`-emitting verb owes the\n// new activity (and a verb that folded a prior result confirms it). Persist\n// these BEFORE the dispatch Cmd (the #67 \"persist the intent before deliver\"\n// rule) so an eviction between persist and dispatch re-fires on wake.\n// - `cmds` — the activity dispatch Cmd(s). Empty on a terminal transition.\n//\n// This is the same `readonly [state, cmds]` discipline as raft's `RaftStep`,\n// extended with the ledger-events channel because activities ARE owed effects.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** A workflow reducer step: the next state, the ledger events to persist\n * (owed-before-dispatch), and the activity Cmds to dispatch. */\nexport interface WorkflowReducerStep<A, R, F> {\n readonly state: WorkflowState<A, R, F>;\n /** Ledger events to persist into the event log, in order, BEFORE `cmds`. */\n readonly ledger: readonly EffectLedgerEvent<WorkflowCmd<A>>[];\n /** Activity dispatch Cmds, after the ledger events are durable. */\n readonly cmds: readonly WorkflowCmd<A>[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// createWorkflow — the factory. Returns the hook bag (init/verbs), mirroring\n// how `createRaftNode` / `createAgent` shape their knobs. The factory owns the\n// #67 monotonic-id recorder so every dispatched activity gets a gap-free\n// delivery id; the recorder is rehydrated from the persisted ledger events on\n// cold wake (see `restore`) so ids resume without gaps.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** The hook bag returned by {@link createWorkflow}. Spread its verbs into a\n * machine; `init(steps)` seeds a fresh workflow and emits the first activity,\n * the verbs fold results and advance, and `survivingActivities` re-emits the\n * owed-but-unconfirmed dispatch on cold wake (idempotent by delivery id). */\nexport interface Workflow<A, R, F> {\n /**\n * Seed a fresh workflow over `steps` (a NON-EMPTY {@link WorkflowSteps}) and\n * dispatch its first activity → `running` with `current` = step 0, owed on the\n * ledger. The \"≥ 1 step\" precondition is carried by the tuple type: an empty\n * sequence is a compile error at the call site, not a runtime throw here — a\n * workflow with nothing to do could never reach `completed` (no final result\n * to carry), so it is unrepresentable by construction.\n */\n init(steps: WorkflowSteps<A>): WorkflowReducerStep<A, R, F>;\n\n /**\n * Fold an activity success. If `msg.id` does not match the in-flight\n * activity's id (a stale/duplicate result from re-emit-on-wake, or a result\n * for an already-advanced step), the state is returned UNCHANGED with no\n * effects — idempotent by delivery id (acceptance criterion 3). Otherwise:\n * record the completed step, confirm the owed activity on the ledger, and\n * either advance to the next step (owing its activity) or transition to\n * `completed` carrying the final result.\n *\n * Only meaningful on `running`; a result arriving for a terminal workflow is\n * a no-op (the workflow is done — at-least-once tolerates the late echo).\n */\n onActivityOk(\n state: WorkflowState<A, R, F>,\n msg: ActivityOk<R>,\n ): WorkflowReducerStep<A, R, F>;\n\n /**\n * Fold an activity failure (#125). Same id-match idempotency guard as\n * {@link onActivityOk}. On a match: confirm the owed activity on the ledger,\n * then PIVOT into compensation — walk the completed-step history in strict\n * reverse and owe the first declared compensation as a durable effect on the\n * same ledger, transitioning to `compensating`. If NO completed step declares\n * a compensation (the empty-rollback edge — the first activity failed, or\n * every completed step is irreversible), settle `failed` directly with\n * nothing to unwind.\n */\n onActivityErr(\n state: WorkflowState<A, R, F>,\n msg: ActivityErr<F>,\n ): WorkflowReducerStep<A, R, F>;\n\n /**\n * Fold a compensation success (#125). Same id-match idempotency guard against\n * the in-flight compensation. On a match: confirm the owed compensation,\n * record the step as compensated, and continue the reverse walk — owe the\n * next declared compensation strictly below the step just undone, or settle\n * `failed_compensated` when the unwind is complete. Only meaningful on\n * `compensating`; a result for a terminal/forward workflow is a no-op.\n */\n onCompensationOk(\n state: WorkflowState<A, R, F>,\n msg: CompensationOk,\n ): WorkflowReducerStep<A, R, F>;\n\n /**\n * Fold a compensation failure (#125). Same id-match idempotency guard. On a\n * match: confirm the owed compensation and HALT the rollback at the terminal\n * `compensation_failed` state — a bounced compensation is visible terminal\n * state to reconcile by hand, never a workflow wedged in `compensating`\n * forever. Mirrors `@demlik/tea/saga`'s `undoErr`.\n */\n onCompensationErr(\n state: WorkflowState<A, R, F>,\n msg: CompensationErr<F>,\n ): WorkflowReducerStep<A, R, F>;\n\n /**\n * The owed-but-unconfirmed dispatch(es) to re-emit on activation — a forward\n * activity OR (#125) a compensation, rebuilt by folding the persisted ledger\n * events. On a fresh / fully-settled workflow this is empty. The re-fired Cmd\n * carries the SAME delivery id, so a duplicate result is a no-op at the\n * reducer (the id-match guard). This is the cold-wake re-emit that makes both\n * activities AND compensations exactly-once-observable despite the\n * at-least-once transport (acceptance criterion 2 & 3) — including a\n * compensation owed mid-rollback when the actor was evicted.\n */\n survivingActivities(\n ledgerEvents: Iterable<EffectLedgerEvent<WorkflowCmd<A>>>,\n ): readonly WorkflowCmd<A>[];\n}\n\n/**\n * Build a workflow hook bag. The factory captures NO clock and NO RNG — pure\n * bookkeeping over the injected #67 recorder, exactly like\n * `pendingEffectsLedger`. `restore` rehydrates the monotonic delivery-id\n * counter from the persisted ledger events on cold wake, so re-dispatched\n * activities resume their ids without gaps.\n *\n * @param restore - the persisted ledger events (read back from the log) and/or\n * the last issued delivery id, so the id counter resumes gap-free. Omit for a\n * fresh workflow.\n */\nexport function createWorkflow<A, R, F>(restore?: {\n readonly events?: Iterable<EffectLedgerEvent<WorkflowCmd<A>>>;\n readonly lastId?: DeliveryId;\n}): Workflow<A, R, F> {\n // The #67 monotonic-id recorder. It owns the gap-free `deliveryId` and folds\n // an in-memory mirror of the ledger from the same owed/confirmed events the\n // host persists. Reused verbatim — activities AND compensations ARE owed\n // effects on this one ledger (do NOT reinvent the ledger).\n const recorder = pendingEffectsLedger<WorkflowCmd<A>>(restore);\n\n /**\n * Owe + dispatch the activity for `step` at `index`: allocate its delivery\n * id, build the dispatch Cmd, and emit BOTH the `effect_owed` ledger event\n * (to persist first) and the dispatch Cmd. The owed effect IS the dispatch\n * Cmd, so a cold wake re-emits the identical dispatch by folding the ledger.\n *\n * Returns the running state's `current` plus the owed event + dispatch Cmd.\n */\n /** Index into a step sequence with a bounds-check narrowing away `undefined`\n * (`noUncheckedIndexedAccess`). Every call site has already proven the index\n * in range — this converts that proof into the non-optional type, throwing\n * only on a logic error that should be unreachable. */\n function stepAt(\n steps: readonly WorkflowStep<A>[],\n index: number,\n ): WorkflowStep<A> {\n const step = steps[index];\n if (step === undefined) {\n throw new Error(\n `createWorkflow: step index ${index} out of range (length ` +\n `${steps.length}) — a reducer bounds proof was violated.`,\n );\n }\n return step;\n }\n\n /**\n * Predict the next delivery id, build the FINAL dispatch Cmd with it via\n * `build`, owe that exact Cmd on the #67 ledger, and assert the issued id\n * equals the prediction.\n *\n * The dispatch Cmd needs the delivery id, and the ledger event carries that\n * Cmd as its `effect` payload — so the id and the Cmd are mutually dependent.\n * Resolve the cycle by predicting the next id (`lastId() + 1`, the recorder's\n * gap-free monotonic rule), building the final Cmd, then handing that exact\n * Cmd to `recorder.owe`. The recorder issues the predicted id and folds the\n * final Cmd into its mirror — so the mirror, the persisted `effect_owed`\n * event, and the dispatch Cmd all carry one identical id and payload (no\n * placeholder, no rebuild). Shared by the forward owe ({@link oweActivity})\n * and the reverse owe ({@link oweCompensation}) so both halves of the\n * transaction allocate ids on the SAME monotonic ledger.\n */\n function oweOnLedger<C extends WorkflowCmd<A>>(\n build: (id: DeliveryId) => C,\n ): {\n readonly id: DeliveryId;\n readonly owed: EffectOwed<WorkflowCmd<A>>;\n readonly cmd: C;\n } {\n const id = (recorder.lastId() + 1) as DeliveryId;\n const cmd = build(id);\n const { id: issued, event } = recorder.owe(cmd);\n // Defensive: the prediction must equal the issued id, or the ledger mirror\n // and the dispatch Cmd would diverge. This holds by the recorder's gap-free\n // monotonic contract; assert it rather than silently carry a mismatch.\n if (issued !== id) {\n throw new Error(\n `createWorkflow: delivery-id prediction (${id}) diverged from the ` +\n `recorder's issued id (${issued}). The #67 recorder's monotonic ` +\n \"counter is not gap-free as assumed — this is a substrate invariant \" +\n \"violation, not a recoverable condition.\",\n );\n }\n return { id, owed: event, cmd };\n }\n\n function oweActivity(\n step: WorkflowStep<A>,\n index: number,\n ): {\n readonly current: InFlightActivity<A>;\n readonly owed: EffectOwed<WorkflowCmd<A>>;\n readonly cmd: ActivityCmd<A>;\n } {\n const { id, owed, cmd } = oweOnLedger<ActivityCmd<A>>((id) => ({\n type: \"workflow_activity\",\n index,\n id,\n activity: step.activity,\n }));\n const current: InFlightActivity<A> = { index, step, id };\n return { current, owed, cmd };\n }\n\n /**\n * Owe + dispatch the COMPENSATION for the completed step at `index` (#125):\n * the reverse-walk mirror of {@link oweActivity}. The compensation is a\n * durable owed effect on the SAME #67 ledger, so a cold wake mid-rollback\n * re-emits the identical compensation dispatch by folding the ledger\n * (acceptance criterion 2). `step.compensation` is required to be present —\n * the caller (the reverse walk) only owes compensations for steps that\n * declared one, skipping the rest.\n */\n function oweCompensation(\n step: WorkflowStep<A>,\n compensation: A,\n index: number,\n ): {\n readonly current: InFlightCompensation<A>;\n readonly owed: EffectOwed<WorkflowCmd<A>>;\n readonly cmd: CompensationCmd<A>;\n } {\n const { id, owed, cmd } = oweOnLedger<CompensationCmd<A>>((id) => ({\n type: \"workflow_compensation\",\n index,\n id,\n compensation,\n }));\n const current: InFlightCompensation<A> = { index, step, id };\n return { current, owed, cmd };\n }\n\n /**\n * Begin (or continue) the reverse walk from `completed`, owing the next\n * compensation strictly BELOW `belowIndex` (exclusive) that declares one. The\n * single source of truth for \"what to undo next\", shared by the failure pivot\n * ({@link Workflow.onActivityErr}) and each `compensation_ok` step\n * ({@link Workflow.onCompensationOk}). Steps with no compensation are SKIPPED\n * (they contribute nothing to the unwind — saga's no-op `undo`).\n *\n * Returns the next compensation to owe (its in-flight record + ledger event +\n * dispatch Cmd), or `null` when no compensable step remains below `belowIndex`\n * — the unwind is complete and the caller settles `failed_compensated`.\n */\n function oweNextCompensation(\n completed: readonly CompletedStep<A, R>[],\n belowIndex: number,\n ): {\n readonly current: InFlightCompensation<A>;\n readonly owed: EffectOwed<WorkflowCmd<A>>;\n readonly cmd: CompensationCmd<A>;\n } | null {\n // Walk the completed history from just below `belowIndex` toward 0 (strict\n // reverse order), stopping at the first step that declares a compensation.\n for (let i = belowIndex - 1; i >= 0; i--) {\n const entry = completed[i];\n // `completed[i]` is the i-th completed step; `i` is also its step index\n // (completed is in execution order, contiguous from 0 up to the failure).\n if (entry !== undefined && entry.step.compensation !== undefined) {\n return oweCompensation(entry.step, entry.step.compensation, i);\n }\n }\n return null;\n }\n\n /**\n * Assemble the next `compensating` step from a `base` (everything but the\n * in-flight compensation — carried identically from the failure pivot OR from\n * the prior compensating state) and the `next` compensation to owe. Both the\n * failure pivot ({@link Workflow.onActivityErr}) and each `compensation_ok`\n * advance ({@link Workflow.onCompensationOk}) continue the unwind the same way,\n * so the confirm-then-owe ledger discipline (`[confirmed, next.owed]`, dispatch\n * `[next.cmd]`) lives here once instead of mirrored at both call sites.\n */\n function continueCompensating(\n base: Omit<CompensatingWorkflow<A, R, F>, \"current\">,\n next: {\n readonly current: InFlightCompensation<A>;\n readonly owed: EffectOwed<WorkflowCmd<A>>;\n readonly cmd: CompensationCmd<A>;\n },\n confirmed: EffectConfirmed,\n ): WorkflowReducerStep<A, R, F> {\n const compensating: CompensatingWorkflow<A, R, F> = {\n ...base,\n current: next.current,\n };\n return {\n state: compensating,\n ledger: [confirmed, next.owed],\n cmds: [next.cmd],\n };\n }\n\n return {\n init(steps) {\n // `steps` is a non-empty `WorkflowSteps` (the empty case is a compile\n // error at the call site), so step 0 always exists — no runtime guard.\n const { current, owed, cmd } = oweActivity(stepAt(steps, 0), 0);\n const state: RunningWorkflow<A, R> = {\n status: \"running\",\n steps,\n completed: [],\n current,\n };\n return { state, ledger: [owed], cmds: [cmd] };\n },\n\n onActivityOk(state, msg) {\n // A result for a terminal workflow, or one whose id does not match the\n // in-flight activity, is a no-op — idempotent by delivery id. This is the\n // guard that makes re-emit-on-wake safe: a re-fired (already-folded)\n // activity's result arrives with an id that is no longer `current.id`.\n if (state.status !== \"running\" || msg.id !== state.current.id) {\n return { state, ledger: [], cmds: [] };\n }\n\n // Record the completed step and confirm the owed activity on the ledger.\n const confirmed: EffectConfirmed = confirm(state.current.id);\n const completedStep: CompletedStep<A, R> = {\n step: state.current.step,\n result: msg.result,\n };\n const completed = [...state.completed, completedStep];\n\n const nextIndex = state.current.index + 1;\n if (nextIndex >= state.steps.length) {\n // The final step's result is the workflow's output → `completed`.\n const done: CompletedWorkflow<A, R> = {\n status: \"completed\",\n steps: state.steps,\n completed,\n output: msg.result,\n };\n return { state: done, ledger: [confirmed], cmds: [] };\n }\n\n // Advance: owe + dispatch the next activity.\n const { current, owed, cmd } = oweActivity(\n stepAt(state.steps, nextIndex),\n nextIndex,\n );\n const running: RunningWorkflow<A, R> = {\n status: \"running\",\n steps: state.steps,\n completed,\n current,\n };\n // Persist the confirm of the prior activity AND the owe of the next, in\n // that order, before dispatching the next. (Confirm-then-owe keeps the\n // ledger holding at most one in-flight activity per workflow.)\n return { state: running, ledger: [confirmed, owed], cmds: [cmd] };\n },\n\n onActivityErr(state, msg) {\n if (state.status !== \"running\" || msg.id !== state.current.id) {\n return { state, ledger: [], cmds: [] };\n }\n const confirmed: EffectConfirmed = confirm(state.current.id);\n\n // #125: the forward failure PIVOTS into compensation. Mirroring saga's\n // `stepErr`: walk the completed history in strict reverse, owing the\n // first declared compensation as a durable effect on this SAME ledger.\n // `state.completed` is preserved precisely so this reverse walk has its\n // history.\n const next = oweNextCompensation(state.completed, state.completed.length);\n\n if (next === null) {\n // Empty rollback: no completed step declares a compensation (the first\n // activity failed, OR every completed step is irreversible). The\n // analogue of saga aborting straight from the first step — settle\n // `failed` directly, nothing to unwind (acceptance criterion 3).\n const failed: FailedWorkflow<A, R, F> = {\n status: \"failed\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.current.step,\n failure: msg.failure,\n };\n return { state: failed, ledger: [confirmed], cmds: [] };\n }\n\n // Pivot to `compensating`, emitting the first compensation. Confirm the\n // failed forward activity AND owe the first compensation, in that order,\n // before dispatching it (the same confirm-then-owe ledger discipline as\n // the forward advance — at most one in-flight effect per workflow). The\n // `base` here is FRESH from the failing activity (failedStep/failure from\n // `state.current`/`msg`, empty `compensated`).\n return continueCompensating(\n {\n status: \"compensating\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.current.step,\n failure: msg.failure,\n compensated: [],\n },\n next,\n confirmed,\n );\n },\n\n onCompensationOk(state, msg) {\n // A compensation result for a non-compensating workflow, or one whose id\n // does not match the in-flight compensation, is a no-op — idempotent by\n // delivery id, the same re-emit-on-wake guard as the forward verbs.\n if (state.status !== \"compensating\" || msg.id !== state.current.id) {\n return { state, ledger: [], cmds: [] };\n }\n\n // Confirm the owed compensation and record the step as compensated.\n const confirmed: EffectConfirmed = confirm(state.current.id);\n const undoneStep = state.completed[state.current.index];\n if (undoneStep === undefined) {\n throw new Error(\n `createWorkflow.onCompensationOk: compensation index ` +\n `${state.current.index} has no completed step — a reverse-walk ` +\n \"bounds proof was violated.\",\n );\n }\n const compensated = [...state.compensated, undoneStep];\n\n // Continue the reverse walk strictly below the step just undone, skipping\n // steps with no compensation.\n const next = oweNextCompensation(state.completed, state.current.index);\n\n if (next === null) {\n // Fully unwound → `failed_compensated` (saga's `aborted`).\n const settled: FailedCompensatedWorkflow<A, R, F> = {\n status: \"failed_compensated\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.failedStep,\n failure: state.failure,\n };\n return { state: settled, ledger: [confirmed], cmds: [] };\n }\n\n // Continue the unwind. The `base` here CARRIES failedStep/failure from the\n // prior compensating `state` and the accumulated `compensated` list.\n return continueCompensating(\n {\n status: \"compensating\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.failedStep,\n failure: state.failure,\n compensated,\n },\n next,\n confirmed,\n );\n },\n\n onCompensationErr(state, msg) {\n // Same id-match idempotency guard as the other verbs.\n if (state.status !== \"compensating\" || msg.id !== state.current.id) {\n return { state, ledger: [], cmds: [] };\n }\n // Confirm the owed compensation (the dispatch is no longer owed — it was\n // delivered and answered, just with a failure), then HALT the rollback at\n // the terminal `compensation_failed` state. Mirroring saga's `undoErr`: a\n // bounced compensation is a real-world fact the consumer must reconcile by\n // hand, never a workflow wedged in `compensating` forever. `compensated`\n // (already rolled back) is left intact for the consumer to read.\n const confirmed: EffectConfirmed = confirm(state.current.id);\n const halted: CompensationFailedWorkflow<A, R, F> = {\n status: \"compensation_failed\",\n steps: state.steps,\n completed: state.completed,\n failedStep: state.failedStep,\n failure: state.failure,\n compensated: state.compensated,\n failedCompensationStep: state.current.step,\n compensationFailure: msg.failure,\n };\n return { state: halted, ledger: [confirmed], cmds: [] };\n },\n\n survivingActivities(ledgerEvents) {\n // Rebuild the ledger by folding the persisted events (NOT a side table),\n // then the surviving owed effects ARE the dispatch Cmds to re-fire — each\n // owed effect's payload is its dispatch Cmd (a forward activity OR a\n // compensation), carrying the same id.\n const ledger: PendingEffectsLedger<WorkflowCmd<A>> =\n foldLedger(ledgerEvents);\n return survivingEffects(ledger).map((owed) => owed.effect);\n },\n };\n\n /** Confirm an owed activity (or compensation) on the recorder's ledger,\n * returning the `effect_confirmed` event to persist. The boolean (was-owed)\n * is not needed by the reducer — the id-match guard above already gates on\n * the in-flight effect — so it is discarded here. */\n function confirm(id: DeliveryId): EffectConfirmed {\n return recorder.confirm(id).event;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// foldWorkflow — the pure replay fold. The determinism contract (acceptance\n// criterion 4) in one function: fold a workflow's full event log — its `init`\n// steps plus the ordered activity-result Msgs — back into a `WorkflowState`,\n// reading no clock and no RNG. Two folds of the same log produce byte-identical\n// state, because every decision is a pure function of (state, msg) and the\n// delivery ids are reissued in the same order from the same fresh recorder.\n//\n// This is the workflow-level twin of `foldLedger`: the host's `replay()` over\n// the event-sourced store re-runs exactly this fold to rebuild a workflow on\n// cold wake. Kept here (not only in the host, #126) so the determinism contract\n// is provable against the pure core alone.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Replay a workflow from its event log: seed over `steps`, then fold each\n * activity-result `Msg` in order. Returns the final {@link WorkflowState}.\n *\n * Determinism: a freshly-constructed {@link createWorkflow} reissues delivery\n * ids monotonically from 0, and the reducer reads no ambient state — so two\n * calls with the same `steps` + `msgs` return deeply-equal states (and equal\n * `JSON.stringify`). This is the byte-identity property #124 pins.\n *\n * Note this folds ONLY the result Msgs; the `init` dispatch + per-step\n * owe/confirm ledger events are an internal effect channel reconstructed by the\n * fresh recorder, not part of the replayed Msg log — exactly as the activity\n * results are the only thing the consumer records and replays.\n */\nexport function foldWorkflow<A, R, F>(\n steps: WorkflowSteps<A>,\n msgs: Iterable<WorkflowMsg<R, F>>,\n): WorkflowState<A, R, F> {\n const wf = createWorkflow<A, R, F>();\n let { state } = wf.init(steps);\n for (const msg of msgs) {\n // Route each result Msg to its verb via the shared dispatcher. The forward\n // results (`activity_*`) and the reverse compensation results\n // (`compensation_*`, #125) are folded by the same fresh recorder, so a\n // replay across a failure+compensation sequence re-issues every delivery id\n // in identical order — byte-identity holds end to end (acceptance\n // criterion 4).\n state = routeWorkflowMsg(wf, state, msg).state;\n }\n return state;\n}\n\n// Re-export the durable-effects types a consumer needs to persist + replay a\n// workflow's ledger (the owed/confirmed events, the delivery id, the empty\n// seed) without reaching into the `../do` subpath directly.\nexport {\n type DeliveryId,\n type EffectConfirmed,\n type EffectLedgerEvent,\n type EffectOwed,\n emptyLedger,\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demlik/tea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "TEA-faithful state machine substrate — one package, every host adapter. Core + subs + testing, plus React, Durable Object, Chrome extension, Node, in-memory, work-queue, devtools, and property-testing modules as subpaths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tea",
|
|
@@ -189,22 +189,6 @@
|
|
|
189
189
|
"types": "./dist/poller/index.d.ts",
|
|
190
190
|
"import": "./dist/poller/index.js"
|
|
191
191
|
},
|
|
192
|
-
"./raft": {
|
|
193
|
-
"types": "./dist/raft/index.d.ts",
|
|
194
|
-
"import": "./dist/raft/index.js"
|
|
195
|
-
},
|
|
196
|
-
"./raft/sim": {
|
|
197
|
-
"types": "./dist/raft/sim.d.ts",
|
|
198
|
-
"import": "./dist/raft/sim.js"
|
|
199
|
-
},
|
|
200
|
-
"./raft/demo": {
|
|
201
|
-
"types": "./dist/raft/demo.d.ts",
|
|
202
|
-
"import": "./dist/raft/demo.js"
|
|
203
|
-
},
|
|
204
|
-
"./raft/timeline": {
|
|
205
|
-
"types": "./dist/raft/timeline.d.ts",
|
|
206
|
-
"import": "./dist/raft/timeline.js"
|
|
207
|
-
},
|
|
208
192
|
"./workflow": {
|
|
209
193
|
"types": "./dist/workflow/index.d.ts",
|
|
210
194
|
"import": "./dist/workflow/index.js"
|
|
@@ -306,6 +290,7 @@
|
|
|
306
290
|
"react": "19.0.0",
|
|
307
291
|
"react-dom": "19.0.0",
|
|
308
292
|
"tsup": "^8.3.5",
|
|
293
|
+
"typedoc": "0.28.20",
|
|
309
294
|
"typescript": "5.7.3",
|
|
310
295
|
"vitest": "~3.0.7",
|
|
311
296
|
"ws": "8.18.1"
|
|
@@ -317,11 +302,11 @@
|
|
|
317
302
|
"build": "NODE_OPTIONS=--max-old-space-size=8192 tsup",
|
|
318
303
|
"lint": "biome check src package.json",
|
|
319
304
|
"test": "vitest run",
|
|
320
|
-
"demo:raft": "vitest run src/raft/demo.test.ts",
|
|
321
|
-
"demo:raft:viz": "vitest run src/raft/timeline.test.ts",
|
|
322
305
|
"demo:saga": "vitest run src/workflow/demo.test.ts",
|
|
323
306
|
"typecheck": "tsc --noEmit",
|
|
324
307
|
"typecheck:examples": "tsc -p examples/tsconfig.json",
|
|
308
|
+
"docs:reference": "TEA_DOCS_WRITE=1 vitest run src/docs/reference/generate-reference.test.ts",
|
|
309
|
+
"docs:reference:check": "pnpm run docs:reference && git diff --exit-code -- docs/reference",
|
|
325
310
|
"verify-exports": "node scripts/verify-exports.mjs"
|
|
326
311
|
}
|
|
327
312
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resilient-call/index.ts"],"names":[],"mappings":"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","file":"chunk-27N2SPTN.js","sourcesContent":["/**\n * @deprecated `@demlik/tea/resilient-call` is deprecated in favor of\n * `@demlik/tea/with-resilience` — the higher-order `withResilience(base,\n * config)` wrapper (TEA-idiomatic: it wraps any existing machine instead of\n * forcing a bespoke embedded slice; epic consolidation verdict, survivor\n * `with-resilience`). The APIs are NOT drop-in; migration map:\n *\n * - `createResilientCall(config, rng)` + hand-wiring `init` / the verbs /\n * `subs` / `handlers` into your machine → `withResilience(base, { target,\n * ...bricks }, rng)`. Nothing is wired by hand anymore: the wrapper\n * intercepts the `target` base Cmd and routes it through the gate.\n * - `ResilientConfig` bricks (`retry` / `circuit` / `rateLimit` / `cache` /\n * `deadline`) → unchanged; `ResilienceConfig` extends the same knob and\n * adds `target` (required), `keyOf?` (per-call key — was `attempt`'s `key`\n * argument), and `at?` (time-as-data reader — REQUIRED with any\n * time-sensitive brick; `withResilience` throws at construction without it).\n * - The slice at your chosen Model field + `liftResilience` → the wrapper\n * owns `model.$resilience`; there is nothing to lift.\n * - Verbs `attempt` / `succeed` / `fail` / `onTimer` → internal to the\n * wrapper; results route as `$resilience:ok` / `$resilience:err` /\n * `$resilience:timer` Msgs through the merged `update` (the old\n * `resilient_run` / `resilient_ok` / `resilient_err` vocabulary is renamed\n * into the `$resilience:` namespace).\n * - `settleFailed` → NO equivalent. It exists for embedded-slice consumers\n * (`authed-call`, `paginated-walk`) that settle a call without touching the\n * breaker; the wrapper exposes no caller-facing settle verb.\n * - `handlers(ports)` / `subs(state)` / the `subscribeDeadline` re-export →\n * handled by the wrapper; run the returned machine like any other.\n *\n * Removal window (\"deprecate, don't delete\"): the `./resilient-call` subpath\n * export survives one minor release after this deprecation ships, then is\n * removed. Internal sibling modules (`with-resilience` itself, `agent`,\n * `llm-call`, `authed-call`, `reconciler`, `paginated-walk`) intentionally keep\n * importing `../resilient-call` — the deprecation targets the PUBLISHED export,\n * not the module's internal role.\n */\n\n/**\n * @demlik/tea/resilient-call — the ROOT composition: a single resilience knob\n * that bundles cache → circuit-breaker → rate-limit → exponential-backoff retry\n * (with a deadline-driven retry timer) around any fallible `(input) => Promise`\n * port.\n *\n * This is `examples/resilient-fetch.ts`'s hand-wired `attempt()` generalized:\n *\n * - It is **keyed**. The example tracked one URL at a time; this tracks N\n * concurrent logical calls, each under a string `key`. The cache, the\n * waiting-retry bookkeeping, and the retry timers are all per-key, while the\n * circuit breaker and token bucket are shared (one downstream target, one\n * breaker / one bucket — the example's implicit single-target assumption\n * made explicit).\n * - It is **config-driven**, not hand-wired. You hand `createResilientCall`\n * a `ResilientConfig` and get back the uniform knob contract every L2\n * composition exposes: `init()`, the four verbs (`attempt` / `succeed` /\n * `fail` / `onTimer`), `subs(state)`, and `handlers(ports)`.\n * - Every brick is **optional**. Omit `cache` and the cache gate disappears\n * (`get` is never consulted); omit `circuit` and every call passes the\n * breaker; omit `rateLimit` and the bucket never throttles; omit `retry`\n * and a failure is terminal (no backoff, no timer); omit `deadline` and no\n * overall wall-clock cap is armed. \"Omit a brick → omit its gate\" is the\n * whole config story — there are no other modes.\n *\n * Four later compositions (`llm-call`, `paginated-walk`, `authed-call`,\n * `reconciler`) inherit THIS slice + verb shape, so the contract here is\n * load-bearing: the slice is a flat record of brick states, every verb returns\n * `readonly [State, Cmd[]]`, time is always an `at` / `now` parameter, and the\n * RNG used for jitter is injected on the config (never read inside a verb).\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is a plain-data Model field (brick states are all\n * JSON-serializable), so it survives DO eviction / reload.\n * - **Replayable** — every transition is a verb returning new state + Cmds;\n * `replay` reconstructs it exactly. Nothing here closes over time or RNG.\n *\n * ## Where the clock / RNG live\n *\n * Inside the verbs: nowhere. Time arrives as `at` on `attempt` / `onTimer` and\n * is carried on the result / timer Msgs the consumer dispatches. Jitter RNG is\n * injected once at `createResilientCall(config, rng)` and threaded into\n * `nextDelayMs` at the verb boundary — deterministic in tests, `Math.random`\n * by default. The ONLY clock read is `Date.now()` inside the `handlers` port\n * (the effect boundary), exactly as `resilient-fetch.ts` stamps its result Msgs.\n *\n * ## Typical wiring\n *\n * const rc = createResilientCall<string, Result>({\n * cache: { ttlMs: 60_000 },\n * circuit: { threshold: 5, cooldownMs: 30_000 },\n * rateLimit: { capacity: 10, refillPerSec: 5 },\n * retry: defaultRetryPolicy,\n * deadline: { ms: 5_000 },\n * });\n *\n * // in the machine:\n * init: () => [{ resilience: rc.init() }, []],\n * update: {\n * fetch: (s, m) => lift(s, rc.attempt(s.resilience, m.key, m.input, m.at)),\n * call_ok: (s, m) => lift(s, rc.succeed(s.resilience, m.key, m)),\n * call_err:(s, m) => lift(s, rc.fail(s.resilience, m.key, m)),\n * retry_due:(s, m) => lift(s, rc.onTimer(s.resilience, m)),\n * },\n * subscriptions: (s) => rc.subs(s.resilience),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: rc.handlers({ run: ctx.call }),\n */\n\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../cache\";\nimport {\n type CircuitState,\n canPass,\n initCircuit,\n onFailure,\n onSuccess,\n} from \"../circuit-breaker\";\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n subscribeDeadline,\n} from \"../deadline\";\nimport { type Cmd, type NoCtx, tryInterpret } from \"../index\";\nimport { MsgType } from \"../protocol\";\nimport { initBucket, type TokenBucket, tryConsume } from \"../rate-limit\";\nimport {\n asRng,\n initRetry,\n nextDelayMs,\n type RetryPolicy,\n type RetryState,\n recordFailure,\n shouldRetry,\n} from \"../retry-backoff\";\n\n// ===========================================================================\n// Config — the knob. Every brick optional; omit a brick → omit its gate.\n// ===========================================================================\n\n/** Circuit-breaker knob — only the two numbers a consumer ever tunes. */\nexport interface CircuitConfig {\n /** Consecutive failures that trip the breaker open. */\n readonly threshold: number;\n /** How long the breaker stays open before admitting a probe, in ms. */\n readonly cooldownMs: number;\n /**\n * Probes admitted in `half_open` before fast-failing the rest of the round.\n * Defaults to 1 (a single probe decides recovery) — matches the circuit-\n * breaker module's `defaultCircuitPolicy`.\n */\n readonly halfOpenMaxProbes?: number;\n}\n\n/** Token-bucket rate-limit knob. */\nexport interface RateLimitConfig {\n /** Burst size — the bucket starts full at `capacity`. */\n readonly capacity: number;\n /** Steady-state refill rate, tokens per second. */\n readonly refillPerSec: number;\n}\n\n/** Per-entry TTL cache knob. */\nexport interface CacheConfig {\n /** How long a cached success stays fresh, in ms. */\n readonly ttlMs: number;\n}\n\n/** Overall wall-clock deadline knob — an absolute cap per in-flight call. */\nexport interface DeadlineConfig {\n /** Budget from the moment a call starts, in ms. */\n readonly ms: number;\n}\n\n/**\n * The resilience knob. EVERY field is optional: omit a brick and its gate is\n * skipped entirely (the slice still carries a default brick state so the shape\n * stays uniform across configs, but the gate is never consulted). An empty\n * config `{}` is a valid pass-through — a bare effect with no resilience at all.\n */\nexport interface ResilientConfig {\n readonly retry?: RetryPolicy;\n readonly circuit?: CircuitConfig;\n readonly rateLimit?: RateLimitConfig;\n readonly cache?: CacheConfig;\n readonly deadline?: DeadlineConfig;\n}\n\n// ===========================================================================\n// Slice — the Model field this knob owns. A flat record of brick states.\n// ===========================================================================\n\n/**\n * Per-key call phase. `key` indexes a logical call; several can be in flight at\n * once (the generalization of the example's single `url`). Discriminated on\n * `phase` so each phase carries only its own data (pattern 11):\n *\n * - `idle` — never attempted, or fully settled and forgotten.\n * - `running` — the effect Cmd is out; awaiting `succeed` / `fail`.\n * Carries `deadlineAtMs` (absolute cap, `0` if no deadline\n * brick) and the last `input` so a retry can re-issue it.\n * - `waiting_retry` — a transient failure backed off; the retry timer is armed\n * for `retryAtMs`. Carries `input` for the re-attempt.\n * - `circuit_open` — the breaker fast-failed this attempt. Terminal for this\n * call until the consumer re-attempts after cooldown.\n * - `succeeded` — settled OK. `result` is the value the port produced.\n * - `failed` — settled Err (out of retries, or no retry brick). `error`\n * carries the last failure.\n */\nexport type CallPhase<I, R> =\n | { readonly phase: \"idle\" }\n | {\n readonly phase: \"running\";\n readonly input: I;\n readonly deadlineAtMs: number;\n }\n | {\n readonly phase: \"waiting_retry\";\n readonly input: I;\n readonly retryAtMs: number;\n readonly deadlineAtMs: number;\n }\n | { readonly phase: \"circuit_open\" }\n | { readonly phase: \"succeeded\"; readonly result: R }\n | { readonly phase: \"failed\"; readonly error: unknown };\n\n/**\n * The slice. `circuit` / `bucket` are SHARED across keys (one downstream target\n * → one breaker, one bucket). `retry` and `cache` are PER KEY (each logical call\n * has its own backoff count + cached result), as is the per-call `CallPhase`.\n * Every field is plain data → the whole slice serializes into a `Store<S>`.\n *\n * `retry` / `cache` keep a default brick value even when the corresponding\n * config brick is omitted, so the slice shape is identical regardless of which\n * bricks the consumer enabled — the verbs simply never consult the unused brick.\n */\nexport interface ResilientState<I, R> {\n readonly circuit: CircuitState;\n readonly bucket: TokenBucket;\n readonly retry: Readonly<Record<string, RetryState>>;\n readonly cache: TtlCache<R>;\n readonly calls: Readonly<Record<string, CallPhase<I, R>>>;\n}\n\n// ===========================================================================\n// Cmds + Msgs the knob speaks. Generic over the port input `I`.\n// ===========================================================================\n\n/**\n * The one effect this knob emits: \"run the port for `key` with `input`\". The\n * consumer's `handlers(ports)` interprets it; it carries no closure — `input`\n * is plain data (invariant 3).\n */\nexport type RunCmd<I> = Cmd<typeof MsgType.ResilientRun> & {\n readonly key: string;\n readonly input: I;\n};\n\n/** Settle Msgs the `handlers` port dispatches back. `at` is stamped at the boundary. */\nexport type SucceedMsg<R> = {\n readonly type: typeof MsgType.ResilientOk;\n readonly key: string;\n readonly result: R;\n readonly at: number;\n};\nexport type FailMsg = {\n readonly type: typeof MsgType.ResilientErr;\n readonly key: string;\n readonly error: unknown;\n readonly at: number;\n};\n\n/** The retry / deadline timer Msg — `DeadlineExceeded`, keyed by the call `key`. */\nexport type ResilientTimerMsg = DeadlineExceeded;\n\n/**\n * The plain-data error a deadline-failed call settles with. A `{_tag, ...}`\n * sentinel — NEVER a `new Error(...)` — so the slice stays JSON-serializable\n * (Error objects round-trip to `{}` and carry a stack trace, both of which break\n * the slice's durability invariant). `id` / `atMs` echo the timer that fired so\n * a consumer can attribute the failure to its deadline.\n */\nexport type DeadlineExceededError = {\n readonly _tag: \"deadline_exceeded\";\n readonly id: string;\n readonly atMs: number;\n};\n\n/** Ports the consumer supplies to `handlers`. */\nexport interface ResilientPorts<I, R> {\n /** The fallible work this knob wraps. Throws on failure → routed to `fail`. */\n readonly run: (input: I, key: string) => Promise<R>;\n}\n\n// ===========================================================================\n// Internal brick-policy derivation — config → the L1 policy shapes.\n// ===========================================================================\n\n// A circuit policy is only built when the brick is configured; absence is\n// represented as `null` so the gate can short-circuit to \"always pass\".\nfunction circuitPolicy(config: ResilientConfig) {\n if (config.circuit === undefined) return null;\n return {\n failureThreshold: config.circuit.threshold,\n cooldownMs: config.circuit.cooldownMs,\n halfOpenMaxProbes: config.circuit.halfOpenMaxProbes ?? 1,\n };\n}\n\n// Sub-id families. Each call's retry timer is a deadline keyed by `key`, so a\n// machine running many concurrent calls reconciles each independently.\nfunction retryTimerId(key: string): string {\n return `resilient:retry:${key}`;\n}\nfunction deadlineTimerId(key: string): string {\n return `resilient:deadline:${key}`;\n}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a resilient-call knob from `config`. `rng` is injected for retry\n * jitter — pass a fixed `() => 0.5` in tests to pin backoff; defaults to\n * `Math.random` (read at the verb boundary, never inside a brick op). It is\n * curried onto the factory rather than threaded through every verb so callers\n * spell the determinism choice once.\n *\n * Returns the uniform L2 knob contract. `I` is the port input type, `R` the\n * port result type.\n *\n * @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — not\n * drop-in; see the migration map at the top of this module.\n */\nexport function createResilientCall<I, R>(\n config: ResilientConfig,\n rng: () => number = Math.random,\n) {\n const cPolicy = circuitPolicy(config);\n // Re-affirm the `[0, 1)` contract at the seam where the injected generator\n // feeds the backoff math (brand from `../retry-backoff`). The factory's\n // public param stays a plain `() => number` so transitive callers are\n // unchanged; `asRng` is the single point it's branded for `nextDelayMs`.\n const rngBranded = asRng(rng);\n\n /** The starting slice. Bricks not in `config` still get a default value. */\n function init(): ResilientState<I, R> {\n return {\n circuit: initCircuit(),\n // The bucket carries a default value even without the rateLimit brick so\n // the slice shape is uniform, but the rate-limit GATE is skipped entirely\n // when the brick is absent (see `gate`) — the bucket is simply never\n // consulted. A `0/0` placeholder bucket would be touched by `tryConsume`;\n // a `1-cap` default is harmless because the gate never calls it.\n bucket: config.rateLimit\n ? initBucket(\n config.rateLimit.capacity,\n config.rateLimit.refillPerSec,\n 0,\n )\n : initBucket(1, 1, 0),\n retry: {},\n cache: initCache<R>(),\n calls: {},\n };\n }\n\n /** Read this key's retry state, defaulting to a fresh one. */\n function retryOf(s: ResilientState<I, R>, key: string): RetryState {\n return s.retry[key] ?? initRetry();\n }\n\n /** Compute the absolute deadline for a call starting at `at` (0 = no cap). */\n function deadlineAt(at: number): number {\n return config.deadline ? at + config.deadline.ms : 0;\n }\n\n /**\n * The shared decision: cache → rate-limit → circuit gate, then either emit\n * the effect or schedule a retry. `deadlineAtMs` is threaded so a re-attempt\n * (from `onTimer`) keeps the ORIGINAL overall deadline rather than restarting\n * it. PURE — `at` is the only clock; `rng` is the injected jitter source.\n *\n * Used by both `attempt` (fresh call) and `onTimer` (retry timer fired) so\n * the gate logic lives in exactly one place — the example's shared `attempt`\n * helper, lifted to a keyed knob.\n *\n * ## Why rate-limit is gated BEFORE the circuit probe\n *\n * The circuit-breaker's `canPass` is not a passive read: in `half_open` it\n * *consumes* a probe slot (advancing `probes`), and the consumed slot is only\n * released when a probe RESOLVES via `onSuccess` / `onFailure`. So a probe\n * slot may only be taken when this attempt is actually going to reach the\n * backend (i.e. emit the `resilient_run` Cmd). The rate-limit gate can reject\n * an attempt and return a backoff WITHOUT any Cmd — no backend call, so no\n * settle Msg ever comes back. If the circuit probe were consumed first, that\n * rate-limited attempt would burn the half-open probe budget on a call that\n * never happened, wedging the breaker `half_open` at its probe cap forever\n * (nothing moves it out of `half_open` but a resolved probe). Running the\n * rate-limit gate first means the circuit probe is consumed ONLY on the path\n * that emits the effect.\n */\n function gate(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n deadlineAtMs: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n // 1) Serve from cache if fresh — no effect at all. Skipped when no cache brick.\n if (config.cache !== undefined) {\n const cached = cacheGet(s.cache, key, at);\n if (cached !== undefined) {\n return [setCall(s, key, { phase: \"succeeded\", result: cached }), []];\n }\n }\n\n // 2) Rate-limit gate FIRST — it may reject with no Cmd, so it must run\n // before any circuit probe is consumed (see header note). Skipped entirely\n // when no rateLimit brick — the bucket is never consulted, so the default\n // placeholder bucket stays untouched.\n let bucket = s.bucket;\n if (config.rateLimit !== undefined) {\n const [next, hasToken] = tryConsume(bucket, at);\n bucket = next;\n if (!hasToken) {\n // Rate-limited: treat as a transient failure and back off via retry.\n // The circuit is untouched — no probe is spent on a call that never ran.\n return backoff(\n { ...s, bucket },\n key,\n input,\n \"rate_limited\",\n at,\n deadlineAtMs,\n );\n }\n }\n\n // 3) Circuit-breaker gate. `null` policy → no breaker → always pass. Reached\n // only once a token is secured, so a half-open probe is consumed exclusively\n // on the path that emits the effect below.\n let circuit = s.circuit;\n if (cPolicy !== null) {\n const [next, ok] = canPass(circuit, cPolicy, at);\n circuit = next;\n if (!ok) {\n return [\n { ...setCall(s, key, { phase: \"circuit_open\" }), bucket, circuit },\n [],\n ];\n }\n }\n\n // 4) All gates passed — emit the effect as data.\n return [\n {\n ...setCall(s, key, { phase: \"running\", input, deadlineAtMs }),\n circuit,\n bucket,\n },\n [{ type: MsgType.ResilientRun, key, input }],\n ];\n }\n\n /**\n * Back off after a transient failure: record it, and if `retry` permits\n * another attempt, enter `waiting_retry` with the retry timer armed; else\n * settle `failed`. When no `retry` brick is configured the call fails on the\n * first failure (no backoff, no timer). PURE.\n */\n function backoff(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n error: unknown,\n at: number,\n deadlineAtMs: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n if (config.retry === undefined) {\n return [setCall(s, key, { phase: \"failed\", error }), []];\n }\n const retry = recordFailure(retryOf(s, key), error);\n const withRetry = { ...s, retry: { ...s.retry, [key]: retry } };\n if (!shouldRetry(retry, config.retry)) {\n return [setCall(withRetry, key, { phase: \"failed\", error }), []];\n }\n const retryAtMs = at + nextDelayMs(retry, config.retry, rngBranded);\n return [\n setCall(withRetry, key, {\n phase: \"waiting_retry\",\n input,\n retryAtMs,\n deadlineAtMs,\n }),\n [],\n ];\n }\n\n // Set the phase for one key, leaving every other field untouched.\n function setCall(\n s: ResilientState<I, R>,\n key: string,\n phase: CallPhase<I, R>,\n ): ResilientState<I, R> {\n return { ...s, calls: { ...s.calls, [key]: phase } };\n }\n\n // === Verb: attempt =======================================================\n\n /**\n * Start (or restart) the call for `key` with `input` at time `at`. Runs the\n * cache → circuit → rate-limit gate and either emits the `resilient_run`\n * effect, schedules a retry, fast-fails (circuit open), or serves the cache.\n * PURE.\n */\n function attempt(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n return gate(s, key, input, at, deadlineAt(at));\n }\n\n // === Verb: succeed =======================================================\n\n /**\n * Record a success for `key`: close the breaker, fill the cache (if a cache\n * brick exists), reset this key's retry count, and settle `succeeded`. PURE —\n * `msg.at` is the cache write clock.\n */\n function succeed(\n s: ResilientState<I, R>,\n key: string,\n msg: SucceedMsg<R>,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n const circuit =\n cPolicy !== null ? onSuccess(s.circuit, cPolicy) : s.circuit;\n const cache =\n config.cache !== undefined\n ? cacheSet(s.cache, key, msg.result, msg.at, config.cache.ttlMs)\n : s.cache;\n // Drop this key's retry counter — a success ends the retry run.\n const { [key]: _dropped, ...retry } = s.retry;\n return [\n {\n ...setCall(s, key, { phase: \"succeeded\", result: msg.result }),\n circuit,\n cache,\n retry,\n },\n [],\n ];\n }\n\n // === Verb: fail ==========================================================\n\n /**\n * Record a failure for `key`: trip the breaker (it may open), then back off —\n * schedule a retry if `retry` permits, else settle `failed`. PURE — `msg.at`\n * stamps the breaker trip + the retry delay base. Re-issues from the call's\n * remembered `input` and preserves the original overall `deadlineAtMs`.\n */\n function fail(\n s: ResilientState<I, R>,\n key: string,\n msg: FailMsg,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n const circuit =\n cPolicy !== null ? onFailure(s.circuit, cPolicy, msg.at) : s.circuit;\n const withCircuit = { ...s, circuit };\n const call = s.calls[key];\n const input = call?.phase === \"running\" ? call.input : undefined;\n const deadlineAtMs =\n call?.phase === \"running\" ? call.deadlineAtMs : deadlineAt(msg.at);\n // No remembered input (e.g. a stray fail for a key not running) → just\n // settle failed after tripping the breaker; nothing to re-issue.\n if (input === undefined) {\n return [\n setCall(withCircuit, key, { phase: \"failed\", error: msg.error }),\n [],\n ];\n }\n return backoff(withCircuit, key, input, msg.error, msg.at, deadlineAtMs);\n }\n\n // === Verb: settleFailed ==================================================\n\n /**\n * Settle `key` to terminal `failed` with `error` WITHOUT touching the circuit\n * breaker or this key's retry counter. PURE.\n *\n * This is the verb for a failure that is NOT a downstream-health signal: the\n * call must end now, but the breaker must not trip and the backoff run must\n * not advance. `fail` is the opposite — it records the failure against the\n * breaker (it may open) and feeds the retry policy (it may back off). Use\n * `settleFailed` when the *reason* the call cannot proceed has nothing to do\n * with downstream health:\n *\n * - `authed-call` settles a 401 it cannot fix (e.g. the refresh budget is\n * spent): a bad token is the caller's problem, not the backend's —\n * tripping the breaker on it would punish a healthy target, and backing\n * off would just retry a request that will 401 again.\n * - `paginated-walk` settles a terminal walk error (e.g. a malformed page\n * the consumer rejects) that should stop the walk without poisoning the\n * shared breaker the next page fetch passes through.\n *\n * Because it leaves `circuit` and `retry[key]` untouched, the breaker keeps\n * its health view of the actual backend and a later `attempt` for this key\n * starts a fresh retry run. Both sibling knobs previously hand-rolled this by\n * splicing `calls` directly; this verb gives them one named, tested entry\n * point so the slice's settle invariants live in exactly one place.\n */\n function settleFailed(\n s: ResilientState<I, R>,\n key: string,\n error: unknown,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n return [setCall(s, key, { phase: \"failed\", error }), []];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A timer fired for some key. Two kinds, disambiguated by the Sub id:\n *\n * - retry timer → re-run the gate for that key's remembered input,\n * re-checking the circuit + bucket at THIS time.\n * - deadline timer → the overall budget elapsed → settle `failed` with a\n * deadline error, no matter the current phase.\n *\n * A timer for a key no longer `waiting_retry` (retry) / no longer in flight\n * (deadline) is a no-op — the reconcile pass races the result; the verb must\n * tolerate a stale fire. PURE.\n */\n function onTimer(\n s: ResilientState<I, R>,\n msg: ResilientTimerMsg,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n for (const [key, call] of Object.entries(s.calls)) {\n if (msg.id === retryTimerId(key)) {\n if (call.phase !== \"waiting_retry\") return [s, []];\n return gate(s, key, call.input, msg.atMs, call.deadlineAtMs);\n }\n if (msg.id === deadlineTimerId(key)) {\n // Deadline only fires for a still-active call. A settled call has no\n // armed deadline Sub, but tolerate a stale fire defensively.\n if (call.phase !== \"running\" && call.phase !== \"waiting_retry\")\n return [s, []];\n // Plain-data sentinel, NOT a `new Error(...)`: an Error object is not\n // JSON-serializable (it round-trips to `{}`) and captures a stack trace,\n // both of which break the slice's durability invariant once it is\n // persisted into a `Store<S>` and reloaded. A `{_tag, ...}` record keeps\n // the slice round-trip-equal under JSON.parse(JSON.stringify(...)).\n return [\n setCall(s, key, {\n phase: \"failed\",\n error: {\n _tag: \"deadline_exceeded\",\n id: msg.id,\n atMs: msg.atMs,\n } satisfies DeadlineExceededError,\n }),\n [],\n ];\n }\n }\n return [s, []];\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions: a retry timer for every `waiting_retry` call, and\n * (when the `deadline` brick is configured) an overall deadline timer for\n * every still-active call. Both are `DeadlineSub`s reconciled by id, so a\n * phase change cancels the matching timer automatically — no manual\n * `clearTimeout`. Wire `subscribe: { deadline: subscribeDeadline }`.\n */\n function subs(s: ResilientState<I, R>): readonly DeadlineSub[] {\n const out: DeadlineSub[] = [];\n for (const [key, call] of Object.entries(s.calls)) {\n if (call.phase === \"waiting_retry\") {\n out.push(deadlineSub(retryTimerId(key), call.retryAtMs));\n }\n if (\n config.deadline !== undefined &&\n (call.phase === \"running\" || call.phase === \"waiting_retry\") &&\n call.deadlineAtMs > 0\n ) {\n out.push(deadlineSub(deadlineTimerId(key), call.deadlineAtMs));\n }\n }\n return out;\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handler for `resilient_run`. Wraps the consumer's\n * `run` port via `tryInterpret` (Railway): success → `resilient_ok`, failure\n * → `resilient_err`, each stamped with `Date.now()` at the effect boundary —\n * the ONE permitted clock read (interpret is impure; the reducer never reads\n * the clock). Assign to the machine's `interpret`:\n *\n * interpret: rc.handlers({ run: (input, key) => ctx.callBackend(input) })\n */\n function handlers(ports: ResilientPorts<I, R>) {\n return {\n resilient_run: tryInterpret<\n RunCmd<I>,\n R,\n SucceedMsg<R> | FailMsg,\n // The work fn reads nothing from ctx — it forwards `cmd.input` to the\n // consumer-supplied `run` port. `NoCtx` (not `unknown`) marks this as a\n // DELIBERATE context-free seam, so callers see intent, not looseness.\n NoCtx\n >(\n (cmd) => ports.run(cmd.input, cmd.key),\n (result, cmd): SucceedMsg<R> => ({\n type: MsgType.ResilientOk,\n key: cmd.key,\n result,\n at: Date.now(),\n }),\n (error, cmd): FailMsg => ({\n type: MsgType.ResilientErr,\n key: cmd.key,\n error,\n at: Date.now(),\n }),\n ),\n };\n }\n\n return {\n init,\n attempt,\n succeed,\n fail,\n settleFailed,\n onTimer,\n subs,\n handlers,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the\n * slice lives at `state.resilience`. A convenience for the common single-slice\n * host; consumers with a differently-named field spread by hand. Pure — a thin\n * record rebuild, no clock / RNG.\n *\n * @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — the\n * wrapper owns `model.$resilience`, so there is nothing to lift; see the\n * migration map at the top of this module.\n */\nexport function liftResilience<\n S extends { resilience: ResilientState<I, R> },\n I,\n R,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [ResilientState<I, R>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, resilience: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives so consumers (and tests) wire one\n * import: `subscribeDeadline` is the `subscribe` cell, `deadlineSub` builds the\n * Sub literal this knob's `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub, DeadlineExceeded };\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rate-limit/index.ts"],"names":[],"mappings":";AAsDO,SAAS,UAAA,CACd,QAAA,EACA,YAAA,EACA,KAAA,EACa;AACb,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,QAAA;AAAA,IACR,QAAA;AAAA,IACA,YAAA;AAAA,IACA,YAAA,EAAc;AAAA,GAChB;AACF;AAcO,SAAS,MAAA,CAAO,QAAqB,KAAA,EAA4B;AACtE,EAAA,MAAM,aAAa,IAAA,CAAK,GAAA,CAAI,GAAG,KAAA,GAAQ,MAAA,CAAO,YAAY,CAAA,GAAI,GAAA;AAC9D,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,MAAA,GAAS,UAAA,GAAa,MAAA,CAAO,YAAA;AACxD,EAAA,OAAO;AAAA,IACL,GAAG,MAAA;AAAA,IACH,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,UAAU,WAAW,CAAA;AAAA,IAC7C,YAAA,EAAc;AAAA,GAChB;AACF;AAeO,SAAS,UAAA,CACd,MAAA,EACA,KAAA,EACA,CAAA,GAAI,CAAA,EAC6B;AACjC,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,MAAA,EAAQ,KAAK,CAAA;AACrC,EAAA,IAAI,QAAA,CAAS,UAAU,CAAA,EAAG;AACxB,IAAA,OAAO,CAAC,EAAE,GAAG,QAAA,EAAU,QAAQ,QAAA,CAAS,MAAA,GAAS,CAAA,EAAE,EAAG,IAAI,CAAA;AAAA,EAC5D;AACA,EAAA,OAAO,CAAC,UAAU,KAAK,CAAA;AACzB;AAqBO,SAAS,UAAA,CAAW,UAAkB,KAAA,EAA8B;AACzE,EAAA,OAAO,EAAE,IAAA,EAAM,EAAC,EAAG,UAAU,KAAA,EAAM;AACrC;AAgBO,SAAS,MAAA,CACd,QACA,KAAA,EACmC;AACnC,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,CAAO,QAAA;AAC9B,EAAA,MAAM,SAAS,MAAA,CAAO,IAAA,CAAK,OAAO,CAAC,CAAA,KAAM,IAAI,MAAM,CAAA;AACnD,EAAA,IAAI,MAAA,CAAO,MAAA,GAAS,MAAA,CAAO,KAAA,EAAO;AAChC,IAAA,OAAO,CAAC,EAAE,GAAG,MAAA,EAAQ,IAAA,EAAM,CAAC,GAAG,MAAA,EAAQ,KAAK,CAAA,EAAE,EAAG,IAAI,CAAA;AAAA,EACvD;AACA,EAAA,OAAO,CAAC,EAAE,GAAG,QAAQ,IAAA,EAAM,MAAA,IAAU,KAAK,CAAA;AAC5C;AAQO,SAAS,SAAA,CAAU,QAAuB,KAAA,EAAuB;AACtE,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,CAAO,QAAA;AAC9B,EAAA,MAAM,IAAA,GAAO,OAAO,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,GAAI,MAAM,CAAA,CAAE,MAAA;AACnD,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,MAAA,CAAO,QAAQ,IAAI,CAAA;AACxC","file":"chunk-2YOE5PIS.js","sourcesContent":["/**\n * @demlik/tea/rate-limit — two rate limiters as pure state + ops.\n *\n * Same shape as `@demlik/tea/work-queue`'s `ops.ts`: a state type plus pure\n * transition functions. Host-agnostic by construction — no `Date.now()`, no\n * timers, no I/O. The caller injects the clock (`nowMs`) and wires the result\n * into its own reducer / interpret. This keeps the limiters inside TEA's\n * invariant 2 (transitions are pure: same inputs → same outputs, no `Date.now`)\n * so a consumer can fold them through `update` and exercise them with `replay`\n * / the pbt fold without a fake clock.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). The `tryConsume` / `record` ops return a\n * `[next, ok]` tuple so the caller branches on the decision while threading the\n * advanced state forward; `refill` / `remaining` are read-shaped helpers.\n *\n * Two complementary algorithms, picked by the cost the caller wants to pay:\n *\n * - **Token bucket** — O(1) state (four numbers), smooths bursts against a\n * steady refill rate. Allows a burst up to `capacity`, then throttles to\n * `refillPerSec`. Cheapest; the default choice.\n * - **Sliding window log** — O(limit) state (one timestamp per in-window\n * hit), exact \"no more than `limit` events per `windowMs`\". Pay the memory\n * for precise window edges when a token bucket's continuous refill would\n * let through too many at a boundary.\n *\n * NOT exported from the package root — reached via the `@demlik/tea/rate-limit`\n * subpath, same one-shape-per-package rule as `work-queue` and `do`.\n */\n\n// ===========================================================================\n// Token bucket\n// ===========================================================================\n\n/**\n * A token bucket: `tokens` of `capacity` available now, replenished at\n * `refillPerSec` tokens per second. `lastRefillMs` is the clock reading at\n * which `tokens` was last computed — `refill` advances it. All four fields are\n * plain numbers so the whole bucket is trivially JSON-serializable into a\n * consumer's `Store<S>`.\n */\nexport interface TokenBucket {\n readonly tokens: number;\n readonly capacity: number;\n readonly refillPerSec: number;\n readonly lastRefillMs: number;\n}\n\n/**\n * Create a full bucket. Starts at `capacity` tokens (the conventional \"a fresh\n * limiter lets the first burst through\" behavior) and stamps `lastRefillMs`\n * with the caller's clock so the first `refill` measures elapsed time from\n * boot, not from epoch 0.\n */\nexport function initBucket(\n capacity: number,\n refillPerSec: number,\n nowMs: number,\n): TokenBucket {\n return {\n tokens: capacity,\n capacity,\n refillPerSec,\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Add the tokens accrued since `lastRefillMs`, clamped to `capacity`, and\n * advance `lastRefillMs` to `nowMs`. PURE — returns a new bucket; the input is\n * untouched.\n *\n * A `nowMs` at or before `lastRefillMs` (clock not advanced, or skewed\n * backwards) adds a non-positive amount; `Math.max(0, …)` floors the elapsed\n * term so a backwards clock can never *remove* tokens. `lastRefillMs` still\n * advances to `nowMs` so the bucket tracks the latest reading the caller\n * presented — the next forward step measures from there, not from a stale\n * future timestamp.\n */\nexport function refill(bucket: TokenBucket, nowMs: number): TokenBucket {\n const elapsedSec = Math.max(0, nowMs - bucket.lastRefillMs) / 1000;\n const replenished = bucket.tokens + elapsedSec * bucket.refillPerSec;\n return {\n ...bucket,\n tokens: Math.min(bucket.capacity, replenished),\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Refill against `nowMs`, then attempt to take `n` tokens (default 1).\n *\n * On success returns `[next, true]` with `n` subtracted; on failure returns\n * `[refilled, false]` — the refilled bucket (so the caller keeps the advanced\n * clock + accrued tokens) with no tokens spent. Branch on the boolean, thread\n * the bucket forward regardless:\n *\n * const [b1, ok] = tryConsume(bucket, nowMs);\n * return ok ? [{ ...state, bucket: b1 }, [doWork]] : [{ ...state, bucket: b1 }, []];\n *\n * PURE — input bucket is never mutated.\n */\nexport function tryConsume(\n bucket: TokenBucket,\n nowMs: number,\n n = 1,\n): readonly [TokenBucket, boolean] {\n const refilled = refill(bucket, nowMs);\n if (refilled.tokens >= n) {\n return [{ ...refilled, tokens: refilled.tokens - n }, true];\n }\n return [refilled, false];\n}\n\n// ===========================================================================\n// Sliding window log\n// ===========================================================================\n\n/**\n * A sliding-window log: the timestamps of every hit still inside the trailing\n * `windowMs`, capped at `limit` events per window. `hits` is `readonly` and\n * kept oldest-first (pruning slices the stale prefix off the front, recording\n * appends to the back), so the array is its own sorted index. Exact at the\n * window edge — unlike a token bucket's continuous refill, a hit leaves the\n * count the instant it ages past `windowMs`.\n */\nexport interface SlidingWindow {\n readonly hits: readonly number[];\n readonly windowMs: number;\n readonly limit: number;\n}\n\n/** Create an empty window. No hits recorded yet. */\nexport function initWindow(windowMs: number, limit: number): SlidingWindow {\n return { hits: [], windowMs, limit };\n}\n\n/**\n * Prune hits older than `nowMs - windowMs`, then record `nowMs` if there is\n * room. Returns `[next, true]` with the hit appended when `hits.length < limit`\n * after pruning, else `[pruned, false]` — the pruned window (capacity recovers\n * as old hits age out) with nothing appended.\n *\n * The cutoff is half-open: a hit exactly `windowMs` old is pruned (`<= cutoff`\n * drops it), so a hit recorded at `t` no longer counts at `t + windowMs`. That\n * makes the window's capacity fully recover `windowMs` after the last hit, the\n * behavior tests pin.\n *\n * PURE — input window is never mutated; `hits` is rebuilt by `filter` +\n * spread.\n */\nexport function record(\n window: SlidingWindow,\n nowMs: number,\n): readonly [SlidingWindow, boolean] {\n const cutoff = nowMs - window.windowMs;\n const pruned = window.hits.filter((t) => t > cutoff);\n if (pruned.length < window.limit) {\n return [{ ...window, hits: [...pruned, nowMs] }, true];\n }\n return [{ ...window, hits: pruned }, false];\n}\n\n/**\n * How many more hits `record` would accept at `nowMs` without blocking —\n * `limit` minus the live (post-prune) hit count, floored at 0. Read-only: it\n * prunes a local copy to answer but returns no new window. Use it to render a\n * \"N requests remaining\" affordance without committing a hit.\n */\nexport function remaining(window: SlidingWindow, nowMs: number): number {\n const cutoff = nowMs - window.windowMs;\n const live = window.hits.filter((t) => t > cutoff).length;\n return Math.max(0, window.limit - live);\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/do/durable-effects.ts"],"names":[],"mappings":";AA+GO,SAAS,WAAA,GAA0C;AACxD,EAAA,2BAAW,GAAA,EAAmB;AAChC;AAcO,SAAS,gBAAA,CACd,QACA,KAAA,EACyB;AACzB,EAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,MAAM,CAAA;AAC3B,EAAA,QAAQ,MAAM,IAAA;AAAM,IAClB,KAAK,aAAA;AACH,MAAA,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,EAAA,EAAI,KAAA,CAAM,MAAM,CAAA;AAC/B,MAAA,OAAO,IAAA;AAAA,IACT,KAAK,kBAAA;AACH,MAAA,IAAA,CAAK,MAAA,CAAO,MAAM,EAAE,CAAA;AACpB,MAAA,OAAO,IAAA;AAAA;AAEb;AAWO,SAAS,WACd,MAAA,EACyB;AACzB,EAAA,IAAI,SAAS,WAAA,EAAe;AAC5B,EAAA,KAAA,MAAW,KAAA,IAAS,MAAA,EAAQ,MAAA,GAAS,gBAAA,CAAiB,QAAQ,KAAK,CAAA;AACnE,EAAA,OAAO,MAAA;AACT;AAaO,SAAS,iBACd,MAAA,EAC0B;AAC1B,EAAA,MAAM,MAAuB,EAAC;AAC9B,EAAA,KAAA,MAAW,CAAC,EAAA,EAAI,MAAM,CAAA,IAAK,MAAA,MAAY,IAAA,CAAK,EAAE,EAAA,EAAI,MAAA,EAAQ,CAAA;AAC1D,EAAA,OAAO,GAAA;AACT;AAGO,SAAS,MAAA,CACd,QACA,EAAA,EACS;AACT,EAAA,OAAO,MAAA,CAAO,IAAI,EAAE,CAAA;AACtB;AAsDO,SAAS,qBAAwB,OAAA,EAGV;AAC5B,EAAA,IAAI,SAAkC,OAAA,EAAS,MAAA,GAC3C,WAAW,OAAA,CAAQ,MAAM,IACzB,WAAA,EAAe;AAKnB,EAAA,IAAI,OAAA,GAAU,SAAS,MAAA,IAAU,CAAA;AACjC,EAAA,IAAI,OAAA,EAAS,MAAA,KAAW,MAAA,IAAa,OAAA,EAAS,MAAA,EAAQ;AACpD,IAAA,KAAA,MAAW,CAAA,IAAK,QAAQ,MAAA,EAAQ,IAAI,EAAE,EAAA,GAAK,OAAA,YAAmB,CAAA,CAAE,EAAA;AAAA,EAClE,CAAA,MAAA,IAAW,OAAA,EAAS,MAAA,KAAW,MAAA,EAAW;AACxC,IAAA,KAAA,MAAW,MAAM,MAAA,CAAO,IAAA,IAAQ,IAAI,EAAA,GAAK,SAAS,OAAA,GAAU,EAAA;AAAA,EAC9D;AAEA,EAAA,OAAO;AAAA,IACL,IAAI,MAAA,EAAQ;AACV,MAAA,OAAA,IAAW,CAAA;AACX,MAAA,MAAM,EAAA,GAAK,OAAA;AACX,MAAA,MAAM,KAAA,GAAuB,EAAE,IAAA,EAAM,aAAA,EAAe,IAAI,MAAA,EAAO;AAC/D,MAAA,MAAA,GAAS,gBAAA,CAAiB,QAAQ,KAAK,CAAA;AACvC,MAAA,OAAO,EAAE,IAAI,KAAA,EAAM;AAAA,IACrB,CAAA;AAAA,IACA,QAAQ,EAAA,EAAI;AACV,MAAA,MAAM,SAAA,GAAY,MAAA,CAAO,GAAA,CAAI,EAAE,CAAA;AAC/B,MAAA,MAAM,KAAA,GAAyB,EAAE,IAAA,EAAM,kBAAA,EAAoB,EAAA,EAAG;AAC9D,MAAA,MAAA,GAAS,gBAAA,CAAiB,QAAQ,KAAK,CAAA;AACvC,MAAA,OAAO,EAAE,WAAW,KAAA,EAAM;AAAA,IAC5B,CAAA;AAAA,IACA,SAAA,GAAY;AACV,MAAA,OAAO,iBAAiB,MAAM,CAAA;AAAA,IAChC,CAAA;AAAA,IACA,MAAA,GAAS;AACP,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,MAAA,GAAS;AACP,MAAA,OAAO,OAAA;AAAA,IACT;AAAA,GACF;AACF","file":"chunk-3545BOYL.js","sourcesContent":["/**\n * @demlik/tea/do — the durable pending-effects ledger (substrate primitive #1).\n *\n * Durable *state* and durable *effects* are different problems. An\n * event-sourced actor rebuilds its STATE by folding the event log\n * (`doEventSourcedStore`, sibling file). But an outbound side effect — a tool\n * round-trip, a message to a sibling grain — is fire-and-forget: if the actor\n * interprets the `Cmd` inside an activation that hibernates mid-flight, the\n * effect is lost, yet nothing in the replayed state records that it is still\n * OWED. This is the one real tax of the event-sourced-virtual-actor target\n * (ADR 0003 primitive #1): \"I owe this effect\" must itself be part of the\n * folded state.\n *\n * Akka's classic `AtLeastOnceDelivery` solves it with a ledger of unconfirmed\n * deliveries kept AS PART OF the persisted state: each `deliver` adds an entry\n * keyed by a monotonic `deliveryId`; each `confirmDelivery` removes it. On\n * recovery the ledger is rebuilt from the SAME events, and any surviving entry\n * is re-sent. At-least-once ⇒ a confirmed-but-not-yet-persisted delivery WILL\n * be re-sent after a crash, so the receiver must deduplicate by id.\n *\n * ## The load-bearing rule (do not violate)\n *\n * The ledger is a PURE FOLD over the same kind of event stream the actor\n * already replays — it is **NOT** a separately-persisted side table:\n *\n * - `effect_owed` — `{ id, effect }` ADDS an entry to the ledger.\n * - `effect_confirmed`— `{ id }` REMOVES the entry.\n *\n * `foldLedger(events)` is `events.reduce(applyEffectEvent, empty())` — the same\n * fold shape `replay` / `foldEvents` / the recorder use. Persist these two\n * events into the SAME log `doEventSourcedStore` already appends (they are just\n * Msg variants), and the ledger is rebuilt for free on the next activation:\n * `foldLedger(log)` after eviction equals `foldLedger(log)` never-evicted, by\n * construction (a pure fold over identical input).\n *\n * On activation (replay completion), `survivingEffects(ledger)` returns the\n * owed-but-unconfirmed entries to re-fire, ordered by their monotonic id. The\n * re-fire is a SIDE EFFECT DERIVED from the surviving ledger — never stored\n * separately, never fired during replay (recovery.md: add to the ledger during\n * recovery; let recovery-END re-emit the survivors).\n *\n * Anti-patterns this file refuses (mirroring durable-effects.md):\n * - A side-table ledger persisted apart from the events. The events ARE the\n * ledger; the Map is rebuilt by folding them.\n * - Re-firing every historical effect during replay. Only `survivingEffects`\n * (post-fold) re-emits.\n * - Assuming the receiver gets each effect exactly once. At-least-once: dedup\n * by `id` at the receiver (`hasOwed` / `isConfirmed` here are the sender's\n * view; the receiver owns its own dedup).\n */\n\nimport type { Cmd } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The two events the ledger folds. These are ordinary Msg variants — persist\n// them into the same event log `doEventSourcedStore` appends, and the ledger\n// rebuilds itself on the next activation with no extra storage.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Monotonic, gap-free delivery id — the single correlation + dedup key. */\nexport type DeliveryId = number;\n\n/**\n * An effect is now OWED: it has been decided but its delivery is not yet\n * confirmed. Adds `{ id → effect }` to the ledger when folded.\n *\n * Persist this BEFORE interpreting the effect (durable-effects.md rule:\n * \"persist the intent before deliver\"). A crash between persist and deliver is\n * fine — the surviving entry re-fires on activation. A crash before the persist\n * means the effect was never owed, so nothing is lost that the actor promised.\n */\nexport interface EffectOwed<E> extends Cmd<\"effect_owed\"> {\n readonly id: DeliveryId;\n readonly effect: E;\n}\n\n/**\n * An owed effect has been CONFIRMED delivered. Removes its entry from the\n * ledger when folded. Persist this BEFORE acting on the confirmation\n * (\"persist the confirmation before confirmDelivery\").\n */\nexport interface EffectConfirmed extends Cmd<\"effect_confirmed\"> {\n readonly id: DeliveryId;\n}\n\n/** The event union the ledger folds over. `E` is the consumer's effect type. */\nexport type EffectLedgerEvent<E> = EffectOwed<E> | EffectConfirmed;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The ledger itself — a pure value. Insertion-ordered by monotonic id, so\n// `survivingEffects` re-emits in owed-order without a sort.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The pending-effects ledger: owed-but-unconfirmed effects keyed by their\n * monotonic delivery id. A pure value reconstructed by `foldLedger` — it is\n * never persisted on its own; the events are.\n *\n * `ReadonlyMap` preserves insertion order, which (because ids are appended\n * monotonically) is exactly id order — so iteration yields the survivors\n * oldest-first with no sort.\n */\nexport type PendingEffectsLedger<E> = ReadonlyMap<DeliveryId, E>;\n\n/** A single surviving entry to re-emit: its id and the effect it owes. */\nexport interface OwedEffect<E> {\n readonly id: DeliveryId;\n readonly effect: E;\n}\n\n/** The empty ledger — the fold's seed (a fresh actor owes nothing). */\nexport function emptyLedger<E>(): PendingEffectsLedger<E> {\n return new Map<DeliveryId, E>();\n}\n\n/**\n * Apply ONE ledger event — the reducer at the heart of the fold.\n *\n * - `effect_owed` → add `{ id → effect }`.\n * - `effect_confirmed` → remove `id` (no-op if already absent: a duplicate\n * confirm, or a confirm for an id evicted with the actor — at-least-once\n * tolerates both).\n *\n * Pure: returns a NEW ledger, never mutates the input — so folding the same\n * events twice (live and post-eviction) yields equal results, and a caller may\n * keep the prior ledger value.\n */\nexport function applyEffectEvent<E>(\n ledger: PendingEffectsLedger<E>,\n event: EffectLedgerEvent<E>,\n): PendingEffectsLedger<E> {\n const next = new Map(ledger);\n switch (event.type) {\n case \"effect_owed\":\n next.set(event.id, event.effect);\n return next;\n case \"effect_confirmed\":\n next.delete(event.id);\n return next;\n }\n}\n\n/**\n * THE FOLD. Rebuild the ledger from an event stream — the same fold shape\n * `replay` uses for state. After a simulated eviction this is called over the\n * persisted log and yields a ledger byte-for-byte equal to the never-evicted\n * one, because a pure fold over identical input is identical.\n *\n * This is the function that proves \"no side table\": the ledger is `events`\n * reduced, nothing more.\n */\nexport function foldLedger<E>(\n events: Iterable<EffectLedgerEvent<E>>,\n): PendingEffectsLedger<E> {\n let ledger = emptyLedger<E>();\n for (const event of events) ledger = applyEffectEvent(ledger, event);\n return ledger;\n}\n\n/**\n * RE-EMIT ON ACTIVATION. Given the rebuilt ledger, the owed-but-unconfirmed\n * effects to re-fire — ordered oldest-first by monotonic id. On a fresh /\n * fully-confirmed actor this is empty (nothing re-emitted).\n *\n * Idempotency is the RECEIVER's job: it must dedup by `id`, because\n * at-least-once means a re-emitted already-handled id WILL arrive again\n * (e.g. an effect confirmed in the activation that hibernated before the\n * `effect_confirmed` event was persisted). Re-firing the same id is therefore\n * a no-op AT THE RECEIVER, not here.\n */\nexport function survivingEffects<E>(\n ledger: PendingEffectsLedger<E>,\n): readonly OwedEffect<E>[] {\n const out: OwedEffect<E>[] = [];\n for (const [id, effect] of ledger) out.push({ id, effect });\n return out;\n}\n\n/** Is this id still owed (unconfirmed) in the ledger? Sender-side view. */\nexport function isOwed<E>(\n ledger: PendingEffectsLedger<E>,\n id: DeliveryId,\n): boolean {\n return ledger.has(id);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// pendingEffectsLedger — the stateful, monotonic-id-issuing recorder.\n//\n// The pure `foldLedger` is the contract; this is the ergonomic driver a host\n// uses live. It owns the monotonic `deliveryId` (gap-free, shared across all\n// destinations — durable-effects.md), emits the `effect_owed` / `effect_confirmed`\n// events for the caller to PERSIST, and keeps an in-memory mirror folded from\n// those same events so `survivingEffects` is available without a re-read.\n//\n// Mirrors `deferredGateway`'s factory shape: no clock / RNG captured; pure\n// bookkeeping. The caller is responsible for persisting each returned event\n// into the log (so the ledger survives eviction) and for actually interpreting\n// the effect after the owed event is durable.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * A live, monotonic-id-issuing ledger recorder.\n *\n * - `owe(effect)` — allocate the next `deliveryId`, return the\n * `effect_owed` event to PERSIST plus the allocated `id`. Folding the event\n * into the recorder's mirror is done for you; the caller persists it.\n * - `confirm(id)` — return the `effect_confirmed` event to PERSIST, and `true`\n * iff the id was owed (a duplicate confirm returns `false` — the boolean is\n * itself a dedup signal, exactly Akka `confirmDelivery`).\n * - `surviving()` — the current owed-but-unconfirmed effects (for re-emit on\n * activation).\n * - `ledger()` — the current folded ledger value.\n * - `lastId()` — the highest id issued (so a host can seed the counter from a\n * rebuilt ledger after eviction; see `restore`).\n */\nexport interface PendingEffectsRecorder<E> {\n owe(effect: E): { id: DeliveryId; event: EffectOwed<E> };\n confirm(id: DeliveryId): { confirmed: boolean; event: EffectConfirmed };\n surviving(): readonly OwedEffect<E>[];\n ledger(): PendingEffectsLedger<E>;\n lastId(): DeliveryId;\n}\n\n/**\n * Build a live recorder. `restore` rehydrates it after an eviction: pass the\n * events read back from the log (or the rebuilt ledger's survivors) so the\n * monotonic counter resumes WITHOUT gaps and the mirror reflects the surviving\n * owed set. Omit `restore` for a fresh actor.\n *\n * Counter seeding rule: the next id is `max(seen ids) + 1`. Restoring only the\n * survivors is sufficient for correctness of re-emit, but to keep ids gap-free\n * across confirmed-and-pruned entries the host should pass `lastId` explicitly\n * when it has it (e.g. persisted alongside the snapshot). When only survivors\n * are known, the counter resumes from the highest surviving id — a confirmed id\n * higher than every survivor could in principle be reissued, so a host that\n * needs strict global monotonicity persists `lastId`.\n */\nexport function pendingEffectsLedger<E>(restore?: {\n readonly events?: Iterable<EffectLedgerEvent<E>>;\n readonly lastId?: DeliveryId;\n}): PendingEffectsRecorder<E> {\n let mirror: PendingEffectsLedger<E> = restore?.events\n ? foldLedger(restore.events)\n : emptyLedger<E>();\n\n // Seed the monotonic counter. Prefer an explicit `lastId` (gap-free across\n // pruned ids); else the highest id observed in the restore events; else the\n // highest surviving id; else 0 (fresh).\n let counter = restore?.lastId ?? 0;\n if (restore?.lastId === undefined && restore?.events) {\n for (const e of restore.events) if (e.id > counter) counter = e.id;\n } else if (restore?.lastId === undefined) {\n for (const id of mirror.keys()) if (id > counter) counter = id;\n }\n\n return {\n owe(effect) {\n counter += 1;\n const id = counter;\n const event: EffectOwed<E> = { type: \"effect_owed\", id, effect };\n mirror = applyEffectEvent(mirror, event);\n return { id, event };\n },\n confirm(id) {\n const confirmed = mirror.has(id);\n const event: EffectConfirmed = { type: \"effect_confirmed\", id };\n mirror = applyEffectEvent(mirror, event);\n return { confirmed, event };\n },\n surviving() {\n return survivingEffects(mirror);\n },\n ledger() {\n return mirror;\n },\n lastId() {\n return counter;\n },\n };\n}\n"]}
|