@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
package/dist/saga/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/saga/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/saga — a forward-then-compensate transaction over an ordered list\n * of reversible steps.\n *\n * The Level-2 combinator for \"run these N steps in order; if any step fails,\n * undo the steps that already succeeded, in reverse.\" It is the distributed-\n * transaction / compensation knob: each step is a `{ do, undo }` pair of Cmds,\n * the saga drives `do` forward one step at a time, and on the first failure it\n * pivots and drives `undo` backward over exactly the steps that completed.\n *\n * **Boundary with `@demlik/tea/workflow` — the compensation test.** Both drive\n * an ordered multi-step transaction with reverse-order compensation; the line\n * between them is whether every step carries a true inverse. Here\n * `SagaStep<D, U>` REQUIRES both `do` and `undo` — a step without its inverse\n * is unrepresentable, so full reversibility is a type-level guarantee.\n * Workflow's `WorkflowStep.compensation` is OPTIONAL — an irreversible step (a\n * sent email) is skipped during the unwind. Pick by the test: **every step\n * must be reversible → saga; some steps are irreversible but the run must\n * finish → workflow.** Cross to workflow when the transaction includes steps\n * that can't be undone (or when you want each activity recorded as a durable\n * owed effect on the `../do` ledger); stay here when the type system should\n * enforce that every step can be undone.\n *\n * The recipe (which bricks it composes):\n * - `../work-queue/adapter` — the saga's forward progress IS a work-queue\n * lifecycle. Each step's record carries the same `{ id, status, input }`\n * vocabulary (`pending → running → done | failed | cancelled`), so the step\n * taxonomy is the substrate's status enum rather than a parallel invention.\n * We reuse the pure `QueueItem` / `QueueItemStatus` types AND delegate every\n * status flip to the `QueueAdapter` verbs: `wq.enqueue` stamps each fresh\n * ledger record, and `wq.patch` performs every\n * `pending → running → done | failed | cancelled` transition. The status\n * transitions are NOT re-rolled here — they live in exactly one place\n * behind the adapter, per the package's one-shape rule. The async\n * `createQueue(store)` adapter is NOT used: its `load → mutate → save` reads\n * the clock and lives at an I/O boundary, while a combinator's verbs must be\n * pure (invariant 2). We bind the same ops to the in-Model ledger instead —\n * durable + replayable, the two non-negotiables every knob preserves.\n *\n * The compensation log is the spec's `done: StepId[]` — the indices of the\n * steps that completed forward, in the order they completed. On `stepErr` the\n * saga walks that log in reverse, emitting one `undo` per `undoOk`, so the\n * unwind is the exact mirror of the wind. If an `undo` ITSELF fails, `undoErr`\n * pivots to the terminal `compensation_failed` phase: a failed compensation is\n * a real-world fact (a refund that bounced, a hold that won't release) that the\n * consumer must see and reconcile by hand — never a saga wedged in\n * `compensating` forever.\n *\n * Typical wiring (combinator form — splice the hooks into your machine):\n *\n * const saga = createSaga<BookFlight | ChargeCard | ..., Cancel | Refund | ...>({\n * steps: [\n * { do: { type: \"book_flight\" }, undo: { type: \"cancel_flight\" } },\n * { do: { type: \"charge_card\" }, undo: { type: \"refund_card\" } },\n * { do: { type: \"reserve_hotel\" },undo: { type: \"release_hotel\" } },\n * ],\n * });\n *\n * // in `update`:\n * begin: (s) => lift(s, saga.start(s.saga)),\n * step_ok: (s) => lift(s, saga.stepOk(s.saga)),\n * step_err: (s, m) => lift(s, saga.stepErr(s.saga, m.error)),\n * undo_ok: (s) => lift(s, saga.undoOk(s.saga)),\n * undo_err: (s, m) => lift(s, saga.undoErr(s.saga, m.error)),\n *\n * Every verb is PURE: it returns a NEW slice plus the Cmds to perform (the next\n * `do` going forward, or the next `undo` rolling back). The reducer never reads\n * a clock or RNG — step identity is positional (the index into `config.steps`),\n * so progress survives a round-trip through the durable slice and resumes\n * exactly where it left off after a DO eviction.\n *\n * There is no `subs` and no `handlers`: a saga runs no timers, and the `do` /\n * `undo` Cmds are fully caller-defined (the consumer's own `interpret` performs\n * each effect and routes Ok/Err back to the `stepOk` / `stepErr` / `undoOk`\n * verbs — the same shape `resilient-fetch` uses with `tryInterpret`). The saga\n * owns the ORDER; the consumer owns the I/O. Pure-ops module, like\n * `retry-backoff`.\n *\n * NOT a substrate primitive: it depends only on `../work-queue`'s pure types\n * and `../index`'s `Cmd` type. Consumers reach it via the `@demlik/tea/saga`\n * subpath.\n */\n\nimport type { Cmd } from \"../index\";\nimport type { QueueItem, QueueItemStatus } from \"../work-queue\";\nimport { queueAdapter } from \"../work-queue/adapter\";\n\n/**\n * A step's stable identity. Positional: the 0-based index into\n * `config.steps`. Positional ids are PURE (no clock, no RNG) and survive a\n * round-trip through the durable slice, so `done` can be replayed to resume an\n * interrupted saga exactly. The compensation log is a list of these.\n */\nexport type StepId = number;\n\n/**\n * One step of the saga: the forward effect and its compensating inverse, both\n * as data (plain Cmds). `do` runs while winding forward; `undo` runs while\n * unwinding after a failure. The pair is the unit of reversibility — a step\n * that completed (`do` succeeded) is exactly a step whose `undo` must run if a\n * later step fails.\n */\nexport interface SagaStep<D extends Cmd, U extends Cmd> {\n /** The forward effect, performed while the saga winds forward. */\n readonly do: D;\n /** The compensating inverse, performed (in reverse) while the saga unwinds. */\n readonly undo: U;\n}\n\n/**\n * The knob. The ordered list of reversible steps. The list is fixed for the\n * life of one saga instance (a saga drives a known transaction); per-run inputs\n * ride on the consumer's Msgs, not here.\n */\nexport interface SagaConfig<D extends Cmd, U extends Cmd> {\n /** The steps, in forward order. `do` runs `0 → n-1`; `undo` runs the completed prefix `n-1 → 0`. */\n readonly steps: readonly SagaStep<D, U>[];\n}\n\n/**\n * The lifecycle phase of a saga, narrowed at the type level so a consumer can\n * branch on `state.phase` (and a Transitions-table reducer can key on it).\n *\n * - `\"idle\"` — created, not started. No step has run.\n * - `\"running\"` — winding forward; `position` is the in-flight step.\n * - `\"compensating\"` — a step failed; unwinding `done` in reverse.\n * - `\"committed\"` — every step completed forward. Terminal success.\n * - `\"aborted\"` — compensation finished (every completed step undone, or\n * the first step itself failed with nothing to undo).\n * Terminal failure, fully unwound.\n * - `\"compensation_failed\"` — an `undo` itself failed mid-rollback. Terminal\n * failure, PARTIALLY unwound: the steps still in `done`\n * were never compensated and must be reconciled by hand.\n * Distinguished from `aborted` precisely so a stuck\n * rollback is visible state, not a saga wedged in\n * `compensating` forever.\n */\nexport type SagaPhase =\n | \"idle\"\n | \"running\"\n | \"compensating\"\n | \"committed\"\n | \"aborted\"\n | \"compensation_failed\";\n\n/**\n * The slice this knob owns.\n *\n * The spec's three fields are load-bearing:\n * - `position` — the index of the step currently in flight while `running`,\n * or the index being compensated while `compensating`. Survives eviction,\n * so `boot` resumes mid-saga from the durable slice.\n * - `done` — the compensation log: the ids of steps that completed forward,\n * in completion order. `undo` walks this in reverse. This is the single\n * source of truth for what must be unwound.\n * - `compensating` — the direction bit. `false` while winding forward, `true`\n * once a failure has pivoted the saga into rollback.\n *\n * Plus:\n * - `phase` — the derived lifecycle tag (see `SagaPhase`). Kept in the slice\n * (not recomputed) so a consumer's view and Transitions reducer can key on\n * `state.phase` directly, and the terminal states (`committed` / `aborted`)\n * are explicit facts rather than \"position past the end\" arithmetic.\n * - `error` — the forward failure that triggered compensation, carried\n * forward verbatim (`unknown`) for the consumer to read, never interpreted\n * (same discipline as `retry-backoff`'s `lastError` and `fan-out`'s\n * `error`).\n * - `compensationError` — the failure of an `undo` itself, set only when the\n * saga reaches `compensation_failed`. Kept distinct from `error` (the\n * forward failure) so a consumer reconciling a stuck rollback can read both\n * \"what broke the transaction\" and \"which compensation then bounced\".\n * Carried verbatim, never interpreted, same as `error`.\n * - `items` — the work-queue ledger, one `QueueItem` per step, keyed by the\n * step's positional id. The single source of truth for each step's status;\n * `phase` / `position` / `done` are the projection the verbs keep in\n * lockstep with it.\n *\n * Every field is `readonly` and JSON-serializable (plain numbers, arrays, and\n * the work-queue's plain `QueueItem` records), so the slice round-trips through\n * a consumer's `Store<S>` unchanged — durable across DO eviction.\n */\nexport interface SagaState<I = StepId> {\n /** The step in flight (`running`) or being compensated (`compensating`). */\n readonly position: number;\n /** The compensation log: ids of completed-forward steps, in completion order. */\n readonly done: readonly StepId[];\n /** Direction bit: `false` winding forward, `true` unwinding. */\n readonly compensating: boolean;\n /** The derived lifecycle tag (see `SagaPhase`). */\n readonly phase: SagaPhase;\n /** The forward failure that triggered compensation, if any. Carried, never interpreted. */\n readonly error?: unknown;\n /** The failure of an `undo` itself (only when `compensation_failed`). Carried, never interpreted. */\n readonly compensationError?: unknown;\n /** The work-queue ledger: one record per step, keyed by positional id. */\n readonly items: readonly QueueItem<I>[];\n}\n\n/** Whether the saga has terminated successfully (every step committed). */\nexport function isCommitted<I>(state: SagaState<I>): boolean {\n return state.phase === \"committed\";\n}\n\n/** Whether the saga has terminated in failure, fully unwound (every completed step undone). */\nexport function isAborted<I>(state: SagaState<I>): boolean {\n return state.phase === \"aborted\";\n}\n\n/**\n * Whether the saga has terminated in failure with an UNFINISHED rollback: an\n * `undo` itself failed, so some completed steps were never compensated and need\n * hand reconciliation. Terminal, but distinct from `aborted` (fully unwound).\n */\nexport function isCompensationFailed<I>(state: SagaState<I>): boolean {\n return state.phase === \"compensation_failed\";\n}\n\n/**\n * Whether the saga has reached a terminal phase (success, fully-unwound\n * failure, or compensation-failed). Derived — reads the slice, never mutates.\n * Consumers branch their own phase off this; no further verb has any effect\n * once it holds (the verbs guard on phase and no-op).\n */\nexport function isSettled<I>(state: SagaState<I>): boolean {\n return (\n state.phase === \"committed\" ||\n state.phase === \"aborted\" ||\n state.phase === \"compensation_failed\"\n );\n}\n\n/**\n * The starting slice: idle, nothing run, winding-forward direction. `items` is\n * empty until `start` stamps one ledger record per configured step — the slice\n * is generic in the ledger payload `I`, defaulting to the positional `StepId`\n * since a saga's \"input\" per step is just its index (the consumer's real inputs\n * ride on its own Msgs).\n */\nexport function initSaga<I = StepId>(): SagaState<I> {\n return {\n position: 0,\n done: [],\n compensating: false,\n phase: \"idle\",\n items: [],\n };\n}\n\n/**\n * Build the saga knob from `config`. Returns the uniform combinator contract:\n * `init` for the owned slice and the five lifecycle verbs (`start`, `stepOk`,\n * `stepErr`, `undoOk`, `undoErr`).\n *\n * No `subs` and no `handlers` — a saga runs no timers, and each step's `do` /\n * `undo` effect is owned by the consumer's `interpret` (the saga emits the Cmd;\n * the consumer performs it and routes the outcome back to a verb). The saga's\n * sole job is the ORDER: which Cmd to emit next, and when to pivot from winding\n * to unwinding (and when a failed `undo` ends the rollback). Pure-ops module —\n * matches `retry-backoff`'s shape.\n */\nexport function createSaga<D extends Cmd, U extends Cmd>(\n config: SagaConfig<D, U>,\n) {\n // The ledger seam, bound once. The saga delegates ledger construction to\n // `wq.enqueue` and every status flip to `wq.patch`, so the `QueueItem<StepId>`\n // shape stays insulated behind the verbs.\n const wq = queueAdapter<StepId>();\n\n /**\n * One ledger record per configured step, keyed by its positional id. Each\n * record is stamped by the `QueueAdapter`'s `enqueue` (the saga delegates\n * ledger construction to the verb rather than building records inline).\n *\n * The positional id is stringified — `QueueItem.id` is a string, the saga's\n * step identity is the index; `String(index)` keeps both the ledger's\n * string-id contract and the positional identity. `enqueuedAt` is `0`: saga\n * verbs are pure and the saga makes no timestamp-based decision (ordering is\n * positional), so there is no clock to read here.\n */\n function freshLedger(): QueueItem<StepId>[] {\n let items: QueueItem<StepId>[] = [];\n config.steps.forEach((_step, index) => {\n items = wq.enqueue(items, index, 0, String(index)).next;\n });\n return items;\n }\n\n /**\n * Flip the ledger record for step `index` to `status`, delegating the\n * transition to the `QueueAdapter`'s `patch` (keyed by the record's string\n * id, `String(index)`) so the status flip is NOT re-rolled here. PURE —\n * `patch` returns a new array; an unknown id returns a copy unchanged (the\n * verbs guard against out-of-range, but the helper is total).\n */\n function setStatus(\n items: readonly QueueItem<StepId>[],\n index: number,\n status: QueueItemStatus,\n ): QueueItem<StepId>[] {\n return wq.patch(items, String(index), (item) => ({\n ...item,\n status,\n })).next;\n }\n\n /**\n * The step at `index`, or `undefined` if out of range. Centralizes the bounds\n * check so the verbs read a step through a narrowing guard instead of a\n * non-null assertion: `const step = stepAt(i); if (step === undefined) …`.\n */\n function stepAt(index: number): SagaStep<D, U> | undefined {\n return config.steps[index];\n }\n\n /** The most-recently-completed step id (tail of `done`), or `undefined` if the log is empty. */\n function lastDone(done: readonly StepId[]): StepId | undefined {\n return done[done.length - 1];\n }\n\n /**\n * Begin the saga: stamp a `pending` ledger record per step, mark step 0\n * `running`, and emit step 0's `do` Cmd. PURE.\n *\n * A saga with zero configured steps commits immediately (nothing to do, so\n * the empty transaction trivially succeeds) and emits no Cmd. Calling `start`\n * on an already-started saga (phase ≠ `\"idle\"`) is a no-op: the slice is\n * returned unchanged with no Cmds, so a stray re-`start` after a boot/replay\n * can't relaunch step 0 on top of an in-flight saga.\n */\n function start(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly D[]] {\n if (state.phase !== \"idle\") return [state, []];\n\n const items = freshLedger();\n const first = stepAt(0);\n if (first === undefined) {\n // Empty transaction: trivially committed, no effect.\n return [{ ...state, phase: \"committed\", items }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n position: 0,\n phase: \"running\",\n items: setStatus(items, 0, \"running\"),\n };\n return [next, [first.do]];\n }\n\n /**\n * Record the in-flight step as committed and advance. PURE.\n *\n * Flips the current step's ledger record `running → done`, appends its id to\n * the compensation log, and:\n * - if a next step exists, marks it `running` and emits its `do` Cmd;\n * - if this was the last step, transitions to `committed` (terminal\n * success) and emits no Cmd.\n *\n * `stepOk` while not `running` (idle, compensating, or already terminal) is a\n * no-op — the runtime's serial dispatch makes a true double-`stepOk`\n * impossible, but a stale success Msg after a boot/replay is real, and\n * swallowing it is the safe direction (the step's outcome is already recorded\n * in the ledger).\n */\n function stepOk(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly D[]] {\n if (state.phase !== \"running\") return [state, []];\n\n const completed = state.position;\n const itemsDone = setStatus(state.items, completed, \"done\");\n const done = [...state.done, completed];\n const nextPosition = completed + 1;\n const nextStep = stepAt(nextPosition);\n\n if (nextStep === undefined) {\n // No next step — the last step committed, so the whole transaction\n // succeeded.\n return [{ ...state, done, items: itemsDone, phase: \"committed\" }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n position: nextPosition,\n done,\n items: setStatus(itemsDone, nextPosition, \"running\"),\n };\n return [next, [nextStep.do]];\n }\n\n /**\n * Pivot from winding to unwinding: the in-flight step failed. PURE.\n *\n * Flips the failed step's ledger record `running → failed`, records the\n * `error`, sets the direction bit, and begins compensation by emitting the\n * `undo` of the MOST-recently-completed step (the tail of the compensation\n * log) — rollback is the mirror of progress, so the last step that succeeded\n * is the first to be undone.\n *\n * If nothing completed forward (the first step itself failed), there is\n * nothing to compensate: the saga goes straight to `aborted` (terminal\n * failure) and emits no `undo`.\n *\n * `stepErr` while not `running` is a no-op (same stale-Msg reasoning as\n * `stepOk`): once a saga is already compensating or terminal, a late failure\n * Msg for an old step must not restart or re-pivot the unwind.\n */\n function stepErr(\n state: SagaState<StepId>,\n error: unknown,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"running\") return [state, []];\n\n const failed = state.position;\n const items = setStatus(state.items, failed, \"failed\");\n\n // Compensate the most-recently-completed step first. `lastDone === undefined`\n // means nothing completed forward (the first step itself failed) — there is\n // nothing to undo, so the saga aborts straight away. The guard narrows\n // `undoIndex` to `StepId` and `undoStep` to a present step without a\n // non-null assertion.\n const undoIndex = lastDone(state.done);\n const undoStep = undoIndex === undefined ? undefined : stepAt(undoIndex);\n if (undoIndex === undefined || undoStep === undefined) {\n return [\n { ...state, items, error, compensating: true, phase: \"aborted\" },\n [],\n ];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n items,\n error,\n compensating: true,\n phase: \"compensating\",\n position: undoIndex,\n };\n return [next, [undoStep.undo]];\n }\n\n /**\n * Continue the rollback: the `undo` for the current compensation position\n * succeeded. PURE.\n *\n * Pops the just-undone step off the tail of the compensation log (flipping\n * its ledger record `done → cancelled` to mark it rolled back), and:\n * - if more completed steps remain, emits the next one's `undo` (continuing\n * in reverse) and moves `position` to it;\n * - if the log is now empty, transitions to `aborted` (terminal failure —\n * the saga is fully unwound) and emits no Cmd.\n *\n * `undoOk` while not `compensating` is a no-op (same stale-Msg reasoning as\n * the forward verbs): an `undo`-success Msg that arrives after the saga has\n * already finished unwinding must not pop a step that isn't there.\n */\n function undoOk(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"compensating\") return [state, []];\n\n // The tail of the log is the step we just compensated — mark it cancelled.\n // `undone === undefined` cannot happen while `compensating` (the phase is\n // only entered with a non-empty log), but the guard keeps the lookup total\n // and narrows `undone` to `StepId` without a non-null assertion.\n const undone = lastDone(state.done);\n if (undone === undefined) return [state, []];\n const remaining = state.done.slice(0, -1);\n const items = setStatus(state.items, undone, \"cancelled\");\n\n // The next step to compensate is the new tail of the log. `nextUndo`\n // narrows to `StepId` here and to a present step via `stepAt`, so the\n // continue-vs-terminate branch needs no non-null assertion.\n const nextUndo = lastDone(remaining);\n const nextStep = nextUndo === undefined ? undefined : stepAt(nextUndo);\n if (nextUndo === undefined || nextStep === undefined) {\n // Fully unwound — terminal failure.\n return [{ ...state, done: remaining, items, phase: \"aborted\" }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n done: remaining,\n items,\n position: nextUndo,\n };\n return [next, [nextStep.undo]];\n }\n\n /**\n * Halt the rollback: the `undo` for the current compensation position itself\n * FAILED. PURE.\n *\n * A failed compensation is a real-world fact — a refund that bounced, a hold\n * that won't release — and it must become visible terminal state, never leave\n * the saga wedged in `compensating` forever waiting for an `undoOk` that will\n * never come. So `undoErr`:\n * - flips the in-flight compensation step's ledger record `done → failed`\n * (its `undo` did not take, so it is NOT `cancelled`);\n * - records the undo failure in `compensationError` (distinct from `error`,\n * the forward failure that started the rollback);\n * - transitions to the terminal `compensation_failed` phase and emits no\n * Cmd. The compensation log is LEFT INTACT (`done` keeps the steps that\n * were never rolled back) so the consumer can see exactly which steps\n * still need hand reconciliation.\n *\n * `undoErr` while not `compensating` is a no-op (same stale-Msg reasoning as\n * the other verbs): an `undo`-failure Msg that arrives after the saga has\n * already settled must not re-pivot a terminal saga.\n */\n function undoErr(\n state: SagaState<StepId>,\n error: unknown,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"compensating\") return [state, []];\n\n // The step whose `undo` just failed is the current compensation position.\n // Its forward effect stands un-reversed, so its record goes `done → failed`,\n // not `cancelled`.\n const items = setStatus(state.items, state.position, \"failed\");\n return [\n {\n ...state,\n items,\n compensationError: error,\n phase: \"compensation_failed\",\n },\n [],\n ];\n }\n\n return {\n init: initSaga<StepId>,\n start,\n stepOk,\n stepErr,\n undoOk,\n undoErr,\n isCommitted,\n isAborted,\n isCompensationFailed,\n isSettled,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/saga/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @packageDocumentation\n * @demlik/tea/saga — a forward-then-compensate transaction over an ordered list\n * of reversible steps.\n *\n * The Level-2 combinator for \"run these N steps in order; if any step fails,\n * undo the steps that already succeeded, in reverse.\" It is the distributed-\n * transaction / compensation knob: each step is a `{ do, undo }` pair of Cmds,\n * the saga drives `do` forward one step at a time, and on the first failure it\n * pivots and drives `undo` backward over exactly the steps that completed.\n *\n * **Boundary with `@demlik/tea/workflow` — the compensation test.** Both drive\n * an ordered multi-step transaction with reverse-order compensation; the line\n * between them is whether every step carries a true inverse. Here\n * `SagaStep<D, U>` REQUIRES both `do` and `undo` — a step without its inverse\n * is unrepresentable, so full reversibility is a type-level guarantee.\n * Workflow's `WorkflowStep.compensation` is OPTIONAL — an irreversible step (a\n * sent email) is skipped during the unwind. Pick by the test: **every step\n * must be reversible → saga; some steps are irreversible but the run must\n * finish → workflow.** Cross to workflow when the transaction includes steps\n * that can't be undone (or when you want each activity recorded as a durable\n * owed effect on the `../do` ledger); stay here when the type system should\n * enforce that every step can be undone.\n *\n * The recipe (which bricks it composes):\n * - `../work-queue/adapter` — the saga's forward progress IS a work-queue\n * lifecycle. Each step's record carries the same `{ id, status, input }`\n * vocabulary (`pending → running → done | failed | cancelled`), so the step\n * taxonomy is the substrate's status enum rather than a parallel invention.\n * We reuse the pure `QueueItem` / `QueueItemStatus` types AND delegate every\n * status flip to the `QueueAdapter` verbs: `wq.enqueue` stamps each fresh\n * ledger record, and `wq.patch` performs every\n * `pending → running → done | failed | cancelled` transition. The status\n * transitions are NOT re-rolled here — they live in exactly one place\n * behind the adapter, per the package's one-shape rule. The async\n * `createQueue(store)` adapter is NOT used: its `load → mutate → save` reads\n * the clock and lives at an I/O boundary, while a combinator's verbs must be\n * pure (invariant 2). We bind the same ops to the in-Model ledger instead —\n * durable + replayable, the two non-negotiables every knob preserves.\n *\n * The compensation log is the spec's `done: StepId[]` — the indices of the\n * steps that completed forward, in the order they completed. On `stepErr` the\n * saga walks that log in reverse, emitting one `undo` per `undoOk`, so the\n * unwind is the exact mirror of the wind. If an `undo` ITSELF fails, `undoErr`\n * pivots to the terminal `compensation_failed` phase: a failed compensation is\n * a real-world fact (a refund that bounced, a hold that won't release) that the\n * consumer must see and reconcile by hand — never a saga wedged in\n * `compensating` forever.\n *\n * Typical wiring (combinator form — splice the hooks into your machine):\n *\n * const saga = createSaga<BookFlight | ChargeCard | ..., Cancel | Refund | ...>({\n * steps: [\n * { do: { type: \"book_flight\" }, undo: { type: \"cancel_flight\" } },\n * { do: { type: \"charge_card\" }, undo: { type: \"refund_card\" } },\n * { do: { type: \"reserve_hotel\" },undo: { type: \"release_hotel\" } },\n * ],\n * });\n *\n * // in `update`:\n * begin: (s) => lift(s, saga.start(s.saga)),\n * step_ok: (s) => lift(s, saga.stepOk(s.saga)),\n * step_err: (s, m) => lift(s, saga.stepErr(s.saga, m.error)),\n * undo_ok: (s) => lift(s, saga.undoOk(s.saga)),\n * undo_err: (s, m) => lift(s, saga.undoErr(s.saga, m.error)),\n *\n * Every verb is PURE: it returns a NEW slice plus the Cmds to perform (the next\n * `do` going forward, or the next `undo` rolling back). The reducer never reads\n * a clock or RNG — step identity is positional (the index into `config.steps`),\n * so progress survives a round-trip through the durable slice and resumes\n * exactly where it left off after a DO eviction.\n *\n * There is no `subs` and no `handlers`: a saga runs no timers, and the `do` /\n * `undo` Cmds are fully caller-defined (the consumer's own `interpret` performs\n * each effect and routes Ok/Err back to the `stepOk` / `stepErr` / `undoOk`\n * verbs — the same shape `resilient-fetch` uses with `tryInterpret`). The saga\n * owns the ORDER; the consumer owns the I/O. Pure-ops module, like\n * `retry-backoff`.\n *\n * NOT a substrate primitive: it depends only on `../work-queue`'s pure types\n * and `../index`'s `Cmd` type. Consumers reach it via the `@demlik/tea/saga`\n * subpath.\n */\n\nimport type { Cmd } from \"../index\";\nimport type { QueueItem, QueueItemStatus } from \"../work-queue\";\nimport { queueAdapter } from \"../work-queue/adapter\";\n\n/**\n * A step's stable identity. Positional: the 0-based index into\n * `config.steps`. Positional ids are PURE (no clock, no RNG) and survive a\n * round-trip through the durable slice, so `done` can be replayed to resume an\n * interrupted saga exactly. The compensation log is a list of these.\n */\nexport type StepId = number;\n\n/**\n * One step of the saga: the forward effect and its compensating inverse, both\n * as data (plain Cmds). `do` runs while winding forward; `undo` runs while\n * unwinding after a failure. The pair is the unit of reversibility — a step\n * that completed (`do` succeeded) is exactly a step whose `undo` must run if a\n * later step fails.\n */\nexport interface SagaStep<D extends Cmd, U extends Cmd> {\n /** The forward effect, performed while the saga winds forward. */\n readonly do: D;\n /** The compensating inverse, performed (in reverse) while the saga unwinds. */\n readonly undo: U;\n}\n\n/**\n * The knob. The ordered list of reversible steps. The list is fixed for the\n * life of one saga instance (a saga drives a known transaction); per-run inputs\n * ride on the consumer's Msgs, not here.\n */\nexport interface SagaConfig<D extends Cmd, U extends Cmd> {\n /** The steps, in forward order. `do` runs `0 → n-1`; `undo` runs the completed prefix `n-1 → 0`. */\n readonly steps: readonly SagaStep<D, U>[];\n}\n\n/**\n * The lifecycle phase of a saga, narrowed at the type level so a consumer can\n * branch on `state.phase` (and a Transitions-table reducer can key on it).\n *\n * - `\"idle\"` — created, not started. No step has run.\n * - `\"running\"` — winding forward; `position` is the in-flight step.\n * - `\"compensating\"` — a step failed; unwinding `done` in reverse.\n * - `\"committed\"` — every step completed forward. Terminal success.\n * - `\"aborted\"` — compensation finished (every completed step undone, or\n * the first step itself failed with nothing to undo).\n * Terminal failure, fully unwound.\n * - `\"compensation_failed\"` — an `undo` itself failed mid-rollback. Terminal\n * failure, PARTIALLY unwound: the steps still in `done`\n * were never compensated and must be reconciled by hand.\n * Distinguished from `aborted` precisely so a stuck\n * rollback is visible state, not a saga wedged in\n * `compensating` forever.\n */\nexport type SagaPhase =\n | \"idle\"\n | \"running\"\n | \"compensating\"\n | \"committed\"\n | \"aborted\"\n | \"compensation_failed\";\n\n/**\n * The slice this knob owns.\n *\n * The spec's three fields are load-bearing:\n * - `position` — the index of the step currently in flight while `running`,\n * or the index being compensated while `compensating`. Survives eviction,\n * so `boot` resumes mid-saga from the durable slice.\n * - `done` — the compensation log: the ids of steps that completed forward,\n * in completion order. `undo` walks this in reverse. This is the single\n * source of truth for what must be unwound.\n * - `compensating` — the direction bit. `false` while winding forward, `true`\n * once a failure has pivoted the saga into rollback.\n *\n * Plus:\n * - `phase` — the derived lifecycle tag (see `SagaPhase`). Kept in the slice\n * (not recomputed) so a consumer's view and Transitions reducer can key on\n * `state.phase` directly, and the terminal states (`committed` / `aborted`)\n * are explicit facts rather than \"position past the end\" arithmetic.\n * - `error` — the forward failure that triggered compensation, carried\n * forward verbatim (`unknown`) for the consumer to read, never interpreted\n * (same discipline as `retry-backoff`'s `lastError` and `fan-out`'s\n * `error`).\n * - `compensationError` — the failure of an `undo` itself, set only when the\n * saga reaches `compensation_failed`. Kept distinct from `error` (the\n * forward failure) so a consumer reconciling a stuck rollback can read both\n * \"what broke the transaction\" and \"which compensation then bounced\".\n * Carried verbatim, never interpreted, same as `error`.\n * - `items` — the work-queue ledger, one `QueueItem` per step, keyed by the\n * step's positional id. The single source of truth for each step's status;\n * `phase` / `position` / `done` are the projection the verbs keep in\n * lockstep with it.\n *\n * Every field is `readonly` and JSON-serializable (plain numbers, arrays, and\n * the work-queue's plain `QueueItem` records), so the slice round-trips through\n * a consumer's `Store<S>` unchanged — durable across DO eviction.\n */\nexport interface SagaState<I = StepId> {\n /** The step in flight (`running`) or being compensated (`compensating`). */\n readonly position: number;\n /** The compensation log: ids of completed-forward steps, in completion order. */\n readonly done: readonly StepId[];\n /** Direction bit: `false` winding forward, `true` unwinding. */\n readonly compensating: boolean;\n /** The derived lifecycle tag (see `SagaPhase`). */\n readonly phase: SagaPhase;\n /** The forward failure that triggered compensation, if any. Carried, never interpreted. */\n readonly error?: unknown;\n /** The failure of an `undo` itself (only when `compensation_failed`). Carried, never interpreted. */\n readonly compensationError?: unknown;\n /** The work-queue ledger: one record per step, keyed by positional id. */\n readonly items: readonly QueueItem<I>[];\n}\n\n/** Whether the saga has terminated successfully (every step committed). */\nexport function isCommitted<I>(state: SagaState<I>): boolean {\n return state.phase === \"committed\";\n}\n\n/** Whether the saga has terminated in failure, fully unwound (every completed step undone). */\nexport function isAborted<I>(state: SagaState<I>): boolean {\n return state.phase === \"aborted\";\n}\n\n/**\n * Whether the saga has terminated in failure with an UNFINISHED rollback: an\n * `undo` itself failed, so some completed steps were never compensated and need\n * hand reconciliation. Terminal, but distinct from `aborted` (fully unwound).\n */\nexport function isCompensationFailed<I>(state: SagaState<I>): boolean {\n return state.phase === \"compensation_failed\";\n}\n\n/**\n * Whether the saga has reached a terminal phase (success, fully-unwound\n * failure, or compensation-failed). Derived — reads the slice, never mutates.\n * Consumers branch their own phase off this; no further verb has any effect\n * once it holds (the verbs guard on phase and no-op).\n */\nexport function isSettled<I>(state: SagaState<I>): boolean {\n return (\n state.phase === \"committed\" ||\n state.phase === \"aborted\" ||\n state.phase === \"compensation_failed\"\n );\n}\n\n/**\n * The starting slice: idle, nothing run, winding-forward direction. `items` is\n * empty until `start` stamps one ledger record per configured step — the slice\n * is generic in the ledger payload `I`, defaulting to the positional `StepId`\n * since a saga's \"input\" per step is just its index (the consumer's real inputs\n * ride on its own Msgs).\n */\nexport function initSaga<I = StepId>(): SagaState<I> {\n return {\n position: 0,\n done: [],\n compensating: false,\n phase: \"idle\",\n items: [],\n };\n}\n\n/**\n * Build the saga knob from `config`. Returns the uniform combinator contract:\n * `init` for the owned slice and the five lifecycle verbs (`start`, `stepOk`,\n * `stepErr`, `undoOk`, `undoErr`).\n *\n * No `subs` and no `handlers` — a saga runs no timers, and each step's `do` /\n * `undo` effect is owned by the consumer's `interpret` (the saga emits the Cmd;\n * the consumer performs it and routes the outcome back to a verb). The saga's\n * sole job is the ORDER: which Cmd to emit next, and when to pivot from winding\n * to unwinding (and when a failed `undo` ends the rollback). Pure-ops module —\n * matches `retry-backoff`'s shape.\n */\nexport function createSaga<D extends Cmd, U extends Cmd>(\n config: SagaConfig<D, U>,\n) {\n // The ledger seam, bound once. The saga delegates ledger construction to\n // `wq.enqueue` and every status flip to `wq.patch`, so the `QueueItem<StepId>`\n // shape stays insulated behind the verbs.\n const wq = queueAdapter<StepId>();\n\n /**\n * One ledger record per configured step, keyed by its positional id. Each\n * record is stamped by the `QueueAdapter`'s `enqueue` (the saga delegates\n * ledger construction to the verb rather than building records inline).\n *\n * The positional id is stringified — `QueueItem.id` is a string, the saga's\n * step identity is the index; `String(index)` keeps both the ledger's\n * string-id contract and the positional identity. `enqueuedAt` is `0`: saga\n * verbs are pure and the saga makes no timestamp-based decision (ordering is\n * positional), so there is no clock to read here.\n */\n function freshLedger(): QueueItem<StepId>[] {\n let items: QueueItem<StepId>[] = [];\n config.steps.forEach((_step, index) => {\n items = wq.enqueue(items, index, 0, String(index)).next;\n });\n return items;\n }\n\n /**\n * Flip the ledger record for step `index` to `status`, delegating the\n * transition to the `QueueAdapter`'s `patch` (keyed by the record's string\n * id, `String(index)`) so the status flip is NOT re-rolled here. PURE —\n * `patch` returns a new array; an unknown id returns a copy unchanged (the\n * verbs guard against out-of-range, but the helper is total).\n */\n function setStatus(\n items: readonly QueueItem<StepId>[],\n index: number,\n status: QueueItemStatus,\n ): QueueItem<StepId>[] {\n return wq.patch(items, String(index), (item) => ({\n ...item,\n status,\n })).next;\n }\n\n /**\n * The step at `index`, or `undefined` if out of range. Centralizes the bounds\n * check so the verbs read a step through a narrowing guard instead of a\n * non-null assertion: `const step = stepAt(i); if (step === undefined) …`.\n */\n function stepAt(index: number): SagaStep<D, U> | undefined {\n return config.steps[index];\n }\n\n /** The most-recently-completed step id (tail of `done`), or `undefined` if the log is empty. */\n function lastDone(done: readonly StepId[]): StepId | undefined {\n return done[done.length - 1];\n }\n\n /**\n * Begin the saga: stamp a `pending` ledger record per step, mark step 0\n * `running`, and emit step 0's `do` Cmd. PURE.\n *\n * A saga with zero configured steps commits immediately (nothing to do, so\n * the empty transaction trivially succeeds) and emits no Cmd. Calling `start`\n * on an already-started saga (phase ≠ `\"idle\"`) is a no-op: the slice is\n * returned unchanged with no Cmds, so a stray re-`start` after a boot/replay\n * can't relaunch step 0 on top of an in-flight saga.\n */\n function start(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly D[]] {\n if (state.phase !== \"idle\") return [state, []];\n\n const items = freshLedger();\n const first = stepAt(0);\n if (first === undefined) {\n // Empty transaction: trivially committed, no effect.\n return [{ ...state, phase: \"committed\", items }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n position: 0,\n phase: \"running\",\n items: setStatus(items, 0, \"running\"),\n };\n return [next, [first.do]];\n }\n\n /**\n * Record the in-flight step as committed and advance. PURE.\n *\n * Flips the current step's ledger record `running → done`, appends its id to\n * the compensation log, and:\n * - if a next step exists, marks it `running` and emits its `do` Cmd;\n * - if this was the last step, transitions to `committed` (terminal\n * success) and emits no Cmd.\n *\n * `stepOk` while not `running` (idle, compensating, or already terminal) is a\n * no-op — the runtime's serial dispatch makes a true double-`stepOk`\n * impossible, but a stale success Msg after a boot/replay is real, and\n * swallowing it is the safe direction (the step's outcome is already recorded\n * in the ledger).\n */\n function stepOk(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly D[]] {\n if (state.phase !== \"running\") return [state, []];\n\n const completed = state.position;\n const itemsDone = setStatus(state.items, completed, \"done\");\n const done = [...state.done, completed];\n const nextPosition = completed + 1;\n const nextStep = stepAt(nextPosition);\n\n if (nextStep === undefined) {\n // No next step — the last step committed, so the whole transaction\n // succeeded.\n return [{ ...state, done, items: itemsDone, phase: \"committed\" }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n position: nextPosition,\n done,\n items: setStatus(itemsDone, nextPosition, \"running\"),\n };\n return [next, [nextStep.do]];\n }\n\n /**\n * Pivot from winding to unwinding: the in-flight step failed. PURE.\n *\n * Flips the failed step's ledger record `running → failed`, records the\n * `error`, sets the direction bit, and begins compensation by emitting the\n * `undo` of the MOST-recently-completed step (the tail of the compensation\n * log) — rollback is the mirror of progress, so the last step that succeeded\n * is the first to be undone.\n *\n * If nothing completed forward (the first step itself failed), there is\n * nothing to compensate: the saga goes straight to `aborted` (terminal\n * failure) and emits no `undo`.\n *\n * `stepErr` while not `running` is a no-op (same stale-Msg reasoning as\n * `stepOk`): once a saga is already compensating or terminal, a late failure\n * Msg for an old step must not restart or re-pivot the unwind.\n */\n function stepErr(\n state: SagaState<StepId>,\n error: unknown,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"running\") return [state, []];\n\n const failed = state.position;\n const items = setStatus(state.items, failed, \"failed\");\n\n // Compensate the most-recently-completed step first. `lastDone === undefined`\n // means nothing completed forward (the first step itself failed) — there is\n // nothing to undo, so the saga aborts straight away. The guard narrows\n // `undoIndex` to `StepId` and `undoStep` to a present step without a\n // non-null assertion.\n const undoIndex = lastDone(state.done);\n const undoStep = undoIndex === undefined ? undefined : stepAt(undoIndex);\n if (undoIndex === undefined || undoStep === undefined) {\n return [\n { ...state, items, error, compensating: true, phase: \"aborted\" },\n [],\n ];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n items,\n error,\n compensating: true,\n phase: \"compensating\",\n position: undoIndex,\n };\n return [next, [undoStep.undo]];\n }\n\n /**\n * Continue the rollback: the `undo` for the current compensation position\n * succeeded. PURE.\n *\n * Pops the just-undone step off the tail of the compensation log (flipping\n * its ledger record `done → cancelled` to mark it rolled back), and:\n * - if more completed steps remain, emits the next one's `undo` (continuing\n * in reverse) and moves `position` to it;\n * - if the log is now empty, transitions to `aborted` (terminal failure —\n * the saga is fully unwound) and emits no Cmd.\n *\n * `undoOk` while not `compensating` is a no-op (same stale-Msg reasoning as\n * the forward verbs): an `undo`-success Msg that arrives after the saga has\n * already finished unwinding must not pop a step that isn't there.\n */\n function undoOk(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"compensating\") return [state, []];\n\n // The tail of the log is the step we just compensated — mark it cancelled.\n // `undone === undefined` cannot happen while `compensating` (the phase is\n // only entered with a non-empty log), but the guard keeps the lookup total\n // and narrows `undone` to `StepId` without a non-null assertion.\n const undone = lastDone(state.done);\n if (undone === undefined) return [state, []];\n const remaining = state.done.slice(0, -1);\n const items = setStatus(state.items, undone, \"cancelled\");\n\n // The next step to compensate is the new tail of the log. `nextUndo`\n // narrows to `StepId` here and to a present step via `stepAt`, so the\n // continue-vs-terminate branch needs no non-null assertion.\n const nextUndo = lastDone(remaining);\n const nextStep = nextUndo === undefined ? undefined : stepAt(nextUndo);\n if (nextUndo === undefined || nextStep === undefined) {\n // Fully unwound — terminal failure.\n return [{ ...state, done: remaining, items, phase: \"aborted\" }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n done: remaining,\n items,\n position: nextUndo,\n };\n return [next, [nextStep.undo]];\n }\n\n /**\n * Halt the rollback: the `undo` for the current compensation position itself\n * FAILED. PURE.\n *\n * A failed compensation is a real-world fact — a refund that bounced, a hold\n * that won't release — and it must become visible terminal state, never leave\n * the saga wedged in `compensating` forever waiting for an `undoOk` that will\n * never come. So `undoErr`:\n * - flips the in-flight compensation step's ledger record `done → failed`\n * (its `undo` did not take, so it is NOT `cancelled`);\n * - records the undo failure in `compensationError` (distinct from `error`,\n * the forward failure that started the rollback);\n * - transitions to the terminal `compensation_failed` phase and emits no\n * Cmd. The compensation log is LEFT INTACT (`done` keeps the steps that\n * were never rolled back) so the consumer can see exactly which steps\n * still need hand reconciliation.\n *\n * `undoErr` while not `compensating` is a no-op (same stale-Msg reasoning as\n * the other verbs): an `undo`-failure Msg that arrives after the saga has\n * already settled must not re-pivot a terminal saga.\n */\n function undoErr(\n state: SagaState<StepId>,\n error: unknown,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"compensating\") return [state, []];\n\n // The step whose `undo` just failed is the current compensation position.\n // Its forward effect stands un-reversed, so its record goes `done → failed`,\n // not `cancelled`.\n const items = setStatus(state.items, state.position, \"failed\");\n return [\n {\n ...state,\n items,\n compensationError: error,\n phase: \"compensation_failed\",\n },\n [],\n ];\n }\n\n return {\n init: initSaga<StepId>,\n start,\n stepOk,\n stepErr,\n undoOk,\n undoErr,\n isCommitted,\n isAborted,\n isCompensationFailed,\n isSettled,\n };\n}\n"]}
|
package/dist/snapshot/index.d.ts
CHANGED
package/dist/snapshot/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { createSnapshot, snapshotFailed, snapshotLoadFailed, snapshotLoaded, snapshotSaved } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
3
|
-
import '../chunk-
|
|
1
|
+
export { createSnapshot, snapshotFailed, snapshotLoadFailed, snapshotLoaded, snapshotSaved } from '../chunk-MVY5DAMF.js';
|
|
2
|
+
import '../chunk-BP2R7TPP.js';
|
|
3
|
+
import '../chunk-PBDYW2TU.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/dist/subs/index.d.ts
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
|
-
import { S as Sub, P as Port } from '../core-
|
|
2
|
-
import { S as SubscribeHandler } from '../types-
|
|
3
|
-
import { Runtime } from '../
|
|
1
|
+
import { S as Sub, P as Port, D as DepKeyedSub, a as SubId } from '../core-42kapW0n.js';
|
|
2
|
+
import { S as SubscribeHandler } from '../types-KbL3W1mj.js';
|
|
3
|
+
import { R as Runtime } from '../runtime-types-gsog27b6.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The imperative listener target, expressed as the `add`/`remove` pair the
|
|
7
|
+
* substrate pairs into a reconciled resource. Both halves receive the SAME
|
|
8
|
+
* `listener` reference — the substrate builds it once — so `remove` can never
|
|
9
|
+
* be handed a different function than `add` saw.
|
|
10
|
+
*
|
|
11
|
+
* `add`/`remove` also receive `(sub, ctx)` so the target may be resolved from
|
|
12
|
+
* `ctx` (a runtime, a port, a DOM node looked up lazily) or keyed on the Sub
|
|
13
|
+
* (an alarm name, a channel name). Resolving the target inside `add` and
|
|
14
|
+
* closing over nothing but the listener keeps a target swap from leaking the
|
|
15
|
+
* old target's listener — same rationale as `fromEventTarget`'s captured
|
|
16
|
+
* target.
|
|
17
|
+
*
|
|
18
|
+
* `Args` is the tuple the platform invokes the listener with — `[Alarm]` for
|
|
19
|
+
* `chrome.alarms.onAlarm`, `[message, sender, sendResponse]` for
|
|
20
|
+
* `chrome.runtime.onMessage`, `[changes, areaName]` for
|
|
21
|
+
* `chrome.storage.onChanged`. The substrate builds ONE function of this exact
|
|
22
|
+
* shape and threads the IDENTICAL reference to both `add` and `remove` — so a
|
|
23
|
+
* platform that matches `removeListener` by reference (every chrome surface)
|
|
24
|
+
* removes exactly what it added. A consumer that wrapped the listener inside
|
|
25
|
+
* `add` would break reference identity (leak #2 above); carrying the native
|
|
26
|
+
* `Args` on the listener itself removes the need for any wrapper.
|
|
27
|
+
*/
|
|
28
|
+
interface ListenerTarget<Args extends readonly unknown[], S extends Sub, Ctx> {
|
|
29
|
+
add: (listener: (...args: Args) => void, sub: S, ctx: Ctx) => void;
|
|
30
|
+
remove: (listener: (...args: Args) => void, sub: S, ctx: Ctx) => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build a listener-backed Sub factory whose cleanup is mandatory and derived.
|
|
34
|
+
*
|
|
35
|
+
* `Args`, `S`, `Ctx` are fixed when the target is defined — they describe the
|
|
36
|
+
* one listener topology this factory owns (the platform arg tuple, which Sub
|
|
37
|
+
* shape it filters on, what context resolves it). `M` is supplied at the
|
|
38
|
+
* subscribe cell because the same listener feeds different Msgs in different
|
|
39
|
+
* machines. Pinning `S` here (rather than at the `msgFn` step) lets
|
|
40
|
+
* `add`/`remove` read the concrete Sub's fields — `sub.alarmName`,
|
|
41
|
+
* `sub.channel` — with NO cast at the substrate edge: the narrowing happens in
|
|
42
|
+
* author-land where the Sub shape is in scope.
|
|
43
|
+
*
|
|
44
|
+
* The returned function is a `SubscribeHandler` factory in the exact shape of
|
|
45
|
+
* every other `@demlik/tea/subs` battery: supply the `msgFn` projection, get a
|
|
46
|
+
* `(sub, ctx, dispatch) => cleanup` handler assignable to `machine.subscribe`.
|
|
47
|
+
* The cleanup the author NEVER writes is `() => remove(listener, sub, ctx)`,
|
|
48
|
+
* with `listener` the identical reference passed to `add`.
|
|
49
|
+
*/
|
|
50
|
+
declare function defineListener<Args extends readonly unknown[], S extends Sub = Sub, Ctx = unknown>(target: ListenerTarget<Args, S, Ctx>): <M>(msgFn: (sub: S, ...args: Args) => M | null) => SubscribeHandler<S, M, Ctx>;
|
|
4
51
|
|
|
5
52
|
interface MinimalEvent {
|
|
6
53
|
readonly type: string;
|
|
@@ -92,4 +139,325 @@ type TimeoutSubData = {
|
|
|
92
139
|
};
|
|
93
140
|
declare function fromTimeout<S extends Sub & TimeoutSubData, M>(msgFn: (sub: S) => M): SubscribeHandler<S, M, unknown>;
|
|
94
141
|
|
|
95
|
-
|
|
142
|
+
/**
|
|
143
|
+
* The Sub the battery builds. The consumer's `Sub` union includes
|
|
144
|
+
* `ManagedResourceSub<TKey>` directly — the substrate reconciles by `id` and
|
|
145
|
+
* `type` like any other Sub.
|
|
146
|
+
*/
|
|
147
|
+
interface ManagedResourceSub<TKey> extends Sub<"managed_resource"> {
|
|
148
|
+
/** The lifetime key. Stable while the resource should be held; a change
|
|
149
|
+
* retires the old resource (release) and acquires a fresh one. */
|
|
150
|
+
readonly key: TKey;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A battery paired with a state-gate. Self-contained: it knows its `name`
|
|
154
|
+
* (for the unique-name assert), how to derive its live Sub from a state
|
|
155
|
+
* (`subFor`), how to handle its own `subscribe`, and whether it `owns` a given
|
|
156
|
+
* Sub id.
|
|
157
|
+
*
|
|
158
|
+
* Generic in the consumer state `S`, the shared lifetime key `TKey`, and the
|
|
159
|
+
* shared `Ctx`. `Handle` is the ONLY thing erased — each battery's handle type
|
|
160
|
+
* differs (a checkpoint saver vs. a tool-call bridge) but never crosses this
|
|
161
|
+
* interface, so a `combineManagedResources` array holds entries of
|
|
162
|
+
* heterogeneous handle types with no cast. The sub type stays
|
|
163
|
+
* `ManagedResourceSub<TKey>` end to end, so the substrate's narrowed Sub flows
|
|
164
|
+
* straight in.
|
|
165
|
+
*/
|
|
166
|
+
interface GatedManagedResource<S, TKey, Ctx> {
|
|
167
|
+
/** The owning battery's `name`. The combinator asserts these are unique. */
|
|
168
|
+
readonly name: string;
|
|
169
|
+
/**
|
|
170
|
+
* Derive this resource's live Sub for a given state: the battery's
|
|
171
|
+
* `.sub(key)` when `when(state)` returns a key, or `null` when it returns
|
|
172
|
+
* `null` (the resource should be torn down in that state).
|
|
173
|
+
*/
|
|
174
|
+
readonly subFor: (state: S) => ManagedResourceSub<TKey> | null;
|
|
175
|
+
/**
|
|
176
|
+
* This battery's own `.subscribe`. The combinator routes an incoming
|
|
177
|
+
* `managed_resource` Sub to the entry whose `owns(sub)` is true, then calls
|
|
178
|
+
* this.
|
|
179
|
+
*/
|
|
180
|
+
readonly subscribe: (sub: ManagedResourceSub<TKey>, ctx: Ctx, dispatch: (msg: never) => void) => () => void;
|
|
181
|
+
/**
|
|
182
|
+
* Does this entry own the given Sub? `sub.id === subIdFor(sub.key)` encodes
|
|
183
|
+
* the battery `name`, so a match is unambiguous.
|
|
184
|
+
*/
|
|
185
|
+
readonly owns: (sub: ManagedResourceSub<TKey>) => boolean;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* What `combineManagedResources` returns: the single managed-resource
|
|
189
|
+
* `subscribe` cell for the machine's `subscribe` record, and a `subs(state)`
|
|
190
|
+
* builder for `subscriptions(state)`.
|
|
191
|
+
*/
|
|
192
|
+
interface CombinedManagedResources<S, TKey, Ctx> {
|
|
193
|
+
/**
|
|
194
|
+
* Map every entry through its `when(state)`, drop the `null`s, and build the
|
|
195
|
+
* live Subs. Spread into `subscriptions(state)`. The derived list and the
|
|
196
|
+
* `subscribe` routing share one source — the entries — so they never drift.
|
|
197
|
+
*/
|
|
198
|
+
readonly subs: (state: S) => ManagedResourceSub<TKey>[];
|
|
199
|
+
/**
|
|
200
|
+
* The SINGLE `subscribe.managed_resource` handler. Routes an incoming Sub to
|
|
201
|
+
* the owning battery by matching `sub.id === battery.subIdFor(sub.key)` (the
|
|
202
|
+
* id encodes the battery `name`). Throws loudly on an unrouted id — a wiring
|
|
203
|
+
* bug, never a silent no-op resource.
|
|
204
|
+
*/
|
|
205
|
+
readonly subscribe: (sub: ManagedResourceSub<TKey>, ctx: Ctx, dispatch: (msg: never) => void) => () => void;
|
|
206
|
+
}
|
|
207
|
+
interface DefineManagedResourceOpts<TKey, Handle, Ctx> {
|
|
208
|
+
/**
|
|
209
|
+
* A short, stable name. The Sub id is `managed:${name}:${stringify(key)}`,
|
|
210
|
+
* so two managed resources in one machine never collide and neither
|
|
211
|
+
* collides with a `fromTransport` Sub keyed on the same identity.
|
|
212
|
+
*/
|
|
213
|
+
readonly name: string;
|
|
214
|
+
/**
|
|
215
|
+
* Build the resource. Runs when the Sub appears in `subscriptions(state)`.
|
|
216
|
+
* The returned Handle is the ONLY thing `release` receives — capture
|
|
217
|
+
* everything teardown needs into it. If `acquire` throws, the substrate's
|
|
218
|
+
* reconcile does NOT register the Sub and `release` is never called (no
|
|
219
|
+
* dangling half-built resource).
|
|
220
|
+
*/
|
|
221
|
+
readonly acquire: (key: TKey, ctx: Ctx) => Handle;
|
|
222
|
+
/**
|
|
223
|
+
* Tear the resource down. Runs when the Sub leaves the desired set (phase
|
|
224
|
+
* exited) or when its key changes. MANDATORY — the guaranteed teardown is
|
|
225
|
+
* the reason this battery exists. May be sync or async; the substrate's
|
|
226
|
+
* cleanup is fire-and-forget (Rule 2), so a rejected teardown is logged at
|
|
227
|
+
* the boundary, not surfaced as a Msg.
|
|
228
|
+
*/
|
|
229
|
+
readonly release: (handle: Handle) => void | Promise<void>;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* What the battery returns: a `.sub(key)` builder for `subscriptions`, the
|
|
233
|
+
* `.subscribe` handler for the machine's `subscribe` record, a `.get(key)`
|
|
234
|
+
* accessor so Cmd handlers can reach the live Handle while the resource is
|
|
235
|
+
* held, and a `.subIdFor(key)` for tests.
|
|
236
|
+
*/
|
|
237
|
+
interface ManagedResourceBattery<TKey, Handle, Ctx> {
|
|
238
|
+
/**
|
|
239
|
+
* Build the Sub for `subscriptions(state)`. The consumer calls this from
|
|
240
|
+
* inside their `subscriptions` cell for the phases where the resource
|
|
241
|
+
* should be live.
|
|
242
|
+
*/
|
|
243
|
+
readonly sub: (key: TKey) => ManagedResourceSub<TKey>;
|
|
244
|
+
/**
|
|
245
|
+
* Pair this battery with a state-gate, producing a self-contained entry for
|
|
246
|
+
* `combineManagedResources`. `when(state)` returns the resource's KEY when
|
|
247
|
+
* it should be live in that state, or `null` when it should be torn down.
|
|
248
|
+
*
|
|
249
|
+
* Folding "is it active?" and "what key?" into ONE declaration at the
|
|
250
|
+
* battery's own definition site is the point: a central `subscriptions(state)`
|
|
251
|
+
* list can silently forget to gate a new phase, but a per-resource gate
|
|
252
|
+
* travels with the resource. The combinator derives BOTH the active-sub list
|
|
253
|
+
* and the subscribe routing from these entries, so the two can never disagree.
|
|
254
|
+
*
|
|
255
|
+
* Generic in the consumer's state `S` (each `.gated` call binds its own `S`).
|
|
256
|
+
* The returned entry erases this battery's `Handle` — `combineManagedResources`
|
|
257
|
+
* holds entries of heterogeneous handle types with no cast — while keeping
|
|
258
|
+
* `TKey` and `Ctx` so the substrate's narrowed `ManagedResourceSub<TKey>`
|
|
259
|
+
* flows straight into the entry's `subscribe`.
|
|
260
|
+
*/
|
|
261
|
+
readonly gated: <S>(when: (state: S) => TKey | null) => GatedManagedResource<S, TKey, Ctx>;
|
|
262
|
+
/**
|
|
263
|
+
* Dep-keyed entry for the machine's `subs` array. `when(state)` returns the
|
|
264
|
+
* resource's KEY when it should be held, or `null` when it should be torn
|
|
265
|
+
* down — the exact gate `.gated` takes, now producing a `DepKeyedSub` the
|
|
266
|
+
* kernel folds directly instead of an entry the `combineManagedResources`
|
|
267
|
+
* router multiplexes.
|
|
268
|
+
*
|
|
269
|
+
* This is the simplification the dep-keyed kernel unlocks: two managed
|
|
270
|
+
* resources used to COLLIDE on the single `subscribe.managed_resource` key
|
|
271
|
+
* (both subs share `type: "managed_resource"`), forcing
|
|
272
|
+
* `combineManagedResources` to route by SubId. A dep-keyed Sub has its OWN
|
|
273
|
+
* reconcile slot (its array position), so there is no shared `type` to route
|
|
274
|
+
* — each resource is one `subs` entry, no combinator, no router. `acquire` /
|
|
275
|
+
* `release` still ride the kernel's dispose-on-change / dispose-on-null
|
|
276
|
+
* reconcile, now via the dep-keyed source's `Dispose` instead of the
|
|
277
|
+
* `subscribe` cell's cleanup.
|
|
278
|
+
*/
|
|
279
|
+
readonly depKeyed: <S>(when: (state: S) => TKey | null) => DepKeyedSub<S, never, Ctx>;
|
|
280
|
+
/**
|
|
281
|
+
* The `subscribe.managed_resource` handler. Drop straight into the
|
|
282
|
+
* machine's `subscribe` record. Runs `acquire` on install, holds the
|
|
283
|
+
* Handle in the battery's handle table, and returns a cleanup that runs
|
|
284
|
+
* `release` (and forgets the Handle) on reconcile-out.
|
|
285
|
+
*/
|
|
286
|
+
readonly subscribe: (sub: ManagedResourceSub<TKey>, ctx: Ctx, dispatch: (msg: never) => void) => () => void;
|
|
287
|
+
/**
|
|
288
|
+
* Accessor for the live Handle, keyed on `key`. Returns `undefined` when
|
|
289
|
+
* the resource is not currently held (no active sub for that key) — the
|
|
290
|
+
* caller decides whether that is an error. A Cmd handler wires its
|
|
291
|
+
* `ctx.getX(key)` through this, so the handler reads the SAME owner the
|
|
292
|
+
* reconciler holds, never a hand-built duplicate.
|
|
293
|
+
*/
|
|
294
|
+
readonly get: (key: TKey) => Handle | undefined;
|
|
295
|
+
/** Pre-derived subId for `(key)` — handy for tests. */
|
|
296
|
+
readonly subIdFor: (key: TKey) => SubId;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Build the battery. One call per managed resource at the machine's host
|
|
300
|
+
* file.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* const checkpointStore = defineManagedResource<RunId, CheckpointSaver, Ctx>({
|
|
304
|
+
* name: "checkpoint",
|
|
305
|
+
* acquire: (runId, ctx) =>
|
|
306
|
+
* new CheckpointSaver({ storage: ctx.storage, bucket: ctx.bucket, runId }),
|
|
307
|
+
* release: (saver) => saver.clear(),
|
|
308
|
+
* });
|
|
309
|
+
*
|
|
310
|
+
* // In the machine:
|
|
311
|
+
* subscriptions: (state) =>
|
|
312
|
+
* state.type === "running" ? [checkpointStore.sub(state.runId)] : [],
|
|
313
|
+
* subscribe: { managed_resource: checkpointStore.subscribe },
|
|
314
|
+
*
|
|
315
|
+
* // In interpret (read the live handle the reconciler holds):
|
|
316
|
+
* start_graph: async (cmd, ctx) => {
|
|
317
|
+
* const saver = checkpointStore.get(cmd.runId); // same owner, no rebuild
|
|
318
|
+
* ...
|
|
319
|
+
* }
|
|
320
|
+
*/
|
|
321
|
+
declare function defineManagedResource<TKey, Handle, Ctx>(opts: DefineManagedResourceOpts<TKey, Handle, Ctx>): ManagedResourceBattery<TKey, Handle, Ctx>;
|
|
322
|
+
declare function combineManagedResources<S, TKey, Ctx>(entries: readonly GatedManagedResource<S, TKey, Ctx>[]): CombinedManagedResources<S, TKey, Ctx>;
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Duplex transport. The host (DO, worker thread, in-process bus) constructs
|
|
326
|
+
* one from whatever the platform offers. `fromTransport` does not know what
|
|
327
|
+
* a WebSocket is — it knows `send`, `onMessage`, `onClose`.
|
|
328
|
+
*/
|
|
329
|
+
interface Transport {
|
|
330
|
+
/** Send a string frame. Best-effort; failures are logged at the boundary. */
|
|
331
|
+
send(data: string): void;
|
|
332
|
+
/**
|
|
333
|
+
* Subscribe to inbound frames. Returns a cleanup that removes the listener.
|
|
334
|
+
* The battery wires this in its Sub's subscribe handler; cleanup runs when
|
|
335
|
+
* the phase exits (the substrate's reconcileSubs).
|
|
336
|
+
*/
|
|
337
|
+
onMessage(listener: (data: string) => void): () => void;
|
|
338
|
+
/**
|
|
339
|
+
* Subscribe to a one-shot close. Returns a cleanup that removes the
|
|
340
|
+
* listener. The battery dispatches `lostMsg` on close.
|
|
341
|
+
*/
|
|
342
|
+
onClose(listener: () => void): () => void;
|
|
343
|
+
/** Close the underlying channel cleanly. Called by the Sub cleanup. */
|
|
344
|
+
close(): void;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Factory the consumer wires to a platform-specific transport. The battery
|
|
348
|
+
* passes the runtime-derived key (`runId`, or whatever identity is on the
|
|
349
|
+
* phase) so per-run transports can be addressed.
|
|
350
|
+
*/
|
|
351
|
+
type TransportFactory<K> = (key: K) => Transport;
|
|
352
|
+
/**
|
|
353
|
+
* The Sub the battery builds. The consumer's `Sub` union includes
|
|
354
|
+
* `TransportSub<TKey>` directly — no wrapping needed, the substrate
|
|
355
|
+
* reconciles by `id` and `type` like any other Sub.
|
|
356
|
+
*/
|
|
357
|
+
interface TransportSub<TKey> extends Sub<"transport"> {
|
|
358
|
+
/** The phase / run identity. Stable for the lifetime of the seam. */
|
|
359
|
+
readonly key: TKey;
|
|
360
|
+
}
|
|
361
|
+
interface FromTransportOpts<TKey, Inbound, Outbound, M, Ctx> {
|
|
362
|
+
/**
|
|
363
|
+
* A short, stable name for this seam. The Sub id is derived from
|
|
364
|
+
* `${name}:${stringify(key)}` so two seams in the same machine (e.g.
|
|
365
|
+
* `hands` + `worker`) never collide.
|
|
366
|
+
*/
|
|
367
|
+
readonly name: string;
|
|
368
|
+
/**
|
|
369
|
+
* Build the transport. The battery owns the lifetime; the factory is the
|
|
370
|
+
* platform-specific constructor (`new WebSocket(...)` adapter on a DO,
|
|
371
|
+
* `MessagePort` adapter in a worker, in-memory stub in tests).
|
|
372
|
+
*/
|
|
373
|
+
readonly openTransport: (key: TKey, ctx: Ctx) => Transport;
|
|
374
|
+
/**
|
|
375
|
+
* Parse one inbound frame. The battery hands `unknown` through (boundary
|
|
376
|
+
* parse is the consumer's responsibility per invariant 8). Returning
|
|
377
|
+
* `null` drops the frame silently — useful for frames the seam doesn't
|
|
378
|
+
* recognize at this layer (e.g. transport-level keepalive).
|
|
379
|
+
*/
|
|
380
|
+
readonly parseInbound: (raw: string, key: TKey) => Inbound | null;
|
|
381
|
+
/**
|
|
382
|
+
* Map a parsed inbound to a domain Msg. Returning `null` drops the
|
|
383
|
+
* dispatch (e.g. a transport-level pong that doesn't change domain state).
|
|
384
|
+
*/
|
|
385
|
+
readonly onInbound: (inbound: Inbound, key: TKey) => M | null;
|
|
386
|
+
/**
|
|
387
|
+
* The Msg dispatched when the transport closes (peer gone, network
|
|
388
|
+
* dropped, normal close). ONE Msg arm, not a Sub, not a heartbeat-derived
|
|
389
|
+
* deadline — the transport's own close signal is the truth.
|
|
390
|
+
*/
|
|
391
|
+
readonly lostMsg: (key: TKey) => M;
|
|
392
|
+
/**
|
|
393
|
+
* Serialize an outbound. Called by the Cmd handler the battery wires.
|
|
394
|
+
* Pure data → string at the seam boundary.
|
|
395
|
+
*/
|
|
396
|
+
readonly serializeOutbound: (outbound: Outbound) => string;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* What the battery returns: a Sub builder (for `subscriptions`), the Sub's
|
|
400
|
+
* `subscribe` handler (for `subscribe.transport`), and a `send(key, outbound)`
|
|
401
|
+
* helper the consumer's Cmd handler calls. Plus a `subIdFor` factory the
|
|
402
|
+
* consumer can use when keying related Subs (e.g. a deadline) to the same seam
|
|
403
|
+
* identity.
|
|
404
|
+
*/
|
|
405
|
+
interface TransportBattery<TKey, Outbound, M, Ctx> {
|
|
406
|
+
/**
|
|
407
|
+
* Build the Sub for `subscriptions(state)`. The consumer calls this from
|
|
408
|
+
* inside their `subscriptions` cell when the seam should be open.
|
|
409
|
+
*/
|
|
410
|
+
readonly sub: (key: TKey) => TransportSub<TKey>;
|
|
411
|
+
/**
|
|
412
|
+
* The `subscribe.transport` handler. Drop straight into the machine's
|
|
413
|
+
* `subscribe` record — the battery wires inbound + close in one shot.
|
|
414
|
+
*/
|
|
415
|
+
readonly subscribe: (sub: TransportSub<TKey>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
|
|
416
|
+
/**
|
|
417
|
+
* Outbound send. The Cmd handler calls this; the battery's handle table
|
|
418
|
+
* (built when subscribe ran) routes to the live transport. If the seam
|
|
419
|
+
* is closed (no active sub), the send is dropped honestly (logged, not
|
|
420
|
+
* thrown — the reducer already moved past caring, same shape as Rule 2
|
|
421
|
+
* fire-and-forget Cmds).
|
|
422
|
+
*/
|
|
423
|
+
readonly send: (key: TKey, outbound: Outbound) => void;
|
|
424
|
+
/** Pre-derived subId for `(key)` — handy for deadline Subs keyed to the same seam. */
|
|
425
|
+
readonly subIdFor: (key: TKey) => SubId;
|
|
426
|
+
/**
|
|
427
|
+
* Pair this seam with a state-gate, producing a dep-keyed Sub for the
|
|
428
|
+
* machine's `subs` array. `when(state)` returns the seam's KEY when it
|
|
429
|
+
* should be open, or `null` when it should be torn down. The kernel derives
|
|
430
|
+
* the id from `structuralHash(key)` and reconciles the seam's lifetime — so
|
|
431
|
+
* the author neither lists this Sub in a central `subscriptions(state)` nor
|
|
432
|
+
* writes its id. Replaces `subscriptions: (s) => active(s) ? [hands.sub(...)] : []`
|
|
433
|
+
* + `subscribe: { transport: hands.subscribe }` with one `subs` entry.
|
|
434
|
+
*
|
|
435
|
+
* The `source` closes over the battery's own `subscribe` (open inbound +
|
|
436
|
+
* close, wire the handle table) using the same `ctx` the kernel passes every
|
|
437
|
+
* Sub source.
|
|
438
|
+
*/
|
|
439
|
+
readonly depKeyed: <S>(when: (state: S) => TKey | null) => DepKeyedSub<S, M, Ctx>;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Build the battery. One call per seam at the machine's host file.
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* const handsSeam = fromTransport<RunId, HandsInbound, HandsOutbound, Msg, Ctx>({
|
|
446
|
+
* name: "hands",
|
|
447
|
+
* openTransport: (runId, ctx) => ctx.openHandsWs(runId),
|
|
448
|
+
* parseInbound: (raw) => parseHandsInbound(JSON.parse(raw)),
|
|
449
|
+
* onInbound: (inbound, runId) => ({ type: "hands_said", runId, inbound }),
|
|
450
|
+
* lostMsg: (runId) => ({ type: "hands_lost", runId }),
|
|
451
|
+
* serializeOutbound: (out) => JSON.stringify(out),
|
|
452
|
+
* });
|
|
453
|
+
*
|
|
454
|
+
* // In the machine:
|
|
455
|
+
* subscriptions: (state) => state.type === "auditing" ? [handsSeam.sub(state.runId)] : [],
|
|
456
|
+
* subscribe: { transport: handsSeam.subscribe },
|
|
457
|
+
*
|
|
458
|
+
* // In interpret:
|
|
459
|
+
* send_hands: async (cmd) => { handsSeam.send(cmd.runId, cmd.outbound); },
|
|
460
|
+
*/
|
|
461
|
+
declare function fromTransport<TKey, Inbound, Outbound, M, Ctx>(opts: FromTransportOpts<TKey, Inbound, Outbound, M, Ctx>): TransportBattery<TKey, Outbound, M, Ctx>;
|
|
462
|
+
|
|
463
|
+
export { type CancelTimer, type CombinedManagedResources, type DefineManagedResourceOpts, type EventSourceFactoryOpts, type FromTransportOpts, type GatedManagedResource, type ListenerTarget, type ManagedResourceBattery, type ManagedResourceSub, type ReconnectingWebSocketFactoryOpts, SubscribeHandler, type Transport, type TransportBattery, type TransportFactory, type TransportSub, type WebSocketFactoryOpts, type WebSocketSubData, combineManagedResources, defineListener, defineManagedResource, fromBroadcastChannel, fromEventSource, fromEventTarget, fromInterval, fromPort, fromReconnectingWebSocket, fromTimeout, fromTransport, fromWebSocket };
|
package/dist/subs/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export { fromBroadcastChannel, fromEventSource, fromEventTarget, fromInterval, fromPort, fromReconnectingWebSocket, fromWebSocket } from '../chunk-
|
|
1
|
+
export { combineManagedResources, defineListener, defineManagedResource, fromBroadcastChannel, fromEventSource, fromEventTarget, fromInterval, fromPort, fromReconnectingWebSocket, fromTransport, fromWebSocket } from '../chunk-LYLDCIPH.js';
|
|
2
2
|
export { fromTimeout } from '../chunk-2YFLOCWX.js';
|
|
3
|
+
import '../chunk-ZJ5AOYBH.js';
|
|
4
|
+
import '../chunk-BP2R7TPP.js';
|
|
5
|
+
import '../chunk-PBDYW2TU.js';
|
|
3
6
|
//# sourceMappingURL=index.js.map
|
|
4
7
|
//# sourceMappingURL=index.js.map
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as Cmd, S as Sub, M as Machine } from '../core-
|
|
2
|
-
import { Runtime } from '../
|
|
1
|
+
import { C as Cmd, S as Sub, M as Machine } from '../core-42kapW0n.js';
|
|
2
|
+
import { R as Runtime } from '../runtime-types-gsog27b6.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The composed Model shape every `withX` wrapper produces: the base machine's
|
package/dist/testing/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { TtlCache } from '../cache/index.js';
|
|
2
2
|
import { DeadlineSub } from '../deadline/index.js';
|
|
3
3
|
export { DeadlineExceeded as ThrottledInputSettled, subscribeDeadline as subscribeThrottledInput, deadlineExceeded as throttledInputSettled } from '../deadline/index.js';
|
|
4
|
-
import { C as Cmd, S as Sub } from '../core-
|
|
4
|
+
import { C as Cmd, S as Sub } from '../core-42kapW0n.js';
|
|
5
5
|
export { Debounced, debounce } from '../debounce/index.js';
|
|
6
6
|
export { Throttled, throttle } from '../throttle/index.js';
|
|
7
|
-
import '../types-
|
|
7
|
+
import '../types-KbL3W1mj.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @demlik/tea/throttled-input — gate a high-frequency input stream into a
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export { debounce } from '../chunk-J6527VRQ.js';
|
|
2
2
|
export { throttle } from '../chunk-HCEVHFJ7.js';
|
|
3
|
-
import { deadlineSub } from '../chunk-
|
|
4
|
-
export { subscribeDeadline as subscribeThrottledInput, deadlineExceeded as throttledInputSettled } from '../chunk-
|
|
5
|
-
import { initCache, get, set } from '../chunk-
|
|
6
|
-
import '../chunk-
|
|
3
|
+
import { deadlineSub } from '../chunk-RMX4EOQK.js';
|
|
4
|
+
export { subscribeDeadline as subscribeThrottledInput, deadlineExceeded as throttledInputSettled } from '../chunk-RMX4EOQK.js';
|
|
5
|
+
import { initCache, get, set } from '../chunk-2KNLKQVN.js';
|
|
6
|
+
import '../chunk-LYLDCIPH.js';
|
|
7
7
|
import '../chunk-2YFLOCWX.js';
|
|
8
|
-
import '../chunk-
|
|
9
|
-
import '../chunk-
|
|
8
|
+
import '../chunk-ZJ5AOYBH.js';
|
|
9
|
+
import '../chunk-BP2R7TPP.js';
|
|
10
|
+
import '../chunk-PBDYW2TU.js';
|
|
10
11
|
|
|
11
12
|
// src/throttled-input/index.ts
|
|
12
13
|
var DEFAULT_SUB_ID = "throttled-input:settle";
|
|
@@ -17,21 +18,21 @@ function keyOf(config, value) {
|
|
|
17
18
|
return config.cacheKey(value);
|
|
18
19
|
}
|
|
19
20
|
function cacheConfig(config, state) {
|
|
20
|
-
return config.cacheTtlMs !== void 0 && state.cache !== void 0 ? config : null;
|
|
21
|
+
return config.cacheTtlMs !== void 0 && state.cache !== void 0 ? { config, cache: state.cache } : null;
|
|
21
22
|
}
|
|
22
23
|
function isCacheHit(config, state, value, at) {
|
|
23
24
|
const cached = cacheConfig(config, state);
|
|
24
|
-
if (cached === null
|
|
25
|
-
return get(
|
|
25
|
+
if (cached === null) return false;
|
|
26
|
+
return get(cached.cache, keyOf(cached.config, value), at) !== void 0;
|
|
26
27
|
}
|
|
27
28
|
function commitEmit(config, state, value, at) {
|
|
28
29
|
const cached = cacheConfig(config, state);
|
|
29
|
-
const cache = cached !== null
|
|
30
|
-
|
|
31
|
-
keyOf(cached, value),
|
|
30
|
+
const cache = cached !== null ? set(
|
|
31
|
+
cached.cache,
|
|
32
|
+
keyOf(cached.config, value),
|
|
32
33
|
value,
|
|
33
34
|
at,
|
|
34
|
-
cached.cacheTtlMs
|
|
35
|
+
cached.config.cacheTtlMs
|
|
35
36
|
) : state.cache;
|
|
36
37
|
const next = {
|
|
37
38
|
...state,
|