@demlik/tea 0.5.0 → 0.5.1
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/LICENSE +1 -1
- package/dist/agent/index.d.ts +5 -5
- package/dist/agent/index.js +10 -10
- package/dist/authed-call/index.d.ts +3 -3
- package/dist/authed-call/index.js +7 -7
- package/dist/await-terminal/index.d.ts +2 -2
- package/dist/await-terminal/index.js +3 -2
- package/dist/await-terminal/index.js.map +1 -1
- package/dist/batch-window/index.d.ts +4 -4
- package/dist/batch-window/index.js +3 -3
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +4 -4
- package/dist/{chunk-PUTHY7LR.js → chunk-2KBEIGYP.js} +3 -3
- package/dist/{chunk-PUTHY7LR.js.map → chunk-2KBEIGYP.js.map} +1 -1
- package/dist/{chunk-QKUWDC4P.js → chunk-2UGTDYOK.js} +3 -3
- package/dist/{chunk-QKUWDC4P.js.map → chunk-2UGTDYOK.js.map} +1 -1
- package/dist/{chunk-ZILCH3WS.js → chunk-66S2KEQQ.js} +4 -4
- package/dist/{chunk-ZILCH3WS.js.map → chunk-66S2KEQQ.js.map} +1 -1
- package/dist/{chunk-362SZ6CS.js → chunk-A7USKARI.js} +3 -3
- package/dist/{chunk-362SZ6CS.js.map → chunk-A7USKARI.js.map} +1 -1
- package/dist/{chunk-ZT3MGQB7.js → chunk-ASX7PL2K.js} +3 -3
- package/dist/{chunk-ZT3MGQB7.js.map → chunk-ASX7PL2K.js.map} +1 -1
- package/dist/{chunk-62QBHM3T.js → chunk-AZ3NHUPD.js} +3 -3
- package/dist/{chunk-62QBHM3T.js.map → chunk-AZ3NHUPD.js.map} +1 -1
- package/dist/{chunk-7FYQGDMF.js → chunk-AZZLE7XR.js} +3 -3
- package/dist/{chunk-7FYQGDMF.js.map → chunk-AZZLE7XR.js.map} +1 -1
- package/dist/{chunk-W2S4HK7J.js → chunk-EC6FO42Q.js} +6 -6
- package/dist/{chunk-W2S4HK7J.js.map → chunk-EC6FO42Q.js.map} +1 -1
- package/dist/{chunk-T6HDAPMR.js → chunk-ERYY5P6W.js} +3 -3
- package/dist/{chunk-T6HDAPMR.js.map → chunk-ERYY5P6W.js.map} +1 -1
- package/dist/{chunk-CIDS43FH.js → chunk-IVHQCVFX.js} +3 -3
- package/dist/{chunk-CIDS43FH.js.map → chunk-IVHQCVFX.js.map} +1 -1
- package/dist/{chunk-DBOJ2UC6.js → chunk-O7ED34JH.js} +4 -2
- package/dist/chunk-O7ED34JH.js.map +1 -0
- package/dist/{chunk-LFW66IIV.js → chunk-S6Y3RNX4.js} +4 -4
- package/dist/{chunk-LFW66IIV.js.map → chunk-S6Y3RNX4.js.map} +1 -1
- package/dist/{chunk-KEFMI24G.js → chunk-UV7VCTNC.js} +7 -7
- package/dist/{chunk-KEFMI24G.js.map → chunk-UV7VCTNC.js.map} +1 -1
- package/dist/{chunk-7UU5P2CX.js → chunk-WF46FABO.js} +3 -3
- package/dist/{chunk-7UU5P2CX.js.map → chunk-WF46FABO.js.map} +1 -1
- package/dist/{chunk-KWPB6BM6.js → chunk-XMHZ44QC.js} +4 -4
- package/dist/{chunk-KWPB6BM6.js.map → chunk-XMHZ44QC.js.map} +1 -1
- package/dist/{chunk-7A74NSG4.js → chunk-XOAP3LOT.js} +3 -3
- package/dist/{chunk-7A74NSG4.js.map → chunk-XOAP3LOT.js.map} +1 -1
- package/dist/{chunk-PBAYOJJO.js → chunk-ZH454CR5.js} +9 -3
- package/dist/chunk-ZH454CR5.js.map +1 -0
- package/dist/{core-DPYOyOJv.d.ts → core-JPHsMZVB.d.ts} +1 -1
- package/dist/deadline/index.d.ts +2 -2
- package/dist/deadline/index.js +3 -3
- package/dist/do/index.d.ts +9 -9
- package/dist/do/index.js +10 -10
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-B0YpoCHg.d.ts → durable-effects-MRnCBCxV.d.ts} +2 -2
- package/dist/extension/index.d.ts +2 -2
- package/dist/extension/index.js +3 -3
- package/dist/extension/react.js +3 -3
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/test-utils.d.ts +1 -1
- package/dist/extension/test-utils.js.map +1 -1
- package/dist/fan-out/index.d.ts +2 -2
- package/dist/idempotent-intake/index.d.ts +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/llm-call/index.d.ts +3 -3
- package/dist/llm-call/index.js +7 -7
- package/dist/{machine-Q_Bsm7Vu.d.ts → machine-nGXCMyHn.d.ts} +3 -3
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/mem/index.d.ts +2 -2
- package/dist/monitored-run/index.d.ts +4 -4
- package/dist/monitored-run/index.js +5 -5
- package/dist/node/index.d.ts +2 -2
- package/dist/paginated-walk/index.d.ts +3 -3
- package/dist/paginated-walk/index.js +7 -7
- package/dist/parity/index.d.ts +2 -2
- package/dist/parity/index.js +3 -3
- package/dist/pbt/arbitraries/index.d.ts +3 -3
- package/dist/pbt/arbitraries/index.js +1 -1
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +4 -4
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -3
- package/dist/poller/index.d.ts +3 -3
- package/dist/poller/index.js +3 -3
- package/dist/prediction/index.d.ts +1 -1
- package/dist/prediction/index.js +2 -2
- package/dist/pure/index.d.ts +1 -1
- package/dist/pure/index.js +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/reconciler/index.d.ts +3 -3
- package/dist/reconciler/index.js +7 -7
- package/dist/recorder/index.d.ts +2 -2
- package/dist/resilient-call/index.d.ts +2 -2
- package/dist/resilient-call/index.js +6 -6
- package/dist/retry-to-success/index.js +2 -0
- package/dist/retry-to-success/index.js.map +1 -1
- package/dist/{runtime-types-uBuVvZqa.d.ts → runtime-types-BbBq4LWo.d.ts} +2 -2
- package/dist/saga/index.d.ts +2 -2
- package/dist/snapshot/index.d.ts +1 -1
- package/dist/snapshot/index.js +3 -3
- package/dist/subs/index.d.ts +3 -3
- package/dist/subs/index.js +3 -3
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/throttled-input/index.d.ts +2 -2
- package/dist/throttled-input/index.js +6 -6
- package/dist/token-refresh/index.d.ts +1 -1
- package/dist/token-refresh/index.js +3 -3
- package/dist/trace-replay/index.d.ts +2 -2
- package/dist/trace-replay/index.js +3 -3
- package/dist/{types-C6mXL9wA.d.ts → types-BMZ7IMrd.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +1 -1
- package/dist/with-deadline/index.js +2 -2
- package/dist/with-resilience/index.d.ts +2 -2
- package/dist/with-resilience/index.js +6 -6
- package/dist/with-telemetry/index.d.ts +1 -1
- package/dist/with-telemetry/index.js +2 -2
- package/dist/work-queue/adapter.d.ts +2 -2
- package/dist/work-queue/index.d.ts +3 -3
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +2 -2
- package/dist/workflow/index.d.ts +3 -3
- package/package.json +12 -4
- package/dist/chunk-DBOJ2UC6.js.map +0 -1
- package/dist/chunk-PBAYOJJO.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resilient-call/index.ts"],"names":[],"mappings":"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","file":"chunk-W2S4HK7J.js","sourcesContent":["/**\n * @packageDocumentation\n *\n * @deprecated `@demlik/tea/resilient-call` is deprecated in favor of\n * `@demlik/tea/with-resilience` — the higher-order `withResilience(base,\n * config)` wrapper (TEA-idiomatic: it wraps any existing machine instead of\n * forcing a bespoke embedded slice; epic consolidation verdict, survivor\n * `with-resilience`). The APIs are NOT drop-in; migration map:\n *\n * - `createResilientCall(config, rng)` + hand-wiring `init` / the verbs /\n * `subs` / `handlers` into your machine → `withResilience(base, { target,\n * ...bricks }, rng)`. Nothing is wired by hand anymore: the wrapper\n * intercepts the `target` base Cmd and routes it through the gate.\n * - `ResilientConfig` bricks (`retry` / `circuit` / `rateLimit` / `cache` /\n * `deadline`) → unchanged; `ResilienceConfig` extends the same knob and\n * adds `target` (required), `keyOf?` (per-call key — was `attempt`'s `key`\n * argument), and `at?` (time-as-data reader — REQUIRED with any\n * time-sensitive brick; `withResilience` throws at construction without it).\n * - The slice at your chosen Model field + `liftResilience` → the wrapper\n * owns `model.$resilience`; there is nothing to lift.\n * - Verbs `attempt` / `succeed` / `fail` / `onTimer` → internal to the\n * wrapper; results route as `$resilience:ok` / `$resilience:err` /\n * `$resilience:timer` Msgs through the merged `update` (the old\n * `resilient_run` / `resilient_ok` / `resilient_err` vocabulary is renamed\n * into the `$resilience:` namespace).\n * - `settleFailed` → NO equivalent. It exists for embedded-slice consumers\n * (`authed-call`, `paginated-walk`) that settle a call without touching the\n * breaker; the wrapper exposes no caller-facing settle verb.\n * - `handlers(ports)` / `subs(state)` / the `subscribeDeadline` re-export →\n * handled by the wrapper; run the returned machine like any other.\n *\n * Removal window (\"deprecate, don't delete\"): the `./resilient-call` subpath\n * export survives one minor release after this deprecation ships, then is\n * removed. Internal sibling modules (`with-resilience` itself, `agent`,\n * `llm-call`, `authed-call`, `reconciler`, `paginated-walk`) intentionally keep\n * importing `../resilient-call` — the deprecation targets the PUBLISHED export,\n * not the module's internal role.\n *\n * @demlik/tea/resilient-call — the ROOT composition: a single resilience knob\n * that bundles cache → circuit-breaker → rate-limit → exponential-backoff retry\n * (with a deadline-driven retry timer) around any fallible `(input) => Promise`\n * port.\n *\n * This is `examples/resilient-fetch.ts`'s hand-wired `attempt()` generalized:\n *\n * - It is **keyed**. The example tracked one URL at a time; this tracks N\n * concurrent logical calls, each under a string `key`. The cache, the\n * waiting-retry bookkeeping, and the retry timers are all per-key, while the\n * circuit breaker and token bucket are shared (one downstream target, one\n * breaker / one bucket — the example's implicit single-target assumption\n * made explicit).\n * - It is **config-driven**, not hand-wired. You hand `createResilientCall`\n * a `ResilientConfig` and get back the uniform knob contract every L2\n * composition exposes: `init()`, the four verbs (`attempt` / `succeed` /\n * `fail` / `onTimer`), `subs(state)`, and `handlers(ports)`.\n * - Every brick is **optional**. Omit `cache` and the cache gate disappears\n * (`get` is never consulted); omit `circuit` and every call passes the\n * breaker; omit `rateLimit` and the bucket never throttles; omit `retry`\n * and a failure is terminal (no backoff, no timer); omit `deadline` and no\n * overall wall-clock cap is armed. \"Omit a brick → omit its gate\" is the\n * whole config story — there are no other modes.\n *\n * Four later compositions (`llm-call`, `paginated-walk`, `authed-call`,\n * `reconciler`) inherit THIS slice + verb shape, so the contract here is\n * load-bearing: the slice is a flat record of brick states, every verb returns\n * `readonly [State, Cmd[]]`, time is always an `at` / `now` parameter, and the\n * RNG used for jitter is injected on the config (never read inside a verb).\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is a plain-data Model field (brick states are all\n * JSON-serializable), so it survives DO eviction / reload.\n * - **Replayable** — every transition is a verb returning new state + Cmds;\n * `replay` reconstructs it exactly. Nothing here closes over time or RNG.\n *\n * ## Where the clock / RNG live\n *\n * Inside the verbs: nowhere. Time arrives as `at` on `attempt` / `onTimer` and\n * is carried on the result / timer Msgs the consumer dispatches. Jitter RNG is\n * injected once at `createResilientCall(config, rng)` and threaded into\n * `nextDelayMs` at the verb boundary — deterministic in tests, `Math.random`\n * by default. The ONLY clock read is `Date.now()` inside the `handlers` port\n * (the effect boundary), exactly as `resilient-fetch.ts` stamps its result Msgs.\n *\n * 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 * // in the machine:\n * init: () => [{ resilience: rc.init() }, []],\n * update: {\n * fetch: (s, m) => lift(s, rc.attempt(s.resilience, m.key, m.input, m.at)),\n * call_ok: (s, m) => lift(s, rc.succeed(s.resilience, m.key, m)),\n * call_err:(s, m) => lift(s, rc.fail(s.resilience, m.key, m)),\n * retry_due:(s, m) => lift(s, rc.onTimer(s.resilience, m)),\n * },\n * subscriptions: (s) => rc.subs(s.resilience),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: rc.handlers({ run: ctx.call }),\n */\n\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../cache\";\nimport {\n type CircuitState,\n canPass,\n initCircuit,\n onFailure,\n onSuccess,\n} from \"../circuit-breaker\";\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n subscribeDeadline,\n} from \"../deadline\";\nimport { type Cmd, type NoCtx, tryInterpret } from \"../index\";\nimport { MsgType } from \"../protocol\";\nimport { without } from \"../pure/core\";\nimport { initBucket, type TokenBucket, tryConsume } from \"../rate-limit\";\nimport {\n type AnyRetryPolicy,\n asRng,\n initRetry,\n nextDelayMs,\n type RetryState,\n recordFailure,\n shouldRetry,\n} from \"../retry-backoff\";\n\n// ===========================================================================\n// Config — the knob. Every brick optional; omit a brick → omit its gate.\n// ===========================================================================\n\n/** Circuit-breaker knob — only the two numbers a consumer ever tunes. */\nexport interface CircuitConfig {\n /** Consecutive failures that trip the breaker open. */\n readonly threshold: number;\n /** How long the breaker stays open before admitting a probe, in ms. */\n readonly cooldownMs: number;\n /**\n * Probes admitted in `half_open` before fast-failing the rest of the round.\n * Defaults to 1 (a single probe decides recovery) — matches the circuit-\n * breaker module's `defaultCircuitPolicy`.\n */\n readonly halfOpenMaxProbes?: number;\n}\n\n/** Token-bucket rate-limit knob. */\nexport interface RateLimitConfig {\n /** Burst size — the bucket starts full at `capacity`. */\n readonly capacity: number;\n /** Steady-state refill rate, tokens per second. */\n readonly refillPerSec: number;\n}\n\n/** Per-entry TTL cache knob. */\nexport interface CacheConfig {\n /** How long a cached success stays fresh, in ms. */\n readonly ttlMs: number;\n}\n\n/** Overall wall-clock deadline knob — an absolute cap per in-flight call. */\nexport interface DeadlineConfig {\n /** Budget from the moment a call starts, in ms. */\n readonly ms: number;\n}\n\n/**\n * The resilience knob. EVERY field is optional: omit a brick and its gate is\n * skipped entirely (the slice still carries a default brick state so the shape\n * stays uniform across configs, but the gate is never consulted). An empty\n * config `{}` is a valid pass-through — a bare effect with no resilience at all.\n */\nexport interface ResilientConfig {\n /**\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 * `fail`'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 * Per-key call phase. `key` indexes a logical call; several can be in flight at\n * once (the generalization of the example's single `url`). Discriminated on\n * `phase` so each phase carries only its own data (pattern 11):\n *\n * - `idle` — never attempted, or fully settled and forgotten.\n * - `running` — the effect Cmd is out; awaiting `succeed` / `fail`.\n * Carries `deadlineAtMs` (absolute cap, `0` if no deadline\n * brick) and the last `input` so a retry can re-issue it.\n * - `waiting_retry` — a transient failure backed off; the retry timer is armed\n * for `retryAtMs`. Carries `input` for the re-attempt.\n * - `circuit_open` — the breaker fast-failed this attempt. Terminal for this\n * call until the consumer re-attempts after cooldown.\n * - `succeeded` — settled OK. `result` is the value the port produced.\n * - `failed` — settled Err (out of retries, or no retry brick). `error`\n * carries the last failure.\n */\nexport type CallPhase<I, R> =\n | { readonly phase: \"idle\" }\n | {\n readonly phase: \"running\";\n readonly input: I;\n readonly deadlineAtMs: number;\n }\n | {\n readonly phase: \"waiting_retry\";\n readonly input: I;\n readonly retryAtMs: number;\n readonly deadlineAtMs: number;\n }\n | { readonly phase: \"circuit_open\" }\n | { readonly phase: \"succeeded\"; readonly result: R }\n | { readonly phase: \"failed\"; readonly error: unknown };\n\n/**\n * The slice. `circuit` / `bucket` are SHARED across keys (one downstream target\n * → one breaker, one bucket). `retry` and `cache` are PER KEY (each logical call\n * has its own backoff count + cached result), as is the per-call `CallPhase`.\n * Every field is plain data → the whole slice serializes into a `Store<S>`.\n *\n * `retry` / `cache` keep a default brick value even when the corresponding\n * config brick is omitted, so the slice shape is identical regardless of which\n * bricks the consumer enabled — the verbs simply never consult the unused brick.\n */\nexport interface ResilientState<I, R> {\n readonly circuit: CircuitState;\n readonly bucket: TokenBucket;\n /**\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\n// ===========================================================================\n// Cmds + Msgs the knob speaks. Generic over the port input `I`.\n// ===========================================================================\n\n/**\n * The one effect this knob emits: \"run the port for `key` with `input`\". The\n * consumer's `handlers(ports)` interprets it; it carries no closure — `input`\n * is plain data (invariant 3).\n */\nexport type RunCmd<I> = Cmd<typeof MsgType.ResilientRun> & {\n readonly key: string;\n readonly input: I;\n};\n\n/** Settle Msgs the `handlers` port dispatches back. `at` is stamped at the boundary. */\nexport type SucceedMsg<R> = {\n readonly type: typeof MsgType.ResilientOk;\n readonly key: string;\n readonly result: R;\n readonly at: number;\n};\nexport type FailMsg = {\n readonly type: typeof MsgType.ResilientErr;\n readonly key: string;\n readonly error: unknown;\n readonly at: number;\n};\n\n/** The retry / deadline timer Msg — `DeadlineExceeded`, keyed by the call `key`. */\nexport type ResilientTimerMsg = DeadlineExceeded;\n\n/**\n * The plain-data error a deadline-failed call settles with. A `{_tag, ...}`\n * sentinel — NEVER a `new Error(...)` — so the slice stays JSON-serializable\n * (Error objects round-trip to `{}` and carry a stack trace, both of which break\n * the slice's durability invariant). `id` / `atMs` echo the timer that fired so\n * a consumer can attribute the failure to its deadline.\n */\nexport type DeadlineExceededError = {\n readonly _tag: \"deadline_exceeded\";\n readonly id: string;\n readonly atMs: number;\n};\n\n/** Ports the consumer supplies to `handlers`. */\nexport interface ResilientPorts<I, R> {\n /** The fallible work this knob wraps. Throws on failure → routed to `fail`. */\n readonly run: (input: I, key: string) => Promise<R>;\n}\n\n// ===========================================================================\n// Internal brick-policy derivation — config → the L1 policy shapes.\n// ===========================================================================\n\n// A circuit policy is only built when the brick is configured; absence is\n// represented as `null` so the gate can short-circuit to \"always pass\".\nfunction circuitPolicy(config: ResilientConfig) {\n if (config.circuit === undefined) return null;\n return {\n failureThreshold: config.circuit.threshold,\n cooldownMs: config.circuit.cooldownMs,\n halfOpenMaxProbes: config.circuit.halfOpenMaxProbes ?? 1,\n };\n}\n\n// Sub-id families. Each call's retry timer is a deadline keyed by `key`, so a\n// machine running many concurrent calls reconciles each independently.\nfunction retryTimerId(key: string): string {\n return `resilient:retry:${key}`;\n}\nfunction deadlineTimerId(key: string): string {\n return `resilient:deadline:${key}`;\n}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a resilient-call knob from `config`. `rng` is injected for retry\n * jitter — pass a fixed `() => 0.5` in tests to pin backoff; defaults to\n * `Math.random` (read at the verb boundary, never inside a brick op). It is\n * curried onto the factory rather than threaded through every verb so callers\n * spell the determinism choice once.\n *\n * Returns the uniform L2 knob contract. `I` is the port input type, `R` the\n * port result type.\n *\n * @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — not\n * drop-in; see the migration map at the top of this module.\n */\nexport function createResilientCall<I, R>(\n config: ResilientConfig,\n rng: () => number = Math.random,\n) {\n const cPolicy = circuitPolicy(config);\n // Re-affirm the `[0, 1)` contract at the seam where the injected generator\n // feeds the backoff math (brand from `../retry-backoff`). The factory's\n // public param stays a plain `() => number` so transitive callers are\n // unchanged; `asRng` is the single point it's branded for `nextDelayMs`.\n const rngBranded = asRng(rng);\n\n /** The starting slice. Bricks not in `config` still get a default value. */\n function init(): ResilientState<I, R> {\n return {\n circuit: initCircuit(),\n // The bucket carries a default value even without the rateLimit brick so\n // the slice shape is uniform, but the rate-limit GATE is skipped entirely\n // when the brick is absent (see `gate`) — the bucket is simply never\n // consulted. A `0/0` placeholder bucket would be touched by `tryConsume`;\n // a `1-cap` default is harmless because the gate never calls it.\n bucket: config.rateLimit\n ? initBucket(\n config.rateLimit.capacity,\n config.rateLimit.refillPerSec,\n 0,\n )\n : initBucket(1, 1, 0),\n retry: {},\n cache: initCache<R>(),\n calls: {},\n };\n }\n\n /** Read this key's retry state, defaulting to a fresh one. */\n function retryOf(s: ResilientState<I, R>, key: string): RetryState {\n return s.retry[key] ?? initRetry();\n }\n\n /** Compute the absolute deadline for a call starting at `at` (0 = no cap). */\n function deadlineAt(at: number): number {\n return config.deadline ? at + config.deadline.ms : 0;\n }\n\n /**\n * The shared decision: cache → rate-limit → circuit gate, then either emit\n * the effect or schedule a retry. `deadlineAtMs` is threaded so a re-attempt\n * (from `onTimer`) keeps the ORIGINAL overall deadline rather than restarting\n * it. PURE — `at` is the only clock; `rng` is the injected jitter source.\n *\n * Used by both `attempt` (fresh call) and `onTimer` (retry timer fired) so\n * the gate logic lives in exactly one place — the example's shared `attempt`\n * helper, lifted to a keyed knob.\n *\n * ## Why rate-limit is gated BEFORE the circuit probe\n *\n * The circuit-breaker's `canPass` is not a passive read: in `half_open` it\n * *consumes* a probe slot (advancing `probes`), and the consumed slot is only\n * released when a probe RESOLVES via `onSuccess` / `onFailure`. So a probe\n * slot may only be taken when this attempt is actually going to reach the\n * backend (i.e. emit the `resilient_run` Cmd). The rate-limit gate can reject\n * an attempt and return a backoff WITHOUT any Cmd — no backend call, so no\n * settle Msg ever comes back. If the circuit probe were consumed first, that\n * rate-limited attempt would burn the half-open probe budget on a call that\n * never happened, wedging the breaker `half_open` at its probe cap forever\n * (nothing moves it out of `half_open` but a resolved probe). Running the\n * rate-limit gate first means the circuit probe is consumed ONLY on the path\n * that emits the effect.\n */\n function gate(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n deadlineAtMs: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n // 1) Serve from cache if fresh — no effect at all. Skipped when no cache brick.\n if (config.cache !== undefined) {\n const cached = cacheGet(s.cache, key, at);\n if (cached !== undefined) {\n return [setCall(s, key, { phase: \"succeeded\", result: cached }), []];\n }\n }\n\n // 2) Rate-limit gate FIRST — it may reject with no Cmd, so it must run\n // before any circuit probe is consumed (see header note). Skipped entirely\n // when no rateLimit brick — the bucket is never consulted, so the default\n // placeholder bucket stays untouched.\n let bucket = s.bucket;\n if (config.rateLimit !== undefined) {\n const [next, hasToken] = tryConsume(bucket, at);\n bucket = next;\n if (!hasToken) {\n // Rate-limited: treat as a transient failure and back off via retry.\n // The circuit is untouched — no probe is spent on a call that never ran.\n return backoff(\n { ...s, bucket },\n key,\n input,\n \"rate_limited\",\n at,\n deadlineAtMs,\n );\n }\n }\n\n // 3) Circuit-breaker gate. `null` policy → no breaker → always pass. Reached\n // only once a token is secured, so a half-open probe is consumed exclusively\n // on the path that emits the effect below.\n let circuit = s.circuit;\n if (cPolicy !== null) {\n const [next, ok] = canPass(circuit, cPolicy, at);\n circuit = next;\n if (!ok) {\n return [\n { ...setCall(s, key, { phase: \"circuit_open\" }), bucket, circuit },\n [],\n ];\n }\n }\n\n // 4) All gates passed — emit the effect as data.\n return [\n {\n ...setCall(s, key, { phase: \"running\", input, deadlineAtMs }),\n circuit,\n bucket,\n },\n [{ type: MsgType.ResilientRun, key, input }],\n ];\n }\n\n /**\n * Back off after a transient failure: record it, and if `retry` permits\n * another attempt, enter `waiting_retry` with the retry timer armed; else\n * settle `failed`. When no `retry` brick is configured the call fails on the\n * first failure (no backoff, no timer). PURE.\n *\n * `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 deadlineAtMs: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n if (config.retry === undefined) {\n return [setCall(s, key, { phase: \"failed\", error }), []];\n }\n const retry = recordFailure(retryOf(s, key), error, 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 deadlineAtMs,\n }),\n [],\n ];\n }\n\n // Set the phase for one key, leaving every other field untouched.\n function setCall(\n s: ResilientState<I, R>,\n key: string,\n phase: CallPhase<I, R>,\n ): ResilientState<I, R> {\n return { ...s, calls: { ...s.calls, [key]: phase } };\n }\n\n // === Verb: attempt =======================================================\n\n /**\n * Start (or restart) the call for `key` with `input` at time `at`. Runs the\n * cache → circuit → rate-limit gate and either emits the `resilient_run`\n * effect, schedules a retry, fast-fails (circuit open), or serves the cache.\n * PURE.\n */\n function attempt(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n return gate(s, key, input, at, deadlineAt(at));\n }\n\n // === Verb: succeed =======================================================\n\n /**\n * Record a success for `key`: close the breaker, fill the cache (if a cache\n * brick exists), reset this key's retry count, and settle `succeeded`. PURE —\n * `msg.at` is the cache write clock.\n */\n function succeed(\n s: ResilientState<I, R>,\n key: string,\n msg: SucceedMsg<R>,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n const circuit =\n cPolicy !== null ? onSuccess(s.circuit, cPolicy) : s.circuit;\n const cache =\n config.cache !== undefined\n ? cacheSet(s.cache, key, msg.result, msg.at, config.cache.ttlMs)\n : s.cache;\n // Drop this key's retry counter — a success ends the retry run.\n const retry = without(s.retry, key);\n return [\n {\n ...setCall(s, key, { phase: \"succeeded\", result: msg.result }),\n circuit,\n cache,\n retry,\n },\n [],\n ];\n }\n\n // === Verb: fail ==========================================================\n\n /**\n * Record a failure for `key`: trip the breaker (it may open), then back off —\n * schedule a retry if `retry` permits, else settle `failed`. PURE — `msg.at`\n * stamps the breaker trip + the retry delay base. Re-issues from the call's\n * remembered `input` and preserves the original overall `deadlineAtMs`.\n */\n function fail(\n s: ResilientState<I, R>,\n key: string,\n msg: FailMsg,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n const circuit =\n cPolicy !== null ? onFailure(s.circuit, cPolicy, msg.at) : s.circuit;\n const withCircuit = { ...s, circuit };\n const call = s.calls[key];\n const input = call?.phase === \"running\" ? call.input : undefined;\n const deadlineAtMs =\n call?.phase === \"running\" ? call.deadlineAtMs : deadlineAt(msg.at);\n // No remembered input (e.g. a stray fail for a key not running) → just\n // settle failed after tripping the breaker; nothing to re-issue.\n if (input === undefined) {\n return [\n setCall(withCircuit, key, { phase: \"failed\", error: msg.error }),\n [],\n ];\n }\n return backoff(withCircuit, key, input, msg.error, msg.at, deadlineAtMs);\n }\n\n // === Verb: settleFailed ==================================================\n\n /**\n * Settle `key` to terminal `failed` with `error` WITHOUT touching the circuit\n * breaker or this key's retry counter. PURE.\n *\n * This is the verb for a failure that is NOT a downstream-health signal: the\n * call must end now, but the breaker must not trip and the backoff run must\n * not advance. `fail` is the opposite — it records the failure against the\n * breaker (it may open) and feeds the retry policy (it may back off). Use\n * `settleFailed` when the *reason* the call cannot proceed has nothing to do\n * with downstream health:\n *\n * - `authed-call` settles a 401 it cannot fix (e.g. the refresh budget is\n * spent): a bad token is the caller's problem, not the backend's —\n * tripping the breaker on it would punish a healthy target, and backing\n * off would just retry a request that will 401 again.\n * - `paginated-walk` settles a terminal walk error (e.g. a malformed page\n * the consumer rejects) that should stop the walk without poisoning the\n * shared breaker the next page fetch passes through.\n *\n * Because it leaves `circuit` and `retry[key]` untouched, the breaker keeps\n * its health view of the actual backend and a later `attempt` for this key\n * starts a fresh retry run. Both sibling knobs previously hand-rolled this by\n * splicing `calls` directly; this verb gives them one named, tested entry\n * point so the slice's settle invariants live in exactly one place.\n */\n function settleFailed(\n s: ResilientState<I, R>,\n key: string,\n error: unknown,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n return [setCall(s, key, { phase: \"failed\", error }), []];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A timer fired for some key. Two kinds, disambiguated by the Sub id:\n *\n * - retry timer → re-run the gate for that key's remembered input,\n * re-checking the circuit + bucket at THIS time.\n * - deadline timer → the overall budget elapsed → settle `failed` with a\n * deadline error, no matter the current phase.\n *\n * A timer for a key no longer `waiting_retry` (retry) / no longer in flight\n * (deadline) is a no-op — the reconcile pass races the result; the verb must\n * tolerate a stale fire. PURE.\n */\n function onTimer(\n s: ResilientState<I, R>,\n msg: ResilientTimerMsg,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n for (const [key, call] of Object.entries(s.calls)) {\n if (msg.id === retryTimerId(key)) {\n if (call.phase !== \"waiting_retry\") return [s, []];\n return gate(s, key, call.input, msg.atMs, call.deadlineAtMs);\n }\n if (msg.id === deadlineTimerId(key)) {\n // Deadline only fires for a still-active call. A settled call has no\n // armed deadline Sub, but tolerate a stale fire defensively.\n if (call.phase !== \"running\" && call.phase !== \"waiting_retry\")\n return [s, []];\n // Plain-data sentinel, NOT a `new Error(...)`: an Error object is not\n // JSON-serializable (it round-trips to `{}`) and captures a stack trace,\n // both of which break the slice's durability invariant once it is\n // persisted into a `Store<S>` and reloaded. A `{_tag, ...}` record keeps\n // the slice round-trip-equal under JSON.parse(JSON.stringify(...)).\n return [\n setCall(s, key, {\n phase: \"failed\",\n error: {\n _tag: \"deadline_exceeded\",\n id: msg.id,\n atMs: msg.atMs,\n } satisfies DeadlineExceededError,\n }),\n [],\n ];\n }\n }\n return [s, []];\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions: a retry timer for every `waiting_retry` call, and\n * (when the `deadline` brick is configured) an overall deadline timer for\n * every still-active call. Both are `DeadlineSub`s reconciled by id, so a\n * phase change cancels the matching timer automatically — no manual\n * `clearTimeout`. Wire `subscribe: { deadline: subscribeDeadline }`.\n */\n function subs(s: ResilientState<I, R>): readonly DeadlineSub[] {\n const out: DeadlineSub[] = [];\n for (const [key, call] of Object.entries(s.calls)) {\n if (call.phase === \"waiting_retry\") {\n out.push(deadlineSub(retryTimerId(key), call.retryAtMs));\n }\n if (\n config.deadline !== undefined &&\n (call.phase === \"running\" || call.phase === \"waiting_retry\") &&\n call.deadlineAtMs > 0\n ) {\n out.push(deadlineSub(deadlineTimerId(key), call.deadlineAtMs));\n }\n }\n return out;\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handler for `resilient_run`. Wraps the consumer's\n * `run` port via `tryInterpret` (Railway): success → `resilient_ok`, failure\n * → `resilient_err`, each stamped with `Date.now()` at the effect boundary —\n * the ONE permitted clock read (interpret is impure; the reducer never reads\n * the clock). Assign to the machine's `interpret`:\n *\n * interpret: rc.handlers({ run: (input, key) => ctx.callBackend(input) })\n */\n function handlers(ports: ResilientPorts<I, R>) {\n return {\n resilient_run: tryInterpret<\n RunCmd<I>,\n R,\n SucceedMsg<R> | FailMsg,\n // The work fn reads nothing from ctx — it forwards `cmd.input` to the\n // consumer-supplied `run` port. `NoCtx` (not `unknown`) marks this as a\n // DELIBERATE context-free seam, so callers see intent, not looseness.\n NoCtx\n >(\n (cmd) => ports.run(cmd.input, cmd.key),\n (result, cmd): SucceedMsg<R> => ({\n type: MsgType.ResilientOk,\n key: cmd.key,\n result,\n at: Date.now(),\n }),\n (error, cmd): FailMsg => ({\n type: MsgType.ResilientErr,\n key: cmd.key,\n error,\n at: Date.now(),\n }),\n ),\n };\n }\n\n return {\n init,\n attempt,\n succeed,\n fail,\n settleFailed,\n onTimer,\n subs,\n handlers,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the\n * slice lives at `state.resilience`. A convenience for the common single-slice\n * host; consumers with a differently-named field spread by hand. Pure — a thin\n * record rebuild, no clock / RNG.\n *\n * @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — the\n * wrapper owns `model.$resilience`, so there is nothing to lift; see the\n * migration map at the top of this module.\n */\nexport function liftResilience<\n S extends { resilience: ResilientState<I, R> },\n I,\n R,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [ResilientState<I, R>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, resilience: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives so consumers (and tests) wire one\n * import: `subscribeDeadline` is the `subscribe` cell, `deadlineSub` builds the\n * Sub literal this knob's `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub, DeadlineExceeded };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/resilient-call/index.ts"],"names":[],"mappings":"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","file":"chunk-EC6FO42Q.js","sourcesContent":["/**\n * @packageDocumentation\n *\n * @deprecated `@demlik/tea/resilient-call` is deprecated in favor of\n * `@demlik/tea/with-resilience` — the higher-order `withResilience(base,\n * config)` wrapper (TEA-idiomatic: it wraps any existing machine instead of\n * forcing a bespoke embedded slice; epic consolidation verdict, survivor\n * `with-resilience`). The APIs are NOT drop-in; migration map:\n *\n * - `createResilientCall(config, rng)` + hand-wiring `init` / the verbs /\n * `subs` / `handlers` into your machine → `withResilience(base, { target,\n * ...bricks }, rng)`. Nothing is wired by hand anymore: the wrapper\n * intercepts the `target` base Cmd and routes it through the gate.\n * - `ResilientConfig` bricks (`retry` / `circuit` / `rateLimit` / `cache` /\n * `deadline`) → unchanged; `ResilienceConfig` extends the same knob and\n * adds `target` (required), `keyOf?` (per-call key — was `attempt`'s `key`\n * argument), and `at?` (time-as-data reader — REQUIRED with any\n * time-sensitive brick; `withResilience` throws at construction without it).\n * - The slice at your chosen Model field + `liftResilience` → the wrapper\n * owns `model.$resilience`; there is nothing to lift.\n * - Verbs `attempt` / `succeed` / `fail` / `onTimer` → internal to the\n * wrapper; results route as `$resilience:ok` / `$resilience:err` /\n * `$resilience:timer` Msgs through the merged `update` (the old\n * `resilient_run` / `resilient_ok` / `resilient_err` vocabulary is renamed\n * into the `$resilience:` namespace).\n * - `settleFailed` → NO equivalent. It exists for embedded-slice consumers\n * (`authed-call`, `paginated-walk`) that settle a call without touching the\n * breaker; the wrapper exposes no caller-facing settle verb.\n * - `handlers(ports)` / `subs(state)` / the `subscribeDeadline` re-export →\n * handled by the wrapper; run the returned machine like any other.\n *\n * Removal window (\"deprecate, don't delete\"): the `./resilient-call` subpath\n * export survives one minor release after this deprecation ships, then is\n * removed. Internal sibling modules (`with-resilience` itself, `agent`,\n * `llm-call`, `authed-call`, `reconciler`, `paginated-walk`) intentionally keep\n * importing `../resilient-call` — the deprecation targets the PUBLISHED export,\n * not the module's internal role.\n *\n * @demlik/tea/resilient-call — the ROOT composition: a single resilience knob\n * that bundles cache → circuit-breaker → rate-limit → exponential-backoff retry\n * (with a deadline-driven retry timer) around any fallible `(input) => Promise`\n * port.\n *\n * This is `examples/resilient-fetch.ts`'s hand-wired `attempt()` generalized:\n *\n * - It is **keyed**. The example tracked one URL at a time; this tracks N\n * concurrent logical calls, each under a string `key`. The cache, the\n * waiting-retry bookkeeping, and the retry timers are all per-key, while the\n * circuit breaker and token bucket are shared (one downstream target, one\n * breaker / one bucket — the example's implicit single-target assumption\n * made explicit).\n * - It is **config-driven**, not hand-wired. You hand `createResilientCall`\n * a `ResilientConfig` and get back the uniform knob contract every L2\n * composition exposes: `init()`, the four verbs (`attempt` / `succeed` /\n * `fail` / `onTimer`), `subs(state)`, and `handlers(ports)`.\n * - Every brick is **optional**. Omit `cache` and the cache gate disappears\n * (`get` is never consulted); omit `circuit` and every call passes the\n * breaker; omit `rateLimit` and the bucket never throttles; omit `retry`\n * and a failure is terminal (no backoff, no timer); omit `deadline` and no\n * overall wall-clock cap is armed. \"Omit a brick → omit its gate\" is the\n * whole config story — there are no other modes.\n *\n * Four later compositions (`llm-call`, `paginated-walk`, `authed-call`,\n * `reconciler`) inherit THIS slice + verb shape, so the contract here is\n * load-bearing: the slice is a flat record of brick states, every verb returns\n * `readonly [State, Cmd[]]`, time is always an `at` / `now` parameter, and the\n * RNG used for jitter is injected on the config (never read inside a verb).\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is a plain-data Model field (brick states are all\n * JSON-serializable), so it survives DO eviction / reload.\n * - **Replayable** — every transition is a verb returning new state + Cmds;\n * `replay` reconstructs it exactly. Nothing here closes over time or RNG.\n *\n * ## Where the clock / RNG live\n *\n * Inside the verbs: nowhere. Time arrives as `at` on `attempt` / `onTimer` and\n * is carried on the result / timer Msgs the consumer dispatches. Jitter RNG is\n * injected once at `createResilientCall(config, rng)` and threaded into\n * `nextDelayMs` at the verb boundary — deterministic in tests, `Math.random`\n * by default. The ONLY clock read is `Date.now()` inside the `handlers` port\n * (the effect boundary), exactly as `resilient-fetch.ts` stamps its result Msgs.\n *\n * 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 * // in the machine:\n * init: () => [{ resilience: rc.init() }, []],\n * update: {\n * fetch: (s, m) => lift(s, rc.attempt(s.resilience, m.key, m.input, m.at)),\n * call_ok: (s, m) => lift(s, rc.succeed(s.resilience, m.key, m)),\n * call_err:(s, m) => lift(s, rc.fail(s.resilience, m.key, m)),\n * retry_due:(s, m) => lift(s, rc.onTimer(s.resilience, m)),\n * },\n * subscriptions: (s) => rc.subs(s.resilience),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: rc.handlers({ run: ctx.call }),\n */\n\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../cache\";\nimport {\n type CircuitState,\n canPass,\n initCircuit,\n onFailure,\n onSuccess,\n} from \"../circuit-breaker\";\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n subscribeDeadline,\n} from \"../deadline\";\nimport { type Cmd, type NoCtx, tryInterpret } from \"../index\";\nimport { MsgType } from \"../protocol\";\nimport { without } from \"../pure/core\";\nimport { initBucket, type TokenBucket, tryConsume } from \"../rate-limit\";\nimport {\n type AnyRetryPolicy,\n asRng,\n initRetry,\n nextDelayMs,\n type RetryState,\n recordFailure,\n shouldRetry,\n} from \"../retry-backoff\";\n\n// ===========================================================================\n// Config — the knob. Every brick optional; omit a brick → omit its gate.\n// ===========================================================================\n\n/** Circuit-breaker knob — only the two numbers a consumer ever tunes. */\nexport interface CircuitConfig {\n /** Consecutive failures that trip the breaker open. */\n readonly threshold: number;\n /** How long the breaker stays open before admitting a probe, in ms. */\n readonly cooldownMs: number;\n /**\n * Probes admitted in `half_open` before fast-failing the rest of the round.\n * Defaults to 1 (a single probe decides recovery) — matches the circuit-\n * breaker module's `defaultCircuitPolicy`.\n */\n readonly halfOpenMaxProbes?: number;\n}\n\n/** Token-bucket rate-limit knob. */\nexport interface RateLimitConfig {\n /** Burst size — the bucket starts full at `capacity`. */\n readonly capacity: number;\n /** Steady-state refill rate, tokens per second. */\n readonly refillPerSec: number;\n}\n\n/** Per-entry TTL cache knob. */\nexport interface CacheConfig {\n /** How long a cached success stays fresh, in ms. */\n readonly ttlMs: number;\n}\n\n/** Overall wall-clock deadline knob — an absolute cap per in-flight call. */\nexport interface DeadlineConfig {\n /** Budget from the moment a call starts, in ms. */\n readonly ms: number;\n}\n\n/**\n * The resilience knob. EVERY field is optional: omit a brick and its gate is\n * skipped entirely (the slice still carries a default brick state so the shape\n * stays uniform across configs, but the gate is never consulted). An empty\n * config `{}` is a valid pass-through — a bare effect with no resilience at all.\n */\nexport interface ResilientConfig {\n /**\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 * `fail`'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 * Per-key call phase. `key` indexes a logical call; several can be in flight at\n * once (the generalization of the example's single `url`). Discriminated on\n * `phase` so each phase carries only its own data (pattern 11):\n *\n * - `idle` — never attempted, or fully settled and forgotten.\n * - `running` — the effect Cmd is out; awaiting `succeed` / `fail`.\n * Carries `deadlineAtMs` (absolute cap, `0` if no deadline\n * brick) and the last `input` so a retry can re-issue it.\n * - `waiting_retry` — a transient failure backed off; the retry timer is armed\n * for `retryAtMs`. Carries `input` for the re-attempt.\n * - `circuit_open` — the breaker fast-failed this attempt. Terminal for this\n * call until the consumer re-attempts after cooldown.\n * - `succeeded` — settled OK. `result` is the value the port produced.\n * - `failed` — settled Err (out of retries, or no retry brick). `error`\n * carries the last failure.\n */\nexport type CallPhase<I, R> =\n | { readonly phase: \"idle\" }\n | {\n readonly phase: \"running\";\n readonly input: I;\n readonly deadlineAtMs: number;\n }\n | {\n readonly phase: \"waiting_retry\";\n readonly input: I;\n readonly retryAtMs: number;\n readonly deadlineAtMs: number;\n }\n | { readonly phase: \"circuit_open\" }\n | { readonly phase: \"succeeded\"; readonly result: R }\n | { readonly phase: \"failed\"; readonly error: unknown };\n\n/**\n * The slice. `circuit` / `bucket` are SHARED across keys (one downstream target\n * → one breaker, one bucket). `retry` and `cache` are PER KEY (each logical call\n * has its own backoff count + cached result), as is the per-call `CallPhase`.\n * Every field is plain data → the whole slice serializes into a `Store<S>`.\n *\n * `retry` / `cache` keep a default brick value even when the corresponding\n * config brick is omitted, so the slice shape is identical regardless of which\n * bricks the consumer enabled — the verbs simply never consult the unused brick.\n */\nexport interface ResilientState<I, R> {\n readonly circuit: CircuitState;\n readonly bucket: TokenBucket;\n /**\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\n// ===========================================================================\n// Cmds + Msgs the knob speaks. Generic over the port input `I`.\n// ===========================================================================\n\n/**\n * The one effect this knob emits: \"run the port for `key` with `input`\". The\n * consumer's `handlers(ports)` interprets it; it carries no closure — `input`\n * is plain data (invariant 3).\n */\nexport type RunCmd<I> = Cmd<typeof MsgType.ResilientRun> & {\n readonly key: string;\n readonly input: I;\n};\n\n/** Settle Msgs the `handlers` port dispatches back. `at` is stamped at the boundary. */\nexport type SucceedMsg<R> = {\n readonly type: typeof MsgType.ResilientOk;\n readonly key: string;\n readonly result: R;\n readonly at: number;\n};\nexport type FailMsg = {\n readonly type: typeof MsgType.ResilientErr;\n readonly key: string;\n readonly error: unknown;\n readonly at: number;\n};\n\n/** The retry / deadline timer Msg — `DeadlineExceeded`, keyed by the call `key`. */\nexport type ResilientTimerMsg = DeadlineExceeded;\n\n/**\n * The plain-data error a deadline-failed call settles with. A `{_tag, ...}`\n * sentinel — NEVER a `new Error(...)` — so the slice stays JSON-serializable\n * (Error objects round-trip to `{}` and carry a stack trace, both of which break\n * the slice's durability invariant). `id` / `atMs` echo the timer that fired so\n * a consumer can attribute the failure to its deadline.\n */\nexport type DeadlineExceededError = {\n readonly _tag: \"deadline_exceeded\";\n readonly id: string;\n readonly atMs: number;\n};\n\n/** Ports the consumer supplies to `handlers`. */\nexport interface ResilientPorts<I, R> {\n /** The fallible work this knob wraps. Throws on failure → routed to `fail`. */\n readonly run: (input: I, key: string) => Promise<R>;\n}\n\n// ===========================================================================\n// Internal brick-policy derivation — config → the L1 policy shapes.\n// ===========================================================================\n\n// A circuit policy is only built when the brick is configured; absence is\n// represented as `null` so the gate can short-circuit to \"always pass\".\nfunction circuitPolicy(config: ResilientConfig) {\n if (config.circuit === undefined) return null;\n return {\n failureThreshold: config.circuit.threshold,\n cooldownMs: config.circuit.cooldownMs,\n halfOpenMaxProbes: config.circuit.halfOpenMaxProbes ?? 1,\n };\n}\n\n// Sub-id families. Each call's retry timer is a deadline keyed by `key`, so a\n// machine running many concurrent calls reconciles each independently.\nfunction retryTimerId(key: string): string {\n return `resilient:retry:${key}`;\n}\nfunction deadlineTimerId(key: string): string {\n return `resilient:deadline:${key}`;\n}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a resilient-call knob from `config`. `rng` is injected for retry\n * jitter — pass a fixed `() => 0.5` in tests to pin backoff; defaults to\n * `Math.random` (read at the verb boundary, never inside a brick op). It is\n * curried onto the factory rather than threaded through every verb so callers\n * spell the determinism choice once.\n *\n * Returns the uniform L2 knob contract. `I` is the port input type, `R` the\n * port result type.\n *\n * @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — not\n * drop-in; see the migration map at the top of this module.\n */\nexport function createResilientCall<I, R>(\n config: ResilientConfig,\n rng: () => number = Math.random,\n) {\n const cPolicy = circuitPolicy(config);\n // Re-affirm the `[0, 1)` contract at the seam where the injected generator\n // feeds the backoff math (brand from `../retry-backoff`). The factory's\n // public param stays a plain `() => number` so transitive callers are\n // unchanged; `asRng` is the single point it's branded for `nextDelayMs`.\n const rngBranded = asRng(rng);\n\n /** The starting slice. Bricks not in `config` still get a default value. */\n function init(): ResilientState<I, R> {\n return {\n circuit: initCircuit(),\n // The bucket carries a default value even without the rateLimit brick so\n // the slice shape is uniform, but the rate-limit GATE is skipped entirely\n // when the brick is absent (see `gate`) — the bucket is simply never\n // consulted. A `0/0` placeholder bucket would be touched by `tryConsume`;\n // a `1-cap` default is harmless because the gate never calls it.\n bucket: config.rateLimit\n ? initBucket(\n config.rateLimit.capacity,\n config.rateLimit.refillPerSec,\n 0,\n )\n : initBucket(1, 1, 0),\n retry: {},\n cache: initCache<R>(),\n calls: {},\n };\n }\n\n /** Read this key's retry state, defaulting to a fresh one. */\n function retryOf(s: ResilientState<I, R>, key: string): RetryState {\n return s.retry[key] ?? initRetry();\n }\n\n /** Compute the absolute deadline for a call starting at `at` (0 = no cap). */\n function deadlineAt(at: number): number {\n return config.deadline ? at + config.deadline.ms : 0;\n }\n\n /**\n * The shared decision: cache → rate-limit → circuit gate, then either emit\n * the effect or schedule a retry. `deadlineAtMs` is threaded so a re-attempt\n * (from `onTimer`) keeps the ORIGINAL overall deadline rather than restarting\n * it. PURE — `at` is the only clock; `rng` is the injected jitter source.\n *\n * Used by both `attempt` (fresh call) and `onTimer` (retry timer fired) so\n * the gate logic lives in exactly one place — the example's shared `attempt`\n * helper, lifted to a keyed knob.\n *\n * ## Why rate-limit is gated BEFORE the circuit probe\n *\n * The circuit-breaker's `canPass` is not a passive read: in `half_open` it\n * *consumes* a probe slot (advancing `probes`), and the consumed slot is only\n * released when a probe RESOLVES via `onSuccess` / `onFailure`. So a probe\n * slot may only be taken when this attempt is actually going to reach the\n * backend (i.e. emit the `resilient_run` Cmd). The rate-limit gate can reject\n * an attempt and return a backoff WITHOUT any Cmd — no backend call, so no\n * settle Msg ever comes back. If the circuit probe were consumed first, that\n * rate-limited attempt would burn the half-open probe budget on a call that\n * never happened, wedging the breaker `half_open` at its probe cap forever\n * (nothing moves it out of `half_open` but a resolved probe). Running the\n * rate-limit gate first means the circuit probe is consumed ONLY on the path\n * that emits the effect.\n */\n function gate(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n deadlineAtMs: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n // 1) Serve from cache if fresh — no effect at all. Skipped when no cache brick.\n if (config.cache !== undefined) {\n const cached = cacheGet(s.cache, key, at);\n if (cached !== undefined) {\n return [setCall(s, key, { phase: \"succeeded\", result: cached }), []];\n }\n }\n\n // 2) Rate-limit gate FIRST — it may reject with no Cmd, so it must run\n // before any circuit probe is consumed (see header note). Skipped entirely\n // when no rateLimit brick — the bucket is never consulted, so the default\n // placeholder bucket stays untouched.\n let bucket = s.bucket;\n if (config.rateLimit !== undefined) {\n const [next, hasToken] = tryConsume(bucket, at);\n bucket = next;\n if (!hasToken) {\n // Rate-limited: treat as a transient failure and back off via retry.\n // The circuit is untouched — no probe is spent on a call that never ran.\n return backoff(\n { ...s, bucket },\n key,\n input,\n \"rate_limited\",\n at,\n deadlineAtMs,\n );\n }\n }\n\n // 3) Circuit-breaker gate. `null` policy → no breaker → always pass. Reached\n // only once a token is secured, so a half-open probe is consumed exclusively\n // on the path that emits the effect below.\n let circuit = s.circuit;\n if (cPolicy !== null) {\n const [next, ok] = canPass(circuit, cPolicy, at);\n circuit = next;\n if (!ok) {\n return [\n { ...setCall(s, key, { phase: \"circuit_open\" }), bucket, circuit },\n [],\n ];\n }\n }\n\n // 4) All gates passed — emit the effect as data.\n return [\n {\n ...setCall(s, key, { phase: \"running\", input, deadlineAtMs }),\n circuit,\n bucket,\n },\n [{ type: MsgType.ResilientRun, key, input }],\n ];\n }\n\n /**\n * Back off after a transient failure: record it, and if `retry` permits\n * another attempt, enter `waiting_retry` with the retry timer armed; else\n * settle `failed`. When no `retry` brick is configured the call fails on the\n * first failure (no backoff, no timer). PURE.\n *\n * `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 deadlineAtMs: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n if (config.retry === undefined) {\n return [setCall(s, key, { phase: \"failed\", error }), []];\n }\n const retry = recordFailure(retryOf(s, key), error, 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 deadlineAtMs,\n }),\n [],\n ];\n }\n\n // Set the phase for one key, leaving every other field untouched.\n function setCall(\n s: ResilientState<I, R>,\n key: string,\n phase: CallPhase<I, R>,\n ): ResilientState<I, R> {\n return { ...s, calls: { ...s.calls, [key]: phase } };\n }\n\n // === Verb: attempt =======================================================\n\n /**\n * Start (or restart) the call for `key` with `input` at time `at`. Runs the\n * cache → circuit → rate-limit gate and either emits the `resilient_run`\n * effect, schedules a retry, fast-fails (circuit open), or serves the cache.\n * PURE.\n */\n function attempt(\n s: ResilientState<I, R>,\n key: string,\n input: I,\n at: number,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n return gate(s, key, input, at, deadlineAt(at));\n }\n\n // === Verb: succeed =======================================================\n\n /**\n * Record a success for `key`: close the breaker, fill the cache (if a cache\n * brick exists), reset this key's retry count, and settle `succeeded`. PURE —\n * `msg.at` is the cache write clock.\n */\n function succeed(\n s: ResilientState<I, R>,\n key: string,\n msg: SucceedMsg<R>,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n const circuit =\n cPolicy !== null ? onSuccess(s.circuit, cPolicy) : s.circuit;\n const cache =\n config.cache !== undefined\n ? cacheSet(s.cache, key, msg.result, msg.at, config.cache.ttlMs)\n : s.cache;\n // Drop this key's retry counter — a success ends the retry run.\n const retry = without(s.retry, key);\n return [\n {\n ...setCall(s, key, { phase: \"succeeded\", result: msg.result }),\n circuit,\n cache,\n retry,\n },\n [],\n ];\n }\n\n // === Verb: fail ==========================================================\n\n /**\n * Record a failure for `key`: trip the breaker (it may open), then back off —\n * schedule a retry if `retry` permits, else settle `failed`. PURE — `msg.at`\n * stamps the breaker trip + the retry delay base. Re-issues from the call's\n * remembered `input` and preserves the original overall `deadlineAtMs`.\n */\n function fail(\n s: ResilientState<I, R>,\n key: string,\n msg: FailMsg,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n const circuit =\n cPolicy !== null ? onFailure(s.circuit, cPolicy, msg.at) : s.circuit;\n const withCircuit = { ...s, circuit };\n const call = s.calls[key];\n const input = call?.phase === \"running\" ? call.input : undefined;\n const deadlineAtMs =\n call?.phase === \"running\" ? call.deadlineAtMs : deadlineAt(msg.at);\n // No remembered input (e.g. a stray fail for a key not running) → just\n // settle failed after tripping the breaker; nothing to re-issue.\n if (input === undefined) {\n return [\n setCall(withCircuit, key, { phase: \"failed\", error: msg.error }),\n [],\n ];\n }\n return backoff(withCircuit, key, input, msg.error, msg.at, deadlineAtMs);\n }\n\n // === Verb: settleFailed ==================================================\n\n /**\n * Settle `key` to terminal `failed` with `error` WITHOUT touching the circuit\n * breaker or this key's retry counter. PURE.\n *\n * This is the verb for a failure that is NOT a downstream-health signal: the\n * call must end now, but the breaker must not trip and the backoff run must\n * not advance. `fail` is the opposite — it records the failure against the\n * breaker (it may open) and feeds the retry policy (it may back off). Use\n * `settleFailed` when the *reason* the call cannot proceed has nothing to do\n * with downstream health:\n *\n * - `authed-call` settles a 401 it cannot fix (e.g. the refresh budget is\n * spent): a bad token is the caller's problem, not the backend's —\n * tripping the breaker on it would punish a healthy target, and backing\n * off would just retry a request that will 401 again.\n * - `paginated-walk` settles a terminal walk error (e.g. a malformed page\n * the consumer rejects) that should stop the walk without poisoning the\n * shared breaker the next page fetch passes through.\n *\n * Because it leaves `circuit` and `retry[key]` untouched, the breaker keeps\n * its health view of the actual backend and a later `attempt` for this key\n * starts a fresh retry run. Both sibling knobs previously hand-rolled this by\n * splicing `calls` directly; this verb gives them one named, tested entry\n * point so the slice's settle invariants live in exactly one place.\n */\n function settleFailed(\n s: ResilientState<I, R>,\n key: string,\n error: unknown,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n return [setCall(s, key, { phase: \"failed\", error }), []];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A timer fired for some key. Two kinds, disambiguated by the Sub id:\n *\n * - retry timer → re-run the gate for that key's remembered input,\n * re-checking the circuit + bucket at THIS time.\n * - deadline timer → the overall budget elapsed → settle `failed` with a\n * deadline error, no matter the current phase.\n *\n * A timer for a key no longer `waiting_retry` (retry) / no longer in flight\n * (deadline) is a no-op — the reconcile pass races the result; the verb must\n * tolerate a stale fire. PURE.\n */\n function onTimer(\n s: ResilientState<I, R>,\n msg: ResilientTimerMsg,\n ): readonly [ResilientState<I, R>, readonly RunCmd<I>[]] {\n for (const [key, call] of Object.entries(s.calls)) {\n if (msg.id === retryTimerId(key)) {\n if (call.phase !== \"waiting_retry\") return [s, []];\n return gate(s, key, call.input, msg.atMs, call.deadlineAtMs);\n }\n if (msg.id === deadlineTimerId(key)) {\n // Deadline only fires for a still-active call. A settled call has no\n // armed deadline Sub, but tolerate a stale fire defensively.\n if (call.phase !== \"running\" && call.phase !== \"waiting_retry\")\n return [s, []];\n // Plain-data sentinel, NOT a `new Error(...)`: an Error object is not\n // JSON-serializable (it round-trips to `{}`) and captures a stack trace,\n // both of which break the slice's durability invariant once it is\n // persisted into a `Store<S>` and reloaded. A `{_tag, ...}` record keeps\n // the slice round-trip-equal under JSON.parse(JSON.stringify(...)).\n return [\n setCall(s, key, {\n phase: \"failed\",\n error: {\n _tag: \"deadline_exceeded\",\n id: msg.id,\n atMs: msg.atMs,\n } satisfies DeadlineExceededError,\n }),\n [],\n ];\n }\n }\n return [s, []];\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions: a retry timer for every `waiting_retry` call, and\n * (when the `deadline` brick is configured) an overall deadline timer for\n * every still-active call. Both are `DeadlineSub`s reconciled by id, so a\n * phase change cancels the matching timer automatically — no manual\n * `clearTimeout`. Wire `subscribe: { deadline: subscribeDeadline }`.\n */\n function subs(s: ResilientState<I, R>): readonly DeadlineSub[] {\n const out: DeadlineSub[] = [];\n for (const [key, call] of Object.entries(s.calls)) {\n if (call.phase === \"waiting_retry\") {\n out.push(deadlineSub(retryTimerId(key), call.retryAtMs));\n }\n if (\n config.deadline !== undefined &&\n (call.phase === \"running\" || call.phase === \"waiting_retry\") &&\n call.deadlineAtMs > 0\n ) {\n out.push(deadlineSub(deadlineTimerId(key), call.deadlineAtMs));\n }\n }\n return out;\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handler for `resilient_run`. Wraps the consumer's\n * `run` port via `tryInterpret` (Railway): success → `resilient_ok`, failure\n * → `resilient_err`, each stamped with `Date.now()` at the effect boundary —\n * the ONE permitted clock read (interpret is impure; the reducer never reads\n * the clock). Assign to the machine's `interpret`:\n *\n * interpret: rc.handlers({ run: (input, key) => ctx.callBackend(input) })\n */\n function handlers(ports: ResilientPorts<I, R>) {\n return {\n resilient_run: tryInterpret<\n RunCmd<I>,\n R,\n SucceedMsg<R> | FailMsg,\n // The work fn reads nothing from ctx — it forwards `cmd.input` to the\n // consumer-supplied `run` port. `NoCtx` (not `unknown`) marks this as a\n // DELIBERATE context-free seam, so callers see intent, not looseness.\n NoCtx\n >(\n (cmd) => ports.run(cmd.input, cmd.key),\n (result, cmd): SucceedMsg<R> => ({\n type: MsgType.ResilientOk,\n key: cmd.key,\n result,\n at: Date.now(),\n }),\n (error, cmd): FailMsg => ({\n type: MsgType.ResilientErr,\n key: cmd.key,\n error,\n at: Date.now(),\n }),\n ),\n };\n }\n\n return {\n init,\n attempt,\n succeed,\n fail,\n settleFailed,\n onTimer,\n subs,\n handlers,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the\n * slice lives at `state.resilience`. A convenience for the common single-slice\n * host; consumers with a differently-named field spread by hand. Pure — a thin\n * record rebuild, no clock / RNG.\n *\n * @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — the\n * wrapper owns `model.$resilience`, so there is nothing to lift; see the\n * migration map at the top of this module.\n */\nexport function liftResilience<\n S extends { resilience: ResilientState<I, R> },\n I,\n R,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [ResilientState<I, R>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, resilience: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives so consumers (and tests) wire one\n * import: `subscribeDeadline` is the `subscribe` cell, `deadlineSub` builds the\n * Sub literal this knob's `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub, DeadlineExceeded };\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createResilientCall, liftResilience } from './chunk-
|
|
1
|
+
import { createResilientCall, liftResilience } from './chunk-EC6FO42Q.js';
|
|
2
2
|
import { MsgType } from './chunk-JELSZN25.js';
|
|
3
3
|
import { describeError } from './chunk-G5E74IJB.js';
|
|
4
4
|
|
|
@@ -84,5 +84,5 @@ function liftLlmCall(state, result) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export { createLlmCall, liftLlmCall };
|
|
87
|
-
//# sourceMappingURL=chunk-
|
|
88
|
-
//# sourceMappingURL=chunk-
|
|
87
|
+
//# sourceMappingURL=chunk-ERYY5P6W.js.map
|
|
88
|
+
//# sourceMappingURL=chunk-ERYY5P6W.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm-call/index.ts"],"names":["msg"],"mappings":"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","file":"chunk-T6HDAPMR.js","sourcesContent":["/**\n * @packageDocumentation\n * @demlik/tea/llm-call — `resilient-call` + structured-output parse + a typed\n * failure variant, around a purpose-discriminated LLM invocation.\n *\n * This is the ~60-line `call_llm` handler from the audit-agent seed\n * (`interpret.ts` / `effects.ts`) collapsed into the uniform L2 knob: a config\n * object, the resilient-call slice it inherits, and one interpret handler that\n * assembles messages, binds the structured-output schema for the call's\n * `purpose`, invokes the model (retry composed from `../resilient-call`, NOT\n * reinvented), parses the output, and RETURNS the enriched resilient settle Msg\n * (`resilient_ok` / `resilient_err`) so it re-enters the host reducer and drives\n * the inherited succeed/fail → backoff → onTimer loop.\n *\n * ## What it adds over `../resilient-call`\n *\n * - **Two DI ports** (the seed's two seams):\n * 1. `model: (modelId) => LLM` — the model factory. Tests pass a fake so\n * the handler runs end-to-end without touching a real provider; the\n * production worker wires `createChatModel(env, …)`.\n * 2. `loadMessages: Loader` — the SDK / message loader. The seed lazy-\n * imports `@langchain/core/messages` (a top-level `import type` blows\n * up the workers test runner); the loader keeps that import lazy and\n * stubbable. It produces the `BaseMessage[]` for a given call.\n * - **Structured-output parse** — each `purpose` maps to a `Schema` in\n * `config.schemas`. The handler binds `model.withStructuredOutput(schema)`\n * so the invoke resolves to a typed object; a `Schema.parse` that throws\n * becomes a `resilient_err` carrying an `LlmErr` (a parse failure is a\n * failure, not a stall — \"errors are data\"). `withStructuredOutput` is the\n * seed's brain-only path; the fake model in tests returns the typed object\n * directly.\n * - **A typed failure variant** — every failure (model throw, timeout-via-\n * retry-exhaustion, schema parse) is surfaced as the `LlmErr` carried on the\n * enriched `resilient_err` settle Msg, tagged with the `purpose` so the\n * consumer's reducer routes per-stage. The seed dispatched\n * `llm_failed{purpose}`; the consumer rebuilds that shape in its reducer arm\n * from `m.error: LlmErr`.\n *\n * ## Inheriting resilient-call by composition (not reinvention)\n *\n * The retry / backoff lives in `../resilient-call`: `createLlmCall` builds a\n * `createResilientCall` knob over the model-invoke port and DELEGATES `init`,\n * `attempt`, `succeed`, `fail`, `onTimer`, `subs` to it verbatim — the slice is\n * literally resilient-call's slice (the spec's \"retry lives here\"). There is no\n * second backoff implementation here. The handler runs the invoke through the\n * resilient-call handler so a transient model failure backs off exactly as a\n * resilient HTTP call would; the `purpose`-branch + structured parse wrap the\n * port the resilient handler drives.\n *\n * ## The two non-negotiables (canon, inherited)\n *\n * - **Durable** — the slice is resilient-call's plain-data slice (a Model\n * field). Input carried on the `resilient_run` Cmd is the plain\n * `LlmCall` request (purpose + modelId + payload) — no closures.\n * - **Replayable** — every transition is a resilient-call verb; the handler\n * is the only impurity, and the one clock read is `Date.now()` at the\n * effect boundary (inside the handler), exactly as the seed stamped its\n * `llm_responded` / `llm_failed` Msgs.\n *\n * ## Typical wiring\n *\n * const llm = createLlmCall<MyPurpose, MyOutputs>({\n * model: (id) => createChatModel(env, id),\n * schemas: { plan: planSchema, report: reportSchema },\n * retry: defaultRetryPolicy,\n * loadMessages: defaultMessagesLoader,\n * });\n *\n * // in the machine:\n * init: () => [{ resilience: llm.init() }, []],\n * update: {\n * call_llm: (s, m) => lift(s, llm.attempt(s.resilience, m.input, m.at)),\n * // `resilient_ok` / `resilient_err` RE-ENTER from `handlers` (the settle\n * // Msg the interpret handler returns). Run the inherited verb FIRST so the\n * // succeed/fail → backoff → onTimer loop advances, THEN fold the enriched\n * // payload (`m.result: LlmOk` / `m.error: LlmErr`) into the host's own\n * // state — the enrichment lives in the reducer arm, not the handler.\n * resilient_ok: (s, m) => {\n * const [slice, cmds] = llm.succeed(s.resilience, m.key, m);\n * return [{ ...s, resilience: slice, output: m.result.output }, cmds];\n * },\n * resilient_err: (s, m) => {\n * const [slice, cmds] = llm.fail(s.resilience, m.key, m);\n * return [{ ...s, resilience: slice, failure: m.error }, cmds];\n * },\n * deadline_exceeded: (s, m) => lift(s, llm.onTimer(s.resilience, m)),\n * },\n * subscriptions: (s) => llm.subs(s.resilience),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: llm.handlers(),\n */\n\nimport { describeError } from \"../describe-error\";\nimport type { Cmd } from \"../index\";\nimport { MsgType } from \"../protocol\";\nimport {\n createResilientCall,\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n type FailMsg,\n liftResilience,\n type ResilientConfig,\n type ResilientState,\n type RunCmd,\n type SucceedMsg,\n subscribeDeadline,\n} from \"../resilient-call\";\nimport type { RetryPolicy } from \"../retry-backoff\";\n\n// ===========================================================================\n// The DI port surfaces — model factory + structured output + message loader.\n// ===========================================================================\n\n/**\n * The minimal structured-output schema contract: `parse(unknown) => T`, the\n * zod-style call the handler uses to validate the model's output before it\n * settles `resilient_ok`. A throwing `parse` (the zod contract on invalid\n * input) propagates out of `invokeOne` and is caught by the resilient handler,\n * which settles `resilient_err` (enriched to `LlmErr`) — a parse failure is a\n * failure, never a silent pass. Structural so a real `z.ZodType<T>` satisfies\n * it without an import.\n */\nexport interface Schema<T> {\n /** Validate + narrow `value` to `T`, or throw on mismatch (the zod contract). */\n parse(value: unknown): T;\n}\n\n/**\n * The minimal chat-model contract every model the handler talks to must\n * satisfy — the seed's `InjectableChatModel`, trimmed to the one operation\n * llm-call drives for brain-only stages:\n *\n * `withStructuredOutput(schema)` → a runnable whose `invoke(messages)`\n * resolves to a typed object matching `schema`.\n *\n * `BaseChatModel` from `@langchain/core` is the runtime type; this surface\n * names only what llm-call calls so a test fake can ignore the rest. Generic\n * over the message shape `Msg` so a consumer's loader and model agree on it\n * without llm-call inspecting messages.\n */\nexport interface Llm<Msg> {\n withStructuredOutput<T>(schema: Schema<T>): {\n invoke(messages: readonly Msg[]): Promise<T>;\n };\n}\n\n/**\n * Build the `Msg[]` the handler hands to the bound model for a given call. The\n * SDK / message-assembly seam — the seed's `buildBaseMessages` + the lazy\n * `loadMessages` loader rolled into one injected port. Async because the seed\n * lazy-imports the SDK (a top-level `import type` of the langchain messages\n * package explodes the workers test runner). Receives the full `LlmCall` so it\n * can branch on `purpose` exactly as the seed's `buildBaseMessages` did.\n */\nexport type MessageLoader<P extends string, Msg> = (\n call: LlmCall<P>,\n) => Promise<readonly Msg[]>;\n\n/**\n * The model factory — the first DI port. `(modelId) => Llm`. Tests pass a fake\n * builder; production wires `createChatModel(env, getModelConfig(id))`. `null`\n * means \"the host's default model\" (the seed's `string | null`).\n */\nexport type ModelFactory<Msg> = (modelId: string | null) => Llm<Msg>;\n\n// ===========================================================================\n// Config — the knob. `model` + `schemas` required (the two DI ports + the\n// per-purpose parse target); `retry` + `loadMessages` optional.\n// ===========================================================================\n\n/**\n * The llm-call knob. `model` and `schemas` are the load-bearing pair — the\n * model factory DI port and the per-purpose structured-output targets; the\n * rest is optional, exactly the resilient-call \"omit a brick → omit its gate\"\n * story for `retry`:\n *\n * - `model` — DI port 1: the model factory.\n * - `schemas` — one `Schema` per `Purpose`. The handler binds\n * `schemas[call.purpose]` as the structured-output target.\n * - `retry` — backoff policy, composed straight into `../resilient-\n * call`. Omit it and a model failure is terminal (no\n * backoff), exactly as resilient-call with no retry brick.\n * - `loadMessages` — DI port 2: the SDK / message loader. Omit it and the\n * handler invokes the bound model with `[]` (a degenerate\n * but valid call) — provide it to assemble real messages.\n *\n * `P` is the purpose union (e.g. `\"plan\" | \"report\"`); `O` maps each purpose to\n * its parsed output type; `Msg` is the model's message shape (threaded through\n * the loader + model so llm-call never inspects a message).\n */\nexport interface LlmCallConfig<\n P extends string,\n O extends Record<P, unknown>,\n Msg = unknown,\n> {\n /** DI port 1 — the model factory. `(modelId) => Llm`. */\n readonly model: ModelFactory<Msg>;\n /** One structured-output schema per purpose; the parse target the handler binds. */\n readonly schemas: { readonly [K in P]: Schema<O[K]> };\n /** Backoff policy, composed into `../resilient-call`. Omit → no backoff. */\n readonly retry?: RetryPolicy;\n /** DI port 2 — the SDK / message loader. Omit → the handler invokes with `[]`. */\n readonly loadMessages?: MessageLoader<P, Msg>;\n}\n\n// ===========================================================================\n// The request / response shapes the knob speaks.\n// ===========================================================================\n\n/**\n * One LLM call request — the resilient-call `input` for this knob, carried on\n * the `resilient_run` Cmd as plain data (no closures, invariant 3). Mirrors the\n * seed's purpose-discriminated `CallLlmCmd`: a `purpose` selecting the schema +\n * prompt assembly, the `model` id, and the opaque per-purpose `payload` the\n * loader consumes.\n *\n * `key` defaults to `purpose` when the consumer calls `attempt(s, purpose, …)`,\n * so one in-flight call per purpose is tracked under the resilient-call slice —\n * the common single-call-per-stage shape. A consumer that fans out many calls\n * of one purpose passes a distinct `key`.\n */\nexport interface LlmCall<P extends string> {\n /** The stage / schema selector — drives both `schemas[purpose]` and message assembly. */\n readonly purpose: P;\n /** The model id to invoke; `null` = the host's default model. */\n readonly model: string | null;\n /** The per-purpose prompt payload the `MessageLoader` consumes. Opaque to the knob. */\n readonly payload: unknown;\n}\n\n/** The parsed, typed success carried on the `resilient_ok` settle Msg, tagged with its purpose. */\nexport interface LlmOk<P extends string, O extends Record<P, unknown>> {\n readonly key: string;\n readonly purpose: P;\n readonly output: O[P];\n}\n\n/** The typed failure variant — every failure path surfaces this, tagged by purpose. */\nexport interface LlmErr<P extends string> {\n readonly key: string;\n readonly purpose: P;\n /** A human-readable cause (model throw, retry-exhaustion, or schema parse). */\n readonly reason: string;\n /** The original error, carried untouched for the consumer to inspect. */\n readonly error: unknown;\n}\n\n/**\n * The settle Msgs llm-call's handler RETURNS from `interpret` so the substrate\n * enqueues them as follow-up Msgs (re-entry) into the host reducer — exactly as\n * `../resilient-call` does. They are the resilient-call settle Msgs with the\n * payloads ENRICHED to llm-call's typed variants: `result` is the parsed,\n * purpose-tagged `LlmOk`; `error` is the typed `LlmErr` (purpose + reason +\n * raw). The host wires `resilient_ok` → `succeed` and `resilient_err` → `fail`\n * reducer arms (the doc-comment wiring), and folds the enriched payload into its\n * own state there — llm-call never forces a Msg vocabulary on the host, and the\n * settle Msg drives the inherited succeed/fail → backoff → onTimer loop instead\n * of bypassing it.\n */\n//\n// The success Msg is `../resilient-call`'s `SucceedMsg` specialized to the parsed\n// `LlmOk` result — NOT a re-declared shape — so the verb returns (`succeed`'s\n// argument, the handler's resolve) thread through with no cast. The failure Msg\n// is the resilient `FailMsg` with its `error: unknown` NARROWED to the typed\n// `LlmErr`: every value the handler enriches is an `LlmErr`, and `LlmErr` is\n// assignable to `unknown`, so `LlmFailMsg` flows into `rc.fail` (which takes the\n// wide `FailMsg`) directly while the host reducer reads the narrow `error` type.\nexport type LlmSucceedMsg<\n P extends string,\n O extends Record<P, unknown>,\n> = SucceedMsg<LlmOk<P, O>>;\nexport type LlmFailMsg<P extends string> = Omit<FailMsg, \"error\"> & {\n readonly error: LlmErr<P>;\n};\n\n/** The retry / deadline timer Msg — `DeadlineExceeded`, inherited from resilient-call. */\nexport type LlmTimerMsg = DeadlineExceeded;\n\n/**\n * The ports the LEGACY detached `handlers(ports)` form takes. Two outbound Msg\n * builders the handler hands the typed Ok / Err; the consumer returns its own\n * Msg (or `undefined`). KEPT only for `../agent`, which still inherits the\n * detached shape; that path does NOT drive the retry loop (it dispatches the\n * consumer's Msg directly and never re-enters the resilient settle Msg). New\n * consumers use the no-arg `handlers()` form, which RETURNS the settle Msg for\n * re-entry and drives the inherited succeed/fail → backoff → onTimer loop.\n * `../agent` carries the same retry-loop gap and is fixed separately.\n *\n * The builders return `M | undefined` — `undefined` is the \"dispatch nothing\"\n * sentinel the detached form checks (`!== undefined`), spelled as `undefined`\n * rather than `void` so the union is unambiguous (a `void` member of a union is\n * confusing and reads as \"may return anything\").\n */\nexport interface LlmCallPorts<\n P extends string,\n O extends Record<P, unknown>,\n M,\n> {\n /** Build the Msg dispatched on a parsed success. Return `undefined` to dispatch nothing. */\n readonly onOk: (ok: LlmOk<P, O>) => M | undefined;\n /** Build the Msg dispatched on any failure (throw / retry-exhausted / parse). */\n readonly onErr: (err: LlmErr<P>) => M | undefined;\n}\n\n/**\n * The effect Cmd the knob emits: run the LLM call for `key` with `input`. The\n * `input` is the plain `LlmCall` request — the handler reads `purpose` /\n * `model` / `payload` off it (invariant 3: Cmds are data).\n *\n * It is `../resilient-call`'s `RunCmd` specialized to the `LlmCall` input — NOT a\n * re-declared shape — so the resilient verbs' return tuples (`[State, RunCmd[]]`)\n * are this exact type and thread through `attempt` / `succeed` / `fail` /\n * `onTimer` with no cast.\n */\nexport type LlmRunCmd<P extends string> = RunCmd<LlmCall<P>>;\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build an llm-call knob from `config`. `rng` is injected for the inherited\n * retry jitter (pass a fixed `() => 0.5` in tests to pin backoff; defaults to\n * `Math.random`, read only at the resilient-call verb boundary).\n *\n * Returns the uniform L2 knob contract. The slice + verbs (`init`, `attempt`,\n * `succeed`, `fail`, `onTimer`, `subs`) are DELEGATED to a `../resilient-call`\n * knob built over the structured-output model-invoke port — no second backoff\n * here. `handlers` is the one new piece: the purpose-branching, schema-parsing\n * wrapper that returns the enriched resilient settle Msg for re-entry.\n *\n * `P` is the purpose union, `O` the purpose→output map, `Msg` the model's\n * message shape.\n */\nexport function createLlmCall<\n P extends string,\n O extends Record<P, unknown>,\n Msg = unknown,\n>(config: LlmCallConfig<P, O, Msg>, rng: () => number = Math.random) {\n // ---- The composed resilient-call knob ----------------------------------\n //\n // The resilient-call input is the full `LlmCall` request; its result is the\n // PARSED, typed `LlmOk`. Only the `retry` brick is forwarded — llm-call does\n // not expose circuit / rate-limit / cache / deadline knobs (a stage call is\n // a single brain invocation, not a keyed downstream target). Omitting those\n // bricks omits their gates, exactly as resilient-call documents.\n const resilientConfig: ResilientConfig = {\n ...(config.retry === undefined ? {} : { retry: config.retry }),\n };\n const rc = createResilientCall<LlmCall<P>, LlmOk<P, O>>(resilientConfig, rng);\n\n /** The slice this knob owns — resilient-call's slice verbatim. */\n type State = ResilientState<LlmCall<P>, LlmOk<P, O>>;\n\n /** The starting slice — resilient-call's. */\n function init(): State {\n return rc.init();\n }\n\n /**\n * Start (or restart) an LLM call. `keyOrPurpose` defaults the resilient-call\n * `key` to the call's `purpose` so one in-flight call per stage is tracked\n * under the slice (the common shape); pass a distinct key to fan out. PURE —\n * delegates straight to resilient-call's gate.\n */\n function attempt(\n s: State,\n input: LlmCall<P>,\n at: number,\n key: string = input.purpose,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.attempt(s, key, input, at);\n }\n\n /** Record a parsed success for `key`. PURE — resilient-call's `succeed`. */\n function succeed(\n s: State,\n key: string,\n msg: LlmSucceedMsg<P, O>,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.succeed(s, key, msg);\n }\n\n /**\n * Record a failure for `key`: back off via the inherited retry, or settle\n * `failed`. PURE — resilient-call's `fail`. The `error` is the typed\n * `LlmErr`, carried on the call's `failed` phase for the consumer to read.\n */\n function fail(\n s: State,\n key: string,\n msg: LlmFailMsg<P>,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.fail(s, key, msg);\n }\n\n /** A retry / deadline timer fired. PURE — resilient-call's `onTimer`. */\n function onTimer(\n s: State,\n msg: LlmTimerMsg,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.onTimer(s, msg);\n }\n\n /** Pre-wired subs — resilient-call's retry-timer subscriptions. */\n function subs(s: State): readonly DeadlineSub[] {\n return rc.subs(s);\n }\n\n // ---- The model-invoke port the resilient handler drives ----------------\n\n /**\n * Run ONE LLM call: assemble messages via the loader, bind the purpose's\n * structured-output schema, invoke, and parse. Returns the typed `LlmOk` on\n * success; throws on any failure (model throw OR schema parse) so the\n * resilient-call handler routes it to a failure Msg + backoff. This is the\n * single port resilient-call wraps — the retry / timeout machinery lives in\n * resilient-call, never here (\"do NOT reinvent backoff\").\n *\n * The seed's two paths collapse to one: every brain-only purpose goes through\n * `withStructuredOutput(schema)`. The `parse` re-validates the output (a fake\n * model might skip validation, and a real provider can drift) so a malformed\n * structured response is a failure, not a corrupt success.\n */\n async function invokeOne(input: LlmCall<P>): Promise<LlmOk<P, O>> {\n const model = config.model(input.model);\n const messages = config.loadMessages\n ? await config.loadMessages(input)\n : [];\n const schema = config.schemas[input.purpose];\n const structured = model.withStructuredOutput(schema);\n const raw = await structured.invoke(messages);\n // Re-validate at the boundary — a parse throw becomes a `resilient_err`\n // (enriched to `LlmErr`) settle Msg upstream, never a corrupt success.\n const output = schema.parse(raw);\n return { key: input.purpose, purpose: input.purpose, output };\n }\n\n // ---- Handlers — the one new piece, composed onto resilient-call --------\n\n /**\n * Pre-wired interpret handler for `resilient_run`. Composes\n * `../resilient-call`'s `resilient_run` handler over the `invokeOne` port and\n * RETURNS the resilient settle Msg — it does NOT dispatch the consumer's Msg\n * itself. Returning the settle Msg is the whole point: the substrate enqueues\n * an interpret handler's returned Msg as a FOLLOW-UP (re-entry) onto the\n * dispatch tail, so the host reducer's `resilient_ok` / `resilient_err` arms\n * run `succeed` / `fail`, which advances the inherited retry loop\n * (succeed/fail → backoff → onTimer → re-issue). A handler that instead\n * `dispatch`ed an enriched Msg directly would settle ONE invoke and bypass\n * that loop entirely — the slice would never leave `running`, the breaker\n * would never trip/close, and the retry counter would never reset.\n *\n * Resilience is composed, not reimplemented: the body delegates to the\n * resilient-call handler's Railway-routed invoke (Ok → `resilient_ok`, Err →\n * `resilient_err`), so a transient model failure backs off via the inherited\n * retry exactly as resilient-call decides. The ONE enrichment llm-call adds is\n * mapping the settle PAYLOADS into its typed variants:\n *\n * - success → `result` is already the parsed `LlmOk` (`invokeOne` returns\n * it), so the resilient `resilient_ok` Msg is the llm-call `LlmSucceedMsg`\n * as-is.\n * - failure → the resilient `resilient_err` carries the RAW throw (a model\n * 503 or a `schema.parse` throw). We rebuild it into the typed `LlmErr`\n * (purpose + reason + raw) so the host reducer routes per-stage.\n *\n * The host folds the enriched payload into its own state in the reducer arm\n * (the doc-comment wiring), NOT here — the handler stays the one impurity\n * (the `invokeOne` await + the `Date.now()` stamp the resilient handler makes)\n * and the reducer stays pure.\n */\n // The resilient-call `resilient_run` handler over `invokeOne` — the shared\n // Railway-routed invoke (Ok → resilient_ok Msg, Err → resilient_err Msg). Both\n // handler forms below build on it; the settle shape is composed, not reinvented.\n const resilientRunHandler = rc.handlers({\n run: (input) => invokeOne(input),\n }).resilient_run;\n\n /**\n * Await one composed invoke and resolve to the ENRICHED resilient settle Msg\n * (`resilient_ok` carrying the parsed `LlmOk`, or `resilient_err` carrying the\n * typed `LlmErr`). The shared core both handler forms reuse.\n */\n async function settleOf(\n cmd: LlmRunCmd<P>,\n ): Promise<LlmSucceedMsg<P, O> | LlmFailMsg<P>> {\n // `resilientRunHandler` never rejects (tryInterpret contract) — it resolves\n // to a `resilient_ok` / `resilient_err` settle Msg stamped with `Date.now()`.\n // Its `ctx` slot is `NoCtx` (the resilient-call work fn reads no ctx — a\n // DELIBERATE context-free seam, not accidental `unknown`), so the empty\n // record satisfies it without a cast.\n const settle = await resilientRunHandler(cmd, {});\n if (settle.type === MsgType.ResilientOk) {\n // `result` is the parsed `LlmOk` from `invokeOne`, which hardcoded its\n // `key` to `purpose` (the port receives no key). Re-key it to `cmd.key` —\n // the fan-out key `attempt` threaded in — so a fanned-out success carries\n // the SAME key its failure would (`settleOf`'s err path below uses\n // `cmd.key`), never the collapsed purpose. `settle.key` is already\n // `cmd.key`; this aligns the enriched `LlmOk.key` inside it.\n return { ...settle, result: { ...settle.result, key: cmd.key } };\n }\n // resilient_err — enrich the raw throw into the typed `LlmErr`, keeping the\n // resilient Msg's `key` / `at` so the host reducer's `fail` verb re-issues\n // from the remembered input and backs off on schedule.\n const rawError: unknown = settle.error;\n const err: LlmErr<P> = {\n key: cmd.key,\n purpose: cmd.input.purpose,\n reason: describeError(rawError),\n error: rawError,\n };\n return {\n type: MsgType.ResilientErr,\n key: settle.key,\n error: err,\n at: settle.at,\n };\n }\n\n /**\n * Pre-wired interpret handler for `resilient_run`. TWO forms:\n *\n * - `handlers()` (no args, the FIXED primary) — RETURNS the enriched\n * resilient settle Msg. The substrate enqueues an interpret handler's\n * returned Msg as a FOLLOW-UP (re-entry) onto the dispatch tail, so the\n * host reducer's `resilient_ok` / `resilient_err` arms run `succeed` /\n * `fail`, advancing the inherited retry loop (succeed/fail → backoff →\n * onTimer → re-issue). This is the correct wiring; the module doc shows it.\n *\n * - `handlers(ports)` (LEGACY, detached) — runs the invoke inside\n * `ctx.waitUntil` and dispatches the consumer's `onOk` / `onErr` Msg\n * directly. KEPT only for `../agent`, which still inherits this shape. It\n * does NOT drive the retry loop (it never re-enters the settle Msg, so\n * `succeed` / `fail` never run) — `../agent` carries the same gap and is\n * fixed in its own pass. New consumers MUST use the no-arg form.\n *\n * Resilience is composed, not reimplemented: both forms delegate to the\n * resilient-call handler's Railway-routed invoke and add only the typed\n * `LlmOk` / `LlmErr` enrichment. The ONE impurity is the `invokeOne` await +\n * the `Date.now()` stamp the resilient handler makes; the reducer stays pure.\n */\n function handlers(): {\n resilient_run: (\n cmd: LlmRunCmd<P>,\n ) => Promise<LlmSucceedMsg<P, O> | LlmFailMsg<P>>;\n };\n function handlers<M>(ports: LlmCallPorts<P, O, M>): {\n resilient_run: (\n cmd: LlmRunCmd<P>,\n ctx: { waitUntil(p: Promise<unknown>): void; dispatch(msg: M): unknown },\n ) => void;\n };\n function handlers<M>(ports?: LlmCallPorts<P, O, M>) {\n if (ports === undefined) {\n // FIXED primary: return the settle Msg for re-entry — drives the loop.\n return { resilient_run: (cmd: LlmRunCmd<P>) => settleOf(cmd) };\n }\n // LEGACY detached form — kept for `../agent` (see doc above). Dispatches the\n // consumer's Msg directly; does not re-enter the settle Msg.\n return {\n resilient_run: (\n cmd: LlmRunCmd<P>,\n ctx: {\n waitUntil(p: Promise<unknown>): void;\n dispatch(msg: M): unknown;\n },\n ): void => {\n const fired = (async () => {\n const settle = await settleOf(cmd);\n if (settle.type === MsgType.ResilientOk) {\n const msg = ports.onOk(settle.result);\n if (msg !== undefined) await ctx.dispatch(msg);\n return;\n }\n const msg = ports.onErr(settle.error);\n if (msg !== undefined) await ctx.dispatch(msg);\n })();\n ctx.waitUntil(fired);\n },\n };\n }\n\n return { init, attempt, succeed, fail, onTimer, subs, handlers, invokeOne };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the\n * slice lives at `state.resilience` — the same convenience `../resilient-call`\n * ships, re-typed for the llm-call slice so consumers wire one import. Pure.\n */\nexport function liftLlmCall<\n S extends { resilience: ResilientState<LlmCall<P>, LlmOk<P, O>> },\n P extends string,\n O extends Record<P, unknown>,\n C extends Cmd,\n>(\n state: S,\n result: readonly [ResilientState<LlmCall<P>, LlmOk<P, O>>, readonly C[]],\n): readonly [S, readonly C[]] {\n return liftResilience(state, result);\n}\n\n/**\n * Re-export the deadline Sub primitives (inherited from resilient-call) so\n * consumers wire one import: `subscribeDeadline` is the `subscribe` cell,\n * `deadlineSub` builds the Sub literal `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub, DeadlineExceeded, ResilientState };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/llm-call/index.ts"],"names":["msg"],"mappings":"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","file":"chunk-ERYY5P6W.js","sourcesContent":["/**\n * @packageDocumentation\n * @demlik/tea/llm-call — `resilient-call` + structured-output parse + a typed\n * failure variant, around a purpose-discriminated LLM invocation.\n *\n * This is the ~60-line `call_llm` handler from the audit-agent seed\n * (`interpret.ts` / `effects.ts`) collapsed into the uniform L2 knob: a config\n * object, the resilient-call slice it inherits, and one interpret handler that\n * assembles messages, binds the structured-output schema for the call's\n * `purpose`, invokes the model (retry composed from `../resilient-call`, NOT\n * reinvented), parses the output, and RETURNS the enriched resilient settle Msg\n * (`resilient_ok` / `resilient_err`) so it re-enters the host reducer and drives\n * the inherited succeed/fail → backoff → onTimer loop.\n *\n * ## What it adds over `../resilient-call`\n *\n * - **Two DI ports** (the seed's two seams):\n * 1. `model: (modelId) => LLM` — the model factory. Tests pass a fake so\n * the handler runs end-to-end without touching a real provider; the\n * production worker wires `createChatModel(env, …)`.\n * 2. `loadMessages: Loader` — the SDK / message loader. The seed lazy-\n * imports `@langchain/core/messages` (a top-level `import type` blows\n * up the workers test runner); the loader keeps that import lazy and\n * stubbable. It produces the `BaseMessage[]` for a given call.\n * - **Structured-output parse** — each `purpose` maps to a `Schema` in\n * `config.schemas`. The handler binds `model.withStructuredOutput(schema)`\n * so the invoke resolves to a typed object; a `Schema.parse` that throws\n * becomes a `resilient_err` carrying an `LlmErr` (a parse failure is a\n * failure, not a stall — \"errors are data\"). `withStructuredOutput` is the\n * seed's brain-only path; the fake model in tests returns the typed object\n * directly.\n * - **A typed failure variant** — every failure (model throw, timeout-via-\n * retry-exhaustion, schema parse) is surfaced as the `LlmErr` carried on the\n * enriched `resilient_err` settle Msg, tagged with the `purpose` so the\n * consumer's reducer routes per-stage. The seed dispatched\n * `llm_failed{purpose}`; the consumer rebuilds that shape in its reducer arm\n * from `m.error: LlmErr`.\n *\n * ## Inheriting resilient-call by composition (not reinvention)\n *\n * The retry / backoff lives in `../resilient-call`: `createLlmCall` builds a\n * `createResilientCall` knob over the model-invoke port and DELEGATES `init`,\n * `attempt`, `succeed`, `fail`, `onTimer`, `subs` to it verbatim — the slice is\n * literally resilient-call's slice (the spec's \"retry lives here\"). There is no\n * second backoff implementation here. The handler runs the invoke through the\n * resilient-call handler so a transient model failure backs off exactly as a\n * resilient HTTP call would; the `purpose`-branch + structured parse wrap the\n * port the resilient handler drives.\n *\n * ## The two non-negotiables (canon, inherited)\n *\n * - **Durable** — the slice is resilient-call's plain-data slice (a Model\n * field). Input carried on the `resilient_run` Cmd is the plain\n * `LlmCall` request (purpose + modelId + payload) — no closures.\n * - **Replayable** — every transition is a resilient-call verb; the handler\n * is the only impurity, and the one clock read is `Date.now()` at the\n * effect boundary (inside the handler), exactly as the seed stamped its\n * `llm_responded` / `llm_failed` Msgs.\n *\n * ## Typical wiring\n *\n * const llm = createLlmCall<MyPurpose, MyOutputs>({\n * model: (id) => createChatModel(env, id),\n * schemas: { plan: planSchema, report: reportSchema },\n * retry: defaultRetryPolicy,\n * loadMessages: defaultMessagesLoader,\n * });\n *\n * // in the machine:\n * init: () => [{ resilience: llm.init() }, []],\n * update: {\n * call_llm: (s, m) => lift(s, llm.attempt(s.resilience, m.input, m.at)),\n * // `resilient_ok` / `resilient_err` RE-ENTER from `handlers` (the settle\n * // Msg the interpret handler returns). Run the inherited verb FIRST so the\n * // succeed/fail → backoff → onTimer loop advances, THEN fold the enriched\n * // payload (`m.result: LlmOk` / `m.error: LlmErr`) into the host's own\n * // state — the enrichment lives in the reducer arm, not the handler.\n * resilient_ok: (s, m) => {\n * const [slice, cmds] = llm.succeed(s.resilience, m.key, m);\n * return [{ ...s, resilience: slice, output: m.result.output }, cmds];\n * },\n * resilient_err: (s, m) => {\n * const [slice, cmds] = llm.fail(s.resilience, m.key, m);\n * return [{ ...s, resilience: slice, failure: m.error }, cmds];\n * },\n * deadline_exceeded: (s, m) => lift(s, llm.onTimer(s.resilience, m)),\n * },\n * subscriptions: (s) => llm.subs(s.resilience),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: llm.handlers(),\n */\n\nimport { describeError } from \"../describe-error\";\nimport type { Cmd } from \"../index\";\nimport { MsgType } from \"../protocol\";\nimport {\n createResilientCall,\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n type FailMsg,\n liftResilience,\n type ResilientConfig,\n type ResilientState,\n type RunCmd,\n type SucceedMsg,\n subscribeDeadline,\n} from \"../resilient-call\";\nimport type { RetryPolicy } from \"../retry-backoff\";\n\n// ===========================================================================\n// The DI port surfaces — model factory + structured output + message loader.\n// ===========================================================================\n\n/**\n * The minimal structured-output schema contract: `parse(unknown) => T`, the\n * zod-style call the handler uses to validate the model's output before it\n * settles `resilient_ok`. A throwing `parse` (the zod contract on invalid\n * input) propagates out of `invokeOne` and is caught by the resilient handler,\n * which settles `resilient_err` (enriched to `LlmErr`) — a parse failure is a\n * failure, never a silent pass. Structural so a real `z.ZodType<T>` satisfies\n * it without an import.\n */\nexport interface Schema<T> {\n /** Validate + narrow `value` to `T`, or throw on mismatch (the zod contract). */\n parse(value: unknown): T;\n}\n\n/**\n * The minimal chat-model contract every model the handler talks to must\n * satisfy — the seed's `InjectableChatModel`, trimmed to the one operation\n * llm-call drives for brain-only stages:\n *\n * `withStructuredOutput(schema)` → a runnable whose `invoke(messages)`\n * resolves to a typed object matching `schema`.\n *\n * `BaseChatModel` from `@langchain/core` is the runtime type; this surface\n * names only what llm-call calls so a test fake can ignore the rest. Generic\n * over the message shape `Msg` so a consumer's loader and model agree on it\n * without llm-call inspecting messages.\n */\nexport interface Llm<Msg> {\n withStructuredOutput<T>(schema: Schema<T>): {\n invoke(messages: readonly Msg[]): Promise<T>;\n };\n}\n\n/**\n * Build the `Msg[]` the handler hands to the bound model for a given call. The\n * SDK / message-assembly seam — the seed's `buildBaseMessages` + the lazy\n * `loadMessages` loader rolled into one injected port. Async because the seed\n * lazy-imports the SDK (a top-level `import type` of the langchain messages\n * package explodes the workers test runner). Receives the full `LlmCall` so it\n * can branch on `purpose` exactly as the seed's `buildBaseMessages` did.\n */\nexport type MessageLoader<P extends string, Msg> = (\n call: LlmCall<P>,\n) => Promise<readonly Msg[]>;\n\n/**\n * The model factory — the first DI port. `(modelId) => Llm`. Tests pass a fake\n * builder; production wires `createChatModel(env, getModelConfig(id))`. `null`\n * means \"the host's default model\" (the seed's `string | null`).\n */\nexport type ModelFactory<Msg> = (modelId: string | null) => Llm<Msg>;\n\n// ===========================================================================\n// Config — the knob. `model` + `schemas` required (the two DI ports + the\n// per-purpose parse target); `retry` + `loadMessages` optional.\n// ===========================================================================\n\n/**\n * The llm-call knob. `model` and `schemas` are the load-bearing pair — the\n * model factory DI port and the per-purpose structured-output targets; the\n * rest is optional, exactly the resilient-call \"omit a brick → omit its gate\"\n * story for `retry`:\n *\n * - `model` — DI port 1: the model factory.\n * - `schemas` — one `Schema` per `Purpose`. The handler binds\n * `schemas[call.purpose]` as the structured-output target.\n * - `retry` — backoff policy, composed straight into `../resilient-\n * call`. Omit it and a model failure is terminal (no\n * backoff), exactly as resilient-call with no retry brick.\n * - `loadMessages` — DI port 2: the SDK / message loader. Omit it and the\n * handler invokes the bound model with `[]` (a degenerate\n * but valid call) — provide it to assemble real messages.\n *\n * `P` is the purpose union (e.g. `\"plan\" | \"report\"`); `O` maps each purpose to\n * its parsed output type; `Msg` is the model's message shape (threaded through\n * the loader + model so llm-call never inspects a message).\n */\nexport interface LlmCallConfig<\n P extends string,\n O extends Record<P, unknown>,\n Msg = unknown,\n> {\n /** DI port 1 — the model factory. `(modelId) => Llm`. */\n readonly model: ModelFactory<Msg>;\n /** One structured-output schema per purpose; the parse target the handler binds. */\n readonly schemas: { readonly [K in P]: Schema<O[K]> };\n /** Backoff policy, composed into `../resilient-call`. Omit → no backoff. */\n readonly retry?: RetryPolicy;\n /** DI port 2 — the SDK / message loader. Omit → the handler invokes with `[]`. */\n readonly loadMessages?: MessageLoader<P, Msg>;\n}\n\n// ===========================================================================\n// The request / response shapes the knob speaks.\n// ===========================================================================\n\n/**\n * One LLM call request — the resilient-call `input` for this knob, carried on\n * the `resilient_run` Cmd as plain data (no closures, invariant 3). Mirrors the\n * seed's purpose-discriminated `CallLlmCmd`: a `purpose` selecting the schema +\n * prompt assembly, the `model` id, and the opaque per-purpose `payload` the\n * loader consumes.\n *\n * `key` defaults to `purpose` when the consumer calls `attempt(s, purpose, …)`,\n * so one in-flight call per purpose is tracked under the resilient-call slice —\n * the common single-call-per-stage shape. A consumer that fans out many calls\n * of one purpose passes a distinct `key`.\n */\nexport interface LlmCall<P extends string> {\n /** The stage / schema selector — drives both `schemas[purpose]` and message assembly. */\n readonly purpose: P;\n /** The model id to invoke; `null` = the host's default model. */\n readonly model: string | null;\n /** The per-purpose prompt payload the `MessageLoader` consumes. Opaque to the knob. */\n readonly payload: unknown;\n}\n\n/** The parsed, typed success carried on the `resilient_ok` settle Msg, tagged with its purpose. */\nexport interface LlmOk<P extends string, O extends Record<P, unknown>> {\n readonly key: string;\n readonly purpose: P;\n readonly output: O[P];\n}\n\n/** The typed failure variant — every failure path surfaces this, tagged by purpose. */\nexport interface LlmErr<P extends string> {\n readonly key: string;\n readonly purpose: P;\n /** A human-readable cause (model throw, retry-exhaustion, or schema parse). */\n readonly reason: string;\n /** The original error, carried untouched for the consumer to inspect. */\n readonly error: unknown;\n}\n\n/**\n * The settle Msgs llm-call's handler RETURNS from `interpret` so the substrate\n * enqueues them as follow-up Msgs (re-entry) into the host reducer — exactly as\n * `../resilient-call` does. They are the resilient-call settle Msgs with the\n * payloads ENRICHED to llm-call's typed variants: `result` is the parsed,\n * purpose-tagged `LlmOk`; `error` is the typed `LlmErr` (purpose + reason +\n * raw). The host wires `resilient_ok` → `succeed` and `resilient_err` → `fail`\n * reducer arms (the doc-comment wiring), and folds the enriched payload into its\n * own state there — llm-call never forces a Msg vocabulary on the host, and the\n * settle Msg drives the inherited succeed/fail → backoff → onTimer loop instead\n * of bypassing it.\n */\n//\n// The success Msg is `../resilient-call`'s `SucceedMsg` specialized to the parsed\n// `LlmOk` result — NOT a re-declared shape — so the verb returns (`succeed`'s\n// argument, the handler's resolve) thread through with no cast. The failure Msg\n// is the resilient `FailMsg` with its `error: unknown` NARROWED to the typed\n// `LlmErr`: every value the handler enriches is an `LlmErr`, and `LlmErr` is\n// assignable to `unknown`, so `LlmFailMsg` flows into `rc.fail` (which takes the\n// wide `FailMsg`) directly while the host reducer reads the narrow `error` type.\nexport type LlmSucceedMsg<\n P extends string,\n O extends Record<P, unknown>,\n> = SucceedMsg<LlmOk<P, O>>;\nexport type LlmFailMsg<P extends string> = Omit<FailMsg, \"error\"> & {\n readonly error: LlmErr<P>;\n};\n\n/** The retry / deadline timer Msg — `DeadlineExceeded`, inherited from resilient-call. */\nexport type LlmTimerMsg = DeadlineExceeded;\n\n/**\n * The ports the LEGACY detached `handlers(ports)` form takes. Two outbound Msg\n * builders the handler hands the typed Ok / Err; the consumer returns its own\n * Msg (or `undefined`). KEPT only for `../agent`, which still inherits the\n * detached shape; that path does NOT drive the retry loop (it dispatches the\n * consumer's Msg directly and never re-enters the resilient settle Msg). New\n * consumers use the no-arg `handlers()` form, which RETURNS the settle Msg for\n * re-entry and drives the inherited succeed/fail → backoff → onTimer loop.\n * `../agent` carries the same retry-loop gap and is fixed separately.\n *\n * The builders return `M | undefined` — `undefined` is the \"dispatch nothing\"\n * sentinel the detached form checks (`!== undefined`), spelled as `undefined`\n * rather than `void` so the union is unambiguous (a `void` member of a union is\n * confusing and reads as \"may return anything\").\n */\nexport interface LlmCallPorts<\n P extends string,\n O extends Record<P, unknown>,\n M,\n> {\n /** Build the Msg dispatched on a parsed success. Return `undefined` to dispatch nothing. */\n readonly onOk: (ok: LlmOk<P, O>) => M | undefined;\n /** Build the Msg dispatched on any failure (throw / retry-exhausted / parse). */\n readonly onErr: (err: LlmErr<P>) => M | undefined;\n}\n\n/**\n * The effect Cmd the knob emits: run the LLM call for `key` with `input`. The\n * `input` is the plain `LlmCall` request — the handler reads `purpose` /\n * `model` / `payload` off it (invariant 3: Cmds are data).\n *\n * It is `../resilient-call`'s `RunCmd` specialized to the `LlmCall` input — NOT a\n * re-declared shape — so the resilient verbs' return tuples (`[State, RunCmd[]]`)\n * are this exact type and thread through `attempt` / `succeed` / `fail` /\n * `onTimer` with no cast.\n */\nexport type LlmRunCmd<P extends string> = RunCmd<LlmCall<P>>;\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build an llm-call knob from `config`. `rng` is injected for the inherited\n * retry jitter (pass a fixed `() => 0.5` in tests to pin backoff; defaults to\n * `Math.random`, read only at the resilient-call verb boundary).\n *\n * Returns the uniform L2 knob contract. The slice + verbs (`init`, `attempt`,\n * `succeed`, `fail`, `onTimer`, `subs`) are DELEGATED to a `../resilient-call`\n * knob built over the structured-output model-invoke port — no second backoff\n * here. `handlers` is the one new piece: the purpose-branching, schema-parsing\n * wrapper that returns the enriched resilient settle Msg for re-entry.\n *\n * `P` is the purpose union, `O` the purpose→output map, `Msg` the model's\n * message shape.\n */\nexport function createLlmCall<\n P extends string,\n O extends Record<P, unknown>,\n Msg = unknown,\n>(config: LlmCallConfig<P, O, Msg>, rng: () => number = Math.random) {\n // ---- The composed resilient-call knob ----------------------------------\n //\n // The resilient-call input is the full `LlmCall` request; its result is the\n // PARSED, typed `LlmOk`. Only the `retry` brick is forwarded — llm-call does\n // not expose circuit / rate-limit / cache / deadline knobs (a stage call is\n // a single brain invocation, not a keyed downstream target). Omitting those\n // bricks omits their gates, exactly as resilient-call documents.\n const resilientConfig: ResilientConfig = {\n ...(config.retry === undefined ? {} : { retry: config.retry }),\n };\n const rc = createResilientCall<LlmCall<P>, LlmOk<P, O>>(resilientConfig, rng);\n\n /** The slice this knob owns — resilient-call's slice verbatim. */\n type State = ResilientState<LlmCall<P>, LlmOk<P, O>>;\n\n /** The starting slice — resilient-call's. */\n function init(): State {\n return rc.init();\n }\n\n /**\n * Start (or restart) an LLM call. `keyOrPurpose` defaults the resilient-call\n * `key` to the call's `purpose` so one in-flight call per stage is tracked\n * under the slice (the common shape); pass a distinct key to fan out. PURE —\n * delegates straight to resilient-call's gate.\n */\n function attempt(\n s: State,\n input: LlmCall<P>,\n at: number,\n key: string = input.purpose,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.attempt(s, key, input, at);\n }\n\n /** Record a parsed success for `key`. PURE — resilient-call's `succeed`. */\n function succeed(\n s: State,\n key: string,\n msg: LlmSucceedMsg<P, O>,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.succeed(s, key, msg);\n }\n\n /**\n * Record a failure for `key`: back off via the inherited retry, or settle\n * `failed`. PURE — resilient-call's `fail`. The `error` is the typed\n * `LlmErr`, carried on the call's `failed` phase for the consumer to read.\n */\n function fail(\n s: State,\n key: string,\n msg: LlmFailMsg<P>,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.fail(s, key, msg);\n }\n\n /** A retry / deadline timer fired. PURE — resilient-call's `onTimer`. */\n function onTimer(\n s: State,\n msg: LlmTimerMsg,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.onTimer(s, msg);\n }\n\n /** Pre-wired subs — resilient-call's retry-timer subscriptions. */\n function subs(s: State): readonly DeadlineSub[] {\n return rc.subs(s);\n }\n\n // ---- The model-invoke port the resilient handler drives ----------------\n\n /**\n * Run ONE LLM call: assemble messages via the loader, bind the purpose's\n * structured-output schema, invoke, and parse. Returns the typed `LlmOk` on\n * success; throws on any failure (model throw OR schema parse) so the\n * resilient-call handler routes it to a failure Msg + backoff. This is the\n * single port resilient-call wraps — the retry / timeout machinery lives in\n * resilient-call, never here (\"do NOT reinvent backoff\").\n *\n * The seed's two paths collapse to one: every brain-only purpose goes through\n * `withStructuredOutput(schema)`. The `parse` re-validates the output (a fake\n * model might skip validation, and a real provider can drift) so a malformed\n * structured response is a failure, not a corrupt success.\n */\n async function invokeOne(input: LlmCall<P>): Promise<LlmOk<P, O>> {\n const model = config.model(input.model);\n const messages = config.loadMessages\n ? await config.loadMessages(input)\n : [];\n const schema = config.schemas[input.purpose];\n const structured = model.withStructuredOutput(schema);\n const raw = await structured.invoke(messages);\n // Re-validate at the boundary — a parse throw becomes a `resilient_err`\n // (enriched to `LlmErr`) settle Msg upstream, never a corrupt success.\n const output = schema.parse(raw);\n return { key: input.purpose, purpose: input.purpose, output };\n }\n\n // ---- Handlers — the one new piece, composed onto resilient-call --------\n\n /**\n * Pre-wired interpret handler for `resilient_run`. Composes\n * `../resilient-call`'s `resilient_run` handler over the `invokeOne` port and\n * RETURNS the resilient settle Msg — it does NOT dispatch the consumer's Msg\n * itself. Returning the settle Msg is the whole point: the substrate enqueues\n * an interpret handler's returned Msg as a FOLLOW-UP (re-entry) onto the\n * dispatch tail, so the host reducer's `resilient_ok` / `resilient_err` arms\n * run `succeed` / `fail`, which advances the inherited retry loop\n * (succeed/fail → backoff → onTimer → re-issue). A handler that instead\n * `dispatch`ed an enriched Msg directly would settle ONE invoke and bypass\n * that loop entirely — the slice would never leave `running`, the breaker\n * would never trip/close, and the retry counter would never reset.\n *\n * Resilience is composed, not reimplemented: the body delegates to the\n * resilient-call handler's Railway-routed invoke (Ok → `resilient_ok`, Err →\n * `resilient_err`), so a transient model failure backs off via the inherited\n * retry exactly as resilient-call decides. The ONE enrichment llm-call adds is\n * mapping the settle PAYLOADS into its typed variants:\n *\n * - success → `result` is already the parsed `LlmOk` (`invokeOne` returns\n * it), so the resilient `resilient_ok` Msg is the llm-call `LlmSucceedMsg`\n * as-is.\n * - failure → the resilient `resilient_err` carries the RAW throw (a model\n * 503 or a `schema.parse` throw). We rebuild it into the typed `LlmErr`\n * (purpose + reason + raw) so the host reducer routes per-stage.\n *\n * The host folds the enriched payload into its own state in the reducer arm\n * (the doc-comment wiring), NOT here — the handler stays the one impurity\n * (the `invokeOne` await + the `Date.now()` stamp the resilient handler makes)\n * and the reducer stays pure.\n */\n // The resilient-call `resilient_run` handler over `invokeOne` — the shared\n // Railway-routed invoke (Ok → resilient_ok Msg, Err → resilient_err Msg). Both\n // handler forms below build on it; the settle shape is composed, not reinvented.\n const resilientRunHandler = rc.handlers({\n run: (input) => invokeOne(input),\n }).resilient_run;\n\n /**\n * Await one composed invoke and resolve to the ENRICHED resilient settle Msg\n * (`resilient_ok` carrying the parsed `LlmOk`, or `resilient_err` carrying the\n * typed `LlmErr`). The shared core both handler forms reuse.\n */\n async function settleOf(\n cmd: LlmRunCmd<P>,\n ): Promise<LlmSucceedMsg<P, O> | LlmFailMsg<P>> {\n // `resilientRunHandler` never rejects (tryInterpret contract) — it resolves\n // to a `resilient_ok` / `resilient_err` settle Msg stamped with `Date.now()`.\n // Its `ctx` slot is `NoCtx` (the resilient-call work fn reads no ctx — a\n // DELIBERATE context-free seam, not accidental `unknown`), so the empty\n // record satisfies it without a cast.\n const settle = await resilientRunHandler(cmd, {});\n if (settle.type === MsgType.ResilientOk) {\n // `result` is the parsed `LlmOk` from `invokeOne`, which hardcoded its\n // `key` to `purpose` (the port receives no key). Re-key it to `cmd.key` —\n // the fan-out key `attempt` threaded in — so a fanned-out success carries\n // the SAME key its failure would (`settleOf`'s err path below uses\n // `cmd.key`), never the collapsed purpose. `settle.key` is already\n // `cmd.key`; this aligns the enriched `LlmOk.key` inside it.\n return { ...settle, result: { ...settle.result, key: cmd.key } };\n }\n // resilient_err — enrich the raw throw into the typed `LlmErr`, keeping the\n // resilient Msg's `key` / `at` so the host reducer's `fail` verb re-issues\n // from the remembered input and backs off on schedule.\n const rawError: unknown = settle.error;\n const err: LlmErr<P> = {\n key: cmd.key,\n purpose: cmd.input.purpose,\n reason: describeError(rawError),\n error: rawError,\n };\n return {\n type: MsgType.ResilientErr,\n key: settle.key,\n error: err,\n at: settle.at,\n };\n }\n\n /**\n * Pre-wired interpret handler for `resilient_run`. TWO forms:\n *\n * - `handlers()` (no args, the FIXED primary) — RETURNS the enriched\n * resilient settle Msg. The substrate enqueues an interpret handler's\n * returned Msg as a FOLLOW-UP (re-entry) onto the dispatch tail, so the\n * host reducer's `resilient_ok` / `resilient_err` arms run `succeed` /\n * `fail`, advancing the inherited retry loop (succeed/fail → backoff →\n * onTimer → re-issue). This is the correct wiring; the module doc shows it.\n *\n * - `handlers(ports)` (LEGACY, detached) — runs the invoke inside\n * `ctx.waitUntil` and dispatches the consumer's `onOk` / `onErr` Msg\n * directly. KEPT only for `../agent`, which still inherits this shape. It\n * does NOT drive the retry loop (it never re-enters the settle Msg, so\n * `succeed` / `fail` never run) — `../agent` carries the same gap and is\n * fixed in its own pass. New consumers MUST use the no-arg form.\n *\n * Resilience is composed, not reimplemented: both forms delegate to the\n * resilient-call handler's Railway-routed invoke and add only the typed\n * `LlmOk` / `LlmErr` enrichment. The ONE impurity is the `invokeOne` await +\n * the `Date.now()` stamp the resilient handler makes; the reducer stays pure.\n */\n function handlers(): {\n resilient_run: (\n cmd: LlmRunCmd<P>,\n ) => Promise<LlmSucceedMsg<P, O> | LlmFailMsg<P>>;\n };\n function handlers<M>(ports: LlmCallPorts<P, O, M>): {\n resilient_run: (\n cmd: LlmRunCmd<P>,\n ctx: { waitUntil(p: Promise<unknown>): void; dispatch(msg: M): unknown },\n ) => void;\n };\n function handlers<M>(ports?: LlmCallPorts<P, O, M>) {\n if (ports === undefined) {\n // FIXED primary: return the settle Msg for re-entry — drives the loop.\n return { resilient_run: (cmd: LlmRunCmd<P>) => settleOf(cmd) };\n }\n // LEGACY detached form — kept for `../agent` (see doc above). Dispatches the\n // consumer's Msg directly; does not re-enter the settle Msg.\n return {\n resilient_run: (\n cmd: LlmRunCmd<P>,\n ctx: {\n waitUntil(p: Promise<unknown>): void;\n dispatch(msg: M): unknown;\n },\n ): void => {\n const fired = (async () => {\n const settle = await settleOf(cmd);\n if (settle.type === MsgType.ResilientOk) {\n const msg = ports.onOk(settle.result);\n if (msg !== undefined) await ctx.dispatch(msg);\n return;\n }\n const msg = ports.onErr(settle.error);\n if (msg !== undefined) await ctx.dispatch(msg);\n })();\n ctx.waitUntil(fired);\n },\n };\n }\n\n return { init, attempt, succeed, fail, onTimer, subs, handlers, invokeOne };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the\n * slice lives at `state.resilience` — the same convenience `../resilient-call`\n * ships, re-typed for the llm-call slice so consumers wire one import. Pure.\n */\nexport function liftLlmCall<\n S extends { resilience: ResilientState<LlmCall<P>, LlmOk<P, O>> },\n P extends string,\n O extends Record<P, unknown>,\n C extends Cmd,\n>(\n state: S,\n result: readonly [ResilientState<LlmCall<P>, LlmOk<P, O>>, readonly C[]],\n): readonly [S, readonly C[]] {\n return liftResilience(state, result);\n}\n\n/**\n * Re-export the deadline Sub primitives (inherited from resilient-call) so\n * consumers wire one import: `subscribeDeadline` is the `subscribe` cell,\n * `deadlineSub` builds the Sub literal `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub, DeadlineExceeded, ResilientState };\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describeError } from './chunk-G5E74IJB.js';
|
|
2
|
-
import { historyTracker } from './chunk-
|
|
2
|
+
import { historyTracker } from './chunk-ZH454CR5.js';
|
|
3
3
|
|
|
4
4
|
// src/extension/bridge.ts
|
|
5
5
|
function channelTypes(channel) {
|
|
@@ -180,5 +180,5 @@ function bridgeTabClient({
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export { bridgeClient, bridgeRuntime, bridgeTabClient, passThroughMsg };
|
|
183
|
-
//# sourceMappingURL=chunk-
|
|
184
|
-
//# sourceMappingURL=chunk-
|
|
183
|
+
//# sourceMappingURL=chunk-IVHQCVFX.js.map
|
|
184
|
+
//# sourceMappingURL=chunk-IVHQCVFX.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/extension/bridge.ts"],"names":[],"mappings":"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","file":"chunk-CIDS43FH.js","sourcesContent":["/**\n * Runtime bridge — expose a `@demlik/tea` runtime running in one chrome context\n * (typically the background service worker) to consumers in other contexts\n * (sidepanel, popup, devtools panel) over `chrome.runtime.sendMessage` /\n * `chrome.runtime.onMessage`.\n *\n * Two halves, one channel name string.\n *\n * ┌──── background (host) ────┐ ┌──── react surface ────┐\n * │ bridgeRuntime(rt, {ch}) │ chrome.* │ useBackgroundRuntime │\n * │ ├ observe → broadcast │ ──────────▶ │ ├ hydrate (initial) │\n * │ └ onMessage(\":dispatch\") │ ◀────────── │ └ dispatch (msgs) │\n * │ └ onMessage(\":hydrate\") │ │ │\n * └────────────────────────────┘ └───────────────────────┘\n *\n * Wire-shape (single `channel` string, three messages):\n *\n * { type: channel, msg, state } // background → surfaces (broadcast)\n * { type: `${channel}:hydrate` } // surface → background (request)\n * { type: `${channel}:dispatch`, msg } // surface → background (input)\n *\n * The `:hydrate` reply is the current `runtime.getState()` snapshot —\n * lets a newly-opened surface render the right view BEFORE the next\n * transition broadcast arrives.\n *\n * The bridge is intentionally agnostic to `chrome.runtime.sendMessage`\n * failure modes (no receiver, sender gone, etc.) — every cross-context send\n * is fire-and-forget with `.catch(() => {})`. Surfaces that need\n * delivery guarantees should layer their own retry, not bake it here.\n */\n\nimport { describeError } from \"../describe-error\";\nimport { type HistoryTracker, historyTracker, type Runtime } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Wire types — internal. Consumers don't import these directly; they pick\n// the channel string and the bridge handles serialization.\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface BroadcastEnvelope<S, M> {\n type: string; // === `channel`\n msg: M | null;\n state: S;\n}\n\ninterface DispatchEnvelope<M> {\n type: string; // === `${channel}:dispatch`\n msg: M;\n}\n\ninterface HydrateRequest {\n type: string; // === `${channel}:hydrate`\n}\n\n/**\n * Wire shape of one entry in the hydrate `backlog`. Mirrors a single\n * broadcast envelope, but without the `type` discriminator (the parent\n * `HydrateResponse` already identified the channel).\n */\ninterface BacklogEntry<V, M> {\n msg: M | null;\n state: V;\n}\n\n/**\n * Single source for the two derived wire-type strings. The `:dispatch` /\n * `:hydrate` suffix convention lives here and nowhere else — `bridgeRuntime`\n * and both clients derive their type constants from this so the convention\n * can't drift across the three functions (or the wire-shape comments above).\n */\nfunction channelTypes(channel: string): {\n dispatchType: string;\n hydrateType: string;\n} {\n return {\n dispatchType: `${channel}:dispatch`,\n hydrateType: `${channel}:hydrate`,\n };\n}\n\ninterface HydrateResponse<S, M> {\n state: S;\n /**\n * The runtime's history (most recent transitions, oldest first) projected\n * through `serialize` if provided. Present when the host runtime was\n * started with `historySize > 0`; an empty array otherwise. The surface\n * replays each entry through registered listeners before firing the\n * current-state marker, so a late subscriber sees the recent msg log AND\n * the current state on connect — not just the current state.\n */\n backlog: BacklogEntry<S, M>[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Background side — bridgeRuntime(runtime, { channel, serialize? })\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface BridgeRuntimeOpts<S, M, V = S> {\n channel: string;\n /**\n * REQUIRED parse for inbound `msg` payloads on `:dispatch` envelopes.\n * Chrome runtime messages are an `unknown` boundary (Inv 8) — the same\n * JSON/chrome serialization seam the symmetric client side parses via\n * `BridgeClientOpts.parseMsg`. An untrusted surface can drive the host\n * reducer with an arbitrary `M`, so the host parses too — never casts. A\n * `msg` that parses to `null` is dropped: the dispatch never reaches the\n * reducer and the surface's `dispatch()` rejects with `{ ok: false }`.\n *\n * Hosts that trust the dispatch channel and treat the msg as opaque opt\n * out EXPLICITLY by passing the {@link passThroughMsg} helper — the unsound\n * cast is then a visible call-site decision, not a silent default.\n */\n parseMsg: (raw: unknown) => M | null;\n /**\n * Optional projection from the runtime's live state `S` to a JSON-safe\n * view `V` that goes over the wire (broadcast + hydrate). Required when\n * `S` carries non-serializable values (DOM refs, Map, Date, …).\n *\n * Identity passthrough by default — existing callers with POJO state pass\n * `serialize` unset and `V = S`.\n *\n * Wire shape with `serialize`:\n * { type: channel, msg, state: serialize(currentState) } // broadcast\n * { type: hydrate } → { state: serialize(currentState) } // hydrate\n *\n * Dispatch is NOT affected — it carries `M` end-to-end. Surfaces parse the\n * `V` they receive (they should pick a type parameter matching the\n * projection).\n */\n serialize?: (state: S) => V;\n /**\n * If > 0, the bridge composes a `historyTracker(runtime, size)` over the\n * runtime and returns the captured `(msg, state)` backlog in every\n * `:hydrate` reply. Late subscribers receive recent traffic on connect —\n * the inspector then sees prior tool calls / phase transitions instead\n * of just the current state snapshot.\n *\n * Defaults to 0 (no tracker, empty backlog). Composition stays opt-in:\n * the substrate is untouched, the dispatch loop is untouched, and the\n * bridge attaches the tracker only when this opt is set. Cleanup is\n * the bridge's responsibility — the disposer returned by `bridgeRuntime`\n * stops the tracker along with the observer.\n *\n * Memory cost: ~size × (msg + state) bytes, in-memory only, dies with\n * the bridge. Privacy: backlog entries flow to every `:hydrate` reply —\n * any surface that can hydrate sees them. Do not enable on bridges\n * carrying user secrets.\n */\n historySize?: number;\n}\n\n/**\n * Install the bridge for a runtime. Returns a cleanup function that removes\n * the observer and the chrome listener.\n *\n * - **Broadcast:** every (msg, state) transition emits\n * `{ type: channel, msg, state }` via `chrome.runtime.sendMessage`. The\n * send is fire-and-forget — when no surface is listening, chrome.runtime\n * rejects with \"Could not establish connection. Receiving end does not\n * exist.\" which we swallow. If `serialize` is provided, `state` carries\n * `serialize(state)` instead of the raw state.\n * - **Hydrate:** `{ type: \":hydrate\" }` is answered via `sendResponse` with\n * the current (optionally projected) `runtime.getState()`. If the runtime\n * is mid-boot (store load not yet resolved), `getState()` throws — we\n * swallow and reply with `state: null`; surfaces should treat null as\n * \"not ready yet\" and wait for the first broadcast.\n * - **Dispatch:** `{ type: \":dispatch\", msg }` is parsed through `parseMsg`\n * (Inv 8 — inbound msgs are an `unknown` boundary) and, if non-null, routes\n * to `runtime.dispatch`. Returns `true` from the listener so chrome keeps the\n * message channel open until dispatch's promise settles (we ack with\n * `{ ok: true }`). A msg that fails the parse is dropped — the reducer never\n * sees it and we reply `{ ok: false }`.\n */\nexport function bridgeRuntime<S, M extends { type: string }, V = S>(\n runtime: Runtime<S, M>,\n { channel, parseMsg, serialize, historySize }: BridgeRuntimeOpts<S, M, V>,\n): () => void {\n const { dispatchType, hydrateType } = channelTypes(channel);\n // Default to identity passthrough when no projection is provided. The\n // type parameter defaults `V = S` so callers with no projection see no\n // change in the wire-shape — the broadcast still carries `S`.\n const project: (state: S) => V =\n serialize ?? ((state: S): V => state as unknown as V);\n // Compose a history tracker only when the caller opted in. Substrate\n // (`@demlik/tea`'s `run()`) knows nothing about history — the tracker is a\n // pure composition over `runtime.observe(...)`, owned by the bridge.\n // `null` when unset means \"no backlog in :hydrate replies\"; the wire\n // shape stays uniform (backlog is always an array, possibly empty).\n const tracker: HistoryTracker<S, M> | null =\n historySize !== undefined && historySize > 0\n ? historyTracker(runtime, historySize)\n : null;\n\n // The broadcast wire still carries `msg: M | null` — surfaces depend on the\n // boot frame (`msg: null`) to hydrate from the bridge. Applied transitions now\n // arrive via `observe` with a total `msg`; the boot frame arrives via `onBoot`\n // (#47). One helper builds the envelope for both.\n const sendBroadcast = (msg: M | null, state: S): void => {\n const envelope: BroadcastEnvelope<V, M | null> = {\n type: channel,\n msg,\n state: project(state),\n };\n chrome.runtime.sendMessage(envelope).catch(() => {\n // No surface listening — drop silently. Costs are negligible at\n // typical audit-event rates; surfaces poll on hydrate on open.\n });\n };\n const unobserve = runtime.observe((msg, state) => {\n sendBroadcast(msg, state);\n });\n const unboot = runtime.onBoot((state) => {\n sendBroadcast(null, state);\n });\n\n const onMessage = (\n message: unknown,\n _sender: chrome.runtime.MessageSender,\n sendResponse: (response: unknown) => void,\n ): boolean | undefined => {\n if (!message || typeof message !== \"object\") return undefined;\n const obj = message as Record<string, unknown>;\n\n if (obj.type === hydrateType) {\n let state: V | null;\n try {\n state = project(runtime.getState());\n } catch {\n // Runtime still booting (store.load pending). Surfaces fall back to\n // waiting for the first broadcast.\n state = null;\n }\n // Read the tracker's snapshot and project each entry's state through\n // `serialize`. When `historySize` was unset (tracker === null) we\n // send `[]` — uniform wire shape means surfaces never branch on\n // \"did the bridge enable history\" — they always iterate.\n const backlog: BacklogEntry<V, M>[] = tracker\n ? tracker\n .snapshot()\n .map((entry) => ({ msg: entry.msg, state: project(entry.state) }))\n : [];\n const response: HydrateResponse<V | null, M> = { state, backlog };\n sendResponse(response);\n return false;\n }\n\n if (obj.type === dispatchType) {\n // Inv 8: the inbound msg crosses the same JSON/chrome boundary the\n // client side parses — parse here too, never cast. A null parse drops\n // the dispatch (the reducer never sees an arbitrary/malformed `M`) and\n // the surface's dispatch() rejects on the `{ ok: false }` reply.\n const msg = parseMsg(obj.msg);\n if (msg === null) {\n sendResponse({ ok: false, error: \"dispatch msg failed parse\" });\n return false;\n }\n runtime\n .dispatch(msg)\n .then(() => {\n sendResponse({ ok: true });\n })\n .catch((err: unknown) => {\n sendResponse({\n ok: false,\n error: describeError(err),\n });\n });\n return true; // keep channel open for async sendResponse\n }\n\n return undefined;\n };\n\n chrome.runtime.onMessage.addListener(onMessage);\n\n return () => {\n unobserve();\n unboot();\n chrome.runtime.onMessage.removeListener(onMessage);\n // Detach the history observer too — leaving it attached after the\n // bridge tears down would silently retain memory and keep observing\n // a runtime whose backlog can no longer reach any consumer.\n tracker?.stop();\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Surface side — the bridge half that lives in a React surface. Exported as\n// a pure helper so the React adapter (or a test) wires the listeners and the\n// hydrate round-trip with the same wire format the host emits.\n//\n// Note: this file deliberately has NO React import. The React hook lives in\n// `react.ts` and consumes these helpers. Keeps the bridge testable in plain\n// vitest without happy-dom + React renderers.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The explicit opt-out of inbound msg parsing: an identity pass-through that\n * casts the raw `unknown` to `M`. Surfaces that gate only on state and treat\n * the broadcast msg as advisory pass this as `parseMsg` to acknowledge — at\n * the call site, in the types — that they are NOT parsing the msg boundary.\n *\n * This is the same behavior the old optional-`parseMsg` default had, but the\n * unsound cast now lives at one named, greppable, opt-in seam instead of being\n * the silent default for everyone. (Inv 8: the boundary parses; this helper is\n * the documented exception, chosen explicitly.)\n */\nexport function passThroughMsg<M>(raw: unknown): M | null {\n return raw as M;\n}\n\nexport interface BridgeClient<S, M> {\n /** Send the hydrate request; resolves with the initial snapshot (or null\n * if the host is still booting OR the response failed to parse). Also\n * primes `getSnapshot()` so React's `useSyncExternalStore` sees the\n * hydrated value on the next render. */\n hydrate(): Promise<S | null>;\n /** Send a dispatch envelope; resolves once the host's dispatch settles. */\n dispatch(msg: M): Promise<void>;\n /** Subscribe to broadcasts. Returns a cleanup function. Broadcasts whose\n * payload fails `parseState` or `parseMsg` are dropped silently — the\n * listener is not called. Each successful broadcast updates\n * `getSnapshot()`. */\n subscribe(listener: (msg: M | null, state: S) => void): () => void;\n /** Synchronous accessor for the latest known state, or `null` until the\n * first successful hydrate or broadcast lands. Required reference stability\n * for `useSyncExternalStore`: returns the SAME object reference when the\n * underlying state hasn't changed since the prior call. */\n getSnapshot(): S | null;\n}\n\nexport interface BridgeClientOpts<S, M> {\n channel: string;\n /**\n * REQUIRED parse for inbound `state` payloads (from `:hydrate` reply AND\n * every broadcast). Chrome runtime messages are an `unknown` boundary\n * (Inv 8) — the surface is the only party that knows the domain type `S`,\n * so it owns the boundary parse. Returning `null` drops the payload\n * silently (subscriber not called; hydrate resolves to `null`). The\n * parser MUST NOT throw — model rejection as `null`.\n */\n parseState: (raw: unknown) => S | null;\n /**\n * REQUIRED parse for inbound `msg` payloads on broadcasts. Chrome runtime\n * messages are an `unknown` boundary (Inv 8) just like the `state` payload,\n * so the msg crosses the same JSON serialization seam and must be parsed —\n * never cast. Returning `null` drops the broadcast silently.\n *\n * Surfaces that treat the msg union as advisory and only gate on state opt\n * out EXPLICITLY by passing the {@link passThroughMsg} helper — the opt-out\n * is now a visible call-site decision, not a hidden default that lets an\n * unparsed `unknown` reach listeners typed as `M`.\n */\n parseMsg: (raw: unknown) => M | null;\n}\n\n/**\n * The state+msg boundary parse (Inv 8) for one inbound envelope, in ONE\n * place. Chrome runtime messages are an `unknown` boundary: a failed\n * `parseState` drops the envelope (null state is indistinguishable from\n * \"host still booting\" and is not a useful signal), and a non-null `msg`\n * that fails `parseMsg` drops it too. `msg: null | undefined` on the wire is\n * the boot/current-state marker and passes through as `null`.\n *\n * Returns `null` to signal \"drop\"; callers translate that to `return`\n * (live `subscribe`) or `continue` (backlog replay). Living here keeps the\n * live-broadcast and backlog-replay paths from drifting apart.\n */\nfunction parseEnvelope<S, M extends { type: string }>(\n obj: Record<string, unknown>,\n parseState: (raw: unknown) => S | null,\n parseMsg: (raw: unknown) => M | null,\n): { msg: M | null; state: S } | null {\n const state = parseState(obj.state);\n if (state === null) return null;\n let msg: M | null;\n if (obj.msg === null || obj.msg === undefined) {\n msg = null;\n } else {\n msg = parseMsg(obj.msg);\n if (msg === null) return null;\n }\n return { msg, state };\n}\n\n/**\n * Shared backlog-replay helper used by both bridge clients. Iterates raw\n * entries from the wire, runs each through the same boundary-parse rules a\n * live broadcast would (via {@link parseEnvelope}), and fires the listeners\n * with the parsed `(msg, state)` pair. Updates the closure-held `latestState`\n * via `commitState` so post-replay `getSnapshot()` returns the most recent\n * backlog state if the current-state marker is absent or unparseable.\n *\n * Extracted so the two clients can't drift in how they parse backlog\n * payloads — both must stay symmetric with their live `subscribe` parse.\n */\nfunction replayBacklog<S, M extends { type: string }>(\n raw: unknown,\n parseState: (raw: unknown) => S | null,\n parseMsg: (raw: unknown) => M | null,\n listeners: Set<(msg: M | null, state: S) => void>,\n commitState: (next: S) => void,\n): void {\n if (!Array.isArray(raw)) return;\n for (const entryRaw of raw) {\n if (!entryRaw || typeof entryRaw !== \"object\") continue;\n const entry = entryRaw as Record<string, unknown>;\n const parsed = parseEnvelope(entry, parseState, parseMsg);\n if (parsed === null) continue;\n commitState(parsed.state);\n for (const l of listeners) l(parsed.msg, parsed.state);\n }\n}\n\n/**\n * Options for the shared client factory. On top of the public\n * `BridgeClient` contract (channel + the two boundary parsers), the factory\n * takes the two axes on which the runtime and tab clients actually differ:\n *\n * - `send` abstracts the request transport — `chrome.runtime.sendMessage`\n * for the runtime client, `chrome.tabs.sendMessage(tabId, …)` for the tab\n * client — used by both `hydrate()` and `dispatch()`.\n * - `senderMatches` filters inbound broadcasts by their `sender`. Defaults\n * to accept-all (runtime client); the tab client passes\n * `sender => sender.tab?.id === tabId` so a per-tab client only sees its\n * own tab's broadcasts.\n */\ninterface MakeBridgeClientOpts<S, M> {\n channel: string;\n parseState: (raw: unknown) => S | null;\n parseMsg: (raw: unknown) => M | null;\n send: (message: unknown) => Promise<unknown>;\n senderMatches?: (sender: chrome.runtime.MessageSender) => boolean;\n}\n\n/**\n * The single client implementation behind {@link bridgeClient} and\n * {@link bridgeTabClient}. Both public constructors are thin wrappers that\n * pick the transport (`send`) and, for the tab client, the sender filter.\n * Everything else — the `latestState`/`listeners` closures, the hydrate\n * round-trip + backlog replay, dispatch, the live-broadcast parse, and\n * `getSnapshot` reference stability — lives here so the two clients cannot\n * drift.\n */\nfunction makeBridgeClient<S, M extends { type: string }>({\n channel,\n parseState,\n parseMsg,\n send,\n senderMatches = () => true,\n}: MakeBridgeClientOpts<S, M>): BridgeClient<S, M> {\n const { dispatchType, hydrateType } = channelTypes(channel);\n // Closure-held latest snapshot. `useSyncExternalStore` reads this via\n // `getSnapshot`; the reference is stable across calls until a hydrate\n // reply or broadcast overwrites it. Identity matters: returning a new\n // object for the same logical state would loop React. `null` is the\n // pre-hydrate sentinel.\n let latestState: S | null = null;\n // Each active subscribe() registers a listener. Hydrate replies notify\n // every subscriber with `msg: null` (no host transition occurred — the\n // change source is the hydrate round-trip) so `useSyncExternalStore`\n // sees the snapshot update on first mount.\n const listeners = new Set<(msg: M | null, state: S) => void>();\n\n const client: BridgeClient<S, M> = {\n async hydrate(): Promise<S | null> {\n const req: HydrateRequest = { type: hydrateType };\n const res = (await send(req)) as\n | { state: unknown; backlog?: unknown }\n | undefined;\n // Replay the backlog FIRST — listeners observing for a msg log see\n // historical (msg, state) pairs in oldest-first order before the\n // current-state marker. Backlog entries that fail boundary parse are\n // dropped silently (same posture as live broadcasts). When the host\n // didn't enable history, `backlog` is `[]` and this loop is a no-op.\n replayBacklog(res?.backlog, parseState, parseMsg, listeners, (next) => {\n latestState = next;\n });\n const raw = res?.state;\n if (raw === undefined || raw === null) return null;\n const parsed = parseState(raw);\n if (parsed === null) return null;\n latestState = parsed;\n // Fire the current-state marker (msg = null) — distinct from any\n // backlog entry. Surfaces that show \"X just connected\" key off this.\n for (const l of listeners) l(null, parsed);\n return parsed;\n },\n\n async dispatch(msg: M): Promise<void> {\n const env: DispatchEnvelope<M> = { type: dispatchType, msg };\n await send(env);\n },\n\n subscribe(listener: (msg: M | null, state: S) => void): () => void {\n listeners.add(listener);\n const onMessage = (\n message: unknown,\n sender: chrome.runtime.MessageSender,\n ): void => {\n if (!senderMatches(sender)) return;\n if (!message || typeof message !== \"object\") return;\n const obj = message as Record<string, unknown>;\n if (obj.type !== channel) return;\n const parsed = parseEnvelope(obj, parseState, parseMsg);\n if (parsed === null) return;\n latestState = parsed.state;\n listener(parsed.msg, parsed.state);\n };\n chrome.runtime.onMessage.addListener(onMessage);\n return () => {\n chrome.runtime.onMessage.removeListener(onMessage);\n listeners.delete(listener);\n };\n },\n\n getSnapshot(): S | null {\n return latestState;\n },\n };\n\n return client;\n}\n\nexport function bridgeClient<S, M extends { type: string }>({\n channel,\n parseState,\n parseMsg,\n}: BridgeClientOpts<S, M>): BridgeClient<S, M> {\n return makeBridgeClient({\n channel,\n parseState,\n parseMsg,\n send: (message) => chrome.runtime.sendMessage(message),\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SW-side client of a content-script-hosted runtime.\n//\n// `bridgeClient` above sends via `chrome.runtime.sendMessage`, which lands on\n// the SW's `chrome.runtime.onMessage`. That's the surface-to-SW direction.\n//\n// For the SW-to-content direction we need:\n// - SENDS via `chrome.tabs.sendMessage(tabId, …)` — `chrome.runtime.sendMessage`\n// does NOT reach content scripts running in a tab.\n// - BROADCAST subscribers must filter on `sender.tab?.id === tabId`, since\n// every tab's content script broadcasts through the same SW\n// `chrome.runtime.onMessage`. Without the filter, a SW client subscribed\n// to \"tab 17\" would also see broadcasts from tabs 22, 31, ….\n//\n// Shape mirrors `BridgeClient<S, M>` so consumers can pick the constructor by\n// context and use the rest of the surface identically.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface BridgeTabClientOpts<S, M> {\n tabId: number;\n channel: string;\n /**\n * REQUIRED parse for inbound `state` payloads. See `BridgeClientOpts.parseState`\n * — same contract, same rationale. Chrome runtime messages from a tab are\n * an `unknown` boundary; the SW client owns the parse.\n */\n parseState: (raw: unknown) => S | null;\n /**\n * REQUIRED parse for inbound `msg` payloads. See `BridgeClientOpts.parseMsg`\n * — same contract, same rationale. Opt out of msg parsing with\n * {@link passThroughMsg}.\n */\n parseMsg: (raw: unknown) => M | null;\n}\n\n/**\n * SW-side client of a `bridgeRuntime` hosted in a content script.\n *\n * Identical contract to `bridgeClient` (same `BridgeClient<S, M>` shape).\n * Differences are confined to transport:\n *\n * - `hydrate()` / `dispatch(msg)` send via `chrome.tabs.sendMessage(tabId, …)`.\n * If the tab is gone, `chrome.tabs.sendMessage` rejects (same shape as\n * `chrome.runtime.sendMessage` with no receiver) — the promise propagates\n * the rejection. Callers decide whether to retry, drop, or surface; the\n * bridge has no opinion. Fire-and-forget posture matches `bridgeRuntime`.\n * - `subscribe(listener)` registers a `chrome.runtime.onMessage` listener\n * filtered by both `envelope.type === channel` AND `sender.tab?.id === tabId`,\n * so broadcasts from other tabs are ignored (no cross-contamination across\n * per-tab content-script runtimes).\n */\nexport function bridgeTabClient<S, M extends { type: string }>({\n tabId,\n channel,\n parseState,\n parseMsg,\n}: BridgeTabClientOpts<S, M>): BridgeClient<S, M> {\n return makeBridgeClient({\n channel,\n parseState,\n parseMsg,\n // SW → content transport: `chrome.runtime.sendMessage` does NOT reach a\n // tab's content script; `chrome.tabs.sendMessage(tabId, …)` does.\n send: (message) => chrome.tabs.sendMessage(tabId, message),\n // Every tab's content script broadcasts through the same SW\n // `chrome.runtime.onMessage`; filter to this tab so a client subscribed\n // to one tab never sees another's broadcasts.\n senderMatches: (sender) => sender.tab?.id === tabId,\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/extension/bridge.ts"],"names":[],"mappings":"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","file":"chunk-IVHQCVFX.js","sourcesContent":["/**\n * Runtime bridge — expose a `@demlik/tea` runtime running in one chrome context\n * (typically the background service worker) to consumers in other contexts\n * (sidepanel, popup, devtools panel) over `chrome.runtime.sendMessage` /\n * `chrome.runtime.onMessage`.\n *\n * Two halves, one channel name string.\n *\n * ┌──── background (host) ────┐ ┌──── react surface ────┐\n * │ bridgeRuntime(rt, {ch}) │ chrome.* │ useBackgroundRuntime │\n * │ ├ observe → broadcast │ ──────────▶ │ ├ hydrate (initial) │\n * │ └ onMessage(\":dispatch\") │ ◀────────── │ └ dispatch (msgs) │\n * │ └ onMessage(\":hydrate\") │ │ │\n * └────────────────────────────┘ └───────────────────────┘\n *\n * Wire-shape (single `channel` string, three messages):\n *\n * { type: channel, msg, state } // background → surfaces (broadcast)\n * { type: `${channel}:hydrate` } // surface → background (request)\n * { type: `${channel}:dispatch`, msg } // surface → background (input)\n *\n * The `:hydrate` reply is the current `runtime.getState()` snapshot —\n * lets a newly-opened surface render the right view BEFORE the next\n * transition broadcast arrives.\n *\n * The bridge is intentionally agnostic to `chrome.runtime.sendMessage`\n * failure modes (no receiver, sender gone, etc.) — every cross-context send\n * is fire-and-forget with `.catch(() => {})`. Surfaces that need\n * delivery guarantees should layer their own retry, not bake it here.\n */\n\nimport { describeError } from \"../describe-error\";\nimport { type HistoryTracker, historyTracker, type Runtime } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Wire types — internal. Consumers don't import these directly; they pick\n// the channel string and the bridge handles serialization.\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface BroadcastEnvelope<S, M> {\n type: string; // === `channel`\n msg: M | null;\n state: S;\n}\n\ninterface DispatchEnvelope<M> {\n type: string; // === `${channel}:dispatch`\n msg: M;\n}\n\ninterface HydrateRequest {\n type: string; // === `${channel}:hydrate`\n}\n\n/**\n * Wire shape of one entry in the hydrate `backlog`. Mirrors a single\n * broadcast envelope, but without the `type` discriminator (the parent\n * `HydrateResponse` already identified the channel).\n */\ninterface BacklogEntry<V, M> {\n msg: M | null;\n state: V;\n}\n\n/**\n * Single source for the two derived wire-type strings. The `:dispatch` /\n * `:hydrate` suffix convention lives here and nowhere else — `bridgeRuntime`\n * and both clients derive their type constants from this so the convention\n * can't drift across the three functions (or the wire-shape comments above).\n */\nfunction channelTypes(channel: string): {\n dispatchType: string;\n hydrateType: string;\n} {\n return {\n dispatchType: `${channel}:dispatch`,\n hydrateType: `${channel}:hydrate`,\n };\n}\n\ninterface HydrateResponse<S, M> {\n state: S;\n /**\n * The runtime's history (most recent transitions, oldest first) projected\n * through `serialize` if provided. Present when the host runtime was\n * started with `historySize > 0`; an empty array otherwise. The surface\n * replays each entry through registered listeners before firing the\n * current-state marker, so a late subscriber sees the recent msg log AND\n * the current state on connect — not just the current state.\n */\n backlog: BacklogEntry<S, M>[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Background side — bridgeRuntime(runtime, { channel, serialize? })\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface BridgeRuntimeOpts<S, M, V = S> {\n channel: string;\n /**\n * REQUIRED parse for inbound `msg` payloads on `:dispatch` envelopes.\n * Chrome runtime messages are an `unknown` boundary (Inv 8) — the same\n * JSON/chrome serialization seam the symmetric client side parses via\n * `BridgeClientOpts.parseMsg`. An untrusted surface can drive the host\n * reducer with an arbitrary `M`, so the host parses too — never casts. A\n * `msg` that parses to `null` is dropped: the dispatch never reaches the\n * reducer and the surface's `dispatch()` rejects with `{ ok: false }`.\n *\n * Hosts that trust the dispatch channel and treat the msg as opaque opt\n * out EXPLICITLY by passing the {@link passThroughMsg} helper — the unsound\n * cast is then a visible call-site decision, not a silent default.\n */\n parseMsg: (raw: unknown) => M | null;\n /**\n * Optional projection from the runtime's live state `S` to a JSON-safe\n * view `V` that goes over the wire (broadcast + hydrate). Required when\n * `S` carries non-serializable values (DOM refs, Map, Date, …).\n *\n * Identity passthrough by default — existing callers with POJO state pass\n * `serialize` unset and `V = S`.\n *\n * Wire shape with `serialize`:\n * { type: channel, msg, state: serialize(currentState) } // broadcast\n * { type: hydrate } → { state: serialize(currentState) } // hydrate\n *\n * Dispatch is NOT affected — it carries `M` end-to-end. Surfaces parse the\n * `V` they receive (they should pick a type parameter matching the\n * projection).\n */\n serialize?: (state: S) => V;\n /**\n * If > 0, the bridge composes a `historyTracker(runtime, size)` over the\n * runtime and returns the captured `(msg, state)` backlog in every\n * `:hydrate` reply. Late subscribers receive recent traffic on connect —\n * the inspector then sees prior tool calls / phase transitions instead\n * of just the current state snapshot.\n *\n * Defaults to 0 (no tracker, empty backlog). Composition stays opt-in:\n * the substrate is untouched, the dispatch loop is untouched, and the\n * bridge attaches the tracker only when this opt is set. Cleanup is\n * the bridge's responsibility — the disposer returned by `bridgeRuntime`\n * stops the tracker along with the observer.\n *\n * Memory cost: ~size × (msg + state) bytes, in-memory only, dies with\n * the bridge. Privacy: backlog entries flow to every `:hydrate` reply —\n * any surface that can hydrate sees them. Do not enable on bridges\n * carrying user secrets.\n */\n historySize?: number;\n}\n\n/**\n * Install the bridge for a runtime. Returns a cleanup function that removes\n * the observer and the chrome listener.\n *\n * - **Broadcast:** every (msg, state) transition emits\n * `{ type: channel, msg, state }` via `chrome.runtime.sendMessage`. The\n * send is fire-and-forget — when no surface is listening, chrome.runtime\n * rejects with \"Could not establish connection. Receiving end does not\n * exist.\" which we swallow. If `serialize` is provided, `state` carries\n * `serialize(state)` instead of the raw state.\n * - **Hydrate:** `{ type: \":hydrate\" }` is answered via `sendResponse` with\n * the current (optionally projected) `runtime.getState()`. If the runtime\n * is mid-boot (store load not yet resolved), `getState()` throws — we\n * swallow and reply with `state: null`; surfaces should treat null as\n * \"not ready yet\" and wait for the first broadcast.\n * - **Dispatch:** `{ type: \":dispatch\", msg }` is parsed through `parseMsg`\n * (Inv 8 — inbound msgs are an `unknown` boundary) and, if non-null, routes\n * to `runtime.dispatch`. Returns `true` from the listener so chrome keeps the\n * message channel open until dispatch's promise settles (we ack with\n * `{ ok: true }`). A msg that fails the parse is dropped — the reducer never\n * sees it and we reply `{ ok: false }`.\n */\nexport function bridgeRuntime<S, M extends { type: string }, V = S>(\n runtime: Runtime<S, M>,\n { channel, parseMsg, serialize, historySize }: BridgeRuntimeOpts<S, M, V>,\n): () => void {\n const { dispatchType, hydrateType } = channelTypes(channel);\n // Default to identity passthrough when no projection is provided. The\n // type parameter defaults `V = S` so callers with no projection see no\n // change in the wire-shape — the broadcast still carries `S`.\n const project: (state: S) => V =\n serialize ?? ((state: S): V => state as unknown as V);\n // Compose a history tracker only when the caller opted in. Substrate\n // (`@demlik/tea`'s `run()`) knows nothing about history — the tracker is a\n // pure composition over `runtime.observe(...)`, owned by the bridge.\n // `null` when unset means \"no backlog in :hydrate replies\"; the wire\n // shape stays uniform (backlog is always an array, possibly empty).\n const tracker: HistoryTracker<S, M> | null =\n historySize !== undefined && historySize > 0\n ? historyTracker(runtime, historySize)\n : null;\n\n // The broadcast wire still carries `msg: M | null` — surfaces depend on the\n // boot frame (`msg: null`) to hydrate from the bridge. Applied transitions now\n // arrive via `observe` with a total `msg`; the boot frame arrives via `onBoot`\n // (#47). One helper builds the envelope for both.\n const sendBroadcast = (msg: M | null, state: S): void => {\n const envelope: BroadcastEnvelope<V, M | null> = {\n type: channel,\n msg,\n state: project(state),\n };\n chrome.runtime.sendMessage(envelope).catch(() => {\n // No surface listening — drop silently. Costs are negligible at\n // typical audit-event rates; surfaces poll on hydrate on open.\n });\n };\n const unobserve = runtime.observe((msg, state) => {\n sendBroadcast(msg, state);\n });\n const unboot = runtime.onBoot((state) => {\n sendBroadcast(null, state);\n });\n\n const onMessage = (\n message: unknown,\n _sender: chrome.runtime.MessageSender,\n sendResponse: (response: unknown) => void,\n ): boolean | undefined => {\n if (!message || typeof message !== \"object\") return undefined;\n const obj = message as Record<string, unknown>;\n\n if (obj.type === hydrateType) {\n let state: V | null;\n try {\n state = project(runtime.getState());\n } catch {\n // Runtime still booting (store.load pending). Surfaces fall back to\n // waiting for the first broadcast.\n state = null;\n }\n // Read the tracker's snapshot and project each entry's state through\n // `serialize`. When `historySize` was unset (tracker === null) we\n // send `[]` — uniform wire shape means surfaces never branch on\n // \"did the bridge enable history\" — they always iterate.\n const backlog: BacklogEntry<V, M>[] = tracker\n ? tracker\n .snapshot()\n .map((entry) => ({ msg: entry.msg, state: project(entry.state) }))\n : [];\n const response: HydrateResponse<V | null, M> = { state, backlog };\n sendResponse(response);\n return false;\n }\n\n if (obj.type === dispatchType) {\n // Inv 8: the inbound msg crosses the same JSON/chrome boundary the\n // client side parses — parse here too, never cast. A null parse drops\n // the dispatch (the reducer never sees an arbitrary/malformed `M`) and\n // the surface's dispatch() rejects on the `{ ok: false }` reply.\n const msg = parseMsg(obj.msg);\n if (msg === null) {\n sendResponse({ ok: false, error: \"dispatch msg failed parse\" });\n return false;\n }\n runtime\n .dispatch(msg)\n .then(() => {\n sendResponse({ ok: true });\n })\n .catch((err: unknown) => {\n sendResponse({\n ok: false,\n error: describeError(err),\n });\n });\n return true; // keep channel open for async sendResponse\n }\n\n return undefined;\n };\n\n chrome.runtime.onMessage.addListener(onMessage);\n\n return () => {\n unobserve();\n unboot();\n chrome.runtime.onMessage.removeListener(onMessage);\n // Detach the history observer too — leaving it attached after the\n // bridge tears down would silently retain memory and keep observing\n // a runtime whose backlog can no longer reach any consumer.\n tracker?.stop();\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Surface side — the bridge half that lives in a React surface. Exported as\n// a pure helper so the React adapter (or a test) wires the listeners and the\n// hydrate round-trip with the same wire format the host emits.\n//\n// Note: this file deliberately has NO React import. The React hook lives in\n// `react.ts` and consumes these helpers. Keeps the bridge testable in plain\n// vitest without happy-dom + React renderers.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The explicit opt-out of inbound msg parsing: an identity pass-through that\n * casts the raw `unknown` to `M`. Surfaces that gate only on state and treat\n * the broadcast msg as advisory pass this as `parseMsg` to acknowledge — at\n * the call site, in the types — that they are NOT parsing the msg boundary.\n *\n * This is the same behavior the old optional-`parseMsg` default had, but the\n * unsound cast now lives at one named, greppable, opt-in seam instead of being\n * the silent default for everyone. (Inv 8: the boundary parses; this helper is\n * the documented exception, chosen explicitly.)\n */\nexport function passThroughMsg<M>(raw: unknown): M | null {\n return raw as M;\n}\n\nexport interface BridgeClient<S, M> {\n /** Send the hydrate request; resolves with the initial snapshot (or null\n * if the host is still booting OR the response failed to parse). Also\n * primes `getSnapshot()` so React's `useSyncExternalStore` sees the\n * hydrated value on the next render. */\n hydrate(): Promise<S | null>;\n /** Send a dispatch envelope; resolves once the host's dispatch settles. */\n dispatch(msg: M): Promise<void>;\n /** Subscribe to broadcasts. Returns a cleanup function. Broadcasts whose\n * payload fails `parseState` or `parseMsg` are dropped silently — the\n * listener is not called. Each successful broadcast updates\n * `getSnapshot()`. */\n subscribe(listener: (msg: M | null, state: S) => void): () => void;\n /** Synchronous accessor for the latest known state, or `null` until the\n * first successful hydrate or broadcast lands. Required reference stability\n * for `useSyncExternalStore`: returns the SAME object reference when the\n * underlying state hasn't changed since the prior call. */\n getSnapshot(): S | null;\n}\n\nexport interface BridgeClientOpts<S, M> {\n channel: string;\n /**\n * REQUIRED parse for inbound `state` payloads (from `:hydrate` reply AND\n * every broadcast). Chrome runtime messages are an `unknown` boundary\n * (Inv 8) — the surface is the only party that knows the domain type `S`,\n * so it owns the boundary parse. Returning `null` drops the payload\n * silently (subscriber not called; hydrate resolves to `null`). The\n * parser MUST NOT throw — model rejection as `null`.\n */\n parseState: (raw: unknown) => S | null;\n /**\n * REQUIRED parse for inbound `msg` payloads on broadcasts. Chrome runtime\n * messages are an `unknown` boundary (Inv 8) just like the `state` payload,\n * so the msg crosses the same JSON serialization seam and must be parsed —\n * never cast. Returning `null` drops the broadcast silently.\n *\n * Surfaces that treat the msg union as advisory and only gate on state opt\n * out EXPLICITLY by passing the {@link passThroughMsg} helper — the opt-out\n * is now a visible call-site decision, not a hidden default that lets an\n * unparsed `unknown` reach listeners typed as `M`.\n */\n parseMsg: (raw: unknown) => M | null;\n}\n\n/**\n * The state+msg boundary parse (Inv 8) for one inbound envelope, in ONE\n * place. Chrome runtime messages are an `unknown` boundary: a failed\n * `parseState` drops the envelope (null state is indistinguishable from\n * \"host still booting\" and is not a useful signal), and a non-null `msg`\n * that fails `parseMsg` drops it too. `msg: null | undefined` on the wire is\n * the boot/current-state marker and passes through as `null`.\n *\n * Returns `null` to signal \"drop\"; callers translate that to `return`\n * (live `subscribe`) or `continue` (backlog replay). Living here keeps the\n * live-broadcast and backlog-replay paths from drifting apart.\n */\nfunction parseEnvelope<S, M extends { type: string }>(\n obj: Record<string, unknown>,\n parseState: (raw: unknown) => S | null,\n parseMsg: (raw: unknown) => M | null,\n): { msg: M | null; state: S } | null {\n const state = parseState(obj.state);\n if (state === null) return null;\n let msg: M | null;\n if (obj.msg === null || obj.msg === undefined) {\n msg = null;\n } else {\n msg = parseMsg(obj.msg);\n if (msg === null) return null;\n }\n return { msg, state };\n}\n\n/**\n * Shared backlog-replay helper used by both bridge clients. Iterates raw\n * entries from the wire, runs each through the same boundary-parse rules a\n * live broadcast would (via {@link parseEnvelope}), and fires the listeners\n * with the parsed `(msg, state)` pair. Updates the closure-held `latestState`\n * via `commitState` so post-replay `getSnapshot()` returns the most recent\n * backlog state if the current-state marker is absent or unparseable.\n *\n * Extracted so the two clients can't drift in how they parse backlog\n * payloads — both must stay symmetric with their live `subscribe` parse.\n */\nfunction replayBacklog<S, M extends { type: string }>(\n raw: unknown,\n parseState: (raw: unknown) => S | null,\n parseMsg: (raw: unknown) => M | null,\n listeners: Set<(msg: M | null, state: S) => void>,\n commitState: (next: S) => void,\n): void {\n if (!Array.isArray(raw)) return;\n for (const entryRaw of raw) {\n if (!entryRaw || typeof entryRaw !== \"object\") continue;\n const entry = entryRaw as Record<string, unknown>;\n const parsed = parseEnvelope(entry, parseState, parseMsg);\n if (parsed === null) continue;\n commitState(parsed.state);\n for (const l of listeners) l(parsed.msg, parsed.state);\n }\n}\n\n/**\n * Options for the shared client factory. On top of the public\n * `BridgeClient` contract (channel + the two boundary parsers), the factory\n * takes the two axes on which the runtime and tab clients actually differ:\n *\n * - `send` abstracts the request transport — `chrome.runtime.sendMessage`\n * for the runtime client, `chrome.tabs.sendMessage(tabId, …)` for the tab\n * client — used by both `hydrate()` and `dispatch()`.\n * - `senderMatches` filters inbound broadcasts by their `sender`. Defaults\n * to accept-all (runtime client); the tab client passes\n * `sender => sender.tab?.id === tabId` so a per-tab client only sees its\n * own tab's broadcasts.\n */\ninterface MakeBridgeClientOpts<S, M> {\n channel: string;\n parseState: (raw: unknown) => S | null;\n parseMsg: (raw: unknown) => M | null;\n send: (message: unknown) => Promise<unknown>;\n senderMatches?: (sender: chrome.runtime.MessageSender) => boolean;\n}\n\n/**\n * The single client implementation behind {@link bridgeClient} and\n * {@link bridgeTabClient}. Both public constructors are thin wrappers that\n * pick the transport (`send`) and, for the tab client, the sender filter.\n * Everything else — the `latestState`/`listeners` closures, the hydrate\n * round-trip + backlog replay, dispatch, the live-broadcast parse, and\n * `getSnapshot` reference stability — lives here so the two clients cannot\n * drift.\n */\nfunction makeBridgeClient<S, M extends { type: string }>({\n channel,\n parseState,\n parseMsg,\n send,\n senderMatches = () => true,\n}: MakeBridgeClientOpts<S, M>): BridgeClient<S, M> {\n const { dispatchType, hydrateType } = channelTypes(channel);\n // Closure-held latest snapshot. `useSyncExternalStore` reads this via\n // `getSnapshot`; the reference is stable across calls until a hydrate\n // reply or broadcast overwrites it. Identity matters: returning a new\n // object for the same logical state would loop React. `null` is the\n // pre-hydrate sentinel.\n let latestState: S | null = null;\n // Each active subscribe() registers a listener. Hydrate replies notify\n // every subscriber with `msg: null` (no host transition occurred — the\n // change source is the hydrate round-trip) so `useSyncExternalStore`\n // sees the snapshot update on first mount.\n const listeners = new Set<(msg: M | null, state: S) => void>();\n\n const client: BridgeClient<S, M> = {\n async hydrate(): Promise<S | null> {\n const req: HydrateRequest = { type: hydrateType };\n const res = (await send(req)) as\n | { state: unknown; backlog?: unknown }\n | undefined;\n // Replay the backlog FIRST — listeners observing for a msg log see\n // historical (msg, state) pairs in oldest-first order before the\n // current-state marker. Backlog entries that fail boundary parse are\n // dropped silently (same posture as live broadcasts). When the host\n // didn't enable history, `backlog` is `[]` and this loop is a no-op.\n replayBacklog(res?.backlog, parseState, parseMsg, listeners, (next) => {\n latestState = next;\n });\n const raw = res?.state;\n if (raw === undefined || raw === null) return null;\n const parsed = parseState(raw);\n if (parsed === null) return null;\n latestState = parsed;\n // Fire the current-state marker (msg = null) — distinct from any\n // backlog entry. Surfaces that show \"X just connected\" key off this.\n for (const l of listeners) l(null, parsed);\n return parsed;\n },\n\n async dispatch(msg: M): Promise<void> {\n const env: DispatchEnvelope<M> = { type: dispatchType, msg };\n await send(env);\n },\n\n subscribe(listener: (msg: M | null, state: S) => void): () => void {\n listeners.add(listener);\n const onMessage = (\n message: unknown,\n sender: chrome.runtime.MessageSender,\n ): void => {\n if (!senderMatches(sender)) return;\n if (!message || typeof message !== \"object\") return;\n const obj = message as Record<string, unknown>;\n if (obj.type !== channel) return;\n const parsed = parseEnvelope(obj, parseState, parseMsg);\n if (parsed === null) return;\n latestState = parsed.state;\n listener(parsed.msg, parsed.state);\n };\n chrome.runtime.onMessage.addListener(onMessage);\n return () => {\n chrome.runtime.onMessage.removeListener(onMessage);\n listeners.delete(listener);\n };\n },\n\n getSnapshot(): S | null {\n return latestState;\n },\n };\n\n return client;\n}\n\nexport function bridgeClient<S, M extends { type: string }>({\n channel,\n parseState,\n parseMsg,\n}: BridgeClientOpts<S, M>): BridgeClient<S, M> {\n return makeBridgeClient({\n channel,\n parseState,\n parseMsg,\n send: (message) => chrome.runtime.sendMessage(message),\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SW-side client of a content-script-hosted runtime.\n//\n// `bridgeClient` above sends via `chrome.runtime.sendMessage`, which lands on\n// the SW's `chrome.runtime.onMessage`. That's the surface-to-SW direction.\n//\n// For the SW-to-content direction we need:\n// - SENDS via `chrome.tabs.sendMessage(tabId, …)` — `chrome.runtime.sendMessage`\n// does NOT reach content scripts running in a tab.\n// - BROADCAST subscribers must filter on `sender.tab?.id === tabId`, since\n// every tab's content script broadcasts through the same SW\n// `chrome.runtime.onMessage`. Without the filter, a SW client subscribed\n// to \"tab 17\" would also see broadcasts from tabs 22, 31, ….\n//\n// Shape mirrors `BridgeClient<S, M>` so consumers can pick the constructor by\n// context and use the rest of the surface identically.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface BridgeTabClientOpts<S, M> {\n tabId: number;\n channel: string;\n /**\n * REQUIRED parse for inbound `state` payloads. See `BridgeClientOpts.parseState`\n * — same contract, same rationale. Chrome runtime messages from a tab are\n * an `unknown` boundary; the SW client owns the parse.\n */\n parseState: (raw: unknown) => S | null;\n /**\n * REQUIRED parse for inbound `msg` payloads. See `BridgeClientOpts.parseMsg`\n * — same contract, same rationale. Opt out of msg parsing with\n * {@link passThroughMsg}.\n */\n parseMsg: (raw: unknown) => M | null;\n}\n\n/**\n * SW-side client of a `bridgeRuntime` hosted in a content script.\n *\n * Identical contract to `bridgeClient` (same `BridgeClient<S, M>` shape).\n * Differences are confined to transport:\n *\n * - `hydrate()` / `dispatch(msg)` send via `chrome.tabs.sendMessage(tabId, …)`.\n * If the tab is gone, `chrome.tabs.sendMessage` rejects (same shape as\n * `chrome.runtime.sendMessage` with no receiver) — the promise propagates\n * the rejection. Callers decide whether to retry, drop, or surface; the\n * bridge has no opinion. Fire-and-forget posture matches `bridgeRuntime`.\n * - `subscribe(listener)` registers a `chrome.runtime.onMessage` listener\n * filtered by both `envelope.type === channel` AND `sender.tab?.id === tabId`,\n * so broadcasts from other tabs are ignored (no cross-contamination across\n * per-tab content-script runtimes).\n */\nexport function bridgeTabClient<S, M extends { type: string }>({\n tabId,\n channel,\n parseState,\n parseMsg,\n}: BridgeTabClientOpts<S, M>): BridgeClient<S, M> {\n return makeBridgeClient({\n channel,\n parseState,\n parseMsg,\n // SW → content transport: `chrome.runtime.sendMessage` does NOT reach a\n // tab's content script; `chrome.tabs.sendMessage(tabId, …)` does.\n send: (message) => chrome.tabs.sendMessage(tabId, message),\n // Every tab's content script broadcasts through the same SW\n // `chrome.runtime.onMessage`; filter to this tab so a client subscribed\n // to one tab never sees another's broadcasts.\n senderMatches: (sender) => sender.tab?.id === tabId,\n });\n}\n"]}
|
|
@@ -75,6 +75,8 @@ var NoCellError = class extends Error {
|
|
|
75
75
|
this.msgType = msgType;
|
|
76
76
|
this.stateName = stateName;
|
|
77
77
|
}
|
|
78
|
+
msgType;
|
|
79
|
+
stateName;
|
|
78
80
|
name = "NoCellError";
|
|
79
81
|
_tag = "NoCellError";
|
|
80
82
|
};
|
|
@@ -228,5 +230,5 @@ function foldMsgs(machine, base, msgs) {
|
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
export { Cmd, NoCellError, applyCell, applyCellChecked, depsInactive, detectUpdateForm, foldMsgs, foldUpdates, formOf, msgKeysOf, structuralHash, subId, without };
|
|
231
|
-
//# sourceMappingURL=chunk-
|
|
232
|
-
//# sourceMappingURL=chunk-
|
|
233
|
+
//# sourceMappingURL=chunk-O7ED34JH.js.map
|
|
234
|
+
//# sourceMappingURL=chunk-O7ED34JH.js.map
|