@demlik/tea 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/agent/index.d.ts +907 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/authed-call/index.d.ts +119 -0
- package/dist/authed-call/index.js +148 -0
- package/dist/authed-call/index.js.map +1 -0
- package/dist/await-terminal/index.d.ts +99 -0
- package/dist/await-terminal/index.js +75 -0
- package/dist/await-terminal/index.js.map +1 -0
- package/dist/batch-window/index.d.ts +259 -0
- package/dist/batch-window/index.js +43 -0
- package/dist/batch-window/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -50
- package/dist/cache/index.js.map +1 -1
- package/dist/chunk-27N2SPTN.js +222 -0
- package/dist/chunk-27N2SPTN.js.map +1 -0
- package/dist/chunk-2YOE5PIS.js +45 -0
- package/dist/chunk-2YOE5PIS.js.map +1 -0
- package/dist/chunk-3545BOYL.js +65 -0
- package/dist/chunk-3545BOYL.js.map +1 -0
- package/dist/chunk-3XJWC7VW.js +61 -0
- package/dist/chunk-3XJWC7VW.js.map +1 -0
- package/dist/chunk-5DEQ5I3U.js +132 -0
- package/dist/chunk-5DEQ5I3U.js.map +1 -0
- package/dist/chunk-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.js.map +1 -0
- package/dist/chunk-6RGBKODV.js +268 -0
- package/dist/chunk-6RGBKODV.js.map +1 -0
- package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
- package/dist/chunk-7FYQGDMF.js.map +1 -0
- package/dist/chunk-7W4LMPUW.js +186 -0
- package/dist/chunk-7W4LMPUW.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
- package/dist/chunk-DILMKOZ7.js.map +1 -0
- package/dist/chunk-EHIX7NYZ.js +93 -0
- package/dist/chunk-EHIX7NYZ.js.map +1 -0
- package/dist/chunk-HCEVHFJ7.js +54 -0
- package/dist/chunk-HCEVHFJ7.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.js.map +1 -0
- package/dist/chunk-J6527VRQ.js +56 -0
- package/dist/chunk-J6527VRQ.js.map +1 -0
- package/dist/chunk-JELSZN25.js +47 -0
- package/dist/chunk-JELSZN25.js.map +1 -0
- package/dist/chunk-K575PT4S.js +58 -0
- package/dist/chunk-K575PT4S.js.map +1 -0
- package/dist/chunk-K7Q23EQ4.js +20 -0
- package/dist/chunk-K7Q23EQ4.js.map +1 -0
- package/dist/chunk-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
- package/dist/chunk-LP7VQBRG.js.map +1 -0
- package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
- package/dist/chunk-MMH7CW5K.js.map +1 -0
- package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
- package/dist/chunk-NSDFA6M6.js.map +1 -0
- package/dist/chunk-ON24RKSE.js +60 -0
- package/dist/chunk-ON24RKSE.js.map +1 -0
- package/dist/chunk-PS23H6KQ.js +87 -0
- package/dist/chunk-PS23H6KQ.js.map +1 -0
- package/dist/chunk-QLA76GIT.js +162 -0
- package/dist/chunk-QLA76GIT.js.map +1 -0
- package/dist/chunk-S3FQN4SL.js +506 -0
- package/dist/chunk-S3FQN4SL.js.map +1 -0
- package/dist/chunk-SC3FAFZN.js +53 -0
- package/dist/chunk-SC3FAFZN.js.map +1 -0
- package/dist/chunk-SINXE47Y.js +55 -0
- package/dist/chunk-SINXE47Y.js.map +1 -0
- package/dist/chunk-SQTCS44W.js +33 -0
- package/dist/chunk-SQTCS44W.js.map +1 -0
- package/dist/chunk-TGG26XMK.js +52 -0
- package/dist/chunk-TGG26XMK.js.map +1 -0
- package/dist/chunk-UV6UWGEO.js +147 -0
- package/dist/chunk-UV6UWGEO.js.map +1 -0
- package/dist/chunk-UYWUVEP6.js +158 -0
- package/dist/chunk-UYWUVEP6.js.map +1 -0
- package/dist/chunk-VLGARN6H.js +202 -0
- package/dist/chunk-VLGARN6H.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-BoB6t7dB.d.ts +188 -0
- package/dist/deadline/index.d.ts +24 -5
- package/dist/deadline/index.js +4 -18
- package/dist/deadline/index.js.map +1 -1
- package/dist/debounce/index.d.ts +6 -0
- package/dist/debounce/index.js +1 -54
- package/dist/debounce/index.js.map +1 -1
- package/dist/devtools/index.d.ts +1 -1
- package/dist/devtools/index.js +18 -6
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +1723 -92
- package/dist/do/index.js +673 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-CO9Uh971.d.ts +180 -0
- package/dist/extension/index.d.ts +31 -13
- package/dist/extension/index.js +5 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +8 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/extension/test-utils.js +11 -3
- package/dist/extension/test-utils.js.map +1 -1
- package/dist/fan-out/index.d.ts +239 -0
- package/dist/fan-out/index.js +5 -0
- package/dist/fan-out/index.js.map +1 -0
- package/dist/idempotency/adapter.d.ts +62 -0
- package/dist/idempotency/adapter.js +4 -0
- package/dist/idempotency/adapter.js.map +1 -0
- package/dist/idempotency/index.js +1 -56
- package/dist/idempotency/index.js.map +1 -1
- package/dist/idempotent-intake/index.d.ts +186 -0
- package/dist/idempotent-intake/index.js +7 -0
- package/dist/idempotent-intake/index.js.map +1 -0
- package/dist/index.d.ts +341 -191
- package/dist/index.js +2 -1
- package/dist/llm-call/index.d.ts +309 -0
- package/dist/llm-call/index.js +14 -0
- package/dist/llm-call/index.js.map +1 -0
- package/dist/machine-viz/index.d.ts +4 -4
- package/dist/machine-viz/index.js +5 -9
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -0
- package/dist/monitored-run/index.d.ts +253 -0
- package/dist/monitored-run/index.js +10 -0
- package/dist/monitored-run/index.js.map +1 -0
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +234 -0
- package/dist/paginated-walk/index.js +15 -0
- package/dist/paginated-walk/index.js.map +1 -0
- package/dist/paginator/index.d.ts +271 -0
- package/dist/paginator/index.js +3 -0
- package/dist/paginator/index.js.map +1 -0
- package/dist/parity/index.d.ts +92 -0
- package/dist/parity/index.js +78 -0
- package/dist/parity/index.js.map +1 -0
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/arbitraries/index.js +1 -1
- package/dist/pbt/index.d.ts +15 -8
- package/dist/pbt/index.js +6 -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 -2
- package/dist/poller/index.d.ts +288 -0
- package/dist/poller/index.js +79 -0
- package/dist/poller/index.js.map +1 -0
- package/dist/prediction/index.d.ts +130 -0
- package/dist/prediction/index.js +4 -0
- package/dist/prediction/index.js.map +1 -0
- package/dist/protocol-B5YmJr0d.d.ts +45 -0
- package/dist/pure/index.d.ts +2 -0
- package/dist/pure/index.js +4 -0
- package/dist/pure/index.js.map +1 -0
- package/dist/raft/demo.d.ts +105 -0
- package/dist/raft/demo.js +172 -0
- package/dist/raft/demo.js.map +1 -0
- package/dist/raft/index.d.ts +548 -0
- package/dist/raft/index.js +7 -0
- package/dist/raft/index.js.map +1 -0
- package/dist/raft/sim.d.ts +222 -0
- package/dist/raft/sim.js +8 -0
- package/dist/raft/sim.js.map +1 -0
- package/dist/raft/timeline.d.ts +46 -0
- package/dist/raft/timeline.js +62 -0
- package/dist/raft/timeline.js.map +1 -0
- package/dist/rate-limit/index.js +1 -43
- package/dist/rate-limit/index.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +43 -20
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +192 -0
- package/dist/reconciler/index.js +166 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/recorder/index.d.ts +7 -3
- package/dist/recorder/index.js +1 -155
- package/dist/recorder/index.js.map +1 -1
- package/dist/resilient-call/index.d.ts +294 -0
- package/dist/resilient-call/index.js +13 -0
- package/dist/resilient-call/index.js.map +1 -0
- package/dist/retry-backoff/index.d.ts +42 -5
- package/dist/retry-backoff/index.js +1 -35
- package/dist/retry-backoff/index.js.map +1 -1
- 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 +238 -0
- package/dist/saga/index.js +150 -0
- package/dist/saga/index.js.map +1 -0
- package/dist/snapshot/index.d.ts +334 -0
- package/dist/snapshot/index.js +5 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/subs/index.d.ts +48 -10
- package/dist/subs/index.js +1 -1
- package/dist/testing/index.d.ts +107 -2
- package/dist/testing/index.js +204 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/throttle/index.d.ts +9 -0
- package/dist/throttle/index.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +380 -0
- package/dist/throttled-input/index.js +88 -0
- package/dist/throttled-input/index.js.map +1 -0
- package/dist/token-refresh/index.d.ts +215 -0
- package/dist/token-refresh/index.js +6 -0
- package/dist/token-refresh/index.js.map +1 -0
- package/dist/trace-replay/index.d.ts +11 -2
- package/dist/trace-replay/index.js +3 -55
- package/dist/trace-replay/index.js.map +1 -1
- package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +111 -0
- package/dist/with-deadline/index.js.map +1 -0
- package/dist/with-resilience/index.d.ts +205 -0
- package/dist/with-resilience/index.js +251 -0
- package/dist/with-resilience/index.js.map +1 -0
- package/dist/with-telemetry/index.d.ts +149 -0
- package/dist/with-telemetry/index.js +70 -0
- package/dist/with-telemetry/index.js.map +1 -0
- package/dist/work-queue/adapter.d.ts +82 -0
- package/dist/work-queue/adapter.js +4 -0
- package/dist/work-queue/adapter.js.map +1 -0
- package/dist/work-queue/index.d.ts +25 -11
- package/dist/work-queue/index.js +1 -46
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +69 -0
- package/dist/work-queue/ops.js +3 -0
- package/dist/work-queue/ops.js.map +1 -0
- package/dist/workflow/index.d.ts +450 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +168 -25
- package/dist/chunk-CMG2KMWX.js.map +0 -1
- package/dist/chunk-PYBQREFU.js.map +0 -1
- package/dist/chunk-PYOCUBQJ.js.map +0 -1
- package/dist/chunk-Y5JM3UUH.js.map +0 -1
- package/dist/chunk-ZRUST6BK.js.map +0 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createSnapshot } from './chunk-EHIX7NYZ.js';
|
|
2
|
+
import { deadlineSub } from './chunk-K7Q23EQ4.js';
|
|
3
|
+
import { queueAdapter } from './chunk-HJO6VNQI.js';
|
|
4
|
+
|
|
5
|
+
// src/monitored-run/index.ts
|
|
6
|
+
function safetyTimerId(runId, progressSeq) {
|
|
7
|
+
return `monitored:safety:${runId}:${progressSeq}`;
|
|
8
|
+
}
|
|
9
|
+
function createMonitoredRun(config = {}) {
|
|
10
|
+
const stages = config.stages ?? [];
|
|
11
|
+
const isPipeline = stages.length > 0;
|
|
12
|
+
const wq = queueAdapter();
|
|
13
|
+
const snap = config.snapshotEvery !== void 0 ? createSnapshot({
|
|
14
|
+
every: config.snapshotEvery,
|
|
15
|
+
...config.snapshotKey !== void 0 ? { key: config.snapshotKey } : {}
|
|
16
|
+
}) : null;
|
|
17
|
+
function init() {
|
|
18
|
+
return {
|
|
19
|
+
phase: "running",
|
|
20
|
+
runId: "",
|
|
21
|
+
stepStates: [],
|
|
22
|
+
startedAt: 0,
|
|
23
|
+
lastProgressAt: 0,
|
|
24
|
+
progressSeq: 0,
|
|
25
|
+
failure: null,
|
|
26
|
+
// A default snapshot slice even without the brick → uniform shape. When
|
|
27
|
+
// `snap` is null the slice is created with a throwaway cadence; its
|
|
28
|
+
// `record` is never called, so the cadence value is inert.
|
|
29
|
+
snapshot: (snap ?? createSnapshot({ every: 1 })).init()
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function seedStages(at) {
|
|
33
|
+
let queue = [];
|
|
34
|
+
stages.forEach((stage, i) => {
|
|
35
|
+
queue = wq.enqueue(queue, stage, at, `stage:${i}`).next;
|
|
36
|
+
});
|
|
37
|
+
const claimed = wq.claim(queue, at);
|
|
38
|
+
return claimed ? claimed.next : queue;
|
|
39
|
+
}
|
|
40
|
+
function currentStage(s) {
|
|
41
|
+
return s.stepStates.find((i) => i.status === "running");
|
|
42
|
+
}
|
|
43
|
+
function start(s, runId, at) {
|
|
44
|
+
const next = {
|
|
45
|
+
...s,
|
|
46
|
+
phase: "running",
|
|
47
|
+
runId,
|
|
48
|
+
stepStates: isPipeline ? seedStages(at) : [],
|
|
49
|
+
startedAt: at,
|
|
50
|
+
lastProgressAt: at,
|
|
51
|
+
progressSeq: 0,
|
|
52
|
+
failure: null
|
|
53
|
+
};
|
|
54
|
+
return [next, []];
|
|
55
|
+
}
|
|
56
|
+
function bumpProgress(s, at) {
|
|
57
|
+
return { ...s, lastProgressAt: at, progressSeq: s.progressSeq + 1 };
|
|
58
|
+
}
|
|
59
|
+
function recordSnapshot(s, payload, at) {
|
|
60
|
+
if (snap === null) return [s, []];
|
|
61
|
+
const [snapshot, cmds] = snap.record(s.snapshot, payload, at);
|
|
62
|
+
return [{ ...s, snapshot }, cmds];
|
|
63
|
+
}
|
|
64
|
+
function progress(s, payload, at) {
|
|
65
|
+
if (s.phase === "done" || s.phase === "failed") return [s, []];
|
|
66
|
+
const bumped = {
|
|
67
|
+
...bumpProgress(s, at),
|
|
68
|
+
phase: "running"
|
|
69
|
+
};
|
|
70
|
+
return recordSnapshot(bumped, payload, at);
|
|
71
|
+
}
|
|
72
|
+
function markStale(s) {
|
|
73
|
+
if (s.phase !== "running") return [s, []];
|
|
74
|
+
return [{ ...s, phase: "stale" }, []];
|
|
75
|
+
}
|
|
76
|
+
function advance(s, payload, result, at) {
|
|
77
|
+
if (s.phase === "done" || s.phase === "failed") return [s, []];
|
|
78
|
+
if (result.kind === "fail") {
|
|
79
|
+
const failed = {
|
|
80
|
+
...s,
|
|
81
|
+
phase: "failed",
|
|
82
|
+
failure: {
|
|
83
|
+
reason: "stage",
|
|
84
|
+
stage: currentStage(s)?.input,
|
|
85
|
+
error: result.error
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return [failed, []];
|
|
89
|
+
}
|
|
90
|
+
const bumped = bumpProgress(s, at);
|
|
91
|
+
if (!isPipeline) {
|
|
92
|
+
const done = { ...bumped, phase: "done" };
|
|
93
|
+
return finishWithSnapshot(done, payload, at);
|
|
94
|
+
}
|
|
95
|
+
const current = currentStage(s);
|
|
96
|
+
const afterDone = current !== void 0 ? wq.markDone(s.stepStates, current.id).next : s.stepStates;
|
|
97
|
+
const claimed = wq.claim(afterDone, at);
|
|
98
|
+
if (claimed === null) {
|
|
99
|
+
const done = {
|
|
100
|
+
...bumped,
|
|
101
|
+
phase: "done",
|
|
102
|
+
stepStates: afterDone
|
|
103
|
+
};
|
|
104
|
+
return finishWithSnapshot(done, payload, at);
|
|
105
|
+
}
|
|
106
|
+
const moved = {
|
|
107
|
+
...bumped,
|
|
108
|
+
stepStates: claimed.next
|
|
109
|
+
};
|
|
110
|
+
return recordSnapshot(moved, payload, at);
|
|
111
|
+
}
|
|
112
|
+
function finishWithSnapshot(s, payload, at) {
|
|
113
|
+
if (snap === null) return [s, []];
|
|
114
|
+
const [snapshot, cmds] = snap.force(s.snapshot, payload, at);
|
|
115
|
+
return [{ ...s, snapshot }, cmds];
|
|
116
|
+
}
|
|
117
|
+
function onDeadline(s, msg) {
|
|
118
|
+
if (s.phase === "done" || s.phase === "failed") return [s, []];
|
|
119
|
+
if (s.runId === "") return [s, []];
|
|
120
|
+
if (msg.id !== safetyTimerId(s.runId, s.progressSeq)) return [s, []];
|
|
121
|
+
const failed = {
|
|
122
|
+
...s,
|
|
123
|
+
phase: "failed",
|
|
124
|
+
failure: { reason: "deadline", at: msg.atMs }
|
|
125
|
+
};
|
|
126
|
+
return [failed, []];
|
|
127
|
+
}
|
|
128
|
+
function boot(s, at) {
|
|
129
|
+
if (s.phase === "done" || s.phase === "failed") return [s, []];
|
|
130
|
+
const [bootedSnapshot] = (snap ?? createSnapshot({ every: 1 })).boot(
|
|
131
|
+
s.snapshot
|
|
132
|
+
);
|
|
133
|
+
const reseeded = {
|
|
134
|
+
...s,
|
|
135
|
+
phase: "running",
|
|
136
|
+
lastProgressAt: at,
|
|
137
|
+
progressSeq: s.progressSeq + 1,
|
|
138
|
+
snapshot: bootedSnapshot
|
|
139
|
+
// Any stage left `running` mid-flight stays the running position; the
|
|
140
|
+
// queue is already correct (a single in-flight stage). resetRunningOp is
|
|
141
|
+
// NOT applied — unlike a work pool, a monitored run has exactly one
|
|
142
|
+
// in-flight stage and resuming it (not re-queuing it) is the correct
|
|
143
|
+
// cold-wake behavior.
|
|
144
|
+
};
|
|
145
|
+
return [reseeded, []];
|
|
146
|
+
}
|
|
147
|
+
function subs(s) {
|
|
148
|
+
if (config.deadlineMs === void 0) return [];
|
|
149
|
+
if (s.runId === "") return [];
|
|
150
|
+
if (s.phase !== "running" && s.phase !== "stale") return [];
|
|
151
|
+
return [
|
|
152
|
+
deadlineSub(
|
|
153
|
+
safetyTimerId(s.runId, s.progressSeq),
|
|
154
|
+
s.lastProgressAt + config.deadlineMs
|
|
155
|
+
)
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
function handlers(ports) {
|
|
159
|
+
const active = snap ?? createSnapshot({ every: 1 });
|
|
160
|
+
return active.handlers({ store: ports.store });
|
|
161
|
+
}
|
|
162
|
+
function confirmSnapshot(s, msg) {
|
|
163
|
+
if (snap === null) return s;
|
|
164
|
+
const [confirmed] = snap.confirm(s.snapshot, msg);
|
|
165
|
+
return { ...s, snapshot: confirmed };
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
init,
|
|
169
|
+
start,
|
|
170
|
+
progress,
|
|
171
|
+
markStale,
|
|
172
|
+
advance,
|
|
173
|
+
onDeadline,
|
|
174
|
+
boot,
|
|
175
|
+
confirmSnapshot,
|
|
176
|
+
subs,
|
|
177
|
+
handlers
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function liftRun(state, [slice, cmds]) {
|
|
181
|
+
return [{ ...state, run: slice }, cmds];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { createMonitoredRun, liftRun };
|
|
185
|
+
//# sourceMappingURL=chunk-7W4LMPUW.js.map
|
|
186
|
+
//# sourceMappingURL=chunk-7W4LMPUW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/monitored-run/index.ts"],"names":[],"mappings":"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","file":"chunk-7W4LMPUW.js","sourcesContent":["/**\n * @demlik/tea/monitored-run — a long-running operation that is BOTH staged and\n * watched: an ordered stage pipeline whose POSITION survives eviction, wrapped\n * by a no-progress safety deadline and (optionally) a periodic durable\n * checkpoint.\n *\n * This is the merge of two field-guide concerns the audit-agent machine\n * hand-wired together (`seed/audit-agent-machine`): the OUTER pipeline position\n * (`RunStage` — planning → testing → summarizing → reporting, replayed on cold\n * wake by re-emitting the one outstanding effect) and the no-progress safety\n * alarm (`subs.ts`, keyed on `progressSeq` so every progress event retires the\n * old alarm and arms a fresh one). `monitored-run` lifts both into one config-\n * driven knob with the uniform L2 contract `resilient-call` established:\n * `init()`, the verbs (`start` / `advance` / `progress` / `onDeadline` /\n * `boot`), `subs(state)`, and `handlers(ports)`.\n *\n * - It is **staged, optionally**. Omit `stages` and the run is single-shot:\n * `start` enters `running`, `advance` finishes it to `done`. Provide\n * `stages` and it becomes an ordered pipeline — each `advance` retires the\n * current stage and claims the next, finishing to `done` only when the last\n * stage is retired. The pipeline POSITION lives in the Model (a\n * `work-queue` of stage items), so an eviction mid-pipeline resumes at the\n * same stage via `boot` — the audit machine's \"pipeline position survives\n * eviction\" property, generalized.\n * - It is **watched**. While `running`, a no-progress deadline is armed at\n * `lastProgressAt + deadlineMs` and keyed on `progressSeq`. Every\n * `progress` / `advance` bumps the seq, so the substrate retires the old\n * alarm and arms a new one — a self-rearming watchdog. If the alarm fires\n * (`onDeadline`) the run goes `failed` with a deadline reason. Omit\n * `deadlineMs` → no watchdog (the `stale` phase is never reachable).\n * - It **checkpoints, optionally**. Provide `snapshotEvery` and every\n * progress unit is accounted to a `../snapshot` slice; once the cadence is\n * reached a checkpoint Cmd is emitted (interpreted by the spliced snapshot\n * `handlers`), so a crashed run restarts from the last durable checkpoint\n * rather than from zero. Omit it → no checkpointing.\n *\n * ## Why the run lifecycle is INLINE (run-tracker not extracted)\n *\n * The phase union (`running` / `done` / `failed` / `stale`) and its\n * bookkeeping (`runId`, `startedAt`, `lastProgressAt`, `progressSeq`) live\n * directly on this slice rather than behind a `run-tracker` sub-knob.\n * Extraction earns its keep when two independent axes vary; here the lifecycle\n * has exactly one anchoring axis (this run), so a separate module would be a\n * premature seam. The two genuinely independent axes — the safety DEADLINE and\n * the periodic SNAPSHOT — ARE extracted (`../deadline`, `../snapshot`), and the\n * stage list is a `../work-queue` slice (`QueueItem<Stage>[]`) whose every\n * status flip is DELEGATED to the blessed `../work-queue/ops` (`enqueueOp` /\n * `claimNextOp` / `markDoneOp`) — never re-rolled here. The lifecycle is the\n * glue between them.\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is plain data (the stage queue is a\n * JSON-serializable `QueueItem<Stage>[]`, the snapshot slice is plain\n * numbers), so it survives DO eviction / reload.\n * - **Replayable** — every transition is a pure verb returning new state +\n * Cmds; nothing here reads the clock or RNG. Time arrives as an `at` / `atMs`\n * parameter; the only clock read is `Date.now()` inside the snapshot\n * `handlers` port (the effect boundary), exactly as `../snapshot` stamps it.\n *\n * ## Typical wiring\n *\n * const run = createMonitoredRun<MyStage, MyState>({\n * stages: [\"plan\", \"test\", \"report\"],\n * deadlineMs: 10 * 60_000,\n * snapshotEvery: 25,\n * });\n *\n * init: (loaded) => loaded ? [loaded, []] : [{ run: run.init() }, []],\n * update: {\n * begin: (s, m) => lift(s, run.start(s.run, m.runId, m.at)),\n * advance: (s, m) => lift(s, run.advance(s.run, checkpoint(s), m.result, m.at)),\n * progress: (s, m) => lift(s, run.progress(s.run, checkpoint(s), m.at)),\n * run_deadline: (s, m) => lift(s, run.onDeadline(s.run, m)),\n * boot: (s, m) => lift(s, run.boot(s.run, m.at)),\n * },\n * subscriptions: (s) => run.subs(s.run),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: run.handlers({ store: r2 }),\n */\n\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n subscribeDeadline,\n} from \"../deadline\";\nimport type { Cmd, Interpret } from \"../index\";\nimport {\n createSnapshot,\n type SnapshotFailedMsg,\n type SnapshotLoadCmd,\n type SnapshotLoadedMsg,\n type SnapshotLoadFailedMsg,\n type SnapshotSavedMsg,\n type SnapshotState,\n type SnapshotStore,\n type SnapshotWriteCmd,\n} from \"../snapshot\";\nimport type { QueueItem } from \"../work-queue\";\nimport { queueAdapter } from \"../work-queue/adapter\";\n\n// ===========================================================================\n// Config — the knob. Every field optional; omit a field → omit its gate.\n// ===========================================================================\n\n/**\n * The monitored-run knob. EVERY field is optional:\n * - omit `stages` → single-shot run (no pipeline; one `advance` finishes).\n * - omit `deadlineMs` → no no-progress watchdog (the run is never auto-failed).\n * - omit `snapshotEvery`→ no periodic checkpoint (no `snapshot_write` Cmds).\n *\n * An empty config `{}` is a valid single-shot, unwatched, uncheckpointed run.\n *\n * `Stage` is the consumer's stage identifier — any JSON-serializable value\n * (a string id, a `{ kind, ... }` record). It rides through the stage queue\n * unchanged so the consumer's reducer can branch on the current stage to emit\n * that stage's outstanding effect (the audit machine's `outstandingEffect`).\n */\nexport interface MonitoredRunConfig<Stage> {\n /**\n * The ordered stages of the pipeline. Position survives eviction (lives in\n * the Model's stage queue). Omit for a single-shot run. An empty array is\n * treated as single-shot too — there is nothing to pipeline through.\n */\n readonly stages?: readonly Stage[];\n /**\n * No-progress watchdog budget, in ms. The safety deadline is armed at\n * `lastProgressAt + deadlineMs` and re-armed on every progress event. Omit\n * for no watchdog.\n */\n readonly deadlineMs?: number;\n /**\n * Checkpoint cadence — write a durable snapshot once this many progress\n * units have accumulated. Threaded straight into `../snapshot`'s `every`.\n * Omit for no checkpointing.\n */\n readonly snapshotEvery?: number;\n /**\n * Store key the rolling checkpoint is written under. Forwarded to\n * `../snapshot`'s `key`; defaults to that module's `@@snapshot`. Ignored\n * when `snapshotEvery` is omitted.\n */\n readonly snapshotKey?: string;\n}\n\n// ===========================================================================\n// Slice — the Model field this knob owns. Run lifecycle INLINE + composed bricks.\n// ===========================================================================\n\n/**\n * Why a run terminated as `failed`. Discriminated so each reason carries only\n * its own data (pattern 11):\n * - `deadline` — the no-progress watchdog fired (`onDeadline`).\n * - `stage` — the consumer reported a stage failure via `advance(..., fail)`.\n *\n * `stale` is a SEPARATE phase (not a fail reason): a run is `stale` only by an\n * explicit consumer signal that progress has wedged WITHOUT the deadline having\n * fired — a soft, recoverable observation. The deadline firing is a hard\n * terminal (`failed { reason: \"deadline\" }`).\n *\n * The `stage` variant carries the FAILING STAGE VALUE (the `Stage` itself), not\n * a positional index: done stages leave the queue (`markDoneOp` filters them),\n * so a residual index drifts as the pipeline advances. The stage value is the\n * stable, meaningful identifier — `undefined` for a single-shot run (no stage).\n */\nexport type RunFailure<Stage> =\n | { readonly reason: \"deadline\"; readonly at: number }\n | {\n readonly reason: \"stage\";\n readonly stage: Stage | undefined;\n readonly error: unknown;\n };\n\n/**\n * Run lifecycle phase. Discriminated on `phase`:\n *\n * - `running` — the run is live. Carries the current pipeline position\n * (`stepStates` is the stage queue; the single `running` item in it is the\n * current stage, `undefined` for a single-shot run) and the watchdog\n * bookkeeping (`lastProgressAt` / `progressSeq` keying the safety alarm).\n * - `stale` — a soft \"no progress observed\" mark the consumer can set and\n * clear; the run is still resumable (a `progress` clears it back to\n * `running`). Distinct from a fired deadline, which is terminal.\n * - `done` — every stage retired (or the single-shot `advance` landed).\n * - `failed` — terminal failure; `failure` carries why.\n */\nexport type RunPhase = \"running\" | \"stale\" | \"done\" | \"failed\";\n\n/**\n * The slice. Plain data end to end:\n * - `phase` — the lifecycle discriminant.\n * - `runId` — host-minted run identity, stamped at `start` (the\n * machine never mints — see the audit seed's\n * \"run identity is host-minted\").\n * - `stepStates` — the stage pipeline as a `work-queue` of stage items.\n * Empty for a single-shot run. The ONE `running` item is\n * the current stage; `done` items are retired stages\n * (they leave the queue via `markDoneOp`, so the queue\n * shrinks as the pipeline advances and is empty at\n * `done`). POSITION survives eviction because this list\n * is durable.\n * - `startedAt` — `at` of `start`. Observability; never compared in a verb.\n * - `lastProgressAt` — `at` of the most recent progress event; the watchdog\n * base. Re-armed by `progress` / `advance`.\n * - `progressSeq` — monotonic progress counter; keys the safety alarm Sub\n * id so every bump retires the old alarm and arms a new\n * one (the audit subs' `safety:{progressSeq}` discipline).\n * - `failure` — present iff `phase === \"failed\"`; the typed reason.\n * - `snapshot` — the `../snapshot` slice (cadence counter + durable\n * watermark). Always present (uniform shape); the gate\n * is simply never driven when `snapshotEvery` is omitted.\n */\nexport interface MonitoredRunState<Stage> {\n readonly phase: RunPhase;\n readonly runId: string;\n readonly stepStates: readonly QueueItem<Stage>[];\n readonly startedAt: number;\n readonly lastProgressAt: number;\n readonly progressSeq: number;\n readonly failure: RunFailure<Stage> | null;\n readonly snapshot: SnapshotState;\n}\n\n/**\n * The outcome a consumer reports to `advance`: the current stage either\n * succeeded (retire it, claim the next) or failed (terminate the run). Plain\n * data — the consumer parses its stage result at its own boundary and hands a\n * settled outcome here.\n */\nexport type StageResult =\n | { readonly kind: \"ok\" }\n | { readonly kind: \"fail\"; readonly error: unknown };\n\n// ===========================================================================\n// Cmd + Msg — re-exported from the composed bricks so consumers wire one import.\n// ===========================================================================\n\n/** The checkpoint-write Cmd, generic over the consumer's checkpoint value `V`. */\nexport type MonitoredRunCmd<V> = SnapshotWriteCmd<V>;\n\n/** The deadline Msg the safety alarm dispatches when the watchdog fires. */\nexport type MonitoredRunTimerMsg = DeadlineExceeded;\n\n/** Ports the consumer supplies to `handlers` — just the checkpoint store. */\nexport interface MonitoredRunPorts<V> {\n /** Where periodic checkpoints are written (R2 / KV / DO sub-key / Map). */\n readonly store: SnapshotStore<V>;\n}\n\n// ===========================================================================\n// Sub-id family. The safety alarm is keyed on `progressSeq` so a progress\n// event retires the old alarm and arms a new one (single-shot, re-armed by id\n// bump — the audit subs discipline).\n// ===========================================================================\n\nfunction safetyTimerId(runId: string, progressSeq: number): string {\n return `monitored:safety:${runId}:${progressSeq}`;\n}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a monitored-run knob from `config`. `Stage` is the consumer's stage\n * identifier type; `V` is the consumer's checkpoint value type (what\n * `snapshotEvery` checkpoints). Returns the uniform L2 knob contract.\n *\n * No `rng` parameter: unlike `resilient-call`, nothing here jitters — the only\n * external inputs are time (an `at` / `atMs` parameter) and the consumer's\n * stage outcomes. The clock is read only inside the snapshot `handlers` port.\n */\nexport function createMonitoredRun<Stage, V = unknown>(\n config: MonitoredRunConfig<Stage> = {},\n) {\n const stages = config.stages ?? [];\n const isPipeline = stages.length > 0;\n\n // The stage-queue seam, bound once. Every stage status flip delegates to the\n // `QueueAdapter` verbs (`wq.enqueue` / `wq.claim` / `wq.markDone`), so the\n // `QueueItem<Stage>` shape stays insulated behind them.\n const wq = queueAdapter<Stage>();\n\n // The snapshot sub-knob is built only when `snapshotEvery` is configured;\n // absence is `null` so the progress gate short-circuits to \"no checkpoint\".\n // When present, the slice still carries a default SnapshotState so the shape\n // is uniform across configs — the gate simply never records into it.\n const snap =\n config.snapshotEvery !== undefined\n ? createSnapshot<V>({\n every: config.snapshotEvery,\n ...(config.snapshotKey !== undefined\n ? { key: config.snapshotKey }\n : {}),\n })\n : null;\n\n /** The starting slice for a never-started run. `runId` is filled at `start`. */\n function init(): MonitoredRunState<Stage> {\n return {\n phase: \"running\",\n runId: \"\",\n stepStates: [],\n startedAt: 0,\n lastProgressAt: 0,\n progressSeq: 0,\n failure: null,\n // A default snapshot slice even without the brick → uniform shape. When\n // `snap` is null the slice is created with a throwaway cadence; its\n // `record` is never called, so the cadence value is inert.\n snapshot: (snap ?? createSnapshot<V>({ every: 1 })).init(),\n };\n }\n\n /**\n * Seed the stage queue from the configured stages: every stage enqueued\n * `pending`, then the first claimed `running`. `at` stamps the claim. PURE —\n * the work-queue ops are pure array transforms; ids are positional (`stage:i`)\n * so they are deterministic and replayable (no `crypto`, no `Math.random`).\n */\n function seedStages(at: number): readonly QueueItem<Stage>[] {\n let queue: readonly QueueItem<Stage>[] = [];\n stages.forEach((stage, i) => {\n queue = wq.enqueue(queue, stage, at, `stage:${i}`).next;\n });\n // Claim the first stage as the running position. A non-empty pipeline always\n // has a pending head here, so the claim succeeds.\n const claimed = wq.claim(queue, at);\n return claimed ? claimed.next : queue;\n }\n\n /** The current (running) stage item, or `undefined` for a single-shot run. */\n function currentStage(\n s: MonitoredRunState<Stage>,\n ): QueueItem<Stage> | undefined {\n return s.stepStates.find((i) => i.status === \"running\");\n }\n\n // === Verb: start =========================================================\n\n /**\n * Start (or restart) the run for `runId` at time `at`. Seeds the stage queue\n * (if a pipeline) and enters `running` with the watchdog clock primed. Stamps\n * `runId` from the host — the machine never mints (invariant: identity is\n * host-minted, parsed at the boundary). PURE.\n */\n function start(\n s: MonitoredRunState<Stage>,\n runId: string,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n const next: MonitoredRunState<Stage> = {\n ...s,\n phase: \"running\",\n runId,\n stepStates: isPipeline ? seedStages(at) : [],\n startedAt: at,\n lastProgressAt: at,\n progressSeq: 0,\n failure: null,\n };\n return [next, []];\n }\n\n /**\n * Bump the watchdog clock: set `lastProgressAt`/`progressSeq` so the safety\n * alarm Sub id changes and the substrate re-arms a fresh deadline. Shared by\n * `progress` and `advance`. PURE.\n */\n function bumpProgress(\n s: MonitoredRunState<Stage>,\n at: number,\n ): MonitoredRunState<Stage> {\n return { ...s, lastProgressAt: at, progressSeq: s.progressSeq + 1 };\n }\n\n /**\n * Account one progress unit to the snapshot slice (when the brick is\n * configured), returning the new slice + any checkpoint Cmd. `payload` is the\n * consumer's checkpoint value to persist. PURE — `snap.record` is pure, the\n * write is realized only when the runtime performs the emitted Cmd.\n */\n function recordSnapshot(\n s: MonitoredRunState<Stage>,\n payload: V,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (snap === null) return [s, []];\n const [snapshot, cmds] = snap.record(s.snapshot, payload, at);\n // `snap.record` only ever emits `SnapshotWriteCmd<V>` (its sole Cmd), but\n // its signature widens to `readonly Cmd[]`. Narrow back to this knob's\n // precise Cmd union so a consumer's host Cmd type can stay exact.\n return [{ ...s, snapshot }, cmds as readonly MonitoredRunCmd<V>[]];\n }\n\n // === Verb: progress ======================================================\n\n /**\n * Record forward progress: bump the watchdog (re-arming the safety alarm),\n * clear any `stale` mark back to `running`, and account a snapshot unit\n * (emitting a checkpoint Cmd on a cadence hit). A no-op on a settled run\n * (`done` / `failed`) — a late progress event after the run ended is ignored.\n * PURE.\n */\n function progress(\n s: MonitoredRunState<Stage>,\n payload: V,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase === \"done\" || s.phase === \"failed\") return [s, []];\n const bumped: MonitoredRunState<Stage> = {\n ...bumpProgress(s, at),\n phase: \"running\",\n };\n return recordSnapshot(bumped, payload, at);\n }\n\n // === Verb: markStale =====================================================\n\n /**\n * Mark the run `stale` — a soft \"no progress observed\" signal the consumer\n * raises WITHOUT the deadline having fired (e.g. an upstream heartbeat gap).\n * Recoverable: the next `progress` flips it back to `running`. A no-op unless\n * the run is currently `running` (a settled run cannot go stale). Does NOT\n * touch `progressSeq` — going stale is not progress, so the armed deadline\n * keeps counting toward the hard terminal. PURE.\n */\n function markStale(\n s: MonitoredRunState<Stage>,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase !== \"running\") return [s, []];\n return [{ ...s, phase: \"stale\" }, []];\n }\n\n // === Verb: advance =======================================================\n\n /**\n * Advance the pipeline on a stage outcome:\n *\n * - `result.kind === \"fail\"` → terminate `failed { reason: \"stage\" }`,\n * carrying the failing stage index + error. No further stages run.\n * - `result.kind === \"ok\"`:\n * - single-shot run → finish to `done` (and force a final checkpoint\n * when checkpointing, so the last durable snapshot is the terminal\n * state).\n * - pipeline → retire the current stage (`markDoneOp`) and claim the\n * next (`claimNextOp`). If none remain, finish to `done`. The new\n * position survives eviction.\n *\n * An advance is itself a progress event, so it bumps the watchdog (re-arming\n * the alarm at the new stage) and accounts a snapshot unit. A no-op on a\n * settled run. PURE — `at` is the only clock, ids are positional.\n */\n function advance(\n s: MonitoredRunState<Stage>,\n payload: V,\n result: StageResult,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase === \"done\" || s.phase === \"failed\") return [s, []];\n\n // Stage failure → hard terminal. Carry the failing stage VALUE (stable),\n // `undefined` for a single-shot run with no stage.\n if (result.kind === \"fail\") {\n const failed: MonitoredRunState<Stage> = {\n ...s,\n phase: \"failed\",\n failure: {\n reason: \"stage\",\n stage: currentStage(s)?.input,\n error: result.error,\n },\n };\n return [failed, []];\n }\n\n // Success: bump the watchdog first (an advance is progress), then move the\n // pipeline position.\n const bumped = bumpProgress(s, at);\n\n // Single-shot: one advance finishes the run.\n if (!isPipeline) {\n const done: MonitoredRunState<Stage> = { ...bumped, phase: \"done\" };\n // Force a terminal checkpoint so the last durable snapshot is `done`,\n // not whatever the cadence happened to land on.\n return finishWithSnapshot(done, payload, at);\n }\n\n // Pipeline: retire the running stage, claim the next.\n const current = currentStage(s);\n const afterDone =\n current !== undefined\n ? wq.markDone(s.stepStates, current.id).next\n : s.stepStates;\n const claimed = wq.claim(afterDone, at);\n\n if (claimed === null) {\n // No more stages — the pipeline is complete.\n const done: MonitoredRunState<Stage> = {\n ...bumped,\n phase: \"done\",\n stepStates: afterDone,\n };\n return finishWithSnapshot(done, payload, at);\n }\n\n // Advance to the next stage — POSITION now points at the claimed item.\n const moved: MonitoredRunState<Stage> = {\n ...bumped,\n stepStates: claimed.next,\n };\n return recordSnapshot(moved, payload, at);\n }\n\n /**\n * Finish helper: force a terminal checkpoint when checkpointing is enabled\n * (so the durable snapshot is the terminal state), else just settle. Shared\n * by single-shot finish and pipeline completion. PURE.\n */\n function finishWithSnapshot(\n s: MonitoredRunState<Stage>,\n payload: V,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (snap === null) return [s, []];\n const [snapshot, cmds] = snap.force(s.snapshot, payload, at);\n // Same narrowing as `recordSnapshot`: `snap.force` emits only the write Cmd.\n return [{ ...s, snapshot }, cmds as readonly MonitoredRunCmd<V>[]];\n }\n\n // === Verb: onDeadline ====================================================\n\n /**\n * The safety alarm fired. Two outcomes by Sub-id match:\n *\n * - the alarm matches the CURRENT `progressSeq` (no progress since it was\n * armed) → the run is wedged → terminate `failed { reason: \"deadline\" }`.\n * - the alarm matches an OLDER seq → a stale fire racing a progress event\n * that already re-armed the alarm → no-op (the substrate retired this id;\n * tolerate a fire still in flight defensively).\n *\n * A no-op on a settled run (its alarm was reconciled away; tolerate a stale\n * fire) and on a NEVER-STARTED run (empty `runId` → `subs` armed nothing, so\n * any alarm reaching it is a rogue fire that must not un-start it). PURE —\n * `msg.atMs` stamps the failure.\n */\n function onDeadline(\n s: MonitoredRunState<Stage>,\n msg: MonitoredRunTimerMsg,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase === \"done\" || s.phase === \"failed\") return [s, []];\n // A never-started run has no host-minted runId, so `subs` never armed a\n // watchdog for it. Any alarm reaching this slice is a born-live/rogue fire;\n // refuse to auto-fail a run that was never started.\n if (s.runId === \"\") return [s, []];\n // Only the alarm armed against the current progressSeq is live. A bumped\n // seq means progress re-armed it; an older fire is stale.\n if (msg.id !== safetyTimerId(s.runId, s.progressSeq)) return [s, []];\n const failed: MonitoredRunState<Stage> = {\n ...s,\n phase: \"failed\",\n failure: { reason: \"deadline\", at: msg.atMs },\n };\n return [failed, []];\n }\n\n // === Verb: boot ==========================================================\n\n /**\n * Resume after a reload (cold wake). Re-seeds the watchdog clock to `at` so\n * the safety alarm re-arms from NOW (the pre-crash `lastProgressAt` would\n * immediately fire a deadline that already elapsed during downtime — a cold\n * wake is not a no-progress wedge). Bumps `progressSeq` so the alarm gets a\n * fresh id, and resets the snapshot cadence counter (un-checkpointed\n * pre-crash progress is moot — recovery resumes from the last durable\n * checkpoint). The pipeline POSITION (`stepStates`) is preserved untouched —\n * that is the whole point of staging: resume at the same stage.\n *\n * A no-op on a settled run. Emits NO Cmd — re-emitting the current stage's\n * outstanding effect is the CONSUMER's job (it knows the per-stage Cmd), the\n * audit machine's `outstandingEffect(stage)` pattern. PURE.\n */\n function boot(\n s: MonitoredRunState<Stage>,\n at: number,\n ): readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]] {\n if (s.phase === \"done\" || s.phase === \"failed\") return [s, []];\n // `snap.boot` returns the reducer-cell shape `[SnapshotState, Cmd[]]`\n // (uniform across every sibling knob); a boot emits no Cmd, so take only the\n // slice. Assigning the tuple itself would put an ARRAY where a SnapshotState\n // belongs — a type error AND a non-round-trippable slice.\n const [bootedSnapshot] = (snap ?? createSnapshot<V>({ every: 1 })).boot(\n s.snapshot,\n );\n const reseeded: MonitoredRunState<Stage> = {\n ...s,\n phase: \"running\",\n lastProgressAt: at,\n progressSeq: s.progressSeq + 1,\n snapshot: bootedSnapshot,\n // Any stage left `running` mid-flight stays the running position; the\n // queue is already correct (a single in-flight stage). resetRunningOp is\n // NOT applied — unlike a work pool, a monitored run has exactly one\n // in-flight stage and resuming it (not re-queuing it) is the correct\n // cold-wake behavior.\n };\n return [reseeded, []];\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions: the no-progress safety deadline, armed only while\n * the run is live (`running` / `stale`) AND a `deadlineMs` is configured. The\n * Sub id is keyed on `progressSeq`, so every progress event retires the old\n * alarm and the substrate arms a fresh one at the new `lastProgressAt +\n * deadlineMs` — a self-rearming watchdog with no manual `clearTimeout`. A\n * settled run desires no subs. Wire `subscribe: { deadline: subscribeDeadline }`.\n *\n * A NEVER-STARTED slice (`init()`) is `running` with an empty `runId` and a\n * zero `lastProgressAt`. Without the `runId` gate it would arm a deadline at\n * `0 + deadlineMs` — already in the past — which fires immediately and\n * auto-fails a run that never began (the \"born live\" defect). `start` is the\n * only verb that stamps a host-minted `runId`, so `runId !== \"\"` is the exact\n * \"has this run actually been started?\" predicate: a pre-start slice arms\n * nothing, and the watchdog only exists once a real run is in flight.\n */\n function subs(s: MonitoredRunState<Stage>): readonly DeadlineSub[] {\n if (config.deadlineMs === undefined) return [];\n if (s.runId === \"\") return [];\n if (s.phase !== \"running\" && s.phase !== \"stale\") return [];\n return [\n deadlineSub(\n safetyTimerId(s.runId, s.progressSeq),\n s.lastProgressAt + config.deadlineMs,\n ),\n ];\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handler for the checkpoint write. Delegates wholesale to\n * `../snapshot`'s `handlers` (which performs the `put` and routes Ok →\n * `snapshot_saved` / Err → `snapshot_failed` via Railway — the write never\n * rejects into the runtime). When checkpointing is disabled (`snapshotEvery`\n * omitted), no `snapshot_write` Cmd is ever emitted, so the returned map is an\n * inert no-op handler — kept for a uniform `handlers(ports)` call shape.\n *\n * Fold the `snapshot_saved` Msg back through this knob's `confirmSnapshot`\n * verb to advance the durable watermark.\n */\n function handlers(\n ports: MonitoredRunPorts<V>,\n ): Interpret<\n | SnapshotSavedMsg\n | SnapshotFailedMsg\n | SnapshotLoadedMsg<V>\n | SnapshotLoadFailedMsg,\n SnapshotWriteCmd<V> | SnapshotLoadCmd,\n unknown\n > {\n const active = snap ?? createSnapshot<V>({ every: 1 });\n return active.handlers({ store: ports.store });\n }\n\n /**\n * Fold a confirmed checkpoint write into the slice — advances the snapshot\n * watermark (forward-only). A no-op pass-through when checkpointing is\n * disabled. PURE. Fold the `snapshot_saved` Msg here.\n */\n function confirmSnapshot(\n s: MonitoredRunState<Stage>,\n msg: SnapshotSavedMsg,\n ): MonitoredRunState<Stage> {\n if (snap === null) return s;\n // `snap.confirm` returns the reducer-cell shape `[SnapshotState, Cmd[]]`\n // (an ack emits nothing). Take only the slice — folding the whole tuple into\n // `snapshot` would store an ARRAY where a SnapshotState belongs (a type error\n // and a non-round-trippable slice).\n const [confirmed] = snap.confirm(s.snapshot, msg);\n return { ...s, snapshot: confirmed };\n }\n\n return {\n init,\n start,\n progress,\n markStale,\n advance,\n onDeadline,\n boot,\n confirmSnapshot,\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.run`. Convenience for the common single-slice host;\n * hosts with a differently-named field spread by hand. PURE — a thin record\n * rebuild, no clock / RNG.\n */\nexport function liftRun<\n S extends { run: MonitoredRunState<Stage> },\n Stage,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [MonitoredRunState<Stage>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, run: 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 {\n DeadlineSub,\n DeadlineExceeded,\n SnapshotSavedMsg,\n SnapshotWriteCmd,\n};\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { evictExpired, remember, recall, seen, initStore } from './chunk-K575PT4S.js';
|
|
2
|
+
|
|
3
|
+
// src/idempotency/adapter.ts
|
|
4
|
+
function idempotencyMemory() {
|
|
5
|
+
return {
|
|
6
|
+
init: (opts) => initStore(opts),
|
|
7
|
+
isSeen: (store, key, now) => seen(store, key, now),
|
|
8
|
+
recall: (store, key, now) => recall(store, key, now),
|
|
9
|
+
remember: (store, key, value, now) => remember(store, key, value, now),
|
|
10
|
+
evict: (store, now) => evictExpired(store, now)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { idempotencyMemory };
|
|
15
|
+
//# sourceMappingURL=chunk-BYQUGDD3.js.map
|
|
16
|
+
//# sourceMappingURL=chunk-BYQUGDD3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/idempotency/adapter.ts"],"names":[],"mappings":";;;AAyEO,SAAS,iBAAA,GAA6C;AAC3D,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,CAAC,IAAA,KAAS,SAAA,CAAa,IAAI,CAAA;AAAA,IACjC,MAAA,EAAQ,CAAC,KAAA,EAAO,GAAA,EAAK,QAAQ,IAAA,CAAQ,KAAA,EAAO,KAAK,GAAG,CAAA;AAAA,IACpD,MAAA,EAAQ,CAAC,KAAA,EAAO,GAAA,EAAK,QAAQ,MAAA,CAAU,KAAA,EAAO,KAAK,GAAG,CAAA;AAAA,IACtD,QAAA,EAAU,CAAC,KAAA,EAAO,GAAA,EAAK,KAAA,EAAO,QAAQ,QAAA,CAAY,KAAA,EAAO,GAAA,EAAK,KAAA,EAAO,GAAG,CAAA;AAAA,IACxE,OAAO,CAAC,KAAA,EAAO,GAAA,KAAQ,YAAA,CAAgB,OAAO,GAAG;AAAA,GACnD;AACF","file":"chunk-BYQUGDD3.js","sourcesContent":["/**\n * @demlik/tea/idempotency/adapter — the verb seam over the pure idempotency ops.\n *\n * The functions in `./index` (`seen` / `recall` / `remember` / `evictExpired`\n * / `initStore`) are the BLESSED transitions, but their names and the\n * `IdempotencyStore<V>` shape they read/return are the STORE's vocabulary. A\n * consumer that owns an `IdempotencyStore<V>` slice (`idempotent-intake`,\n * `poller`) used to import those functions by name and call them directly, so\n * a change to the op surface — or to `IdempotencyStore`'s fields — cascaded to\n * every call site.\n *\n * `IdempotencyMemory<V>` is the insulating interface: five VERBS (`isSeen` /\n * `recall` / `remember` / `evict` / `init`) consumers depend on instead of the\n * raw functions. The verbs keep the ops' pure contract — no I/O, no `Date.now`\n * (the clock is injected as `now`) — so a consumer folding the adapter through\n * its reducer stays inside invariant 2 (pure transitions). `isSeen` renames the\n * blessed `seen` predicate (a noun-y `seen` reads ambiguously as a slice field;\n * the verb form states the question it answers), and `evict` is the verb form\n * of `evictExpired`.\n *\n * `idempotencyMemory<V>()` is the canonical implementation, a thin binding of\n * the blessed ops. Because the verbs are the only surface consumers touch, an\n * `IdempotencyStore` shape change is absorbed HERE rather than at the consumer\n * call sites.\n *\n * INTERNAL seam (no public-API port): consumers import the adapter from this\n * subpath the same way they imported the ops. The ops remain exported from\n * `./index` for any call site that genuinely wants a function directly.\n */\n\nimport {\n evictExpired,\n type IdempotencyStore,\n initStore,\n recall,\n remember,\n seen,\n} from \"./index\";\n\n/**\n * The verb interface over an `IdempotencyStore<V>`. Each verb mirrors a blessed\n * op's pure contract but speaks the consumer's dedupe vocabulary, so the\n * consumer never names a store function or reaches into an `IdempotencyStore`\n * field it did not put there.\n */\nexport interface IdempotencyMemory<V> {\n /** Create an empty store, optionally bounded by `capacity` and/or `ttlMs`. */\n init(opts?: { capacity?: number; ttlMs?: number }): IdempotencyStore<V>;\n\n /** True iff `key` is present AND not expired at `now`. Read-only. */\n isSeen(store: IdempotencyStore<V>, key: string, now: number): boolean;\n\n /** The cached value for `key` if present and unexpired at `now`. Read-only. */\n recall(store: IdempotencyStore<V>, key: string, now: number): V | undefined;\n\n /** Record `key → value` at `now`, enforcing the store's bounds. Returns a new store. */\n remember(\n store: IdempotencyStore<V>,\n key: string,\n value: V,\n now: number,\n ): IdempotencyStore<V>;\n\n /** Drop every entry expired at `now`. Returns a new store. */\n evict(store: IdempotencyStore<V>, now: number): IdempotencyStore<V>;\n}\n\n/**\n * The canonical `IdempotencyMemory<V>`, binding each verb to its blessed op.\n * Pure and stateless — call once at module scope in a consumer and fold its\n * verbs through the reducer. An `IdempotencyStore` shape change is absorbed by\n * re-binding a verb here, not at the consumers.\n */\nexport function idempotencyMemory<V>(): IdempotencyMemory<V> {\n return {\n init: (opts) => initStore<V>(opts),\n isSeen: (store, key, now) => seen<V>(store, key, now),\n recall: (store, key, now) => recall<V>(store, key, now),\n remember: (store, key, value, now) => remember<V>(store, key, value, now),\n evict: (store, now) => evictExpired<V>(store, now),\n };\n}\n"]}
|
|
@@ -1,33 +1,19 @@
|
|
|
1
|
-
import { replay } from './chunk-
|
|
1
|
+
import { replay } from './chunk-MMH7CW5K.js';
|
|
2
|
+
import { applyCell } from './chunk-QLA76GIT.js';
|
|
2
3
|
import * as fc from 'fast-check';
|
|
3
4
|
|
|
4
5
|
// src/pbt/runners/replay-fold.ts
|
|
5
6
|
function foldEvents(machine, ctx, loaded, msgs) {
|
|
6
|
-
const { state: initialState } = replay(machine, {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const firstValue = updateForm[firstKey];
|
|
12
|
-
return typeof firstValue === "function" ? "reducer" : "transitions";
|
|
13
|
-
})();
|
|
7
|
+
const { state: initialState } = replay(machine, {
|
|
8
|
+
msgs: [],
|
|
9
|
+
ctx,
|
|
10
|
+
loaded: loaded ?? null
|
|
11
|
+
});
|
|
14
12
|
const states = [initialState];
|
|
15
13
|
const steps = [];
|
|
16
14
|
let current = initialState;
|
|
17
15
|
for (const msg of msgs) {
|
|
18
|
-
|
|
19
|
-
let emitted;
|
|
20
|
-
if (updateMode === "reducer") {
|
|
21
|
-
const record = updateForm;
|
|
22
|
-
[next, emitted] = record[msg.type](
|
|
23
|
-
current,
|
|
24
|
-
msg
|
|
25
|
-
);
|
|
26
|
-
} else {
|
|
27
|
-
const table = updateForm;
|
|
28
|
-
const stateKey = current.type;
|
|
29
|
-
[next, emitted] = table[stateKey][msg.type](current, msg);
|
|
30
|
-
}
|
|
16
|
+
const [next, emitted] = applyCell(machine, current, msg);
|
|
31
17
|
steps.push({ prev: current, msg, next, cmds: [...emitted] });
|
|
32
18
|
states.push(next);
|
|
33
19
|
current = next;
|
|
@@ -43,7 +29,12 @@ function propertyTerminates(machine, ctx, seqArb, terminal, opts) {
|
|
|
43
29
|
};
|
|
44
30
|
fc.assert(
|
|
45
31
|
fc.property(seqArb, (msgs) => {
|
|
46
|
-
const { finalState } = foldEvents(
|
|
32
|
+
const { finalState } = foldEvents(
|
|
33
|
+
machine,
|
|
34
|
+
ctx,
|
|
35
|
+
opts?.loaded ?? null,
|
|
36
|
+
msgs
|
|
37
|
+
);
|
|
47
38
|
if (!terminal(finalState)) {
|
|
48
39
|
throw new Error(
|
|
49
40
|
`propertyTerminates: final state did not match terminal predicate. Final: ${JSON.stringify(finalState)}`
|
|
@@ -79,7 +70,12 @@ function propertyTrace(machine, ctx, seqArb, predicate, opts) {
|
|
|
79
70
|
};
|
|
80
71
|
fc.assert(
|
|
81
72
|
fc.property(seqArb, (msgs) => {
|
|
82
|
-
const { steps, finalState } = foldEvents(
|
|
73
|
+
const { steps, finalState } = foldEvents(
|
|
74
|
+
machine,
|
|
75
|
+
ctx,
|
|
76
|
+
opts?.loaded ?? null,
|
|
77
|
+
msgs
|
|
78
|
+
);
|
|
83
79
|
if (!predicate(steps, finalState)) {
|
|
84
80
|
throw new Error(
|
|
85
81
|
`propertyTrace: predicate failed on trace length=${steps.length}, final=${JSON.stringify(finalState)}`
|
|
@@ -91,5 +87,5 @@ function propertyTrace(machine, ctx, seqArb, predicate, opts) {
|
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace };
|
|
94
|
-
//# sourceMappingURL=chunk-
|
|
95
|
-
//# sourceMappingURL=chunk-
|
|
90
|
+
//# sourceMappingURL=chunk-DILMKOZ7.js.map
|
|
91
|
+
//# sourceMappingURL=chunk-DILMKOZ7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pbt/runners/replay-fold.ts","../src/pbt/runners/property.ts"],"names":[],"mappings":"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","file":"chunk-DILMKOZ7.js","sourcesContent":["// ---------------------------------------------------------------------------\n// `foldEvents` — pure deterministic fold over a Msg sequence.\n//\n// PBT needs the per-step (prev/msg/next/cmds) structure to assert\n// step-shaped invariants. The substrate's `replay(machine, opts)` returns\n// only final state + cumulative cmds — insufficient. We use `replay` for\n// the boot step (calls `machine.init(loaded, ctx)` faithfully) then walk\n// msgs ourselves through `applyCell`, the substrate's own cell dispatch.\n//\n// Why walk cells directly instead of looping `replay` once per msg with\n// `loaded: prevState`? Because `init(loaded)` runs every call, and on\n// machines with non-trivial init transforms (e.g. the audit machine's\n// `auditing → attach_debugger` resume cell) that would either (a) re-fire\n// the transform on every step, or (b) require fragile cmd-subtraction to\n// isolate this step's cmds. Calling the cells directly keeps the fold pure\n// and the cmds per-step exactly.\n//\n// Dispatches through `applyCell` — the single primitive `run`/`replay` use —\n// so form classification agrees with production by construction (#275).\n//\n// `foldEvents` does NOT call `interpret`, does NOT touch a Store, does NOT\n// start Subs. The reducer surface is the contract.\n// ---------------------------------------------------------------------------\n\nimport {\n applyCell,\n type Cmd,\n type Machine,\n replay,\n type Sub,\n} from \"../../index\";\n\n/**\n * One unit in a fold trace — the four pieces of data a property\n * invariant typically asserts on. `prev` is the state before the msg\n * applied; `next` is the state after; `cmds` is the cmds emitted by this\n * single step (NOT the running total).\n */\nexport interface Step<S, M, C> {\n readonly prev: S;\n readonly msg: M;\n readonly next: S;\n readonly cmds: readonly C[];\n}\n\n/**\n * Fold a Msg sequence through a machine's pure reducer, returning the\n * per-step trace, every state in order, and the final state. Initial\n * state comes from `replay(machine, { msgs: [], ctx, loaded })` — i.e.\n * `machine.init(loaded ?? null, ctx)` faithfully. Init's emitted cmds\n * are NOT included in any `Step` (they're the boot moment, not a Msg).\n *\n * @param machine The machine under test.\n * @param ctx The Ctx — typically `stubCtxThrowingProxy<Ctx>()` when\n * init is pure. Pass a real ctx if init reads from it.\n * @param loaded Optional pre-init snapshot (the `loaded` arg to\n * `machine.init`). Defaults to `null` (fresh boot).\n * @param msgs The Msg sequence to fold.\n *\n * @example\n * const { states, steps, finalState } = foldEvents(machine, ctx, null, [\n * { type: \"start\" }, { type: \"tick\" }, { type: \"stop\" },\n * ]);\n * // steps[1] = { prev: { type: \"counting\", n: 0 }, msg: { type: \"tick\" }, ... }\n */\nexport function foldEvents<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n loaded: NoInfer<S> | null,\n msgs: readonly NoInfer<M>[],\n): {\n readonly states: readonly S[];\n readonly steps: readonly Step<S, M, C>[];\n readonly finalState: S;\n} {\n // Boot through `replay` so `init` runs faithfully and we honor the\n // substrate's loaded-snapshot contract (init may transform the loaded\n // value before the first Msg lands — e.g. the audit machine flips\n // `debuggerAttached: false` on resume).\n const { state: initialState } = replay(machine, {\n msgs: [],\n ctx,\n loaded: loaded ?? null,\n });\n\n // Per-msg dispatch goes through `applyCell` — the same primitive `run` and\n // `replay` use — so this runner classifies the update form identically to\n // production (`formOf` honors the `__form` tag; #275).\n const states: S[] = [initialState];\n const steps: Step<S, M, C>[] = [];\n let current = initialState;\n\n for (const msg of msgs) {\n const [next, emitted] = applyCell<S, M, C>(machine, current, msg);\n steps.push({ prev: current, msg, next, cmds: [...emitted] });\n states.push(next);\n current = next;\n }\n\n return { states, steps, finalState: current };\n}\n","// ---------------------------------------------------------------------------\n// Property runners — three thin wrappers over `fc.assert(fc.property(...))`\n// that fold a generated Msg sequence through the machine via `foldEvents`\n// and pipe the result into a TEA-shaped predicate.\n//\n// - `propertyTerminates` — every sequence ends in a terminal state.\n// - `propertyInvariant` — predicate holds on every (prev, msg, next, cmds) step.\n// - `propertyTrace` — predicate holds on the full (trace, final) shape.\n//\n// Each runner accepts `numRuns` (passed through to fast-check), an optional\n// `loaded` snapshot (forwarded to `foldEvents`), and the Msg-sequence\n// arbitrary. They return `void`; failures throw via fast-check's standard\n// shrinking output.\n//\n// Default shrinker is fast-check's built-in (open question 4): drop-msg /\n// prefix shrinkers can be layered later but are opt-in, not default. The\n// built-in shrinker handles array-shape shrinking + per-element variant\n// shrinking automatically.\n// ---------------------------------------------------------------------------\n\nimport * as fc from \"fast-check\";\nimport type { Cmd, Machine, Sub } from \"../../index\";\nimport { foldEvents, type Step } from \"./replay-fold\";\n\n/** Common options shared by every property runner. */\ninterface PropertyOpts<S> {\n /** Pre-init snapshot — passed to `machine.init(loaded, ctx)`. Default `null`. */\n readonly loaded?: S | null;\n /** Number of generated sequences to test. Forwarded to `fc.assert`. */\n readonly numRuns?: number;\n /** Optional fast-check seed for reproducibility. */\n readonly seed?: number;\n}\n\n/**\n * Assert every generated Msg sequence ends in a terminal state. `terminal`\n * is the predicate that names the acceptable final states (often\n * `(s) => s.type === \"done\" || s.type === \"idle\"`).\n *\n * The \"every sequence\" property catches stuck states (machine gets to a\n * non-terminal state and no further msg can escape) AND silent loops\n * (every msg variant is a noop from a given state).\n *\n * @example\n * propertyTerminates(\n * auditBackgroundMachine,\n * stubCtxThrowingProxy<BackgroundCtx>(),\n * arbMsgSequence(msgArb, { minLength: 1, maxLength: 30 }),\n * (s) => s.type === \"done\" || s.type === \"idle\",\n * { loaded: auditingState, numRuns: 500 },\n * );\n */\nexport function propertyTerminates<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n seqArb: fc.Arbitrary<readonly NoInfer<M>[]>,\n terminal: (state: NoInfer<S>) => boolean,\n opts?: NoInfer<PropertyOpts<S>>,\n): void {\n const fcParams: fc.Parameters<[readonly M[]]> = {\n numRuns: opts?.numRuns ?? 200,\n ...(opts?.seed !== undefined ? { seed: opts.seed } : {}),\n };\n fc.assert(\n fc.property(seqArb, (msgs) => {\n const { finalState } = foldEvents(\n machine,\n ctx,\n opts?.loaded ?? null,\n msgs,\n );\n if (!terminal(finalState)) {\n // Throwing here (vs returning false) lets the error message carry\n // the offending final state — fast-check pretty-prints it inside\n // the shrunk counter-example.\n throw new Error(\n `propertyTerminates: final state did not match terminal predicate. ` +\n `Final: ${JSON.stringify(finalState)}`,\n );\n }\n }),\n fcParams,\n );\n}\n\n/**\n * Assert a predicate holds on EVERY transition step in a generated\n * sequence. The predicate sees `{ prev, msg, next, cmds }` per step;\n * return true to accept, false (or throw) to fail.\n *\n * Use to pin a \"when X happens in state Y, cmd Z must be emitted\" rule.\n * The shrinker minimizes the offending sequence to the smallest Msg list\n * that still violates the predicate.\n *\n * @example\n * // Pin the resume-handshake fix: `ws:client:hello` in auditing must\n * // emit `send_ws { client:ready }`.\n * propertyInvariant(\n * auditBackgroundMachine, ctx,\n * arbMsgSequence(msgArb, { prefix: [{ type: \"ws:client:hello\" }] }),\n * ({ prev, msg, cmds }) =>\n * !(prev.type === \"auditing\" && msg.type === \"ws:client:hello\") ||\n * cmds.some((c) => c.type === \"send_ws\" && c.msg.type === \"client:ready\"),\n * { loaded: auditingState, numRuns: 200 },\n * );\n */\nexport function propertyInvariant<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n seqArb: fc.Arbitrary<readonly NoInfer<M>[]>,\n invariant: (step: Step<NoInfer<S>, NoInfer<M>, NoInfer<C>>) => boolean,\n opts?: NoInfer<PropertyOpts<S>>,\n): void {\n const fcParams: fc.Parameters<[readonly M[]]> = {\n numRuns: opts?.numRuns ?? 200,\n ...(opts?.seed !== undefined ? { seed: opts.seed } : {}),\n };\n fc.assert(\n fc.property(seqArb, (msgs) => {\n const { steps } = foldEvents(machine, ctx, opts?.loaded ?? null, msgs);\n for (const step of steps) {\n if (!invariant(step)) {\n throw new Error(\n `propertyInvariant: failed at step msg=${JSON.stringify(step.msg)} ` +\n `prev=${JSON.stringify(step.prev)} next=${JSON.stringify(step.next)} ` +\n `cmds=${JSON.stringify(step.cmds)}`,\n );\n }\n }\n }),\n fcParams,\n );\n}\n\n/**\n * Assert a predicate over the full trace plus the final state. Use when\n * the property is \"some Msg appeared in the trace before reaching the\n * final state\" or \"no two consecutive steps had the same state.type\" —\n * predicates that need the full sequence, not a single step.\n *\n * @example\n * propertyTrace(\n * machine, ctx, seqArb,\n * (steps, final) =>\n * final.type !== \"done\" ||\n * steps.some((step) => step.msg.type === \"ws:audit:done\"),\n * { loaded: auditingState, numRuns: 500 },\n * );\n */\nexport function propertyTrace<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n seqArb: fc.Arbitrary<readonly NoInfer<M>[]>,\n predicate: (\n steps: readonly Step<NoInfer<S>, NoInfer<M>, NoInfer<C>>[],\n finalState: NoInfer<S>,\n ) => boolean,\n opts?: NoInfer<PropertyOpts<S>>,\n): void {\n const fcParams: fc.Parameters<[readonly M[]]> = {\n numRuns: opts?.numRuns ?? 200,\n ...(opts?.seed !== undefined ? { seed: opts.seed } : {}),\n };\n fc.assert(\n fc.property(seqArb, (msgs) => {\n const { steps, finalState } = foldEvents(\n machine,\n ctx,\n opts?.loaded ?? null,\n msgs,\n );\n if (!predicate(steps, finalState)) {\n throw new Error(\n `propertyTrace: predicate failed on trace length=${steps.length}, ` +\n `final=${JSON.stringify(finalState)}`,\n );\n }\n }),\n fcParams,\n );\n}\n"]}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { tryInterpret } from './chunk-MMH7CW5K.js';
|
|
2
|
+
import { Cmd } from './chunk-QLA76GIT.js';
|
|
3
|
+
|
|
4
|
+
// src/snapshot/index.ts
|
|
5
|
+
var DEFAULT_SNAPSHOT_KEY = "@@snapshot";
|
|
6
|
+
function snapshotSaved(seq, at) {
|
|
7
|
+
return { type: "snapshot_saved", seq, at };
|
|
8
|
+
}
|
|
9
|
+
function snapshotFailed(seq, error) {
|
|
10
|
+
return { type: "snapshot_failed", seq, error };
|
|
11
|
+
}
|
|
12
|
+
function snapshotLoaded(payload) {
|
|
13
|
+
return { type: "snapshot_loaded", payload };
|
|
14
|
+
}
|
|
15
|
+
function snapshotLoadFailed(error) {
|
|
16
|
+
return { type: "snapshot_load_failed", error };
|
|
17
|
+
}
|
|
18
|
+
function createSnapshot(config) {
|
|
19
|
+
const every = config.every >= 1 ? config.every : 1;
|
|
20
|
+
const key = config.key ?? DEFAULT_SNAPSHOT_KEY;
|
|
21
|
+
function emitWrite(state, payload, at) {
|
|
22
|
+
const seq = state.seq + 1;
|
|
23
|
+
const next = { ...state, sinceLast: 0, seq };
|
|
24
|
+
const cmd = {
|
|
25
|
+
type: "snapshot_write",
|
|
26
|
+
key,
|
|
27
|
+
seq,
|
|
28
|
+
at,
|
|
29
|
+
payload
|
|
30
|
+
};
|
|
31
|
+
return [next, [cmd]];
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
init() {
|
|
35
|
+
return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };
|
|
36
|
+
},
|
|
37
|
+
record(state, payload, at) {
|
|
38
|
+
const sinceLast = state.sinceLast + 1;
|
|
39
|
+
if (sinceLast < every) {
|
|
40
|
+
return [{ ...state, sinceLast }, Cmd.none];
|
|
41
|
+
}
|
|
42
|
+
return emitWrite(state, payload, at);
|
|
43
|
+
},
|
|
44
|
+
force(state, payload, at) {
|
|
45
|
+
return emitWrite(state, payload, at);
|
|
46
|
+
},
|
|
47
|
+
confirm(state, msg) {
|
|
48
|
+
if (state.lastSavedSeq !== null && msg.seq <= state.lastSavedSeq) {
|
|
49
|
+
return [state, Cmd.none];
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
{ ...state, lastSavedSeq: msg.seq, lastSavedAt: msg.at },
|
|
53
|
+
Cmd.none
|
|
54
|
+
];
|
|
55
|
+
},
|
|
56
|
+
boot(state) {
|
|
57
|
+
return [{ ...state, sinceLast: 0 }, Cmd.none];
|
|
58
|
+
},
|
|
59
|
+
requestLoad(state) {
|
|
60
|
+
const cmd = { type: "snapshot_load", key };
|
|
61
|
+
return [state, [cmd]];
|
|
62
|
+
},
|
|
63
|
+
handlers(ports) {
|
|
64
|
+
const { store } = ports;
|
|
65
|
+
return {
|
|
66
|
+
// The write effect. Performs the `put`; on success the saved `seq`/`at`
|
|
67
|
+
// ride the `snapshot_saved` Msg back into `confirm`; on failure the
|
|
68
|
+
// error rides `snapshot_failed`. Railway via the core's `tryInterpret`
|
|
69
|
+
// (matching every other module's handler) — the handler always resolves
|
|
70
|
+
// with a Msg, never rejects into the runtime.
|
|
71
|
+
snapshot_write: tryInterpret(
|
|
72
|
+
(cmd) => store.put(cmd.key, cmd.payload),
|
|
73
|
+
(_value, cmd) => snapshotSaved(cmd.seq, cmd.at),
|
|
74
|
+
(error, cmd) => snapshotFailed(cmd.seq, error)
|
|
75
|
+
),
|
|
76
|
+
// The read effect — the recovery half. Performs the `get`; on success
|
|
77
|
+
// the recovered payload (or `null` when no checkpoint exists) rides the
|
|
78
|
+
// `snapshot_loaded` Msg the consumer folds into `init`'s loaded branch;
|
|
79
|
+
// on failure the error rides `snapshot_load_failed`. Same Railway
|
|
80
|
+
// discipline via `tryInterpret` — the read never rejects into the runtime.
|
|
81
|
+
snapshot_load: tryInterpret(
|
|
82
|
+
(cmd) => store.get(cmd.key),
|
|
83
|
+
(payload) => snapshotLoaded(payload),
|
|
84
|
+
(error) => snapshotLoadFailed(error)
|
|
85
|
+
)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { createSnapshot, snapshotFailed, snapshotLoadFailed, snapshotLoaded, snapshotSaved };
|
|
92
|
+
//# sourceMappingURL=chunk-EHIX7NYZ.js.map
|
|
93
|
+
//# sourceMappingURL=chunk-EHIX7NYZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-EHIX7NYZ.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"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/throttle/index.ts
|
|
2
|
+
function throttle(fn, ms, opts) {
|
|
3
|
+
const leading = opts?.leading ?? true;
|
|
4
|
+
const trailing = opts?.trailing ?? true;
|
|
5
|
+
let handle = null;
|
|
6
|
+
let pendingArgs = null;
|
|
7
|
+
function clearHandle() {
|
|
8
|
+
if (handle !== null) {
|
|
9
|
+
clearTimeout(handle);
|
|
10
|
+
handle = null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function onWindowExpired() {
|
|
14
|
+
if (trailing && pendingArgs !== null) {
|
|
15
|
+
const args = pendingArgs;
|
|
16
|
+
pendingArgs = null;
|
|
17
|
+
handle = setTimeout(onWindowExpired, ms);
|
|
18
|
+
fn(...args);
|
|
19
|
+
} else {
|
|
20
|
+
handle = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const throttled = (...args) => {
|
|
24
|
+
if (handle === null) {
|
|
25
|
+
if (leading) {
|
|
26
|
+
fn(...args);
|
|
27
|
+
} else if (trailing) {
|
|
28
|
+
pendingArgs = args;
|
|
29
|
+
}
|
|
30
|
+
handle = setTimeout(onWindowExpired, ms);
|
|
31
|
+
} else if (trailing) {
|
|
32
|
+
pendingArgs = args;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
throttled.cancel = () => {
|
|
36
|
+
clearHandle();
|
|
37
|
+
pendingArgs = null;
|
|
38
|
+
};
|
|
39
|
+
throttled.flush = () => {
|
|
40
|
+
if (pendingArgs !== null) {
|
|
41
|
+
const args = pendingArgs;
|
|
42
|
+
pendingArgs = null;
|
|
43
|
+
clearHandle();
|
|
44
|
+
fn(...args);
|
|
45
|
+
} else {
|
|
46
|
+
clearHandle();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return throttled;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { throttle };
|
|
53
|
+
//# sourceMappingURL=chunk-HCEVHFJ7.js.map
|
|
54
|
+
//# sourceMappingURL=chunk-HCEVHFJ7.js.map
|