@demlik/tea 0.16.0 → 0.17.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/dist/agent/index.d.ts +13 -13
- package/dist/agent/index.js +9 -9
- package/dist/{chunk-CIHSHLQX.js → chunk-4NTCPTNQ.js} +3 -3
- package/dist/{chunk-CIHSHLQX.js.map → chunk-4NTCPTNQ.js.map} +1 -1
- package/dist/{chunk-7MGXQZPW.js → chunk-4PXNZ5UY.js} +4 -4
- package/dist/{chunk-7MGXQZPW.js.map → chunk-4PXNZ5UY.js.map} +1 -1
- package/dist/{chunk-6NG4V5SB.js → chunk-6XBEYZME.js} +5 -5
- package/dist/{chunk-6NG4V5SB.js.map → chunk-6XBEYZME.js.map} +1 -1
- package/dist/{chunk-GOX2CYIM.js → chunk-C4U5D5HP.js} +3 -3
- package/dist/{chunk-GOX2CYIM.js.map → chunk-C4U5D5HP.js.map} +1 -1
- package/dist/{chunk-MVZS5FDM.js → chunk-DFUEAA6L.js} +3 -3
- package/dist/{chunk-MVZS5FDM.js.map → chunk-DFUEAA6L.js.map} +1 -1
- package/dist/{chunk-IVJP3GKJ.js → chunk-GWWCCSDH.js} +4 -4
- package/dist/{chunk-IVJP3GKJ.js.map → chunk-GWWCCSDH.js.map} +1 -1
- package/dist/{chunk-CH5KTOEZ.js → chunk-K4ETBNKS.js} +32 -9
- package/dist/chunk-K4ETBNKS.js.map +1 -0
- package/dist/{chunk-5XU6ZJO2.js → chunk-KWNCM6XM.js} +19 -13
- package/dist/chunk-KWNCM6XM.js.map +1 -0
- package/dist/{chunk-BCTEOVOU.js → chunk-LYVOGOBC.js} +3 -3
- package/dist/{chunk-BCTEOVOU.js.map → chunk-LYVOGOBC.js.map} +1 -1
- package/dist/{chunk-MYNLY3HP.js → chunk-OPH6Y4UT.js} +3 -3
- package/dist/{chunk-MYNLY3HP.js.map → chunk-OPH6Y4UT.js.map} +1 -1
- package/dist/{chunk-NSIAORRY.js → chunk-OYR7ENKU.js} +3 -3
- package/dist/{chunk-NSIAORRY.js.map → chunk-OYR7ENKU.js.map} +1 -1
- package/dist/{chunk-WT5PBPLB.js → chunk-QWIPH4YH.js} +4 -4
- package/dist/{chunk-WT5PBPLB.js.map → chunk-QWIPH4YH.js.map} +1 -1
- package/dist/{chunk-EHUDXAKC.js → chunk-XBZQT2TD.js} +7 -7
- package/dist/chunk-XBZQT2TD.js.map +1 -0
- package/dist/{core-BbgtiXdg.d.ts → core-rDbOaJTN.d.ts} +7 -3
- package/dist/do/index.d.ts +12 -12
- package/dist/do/index.js +10 -10
- package/dist/{durable-effects-7MTK_jnj.d.ts → durable-effects-CNaw97Be.d.ts} +1 -1
- package/dist/effect/index.d.ts +2 -2
- package/dist/effect/index.js +2 -2
- package/dist/extension/index.d.ts +3 -3
- package/dist/extension/index.js +2 -2
- package/dist/flow/index.d.ts +13 -13
- package/dist/flow/index.js +10 -10
- package/dist/idempotency/index.d.ts +3 -3
- package/dist/idempotency/index.js +3 -3
- package/dist/{index-Bz5Rh9Ga.d.ts → index-BOrsbdAf.d.ts} +4 -4
- package/dist/{index-DU6JpQ8G.d.ts → index-B_BxplWi.d.ts} +1 -1
- package/dist/{index-CjFDUBeq.d.ts → index-CWmsXNdX.d.ts} +1 -1
- package/dist/{index-_dOR3xqA.d.ts → index-Cberb5Rr.d.ts} +2 -2
- package/dist/{index-7vV8xmsD.d.ts → index-CcrKLtse.d.ts} +1 -1
- package/dist/{index-CkFYA9Y6.d.ts → index-Cx0F2M34.d.ts} +2 -2
- package/dist/{index-Xufl55W8.d.ts → index-DaX_P35Q.d.ts} +2 -2
- package/dist/{index-Cou86-GX.d.ts → index-q1PoukOZ.d.ts} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/jev/index.d.ts +8 -8
- package/dist/jev/index.js +5 -5
- package/dist/{machine-BQzTV8On.d.ts → machine-BcfCFMYF.d.ts} +7 -7
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/mem/index.d.ts +2 -2
- package/dist/mem/index.js +2 -2
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +2 -2
- package/dist/paginate/index.d.ts +4 -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 +9 -10
- package/dist/persistence/index.js +4 -4
- package/dist/promise/index.d.ts +2 -2
- package/dist/promise/index.js +3 -3
- package/dist/react/index.d.ts +2 -2
- package/dist/resilience/index.d.ts +6 -6
- package/dist/resilience/index.js +5 -5
- package/dist/{runtime-types-CiVpC0HN.d.ts → runtime-types-B62Ucnxr.d.ts} +2 -2
- package/dist/testing/index.d.ts +22 -108
- package/dist/testing/index.js +27 -177
- package/dist/testing/index.js.map +1 -1
- package/dist/timing/index.d.ts +4 -4
- package/dist/timing/index.js +4 -4
- package/dist/{types-LxZxGYda.d.ts → types-DPm5YCDQ.d.ts} +1 -1
- package/dist/work-queue/index.d.ts +4 -4
- package/package.json +3 -3
- package/dist/chunk-5XU6ZJO2.js.map +0 -1
- package/dist/chunk-CH5KTOEZ.js.map +0 -1
- package/dist/chunk-EHUDXAKC.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/engine/builtins.ts","../src/internal/engine/loop.ts"],"names":[],"mappings":"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","file":"chunk-KWNCM6XM.js","sourcesContent":["/**\n * tea's built-ins, each one an {@link Extension} of the core loop (`./loop`,\n * #280). None is a public plugin API (#268): each engine's `run` builds them\n * from its options through {@link builtinExtensions}, which fixes their order.\n */\n\nimport type { AnyCmdDef, Identity, Port } from \"../../pure/core\";\nimport {\n checkedStep,\n cmdContractOver,\n cmdEdge,\n Outcome,\n structuralHash,\n} from \"../../pure/core\";\nimport type {\n Store,\n Supervision,\n TelemetryEvent,\n TelemetrySink,\n} from \"../../runtime-types\";\nimport { IdentityDropNotice, isFencedStore } from \"../../runtime-types\";\nimport type { Extension, ExtensionFactory, LoopServices } from \"./loop\";\n\ntype AnyExtension<S, M, C> = ExtensionFactory<S, M, C>;\n\n// === update middleware ===\n\n/**\n * Supervision: a throw from the fold (the reducer, or any update middleware\n * inside this one) is reported under `\"reduce\"` for every strategy, then\n * `restart` commits the host's rehydrated State with no Cmds, `escalate`\n * rethrows with the runtime still live, and `stop` (the default) halts the gate\n * and rethrows, so THIS dispatch rejects too.\n */\nexport function supervision<S, M extends { type: string }, C>(\n declared: Supervision<S, M> | undefined,\n): AnyExtension<S, M, C> {\n const policy =\n declared === undefined\n ? ({ strategy: \"stop\" } as const)\n : typeof declared === \"string\"\n ? ({ strategy: declared } as const)\n : declared;\n return (loop) => ({\n update: (next) => (state, msg) => {\n try {\n return next(state, msg);\n } catch (reduceError) {\n loop.report(reduceError, \"reduce\");\n switch (policy.strategy) {\n case \"restart\":\n // A throw inside `rehydrate` is NOT caught — it reaches the caller.\n return [policy.rehydrate(state, msg, reduceError), []];\n case \"escalate\":\n throw reduceError;\n default:\n loop.halt();\n throw reduceError;\n }\n }\n },\n });\n}\n\n/**\n * The instance-identity filter: drop a Msg addressed to a DIFFERENT instance\n * than the one this State owns, before the reducer sees it. A Msg with no\n * identity, or a State that has none yet, always proceeds. The drop is no\n * transition, and it RESOLVES the dispatch — a mis-addressed Msg is not the\n * caller's error — so it is reported as an `IdentityDropNotice` (warn by\n * default) or the caller could not tell \"applied\" from \"discarded\".\n *\n * Sits INSIDE {@link supervision}, so a throwing `ofMsg` / `ofState` is reported\n * and supervised exactly like a reducer throw (spike #264 run 3).\n */\nexport function identityFilter<S, M extends { type: string }, C>(\n identity: Identity<S, M> | undefined,\n): AnyExtension<S, M, C> {\n return (loop) => {\n if (identity === undefined) return {};\n const misaddressed = (msg: M, state: S): boolean => {\n const addressed = identity.ofMsg(msg);\n if (addressed === undefined) return false;\n const own = identity.ofState(state);\n if (own === undefined) return false;\n return structuralHash(addressed) !== structuralHash(own);\n };\n return {\n update: (next) => (state, msg) => {\n if (!misaddressed(msg, state)) return next(state, msg);\n loop.report(new IdentityDropNotice(msg.type), \"identity-drop\");\n return null;\n },\n };\n };\n}\n\n/**\n * The dev pre/post pair around the reducer (`checkedStep`): freeze the input\n * State so an in-place mutation trips, and check the returned `[state, cmds]`\n * shape. Both compile out of production. Innermost, so it sees exactly what the\n * reducer saw.\n */\nexport function devChecks<S, M extends { type: string }, C>(): AnyExtension<\n S,\n M,\n C\n> {\n return () => ({\n update: (next) => (state, msg) => checkedStep(state, msg, next),\n });\n}\n\n// === interpret middleware ===\n\n/**\n * The `Cmd.define` edge (ADR 0021): a defined Cmd's handler return is minted\n * into its `_ok` / `_err` Msg, its `ok` value parsed against the def's schema\n * and stamped with `at` from `clock`. A defined Cmd's handler failing outside\n * its declared channel goes to the sink under `\"interpret\"`; a hand-written\n * Cmd's handler throw keeps rejecting the dispatch.\n *\n * A defined Cmd's handler that DISPATCHES its own `_ok` / `_err`, built by hand\n * rather than minted by this edge, breaks the same contract as one that\n * returns it: the Msg is dropped and the `OutcomeContractError` goes to the\n * sink under `\"interpret\"`. Its other Msgs are delivered as before.\n *\n * The same edge rides on ctx under `cmdEdge`, beside the `ok` / `err` builders,\n * so a handler that runs a base handler inside its own settles the base's\n * outcome through this one edge (#66).\n */\nexport function cmdDefinitions<S, M extends { type: string }, C>(\n defs: readonly AnyCmdDef[],\n clock: () => number,\n): AnyExtension<S, M, C> {\n return (loop) => {\n const { settle, checkDispatch: check } = cmdContractOver(defs, clock);\n const defined = new Set(defs.map((d) => d.cmdType));\n return {\n interpret: (next) => async (cmd, dispatch) => {\n const type = (cmd as { type: string }).type;\n const checked = defined.has(type)\n ? (msg: M) => {\n try {\n check(cmd as { type: string }, msg);\n } catch (err) {\n loop.report(err, \"interpret\");\n return;\n }\n dispatch(msg);\n }\n : dispatch;\n try {\n return settle(cmd as { type: string }, await next(cmd, checked));\n } catch (err) {\n if (!defined.has(type)) throw err;\n loop.report(err, \"interpret\");\n return undefined;\n }\n },\n ctx: { ok: Outcome.ok, err: Outcome.err, [cmdEdge]: settle },\n };\n };\n}\n\n// === store wrapper ===\n\n/**\n * Fencing is a property of the store the caller handed `run`, never a flag\n * (#143). A fenced store is read with its version at boot and every save is a\n * compare-and-swap against the last version this run saw, so a second writer\n * that took over is refused with `StoreConflictError` at this run's next save —\n * before that transition's Cmds run. A plain store passes through unchanged.\n */\nexport function fencing<S, M, C>(): AnyExtension<S, M, C> {\n return () => ({\n store: (store: Store<S>): Store<S> => {\n if (!isFencedStore(store)) return store;\n let version = 0;\n return {\n async load() {\n const read = await store.loadFenced();\n version = read.version;\n return read.raw;\n },\n async save(state) {\n version = await store.saveFenced(state, version);\n },\n migrate: (raw) => store.migrate(raw),\n };\n },\n });\n}\n\n// === ctx + handle: ports ===\n\n/**\n * Ports (`definePort`): `ctx.emit(port, value)` from a handler and\n * `emitPort` / `subscribePort` on the handle. Fanout is synchronous and\n * throw-isolated under `\"port-emit\"`; no subscribers is a no-op.\n */\nexport function ports<S, M, C>(): AnyExtension<S, M, C> {\n return (loop) => {\n const registry = new Map<Port<unknown>, Set<(value: unknown) => void>>();\n function emit<T>(port: Port<T>, value: T): void {\n const subscribers = registry.get(port as Port<unknown>);\n if (!subscribers || subscribers.size === 0) return;\n loop.fanout(subscribers, \"port-emit\", (listener) => listener(value));\n }\n return {\n ctx: { emit },\n handle: {\n subscribePort<T>(\n port: Port<T>,\n listener: (value: T) => void,\n ): () => void {\n const key = port as Port<unknown>;\n const bucket = registry.get(key) ?? new Set();\n registry.set(key, bucket);\n const erased = listener as (value: unknown) => void;\n bucket.add(erased);\n return () => {\n bucket.delete(erased);\n if (bucket.size === 0 && registry.get(key) === bucket)\n registry.delete(key);\n };\n },\n emitPort: emit,\n },\n };\n };\n}\n\n// === commit observers ===\n\n/**\n * `subscribe` (zero-arg change notifiers), then `observe` (every applied\n * transition's `(msg, state)`) or, at boot, `onBoot` (the initial State, once;\n * a handler registered after boot fires at once). Each fanout is\n * throw-isolated under its own phase.\n */\nexport function observation<S, M, C>(): AnyExtension<S, M, C> {\n return (loop) => {\n const listeners = new Set<() => void>();\n const observers = new Set<(msg: M, state: S) => void>();\n const bootHandlers = new Set<(state: S) => void>();\n let booted = false;\n return {\n commit(msg, state) {\n loop.fanout(listeners, \"listener\", (listener) => listener());\n if (msg === undefined) {\n booted = true;\n loop.fanout(bootHandlers, \"boot\", (handler) => handler(state));\n return;\n }\n loop.fanout(observers, \"observer\", (observer) => observer(msg, state));\n },\n handle: {\n subscribe: subscribeTo(listeners),\n observe: subscribeTo(observers),\n onBoot(handler: (state: S) => void): () => void {\n const state = loop.state();\n if (booted && state !== undefined) {\n loop.fanout([handler], \"boot\", (h) => h(state));\n return () => {};\n }\n return subscribeTo(bootHandlers)(handler);\n },\n },\n };\n };\n}\n\nfunction subscribeTo<T>(set: Set<T>): (item: T) => () => void {\n return (item) => {\n set.add(item);\n return () => {\n set.delete(item);\n };\n };\n}\n\n/**\n * The semantic event channel: `project` maps each APPLIED transition to zero or\n * more public events, and `on(type, handler)` receives only its own type. The\n * machine's private Msg names never reach `on`. A throw in the projector or a\n * handler is isolated under `\"event\"`. Boot projects nothing.\n */\nexport function semanticEvents<S, M, C, E extends { type: string }>(\n project: ((msg: M, state: S) => readonly E[]) | undefined,\n): AnyExtension<S, M, C> {\n return (loop) => {\n const handlers = new Map<string, Set<(event: E) => void>>();\n return {\n commit(msg, state) {\n if (msg === undefined || project === undefined || handlers.size === 0)\n return;\n let events: readonly E[];\n try {\n events = project(msg, state);\n } catch (err) {\n loop.report(err, \"event\");\n return;\n }\n for (const event of events) {\n const bucket = handlers.get(event.type);\n if (bucket === undefined) continue;\n loop.fanout(bucket, \"event\", (handler) => handler(event));\n }\n },\n handle: {\n on(type: string, handler: (event: E) => void): () => void {\n const bucket = handlers.get(type) ?? new Set();\n handlers.set(type, bucket);\n bucket.add(handler);\n return () => {\n bucket.delete(handler);\n if (bucket.size === 0 && handlers.get(type) === bucket)\n handlers.delete(type);\n };\n },\n },\n };\n };\n}\n\n/**\n * Run terminality: `result()` reads the State when `isTerminal` holds for it,\n * and `done()` resolves on the first commit (boot included — a rehydrated run\n * can land terminal) where it does. No predicate → never terminal.\n */\nexport function terminality<S, M, C>(\n isTerminal: ((state: S) => boolean) | undefined,\n): AnyExtension<S, M, C> {\n const holds = isTerminal ?? (() => false);\n return (loop) => {\n const waiters = new Set<(state: S) => void>();\n return {\n commit(_msg, state) {\n if (waiters.size === 0 || !holds(state)) return;\n const parked = [...waiters];\n waiters.clear();\n for (const resolve of parked) resolve(state);\n },\n handle: {\n result(): S | undefined {\n const current = loop.state() as S;\n return holds(current) ? current : undefined;\n },\n done(): Promise<S> {\n const current = loop.state() as S;\n if (holds(current)) return Promise.resolve(current);\n return new Promise<S>((resolve) => {\n waiters.add(resolve);\n });\n },\n },\n };\n };\n}\n\n/**\n * Telemetry (#268: `withTelemetry` moved inside tea). Every applied transition\n * hands the sink `{ seq, msgType, at }` — `seq` counts this run's applied\n * transitions from 1, `at` reads `clock`. Fire-and-forget: the loop never waits\n * on the sink, and a sink that throws or rejects reaches `onError` under\n * `\"observer\"` without touching the run.\n */\nexport function telemetry<S, M extends { type: string }, C>(\n sink: TelemetrySink | undefined,\n clock: () => number,\n): AnyExtension<S, M, C> {\n return (loop: LoopServices<S>): Extension<S, M, C> => {\n if (sink === undefined) return {};\n let seq = 0;\n return {\n commit(msg) {\n if (msg === undefined) return;\n seq += 1;\n const event: TelemetryEvent = { seq, msgType: msg.type, at: clock() };\n try {\n Promise.resolve(sink(event)).catch((err: unknown) =>\n loop.report(err, \"observer\"),\n );\n } catch (err) {\n loop.report(err, \"observer\");\n }\n },\n };\n };\n}\n\n// === assembly: the one order both engines run the built-ins in ===\n\n/** The `run` options the built-ins read. Every engine's `run` accepts them. */\nexport interface BuiltinOptions<\n S,\n M extends { type: string },\n E extends { type: string },\n> {\n readonly clock?: () => number;\n readonly events?: (msg: M, state: S) => readonly E[];\n readonly supervision?: Supervision<S, M>;\n readonly terminal?: (state: S) => boolean;\n readonly telemetry?: TelemetrySink;\n}\n\n/**\n * The built-ins, in the one order that gives them their meaning. Every engine\n * builds its loop from this list, so a machine means the same thing on each.\n *\n * Update middleware nests first-outermost: supervision wraps the identity\n * filter (a throwing `ofMsg` is supervised like a reducer throw — spike #264\n * run 3), which wraps the dev checks around the reducer itself. Commit\n * callbacks run in order: change listeners, then `observe` / `onBoot`, then\n * semantic events, then `done()` waiters, then telemetry.\n */\nexport function builtinExtensions<\n S,\n M extends { type: string },\n C,\n E extends { type: string },\n>(\n machine: {\n readonly identity?: Identity<S, M>;\n readonly cmds?: readonly AnyCmdDef[];\n },\n opts: BuiltinOptions<S, M, E>,\n): readonly ExtensionFactory<S, M, C>[] {\n const clock = opts.clock ?? Date.now;\n return [\n supervision(opts.supervision),\n identityFilter(machine.identity),\n devChecks(),\n cmdDefinitions(machine.cmds ?? [], clock),\n fencing(),\n ports(),\n observation(),\n semanticEvents(opts.events),\n terminality(opts.terminal),\n telemetry(opts.telemetry, clock),\n ];\n}\n","/**\n * The core loop both engines run (#280, spike #264). It does five things and\n * nothing else: it keeps one serial tail, folds a Msg into a transition, saves\n * before any effect, reconciles the running Subs, and runs the transition's\n * Cmds, dispatching what they return. It has no special case for any built-in.\n *\n * One loop, not one per engine (#283). The loop calls a handler and a runner\n * through erased signatures and awaits a Promise either way, so an engine's\n * own types never pass through it. The Effect engine turns each handler and\n * runner into a fiber at its own edge, run with the services the caller\n * provided, so Effect services (`R`) reach every handler without the loop\n * carrying a type slot for them — the gap spike #260's generic loop left open.\n * Sharing the loop is also what makes the two engines' traces identical: the\n * fold, the save order and the built-ins are the same code.\n *\n * Everything else `run` offers is an {@link Extension} built on five points:\n *\n * 1. `update` — middleware over the fold. Returning `null` means \"no\n * transition\": no save, no effects, no commit.\n * 2. `interpret` — middleware over one Cmd's handler call; it maps what the\n * handler returned to the Msg to dispatch, if any.\n * 3. `store` — a wrapper over the store the caller handed `run`.\n * 4. `commit` — called after a transition's effects, with the Msg and the\n * committed State. Boot passes `undefined` for the Msg.\n * 5. `ctx` — keys added to the ctx every Cmd handler receives, so a\n * handler that runs another handler inside its own can reach\n * the same machinery the loop applies to it.\n *\n * An extension may also add methods to the run handle (`handle`). Array order\n * is nesting order: the first extension's middleware is the outermost, and\n * commit callbacks run first to last. `builtinExtensions` (`./builtins`) fixes\n * that order for every engine; it is not a user-facing plugin API (#268).\n *\n * `stop()` is the core's own lifecycle, because the gate it drives is what every\n * dispatch passes through: open → draining → closed, never backwards.\n */\n\nimport type { Dispose, Sub, SubEntry } from \"../../pure/core\";\nimport { desiredSub, detachWork } from \"../../pure/core\";\nimport type {\n DispatchSettle,\n OnError,\n RuntimeErrorContext,\n RuntimeErrorPhase,\n Store,\n} from \"../../runtime-types\";\nimport {\n DispatchDiscardedError,\n DisposeTimeoutNotice,\n QuiescenceTimeoutError,\n RuntimeDiscardedError,\n RuntimeDiscardNotice,\n} from \"../../runtime-types\";\n\n// === The extension points ===\n\n/** A fold's result: the next State and the Cmds it emits. */\nexport type Transition<S, C> = readonly [S, readonly C[]];\n\n/** One fold step. `null` means the Msg produced no transition. */\nexport type Step<S, M, C> = (state: S, msg: M) => Transition<S, C> | null;\n\n/**\n * One Cmd's handler call; resolves to the Msg to dispatch, or nothing.\n * `dispatch` is what the handler is handed for Msgs it fires itself, so an\n * extension can check them on the way out.\n */\nexport type InterpretStep<C, M> = (\n cmd: C,\n dispatch: (msg: M) => void,\n) => Promise<unknown>;\n\n/** What the loop lends an extension. */\nexport interface LoopServices<S> {\n /** Route a failure with no caller to the `onError` sink. */\n report(error: unknown, phase: RuntimeErrorPhase): void;\n /** Call `fn` for each item, routing every throw to the sink under `phase`. */\n fanout<T>(\n items: Iterable<T>,\n phase: RuntimeErrorPhase,\n fn: (item: T) => void,\n ): void;\n /** Close the gate for good: every later dispatch is refused. */\n halt(): void;\n /** The committed State, or `undefined` before boot has produced one. */\n state(): S | undefined;\n}\n\n/** One built-in, as the loop sees it. Every member is optional. */\nexport interface Extension<S, M, C> {\n readonly update?: (next: Step<S, M, C>) => Step<S, M, C>;\n readonly interpret?: (next: InterpretStep<C, M>) => InterpretStep<C, M>;\n readonly store?: (store: Store<S>) => Store<S>;\n readonly commit?: (msg: M | undefined, state: S) => void;\n readonly ctx?: object;\n readonly handle?: object;\n}\n\n/** An extension is built once per run, from the loop's services. */\nexport type ExtensionFactory<S, M, C> = (\n loop: LoopServices<S>,\n) => Extension<S, M, C>;\n\n/** A Cmd handler as the loop calls it. */\nexport type LoopHandler<M> = (\n cmd: never,\n ctx: unknown,\n dispatch: (msg: M) => void,\n) => unknown;\n\n/** A Sub runner as the loop calls it. */\nexport type LoopRunner<Ctx, M> = (\n sub: Sub,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n) => Dispose;\n\n/** Everything the loop needs from `run`. */\nexport interface LoopConfig<S, M extends { type: string }, C, Ctx> {\n readonly init: (loaded: S | null, ctx: Ctx) => Transition<S, C>;\n /** The innermost fold: the machine's own `update`. */\n readonly reduce: (state: S, msg: M) => Transition<S, C>;\n readonly subs: readonly SubEntry<S>[];\n readonly runnerFor: (type: string) => LoopRunner<Ctx, M> | undefined;\n readonly handlerFor: (type: string) => LoopHandler<M> | undefined;\n readonly store: Store<S> | undefined;\n readonly ctx: Ctx;\n readonly onError: OnError | undefined;\n readonly disposeTimeoutMs: number;\n readonly idleCap: number;\n readonly extensions: readonly ExtensionFactory<S, M, C>[];\n}\n\n// === liveWork: \"can anything still transition me without a dispatch?\" ===\n//\n// The one read `driveToDone` needs that the public `Runtime` does not carry:\n// after `start`'s chain quiesces, only a live Sub or an in-flight Cmd can still\n// enqueue a transition; with neither, waiting is a leak (#68). Module-private\n// and symbol-keyed so it reaches no export (the export map is the contract —\n// MAINTAINING.md), yet enumerable so it survives the spread a wrapper does over\n// the handle.\nexport const liveWork: unique symbol = Symbol(\"demlik-tea.liveWork\");\n\n/** The runtime's own sources of a caller-less transition, counted. */\nexport interface LiveWork {\n /** Live Subs — the running `machine.subs` entries. */\n readonly subs: number;\n /** Interpret handlers currently awaiting. */\n readonly cmds: number;\n}\n\nexport interface LiveWorkProbe {\n readonly [liveWork]: () => LiveWork;\n}\n\n/** The members the core itself puts on the run handle. */\nexport interface LoopHandle<S, M> extends LiveWorkProbe {\n dispatch(msg: M, opts?: { readonly settle?: DispatchSettle }): Promise<void>;\n dispatchOnce(msg: M): Promise<void>;\n getState(): S;\n ready: Promise<unknown>;\n idle(): Promise<void>;\n stop(): Promise<void>;\n}\n\n// Default `onError` sink: re-throw on a fresh macrotask so the failure reaches\n// the host's global error handler instead of vanishing — surface, not swallow\n// (invariant 6).\n//\n// A `RuntimeDiscardNotice` is NOT a failure of the runtime's own contract — a\n// host tearing a runtime down with Cmds in flight (a React ctx change, a\n// navigation) is legal, merely lossy. Rethrowing it would make every\n// unmount-during-fetch an uncaught error for consumers who never configured a\n// sink, so the default WARNS: loud enough that the silent discard #365\n// describes cannot happen again, never fatal. A configured `onError` sees\n// `\"discard\"` like any other phase and can route or ignore it.\n//\n// The branch keys on the ERROR CLASS, never on `context.phase`: `report`\n// hands a THROWING consumer sink's own error back here with the phase it was\n// handling, so a phase-keyed branch would warn away a broken sink and re-create\n// the very silent failure this exists to remove.\nfunction defaultOnError(error: unknown, _context: RuntimeErrorContext): void {\n if (error instanceof RuntimeDiscardNotice) {\n console.warn(error);\n return;\n }\n setTimeout(() => {\n throw error;\n }, 0);\n}\n\n/**\n * Start the loop. Returns the handle synchronously; boot runs as the FIRST entry\n * on the serial tail. Save-then-effects ordering is structural: every\n * transition installs State, awaits the save, reconciles Subs, runs the Cmds,\n * then calls the commit callbacks.\n */\nexport function startLoop<S, M extends { type: string }, C, Ctx>(\n config: LoopConfig<S, M, C, Ctx>,\n): LoopHandle<S, M> {\n const { idleCap, disposeTimeoutMs } = config;\n const onError: OnError = config.onError ?? defaultOnError;\n\n // A throwing sink routes THAT throw through `defaultOnError`, with the context\n // the sink was handling, so it cannot re-create a silent failure.\n function report(error: unknown, phase: RuntimeErrorPhase): void {\n const context: RuntimeErrorContext = { phase };\n try {\n onError(error, context);\n } catch (sinkError) {\n defaultOnError(sinkError, context);\n }\n }\n\n // Throw-isolated fanout: one bad consumer never strands its siblings\n // (invariant 6). The single home for that isolation discipline.\n function fanout<T>(\n items: Iterable<T>,\n phase: RuntimeErrorPhase,\n fn: (item: T) => void,\n ): void {\n for (const item of items) {\n try {\n fn(item);\n } catch (err) {\n report(err, phase);\n }\n }\n }\n\n // `state` is late-initialized: synchronously when there is no store to read,\n // otherwise inside the boot step (the head of the tail).\n let state: S | undefined;\n let bootError: unknown = null;\n // The dispatch gate. It only ever ADVANCES — `\"open\"` → `\"draining\"` →\n // `\"closed\"` — so the discard window can never re-open under a second `stop()`.\n //\n // \"open\" — normal operation; `enqueueDispatch` accepts.\n // \"draining\" — inside `stop()`, before the tail has settled. New work is\n // still refused (refusing is what makes the drain terminate),\n // but a Msg refused HERE was discarded BY the teardown: an\n // in-flight Cmd's follow-up, a detached handler's last Msg, or a\n // Sub still live because subs are torn down after the drain. So\n // the rejection is a `DispatchDiscardedError` reported under\n // `phase: \"discard\"` — lossy, legal, warn-only.\n // \"closed\" — halted by an extension, or `stop()` has returned. A dispatch\n // refused here is a consumer using a retired runtime — a real\n // error, and it stays loud.\n let gate: \"open\" | \"draining\" | \"closed\" = \"open\";\n // Interpret handlers currently awaiting. Written only by `trackInFlight`, read\n // by `stop()` to make a mid-flight teardown LOUD instead of silent (#365).\n let inFlightCmds = 0;\n\n const services: LoopServices<S> = {\n report,\n fanout,\n halt() {\n gate = \"closed\";\n },\n state: () => state,\n };\n const extensions = config.extensions.map((make) => make(services));\n\n // The fold, the handler call and the store, each wrapped by the extensions\n // that ask for it. `reduceRight` makes the first extension the outermost.\n const update: Step<S, M, C> = extensions.reduceRight<Step<S, M, C>>(\n (next, ext) => (ext.update ? ext.update(next) : next),\n config.reduce,\n );\n const interpret: InterpretStep<C, M> = extensions.reduceRight<\n InterpretStep<C, M>\n >((next, ext) => (ext.interpret ? ext.interpret(next) : next), callHandler);\n const store: Store<S> | undefined =\n config.store === undefined\n ? undefined\n : extensions.reduceRight<Store<S>>(\n (inner, ext) => (ext.store ? ext.store(inner) : inner),\n config.store,\n );\n const commitCallbacks = extensions.flatMap((ext) =>\n ext.commit ? [ext.commit] : [],\n );\n\n // Copied into a fresh object so handlers get the contributed keys without\n // mutating the caller's ctx (which may be shared across runtimes / tests).\n // `Object.assign`, not a spread: tsc cannot prove a generic `Ctx` is an\n // object type, and a spread refuses it.\n const handlerCtx: unknown = Object.assign(\n {},\n config.ctx,\n ...extensions.map((ext) => ext.ctx ?? {}),\n { [detachWork]: detachInFlight },\n );\n\n // Every step chains onto `tail` — the single concurrency gate.\n let tail: Promise<void> = Promise.resolve();\n\n // The running Subs, keyed by their derived id. An id present here is a runner\n // that started and has not been disposed.\n const subRegistry = new Map<string, { type: string; dispose: Dispose }>();\n\n // Tear down each named sub: run its cleanup (throws routed to the sink under\n // `\"sub-cleanup\"`), then drop it from the registry regardless.\n function stopSubs(ids: Iterable<string>): void {\n for (const id of ids) {\n const running = subRegistry.get(id);\n if (running === undefined) continue;\n try {\n trackDisposal(running.dispose());\n } catch (err) {\n report(err, \"sub-cleanup\");\n }\n subRegistry.delete(id);\n }\n }\n\n // Teardown work that returned a Promise and has not settled. Reconcile runs\n // inside the synchronous transition path (invariant 2) and cannot await a\n // cleanup, so every async disposal is remembered here the moment it starts,\n // and `stop()` drains the set before resolving: `await runtime.stop()` then\n // means \"teardown is done\", the only reading a host evicting an isolate can\n // act on. The rejection is attached HERE, once, so it can never become an\n // unhandled rejection.\n const pendingDisposals = new Set<Promise<void>>();\n\n function trackDisposal(result: void | Promise<void>): void {\n if (!(result instanceof Promise)) return;\n const settling: Promise<void> = result\n .catch((err) => {\n report(err, \"sub-cleanup\");\n })\n .finally(() => {\n pendingDisposals.delete(settling);\n });\n pendingDisposals.add(settling);\n }\n\n /**\n * Wait for the disposals started so far, bounded by `disposeTimeoutMs`. On\n * expiry report a `DisposeTimeoutNotice` and return anyway — `stop()` resolves\n * regardless (contract), and a release that never settles must not become a\n * host that never shuts down.\n */\n async function drainDisposals(): Promise<void> {\n if (pendingDisposals.size === 0) return;\n const outstanding = [...pendingDisposals];\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const outcome = await Promise.race([\n Promise.all(outstanding).then(() => \"settled\" as const),\n new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), disposeTimeoutMs);\n }),\n ]);\n if (outcome === \"timeout\") {\n report(\n new DisposeTimeoutNotice(outstanding.length, disposeTimeoutMs),\n \"discard\",\n );\n }\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n }\n\n /**\n * Reconcile the running Subs against `state`, after every save. An id already\n * running is left alone, a running id no longer wanted is stopped, and a\n * wanted id not yet running is started through its runner.\n *\n * Every failure is collected, never thrown mid-pass, so one bad entry cannot\n * strand its siblings: a throwing `deps` leaves the running Subs of that type\n * untouched, and a throwing runner is not registered. The first error is\n * thrown after the pass. A Sub type with no runner is a failure too — nothing\n * would ever start it.\n */\n function reconcileSubs(): void {\n let firstError: unknown = null;\n const remember = (err: unknown): void => {\n if (firstError === null) firstError = err;\n };\n const desired = new Map<string, Sub>();\n // Types whose entry could not be read this pass: keep what is running.\n const unreadTypes = new Set<string>();\n for (const entry of config.subs) {\n try {\n const sub = desiredSub(entry, state as S);\n if (sub !== null) desired.set(sub.id, sub);\n } catch (err) {\n remember(err);\n unreadTypes.add(entry.type);\n }\n }\n\n stopSubs(\n [...subRegistry].flatMap(([id, running]) =>\n desired.has(id) || unreadTypes.has(running.type) ? [] : [id],\n ),\n );\n\n for (const [id, sub] of desired) {\n if (subRegistry.has(id)) continue;\n const runner = config.runnerFor(sub.type);\n if (runner === undefined) {\n remember(\n new Error(\n `@demlik/tea: no subscribe runner for Sub type \"${sub.type}\". ` +\n \"Pass one to run in `subscribe`.\",\n ),\n );\n continue;\n }\n try {\n // A runner's dispatch is unawaited by construction, so it gets the\n // wrapped form, which also queues the transition behind this step —\n // never on the runner's own stack (spike #260).\n const dispose = runner(sub, config.ctx, dispatchUnawaited);\n subRegistry.set(id, { type: sub.type, dispose });\n } catch (err) {\n remember(err);\n }\n }\n if (firstError !== null) throw firstError;\n }\n\n // The ONE `(msg) => void` handed to every producer that cannot await its own\n // dispatch: a Cmd handler's late tail, a Sub runner. Its rejection has no\n // caller, so it routes to the sink with the phase derived from the error\n // class (`reportUndelivered`). Handing those sites the raw `enqueueDispatch`\n // would turn a Sub firing during `stop()`'s drain into an unhandled rejection\n // the sink never sees.\n function dispatchUnawaited(msg: M): void {\n enqueueDispatch(msg).catch(reportUndelivered);\n }\n\n // A `DispatchDiscardedError` means the teardown refused the Msg (`\"discard\"`,\n // warn-only); anything else is a genuine failure of the follow-up itself.\n function reportUndelivered(error: unknown): void {\n report(\n error,\n error instanceof DispatchDiscardedError ? \"discard\" : \"follow-up\",\n );\n }\n\n /**\n * Count a handler's work as in flight for exactly the lifetime of its\n * promise. `finally`-balanced, so a rejecting handler can never strand the\n * count above zero. A handler that throws SYNCHRONOUSLY never reaches here,\n * which is correct — it was never in flight.\n */\n function trackInFlight<T>(work: T | Promise<T>): Promise<T> {\n inFlightCmds++;\n return Promise.resolve(work).finally(() => {\n inFlightCmds--;\n });\n }\n\n /**\n * Enlist work a handler will NOT return — the `detachWork` key on ctx. A\n * handler that fans out inside its own Cmd (ADR 0018) returns before its\n * effect finishes; handing the promise here puts it back on both accountings\n * a returned promise had: `trackInFlight`, so `stop()` still reports it, and\n * the serial `tail`, so `idle()` drains it. The tail extension is\n * SYNCHRONOUS, so a `drainToQuiescence` that already read `tail` sees the\n * reference change and loops again.\n */\n function detachInFlight(work: Promise<unknown>): void {\n const tracked = trackInFlight(work);\n tracked.catch(reportUndelivered);\n tail = tail\n .then(async () => {\n await tracked;\n })\n .catch(() => {});\n }\n\n // The innermost interpret step: call the Cmd's handler, if one is wired. A\n // missing handler returns nothing, which is invariant 6's forward progress\n // for a miswired consumer.\n function callHandler(cmd: C, dispatch: (msg: M) => void): Promise<unknown> {\n const handler = config.handlerFor((cmd as { type: string }).type);\n if (handler === undefined) return Promise.resolve(undefined);\n return trackInFlight(handler(cmd as never, handlerCtx, dispatch));\n }\n\n /**\n * Run each emitted Cmd. A returned Msg is enqueued onto the tail, never\n * dispatched re-entrantly. The first error stops further handlers in this\n * transition.\n *\n * THIS LOOP IS SERIAL BY RULING, NOT BY OVERSIGHT (ADR 0018). It runs a\n * transition's Cmds one at a time and never interleaves two handlers, so the\n * Msgs it enqueues fold in Cmd-EMISSION order and a replayed log reproduces\n * the same fold — invariant 2's serializability. Overlap in time belongs\n * INSIDE one Cmd's handler, never across the Cmds of one transition.\n */\n async function runCmds(cmds: readonly C[]): Promise<void> {\n for (const cmd of cmds) {\n const follow = await interpret(cmd, dispatchUnawaited);\n if (follow !== undefined && follow !== null) {\n enqueueDispatch(follow as M).catch(reportUndelivered);\n }\n }\n }\n\n // Install State, then save → reconcile subs → run Cmds → commit callbacks.\n // Save-before-effects is the hard ordering; tests pin it. Boot and every\n // transition end here; boot passes no Msg.\n async function commit(\n next: S,\n msg: M | undefined,\n cmds: readonly C[],\n ): Promise<void> {\n state = next;\n if (store) await store.save(next);\n reconcileSubs();\n await runCmds(cmds);\n for (const callback of commitCallbacks) callback(msg, next);\n }\n\n // One full transition. A `null` from the fold is no transition at all, and\n // it RESOLVES the dispatch. A throw from the fold rejects it.\n async function stepDispatch(msg: M): Promise<void> {\n if (state === undefined) {\n throw new Error(\"@demlik/tea: runtime not booted\");\n }\n const transition = update(state, msg);\n if (transition === null) return;\n await commit(transition[0], msg, transition[1]);\n }\n\n // With NO store there is nothing to read, so `init` runs IMMEDIATELY and\n // `getState()` is observable synchronously (what `useSyncExternalStore`\n // consumers need to render the first commit without a flicker); an `init`\n // throw leaves `run` itself. With a store, the read is the first thing boot\n // awaits; a `load` or `migrate` throw rejects `ready`.\n const storeless: Transition<S, C> | null =\n store === undefined ? config.init(null, config.ctx) : null;\n if (storeless !== null) state = storeless[0];\n\n async function boot(): Promise<void> {\n // Boundary parse (invariant 8): `store.load()` returns `unknown`;\n // `store.migrate(raw)` is the required parse — `S` on a recognized shape,\n // `null` on an unrecognized one (boots fresh).\n const [initial, cmds] =\n storeless ??\n config.init(\n (store as Store<S>).migrate(await (store as Store<S>).load()),\n config.ctx,\n );\n await commit(initial, undefined, cmds);\n }\n\n /**\n * Enqueue a dispatch on the tail — the single gate re-entrant handler and\n * runner calls also go through. Rejects when the gate is not open (with a\n * `DispatchDiscardedError` while `stop()` drains, a plain stopped Error once\n * it has), boot failed, or the fold / save / sub start / handler throws.\n */\n function enqueueDispatch(msg: M): Promise<void> {\n if (gate !== \"open\") {\n return Promise.reject(\n gate === \"draining\"\n ? new DispatchDiscardedError(msg.type)\n : new Error(\"@demlik/tea: runtime stopped\"),\n );\n }\n const next = tail.then(() => {\n if (bootError !== null) throw bootError;\n return stepDispatch(msg);\n });\n // Swallow the rejection on the tail so one failing dispatch does NOT poison\n // every later one. The original `next` still rejects for the caller.\n tail = next.catch(() => {});\n return next;\n }\n\n /**\n * Drain the serial tail to quiescence. Every follow-up reassigns `tail`\n * SYNCHRONOUSLY before its parent step resolves, so awaiting the current\n * `tail` and re-reading it catches every transitively enqueued follow-up.\n * Bounded by `idleCap` — on cap REJECT with `QuiescenceTimeoutError`, keeping\n * a livelock distinguishable from quiescence (invariant 6).\n */\n async function drainToQuiescence(): Promise<void> {\n for (let i = 0; i < idleCap; i++) {\n const observed = tail;\n await observed;\n if (tail === observed) return;\n }\n throw new QuiescenceTimeoutError(idleCap);\n }\n\n /**\n * The public `dispatch`: await the Msg's OWN transition first (so its own\n * failure surfaces before any drain), then — unless `{ settle: \"once\" }` —\n * drain the transitive follow-up chain.\n */\n async function dispatchToQuiescence(\n msg: M,\n opts?: { readonly settle?: DispatchSettle },\n ): Promise<void> {\n await enqueueDispatch(msg);\n if (opts?.settle === \"once\") return;\n await drainToQuiescence();\n }\n\n // `bootPromise` is the un-swallowed promise `ready` chains off (so callers\n // see the boot error directly); `tail` gets the swallowed branch so a failed\n // boot does not poison every later dispatch's chain (each surfaces\n // `bootError` in `enqueueDispatch`).\n const bootPromise = boot();\n tail = bootPromise.catch((err) => {\n bootError = err;\n });\n\n const core: LoopHandle<S, M> = {\n [liveWork]: () => ({ subs: subRegistry.size, cmds: inFlightCmds }),\n dispatch: dispatchToQuiescence,\n dispatchOnce: enqueueDispatch,\n getState(): S {\n // TOTAL. A booted handle is only obtainable by awaiting `ready`, which\n // resolves AFTER boot set `state`.\n return state as S;\n },\n // A failed boot never hands out a handle. The forward reference resolves\n // at `.then` time, after `handle` below is built.\n ready: bootPromise.then(() => handle),\n idle: drainToQuiescence,\n async stop(): Promise<void> {\n // Open the discard window unless the gate is already `\"closed\"` (a halt,\n // or a redundant second `stop()`): the gate only advances.\n if (gate === \"open\") gate = \"draining\";\n // Report BEFORE the drain: after `await tail` the count is zero by\n // construction, and the fact worth surfacing is what was outstanding at\n // the moment the host let go (#365).\n if (inFlightCmds > 0) {\n report(new RuntimeDiscardedError(inFlightCmds), \"discard\");\n }\n // Drain in-flight work. Tail rejections were swallowed at enqueue time, so\n // this always resolves. It TERMINATES because the gate refuses the\n // follow-ups those handlers return.\n await tail;\n gate = \"closed\";\n stopSubs([...subRegistry.keys()]);\n // …then WAIT for the teardown still settling, bounded by\n // `disposeTimeoutMs` (see `drainDisposals`).\n await drainDisposals();\n // Flush final State. A save throw does not reject `stop()` (contract:\n // resolves regardless) but IS loss of the last write, so it reaches the\n // sink (invariant 6).\n if (store && state !== undefined && bootError === null) {\n try {\n await store.save(state);\n } catch (error) {\n report(error, \"stop-save\");\n }\n }\n },\n };\n const handle: LoopHandle<S, M> = Object.assign(\n core,\n ...extensions.map((ext) => ext.handle ?? {}),\n );\n return handle;\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { replay } from './chunk-
|
|
1
|
+
import { replay } from './chunk-K4ETBNKS.js';
|
|
2
2
|
import { typeOf, indexPath, unionKeys, keyPath } from './chunk-HWVW4AXC.js';
|
|
3
3
|
|
|
4
4
|
// src/internal/persistence/recorder/index.ts
|
|
@@ -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-LYVOGOBC.js.map
|
|
208
|
+
//# sourceMappingURL=chunk-LYVOGOBC.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-BCTEOVOU.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 fourth deep-compare. It is one\n * of three structural walkers in the package, divergent on purpose:\n * this `deepEqual` returns a boolean verdict (order-insensitive, for\n * replay/parity equality), `devtools`'s `diffState` returns a change list (for\n * a human-readable diff view), and `testing`'s `__deepEqual` is the assertion\n * helpers' internal compare — same shape, three 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-LYVOGOBC.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,7 +1,7 @@
|
|
|
1
1
|
import { idempotencyMemory } from './chunk-PR6TB3BL.js';
|
|
2
2
|
import { queueAdapter } from './chunk-TXN2KFHF.js';
|
|
3
3
|
import { undefinedOnly, unchecked } from './chunk-4LPY67E4.js';
|
|
4
|
-
import { Cmd } from './chunk-
|
|
4
|
+
import { Cmd } from './chunk-K4ETBNKS.js';
|
|
5
5
|
|
|
6
6
|
// src/internal/idempotency/idempotent-intake/index.ts
|
|
7
7
|
function intakeProcessDef() {
|
|
@@ -102,5 +102,5 @@ function createIntake(config) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
export { createIntake, intakeProcessDef, intakeReplayDef };
|
|
105
|
-
//# sourceMappingURL=chunk-
|
|
106
|
-
//# sourceMappingURL=chunk-
|
|
105
|
+
//# sourceMappingURL=chunk-OPH6Y4UT.js.map
|
|
106
|
+
//# sourceMappingURL=chunk-OPH6Y4UT.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/idempotency/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-MYNLY3HP.js","sourcesContent":["/**\n * internal/idempotency/idempotent-intake — receive-once intake for webhooks / queue\n * messages: dedupe by key, enqueue the new ones, replay the cached result to\n * the duplicates. The \"process this payload exactly once even though it\n * arrives N times\" knob.\n *\n * **The recipe.** This is a Level-2 composition — it owns no primitive\n * queue/cache logic of its own, it *splices two L1 bricks into one Model slice*\n * and DELEGATES every transition to their VERB SEAMS — `IdempotencyMemory`\n * (`../idempotency/adapter`) and `QueueAdapter` (`../work-queue/adapter`) —\n * so the store ops and the `QueueItem` / `IdempotencyStore` shapes stay\n * insulated behind verbs the intake calls:\n *\n * - `../idempotency/adapter` — the dedupe-by-key + last-result cache. The\n * `seen` half of the slice is its `IdempotencyStore`, keyed by\n * `keyOf(payload)`. A key is recorded once `receive` calls `memory.remember`;\n * `complete` calls `memory.remember` again to swap its cached value from\n * `pending` to the finished `result`. The cache write is the brick's, never\n * re-rolled here.\n * - `../work-queue/adapter` — the pending-work list. The `queue` half of the\n * slice is its `QueueItem<P>[]`. Every status flip routes through the\n * adapter verbs: `receive` enqueues via `queue.enqueue`, `claimNext` stamps\n * the `running` claim via `queue.patch`, `boot` re-arms a dead worker's\n * claim via `queue.patch`. Intake owns only the SELECTION (which item,\n * gated on the done-cache reconcile below) — the array transition is the\n * brick's. The consumer drains it (claim → process → `complete`) at its own\n * pace.\n *\n * Both bricks are pure-state + pure-ops, host- and clock-agnostic by\n * construction, so the combined slice inherits TEA's two non-negotiables:\n *\n * - **Durable** — the slice is a plain serializable value (a record-backed\n * idempotency store + an array of queue items, no `Map`, no closures), so\n * it survives a Durable-Object eviction / reload and rehydrates whole.\n * - **Replayable** — every verb is a pure `(state, …) => [state, Cmd[]]`\n * transition; no `Date.now()`, no `Math.random()`, no I/O inside a verb.\n * `at` (the clock) and `id` (the queue-item id) are *injected* at the call\n * site, so `trace-replay` reconstructs each transition exactly.\n *\n * **The decision lives in a Cmd, not in a return shape.** `receive` cannot\n * \"return\" what it decided (the knob contract pins the shape to\n * `[State, Cmd[]]`), so the decision is *reified as a Cmd* the runtime\n * performs:\n *\n * - first sighting of a key → mark it `pending`, enqueue the payload, emit\n * `IntakeProcess` (go run the work).\n * - a key already `pending` → drop silently (the original is in flight);\n * emit nothing.\n * - a key already `done` → emit `IntakeReplay` carrying the cached\n * result so the duplicate caller gets the SAME response the first one\n * produced, with zero re-execution of the side effect.\n *\n * **Two crash-safety guarantees the verbs enforce (and the unit specs alone\n * missed — see the wired-machine test):**\n *\n * - *A slow worker is never double-enqueued.* The TTL clock starts at\n * `complete`, not at `receive`: a `pending` entry NEVER ages out, so a\n * duplicate of an in-flight key is always dropped no matter how long the\n * worker takes. Only a `done` entry expires (see `liveEntry`).\n * - *A complete-then-crash never re-runs.* `boot` and `claimNext` reconcile\n * the queue against the done cache: a queue item whose key already resolved\n * to `done` in `seen` is skipped (never reset to pending, never claimed),\n * so a worker that finished and cached its result before the DO evicted is\n * not re-executed on resume.\n *\n * Typical wiring (the combinator form — splice ~3 hooks into a host machine):\n *\n * const intake = createIntake<StripeEvent, ChargeResult>({\n * keyOf: (e) => e.id, // Stripe's idempotency key\n * ttlMs: 24 * 60 * 60 * 1000, // a replayed webhook 24h after it COMPLETED is re-run\n * });\n *\n * // in `init`:\n * intake: intake.init(),\n * // in `update`, on the inbound-webhook Msg:\n * const [next, cmds] = intake.receive(state.intake, payload, at, id);\n * // …route `IntakeProcess` to your worker, `IntakeReplay` to the responder.\n * // on the worker's done Msg:\n * const [next, _] = intake.complete(state.intake, key, result, at);\n * // once, after boot (DO reload), before resuming the drain:\n * const [next, _] = intake.boot(state.intake);\n *\n * Escape hatch: reach the raw L1 ops via `../idempotency` and `../../work-queue`\n * and thread them by hand.\n */\n\nimport { Cmd, type CmdOf } from \"../../../index\";\nimport { unchecked, undefinedOnly } from \"../../schema\";\nimport type { QueueItem } from \"../../work-queue\";\nimport { queueAdapter } from \"../../work-queue/adapter\";\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\n\n/**\n * A cached intake entry: either a key whose work is still in flight\n * (`pending`) or one whose work has finished and whose `result` is held for\n * replay to later duplicates (`done`). Discriminated on `phase` — `pending`\n * carries no payload, `done` carries the result `R`.\n *\n * Lives inside the `seen` half of the slice (`IdempotencyStore<IntakeEntry<R>>`).\n * It is the dedupe ledger's *value*; the ledger's *key* is `keyOf(payload)`.\n */\nexport type IntakeEntry<R> =\n | { readonly phase: \"pending\" }\n | { readonly phase: \"done\"; readonly result: R };\n\n/**\n * The intake knob's config — `keyOf` derives the idempotency key from a\n * payload, `ttlMs` (optional) ages a *completed* cached key out so a webhook\n * replayed long after the work finished is treated as new and re-processed.\n *\n * **The TTL clock starts at `complete`, not at `receive`.** A `pending` entry\n * (work still in flight) NEVER ages out, no matter how slow the worker is —\n * ageing a pending key would let the duplicate re-enqueue the in-flight\n * payload and double-process it. Only a `done` entry expires, measured from\n * the `at` passed to `complete` (the moment the result was cached). Inside the\n * window a duplicate replays the cached result; at/after the window it is\n * re-accepted and re-processed.\n *\n * Omit `ttlMs` for a cache whose `done` entries never expire by age (keys live\n * forever — fine for a bounded universe of ids, unbounded growth otherwise).\n *\n * The TTL is owned HERE, not delegated to the composed `IdempotencyStore`: the\n * store's uniform per-entry TTL cannot tell `pending` from `done`, so intake\n * keeps the store TTL-free and applies the phase-aware rule itself (see\n * `liveEntry`).\n */\nexport interface IntakeConfig<P> {\n /** Derive the idempotency key from a payload. Must be pure + deterministic. */\n readonly keyOf: (payload: P) => string;\n /**\n * Age a *completed* key out after this many ms, measured from the `at`\n * passed to `complete`. Pending keys never age out.\n */\n readonly ttlMs?: number;\n}\n\n/**\n * The Model slice this knob owns. `seen` is the dedupe + last-result cache\n * (the `../idempotency` brick); `queue` is the pending-work list (the\n * `../work-queue` brick). Both are plain serializable values — the whole slice\n * round-trips through `JSON` so it survives eviction and replays exactly.\n */\nexport interface IntakeState<P, R> {\n readonly seen: IdempotencyStore<IntakeEntry<R>>;\n readonly queue: readonly QueueItem<P>[];\n}\n\n// === Cmds the verbs reify ===\n//\n// The knob contract pins verbs to `(state, …) => [State, Cmd[]]`, so a verb's\n// *decision* (new vs. duplicate-pending vs. duplicate-done) cannot be a return\n// value — it is emitted as a Cmd the runtime's interpret handler performs.\n// Each Cmd is plain data (invariant 1): the payload / key / result it carries\n// are values, never closures.\n//\n// Both are `Cmd.define`d (ADR 0014). The payload `P` / result `R` are the\n// knob's own type parameters, so each def is minted per knob by a factory\n// (`createIntake` calls it once) rather than a module-scope constant — the\n// same shape `resilient-call`'s `runCmdDef<I, R>()` takes. The host performs\n// them and settles nothing back through intake, hence `ok: undefinedOnly` and an\n// empty `err` list.\n\n/** A genuinely-new payload was accepted and enqueued — go run the work. */\nexport function intakeProcessDef<P>() {\n return Cmd.define(\"intake:process\", {\n input: unchecked<{\n /** The dedupe key the payload was filed under. */\n readonly key: string;\n /** The enqueued queue-item id (the same `id` passed to `receive`). */\n readonly itemId: string;\n /** The original payload, for the worker to process. */\n readonly payload: P;\n }>(),\n ok: undefinedOnly,\n err: [],\n });\n}\nexport type IntakeProcessCmd<P> = CmdOf<ReturnType<typeof intakeProcessDef<P>>>;\n\n/**\n * A duplicate of an already-completed key arrived — replay the cached result\n * to the caller instead of re-running the side effect.\n */\nexport function intakeReplayDef<R>() {\n return Cmd.define(\"intake:replay\", {\n input: unchecked<{\n /** The dedupe key whose cached result is being replayed. */\n readonly key: string;\n /** The result the ORIGINAL request produced, recalled from the cache. */\n readonly result: R;\n }>(),\n ok: undefinedOnly,\n err: [],\n });\n}\nexport type IntakeReplayCmd<R> = CmdOf<ReturnType<typeof intakeReplayDef<R>>>;\n\n/** The Cmd union the intake verbs emit. */\nexport type IntakeCmd<P, R> = IntakeProcessCmd<P> | IntakeReplayCmd<R>;\n\n/**\n * Build an idempotent-intake knob from `config`. Returns the uniform L2\n * contract — `init()` + the verbs (`receive`, `complete`, `boot`, `claimNext`).\n * No `subs` / `handlers`: intake owns no timers and does no I/O of its own (the\n * composed bricks are pure-ops; the worker that runs `IntakeProcess` and the\n * responder that runs `IntakeReplay` live in the host's interpret map).\n */\nexport function createIntake<P, R>(config: IntakeConfig<P>) {\n const { keyOf, ttlMs } = config;\n\n // The two store seams, bound once for this knob's `P`/`R`. The intake\n // delegates every queue status flip to `queue.*` and every dedupe-cache\n // transition to `memory.*`, so the `QueueItem<P>` / `IdempotencyStore` shapes\n // are insulated behind the verbs rather than reached by raw ops.\n const queue = queueAdapter<P>();\n const memory = idempotencyMemory<IntakeEntry<R>>();\n\n // The two Cmd constructors, minted once for this knob's `P`/`R`.\n const intakeProcess = intakeProcessDef<P>();\n const intakeReplay = intakeReplayDef<R>();\n\n /**\n * The empty slice: a TTL-free idempotency store (intake owns the TTL itself —\n * see `liveEntry`, because the store cannot tell `pending` from `done`) plus\n * an empty work queue. The rehydrate path is the host's `init(loaded)` — it\n * passes the persisted slice straight back, this is only the fresh-boot value.\n */\n function init(): IntakeState<P, R> {\n return {\n seen: memory.init(),\n queue: [],\n };\n }\n\n /**\n * The live cached entry for `key` at clock reading `at`, or `undefined` if\n * the key is absent or its `done` window has elapsed. This is intake's\n * phase-aware expiry rule, applied HERE rather than by the store's uniform\n * per-entry TTL:\n *\n * - **`pending`** — work is in flight; NEVER expires. Ageing a pending key\n * would let a slow worker's payload be re-enqueued and double-processed.\n * - **`done`** — expires `ttlMs` after the `atMs` the result was cached at\n * (the `at` passed to `complete`). Half-open, matching the store /\n * rate-limit window edge: `at - atMs >= ttlMs` reads as expired. No\n * `ttlMs` ⇒ a `done` entry never expires by age.\n *\n * PURE — reads `state.seen.entries` directly (no `Date.now`), never mutates.\n */\n function liveEntry(\n store: IdempotencyStore<IntakeEntry<R>>,\n key: string,\n at: number,\n ): IntakeEntry<R> | undefined {\n const cached = store.entries[key];\n if (cached === undefined) return undefined;\n const entry = cached.value;\n if (entry.phase === \"pending\") return entry; // in flight — never ages out\n if (ttlMs !== undefined && at - cached.atMs >= ttlMs) return undefined;\n return entry;\n }\n\n /**\n * Whether the queue item `it`'s key has already resolved to a live `done`\n * entry in `seen` (the work completed and its result is cached). Such an\n * item must NOT be re-armed by `boot` or claimed by `claimNext` — doing so\n * re-runs work whose result is already known (the complete-then-crash bug).\n *\n * `at = +Infinity` is the right reading for the reconcile check: a `done`\n * entry counts as done for reconciliation regardless of how far past its\n * replay TTL it is. (TTL governs whether a *duplicate receive* re-processes,\n * not whether a *queued, already-completed* item should re-run — that one\n * never should.)\n */\n function isAlreadyDone(state: IntakeState<P, R>, it: QueueItem<P>): boolean {\n return state.seen.entries[keyOf(it.input)]?.value.phase === \"done\";\n }\n\n /**\n * Receive a payload at clock reading `at`, filing it under the queue-item\n * id `id`. PURE — returns a NEW slice; the input is untouched.\n *\n * Three outcomes, each reified as a Cmd (see the Cmd union above):\n *\n * 1. **New key** (`liveEntry` is absent at `at`) — `remember` the key as\n * `pending`, `enqueue` the payload onto the work queue, and emit\n * `intake:process`. The only path that grows either half of the slice.\n * 2. **Duplicate, still pending** — the original is in flight. Drop\n * silently: the slice is returned unchanged and no Cmd is emitted, so\n * the duplicate neither re-enqueues nor double-processes. A pending\n * entry never ages out, so this holds no matter how slow the worker is.\n * 3. **Duplicate, already done (within TTL)** — emit `intake:replay`\n * carrying the cached `result` so the caller gets the original response.\n * The slice is unchanged (a replay touches neither cache nor queue). A\n * `done` key past its `ttlMs` window reads as absent and falls to (1).\n *\n * `id` is injected (work-queue's `genId` reads `Date.now()` / `crypto`, so\n * it cannot be called inside a pure verb). Generate it at the effect\n * boundary and thread it in; it becomes the enqueued item's id AND the\n * `itemId` on the emitted `intake:process` Cmd, so the worker can address the\n * exact queue item it must `complete`.\n */\n function receive(\n state: IntakeState<P, R>,\n payload: P,\n at: number,\n id: string,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const key = keyOf(payload);\n\n // Duplicate path — the key has a live entry at `at` (pending never ages;\n // done ages only past `ttlMs` measured from `complete`).\n const entry = liveEntry(state.seen, key, at);\n if (entry !== undefined) {\n if (entry.phase === \"done\") {\n // Replay the original result; touch nothing.\n return [state, [intakeReplay({ key, result: entry.result })]];\n }\n // phase === \"pending\": original is in flight — drop silently.\n return [state, []];\n }\n\n // New path — record the key as pending, enqueue the payload, process it.\n const seenNext = memory.remember(state.seen, key, { phase: \"pending\" }, at);\n const { next: queueNext, item } = queue.enqueue(\n state.queue,\n payload,\n at,\n id,\n );\n const nextState: IntakeState<P, R> = { seen: seenNext, queue: queueNext };\n return [nextState, [intakeProcess({ key, itemId: item.id, payload })]];\n }\n\n /**\n * Record the finished `result` for `key` at clock reading `at`. PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds (completion is\n * a pure cache write; the host marks the queue item done via the work-queue\n * adapter at the effect boundary).\n *\n * Swaps the key's cached entry from `pending` to `done` so every subsequent\n * `receive` of the same key replays this `result`. `remember` stamps the\n * entry's `atMs` to `at`, which is the moment intake's `done`-TTL clock\n * starts (see `liveEntry`): a completed key stays replayable for `ttlMs`\n * past completion, not past first receipt.\n *\n * `complete` does NOT remove the queue item — the queue is the host's drain\n * ledger, and which items leave it (and when) is the host's call via the\n * work-queue adapter. `complete` owns the cache half only.\n */\n function complete(\n state: IntakeState<P, R>,\n key: string,\n result: R,\n at: number,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const seenNext = memory.remember(\n state.seen,\n key,\n { phase: \"done\", result },\n at,\n );\n return [{ ...state, seen: seenNext }, []];\n }\n\n /**\n * Resume after a boot (Durable-Object reload / process restart). PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds.\n *\n * Resets every `running` queue item back to `pending` so the host's drain\n * re-claims it — EXCEPT an item whose key already resolved to `done` in\n * `seen`. That item's worker finished and cached its result before the crash;\n * re-arming it would re-run completed work (the complete-then-crash bug). It\n * is left as-is (and `claimNext` will likewise skip it), so the host drains\n * it via `markDone` without ever re-executing the side effect.\n *\n * A `running` item still `pending` in the cache is one a now-dead worker\n * claimed but never finished — that one IS reset, so it gets re-claimed.\n *\n * Returns the same reference when nothing changed (no running item needed\n * re-arming) so the host can skip a redundant save.\n *\n * Call once from a `boot` Msg the host dispatches after `run(...)` returns —\n * never from `init`'s rehydrate branch (that branch must be a pure\n * passthrough; see the substrate's `init` contract).\n */\n function boot(\n state: IntakeState<P, R>,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n // Selection lives HERE (the done-cache reconcile is intake's own rule); the\n // status FLIP is delegated to the `QueueAdapter`. A blanket reset would\n // re-arm completed-then-crashed items (it cannot see the done cache), so\n // intake picks the items and `queue.patch` performs each transition — the\n // same spread the work-queue lifecycle uses, not a re-rolled one.\n let items = state.queue;\n let changed = false;\n for (const it of state.queue) {\n if (it.status !== \"running\") continue;\n // Reconcile against the done cache: a completed-then-crashed item must\n // NOT be re-armed (it would re-run already-cached work).\n if (isAlreadyDone(state, it)) continue;\n const flip = queue.patch(items, it.id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n }));\n items = flip.next;\n changed = changed || flip.changed;\n }\n if (!changed) return [state, []];\n return [{ ...state, queue: items }, []];\n }\n\n /**\n * Claim the next pending queue item, flipping it `running` and stamping\n * `startedAt` to `at`. PURE — returns a NEW slice plus the claimed item, or\n * `null` if nothing claimable remains.\n *\n * \"Claimable\" is a pending item WHOSE KEY IS NOT ALREADY `done` in `seen`.\n * An item whose key completed before a crash (its `intake:process` ran, work\n * finished, result cached, but the item never left the queue) is skipped —\n * claiming it would re-run already-cached work. Such items are inert here;\n * the host drains them via `markDone`.\n *\n * This is the drain side of the intake, the mirror of `receive`'s enqueue.\n * It is a pure helper (not a verb that emits Cmds) because \"what to do with\n * the claimed item\" is the host's decision — typically: dispatch the worker,\n * await it, then call `complete`. Threading it through the slice keeps the\n * claim durable: the `running` stamp survives eviction, and `boot` re-arms\n * it if the worker died.\n */\n function claimNext(\n state: IntakeState<P, R>,\n at: number,\n ): {\n readonly state: IntakeState<P, R>;\n readonly claimed: QueueItem<P>;\n } | null {\n // Selection lives HERE (skip an item whose key already resolved `done`);\n // the status FLIP is delegated to the `QueueAdapter`. A blind `queue.claim`\n // would take the first PENDING item regardless of the done cache, which\n // would re-run already-cached work — so intake selects the eligible item and\n // `queue.patch` performs the running-stamp transition (the work-queue\n // lifecycle's own spread, not a re-rolled one).\n const target = state.queue.find(\n (it) => it.status === \"pending\" && !isAlreadyDone(state, it),\n );\n if (target === undefined) return null;\n // The patch closure is the single place the claimed item is materialized;\n // capture it here so the result needs no re-scan of the new queue.\n let claimed: QueueItem<P> = target;\n const { next } = queue.patch(state.queue, target.id, (item) => {\n claimed = { ...item, status: \"running\", startedAt: at };\n return claimed;\n });\n return { state: { ...state, queue: next }, claimed };\n }\n\n return { init, receive, complete, boot, claimNext };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/idempotency/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-OPH6Y4UT.js","sourcesContent":["/**\n * internal/idempotency/idempotent-intake — receive-once intake for webhooks / queue\n * messages: dedupe by key, enqueue the new ones, replay the cached result to\n * the duplicates. The \"process this payload exactly once even though it\n * arrives N times\" knob.\n *\n * **The recipe.** This is a Level-2 composition — it owns no primitive\n * queue/cache logic of its own, it *splices two L1 bricks into one Model slice*\n * and DELEGATES every transition to their VERB SEAMS — `IdempotencyMemory`\n * (`../idempotency/adapter`) and `QueueAdapter` (`../work-queue/adapter`) —\n * so the store ops and the `QueueItem` / `IdempotencyStore` shapes stay\n * insulated behind verbs the intake calls:\n *\n * - `../idempotency/adapter` — the dedupe-by-key + last-result cache. The\n * `seen` half of the slice is its `IdempotencyStore`, keyed by\n * `keyOf(payload)`. A key is recorded once `receive` calls `memory.remember`;\n * `complete` calls `memory.remember` again to swap its cached value from\n * `pending` to the finished `result`. The cache write is the brick's, never\n * re-rolled here.\n * - `../work-queue/adapter` — the pending-work list. The `queue` half of the\n * slice is its `QueueItem<P>[]`. Every status flip routes through the\n * adapter verbs: `receive` enqueues via `queue.enqueue`, `claimNext` stamps\n * the `running` claim via `queue.patch`, `boot` re-arms a dead worker's\n * claim via `queue.patch`. Intake owns only the SELECTION (which item,\n * gated on the done-cache reconcile below) — the array transition is the\n * brick's. The consumer drains it (claim → process → `complete`) at its own\n * pace.\n *\n * Both bricks are pure-state + pure-ops, host- and clock-agnostic by\n * construction, so the combined slice inherits TEA's two non-negotiables:\n *\n * - **Durable** — the slice is a plain serializable value (a record-backed\n * idempotency store + an array of queue items, no `Map`, no closures), so\n * it survives a Durable-Object eviction / reload and rehydrates whole.\n * - **Replayable** — every verb is a pure `(state, …) => [state, Cmd[]]`\n * transition; no `Date.now()`, no `Math.random()`, no I/O inside a verb.\n * `at` (the clock) and `id` (the queue-item id) are *injected* at the call\n * site, so `trace-replay` reconstructs each transition exactly.\n *\n * **The decision lives in a Cmd, not in a return shape.** `receive` cannot\n * \"return\" what it decided (the knob contract pins the shape to\n * `[State, Cmd[]]`), so the decision is *reified as a Cmd* the runtime\n * performs:\n *\n * - first sighting of a key → mark it `pending`, enqueue the payload, emit\n * `IntakeProcess` (go run the work).\n * - a key already `pending` → drop silently (the original is in flight);\n * emit nothing.\n * - a key already `done` → emit `IntakeReplay` carrying the cached\n * result so the duplicate caller gets the SAME response the first one\n * produced, with zero re-execution of the side effect.\n *\n * **Two crash-safety guarantees the verbs enforce (and the unit specs alone\n * missed — see the wired-machine test):**\n *\n * - *A slow worker is never double-enqueued.* The TTL clock starts at\n * `complete`, not at `receive`: a `pending` entry NEVER ages out, so a\n * duplicate of an in-flight key is always dropped no matter how long the\n * worker takes. Only a `done` entry expires (see `liveEntry`).\n * - *A complete-then-crash never re-runs.* `boot` and `claimNext` reconcile\n * the queue against the done cache: a queue item whose key already resolved\n * to `done` in `seen` is skipped (never reset to pending, never claimed),\n * so a worker that finished and cached its result before the DO evicted is\n * not re-executed on resume.\n *\n * Typical wiring (the combinator form — splice ~3 hooks into a host machine):\n *\n * const intake = createIntake<StripeEvent, ChargeResult>({\n * keyOf: (e) => e.id, // Stripe's idempotency key\n * ttlMs: 24 * 60 * 60 * 1000, // a replayed webhook 24h after it COMPLETED is re-run\n * });\n *\n * // in `init`:\n * intake: intake.init(),\n * // in `update`, on the inbound-webhook Msg:\n * const [next, cmds] = intake.receive(state.intake, payload, at, id);\n * // …route `IntakeProcess` to your worker, `IntakeReplay` to the responder.\n * // on the worker's done Msg:\n * const [next, _] = intake.complete(state.intake, key, result, at);\n * // once, after boot (DO reload), before resuming the drain:\n * const [next, _] = intake.boot(state.intake);\n *\n * Escape hatch: reach the raw L1 ops via `../idempotency` and `../../work-queue`\n * and thread them by hand.\n */\n\nimport { Cmd, type CmdOf } from \"../../../index\";\nimport { unchecked, undefinedOnly } from \"../../schema\";\nimport type { QueueItem } from \"../../work-queue\";\nimport { queueAdapter } from \"../../work-queue/adapter\";\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\n\n/**\n * A cached intake entry: either a key whose work is still in flight\n * (`pending`) or one whose work has finished and whose `result` is held for\n * replay to later duplicates (`done`). Discriminated on `phase` — `pending`\n * carries no payload, `done` carries the result `R`.\n *\n * Lives inside the `seen` half of the slice (`IdempotencyStore<IntakeEntry<R>>`).\n * It is the dedupe ledger's *value*; the ledger's *key* is `keyOf(payload)`.\n */\nexport type IntakeEntry<R> =\n | { readonly phase: \"pending\" }\n | { readonly phase: \"done\"; readonly result: R };\n\n/**\n * The intake knob's config — `keyOf` derives the idempotency key from a\n * payload, `ttlMs` (optional) ages a *completed* cached key out so a webhook\n * replayed long after the work finished is treated as new and re-processed.\n *\n * **The TTL clock starts at `complete`, not at `receive`.** A `pending` entry\n * (work still in flight) NEVER ages out, no matter how slow the worker is —\n * ageing a pending key would let the duplicate re-enqueue the in-flight\n * payload and double-process it. Only a `done` entry expires, measured from\n * the `at` passed to `complete` (the moment the result was cached). Inside the\n * window a duplicate replays the cached result; at/after the window it is\n * re-accepted and re-processed.\n *\n * Omit `ttlMs` for a cache whose `done` entries never expire by age (keys live\n * forever — fine for a bounded universe of ids, unbounded growth otherwise).\n *\n * The TTL is owned HERE, not delegated to the composed `IdempotencyStore`: the\n * store's uniform per-entry TTL cannot tell `pending` from `done`, so intake\n * keeps the store TTL-free and applies the phase-aware rule itself (see\n * `liveEntry`).\n */\nexport interface IntakeConfig<P> {\n /** Derive the idempotency key from a payload. Must be pure + deterministic. */\n readonly keyOf: (payload: P) => string;\n /**\n * Age a *completed* key out after this many ms, measured from the `at`\n * passed to `complete`. Pending keys never age out.\n */\n readonly ttlMs?: number;\n}\n\n/**\n * The Model slice this knob owns. `seen` is the dedupe + last-result cache\n * (the `../idempotency` brick); `queue` is the pending-work list (the\n * `../work-queue` brick). Both are plain serializable values — the whole slice\n * round-trips through `JSON` so it survives eviction and replays exactly.\n */\nexport interface IntakeState<P, R> {\n readonly seen: IdempotencyStore<IntakeEntry<R>>;\n readonly queue: readonly QueueItem<P>[];\n}\n\n// === Cmds the verbs reify ===\n//\n// The knob contract pins verbs to `(state, …) => [State, Cmd[]]`, so a verb's\n// *decision* (new vs. duplicate-pending vs. duplicate-done) cannot be a return\n// value — it is emitted as a Cmd the runtime's interpret handler performs.\n// Each Cmd is plain data (invariant 1): the payload / key / result it carries\n// are values, never closures.\n//\n// Both are `Cmd.define`d (ADR 0014). The payload `P` / result `R` are the\n// knob's own type parameters, so each def is minted per knob by a factory\n// (`createIntake` calls it once) rather than a module-scope constant — the\n// same shape `resilient-call`'s `runCmdDef<I, R>()` takes. The host performs\n// them and settles nothing back through intake, hence `ok: undefinedOnly` and an\n// empty `err` list.\n\n/** A genuinely-new payload was accepted and enqueued — go run the work. */\nexport function intakeProcessDef<P>() {\n return Cmd.define(\"intake:process\", {\n input: unchecked<{\n /** The dedupe key the payload was filed under. */\n readonly key: string;\n /** The enqueued queue-item id (the same `id` passed to `receive`). */\n readonly itemId: string;\n /** The original payload, for the worker to process. */\n readonly payload: P;\n }>(),\n ok: undefinedOnly,\n err: [],\n });\n}\nexport type IntakeProcessCmd<P> = CmdOf<ReturnType<typeof intakeProcessDef<P>>>;\n\n/**\n * A duplicate of an already-completed key arrived — replay the cached result\n * to the caller instead of re-running the side effect.\n */\nexport function intakeReplayDef<R>() {\n return Cmd.define(\"intake:replay\", {\n input: unchecked<{\n /** The dedupe key whose cached result is being replayed. */\n readonly key: string;\n /** The result the ORIGINAL request produced, recalled from the cache. */\n readonly result: R;\n }>(),\n ok: undefinedOnly,\n err: [],\n });\n}\nexport type IntakeReplayCmd<R> = CmdOf<ReturnType<typeof intakeReplayDef<R>>>;\n\n/** The Cmd union the intake verbs emit. */\nexport type IntakeCmd<P, R> = IntakeProcessCmd<P> | IntakeReplayCmd<R>;\n\n/**\n * Build an idempotent-intake knob from `config`. Returns the uniform L2\n * contract — `init()` + the verbs (`receive`, `complete`, `boot`, `claimNext`).\n * No `subs` / `handlers`: intake owns no timers and does no I/O of its own (the\n * composed bricks are pure-ops; the worker that runs `IntakeProcess` and the\n * responder that runs `IntakeReplay` live in the host's interpret map).\n */\nexport function createIntake<P, R>(config: IntakeConfig<P>) {\n const { keyOf, ttlMs } = config;\n\n // The two store seams, bound once for this knob's `P`/`R`. The intake\n // delegates every queue status flip to `queue.*` and every dedupe-cache\n // transition to `memory.*`, so the `QueueItem<P>` / `IdempotencyStore` shapes\n // are insulated behind the verbs rather than reached by raw ops.\n const queue = queueAdapter<P>();\n const memory = idempotencyMemory<IntakeEntry<R>>();\n\n // The two Cmd constructors, minted once for this knob's `P`/`R`.\n const intakeProcess = intakeProcessDef<P>();\n const intakeReplay = intakeReplayDef<R>();\n\n /**\n * The empty slice: a TTL-free idempotency store (intake owns the TTL itself —\n * see `liveEntry`, because the store cannot tell `pending` from `done`) plus\n * an empty work queue. The rehydrate path is the host's `init(loaded)` — it\n * passes the persisted slice straight back, this is only the fresh-boot value.\n */\n function init(): IntakeState<P, R> {\n return {\n seen: memory.init(),\n queue: [],\n };\n }\n\n /**\n * The live cached entry for `key` at clock reading `at`, or `undefined` if\n * the key is absent or its `done` window has elapsed. This is intake's\n * phase-aware expiry rule, applied HERE rather than by the store's uniform\n * per-entry TTL:\n *\n * - **`pending`** — work is in flight; NEVER expires. Ageing a pending key\n * would let a slow worker's payload be re-enqueued and double-processed.\n * - **`done`** — expires `ttlMs` after the `atMs` the result was cached at\n * (the `at` passed to `complete`). Half-open, matching the store /\n * rate-limit window edge: `at - atMs >= ttlMs` reads as expired. No\n * `ttlMs` ⇒ a `done` entry never expires by age.\n *\n * PURE — reads `state.seen.entries` directly (no `Date.now`), never mutates.\n */\n function liveEntry(\n store: IdempotencyStore<IntakeEntry<R>>,\n key: string,\n at: number,\n ): IntakeEntry<R> | undefined {\n const cached = store.entries[key];\n if (cached === undefined) return undefined;\n const entry = cached.value;\n if (entry.phase === \"pending\") return entry; // in flight — never ages out\n if (ttlMs !== undefined && at - cached.atMs >= ttlMs) return undefined;\n return entry;\n }\n\n /**\n * Whether the queue item `it`'s key has already resolved to a live `done`\n * entry in `seen` (the work completed and its result is cached). Such an\n * item must NOT be re-armed by `boot` or claimed by `claimNext` — doing so\n * re-runs work whose result is already known (the complete-then-crash bug).\n *\n * `at = +Infinity` is the right reading for the reconcile check: a `done`\n * entry counts as done for reconciliation regardless of how far past its\n * replay TTL it is. (TTL governs whether a *duplicate receive* re-processes,\n * not whether a *queued, already-completed* item should re-run — that one\n * never should.)\n */\n function isAlreadyDone(state: IntakeState<P, R>, it: QueueItem<P>): boolean {\n return state.seen.entries[keyOf(it.input)]?.value.phase === \"done\";\n }\n\n /**\n * Receive a payload at clock reading `at`, filing it under the queue-item\n * id `id`. PURE — returns a NEW slice; the input is untouched.\n *\n * Three outcomes, each reified as a Cmd (see the Cmd union above):\n *\n * 1. **New key** (`liveEntry` is absent at `at`) — `remember` the key as\n * `pending`, `enqueue` the payload onto the work queue, and emit\n * `intake:process`. The only path that grows either half of the slice.\n * 2. **Duplicate, still pending** — the original is in flight. Drop\n * silently: the slice is returned unchanged and no Cmd is emitted, so\n * the duplicate neither re-enqueues nor double-processes. A pending\n * entry never ages out, so this holds no matter how slow the worker is.\n * 3. **Duplicate, already done (within TTL)** — emit `intake:replay`\n * carrying the cached `result` so the caller gets the original response.\n * The slice is unchanged (a replay touches neither cache nor queue). A\n * `done` key past its `ttlMs` window reads as absent and falls to (1).\n *\n * `id` is injected (work-queue's `genId` reads `Date.now()` / `crypto`, so\n * it cannot be called inside a pure verb). Generate it at the effect\n * boundary and thread it in; it becomes the enqueued item's id AND the\n * `itemId` on the emitted `intake:process` Cmd, so the worker can address the\n * exact queue item it must `complete`.\n */\n function receive(\n state: IntakeState<P, R>,\n payload: P,\n at: number,\n id: string,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const key = keyOf(payload);\n\n // Duplicate path — the key has a live entry at `at` (pending never ages;\n // done ages only past `ttlMs` measured from `complete`).\n const entry = liveEntry(state.seen, key, at);\n if (entry !== undefined) {\n if (entry.phase === \"done\") {\n // Replay the original result; touch nothing.\n return [state, [intakeReplay({ key, result: entry.result })]];\n }\n // phase === \"pending\": original is in flight — drop silently.\n return [state, []];\n }\n\n // New path — record the key as pending, enqueue the payload, process it.\n const seenNext = memory.remember(state.seen, key, { phase: \"pending\" }, at);\n const { next: queueNext, item } = queue.enqueue(\n state.queue,\n payload,\n at,\n id,\n );\n const nextState: IntakeState<P, R> = { seen: seenNext, queue: queueNext };\n return [nextState, [intakeProcess({ key, itemId: item.id, payload })]];\n }\n\n /**\n * Record the finished `result` for `key` at clock reading `at`. PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds (completion is\n * a pure cache write; the host marks the queue item done via the work-queue\n * adapter at the effect boundary).\n *\n * Swaps the key's cached entry from `pending` to `done` so every subsequent\n * `receive` of the same key replays this `result`. `remember` stamps the\n * entry's `atMs` to `at`, which is the moment intake's `done`-TTL clock\n * starts (see `liveEntry`): a completed key stays replayable for `ttlMs`\n * past completion, not past first receipt.\n *\n * `complete` does NOT remove the queue item — the queue is the host's drain\n * ledger, and which items leave it (and when) is the host's call via the\n * work-queue adapter. `complete` owns the cache half only.\n */\n function complete(\n state: IntakeState<P, R>,\n key: string,\n result: R,\n at: number,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const seenNext = memory.remember(\n state.seen,\n key,\n { phase: \"done\", result },\n at,\n );\n return [{ ...state, seen: seenNext }, []];\n }\n\n /**\n * Resume after a boot (Durable-Object reload / process restart). PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds.\n *\n * Resets every `running` queue item back to `pending` so the host's drain\n * re-claims it — EXCEPT an item whose key already resolved to `done` in\n * `seen`. That item's worker finished and cached its result before the crash;\n * re-arming it would re-run completed work (the complete-then-crash bug). It\n * is left as-is (and `claimNext` will likewise skip it), so the host drains\n * it via `markDone` without ever re-executing the side effect.\n *\n * A `running` item still `pending` in the cache is one a now-dead worker\n * claimed but never finished — that one IS reset, so it gets re-claimed.\n *\n * Returns the same reference when nothing changed (no running item needed\n * re-arming) so the host can skip a redundant save.\n *\n * Call once from a `boot` Msg the host dispatches after `run(...)` returns —\n * never from `init`'s rehydrate branch (that branch must be a pure\n * passthrough; see the substrate's `init` contract).\n */\n function boot(\n state: IntakeState<P, R>,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n // Selection lives HERE (the done-cache reconcile is intake's own rule); the\n // status FLIP is delegated to the `QueueAdapter`. A blanket reset would\n // re-arm completed-then-crashed items (it cannot see the done cache), so\n // intake picks the items and `queue.patch` performs each transition — the\n // same spread the work-queue lifecycle uses, not a re-rolled one.\n let items = state.queue;\n let changed = false;\n for (const it of state.queue) {\n if (it.status !== \"running\") continue;\n // Reconcile against the done cache: a completed-then-crashed item must\n // NOT be re-armed (it would re-run already-cached work).\n if (isAlreadyDone(state, it)) continue;\n const flip = queue.patch(items, it.id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n }));\n items = flip.next;\n changed = changed || flip.changed;\n }\n if (!changed) return [state, []];\n return [{ ...state, queue: items }, []];\n }\n\n /**\n * Claim the next pending queue item, flipping it `running` and stamping\n * `startedAt` to `at`. PURE — returns a NEW slice plus the claimed item, or\n * `null` if nothing claimable remains.\n *\n * \"Claimable\" is a pending item WHOSE KEY IS NOT ALREADY `done` in `seen`.\n * An item whose key completed before a crash (its `intake:process` ran, work\n * finished, result cached, but the item never left the queue) is skipped —\n * claiming it would re-run already-cached work. Such items are inert here;\n * the host drains them via `markDone`.\n *\n * This is the drain side of the intake, the mirror of `receive`'s enqueue.\n * It is a pure helper (not a verb that emits Cmds) because \"what to do with\n * the claimed item\" is the host's decision — typically: dispatch the worker,\n * await it, then call `complete`. Threading it through the slice keeps the\n * claim durable: the `running` stamp survives eviction, and `boot` re-arms\n * it if the worker died.\n */\n function claimNext(\n state: IntakeState<P, R>,\n at: number,\n ): {\n readonly state: IntakeState<P, R>;\n readonly claimed: QueueItem<P>;\n } | null {\n // Selection lives HERE (skip an item whose key already resolved `done`);\n // the status FLIP is delegated to the `QueueAdapter`. A blind `queue.claim`\n // would take the first PENDING item regardless of the done cache, which\n // would re-run already-cached work — so intake selects the eligible item and\n // `queue.patch` performs the running-stamp transition (the work-queue\n // lifecycle's own spread, not a re-rolled one).\n const target = state.queue.find(\n (it) => it.status === \"pending\" && !isAlreadyDone(state, it),\n );\n if (target === undefined) return null;\n // The patch closure is the single place the claimed item is materialized;\n // capture it here so the result needs no re-scan of the new queue.\n let claimed: QueueItem<P> = target;\n const { next } = queue.patch(state.queue, target.id, (item) => {\n claimed = { ...item, status: \"running\", startedAt: at };\n return claimed;\n });\n return { state: { ...state, queue: next }, claimed };\n }\n\n return { init, receive, complete, boot, claimNext };\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createResilientCall } from './chunk-
|
|
1
|
+
import { createResilientCall } from './chunk-6XBEYZME.js';
|
|
2
2
|
|
|
3
3
|
// src/internal/paginate/paginator/index.ts
|
|
4
4
|
var defaultPaginatorPolicy = {
|
|
@@ -166,5 +166,5 @@ function liftWalk(state, [slice, cmds]) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
export { PAGE_KEY, createPaginatedWalk, defaultPaginatorPolicy, drain, initPaginator, isDone, liftWalk, recordPage, resume, start };
|
|
169
|
-
//# sourceMappingURL=chunk-
|
|
170
|
-
//# sourceMappingURL=chunk-
|
|
169
|
+
//# sourceMappingURL=chunk-OYR7ENKU.js.map
|
|
170
|
+
//# sourceMappingURL=chunk-OYR7ENKU.js.map
|