@demlik/tea 0.19.0 → 0.20.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 +25 -18
- package/dist/agent/index.d.ts +44 -13
- package/dist/agent/index.js +10 -10
- package/dist/{chunk-PCJA2HTZ.js → chunk-2774I242.js} +7 -7
- package/dist/chunk-2774I242.js.map +1 -0
- package/dist/{chunk-YIMOFXST.js → chunk-2R62Q5UK.js} +3 -3
- package/dist/chunk-2R62Q5UK.js.map +1 -0
- package/dist/{chunk-PQSDUX6B.js → chunk-5HVKDHY6.js} +3 -3
- package/dist/{chunk-PQSDUX6B.js.map → chunk-5HVKDHY6.js.map} +1 -1
- package/dist/{chunk-WTSOW2MD.js → chunk-72CN3VPY.js} +5 -5
- package/dist/{chunk-WTSOW2MD.js.map → chunk-72CN3VPY.js.map} +1 -1
- package/dist/{chunk-JVE2LBFJ.js → chunk-A73IJ27X.js} +3 -3
- package/dist/{chunk-JVE2LBFJ.js.map → chunk-A73IJ27X.js.map} +1 -1
- package/dist/{chunk-I7E7AD2S.js → chunk-AIXC33SE.js} +4 -4
- package/dist/chunk-AIXC33SE.js.map +1 -0
- package/dist/{chunk-FNGZMVIT.js → chunk-DVNXEB7V.js} +3 -3
- package/dist/chunk-DVNXEB7V.js.map +1 -0
- package/dist/{chunk-XVIKSRCK.js → chunk-EA47MI5Q.js} +4 -4
- package/dist/{chunk-XVIKSRCK.js.map → chunk-EA47MI5Q.js.map} +1 -1
- package/dist/{chunk-IHUNKHDX.js → chunk-FTCYY2DO.js} +3 -3
- package/dist/{chunk-IHUNKHDX.js.map → chunk-FTCYY2DO.js.map} +1 -1
- package/dist/{chunk-JVKU2UQC.js → chunk-HX3C6LDK.js} +3 -3
- package/dist/{chunk-JVKU2UQC.js.map → chunk-HX3C6LDK.js.map} +1 -1
- package/dist/{chunk-H2ELKHI6.js → chunk-JAKM53JF.js} +4 -4
- package/dist/chunk-JAKM53JF.js.map +1 -0
- package/dist/{chunk-JYNYG5SF.js → chunk-NGYFBD5G.js} +3 -3
- package/dist/chunk-NGYFBD5G.js.map +1 -0
- package/dist/{chunk-AIIXV4XB.js → chunk-RKVUVALV.js} +2 -2
- package/dist/chunk-RKVUVALV.js.map +1 -0
- package/dist/{chunk-C3G2QFEZ.js → chunk-TAKPC6SD.js} +4 -4
- package/dist/{chunk-C3G2QFEZ.js.map → chunk-TAKPC6SD.js.map} +1 -1
- package/dist/{chunk-BRX3243I.js → chunk-TGJLXZAU.js} +3 -3
- package/dist/{chunk-BRX3243I.js.map → chunk-TGJLXZAU.js.map} +1 -1
- package/dist/{chunk-AHW2CLPT.js → chunk-UHAQ4PI6.js} +4 -4
- package/dist/{chunk-AHW2CLPT.js.map → chunk-UHAQ4PI6.js.map} +1 -1
- package/dist/{chunk-H3MWNH7A.js → chunk-XWIE5V7R.js} +4 -4
- package/dist/{chunk-H3MWNH7A.js.map → chunk-XWIE5V7R.js.map} +1 -1
- package/dist/{core-Ce9ncpuv.d.ts → core-CtvAyPpH.d.ts} +153 -1
- package/dist/devtools/index.d.ts +17 -0
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +12 -12
- package/dist/do/index.js +11 -11
- package/dist/{drive-loop-CsUsW3Wo.d.ts → drive-loop-Dw1sGSsW.d.ts} +1 -1
- package/dist/{durable-effects-BmUrRA-h.d.ts → durable-effects-Ump5lSdO.d.ts} +1 -1
- package/dist/effect/index.d.ts +124 -5
- package/dist/effect/index.js +33 -4
- package/dist/effect/index.js.map +1 -1
- package/dist/extension/index.d.ts +84 -3
- package/dist/extension/index.js +2 -2
- package/dist/extension/index.js.map +1 -1
- package/dist/flow/index.d.ts +29 -13
- package/dist/flow/index.js +11 -11
- package/dist/flow/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +11 -3
- package/dist/idempotency/index.js +3 -3
- package/dist/{index-D8sYVzCI.d.ts → index-Bk2hBUdK.d.ts} +6 -1
- package/dist/{index-2kNxy6az.d.ts → index-CKtEKGyp.d.ts} +15 -3
- package/dist/{index-e36uEyl4.d.ts → index-CVU7SK3a.d.ts} +2 -2
- package/dist/{index-Dy3379ve.d.ts → index-CZZ3HI-Z.d.ts} +4 -4
- package/dist/{index-Ce5GYAuI.d.ts → index-CrcoVfd_.d.ts} +2 -2
- package/dist/{index-DWdg7R1l.d.ts → index-D3NY3VCN.d.ts} +5 -1
- package/dist/{index-B1Wzarkl.d.ts → index-Dl71V7kt.d.ts} +1 -1
- package/dist/{index-Bf9_bNql.d.ts → index-EIgfl83J.d.ts} +2 -2
- package/dist/index.d.ts +66 -5
- package/dist/index.js +2 -2
- package/dist/jev/index.d.ts +39 -27
- package/dist/jev/index.js +12 -8
- package/dist/jev/index.js.map +1 -1
- package/dist/{machine-CuojfZup.d.ts → machine-Dtre2TmN.d.ts} +9 -7
- package/dist/machine-viz/index.d.ts +2 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +2 -2
- package/dist/mem/index.js +2 -2
- package/dist/node/index.d.ts +11 -2
- package/dist/node/index.js +2 -2
- package/dist/node/index.js.map +1 -1
- package/dist/otel/index.d.ts +10 -10
- package/dist/otel/index.js +10 -10
- package/dist/paginate/index.d.ts +5 -4
- package/dist/paginate/index.js +5 -5
- package/dist/parity/index.d.ts +3 -3
- package/dist/parity/index.js +3 -3
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +2 -2
- package/dist/persistence/index.d.ts +5 -5
- package/dist/persistence/index.js +4 -4
- package/dist/promise/index.d.ts +7 -2
- package/dist/promise/index.js +4 -4
- package/dist/react/index.d.ts +2 -2
- package/dist/resilience/index.d.ts +7 -6
- package/dist/resilience/index.js +5 -5
- package/dist/resilience/index.js.map +1 -1
- package/dist/{index-BnsAqn0-.d.ts → run-fiBe7pOG.d.ts} +8 -5
- package/dist/{runtime-types-BKg2CGMx.d.ts → runtime-types-DEluQgGO.d.ts} +74 -2
- package/dist/testing/effect/index.d.ts +5 -5
- package/dist/testing/effect/index.js +8 -8
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +3 -3
- package/dist/testing/promise/index.d.ts +4 -4
- package/dist/testing/promise/index.js +5 -5
- package/dist/timing/index.d.ts +4 -4
- package/dist/timing/index.js +4 -4
- package/dist/types-CXNzlbAg.d.ts +9 -0
- package/dist/work-queue/index.d.ts +4 -4
- package/dist/work-queue/index.js +2 -2
- package/dist/work-queue/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-AIIXV4XB.js.map +0 -1
- package/dist/chunk-FNGZMVIT.js.map +0 -1
- package/dist/chunk-H2ELKHI6.js.map +0 -1
- package/dist/chunk-I7E7AD2S.js.map +0 -1
- package/dist/chunk-JYNYG5SF.js.map +0 -1
- package/dist/chunk-PCJA2HTZ.js.map +0 -1
- package/dist/chunk-YIMOFXST.js.map +0 -1
- package/dist/types-SzNSHKlV.d.ts +0 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/resilience/deadline/index.ts","../src/internal/resilience/cache/index.ts"],"names":[],"mappings":"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","file":"chunk-C3G2QFEZ.js","sourcesContent":["/**\n * internal/resilience/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock\n * deadline is reached.\n *\n * Generalizes the recurring \"auto-fail a machine at T+N\" / \"15-minute stale\n * guard\" pattern: a deadline a battery lists only while it should be armed,\n * which dispatches a single `DeadlineExceeded` Msg when the wall clock crosses\n * `atMs`.\n *\n * A battery's `subs(slice)` returns its deadlines as a LIST (`DeadlineSub[]`,\n * one per retry timer, say). A machine declares that list as ONE Sub of type\n * `\"deadline\"` whose `deps` is the list (`deadlinesSub(select)`), and the\n * `deadline` runner arms every deadline in it. A change to the list restarts\n * the runner, which re-arms each deadline for its REMAINING time — `atMs` is\n * absolute, so a restart never moves a deadline.\n *\n * Difference from `fromTimeout` (relative — \"fire after N ms\"): a deadline is\n * an ABSOLUTE target. The delay is computed at subscribe time as\n * `max(0, atMs - Date.now())`. The consequence that matters: subscribing LATE —\n * e.g. after a `Store` rehydrate that resumes a machine whose deadline was set\n * before the page reloaded — still fires at the correct absolute moment,\n * because the remaining delay is recomputed from the current clock, not stored\n * in the Sub. A deadline already in the past at subscribe time yields delay `0`,\n * so `setTimeout(fn, 0)` fires on the NEXT tick (never synchronously inside the\n * reconcile pass — the substrate must finish wiring all subs before any Msg\n * lands).\n *\n * `deadlineSub` produces one deadline literal carrying `atMs`; the runner\n * (`subscribeDeadline`) translates each `atMs` → a relative delay and arms a\n * `setTimeout` / `clearTimeout` pair per deadline. The clock\n * read (`Date.now()`) lives only in the subscribe handler — never in a reducer\n * (invariant 2) — and is exercised in tests via vitest fake timers, matching\n * the no-injection convention of the `fromTimeout` / `fromInterval` exemplars.\n *\n * WHAT BACKS THE TIMER IS THE HOST'S CHOICE. `setTimeout` is the universal\n * default, but it is not universal: a Durable Object that hibernates has no\n * live `setTimeout` to wake it, so its deadline must be backed by a `do_alarm`\n * registered on the alarm registry; a test host wants a fake-timer schedule.\n * That backing is the `ArmTimer` seam — `subscribeWith(armTimer)` builds the\n * `subscribe.deadline` handler from a host-plugged timer, and `subscribeDeadline`\n * IS `subscribeWith(setTimeoutArmTimer())`. One deadline surface, three hosts;\n * there is no second Sub type and no second `atMs` anchor to keep in sync.\n *\n * The anchor is what makes the seam safe. `atMs` is ABSOLUTE, so whatever backs\n * it arms to a fixed wall-clock instant: the host computes the gap itself\n * (`atMs - Date.now()`), and on a host that just rehydrated after hibernation\n * that gap is the SHRUNKEN remainder — the deadline honours the original target\n * rather than resetting to a fresh full-length window.\n *\n * Strengthens invariant 4 (external time is a subscription — the deadline is a\n * `Sub` the runtime reconciles, never a `setTimeout` leaked into a reducer) and\n * invariant 7 (identity is explicit — the Sub carries a stable `SubId`, so the\n * reconcile pass leaves it running across transitions instead of churning it).\n */\n\nimport type {\n DepKeyedSub,\n Dispose,\n Sub,\n SubId,\n TimerDeps,\n} from \"../../../index\";\nimport { subId } from \"../../../index\";\n\n/**\n * One deadline, as a battery lists it. `atMs` is the absolute target — epoch\n * milliseconds (the `Date.now()` / `Date.parse(...)` scale), NOT a relative\n * delay. `id` names the deadline inside its list: it rides on the dispatched\n * Msg so a reducer can tell WHICH deadline fired, and a registry-backed timer\n * (a DO alarm slot) keys its entry on it.\n *\n * It is plain data — it becomes part of the `deadline` Sub's `deps`, which the\n * engine hashes — and is not itself a running Sub.\n */\nexport type DeadlineSub<N extends string | undefined = undefined> = {\n readonly id: SubId;\n readonly type: \"deadline\";\n readonly atMs: number;\n} & DeadlineOpts<N>;\n\n/**\n * The running `\"deadline\"` Sub: its `deps` is the non-empty list of deadlines\n * to arm. Name it in `types.sub` for a machine that arms deadlines.\n */\nexport type DeadlinesSub<N extends string | undefined = undefined> = Sub<\n \"deadline\",\n readonly DeadlineSub<N>[]\n>;\n\n/**\n * The `deps` of a `deadline` Sub: the list, or `null` when it is empty — an\n * empty list is a Sub with nothing to arm, and an off Sub is not a live one\n * (`driveToDone` reads live Subs to tell a waiting machine from a stalled one).\n */\nexport function deadlines<N extends string | undefined = undefined>(\n list: readonly DeadlineSub<N>[],\n): readonly DeadlineSub<N>[] | null {\n return list.length === 0 ? null : list;\n}\n\n/**\n * The `subs` entry that arms whatever deadlines `select` lists at a state:\n *\n * subs: [deadlinesSub((s: State) => rc.subs(s.resilience))],\n * // run(machine, { subscribe: { deadline: subscribeDeadline } })\n */\nexport function deadlinesSub<S, N extends string | undefined = undefined>(\n select: (state: S) => readonly DeadlineSub<N>[],\n): DepKeyedSub<S, DeadlinesSub<N>> {\n return { type: \"deadline\", deps: (state) => deadlines(select(state)) };\n}\n\n/**\n * Additive options the `deadlineSub` factory folds onto the Sub literal. This\n * is the extension seam (invariant 5 — composition by reduction): future fields\n * — `repeatMs` for a re-arming deadline, `jitterMs` for spread, etc. — land HERE\n * as optional members, and every existing call site keeps compiling because the\n * factory's third parameter is optional and the fields are `readonly` optionals\n * on `DeadlineSub`. Callers are insulated from the concrete `DeadlineSub` shape:\n * they ask for what they need by name, never positionally, so a shape extension\n * never rewrites the ~13 `deadlineSub(id, atMs)` call sites.\n *\n * The first field to land here is {@link DeadlineOpts.name} — the knob name the\n * dispatched Msg tag is derived from — and it landed exactly that way: one\n * optional member, zero rewritten call sites.\n */\nexport type DeadlineOpts<N extends string | undefined = undefined> = {\n /**\n * The knob this deadline belongs to. Present → the dispatched Msg's tag is\n * `` `${name}_deadline` ``; absent → the tag stays the bare\n * `\"deadline_exceeded\"` every machine wired before this field is already\n * handling. `N` is the name as a TYPE as well as a value, so a consumer's\n * cell key and the Msg type are derived from one place and cannot drift.\n *\n * Name a deadline when a machine arms more than one family of them. The Sub\n * *id* was already scoped (`` `${name}:deadline:${key}` ``); this scopes the\n * Msg the Sub dispatches, which is what a reducer cell keys off.\n */\n readonly name?: N;\n};\n\n/**\n * The Msg the deadline dispatches when the wall clock crosses `atMs`. Exported\n * as a tagged shape so consumers can union it into their machine's Msg type and\n * handle it in a reducer cell. `id` lets a reducer disambiguate WHICH deadline\n * fired when a machine arms several at once; `atMs` echoes the target the timer\n * was scheduled against (useful for assertions / observability).\n *\n * Snake-shaped `\"deadline_exceeded\"` matches the tagged-Msg style this module's\n * orchestrator requested; the actor-in-name PascalCase convention\n * (`TimerFiredDeadline`) is the consumer's to apply at their own boundary if\n * they prefer — `deadlineExceeded(...)` only fixes the wire shape, not the\n * consumer's local dialect.\n */\nexport type DeadlineExceeded<N extends string | undefined = undefined> = {\n readonly type: DeadlineMsgType<N>;\n readonly id: string;\n readonly atMs: number;\n};\n\n/**\n * The dispatched Msg's tag, derived from the deadline's optional name. A named\n * deadline speaks `` `${N}_deadline` ``; an unnamed one speaks the bare\n * `\"deadline_exceeded\"` literal, which is what `DeadlineExceeded` with no type\n * argument still means — so every machine wired before the name existed keeps\n * the exact type it had.\n */\nexport type DeadlineMsgType<N extends string | undefined> = N extends string\n ? `${N}_deadline`\n : \"deadline_exceeded\";\n\n/**\n * Build a deadline literal. Pure data — no clock read, no timer; the timer\n * is armed later by the `deadline` runner. `id` is branded via `subId(...)` so\n * accidental raw-string drift fails at the type level (invariant 7).\n *\n * List it from a battery's `subs(slice)` while the deadline should be armed;\n * drop it to disarm — the engine restarts the `deadline` Sub, whose cleanup\n * clears the pending timer before it fires. That is the \"cancel on state exit\"\n * lifecycle.\n *\n * @param id Identity for this deadline, echoed on the Msg it fires. Use\n * distinct ids for distinct deadlines on the same machine.\n * @param atMs Absolute target in epoch milliseconds (e.g. `Date.now() + 900_000`\n * for a 15-minute guard, or a persisted `expiresAt`).\n * @param opts Optional additive fields folded onto the Sub literal (see\n * {@link DeadlineOpts}). Omit it for the common case; pass it when a\n * future field (e.g. `repeatMs`) needs to ride on the deadline. The\n * optionality is the future-proofing seam — extending the shape\n * never breaks an existing `deadlineSub(id, atMs)` call site.\n */\nexport function deadlineSub<N extends string | undefined = undefined>(\n id: string,\n atMs: number,\n opts?: DeadlineOpts<N>,\n): DeadlineSub<N> {\n return { ...opts, id: subId(id), type: \"deadline\", atMs };\n}\n\n/**\n * The host-plugged timer backing. Given the deadline's stable `id`, its\n * absolute `atMs`, and the Msg to fire, arm the platform-appropriate timer\n * (DO alarm registry, `setTimeout`, fake timer) and return a cleanup that\n * cancels the pending fire.\n *\n * `id` is the deadline's own id — a registry-backed host (the DO alarm slot)\n * keys its entry on `id` so the cleanup deletes the EXACT entry it armed. A\n * `setTimeout`-backed host ignores `id` (the closure holds the handle).\n *\n * The host computes the gap itself: `atMs - Date.now()` is the REMAINING time.\n * On a host that just rehydrated after hibernation that gap is the shrunken\n * remainder — the deadline is NOT reset to its full length. That recomputation\n * is the reason `atMs` rides on the Sub as an absolute instant rather than a\n * delay: the anchor is what every backing agrees on.\n */\nexport type ArmTimer<M> = (\n id: SubId,\n atMs: number,\n msg: M,\n dispatch: (msg: M) => void,\n) => () => void;\n\n/**\n * Build the `deadline` runner from a host-plugged `armTimer`. This module owns\n * the deadline shape, the anchor, and the Msg; the HOST owns what backs the\n * timer. The runner arms every deadline in the Sub's `deps` list — reading\n * `id` + `atMs`, building the `deadlineExceeded(...)` Msg and handing all three\n * to `armTimer` — and its cleanup cancels them all.\n *\n * Use it when `setTimeout` is the wrong backing — most concretely a Durable\n * Object, which hibernates and must register a `do_alarm` instead:\n *\n * run(machine, {\n * subscribe: {\n * deadline: subscribeWith((id, atMs, msg, dispatch) =>\n * alarms.register(id, atMs, () => dispatch(msg)),\n * ),\n * },\n * })\n *\n * For the `setTimeout` default, use {@link subscribeDeadline} — it is exactly\n * `subscribeWith(setTimeoutArmTimer())`, so there is one deadline surface and\n * one anchor, not a second one per host.\n */\nexport function subscribeWith<N extends string | undefined = undefined>(\n armTimer: ArmTimer<DeadlineExceeded<N>>,\n): (\n sub: DeadlinesSub<N>,\n ctx: unknown,\n dispatch: (msg: DeadlineExceeded<N>) => void,\n) => Dispose {\n return (sub, _ctx, dispatch) => {\n const cancels = sub.deps.map((deadline) =>\n armTimer(\n deadline.id,\n deadline.atMs,\n deadlineExceeded(deadline.id, deadline.atMs, deadline.name),\n dispatch,\n ),\n );\n return () => {\n for (const cancel of cancels) cancel();\n };\n };\n}\n\n/**\n * The `setTimeout` timer backing — for node / browser / any host whose timer is\n * a plain `setTimeout`. Arms for the REMAINING time (`max(0, atMs - Date.now())`,\n * floored at 0 so an already-past deadline fires on the NEXT tick rather than\n * synchronously inside the reconcile pass), so a deadline re-derived after a\n * rehydrate — or re-armed by a restart — fires at the original instant, not a\n * fresh full window. A hibernating host does NOT use this — it plugs its own\n * `armTimer` (a `do_alarm` registration) into {@link subscribeWith}.\n */\nexport function setTimeoutArmTimer<\n N extends string | undefined = undefined,\n>(): ArmTimer<DeadlineExceeded<N>> {\n return (_id, atMs, msg, dispatch) => {\n // Recompute the remaining delay from the CURRENT clock so a late start\n // (post-rehydrate, or a restart) still targets the same absolute moment.\n const handle = setTimeout(\n () => dispatch(msg),\n Math.max(0, atMs - Date.now()),\n );\n return () => clearTimeout(handle);\n };\n}\n\n/**\n * The `deadline` runner for the DEFAULT `setTimeout` backing. Arms a one-shot\n * timer per listed deadline for its remaining delay `max(0, atMs - Date.now())`\n * and dispatches `deadlineExceeded(...)` when each fires; the cleanup clears\n * every pending timer.\n *\n * It is `subscribeWith(setTimeoutArmTimer())` — the default backing named, not\n * a separate implementation, so the host-plugged path and the default path can\n * never disagree about the anchor.\n *\n * run(machine, { subscribe: { deadline: subscribeDeadline } })\n *\n * Generic in the deadline's name so ONE runner serves every knob in a machine:\n * the tag it dispatches is read off each deadline's own `name`, so a machine\n * listing a named knob's deadlines beside an unnamed one's wires this once.\n */\nexport const subscribeDeadline: <N extends string | undefined = undefined>(\n sub: DeadlinesSub<N>,\n ctx: unknown,\n dispatch: (msg: DeadlineExceeded<N>) => void,\n) => Dispose = <N extends string | undefined>(\n sub: DeadlinesSub<N>,\n ctx: unknown,\n dispatch: (msg: DeadlineExceeded<N>) => void,\n) => subscribeWith<N>(setTimeoutArmTimer<N>())(sub, ctx, dispatch);\n\n/**\n * Construct the Msg the deadline dispatches. Exported so consumers can build /\n * assert the same shape (and so the runner and tests share one\n * constructor rather than two literals that can drift).\n */\nexport function deadlineExceeded<N extends string | undefined = undefined>(\n id: string,\n atMs: number,\n name?: N,\n): DeadlineExceeded<N> {\n return { type: deadlineMsgType(name), id, atMs };\n}\n\n/**\n * The one place the named tag is spelled. `undefined` → the bare\n * `\"deadline_exceeded\"` literal; a name → `` `${name}_deadline` ``. Exported so\n * a consumer keying a reducer cell off a named deadline derives the key from\n * the same function the Sub dispatches through, instead of re-spelling the\n * template and drifting.\n */\nexport function deadlineMsgType<N extends string | undefined>(\n name?: N,\n): DeadlineMsgType<N> {\n return (\n name === undefined ? \"deadline_exceeded\" : `${name}_deadline`\n ) as DeadlineMsgType<N>;\n}\n\n/**\n * The built-in `timer` Sub's deps for the SOONEST deadline in `list`, counted\n * from `nowMs`; `null` when the list is empty (no timer armed).\n *\n * This is how a battery arms a whole deadline list through the engine's\n * built-in `timer` instead of a `deadline` runner: a machine declares ONE\n * `{ type: \"timer\", deps: (s) => nextTimer(list(s), clock(s)) }` entry. The\n * timer fires the soonest deadline's `DeadlineExceeded` Msg; the transition it\n * causes drops that deadline from the list, and the next soonest is armed.\n *\n * `nowMs` is the instant the list was last computed against — the battery's\n * own record of its latest transition, never a clock read (invariant 2). The\n * countdown is `atMs - nowMs`, clamped at `0`, and because `ms` is part of the\n * deps, every new `nowMs` restarts the countdown from that instant. PURE.\n */\nexport function nextTimer<N extends string | undefined = undefined>(\n list: readonly DeadlineSub<N>[],\n nowMs: number,\n): TimerDeps<DeadlineExceeded<N>> | null {\n let soonest: DeadlineSub<N> | undefined;\n for (const deadline of list) {\n if (soonest === undefined || deadline.atMs < soonest.atMs) {\n soonest = deadline;\n }\n }\n if (soonest === undefined) return null;\n return {\n ms: Math.max(0, soonest.atMs - nowMs),\n msg: deadlineExceeded(soonest.id, soonest.atMs, soonest.name),\n };\n}\n","/**\n * internal/resilience/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.\n *\n * Same pure-state-ops shape as `../../idempotency/` and\n * `../../work-queue/ops.ts`: a state type plus pure transition functions. Host-\n * agnostic by construction — no `Date.now()`, no timers, no I/O inside the ops.\n * The caller injects the clock (`nowMs`) and folds the result through its own\n * reducer, keeping the store inside TEA's invariant 2 (transitions are pure:\n * same inputs → same outputs) so a consumer can exercise it with `replay`\n * without a fake clock.\n *\n * Every op returns a NEW cache value and never mutates its input (invariant 1 —\n * state is a value, not a place). `get` / `has` are read-shaped; `set` /\n * `remove` / `evictExpired` return a new cache.\n *\n * **Distinct from `/idempotency`.** Both are TTL-bounded key maps, but they\n * answer different questions and so carry different shapes:\n *\n * - `/idempotency` is DEDUPE-focused — \"have I already processed this key?\"\n * TTL is a single store-level policy (`ttlMs`), each entry stores the\n * `atMs` it was remembered at, and expiry is derived (`now - atMs >=\n * ttlMs`). It carries an `order` ledger + `capacity` because the use case\n * (webhook receivers, retried messages) needs bounded LRU eviction.\n * - `/cache` is RETRIEVAL-focused — \"what is the value for this key right\n * now?\" TTL is set PER ENTRY at write time: `set(key, value, nowMs,\n * ttlMs)` bakes the absolute `expiresAtMs = nowMs + ttlMs` into the entry.\n * Different keys can carry different lifetimes (a hot key cached for 1s, a\n * cold one for 1h) — the dimension idempotency's single store-level\n * `ttlMs` cannot express. No `order`, no `capacity`: a cache bounds itself\n * by TTL + periodic eviction, not by LRU count.\n *\n * The eviction Sub is the \"eviction is a Msg\" half: a `setInterval`-shaped Sub\n * (composed directly on `fromInterval`, from `src/subs/`) that periodically\n * dispatches a `cache_evict` Msg the consumer's reducer handles by calling\n * `evictExpired`. Time still never enters the reducer — the Sub fires the Msg,\n * the reducer reads `nowMs` off the Msg (the consumer stamps it at the Sub\n * boundary), and `evictExpired` does the pure drop.\n *\n * Internal to the package (`src/internal/resilience/`), not a published\n * subpath (#46); same one-shape-per-module rule as `work-queue`, `idempotency`,\n * and `rate-limit`.\n */\n\nimport type { DepKeyedSub, Sub } from \"../../../index\";\nimport { fromInterval } from \"../../../subs\";\nimport type { SubscribeHandler } from \"../../../subs/types\";\n\n/**\n * A cached entry: the `value` and the absolute clock reading `expiresAtMs` it\n * expires at. Unlike `/idempotency`'s entry (which stores `atMs` and derives\n * expiry from a store-level `ttlMs`), the expiry instant is baked in at write\n * time — so per-entry TTLs are a first-class capability. Both fields are plain\n * data so the whole cache is trivially JSON-serializable into a consumer's\n * `Store<S>`.\n */\nexport interface CacheEntry<V> {\n readonly value: V;\n readonly expiresAtMs: number;\n}\n\n/**\n * The cache: a map of `key → entry`. No `order` ledger and no `capacity` (cf.\n * `/idempotency`) — a TTL cache bounds itself by expiry + periodic eviction,\n * not by LRU count. Each entry carries its own `expiresAtMs`, so the cache\n * needs no store-level TTL policy field.\n */\nexport interface TtlCache<V> {\n readonly entries: Readonly<Record<string, CacheEntry<V>>>;\n}\n\n/** Create an empty cache. */\nexport function initCache<V>(): TtlCache<V> {\n return { entries: {} };\n}\n\n/**\n * Whether `entry` is expired at `nowMs`. Half-open: an entry whose\n * `expiresAtMs` exactly equals `nowMs` is expired (`nowMs >= expiresAtMs`),\n * matching `/idempotency` and `/rate-limit`'s window-edge cutoff — a value\n * cached at `t` with a `ttlMs` lifetime is gone at exactly `t + ttlMs`.\n */\nfunction isExpired<V>(entry: CacheEntry<V>, nowMs: number): boolean {\n return nowMs >= entry.expiresAtMs;\n}\n\n/**\n * The cached `value` for `key` iff present AND unexpired at `nowMs`, else\n * `undefined`. An expired entry reads as absent even though it is physically\n * still in `entries` until the next `evictExpired` (or an overwriting `set`)\n * drops it. PURE — reads only, never mutates.\n */\nexport function get<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): V | undefined {\n const entry = cache.entries[key];\n if (entry === undefined || isExpired(entry, nowMs)) return undefined;\n return entry.value;\n}\n\n/**\n * True iff `key` is present AND unexpired at `nowMs`. The boolean sibling of\n * `get` for callers that only need presence, not the value. PURE.\n */\nexport function has<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): boolean {\n const entry = cache.entries[key];\n if (entry === undefined) return false;\n return !isExpired(entry, nowMs);\n}\n\n/**\n * Write `key → value` with an absolute expiry of `nowMs + ttlMs`. Re-`set`ting\n * an existing key overwrites both the value and the expiry — the TTL clock\n * restarts from the new `nowMs`. PURE — returns a new cache; the input is\n * untouched (a fresh shallow copy of `entries`).\n */\nexport function set<V>(\n cache: TtlCache<V>,\n key: string,\n value: V,\n nowMs: number,\n ttlMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n nextEntries[key] = { value, expiresAtMs: nowMs + ttlMs };\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Drop `key` regardless of expiry. PURE — returns a new cache; the input is\n * untouched. Removing an absent key returns an equivalent cache (the shallow\n * copy still happens, but no entry is dropped).\n */\nexport function remove<V>(cache: TtlCache<V>, key: string): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n delete nextEntries[key];\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Physically drop every entry expired at `nowMs` (`nowMs >= expiresAtMs`).\n * Returns a NEW cache with the survivors. This is the pure work the eviction\n * Sub's `cache_evict` Msg triggers — `get` / `has` already treat expired\n * entries as absent, so eviction is purely a memory-reclaim pass, not a\n * correctness one. PURE — the input cache is never mutated.\n */\nexport function evictExpired<V>(\n cache: TtlCache<V>,\n nowMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = {};\n for (const [key, entry] of Object.entries(cache.entries)) {\n if (isExpired(entry, nowMs)) continue;\n nextEntries[key] = entry;\n }\n return { ...cache, entries: nextEntries };\n}\n\n// ---------------------------------------------------------------------------\n// Eviction Sub — \"eviction is a Msg\".\n//\n// A periodic timer Sub that dispatches `cache_evict` so the consumer's reducer\n// can call `evictExpired`. It is `fromInterval` specialized to a fixed Msg\n// shape — the interval rides in the Sub's `deps` (which the engine keys the\n// Sub on) and the runner dispatches a `cache_evict` Msg tagged with the cache's\n// name, so a consumer running several caches can route each tick to the right\n// cache by `msg.id`.\n//\n// Mirrors `fromInterval` exactly (factory + SubscribeHandler cell + exported\n// Msg constructor); it does NOT reimplement the timer lifecycle — it composes\n// directly on `fromInterval`, which owns `setInterval` / `clearInterval`. That\n// keeps the timer-handle bookkeeping (and its cleanup) in one place and this\n// module purely declarative.\n//\n// Strengthens invariant 4 (external time is a Sub the engine reconciles) and\n// invariant 9 (named, small Sub surface composed on the shared factory).\n// ---------------------------------------------------------------------------\n\n/**\n * The Msg the eviction Sub dispatches each tick. `id` is the cache's name so a\n * consumer with multiple caches can route the tick to the matching cache. The\n * reducer reads `Date.now()`-free time by stamping `nowMs` itself at the Sub\n * boundary if needed; the canonical reducer cell is\n * `cache_evict: (s) => [{ ...s, cache: evictExpired(s.cache, nowMs) }, []]`.\n */\nexport interface CacheEvictMsg {\n readonly type: \"cache_evict\";\n readonly id: string;\n}\n\n/** Construct a `cache_evict` Msg for the cache identified by `id`. */\nexport function cacheEvictMsg(id: string): CacheEvictMsg {\n return { type: \"cache_evict\", id };\n}\n\n/**\n * The `deps` of an eviction Sub: which cache it ticks for (`name`, echoed as\n * the Msg's `id`) and the tick period (`intervalMs`, which `fromInterval`\n * reads).\n */\nexport type CacheEvictionDeps = {\n readonly name: string;\n readonly intervalMs: number;\n};\n\n/**\n * The running eviction Sub: a `setInterval`-shaped Sub whose `deps` carry the\n * cache's name and tick period. `type: \"cache\"` is the lowercase source-noun\n * Sub convention (canon §2.5 / naming-style).\n */\nexport type CacheEvictionSub = Sub<\"cache\", CacheEvictionDeps>;\n\n/**\n * The `subs` entry for an eviction tick on the cache named `name`, every\n * `everyMs`. It is on for the machine's life:\n *\n * subs: [cacheEvictionSub(\"session-cache\", 30_000)],\n * // run(machine, { subscribe: { cache: cacheEvictionSubscribe } })\n *\n * The engine keys the Sub on its `deps`, so a machine that declares the same\n * name and period keeps one running interval, and a changed period restarts\n * it at the new one.\n */\nexport function cacheEvictionSub(\n name: string,\n everyMs: number,\n): DepKeyedSub<unknown, CacheEvictionSub> {\n return { type: \"cache\", deps: () => ({ name, intervalMs: everyMs }) };\n}\n\n/**\n * The `cache` runner for the eviction Sub. Hand it to `run`:\n *\n * run(machine, { subscribe: { cache: cacheEvictionSubscribe } })\n *\n * Composed directly on `fromInterval` — it owns the `setInterval` /\n * `clearInterval` lifecycle; this runner only pins the Msg the tick dispatches\n * (`cacheEvictMsg(sub.deps.name)`). The cleanup `fromInterval` returns stops\n * the interval when the Sub stops.\n */\nexport const cacheEvictionSubscribe: SubscribeHandler<\n CacheEvictionSub,\n CacheEvictMsg,\n unknown\n> = fromInterval((sub) => cacheEvictMsg(sub.deps.name));\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/resilience/deadline/index.ts","../src/internal/resilience/cache/index.ts"],"names":[],"mappings":";;;;AA8FO,SAAS,UACd,IAAA,EACkC;AAClC,EAAA,OAAO,IAAA,CAAK,MAAA,KAAW,CAAA,GAAI,IAAA,GAAO,IAAA;AACpC;AAQO,SAAS,aACd,MAAA,EACiC;AACjC,EAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,IAAA,EAAM,CAAC,UAAU,SAAA,CAAU,MAAA,CAAO,KAAK,CAAC,CAAA,EAAE;AACvE;AAiFO,SAAS,WAAA,CACd,EAAA,EACA,IAAA,EACA,IAAA,EACgB;AAChB,EAAA,OAAO,EAAE,GAAG,IAAA,EAAM,EAAA,EAAI,MAAM,EAAE,CAAA,EAAG,IAAA,EAAM,UAAA,EAAY,IAAA,EAAK;AAC1D;AA+CO,SAAS,cACd,QAAA,EAKW;AACX,EAAA,OAAO,CAAC,GAAA,EAAK,IAAA,EAAM,QAAA,KAAa;AAC9B,IAAA,MAAM,OAAA,GAAU,IAAI,IAAA,CAAK,GAAA;AAAA,MAAI,CAAC,QAAA,KAC5B,QAAA;AAAA,QACE,QAAA,CAAS,EAAA;AAAA,QACT,QAAA,CAAS,IAAA;AAAA,QACT,iBAAiB,QAAA,CAAS,EAAA,EAAI,QAAA,CAAS,IAAA,EAAM,SAAS,IAAI,CAAA;AAAA,QAC1D;AAAA;AACF,KACF;AACA,IAAA,OAAO,MAAM;AACX,MAAA,KAAA,MAAW,MAAA,IAAU,SAAS,MAAA,EAAO;AAAA,IACvC,CAAA;AAAA,EACF,CAAA;AACF;AAWO,SAAS,kBAAA,GAEmB;AACjC,EAAA,OAAO,CAAC,GAAA,EAAK,IAAA,EAAM,GAAA,EAAK,QAAA,KAAa;AAGnC,IAAA,MAAM,MAAA,GAAS,UAAA;AAAA,MACb,MAAM,SAAS,GAAG,CAAA;AAAA,MAClB,KAAK,GAAA,CAAI,CAAA,EAAG,IAAA,GAAO,IAAA,CAAK,KAAK;AAAA,KAC/B;AACA,IAAA,OAAO,MAAM,aAAa,MAAM,CAAA;AAAA,EAClC,CAAA;AACF;AAkBO,IAAM,iBAAA,GAIE,CACb,GAAA,EACA,GAAA,EACA,QAAA,KACG,aAAA,CAAiB,kBAAA,EAAuB,CAAA,CAAE,GAAA,EAAK,GAAA,EAAK,QAAQ;AAO1D,SAAS,gBAAA,CACd,EAAA,EACA,IAAA,EACA,IAAA,EACqB;AACrB,EAAA,OAAO,EAAE,IAAA,EAAM,eAAA,CAAgB,IAAI,CAAA,EAAG,IAAI,IAAA,EAAK;AACjD;AASO,SAAS,gBACd,IAAA,EACoB;AACpB,EAAA,OACE,IAAA,KAAS,MAAA,GAAY,mBAAA,GAAsB,CAAA,EAAG,IAAI,CAAA,SAAA,CAAA;AAEtD;AAiBO,SAAS,SAAA,CACd,MACA,KAAA,EACuC;AACvC,EAAA,IAAI,OAAA;AACJ,EAAA,KAAA,MAAW,YAAY,IAAA,EAAM;AAC3B,IAAA,IAAI,OAAA,KAAY,MAAA,IAAa,QAAA,CAAS,IAAA,GAAO,QAAQ,IAAA,EAAM;AACzD,MAAA,OAAA,GAAU,QAAA;AAAA,IACZ;AAAA,EACF;AACA,EAAA,IAAI,OAAA,KAAY,QAAW,OAAO,IAAA;AAClC,EAAA,OAAO;AAAA,IACL,IAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,OAAA,CAAQ,OAAO,KAAK,CAAA;AAAA,IACpC,KAAK,gBAAA,CAAiB,OAAA,CAAQ,IAAI,OAAA,CAAQ,IAAA,EAAM,QAAQ,IAAI;AAAA,GAC9D;AACF;;;AC9SO,SAAS,SAAA,GAA4B;AAC1C,EAAA,OAAO,EAAE,OAAA,EAAS,EAAC,EAAE;AACvB;AAQA,SAAS,SAAA,CAAa,OAAsB,KAAA,EAAwB;AAClE,EAAA,OAAO,SAAS,KAAA,CAAM,WAAA;AACxB;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,UAAU,MAAA,IAAa,SAAA,CAAU,KAAA,EAAO,KAAK,GAAG,OAAO,MAAA;AAC3D,EAAA,OAAO,KAAA,CAAM,KAAA;AACf;AAMO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACS;AACT,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,CAAC,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA;AAChC;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACA,OACA,KAAA,EACa;AACb,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,WAAA,CAAY,GAAG,CAAA,GAAI,EAAE,KAAA,EAAO,WAAA,EAAa,QAAQ,KAAA,EAAM;AACvD,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAOO,SAAS,MAAA,CAAU,OAAoB,GAAA,EAA0B;AACtE,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,OAAO,YAAY,GAAG,CAAA;AACtB,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AASO,SAAS,YAAA,CACd,OACA,KAAA,EACa;AACb,EAAA,MAAM,cAA6C,EAAC;AACpD,EAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA,EAAG;AACxD,IAAA,IAAI,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA,EAAG;AAC7B,IAAA,WAAA,CAAY,GAAG,CAAA,GAAI,KAAA;AAAA,EACrB;AACA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAmCO,SAAS,cAAc,EAAA,EAA2B;AACvD,EAAA,OAAO,EAAE,IAAA,EAAM,aAAA,EAAe,EAAA,EAAG;AACnC;AA8BO,SAAS,gBAAA,CACd,MACA,OAAA,EACwC;AACxC,EAAA,OAAO,EAAE,MAAM,OAAA,EAAS,IAAA,EAAM,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,OAAA,EAAQ,CAAA,EAAG;AACtE;AAYO,IAAM,sBAAA,GAIT,aAAa,CAAC,GAAA,KAAQ,cAAc,GAAA,CAAI,IAAA,CAAK,IAAI,CAAC","file":"chunk-TAKPC6SD.js","sourcesContent":["/**\n * internal/resilience/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock\n * deadline is reached.\n *\n * Generalizes the recurring \"auto-fail a machine at T+N\" / \"15-minute stale\n * guard\" pattern: a deadline a battery lists only while it should be armed,\n * which dispatches a single `DeadlineExceeded` Msg when the wall clock crosses\n * `atMs`.\n *\n * A battery's `subs(slice)` returns its deadlines as a LIST (`DeadlineSub[]`,\n * one per retry timer, say). A machine declares that list as ONE Sub of type\n * `\"deadline\"` whose `deps` is the list (`deadlinesSub(select)`), and the\n * `deadline` runner arms every deadline in it. A change to the list restarts\n * the runner, which re-arms each deadline for its REMAINING time — `atMs` is\n * absolute, so a restart never moves a deadline.\n *\n * Difference from `fromTimeout` (relative — \"fire after N ms\"): a deadline is\n * an ABSOLUTE target. The delay is computed at subscribe time as\n * `max(0, atMs - Date.now())`. The consequence that matters: subscribing LATE —\n * e.g. after a `Store` rehydrate that resumes a machine whose deadline was set\n * before the page reloaded — still fires at the correct absolute moment,\n * because the remaining delay is recomputed from the current clock, not stored\n * in the Sub. A deadline already in the past at subscribe time yields delay `0`,\n * so `setTimeout(fn, 0)` fires on the NEXT tick (never synchronously inside the\n * reconcile pass — the substrate must finish wiring all subs before any Msg\n * lands).\n *\n * `deadlineSub` produces one deadline literal carrying `atMs`; the runner\n * (`subscribeDeadline`) translates each `atMs` → a relative delay and arms a\n * `setTimeout` / `clearTimeout` pair per deadline. The clock\n * read (`Date.now()`) lives only in the subscribe handler — never in a reducer\n * (invariant 2) — and is exercised in tests via vitest fake timers, matching\n * the no-injection convention of the `fromTimeout` / `fromInterval` exemplars.\n *\n * WHAT BACKS THE TIMER IS THE HOST'S CHOICE. `setTimeout` is the universal\n * default, but it is not universal: a Durable Object that hibernates has no\n * live `setTimeout` to wake it, so its deadline must be backed by a `do_alarm`\n * registered on the alarm registry; a test host wants a fake-timer schedule.\n * That backing is the `ArmTimer` seam — `subscribeWith(armTimer)` builds the\n * `subscribe.deadline` handler from a host-plugged timer, and `subscribeDeadline`\n * IS `subscribeWith(setTimeoutArmTimer())`. One deadline surface, three hosts;\n * there is no second Sub type and no second `atMs` anchor to keep in sync.\n *\n * The anchor is what makes the seam safe. `atMs` is ABSOLUTE, so whatever backs\n * it arms to a fixed wall-clock instant: the host computes the gap itself\n * (`atMs - Date.now()`), and on a host that just rehydrated after hibernation\n * that gap is the SHRUNKEN remainder — the deadline honours the original target\n * rather than resetting to a fresh full-length window.\n *\n * Strengthens invariant 4 (external time is a subscription — the deadline is a\n * `Sub` the runtime reconciles, never a `setTimeout` leaked into a reducer) and\n * invariant 7 (identity is explicit — the Sub carries a stable `SubId`, so the\n * reconcile pass leaves it running across transitions instead of churning it).\n */\n\nimport type {\n DepKeyedSub,\n Dispose,\n Sub,\n SubId,\n TimerDeps,\n} from \"../../../index\";\nimport { subId } from \"../../../index\";\n\n/**\n * One deadline, as a battery lists it. `atMs` is the absolute target — epoch\n * milliseconds (the `Date.now()` / `Date.parse(...)` scale), NOT a relative\n * delay. `id` names the deadline inside its list: it rides on the dispatched\n * Msg so a reducer can tell WHICH deadline fired, and a registry-backed timer\n * (a DO alarm slot) keys its entry on it.\n *\n * It is plain data — it becomes part of the `deadline` Sub's `deps`, which the\n * engine hashes — and is not itself a running Sub.\n */\nexport type DeadlineSub<N extends string | undefined = undefined> = {\n readonly id: SubId;\n readonly type: \"deadline\";\n readonly atMs: number;\n} & DeadlineOpts<N>;\n\n/**\n * The running `\"deadline\"` Sub: its `deps` is the non-empty list of deadlines\n * to arm. Name it in `types.sub` for a machine that arms deadlines.\n */\nexport type DeadlinesSub<N extends string | undefined = undefined> = Sub<\n \"deadline\",\n readonly DeadlineSub<N>[]\n>;\n\n/**\n * The `deps` of a `deadline` Sub: the list, or `null` when it is empty — an\n * empty list is a Sub with nothing to arm, and an off Sub is not a live one\n * (`driveToDone` reads live Subs to tell a waiting machine from a stalled one).\n */\nexport function deadlines<N extends string | undefined = undefined>(\n list: readonly DeadlineSub<N>[],\n): readonly DeadlineSub<N>[] | null {\n return list.length === 0 ? null : list;\n}\n\n/**\n * The `subs` entry that arms whatever deadlines `select` lists at a state:\n *\n * subs: [deadlinesSub((s: State) => rc.subs(s.resilience))],\n * // run(machine, { subscribe: { deadline: subscribeDeadline } })\n */\nexport function deadlinesSub<S, N extends string | undefined = undefined>(\n select: (state: S) => readonly DeadlineSub<N>[],\n): DepKeyedSub<S, DeadlinesSub<N>> {\n return { type: \"deadline\", deps: (state) => deadlines(select(state)) };\n}\n\n/**\n * Additive options the `deadlineSub` factory folds onto the Sub literal. This\n * is the extension seam (invariant 5 — composition by reduction): future fields\n * — `repeatMs` for a re-arming deadline, `jitterMs` for spread, etc. — land HERE\n * as optional members, and every existing call site keeps compiling because the\n * factory's third parameter is optional and the fields are `readonly` optionals\n * on `DeadlineSub`. Callers are insulated from the concrete `DeadlineSub` shape:\n * they ask for what they need by name, never positionally, so a shape extension\n * never rewrites the ~13 `deadlineSub(id, atMs)` call sites.\n *\n * The first field to land here is {@link DeadlineOpts.name} — the knob name the\n * dispatched Msg tag is derived from — and it landed exactly that way: one\n * optional member, zero rewritten call sites.\n */\nexport type DeadlineOpts<N extends string | undefined = undefined> = {\n /**\n * The knob this deadline belongs to. Present → the dispatched Msg's tag is\n * `` `${name}_deadline` ``; absent → the tag stays the bare\n * `\"deadline_exceeded\"` every machine wired before this field is already\n * handling. `N` is the name as a TYPE as well as a value, so a consumer's\n * cell key and the Msg type are derived from one place and cannot drift.\n *\n * Name a deadline when a machine arms more than one family of them. The Sub\n * *id* was already scoped (`` `${name}:deadline:${key}` ``); this scopes the\n * Msg the Sub dispatches, which is what a reducer cell keys off.\n */\n readonly name?: N;\n};\n\n/**\n * The Msg the deadline dispatches when the wall clock crosses `atMs`. Exported\n * as a tagged shape so consumers can union it into their machine's Msg type and\n * handle it in a reducer cell. `id` lets a reducer disambiguate WHICH deadline\n * fired when a machine arms several at once; `atMs` echoes the target the timer\n * was scheduled against (useful for assertions / observability).\n *\n * Snake-shaped `\"deadline_exceeded\"` matches the tagged-Msg style this module's\n * orchestrator requested; the actor-in-name PascalCase convention\n * (`TimerFiredDeadline`) is the consumer's to apply at their own boundary if\n * they prefer — `deadlineExceeded(...)` only fixes the wire shape, not the\n * consumer's local dialect.\n */\nexport type DeadlineExceeded<N extends string | undefined = undefined> = {\n readonly type: DeadlineMsgType<N>;\n readonly id: string;\n readonly atMs: number;\n};\n\n/**\n * The dispatched Msg's tag, derived from the deadline's optional name. A named\n * deadline speaks `` `${N}_deadline` ``; an unnamed one speaks the bare\n * `\"deadline_exceeded\"` literal, which is what `DeadlineExceeded` with no type\n * argument still means — so every machine wired before the name existed keeps\n * the exact type it had.\n */\nexport type DeadlineMsgType<N extends string | undefined> = N extends string\n ? `${N}_deadline`\n : \"deadline_exceeded\";\n\n/**\n * Build a deadline literal. Pure data — no clock read, no timer; the timer\n * is armed later by the `deadline` runner. `id` is branded via `subId(...)` so\n * accidental raw-string drift fails at the type level (invariant 7).\n *\n * List it from a battery's `subs(slice)` while the deadline should be armed;\n * drop it to disarm — the engine restarts the `deadline` Sub, whose cleanup\n * clears the pending timer before it fires. That is the \"cancel on state exit\"\n * lifecycle.\n *\n * @param id Identity for this deadline, echoed on the Msg it fires. Use\n * distinct ids for distinct deadlines on the same machine.\n * @param atMs Absolute target in epoch milliseconds (e.g. `Date.now() + 900_000`\n * for a 15-minute guard, or a persisted `expiresAt`).\n * @param opts Optional additive fields folded onto the Sub literal (see\n * {@link DeadlineOpts}). Omit it for the common case; pass it when a\n * future field (e.g. `repeatMs`) needs to ride on the deadline. The\n * optionality is the future-proofing seam — extending the shape\n * never breaks an existing `deadlineSub(id, atMs)` call site.\n */\nexport function deadlineSub<N extends string | undefined = undefined>(\n id: string,\n atMs: number,\n opts?: DeadlineOpts<N>,\n): DeadlineSub<N> {\n return { ...opts, id: subId(id), type: \"deadline\", atMs };\n}\n\n/**\n * The host-plugged timer backing. Given the deadline's stable `id`, its\n * absolute `atMs`, and the Msg to fire, arm the platform-appropriate timer\n * (DO alarm registry, `setTimeout`, fake timer) and return a cleanup that\n * cancels the pending fire.\n *\n * `id` is the deadline's own id — a registry-backed host (the DO alarm slot)\n * keys its entry on `id` so the cleanup deletes the EXACT entry it armed. A\n * `setTimeout`-backed host ignores `id` (the closure holds the handle).\n *\n * The host computes the gap itself: `atMs - Date.now()` is the REMAINING time.\n * On a host that just rehydrated after hibernation that gap is the shrunken\n * remainder — the deadline is NOT reset to its full length. That recomputation\n * is the reason `atMs` rides on the Sub as an absolute instant rather than a\n * delay: the anchor is what every backing agrees on.\n */\nexport type ArmTimer<M> = (\n id: SubId,\n atMs: number,\n msg: M,\n dispatch: (msg: M) => void,\n) => () => void;\n\n/**\n * Build the `deadline` runner from a host-plugged `armTimer`. This module owns\n * the deadline shape, the anchor, and the Msg; the HOST owns what backs the\n * timer. The runner arms every deadline in the Sub's `deps` list — reading\n * `id` + `atMs`, building the `deadlineExceeded(...)` Msg and handing all three\n * to `armTimer` — and its cleanup cancels them all.\n *\n * Use it when `setTimeout` is the wrong backing — most concretely a Durable\n * Object, which hibernates and must register a `do_alarm` instead:\n *\n * run(machine, {\n * subscribe: {\n * deadline: subscribeWith((id, atMs, msg, dispatch) =>\n * alarms.register(id, atMs, () => dispatch(msg)),\n * ),\n * },\n * })\n *\n * For the `setTimeout` default, use {@link subscribeDeadline} — it is exactly\n * `subscribeWith(setTimeoutArmTimer())`, so there is one deadline surface and\n * one anchor, not a second one per host.\n */\nexport function subscribeWith<N extends string | undefined = undefined>(\n armTimer: ArmTimer<DeadlineExceeded<N>>,\n): (\n sub: DeadlinesSub<N>,\n ctx: unknown,\n dispatch: (msg: DeadlineExceeded<N>) => void,\n) => Dispose {\n return (sub, _ctx, dispatch) => {\n const cancels = sub.deps.map((deadline) =>\n armTimer(\n deadline.id,\n deadline.atMs,\n deadlineExceeded(deadline.id, deadline.atMs, deadline.name),\n dispatch,\n ),\n );\n return () => {\n for (const cancel of cancels) cancel();\n };\n };\n}\n\n/**\n * The `setTimeout` timer backing — for node / browser / any host whose timer is\n * a plain `setTimeout`. Arms for the REMAINING time (`max(0, atMs - Date.now())`,\n * floored at 0 so an already-past deadline fires on the NEXT tick rather than\n * synchronously inside the reconcile pass), so a deadline re-derived after a\n * rehydrate — or re-armed by a restart — fires at the original instant, not a\n * fresh full window. A hibernating host does NOT use this — it plugs its own\n * `armTimer` (a `do_alarm` registration) into {@link subscribeWith}.\n */\nexport function setTimeoutArmTimer<\n N extends string | undefined = undefined,\n>(): ArmTimer<DeadlineExceeded<N>> {\n return (_id, atMs, msg, dispatch) => {\n // Recompute the remaining delay from the CURRENT clock so a late start\n // (post-rehydrate, or a restart) still targets the same absolute moment.\n const handle = setTimeout(\n () => dispatch(msg),\n Math.max(0, atMs - Date.now()),\n );\n return () => clearTimeout(handle);\n };\n}\n\n/**\n * The `deadline` runner for the DEFAULT `setTimeout` backing. Arms a one-shot\n * timer per listed deadline for its remaining delay `max(0, atMs - Date.now())`\n * and dispatches `deadlineExceeded(...)` when each fires; the cleanup clears\n * every pending timer.\n *\n * It is `subscribeWith(setTimeoutArmTimer())` — the default backing named, not\n * a separate implementation, so the host-plugged path and the default path can\n * never disagree about the anchor.\n *\n * run(machine, { subscribe: { deadline: subscribeDeadline } })\n *\n * Generic in the deadline's name so ONE runner serves every knob in a machine:\n * the tag it dispatches is read off each deadline's own `name`, so a machine\n * listing a named knob's deadlines beside an unnamed one's wires this once.\n */\nexport const subscribeDeadline: <N extends string | undefined = undefined>(\n sub: DeadlinesSub<N>,\n ctx: unknown,\n dispatch: (msg: DeadlineExceeded<N>) => void,\n) => Dispose = <N extends string | undefined>(\n sub: DeadlinesSub<N>,\n ctx: unknown,\n dispatch: (msg: DeadlineExceeded<N>) => void,\n) => subscribeWith<N>(setTimeoutArmTimer<N>())(sub, ctx, dispatch);\n\n/**\n * Construct the Msg the deadline dispatches. Exported so consumers can build /\n * assert the same shape (and so the runner and tests share one\n * constructor rather than two literals that can drift).\n */\nexport function deadlineExceeded<N extends string | undefined = undefined>(\n id: string,\n atMs: number,\n name?: N,\n): DeadlineExceeded<N> {\n return { type: deadlineMsgType(name), id, atMs };\n}\n\n/**\n * The one place the named tag is spelled. `undefined` → the bare\n * `\"deadline_exceeded\"` literal; a name → `` `${name}_deadline` ``. Exported so\n * a consumer keying a reducer cell off a named deadline derives the key from\n * the same function the Sub dispatches through, instead of re-spelling the\n * template and drifting.\n */\nexport function deadlineMsgType<N extends string | undefined>(\n name?: N,\n): DeadlineMsgType<N> {\n return (\n name === undefined ? \"deadline_exceeded\" : `${name}_deadline`\n ) as DeadlineMsgType<N>;\n}\n\n/**\n * The built-in `timer` Sub's deps for the SOONEST deadline in `list`, counted\n * from `nowMs`; `null` when the list is empty (no timer armed).\n *\n * This is how a battery arms a whole deadline list through the engine's\n * built-in `timer` instead of a `deadline` runner: a machine declares ONE\n * `{ type: \"timer\", deps: (s) => nextTimer(list(s), clock(s)) }` entry. The\n * timer fires the soonest deadline's `DeadlineExceeded` Msg; the transition it\n * causes drops that deadline from the list, and the next soonest is armed.\n *\n * `nowMs` is the instant the list was last computed against — the battery's\n * own record of its latest transition, never a clock read (invariant 2). The\n * countdown is `atMs - nowMs`, clamped at `0`, and because `ms` is part of the\n * deps, every new `nowMs` restarts the countdown from that instant. PURE.\n */\nexport function nextTimer<N extends string | undefined = undefined>(\n list: readonly DeadlineSub<N>[],\n nowMs: number,\n): TimerDeps<DeadlineExceeded<N>> | null {\n let soonest: DeadlineSub<N> | undefined;\n for (const deadline of list) {\n if (soonest === undefined || deadline.atMs < soonest.atMs) {\n soonest = deadline;\n }\n }\n if (soonest === undefined) return null;\n return {\n ms: Math.max(0, soonest.atMs - nowMs),\n msg: deadlineExceeded(soonest.id, soonest.atMs, soonest.name),\n };\n}\n","/**\n * internal/resilience/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.\n *\n * Same pure-state-ops shape as `../../idempotency/` and\n * `../../work-queue/ops.ts`: a state type plus pure transition functions. Host-\n * agnostic by construction — no `Date.now()`, no timers, no I/O inside the ops.\n * The caller injects the clock (`nowMs`) and folds the result through its own\n * reducer, keeping the store inside TEA's invariant 2 (transitions are pure:\n * same inputs → same outputs) so a consumer can exercise it with `replay`\n * without a fake clock.\n *\n * Every op returns a NEW cache value and never mutates its input (invariant 1 —\n * state is a value, not a place). `get` / `has` are read-shaped; `set` /\n * `remove` / `evictExpired` return a new cache.\n *\n * **Distinct from `/idempotency`.** Both are TTL-bounded key maps, but they\n * answer different questions and so carry different shapes:\n *\n * - `/idempotency` is DEDUPE-focused — \"have I already processed this key?\"\n * TTL is a single store-level policy (`ttlMs`), each entry stores the\n * `atMs` it was remembered at, and expiry is derived (`now - atMs >=\n * ttlMs`). It carries an `order` ledger + `capacity` because the use case\n * (webhook receivers, retried messages) needs bounded LRU eviction.\n * - `/cache` is RETRIEVAL-focused — \"what is the value for this key right\n * now?\" TTL is set PER ENTRY at write time: `set(key, value, nowMs,\n * ttlMs)` bakes the absolute `expiresAtMs = nowMs + ttlMs` into the entry.\n * Different keys can carry different lifetimes (a hot key cached for 1s, a\n * cold one for 1h) — the dimension idempotency's single store-level\n * `ttlMs` cannot express. No `order`, no `capacity`: a cache bounds itself\n * by TTL + periodic eviction, not by LRU count.\n *\n * The eviction Sub is the \"eviction is a Msg\" half: a `setInterval`-shaped Sub\n * (composed directly on `fromInterval`, from `src/subs/`) that periodically\n * dispatches a `cache_evict` Msg the consumer's reducer handles by calling\n * `evictExpired`. Time still never enters the reducer — the Sub fires the Msg,\n * the reducer reads `nowMs` off the Msg (the consumer stamps it at the Sub\n * boundary), and `evictExpired` does the pure drop.\n *\n * Internal to the package (`src/internal/resilience/`), not a published\n * subpath (#46); same one-shape-per-module rule as `work-queue`, `idempotency`,\n * and `rate-limit`.\n */\n\nimport type { DepKeyedSub, Sub } from \"../../../index\";\nimport { fromInterval } from \"../../../subs\";\nimport type { SubscribeHandler } from \"../../../subs/types\";\n\n/**\n * A cached entry: the `value` and the absolute clock reading `expiresAtMs` it\n * expires at. Unlike `/idempotency`'s entry (which stores `atMs` and derives\n * expiry from a store-level `ttlMs`), the expiry instant is baked in at write\n * time — so per-entry TTLs are a first-class capability. Both fields are plain\n * data so the whole cache is trivially JSON-serializable into a consumer's\n * `Store<S>`.\n */\nexport interface CacheEntry<V> {\n readonly value: V;\n readonly expiresAtMs: number;\n}\n\n/**\n * The cache: a map of `key → entry`. No `order` ledger and no `capacity` (cf.\n * `/idempotency`) — a TTL cache bounds itself by expiry + periodic eviction,\n * not by LRU count. Each entry carries its own `expiresAtMs`, so the cache\n * needs no store-level TTL policy field.\n */\nexport interface TtlCache<V> {\n readonly entries: Readonly<Record<string, CacheEntry<V>>>;\n}\n\n/** Create an empty cache. */\nexport function initCache<V>(): TtlCache<V> {\n return { entries: {} };\n}\n\n/**\n * Whether `entry` is expired at `nowMs`. Half-open: an entry whose\n * `expiresAtMs` exactly equals `nowMs` is expired (`nowMs >= expiresAtMs`),\n * matching `/idempotency` and `/rate-limit`'s window-edge cutoff — a value\n * cached at `t` with a `ttlMs` lifetime is gone at exactly `t + ttlMs`.\n */\nfunction isExpired<V>(entry: CacheEntry<V>, nowMs: number): boolean {\n return nowMs >= entry.expiresAtMs;\n}\n\n/**\n * The cached `value` for `key` iff present AND unexpired at `nowMs`, else\n * `undefined`. An expired entry reads as absent even though it is physically\n * still in `entries` until the next `evictExpired` (or an overwriting `set`)\n * drops it. PURE — reads only, never mutates.\n */\nexport function get<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): V | undefined {\n const entry = cache.entries[key];\n if (entry === undefined || isExpired(entry, nowMs)) return undefined;\n return entry.value;\n}\n\n/**\n * True iff `key` is present AND unexpired at `nowMs`. The boolean sibling of\n * `get` for callers that only need presence, not the value. PURE.\n */\nexport function has<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): boolean {\n const entry = cache.entries[key];\n if (entry === undefined) return false;\n return !isExpired(entry, nowMs);\n}\n\n/**\n * Write `key → value` with an absolute expiry of `nowMs + ttlMs`. Re-`set`ting\n * an existing key overwrites both the value and the expiry — the TTL clock\n * restarts from the new `nowMs`. PURE — returns a new cache; the input is\n * untouched (a fresh shallow copy of `entries`).\n */\nexport function set<V>(\n cache: TtlCache<V>,\n key: string,\n value: V,\n nowMs: number,\n ttlMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n nextEntries[key] = { value, expiresAtMs: nowMs + ttlMs };\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Drop `key` regardless of expiry. PURE — returns a new cache; the input is\n * untouched. Removing an absent key returns an equivalent cache (the shallow\n * copy still happens, but no entry is dropped).\n */\nexport function remove<V>(cache: TtlCache<V>, key: string): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n delete nextEntries[key];\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Physically drop every entry expired at `nowMs` (`nowMs >= expiresAtMs`).\n * Returns a NEW cache with the survivors. This is the pure work the eviction\n * Sub's `cache_evict` Msg triggers — `get` / `has` already treat expired\n * entries as absent, so eviction is purely a memory-reclaim pass, not a\n * correctness one. PURE — the input cache is never mutated.\n */\nexport function evictExpired<V>(\n cache: TtlCache<V>,\n nowMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = {};\n for (const [key, entry] of Object.entries(cache.entries)) {\n if (isExpired(entry, nowMs)) continue;\n nextEntries[key] = entry;\n }\n return { ...cache, entries: nextEntries };\n}\n\n// ---------------------------------------------------------------------------\n// Eviction Sub — \"eviction is a Msg\".\n//\n// A periodic timer Sub that dispatches `cache_evict` so the consumer's reducer\n// can call `evictExpired`. It is `fromInterval` specialized to a fixed Msg\n// shape — the interval rides in the Sub's `deps` (which the engine keys the\n// Sub on) and the runner dispatches a `cache_evict` Msg tagged with the cache's\n// name, so a consumer running several caches can route each tick to the right\n// cache by `msg.id`.\n//\n// Mirrors `fromInterval` exactly (factory + SubscribeHandler cell + exported\n// Msg constructor); it does NOT reimplement the timer lifecycle — it composes\n// directly on `fromInterval`, which owns `setInterval` / `clearInterval`. That\n// keeps the timer-handle bookkeeping (and its cleanup) in one place and this\n// module purely declarative.\n//\n// Strengthens invariant 4 (external time is a Sub the engine reconciles) and\n// invariant 9 (named, small Sub surface composed on the shared factory).\n// ---------------------------------------------------------------------------\n\n/**\n * The Msg the eviction Sub dispatches each tick. `id` is the cache's name so a\n * consumer with multiple caches can route the tick to the matching cache. The\n * reducer reads `Date.now()`-free time by stamping `nowMs` itself at the Sub\n * boundary if needed; the canonical reducer cell is\n * `cache_evict: (s) => [{ ...s, cache: evictExpired(s.cache, nowMs) }, []]`.\n */\nexport interface CacheEvictMsg {\n readonly type: \"cache_evict\";\n readonly id: string;\n}\n\n/** Construct a `cache_evict` Msg for the cache identified by `id`. */\nexport function cacheEvictMsg(id: string): CacheEvictMsg {\n return { type: \"cache_evict\", id };\n}\n\n/**\n * The `deps` of an eviction Sub: which cache it ticks for (`name`, echoed as\n * the Msg's `id`) and the tick period (`intervalMs`, which `fromInterval`\n * reads).\n */\nexport type CacheEvictionDeps = {\n readonly name: string;\n readonly intervalMs: number;\n};\n\n/**\n * The running eviction Sub: a `setInterval`-shaped Sub whose `deps` carry the\n * cache's name and tick period. `type: \"cache\"` is the lowercase source-noun\n * Sub convention (canon §2.5 / naming-style).\n */\nexport type CacheEvictionSub = Sub<\"cache\", CacheEvictionDeps>;\n\n/**\n * The `subs` entry for an eviction tick on the cache named `name`, every\n * `everyMs`. It is on for the machine's life:\n *\n * subs: [cacheEvictionSub(\"session-cache\", 30_000)],\n * // run(machine, { subscribe: { cache: cacheEvictionSubscribe } })\n *\n * The engine keys the Sub on its `deps`, so a machine that declares the same\n * name and period keeps one running interval, and a changed period restarts\n * it at the new one.\n */\nexport function cacheEvictionSub(\n name: string,\n everyMs: number,\n): DepKeyedSub<unknown, CacheEvictionSub> {\n return { type: \"cache\", deps: () => ({ name, intervalMs: everyMs }) };\n}\n\n/**\n * The `cache` runner for the eviction Sub. Hand it to `run`:\n *\n * run(machine, { subscribe: { cache: cacheEvictionSubscribe } })\n *\n * Composed directly on `fromInterval` — it owns the `setInterval` /\n * `clearInterval` lifecycle; this runner only pins the Msg the tick dispatches\n * (`cacheEvictMsg(sub.deps.name)`). The cleanup `fromInterval` returns stops\n * the interval when the Sub stops.\n */\nexport const cacheEvictionSubscribe: SubscribeHandler<\n CacheEvictionSub,\n CacheEvictMsg,\n unknown\n> = fromInterval((sub) => cacheEvictMsg(sub.deps.name));\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { typeOf, indexPath, unionKeys, keyPath } from './chunk-HWVW4AXC.js';
|
|
2
|
-
import { replay } from './chunk-
|
|
2
|
+
import { replay } from './chunk-RKVUVALV.js';
|
|
3
3
|
|
|
4
4
|
// src/internal/persistence/recorder/index.ts
|
|
5
5
|
function recorder(runtime, opts) {
|
|
@@ -204,5 +204,5 @@ function firstDivergence(expected, actual, path) {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
export { breadcrumbsFromTrace, deepEqual, parseJSONL, recorder, replayTrace, traceAttachment };
|
|
207
|
-
//# sourceMappingURL=chunk-
|
|
208
|
-
//# sourceMappingURL=chunk-
|
|
207
|
+
//# sourceMappingURL=chunk-TGJLXZAU.js.map
|
|
208
|
+
//# sourceMappingURL=chunk-TGJLXZAU.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/persistence/recorder/index.ts","../src/internal/persistence/trace-replay/index.ts"],"names":[],"mappings":"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","file":"chunk-BRX3243I.js","sourcesContent":["// ---------------------------------------------------------------------------\n// recorder — capture a Runtime's Msg sequence for later replay.\n//\n// `historyTracker` (in ../index) is the exemplar: a BOUNDED, in-memory ring\n// buffer over `runtime.observe`. The recorder is its UNBOUNDED, SERIALIZABLE\n// cousin. Where historyTracker exists to answer \"what were the last N\n// transitions?\", the recorder exists to answer \"reproduce this entire run\n// locally against the current reducer.\"\n//\n// The killer feature this enables: record a prod Msg sequence, dump it as\n// JSONL, ship the file home, and feed it to `../trace-replay`'s `replayTrace`\n// — which folds the SAME msgs through `replay(machine, ...)` and diffs the\n// resulting state against the recorded final state. If they diverge, the\n// reducer changed behavior for that input. Regression-as-a-trace.\n//\n// What `replay` needs to reproduce a run (see `replay` in ../index):\n// - `loaded` — the value `init` was booted with. Captured at the boot\n// observe, where `msg === null` and `state` is the post-init\n// initial state. (We capture the post-init state, not the raw\n// storage value, because replay re-runs `init(loaded, ctx)`\n// itself — and for a faithful re-run we feed it that initial\n// state as `loaded` so `init` returns `[loaded, []]`. This is\n// the rehydrate path, which the substrate guarantees is a pure\n// passthrough; see Invariant 2.)\n// - `msgs` — every non-null msg, in dispatch order.\n// - `ctx` — NOT captured here. Ctx is host wiring (sibling runtimes,\n// clients), supplied fresh at replay time. The trace is the\n// INPUT (loaded + msgs); ctx is the ENVIRONMENT.\n//\n// This module OWNS the `Trace` type. `../trace-replay` imports it.\n// ---------------------------------------------------------------------------\n\nimport type { BootingRuntime } from \"../../../index\";\n\n/**\n * A recorded run, sufficient to reproduce it via `../trace-replay`.\n *\n * The recorder OWNS this type; `../trace-replay` imports it. It is the wire\n * contract between the two halves of the record/replay feature.\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport interface Trace<S, M> {\n /**\n * The initial state captured at the boot observe (the transition where\n * `msg === null`). Fed back to replay as `loaded` so `init(loaded, ctx)`\n * reproduces the exact starting point. `null` only if the recorder attached\n * AFTER boot (it missed the boot observe) — replay then re-runs fresh `init`.\n */\n readonly loaded: S | null;\n /** The ordered, non-null msgs as observed. The replay input sequence. */\n readonly msgs: readonly M[];\n /**\n * The last observed state. The oracle: `replayTrace` deep-compares its\n * recomputed final state against this. Equal ⇒ the reducer still produces\n * the same result for this input.\n */\n readonly finalState: S;\n /**\n * Per-transition `(msg, post-state)` pairs, in dispatch order, for human\n * inspection (e.g. \"show me the state after step 3\"). When present, there\n * is exactly one entry per recorded msg and `state` is the POST-transition\n * state for that msg (so `steps[i].msg === msgs[i]` and `steps.at(-1).state`\n * deep-equals `finalState`). NOT required for replay — replay only needs\n * `loaded` + `msgs` + `finalState`.\n *\n * Present iff the recorder was created with `captureSteps: true`, OR the\n * Trace was re-hydrated from JSONL via {@link parseJSONL} (the JSONL format\n * carries per-step state, so the parser populates `steps` whenever a step\n * line had a non-null state). A trace dumped with `captureSteps: false` has\n * `steps === undefined`.\n */\n readonly steps?: readonly { readonly msg: M; readonly state: S }[];\n}\n\n/** Options for {@link recorder}. */\nexport interface RecorderOptions {\n /**\n * Fraction of msgs to record, in `(0, 1]`. Default `1` (record everything).\n *\n * IMPORTANT — sampling breaks replay fidelity. A sampled trace DROPS msgs,\n * so its `msgs` array is no longer the complete input sequence and\n * `replayTrace` will NOT reproduce `finalState`. Sampling exists for\n * VOLUME STATISTICS (how much traffic, msg-type distribution), not for\n * faithful replay. Use the default `1` for any trace you intend to replay.\n *\n * The boot observe (`msg === null`) is ALWAYS recorded regardless of\n * `sampleRate` — `loaded` and `finalState` are anchors, not samples.\n */\n readonly sampleRate?: number;\n /**\n * When `true`, retain a per-transition `(msg, post-state)` pair in\n * {@link Trace.steps} for EVERY recorded transition. Default `false`.\n *\n * Size vs. fidelity — the precise trade-off:\n *\n * - `captureSteps: false` (default) — the MINIMAL replay-faithful trace.\n * The recorder keeps `loaded` (boot state), the ordered `msgs`, and\n * `finalState`. That is exactly what `../trace-replay` needs to refold\n * the run and diff the result. Memory is O(msgs) — one msg ref each, no\n * per-step state snapshot. `dump().steps` is `undefined`; in JSONL,\n * interior step lines carry `state: null` (the post-states were never\n * retained) and only the LAST step line carries `finalState`. A trace\n * dumped this way REPLAYS correctly but cannot answer \"what was the\n * state after step 3?\".\n *\n * - `captureSteps: true` — the FULL-fidelity trace. The recorder also\n * snapshots the post-transition state for every msg, so `dump().steps`\n * holds `{ msg, state }` for every transition (post-state) and\n * `toJSONL()` emits a non-null `state` on every step line. Memory is\n * O(msgs × |state|) — a state ref per transition (refs, not deep\n * copies; TEA states are conventionally immutable, so the runtime's\n * post-transition state object is retained by reference). This is the\n * mode for per-step inspection and for a JSONL file that round-trips\n * `steps` exactly via {@link parseJSONL}.\n *\n * Replay fidelity is identical in both modes — `../trace-replay` reads\n * `loaded` + `msgs` + `finalState` only and never touches `steps`. The\n * choice is purely \"do I also want to inspect intermediate states?\".\n */\n readonly captureSteps?: boolean;\n}\n\n/**\n * A live recorder attached to a Runtime. Mirrors `historyTracker`'s\n * stop/snapshot semantics: `dump()` works at any time (including after\n * `stop()`), `stop()` is idempotent and detaches the observer.\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport interface Recorder<S, M> {\n /**\n * Snapshot the recording so far as a {@link Trace}. Returns a fresh object\n * with copied arrays on every call (callers may slice/replay without\n * affecting the recorder's buffers). Entry values are references to the\n * originals (TEA states/msgs are conventionally immutable).\n *\n * Throws only if called before the boot observe has fired AND no msgs have\n * been recorded — i.e. there is no `finalState` yet. In practice callers\n * `await runtime.ready` before `dump()`, after which boot has fired.\n */\n dump(): Trace<S, M>;\n /**\n * Serialize the recording as JSONL (one JSON object per line) — a streamable\n * format that appends cleanly and survives truncation (a partial tail line\n * is the only loss). The first line is the boot header\n * `{ kind: \"boot\", state }`; each subsequent line is\n * `{ kind: \"step\", msg, state }` for one recorded transition.\n *\n * Re-hydrate with {@link parseJSONL}.\n *\n * Round-trip fidelity depends on `captureSteps`:\n * - `captureSteps: true` — every step line carries its non-null\n * post-state, so `parseJSONL(rec.toJSONL())` reconstructs `steps`\n * EXACTLY and deep-equals `rec.dump()` (which also has `steps`). Full\n * per-step fidelity round-trips.\n * - `captureSteps: false` (default) — interior step lines carry\n * `state: null` (those post-states were never retained); only the last\n * step line carries `finalState`. `parseJSONL` then populates `steps`\n * with just the entries that had a non-null state (the final one), so\n * the re-hydrated Trace is NOT deep-equal to `rec.dump()` (which has no\n * `steps`). It is still fully replay-faithful: `loaded`, `msgs`, and\n * `finalState` are intact. Record with `captureSteps: true` if you need\n * interior states to survive the round-trip.\n */\n toJSONL(): string;\n /**\n * Detach the underlying observer. After `stop()`:\n * - no new transitions are recorded;\n * - `dump()` / `toJSONL()` keep working against the buffered contents;\n * - the recorder holds no observer reference into the runtime.\n *\n * Idempotent — subsequent calls are no-ops.\n */\n stop(): void;\n}\n\n/**\n * Attach a recorder to a Runtime. Subscribes via `runtime.observe`, capturing\n * the boot state as `loaded` and every subsequent non-null msg in order.\n *\n * One-line creation, mirroring `historyTracker`:\n *\n * const rec = recorder(runtime);\n * // ... drive the runtime ...\n * await runtime.ready; // boot observe has fired\n * const trace = rec.dump(); // { loaded, msgs, finalState }\n *\n * @param runtime - any `BootingRuntime<S, M>` (a full `Runtime` satisfies it).\n * The recorder uses only `observe`, which is total before boot\n * — so you attach it to the synchronous `run()` handle to\n * capture the boot transition as `loaded`.\n * @param opts - see {@link RecorderOptions}. Default records everything,\n * no step retention.\n */\nexport function recorder<S, M extends { type: string }>(\n runtime: BootingRuntime<S, M>,\n opts?: RecorderOptions,\n): Recorder<S, M> {\n // Clamp sampleRate into (0, 1]. A non-positive or NaN rate would record\n // nothing (and break replay anchors); default it to 1 (record all). Rates\n // above 1 are meaningless — clamp to 1.\n const rawRate = opts?.sampleRate;\n const sampleRate =\n rawRate === undefined || Number.isNaN(rawRate) || rawRate <= 0\n ? 1\n : Math.min(rawRate, 1);\n const captureSteps = opts?.captureSteps ?? false;\n\n // `loaded` is the post-init state captured at the boot observe. `undefined`\n // sentinel = boot not yet seen; resolved to `S | null` in dump() (null when\n // the recorder attached after boot and never observed msg === null).\n let loaded: S | undefined;\n let sawBoot = false;\n // The ordered, recorded msgs. Unbounded (the recorder's whole point vs the\n // bounded historyTracker).\n const msgs: M[] = [];\n // Per-step (msg, state) pairs — only populated when captureSteps is on.\n const steps: { msg: M; state: S }[] = [];\n // The last observed state — boot state until the first step, then advances\n // with every recorded OR dropped transition (finalState must reflect the\n // true latest state even under sampling, so it stays a valid replay oracle\n // for the unsampled case and an honest \"where the run ended\" marker).\n let finalState: S | undefined;\n let stopped = false;\n\n // The boot transition — the post-init initial state the old `observe(msg ===\n // null)` arm captured as `loaded` — now arrives via `onBoot` (#47). Always\n // captured (boot is the replay anchor). It is also the first `finalState`\n // until the first applied transition advances it.\n const unboot = runtime.onBoot((state) => {\n finalState = state;\n loaded = state;\n sawBoot = true;\n });\n\n const unobserve = runtime.observe((msg, state) => {\n // finalState tracks the latest state regardless of sampling — it is the\n // run's endpoint, an anchor, not a sample.\n finalState = state;\n\n // Sampling decision. sampleRate === 1 short-circuits to always-record so\n // the common (faithful) path pays no RNG cost. Math.random() < rate keeps\n // a `rate` fraction in expectation.\n if (sampleRate < 1 && Math.random() >= sampleRate) return;\n\n msgs.push(msg);\n if (captureSteps) steps.push({ msg, state });\n });\n\n return {\n dump(): Trace<S, M> {\n if (finalState === undefined) {\n throw new Error(\n \"tea recorder: dump() called before any transition was observed. \" +\n \"Await `runtime.ready` (boot fires the first observe) before dumping.\",\n );\n }\n return {\n // sawBoot ⇒ `loaded` is the captured boot state; otherwise the\n // recorder attached post-boot and replay must re-run fresh init.\n loaded: sawBoot ? (loaded as S) : null,\n msgs: msgs.slice(),\n finalState,\n // Only attach `steps` when captureSteps was requested — keeps the\n // dumped Trace shape honest about what was retained.\n ...(captureSteps ? { steps: steps.slice() } : {}),\n };\n },\n toJSONL(): string {\n if (finalState === undefined) {\n throw new Error(\n \"tea recorder: toJSONL() called before any transition was observed. \" +\n \"Await `runtime.ready` (boot fires the first observe) before serializing.\",\n );\n }\n // Serialize the live snapshot via the shared Trace→JSONL writer so the\n // recorder and the standalone `traceAttachment` path emit byte-identical\n // wire format. `dump()` resolves the captureSteps logic (steps present\n // iff captureSteps was on); `traceToJSONL` then emits a non-null state\n // on every step line when steps exist, or `null` interior + finalState\n // on the last line when they don't. The replay path never reads step\n // states, so the captureSteps:false null-interiors never affect replay.\n return traceToJSONL(this.dump());\n },\n stop(): void {\n if (stopped) return;\n stopped = true;\n unobserve();\n unboot();\n },\n };\n}\n\n/** One line of the JSONL format produced by {@link Recorder.toJSONL}. */\ntype JsonlLine<S, M> =\n | { readonly kind: \"boot\"; readonly state: S | null }\n | { readonly kind: \"step\"; readonly msg: M; readonly state: S | null };\n\n/**\n * Re-hydrate a {@link Trace} from the JSONL produced by\n * {@link Recorder.toJSONL}. The inverse of `toJSONL`, so\n * `parseJSONL(rec.toJSONL())` deep-equals `rec.dump()`.\n *\n * Parsing rules:\n * - The first non-blank line MUST be the boot header `{ kind: \"boot\", state }`.\n * Its `state` becomes `Trace.loaded`.\n * - Each `{ kind: \"step\", msg, state }` line contributes `msg` to\n * `Trace.msgs` and `(msg, state)` to `Trace.steps`.\n * - `Trace.finalState` is the `state` of the LAST step line, or the boot\n * `state` when there are no steps (a boot-only trace).\n *\n * Blank lines are skipped (tolerant of trailing newlines / hand-edited files).\n * A malformed line, a missing/duplicate boot header, or a step with `null`\n * final state throws — the trace is unusable for replay and silent corruption\n * is worse than a loud parse error (errors are data; surface them).\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport function parseJSONL<S, M>(jsonl: string): Trace<S, M> {\n const rawLines = jsonl.split(\"\\n\");\n let loaded: S | null = null;\n let sawBoot = false;\n const msgs: M[] = [];\n const steps: { msg: M; state: S }[] = [];\n let finalState: S | null = null;\n let finalKnown = false;\n\n for (const [i, rawLine] of rawLines.entries()) {\n const line = rawLine.trim();\n if (line === \"\") continue; // tolerate blank / trailing-newline lines\n\n let parsed: JsonlLine<S, M>;\n try {\n parsed = JSON.parse(line) as JsonlLine<S, M>;\n } catch (err) {\n throw new Error(\n `tea recorder: parseJSONL failed on line ${i + 1}: not valid JSON. ${String(err)}`,\n );\n }\n\n if (parsed.kind === \"boot\") {\n if (sawBoot) {\n throw new Error(\n `tea recorder: parseJSONL saw a second boot header on line ${i + 1}. ` +\n \"A trace has exactly one boot line.\",\n );\n }\n sawBoot = true;\n loaded = parsed.state;\n // The boot state is the finalState until a step supersedes it (boot-only\n // trace case).\n finalState = parsed.state;\n finalKnown = parsed.state !== null;\n } else if (parsed.kind === \"step\") {\n if (!sawBoot) {\n throw new Error(\n `tea recorder: parseJSONL saw a step line before the boot header on line ${i + 1}.`,\n );\n }\n msgs.push(parsed.msg);\n if (parsed.state !== null) {\n steps.push({ msg: parsed.msg, state: parsed.state });\n finalState = parsed.state;\n finalKnown = true;\n }\n } else {\n throw new Error(\n `tea recorder: parseJSONL saw an unknown line kind on line ${i + 1}: ` +\n JSON.stringify((parsed as { kind?: unknown }).kind),\n );\n }\n }\n\n if (!sawBoot) {\n throw new Error(\n \"tea recorder: parseJSONL found no boot header. A trace must start with \" +\n '{ kind: \"boot\", state }.',\n );\n }\n if (!finalKnown || finalState === null) {\n throw new Error(\n \"tea recorder: parseJSONL could not determine finalState — the boot state \" +\n \"was null and no step carried a non-null state. The trace is unusable for replay.\",\n );\n }\n\n return { loaded, msgs, finalState, steps };\n}\n\n// ---------------------------------------------------------------------------\n// Sentry crash→trace adapters — pure, zero-dependency, Sentry-SHAPED.\n//\n// The killer DevX flow: a machine crashes in prod → its recorded Trace rides\n// along on the Sentry event → one click later you `parseJSONL` it and replay\n// the exact run locally against the (possibly fixed) reducer. These two\n// adapters are the bridge from a `Trace` to the two Sentry surfaces that carry\n// it: an Attachment (the full replayable trace) and Breadcrumbs (a human-\n// readable timeline of the msgs that led to the crash).\n//\n// They take NO dependency on `@sentry/*`. They emit PLAIN OBJECTS whose shapes\n// match Sentry's public `Attachment` and `Breadcrumb` types, and the consumer\n// wires them at their own Sentry boundary (browser or node). This keeps the\n// recorder runnable anywhere a machine runs (a Worker, a Node script, a test)\n// and lets the consumer pin whatever Sentry SDK version they already ship.\n// ---------------------------------------------------------------------------\n\n/**\n * A Sentry-shaped attachment carrying the full {@link Trace} as JSONL. The\n * shape matches Sentry's `Attachment` type so the consumer can hand it\n * straight to `addAttachment(...)` without any adaptation.\n *\n * - `filename` — `\"tea-trace.jsonl\"`. The name Sentry shows in the UI and\n * uses for download; `.jsonl` signals the streamable format.\n * - `data` — the trace serialized via {@link Recorder.toJSONL}'s format\n * (re-hydrate downstream with {@link parseJSONL}).\n * - `contentType` — `\"application/x-ndjson\"`, the standard MIME for\n * newline-delimited JSON.\n */\nexport interface TraceAttachment {\n readonly filename: string;\n readonly data: string;\n readonly contentType: string;\n}\n\n/**\n * Serialize a {@link Trace} into a Sentry-shaped attachment so a crash event\n * carries the full, replayable run. Pure — no Sentry import, no I/O. The\n * returned object matches Sentry's `Attachment` shape exactly.\n *\n * The `data` field is JSONL in the same format {@link Recorder.toJSONL}\n * produces, so the home-side flow is `parseJSONL(attachment.data)` →\n * `replayTrace(machine, trace, ctx)`.\n *\n * Consumer wiring (no Sentry import here — the consumer owns the boundary):\n *\n * ```ts\n * import * as Sentry from \"@sentry/browser\"; // or \"@sentry/node\"\n * import { traceAttachment } from \"./internal/persistence/recorder\"; // internal module\n * Sentry.getCurrentScope().addAttachment(traceAttachment(rec.dump()));\n * ```\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport function traceAttachment<S, M extends { type: string }>(\n trace: Trace<S, M>,\n): TraceAttachment {\n return {\n filename: \"tea-trace.jsonl\",\n data: traceToJSONL(trace),\n contentType: \"application/x-ndjson\",\n };\n}\n\n/** A Sentry-shaped breadcrumb. Matches the fields of Sentry's `Breadcrumb`. */\nexport interface TraceBreadcrumb {\n readonly category: string;\n readonly type: string;\n readonly level: string;\n readonly message: string;\n readonly data?: Record<string, unknown>;\n}\n\n/** Options for {@link breadcrumbsFromTrace}. */\nexport interface BreadcrumbsOptions<M> {\n /**\n * Max breadcrumbs to emit. Default `100` (Sentry's own default breadcrumb\n * cap). When the trace has more msgs than `limit`, the OLDEST are dropped —\n * the returned list is the newest-last tail, matching how a Sentry\n * breadcrumb ring buffer behaves (the crash context is the recent past).\n */\n readonly limit?: number;\n /**\n * Customize each breadcrumb's `message`. Default uses `msg.type`. Provide a\n * serializer to surface a richer one-liner (e.g. include an id). The msg is\n * ALSO always attached verbatim under `data.msg` regardless of this option,\n * so the structured payload is never lost to the string rendering.\n */\n readonly serialize?: (msg: M) => string;\n}\n\n/**\n * Project a {@link Trace}'s recorded msgs into Sentry-shaped breadcrumbs — a\n * human-readable timeline of \"what happened before the crash\". Pure — no\n * Sentry import, no I/O. Each returned object matches Sentry's `Breadcrumb`\n * shape, so the consumer can loop and call `addBreadcrumb(...)`.\n *\n * One breadcrumb per recorded msg, in dispatch order (NEWEST LAST — Sentry\n * renders breadcrumbs oldest-first and the crash is the most recent event).\n * Each breadcrumb:\n * - `category` — `\"tea\"` (groups them in the Sentry breadcrumb panel).\n * - `type` — `\"default\"` (the generic Sentry breadcrumb type).\n * - `level` — `\"info\"` (msgs are normal flow; the crash is the event).\n * - `message` — `msg.type`, or `serialize(msg)` when provided.\n * - `data.msg` — the full msg object (structured detail kept verbatim).\n *\n * Capped at `opts.limit` (default 100). When the trace has more msgs than the\n * cap, the OLDEST are dropped and the newest `limit` are kept — the recent\n * lead-up to the crash is the useful context.\n *\n * Consumer wiring (no Sentry import here — the consumer owns the boundary):\n *\n * ```ts\n * import * as Sentry from \"@sentry/browser\"; // or \"@sentry/node\"\n * import { breadcrumbsFromTrace } from \"./internal/persistence/recorder\"; // internal module\n * for (const b of breadcrumbsFromTrace(rec.dump())) Sentry.addBreadcrumb(b);\n * ```\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport function breadcrumbsFromTrace<S, M extends { type: string }>(\n trace: Trace<S, M>,\n opts?: BreadcrumbsOptions<M>,\n): TraceBreadcrumb[] {\n const rawLimit = opts?.limit;\n // Clamp limit to a non-negative integer. `undefined`/NaN → 100 (Sentry's\n // default). A negative or zero limit yields an empty list (the caller asked\n // for no breadcrumbs); fractional values floor (you can't emit half a crumb).\n const limit =\n rawLimit === undefined || Number.isNaN(rawLimit)\n ? 100\n : Math.max(0, Math.floor(rawLimit));\n const serialize = opts?.serialize;\n\n // Keep the NEWEST `limit` msgs (the recent lead-up to the crash). Slicing\n // from `length - limit` (clamped to 0) preserves dispatch order within the\n // kept window. Note we do NOT use `slice(-limit)`: `slice(-0)` is `slice(0)`\n // (JS treats -0 as 0), which would return the WHOLE array for `limit: 0`.\n const start = Math.max(0, trace.msgs.length - limit);\n const kept = start === 0 ? trace.msgs : trace.msgs.slice(start);\n\n const crumbs: TraceBreadcrumb[] = [];\n for (const msg of kept) {\n crumbs.push({\n category: \"tea\",\n type: \"default\",\n level: \"info\",\n message: serialize ? serialize(msg) : msg.type,\n // Full msg kept verbatim so the structured payload survives even when\n // `message` is a terse one-liner. Cast: a Msg is a `{ type: string }`\n // record, which is a `Record<string, unknown>` for Sentry's `data`.\n data: { msg: msg as unknown as Record<string, unknown> },\n });\n }\n return crumbs;\n}\n\n// ---------------------------------------------------------------------------\n// traceToJSONL — serialize a STANDALONE Trace to JSONL (the dump→file path).\n//\n// `Recorder.toJSONL()` serializes the recorder's live buffers. `traceAttachment`\n// works over a plain `Trace` value (a `dump()` snapshot or a `parseJSONL`\n// result), which has no recorder behind it — so it needs a Trace→JSONL\n// function. This is the same wire format `Recorder.toJSONL` emits and\n// `parseJSONL` reads, so `parseJSONL(traceToJSONL(trace))` round-trips.\n//\n// When `trace.steps` is present (captureSteps was on, or the trace came from\n// parseJSONL), every step line carries its post-state. When absent, interior\n// step states are unknown — emitted as `null`, with `finalState` on the LAST\n// step line (identical to the captureSteps:false path in `Recorder.toJSONL`).\n// ---------------------------------------------------------------------------\nfunction traceToJSONL<S, M>(trace: Trace<S, M>): string {\n const lines: string[] = [];\n lines.push(JSON.stringify({ kind: \"boot\", state: trace.loaded }));\n\n const steps = trace.steps;\n for (let i = 0; i < trace.msgs.length; i++) {\n const msg = trace.msgs[i];\n const stepState = steps\n ? steps[i]?.state\n : i === trace.msgs.length - 1\n ? trace.finalState\n : null;\n lines.push(JSON.stringify({ kind: \"step\", msg, state: stepState }));\n }\n return lines.join(\"\\n\");\n}\n","// ---------------------------------------------------------------------------\n// trace-replay — replay a recorded Trace against a machine and diff the state.\n//\n// This is the back half of the record/replay feature. `../recorder` captures\n// a run (`loaded` + `msgs` + `finalState`); this module feeds that input\n// through the core `replay(machine, { msgs, ctx, loaded })` fold and asks:\n//\n// does the current reducer still produce the SAME final state for this\n// recorded input?\n//\n// Equal ⇒ the reducer is behavior-compatible with the recording. Diverged ⇒\n// the reducer changed for that input, and the divergence path\n// (`state.queue[2].status`) points at exactly the cell that drifted. This is\n// the regression / production-repro tool: record a prod incident, replay it\n// against the fixed reducer locally, confirm the fix changes the state at the\n// path you expected — or catch an unintended drift before it ships.\n//\n// Standalone by design. It imports ONLY the `Trace` type from `../recorder`\n// and the `replay` engine from `../index`. The deep-equal + first-divergence\n// path finder is self-contained (no external diff lib) — a richer pretty-diff\n// lands in `../devtools` later; trace-replay stays dependency-free so it can\n// run anywhere a machine runs (CI, a Worker, a Node script).\n// ---------------------------------------------------------------------------\n\nimport { type Cmd, type Machine, replay, type Sub } from \"../../../index\";\nimport type { Trace } from \"../recorder\";\nimport { indexPath, keyPath, typeOf, unionKeys } from \"./state-walk\";\n\n/**\n * The outcome of {@link replayTrace}.\n *\n * @typeParam S - the machine's State type.\n */\nexport interface ReplayResult<S> {\n /** `true` iff the recomputed final state deep-equals the trace's `finalState`. */\n matches: boolean;\n /** The trace's recorded `finalState` — what the run originally produced. */\n expected: S;\n /** The state the current reducer produced for the same input. */\n actual: S;\n /**\n * Present iff `matches` is `false`. The FIRST point of divergence found by\n * a deterministic pre-order walk:\n * - `path` — a dotted/indexed access path from the state root, e.g.\n * `state.queue[2].status` or `state` for a root-level\n * scalar mismatch.\n * - `expected` — the recorded value at that path.\n * - `actual` — the recomputed value at that path.\n */\n divergence?: { path: string; expected: unknown; actual: unknown };\n}\n\n/**\n * Replay a recorded {@link Trace} against `machine` and diff the recomputed\n * final state against the recorded one.\n *\n * Runs the core pure fold `replay(machine, { msgs: trace.msgs, ctx,\n * loaded: trace.loaded })` — init + update only, never `interpret`, never a\n * `Store`, never a live subscription (see `replay` in ../index). The `ctx` is\n * supplied fresh by the caller: the trace is the INPUT (loaded + msgs); ctx is\n * the ENVIRONMENT the reducer runs against now.\n *\n * @param machine - the machine to replay against. Mutating its reducer (the\n * whole point of a regression check) is what makes a recorded\n * trace diverge.\n * @param trace - a Trace from `../recorder` (live `dump()` or a re-hydrated\n * `parseJSONL(...)`).\n * @param ctx - the runtime ctx, supplied fresh at replay time.\n */\nexport function replayTrace<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n trace: Trace<S, M>,\n ctx: Ctx,\n): ReplayResult<S> {\n const result = replay(machine, {\n msgs: trace.msgs,\n ctx,\n loaded: trace.loaded,\n });\n const actual = result.state;\n const expected = trace.finalState;\n\n const divergence = firstDivergence(expected, actual, \"state\");\n if (divergence === null) {\n return { matches: true, expected, actual };\n }\n return { matches: false, expected, actual, divergence };\n}\n\n/**\n * Order-insensitive structural deep-equality — the comparator the\n * record/replay lane owns, exposed so consumers (e.g. `@demlik/tea/parity`'s\n * `parityEqual`) reuse it rather than adding a third deep-compare. It is one\n * of two structural walkers in the package, divergent on purpose:\n * this `deepEqual` returns a boolean verdict (order-insensitive, for\n * replay/parity equality), and `devtools`'s `diffState` returns a change list\n * (for a human-readable diff view) — same shape, two jobs, not one to collapse.\n * `true` iff `a` and `b` are structurally equal under {@link firstDivergence}'s\n * walk (object keys compared order-insensitively, arrays index-by-index).\n */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n return firstDivergence(a, b, \"state\") === null;\n}\n\n// === deep-equal + first-divergence path finder ===\n//\n// One walk does both jobs: it returns the FIRST point where `expected` and\n// `actual` differ (as a path + the two values), or `null` when they are\n// deeply equal. \"First\" is defined by a deterministic pre-order traversal:\n// the node itself, then its children in a stable key order (array index\n// order; object keys sorted so the result does not depend on insertion\n// order — two states with the same data but different key order are equal,\n// and the reported path for a real mismatch is reproducible run-to-run).\n//\n// The path grammar (`typeOf`, `indexPath`/`keyPath` segments, `unionKeys`\n// order) is shared with devtools' `diffState` via `./state-walk`, so the two\n// walkers print the SAME path for the same cell by construction. Only the leaf\n// policy differs, on purpose: first-hit here, collect-all there.\n//\n// Scope: TEA state is plain JSON-shaped data (the substrate's __DEV__ guards\n// reject closures in Cmds, and states are conventionally serializable). So\n// the walk handles: primitives, null/undefined, arrays, and plain objects.\n// It treats other object kinds (Date, Map, Set, class instances) by reference\n// identity + a structural fallback via Object.entries — sufficient for the\n// JSON-shaped states this tool targets; a richer differ lives in ../devtools.\n\n/** A divergence record, or `null` when the two values are deeply equal. */\ntype Divergence = { path: string; expected: unknown; actual: unknown } | null;\n\nfunction firstDivergence(\n expected: unknown,\n actual: unknown,\n path: string,\n): Divergence {\n // Identity / primitive fast path. Handles same-reference, and equal\n // primitives. `Object.is` so NaN === NaN and +0 ≠ -0 (matches structural\n // equality intent better than ===).\n if (Object.is(expected, actual)) return null;\n\n const te = typeOf(expected);\n const ta = typeOf(actual);\n // Different kinds at this node (e.g. object vs array, number vs string,\n // present vs null) — this node IS the divergence.\n if (te !== ta) return { path, expected, actual };\n\n if (te === \"array\") {\n const ae = expected as unknown[];\n const aa = actual as unknown[];\n if (ae.length !== aa.length) {\n // Length mismatch reported at the array node itself — the shape differs,\n // which is more informative than an arbitrary element index.\n return { path, expected, actual };\n }\n for (let i = 0; i < ae.length; i++) {\n const d = firstDivergence(ae[i], aa[i], indexPath(path, i));\n if (d !== null) return d;\n }\n return null;\n }\n\n if (te === \"object\") {\n const oe = expected as Record<string, unknown>;\n const oa = actual as Record<string, unknown>;\n // Sorted union of keys for deterministic traversal. A key present in one\n // but absent in the other diverges at that child path (its value is\n // `undefined` on the missing side).\n for (const key of unionKeys(oe, oa)) {\n const d = firstDivergence(oe[key], oa[key], keyPath(path, key));\n if (d !== null) return d;\n }\n return null;\n }\n\n // Primitives (string/number/boolean/bigint/symbol/function) that failed the\n // Object.is fast path, or null/undefined that differ — this node diverges.\n return { path, expected, actual };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/persistence/recorder/index.ts","../src/internal/persistence/trace-replay/index.ts"],"names":[],"mappings":"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","file":"chunk-TGJLXZAU.js","sourcesContent":["// ---------------------------------------------------------------------------\n// recorder — capture a Runtime's Msg sequence for later replay.\n//\n// `historyTracker` (in ../index) is the exemplar: a BOUNDED, in-memory ring\n// buffer over `runtime.observe`. The recorder is its UNBOUNDED, SERIALIZABLE\n// cousin. Where historyTracker exists to answer \"what were the last N\n// transitions?\", the recorder exists to answer \"reproduce this entire run\n// locally against the current reducer.\"\n//\n// The killer feature this enables: record a prod Msg sequence, dump it as\n// JSONL, ship the file home, and feed it to `../trace-replay`'s `replayTrace`\n// — which folds the SAME msgs through `replay(machine, ...)` and diffs the\n// resulting state against the recorded final state. If they diverge, the\n// reducer changed behavior for that input. Regression-as-a-trace.\n//\n// What `replay` needs to reproduce a run (see `replay` in ../index):\n// - `loaded` — the value `init` was booted with. Captured at the boot\n// observe, where `msg === null` and `state` is the post-init\n// initial state. (We capture the post-init state, not the raw\n// storage value, because replay re-runs `init(loaded, ctx)`\n// itself — and for a faithful re-run we feed it that initial\n// state as `loaded` so `init` returns `[loaded, []]`. This is\n// the rehydrate path, which the substrate guarantees is a pure\n// passthrough; see Invariant 2.)\n// - `msgs` — every non-null msg, in dispatch order.\n// - `ctx` — NOT captured here. Ctx is host wiring (sibling runtimes,\n// clients), supplied fresh at replay time. The trace is the\n// INPUT (loaded + msgs); ctx is the ENVIRONMENT.\n//\n// This module OWNS the `Trace` type. `../trace-replay` imports it.\n// ---------------------------------------------------------------------------\n\nimport type { BootingRuntime } from \"../../../index\";\n\n/**\n * A recorded run, sufficient to reproduce it via `../trace-replay`.\n *\n * The recorder OWNS this type; `../trace-replay` imports it. It is the wire\n * contract between the two halves of the record/replay feature.\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport interface Trace<S, M> {\n /**\n * The initial state captured at the boot observe (the transition where\n * `msg === null`). Fed back to replay as `loaded` so `init(loaded, ctx)`\n * reproduces the exact starting point. `null` only if the recorder attached\n * AFTER boot (it missed the boot observe) — replay then re-runs fresh `init`.\n */\n readonly loaded: S | null;\n /** The ordered, non-null msgs as observed. The replay input sequence. */\n readonly msgs: readonly M[];\n /**\n * The last observed state. The oracle: `replayTrace` deep-compares its\n * recomputed final state against this. Equal ⇒ the reducer still produces\n * the same result for this input.\n */\n readonly finalState: S;\n /**\n * Per-transition `(msg, post-state)` pairs, in dispatch order, for human\n * inspection (e.g. \"show me the state after step 3\"). When present, there\n * is exactly one entry per recorded msg and `state` is the POST-transition\n * state for that msg (so `steps[i].msg === msgs[i]` and `steps.at(-1).state`\n * deep-equals `finalState`). NOT required for replay — replay only needs\n * `loaded` + `msgs` + `finalState`.\n *\n * Present iff the recorder was created with `captureSteps: true`, OR the\n * Trace was re-hydrated from JSONL via {@link parseJSONL} (the JSONL format\n * carries per-step state, so the parser populates `steps` whenever a step\n * line had a non-null state). A trace dumped with `captureSteps: false` has\n * `steps === undefined`.\n */\n readonly steps?: readonly { readonly msg: M; readonly state: S }[];\n}\n\n/** Options for {@link recorder}. */\nexport interface RecorderOptions {\n /**\n * Fraction of msgs to record, in `(0, 1]`. Default `1` (record everything).\n *\n * IMPORTANT — sampling breaks replay fidelity. A sampled trace DROPS msgs,\n * so its `msgs` array is no longer the complete input sequence and\n * `replayTrace` will NOT reproduce `finalState`. Sampling exists for\n * VOLUME STATISTICS (how much traffic, msg-type distribution), not for\n * faithful replay. Use the default `1` for any trace you intend to replay.\n *\n * The boot observe (`msg === null`) is ALWAYS recorded regardless of\n * `sampleRate` — `loaded` and `finalState` are anchors, not samples.\n */\n readonly sampleRate?: number;\n /**\n * When `true`, retain a per-transition `(msg, post-state)` pair in\n * {@link Trace.steps} for EVERY recorded transition. Default `false`.\n *\n * Size vs. fidelity — the precise trade-off:\n *\n * - `captureSteps: false` (default) — the MINIMAL replay-faithful trace.\n * The recorder keeps `loaded` (boot state), the ordered `msgs`, and\n * `finalState`. That is exactly what `../trace-replay` needs to refold\n * the run and diff the result. Memory is O(msgs) — one msg ref each, no\n * per-step state snapshot. `dump().steps` is `undefined`; in JSONL,\n * interior step lines carry `state: null` (the post-states were never\n * retained) and only the LAST step line carries `finalState`. A trace\n * dumped this way REPLAYS correctly but cannot answer \"what was the\n * state after step 3?\".\n *\n * - `captureSteps: true` — the FULL-fidelity trace. The recorder also\n * snapshots the post-transition state for every msg, so `dump().steps`\n * holds `{ msg, state }` for every transition (post-state) and\n * `toJSONL()` emits a non-null `state` on every step line. Memory is\n * O(msgs × |state|) — a state ref per transition (refs, not deep\n * copies; TEA states are conventionally immutable, so the runtime's\n * post-transition state object is retained by reference). This is the\n * mode for per-step inspection and for a JSONL file that round-trips\n * `steps` exactly via {@link parseJSONL}.\n *\n * Replay fidelity is identical in both modes — `../trace-replay` reads\n * `loaded` + `msgs` + `finalState` only and never touches `steps`. The\n * choice is purely \"do I also want to inspect intermediate states?\".\n */\n readonly captureSteps?: boolean;\n}\n\n/**\n * A live recorder attached to a Runtime. Mirrors `historyTracker`'s\n * stop/snapshot semantics: `dump()` works at any time (including after\n * `stop()`), `stop()` is idempotent and detaches the observer.\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport interface Recorder<S, M> {\n /**\n * Snapshot the recording so far as a {@link Trace}. Returns a fresh object\n * with copied arrays on every call (callers may slice/replay without\n * affecting the recorder's buffers). Entry values are references to the\n * originals (TEA states/msgs are conventionally immutable).\n *\n * Throws only if called before the boot observe has fired AND no msgs have\n * been recorded — i.e. there is no `finalState` yet. In practice callers\n * `await runtime.ready` before `dump()`, after which boot has fired.\n */\n dump(): Trace<S, M>;\n /**\n * Serialize the recording as JSONL (one JSON object per line) — a streamable\n * format that appends cleanly and survives truncation (a partial tail line\n * is the only loss). The first line is the boot header\n * `{ kind: \"boot\", state }`; each subsequent line is\n * `{ kind: \"step\", msg, state }` for one recorded transition.\n *\n * Re-hydrate with {@link parseJSONL}.\n *\n * Round-trip fidelity depends on `captureSteps`:\n * - `captureSteps: true` — every step line carries its non-null\n * post-state, so `parseJSONL(rec.toJSONL())` reconstructs `steps`\n * EXACTLY and deep-equals `rec.dump()` (which also has `steps`). Full\n * per-step fidelity round-trips.\n * - `captureSteps: false` (default) — interior step lines carry\n * `state: null` (those post-states were never retained); only the last\n * step line carries `finalState`. `parseJSONL` then populates `steps`\n * with just the entries that had a non-null state (the final one), so\n * the re-hydrated Trace is NOT deep-equal to `rec.dump()` (which has no\n * `steps`). It is still fully replay-faithful: `loaded`, `msgs`, and\n * `finalState` are intact. Record with `captureSteps: true` if you need\n * interior states to survive the round-trip.\n */\n toJSONL(): string;\n /**\n * Detach the underlying observer. After `stop()`:\n * - no new transitions are recorded;\n * - `dump()` / `toJSONL()` keep working against the buffered contents;\n * - the recorder holds no observer reference into the runtime.\n *\n * Idempotent — subsequent calls are no-ops.\n */\n stop(): void;\n}\n\n/**\n * Attach a recorder to a Runtime. Subscribes via `runtime.observe`, capturing\n * the boot state as `loaded` and every subsequent non-null msg in order.\n *\n * One-line creation, mirroring `historyTracker`:\n *\n * const rec = recorder(runtime);\n * // ... drive the runtime ...\n * await runtime.ready; // boot observe has fired\n * const trace = rec.dump(); // { loaded, msgs, finalState }\n *\n * @param runtime - any `BootingRuntime<S, M>` (a full `Runtime` satisfies it).\n * The recorder uses only `observe`, which is total before boot\n * — so you attach it to the synchronous `run()` handle to\n * capture the boot transition as `loaded`.\n * @param opts - see {@link RecorderOptions}. Default records everything,\n * no step retention.\n */\nexport function recorder<S, M extends { type: string }>(\n runtime: BootingRuntime<S, M>,\n opts?: RecorderOptions,\n): Recorder<S, M> {\n // Clamp sampleRate into (0, 1]. A non-positive or NaN rate would record\n // nothing (and break replay anchors); default it to 1 (record all). Rates\n // above 1 are meaningless — clamp to 1.\n const rawRate = opts?.sampleRate;\n const sampleRate =\n rawRate === undefined || Number.isNaN(rawRate) || rawRate <= 0\n ? 1\n : Math.min(rawRate, 1);\n const captureSteps = opts?.captureSteps ?? false;\n\n // `loaded` is the post-init state captured at the boot observe. `undefined`\n // sentinel = boot not yet seen; resolved to `S | null` in dump() (null when\n // the recorder attached after boot and never observed msg === null).\n let loaded: S | undefined;\n let sawBoot = false;\n // The ordered, recorded msgs. Unbounded (the recorder's whole point vs the\n // bounded historyTracker).\n const msgs: M[] = [];\n // Per-step (msg, state) pairs — only populated when captureSteps is on.\n const steps: { msg: M; state: S }[] = [];\n // The last observed state — boot state until the first step, then advances\n // with every recorded OR dropped transition (finalState must reflect the\n // true latest state even under sampling, so it stays a valid replay oracle\n // for the unsampled case and an honest \"where the run ended\" marker).\n let finalState: S | undefined;\n let stopped = false;\n\n // The boot transition — the post-init initial state the old `observe(msg ===\n // null)` arm captured as `loaded` — now arrives via `onBoot` (#47). Always\n // captured (boot is the replay anchor). It is also the first `finalState`\n // until the first applied transition advances it.\n const unboot = runtime.onBoot((state) => {\n finalState = state;\n loaded = state;\n sawBoot = true;\n });\n\n const unobserve = runtime.observe((msg, state) => {\n // finalState tracks the latest state regardless of sampling — it is the\n // run's endpoint, an anchor, not a sample.\n finalState = state;\n\n // Sampling decision. sampleRate === 1 short-circuits to always-record so\n // the common (faithful) path pays no RNG cost. Math.random() < rate keeps\n // a `rate` fraction in expectation.\n if (sampleRate < 1 && Math.random() >= sampleRate) return;\n\n msgs.push(msg);\n if (captureSteps) steps.push({ msg, state });\n });\n\n return {\n dump(): Trace<S, M> {\n if (finalState === undefined) {\n throw new Error(\n \"tea recorder: dump() called before any transition was observed. \" +\n \"Await `runtime.ready` (boot fires the first observe) before dumping.\",\n );\n }\n return {\n // sawBoot ⇒ `loaded` is the captured boot state; otherwise the\n // recorder attached post-boot and replay must re-run fresh init.\n loaded: sawBoot ? (loaded as S) : null,\n msgs: msgs.slice(),\n finalState,\n // Only attach `steps` when captureSteps was requested — keeps the\n // dumped Trace shape honest about what was retained.\n ...(captureSteps ? { steps: steps.slice() } : {}),\n };\n },\n toJSONL(): string {\n if (finalState === undefined) {\n throw new Error(\n \"tea recorder: toJSONL() called before any transition was observed. \" +\n \"Await `runtime.ready` (boot fires the first observe) before serializing.\",\n );\n }\n // Serialize the live snapshot via the shared Trace→JSONL writer so the\n // recorder and the standalone `traceAttachment` path emit byte-identical\n // wire format. `dump()` resolves the captureSteps logic (steps present\n // iff captureSteps was on); `traceToJSONL` then emits a non-null state\n // on every step line when steps exist, or `null` interior + finalState\n // on the last line when they don't. The replay path never reads step\n // states, so the captureSteps:false null-interiors never affect replay.\n return traceToJSONL(this.dump());\n },\n stop(): void {\n if (stopped) return;\n stopped = true;\n unobserve();\n unboot();\n },\n };\n}\n\n/** One line of the JSONL format produced by {@link Recorder.toJSONL}. */\ntype JsonlLine<S, M> =\n | { readonly kind: \"boot\"; readonly state: S | null }\n | { readonly kind: \"step\"; readonly msg: M; readonly state: S | null };\n\n/**\n * Re-hydrate a {@link Trace} from the JSONL produced by\n * {@link Recorder.toJSONL}. The inverse of `toJSONL`, so\n * `parseJSONL(rec.toJSONL())` deep-equals `rec.dump()`.\n *\n * Parsing rules:\n * - The first non-blank line MUST be the boot header `{ kind: \"boot\", state }`.\n * Its `state` becomes `Trace.loaded`.\n * - Each `{ kind: \"step\", msg, state }` line contributes `msg` to\n * `Trace.msgs` and `(msg, state)` to `Trace.steps`.\n * - `Trace.finalState` is the `state` of the LAST step line, or the boot\n * `state` when there are no steps (a boot-only trace).\n *\n * Blank lines are skipped (tolerant of trailing newlines / hand-edited files).\n * A malformed line, a missing/duplicate boot header, or a step with `null`\n * final state throws — the trace is unusable for replay and silent corruption\n * is worse than a loud parse error (errors are data; surface them).\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport function parseJSONL<S, M>(jsonl: string): Trace<S, M> {\n const rawLines = jsonl.split(\"\\n\");\n let loaded: S | null = null;\n let sawBoot = false;\n const msgs: M[] = [];\n const steps: { msg: M; state: S }[] = [];\n let finalState: S | null = null;\n let finalKnown = false;\n\n for (const [i, rawLine] of rawLines.entries()) {\n const line = rawLine.trim();\n if (line === \"\") continue; // tolerate blank / trailing-newline lines\n\n let parsed: JsonlLine<S, M>;\n try {\n parsed = JSON.parse(line) as JsonlLine<S, M>;\n } catch (err) {\n throw new Error(\n `tea recorder: parseJSONL failed on line ${i + 1}: not valid JSON. ${String(err)}`,\n );\n }\n\n if (parsed.kind === \"boot\") {\n if (sawBoot) {\n throw new Error(\n `tea recorder: parseJSONL saw a second boot header on line ${i + 1}. ` +\n \"A trace has exactly one boot line.\",\n );\n }\n sawBoot = true;\n loaded = parsed.state;\n // The boot state is the finalState until a step supersedes it (boot-only\n // trace case).\n finalState = parsed.state;\n finalKnown = parsed.state !== null;\n } else if (parsed.kind === \"step\") {\n if (!sawBoot) {\n throw new Error(\n `tea recorder: parseJSONL saw a step line before the boot header on line ${i + 1}.`,\n );\n }\n msgs.push(parsed.msg);\n if (parsed.state !== null) {\n steps.push({ msg: parsed.msg, state: parsed.state });\n finalState = parsed.state;\n finalKnown = true;\n }\n } else {\n throw new Error(\n `tea recorder: parseJSONL saw an unknown line kind on line ${i + 1}: ` +\n JSON.stringify((parsed as { kind?: unknown }).kind),\n );\n }\n }\n\n if (!sawBoot) {\n throw new Error(\n \"tea recorder: parseJSONL found no boot header. A trace must start with \" +\n '{ kind: \"boot\", state }.',\n );\n }\n if (!finalKnown || finalState === null) {\n throw new Error(\n \"tea recorder: parseJSONL could not determine finalState — the boot state \" +\n \"was null and no step carried a non-null state. The trace is unusable for replay.\",\n );\n }\n\n return { loaded, msgs, finalState, steps };\n}\n\n// ---------------------------------------------------------------------------\n// Sentry crash→trace adapters — pure, zero-dependency, Sentry-SHAPED.\n//\n// The killer DevX flow: a machine crashes in prod → its recorded Trace rides\n// along on the Sentry event → one click later you `parseJSONL` it and replay\n// the exact run locally against the (possibly fixed) reducer. These two\n// adapters are the bridge from a `Trace` to the two Sentry surfaces that carry\n// it: an Attachment (the full replayable trace) and Breadcrumbs (a human-\n// readable timeline of the msgs that led to the crash).\n//\n// They take NO dependency on `@sentry/*`. They emit PLAIN OBJECTS whose shapes\n// match Sentry's public `Attachment` and `Breadcrumb` types, and the consumer\n// wires them at their own Sentry boundary (browser or node). This keeps the\n// recorder runnable anywhere a machine runs (a Worker, a Node script, a test)\n// and lets the consumer pin whatever Sentry SDK version they already ship.\n// ---------------------------------------------------------------------------\n\n/**\n * A Sentry-shaped attachment carrying the full {@link Trace} as JSONL. The\n * shape matches Sentry's `Attachment` type so the consumer can hand it\n * straight to `addAttachment(...)` without any adaptation.\n *\n * - `filename` — `\"tea-trace.jsonl\"`. The name Sentry shows in the UI and\n * uses for download; `.jsonl` signals the streamable format.\n * - `data` — the trace serialized via {@link Recorder.toJSONL}'s format\n * (re-hydrate downstream with {@link parseJSONL}).\n * - `contentType` — `\"application/x-ndjson\"`, the standard MIME for\n * newline-delimited JSON.\n */\nexport interface TraceAttachment {\n readonly filename: string;\n readonly data: string;\n readonly contentType: string;\n}\n\n/**\n * Serialize a {@link Trace} into a Sentry-shaped attachment so a crash event\n * carries the full, replayable run. Pure — no Sentry import, no I/O. The\n * returned object matches Sentry's `Attachment` shape exactly.\n *\n * The `data` field is JSONL in the same format {@link Recorder.toJSONL}\n * produces, so the home-side flow is `parseJSONL(attachment.data)` →\n * `replayTrace(machine, trace, ctx)`.\n *\n * Consumer wiring (no Sentry import here — the consumer owns the boundary):\n *\n * ```ts\n * import * as Sentry from \"@sentry/browser\"; // or \"@sentry/node\"\n * import { traceAttachment } from \"./internal/persistence/recorder\"; // internal module\n * Sentry.getCurrentScope().addAttachment(traceAttachment(rec.dump()));\n * ```\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport function traceAttachment<S, M extends { type: string }>(\n trace: Trace<S, M>,\n): TraceAttachment {\n return {\n filename: \"tea-trace.jsonl\",\n data: traceToJSONL(trace),\n contentType: \"application/x-ndjson\",\n };\n}\n\n/** A Sentry-shaped breadcrumb. Matches the fields of Sentry's `Breadcrumb`. */\nexport interface TraceBreadcrumb {\n readonly category: string;\n readonly type: string;\n readonly level: string;\n readonly message: string;\n readonly data?: Record<string, unknown>;\n}\n\n/** Options for {@link breadcrumbsFromTrace}. */\nexport interface BreadcrumbsOptions<M> {\n /**\n * Max breadcrumbs to emit. Default `100` (Sentry's own default breadcrumb\n * cap). When the trace has more msgs than `limit`, the OLDEST are dropped —\n * the returned list is the newest-last tail, matching how a Sentry\n * breadcrumb ring buffer behaves (the crash context is the recent past).\n */\n readonly limit?: number;\n /**\n * Customize each breadcrumb's `message`. Default uses `msg.type`. Provide a\n * serializer to surface a richer one-liner (e.g. include an id). The msg is\n * ALSO always attached verbatim under `data.msg` regardless of this option,\n * so the structured payload is never lost to the string rendering.\n */\n readonly serialize?: (msg: M) => string;\n}\n\n/**\n * Project a {@link Trace}'s recorded msgs into Sentry-shaped breadcrumbs — a\n * human-readable timeline of \"what happened before the crash\". Pure — no\n * Sentry import, no I/O. Each returned object matches Sentry's `Breadcrumb`\n * shape, so the consumer can loop and call `addBreadcrumb(...)`.\n *\n * One breadcrumb per recorded msg, in dispatch order (NEWEST LAST — Sentry\n * renders breadcrumbs oldest-first and the crash is the most recent event).\n * Each breadcrumb:\n * - `category` — `\"tea\"` (groups them in the Sentry breadcrumb panel).\n * - `type` — `\"default\"` (the generic Sentry breadcrumb type).\n * - `level` — `\"info\"` (msgs are normal flow; the crash is the event).\n * - `message` — `msg.type`, or `serialize(msg)` when provided.\n * - `data.msg` — the full msg object (structured detail kept verbatim).\n *\n * Capped at `opts.limit` (default 100). When the trace has more msgs than the\n * cap, the OLDEST are dropped and the newest `limit` are kept — the recent\n * lead-up to the crash is the useful context.\n *\n * Consumer wiring (no Sentry import here — the consumer owns the boundary):\n *\n * ```ts\n * import * as Sentry from \"@sentry/browser\"; // or \"@sentry/node\"\n * import { breadcrumbsFromTrace } from \"./internal/persistence/recorder\"; // internal module\n * for (const b of breadcrumbsFromTrace(rec.dump())) Sentry.addBreadcrumb(b);\n * ```\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport function breadcrumbsFromTrace<S, M extends { type: string }>(\n trace: Trace<S, M>,\n opts?: BreadcrumbsOptions<M>,\n): TraceBreadcrumb[] {\n const rawLimit = opts?.limit;\n // Clamp limit to a non-negative integer. `undefined`/NaN → 100 (Sentry's\n // default). A negative or zero limit yields an empty list (the caller asked\n // for no breadcrumbs); fractional values floor (you can't emit half a crumb).\n const limit =\n rawLimit === undefined || Number.isNaN(rawLimit)\n ? 100\n : Math.max(0, Math.floor(rawLimit));\n const serialize = opts?.serialize;\n\n // Keep the NEWEST `limit` msgs (the recent lead-up to the crash). Slicing\n // from `length - limit` (clamped to 0) preserves dispatch order within the\n // kept window. Note we do NOT use `slice(-limit)`: `slice(-0)` is `slice(0)`\n // (JS treats -0 as 0), which would return the WHOLE array for `limit: 0`.\n const start = Math.max(0, trace.msgs.length - limit);\n const kept = start === 0 ? trace.msgs : trace.msgs.slice(start);\n\n const crumbs: TraceBreadcrumb[] = [];\n for (const msg of kept) {\n crumbs.push({\n category: \"tea\",\n type: \"default\",\n level: \"info\",\n message: serialize ? serialize(msg) : msg.type,\n // Full msg kept verbatim so the structured payload survives even when\n // `message` is a terse one-liner. Cast: a Msg is a `{ type: string }`\n // record, which is a `Record<string, unknown>` for Sentry's `data`.\n data: { msg: msg as unknown as Record<string, unknown> },\n });\n }\n return crumbs;\n}\n\n// ---------------------------------------------------------------------------\n// traceToJSONL — serialize a STANDALONE Trace to JSONL (the dump→file path).\n//\n// `Recorder.toJSONL()` serializes the recorder's live buffers. `traceAttachment`\n// works over a plain `Trace` value (a `dump()` snapshot or a `parseJSONL`\n// result), which has no recorder behind it — so it needs a Trace→JSONL\n// function. This is the same wire format `Recorder.toJSONL` emits and\n// `parseJSONL` reads, so `parseJSONL(traceToJSONL(trace))` round-trips.\n//\n// When `trace.steps` is present (captureSteps was on, or the trace came from\n// parseJSONL), every step line carries its post-state. When absent, interior\n// step states are unknown — emitted as `null`, with `finalState` on the LAST\n// step line (identical to the captureSteps:false path in `Recorder.toJSONL`).\n// ---------------------------------------------------------------------------\nfunction traceToJSONL<S, M>(trace: Trace<S, M>): string {\n const lines: string[] = [];\n lines.push(JSON.stringify({ kind: \"boot\", state: trace.loaded }));\n\n const steps = trace.steps;\n for (let i = 0; i < trace.msgs.length; i++) {\n const msg = trace.msgs[i];\n const stepState = steps\n ? steps[i]?.state\n : i === trace.msgs.length - 1\n ? trace.finalState\n : null;\n lines.push(JSON.stringify({ kind: \"step\", msg, state: stepState }));\n }\n return lines.join(\"\\n\");\n}\n","// ---------------------------------------------------------------------------\n// trace-replay — replay a recorded Trace against a machine and diff the state.\n//\n// This is the back half of the record/replay feature. `../recorder` captures\n// a run (`loaded` + `msgs` + `finalState`); this module feeds that input\n// through the core `replay(machine, { msgs, ctx, loaded })` fold and asks:\n//\n// does the current reducer still produce the SAME final state for this\n// recorded input?\n//\n// Equal ⇒ the reducer is behavior-compatible with the recording. Diverged ⇒\n// the reducer changed for that input, and the divergence path\n// (`state.queue[2].status`) points at exactly the cell that drifted. This is\n// the regression / production-repro tool: record a prod incident, replay it\n// against the fixed reducer locally, confirm the fix changes the state at the\n// path you expected — or catch an unintended drift before it ships.\n//\n// Standalone by design. It imports ONLY the `Trace` type from `../recorder`\n// and the `replay` engine from `../index`. The deep-equal + first-divergence\n// path finder is self-contained (no external diff lib) — a richer pretty-diff\n// lands in `../devtools` later; trace-replay stays dependency-free so it can\n// run anywhere a machine runs (CI, a Worker, a Node script).\n// ---------------------------------------------------------------------------\n\nimport { type Cmd, type Machine, replay, type Sub } from \"../../../index\";\nimport type { Trace } from \"../recorder\";\nimport { indexPath, keyPath, typeOf, unionKeys } from \"./state-walk\";\n\n/**\n * The outcome of {@link replayTrace}.\n *\n * @typeParam S - the machine's State type.\n */\nexport interface ReplayResult<S> {\n /** `true` iff the recomputed final state deep-equals the trace's `finalState`. */\n matches: boolean;\n /** The trace's recorded `finalState` — what the run originally produced. */\n expected: S;\n /** The state the current reducer produced for the same input. */\n actual: S;\n /**\n * Present iff `matches` is `false`. The FIRST point of divergence found by\n * a deterministic pre-order walk:\n * - `path` — a dotted/indexed access path from the state root, e.g.\n * `state.queue[2].status` or `state` for a root-level\n * scalar mismatch.\n * - `expected` — the recorded value at that path.\n * - `actual` — the recomputed value at that path.\n */\n divergence?: { path: string; expected: unknown; actual: unknown };\n}\n\n/**\n * Replay a recorded {@link Trace} against `machine` and diff the recomputed\n * final state against the recorded one.\n *\n * Runs the core pure fold `replay(machine, { msgs: trace.msgs, ctx,\n * loaded: trace.loaded })` — init + update only, never `interpret`, never a\n * `Store`, never a live subscription (see `replay` in ../index). The `ctx` is\n * supplied fresh by the caller: the trace is the INPUT (loaded + msgs); ctx is\n * the ENVIRONMENT the reducer runs against now.\n *\n * @param machine - the machine to replay against. Mutating its reducer (the\n * whole point of a regression check) is what makes a recorded\n * trace diverge.\n * @param trace - a Trace from `../recorder` (live `dump()` or a re-hydrated\n * `parseJSONL(...)`).\n * @param ctx - the runtime ctx, supplied fresh at replay time.\n */\nexport function replayTrace<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n trace: Trace<S, M>,\n ctx: Ctx,\n): ReplayResult<S> {\n const result = replay(machine, {\n msgs: trace.msgs,\n ctx,\n loaded: trace.loaded,\n });\n const actual = result.state;\n const expected = trace.finalState;\n\n const divergence = firstDivergence(expected, actual, \"state\");\n if (divergence === null) {\n return { matches: true, expected, actual };\n }\n return { matches: false, expected, actual, divergence };\n}\n\n/**\n * Order-insensitive structural deep-equality — the comparator the\n * record/replay lane owns, exposed so consumers (e.g. `@demlik/tea/parity`'s\n * `parityEqual`) reuse it rather than adding a third deep-compare. It is one\n * of two structural walkers in the package, divergent on purpose:\n * this `deepEqual` returns a boolean verdict (order-insensitive, for\n * replay/parity equality), and `devtools`'s `diffState` returns a change list\n * (for a human-readable diff view) — same shape, two jobs, not one to collapse.\n * `true` iff `a` and `b` are structurally equal under {@link firstDivergence}'s\n * walk (object keys compared order-insensitively, arrays index-by-index).\n */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n return firstDivergence(a, b, \"state\") === null;\n}\n\n// === deep-equal + first-divergence path finder ===\n//\n// One walk does both jobs: it returns the FIRST point where `expected` and\n// `actual` differ (as a path + the two values), or `null` when they are\n// deeply equal. \"First\" is defined by a deterministic pre-order traversal:\n// the node itself, then its children in a stable key order (array index\n// order; object keys sorted so the result does not depend on insertion\n// order — two states with the same data but different key order are equal,\n// and the reported path for a real mismatch is reproducible run-to-run).\n//\n// The path grammar (`typeOf`, `indexPath`/`keyPath` segments, `unionKeys`\n// order) is shared with devtools' `diffState` via `./state-walk`, so the two\n// walkers print the SAME path for the same cell by construction. Only the leaf\n// policy differs, on purpose: first-hit here, collect-all there.\n//\n// Scope: TEA state is plain JSON-shaped data (the substrate's __DEV__ guards\n// reject closures in Cmds, and states are conventionally serializable). So\n// the walk handles: primitives, null/undefined, arrays, and plain objects.\n// It treats other object kinds (Date, Map, Set, class instances) by reference\n// identity + a structural fallback via Object.entries — sufficient for the\n// JSON-shaped states this tool targets; a richer differ lives in ../devtools.\n\n/** A divergence record, or `null` when the two values are deeply equal. */\ntype Divergence = { path: string; expected: unknown; actual: unknown } | null;\n\nfunction firstDivergence(\n expected: unknown,\n actual: unknown,\n path: string,\n): Divergence {\n // Identity / primitive fast path. Handles same-reference, and equal\n // primitives. `Object.is` so NaN === NaN and +0 ≠ -0 (matches structural\n // equality intent better than ===).\n if (Object.is(expected, actual)) return null;\n\n const te = typeOf(expected);\n const ta = typeOf(actual);\n // Different kinds at this node (e.g. object vs array, number vs string,\n // present vs null) — this node IS the divergence.\n if (te !== ta) return { path, expected, actual };\n\n if (te === \"array\") {\n const ae = expected as unknown[];\n const aa = actual as unknown[];\n if (ae.length !== aa.length) {\n // Length mismatch reported at the array node itself — the shape differs,\n // which is more informative than an arbitrary element index.\n return { path, expected, actual };\n }\n for (let i = 0; i < ae.length; i++) {\n const d = firstDivergence(ae[i], aa[i], indexPath(path, i));\n if (d !== null) return d;\n }\n return null;\n }\n\n if (te === \"object\") {\n const oe = expected as Record<string, unknown>;\n const oa = actual as Record<string, unknown>;\n // Sorted union of keys for deterministic traversal. A key present in one\n // but absent in the other diverges at that child path (its value is\n // `undefined` on the missing side).\n for (const key of unionKeys(oe, oa)) {\n const d = firstDivergence(oe[key], oa[key], keyPath(path, key));\n if (d !== null) return d;\n }\n return null;\n }\n\n // Primitives (string/number/boolean/bigint/symbol/function) that failed the\n // Object.is fast path, or null/undefined that differ — this node diverges.\n return { path, expected, actual };\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { restore } from './chunk-
|
|
2
|
-
import { RuntimeDiscardNotice, detachWork, DispatchDiscardedError, QuiescenceTimeoutError, RuntimeDiscardedError, IdentityDropNotice, checkedStep, cmdContractOver, Outcome, cmdEdge, isFencedStore, DisposeTimeoutNotice, NoCellError, lookupCell, structuralHash, desiredSub, followUps } from './chunk-
|
|
1
|
+
import { restore } from './chunk-A73IJ27X.js';
|
|
2
|
+
import { RuntimeDiscardNotice, detachWork, DispatchDiscardedError, QuiescenceTimeoutError, RuntimeDiscardedError, IdentityDropNotice, checkedStep, cmdContractOver, Outcome, cmdEdge, isFencedStore, DisposeTimeoutNotice, NoCellError, lookupCell, structuralHash, desiredSub, followUps } from './chunk-RKVUVALV.js';
|
|
3
3
|
|
|
4
4
|
// src/internal/engine/builtins.ts
|
|
5
5
|
function supervision(declared, machine) {
|
|
@@ -582,5 +582,5 @@ function startLoop(config) {
|
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
export { RuntimeStoppedError, builtinExtensions, defaultOnError, liveWork, startLoop };
|
|
585
|
-
//# sourceMappingURL=chunk-
|
|
586
|
-
//# sourceMappingURL=chunk-
|
|
585
|
+
//# sourceMappingURL=chunk-UHAQ4PI6.js.map
|
|
586
|
+
//# sourceMappingURL=chunk-UHAQ4PI6.js.map
|