@demlik/tea 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +8 -8
- package/dist/authed-call/index.d.ts +3 -3
- package/dist/authed-call/index.js +5 -5
- package/dist/await-terminal/index.d.ts +1 -1
- package/dist/await-terminal/index.js +2 -2
- package/dist/batch-window/index.d.ts +2 -2
- package/dist/batch-window/index.js +4 -4
- package/dist/cache/index.d.ts +2 -2
- package/dist/{chunk-EIZJTYKR.js → chunk-5KVHID6A.js} +4 -4
- package/dist/{chunk-EIZJTYKR.js.map → chunk-5KVHID6A.js.map} +1 -1
- package/dist/{chunk-RVJFGRGK.js → chunk-6GIGQT6C.js} +4 -4
- package/dist/chunk-6GIGQT6C.js.map +1 -0
- package/dist/{chunk-FH5RUIIL.js → chunk-6OUDPSSG.js} +3 -3
- package/dist/{chunk-FH5RUIIL.js.map → chunk-6OUDPSSG.js.map} +1 -1
- package/dist/{chunk-2WZHS4P2.js → chunk-6VDCTR7N.js} +25 -40
- package/dist/chunk-6VDCTR7N.js.map +1 -0
- package/dist/{chunk-BNJYE4AU.js → chunk-DYRKDCJI.js} +46 -16
- package/dist/chunk-DYRKDCJI.js.map +1 -0
- package/dist/{chunk-RGTPPF4U.js → chunk-EGWTT5XJ.js} +3 -3
- package/dist/{chunk-RGTPPF4U.js.map → chunk-EGWTT5XJ.js.map} +1 -1
- package/dist/{chunk-A56GNPI7.js → chunk-EMJTPHI7.js} +5 -23
- package/dist/chunk-EMJTPHI7.js.map +1 -0
- package/dist/{chunk-IFBQJYGI.js → chunk-FOGL3TC7.js} +3 -3
- package/dist/{chunk-IFBQJYGI.js.map → chunk-FOGL3TC7.js.map} +1 -1
- package/dist/{chunk-RVU3RBE6.js → chunk-G2U7DJHL.js} +6 -4
- package/dist/chunk-G2U7DJHL.js.map +1 -0
- package/dist/{chunk-WUV3PSCZ.js → chunk-HCEVHFJ7.js} +2 -2
- package/dist/chunk-HCEVHFJ7.js.map +1 -0
- package/dist/{chunk-EDGDR5XX.js → chunk-IK4S5DJN.js} +3 -3
- package/dist/{chunk-EDGDR5XX.js.map → chunk-IK4S5DJN.js.map} +1 -1
- package/dist/{chunk-EQRUAP2A.js → chunk-J6527VRQ.js} +2 -2
- package/dist/chunk-J6527VRQ.js.map +1 -0
- package/dist/{chunk-TAO4YOLO.js → chunk-M6UZT7HS.js} +3 -3
- package/dist/{chunk-TAO4YOLO.js.map → chunk-M6UZT7HS.js.map} +1 -1
- package/dist/{chunk-QGZW4W3E.js → chunk-SKAMZKTE.js} +3 -3
- package/dist/{chunk-QGZW4W3E.js.map → chunk-SKAMZKTE.js.map} +1 -1
- package/dist/{chunk-TC4HWABA.js → chunk-VFGKTGGC.js} +7 -7
- package/dist/{chunk-TC4HWABA.js.map → chunk-VFGKTGGC.js.map} +1 -1
- package/dist/{chunk-D5EQDLDV.js → chunk-ZDRN3YCE.js} +4 -4
- package/dist/{chunk-D5EQDLDV.js.map → chunk-ZDRN3YCE.js.map} +1 -1
- package/dist/{core-CymXZCeY.d.ts → core-BmlJOvxM.d.ts} +20 -1
- package/dist/deadline/index.d.ts +2 -2
- package/dist/deadline/index.js +3 -3
- package/dist/debounce/index.d.ts +6 -0
- package/dist/debounce/index.js +1 -1
- package/dist/do/index.d.ts +422 -315
- package/dist/do/index.js +173 -149
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-2MDxDs45.d.ts → durable-effects-9LxFpmh3.d.ts} +1 -1
- package/dist/extension/index.d.ts +1 -1
- 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/fan-out/index.d.ts +1 -1
- package/dist/idempotent-intake/index.d.ts +1 -1
- package/dist/index.d.ts +20 -3
- package/dist/index.js +2 -2
- package/dist/llm-call/index.d.ts +2 -2
- package/dist/llm-call/index.js +5 -5
- package/dist/machine-viz/index.d.ts +4 -4
- package/dist/machine-viz/index.js +4 -8
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -1
- package/dist/monitored-run/index.d.ts +2 -2
- package/dist/monitored-run/index.js +5 -5
- package/dist/node/index.d.ts +1 -1
- package/dist/paginated-walk/index.d.ts +2 -2
- package/dist/paginated-walk/index.js +5 -5
- package/dist/parity/index.d.ts +10 -4
- package/dist/parity/index.js +6 -5
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/index.d.ts +15 -8
- package/dist/pbt/index.js +5 -11
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -3
- package/dist/poller/index.d.ts +2 -2
- package/dist/poller/index.js +3 -3
- package/dist/prediction/index.d.ts +23 -2
- package/dist/prediction/index.js +2 -2
- package/dist/pure/index.d.ts +2 -2
- package/dist/pure/index.js +2 -2
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +8 -4
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +2 -2
- package/dist/reconciler/index.js +5 -5
- package/dist/recorder/index.d.ts +1 -1
- package/dist/resilient-call/index.d.ts +45 -2
- package/dist/resilient-call/index.js +4 -4
- package/dist/retry-to-success/index.d.ts +109 -0
- package/dist/retry-to-success/index.js +35 -0
- package/dist/retry-to-success/index.js.map +1 -0
- package/dist/saga/index.d.ts +14 -1
- package/dist/saga/index.js.map +1 -1
- package/dist/snapshot/index.d.ts +1 -1
- package/dist/snapshot/index.js +3 -3
- package/dist/subs/index.d.ts +2 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/throttle/index.d.ts +9 -0
- package/dist/throttle/index.js +1 -1
- package/dist/throttled-input/index.d.ts +10 -2
- package/dist/throttled-input/index.js +6 -6
- package/dist/throttled-input/index.js.map +1 -1
- package/dist/token-refresh/index.d.ts +1 -1
- package/dist/token-refresh/index.js +3 -3
- package/dist/trace-replay/index.d.ts +1 -1
- package/dist/trace-replay/index.js +3 -3
- package/dist/{types-N9LZbdnr.d.ts → types-BoUWgwSm.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +1 -1
- package/dist/with-deadline/index.js +5 -24
- package/dist/with-deadline/index.js.map +1 -1
- package/dist/with-resilience/index.d.ts +2 -2
- package/dist/with-resilience/index.js +13 -36
- package/dist/with-resilience/index.js.map +1 -1
- package/dist/with-telemetry/index.d.ts +1 -1
- package/dist/with-telemetry/index.js +4 -29
- package/dist/with-telemetry/index.js.map +1 -1
- package/dist/work-queue/adapter.d.ts +1 -1
- package/dist/work-queue/index.d.ts +1 -1
- package/dist/work-queue/ops.d.ts +1 -1
- package/dist/workflow/index.d.ts +16 -10
- package/dist/workflow/index.js.map +1 -1
- package/package.json +8 -20
- package/dist/chunk-2WZHS4P2.js.map +0 -1
- package/dist/chunk-7VEG4KHB.js +0 -202
- package/dist/chunk-7VEG4KHB.js.map +0 -1
- package/dist/chunk-A56GNPI7.js.map +0 -1
- package/dist/chunk-BNJYE4AU.js.map +0 -1
- package/dist/chunk-EQRUAP2A.js.map +0 -1
- package/dist/chunk-RVJFGRGK.js.map +0 -1
- package/dist/chunk-RVU3RBE6.js.map +0 -1
- package/dist/chunk-WUV3PSCZ.js.map +0 -1
- package/dist/chunk-XRXKH7KA.js +0 -268
- package/dist/chunk-XRXKH7KA.js.map +0 -1
- package/dist/raft/demo.d.ts +0 -105
- package/dist/raft/demo.js +0 -172
- package/dist/raft/demo.js.map +0 -1
- package/dist/raft/index.d.ts +0 -548
- package/dist/raft/index.js +0 -7
- package/dist/raft/index.js.map +0 -1
- package/dist/raft/sim.d.ts +0 -222
- package/dist/raft/sim.js +0 -8
- package/dist/raft/sim.js.map +0 -1
- package/dist/raft/timeline.d.ts +0 -46
- package/dist/raft/timeline.js +0 -62
- package/dist/raft/timeline.js.map +0 -1
package/dist/agent/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FanOutState } from '../fan-out/index.js';
|
|
2
|
-
import { C as Cmd, I as Interpret, M as Machine } from '../core-
|
|
2
|
+
import { C as Cmd, I as Interpret, M as Machine } from '../core-BmlJOvxM.js';
|
|
3
3
|
import { LlmCall, LlmOk, LlmSucceedMsg, LlmFailMsg, LlmErr, LlmTimerMsg, Schema, MessageLoader, ModelFactory, LlmRunCmd, LlmCallPorts } from '../llm-call/index.js';
|
|
4
4
|
import { MonitoredRunState, RunFailure, MonitoredRunCmd } from '../monitored-run/index.js';
|
|
5
5
|
import { M as MsgType } from '../protocol-B5YmJr0d.js';
|
|
@@ -11,7 +11,7 @@ import '../work-queue/index.js';
|
|
|
11
11
|
import '../index.js';
|
|
12
12
|
import '../snapshot/index.js';
|
|
13
13
|
import '../cache/index.js';
|
|
14
|
-
import '../types-
|
|
14
|
+
import '../types-BoUWgwSm.js';
|
|
15
15
|
import '../circuit-breaker/index.js';
|
|
16
16
|
import '../rate-limit/index.js';
|
|
17
17
|
|
package/dist/agent/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export { COMPACTION_PURPOSE, agentBootMsg, agentEvents, agentTurnSchema, compactionSummarySchema, createAgent, isAgentTurn, isCompactionSummary, liftAgent, mergeInterpret, status } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
3
|
-
import '../chunk-
|
|
4
|
-
import '../chunk-
|
|
5
|
-
import '../chunk-
|
|
1
|
+
export { COMPACTION_PURPOSE, agentBootMsg, agentEvents, agentTurnSchema, compactionSummarySchema, createAgent, isAgentTurn, isCompactionSummary, liftAgent, mergeInterpret, status } from '../chunk-VFGKTGGC.js';
|
|
2
|
+
import '../chunk-ZDRN3YCE.js';
|
|
3
|
+
import '../chunk-M6UZT7HS.js';
|
|
4
|
+
import '../chunk-5KVHID6A.js';
|
|
5
|
+
import '../chunk-6GIGQT6C.js';
|
|
6
6
|
import '../chunk-JELSZN25.js';
|
|
7
7
|
import '../chunk-UV6UWGEO.js';
|
|
8
|
-
export { deadlineSub, subscribeDeadline } from '../chunk-
|
|
8
|
+
export { deadlineSub, subscribeDeadline } from '../chunk-SKAMZKTE.js';
|
|
9
9
|
import '../chunk-5VNWZADP.js';
|
|
10
10
|
import '../chunk-2YOE5PIS.js';
|
|
11
11
|
import '../chunk-SC3FAFZN.js';
|
|
@@ -14,7 +14,7 @@ import '../chunk-HJO6VNQI.js';
|
|
|
14
14
|
import '../chunk-TGG26XMK.js';
|
|
15
15
|
import '../chunk-NSDFA6M6.js';
|
|
16
16
|
import '../chunk-2YFLOCWX.js';
|
|
17
|
-
import '../chunk-
|
|
18
|
-
import '../chunk-
|
|
17
|
+
import '../chunk-6VDCTR7N.js';
|
|
18
|
+
import '../chunk-DYRKDCJI.js';
|
|
19
19
|
//# sourceMappingURL=index.js.map
|
|
20
20
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { c as PortEmitter, C as Cmd } from '../core-
|
|
1
|
+
import { c as PortEmitter, C as Cmd } from '../core-BmlJOvxM.js';
|
|
2
2
|
import { M as MsgType } from '../protocol-B5YmJr0d.js';
|
|
3
3
|
import { ResilientConfig, ResilientState, RunCmd, ResilientPorts, SucceedMsg, FailMsg, ResilientTimerMsg } from '../resilient-call/index.js';
|
|
4
4
|
import { TokenRefreshConfig, Token, TokenState, RefreshTokenCmd, TokenRefreshMsg } from '../token-refresh/index.js';
|
|
5
5
|
import { DeadlineSub } from '../deadline/index.js';
|
|
6
6
|
export { DeadlineExceeded, deadlineSub, subscribeDeadline } from '../deadline/index.js';
|
|
7
7
|
import '../cache/index.js';
|
|
8
|
-
import '../types-
|
|
8
|
+
import '../types-BoUWgwSm.js';
|
|
9
9
|
import '../circuit-breaker/index.js';
|
|
10
10
|
import '../rate-limit/index.js';
|
|
11
11
|
import '../retry-backoff/index.js';
|
|
@@ -101,7 +101,7 @@ declare function createAuthedCall<I, R>(config: AuthedConfig, rng?: () => number
|
|
|
101
101
|
subs: (s: AuthedState<I, R>) => readonly DeadlineSub[];
|
|
102
102
|
handlers: (ports: AuthedPorts<I, R>) => {
|
|
103
103
|
refresh_token: (cmd: RefreshTokenCmd, ctx: PortEmitter) => Promise<void | TokenRefreshMsg>;
|
|
104
|
-
resilient_run: (cmd: RunCmd<I>, ctx: Readonly<Record<never, never>>) => Promise<SucceedMsg<R
|
|
104
|
+
resilient_run: (cmd: RunCmd<I>, ctx: Readonly<Record<never, never>>) => Promise<FailMsg | SucceedMsg<R>>;
|
|
105
105
|
};
|
|
106
106
|
};
|
|
107
107
|
/** The shape `createAuthedCall` returns — useful for typing a held knob. */
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { createTokenRefresh, refreshTokenCmd } from '../chunk-
|
|
2
|
-
import { createResilientCall } from '../chunk-
|
|
1
|
+
import { createTokenRefresh, refreshTokenCmd } from '../chunk-6OUDPSSG.js';
|
|
2
|
+
import { createResilientCall } from '../chunk-6GIGQT6C.js';
|
|
3
3
|
import '../chunk-JELSZN25.js';
|
|
4
|
-
export { deadlineSub, subscribeDeadline } from '../chunk-
|
|
4
|
+
export { deadlineSub, subscribeDeadline } from '../chunk-SKAMZKTE.js';
|
|
5
5
|
import '../chunk-5VNWZADP.js';
|
|
6
6
|
import '../chunk-2YOE5PIS.js';
|
|
7
7
|
import '../chunk-SC3FAFZN.js';
|
|
8
8
|
import '../chunk-IN33Q4VJ.js';
|
|
9
9
|
import '../chunk-NSDFA6M6.js';
|
|
10
10
|
import '../chunk-2YFLOCWX.js';
|
|
11
|
-
import '../chunk-
|
|
12
|
-
import '../chunk-
|
|
11
|
+
import '../chunk-6VDCTR7N.js';
|
|
12
|
+
import '../chunk-DYRKDCJI.js';
|
|
13
13
|
|
|
14
14
|
// src/authed-call/index.ts
|
|
15
15
|
function createAuthedCall(config, rng = Math.random) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Runtime, CtxArg } from '../index.js';
|
|
2
|
-
import { C as Cmd, S as Sub, M as Machine } from '../core-
|
|
2
|
+
import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @demlik/tea/await-terminal — run a ONE-SHOT / decision-procedure machine to a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as Cmd } from '../core-
|
|
1
|
+
import { C as Cmd } from '../core-BmlJOvxM.js';
|
|
2
2
|
export { Debounced, debounce } from '../debounce/index.js';
|
|
3
3
|
import { DeadlineSub, DeadlineExceeded } from '../deadline/index.js';
|
|
4
|
-
import { S as SubscribeHandler } from '../types-
|
|
4
|
+
import { S as SubscribeHandler } from '../types-BoUWgwSm.js';
|
|
5
5
|
export { EnqueueInput } from '../work-queue/index.js';
|
|
6
6
|
import '../index.js';
|
|
7
7
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { debounce } from '../chunk-
|
|
2
|
-
import { subscribeDeadline, deadlineExceeded, deadlineSub } from '../chunk-
|
|
1
|
+
export { debounce } from '../chunk-J6527VRQ.js';
|
|
2
|
+
import { subscribeDeadline, deadlineExceeded, deadlineSub } from '../chunk-SKAMZKTE.js';
|
|
3
3
|
import '../chunk-2YFLOCWX.js';
|
|
4
|
-
import '../chunk-
|
|
5
|
-
import '../chunk-
|
|
4
|
+
import '../chunk-6VDCTR7N.js';
|
|
5
|
+
import '../chunk-DYRKDCJI.js';
|
|
6
6
|
|
|
7
7
|
// src/batch-window/index.ts
|
|
8
8
|
function initBatchWindow() {
|
package/dist/cache/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as Sub } from '../core-
|
|
2
|
-
import { S as SubscribeHandler } from '../types-
|
|
1
|
+
import { S as Sub } from '../core-BmlJOvxM.js';
|
|
2
|
+
import { S as SubscribeHandler } from '../types-BoUWgwSm.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @demlik/tea/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { tryInterpret } from './chunk-
|
|
2
|
-
import { Cmd } from './chunk-
|
|
1
|
+
import { tryInterpret } from './chunk-6VDCTR7N.js';
|
|
2
|
+
import { Cmd } from './chunk-DYRKDCJI.js';
|
|
3
3
|
|
|
4
4
|
// src/snapshot/index.ts
|
|
5
5
|
var DEFAULT_SNAPSHOT_KEY = "@@snapshot";
|
|
@@ -89,5 +89,5 @@ function createSnapshot(config) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export { createSnapshot, snapshotFailed, snapshotLoadFailed, snapshotLoaded, snapshotSaved };
|
|
92
|
-
//# sourceMappingURL=chunk-
|
|
93
|
-
//# sourceMappingURL=chunk-
|
|
92
|
+
//# sourceMappingURL=chunk-5KVHID6A.js.map
|
|
93
|
+
//# sourceMappingURL=chunk-5KVHID6A.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-EIZJTYKR.js","sourcesContent":["/**\n * @demlik/tea/snapshot — periodic state checkpoint to a host store (R2/KV-shaped).\n *\n * The problem it solves: a long-running machine (a multi-stage run, a paginated\n * crawl, a saga) lives in TEA's in-memory `State` and is persisted as ONE blob\n * by the substrate's `Store<S>` on every transition. That blob is fine for\n * crash recovery of the *runtime* — but it is NOT a coarse, named checkpoint a\n * human (or a separate recovery job) can point a restart at. `snapshot` adds\n * the \"every N units of progress, write a durable checkpoint to R2/KV\"\n * discipline on top, so a crashed long run restarts from the last checkpoint\n * instead of from zero (the field-guide pain row).\n *\n * Shape: this is the same knob contract every L2 composition follows —\n * `createSnapshot(config)` hands back a `{ init, <verbs>, handlers }` bundle you\n * splice into your machine. The state it owns is a Model slice (durable +\n * replayable, the two non-negotiables); the verbs are PURE decisions; the I/O\n * (the actual `put` to the store) lives only in `handlers`.\n *\n * It is a LEAF module: it imports only the core substrate (`../index`, for\n * `Cmd` / `Interpret`) and no sibling subpath. (It does NOT reuse\n * `@demlik/tea/retry-backoff` — that is an even-leafier utility that depends on\n * nothing at all, not even the core; the kinship here is \"leaf with injected\n * time\", not a shared dependency.)\n * Nothing here reads the clock or the RNG on its own behalf — time enters as an\n * `at`/`now` parameter at the verb boundary and the store I/O is realized only\n * when the runtime performs the emitted `Cmd`. That keeps every verb\n * deterministic (invariant 2) and the checkpoint write a `Cmd` the runtime\n * interprets (invariant 3).\n *\n * The Store port is deliberately ABSTRACT — `{ get, put }`, the lowest common\n * shape of R2 and KV — so this module never imports `@cloudflare/workers-types`\n * and can be checkpointed to anything (R2, KV, a DO sub-key, an in-memory map in\n * tests). The host binds the concrete adapter at `handlers({ store })`.\n *\n * Typical wiring:\n *\n * const snap = createSnapshot<RunState>({ every: 25, key: \"run/checkpoint\" });\n *\n * // machine state:\n * type State = { run: RunState; snapshot: SnapshotState };\n * init: (loaded) => loaded ? [loaded, []] : [{ run: initRun(), snapshot: snap.init() }, []],\n *\n * // in `update`, on each progress Msg, fold the snapshot slice:\n * progress: (s, msg) => {\n * const run = advance(s.run, msg);\n * const [snapshot, cmds] = snap.record(s.snapshot, run, msg.at);\n * return [{ ...s, run, snapshot }, cmds];\n * },\n * // the write completed → durable ack folds back through update:\n * snapshot_saved: (s, msg) => {\n * const [snapshot, cmds] = snap.confirm(s.snapshot, msg);\n * return [{ ...s, snapshot }, cmds];\n * },\n *\n * // RECOVERY — the half that makes a checkpoint a restart point, not just a\n * // backup. A `boot` Msg the host dispatches once after `run(...)` asks the\n * // store for the durable checkpoint; the answer folds the recovered payload\n * // back into the run slice:\n * boot: (s) => snap.requestLoad(s), // emits snapshot_load Cmd\n * snapshot_loaded: (s, msg) => {\n * if (msg.payload === null) return [s, []]; // no checkpoint → keep fresh init\n * const [snapshot, cmds] = snap.boot(s.snapshot);\n * return [{ ...s, run: msg.payload, snapshot }, cmds];\n * },\n * snapshot_load_failed: (s) => [s, []], // errors are data — decide here\n *\n * // splice the I/O — both effect cells (write + load):\n * interpret: { ...snap.handlers({ store: r2Adapter }) },\n */\n\nimport { Cmd, type Interpret, tryInterpret } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config — the knob.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot knob.\n *\n * `every` is the cadence: a checkpoint is written once `every` units of\n * progress have accumulated since the last one (see `record`). It is the\n * `snapshotEvery` field `monitored-run` threads through to this module.\n *\n * `key` is the store key the checkpoint is written under. A single machine\n * keeps a single rolling checkpoint, so the key is fixed at config time rather\n * than passed per write — the latest checkpoint overwrites the previous one\n * (R2/KV are last-write-wins on a key). Defaults to `\"@@snapshot\"`, mirroring\n * `doStore`'s `@@state` default-key convention.\n */\nexport interface SnapshotConfig {\n /**\n * Write a checkpoint once this many progress units have accumulated since the\n * last write. `every: 1` checkpoints on every `record`; a value `<= 0` is\n * treated as `1` (every progress unit triggers a write) — a non-positive\n * cadence has no other sensible meaning, and clamping keeps `record` from\n * never checkpointing on a misconfigured `0`.\n */\n readonly every: number;\n /** Store key the rolling checkpoint is written under. Defaults to `\"@@snapshot\"`. */\n readonly key?: string;\n}\n\nconst DEFAULT_SNAPSHOT_KEY = \"@@snapshot\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Slice — the Model field this knob owns.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot bookkeeping slice. Plain data — JSON-serializable so it rides\n * inside the consumer's `Store<S>` blob and survives DO eviction / reload (the\n * \"durable\" non-negotiable). It carries NO payload: the thing being\n * checkpointed lives elsewhere in the machine's state; this slice only tracks\n * *when* to checkpoint and *what has been acknowledged durable*.\n *\n * - `sinceLast` — progress units accumulated since the last checkpoint\n * DECISION (reset to 0 the moment `record` decides to write, not when the\n * write lands). The cadence counter.\n * - `seq` — monotonic checkpoint sequence. Bumped each time a write is\n * emitted, so the write Cmd and its later acknowledgement can be matched\n * even if several are in flight. The \"version\" of the latest emitted\n * checkpoint.\n * - `lastSavedSeq` — the highest `seq` whose write the host has confirmed\n * durable (via `confirm`). `null` until the first confirmed write. The gap\n * `seq - lastSavedSeq` is the \"writes emitted but not yet acknowledged\"\n * depth — observable for backpressure / health.\n * - `lastSavedAt` — the `at` of the most recent confirmed write (epoch ms),\n * `null` until the first. Carried for observability, never compared inside\n * a verb (time is injected, never read here).\n */\nexport interface SnapshotState {\n readonly sinceLast: number;\n readonly seq: number;\n readonly lastSavedSeq: number | null;\n readonly lastSavedAt: number | null;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Store port — abstract R2/KV-shaped checkpoint sink.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The abstract checkpoint sink — the intersection of R2's and KV's surface,\n * narrowed to exactly the two methods a rolling checkpoint needs. Deliberately\n * NOT the substrate's `Store<S>` (`{ load, save, migrate }`): that is the\n * runtime's whole-state persistence boundary keyed at a fixed cell, whereas a\n * checkpoint store is keyed, value-typed, and host-supplied.\n *\n * - `put(key, value)` — write the checkpoint. Last-write-wins on `key`.\n * - `get(key)` — read the latest checkpoint back, or `null` if none. Used by\n * a recovery path to seed `init`'s `loaded` from the durable checkpoint\n * when the runtime's own `Store<S>` blob is gone.\n *\n * No `@cloudflare/workers-types` import — an R2 bucket, a KV namespace, a DO\n * sub-key, or an in-memory `Map` all satisfy this shape with a 3-line adapter\n * at the host.\n */\nexport interface SnapshotStore<V> {\n get(key: string): Promise<V | null>;\n put(key: string, value: V): Promise<void>;\n}\n\n/**\n * The ports `handlers` binds. Just the store today — named so a future port\n * (a metrics sink, a clock) can grow without breaking the `handlers(ports)`\n * call shape.\n */\nexport interface SnapshotPorts<V> {\n readonly store: SnapshotStore<V>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmd + Msg — the write effect and its durable acknowledgement.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The checkpoint-write Cmd a `record` / `force` decision emits. Plain data\n * (invariant 1 — Cmds are data, not closures): it carries the `payload` to\n * write, the `key` to write it under, the `seq` it represents, and the `at` it\n * was decided. The handler performs the `put` and routes the result back to a\n * `confirm` / fail Msg.\n *\n * `payload` is `V` — the consumer's serializable checkpoint value (the run\n * state, the cursor, whatever the machine wants restartable). The verb takes it\n * as an argument so the snapshot slice never has to hold a copy of the\n * machine's domain state.\n */\nexport type SnapshotWriteCmd<V> = Cmd<\"snapshot_write\"> & {\n readonly key: string;\n readonly seq: number;\n readonly at: number;\n readonly payload: V;\n};\n\n/**\n * The Msg the write handler dispatches on a SUCCESSFUL `put`. Fold it back\n * through `confirm(state, msg)` to advance `lastSavedSeq` / `lastSavedAt`. The\n * `seq` matches the emitting Cmd so a late acknowledgement of an older write\n * can never regress a newer confirmed one.\n */\nexport type SnapshotSavedMsg = {\n readonly type: \"snapshot_saved\";\n readonly seq: number;\n readonly at: number;\n};\n\n/**\n * The Msg the write handler dispatches on a FAILED `put`. Errors are data\n * (never swallowed): the consumer's reducer decides whether a failed checkpoint\n * is fatal, retryable (compose `@demlik/tea/retry-backoff`), or merely logged.\n * `seq` identifies which checkpoint failed; `error` is carried, never\n * interpreted here.\n */\nexport type SnapshotFailedMsg = {\n readonly type: \"snapshot_failed\";\n readonly seq: number;\n readonly error: unknown;\n};\n\n/**\n * The checkpoint-READ Cmd a `requestLoad` decision emits — the recovery half of\n * the module. Plain data (invariant 1): it carries only the `key` to read. The\n * handler performs `store.get(key)` and routes the result back to a\n * `snapshot_loaded` (Ok, payload-or-null) / `snapshot_load_failed` (Err) Msg.\n *\n * Distinct from `snapshot_write`: a write carries a payload OUT to the store; a\n * load asks the store to hand a payload BACK. The consumer dispatches the\n * triggering Msg once on resume (a `boot` Msg, never `init`'s rehydrate branch —\n * invariant 2), folds the recovered payload into its run slice, then continues.\n */\nexport type SnapshotLoadCmd = Cmd<\"snapshot_load\"> & {\n readonly key: string;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` RESOLVES. `payload` is\n * the recovered checkpoint value, or `null` when the store has no checkpoint\n * under `key` (fresh boot — never checkpointed, or a wiped store). The consumer\n * folds this into `init`'s loaded branch: `payload === null` → keep the fresh\n * `init` state; non-null → seed the run slice from the durable checkpoint and\n * `boot` the snapshot slice's cadence counter.\n *\n * A `null` payload is NOT an error — \"no checkpoint yet\" is a normal first-run\n * state, distinct from \"the read failed\" (`snapshot_load_failed`).\n */\nexport type SnapshotLoadedMsg<V> = {\n readonly type: \"snapshot_loaded\";\n readonly payload: V | null;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` REJECTS. Errors are data\n * (never swallowed — the field-guide rule): a failed recovery read is a visible\n * signal the consumer's reducer decides on (fatal-abort, retry via\n * `@demlik/tea/retry-backoff`, or boot-fresh-and-log). `error` is carried, never\n * interpreted here. Kept separate from `snapshot_loaded`'s `null` so \"the store\n * has no checkpoint\" can never masquerade as \"the read failed\".\n */\nexport type SnapshotLoadFailedMsg = {\n readonly type: \"snapshot_load_failed\";\n readonly error: unknown;\n};\n\n/** Construct a `snapshot_saved` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotSaved(seq: number, at: number): SnapshotSavedMsg {\n return { type: \"snapshot_saved\", seq, at };\n}\n\n/** Construct a `snapshot_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotFailed(seq: number, error: unknown): SnapshotFailedMsg {\n return { type: \"snapshot_failed\", seq, error };\n}\n\n/** Construct a `snapshot_loaded` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoaded<V>(payload: V | null): SnapshotLoadedMsg<V> {\n return { type: \"snapshot_loaded\", payload };\n}\n\n/** Construct a `snapshot_load_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoadFailed(error: unknown): SnapshotLoadFailedMsg {\n return { type: \"snapshot_load_failed\", error };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The combinator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The bundle `createSnapshot` returns — the uniform knob contract. `subs` is\n * omitted: a checkpoint is driven by progress (a `record` call from the\n * consumer's reducer), not by a timer, so this module has no subscriptions.\n */\nexport interface SnapshotKnob<V> {\n /** The initial slice — zero progress, no checkpoint emitted or confirmed yet. */\n init(): SnapshotState;\n /**\n * Account one unit of progress. PURE. Increments `sinceLast`; once the\n * accumulated count reaches the configured cadence, DECIDES to checkpoint:\n * resets `sinceLast` to 0, bumps `seq`, and emits a single `snapshot_write`\n * Cmd carrying `payload` + `key` + the new `seq` + `at`. Below the cadence it\n * returns the bumped slice and NO Cmd.\n *\n * `payload` is the checkpoint value to persist (the consumer's domain state);\n * `at` is the caller-stamped time the progress occurred (epoch ms) — injected,\n * never read from the clock here.\n */\n record(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Force a checkpoint regardless of the cadence counter. PURE. Always bumps\n * `seq`, resets `sinceLast`, and emits the write Cmd. Use on a phase boundary\n * the cadence would otherwise miss — e.g. \"the run just finished, checkpoint\n * the terminal state now\" — so the last durable checkpoint is the final state,\n * not whatever the modulo happened to land on.\n */\n force(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Record a confirmed durable write. PURE. Advances `lastSavedSeq` /\n * `lastSavedAt` to the acknowledged `seq` / `at`, but only forward: an\n * out-of-order acknowledgement of an OLDER `seq` (a slow write landing after a\n * newer one) is ignored, so the confirmed watermark is monotonic. Fold the\n * `snapshot_saved` Msg here.\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — an ack emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_saved` cell without the caller\n * re-wrapping a bare State.\n */\n confirm(\n state: SnapshotState,\n msg: SnapshotSavedMsg,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Resume after a reload. PURE. Resets `sinceLast` to 0 because any\n * un-checkpointed progress accumulated before the crash is moot — recovery\n * resumes from the last DURABLE checkpoint (`get` from the store), so the\n * cadence counter starts fresh from there. `seq` / `lastSavedSeq` /\n * `lastSavedAt` are preserved (they describe what is durable, which a reload\n * does not change). Call from a `boot` Msg the host dispatches once after\n * `run(...)`, never from `init`'s rehydrate branch (invariant 2).\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — a resume emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_loaded` cell without the caller\n * re-wrapping a bare State.\n */\n boot(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Ask the store for the durable checkpoint. PURE. Emits a single\n * `snapshot_load` Cmd carrying the configured `key`; returns the slice\n * UNCHANGED (a read decides nothing about the cadence bookkeeping — only the\n * later `snapshot_loaded` fold, via `boot`, touches the slice). This is the\n * entry point of the recovery loop: the consumer dispatches a `boot` Msg once\n * after `run(...)`, folds `requestLoad` here, and the emitted Cmd's handler\n * reads `store.get(key)` and dispatches `snapshot_loaded` / `snapshot_load_failed`.\n *\n * Returns `readonly [SnapshotState, readonly Cmd[]]` (the reducer-cell shape)\n * so it drops straight into a `boot: (s) => snap.requestLoad(s)` cell.\n */\n requestLoad(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * The `interpret` map for this knob's two effect Cmds:\n *\n * - `snapshot_write` — binds the store and performs the `put`, routing\n * Ok → `snapshot_saved` / Err → `snapshot_failed`.\n * - `snapshot_load` — binds the store and performs the `get`, routing\n * Ok → `snapshot_loaded` (payload-or-null) / Err → `snapshot_load_failed`.\n *\n * Railway throughout — neither effect throws into the runtime; the outcome is\n * always a Msg. Spread into your machine's `interpret`.\n */\n handlers(\n ports: SnapshotPorts<V>,\n ): Interpret<\n | SnapshotSavedMsg\n | SnapshotFailedMsg\n | SnapshotLoadedMsg<V>\n | SnapshotLoadFailedMsg,\n SnapshotWriteCmd<V> | SnapshotLoadCmd,\n unknown\n >;\n}\n\n/**\n * Create a snapshot knob from a `SnapshotConfig`. The returned bundle is spliced\n * into a consumer machine: `init()` seeds the slice, `record` / `force` are\n * folded in reducer cells, `confirm` / `boot` handle the acknowledgement +\n * resume Msgs, and `handlers({ store })` supplies the write `interpret` cell.\n */\nexport function createSnapshot<V>(config: SnapshotConfig): SnapshotKnob<V> {\n // Clamp the cadence to >= 1 at construction so `record`'s threshold check is\n // branchless and a misconfigured `0` / negative `every` checkpoints on every\n // progress unit instead of never.\n const every = config.every >= 1 ? config.every : 1;\n const key = config.key ?? DEFAULT_SNAPSHOT_KEY;\n\n /**\n * Emit the write Cmd for a checkpoint decision. Shared by `record`'s\n * cadence-hit branch and `force`. Bumps `seq`, resets `sinceLast`, returns the\n * new slice + the single `snapshot_write` Cmd. The input slice is never\n * mutated (invariant 1).\n */\n function emitWrite(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]] {\n const seq = state.seq + 1;\n const next: SnapshotState = { ...state, sinceLast: 0, seq };\n const cmd: SnapshotWriteCmd<V> = {\n type: \"snapshot_write\",\n key,\n seq,\n at,\n payload,\n };\n return [next, [cmd]];\n }\n\n return {\n init(): SnapshotState {\n return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };\n },\n\n record(state, payload, at) {\n const sinceLast = state.sinceLast + 1;\n // Below the cadence: accumulate, emit nothing.\n if (sinceLast < every) {\n return [{ ...state, sinceLast }, Cmd.none];\n }\n // Cadence reached: checkpoint. `emitWrite` resets the counter + bumps seq.\n return emitWrite(state, payload, at);\n },\n\n force(state, payload, at) {\n return emitWrite(state, payload, at);\n },\n\n confirm(state, msg) {\n // Forward-only watermark: ignore a stale ack of an older seq so a slow\n // write landing after a newer one can't regress `lastSavedSeq`.\n if (state.lastSavedSeq !== null && msg.seq <= state.lastSavedSeq) {\n return [state, Cmd.none];\n }\n return [\n { ...state, lastSavedSeq: msg.seq, lastSavedAt: msg.at },\n Cmd.none,\n ];\n },\n\n boot(state) {\n return [{ ...state, sinceLast: 0 }, Cmd.none];\n },\n\n requestLoad(state) {\n // A read decides nothing about the cadence bookkeeping — return the slice\n // unchanged and emit the single read Cmd. The recovered payload folds back\n // through `snapshot_loaded` (the consumer's reducer), not here.\n const cmd: SnapshotLoadCmd = { type: \"snapshot_load\", key };\n return [state, [cmd]];\n },\n\n handlers(ports) {\n const { store } = ports;\n return {\n // The write effect. Performs the `put`; on success the saved `seq`/`at`\n // ride the `snapshot_saved` Msg back into `confirm`; on failure the\n // error rides `snapshot_failed`. Railway via the core's `tryInterpret`\n // (matching every other module's handler) — the handler always resolves\n // with a Msg, never rejects into the runtime.\n snapshot_write: tryInterpret<\n SnapshotWriteCmd<V>,\n void,\n SnapshotSavedMsg | SnapshotFailedMsg,\n unknown\n >(\n (cmd) => store.put(cmd.key, cmd.payload),\n (_value, cmd) => snapshotSaved(cmd.seq, cmd.at),\n (error, cmd) => snapshotFailed(cmd.seq, error),\n ),\n // The read effect — the recovery half. Performs the `get`; on success\n // the recovered payload (or `null` when no checkpoint exists) rides the\n // `snapshot_loaded` Msg the consumer folds into `init`'s loaded branch;\n // on failure the error rides `snapshot_load_failed`. Same Railway\n // discipline via `tryInterpret` — the read never rejects into the runtime.\n snapshot_load: tryInterpret<\n SnapshotLoadCmd,\n V | null,\n SnapshotLoadedMsg<V> | SnapshotLoadFailedMsg,\n unknown\n >(\n (cmd) => store.get(cmd.key),\n (payload) => snapshotLoaded(payload),\n (error) => snapshotLoadFailed(error),\n ),\n };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-5KVHID6A.js","sourcesContent":["/**\n * @demlik/tea/snapshot — periodic state checkpoint to a host store (R2/KV-shaped).\n *\n * The problem it solves: a long-running machine (a multi-stage run, a paginated\n * crawl, a saga) lives in TEA's in-memory `State` and is persisted as ONE blob\n * by the substrate's `Store<S>` on every transition. That blob is fine for\n * crash recovery of the *runtime* — but it is NOT a coarse, named checkpoint a\n * human (or a separate recovery job) can point a restart at. `snapshot` adds\n * the \"every N units of progress, write a durable checkpoint to R2/KV\"\n * discipline on top, so a crashed long run restarts from the last checkpoint\n * instead of from zero (the field-guide pain row).\n *\n * Shape: this is the same knob contract every L2 composition follows —\n * `createSnapshot(config)` hands back a `{ init, <verbs>, handlers }` bundle you\n * splice into your machine. The state it owns is a Model slice (durable +\n * replayable, the two non-negotiables); the verbs are PURE decisions; the I/O\n * (the actual `put` to the store) lives only in `handlers`.\n *\n * It is a LEAF module: it imports only the core substrate (`../index`, for\n * `Cmd` / `Interpret`) and no sibling subpath. (It does NOT reuse\n * `@demlik/tea/retry-backoff` — that is an even-leafier utility that depends on\n * nothing at all, not even the core; the kinship here is \"leaf with injected\n * time\", not a shared dependency.)\n * Nothing here reads the clock or the RNG on its own behalf — time enters as an\n * `at`/`now` parameter at the verb boundary and the store I/O is realized only\n * when the runtime performs the emitted `Cmd`. That keeps every verb\n * deterministic (invariant 2) and the checkpoint write a `Cmd` the runtime\n * interprets (invariant 3).\n *\n * The Store port is deliberately ABSTRACT — `{ get, put }`, the lowest common\n * shape of R2 and KV — so this module never imports `@cloudflare/workers-types`\n * and can be checkpointed to anything (R2, KV, a DO sub-key, an in-memory map in\n * tests). The host binds the concrete adapter at `handlers({ store })`.\n *\n * Typical wiring:\n *\n * const snap = createSnapshot<RunState>({ every: 25, key: \"run/checkpoint\" });\n *\n * // machine state:\n * type State = { run: RunState; snapshot: SnapshotState };\n * init: (loaded) => loaded ? [loaded, []] : [{ run: initRun(), snapshot: snap.init() }, []],\n *\n * // in `update`, on each progress Msg, fold the snapshot slice:\n * progress: (s, msg) => {\n * const run = advance(s.run, msg);\n * const [snapshot, cmds] = snap.record(s.snapshot, run, msg.at);\n * return [{ ...s, run, snapshot }, cmds];\n * },\n * // the write completed → durable ack folds back through update:\n * snapshot_saved: (s, msg) => {\n * const [snapshot, cmds] = snap.confirm(s.snapshot, msg);\n * return [{ ...s, snapshot }, cmds];\n * },\n *\n * // RECOVERY — the half that makes a checkpoint a restart point, not just a\n * // backup. A `boot` Msg the host dispatches once after `run(...)` asks the\n * // store for the durable checkpoint; the answer folds the recovered payload\n * // back into the run slice:\n * boot: (s) => snap.requestLoad(s), // emits snapshot_load Cmd\n * snapshot_loaded: (s, msg) => {\n * if (msg.payload === null) return [s, []]; // no checkpoint → keep fresh init\n * const [snapshot, cmds] = snap.boot(s.snapshot);\n * return [{ ...s, run: msg.payload, snapshot }, cmds];\n * },\n * snapshot_load_failed: (s) => [s, []], // errors are data — decide here\n *\n * // splice the I/O — both effect cells (write + load):\n * interpret: { ...snap.handlers({ store: r2Adapter }) },\n */\n\nimport { Cmd, type Interpret, tryInterpret } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config — the knob.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot knob.\n *\n * `every` is the cadence: a checkpoint is written once `every` units of\n * progress have accumulated since the last one (see `record`). It is the\n * `snapshotEvery` field `monitored-run` threads through to this module.\n *\n * `key` is the store key the checkpoint is written under. A single machine\n * keeps a single rolling checkpoint, so the key is fixed at config time rather\n * than passed per write — the latest checkpoint overwrites the previous one\n * (R2/KV are last-write-wins on a key). Defaults to `\"@@snapshot\"`, mirroring\n * `doStore`'s `@@state` default-key convention.\n */\nexport interface SnapshotConfig {\n /**\n * Write a checkpoint once this many progress units have accumulated since the\n * last write. `every: 1` checkpoints on every `record`; a value `<= 0` is\n * treated as `1` (every progress unit triggers a write) — a non-positive\n * cadence has no other sensible meaning, and clamping keeps `record` from\n * never checkpointing on a misconfigured `0`.\n */\n readonly every: number;\n /** Store key the rolling checkpoint is written under. Defaults to `\"@@snapshot\"`. */\n readonly key?: string;\n}\n\nconst DEFAULT_SNAPSHOT_KEY = \"@@snapshot\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Slice — the Model field this knob owns.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot bookkeeping slice. Plain data — JSON-serializable so it rides\n * inside the consumer's `Store<S>` blob and survives DO eviction / reload (the\n * \"durable\" non-negotiable). It carries NO payload: the thing being\n * checkpointed lives elsewhere in the machine's state; this slice only tracks\n * *when* to checkpoint and *what has been acknowledged durable*.\n *\n * - `sinceLast` — progress units accumulated since the last checkpoint\n * DECISION (reset to 0 the moment `record` decides to write, not when the\n * write lands). The cadence counter.\n * - `seq` — monotonic checkpoint sequence. Bumped each time a write is\n * emitted, so the write Cmd and its later acknowledgement can be matched\n * even if several are in flight. The \"version\" of the latest emitted\n * checkpoint.\n * - `lastSavedSeq` — the highest `seq` whose write the host has confirmed\n * durable (via `confirm`). `null` until the first confirmed write. The gap\n * `seq - lastSavedSeq` is the \"writes emitted but not yet acknowledged\"\n * depth — observable for backpressure / health.\n * - `lastSavedAt` — the `at` of the most recent confirmed write (epoch ms),\n * `null` until the first. Carried for observability, never compared inside\n * a verb (time is injected, never read here).\n */\nexport interface SnapshotState {\n readonly sinceLast: number;\n readonly seq: number;\n readonly lastSavedSeq: number | null;\n readonly lastSavedAt: number | null;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Store port — abstract R2/KV-shaped checkpoint sink.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The abstract checkpoint sink — the intersection of R2's and KV's surface,\n * narrowed to exactly the two methods a rolling checkpoint needs. Deliberately\n * NOT the substrate's `Store<S>` (`{ load, save, migrate }`): that is the\n * runtime's whole-state persistence boundary keyed at a fixed cell, whereas a\n * checkpoint store is keyed, value-typed, and host-supplied.\n *\n * - `put(key, value)` — write the checkpoint. Last-write-wins on `key`.\n * - `get(key)` — read the latest checkpoint back, or `null` if none. Used by\n * a recovery path to seed `init`'s `loaded` from the durable checkpoint\n * when the runtime's own `Store<S>` blob is gone.\n *\n * No `@cloudflare/workers-types` import — an R2 bucket, a KV namespace, a DO\n * sub-key, or an in-memory `Map` all satisfy this shape with a 3-line adapter\n * at the host.\n */\nexport interface SnapshotStore<V> {\n get(key: string): Promise<V | null>;\n put(key: string, value: V): Promise<void>;\n}\n\n/**\n * The ports `handlers` binds. Just the store today — named so a future port\n * (a metrics sink, a clock) can grow without breaking the `handlers(ports)`\n * call shape.\n */\nexport interface SnapshotPorts<V> {\n readonly store: SnapshotStore<V>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmd + Msg — the write effect and its durable acknowledgement.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The checkpoint-write Cmd a `record` / `force` decision emits. Plain data\n * (invariant 1 — Cmds are data, not closures): it carries the `payload` to\n * write, the `key` to write it under, the `seq` it represents, and the `at` it\n * was decided. The handler performs the `put` and routes the result back to a\n * `confirm` / fail Msg.\n *\n * `payload` is `V` — the consumer's serializable checkpoint value (the run\n * state, the cursor, whatever the machine wants restartable). The verb takes it\n * as an argument so the snapshot slice never has to hold a copy of the\n * machine's domain state.\n */\nexport type SnapshotWriteCmd<V> = Cmd<\"snapshot_write\"> & {\n readonly key: string;\n readonly seq: number;\n readonly at: number;\n readonly payload: V;\n};\n\n/**\n * The Msg the write handler dispatches on a SUCCESSFUL `put`. Fold it back\n * through `confirm(state, msg)` to advance `lastSavedSeq` / `lastSavedAt`. The\n * `seq` matches the emitting Cmd so a late acknowledgement of an older write\n * can never regress a newer confirmed one.\n */\nexport type SnapshotSavedMsg = {\n readonly type: \"snapshot_saved\";\n readonly seq: number;\n readonly at: number;\n};\n\n/**\n * The Msg the write handler dispatches on a FAILED `put`. Errors are data\n * (never swallowed): the consumer's reducer decides whether a failed checkpoint\n * is fatal, retryable (compose `@demlik/tea/retry-backoff`), or merely logged.\n * `seq` identifies which checkpoint failed; `error` is carried, never\n * interpreted here.\n */\nexport type SnapshotFailedMsg = {\n readonly type: \"snapshot_failed\";\n readonly seq: number;\n readonly error: unknown;\n};\n\n/**\n * The checkpoint-READ Cmd a `requestLoad` decision emits — the recovery half of\n * the module. Plain data (invariant 1): it carries only the `key` to read. The\n * handler performs `store.get(key)` and routes the result back to a\n * `snapshot_loaded` (Ok, payload-or-null) / `snapshot_load_failed` (Err) Msg.\n *\n * Distinct from `snapshot_write`: a write carries a payload OUT to the store; a\n * load asks the store to hand a payload BACK. The consumer dispatches the\n * triggering Msg once on resume (a `boot` Msg, never `init`'s rehydrate branch —\n * invariant 2), folds the recovered payload into its run slice, then continues.\n */\nexport type SnapshotLoadCmd = Cmd<\"snapshot_load\"> & {\n readonly key: string;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` RESOLVES. `payload` is\n * the recovered checkpoint value, or `null` when the store has no checkpoint\n * under `key` (fresh boot — never checkpointed, or a wiped store). The consumer\n * folds this into `init`'s loaded branch: `payload === null` → keep the fresh\n * `init` state; non-null → seed the run slice from the durable checkpoint and\n * `boot` the snapshot slice's cadence counter.\n *\n * A `null` payload is NOT an error — \"no checkpoint yet\" is a normal first-run\n * state, distinct from \"the read failed\" (`snapshot_load_failed`).\n */\nexport type SnapshotLoadedMsg<V> = {\n readonly type: \"snapshot_loaded\";\n readonly payload: V | null;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` REJECTS. Errors are data\n * (never swallowed — the field-guide rule): a failed recovery read is a visible\n * signal the consumer's reducer decides on (fatal-abort, retry via\n * `@demlik/tea/retry-backoff`, or boot-fresh-and-log). `error` is carried, never\n * interpreted here. Kept separate from `snapshot_loaded`'s `null` so \"the store\n * has no checkpoint\" can never masquerade as \"the read failed\".\n */\nexport type SnapshotLoadFailedMsg = {\n readonly type: \"snapshot_load_failed\";\n readonly error: unknown;\n};\n\n/** Construct a `snapshot_saved` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotSaved(seq: number, at: number): SnapshotSavedMsg {\n return { type: \"snapshot_saved\", seq, at };\n}\n\n/** Construct a `snapshot_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotFailed(seq: number, error: unknown): SnapshotFailedMsg {\n return { type: \"snapshot_failed\", seq, error };\n}\n\n/** Construct a `snapshot_loaded` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoaded<V>(payload: V | null): SnapshotLoadedMsg<V> {\n return { type: \"snapshot_loaded\", payload };\n}\n\n/** Construct a `snapshot_load_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoadFailed(error: unknown): SnapshotLoadFailedMsg {\n return { type: \"snapshot_load_failed\", error };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The combinator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The bundle `createSnapshot` returns — the uniform knob contract. `subs` is\n * omitted: a checkpoint is driven by progress (a `record` call from the\n * consumer's reducer), not by a timer, so this module has no subscriptions.\n */\nexport interface SnapshotKnob<V> {\n /** The initial slice — zero progress, no checkpoint emitted or confirmed yet. */\n init(): SnapshotState;\n /**\n * Account one unit of progress. PURE. Increments `sinceLast`; once the\n * accumulated count reaches the configured cadence, DECIDES to checkpoint:\n * resets `sinceLast` to 0, bumps `seq`, and emits a single `snapshot_write`\n * Cmd carrying `payload` + `key` + the new `seq` + `at`. Below the cadence it\n * returns the bumped slice and NO Cmd.\n *\n * `payload` is the checkpoint value to persist (the consumer's domain state);\n * `at` is the caller-stamped time the progress occurred (epoch ms) — injected,\n * never read from the clock here.\n */\n record(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Force a checkpoint regardless of the cadence counter. PURE. Always bumps\n * `seq`, resets `sinceLast`, and emits the write Cmd. Use on a phase boundary\n * the cadence would otherwise miss — e.g. \"the run just finished, checkpoint\n * the terminal state now\" — so the last durable checkpoint is the final state,\n * not whatever the modulo happened to land on.\n */\n force(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Record a confirmed durable write. PURE. Advances `lastSavedSeq` /\n * `lastSavedAt` to the acknowledged `seq` / `at`, but only forward: an\n * out-of-order acknowledgement of an OLDER `seq` (a slow write landing after a\n * newer one) is ignored, so the confirmed watermark is monotonic. Fold the\n * `snapshot_saved` Msg here.\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — an ack emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_saved` cell without the caller\n * re-wrapping a bare State.\n */\n confirm(\n state: SnapshotState,\n msg: SnapshotSavedMsg,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Resume after a reload. PURE. Resets `sinceLast` to 0 because any\n * un-checkpointed progress accumulated before the crash is moot — recovery\n * resumes from the last DURABLE checkpoint (`get` from the store), so the\n * cadence counter starts fresh from there. `seq` / `lastSavedSeq` /\n * `lastSavedAt` are preserved (they describe what is durable, which a reload\n * does not change). Call from a `boot` Msg the host dispatches once after\n * `run(...)`, never from `init`'s rehydrate branch (invariant 2).\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — a resume emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_loaded` cell without the caller\n * re-wrapping a bare State.\n */\n boot(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Ask the store for the durable checkpoint. PURE. Emits a single\n * `snapshot_load` Cmd carrying the configured `key`; returns the slice\n * UNCHANGED (a read decides nothing about the cadence bookkeeping — only the\n * later `snapshot_loaded` fold, via `boot`, touches the slice). This is the\n * entry point of the recovery loop: the consumer dispatches a `boot` Msg once\n * after `run(...)`, folds `requestLoad` here, and the emitted Cmd's handler\n * reads `store.get(key)` and dispatches `snapshot_loaded` / `snapshot_load_failed`.\n *\n * Returns `readonly [SnapshotState, readonly Cmd[]]` (the reducer-cell shape)\n * so it drops straight into a `boot: (s) => snap.requestLoad(s)` cell.\n */\n requestLoad(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * The `interpret` map for this knob's two effect Cmds:\n *\n * - `snapshot_write` — binds the store and performs the `put`, routing\n * Ok → `snapshot_saved` / Err → `snapshot_failed`.\n * - `snapshot_load` — binds the store and performs the `get`, routing\n * Ok → `snapshot_loaded` (payload-or-null) / Err → `snapshot_load_failed`.\n *\n * Railway throughout — neither effect throws into the runtime; the outcome is\n * always a Msg. Spread into your machine's `interpret`.\n */\n handlers(\n ports: SnapshotPorts<V>,\n ): Interpret<\n | SnapshotSavedMsg\n | SnapshotFailedMsg\n | SnapshotLoadedMsg<V>\n | SnapshotLoadFailedMsg,\n SnapshotWriteCmd<V> | SnapshotLoadCmd,\n unknown\n >;\n}\n\n/**\n * Create a snapshot knob from a `SnapshotConfig`. The returned bundle is spliced\n * into a consumer machine: `init()` seeds the slice, `record` / `force` are\n * folded in reducer cells, `confirm` / `boot` handle the acknowledgement +\n * resume Msgs, and `handlers({ store })` supplies the write `interpret` cell.\n */\nexport function createSnapshot<V>(config: SnapshotConfig): SnapshotKnob<V> {\n // Clamp the cadence to >= 1 at construction so `record`'s threshold check is\n // branchless and a misconfigured `0` / negative `every` checkpoints on every\n // progress unit instead of never.\n const every = config.every >= 1 ? config.every : 1;\n const key = config.key ?? DEFAULT_SNAPSHOT_KEY;\n\n /**\n * Emit the write Cmd for a checkpoint decision. Shared by `record`'s\n * cadence-hit branch and `force`. Bumps `seq`, resets `sinceLast`, returns the\n * new slice + the single `snapshot_write` Cmd. The input slice is never\n * mutated (invariant 1).\n */\n function emitWrite(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]] {\n const seq = state.seq + 1;\n const next: SnapshotState = { ...state, sinceLast: 0, seq };\n const cmd: SnapshotWriteCmd<V> = {\n type: \"snapshot_write\",\n key,\n seq,\n at,\n payload,\n };\n return [next, [cmd]];\n }\n\n return {\n init(): SnapshotState {\n return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };\n },\n\n record(state, payload, at) {\n const sinceLast = state.sinceLast + 1;\n // Below the cadence: accumulate, emit nothing.\n if (sinceLast < every) {\n return [{ ...state, sinceLast }, Cmd.none];\n }\n // Cadence reached: checkpoint. `emitWrite` resets the counter + bumps seq.\n return emitWrite(state, payload, at);\n },\n\n force(state, payload, at) {\n return emitWrite(state, payload, at);\n },\n\n confirm(state, msg) {\n // Forward-only watermark: ignore a stale ack of an older seq so a slow\n // write landing after a newer one can't regress `lastSavedSeq`.\n if (state.lastSavedSeq !== null && msg.seq <= state.lastSavedSeq) {\n return [state, Cmd.none];\n }\n return [\n { ...state, lastSavedSeq: msg.seq, lastSavedAt: msg.at },\n Cmd.none,\n ];\n },\n\n boot(state) {\n return [{ ...state, sinceLast: 0 }, Cmd.none];\n },\n\n requestLoad(state) {\n // A read decides nothing about the cadence bookkeeping — return the slice\n // unchanged and emit the single read Cmd. The recovered payload folds back\n // through `snapshot_loaded` (the consumer's reducer), not here.\n const cmd: SnapshotLoadCmd = { type: \"snapshot_load\", key };\n return [state, [cmd]];\n },\n\n handlers(ports) {\n const { store } = ports;\n return {\n // The write effect. Performs the `put`; on success the saved `seq`/`at`\n // ride the `snapshot_saved` Msg back into `confirm`; on failure the\n // error rides `snapshot_failed`. Railway via the core's `tryInterpret`\n // (matching every other module's handler) — the handler always resolves\n // with a Msg, never rejects into the runtime.\n snapshot_write: tryInterpret<\n SnapshotWriteCmd<V>,\n void,\n SnapshotSavedMsg | SnapshotFailedMsg,\n unknown\n >(\n (cmd) => store.put(cmd.key, cmd.payload),\n (_value, cmd) => snapshotSaved(cmd.seq, cmd.at),\n (error, cmd) => snapshotFailed(cmd.seq, error),\n ),\n // The read effect — the recovery half. Performs the `get`; on success\n // the recovered payload (or `null` when no checkpoint exists) rides the\n // `snapshot_loaded` Msg the consumer folds into `init`'s loaded branch;\n // on failure the error rides `snapshot_load_failed`. Same Railway\n // discipline via `tryInterpret` — the read never rejects into the runtime.\n snapshot_load: tryInterpret<\n SnapshotLoadCmd,\n V | null,\n SnapshotLoadedMsg<V> | SnapshotLoadFailedMsg,\n unknown\n >(\n (cmd) => store.get(cmd.key),\n (payload) => snapshotLoaded(payload),\n (error) => snapshotLoadFailed(error),\n ),\n };\n },\n };\n}\n"]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { MsgType } from './chunk-JELSZN25.js';
|
|
2
|
-
import { deadlineSub } from './chunk-
|
|
2
|
+
import { deadlineSub } from './chunk-SKAMZKTE.js';
|
|
3
3
|
import { asRng, recordFailure, shouldRetry, nextDelayMs, initRetry } from './chunk-5VNWZADP.js';
|
|
4
4
|
import { initBucket, tryConsume } from './chunk-2YOE5PIS.js';
|
|
5
5
|
import { initCircuit, onSuccess, onFailure, canPass } from './chunk-SC3FAFZN.js';
|
|
6
6
|
import { initCache, set, get } from './chunk-IN33Q4VJ.js';
|
|
7
|
-
import { tryInterpret } from './chunk-
|
|
7
|
+
import { tryInterpret } from './chunk-6VDCTR7N.js';
|
|
8
8
|
|
|
9
9
|
// src/resilient-call/index.ts
|
|
10
10
|
function circuitPolicy(config) {
|
|
@@ -218,5 +218,5 @@ function liftResilience(state, [slice, cmds]) {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
export { createResilientCall, liftResilience };
|
|
221
|
-
//# sourceMappingURL=chunk-
|
|
222
|
-
//# sourceMappingURL=chunk-
|
|
221
|
+
//# sourceMappingURL=chunk-6GIGQT6C.js.map
|
|
222
|
+
//# sourceMappingURL=chunk-6GIGQT6C.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resilient-call/index.ts"],"names":[],"mappings":"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","file":"chunk-6GIGQT6C.js","sourcesContent":["/**\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\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 * ## 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 { initBucket, type TokenBucket, tryConsume } from \"../rate-limit\";\nimport {\n asRng,\n initRetry,\n nextDelayMs,\n type RetryPolicy,\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 readonly retry?: RetryPolicy;\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 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 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);\n const withRetry = { ...s, retry: { ...s.retry, [key]: retry } };\n if (!shouldRetry(retry, config.retry)) {\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 { [key]: _dropped, ...retry } = s.retry;\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,5 +1,5 @@
|
|
|
1
1
|
import { MsgType } from './chunk-JELSZN25.js';
|
|
2
|
-
import { tryInterpret } from './chunk-
|
|
2
|
+
import { tryInterpret } from './chunk-6VDCTR7N.js';
|
|
3
3
|
|
|
4
4
|
// src/token-refresh/index.ts
|
|
5
5
|
function initTokenRefresh() {
|
|
@@ -57,5 +57,5 @@ function createTokenRefresh(config = {}) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export { createTokenRefresh, initTokenRefresh, refreshTokenCmd, tokenRefreshFailedMsg, tokenRefreshedMsg };
|
|
60
|
-
//# sourceMappingURL=chunk-
|
|
61
|
-
//# sourceMappingURL=chunk-
|
|
60
|
+
//# sourceMappingURL=chunk-6OUDPSSG.js.map
|
|
61
|
+
//# sourceMappingURL=chunk-6OUDPSSG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/token-refresh/index.ts"],"names":[],"mappings":";;;;AAkJO,SAAS,gBAAA,GAA+B;AAC7C,EAAA,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,KAAA,EAAO,KAAA,EAAM;AACrC;AAYO,SAAS,eAAA,GAAmC;AACjD,EAAA,OAAO,EAAE,MAAM,eAAA,EAAgB;AACjC;AAaO,SAAS,kBAAkB,KAAA,EAAiC;AACjE,EAAA,OAAO,EAAE,IAAA,EAAM,OAAA,CAAQ,cAAA,EAAgB,KAAA,EAAM;AAC/C;AAgBO,SAAS,sBAAsB,KAAA,EAAuC;AAC3E,EAAA,OAAO,EAAE,IAAA,EAAM,OAAA,CAAQ,kBAAA,EAAoB,KAAA,EAAM;AACnD;AA0BO,SAAS,kBAAA,CAAmB,MAAA,GAA6B,EAAC,EAAG;AAIlE,EAAA,MAAM,MAAA,GACJ,OAAO,MAAA,KAAW,MAAA,IAAa,OAAO,MAAA,GAAS,CAAA,GAAI,OAAO,MAAA,GAAS,CAAA;AAsBrE,EAAA,SAAS,YAAA,CAAa,OAAmB,EAAA,EAAqB;AAC5D,IAAA,IAAI,KAAA,CAAM,KAAA,KAAU,IAAA,EAAM,OAAO,IAAA;AACjC,IAAA,IAAI,KAAA,CAAM,OAAO,OAAO,IAAA;AAKxB,IAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,MAAM,KAAA,CAAM,SAAS,GAAG,OAAO,IAAA;AACpD,IAAA,OAAO,EAAA,IAAM,KAAA,CAAM,KAAA,CAAM,SAAA,GAAY,MAAA;AAAA,EACvC;AAcA,EAAA,SAAS,MAAM,KAAA,EAA+B;AAC5C,IAAA,IAAI,KAAA,CAAM,OAAO,OAAO,KAAA;AACxB,IAAA,OAAO,EAAE,GAAG,KAAA,EAAO,KAAA,EAAO,IAAA,EAAK;AAAA,EACjC;AAmBA,EAAA,SAAS,SAAA,CAAU,OAAmB,KAAA,EAA0B;AAC9D,IAAA,MAAM,UAAA,GAAoB,MAAA,CAAO,EAAA,CAAG,KAAA,CAAM,SAAA,EAAW,EAAE,CAAA,GACnD,EAAE,GAAG,KAAA,EAAO,SAAA,EAAW,CAAA,EAAE,GACzB,KAAA;AACJ,IAAA,OAAO,EAAE,GAAG,KAAA,EAAO,KAAA,EAAO,UAAA,EAAY,OAAO,KAAA,EAAM;AAAA,EACrD;AAgBA,EAAA,SAAS,WAAA,CACP,OACA,EAAA,EACuC;AACvC,IAAA,IAAI,YAAA,CAAa,KAAA,EAAO,EAAE,CAAA,EAAG;AAC3B,MAAA,OAAO,CAAC,KAAA,EAAO,CAAC,eAAA,EAAiB,CAAC,CAAA;AAAA,IACpC;AACA,IAAA,OAAO,CAAC,KAAA,EAAO,EAAE,CAAA;AAAA,EACnB;AAgBA,EAAA,SAAS,SACP,KAAA,EACsD;AACtD,IAAA,OAAO;AAAA,MACL,aAAA,EAAe,YAAA;AAAA,QAMb,MAAM,MAAM,OAAA,EAAQ;AAAA,QACpB,CAAC,KAAA,KAAU,iBAAA,CAAkB,KAAK,CAAA;AAAA,QAClC,CAAC,KAAA,KAAU,qBAAA,CAAsB,KAAK;AAAA;AACxC,KACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA;AAAA,IAEL,IAAA,EAAM,gBAAA;AAAA,IACN,YAAA;AAAA,IACA,KAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF;AACF","file":"chunk-FH5RUIIL.js","sourcesContent":["/**\n * @demlik/tea/token-refresh — a credential's lifecycle as pure state + verbs,\n * with the actual token fetch deferred to a single injected port.\n *\n * This is the L1 brick `authed-call` (#10 in the composition specs) stacks on:\n * a token slice `{ token, stale }` plus pure transitions that decide *when* a\n * credential must be re-minted, and one `handlers(ports)` splice that performs\n * the re-mint. It follows the uniform knob contract every composition wears —\n * `createTokenRefresh(config) → { init, <verbs>, handlers }` — so a consumer\n * spreads ~3 hooks into their machine instead of hand-wiring the refresh dance.\n *\n * It is a LEAF module in the same mold as `@demlik/tea/retry-backoff`: it\n * depends only on the core (`../index`), never on a sibling brick. The two\n * non-negotiables of every knob hold here by construction:\n *\n * - **Durable** — the token slice is plain JSON-serializable data, so it\n * survives a Durable Object eviction / page reload. A rehydrated machine\n * resumes knowing exactly which token it holds and whether it is stale.\n * - **Replayable** — every transition is a pure verb returning\n * `readonly [State, Cmd[]]`. No verb reads the wall clock or performs I/O,\n * so `replay` reconstructs the slice exactly. Time enters ONLY as an `at`\n * parameter the caller stamps; the network fetch enters ONLY at the\n * `handlers` boundary. This keeps the substrate's purity invariants intact\n * (invariant 2: pure transitions; invariant 4: I/O is an effect, not a\n * reducer body).\n *\n * ## The staleness model — two independent reasons to refresh\n *\n * A token becomes unusable for two unrelated reasons, and the slice tracks both\n * without conflating them:\n *\n * 1. **Expiry** — the token has a known `expiresAt` (epoch ms). It is \"near\n * expiry\" once `at >= expiresAt - skewMs`. The `skewMs` skew refreshes\n * *ahead* of the hard deadline so an in-flight call never races a token\n * that expires mid-request. This is the proactive, time-driven reason and\n * is computed purely from the injected `at` — no clock in the verb.\n *\n * 2. **Server rejection (401)** — the server rejected a request the client\n * believed was still valid (clock skew, a server-side revocation, an early\n * rotation). `on401` flips a `stale` flag regardless of `expiresAt`. This\n * is the reactive reason: the client's belief about validity was wrong, so\n * the next `needsRefresh` returns true even though the clock says the\n * token is fine.\n *\n * `needsRefresh` is the union of the two (plus \"no token at all\"). One source\n * of truth for \"should I fetch a fresh credential before the next call?\", read\n * by the consumer's reducer at the call boundary.\n *\n * Typical wiring (inside a consumer machine):\n *\n * const tr = createTokenRefresh({ skewMs: 30_000 });\n *\n * // in init:\n * init: (loaded) => [loaded ?? { auth: tr.init() }, Cmd.none],\n *\n * // in a reducer cell that is about to make an authed call:\n * before_call: (s, msg) => {\n * const [auth, cmds] = tr.ensureFresh(s.auth, msg.at);\n * return [{ ...s, auth }, cmds]; // emits a refresh Cmd iff a refresh is due\n * },\n *\n * // on a 401 from a guarded call:\n * got_401: (s) => [{ ...s, auth: tr.on401(s.auth) }, []],\n *\n * // when the refresh port resolves:\n * token_refreshed: (s, msg) => [{ ...s, auth: tr.refreshed(s.auth, msg.token) }, []],\n *\n * // splice the I/O:\n * interpret: { ...tr.handlers({ refresh: () => mySdk.mintToken() }) },\n *\n * NOT a substrate primitive: from `../index` it reuses only the `Cmd` /\n * `Interpret` types and the `tryInterpret` Railway helper (a runtime function,\n * not a type) that every effectful brick uses — and no sibling brick. It is a\n * leaf utility consumers reach via the `@demlik/tea/token-refresh` subpath.\n */\n\nimport { type Cmd, type Interpret, tryInterpret } from \"../index\";\nimport { MsgType } from \"../protocol\";\n\n/**\n * A minted credential: the opaque `value` to send on the wire, and the absolute\n * `expiresAt` (epoch milliseconds — the `Date.now()` scale) the issuer stamped\n * it with. Both fields are plain data so a `Token` is trivially serializable\n * into a consumer's `Store<S>`.\n *\n * `value` is `string` because that is the universal shape of a bearer token /\n * API key / session cookie on the wire. A consumer whose refresh port yields a\n * richer object can JSON-encode it into `value`, or keep this module's slice as\n * the \"is it fresh?\" gate and hold the richer object elsewhere in their Model.\n */\nexport interface Token {\n /** The opaque credential to present on each request (bearer token, API key). */\n readonly value: string;\n /** Absolute expiry instant, epoch milliseconds. The `Date.now()` scale. */\n readonly expiresAt: number;\n}\n\n/**\n * Pure configuration — the knob's only field. Carries no mutable state;\n * `TokenState` does. One config is shared across all the refreshes of one\n * logical credential (often a module-scope const).\n */\nexport interface TokenRefreshConfig {\n /**\n * Refresh-ahead skew, in milliseconds. A token is treated as needing a\n * refresh once `at >= expiresAt - skewMs`, i.e. `skewMs` BEFORE its hard\n * expiry. This buys headroom so a call that starts just under the wire never\n * races a token that lapses mid-flight, and absorbs small client/server clock\n * disagreement. `0` means \"refresh exactly at expiry\"; a negative value is\n * clamped to `0` (a skew that pushes the trigger past expiry is meaningless).\n *\n * @default 0\n */\n readonly skewMs?: number;\n}\n\n/**\n * The token slice this knob owns — the Model field a consumer spreads in via\n * `init()`. A two-field record rather than a phase union because the two\n * dimensions are orthogonal: a token can be present-and-fresh, present-but-stale\n * (a 401 came back), absent, or absent-and-stale (cleared after a failed\n * refresh). Every combination is meaningful, so neither field implies the\n * other's value and a discriminated union would only manufacture impossible\n * arms.\n *\n * - `token` — the currently held credential, or `null` when none has been\n * minted yet (fresh boot) or the last refresh failed.\n * - `stale` — set by `on401` when the server rejected a request the client\n * believed valid. Forces the next `needsRefresh` to true regardless of\n * `expiresAt`; cleared by `refreshed` once a new credential lands.\n *\n * Both fields `readonly` so the whole slice threads safely through a pure\n * reducer and serializes into a `Store<S>`.\n */\nexport interface TokenState {\n /** The held credential, or `null` if none is available (boot / failed refresh). */\n readonly token: Token | null;\n /** Whether a 401 has marked the held token unusable ahead of its `expiresAt`. */\n readonly stale: boolean;\n}\n\n/**\n * The starting slice: no token held, not stale. The first `needsRefresh` after\n * this returns `true` (no token), so a consumer's first guarded call mints a\n * credential before going out.\n */\nexport function initTokenRefresh(): TokenState {\n return { token: null, stale: false };\n}\n\n/**\n * The Cmd this knob emits to ask the runtime to mint a fresh token. Plain data\n * (no closure — invariant 1) carrying nothing: the refresh port takes no input,\n * and the slice the result installs is read off the follow-up Msg, not this\n * Cmd. The consumer unions this into their machine's Cmd type and lets\n * `handlers` interpret it.\n */\nexport interface RefreshTokenCmd extends Cmd<\"refresh_token\"> {}\n\n/** Construct a `refresh_token` Cmd. */\nexport function refreshTokenCmd(): RefreshTokenCmd {\n return { type: \"refresh_token\" };\n}\n\n/**\n * The Msg the refresh port dispatches on success — carries the freshly minted\n * `Token`. The consumer unions this into their Msg type and handles it by\n * folding `refreshed(state, msg.token)` into the slice.\n */\nexport interface TokenRefreshedMsg {\n readonly type: typeof MsgType.TokenRefreshed;\n readonly token: Token;\n}\n\n/** Construct a `token_refreshed` Msg carrying the new token. */\nexport function tokenRefreshedMsg(token: Token): TokenRefreshedMsg {\n return { type: MsgType.TokenRefreshed, token };\n}\n\n/**\n * The Msg the refresh port dispatches on failure — carries the rejection so the\n * consumer's reducer can decide whether to back off, surface an error, or give\n * up (errors are data: it is dispatched, never swallowed). The slice itself is\n * left untouched by a failed refresh — the held token (if any) stays held and\n * stale stays set, so the consumer can retry; clearing it is the consumer's\n * policy, expressed in their reducer.\n */\nexport interface TokenRefreshFailedMsg {\n readonly type: typeof MsgType.TokenRefreshFailed;\n readonly error: unknown;\n}\n\n/** Construct a `token_refresh_failed` Msg carrying the rejection. */\nexport function tokenRefreshFailedMsg(error: unknown): TokenRefreshFailedMsg {\n return { type: MsgType.TokenRefreshFailed, error };\n}\n\n/** The two Msgs the refresh port can produce. Union for a consumer's Msg type. */\nexport type TokenRefreshMsg = TokenRefreshedMsg | TokenRefreshFailedMsg;\n\n/** The ports this knob's `handlers` splice needs — the single refresh DI seam. */\nexport interface TokenRefreshPorts {\n /**\n * Mint a fresh credential. Called ONLY from the `refresh_token` interpret\n * handler — never from a verb — so the network I/O and any clock the issuer\n * reads stay at the effect boundary. May reject; the rejection is routed to\n * `token_refresh_failed` (errors are data), never thrown into the runtime.\n */\n readonly refresh: () => Promise<Token>;\n}\n\n/**\n * The knob: hand it a config, get back the slice initializer, the pure verbs,\n * and the `handlers` splice. Mirrors every L2 composition's uniform contract so\n * the call sites read identically across bricks.\n *\n * @example\n * const tr = createTokenRefresh({ skewMs: 30_000 });\n * const s0 = tr.init();\n * const [s1, cmds] = tr.ensureFresh(s0, Date.now()); // cmds = [refreshTokenCmd()]\n */\nexport function createTokenRefresh(config: TokenRefreshConfig = {}) {\n // Clamp once at construction: a negative skew would push the refresh trigger\n // PAST expiry, which inverts the knob's intent (\"refresh ahead\"). NaN (a\n // miswired config) collapses to 0 so `needsRefresh`'s comparison stays total.\n const skewMs =\n config.skewMs !== undefined && config.skewMs > 0 ? config.skewMs : 0;\n\n /**\n * Whether the held token must be refreshed before the next guarded call, at\n * the injected instant `at` (epoch ms). PURE — reads only, no clock, no\n * mutation. True iff ANY of:\n *\n * - no token is held (`token === null`) — nothing to present;\n * - the token was marked `stale` by a 401 — the server disagrees with the\n * client's belief that it is valid;\n * - the token's `expiresAt` is not a finite number (`NaN` / `±Infinity`) —\n * a miswired issuer or a corrupted slice; we cannot reason about when it\n * lapses, so we fail SAFE toward minting a fresh one rather than letting\n * `at >= NaN` (always `false`) or `at >= Infinity` (always `false`) mask\n * a broken expiry as \"fresh forever\";\n * - the token is at or past its skewed expiry (`at >= expiresAt - skewMs`)\n * — the proactive refresh-ahead window has opened.\n *\n * The expiry cutoff is half-open (`>=`), matching the window edges in\n * `rate-limit` / `cache` / `circuit-breaker`: a token minted to expire at `t`\n * with zero skew is considered due at exactly `t`.\n */\n function needsRefresh(state: TokenState, at: number): boolean {\n if (state.token === null) return true;\n if (state.stale) return true;\n // Non-finite expiry (NaN/±Infinity) can never be compared into a \"due\"\n // verdict — `at >= NaN` and `at >= Infinity` are both `false`, which would\n // pin a broken token as fresh forever. Fail safe: treat it as needing a\n // refresh so the next call mints a credential with a real deadline.\n if (!Number.isFinite(state.token.expiresAt)) return true;\n return at >= state.token.expiresAt - skewMs;\n }\n\n /**\n * Mark the held token unusable because a request the client believed valid\n * came back 401. PURE — returns a new slice; the input is never mutated.\n * Sets `stale` regardless of `expiresAt`, so the next `needsRefresh` returns\n * true even when the clock still says the token is fine. The token VALUE is\n * kept (not nulled) — it is the consumer's last-known credential until a\n * refresh replaces it, and keeping it lets observability show what was\n * rejected.\n *\n * Idempotent: a second 401 on an already-stale slice returns the same shape.\n * A 401 with no token held is a no-op (already needs a refresh).\n */\n function on401(state: TokenState): TokenState {\n if (state.stale) return state;\n return { ...state, stale: true };\n }\n\n /**\n * Install a freshly minted `token`, clearing `stale`. PURE — returns a new\n * slice; the input is never mutated. This is the fold for the\n * `token_refreshed` Msg: after it, `needsRefresh(state, at)` is false for any\n * `at` below the new skewed expiry, and the stale flag is gone (the new\n * credential supersedes whatever the 401 rejected).\n *\n * This is the slice's WRITE boundary for a credential, so it is where the\n * durability invariant is paid for: it normalizes a negative-zero `expiresAt`\n * to `+0` before the value enters the slice. `-0` is the one finite double\n * JSON cannot round-trip — `JSON.stringify(-0)` is `\"0\"`, which parses back as\n * `+0`, so a slice carrying `-0` would diverge from its persisted form across\n * a Durable Object eviction / page reload (invariant: the boundary parses, the\n * core trusts). `-0` and `+0` denote the same epoch-ms instant, so collapsing\n * them loses no information and makes the un-round-trippable state\n * unrepresentable in the slice by construction.\n */\n function refreshed(state: TokenState, token: Token): TokenState {\n const normalized: Token = Object.is(token.expiresAt, -0)\n ? { ...token, expiresAt: 0 }\n : token;\n return { ...state, token: normalized, stale: false };\n }\n\n /**\n * The verb a consumer calls at the call boundary: \"make sure I have a fresh\n * credential before the next request.\" PURE — returns `[next, cmds]`. When a\n * refresh is due (`needsRefresh(state, at)`), emits a single `refresh_token`\n * Cmd; otherwise emits none. The slice is returned UNCHANGED in both branches\n * — the refresh itself is async and lands later via `refreshed`, so there is\n * no in-flight phase to record here. (A consumer that wants single-flight —\n * \"don't fire a second refresh while one is pending\" — layers that in their\n * own reducer, e.g. by tracking a pending flag; this brick stays minimal and\n * lets the consumer own that policy.)\n *\n * The `at` is the caller's stamped clock reading (epoch ms), never read here —\n * keeping the verb pure and `replay`-able.\n */\n function ensureFresh(\n state: TokenState,\n at: number,\n ): readonly [TokenState, readonly Cmd[]] {\n if (needsRefresh(state, at)) {\n return [state, [refreshTokenCmd()]];\n }\n return [state, []];\n }\n\n /**\n * The interpret splice: wraps the injected `refresh` port and routes its\n * Ok/Err to `token_refreshed` / `token_refresh_failed` Msgs via the core's\n * `tryInterpret` Railway helper. Spread the result into a consumer's\n * `interpret` map:\n *\n * interpret: { ...other, ...tr.handlers({ refresh: () => sdk.mintToken() }) }\n *\n * The port is the ONLY place the network (and any clock the issuer reads) is\n * touched — every verb above stays pure. `tryInterpret` guarantees the\n * handler never rejects: a port rejection becomes a `token_refresh_failed`\n * Msg (errors are data), so the runtime's dispatch loop is never poisoned by\n * an unhandled rejection.\n */\n function handlers(\n ports: TokenRefreshPorts,\n ): Interpret<TokenRefreshMsg, RefreshTokenCmd, unknown> {\n return {\n refresh_token: tryInterpret<\n RefreshTokenCmd,\n Token,\n TokenRefreshMsg,\n unknown\n >(\n () => ports.refresh(),\n (token) => tokenRefreshedMsg(token),\n (error) => tokenRefreshFailedMsg(error),\n ),\n };\n }\n\n return {\n /** The slice initializer — spread into the consumer's `init`. */\n init: initTokenRefresh,\n needsRefresh,\n on401,\n refreshed,\n ensureFresh,\n handlers,\n };\n}\n\n/** The shape `createTokenRefresh` returns — useful for typing a held knob. */\nexport type TokenRefresh = ReturnType<typeof createTokenRefresh>;\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/token-refresh/index.ts"],"names":[],"mappings":";;;;AAkJO,SAAS,gBAAA,GAA+B;AAC7C,EAAA,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,KAAA,EAAO,KAAA,EAAM;AACrC;AAYO,SAAS,eAAA,GAAmC;AACjD,EAAA,OAAO,EAAE,MAAM,eAAA,EAAgB;AACjC;AAaO,SAAS,kBAAkB,KAAA,EAAiC;AACjE,EAAA,OAAO,EAAE,IAAA,EAAM,OAAA,CAAQ,cAAA,EAAgB,KAAA,EAAM;AAC/C;AAgBO,SAAS,sBAAsB,KAAA,EAAuC;AAC3E,EAAA,OAAO,EAAE,IAAA,EAAM,OAAA,CAAQ,kBAAA,EAAoB,KAAA,EAAM;AACnD;AA0BO,SAAS,kBAAA,CAAmB,MAAA,GAA6B,EAAC,EAAG;AAIlE,EAAA,MAAM,MAAA,GACJ,OAAO,MAAA,KAAW,MAAA,IAAa,OAAO,MAAA,GAAS,CAAA,GAAI,OAAO,MAAA,GAAS,CAAA;AAsBrE,EAAA,SAAS,YAAA,CAAa,OAAmB,EAAA,EAAqB;AAC5D,IAAA,IAAI,KAAA,CAAM,KAAA,KAAU,IAAA,EAAM,OAAO,IAAA;AACjC,IAAA,IAAI,KAAA,CAAM,OAAO,OAAO,IAAA;AAKxB,IAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,MAAM,KAAA,CAAM,SAAS,GAAG,OAAO,IAAA;AACpD,IAAA,OAAO,EAAA,IAAM,KAAA,CAAM,KAAA,CAAM,SAAA,GAAY,MAAA;AAAA,EACvC;AAcA,EAAA,SAAS,MAAM,KAAA,EAA+B;AAC5C,IAAA,IAAI,KAAA,CAAM,OAAO,OAAO,KAAA;AACxB,IAAA,OAAO,EAAE,GAAG,KAAA,EAAO,KAAA,EAAO,IAAA,EAAK;AAAA,EACjC;AAmBA,EAAA,SAAS,SAAA,CAAU,OAAmB,KAAA,EAA0B;AAC9D,IAAA,MAAM,UAAA,GAAoB,MAAA,CAAO,EAAA,CAAG,KAAA,CAAM,SAAA,EAAW,EAAE,CAAA,GACnD,EAAE,GAAG,KAAA,EAAO,SAAA,EAAW,CAAA,EAAE,GACzB,KAAA;AACJ,IAAA,OAAO,EAAE,GAAG,KAAA,EAAO,KAAA,EAAO,UAAA,EAAY,OAAO,KAAA,EAAM;AAAA,EACrD;AAgBA,EAAA,SAAS,WAAA,CACP,OACA,EAAA,EACuC;AACvC,IAAA,IAAI,YAAA,CAAa,KAAA,EAAO,EAAE,CAAA,EAAG;AAC3B,MAAA,OAAO,CAAC,KAAA,EAAO,CAAC,eAAA,EAAiB,CAAC,CAAA;AAAA,IACpC;AACA,IAAA,OAAO,CAAC,KAAA,EAAO,EAAE,CAAA;AAAA,EACnB;AAgBA,EAAA,SAAS,SACP,KAAA,EACsD;AACtD,IAAA,OAAO;AAAA,MACL,aAAA,EAAe,YAAA;AAAA,QAMb,MAAM,MAAM,OAAA,EAAQ;AAAA,QACpB,CAAC,KAAA,KAAU,iBAAA,CAAkB,KAAK,CAAA;AAAA,QAClC,CAAC,KAAA,KAAU,qBAAA,CAAsB,KAAK;AAAA;AACxC,KACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA;AAAA,IAEL,IAAA,EAAM,gBAAA;AAAA,IACN,YAAA;AAAA,IACA,KAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF;AACF","file":"chunk-6OUDPSSG.js","sourcesContent":["/**\n * @demlik/tea/token-refresh — a credential's lifecycle as pure state + verbs,\n * with the actual token fetch deferred to a single injected port.\n *\n * This is the L1 brick `authed-call` (#10 in the composition specs) stacks on:\n * a token slice `{ token, stale }` plus pure transitions that decide *when* a\n * credential must be re-minted, and one `handlers(ports)` splice that performs\n * the re-mint. It follows the uniform knob contract every composition wears —\n * `createTokenRefresh(config) → { init, <verbs>, handlers }` — so a consumer\n * spreads ~3 hooks into their machine instead of hand-wiring the refresh dance.\n *\n * It is a LEAF module in the same mold as `@demlik/tea/retry-backoff`: it\n * depends only on the core (`../index`), never on a sibling brick. The two\n * non-negotiables of every knob hold here by construction:\n *\n * - **Durable** — the token slice is plain JSON-serializable data, so it\n * survives a Durable Object eviction / page reload. A rehydrated machine\n * resumes knowing exactly which token it holds and whether it is stale.\n * - **Replayable** — every transition is a pure verb returning\n * `readonly [State, Cmd[]]`. No verb reads the wall clock or performs I/O,\n * so `replay` reconstructs the slice exactly. Time enters ONLY as an `at`\n * parameter the caller stamps; the network fetch enters ONLY at the\n * `handlers` boundary. This keeps the substrate's purity invariants intact\n * (invariant 2: pure transitions; invariant 4: I/O is an effect, not a\n * reducer body).\n *\n * ## The staleness model — two independent reasons to refresh\n *\n * A token becomes unusable for two unrelated reasons, and the slice tracks both\n * without conflating them:\n *\n * 1. **Expiry** — the token has a known `expiresAt` (epoch ms). It is \"near\n * expiry\" once `at >= expiresAt - skewMs`. The `skewMs` skew refreshes\n * *ahead* of the hard deadline so an in-flight call never races a token\n * that expires mid-request. This is the proactive, time-driven reason and\n * is computed purely from the injected `at` — no clock in the verb.\n *\n * 2. **Server rejection (401)** — the server rejected a request the client\n * believed was still valid (clock skew, a server-side revocation, an early\n * rotation). `on401` flips a `stale` flag regardless of `expiresAt`. This\n * is the reactive reason: the client's belief about validity was wrong, so\n * the next `needsRefresh` returns true even though the clock says the\n * token is fine.\n *\n * `needsRefresh` is the union of the two (plus \"no token at all\"). One source\n * of truth for \"should I fetch a fresh credential before the next call?\", read\n * by the consumer's reducer at the call boundary.\n *\n * Typical wiring (inside a consumer machine):\n *\n * const tr = createTokenRefresh({ skewMs: 30_000 });\n *\n * // in init:\n * init: (loaded) => [loaded ?? { auth: tr.init() }, Cmd.none],\n *\n * // in a reducer cell that is about to make an authed call:\n * before_call: (s, msg) => {\n * const [auth, cmds] = tr.ensureFresh(s.auth, msg.at);\n * return [{ ...s, auth }, cmds]; // emits a refresh Cmd iff a refresh is due\n * },\n *\n * // on a 401 from a guarded call:\n * got_401: (s) => [{ ...s, auth: tr.on401(s.auth) }, []],\n *\n * // when the refresh port resolves:\n * token_refreshed: (s, msg) => [{ ...s, auth: tr.refreshed(s.auth, msg.token) }, []],\n *\n * // splice the I/O:\n * interpret: { ...tr.handlers({ refresh: () => mySdk.mintToken() }) },\n *\n * NOT a substrate primitive: from `../index` it reuses only the `Cmd` /\n * `Interpret` types and the `tryInterpret` Railway helper (a runtime function,\n * not a type) that every effectful brick uses — and no sibling brick. It is a\n * leaf utility consumers reach via the `@demlik/tea/token-refresh` subpath.\n */\n\nimport { type Cmd, type Interpret, tryInterpret } from \"../index\";\nimport { MsgType } from \"../protocol\";\n\n/**\n * A minted credential: the opaque `value` to send on the wire, and the absolute\n * `expiresAt` (epoch milliseconds — the `Date.now()` scale) the issuer stamped\n * it with. Both fields are plain data so a `Token` is trivially serializable\n * into a consumer's `Store<S>`.\n *\n * `value` is `string` because that is the universal shape of a bearer token /\n * API key / session cookie on the wire. A consumer whose refresh port yields a\n * richer object can JSON-encode it into `value`, or keep this module's slice as\n * the \"is it fresh?\" gate and hold the richer object elsewhere in their Model.\n */\nexport interface Token {\n /** The opaque credential to present on each request (bearer token, API key). */\n readonly value: string;\n /** Absolute expiry instant, epoch milliseconds. The `Date.now()` scale. */\n readonly expiresAt: number;\n}\n\n/**\n * Pure configuration — the knob's only field. Carries no mutable state;\n * `TokenState` does. One config is shared across all the refreshes of one\n * logical credential (often a module-scope const).\n */\nexport interface TokenRefreshConfig {\n /**\n * Refresh-ahead skew, in milliseconds. A token is treated as needing a\n * refresh once `at >= expiresAt - skewMs`, i.e. `skewMs` BEFORE its hard\n * expiry. This buys headroom so a call that starts just under the wire never\n * races a token that lapses mid-flight, and absorbs small client/server clock\n * disagreement. `0` means \"refresh exactly at expiry\"; a negative value is\n * clamped to `0` (a skew that pushes the trigger past expiry is meaningless).\n *\n * @default 0\n */\n readonly skewMs?: number;\n}\n\n/**\n * The token slice this knob owns — the Model field a consumer spreads in via\n * `init()`. A two-field record rather than a phase union because the two\n * dimensions are orthogonal: a token can be present-and-fresh, present-but-stale\n * (a 401 came back), absent, or absent-and-stale (cleared after a failed\n * refresh). Every combination is meaningful, so neither field implies the\n * other's value and a discriminated union would only manufacture impossible\n * arms.\n *\n * - `token` — the currently held credential, or `null` when none has been\n * minted yet (fresh boot) or the last refresh failed.\n * - `stale` — set by `on401` when the server rejected a request the client\n * believed valid. Forces the next `needsRefresh` to true regardless of\n * `expiresAt`; cleared by `refreshed` once a new credential lands.\n *\n * Both fields `readonly` so the whole slice threads safely through a pure\n * reducer and serializes into a `Store<S>`.\n */\nexport interface TokenState {\n /** The held credential, or `null` if none is available (boot / failed refresh). */\n readonly token: Token | null;\n /** Whether a 401 has marked the held token unusable ahead of its `expiresAt`. */\n readonly stale: boolean;\n}\n\n/**\n * The starting slice: no token held, not stale. The first `needsRefresh` after\n * this returns `true` (no token), so a consumer's first guarded call mints a\n * credential before going out.\n */\nexport function initTokenRefresh(): TokenState {\n return { token: null, stale: false };\n}\n\n/**\n * The Cmd this knob emits to ask the runtime to mint a fresh token. Plain data\n * (no closure — invariant 1) carrying nothing: the refresh port takes no input,\n * and the slice the result installs is read off the follow-up Msg, not this\n * Cmd. The consumer unions this into their machine's Cmd type and lets\n * `handlers` interpret it.\n */\nexport interface RefreshTokenCmd extends Cmd<\"refresh_token\"> {}\n\n/** Construct a `refresh_token` Cmd. */\nexport function refreshTokenCmd(): RefreshTokenCmd {\n return { type: \"refresh_token\" };\n}\n\n/**\n * The Msg the refresh port dispatches on success — carries the freshly minted\n * `Token`. The consumer unions this into their Msg type and handles it by\n * folding `refreshed(state, msg.token)` into the slice.\n */\nexport interface TokenRefreshedMsg {\n readonly type: typeof MsgType.TokenRefreshed;\n readonly token: Token;\n}\n\n/** Construct a `token_refreshed` Msg carrying the new token. */\nexport function tokenRefreshedMsg(token: Token): TokenRefreshedMsg {\n return { type: MsgType.TokenRefreshed, token };\n}\n\n/**\n * The Msg the refresh port dispatches on failure — carries the rejection so the\n * consumer's reducer can decide whether to back off, surface an error, or give\n * up (errors are data: it is dispatched, never swallowed). The slice itself is\n * left untouched by a failed refresh — the held token (if any) stays held and\n * stale stays set, so the consumer can retry; clearing it is the consumer's\n * policy, expressed in their reducer.\n */\nexport interface TokenRefreshFailedMsg {\n readonly type: typeof MsgType.TokenRefreshFailed;\n readonly error: unknown;\n}\n\n/** Construct a `token_refresh_failed` Msg carrying the rejection. */\nexport function tokenRefreshFailedMsg(error: unknown): TokenRefreshFailedMsg {\n return { type: MsgType.TokenRefreshFailed, error };\n}\n\n/** The two Msgs the refresh port can produce. Union for a consumer's Msg type. */\nexport type TokenRefreshMsg = TokenRefreshedMsg | TokenRefreshFailedMsg;\n\n/** The ports this knob's `handlers` splice needs — the single refresh DI seam. */\nexport interface TokenRefreshPorts {\n /**\n * Mint a fresh credential. Called ONLY from the `refresh_token` interpret\n * handler — never from a verb — so the network I/O and any clock the issuer\n * reads stay at the effect boundary. May reject; the rejection is routed to\n * `token_refresh_failed` (errors are data), never thrown into the runtime.\n */\n readonly refresh: () => Promise<Token>;\n}\n\n/**\n * The knob: hand it a config, get back the slice initializer, the pure verbs,\n * and the `handlers` splice. Mirrors every L2 composition's uniform contract so\n * the call sites read identically across bricks.\n *\n * @example\n * const tr = createTokenRefresh({ skewMs: 30_000 });\n * const s0 = tr.init();\n * const [s1, cmds] = tr.ensureFresh(s0, Date.now()); // cmds = [refreshTokenCmd()]\n */\nexport function createTokenRefresh(config: TokenRefreshConfig = {}) {\n // Clamp once at construction: a negative skew would push the refresh trigger\n // PAST expiry, which inverts the knob's intent (\"refresh ahead\"). NaN (a\n // miswired config) collapses to 0 so `needsRefresh`'s comparison stays total.\n const skewMs =\n config.skewMs !== undefined && config.skewMs > 0 ? config.skewMs : 0;\n\n /**\n * Whether the held token must be refreshed before the next guarded call, at\n * the injected instant `at` (epoch ms). PURE — reads only, no clock, no\n * mutation. True iff ANY of:\n *\n * - no token is held (`token === null`) — nothing to present;\n * - the token was marked `stale` by a 401 — the server disagrees with the\n * client's belief that it is valid;\n * - the token's `expiresAt` is not a finite number (`NaN` / `±Infinity`) —\n * a miswired issuer or a corrupted slice; we cannot reason about when it\n * lapses, so we fail SAFE toward minting a fresh one rather than letting\n * `at >= NaN` (always `false`) or `at >= Infinity` (always `false`) mask\n * a broken expiry as \"fresh forever\";\n * - the token is at or past its skewed expiry (`at >= expiresAt - skewMs`)\n * — the proactive refresh-ahead window has opened.\n *\n * The expiry cutoff is half-open (`>=`), matching the window edges in\n * `rate-limit` / `cache` / `circuit-breaker`: a token minted to expire at `t`\n * with zero skew is considered due at exactly `t`.\n */\n function needsRefresh(state: TokenState, at: number): boolean {\n if (state.token === null) return true;\n if (state.stale) return true;\n // Non-finite expiry (NaN/±Infinity) can never be compared into a \"due\"\n // verdict — `at >= NaN` and `at >= Infinity` are both `false`, which would\n // pin a broken token as fresh forever. Fail safe: treat it as needing a\n // refresh so the next call mints a credential with a real deadline.\n if (!Number.isFinite(state.token.expiresAt)) return true;\n return at >= state.token.expiresAt - skewMs;\n }\n\n /**\n * Mark the held token unusable because a request the client believed valid\n * came back 401. PURE — returns a new slice; the input is never mutated.\n * Sets `stale` regardless of `expiresAt`, so the next `needsRefresh` returns\n * true even when the clock still says the token is fine. The token VALUE is\n * kept (not nulled) — it is the consumer's last-known credential until a\n * refresh replaces it, and keeping it lets observability show what was\n * rejected.\n *\n * Idempotent: a second 401 on an already-stale slice returns the same shape.\n * A 401 with no token held is a no-op (already needs a refresh).\n */\n function on401(state: TokenState): TokenState {\n if (state.stale) return state;\n return { ...state, stale: true };\n }\n\n /**\n * Install a freshly minted `token`, clearing `stale`. PURE — returns a new\n * slice; the input is never mutated. This is the fold for the\n * `token_refreshed` Msg: after it, `needsRefresh(state, at)` is false for any\n * `at` below the new skewed expiry, and the stale flag is gone (the new\n * credential supersedes whatever the 401 rejected).\n *\n * This is the slice's WRITE boundary for a credential, so it is where the\n * durability invariant is paid for: it normalizes a negative-zero `expiresAt`\n * to `+0` before the value enters the slice. `-0` is the one finite double\n * JSON cannot round-trip — `JSON.stringify(-0)` is `\"0\"`, which parses back as\n * `+0`, so a slice carrying `-0` would diverge from its persisted form across\n * a Durable Object eviction / page reload (invariant: the boundary parses, the\n * core trusts). `-0` and `+0` denote the same epoch-ms instant, so collapsing\n * them loses no information and makes the un-round-trippable state\n * unrepresentable in the slice by construction.\n */\n function refreshed(state: TokenState, token: Token): TokenState {\n const normalized: Token = Object.is(token.expiresAt, -0)\n ? { ...token, expiresAt: 0 }\n : token;\n return { ...state, token: normalized, stale: false };\n }\n\n /**\n * The verb a consumer calls at the call boundary: \"make sure I have a fresh\n * credential before the next request.\" PURE — returns `[next, cmds]`. When a\n * refresh is due (`needsRefresh(state, at)`), emits a single `refresh_token`\n * Cmd; otherwise emits none. The slice is returned UNCHANGED in both branches\n * — the refresh itself is async and lands later via `refreshed`, so there is\n * no in-flight phase to record here. (A consumer that wants single-flight —\n * \"don't fire a second refresh while one is pending\" — layers that in their\n * own reducer, e.g. by tracking a pending flag; this brick stays minimal and\n * lets the consumer own that policy.)\n *\n * The `at` is the caller's stamped clock reading (epoch ms), never read here —\n * keeping the verb pure and `replay`-able.\n */\n function ensureFresh(\n state: TokenState,\n at: number,\n ): readonly [TokenState, readonly Cmd[]] {\n if (needsRefresh(state, at)) {\n return [state, [refreshTokenCmd()]];\n }\n return [state, []];\n }\n\n /**\n * The interpret splice: wraps the injected `refresh` port and routes its\n * Ok/Err to `token_refreshed` / `token_refresh_failed` Msgs via the core's\n * `tryInterpret` Railway helper. Spread the result into a consumer's\n * `interpret` map:\n *\n * interpret: { ...other, ...tr.handlers({ refresh: () => sdk.mintToken() }) }\n *\n * The port is the ONLY place the network (and any clock the issuer reads) is\n * touched — every verb above stays pure. `tryInterpret` guarantees the\n * handler never rejects: a port rejection becomes a `token_refresh_failed`\n * Msg (errors are data), so the runtime's dispatch loop is never poisoned by\n * an unhandled rejection.\n */\n function handlers(\n ports: TokenRefreshPorts,\n ): Interpret<TokenRefreshMsg, RefreshTokenCmd, unknown> {\n return {\n refresh_token: tryInterpret<\n RefreshTokenCmd,\n Token,\n TokenRefreshMsg,\n unknown\n >(\n () => ports.refresh(),\n (token) => tokenRefreshedMsg(token),\n (error) => tokenRefreshFailedMsg(error),\n ),\n };\n }\n\n return {\n /** The slice initializer — spread into the consumer's `init`. */\n init: initTokenRefresh,\n needsRefresh,\n on401,\n refreshed,\n ensureFresh,\n handlers,\n };\n}\n\n/** The shape `createTokenRefresh` returns — useful for typing a held knob. */\nexport type TokenRefresh = ReturnType<typeof createTokenRefresh>;\n"]}
|