@demlik/tea 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -20
- package/dist/agent/index.d.ts +48 -17
- package/dist/agent/index.js +10 -10
- package/dist/{chunk-IHUNKHDX.js → chunk-4I2LCQKB.js} +3 -3
- package/dist/{chunk-IHUNKHDX.js.map → chunk-4I2LCQKB.js.map} +1 -1
- package/dist/{chunk-XVIKSRCK.js → chunk-6NIQIUGR.js} +4 -4
- package/dist/{chunk-XVIKSRCK.js.map → chunk-6NIQIUGR.js.map} +1 -1
- package/dist/{chunk-JVKU2UQC.js → chunk-7Z2IDZ7R.js} +3 -3
- package/dist/{chunk-JVKU2UQC.js.map → chunk-7Z2IDZ7R.js.map} +1 -1
- package/dist/{chunk-H3MWNH7A.js → chunk-BFSZ4L6Y.js} +4 -4
- package/dist/{chunk-H3MWNH7A.js.map → chunk-BFSZ4L6Y.js.map} +1 -1
- package/dist/{chunk-H2ELKHI6.js → chunk-EB5OMOXX.js} +4 -4
- package/dist/chunk-EB5OMOXX.js.map +1 -0
- package/dist/{chunk-AIIXV4XB.js → chunk-GAVLND5C.js} +19 -4
- package/dist/chunk-GAVLND5C.js.map +1 -0
- package/dist/{chunk-I7E7AD2S.js → chunk-HU3Y2AZY.js} +4 -4
- package/dist/chunk-HU3Y2AZY.js.map +1 -0
- package/dist/{chunk-WTSOW2MD.js → chunk-MBQR3LFW.js} +5 -5
- package/dist/{chunk-WTSOW2MD.js.map → chunk-MBQR3LFW.js.map} +1 -1
- package/dist/{chunk-C3G2QFEZ.js → chunk-QIE4Y6BK.js} +4 -4
- package/dist/{chunk-C3G2QFEZ.js.map → chunk-QIE4Y6BK.js.map} +1 -1
- package/dist/{chunk-BRX3243I.js → chunk-TMYUDZLE.js} +3 -3
- package/dist/{chunk-BRX3243I.js.map → chunk-TMYUDZLE.js.map} +1 -1
- package/dist/{chunk-FNGZMVIT.js → chunk-VHKDUBXC.js} +3 -3
- package/dist/chunk-VHKDUBXC.js.map +1 -0
- package/dist/{chunk-AHW2CLPT.js → chunk-VM7PMFV5.js} +70 -47
- package/dist/chunk-VM7PMFV5.js.map +1 -0
- package/dist/{chunk-PCJA2HTZ.js → chunk-W6XWI6QE.js} +20 -20
- package/dist/chunk-W6XWI6QE.js.map +1 -0
- package/dist/{chunk-JYNYG5SF.js → chunk-YCJPLPQQ.js} +3 -3
- package/dist/chunk-YCJPLPQQ.js.map +1 -0
- package/dist/{chunk-YIMOFXST.js → chunk-YDF6P74H.js} +3 -3
- package/dist/chunk-YDF6P74H.js.map +1 -0
- package/dist/{chunk-JVE2LBFJ.js → chunk-Z7IL735U.js} +3 -3
- package/dist/{chunk-JVE2LBFJ.js.map → chunk-Z7IL735U.js.map} +1 -1
- package/dist/{chunk-PQSDUX6B.js → chunk-ZV4WGW74.js} +3 -3
- package/dist/{chunk-PQSDUX6B.js.map → chunk-ZV4WGW74.js.map} +1 -1
- package/dist/{core-Ce9ncpuv.d.ts → core-BKJW1Kmq.d.ts} +154 -2
- package/dist/devtools/index.d.ts +17 -0
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +17 -15
- package/dist/do/index.js +11 -11
- package/dist/do/index.js.map +1 -1
- package/dist/{drive-loop-CsUsW3Wo.d.ts → drive-loop-Dc6Z8Vbs.d.ts} +1 -1
- package/dist/{durable-effects-BmUrRA-h.d.ts → durable-effects-CgrLNccA.d.ts} +1 -1
- package/dist/effect/index.d.ts +191 -5
- package/dist/effect/index.js +71 -4
- package/dist/effect/index.js.map +1 -1
- package/dist/extension/index.d.ts +84 -3
- package/dist/extension/index.js +2 -2
- package/dist/extension/index.js.map +1 -1
- package/dist/flow/index.d.ts +33 -13
- package/dist/flow/index.js +13 -12
- package/dist/flow/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +11 -3
- package/dist/idempotency/index.js +3 -3
- package/dist/{index-Ce5GYAuI.d.ts → index-1hit3dJ9.d.ts} +2 -2
- package/dist/{index-2kNxy6az.d.ts → index-B-VC4qjx.d.ts} +16 -4
- package/dist/{index-Bf9_bNql.d.ts → index-BjwRcjBi.d.ts} +2 -2
- package/dist/{index-Dy3379ve.d.ts → index-CYrUWi1c.d.ts} +4 -4
- package/dist/{index-DWdg7R1l.d.ts → index-Cuy1_xTh.d.ts} +5 -1
- package/dist/{index-D8sYVzCI.d.ts → index-DB8Yn0-K.d.ts} +6 -1
- package/dist/{index-e36uEyl4.d.ts → index-Diyo8WBO.d.ts} +2 -2
- package/dist/{index-B1Wzarkl.d.ts → index-Z6Uw7C9t.d.ts} +1 -1
- package/dist/index.d.ts +70 -9
- package/dist/index.js +2 -2
- package/dist/jev/index.d.ts +46 -34
- package/dist/jev/index.js +12 -8
- package/dist/jev/index.js.map +1 -1
- package/dist/{machine-CuojfZup.d.ts → machine-BuPTS1QH.d.ts} +11 -9
- package/dist/machine-viz/index.d.ts +2 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +2 -2
- package/dist/mem/index.js +2 -2
- package/dist/node/index.d.ts +11 -2
- package/dist/node/index.js +2 -2
- package/dist/node/index.js.map +1 -1
- package/dist/otel/index.d.ts +10 -10
- package/dist/otel/index.js +10 -10
- package/dist/paginate/index.d.ts +5 -4
- package/dist/paginate/index.js +5 -5
- package/dist/parity/index.d.ts +3 -3
- package/dist/parity/index.js +3 -3
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +2 -2
- package/dist/persistence/index.d.ts +5 -5
- package/dist/persistence/index.js +4 -4
- package/dist/promise/index.d.ts +30 -10
- package/dist/promise/index.js +4 -4
- package/dist/react/index.d.ts +7 -6
- package/dist/react/index.js.map +1 -1
- package/dist/resilience/index.d.ts +7 -6
- package/dist/resilience/index.js +5 -5
- package/dist/resilience/index.js.map +1 -1
- package/dist/{index-BnsAqn0-.d.ts → run-D0sxbyno.d.ts} +23 -7
- package/dist/{runtime-types-BKg2CGMx.d.ts → runtime-types-BPKp2bGo.d.ts} +120 -9
- package/dist/testing/effect/index.d.ts +5 -5
- package/dist/testing/effect/index.js +8 -8
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +3 -3
- package/dist/testing/promise/index.d.ts +4 -4
- package/dist/testing/promise/index.js +5 -5
- package/dist/timing/index.d.ts +4 -4
- package/dist/timing/index.js +4 -4
- package/dist/types-BiXj7k_k.d.ts +9 -0
- package/dist/work-queue/index.d.ts +4 -4
- package/dist/work-queue/index.js +2 -2
- package/dist/work-queue/index.js.map +1 -1
- package/package.json +6 -3
- package/dist/chunk-AHW2CLPT.js.map +0 -1
- package/dist/chunk-AIIXV4XB.js.map +0 -1
- package/dist/chunk-FNGZMVIT.js.map +0 -1
- package/dist/chunk-H2ELKHI6.js.map +0 -1
- package/dist/chunk-I7E7AD2S.js.map +0 -1
- package/dist/chunk-JYNYG5SF.js.map +0 -1
- package/dist/chunk-PCJA2HTZ.js.map +0 -1
- package/dist/chunk-YIMOFXST.js.map +0 -1
- package/dist/types-SzNSHKlV.d.ts +0 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/effect/failures.ts","../src/effect/handle.ts","../src/effect/run.ts"],"names":["Effect","Exit"],"mappings":"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","file":"chunk-HU3Y2AZY.js","sourcesContent":["/**\n * The Effect engine's typed failures (#308, ruling R4.1 of #325), and the edge\n * that turns what the shared loop rejects with into them.\n *\n * The loop is engine-neutral: it rejects a dispatch with whatever was thrown.\n * Two throws it cannot tell apart from a bug — a hand-written cell's declared\n * failure and a store's failed save — are wrapped here on the way in, so the\n * edge can read them on the way out. The wrappers never reach the caller: a\n * handle verb fails with the typed value, and the `onError` sink is handed the\n * value it was always handed.\n */\n\nimport { Data, Exit } from \"effect\";\nimport {\n type ExtensionFactory,\n RuntimeStoppedError,\n} from \"../internal/engine/loop\";\nimport {\n DispatchDiscardedError,\n type Store,\n StoreRefusedError,\n} from \"../runtime-types\";\n\n/**\n * A dispatch the run refused because it is stopping or has stopped. The Msg\n * was never folded.\n *\n * `when` is `\"stopping\"` for a Msg that arrived while `stop()` (or a closing\n * Scope) drained the run's in-flight work, and `\"stopped\"` once it had.\n */\nexport class Stopped extends Data.TaggedError(\"Stopped\")<{\n readonly msgType: string;\n readonly when: \"stopping\" | \"stopped\";\n}> {\n override get message(): string {\n return (\n `@demlik/tea: Msg \"${this.msgType}\" was refused — the run is ` +\n `${this.when}.`\n );\n }\n}\n\n/**\n * The run's store failed. `operation` is `\"load\"` when the saved state could\n * not be restored at boot — `cause` is then the `StoreRefusedError` that says\n * why — and `\"save\"` when a transition's save threw, a fenced store's\n * `StoreConflictError` included; `cause` is then the store's own throw.\n */\nexport class StoreFailed extends Data.TaggedError(\"StoreFailed\")<{\n readonly operation: \"load\" | \"save\";\n readonly cause: unknown;\n}> {\n override get message(): string {\n return `@demlik/tea: store ${this.operation} failed.`;\n }\n}\n\n/** A hand-written cell's declared failure, on its way through the loop. */\nexport class CellFailure {\n constructor(readonly failure: unknown) {}\n}\n\n/** A store's failed save, on its way through the loop. */\nclass SaveFailure {\n constructor(readonly cause: unknown) {}\n}\n\n/** What was thrown, with the edge's wrapper taken off. */\nexport function unwrapped(error: unknown): unknown {\n if (error instanceof CellFailure) return error.failure;\n if (error instanceof SaveFailure) return error.cause;\n return error;\n}\n\n/**\n * The outermost store wrapper: every save that throws is marked as a store\n * failure. Outermost, so it wraps the fenced compare-and-swap too, and its\n * `StoreConflictError` fails as a `StoreFailed`.\n */\nexport function saveFailures<S, M, C>(): ExtensionFactory<S, M, C> {\n return () => ({\n store: (store: Store<S>): Store<S> => ({\n load: () => store.load(),\n async save(state) {\n try {\n await store.save(state);\n } catch (cause) {\n throw new SaveFailure(cause);\n }\n },\n migrate: (raw) => store.migrate(raw),\n }),\n });\n}\n\n/**\n * The Exit a handle verb ends with when the loop rejected with `error`: a\n * typed failure for a refused dispatch, a failed store and a cell's declared\n * failure, and a defect for anything else — a reducer throw, a Msg with no\n * cell, a livelock — which ADR 0011 keeps a contract breach.\n */\nexport function exitOf<Err>(\n error: unknown,\n): Exit.Exit<never, Err | Stopped | StoreFailed> {\n if (error instanceof CellFailure) return Exit.fail(error.failure as Err);\n if (error instanceof SaveFailure) {\n return Exit.fail(\n new StoreFailed({ operation: \"save\", cause: error.cause }),\n );\n }\n if (error instanceof StoreRefusedError) {\n return Exit.fail(new StoreFailed({ operation: \"load\", cause: error }));\n }\n if (error instanceof DispatchDiscardedError) {\n return Exit.fail(new Stopped({ msgType: error.msgType, when: \"stopping\" }));\n }\n if (error instanceof RuntimeStoppedError) {\n return Exit.fail(new Stopped({ msgType: error.msgType, when: \"stopped\" }));\n }\n return Exit.die(error);\n}\n","/**\n * The Effect engine's run handle (#308, ruling R4.1 of #325). Its members have\n * the Promise engine's names (the shared-handle ruling of #250); only the\n * return types differ. Each member that returns a Promise there returns an\n * Effect here, with a typed error channel a host reads with `Effect.catchTags`.\n * Members that neither wait nor fail — `getState`, `result` and the listener\n * registrations — are the same functions on both engines.\n */\n\nimport { Effect } from \"effect\";\nimport { type LiveWorkProbe, liveWork } from \"../internal/engine/loop\";\nimport type { Port } from \"../pure/core\";\nimport type { DispatchSettle, Runtime } from \"../runtime-types\";\nimport { exitOf, type Stopped, type StoreFailed } from \"./failures\";\n\n/**\n * What the Effect engine's `run` yields while boot is in flight. `Err` is the\n * union of the declared failures of the run's hand-written cells: a dispatch\n * whose Cmd's cell fails with one fails with it. Reading State and waiting for\n * quiescence need boot to have run, so they live on the {@link EffectRuntime}\n * that `ready` succeeds with.\n */\nexport interface EffectBootingRuntime<\n S,\n M extends { type: string },\n E extends { type: string } = never,\n Err = never,\n> {\n /**\n * Fold `msg` and run to quiescence, as the Promise engine's `dispatch` does.\n * Fails with `Stopped` once the run is stopping or stopped, `StoreFailed`\n * when the transition's save fails, and a hand-written cell's declared\n * failure when the transition's Cmd fails with one.\n */\n dispatch(\n msg: M,\n opts?: { readonly settle?: DispatchSettle },\n ): Effect.Effect<void, Err | Stopped | StoreFailed>;\n /** `dispatch(msg, { settle: \"once\" })`: one transition, no follow-up drain. */\n dispatchOnce(msg: M): Effect.Effect<void, Err | Stopped | StoreFailed>;\n /** A zero-arg change notifier, fired after each applied transition. */\n subscribe(listener: () => void): () => void;\n /** A `(msg, state)` hook, fired after each applied transition. */\n observe(observer: (msg: M, state: S) => void): () => void;\n /** Fires once with the initial State — at once if boot already ran. */\n onBoot(handler: (state: S) => void): () => void;\n /** Subscribe to the semantic event of `type` the run's `events` projects. */\n on<K extends E[\"type\"]>(\n type: K,\n handler: (event: Extract<E, { type: K }>) => void,\n ): () => void;\n /** Subscribe to a typed Port. */\n subscribePort<T>(port: Port<T>, listener: (value: T) => void): () => void;\n /** Emit a value on a Port from outside a Cmd handler. */\n emitPort<T>(port: Port<T>, value: T): void;\n /**\n * Succeeds with the booted handle once boot completes. Fails with\n * `StoreFailed` when the saved state could not be restored (`\"load\"`) or the\n * initial save failed (`\"save\"`), and with a hand-written cell's declared\n * failure when a boot Cmd fails with one.\n */\n readonly ready: Effect.Effect<EffectRuntime<S, M, E, Err>, Err | StoreFailed>;\n /**\n * Stop the run: drain in-flight work, up to `disposeTimeoutMs`, stop its\n * Subs, flush the final State. `stop()` interrupts every handler still in\n * flight, so the limit is reached only by a handler that cannot be\n * interrupted. Closing the run's Scope stops the run the same way, and\n * `stop()` leaves that Scope open. So for a child under `spawn` the two\n * differ: `stop()` does not take the child out of the host's table or tell\n * the parent. Stop such a child with the `stop` helper.\n */\n stop(): Effect.Effect<void>;\n}\n\n/** The Effect engine's booted handle — what `ready` succeeds with. */\nexport interface EffectRuntime<\n S,\n M extends { type: string },\n E extends { type: string } = never,\n Err = never,\n> extends EffectBootingRuntime<S, M, E, Err> {\n /** The current State. Total. */\n getState(): S;\n readonly ready: Effect.Effect<EffectRuntime<S, M, E, Err>, Err | StoreFailed>;\n /** Succeeds once every dispatched Msg and its follow-ups are processed. */\n idle(): Effect.Effect<void>;\n /** The terminal State, or `undefined` while the run is in flight. */\n result(): S | undefined;\n /** Succeeds with the terminal State the first time the run reaches one. */\n done(): Effect.Effect<S>;\n}\n\n/** Run a loop call, ending with the typed Exit its rejection maps to. */\nfunction settled<A, Err>(\n call: () => Promise<A>,\n): Effect.Effect<A, Err | Stopped | StoreFailed> {\n return Effect.flatten(\n Effect.promise(() =>\n call().then(\n (value) => Effect.succeed(value),\n (error: unknown) => exitOf<Err>(error),\n ),\n ),\n );\n}\n\n/** Build the Effect handle over the loop's Promise one. */\nexport function effectHandle<\n S,\n M extends { type: string },\n E extends { type: string },\n Err,\n>(\n loop: Runtime<S, M, E> & LiveWorkProbe,\n): EffectRuntime<S, M, E, Err> & LiveWorkProbe {\n const handle: EffectRuntime<S, M, E, Err> & LiveWorkProbe = {\n [liveWork]: loop[liveWork],\n dispatch: (msg, opts) => settled(() => loop.dispatch(msg, opts)),\n dispatchOnce: (msg) => settled(() => loop.dispatchOnce(msg)),\n subscribe: (listener) => loop.subscribe(listener),\n observe: (observer) => loop.observe(observer),\n onBoot: (handler) => loop.onBoot(handler),\n on: (type, handler) => loop.on(type, handler),\n subscribePort: (port, listener) => loop.subscribePort(port, listener),\n emitPort: (port, value) => loop.emitPort(port, value),\n getState: () => loop.getState(),\n result: () => loop.result(),\n // Boot never passes the dispatch gate, so `ready` never fails `Stopped`.\n // Suspended: `handle` is not built yet when this line runs.\n ready: Effect.suspend(() =>\n settled<unknown, Err>(() => loop.ready).pipe(Effect.as(handle)),\n ) as Effect.Effect<EffectRuntime<S, M, E, Err>, Err | StoreFailed>,\n // `idle` rejects only on a livelock, and `done` and `stop` never do: a\n // rejection here is a defect.\n idle: () => Effect.promise(() => loop.idle()),\n done: () => Effect.promise(() => loop.done()),\n stop: () => Effect.promise(() => loop.stop()),\n };\n return handle;\n}\n","/**\n * The Effect engine's `run` (#283): the same machine the Promise engine runs,\n * with Effect handlers, Effect sub runners, the caller's Layers and\n * interruption on stop.\n *\n * It runs the one core loop both engines share (`src/internal/engine/loop.ts`)\n * with the same built-ins in the same order, so a machine folds the same way on\n * either engine. What this file adds is the Effect edge:\n *\n * - an `interpret` cell returns `Effect<Ok, E, R>`, read with\n * `Effect.result`: a success settles `_ok`, a declared failure settles\n * `_err`, a defect goes to the error sink;\n * - a `subscribe` runner returns a `Stream<Msg>`, drained on its own fiber;\n * - every handler and runner fiber runs with the services the caller\n * provided, so `R` reaches them from the caller's Layers;\n * - stopping interrupts every handler fiber still in flight;\n * - the handle's verbs are Effects with a typed error channel\n * (`./handle`, `./failures`).\n */\n\nimport { Cause, Effect, Exit, Fiber, Result, Scope, Stream } from \"effect\";\nimport { builtinExtensions } from \"../internal/engine/builtins\";\nimport {\n defaultOnError,\n type LiveWorkProbe,\n type LoopHandler,\n type LoopRunner,\n startLoop,\n} from \"../internal/engine/loop\";\nimport type {\n BuiltinSub,\n BuiltinSubType,\n Cmd,\n DeclaredErrorsOf,\n ErrorsOf,\n Machine,\n OkOfCmd,\n Sub,\n TimerSub,\n} from \"../pure/core\";\nimport { applyCell, Outcome, subEntriesOf } from \"../pure/core\";\nimport type {\n CtxArg,\n OnError,\n Runtime,\n Store,\n Supervision,\n TelemetrySink,\n} from \"../runtime-types\";\nimport { CellFailure, saveFailures, unwrapped } from \"./failures\";\nimport { type EffectBootingRuntime, effectHandle } from \"./handle\";\n\n// === The handler and runner shapes ===\n\n/**\n * One Effect `interpret` cell. For a `Cmd.define`d Cmd it succeeds with the\n * Cmd's `Ok` and fails with one of its declared tags, and the engine mints\n * `<name>_ok` / `<name>_err` from that (ADR 0021). For a hand-written Cmd it\n * succeeds with a follow-up Msg, a list of them (dispatched in order), or\n * nothing, and its failure fails the dispatch with that error, typed on the\n * handle (see {@link CellErrors}). `R` is whatever services it reads.\n */\nexport type EffectInterpretCell<\n M extends { type: string },\n C extends Cmd,\n R = unknown,\n> =\n unknown extends ErrorsOf<C>\n ? // biome-ignore lint/suspicious/noConfusingVoidType: a hand-written Cmd's handler may settle with nothing\n (cmd: C) => Effect.Effect<M | readonly M[] | void, unknown, R>\n : (cmd: C) => Effect.Effect<OkOfCmd<C>, DeclaredErrorsOf<C>, R>;\n\n/**\n * The Effect engine's `interpret` map: one cell per Cmd variant, each reading\n * services within `R`.\n */\nexport type EffectInterpret<\n M extends { type: string },\n C extends Cmd,\n R = unknown,\n> = {\n readonly [K in C[\"type\"]]: EffectInterpretCell<M, Extract<C, { type: K }>, R>;\n};\n\n/**\n * One Effect sub runner: the Sub in, a `Stream` of Msgs out. The engine drains\n * it on its own fiber and interrupts that fiber when the Sub stops.\n *\n * A Sub maps the errors it expects into Msgs itself, the way an Elm `Sub msg`\n * carries no error type: `Stream.catchTag(\"Closed\", () => Stream.make({ type:\n * \"disconnected\" }))`. A stream that still fails reaches the error sink under\n * `\"sub\"` and stops the run: the run's Scope closes with that failure (#309).\n */\nexport type EffectRunner<\n M extends { type: string },\n U extends Sub,\n R = unknown,\n> = (sub: U) => Stream.Stream<M, unknown, R>;\n\n/**\n * The Effect engine's `subscribe` map: a runner for every Sub type the machine\n * declares beyond the built-ins, and optionally one for a built-in, which\n * replaces the engine's own (#270 R2.1).\n */\nexport type EffectSubscribe<\n M extends { type: string },\n U extends Sub,\n R = unknown,\n> = {\n readonly [K in Exclude<U[\"type\"], BuiltinSubType>]: EffectRunner<\n M,\n Extract<U, { type: K }>,\n R\n >;\n} & {\n readonly [K in BuiltinSubType]?: EffectRunner<\n M,\n Extract<BuiltinSub<M>, { type: K }>,\n R\n >;\n};\n\n/** The services every cell of an `interpret` map reads. */\nexport type InterpretServices<I> = {\n [K in keyof I]: ServicesOfCell<I[K]>;\n}[keyof I];\n\ntype ServicesOfCell<F> = F extends (\n cmd: never,\n) => Effect.Effect<unknown, unknown, infer R>\n ? R\n : never;\n\n/**\n * The failures a dispatch on the handle can end with from an `interpret` map:\n * the error type of every hand-written Cmd's cell. A `Cmd.define`d Cmd's\n * declared failure settles its `_err` Msg instead, so it adds nothing here.\n */\nexport type CellErrors<C extends Cmd, I> = {\n [K in keyof I & C[\"type\"]]: unknown extends ErrorsOf<Extract<C, { type: K }>>\n ? ErrorOfCell<I[K]>\n : never;\n}[keyof I & C[\"type\"]];\n\ntype ErrorOfCell<F> = F extends (\n cmd: never,\n) => Effect.Effect<unknown, infer Err, unknown>\n ? Err\n : never;\n\n/** The services every runner of a `subscribe` map reads. */\nexport type SubscribeServices<B> = {\n [K in keyof B]-?: ServicesOfRunner<NonNullable<B[K]>>;\n}[keyof B];\n\ntype ServicesOfRunner<F> = F extends (\n sub: never,\n) => Stream.Stream<unknown, unknown, infer R>\n ? R\n : never;\n\n// `interpret` is optional for a machine that emits no Cmd, and `subscribe` for\n// one that declares only built-in Subs — the same requiredness the Promise\n// engine's `RunHandlers` carries.\ntype InterpretOption<C extends Cmd, I> = [C] extends [Cmd<never>]\n ? { readonly interpret?: I }\n : { readonly interpret: I };\n\nexport type SubscribeOption<U extends Sub, B> = [\n Exclude<U[\"type\"], BuiltinSubType>,\n] extends [never]\n ? { readonly subscribe?: B }\n : { readonly subscribe: B };\n\n/**\n * The options of the Effect engine's `run`. Beside `interpret` and\n * `subscribe`, they are the Promise engine's, with the same meaning; see `run`\n * in `@demlik/tea/promise` for each one's contract.\n */\nexport type EffectRunOptions<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n E extends { type: string },\n I,\n B,\n> = CtxArg<Ctx> &\n InterpretOption<C, I> &\n SubscribeOption<U, B> & {\n readonly store?: Store<S>;\n readonly onError?: OnError;\n /** Stamps `at` on minted Msgs and telemetry. Defaults to `Date.now`. */\n readonly clock?: () => number;\n /** Projects each applied transition to the public events `on` serves. */\n readonly events?: (msg: M, state: S) => readonly E[];\n /** The policy for a reducer throw. Defaults to `\"stop\"`. */\n readonly supervision?: Supervision<S, M>;\n /** The terminal predicate `result()` and `done()` read. */\n readonly terminal?: (state: S) => boolean;\n /** A fire-and-forget sink for every applied transition. */\n readonly telemetry?: TelemetrySink;\n /**\n * How long `stop()` waits for the queue, and then for async teardown,\n * before giving up on each. Defaults to 5_000ms. `stop()` interrupts every\n * handler here, so the first wait ends early unless a handler cannot be\n * interrupted.\n *\n * A store call is not interrupted. tea starts none after `stop()` has\n * returned, but one already under way when `stop()` gave up can finish\n * after it. On a plain store that late save can overwrite what a newer\n * run saved; a fenced store refuses it. So a host that starts a new run on\n * the same store after a `stop()` that gave up needs a fenced store.\n */\n readonly disposeTimeoutMs?: number;\n };\n\n// === Built-in runners ===\n\n/** `timer`: emit `deps.msg` once, `deps.ms` after the Sub starts. */\nfunction timer<M>(sub: TimerSub<M>): Stream.Stream<M> {\n return Stream.fromEffect(Effect.as(Effect.sleep(sub.deps.ms), sub.deps.msg));\n}\n\n/** The Effect engine's built-in runners, keyed by the Sub type each runs. */\nexport const builtinRunners = { timer } as const satisfies Record<\n BuiltinSubType,\n unknown\n>;\n\n// === Settling a cell ===\n\n/**\n * The Effect a cell's Effect is run as: `Effect.result` makes a declared\n * failure a value, so only a defect or an interruption is left in the Exit's\n * cause. Read the Exit back with {@link settleCellExit}.\n */\nexport function cellResult(\n effect: Effect.Effect<unknown, unknown>,\n): Effect.Effect<Result.Result<unknown, unknown>> {\n return Effect.result(effect);\n}\n\n/**\n * What one cell's run settles to, the rule the engine and the Effect `drive`\n * share so they cannot drift:\n *\n * - interrupted — `undefined`, nothing to dispatch;\n * - a defect — thrown;\n * - a `Cmd.define`d Cmd (`defined`) — its success as `Outcome.ok`, its\n * declared failure as `Outcome.err`, for the `Cmd.define` edge to mint;\n * - a hand-written Cmd — its success (the follow-up Msgs) as is, its failure\n * thrown as a {@link CellFailure}, so a caller types it.\n */\nexport function settleCellExit(\n exit: Exit.Exit<unknown, unknown>,\n defined: boolean,\n): unknown {\n if (Exit.isFailure(exit)) {\n if (Cause.hasInterruptsOnly(exit.cause)) return undefined;\n throw Cause.squash(exit.cause);\n }\n const result = exit.value as Result.Result<unknown, unknown>;\n if (defined) {\n return Result.isSuccess(result)\n ? Outcome.ok(result.success)\n : Outcome.err(result.failure);\n }\n if (Result.isFailure(result)) throw new CellFailure(result.failure);\n return result.success;\n}\n\n// === run ===\n\n/**\n * Run `machine` on the Effect engine. The returned Effect needs a `Scope` and\n * the services its handlers and runners read, and yields the run handle. The\n * handle's members have the Promise engine's names; the ones that return a\n * Promise there return an Effect here, failing with `Stopped`, `StoreFailed`\n * or a hand-written cell's declared failure, so a host sorts them with\n * `Effect.catchTags`.\n *\n * Closing the scope stops the run and its Subs: every handler still in flight\n * is interrupted (its finalizers run), its Msg is never dispatched, and a\n * dispatch after stop fails with `Stopped`. The handle's `stop()` does the\n * same.\n *\n * A Sub whose Stream fails with an error it did not map to a Msg stops the\n * run the other way round: the engine closes that Scope with the failure.\n *\n * A fenced store is fenced here exactly as on the Promise engine: the check is\n * one of the built-ins both engines run.\n *\n * ```ts\n * const program = Effect.gen(function* () {\n * const handle = yield* run(machine, {\n * interpret: {\n * fetch: (cmd) =>\n * Effect.gen(function* () {\n * const api = yield* Api;\n * return yield* api.get(cmd.id);\n * }),\n * },\n * });\n * const runtime = yield* handle.ready;\n * yield* runtime.dispatch({ type: \"go\", id: \"7\" });\n * return runtime.getState();\n * });\n * Effect.runPromise(program.pipe(Effect.scoped, Effect.provide(ApiLive)));\n * ```\n */\nexport function run<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n // The defaults read no services, so an omitted map adds none to `R`.\n I extends EffectInterpret<M, C> = EffectInterpret<M, C, never>,\n B extends EffectSubscribe<M, U> = EffectSubscribe<M, U, never>,\n E extends { type: string } = never,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n opts: EffectRunOptions<S, M, C, U, Ctx, E, I, B>,\n): Effect.Effect<\n EffectBootingRuntime<S, M, E, CellErrors<C, I>>,\n never,\n Scope.Scope | InterpretServices<I> | SubscribeServices<B>\n> {\n return Effect.gen(function* () {\n const services = yield* Effect.context<\n InterpretServices<I> | SubscribeServices<B>\n >();\n const scope = yield* Scope.Scope;\n const fork = Effect.runForkWith(services) as ForkEffect;\n return yield* Effect.acquireRelease(\n Effect.sync(() =>\n effectHandle<S, M, E, CellErrors<C, I>>(\n start<S, M, C, U, Ctx, E>(\n machine,\n opts as EffectRunOptions<S, M, C, U, Ctx, E, unknown, unknown>,\n Effect.runPromiseExitWith(services) as RunEffect,\n fork,\n // Closing the Scope runs the release below, which stops the run.\n // A Scope that closes before that release is added runs it on add.\n (exit) => fork(Scope.close(scope, exit)),\n ),\n ),\n ),\n (runtime) => runtime.stop(),\n );\n });\n}\n\n/** Run an erased Effect to its Exit, interruptible through `signal`. */\ntype RunEffect = (\n effect: Effect.Effect<unknown, unknown>,\n options: { readonly signal: AbortSignal },\n) => Promise<Exit.Exit<unknown, unknown>>;\n\n/** Fork an erased Effect onto its own fiber. */\ntype ForkEffect = (\n effect: Effect.Effect<unknown, unknown>,\n) => Fiber.Fiber<unknown, unknown>;\n\ntype AnyCell = (cmd: unknown) => Effect.Effect<unknown, unknown>;\ntype AnyRunner = (sub: Sub) => Stream.Stream<unknown, unknown>;\n\nfunction start<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n E extends { type: string },\n>(\n machine: Machine<S, M, C, U, Ctx>,\n opts: EffectRunOptions<S, M, C, U, Ctx, E, unknown, unknown>,\n runEffect: RunEffect,\n fork: ForkEffect,\n closeScope: (exit: Exit.Exit<never, unknown>) => void,\n): Runtime<S, M, E> & LiveWorkProbe {\n const interpret = ((opts as { interpret?: unknown }).interpret ??\n {}) as Readonly<Record<string, AnyCell | undefined>>;\n const subscribe = ((opts as { subscribe?: unknown }).subscribe ??\n {}) as Readonly<Record<string, AnyRunner | undefined>>;\n const builtins = builtinRunners as unknown as Readonly<\n Record<string, AnyRunner | undefined>\n >;\n const defined = new Set((machine.cmds ?? []).map((def) => def.cmdType));\n\n // Aborted on stop: every handler fiber runs under this signal, so aborting\n // it interrupts all of them at once.\n const interruption = new AbortController();\n\n // Interrupted by stop, a cell settles to nothing. A defect throws: for a\n // defined Cmd the `Cmd.define` edge routes it to the error sink, for a\n // hand-written one it rejects the dispatch. A hand-written cell's failure is\n // marked, so the handle fails the dispatch with it as a typed error.\n async function settle(cell: AnyCell, cmd: unknown): Promise<unknown> {\n const type = (cmd as { type: string }).type;\n const exit = await runEffect(cellResult(cell(cmd)), {\n signal: interruption.signal,\n });\n return settleCellExit(exit, defined.has(type));\n }\n\n function handlerFor(type: string): LoopHandler<M> | undefined {\n const cell = interpret[type];\n if (cell === undefined) return undefined;\n return (cmd) => settle(cell, cmd);\n }\n\n // The Cause the run's Scope closes with when a Sub fails: the Stream's own\n // when a Stream failed, else the runner's throw as a defect.\n let subFailure: Cause.Cause<unknown> | undefined;\n\n // A runner's Msgs reach the loop through its `dispatch`, which queues each\n // one behind the step in progress — never a transition on the stream's own\n // fiber (spike #260). A Stream that fails with anything but an interrupt\n // failed with an error the Sub did not map to a Msg, and the run stops\n // (#309).\n function runnerFor(type: string): LoopRunner<Ctx, M> | undefined {\n const make = subscribe[type] ?? builtins[type];\n if (make === undefined) return undefined;\n return (sub, _ctx, dispatch, fail) => {\n const drain = Stream.runForEach(make(sub), (msg) =>\n Effect.sync(() => dispatch(msg as M)),\n ).pipe(\n Effect.catchCause((cause) =>\n Cause.hasInterruptsOnly(cause)\n ? Effect.void\n : Effect.sync(() => {\n subFailure ??= cause;\n fail(Cause.squash(cause));\n }),\n ),\n );\n const fiber = fork(drain);\n return () => Effect.runPromise(Fiber.interrupt(fiber));\n };\n }\n\n // The sink is handed what was thrown, never the edge's marker around it.\n const sink = opts.onError ?? defaultOnError;\n\n const handle = startLoop<S, M, C, Ctx>({\n init: machine.init,\n reduce: (state, msg) => applyCell<S, M, C>(machine, state, msg),\n subs: subEntriesOf<S>(machine),\n runnerFor,\n handlerFor,\n store: opts.store,\n ctx: (opts.ctx ?? {}) as Ctx,\n onError: (error, context) => sink(unwrapped(error), context),\n disposeTimeoutMs: opts.disposeTimeoutMs ?? 5_000,\n idleCap: 100_000,\n // The save marker wraps outermost, around the fence the built-ins add.\n extensions: [\n saveFailures<S, M, C>(),\n ...builtinExtensions<S, M, C, E>(machine, opts),\n ],\n stopOnSubFailure: (error) =>\n closeScope(Exit.failCause(subFailure ?? Cause.die(error))),\n });\n\n // Stop closes the gate first, so nothing an interrupted handler leaves\n // behind is dispatched, then interrupts every handler still in flight so the\n // drain it waits on can finish.\n const stopLoop = handle.stop;\n handle.stop = () => {\n const stopped = stopLoop();\n interruption.abort();\n return stopped;\n };\n return handle as unknown as Runtime<S, M, E> & LiveWorkProbe;\n}\n"]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { initCache, deadlineSub, nextTimer, get, set } from './chunk-
|
|
1
|
+
import { initCache, deadlineSub, nextTimer, get, set } from './chunk-QIE4Y6BK.js';
|
|
2
2
|
import { asRng, recordFailure, shouldRetry, nextDelayMs, initRetry } from './chunk-VBDU4JL6.js';
|
|
3
3
|
import { unchecked } from './chunk-4LPY67E4.js';
|
|
4
|
-
import { liftSlice } from './chunk-
|
|
5
|
-
import { Cmd, without } from './chunk-
|
|
4
|
+
import { liftSlice } from './chunk-VHKDUBXC.js';
|
|
5
|
+
import { Cmd, without } from './chunk-GAVLND5C.js';
|
|
6
6
|
|
|
7
7
|
// src/internal/resilience/circuit-breaker/index.ts
|
|
8
8
|
var defaultCircuitPolicy = {
|
|
@@ -391,5 +391,5 @@ function liftResilience(state, result) {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
export { DEFAULT_RESILIENT_NAME, canPass, createResilientCall, defaultCircuitPolicy, initBucket, initCircuit, initWindow, liftResilience, onFailure, onSuccess, record, refill, remaining, runCmdDef, tryConsume };
|
|
394
|
-
//# sourceMappingURL=chunk-
|
|
395
|
-
//# sourceMappingURL=chunk-
|
|
394
|
+
//# sourceMappingURL=chunk-MBQR3LFW.js.map
|
|
395
|
+
//# sourceMappingURL=chunk-MBQR3LFW.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/resilience/circuit-breaker/index.ts","../src/internal/resilience/rate-limit/index.ts","../src/internal/resilience/resilient-call/index.ts"],"names":[],"mappings":"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","file":"chunk-WTSOW2MD.js","sourcesContent":["/**\n * @packageDocumentation\n * internal/resilience/circuit-breaker — per-target failure tracking as pure state + ops.\n *\n * Same shape as `../../work-queue/ops.ts`, `../rate-limit/`, and\n * `../../idempotency/`: a state type plus pure transition functions.\n * Host-agnostic by construction — no `Date.now()`, no timers, no I/O. The\n * caller injects the clock (`nowMs`) and wires the result into its own reducer\n * / interpret. This keeps the breaker inside TEA's invariant 2 (transitions are\n * pure: same inputs → same outputs, no `Date.now`) so a consumer can fold it\n * through `update` and exercise it with `replay` / the pbt fold without a fake\n * clock.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). `canPass` returns a `[next, allowed]` tuple so\n * the caller branches on the decision while threading the advanced state forward\n * — same convention as `rate-limit`'s `tryConsume` / `record`. `onSuccess` /\n * `onFailure` are the outcome-recording transitions the caller folds in once the\n * guarded call resolves.\n *\n * ## The phase machine\n *\n * A circuit breaker guards a fallible downstream target (an HTTP backend, a DB,\n * a flaky tool call). Instead of hammering a target that is already failing, it\n * trips OPEN after enough failures and fast-fails subsequent calls for a\n * cooldown, then probes with a few trial calls before fully closing again.\n * Three phases, each carrying ONLY its own data so impossible states are\n * unrepresentable (pattern 11):\n *\n * - **closed** — the healthy steady state. Calls pass through. Carries\n * `failures`: the count of *consecutive* failures since the last success.\n * A success resets it to 0; the `failureThreshold`-th failure trips the\n * breaker OPEN. Only `closed` has a failure counter — `open` and\n * `half_open` don't, because in those phases the count is irrelevant\n * (the breaker has already tripped / is already probing).\n *\n * - **open** — the tripped state. All calls fast-fail (no downstream load).\n * Carries `openedAtMs`: the clock reading at which the breaker tripped.\n * `canPass` compares `nowMs - openedAtMs` against `cooldownMs` to decide\n * when to start probing. Only `open` carries a timestamp — neither other\n * phase is timing anything.\n *\n * - **half_open** — the probing state. A limited number of trial calls are\n * allowed through to test whether the target has recovered. Carries\n * `probes`: the count of probes admitted so far this probing round.\n * `canPass` admits while `probes < halfOpenMaxProbes`, then fast-fails the\n * rest (so a slow target can't be flooded by every queued caller at once).\n * A probe success closes the breaker; a probe failure re-opens it (and\n * resets the cooldown clock). Only `half_open` counts in-flight probes.\n *\n * ### Transition table\n *\n * Rows are the current phase; columns are the event. `→` denotes the next\n * phase; `self` means unchanged. `nowMs` is the injected clock.\n *\n * | from \\ event | onSuccess | onFailure | canPass (→ allowed?) |\n * | ------------ | -------------- | -------------------------------------- | ------------------------------------------------------------------------------------ |\n * | closed | closed(0) | failures+1; trip → open(nowMs) at thr. | self → true (always) |\n * | open | self *(¹)* | self | elapsed ≥ cooldown → half_open(1), true; else self → false |\n * | half_open | → closed(0) | → open(nowMs) (probe failed) | probes < max → half_open(probes+1), true; else self → false |\n *\n * *(¹)* `onSuccess` on `open` \"shouldn't happen\": no call was admitted while\n * open, so no success can be reported for one. We leave the state unchanged\n * rather than throw — the breaker is a coordination aid, not a correctness\n * gate, and a spurious success report (e.g. a late-arriving result from before\n * the trip) must not silently un-trip the breaker. `onFailure` on `open` is\n * likewise a no-op: the breaker is already tripped; the `open` clock is anchored\n * at the trip, not bumped by late failures.\n *\n * ### The probe-admission asymmetry\n *\n * Entering `half_open` happens inside `canPass`, not `onSuccess` / `onFailure`,\n * because the cooldown is a *time* condition and only `canPass` reads the clock\n * against `openedAtMs`. The first `canPass` after the cooldown elapses both\n * transitions the phase AND admits the probe (`[half_open(1), true]`) — one\n * caller drives the recovery attempt. Subsequent `canPass` calls in the same\n * round admit up to `halfOpenMaxProbes` total, then fast-fail until a probe\n * resolves and `onSuccess` / `onFailure` moves the phase out of `half_open`.\n *\n * This module is a textbook three-state machine — a clean `/machine-viz` demo.\n *\n * Internal to the package (`src/internal/resilience/`), not a published\n * subpath (#46); same one-shape-per-module rule as `work-queue`, `rate-limit`,\n * and `idempotency`.\n */\n\n/**\n * Circuit-breaker policy — pure configuration, no mutable state. One policy is\n * shared across all the calls guarded by one logical breaker (often a\n * module-scope const). `CircuitState` carries the mutable counts; this carries\n * the thresholds those counts are compared against.\n */\nexport interface CircuitPolicy {\n /**\n * Consecutive failures in `closed` that trip the breaker `open`. The\n * `failureThreshold`-th failure is the one that trips — with a threshold of\n * 5, the breaker survives 4 failures and opens on the 5th.\n */\n readonly failureThreshold: number;\n /**\n * How long the breaker stays `open` before `canPass` starts admitting\n * probes, in milliseconds. Measured from `openedAtMs`; the cutoff is\n * half-open (`elapsed >= cooldownMs` admits), matching `rate-limit` /\n * `idempotency` window edges — a breaker opened at `t` first probes at\n * `t + cooldownMs`.\n */\n readonly cooldownMs: number;\n /**\n * How many probe calls `half_open` admits before fast-failing the rest of\n * the round. Caps the load on a recovering target — a single slow probe\n * can't be joined by every queued caller. The round ends when a probe\n * resolves: success closes the breaker, failure re-opens it.\n */\n readonly halfOpenMaxProbes: number;\n}\n\n/**\n * Sensible defaults: trip after 5 consecutive failures, cool down for 30s,\n * admit a single probe before deciding. Tuned for guarding a network backend;\n * override per call site rather than mutating this shared const.\n */\nexport const defaultCircuitPolicy: CircuitPolicy = {\n failureThreshold: 5,\n cooldownMs: 30_000,\n halfOpenMaxProbes: 1,\n};\n\n/**\n * The breaker's phase. A discriminated union on `phase` so each phase carries\n * ONLY its own data and impossible combinations are unrepresentable (pattern\n * 11): there is no state with both a failure count AND an open timestamp,\n * because such a state is meaningless. `phase` is the discriminant; every field\n * is `readonly` so the whole value is trivially JSON-serializable into a\n * consumer's `Store<S>` and safe to thread through a pure reducer.\n *\n * - `closed` carries `failures` — consecutive failures since last success.\n * - `open` carries `openedAtMs` — clock reading when it tripped.\n * - `half_open` carries `probes` — probes admitted this round.\n */\nexport type CircuitState =\n | { readonly phase: \"closed\"; readonly failures: number }\n | { readonly phase: \"open\"; readonly openedAtMs: number }\n | { readonly phase: \"half_open\"; readonly probes: number };\n\n/** The starting state: closed with zero recorded failures. */\nexport function initCircuit(): CircuitState {\n return { phase: \"closed\", failures: 0 };\n}\n\n/**\n * Record a successful guarded call. PURE — returns a new state; the input is\n * never mutated.\n *\n * - `closed` → reset `failures` to 0 (a success breaks the consecutive run).\n * - `half_open` → the probe succeeded; the target has recovered → `closed`.\n * - `open` → unchanged. A success \"shouldn't happen\" while open (no call\n * was admitted to produce one); a late/spurious success must\n * not silently un-trip the breaker. See module header note ¹.\n */\nexport function onSuccess(\n state: CircuitState,\n _policy: CircuitPolicy,\n): CircuitState {\n switch (state.phase) {\n case \"closed\":\n // Already closed at 0 → return the same shape (cheap; the consumer's\n // store only re-persists on a real change if it dedupes).\n return state.failures === 0 ? state : { phase: \"closed\", failures: 0 };\n case \"half_open\":\n return { phase: \"closed\", failures: 0 };\n case \"open\":\n return state;\n }\n}\n\n/**\n * Record a failed guarded call. PURE — returns a new state; the input is never\n * mutated. `nowMs` is the injected clock, stamped onto the `open` phase when the\n * breaker trips so `canPass` can measure the cooldown later.\n *\n * - `closed` → `failures + 1`; if that reaches `failureThreshold`, trip to\n * `open(nowMs)`. The threshold-th failure is the one that trips.\n * - `half_open` → the probe failed; the target is still unhealthy →\n * `open(nowMs)`, restarting the cooldown clock so a fresh\n * cooldown elapses before the next probe round.\n * - `open` → unchanged. The breaker is already tripped; the `open` clock\n * is anchored at the original trip, not bumped by late\n * failures. See module header note ¹.\n */\nexport function onFailure(\n state: CircuitState,\n policy: CircuitPolicy,\n nowMs: number,\n): CircuitState {\n switch (state.phase) {\n case \"closed\": {\n const failures = state.failures + 1;\n // `>=` not `===`: a threshold of 0 (or any already-exceeded count from a\n // policy change) trips immediately rather than over-counting forever.\n return failures >= policy.failureThreshold\n ? { phase: \"open\", openedAtMs: nowMs }\n : { phase: \"closed\", failures };\n }\n case \"half_open\":\n // A failed probe re-opens and resets the cooldown clock to nowMs.\n return { phase: \"open\", openedAtMs: nowMs };\n case \"open\":\n return state;\n }\n}\n\n/**\n * Decide whether a call may pass, advancing the phase as a side effect of the\n * decision. PURE — returns `[next, allowed]`; the input is never mutated.\n * Branch on the boolean, thread the state forward regardless:\n *\n * const [c1, allowed] = canPass(state.circuit, policy, nowMs);\n * if (!allowed) return [{ ...state, circuit: c1 }, [fastFailCmd]];\n * return [{ ...state, circuit: c1 }, [doGuardedCallCmd]];\n *\n * - `closed` → `[state, true]` — always passes; failures are recorded by\n * `onFailure` once the call resolves, not gated here.\n * - `open` → if `nowMs - openedAtMs >= cooldownMs` the cooldown has\n * elapsed → enter `half_open` and admit this caller as the\n * first probe: `[half_open(1), true]`. Otherwise still cooling\n * down → `[state, false]` (fast-fail, no downstream load).\n * - `half_open` → admit while `probes < halfOpenMaxProbes`, incrementing the\n * count: `[half_open(probes+1), true]`. Once the cap is hit →\n * `[state, false]` until a probe resolves and `onSuccess` /\n * `onFailure` moves the phase out of `half_open`.\n *\n * Entering `half_open` lives here (not in `onSuccess` / `onFailure`) because the\n * cooldown is a time condition and only `canPass` reads `nowMs` against\n * `openedAtMs`. See the module header's probe-admission note.\n */\nexport function canPass(\n state: CircuitState,\n policy: CircuitPolicy,\n nowMs: number,\n): readonly [CircuitState, boolean] {\n switch (state.phase) {\n case \"closed\":\n return [state, true];\n case \"open\": {\n const elapsed = nowMs - state.openedAtMs;\n // Half-open cutoff: elapsed exactly == cooldownMs admits the first probe,\n // matching the >= edge `rate-limit` / `idempotency` use for their windows.\n if (elapsed >= policy.cooldownMs) {\n return [{ phase: \"half_open\", probes: 1 }, true];\n }\n return [state, false];\n }\n case \"half_open\":\n if (state.probes < policy.halfOpenMaxProbes) {\n return [{ phase: \"half_open\", probes: state.probes + 1 }, true];\n }\n return [state, false];\n }\n}\n","/**\n * internal/resilience/rate-limit — two rate limiters as pure state + ops.\n *\n * Same shape as `../../work-queue/ops.ts`: a state type plus pure\n * transition functions. Host-agnostic by construction — no `Date.now()`, no\n * timers, no I/O. The caller injects the clock (`nowMs`) and wires the result\n * into its own reducer / interpret. This keeps the limiters inside TEA's\n * invariant 2 (transitions are pure: same inputs → same outputs, no `Date.now`)\n * so a consumer can fold them through `update` and exercise them with `replay`\n * / the pbt fold without a fake clock.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). The `tryConsume` / `record` ops return a\n * `[next, ok]` tuple so the caller branches on the decision while threading the\n * advanced state forward; `refill` / `remaining` are read-shaped helpers.\n *\n * Two complementary algorithms, picked by the cost the caller wants to pay:\n *\n * - **Token bucket** — O(1) state (four numbers), smooths bursts against a\n * steady refill rate. Allows a burst up to `capacity`, then throttles to\n * `refillPerSec`. Cheapest; the default choice.\n * - **Sliding window log** — O(limit) state (one timestamp per in-window\n * hit), exact \"no more than `limit` events per `windowMs`\". Pay the memory\n * for precise window edges when a token bucket's continuous refill would\n * let through too many at a boundary.\n *\n * Internal to the package (`src/internal/resilience/`), not a published\n * subpath (#46); same one-shape-per-module rule as `work-queue` and `do`.\n */\n\n// ===========================================================================\n// Token bucket\n// ===========================================================================\n\n/**\n * A token bucket: `tokens` of `capacity` available now, replenished at\n * `refillPerSec` tokens per second. `lastRefillMs` is the clock reading at\n * which `tokens` was last computed — `refill` advances it. All four fields are\n * plain numbers so the whole bucket is trivially JSON-serializable into a\n * consumer's `Store<S>`.\n */\nexport interface TokenBucket {\n readonly tokens: number;\n readonly capacity: number;\n readonly refillPerSec: number;\n readonly lastRefillMs: number;\n}\n\n/**\n * Create a full bucket. Starts at `capacity` tokens (the conventional \"a fresh\n * limiter lets the first burst through\" behavior) and stamps `lastRefillMs`\n * with the caller's clock so the first `refill` measures elapsed time from\n * boot, not from epoch 0.\n */\nexport function initBucket(\n capacity: number,\n refillPerSec: number,\n nowMs: number,\n): TokenBucket {\n return {\n tokens: capacity,\n capacity,\n refillPerSec,\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Add the tokens accrued since `lastRefillMs`, clamped to `capacity`, and\n * advance `lastRefillMs` to `nowMs`. PURE — returns a new bucket; the input is\n * untouched.\n *\n * A `nowMs` at or before `lastRefillMs` (clock not advanced, or skewed\n * backwards) adds a non-positive amount; `Math.max(0, …)` floors the elapsed\n * term so a backwards clock can never *remove* tokens. `lastRefillMs` still\n * advances to `nowMs` so the bucket tracks the latest reading the caller\n * presented — the next forward step measures from there, not from a stale\n * future timestamp.\n */\nexport function refill(bucket: TokenBucket, nowMs: number): TokenBucket {\n const elapsedSec = Math.max(0, nowMs - bucket.lastRefillMs) / 1000;\n const replenished = bucket.tokens + elapsedSec * bucket.refillPerSec;\n return {\n ...bucket,\n tokens: Math.min(bucket.capacity, replenished),\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Refill against `nowMs`, then attempt to take `n` tokens (default 1).\n *\n * On success returns `[next, true]` with `n` subtracted; on failure returns\n * `[refilled, false]` — the refilled bucket (so the caller keeps the advanced\n * clock + accrued tokens) with no tokens spent. Branch on the boolean, thread\n * the bucket forward regardless:\n *\n * const [b1, ok] = tryConsume(bucket, nowMs);\n * return ok ? [{ ...state, bucket: b1 }, [doWork]] : [{ ...state, bucket: b1 }, []];\n *\n * PURE — input bucket is never mutated.\n */\nexport function tryConsume(\n bucket: TokenBucket,\n nowMs: number,\n n = 1,\n): readonly [TokenBucket, boolean] {\n const refilled = refill(bucket, nowMs);\n if (refilled.tokens >= n) {\n return [{ ...refilled, tokens: refilled.tokens - n }, true];\n }\n return [refilled, false];\n}\n\n// ===========================================================================\n// Sliding window log\n// ===========================================================================\n\n/**\n * A sliding-window log: the timestamps of every hit still inside the trailing\n * `windowMs`, capped at `limit` events per window. `hits` is `readonly` and\n * kept oldest-first (pruning slices the stale prefix off the front, recording\n * appends to the back), so the array is its own sorted index. Exact at the\n * window edge — unlike a token bucket's continuous refill, a hit leaves the\n * count the instant it ages past `windowMs`.\n */\nexport interface SlidingWindow {\n readonly hits: readonly number[];\n readonly windowMs: number;\n readonly limit: number;\n}\n\n/** Create an empty window. No hits recorded yet. */\nexport function initWindow(windowMs: number, limit: number): SlidingWindow {\n return { hits: [], windowMs, limit };\n}\n\n/**\n * The window-edge rule, in one place: keep only the hits still live at `nowMs`.\n * The cutoff is half-open — a hit exactly `windowMs` old is dropped (`> cutoff`,\n * not `>=`), so a hit recorded at `t` no longer counts at `t + windowMs` and the\n * window's capacity fully recovers `windowMs` after the last hit. `record` (the\n * writer) and `remaining` (the read helper) both prune through here so a change\n * to the edge semantics can't leave the reader disagreeing with the writer.\n */\nfunction prune(window: SlidingWindow, nowMs: number): readonly number[] {\n const cutoff = nowMs - window.windowMs;\n return window.hits.filter((t) => t > cutoff);\n}\n\n/**\n * Prune hits older than `nowMs - windowMs`, then record `nowMs` if there is\n * room. Returns `[next, true]` with the hit appended when `hits.length < limit`\n * after pruning, else `[pruned, false]` — the pruned window (capacity recovers\n * as old hits age out) with nothing appended.\n *\n * The cutoff is half-open (see {@link prune}): a hit exactly `windowMs` old is\n * pruned, so a hit recorded at `t` no longer counts at `t + windowMs`. That\n * makes the window's capacity fully recover `windowMs` after the last hit, the\n * behavior tests pin.\n *\n * PURE — input window is never mutated; `hits` is rebuilt by `filter` +\n * spread.\n */\nexport function record(\n window: SlidingWindow,\n nowMs: number,\n): readonly [SlidingWindow, boolean] {\n const pruned = prune(window, nowMs);\n if (pruned.length < window.limit) {\n return [{ ...window, hits: [...pruned, nowMs] }, true];\n }\n return [{ ...window, hits: pruned }, false];\n}\n\n/**\n * How many more hits `record` would accept at `nowMs` without blocking —\n * `limit` minus the live (post-prune) hit count, floored at 0. Read-only: it\n * prunes a local copy to answer but returns no new window. Use it to render a\n * \"N requests remaining\" affordance without committing a hit.\n */\nexport function remaining(window: SlidingWindow, nowMs: number): number {\n const live = prune(window, nowMs).length;\n return Math.max(0, window.limit - live);\n}\n","/**\n * @packageDocumentation\n *\n * Internal to the package (`src/internal/resilience/`, #46); consumers reach it\n * through `@demlik/tea/resilience`.\n *\n * internal/resilience/resilient-call — the ROOT composition: a single resilience knob\n * that bundles cache → circuit-breaker → rate-limit → exponential-backoff retry\n * (with a deadline-driven retry timer) around one fallible piece of work.\n *\n * This is `examples/resilient-fetch.ts`'s hand-wired `attempt()` generalized:\n *\n * - It is **keyed**. The example tracked one URL at a time; this tracks N\n * concurrent logical calls, each under a string `key`. The cache, the\n * waiting-retry bookkeeping, and the retry timers are all per-key, while the\n * circuit breaker and token bucket are shared (one downstream target, one\n * breaker / one bucket — the example's implicit single-target assumption\n * made explicit).\n * - It is **config-driven**. You hand `createResilientCall` a\n * `ResilientConfig` and get back plain functions: `init()`, the verbs\n * (`attempt` / `settle` / `onTimer` / `resume` / `settleFailed`), the\n * `deadlines(state)` list and the `timer(state)` that arms it, and `run` —\n * the `Cmd.define`d Cmd the knob emits. You call them from your own\n * `update` (ADR 0022); nothing mounts or wraps your machine.\n * - Every brick is **optional**. Omit `cache` and the cache gate disappears\n * (`get` is never consulted); omit `circuit` and every call passes the\n * breaker; omit `rateLimit` and the bucket never throttles; omit `retry`\n * and a failure is terminal (no backoff, no timer); omit `deadline` and no\n * overall time budget is armed. \"Omit a brick → omit its gate\" is the\n * whole config story — there are no other modes.\n *\n * Four later compositions (`llm-call`, `paginated-walk`, `authed-call`,\n * `reconciler`) inherit THIS slice + verb shape, so the contract here is\n * load-bearing: the slice is a flat record of brick states, every verb returns\n * `readonly [State, Cmd[]]`, time is always an `at` / `now` parameter, and the\n * RNG used for jitter is injected on the config (never read inside a verb).\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is a plain-data Model field (brick states are all\n * JSON-serializable), so it survives DO eviction / reload.\n * - **Replayable** — every transition is a verb returning new state + Cmds;\n * `replay` reconstructs it exactly. Nothing here closes over time or RNG.\n *\n * ## The work runs in your handler (ADR 0021)\n *\n * The knob ships no engine code. Its one effect is the `run` Cmd\n * (`<name>_run`), built with `Cmd.define`, and you interpret it in your\n * engine's own style. The handler returns an outcome and the engine mints\n * `<name>_run_ok` / `<name>_run_err`, which `settle` reads:\n *\n * interpret: {\n * resilient_run: async (cmd, { ok, err }) => {\n * try { return ok(await fetchUser(cmd.input)); }\n * catch (cause) { return err({ _tag: \"port_rejected\", cause }); }\n * },\n * }\n *\n * A failure the handler does not return as a declared tag (a throw, an\n * undeclared tag) is a contract breach: it goes to the error sink and never\n * reaches `settle`, so it is never retried.\n *\n * ## Where the clock / RNG live\n *\n * Inside the verbs: nowhere. Time arrives as `at` on `attempt` / `resume` /\n * `settleFailed`, as the `at` the engine stamps on a settled Msg, and as `atMs`\n * on a timer Msg. Jitter RNG is injected once at\n * `createResilientCall(config, rng)` and threaded into `nextDelayMs` at the\n * verb boundary — deterministic in tests, `Math.random` by default.\n *\n * Every clocked verb also records its instant on the slice (`clockMs`). That is\n * what lets `timer(state)` hand the built-in `timer` Sub a RELATIVE countdown to\n * the soonest deadline without reading a clock: the Sub's deps change whenever\n * the slice's clock moves, so the engine restarts the countdown from that\n * instant.\n *\n * That same `at` is what makes a DURATION-bounded retry budget reachable here:\n * `recordFailure` / `shouldRetry` are fed the failure's observation instant,\n * so `config.retry` accepts any `AnyRetryPolicy` — a count, a wall-clock outage\n * budget (`maxElapsedMs`), or explicit `unbounded: true` — with no new argument\n * on any verb and no clock read inside one.\n *\n * ## Typical wiring\n *\n * const rc = createResilientCall<string, Result>({\n * cache: { ttlMs: 60_000 },\n * circuit: { threshold: 5, cooldownMs: 30_000 },\n * rateLimit: { capacity: 10, refillPerSec: 5 },\n * retry: defaultRetryPolicy,\n * deadline: { ms: 5_000 },\n * });\n *\n * // One helper of your own folds the outcome into your Model. The value is\n * // only reachable through `r.outcome`, so the slice has always settled first.\n * function onSettle(s: Model, r: ReturnType<typeof rc.settle>) {\n * const next = { ...s, resilience: r.call };\n * switch (r.outcome.kind) {\n * case \"done\": return [{ ...next, result: r.outcome.value }, r.cmds] as const;\n * case \"failed\": return [next, r.cmds] as const;\n * case \"retrying\": return [next, r.cmds] as const;\n * }\n * }\n *\n * // in the machine:\n * update: {\n * fetch: (s, m) => liftResilience(s, rc.attempt(s.resilience, m.key, m.input, m.at)),\n * resilient_run_ok: (s, m) => onSettle(s, rc.settle(s.resilience, m)),\n * resilient_run_err: (s, m) => onSettle(s, rc.settle(s.resilience, m)),\n * deadline_exceeded: (s, m) => liftResilience(s, rc.onTimer(s.resilience, m)),\n * },\n * subs: [{ type: \"timer\", deps: (s) => rc.timer(s.resilience) }],\n *\n * `timer` is the engine's built-in Sub, so `run` needs no `subscribe` for it.\n * `docs/how-to/hand-wire-a-resilient-call.md` walks the whole machine.\n *\n * ## Naming a knob, when a machine holds more than one\n *\n * Every knob above speaks `resilient_run` / `resilient_run_ok` /\n * `resilient_run_err`, so TWO of them in one machine meet in one\n * `resilient_run_ok` cell whose `value` is the union of both payloads —\n * discriminated by hand on `cmd.key`, which the type checker cannot grade.\n * `config.name` renames the whole family, Cmd and both settle Msgs together:\n *\n * const jev = createResilientCall<JevRequest, JevOk, \"jev\">({ name: \"jev\" });\n * const llm = createResilientCall<LlmCall, LlmOk, \"llm\">({ name: \"llm\" });\n *\n * update: {\n * jev_run_ok: (s, m) => …, // m.value is JevOk — no `key` switch\n * llm_run_ok: (s, m) => …, // m.value is LlmOk\n * }\n *\n * The name leads the retry / deadline ids too (`jev:retry:<key>`), because a\n * fired timer is routed by its id and two knobs would otherwise answer each\n * other's timer on a shared key, and it names the timer Msg (`jev_deadline`).\n * `N` is not inferable from `I` / `R`, so an opted-in knob spells all three\n * type arguments; `config.name` is typed at `N`, so value and type cannot\n * drift. Omit `name` and every one of those strings is `resilient*`.\n */\n\nimport { liftSlice } from \"../../../compose\";\nimport {\n Cmd,\n type CmdOf,\n type MalformedResult,\n type TaggedError,\n type TimerDeps,\n} from \"../../../index\";\nimport type { MsgType } from \"../../../protocol\";\nimport { without } from \"../../../pure/core\";\nimport {\n type AnyRetryPolicy,\n asRng,\n initRetry,\n nextDelayMs,\n type RetryState,\n recordFailure,\n shouldRetry,\n} from \"../../../retry-backoff\";\nimport { unchecked } from \"../../schema\";\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../cache\";\nimport {\n type CircuitState,\n canPass,\n initCircuit,\n onFailure,\n onSuccess,\n} from \"../circuit-breaker\";\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n nextTimer,\n} from \"../deadline\";\nimport { initBucket, type TokenBucket, tryConsume } from \"../rate-limit\";\n\n// ===========================================================================\n// Config — the knob. Every brick optional; omit a brick → omit its gate.\n// ===========================================================================\n\n/** Circuit-breaker knob — only the two numbers a consumer ever tunes. */\nexport interface CircuitConfig {\n /** Consecutive failures that trip the breaker open. */\n readonly threshold: number;\n /** How long the breaker stays open before admitting a probe, in ms. */\n readonly cooldownMs: number;\n /**\n * Probes admitted in `half_open` before fast-failing the rest of the round.\n * Defaults to 1 (a single probe decides recovery) — matches the circuit-\n * breaker module's `defaultCircuitPolicy`.\n */\n readonly halfOpenMaxProbes?: number;\n}\n\n/** Token-bucket rate-limit knob. */\nexport interface RateLimitConfig {\n /** Burst size — the bucket starts full at `capacity`. */\n readonly capacity: number;\n /** Steady-state refill rate, tokens per second. */\n readonly refillPerSec: number;\n}\n\n/** Per-entry TTL cache knob. */\nexport interface CacheConfig {\n /** How long a cached success stays fresh, in ms. */\n readonly ttlMs: number;\n}\n\n/**\n * Overall deadline knob — a budget of IN-PROCESS time per in-flight call.\n *\n * The budget is charged in elapsed time this process actually observed, not by\n * a position on the host's wall clock. Time in which no process exists — a DO\n * eviction, a crash between attempts, a redeploy — is NOT charged: `resume`\n * re-anchors every live call's charging clock, so downtime lands outside every\n * segment. A timeout therefore means \"the tool took too long\", never \"the host\n * was away too long\" (#144).\n */\nexport interface DeadlineConfig {\n /** Budget of in-process time from the moment a call starts, in ms. */\n readonly ms: number;\n}\n\n// ===========================================================================\n// The Msg-name family — one `name` derives the Cmd and both settle Msgs.\n// ===========================================================================\n\n/**\n * The family every unnamed knob speaks: `resilient_run` / `resilient_run_ok` /\n * `resilient_run_err`. Identical to the `MsgType.Resilient*` literals, which\n * stay the vocabulary of every knob that passes no name.\n */\nexport const DEFAULT_RESILIENT_NAME = \"resilient\";\nexport type DefaultResilientName = typeof DEFAULT_RESILIENT_NAME;\n\n// The default family IS the protocol's `MsgType.Resilient*` vocabulary, not a\n// parallel spelling of it. These three lines fail to compile the moment the two\n// drift, which is the only thing keeping \"omit the name and nothing changes\"\n// true for every machine wired before the name existed.\nconst _runIsProtocol: typeof MsgType.ResilientRun =\n `${DEFAULT_RESILIENT_NAME}_run` as const;\nconst _okIsProtocol: typeof MsgType.ResilientOk =\n `${DEFAULT_RESILIENT_NAME}_run_ok` as const;\nconst _errIsProtocol: typeof MsgType.ResilientErr =\n `${DEFAULT_RESILIENT_NAME}_run_err` as const;\nvoid [_runIsProtocol, _okIsProtocol, _errIsProtocol];\n\n/** The run Cmd's `type` for the `N` family. */\nexport type ResilientRunType<N extends string> = `${N}_run`;\n/** The success settle Msg's `type` for the `N` family — minted by the engine. */\nexport type ResilientOkType<N extends string> = `${N}_run_ok`;\n/** The failure settle Msg's `type` for the `N` family — minted by the engine. */\nexport type ResilientErrType<N extends string> = `${N}_run_err`;\n\n/**\n * The resilience knob. EVERY field is optional: omit a brick and its gate is\n * skipped entirely (the slice still carries a default brick state so the shape\n * stays uniform across configs, but the gate is never consulted). An empty\n * config `{}` is a valid pass-through — a bare effect with no resilience at all.\n *\n * `N` is the knob's Msg-name family (see {@link ResilientName}); it is carried on\n * the config so the run Cmd and the two settle Msgs are named from ONE place.\n */\nexport interface ResilientConfig<N extends string = DefaultResilientName> {\n /**\n * What this knob's Cmd and settle Msgs are called: `<name>_run`, and the\n * `<name>_run_ok` / `<name>_run_err` the engine mints from the run handler's\n * outcome. Omit it and the family is `resilient` — `resilient_run` /\n * `resilient_run_ok` / `resilient_run_err`.\n *\n * Name a knob when a machine holds more than one of the resilient family.\n * Two unnamed knobs meet in ONE `resilient_run_ok` cell whose payload is the union\n * of both results, and the consumer discriminates by hand on `key` — which the\n * type checker cannot see. Under distinct names each knob settles into its own\n * cell, already narrowed to its own payload.\n *\n * The name is a value AND a type: the settle Msg types are generic in `N`, and\n * `N` is not inferable from `I` / `R`, so an opted-in knob spells all three\n * type arguments — `createResilientCall<In, Out, \"jev\">({ name: \"jev\", … })`.\n * The `name` field is typed at `N`, so the two cannot drift apart.\n */\n readonly name?: N;\n /**\n * The backoff brick. Omit and a failure is terminal (no backoff, no timer).\n *\n * Any bound the `../retry-backoff` union admits: a count (`RetryPolicy`), a\n * wall-clock outage budget (`DurationRetryPolicy`), or explicit\n * `unbounded: true`. A duration bound needs no extra wiring: every path that\n * records a failure already holds the instant it was observed as DATA —\n * `settle`'s `msg.at` (stamped at the interpret boundary) and `gate`'s `at`\n * (the caller's `attempt` / the retry timer's `atMs`) — so the streak clock\n * is fed from a Msg, never from a `Date.now()` inside a verb (invariant 2).\n */\n readonly retry?: AnyRetryPolicy;\n readonly circuit?: CircuitConfig;\n readonly rateLimit?: RateLimitConfig;\n readonly cache?: CacheConfig;\n readonly deadline?: DeadlineConfig;\n}\n\n// ===========================================================================\n// Slice — the Model field this knob owns. A flat record of brick states.\n// ===========================================================================\n\n/**\n * What is left of one call's deadline budget, and where the currently open\n * charging segment starts. Plain data, so it persists with the rest of the slice\n * — but the DURABLE fact is `remainingMs`, a duration, never an instant.\n *\n * The accounting has one rule, applied at every transition that carries a clock\n * (`attempt`'s `at`, a settle Msg's `at`, a timer's `atMs`): charge the segment\n * that just closed and open a new one at the same instant —\n * `remainingMs -= at - chargingSinceMs; chargingSinceMs = at`. See {@link charge}.\n *\n * `chargingSinceMs` is an IN-PROCESS anchor, not a persisted deadline. It is\n * meaningful only while the process that wrote it is alive, and `resume` re-bases\n * it on every live call before anything reads it after a reload — which is what\n * keeps downtime out of the budget. Nothing derives a timeout from it across a\n * resume; the only durable answer to \"how much budget is left\" is `remainingMs`.\n *\n * `remainingMs` is `0` when the `deadline` brick is absent, which is also the\n * \"no cap armed\" reading `subs` uses — a call with a real budget always has\n * `remainingMs > 0`, because `gate` settles a call whose budget reached zero\n * rather than letting it enter `running`.\n */\nexport interface CallBudget {\n /** In-process ms left when the open segment started. `0` = no deadline brick. */\n readonly remainingMs: number;\n /** The instant the open segment started, on the clock of the process that opened it. */\n readonly chargingSinceMs: number;\n}\n\n/**\n * Per-key call phase. `key` indexes a logical call; several can be in flight at\n * once (the generalization of the example's single `url`). Discriminated on\n * `phase` so each phase carries only its own data (pattern 11):\n *\n * - `idle` — never attempted, or fully settled and forgotten.\n * - `running` — the effect Cmd is out; awaiting `settle`.\n * Carries the deadline budget ({@link CallBudget}) and the\n * last `input` so a retry can re-issue it.\n * - `waiting_retry` — a transient failure backed off; the retry timer is armed\n * for `retryAtMs`. Carries `input` for the re-attempt, and\n * the same budget — backoff is in-process time, so it is\n * charged like an attempt.\n * - `circuit_open` — the breaker fast-failed this attempt. Terminal for this\n * call until the consumer re-attempts after cooldown.\n * - `succeeded` — settled OK. `result` is the value the port produced.\n * - `failed` — settled Err (out of retries, or no retry brick). `error`\n * carries the last failure.\n */\nexport type CallPhase<I, R> =\n | { readonly phase: \"idle\" }\n | {\n readonly phase: \"running\";\n readonly input: I;\n readonly budget: CallBudget;\n }\n | {\n readonly phase: \"waiting_retry\";\n readonly input: I;\n readonly retryAtMs: number;\n readonly budget: CallBudget;\n }\n | { readonly phase: \"circuit_open\" }\n | { readonly phase: \"succeeded\"; readonly result: R }\n | { readonly phase: \"failed\"; readonly error: unknown };\n\n/**\n * The slice. `circuit` / `bucket` are SHARED across keys (one downstream target\n * → one breaker, one bucket). `retry` and `cache` are PER KEY (each logical call\n * has its own backoff count + cached result), as is the per-call `CallPhase`.\n * Every field is plain data → the whole slice serializes into a `Store<S>`.\n *\n * `retry` / `cache` keep a default brick value even when the corresponding\n * config brick is omitted, so the slice shape is identical regardless of which\n * bricks the consumer enabled — the verbs simply never consult the unused brick.\n */\nexport interface ResilientState<I, R> {\n readonly circuit: CircuitState;\n readonly bucket: TokenBucket;\n /**\n * Per-key backoff counter. Entries minted by `backoff` are `TimedRetryState`s\n * (they carry the streak's `firstFailureAtMs`, since every failure path holds\n * the observation instant); the field is typed at the `RetryState` supertype\n * because a key that has never failed has no streak, and a slice persisted\n * before the origin existed rehydrates without one. Still plain data.\n */\n readonly retry: Readonly<Record<string, RetryState>>;\n readonly cache: TtlCache<R>;\n readonly calls: Readonly<Record<string, CallPhase<I, R>>>;\n /**\n * The instant of the latest clocked transition this slice saw — the `at` of\n * the last `attempt` / `resume` / `settleFailed`, settled Msg or timer fire.\n * `timer` counts the soonest deadline down from it, so the built-in `timer`\n * Sub gets a relative `ms` without anything reading a clock. `0` until the\n * first clocked transition.\n */\n readonly clockMs: number;\n}\n\n// ===========================================================================\n// Cmds + Msgs the knob speaks. Generic over the port input `I`.\n// ===========================================================================\n\n/**\n * The failures a run handler may return (ADR 0021). `port_rejected` is the\n * work failing — carry the cause beside the tag (`err({ _tag: \"port_rejected\",\n * cause })`); `deadline_exceeded` is a handler that timed the work out itself.\n * Either one settles as `<name>_run_err` and feeds the retry / breaker. Any\n * other failure is a contract breach and goes to the error sink.\n */\nexport type RunErr = TaggedError<\"port_rejected\" | \"deadline_exceeded\">;\n\n/**\n * The one effect this knob emits: \"run the work for `key` with `input`\". It is\n * `Cmd.define`d, so the handler you write for it returns an outcome and the\n * engine mints `<name>_run_ok` / `<name>_run_err` (ADR 0021). It carries no\n * closure — `input` is plain data (invariant 3).\n */\nexport function runCmdDef<I, R, N extends string = DefaultResilientName>(\n name: N = DEFAULT_RESILIENT_NAME as N,\n) {\n return Cmd.define(`${name}_run` as ResilientRunType<N>, {\n input: unchecked<{ readonly key: string; readonly input: I }>(),\n ok: unchecked<R>(),\n err: [\"port_rejected\", \"deadline_exceeded\"],\n });\n}\nexport type RunCmdDef<\n I,\n R,\n N extends string = DefaultResilientName,\n> = ReturnType<typeof runCmdDef<I, R, N>>;\n/**\n * The run Cmd value. `R` is its settle value — `unknown` reads any knob's run\n * Cmd; the knob's own verbs emit it at their `R`, which is what lets a machine\n * list `cmds: [rc.run]` and take the verbs' Cmds as its own.\n */\nexport type RunCmd<\n I,\n N extends string = DefaultResilientName,\n R = unknown,\n> = CmdOf<RunCmdDef<I, R, N>>;\n\n/**\n * The settle Msgs the engine mints from a run handler's outcome:\n * `<name>_run_ok` carrying the handler's `value`, `<name>_run_err` carrying its\n * declared `error`. Each carries the run Cmd it settles (`cmd.key` names the\n * call) and the `at` the engine stamped. Both are generic in the knob's name\n * family `N`, so two knobs named apart land in two `update` cells whose\n * payloads are already narrowed.\n *\n * They are the `SettledOk` / `SettledErr` pair `Cmd.define` mints, spelled out\n * so a helper that folds the error into a richer type of its own (llm-call's\n * `LlmErr`, jev-ask's `JevAskErr`) can hand `settle` its Msg with `E` widened.\n */\nexport type SucceedMsg<\n R,\n N extends string = DefaultResilientName,\n I = unknown,\n> = {\n readonly type: ResilientOkType<N>;\n readonly cmd: RunCmd<I, N>;\n readonly value: R;\n readonly at: number;\n};\nexport type FailMsg<\n N extends string = DefaultResilientName,\n I = unknown,\n E = RunErr | MalformedResult,\n> = {\n readonly type: ResilientErrType<N>;\n readonly cmd: RunCmd<I, N>;\n readonly error: E;\n readonly at: number;\n};\n\n/**\n * What `settle` reads off a settled Msg: the call's `key` (on `cmd`), the\n * engine's `at`, and the handler's `value` or `error`. {@link SucceedMsg} and\n * {@link FailMsg} are both one of these, so an engine-minted Msg passes\n * straight in; a helper that settles from its own data (a fallback answer, a\n * compaction summary) builds the record without inventing a whole run Cmd.\n */\nexport type SettleMsg<R, N extends string = DefaultResilientName> =\n | {\n readonly type?: ResilientOkType<N>;\n readonly cmd: { readonly key: string };\n readonly value: R;\n readonly at: number;\n }\n | {\n readonly type?: ResilientErrType<N>;\n readonly cmd: { readonly key: string };\n readonly error: unknown;\n readonly at: number;\n };\n\n/**\n * How a settled call ended — the third thing `settle` hands back, and the only\n * place the port's value can be read from.\n *\n * - `done` — the port answered; `value` is its result.\n * - `failed` — no retry is left (or no retry brick); `error` is the last\n * failure.\n * - `retrying` — backed off; the retry timer is armed and the call is still\n * live.\n */\nexport type SettleOutcome<R> =\n | { readonly kind: \"done\"; readonly value: R }\n | { readonly kind: \"failed\"; readonly error: unknown }\n | { readonly kind: \"retrying\" };\n\n/**\n * What `settle` returns: the settled slice, the Cmds it emitted, and the\n * {@link SettleOutcome}. One record, so the slice update and the result read\n * cannot be split apart in a hand-wired `update` cell.\n */\nexport interface SettleResult<I, R, N extends string = DefaultResilientName> {\n readonly call: ResilientState<I, R>;\n readonly cmds: readonly RunCmd<I, N, R>[];\n readonly outcome: SettleOutcome<R>;\n}\n\n/**\n * The deadline Msg tag this knob's timers dispatch, derived from its name the\n * same way the settle Msgs are. The DEFAULT family keeps the bare\n * `\"deadline_exceeded\"` literal.\n */\nexport type ResilientDeadlineType<N extends string = DefaultResilientName> =\n N extends DefaultResilientName ? \"deadline_exceeded\" : `${N}_deadline`;\n\n/**\n * The name a deadline carries for the `N` family — `undefined` for the\n * default family (no name at all, so the bare literal is dispatched), the name\n * itself otherwise. One conditional, so the value and {@link\n * ResilientDeadlineType} can never disagree about which family is unnamed.\n */\nexport type DeadlineNameOf<N extends string = DefaultResilientName> =\n N extends DefaultResilientName ? undefined : N;\n\n/**\n * The retry / deadline timer Msg — a `DeadlineExceeded` whose tag is this\n * knob's ({@link ResilientDeadlineType}), keyed by the call `key` through the\n * Sub `id`. Bare `ResilientTimerMsg` is the default family's, so it is the same\n * `DeadlineExceeded` it has always been.\n */\nexport type ResilientTimerMsg<N extends string = DefaultResilientName> =\n DeadlineExceeded<DeadlineNameOf<N>>;\n\n/**\n * The plain-data error a deadline-failed call settles with. A `{_tag, ...}`\n * sentinel — NEVER a `new Error(...)` — so the slice stays JSON-serializable\n * (Error objects round-trip to `{}` and carry a stack trace, both of which break\n * the slice's durability invariant). `id` / `atMs` echo the timer that fired so\n * a consumer can attribute the failure to its deadline.\n */\nexport type DeadlineExceededError = {\n readonly _tag: \"deadline_exceeded\";\n readonly id: string;\n readonly atMs: number;\n};\n\n// ===========================================================================\n// Internal brick-policy derivation — config → the L1 policy shapes.\n// ===========================================================================\n\n// A circuit policy is only built when the brick is configured; absence is\n// represented as `null` so the gate can short-circuit to \"always pass\".\nfunction circuitPolicy(config: ResilientConfig<string>) {\n if (config.circuit === undefined) return null;\n return {\n failureThreshold: config.circuit.threshold,\n cooldownMs: config.circuit.cooldownMs,\n halfOpenMaxProbes: config.circuit.halfOpenMaxProbes ?? 1,\n };\n}\n\n/**\n * Close the open charging segment at `at` and open the next one there: the ONE\n * place a {@link CallBudget} is debited. Every caller holds `at` as data (a\n * verb's argument, a Msg's stamp), so the budget advances without any verb\n * reading a clock. PURE.\n *\n * A budget with no deadline brick (`remainingMs` 0) is left alone — there is\n * nothing to spend, and debiting it would push it negative and read as \"spent\".\n * `at` before the anchor cannot happen on a monotonic feed, but a clamped charge\n * keeps a skewed one from CREDITING budget back.\n */\nfunction charge(budget: CallBudget, at: number): CallBudget {\n if (budget.remainingMs <= 0) return { ...budget, chargingSinceMs: at };\n const elapsed = Math.max(0, at - budget.chargingSinceMs);\n return { remainingMs: budget.remainingMs - elapsed, chargingSinceMs: at };\n}\n\n// Deadline-id families. Each call's retry timer is a deadline keyed by `key`, so\n// a machine running many concurrent calls arms and routes each independently.\n// The knob's `name` leads the id for the same reason it leads the Msg types: a\n// fired timer is routed by its id, so two knobs sharing a key would otherwise\n// answer each other's timer.\n// The default `resilient` name reproduces the ids exactly as they have always\n// read.\nfunction retryTimerId(name: string, key: string): string {\n return `${name}:retry:${key}`;\n}\nfunction deadlineTimerId(name: string, key: string): string {\n return `${name}:deadline:${key}`;\n}\n\n/**\n * The deadline Sub's `name` for a knob's family — the value side of\n * {@link DeadlineNameOf}. The default family maps to `undefined` (no name),\n * which is what keeps an unnamed knob dispatching the bare `deadline_exceeded`\n * literal rather than a `resilient_deadline` nobody is wired for.\n */\nfunction deadlineNameOf<N extends string>(name: N): DeadlineNameOf<N> {\n return (\n name === DEFAULT_RESILIENT_NAME ? undefined : name\n ) as DeadlineNameOf<N>;\n}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a resilient-call knob from `config`. `rng` is injected for retry\n * jitter — pass a fixed `() => 0.5` in tests to pin backoff; defaults to\n * `Math.random` (read at the verb boundary, never inside a brick op). It is\n * curried onto the factory rather than threaded through every verb so callers\n * spell the determinism choice once.\n *\n * Returns plain functions and the run Cmd def (ADR 0022) — see the module doc\n * for the wiring. `I` is the work's input type, `R` its result type.\n */\nexport function createResilientCall<\n I,\n R,\n N extends string = DefaultResilientName,\n>(config: ResilientConfig<N>, rng: () => number = Math.random) {\n const name = config.name ?? (DEFAULT_RESILIENT_NAME as N);\n const cPolicy = circuitPolicy(config);\n // Re-affirm the `[0, 1)` contract at the seam where the injected generator\n // feeds the backoff math (brand from `../retry-backoff`). The factory's\n // public param stays a plain `() => number` so transitive callers are\n // unchanged; `asRng` is the single point it's branded for `nextDelayMs`.\n const rngBranded = asRng(rng);\n const runCmd = runCmdDef<I, R, N>(name);\n // The name each deadline carries, which is what fixes the Msg tag it\n // dispatches. `undefined` for the DEFAULT family: an unnamed knob must keep\n // dispatching the bare `deadline_exceeded` literal every existing machine is\n // wired against, so the default is not merely spelled the same — it is the\n // absence of a name, exactly as before this parameter existed.\n const deadlineName = deadlineNameOf(name);\n\n /** The starting slice. Bricks not in `config` still get a default value. */\n function init(): ResilientState<I, R> {\n return {\n circuit: initCircuit(),\n // The bucket carries a default value even without the rateLimit brick so\n // the slice shape is uniform, but the rate-limit GATE is skipped entirely\n // when the brick is absent (see `gate`) — the bucket is simply never\n // consulted. A `0/0` placeholder bucket would be touched by `tryConsume`;\n // a `1-cap` default is harmless because the gate never calls it.\n bucket: config.rateLimit\n ? initBucket(\n config.rateLimit.capacity,\n config.rateLimit.refillPerSec,\n 0,\n )\n : initBucket(1, 1, 0),\n retry: {},\n cache: initCache<R>(),\n calls: {},\n clockMs: 0,\n };\n }\n\n /** Record `at` as the latest instant this slice saw (see `clockMs`). */\n function tick(s: ResilientState<I, R>, at: number): ResilientState<I, R> {\n return s.clockMs === at ? s : { ...s, clockMs: at };\n }\n\n /** Read this key's retry state, defaulting to a fresh one. */\n function retryOf(s: ResilientState<I, R>, key: string): RetryState {\n return s.retry[key] ?? initRetry();\n }\n\n /** A fresh full budget for a call starting at `at` (`remainingMs` 0 = no cap). */\n function freshBudget(at: number): CallBudget {\n return {\n remainingMs: config.deadline ? config.deadline.ms : 0,\n chargingSinceMs: at,\n };\n }\n\n /**\n * The budget a call already live under `key` carries forward, charged up to\n * `at`; a fresh full budget when the key has no live call. This is what makes a\n * re-attempt share ONE budget with the attempts before it rather than buying a\n * new one — the in-process invariant the deadline brick has always had.\n */\n function budgetFor(\n s: ResilientState<I, R>,\n key: string,\n at: number,\n ): CallBudget {\n const call = s.calls[key];\n if (call?.phase === \"running\" || call?.phase === \"waiting_retry\") {\n return charge(call.budget, at);\n }\n return freshBudget(at);\n }\n\n /**\n * The shared decision: budget → cache → rate-limit → circuit gate, then either\n * emit the effect or schedule a retry. `budget` is threaded so a re-attempt\n * (from `onTimer`) keeps the ORIGINAL overall budget rather than restarting it.\n * PURE — `at` is the only clock; `rng` is the injected jitter source.\n *\n * ## Why the budget is checked BEFORE the effect is emitted\n *\n * The check is step 0 and it settles rather than dispatches. A call whose\n * budget is spent must not reach the port at all: the port is a real side\n * effect, and re-issuing an attempt only to settle `deadline_exceeded` when it\n * returns spends that effect for an answer nobody reads. This is exactly the\n * resume path's shape — a process that comes back to a spent budget settles\n * here, with zero further port calls (#144).\n *\n * Used by both `attempt` (fresh call) and `onTimer` (retry timer fired) so\n * the gate logic lives in exactly one place — the example's shared `attempt`\n * helper, lifted to a keyed knob.\n *\n * ## Why rate-limit is gated BEFORE the circuit probe\n *\n * The circuit-breaker's `canPass` is not a passive read: in `half_open` it\n * *consumes* a probe slot (advancing `probes`), and the consumed slot is only\n * released when a probe RESOLVES via `onSuccess` / `onFailure`. So a probe\n * slot may only be taken when this attempt is actually going to reach the\n * backend (i.e. emit the `resilient_run` Cmd). The rate-limit gate can reject\n * an attempt and return a backoff WITHOUT any Cmd — no backend call, so no\n * settle Msg ever comes back. If the circuit probe were consumed first, that\n * rate-limited attempt would burn the half-open probe budget on a call that\n * never happened, wedging the breaker `half_open` at its probe cap forever\n * (nothing moves it out of `half_open` but a resolved probe). Running the\n * rate-limit gate first means the circuit probe is consumed ONLY on the path\n * that emits the effect.\n */\n function gate(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n budget: CallBudget,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n // 0) Budget gate. Skipped without the deadline brick (`remainingMs` stays 0\n // and never moves). A spent budget settles here — before the cache read, the\n // token spend and the circuit probe, none of which a dead call should touch.\n if (config.deadline !== undefined && budget.remainingMs <= 0) {\n return [\n setCall(s, key, {\n phase: \"failed\",\n error: {\n _tag: \"deadline_exceeded\",\n id: deadlineTimerId(name, key),\n atMs: at,\n } satisfies DeadlineExceededError,\n }),\n [],\n ];\n }\n\n // 1) Serve from cache if fresh — no effect at all. Skipped when no cache brick.\n if (config.cache !== undefined) {\n const cached = cacheGet(s.cache, key, at);\n if (cached !== undefined) {\n return [setCall(s, key, { phase: \"succeeded\", result: cached }), []];\n }\n }\n\n // 2) Rate-limit gate FIRST — it may reject with no Cmd, so it must run\n // before any circuit probe is consumed (see header note). Skipped entirely\n // when no rateLimit brick — the bucket is never consulted, so the default\n // placeholder bucket stays untouched.\n let bucket = s.bucket;\n if (config.rateLimit !== undefined) {\n const [next, hasToken] = tryConsume(bucket, at);\n bucket = next;\n if (!hasToken) {\n // Rate-limited: treat as a transient failure and back off via retry.\n // The circuit is untouched — no probe is spent on a call that never ran.\n return backoff(\n { ...s, bucket },\n key,\n input,\n \"rate_limited\",\n at,\n budget,\n );\n }\n }\n\n // 3) Circuit-breaker gate. `null` policy → no breaker → always pass. Reached\n // only once a token is secured, so a half-open probe is consumed exclusively\n // on the path that emits the effect below.\n let circuit = s.circuit;\n if (cPolicy !== null) {\n const [next, ok] = canPass(circuit, cPolicy, at);\n circuit = next;\n if (!ok) {\n return [\n { ...setCall(s, key, { phase: \"circuit_open\" }), bucket, circuit },\n [],\n ];\n }\n }\n\n // 4) All gates passed — emit the effect as data.\n return [\n {\n ...setCall(s, key, { phase: \"running\", input, budget }),\n circuit,\n bucket,\n },\n [runCmd({ key, input })],\n ];\n }\n\n /**\n * Back off after a transient failure: record it, and if `retry` permits\n * another attempt, enter `waiting_retry` with the retry timer armed; else\n * settle `failed`. When no `retry` brick is configured the call fails on the\n * first failure (no backoff, no timer). PURE.\n *\n * `at` is both the backoff anchor AND the streak clock: it is passed to\n * `recordFailure` (which starts `firstFailureAtMs` on the streak's first\n * failure and preserves it on every later one) and to `shouldRetry`, so a\n * `DurationRetryPolicy` on `config.retry` is honoured with no extra wiring.\n * A success drops `retry[key]` entirely, which drops the origin with it —\n * only an unbroken run of failures grows toward a duration budget.\n */\n function backoff(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n error: unknown,\n at: number,\n budget: CallBudget,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n if (config.retry === undefined) {\n return [setCall(s, key, { phase: \"failed\", error }), []];\n }\n const retry = recordFailure(retryOf(s, key), error, at);\n const withRetry = { ...s, retry: { ...s.retry, [key]: retry } };\n if (!shouldRetry(retry, config.retry, at)) {\n return [setCall(withRetry, key, { phase: \"failed\", error }), []];\n }\n const retryAtMs = at + nextDelayMs(retry, config.retry, rngBranded);\n return [\n setCall(withRetry, key, {\n phase: \"waiting_retry\",\n input,\n retryAtMs,\n budget,\n }),\n [],\n ];\n }\n\n // Set the phase for one key, leaving every other field untouched.\n function setCall(\n s: ResilientState<I, R>,\n key: string,\n phase: CallPhase<I, R>,\n ): ResilientState<I, R> {\n return { ...s, calls: { ...s.calls, [key]: phase } };\n }\n\n // === Verb: attempt =======================================================\n\n /**\n * Start (or restart) the call for `key` with `input` at time `at`. Runs the\n * cache → circuit → rate-limit gate and either emits the `resilient_run`\n * effect, schedules a retry, fast-fails (circuit open), or serves the cache.\n * PURE.\n *\n * A key already live keeps the budget it is under (charged up to `at`) rather\n * than being handed a fresh one, so re-issuing an in-flight call — the boot\n * path's move — cannot silently extend its deadline. Only a key with no live\n * call starts a full budget.\n */\n function attempt(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n return gate(tick(s, at), key, input, at, budgetFor(s, key, at));\n }\n\n // === Verb: resume ========================================================\n\n /**\n * Re-anchor every live call's charging clock to `at`: the verb a host calls\n * once on the boot path, before any Sub is armed or any attempt re-issued.\n *\n * This is what keeps DOWNTIME out of the budget. `chargingSinceMs` is an\n * instant on the clock of the process that wrote it; after an eviction, a crash\n * or a redeploy, the gap between it and now is time in which no attempt ran and\n * no port was called, so charging it would time a call out on the host's\n * absence rather than on the tool's slowness. `remainingMs` — the durable half\n * of the budget — is left exactly as the last live transition set it. PURE.\n */\n function resume(s: ResilientState<I, R>, at: number): ResilientState<I, R> {\n const calls: Record<string, CallPhase<I, R>> = { ...s.calls };\n for (const [key, call] of Object.entries(calls)) {\n if (call.phase !== \"running\" && call.phase !== \"waiting_retry\") continue;\n calls[key] = {\n ...call,\n budget: { ...call.budget, chargingSinceMs: at },\n };\n }\n return { ...s, calls, clockMs: at };\n }\n\n // === The two record steps `settle` is built from ==========================\n\n /**\n * Record a success for `msg.key`: close the breaker, fill the cache (if a\n * cache brick exists), reset this key's retry count, and settle `succeeded`.\n * PURE — `msg.at` is the cache write clock. Private: {@link settle} is the\n * verb.\n */\n function recordOk(\n s: ResilientState<I, R>,\n msg: Extract<SettleMsg<R, N>, { readonly value: R }>,\n ): ResilientState<I, R> {\n const { key } = msg.cmd;\n const circuit =\n cPolicy !== null ? onSuccess(s.circuit, cPolicy) : s.circuit;\n const cache =\n config.cache !== undefined\n ? cacheSet(s.cache, key, msg.value, msg.at, config.cache.ttlMs)\n : s.cache;\n // Drop this key's retry counter — a success ends the retry run.\n const retry = without(s.retry, key);\n return {\n ...setCall(s, key, { phase: \"succeeded\", result: msg.value }),\n circuit,\n cache,\n retry,\n };\n }\n\n /**\n * Record a failure for `msg.key`: trip the breaker (it may open), then back\n * off — schedule a retry if `retry` permits, else settle `failed`. PURE —\n * `msg.at` stamps the breaker trip + the retry delay base. Re-issues from the\n * call's remembered `input` and carries the original budget forward, charged\n * up to `msg.at` — the attempt that just failed spent in-process time, and a\n * retry ladder shares ONE budget across its attempts rather than getting a\n * fresh one per attempt. Private: {@link settle} is the verb.\n */\n function recordErr(\n s: ResilientState<I, R>,\n msg: Extract<SettleMsg<R, N>, { readonly error: unknown }>,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n const { key } = msg.cmd;\n const circuit =\n cPolicy !== null ? onFailure(s.circuit, cPolicy, msg.at) : s.circuit;\n const withCircuit = { ...s, circuit };\n const call = s.calls[key];\n const input = call?.phase === \"running\" ? call.input : undefined;\n const budget = budgetFor(s, key, msg.at);\n // No remembered input (e.g. a stray fail for a key not running) → just\n // settle failed after tripping the breaker; nothing to re-issue.\n if (input === undefined) {\n return [\n setCall(withCircuit, key, { phase: \"failed\", error: msg.error }),\n [],\n ];\n }\n return backoff(withCircuit, key, input, msg.error, msg.at, budget);\n }\n\n function isOk(\n msg: SettleMsg<R, N>,\n ): msg is Extract<SettleMsg<R, N>, { readonly value: R }> {\n return \"value\" in msg;\n }\n\n // === Verb: settle ========================================================\n\n /**\n * Settle the call a `<name>_run_ok` / `<name>_run_err` Msg answers, keyed off\n * `msg.cmd.key`, and say how it ended. PURE.\n *\n * A success closes the breaker, fills the cache and ends the retry run; the\n * outcome is `done` with the handler's value. A failure trips the breaker and\n * backs off: `retrying` while the retry policy allows another attempt, and\n * `failed` with the error once it does not. A `retrying` call waits on the\n * retry timer `timer` arms; its fire re-issues the run Cmd through `onTimer`.\n *\n * The settled slice, its Cmds and the outcome come back together, and the\n * result value is reachable ONLY through `outcome`. So a hand-wired settle\n * cell cannot fold the answer into its Model before the slice has settled —\n * the order that used to leave a call stuck at `running` cannot be written.\n */\n function settle(\n s: ResilientState<I, R>,\n msg: SettleMsg<R, N>,\n ): SettleResult<I, R, N> {\n const now = tick(s, msg.at);\n if (isOk(msg)) {\n return {\n call: recordOk(now, msg),\n cmds: [],\n outcome: { kind: \"done\", value: msg.value },\n };\n }\n const [call, cmds] = recordErr(now, msg);\n return {\n call,\n cmds,\n outcome:\n call.calls[msg.cmd.key]?.phase === \"waiting_retry\"\n ? { kind: \"retrying\" }\n : { kind: \"failed\", error: msg.error },\n };\n }\n\n // === Verb: settleFailed ==================================================\n\n /**\n * Settle `key` to terminal `failed` with `error` WITHOUT touching the circuit\n * breaker or this key's retry counter. PURE.\n *\n * This is the verb for a failure that is NOT a downstream-health signal: the\n * call must end now, but the breaker must not trip and the backoff run must\n * not advance. `settle` on an `_err` Msg is the opposite — it records the\n * failure against the breaker (it may open) and feeds the retry policy (it\n * may back off). Use\n * `settleFailed` when the *reason* the call cannot proceed has nothing to do\n * with downstream health:\n *\n * - `authed-call` settles a 401 it cannot fix (e.g. the refresh budget is\n * spent): a bad token is the caller's problem, not the backend's —\n * tripping the breaker on it would punish a healthy target, and backing\n * off would just retry a request that will 401 again.\n * - `paginated-walk` settles a terminal walk error (e.g. a malformed page\n * the consumer rejects) that should stop the walk without poisoning the\n * shared breaker the next page fetch passes through.\n *\n * Because it leaves `circuit` and `retry[key]` untouched, the breaker keeps\n * its health view of the actual backend and a later `attempt` for this key\n * starts a fresh retry run. Both sibling knobs previously hand-rolled this by\n * splicing `calls` directly; this verb gives them one named, tested entry\n * point so the slice's settle invariants live in exactly one place.\n *\n * `at` is the instant the caller decided it — recorded as the slice's clock,\n * since ending a call can change which deadline `timer` counts down to.\n */\n function settleFailed(\n s: ResilientState<I, R>,\n key: string,\n error: unknown,\n at: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n return [setCall(tick(s, at), key, { phase: \"failed\", error }), []];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A timer fired for some key. Two kinds, disambiguated by the Sub id:\n *\n * - retry timer → re-run the gate for that key's remembered input,\n * re-checking the circuit + bucket at THIS time.\n * - deadline timer → the overall budget elapsed → settle `failed` with a\n * deadline error, no matter the current phase.\n *\n * A timer for a key no longer `waiting_retry` (retry) / no longer in flight\n * (deadline) leaves the calls alone — the reconcile pass races the result;\n * the verb must tolerate a stale fire. Every fire records `msg.atMs` as the\n * slice's clock, so `timer` re-arms from it. PURE.\n */\n function onTimer(\n slice: ResilientState<I, R>,\n msg: { readonly id: string; readonly atMs: number },\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n const s = tick(slice, msg.atMs);\n for (const [key, call] of Object.entries(s.calls)) {\n if (msg.id === retryTimerId(name, key)) {\n if (call.phase !== \"waiting_retry\") return [s, []];\n // The backoff wait was in-process time, so it is charged before the gate\n // reads the budget — and the gate settles rather than dispatching when\n // that leaves nothing, which is how a resumed ladder ends without one\n // more port call.\n return gate(\n s,\n key,\n call.input,\n msg.atMs,\n charge(call.budget, msg.atMs),\n );\n }\n if (msg.id === deadlineTimerId(name, key)) {\n // Deadline only fires for a still-active call. A settled call has no\n // armed deadline Sub, but tolerate a stale fire defensively.\n if (call.phase !== \"running\" && call.phase !== \"waiting_retry\")\n return [s, []];\n // Plain-data sentinel, NOT a `new Error(...)`: an Error object is not\n // JSON-serializable (it round-trips to `{}`) and captures a stack trace,\n // both of which break the slice's durability invariant once it is\n // persisted into a `Store<S>` and reloaded. A `{_tag, ...}` record keeps\n // the slice round-trip-equal under JSON.parse(JSON.stringify(...)).\n return [\n setCall(s, key, {\n phase: \"failed\",\n error: {\n _tag: \"deadline_exceeded\",\n id: msg.id,\n atMs: msg.atMs,\n } satisfies DeadlineExceededError,\n }),\n [],\n ];\n }\n }\n return [s, []];\n }\n\n // === Timers ==============================================================\n\n /**\n * The deadlines this slice is waiting on: a retry timer for every\n * `waiting_retry` call, and (when the `deadline` brick is configured) an\n * overall deadline timer for every still-active call. Each is an absolute\n * instant; `timer` arms the soonest. PURE.\n */\n function deadlines(\n s: ResilientState<I, R>,\n ): readonly DeadlineSub<DeadlineNameOf<N>>[] {\n const out: DeadlineSub<DeadlineNameOf<N>>[] = [];\n for (const [key, call] of Object.entries(s.calls)) {\n if (call.phase === \"waiting_retry\") {\n out.push(\n deadlineSub(retryTimerId(name, key), call.retryAtMs, {\n name: deadlineName,\n }),\n );\n }\n if (\n config.deadline !== undefined &&\n (call.phase === \"running\" || call.phase === \"waiting_retry\") &&\n call.budget.remainingMs > 0\n ) {\n // The absolute instant the timer arms at is DERIVED here, from the open\n // segment's anchor plus what is left — it is never persisted, so a slice\n // that came back from storage arms off the anchor `resume` just re-based\n // rather than off a stamp the downtime ran past.\n out.push(\n deadlineSub(\n deadlineTimerId(name, key),\n call.budget.chargingSinceMs + call.budget.remainingMs,\n { name: deadlineName },\n ),\n );\n }\n }\n return out;\n }\n\n /**\n * The built-in `timer` Sub's deps for this slice: the soonest of\n * {@link deadlines}, counted down from the slice's `clockMs`, or `null` when\n * nothing is waiting. Declare it in the machine and `run` needs no runner:\n *\n * subs: [{ type: \"timer\", deps: (s) => rc.timer(s.call) }]\n *\n * Its fire is this knob's timer Msg (`deadline_exceeded`, or\n * `<name>_deadline` for a named knob); route it to `onTimer`. A phase change\n * drops the matching deadline, and the new deps restart the countdown — no\n * manual `clearTimeout`. PURE.\n */\n function timer(\n s: ResilientState<I, R>,\n ): TimerDeps<ResilientTimerMsg<N>> | null {\n return nextTimer(deadlines(s), s.clockMs);\n }\n\n return {\n // The Msg-name family this knob speaks, as a VALUE typed at `N`: the Cmd,\n // the settle Msgs and the timer ids all derive from it.\n name,\n /** The `Cmd.define`d run Cmd; list it in the machine's `cmds`. */\n run: runCmd,\n init,\n attempt,\n resume,\n settle,\n settleFailed,\n onTimer,\n deadlines,\n timer,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the\n * slice lives at `state.resilience`. A convenience for the common single-slice\n * host; consumers with a differently-named field spread by hand. Pure — a thin\n * record rebuild, no clock / RNG.\n *\n * The record rebuild itself is `liftSlice` from the root door (#231) — this\n * stays as the named, pre-keyed convenience for the `state.resilience` field,\n * unchanged in name, signature and subpath.\n */\nexport function liftResilience<\n S extends { resilience: ResilientState<I, R> },\n I,\n R,\n C extends Cmd,\n>(\n state: S,\n result: readonly [ResilientState<I, R>, readonly C[]],\n): readonly [S, readonly C[]] {\n return liftSlice(\"resilience\", state, result);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/resilience/circuit-breaker/index.ts","../src/internal/resilience/rate-limit/index.ts","../src/internal/resilience/resilient-call/index.ts"],"names":[],"mappings":"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","file":"chunk-MBQR3LFW.js","sourcesContent":["/**\n * @packageDocumentation\n * internal/resilience/circuit-breaker — per-target failure tracking as pure state + ops.\n *\n * Same shape as `../../work-queue/ops.ts`, `../rate-limit/`, and\n * `../../idempotency/`: a state type plus pure transition functions.\n * Host-agnostic by construction — no `Date.now()`, no timers, no I/O. The\n * caller injects the clock (`nowMs`) and wires the result into its own reducer\n * / interpret. This keeps the breaker inside TEA's invariant 2 (transitions are\n * pure: same inputs → same outputs, no `Date.now`) so a consumer can fold it\n * through `update` and exercise it with `replay` / the pbt fold without a fake\n * clock.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). `canPass` returns a `[next, allowed]` tuple so\n * the caller branches on the decision while threading the advanced state forward\n * — same convention as `rate-limit`'s `tryConsume` / `record`. `onSuccess` /\n * `onFailure` are the outcome-recording transitions the caller folds in once the\n * guarded call resolves.\n *\n * ## The phase machine\n *\n * A circuit breaker guards a fallible downstream target (an HTTP backend, a DB,\n * a flaky tool call). Instead of hammering a target that is already failing, it\n * trips OPEN after enough failures and fast-fails subsequent calls for a\n * cooldown, then probes with a few trial calls before fully closing again.\n * Three phases, each carrying ONLY its own data so impossible states are\n * unrepresentable (pattern 11):\n *\n * - **closed** — the healthy steady state. Calls pass through. Carries\n * `failures`: the count of *consecutive* failures since the last success.\n * A success resets it to 0; the `failureThreshold`-th failure trips the\n * breaker OPEN. Only `closed` has a failure counter — `open` and\n * `half_open` don't, because in those phases the count is irrelevant\n * (the breaker has already tripped / is already probing).\n *\n * - **open** — the tripped state. All calls fast-fail (no downstream load).\n * Carries `openedAtMs`: the clock reading at which the breaker tripped.\n * `canPass` compares `nowMs - openedAtMs` against `cooldownMs` to decide\n * when to start probing. Only `open` carries a timestamp — neither other\n * phase is timing anything.\n *\n * - **half_open** — the probing state. A limited number of trial calls are\n * allowed through to test whether the target has recovered. Carries\n * `probes`: the count of probes admitted so far this probing round.\n * `canPass` admits while `probes < halfOpenMaxProbes`, then fast-fails the\n * rest (so a slow target can't be flooded by every queued caller at once).\n * A probe success closes the breaker; a probe failure re-opens it (and\n * resets the cooldown clock). Only `half_open` counts in-flight probes.\n *\n * ### Transition table\n *\n * Rows are the current phase; columns are the event. `→` denotes the next\n * phase; `self` means unchanged. `nowMs` is the injected clock.\n *\n * | from \\ event | onSuccess | onFailure | canPass (→ allowed?) |\n * | ------------ | -------------- | -------------------------------------- | ------------------------------------------------------------------------------------ |\n * | closed | closed(0) | failures+1; trip → open(nowMs) at thr. | self → true (always) |\n * | open | self *(¹)* | self | elapsed ≥ cooldown → half_open(1), true; else self → false |\n * | half_open | → closed(0) | → open(nowMs) (probe failed) | probes < max → half_open(probes+1), true; else self → false |\n *\n * *(¹)* `onSuccess` on `open` \"shouldn't happen\": no call was admitted while\n * open, so no success can be reported for one. We leave the state unchanged\n * rather than throw — the breaker is a coordination aid, not a correctness\n * gate, and a spurious success report (e.g. a late-arriving result from before\n * the trip) must not silently un-trip the breaker. `onFailure` on `open` is\n * likewise a no-op: the breaker is already tripped; the `open` clock is anchored\n * at the trip, not bumped by late failures.\n *\n * ### The probe-admission asymmetry\n *\n * Entering `half_open` happens inside `canPass`, not `onSuccess` / `onFailure`,\n * because the cooldown is a *time* condition and only `canPass` reads the clock\n * against `openedAtMs`. The first `canPass` after the cooldown elapses both\n * transitions the phase AND admits the probe (`[half_open(1), true]`) — one\n * caller drives the recovery attempt. Subsequent `canPass` calls in the same\n * round admit up to `halfOpenMaxProbes` total, then fast-fail until a probe\n * resolves and `onSuccess` / `onFailure` moves the phase out of `half_open`.\n *\n * This module is a textbook three-state machine — a clean `/machine-viz` demo.\n *\n * Internal to the package (`src/internal/resilience/`), not a published\n * subpath (#46); same one-shape-per-module rule as `work-queue`, `rate-limit`,\n * and `idempotency`.\n */\n\n/**\n * Circuit-breaker policy — pure configuration, no mutable state. One policy is\n * shared across all the calls guarded by one logical breaker (often a\n * module-scope const). `CircuitState` carries the mutable counts; this carries\n * the thresholds those counts are compared against.\n */\nexport interface CircuitPolicy {\n /**\n * Consecutive failures in `closed` that trip the breaker `open`. The\n * `failureThreshold`-th failure is the one that trips — with a threshold of\n * 5, the breaker survives 4 failures and opens on the 5th.\n */\n readonly failureThreshold: number;\n /**\n * How long the breaker stays `open` before `canPass` starts admitting\n * probes, in milliseconds. Measured from `openedAtMs`; the cutoff is\n * half-open (`elapsed >= cooldownMs` admits), matching `rate-limit` /\n * `idempotency` window edges — a breaker opened at `t` first probes at\n * `t + cooldownMs`.\n */\n readonly cooldownMs: number;\n /**\n * How many probe calls `half_open` admits before fast-failing the rest of\n * the round. Caps the load on a recovering target — a single slow probe\n * can't be joined by every queued caller. The round ends when a probe\n * resolves: success closes the breaker, failure re-opens it.\n */\n readonly halfOpenMaxProbes: number;\n}\n\n/**\n * Sensible defaults: trip after 5 consecutive failures, cool down for 30s,\n * admit a single probe before deciding. Tuned for guarding a network backend;\n * override per call site rather than mutating this shared const.\n */\nexport const defaultCircuitPolicy: CircuitPolicy = {\n failureThreshold: 5,\n cooldownMs: 30_000,\n halfOpenMaxProbes: 1,\n};\n\n/**\n * The breaker's phase. A discriminated union on `phase` so each phase carries\n * ONLY its own data and impossible combinations are unrepresentable (pattern\n * 11): there is no state with both a failure count AND an open timestamp,\n * because such a state is meaningless. `phase` is the discriminant; every field\n * is `readonly` so the whole value is trivially JSON-serializable into a\n * consumer's `Store<S>` and safe to thread through a pure reducer.\n *\n * - `closed` carries `failures` — consecutive failures since last success.\n * - `open` carries `openedAtMs` — clock reading when it tripped.\n * - `half_open` carries `probes` — probes admitted this round.\n */\nexport type CircuitState =\n | { readonly phase: \"closed\"; readonly failures: number }\n | { readonly phase: \"open\"; readonly openedAtMs: number }\n | { readonly phase: \"half_open\"; readonly probes: number };\n\n/** The starting state: closed with zero recorded failures. */\nexport function initCircuit(): CircuitState {\n return { phase: \"closed\", failures: 0 };\n}\n\n/**\n * Record a successful guarded call. PURE — returns a new state; the input is\n * never mutated.\n *\n * - `closed` → reset `failures` to 0 (a success breaks the consecutive run).\n * - `half_open` → the probe succeeded; the target has recovered → `closed`.\n * - `open` → unchanged. A success \"shouldn't happen\" while open (no call\n * was admitted to produce one); a late/spurious success must\n * not silently un-trip the breaker. See module header note ¹.\n */\nexport function onSuccess(\n state: CircuitState,\n _policy: CircuitPolicy,\n): CircuitState {\n switch (state.phase) {\n case \"closed\":\n // Already closed at 0 → return the same shape (cheap; the consumer's\n // store only re-persists on a real change if it dedupes).\n return state.failures === 0 ? state : { phase: \"closed\", failures: 0 };\n case \"half_open\":\n return { phase: \"closed\", failures: 0 };\n case \"open\":\n return state;\n }\n}\n\n/**\n * Record a failed guarded call. PURE — returns a new state; the input is never\n * mutated. `nowMs` is the injected clock, stamped onto the `open` phase when the\n * breaker trips so `canPass` can measure the cooldown later.\n *\n * - `closed` → `failures + 1`; if that reaches `failureThreshold`, trip to\n * `open(nowMs)`. The threshold-th failure is the one that trips.\n * - `half_open` → the probe failed; the target is still unhealthy →\n * `open(nowMs)`, restarting the cooldown clock so a fresh\n * cooldown elapses before the next probe round.\n * - `open` → unchanged. The breaker is already tripped; the `open` clock\n * is anchored at the original trip, not bumped by late\n * failures. See module header note ¹.\n */\nexport function onFailure(\n state: CircuitState,\n policy: CircuitPolicy,\n nowMs: number,\n): CircuitState {\n switch (state.phase) {\n case \"closed\": {\n const failures = state.failures + 1;\n // `>=` not `===`: a threshold of 0 (or any already-exceeded count from a\n // policy change) trips immediately rather than over-counting forever.\n return failures >= policy.failureThreshold\n ? { phase: \"open\", openedAtMs: nowMs }\n : { phase: \"closed\", failures };\n }\n case \"half_open\":\n // A failed probe re-opens and resets the cooldown clock to nowMs.\n return { phase: \"open\", openedAtMs: nowMs };\n case \"open\":\n return state;\n }\n}\n\n/**\n * Decide whether a call may pass, advancing the phase as a side effect of the\n * decision. PURE — returns `[next, allowed]`; the input is never mutated.\n * Branch on the boolean, thread the state forward regardless:\n *\n * const [c1, allowed] = canPass(state.circuit, policy, nowMs);\n * if (!allowed) return [{ ...state, circuit: c1 }, [fastFailCmd]];\n * return [{ ...state, circuit: c1 }, [doGuardedCallCmd]];\n *\n * - `closed` → `[state, true]` — always passes; failures are recorded by\n * `onFailure` once the call resolves, not gated here.\n * - `open` → if `nowMs - openedAtMs >= cooldownMs` the cooldown has\n * elapsed → enter `half_open` and admit this caller as the\n * first probe: `[half_open(1), true]`. Otherwise still cooling\n * down → `[state, false]` (fast-fail, no downstream load).\n * - `half_open` → admit while `probes < halfOpenMaxProbes`, incrementing the\n * count: `[half_open(probes+1), true]`. Once the cap is hit →\n * `[state, false]` until a probe resolves and `onSuccess` /\n * `onFailure` moves the phase out of `half_open`.\n *\n * Entering `half_open` lives here (not in `onSuccess` / `onFailure`) because the\n * cooldown is a time condition and only `canPass` reads `nowMs` against\n * `openedAtMs`. See the module header's probe-admission note.\n */\nexport function canPass(\n state: CircuitState,\n policy: CircuitPolicy,\n nowMs: number,\n): readonly [CircuitState, boolean] {\n switch (state.phase) {\n case \"closed\":\n return [state, true];\n case \"open\": {\n const elapsed = nowMs - state.openedAtMs;\n // Half-open cutoff: elapsed exactly == cooldownMs admits the first probe,\n // matching the >= edge `rate-limit` / `idempotency` use for their windows.\n if (elapsed >= policy.cooldownMs) {\n return [{ phase: \"half_open\", probes: 1 }, true];\n }\n return [state, false];\n }\n case \"half_open\":\n if (state.probes < policy.halfOpenMaxProbes) {\n return [{ phase: \"half_open\", probes: state.probes + 1 }, true];\n }\n return [state, false];\n }\n}\n","/**\n * internal/resilience/rate-limit — two rate limiters as pure state + ops.\n *\n * Same shape as `../../work-queue/ops.ts`: a state type plus pure\n * transition functions. Host-agnostic by construction — no `Date.now()`, no\n * timers, no I/O. The caller injects the clock (`nowMs`) and wires the result\n * into its own reducer / interpret. This keeps the limiters inside TEA's\n * invariant 2 (transitions are pure: same inputs → same outputs, no `Date.now`)\n * so a consumer can fold them through `update` and exercise them with `replay`\n * / the pbt fold without a fake clock.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). The `tryConsume` / `record` ops return a\n * `[next, ok]` tuple so the caller branches on the decision while threading the\n * advanced state forward; `refill` / `remaining` are read-shaped helpers.\n *\n * Two complementary algorithms, picked by the cost the caller wants to pay:\n *\n * - **Token bucket** — O(1) state (four numbers), smooths bursts against a\n * steady refill rate. Allows a burst up to `capacity`, then throttles to\n * `refillPerSec`. Cheapest; the default choice.\n * - **Sliding window log** — O(limit) state (one timestamp per in-window\n * hit), exact \"no more than `limit` events per `windowMs`\". Pay the memory\n * for precise window edges when a token bucket's continuous refill would\n * let through too many at a boundary.\n *\n * Internal to the package (`src/internal/resilience/`), not a published\n * subpath (#46); same one-shape-per-module rule as `work-queue` and `do`.\n */\n\n// ===========================================================================\n// Token bucket\n// ===========================================================================\n\n/**\n * A token bucket: `tokens` of `capacity` available now, replenished at\n * `refillPerSec` tokens per second. `lastRefillMs` is the clock reading at\n * which `tokens` was last computed — `refill` advances it. All four fields are\n * plain numbers so the whole bucket is trivially JSON-serializable into a\n * consumer's `Store<S>`.\n */\nexport interface TokenBucket {\n readonly tokens: number;\n readonly capacity: number;\n readonly refillPerSec: number;\n readonly lastRefillMs: number;\n}\n\n/**\n * Create a full bucket. Starts at `capacity` tokens (the conventional \"a fresh\n * limiter lets the first burst through\" behavior) and stamps `lastRefillMs`\n * with the caller's clock so the first `refill` measures elapsed time from\n * boot, not from epoch 0.\n */\nexport function initBucket(\n capacity: number,\n refillPerSec: number,\n nowMs: number,\n): TokenBucket {\n return {\n tokens: capacity,\n capacity,\n refillPerSec,\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Add the tokens accrued since `lastRefillMs`, clamped to `capacity`, and\n * advance `lastRefillMs` to `nowMs`. PURE — returns a new bucket; the input is\n * untouched.\n *\n * A `nowMs` at or before `lastRefillMs` (clock not advanced, or skewed\n * backwards) adds a non-positive amount; `Math.max(0, …)` floors the elapsed\n * term so a backwards clock can never *remove* tokens. `lastRefillMs` still\n * advances to `nowMs` so the bucket tracks the latest reading the caller\n * presented — the next forward step measures from there, not from a stale\n * future timestamp.\n */\nexport function refill(bucket: TokenBucket, nowMs: number): TokenBucket {\n const elapsedSec = Math.max(0, nowMs - bucket.lastRefillMs) / 1000;\n const replenished = bucket.tokens + elapsedSec * bucket.refillPerSec;\n return {\n ...bucket,\n tokens: Math.min(bucket.capacity, replenished),\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Refill against `nowMs`, then attempt to take `n` tokens (default 1).\n *\n * On success returns `[next, true]` with `n` subtracted; on failure returns\n * `[refilled, false]` — the refilled bucket (so the caller keeps the advanced\n * clock + accrued tokens) with no tokens spent. Branch on the boolean, thread\n * the bucket forward regardless:\n *\n * const [b1, ok] = tryConsume(bucket, nowMs);\n * return ok ? [{ ...state, bucket: b1 }, [doWork]] : [{ ...state, bucket: b1 }, []];\n *\n * PURE — input bucket is never mutated.\n */\nexport function tryConsume(\n bucket: TokenBucket,\n nowMs: number,\n n = 1,\n): readonly [TokenBucket, boolean] {\n const refilled = refill(bucket, nowMs);\n if (refilled.tokens >= n) {\n return [{ ...refilled, tokens: refilled.tokens - n }, true];\n }\n return [refilled, false];\n}\n\n// ===========================================================================\n// Sliding window log\n// ===========================================================================\n\n/**\n * A sliding-window log: the timestamps of every hit still inside the trailing\n * `windowMs`, capped at `limit` events per window. `hits` is `readonly` and\n * kept oldest-first (pruning slices the stale prefix off the front, recording\n * appends to the back), so the array is its own sorted index. Exact at the\n * window edge — unlike a token bucket's continuous refill, a hit leaves the\n * count the instant it ages past `windowMs`.\n */\nexport interface SlidingWindow {\n readonly hits: readonly number[];\n readonly windowMs: number;\n readonly limit: number;\n}\n\n/** Create an empty window. No hits recorded yet. */\nexport function initWindow(windowMs: number, limit: number): SlidingWindow {\n return { hits: [], windowMs, limit };\n}\n\n/**\n * The window-edge rule, in one place: keep only the hits still live at `nowMs`.\n * The cutoff is half-open — a hit exactly `windowMs` old is dropped (`> cutoff`,\n * not `>=`), so a hit recorded at `t` no longer counts at `t + windowMs` and the\n * window's capacity fully recovers `windowMs` after the last hit. `record` (the\n * writer) and `remaining` (the read helper) both prune through here so a change\n * to the edge semantics can't leave the reader disagreeing with the writer.\n */\nfunction prune(window: SlidingWindow, nowMs: number): readonly number[] {\n const cutoff = nowMs - window.windowMs;\n return window.hits.filter((t) => t > cutoff);\n}\n\n/**\n * Prune hits older than `nowMs - windowMs`, then record `nowMs` if there is\n * room. Returns `[next, true]` with the hit appended when `hits.length < limit`\n * after pruning, else `[pruned, false]` — the pruned window (capacity recovers\n * as old hits age out) with nothing appended.\n *\n * The cutoff is half-open (see {@link prune}): a hit exactly `windowMs` old is\n * pruned, so a hit recorded at `t` no longer counts at `t + windowMs`. That\n * makes the window's capacity fully recover `windowMs` after the last hit, the\n * behavior tests pin.\n *\n * PURE — input window is never mutated; `hits` is rebuilt by `filter` +\n * spread.\n */\nexport function record(\n window: SlidingWindow,\n nowMs: number,\n): readonly [SlidingWindow, boolean] {\n const pruned = prune(window, nowMs);\n if (pruned.length < window.limit) {\n return [{ ...window, hits: [...pruned, nowMs] }, true];\n }\n return [{ ...window, hits: pruned }, false];\n}\n\n/**\n * How many more hits `record` would accept at `nowMs` without blocking —\n * `limit` minus the live (post-prune) hit count, floored at 0. Read-only: it\n * prunes a local copy to answer but returns no new window. Use it to render a\n * \"N requests remaining\" affordance without committing a hit.\n */\nexport function remaining(window: SlidingWindow, nowMs: number): number {\n const live = prune(window, nowMs).length;\n return Math.max(0, window.limit - live);\n}\n","/**\n * @packageDocumentation\n *\n * Internal to the package (`src/internal/resilience/`, #46); consumers reach it\n * through `@demlik/tea/resilience`.\n *\n * internal/resilience/resilient-call — the ROOT composition: a single resilience knob\n * that bundles cache → circuit-breaker → rate-limit → exponential-backoff retry\n * (with a deadline-driven retry timer) around one fallible piece of work.\n *\n * This is `examples/resilient-fetch.ts`'s hand-wired `attempt()` generalized:\n *\n * - It is **keyed**. The example tracked one URL at a time; this tracks N\n * concurrent logical calls, each under a string `key`. The cache, the\n * waiting-retry bookkeeping, and the retry timers are all per-key, while the\n * circuit breaker and token bucket are shared (one downstream target, one\n * breaker / one bucket — the example's implicit single-target assumption\n * made explicit).\n * - It is **config-driven**. You hand `createResilientCall` a\n * `ResilientConfig` and get back plain functions: `init()`, the verbs\n * (`attempt` / `settle` / `onTimer` / `resume` / `settleFailed`), the\n * `deadlines(state)` list and the `timer(state)` that arms it, and `run` —\n * the `Cmd.define`d Cmd the knob emits. You call them from your own\n * `update` (ADR 0022); nothing mounts or wraps your machine.\n * - Every brick is **optional**. Omit `cache` and the cache gate disappears\n * (`get` is never consulted); omit `circuit` and every call passes the\n * breaker; omit `rateLimit` and the bucket never throttles; omit `retry`\n * and a failure is terminal (no backoff, no timer); omit `deadline` and no\n * overall time budget is armed. \"Omit a brick → omit its gate\" is the\n * whole config story — there are no other modes.\n *\n * Four later compositions (`llm-call`, `paginated-walk`, `authed-call`,\n * `reconciler`) inherit THIS slice + verb shape, so the contract here is\n * load-bearing: the slice is a flat record of brick states, every verb returns\n * `readonly [State, Cmd[]]`, time is always an `at` / `now` parameter, and the\n * RNG used for jitter is injected on the config (never read inside a verb).\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is a plain-data Model field (brick states are all\n * JSON-serializable), so it survives DO eviction / reload.\n * - **Replayable** — every transition is a verb returning new state + Cmds;\n * `replay` reconstructs it exactly. Nothing here closes over time or RNG.\n *\n * ## The work runs in your handler (ADR 0021)\n *\n * The knob ships no engine code. Its one effect is the `run` Cmd\n * (`<name>_run`), built with `Cmd.define`, and you interpret it in your\n * engine's own style. The handler returns an outcome and the engine mints\n * `<name>_run_ok` / `<name>_run_err`, which `settle` reads:\n *\n * interpret: {\n * resilient_run: async (cmd, { ok, err }) => {\n * try { return ok(await fetchUser(cmd.input)); }\n * catch (cause) { return err({ _tag: \"port_rejected\", cause }); }\n * },\n * }\n *\n * A failure the handler does not return as a declared tag (a throw, an\n * undeclared tag) is a contract breach: it goes to the error sink and never\n * reaches `settle`, so it is never retried.\n *\n * ## Where the clock / RNG live\n *\n * Inside the verbs: nowhere. Time arrives as `at` on `attempt` / `resume` /\n * `settleFailed`, as the `at` the engine stamps on a settled Msg, and as `atMs`\n * on a timer Msg. Jitter RNG is injected once at\n * `createResilientCall(config, rng)` and threaded into `nextDelayMs` at the\n * verb boundary — deterministic in tests, `Math.random` by default.\n *\n * Every clocked verb also records its instant on the slice (`clockMs`). That is\n * what lets `timer(state)` hand the built-in `timer` Sub a RELATIVE countdown to\n * the soonest deadline without reading a clock: the Sub's deps change whenever\n * the slice's clock moves, so the engine restarts the countdown from that\n * instant.\n *\n * That same `at` is what makes a DURATION-bounded retry budget reachable here:\n * `recordFailure` / `shouldRetry` are fed the failure's observation instant,\n * so `config.retry` accepts any `AnyRetryPolicy` — a count, a wall-clock outage\n * budget (`maxElapsedMs`), or explicit `unbounded: true` — with no new argument\n * on any verb and no clock read inside one.\n *\n * ## Typical wiring\n *\n * const rc = createResilientCall<string, Result>({\n * cache: { ttlMs: 60_000 },\n * circuit: { threshold: 5, cooldownMs: 30_000 },\n * rateLimit: { capacity: 10, refillPerSec: 5 },\n * retry: defaultRetryPolicy,\n * deadline: { ms: 5_000 },\n * });\n *\n * // One helper of your own folds the outcome into your Model. The value is\n * // only reachable through `r.outcome`, so the slice has always settled first.\n * function onSettle(s: Model, r: ReturnType<typeof rc.settle>) {\n * const next = { ...s, resilience: r.call };\n * switch (r.outcome.kind) {\n * case \"done\": return [{ ...next, result: r.outcome.value }, r.cmds] as const;\n * case \"failed\": return [next, r.cmds] as const;\n * case \"retrying\": return [next, r.cmds] as const;\n * }\n * }\n *\n * // in the machine:\n * update: {\n * fetch: (s, m) => liftResilience(s, rc.attempt(s.resilience, m.key, m.input, m.at)),\n * resilient_run_ok: (s, m) => onSettle(s, rc.settle(s.resilience, m)),\n * resilient_run_err: (s, m) => onSettle(s, rc.settle(s.resilience, m)),\n * deadline_exceeded: (s, m) => liftResilience(s, rc.onTimer(s.resilience, m)),\n * },\n * subs: [{ type: \"timer\", deps: (s) => rc.timer(s.resilience) }],\n *\n * `timer` is the engine's built-in Sub, so `run` needs no `subscribe` for it.\n * `docs/how-to/hand-wire-a-resilient-call.md` walks the whole machine.\n *\n * ## Naming a knob, when a machine holds more than one\n *\n * Every knob above speaks `resilient_run` / `resilient_run_ok` /\n * `resilient_run_err`, so TWO of them in one machine meet in one\n * `resilient_run_ok` cell whose `value` is the union of both payloads —\n * discriminated by hand on `cmd.key`, which the type checker cannot grade.\n * `config.name` renames the whole family, Cmd and both settle Msgs together:\n *\n * const jev = createResilientCall<JevRequest, JevOk, \"jev\">({ name: \"jev\" });\n * const llm = createResilientCall<LlmCall, LlmOk, \"llm\">({ name: \"llm\" });\n *\n * update: {\n * jev_run_ok: (s, m) => …, // m.value is JevOk — no `key` switch\n * llm_run_ok: (s, m) => …, // m.value is LlmOk\n * }\n *\n * The name leads the retry / deadline ids too (`jev:retry:<key>`), because a\n * fired timer is routed by its id and two knobs would otherwise answer each\n * other's timer on a shared key, and it names the timer Msg (`jev_deadline`).\n * `N` is not inferable from `I` / `R`, so an opted-in knob spells all three\n * type arguments; `config.name` is typed at `N`, so value and type cannot\n * drift. Omit `name` and every one of those strings is `resilient*`.\n */\n\nimport { liftSlice } from \"../../../compose\";\nimport {\n Cmd,\n type CmdOf,\n type MalformedResult,\n type TaggedError,\n type TimerDeps,\n} from \"../../../index\";\nimport type { MsgType } from \"../../../protocol\";\nimport { without } from \"../../../pure/core\";\nimport {\n type AnyRetryPolicy,\n asRng,\n initRetry,\n nextDelayMs,\n type RetryState,\n recordFailure,\n shouldRetry,\n} from \"../../../retry-backoff\";\nimport { unchecked } from \"../../schema\";\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../cache\";\nimport {\n type CircuitState,\n canPass,\n initCircuit,\n onFailure,\n onSuccess,\n} from \"../circuit-breaker\";\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n nextTimer,\n} from \"../deadline\";\nimport { initBucket, type TokenBucket, tryConsume } from \"../rate-limit\";\n\n// ===========================================================================\n// Config — the knob. Every brick optional; omit a brick → omit its gate.\n// ===========================================================================\n\n/** Circuit-breaker knob — only the two numbers a consumer ever tunes. */\nexport interface CircuitConfig {\n /** Consecutive failures that trip the breaker open. */\n readonly threshold: number;\n /** How long the breaker stays open before admitting a probe, in ms. */\n readonly cooldownMs: number;\n /**\n * Probes admitted in `half_open` before fast-failing the rest of the round.\n * Defaults to 1 (a single probe decides recovery) — matches the circuit-\n * breaker module's `defaultCircuitPolicy`.\n */\n readonly halfOpenMaxProbes?: number;\n}\n\n/** Token-bucket rate-limit knob. */\nexport interface RateLimitConfig {\n /** Burst size — the bucket starts full at `capacity`. */\n readonly capacity: number;\n /** Steady-state refill rate, tokens per second. */\n readonly refillPerSec: number;\n}\n\n/** Per-entry TTL cache knob. */\nexport interface CacheConfig {\n /** How long a cached success stays fresh, in ms. */\n readonly ttlMs: number;\n}\n\n/**\n * Overall deadline knob — a budget of IN-PROCESS time per in-flight call.\n *\n * The budget is charged in elapsed time this process actually observed, not by\n * a position on the host's wall clock. Time in which no process exists — a DO\n * eviction, a crash between attempts, a redeploy — is NOT charged: `resume`\n * re-anchors every live call's charging clock, so downtime lands outside every\n * segment. A timeout therefore means \"the tool took too long\", never \"the host\n * was away too long\" (#144).\n */\nexport interface DeadlineConfig {\n /** Budget of in-process time from the moment a call starts, in ms. */\n readonly ms: number;\n}\n\n// ===========================================================================\n// The Msg-name family — one `name` derives the Cmd and both settle Msgs.\n// ===========================================================================\n\n/**\n * The family every unnamed knob speaks: `resilient_run` / `resilient_run_ok` /\n * `resilient_run_err`. Identical to the `MsgType.Resilient*` literals, which\n * stay the vocabulary of every knob that passes no name.\n */\nexport const DEFAULT_RESILIENT_NAME = \"resilient\";\n/**\n * The Msg-name family a resilient call uses when it is given no name:\n * `resilient`.\n */\nexport type DefaultResilientName = typeof DEFAULT_RESILIENT_NAME;\n\n// The default family IS the protocol's `MsgType.Resilient*` vocabulary, not a\n// parallel spelling of it. These three lines fail to compile the moment the two\n// drift, which is the only thing keeping \"omit the name and nothing changes\"\n// true for every machine wired before the name existed.\nconst _runIsProtocol: typeof MsgType.ResilientRun =\n `${DEFAULT_RESILIENT_NAME}_run` as const;\nconst _okIsProtocol: typeof MsgType.ResilientOk =\n `${DEFAULT_RESILIENT_NAME}_run_ok` as const;\nconst _errIsProtocol: typeof MsgType.ResilientErr =\n `${DEFAULT_RESILIENT_NAME}_run_err` as const;\nvoid [_runIsProtocol, _okIsProtocol, _errIsProtocol];\n\n/** The run Cmd's `type` for the `N` family. */\nexport type ResilientRunType<N extends string> = `${N}_run`;\n/** The success settle Msg's `type` for the `N` family — minted by the engine. */\nexport type ResilientOkType<N extends string> = `${N}_run_ok`;\n/** The failure settle Msg's `type` for the `N` family — minted by the engine. */\nexport type ResilientErrType<N extends string> = `${N}_run_err`;\n\n/**\n * The resilience knob. EVERY field is optional: omit a brick and its gate is\n * skipped entirely (the slice still carries a default brick state so the shape\n * stays uniform across configs, but the gate is never consulted). An empty\n * config `{}` is a valid pass-through — a bare effect with no resilience at all.\n *\n * `N` is the knob's Msg-name family (see {@link ResilientName}); it is carried on\n * the config so the run Cmd and the two settle Msgs are named from ONE place.\n */\nexport interface ResilientConfig<N extends string = DefaultResilientName> {\n /**\n * What this knob's Cmd and settle Msgs are called: `<name>_run`, and the\n * `<name>_run_ok` / `<name>_run_err` the engine mints from the run handler's\n * outcome. Omit it and the family is `resilient` — `resilient_run` /\n * `resilient_run_ok` / `resilient_run_err`.\n *\n * Name a knob when a machine holds more than one of the resilient family.\n * Two unnamed knobs meet in ONE `resilient_run_ok` cell whose payload is the union\n * of both results, and the consumer discriminates by hand on `key` — which the\n * type checker cannot see. Under distinct names each knob settles into its own\n * cell, already narrowed to its own payload.\n *\n * The name is a value AND a type: the settle Msg types are generic in `N`, and\n * `N` is not inferable from `I` / `R`, so an opted-in knob spells all three\n * type arguments — `createResilientCall<In, Out, \"jev\">({ name: \"jev\", … })`.\n * The `name` field is typed at `N`, so the two cannot drift apart.\n */\n readonly name?: N;\n /**\n * The backoff brick. Omit and a failure is terminal (no backoff, no timer).\n *\n * Any bound the `../retry-backoff` union admits: a count (`RetryPolicy`), a\n * wall-clock outage budget (`DurationRetryPolicy`), or explicit\n * `unbounded: true`. A duration bound needs no extra wiring: every path that\n * records a failure already holds the instant it was observed as DATA —\n * `settle`'s `msg.at` (stamped at the interpret boundary) and `gate`'s `at`\n * (the caller's `attempt` / the retry timer's `atMs`) — so the streak clock\n * is fed from a Msg, never from a `Date.now()` inside a verb (invariant 2).\n */\n readonly retry?: AnyRetryPolicy;\n readonly circuit?: CircuitConfig;\n readonly rateLimit?: RateLimitConfig;\n readonly cache?: CacheConfig;\n readonly deadline?: DeadlineConfig;\n}\n\n// ===========================================================================\n// Slice — the Model field this knob owns. A flat record of brick states.\n// ===========================================================================\n\n/**\n * What is left of one call's deadline budget, and where the currently open\n * charging segment starts. Plain data, so it persists with the rest of the slice\n * — but the DURABLE fact is `remainingMs`, a duration, never an instant.\n *\n * The accounting has one rule, applied at every transition that carries a clock\n * (`attempt`'s `at`, a settle Msg's `at`, a timer's `atMs`): charge the segment\n * that just closed and open a new one at the same instant —\n * `remainingMs -= at - chargingSinceMs; chargingSinceMs = at`. See {@link charge}.\n *\n * `chargingSinceMs` is an IN-PROCESS anchor, not a persisted deadline. It is\n * meaningful only while the process that wrote it is alive, and `resume` re-bases\n * it on every live call before anything reads it after a reload — which is what\n * keeps downtime out of the budget. Nothing derives a timeout from it across a\n * resume; the only durable answer to \"how much budget is left\" is `remainingMs`.\n *\n * `remainingMs` is `0` when the `deadline` brick is absent, which is also the\n * \"no cap armed\" reading `subs` uses — a call with a real budget always has\n * `remainingMs > 0`, because `gate` settles a call whose budget reached zero\n * rather than letting it enter `running`.\n */\nexport interface CallBudget {\n /** In-process ms left when the open segment started. `0` = no deadline brick. */\n readonly remainingMs: number;\n /** The instant the open segment started, on the clock of the process that opened it. */\n readonly chargingSinceMs: number;\n}\n\n/**\n * Per-key call phase. `key` indexes a logical call; several can be in flight at\n * once (the generalization of the example's single `url`). Discriminated on\n * `phase` so each phase carries only its own data (pattern 11):\n *\n * - `idle` — never attempted, or fully settled and forgotten.\n * - `running` — the effect Cmd is out; awaiting `settle`.\n * Carries the deadline budget ({@link CallBudget}) and the\n * last `input` so a retry can re-issue it.\n * - `waiting_retry` — a transient failure backed off; the retry timer is armed\n * for `retryAtMs`. Carries `input` for the re-attempt, and\n * the same budget — backoff is in-process time, so it is\n * charged like an attempt.\n * - `circuit_open` — the breaker fast-failed this attempt. Terminal for this\n * call until the consumer re-attempts after cooldown.\n * - `succeeded` — settled OK. `result` is the value the port produced.\n * - `failed` — settled Err (out of retries, or no retry brick). `error`\n * carries the last failure.\n */\nexport type CallPhase<I, R> =\n | { readonly phase: \"idle\" }\n | {\n readonly phase: \"running\";\n readonly input: I;\n readonly budget: CallBudget;\n }\n | {\n readonly phase: \"waiting_retry\";\n readonly input: I;\n readonly retryAtMs: number;\n readonly budget: CallBudget;\n }\n | { readonly phase: \"circuit_open\" }\n | { readonly phase: \"succeeded\"; readonly result: R }\n | { readonly phase: \"failed\"; readonly error: unknown };\n\n/**\n * The slice. `circuit` / `bucket` are SHARED across keys (one downstream target\n * → one breaker, one bucket). `retry` and `cache` are PER KEY (each logical call\n * has its own backoff count + cached result), as is the per-call `CallPhase`.\n * Every field is plain data → the whole slice serializes into a `Store<S>`.\n *\n * `retry` / `cache` keep a default brick value even when the corresponding\n * config brick is omitted, so the slice shape is identical regardless of which\n * bricks the consumer enabled — the verbs simply never consult the unused brick.\n */\nexport interface ResilientState<I, R> {\n readonly circuit: CircuitState;\n readonly bucket: TokenBucket;\n /**\n * Per-key backoff counter. Entries minted by `backoff` are `TimedRetryState`s\n * (they carry the streak's `firstFailureAtMs`, since every failure path holds\n * the observation instant); the field is typed at the `RetryState` supertype\n * because a key that has never failed has no streak, and a slice persisted\n * before the origin existed rehydrates without one. Still plain data.\n */\n readonly retry: Readonly<Record<string, RetryState>>;\n readonly cache: TtlCache<R>;\n readonly calls: Readonly<Record<string, CallPhase<I, R>>>;\n /**\n * The instant of the latest clocked transition this slice saw — the `at` of\n * the last `attempt` / `resume` / `settleFailed`, settled Msg or timer fire.\n * `timer` counts the soonest deadline down from it, so the built-in `timer`\n * Sub gets a relative `ms` without anything reading a clock. `0` until the\n * first clocked transition.\n */\n readonly clockMs: number;\n}\n\n// ===========================================================================\n// Cmds + Msgs the knob speaks. Generic over the port input `I`.\n// ===========================================================================\n\n/**\n * The failures a run handler may return (ADR 0021). `port_rejected` is the\n * work failing — carry the cause beside the tag (`err({ _tag: \"port_rejected\",\n * cause })`); `deadline_exceeded` is a handler that timed the work out itself.\n * Either one settles as `<name>_run_err` and feeds the retry / breaker. Any\n * other failure is a contract breach and goes to the error sink.\n */\nexport type RunErr = TaggedError<\"port_rejected\" | \"deadline_exceeded\">;\n\n/**\n * The one effect this knob emits: \"run the work for `key` with `input`\". It is\n * `Cmd.define`d, so the handler you write for it returns an outcome and the\n * engine mints `<name>_run_ok` / `<name>_run_err` (ADR 0021). It carries no\n * closure — `input` is plain data (invariant 3).\n */\nexport function runCmdDef<I, R, N extends string = DefaultResilientName>(\n name: N = DEFAULT_RESILIENT_NAME as N,\n) {\n return Cmd.define(`${name}_run` as ResilientRunType<N>, {\n input: unchecked<{ readonly key: string; readonly input: I }>(),\n ok: unchecked<R>(),\n err: [\"port_rejected\", \"deadline_exceeded\"],\n });\n}\n/**\n * The type of the `Cmd.define` definition that `runCmdDef` returns for a\n * resilient call's run Cmd.\n */\nexport type RunCmdDef<\n I,\n R,\n N extends string = DefaultResilientName,\n> = ReturnType<typeof runCmdDef<I, R, N>>;\n/**\n * The run Cmd value. `R` is its settle value — `unknown` reads any knob's run\n * Cmd; the knob's own verbs emit it at their `R`, which is what lets a machine\n * list `cmds: [rc.run]` and take the verbs' Cmds as its own.\n */\nexport type RunCmd<\n I,\n N extends string = DefaultResilientName,\n R = unknown,\n> = CmdOf<RunCmdDef<I, R, N>>;\n\n/**\n * The settle Msgs the engine mints from a run handler's outcome:\n * `<name>_run_ok` carrying the handler's `value`, `<name>_run_err` carrying its\n * declared `error`. Each carries the run Cmd it settles (`cmd.key` names the\n * call) and the `at` the engine stamped. Both are generic in the knob's name\n * family `N`, so two knobs named apart land in two `update` cells whose\n * payloads are already narrowed.\n *\n * They are the `SettledOk` / `SettledErr` pair `Cmd.define` mints, spelled out\n * so a helper that folds the error into a richer type of its own (llm-call's\n * `LlmErr`, jev-ask's `JevAskErr`) can hand `settle` its Msg with `E` widened.\n */\nexport type SucceedMsg<\n R,\n N extends string = DefaultResilientName,\n I = unknown,\n> = {\n readonly type: ResilientOkType<N>;\n readonly cmd: RunCmd<I, N>;\n readonly value: R;\n readonly at: number;\n};\n/**\n * The Msg the engine mints when a resilient call's handler fails:\n * `<name>_run_err`, carrying the Cmd and the error.\n */\nexport type FailMsg<\n N extends string = DefaultResilientName,\n I = unknown,\n E = RunErr | MalformedResult,\n> = {\n readonly type: ResilientErrType<N>;\n readonly cmd: RunCmd<I, N>;\n readonly error: E;\n readonly at: number;\n};\n\n/**\n * What `settle` reads off a settled Msg: the call's `key` (on `cmd`), the\n * engine's `at`, and the handler's `value` or `error`. {@link SucceedMsg} and\n * {@link FailMsg} are both one of these, so an engine-minted Msg passes\n * straight in; a helper that settles from its own data (a fallback answer, a\n * compaction summary) builds the record without inventing a whole run Cmd.\n */\nexport type SettleMsg<R, N extends string = DefaultResilientName> =\n | {\n readonly type?: ResilientOkType<N>;\n readonly cmd: { readonly key: string };\n readonly value: R;\n readonly at: number;\n }\n | {\n readonly type?: ResilientErrType<N>;\n readonly cmd: { readonly key: string };\n readonly error: unknown;\n readonly at: number;\n };\n\n/**\n * How a settled call ended — the third thing `settle` hands back, and the only\n * place the port's value can be read from.\n *\n * - `done` — the port answered; `value` is its result.\n * - `failed` — no retry is left (or no retry brick); `error` is the last\n * failure.\n * - `retrying` — backed off; the retry timer is armed and the call is still\n * live.\n */\nexport type SettleOutcome<R> =\n | { readonly kind: \"done\"; readonly value: R }\n | { readonly kind: \"failed\"; readonly error: unknown }\n | { readonly kind: \"retrying\" };\n\n/**\n * What `settle` returns: the settled slice, the Cmds it emitted, and the\n * {@link SettleOutcome}. One record, so the slice update and the result read\n * cannot be split apart in a hand-wired `update` cell.\n */\nexport interface SettleResult<I, R, N extends string = DefaultResilientName> {\n readonly call: ResilientState<I, R>;\n readonly cmds: readonly RunCmd<I, N, R>[];\n readonly outcome: SettleOutcome<R>;\n}\n\n/**\n * The deadline Msg tag this knob's timers dispatch, derived from its name the\n * same way the settle Msgs are. The DEFAULT family keeps the bare\n * `\"deadline_exceeded\"` literal.\n */\nexport type ResilientDeadlineType<N extends string = DefaultResilientName> =\n N extends DefaultResilientName ? \"deadline_exceeded\" : `${N}_deadline`;\n\n/**\n * The name a deadline carries for the `N` family — `undefined` for the\n * default family (no name at all, so the bare literal is dispatched), the name\n * itself otherwise. One conditional, so the value and {@link\n * ResilientDeadlineType} can never disagree about which family is unnamed.\n */\nexport type DeadlineNameOf<N extends string = DefaultResilientName> =\n N extends DefaultResilientName ? undefined : N;\n\n/**\n * The retry / deadline timer Msg — a `DeadlineExceeded` whose tag is this\n * knob's ({@link ResilientDeadlineType}), keyed by the call `key` through the\n * Sub `id`. Bare `ResilientTimerMsg` is the default family's, so it is the same\n * `DeadlineExceeded` it has always been.\n */\nexport type ResilientTimerMsg<N extends string = DefaultResilientName> =\n DeadlineExceeded<DeadlineNameOf<N>>;\n\n/**\n * The plain-data error a deadline-failed call settles with. A `{_tag, ...}`\n * sentinel — NEVER a `new Error(...)` — so the slice stays JSON-serializable\n * (Error objects round-trip to `{}` and carry a stack trace, both of which break\n * the slice's durability invariant). `id` / `atMs` echo the timer that fired so\n * a consumer can attribute the failure to its deadline.\n */\nexport type DeadlineExceededError = {\n readonly _tag: \"deadline_exceeded\";\n readonly id: string;\n readonly atMs: number;\n};\n\n// ===========================================================================\n// Internal brick-policy derivation — config → the L1 policy shapes.\n// ===========================================================================\n\n// A circuit policy is only built when the brick is configured; absence is\n// represented as `null` so the gate can short-circuit to \"always pass\".\nfunction circuitPolicy(config: ResilientConfig<string>) {\n if (config.circuit === undefined) return null;\n return {\n failureThreshold: config.circuit.threshold,\n cooldownMs: config.circuit.cooldownMs,\n halfOpenMaxProbes: config.circuit.halfOpenMaxProbes ?? 1,\n };\n}\n\n/**\n * Close the open charging segment at `at` and open the next one there: the ONE\n * place a {@link CallBudget} is debited. Every caller holds `at` as data (a\n * verb's argument, a Msg's stamp), so the budget advances without any verb\n * reading a clock. PURE.\n *\n * A budget with no deadline brick (`remainingMs` 0) is left alone — there is\n * nothing to spend, and debiting it would push it negative and read as \"spent\".\n * `at` before the anchor cannot happen on a monotonic feed, but a clamped charge\n * keeps a skewed one from CREDITING budget back.\n */\nfunction charge(budget: CallBudget, at: number): CallBudget {\n if (budget.remainingMs <= 0) return { ...budget, chargingSinceMs: at };\n const elapsed = Math.max(0, at - budget.chargingSinceMs);\n return { remainingMs: budget.remainingMs - elapsed, chargingSinceMs: at };\n}\n\n// Deadline-id families. Each call's retry timer is a deadline keyed by `key`, so\n// a machine running many concurrent calls arms and routes each independently.\n// The knob's `name` leads the id for the same reason it leads the Msg types: a\n// fired timer is routed by its id, so two knobs sharing a key would otherwise\n// answer each other's timer.\n// The default `resilient` name reproduces the ids exactly as they have always\n// read.\nfunction retryTimerId(name: string, key: string): string {\n return `${name}:retry:${key}`;\n}\nfunction deadlineTimerId(name: string, key: string): string {\n return `${name}:deadline:${key}`;\n}\n\n/**\n * The deadline Sub's `name` for a knob's family — the value side of\n * {@link DeadlineNameOf}. The default family maps to `undefined` (no name),\n * which is what keeps an unnamed knob dispatching the bare `deadline_exceeded`\n * literal rather than a `resilient_deadline` nobody is wired for.\n */\nfunction deadlineNameOf<N extends string>(name: N): DeadlineNameOf<N> {\n return (\n name === DEFAULT_RESILIENT_NAME ? undefined : name\n ) as DeadlineNameOf<N>;\n}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a resilient-call knob from `config`. `rng` is injected for retry\n * jitter — pass a fixed `() => 0.5` in tests to pin backoff; defaults to\n * `Math.random` (read at the verb boundary, never inside a brick op). It is\n * curried onto the factory rather than threaded through every verb so callers\n * spell the determinism choice once.\n *\n * Returns plain functions and the run Cmd def (ADR 0022) — see the module doc\n * for the wiring. `I` is the work's input type, `R` its result type.\n */\nexport function createResilientCall<\n I,\n R,\n N extends string = DefaultResilientName,\n>(config: ResilientConfig<N>, rng: () => number = Math.random) {\n const name = config.name ?? (DEFAULT_RESILIENT_NAME as N);\n const cPolicy = circuitPolicy(config);\n // Re-affirm the `[0, 1)` contract at the seam where the injected generator\n // feeds the backoff math (brand from `../retry-backoff`). The factory's\n // public param stays a plain `() => number` so transitive callers are\n // unchanged; `asRng` is the single point it's branded for `nextDelayMs`.\n const rngBranded = asRng(rng);\n const runCmd = runCmdDef<I, R, N>(name);\n // The name each deadline carries, which is what fixes the Msg tag it\n // dispatches. `undefined` for the DEFAULT family: an unnamed knob must keep\n // dispatching the bare `deadline_exceeded` literal every existing machine is\n // wired against, so the default is not merely spelled the same — it is the\n // absence of a name, exactly as before this parameter existed.\n const deadlineName = deadlineNameOf(name);\n\n /** The starting slice. Bricks not in `config` still get a default value. */\n function init(): ResilientState<I, R> {\n return {\n circuit: initCircuit(),\n // The bucket carries a default value even without the rateLimit brick so\n // the slice shape is uniform, but the rate-limit GATE is skipped entirely\n // when the brick is absent (see `gate`) — the bucket is simply never\n // consulted. A `0/0` placeholder bucket would be touched by `tryConsume`;\n // a `1-cap` default is harmless because the gate never calls it.\n bucket: config.rateLimit\n ? initBucket(\n config.rateLimit.capacity,\n config.rateLimit.refillPerSec,\n 0,\n )\n : initBucket(1, 1, 0),\n retry: {},\n cache: initCache<R>(),\n calls: {},\n clockMs: 0,\n };\n }\n\n /** Record `at` as the latest instant this slice saw (see `clockMs`). */\n function tick(s: ResilientState<I, R>, at: number): ResilientState<I, R> {\n return s.clockMs === at ? s : { ...s, clockMs: at };\n }\n\n /** Read this key's retry state, defaulting to a fresh one. */\n function retryOf(s: ResilientState<I, R>, key: string): RetryState {\n return s.retry[key] ?? initRetry();\n }\n\n /** A fresh full budget for a call starting at `at` (`remainingMs` 0 = no cap). */\n function freshBudget(at: number): CallBudget {\n return {\n remainingMs: config.deadline ? config.deadline.ms : 0,\n chargingSinceMs: at,\n };\n }\n\n /**\n * The budget a call already live under `key` carries forward, charged up to\n * `at`; a fresh full budget when the key has no live call. This is what makes a\n * re-attempt share ONE budget with the attempts before it rather than buying a\n * new one — the in-process invariant the deadline brick has always had.\n */\n function budgetFor(\n s: ResilientState<I, R>,\n key: string,\n at: number,\n ): CallBudget {\n const call = s.calls[key];\n if (call?.phase === \"running\" || call?.phase === \"waiting_retry\") {\n return charge(call.budget, at);\n }\n return freshBudget(at);\n }\n\n /**\n * The shared decision: budget → cache → rate-limit → circuit gate, then either\n * emit the effect or schedule a retry. `budget` is threaded so a re-attempt\n * (from `onTimer`) keeps the ORIGINAL overall budget rather than restarting it.\n * PURE — `at` is the only clock; `rng` is the injected jitter source.\n *\n * ## Why the budget is checked BEFORE the effect is emitted\n *\n * The check is step 0 and it settles rather than dispatches. A call whose\n * budget is spent must not reach the port at all: the port is a real side\n * effect, and re-issuing an attempt only to settle `deadline_exceeded` when it\n * returns spends that effect for an answer nobody reads. This is exactly the\n * resume path's shape — a process that comes back to a spent budget settles\n * here, with zero further port calls (#144).\n *\n * Used by both `attempt` (fresh call) and `onTimer` (retry timer fired) so\n * the gate logic lives in exactly one place — the example's shared `attempt`\n * helper, lifted to a keyed knob.\n *\n * ## Why rate-limit is gated BEFORE the circuit probe\n *\n * The circuit-breaker's `canPass` is not a passive read: in `half_open` it\n * *consumes* a probe slot (advancing `probes`), and the consumed slot is only\n * released when a probe RESOLVES via `onSuccess` / `onFailure`. So a probe\n * slot may only be taken when this attempt is actually going to reach the\n * backend (i.e. emit the `resilient_run` Cmd). The rate-limit gate can reject\n * an attempt and return a backoff WITHOUT any Cmd — no backend call, so no\n * settle Msg ever comes back. If the circuit probe were consumed first, that\n * rate-limited attempt would burn the half-open probe budget on a call that\n * never happened, wedging the breaker `half_open` at its probe cap forever\n * (nothing moves it out of `half_open` but a resolved probe). Running the\n * rate-limit gate first means the circuit probe is consumed ONLY on the path\n * that emits the effect.\n */\n function gate(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n budget: CallBudget,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n // 0) Budget gate. Skipped without the deadline brick (`remainingMs` stays 0\n // and never moves). A spent budget settles here — before the cache read, the\n // token spend and the circuit probe, none of which a dead call should touch.\n if (config.deadline !== undefined && budget.remainingMs <= 0) {\n return [\n setCall(s, key, {\n phase: \"failed\",\n error: {\n _tag: \"deadline_exceeded\",\n id: deadlineTimerId(name, key),\n atMs: at,\n } satisfies DeadlineExceededError,\n }),\n [],\n ];\n }\n\n // 1) Serve from cache if fresh — no effect at all. Skipped when no cache brick.\n if (config.cache !== undefined) {\n const cached = cacheGet(s.cache, key, at);\n if (cached !== undefined) {\n return [setCall(s, key, { phase: \"succeeded\", result: cached }), []];\n }\n }\n\n // 2) Rate-limit gate FIRST — it may reject with no Cmd, so it must run\n // before any circuit probe is consumed (see header note). Skipped entirely\n // when no rateLimit brick — the bucket is never consulted, so the default\n // placeholder bucket stays untouched.\n let bucket = s.bucket;\n if (config.rateLimit !== undefined) {\n const [next, hasToken] = tryConsume(bucket, at);\n bucket = next;\n if (!hasToken) {\n // Rate-limited: treat as a transient failure and back off via retry.\n // The circuit is untouched — no probe is spent on a call that never ran.\n return backoff(\n { ...s, bucket },\n key,\n input,\n \"rate_limited\",\n at,\n budget,\n );\n }\n }\n\n // 3) Circuit-breaker gate. `null` policy → no breaker → always pass. Reached\n // only once a token is secured, so a half-open probe is consumed exclusively\n // on the path that emits the effect below.\n let circuit = s.circuit;\n if (cPolicy !== null) {\n const [next, ok] = canPass(circuit, cPolicy, at);\n circuit = next;\n if (!ok) {\n return [\n { ...setCall(s, key, { phase: \"circuit_open\" }), bucket, circuit },\n [],\n ];\n }\n }\n\n // 4) All gates passed — emit the effect as data.\n return [\n {\n ...setCall(s, key, { phase: \"running\", input, budget }),\n circuit,\n bucket,\n },\n [runCmd({ key, input })],\n ];\n }\n\n /**\n * Back off after a transient failure: record it, and if `retry` permits\n * another attempt, enter `waiting_retry` with the retry timer armed; else\n * settle `failed`. When no `retry` brick is configured the call fails on the\n * first failure (no backoff, no timer). PURE.\n *\n * `at` is both the backoff anchor AND the streak clock: it is passed to\n * `recordFailure` (which starts `firstFailureAtMs` on the streak's first\n * failure and preserves it on every later one) and to `shouldRetry`, so a\n * `DurationRetryPolicy` on `config.retry` is honoured with no extra wiring.\n * A success drops `retry[key]` entirely, which drops the origin with it —\n * only an unbroken run of failures grows toward a duration budget.\n */\n function backoff(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n error: unknown,\n at: number,\n budget: CallBudget,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n if (config.retry === undefined) {\n return [setCall(s, key, { phase: \"failed\", error }), []];\n }\n const retry = recordFailure(retryOf(s, key), error, at);\n const withRetry = { ...s, retry: { ...s.retry, [key]: retry } };\n if (!shouldRetry(retry, config.retry, at)) {\n return [setCall(withRetry, key, { phase: \"failed\", error }), []];\n }\n const retryAtMs = at + nextDelayMs(retry, config.retry, rngBranded);\n return [\n setCall(withRetry, key, {\n phase: \"waiting_retry\",\n input,\n retryAtMs,\n budget,\n }),\n [],\n ];\n }\n\n // Set the phase for one key, leaving every other field untouched.\n function setCall(\n s: ResilientState<I, R>,\n key: string,\n phase: CallPhase<I, R>,\n ): ResilientState<I, R> {\n return { ...s, calls: { ...s.calls, [key]: phase } };\n }\n\n // === Verb: attempt =======================================================\n\n /**\n * Start (or restart) the call for `key` with `input` at time `at`. Runs the\n * cache → circuit → rate-limit gate and either emits the `resilient_run`\n * effect, schedules a retry, fast-fails (circuit open), or serves the cache.\n * PURE.\n *\n * A key already live keeps the budget it is under (charged up to `at`) rather\n * than being handed a fresh one, so re-issuing an in-flight call — the boot\n * path's move — cannot silently extend its deadline. Only a key with no live\n * call starts a full budget.\n */\n function attempt(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n return gate(tick(s, at), key, input, at, budgetFor(s, key, at));\n }\n\n // === Verb: resume ========================================================\n\n /**\n * Re-anchor every live call's charging clock to `at`: the verb a host calls\n * once on the boot path, before any Sub is armed or any attempt re-issued.\n *\n * This is what keeps DOWNTIME out of the budget. `chargingSinceMs` is an\n * instant on the clock of the process that wrote it; after an eviction, a crash\n * or a redeploy, the gap between it and now is time in which no attempt ran and\n * no port was called, so charging it would time a call out on the host's\n * absence rather than on the tool's slowness. `remainingMs` — the durable half\n * of the budget — is left exactly as the last live transition set it. PURE.\n */\n function resume(s: ResilientState<I, R>, at: number): ResilientState<I, R> {\n const calls: Record<string, CallPhase<I, R>> = { ...s.calls };\n for (const [key, call] of Object.entries(calls)) {\n if (call.phase !== \"running\" && call.phase !== \"waiting_retry\") continue;\n calls[key] = {\n ...call,\n budget: { ...call.budget, chargingSinceMs: at },\n };\n }\n return { ...s, calls, clockMs: at };\n }\n\n // === The two record steps `settle` is built from ==========================\n\n /**\n * Record a success for `msg.key`: close the breaker, fill the cache (if a\n * cache brick exists), reset this key's retry count, and settle `succeeded`.\n * PURE — `msg.at` is the cache write clock. Private: {@link settle} is the\n * verb.\n */\n function recordOk(\n s: ResilientState<I, R>,\n msg: Extract<SettleMsg<R, N>, { readonly value: R }>,\n ): ResilientState<I, R> {\n const { key } = msg.cmd;\n const circuit =\n cPolicy !== null ? onSuccess(s.circuit, cPolicy) : s.circuit;\n const cache =\n config.cache !== undefined\n ? cacheSet(s.cache, key, msg.value, msg.at, config.cache.ttlMs)\n : s.cache;\n // Drop this key's retry counter — a success ends the retry run.\n const retry = without(s.retry, key);\n return {\n ...setCall(s, key, { phase: \"succeeded\", result: msg.value }),\n circuit,\n cache,\n retry,\n };\n }\n\n /**\n * Record a failure for `msg.key`: trip the breaker (it may open), then back\n * off — schedule a retry if `retry` permits, else settle `failed`. PURE —\n * `msg.at` stamps the breaker trip + the retry delay base. Re-issues from the\n * call's remembered `input` and carries the original budget forward, charged\n * up to `msg.at` — the attempt that just failed spent in-process time, and a\n * retry ladder shares ONE budget across its attempts rather than getting a\n * fresh one per attempt. Private: {@link settle} is the verb.\n */\n function recordErr(\n s: ResilientState<I, R>,\n msg: Extract<SettleMsg<R, N>, { readonly error: unknown }>,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n const { key } = msg.cmd;\n const circuit =\n cPolicy !== null ? onFailure(s.circuit, cPolicy, msg.at) : s.circuit;\n const withCircuit = { ...s, circuit };\n const call = s.calls[key];\n const input = call?.phase === \"running\" ? call.input : undefined;\n const budget = budgetFor(s, key, msg.at);\n // No remembered input (e.g. a stray fail for a key not running) → just\n // settle failed after tripping the breaker; nothing to re-issue.\n if (input === undefined) {\n return [\n setCall(withCircuit, key, { phase: \"failed\", error: msg.error }),\n [],\n ];\n }\n return backoff(withCircuit, key, input, msg.error, msg.at, budget);\n }\n\n function isOk(\n msg: SettleMsg<R, N>,\n ): msg is Extract<SettleMsg<R, N>, { readonly value: R }> {\n return \"value\" in msg;\n }\n\n // === Verb: settle ========================================================\n\n /**\n * Settle the call a `<name>_run_ok` / `<name>_run_err` Msg answers, keyed off\n * `msg.cmd.key`, and say how it ended. PURE.\n *\n * A success closes the breaker, fills the cache and ends the retry run; the\n * outcome is `done` with the handler's value. A failure trips the breaker and\n * backs off: `retrying` while the retry policy allows another attempt, and\n * `failed` with the error once it does not. A `retrying` call waits on the\n * retry timer `timer` arms; its fire re-issues the run Cmd through `onTimer`.\n *\n * The settled slice, its Cmds and the outcome come back together, and the\n * result value is reachable ONLY through `outcome`. So a hand-wired settle\n * cell cannot fold the answer into its Model before the slice has settled —\n * the order that used to leave a call stuck at `running` cannot be written.\n */\n function settle(\n s: ResilientState<I, R>,\n msg: SettleMsg<R, N>,\n ): SettleResult<I, R, N> {\n const now = tick(s, msg.at);\n if (isOk(msg)) {\n return {\n call: recordOk(now, msg),\n cmds: [],\n outcome: { kind: \"done\", value: msg.value },\n };\n }\n const [call, cmds] = recordErr(now, msg);\n return {\n call,\n cmds,\n outcome:\n call.calls[msg.cmd.key]?.phase === \"waiting_retry\"\n ? { kind: \"retrying\" }\n : { kind: \"failed\", error: msg.error },\n };\n }\n\n // === Verb: settleFailed ==================================================\n\n /**\n * Settle `key` to terminal `failed` with `error` WITHOUT touching the circuit\n * breaker or this key's retry counter. PURE.\n *\n * This is the verb for a failure that is NOT a downstream-health signal: the\n * call must end now, but the breaker must not trip and the backoff run must\n * not advance. `settle` on an `_err` Msg is the opposite — it records the\n * failure against the breaker (it may open) and feeds the retry policy (it\n * may back off). Use\n * `settleFailed` when the *reason* the call cannot proceed has nothing to do\n * with downstream health:\n *\n * - `authed-call` settles a 401 it cannot fix (e.g. the refresh budget is\n * spent): a bad token is the caller's problem, not the backend's —\n * tripping the breaker on it would punish a healthy target, and backing\n * off would just retry a request that will 401 again.\n * - `paginated-walk` settles a terminal walk error (e.g. a malformed page\n * the consumer rejects) that should stop the walk without poisoning the\n * shared breaker the next page fetch passes through.\n *\n * Because it leaves `circuit` and `retry[key]` untouched, the breaker keeps\n * its health view of the actual backend and a later `attempt` for this key\n * starts a fresh retry run. Both sibling knobs previously hand-rolled this by\n * splicing `calls` directly; this verb gives them one named, tested entry\n * point so the slice's settle invariants live in exactly one place.\n *\n * `at` is the instant the caller decided it — recorded as the slice's clock,\n * since ending a call can change which deadline `timer` counts down to.\n */\n function settleFailed(\n s: ResilientState<I, R>,\n key: string,\n error: unknown,\n at: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n return [setCall(tick(s, at), key, { phase: \"failed\", error }), []];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A timer fired for some key. Two kinds, disambiguated by the Sub id:\n *\n * - retry timer → re-run the gate for that key's remembered input,\n * re-checking the circuit + bucket at THIS time.\n * - deadline timer → the overall budget elapsed → settle `failed` with a\n * deadline error, no matter the current phase.\n *\n * A timer for a key no longer `waiting_retry` (retry) / no longer in flight\n * (deadline) leaves the calls alone — the reconcile pass races the result;\n * the verb must tolerate a stale fire. Every fire records `msg.atMs` as the\n * slice's clock, so `timer` re-arms from it. PURE.\n */\n function onTimer(\n slice: ResilientState<I, R>,\n msg: { readonly id: string; readonly atMs: number },\n ): readonly [ResilientState<I, R>, readonly RunCmd<I, N, R>[]] {\n const s = tick(slice, msg.atMs);\n for (const [key, call] of Object.entries(s.calls)) {\n if (msg.id === retryTimerId(name, key)) {\n if (call.phase !== \"waiting_retry\") return [s, []];\n // The backoff wait was in-process time, so it is charged before the gate\n // reads the budget — and the gate settles rather than dispatching when\n // that leaves nothing, which is how a resumed ladder ends without one\n // more port call.\n return gate(\n s,\n key,\n call.input,\n msg.atMs,\n charge(call.budget, msg.atMs),\n );\n }\n if (msg.id === deadlineTimerId(name, key)) {\n // Deadline only fires for a still-active call. A settled call has no\n // armed deadline Sub, but tolerate a stale fire defensively.\n if (call.phase !== \"running\" && call.phase !== \"waiting_retry\")\n return [s, []];\n // Plain-data sentinel, NOT a `new Error(...)`: an Error object is not\n // JSON-serializable (it round-trips to `{}`) and captures a stack trace,\n // both of which break the slice's durability invariant once it is\n // persisted into a `Store<S>` and reloaded. A `{_tag, ...}` record keeps\n // the slice round-trip-equal under JSON.parse(JSON.stringify(...)).\n return [\n setCall(s, key, {\n phase: \"failed\",\n error: {\n _tag: \"deadline_exceeded\",\n id: msg.id,\n atMs: msg.atMs,\n } satisfies DeadlineExceededError,\n }),\n [],\n ];\n }\n }\n return [s, []];\n }\n\n // === Timers ==============================================================\n\n /**\n * The deadlines this slice is waiting on: a retry timer for every\n * `waiting_retry` call, and (when the `deadline` brick is configured) an\n * overall deadline timer for every still-active call. Each is an absolute\n * instant; `timer` arms the soonest. PURE.\n */\n function deadlines(\n s: ResilientState<I, R>,\n ): readonly DeadlineSub<DeadlineNameOf<N>>[] {\n const out: DeadlineSub<DeadlineNameOf<N>>[] = [];\n for (const [key, call] of Object.entries(s.calls)) {\n if (call.phase === \"waiting_retry\") {\n out.push(\n deadlineSub(retryTimerId(name, key), call.retryAtMs, {\n name: deadlineName,\n }),\n );\n }\n if (\n config.deadline !== undefined &&\n (call.phase === \"running\" || call.phase === \"waiting_retry\") &&\n call.budget.remainingMs > 0\n ) {\n // The absolute instant the timer arms at is DERIVED here, from the open\n // segment's anchor plus what is left — it is never persisted, so a slice\n // that came back from storage arms off the anchor `resume` just re-based\n // rather than off a stamp the downtime ran past.\n out.push(\n deadlineSub(\n deadlineTimerId(name, key),\n call.budget.chargingSinceMs + call.budget.remainingMs,\n { name: deadlineName },\n ),\n );\n }\n }\n return out;\n }\n\n /**\n * The built-in `timer` Sub's deps for this slice: the soonest of\n * {@link deadlines}, counted down from the slice's `clockMs`, or `null` when\n * nothing is waiting. Declare it in the machine and `run` needs no runner:\n *\n * subs: [{ type: \"timer\", deps: (s) => rc.timer(s.call) }]\n *\n * Its fire is this knob's timer Msg (`deadline_exceeded`, or\n * `<name>_deadline` for a named knob); route it to `onTimer`. A phase change\n * drops the matching deadline, and the new deps restart the countdown — no\n * manual `clearTimeout`. PURE.\n */\n function timer(\n s: ResilientState<I, R>,\n ): TimerDeps<ResilientTimerMsg<N>> | null {\n return nextTimer(deadlines(s), s.clockMs);\n }\n\n return {\n // The Msg-name family this knob speaks, as a VALUE typed at `N`: the Cmd,\n // the settle Msgs and the timer ids all derive from it.\n name,\n /** The `Cmd.define`d run Cmd; list it in the machine's `cmds`. */\n run: runCmd,\n init,\n attempt,\n resume,\n settle,\n settleFailed,\n onTimer,\n deadlines,\n timer,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the\n * slice lives at `state.resilience`. A convenience for the common single-slice\n * host; consumers with a differently-named field spread by hand. Pure — a thin\n * record rebuild, no clock / RNG.\n *\n * The record rebuild itself is `liftSlice` from the root entry (#231) — this\n * stays as the named, pre-keyed convenience for the `state.resilience` field,\n * unchanged in name, signature and subpath.\n */\nexport function liftResilience<\n S extends { resilience: ResilientState<I, R> },\n I,\n R,\n C extends Cmd,\n>(\n state: S,\n result: readonly [ResilientState<I, R>, readonly C[]],\n): readonly [S, readonly C[]] {\n return liftSlice(\"resilience\", state, result);\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { fromInterval } from './chunk-
|
|
2
|
-
import { subId } from './chunk-
|
|
1
|
+
import { fromInterval } from './chunk-VHKDUBXC.js';
|
|
2
|
+
import { subId } from './chunk-GAVLND5C.js';
|
|
3
3
|
|
|
4
4
|
// src/internal/resilience/deadline/index.ts
|
|
5
5
|
function deadlines(list) {
|
|
@@ -100,5 +100,5 @@ function cacheEvictionSub(name, everyMs) {
|
|
|
100
100
|
var cacheEvictionSubscribe = fromInterval((sub) => cacheEvictMsg(sub.deps.name));
|
|
101
101
|
|
|
102
102
|
export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, deadlineExceeded, deadlineMsgType, deadlineSub, deadlines, deadlinesSub, evictExpired, get, has, initCache, nextTimer, remove, set, setTimeoutArmTimer, subscribeDeadline, subscribeWith };
|
|
103
|
-
//# sourceMappingURL=chunk-
|
|
104
|
-
//# sourceMappingURL=chunk-
|
|
103
|
+
//# sourceMappingURL=chunk-QIE4Y6BK.js.map
|
|
104
|
+
//# sourceMappingURL=chunk-QIE4Y6BK.js.map
|