@demlik/tea 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +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-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
- package/dist/chunk-2WZHS4P2.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-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.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-7VEG4KHB.js +202 -0
- package/dist/chunk-7VEG4KHB.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
- package/dist/chunk-A56GNPI7.js.map +1 -0
- package/dist/chunk-BNJYE4AU.js +157 -0
- package/dist/chunk-BNJYE4AU.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/chunk-D5EQDLDV.js +186 -0
- package/dist/chunk-D5EQDLDV.js.map +1 -0
- package/dist/chunk-EDGDR5XX.js +60 -0
- package/dist/chunk-EDGDR5XX.js.map +1 -0
- package/dist/chunk-EIZJTYKR.js +93 -0
- package/dist/chunk-EIZJTYKR.js.map +1 -0
- package/dist/chunk-EQRUAP2A.js +56 -0
- package/dist/chunk-EQRUAP2A.js.map +1 -0
- package/dist/chunk-FH5RUIIL.js +61 -0
- package/dist/chunk-FH5RUIIL.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IFBQJYGI.js +132 -0
- package/dist/chunk-IFBQJYGI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.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-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.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-QGZW4W3E.js +20 -0
- package/dist/chunk-QGZW4W3E.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
- package/dist/chunk-RGTPPF4U.js.map +1 -0
- package/dist/chunk-RVJFGRGK.js +222 -0
- package/dist/chunk-RVJFGRGK.js.map +1 -0
- package/dist/chunk-RVU3RBE6.js +31 -0
- package/dist/chunk-RVU3RBE6.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-TAO4YOLO.js +87 -0
- package/dist/chunk-TAO4YOLO.js.map +1 -0
- package/dist/chunk-TC4HWABA.js +506 -0
- package/dist/chunk-TC4HWABA.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-WUV3PSCZ.js +54 -0
- package/dist/chunk-WUV3PSCZ.js.map +1 -0
- package/dist/chunk-XRXKH7KA.js +268 -0
- package/dist/chunk-XRXKH7KA.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-CymXZCeY.d.ts +178 -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.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 +1732 -92
- package/dist/do/index.js +677 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-2MDxDs45.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 +1 -1
- package/dist/machine-viz/index.js +1 -1
- 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 +86 -0
- package/dist/parity/index.js +77 -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 +1 -1
- package/dist/pbt/index.js +5 -4
- 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 +109 -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 +39 -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 +251 -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/saga/index.d.ts +225 -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.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +372 -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-N9LZbdnr.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +130 -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 +274 -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 +95 -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 +444 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +162 -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,202 @@
|
|
|
1
|
+
import { createRaftNode } from './chunk-XRXKH7KA.js';
|
|
2
|
+
|
|
3
|
+
// src/raft/sim.ts
|
|
4
|
+
function clusterConfigs(n, over) {
|
|
5
|
+
const ids = Array.from({ length: n }, (_, i) => `n${i}`);
|
|
6
|
+
return ids.map((self) => ({
|
|
7
|
+
self,
|
|
8
|
+
peers: ids.filter((id) => id !== self),
|
|
9
|
+
electionTimeout: over?.electionTimeout ?? { minMs: 150, maxMs: 300 },
|
|
10
|
+
heartbeatMs: over?.heartbeatMs ?? 50
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
function fixedRng(index, count) {
|
|
14
|
+
const value = count > 0 ? index / count : 0;
|
|
15
|
+
return () => value;
|
|
16
|
+
}
|
|
17
|
+
function cmdToInbound(sender, cmd) {
|
|
18
|
+
switch (cmd.type) {
|
|
19
|
+
case "raft:send_request_vote": {
|
|
20
|
+
const msg = {
|
|
21
|
+
_tag: "request_vote_request",
|
|
22
|
+
term: cmd.term,
|
|
23
|
+
candidateId: cmd.candidateId,
|
|
24
|
+
lastLogIndex: cmd.lastLogIndex,
|
|
25
|
+
lastLogTerm: cmd.lastLogTerm
|
|
26
|
+
};
|
|
27
|
+
return msg;
|
|
28
|
+
}
|
|
29
|
+
case "raft:send_request_vote_reply": {
|
|
30
|
+
const msg = {
|
|
31
|
+
_tag: "request_vote_reply",
|
|
32
|
+
term: cmd.term,
|
|
33
|
+
from: sender,
|
|
34
|
+
voteGranted: cmd.voteGranted
|
|
35
|
+
};
|
|
36
|
+
return msg;
|
|
37
|
+
}
|
|
38
|
+
case "raft:send_append_entries": {
|
|
39
|
+
const msg = {
|
|
40
|
+
_tag: "append_entries_request",
|
|
41
|
+
term: cmd.term,
|
|
42
|
+
leaderId: cmd.leaderId,
|
|
43
|
+
prevLogIndex: cmd.prevLogIndex,
|
|
44
|
+
prevLogTerm: cmd.prevLogTerm,
|
|
45
|
+
entries: cmd.entries,
|
|
46
|
+
leaderCommit: cmd.leaderCommit
|
|
47
|
+
};
|
|
48
|
+
return msg;
|
|
49
|
+
}
|
|
50
|
+
case "raft:send_append_entries_reply": {
|
|
51
|
+
const msg = {
|
|
52
|
+
_tag: "append_entries_reply",
|
|
53
|
+
term: cmd.term,
|
|
54
|
+
from: sender,
|
|
55
|
+
success: cmd.success,
|
|
56
|
+
matchIndex: cmd.matchIndex
|
|
57
|
+
};
|
|
58
|
+
return msg;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function applyInbound(node, state, msg, at) {
|
|
63
|
+
switch (msg._tag) {
|
|
64
|
+
case "election_timeout_fired":
|
|
65
|
+
return node.onElectionTimeout(state, at);
|
|
66
|
+
case "heartbeat_fired":
|
|
67
|
+
return node.onHeartbeat(state, at);
|
|
68
|
+
case "request_vote_request":
|
|
69
|
+
return node.onRequestVote(state, msg);
|
|
70
|
+
case "request_vote_reply":
|
|
71
|
+
return node.onRequestVoteReply(state, msg);
|
|
72
|
+
case "append_entries_request":
|
|
73
|
+
return node.onAppendEntries(state, msg);
|
|
74
|
+
case "append_entries_reply":
|
|
75
|
+
return node.onAppendEntriesReply(state, msg);
|
|
76
|
+
case "client_command":
|
|
77
|
+
return node.onClientCommand(state, msg);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function park(world, actor, emitted) {
|
|
81
|
+
for (const cmd of emitted) {
|
|
82
|
+
world.pending.push({ from: actor, cmd });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function deliverOne(world, index) {
|
|
86
|
+
if (world.pending.length === 0) {
|
|
87
|
+
world.clock += 1;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
world.clock += 1;
|
|
91
|
+
const at = world.clock;
|
|
92
|
+
const i = (index % world.pending.length + world.pending.length) % world.pending.length;
|
|
93
|
+
const [inFlight] = world.pending.splice(i, 1);
|
|
94
|
+
if (!inFlight) return null;
|
|
95
|
+
const target = inFlight.cmd.to;
|
|
96
|
+
if (world.down.has(target) || world.down.has(inFlight.from)) {
|
|
97
|
+
return inFlight;
|
|
98
|
+
}
|
|
99
|
+
const node = world.handles.get(target);
|
|
100
|
+
const state = world.states[target];
|
|
101
|
+
if (node && state) {
|
|
102
|
+
const inbound = cmdToInbound(inFlight.from, inFlight.cmd);
|
|
103
|
+
const [next, cmds] = applyInbound(node, state, inbound, at);
|
|
104
|
+
world.states[target] = next;
|
|
105
|
+
park(world, target, cmds);
|
|
106
|
+
}
|
|
107
|
+
return inFlight;
|
|
108
|
+
}
|
|
109
|
+
function deliveredOf(inFlight) {
|
|
110
|
+
return { from: inFlight.from, to: inFlight.cmd.to, kind: inFlight.cmd.type };
|
|
111
|
+
}
|
|
112
|
+
function applyEvent(world, event, steps) {
|
|
113
|
+
if (event.kind === "partition") {
|
|
114
|
+
world.clock += 1;
|
|
115
|
+
world.down = new Set(event.down);
|
|
116
|
+
steps.push({
|
|
117
|
+
event,
|
|
118
|
+
pending: world.pending.length,
|
|
119
|
+
states: snapshot(world.states)
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (event.kind === "settle") {
|
|
124
|
+
const bound = Math.max(0, event.bound);
|
|
125
|
+
for (let n = 0; n < bound && world.pending.length > 0; n++) {
|
|
126
|
+
const inFlight = deliverOne(world, 0);
|
|
127
|
+
steps.push({
|
|
128
|
+
event: { kind: "deliver", index: 0 },
|
|
129
|
+
pending: world.pending.length,
|
|
130
|
+
states: snapshot(world.states),
|
|
131
|
+
// Each drained delivery is its OWN step, so record that step's own
|
|
132
|
+
// delivered message (not "the last of the settle"). The pool was
|
|
133
|
+
// non-empty (loop guard), so `inFlight` is never null here.
|
|
134
|
+
...inFlight ? { delivered: deliveredOf(inFlight) } : {}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
let delivered = null;
|
|
140
|
+
if (event.kind === "deliver") {
|
|
141
|
+
delivered = deliverOne(world, event.index);
|
|
142
|
+
} else {
|
|
143
|
+
world.clock += 1;
|
|
144
|
+
const at = world.clock;
|
|
145
|
+
const actor = event.node;
|
|
146
|
+
const node = world.handles.get(actor);
|
|
147
|
+
const state = world.states[actor];
|
|
148
|
+
if (node && state) {
|
|
149
|
+
const inbound = event.kind === "timer" ? event.timer === "election" ? { _tag: "election_timeout_fired", at } : { _tag: "heartbeat_fired", at } : { _tag: "client_command", command: event.command };
|
|
150
|
+
const [next, cmds] = applyInbound(node, state, inbound, at);
|
|
151
|
+
world.states[actor] = next;
|
|
152
|
+
park(world, actor, cmds);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
steps.push({
|
|
156
|
+
event,
|
|
157
|
+
pending: world.pending.length,
|
|
158
|
+
states: snapshot(world.states),
|
|
159
|
+
// Additive read-off: present only when a `deliver` actually consumed a
|
|
160
|
+
// message. Absent (key omitted) for timer/client and empty-pool delivers,
|
|
161
|
+
// so the step is structurally identical to a pre-#144 trace.
|
|
162
|
+
...delivered ? { delivered: deliveredOf(delivered) } : {}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function snapshot(states) {
|
|
166
|
+
return { ...states };
|
|
167
|
+
}
|
|
168
|
+
function runSchedule(configs, schedule) {
|
|
169
|
+
const handles = /* @__PURE__ */ new Map();
|
|
170
|
+
const states = {};
|
|
171
|
+
configs.forEach((config, i) => {
|
|
172
|
+
const node = createRaftNode(config, fixedRng(i, configs.length));
|
|
173
|
+
handles.set(config.self, node);
|
|
174
|
+
states[config.self] = node.init();
|
|
175
|
+
});
|
|
176
|
+
const world = {
|
|
177
|
+
handles,
|
|
178
|
+
states,
|
|
179
|
+
pending: [],
|
|
180
|
+
clock: 0,
|
|
181
|
+
down: /* @__PURE__ */ new Set()
|
|
182
|
+
};
|
|
183
|
+
const steps = [];
|
|
184
|
+
for (const event of schedule) {
|
|
185
|
+
applyEvent(world, event, steps);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
nodes: configs.map((c) => c.self),
|
|
189
|
+
steps,
|
|
190
|
+
final: snapshot(world.states)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function replaySchedule(configs, schedule) {
|
|
194
|
+
return runSchedule(configs, schedule);
|
|
195
|
+
}
|
|
196
|
+
function clusterStatesEqual(a, b) {
|
|
197
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { clusterConfigs, clusterStatesEqual, replaySchedule, runSchedule };
|
|
201
|
+
//# sourceMappingURL=chunk-7VEG4KHB.js.map
|
|
202
|
+
//# sourceMappingURL=chunk-7VEG4KHB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/raft/sim.ts"],"names":[],"mappings":"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","file":"chunk-7VEG4KHB.js","sourcesContent":["/**\n * @demlik/tea/raft — a deterministic, in-memory multi-node Raft simulation\n * driver. Slice #121 (the safety-property half) of the consensus epic (#117):\n * the headline payoff is consensus verified by **deterministic replay**, not by\n * flaky wall-clock timing.\n *\n * This module owns NO wall clock, NO real timers, and NO networking. Time and\n * message ordering are entirely the *schedule* — a finite list of {@link SimEvent}s\n * the caller (a fast-check generator or an explicit fixture) hands the driver.\n * The driver folds the schedule one event at a time over a cluster of pure\n * {@link createRaftNode}s, routing every emitted `Cmd` into a pending-message\n * pool and recording the full `(event → resulting cluster states)` trace.\n *\n * Because the underlying reducer is pure (no ambient clock — `at` rides on the\n * Msg; no ambient RNG — `rng` injected once per node), the same schedule re-run\n * yields **byte-identical** cluster state. That replay identity is the property\n * the safety suite (`safety.test.ts`) leans on: a violated invariant shrinks to\n * a *replayable schedule*, not an irreproducible timing fluke (errors are data).\n *\n * ## The schedule model\n *\n * The cluster is N nodes plus a pending-message pool. Each {@link SimEvent} is\n * one of two moves:\n *\n * - `{ kind: \"timer\", node, timer }` — fire node's election OR heartbeat timer.\n * Drives an `onElectionTimeout` / `onHeartbeat` verb on that node. The\n * firing `at` is the driver's monotonic logical clock (one tick per event),\n * so no verb reads a real clock.\n * - `{ kind: \"deliver\", index }` — deliver the in-flight message at `index` in\n * the pending pool to its target node, as the matching inbound `RaftMsg`.\n * An out-of-range `index` is taken modulo the pool size (so a fast-check\n * integer always names *some* deliverable message when the pool is\n * non-empty); a `deliver` against an empty pool is a no-op event (recorded,\n * advances the clock, changes nothing) — the schedule stays total.\n *\n * Delivering a message removes it from the pool and feeds it to the target's\n * verb; any Cmds that verb emits are appended to the pool (each Cmd carries its\n * own `to`, so routing is just a lookup). This is the in-memory transport: a\n * `SendRequestVote{to}` delivered becomes a `RequestVoteRequest` on `to`; the\n * `SendRequestVoteReply{to}` it emits becomes a `RequestVoteReply` on the\n * original candidate; and so on around the RPC pairs.\n *\n * ## Record / replay\n *\n * {@link runSchedule} returns a {@link SimTrace}: the ordered list of steps, each\n * the event applied and the resulting per-node {@link RaftState} snapshot, plus\n * the final cluster. {@link replaySchedule} re-runs the identical schedule from a\n * fresh cluster; {@link clusterStatesEqual} compares two cluster snapshots\n * structurally. The safety suite asserts `runSchedule(s)` and\n * `replaySchedule(s)` agree byte-for-byte.\n *\n * NOT a substrate primitive and NOT shipped in the node's public verb surface:\n * it depends only on `./index` (the pure node) read-only, and lives beside the\n * node as a test/replay harness.\n */\n\nimport {\n type AppendEntriesReply,\n type AppendEntriesRequest,\n createRaftNode,\n type LogEntry,\n type NodeId,\n type RaftCmd,\n type RaftConfig,\n type RaftMsg,\n type RaftNode,\n type RaftState,\n type RequestVoteReply,\n type RequestVoteRequest,\n} from \"./index\";\n\n// ===========================================================================\n// Schedule — the controllable, wall-clock-free event stream\n// ===========================================================================\n\n/** Which of a node's two timers a `timer` event fires. */\nexport type TimerKind = \"election\" | \"heartbeat\";\n\n/**\n * One move in a schedule. The whole simulation is a fold of these — there is no\n * other source of time or ordering. `_tag`-style `kind` discriminant matches the\n * engine's tagged-union convention.\n */\nexport type SimEvent =\n | {\n /** Fire a node's timer (drives `onElectionTimeout` / `onHeartbeat`). */\n readonly kind: \"timer\";\n /** The node whose timer fires. */\n readonly node: NodeId;\n /** Which timer. */\n readonly timer: TimerKind;\n }\n | {\n /**\n * Deliver an in-flight message from the pending pool to its target. The\n * index is taken modulo the pool size, so any integer names a deliverable\n * message when the pool is non-empty; a deliver against an empty pool is a\n * recorded no-op (the clock still advances).\n */\n readonly kind: \"deliver\";\n /** Which pending message to deliver (modulo pool size). */\n readonly index: number;\n }\n | {\n /**\n * A client submits a command to a node (Figure 2 §5.3). On the leader it\n * appends + replicates; on a non-leader it is a no-op. This is how the\n * replicated log grows in a simulation.\n */\n readonly kind: \"client\";\n /** The node the client contacts. */\n readonly node: NodeId;\n /** The opaque command to submit. */\n readonly command: number;\n }\n | {\n /**\n * Drain the pending pool to quiescence: deliver messages FIFO (each new\n * Cmd a delivery emits is appended and itself delivered) until the pool is\n * empty or `bound` deliveries have run. This is the \"let the network\n * settle\" move — it lets a started election or a submitted client command\n * run its RPC round(s) to completion, which is what actually produces\n * leaders and committed entries. Without it, a purely-random `deliver`\n * index almost never threads the exact sequence an election + replication\n * needs (measured: 0 commits in 2000 random schedules vs. 529 with settle),\n * so the safety properties would pass *vacuously* over empty logs.\n *\n * The `bound` caps the drain so a pathological re-emit loop cannot diverge;\n * every step is still recorded (one trace step per delivery), so a settle\n * is a deterministic, replayable macro — not a black box.\n */\n readonly kind: \"settle\";\n /** Max deliveries to run (the drain is FIFO; stops early when empty). */\n readonly bound: number;\n }\n | {\n /**\n * Set the cluster's *partitioned* node set (a network fault, NOT a state\n * change). A node in `down` is isolated: the transport DROPS every message\n * to OR from it (both directions, as a real partition does) while its\n * timers still fire — so a crashed/partitioned leader stops being able to\n * heartbeat and the surviving majority elects a new leader. This is the\n * \"kill the leader\" / \"heal the partition\" move the failover demo needs.\n *\n * It names the WHOLE down-set absolutely (not a toggle): `{ down: [n0] }`\n * isolates n0; a later `{ down: [] }` heals every partition. Recorded as\n * one step (clock advances, no message moves), so it replays deterministically.\n */\n readonly kind: \"partition\";\n /** The nodes that are currently isolated (drop all their traffic). */\n readonly down: readonly NodeId[];\n };\n\n/** A whole schedule: the finite event stream the driver folds. */\nexport type Schedule = readonly SimEvent[];\n\n// ===========================================================================\n// In-flight messages — emitted Cmds parked in the pending pool\n// ===========================================================================\n\n/**\n * A Cmd a node emitted, parked until a `deliver` event routes it. We keep the\n * raw {@link RaftCmd} (it already carries `to`) plus the sender, so a delivery\n * can build the correct inbound {@link RaftMsg} on the target. Commands over a\n * numeric payload (the simulation's `C = number`).\n */\nexport interface InFlight {\n /** The node that emitted this Cmd (the inbound Msg's `from` / sender). */\n readonly from: NodeId;\n /** The emitted Cmd, addressed to `cmd.to`. */\n readonly cmd: RaftCmd<number>;\n}\n\n// ===========================================================================\n// Cluster + trace\n// ===========================================================================\n\n/** A snapshot of every node's state, keyed by id. */\nexport type ClusterStates = Readonly<Record<NodeId, RaftState<number>>>;\n\n/**\n * A read-off of the message a `deliver` step actually pulled off the pending\n * pool — sender, target, and the {@link RaftCmd} discriminant (`cmd.type`). This\n * is a pure projection of what the fold *already did* (the InFlight that\n * {@link deliverOne} consumed), recorded so a renderer can draw the RPC flow.\n * It is NOT a new input and changes NO simulation behavior. Recorded even when\n * the packet is dropped by a partition (the RPC was still attempted), so the\n * arrow shows the intent while the greyed node shows the drop.\n */\nexport interface DeliveredMessage {\n /** The node that emitted the delivered Cmd (the inbound Msg's sender). */\n readonly from: NodeId;\n /** The node the Cmd was addressed to (`cmd.to`). */\n readonly to: NodeId;\n /** The delivered Cmd's discriminant (`RaftCmd[\"type\"]`). */\n readonly kind: RaftCmd<number>[\"type\"];\n}\n\n/** One recorded step: the event applied and the cluster state it produced. */\nexport interface SimStep {\n /** The event applied at this step. */\n readonly event: SimEvent;\n /** The pending-pool size AFTER applying the event. */\n readonly pending: number;\n /** Every node's state after the event. */\n readonly states: ClusterStates;\n /**\n * On a `deliver` step (incl. each delivery a `settle` drains), the message\n * that was actually pulled off the pool. ABSENT on every other step kind and\n * on a `deliver` against an empty pool — additive + backward-compatible, so a\n * trace recorded before this field still folds and renders identically (field\n * absent ⇒ no arrow). Purely a read-off of {@link deliverOne}'s work.\n */\n readonly delivered?: DeliveredMessage;\n}\n\n/**\n * The full record of a run: the per-node configs, the ordered steps, and the\n * final cluster. Replaying the same schedule reproduces this byte-for-byte.\n */\nexport interface SimTrace {\n /** The cluster's node ids, in config order. */\n readonly nodes: readonly NodeId[];\n /** One step per event in the schedule (in order). */\n readonly steps: readonly SimStep[];\n /** The final per-node states (== `steps.at(-1)?.states` when non-empty). */\n readonly final: ClusterStates;\n}\n\n// ===========================================================================\n// Building the cluster\n// ===========================================================================\n\n/**\n * Build N node configs sharing one election-timeout window + heartbeat. Each\n * node's `peers` is every OTHER id. Ids are `n0..n{N-1}`.\n */\nexport function clusterConfigs(\n n: number,\n over?: Partial<Omit<RaftConfig, \"self\" | \"peers\">>,\n): readonly RaftConfig[] {\n const ids = Array.from({ length: n }, (_, i) => `n${i}`);\n return ids.map((self) => ({\n self,\n peers: ids.filter((id) => id !== self),\n electionTimeout: over?.electionTimeout ?? { minMs: 150, maxMs: 300 },\n heartbeatMs: over?.heartbeatMs ?? 50,\n }));\n}\n\n/**\n * A fixed-jitter rng for a node, derived from its index so that distinct nodes\n * draw *distinct but deterministic* election timeouts (the real-cluster effect\n * that breaks split votes), while a replay with the same configs reproduces them\n * exactly. Pure function of the index — no ambient randomness.\n */\nfunction fixedRng(index: number, count: number): () => number {\n // Spread the [0,1) jitter evenly across the cluster: node i draws i/count.\n const value = count > 0 ? index / count : 0;\n return () => value;\n}\n\n// ===========================================================================\n// The driver\n// ===========================================================================\n\n/**\n * The mutable bag the fold threads. Kept local to {@link runSchedule}; never\n * escapes. `clock` is the monotonic logical clock — incremented once per event\n * and used as the `at` for timer firings, so the reducer never reads a real\n * clock and a replay reproduces every `at`.\n */\ninterface SimWorld {\n readonly handles: ReadonlyMap<NodeId, RaftNode<number>>;\n states: Record<NodeId, RaftState<number>>;\n pending: InFlight[];\n clock: number;\n /**\n * The currently-partitioned nodes (a `partition` event sets this absolutely).\n * The transport drops any in-flight message to OR from a down node — its\n * timers still fire, but its RPCs never land and it never hears the cluster.\n */\n down: Set<NodeId>;\n}\n\n/**\n * Translate one emitted {@link RaftCmd} into the inbound {@link RaftMsg} the\n * target node should receive. Pure mapping over the RPC pairs (Figure 2 wire\n * shapes); `sender` becomes the Msg's `from` / candidateId / leaderId as\n * appropriate. The target is always `cmd.to`.\n */\nfunction cmdToInbound(sender: NodeId, cmd: RaftCmd<number>): RaftMsg<number> {\n switch (cmd.type) {\n case \"raft:send_request_vote\": {\n const msg: RequestVoteRequest = {\n _tag: \"request_vote_request\",\n term: cmd.term,\n candidateId: cmd.candidateId,\n lastLogIndex: cmd.lastLogIndex,\n lastLogTerm: cmd.lastLogTerm,\n };\n return msg;\n }\n case \"raft:send_request_vote_reply\": {\n const msg: RequestVoteReply = {\n _tag: \"request_vote_reply\",\n term: cmd.term,\n from: sender,\n voteGranted: cmd.voteGranted,\n };\n return msg;\n }\n case \"raft:send_append_entries\": {\n const msg: AppendEntriesRequest<number> = {\n _tag: \"append_entries_request\",\n term: cmd.term,\n leaderId: cmd.leaderId,\n prevLogIndex: cmd.prevLogIndex,\n prevLogTerm: cmd.prevLogTerm,\n entries: cmd.entries,\n leaderCommit: cmd.leaderCommit,\n };\n return msg;\n }\n case \"raft:send_append_entries_reply\": {\n const msg: AppendEntriesReply = {\n _tag: \"append_entries_reply\",\n term: cmd.term,\n from: sender,\n success: cmd.success,\n matchIndex: cmd.matchIndex,\n };\n return msg;\n }\n }\n}\n\n/** Run one node verb for the inbound Msg, returning `[next, cmds]`. */\nfunction applyInbound(\n node: RaftNode<number>,\n state: RaftState<number>,\n msg: RaftMsg<number>,\n at: number,\n) {\n switch (msg._tag) {\n case \"election_timeout_fired\":\n return node.onElectionTimeout(state, at);\n case \"heartbeat_fired\":\n return node.onHeartbeat(state, at);\n case \"request_vote_request\":\n return node.onRequestVote(state, msg);\n case \"request_vote_reply\":\n return node.onRequestVoteReply(state, msg);\n case \"append_entries_request\":\n return node.onAppendEntries(state, msg);\n case \"append_entries_reply\":\n return node.onAppendEntriesReply(state, msg);\n case \"client_command\":\n return node.onClientCommand(state, msg);\n }\n}\n\n/** Park the Cmds an acting node emitted into the pending pool (FIFO append). */\nfunction park(\n world: SimWorld,\n actor: NodeId,\n emitted: readonly RaftCmd<number>[],\n): void {\n for (const cmd of emitted) {\n world.pending.push({ from: actor, cmd });\n }\n}\n\n/**\n * Deliver the pending message at `index` (modulo the pool size) to its target,\n * parking whatever it emits. Advances the clock and returns the {@link InFlight}\n * it consumed from the pool (so a caller can record what was delivered),\n * or `null` when the pool was empty (a recorded no-op). A single atomic unit of\n * simulated work — one trace step.\n *\n * The return value is a PURE read-off of work the function already does (the\n * splice was always there); returning it instead of a `boolean` adds no\n * behavior — every existing call-site path (apply, partition-drop, empty-pool)\n * is byte-for-byte unchanged.\n */\nfunction deliverOne(world: SimWorld, index: number): InFlight | null {\n if (world.pending.length === 0) {\n world.clock += 1;\n return null;\n }\n world.clock += 1;\n const at = world.clock;\n const i =\n ((index % world.pending.length) + world.pending.length) %\n world.pending.length;\n const [inFlight] = world.pending.splice(i, 1);\n if (!inFlight) return null;\n const target = inFlight.cmd.to;\n // Partition: a message to OR from an isolated node is dropped — consumed from\n // the pool (so a `settle` still terminates) but never applied (the packet was\n // lost on the wire). Both directions, as a real partition severs.\n if (world.down.has(target) || world.down.has(inFlight.from)) {\n return inFlight;\n }\n const node = world.handles.get(target);\n const state = world.states[target];\n if (node && state) {\n const inbound = cmdToInbound(inFlight.from, inFlight.cmd);\n const [next, cmds] = applyInbound(node, state, inbound, at);\n world.states[target] = next;\n park(world, target, cmds);\n }\n return inFlight;\n}\n\n/** Project a consumed {@link InFlight} into the additive {@link DeliveredMessage} read-off. */\nfunction deliveredOf(inFlight: InFlight): DeliveredMessage {\n return { from: inFlight.from, to: inFlight.cmd.to, kind: inFlight.cmd.type };\n}\n\n/**\n * Apply one {@link SimEvent} to the world, pushing one trace step PER unit of\n * work onto `steps`. A `timer` / `client` / `deliver` produces exactly one step;\n * a `settle` produces one step per drained delivery (so the macro stays a\n * transparent, replayable sequence — not a black box). Mutates the world's\n * `states` / `pending` / `clock` in place (the local fold bag).\n */\nfunction applyEvent(world: SimWorld, event: SimEvent, steps: SimStep[]): void {\n if (event.kind === \"partition\") {\n // A pure network-fault move: replace the down-set, advance the clock, record\n // a step. No message moves and no node state changes — the effect surfaces\n // later when `deliverOne` drops traffic to/from a down node.\n world.clock += 1;\n world.down = new Set(event.down);\n steps.push({\n event,\n pending: world.pending.length,\n states: snapshot(world.states),\n });\n return;\n }\n\n if (event.kind === \"settle\") {\n // Drain FIFO (index 0) until empty or the bound is hit. Each delivery is its\n // own recorded step, tagged as the deliver-index-0 it actually is.\n const bound = Math.max(0, event.bound);\n for (let n = 0; n < bound && world.pending.length > 0; n++) {\n const inFlight = deliverOne(world, 0);\n steps.push({\n event: { kind: \"deliver\", index: 0 },\n pending: world.pending.length,\n states: snapshot(world.states),\n // Each drained delivery is its OWN step, so record that step's own\n // delivered message (not \"the last of the settle\"). The pool was\n // non-empty (loop guard), so `inFlight` is never null here.\n ...(inFlight ? { delivered: deliveredOf(inFlight) } : {}),\n });\n }\n return;\n }\n\n // The message a top-level `deliver` consumed (null on an empty-pool no-op);\n // read off below into the step's additive `delivered` field. `undefined` for\n // every non-deliver event kind.\n let delivered: InFlight | null = null;\n if (event.kind === \"deliver\") {\n delivered = deliverOne(world, event.index);\n } else {\n // timer / client: drive the acting node's verb directly.\n world.clock += 1;\n const at = world.clock;\n const actor = event.node;\n const node = world.handles.get(actor);\n const state = world.states[actor];\n if (node && state) {\n const inbound: RaftMsg<number> =\n event.kind === \"timer\"\n ? event.timer === \"election\"\n ? { _tag: \"election_timeout_fired\", at }\n : { _tag: \"heartbeat_fired\", at }\n : { _tag: \"client_command\", command: event.command };\n const [next, cmds] = applyInbound(node, state, inbound, at);\n world.states[actor] = next;\n park(world, actor, cmds);\n }\n }\n\n steps.push({\n event,\n pending: world.pending.length,\n states: snapshot(world.states),\n // Additive read-off: present only when a `deliver` actually consumed a\n // message. Absent (key omitted) for timer/client and empty-pool delivers,\n // so the step is structurally identical to a pre-#144 trace.\n ...(delivered ? { delivered: deliveredOf(delivered) } : {}),\n });\n}\n\n/** Deep-freeze-free structural copy of a cluster snapshot (states are already immutable values). */\nfunction snapshot(states: Record<NodeId, RaftState<number>>): ClusterStates {\n return { ...states };\n}\n\n/**\n * Run a schedule against a fresh N-node cluster, recording the full trace. Pure\n * given `(configs, schedule)`: no clock, no RNG, no IO — the only \"time\" is the\n * driver's logical clock, advanced one tick per event. Each node gets a\n * fixed-jitter rng derived from its index (distinct but deterministic timeouts).\n */\nexport function runSchedule(\n configs: readonly RaftConfig[],\n schedule: Schedule,\n): SimTrace {\n const handles = new Map<NodeId, RaftNode<number>>();\n const states: Record<NodeId, RaftState<number>> = {};\n configs.forEach((config, i) => {\n const node = createRaftNode<number>(config, fixedRng(i, configs.length));\n handles.set(config.self, node);\n states[config.self] = node.init();\n });\n\n const world: SimWorld = {\n handles,\n states,\n pending: [],\n clock: 0,\n down: new Set(),\n };\n const steps: SimStep[] = [];\n\n for (const event of schedule) {\n applyEvent(world, event, steps);\n }\n\n return {\n nodes: configs.map((c) => c.self),\n steps,\n final: snapshot(world.states),\n };\n}\n\n/**\n * Replay a schedule from a fresh cluster. Identical to {@link runSchedule} (it\n * IS `runSchedule`) — exposed under a name that documents intent at the call\n * site: a replay is meant to reproduce a prior run's trace byte-for-byte.\n */\nexport function replaySchedule(\n configs: readonly RaftConfig[],\n schedule: Schedule,\n): SimTrace {\n return runSchedule(configs, schedule);\n}\n\n// ===========================================================================\n// Comparison helpers (for the determinism property + counterexample reports)\n// ===========================================================================\n\n/**\n * Structural equality of two cluster snapshots. Used by the determinism\n * property: a run and its replay must produce identical per-node state for every\n * node. (Tests use vitest `toEqual` for the same comparison; this is the\n * dependency-free version the driver can expose for ad-hoc checks.)\n */\nexport function clusterStatesEqual(\n a: ClusterStates,\n b: ClusterStates,\n): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n// ===========================================================================\n// Re-exports the safety suite reads (so it imports the harness, not ./index)\n// ===========================================================================\n\nexport type { LogEntry, NodeId, RaftConfig, RaftState };\n"]}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { replay } from './chunk-
|
|
1
|
+
import { replay } from './chunk-2WZHS4P2.js';
|
|
2
2
|
import * as fc from 'fast-check';
|
|
3
3
|
|
|
4
4
|
// src/pbt/runners/replay-fold.ts
|
|
5
5
|
function foldEvents(machine, ctx, loaded, msgs) {
|
|
6
|
-
const { state: initialState } = replay(machine, {
|
|
6
|
+
const { state: initialState } = replay(machine, {
|
|
7
|
+
msgs: [],
|
|
8
|
+
ctx,
|
|
9
|
+
loaded: loaded ?? null
|
|
10
|
+
});
|
|
7
11
|
const updateForm = machine.update;
|
|
8
12
|
const updateMode = (() => {
|
|
9
13
|
const firstKey = Object.keys(updateForm)[0];
|
|
@@ -43,7 +47,12 @@ function propertyTerminates(machine, ctx, seqArb, terminal, opts) {
|
|
|
43
47
|
};
|
|
44
48
|
fc.assert(
|
|
45
49
|
fc.property(seqArb, (msgs) => {
|
|
46
|
-
const { finalState } = foldEvents(
|
|
50
|
+
const { finalState } = foldEvents(
|
|
51
|
+
machine,
|
|
52
|
+
ctx,
|
|
53
|
+
opts?.loaded ?? null,
|
|
54
|
+
msgs
|
|
55
|
+
);
|
|
47
56
|
if (!terminal(finalState)) {
|
|
48
57
|
throw new Error(
|
|
49
58
|
`propertyTerminates: final state did not match terminal predicate. Final: ${JSON.stringify(finalState)}`
|
|
@@ -79,7 +88,12 @@ function propertyTrace(machine, ctx, seqArb, predicate, opts) {
|
|
|
79
88
|
};
|
|
80
89
|
fc.assert(
|
|
81
90
|
fc.property(seqArb, (msgs) => {
|
|
82
|
-
const { steps, finalState } = foldEvents(
|
|
91
|
+
const { steps, finalState } = foldEvents(
|
|
92
|
+
machine,
|
|
93
|
+
ctx,
|
|
94
|
+
opts?.loaded ?? null,
|
|
95
|
+
msgs
|
|
96
|
+
);
|
|
83
97
|
if (!predicate(steps, finalState)) {
|
|
84
98
|
throw new Error(
|
|
85
99
|
`propertyTrace: predicate failed on trace length=${steps.length}, final=${JSON.stringify(finalState)}`
|
|
@@ -91,5 +105,5 @@ function propertyTrace(machine, ctx, seqArb, predicate, opts) {
|
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace };
|
|
94
|
-
//# sourceMappingURL=chunk-
|
|
95
|
-
//# sourceMappingURL=chunk-
|
|
108
|
+
//# sourceMappingURL=chunk-A56GNPI7.js.map
|
|
109
|
+
//# sourceMappingURL=chunk-A56GNPI7.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-A56GNPI7.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 using the SAME cell-dispatch shape the substrate uses in\n// both `run()` and `replay()`.\n//\n// Why duplicate the cell-dispatch (5 LOC) instead of looping `replay` once\n// per msg with `loaded: prevState`? Because `init(loaded)` runs every\n// call, and on machines with non-trivial init transforms (e.g. the audit\n// machine's `auditing → attach_debugger` resume cell) that would either\n// (a) re-fire the transform on every step, or (b) require fragile cmd-\n// subtraction to isolate this step's cmds. Calling `update` directly\n// keeps the fold pure and the cmds per-step exactly.\n//\n// Mirrors the dispatch in `packages/tea/src/index.ts` `applyUpdate` and\n// `replay`. The form-detect is identical, the same single-property lookup,\n// the same `as unknown as` cast at the table boundary.\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 type Cmd,\n type Machine,\n type Reducer,\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 mirrors the substrate's `applyUpdate` in `run()` and\n // the same loop in `replay()`. Two record forms; structural detect once.\n type CellFn = (state: S, msg: M) => readonly [S, readonly C[]];\n type ReducerRecord = Reducer<S, M, C>;\n type TransitionsTable = {\n [stateType: string]: { [msgType: string]: CellFn };\n };\n\n const updateForm = machine.update;\n type UpdateMode = \"reducer\" | \"transitions\";\n const updateMode: UpdateMode = (() => {\n const firstKey = Object.keys(updateForm as object)[0];\n if (firstKey === undefined) return \"reducer\";\n const firstValue = (updateForm as Record<string, unknown>)[firstKey];\n return typeof firstValue === \"function\" ? \"reducer\" : \"transitions\";\n })();\n\n const states: S[] = [initialState];\n const steps: Step<S, M, C>[] = [];\n let current = initialState;\n\n for (const msg of msgs) {\n let next: S;\n let emitted: readonly C[];\n if (updateMode === \"reducer\") {\n const record = updateForm as ReducerRecord;\n [next, emitted] = record[msg.type as M[\"type\"]](\n current,\n msg as Extract<M, { type: M[\"type\"] }>,\n );\n } else {\n const table = updateForm as unknown as TransitionsTable;\n const stateKey = (current as unknown as { type: string }).type;\n // biome-ignore lint/style/noNonNullAssertion: Transitions overload guarantees the cell exists; `!` required under noUncheckedIndexedAccess\n [next, emitted] = table[stateKey]![msg.type]!(current, msg);\n }\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,157 @@
|
|
|
1
|
+
// src/pure/core.ts
|
|
2
|
+
var __DEV__ = (() => {
|
|
3
|
+
try {
|
|
4
|
+
const meta = import.meta;
|
|
5
|
+
if (meta.env?.DEV !== void 0) {
|
|
6
|
+
return meta.env.DEV;
|
|
7
|
+
}
|
|
8
|
+
} catch {
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
})();
|
|
16
|
+
function deepFreeze(obj, seen) {
|
|
17
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
18
|
+
const proto = Object.getPrototypeOf(obj);
|
|
19
|
+
if (proto !== Object.prototype && proto !== Array.prototype && proto !== null)
|
|
20
|
+
return obj;
|
|
21
|
+
const visited = seen ?? /* @__PURE__ */ new WeakSet();
|
|
22
|
+
if (visited.has(obj)) return obj;
|
|
23
|
+
visited.add(obj);
|
|
24
|
+
Object.freeze(obj);
|
|
25
|
+
for (const v of Object.values(obj))
|
|
26
|
+
deepFreeze(v, visited);
|
|
27
|
+
return obj;
|
|
28
|
+
}
|
|
29
|
+
function hasFunctionValues(obj) {
|
|
30
|
+
if (obj === null || typeof obj !== "object") return null;
|
|
31
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
32
|
+
if (typeof value === "function") return key;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function assertPureResult(result, msgType) {
|
|
37
|
+
if (result && typeof result.then === "function") {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`@demlik/tea: update cell "${msgType}" returned a Promise. update must be synchronous. Move async work to interpret.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(result) || result.length < 2 || !Array.isArray(result[1])) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`@demlik/tea: update cell "${msgType}" returned a non-tuple. Expected [State, Cmd[]], got ${typeof result}.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const cmds = result[1];
|
|
48
|
+
for (const cmd of cmds) {
|
|
49
|
+
const fnField = hasFunctionValues(cmd);
|
|
50
|
+
if (fnField !== null) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`@demlik/tea: Cmd "${cmd.type}" has function field "${fnField}". Cmds must be plain data, not closures.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function detectUpdateForm(update) {
|
|
58
|
+
const firstKey = Object.keys(update)[0];
|
|
59
|
+
if (firstKey === void 0) return "reducer";
|
|
60
|
+
const firstValue = update[firstKey];
|
|
61
|
+
return typeof firstValue === "function" ? "reducer" : "transitions";
|
|
62
|
+
}
|
|
63
|
+
function formOf(machine) {
|
|
64
|
+
return machine.__form ?? detectUpdateForm(machine.update);
|
|
65
|
+
}
|
|
66
|
+
var Cmd = {
|
|
67
|
+
/**
|
|
68
|
+
* The empty Cmd array. Typed `readonly never[]` so it's assignable to any
|
|
69
|
+
* `readonly C[]` for any `C extends Cmd`. Use in `init` returns and
|
|
70
|
+
* Transitions cells that emit zero effects:
|
|
71
|
+
*
|
|
72
|
+
* init: (loaded) => [loaded ?? initial, Cmd.none],
|
|
73
|
+
* tick: (state) => [state, Cmd.none],
|
|
74
|
+
*
|
|
75
|
+
* Elm's `Cmd.none` analogue. Cultural signal alongside the runtime help:
|
|
76
|
+
* `[state, Cmd.none]` reads as intent ("this transition emits nothing"),
|
|
77
|
+
* `[state, []]` reads as "empty array of what".
|
|
78
|
+
*
|
|
79
|
+
* Frozen at runtime so a downstream consumer can't `.push()` into the
|
|
80
|
+
* shared reference.
|
|
81
|
+
*/
|
|
82
|
+
none: Object.freeze([]),
|
|
83
|
+
/**
|
|
84
|
+
* Flat concat of cmd arrays. Use when a cell composes effects from
|
|
85
|
+
* multiple conditional sources:
|
|
86
|
+
*
|
|
87
|
+
* return [next, Cmd.batch(
|
|
88
|
+
* Cmd.whenDefined(state.queueItemId, (id) => ({ type: "complete", id })),
|
|
89
|
+
* Cmd.when(state.windowId !== undefined, { type: "close_window", windowId }),
|
|
90
|
+
* [{ type: "detach_debugger", tabId: state.tabId }],
|
|
91
|
+
* )];
|
|
92
|
+
*
|
|
93
|
+
* `<const C>` keeps inline cmd literals' discriminants narrow (same
|
|
94
|
+
* rationale as `when` / `whenDefined` above).
|
|
95
|
+
*/
|
|
96
|
+
batch: (...arrs) => {
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const arr of arrs) out.push(...arr);
|
|
99
|
+
return out;
|
|
100
|
+
},
|
|
101
|
+
/**
|
|
102
|
+
* Emit `cmd` wrapped in a single-element array iff `cond` is true.
|
|
103
|
+
* Otherwise return the empty array. Spreads cleanly into the cmds array
|
|
104
|
+
* returned by a Transitions cell.
|
|
105
|
+
*
|
|
106
|
+
* `const C` (TypeScript 5.0+) keeps the inferred `type:` discriminator
|
|
107
|
+
* literal narrow when called with an object literal — so `Cmd.when(b,
|
|
108
|
+
* { type: "x", ... })` infers as `{ type: "x", ... }`, not `{ type:
|
|
109
|
+
* string, ... }`, and stays assignable to a discriminated-union arm
|
|
110
|
+
* without `as const` at the call site.
|
|
111
|
+
*/
|
|
112
|
+
when: (cond, cmd) => cond ? [cmd] : [],
|
|
113
|
+
/**
|
|
114
|
+
* If `value` is defined, call `build(value)` and emit the resulting cmd
|
|
115
|
+
* wrapped in a single-element array. If `value` is `undefined`, return
|
|
116
|
+
* the empty array. `value` is narrowed to `T` inside `build`.
|
|
117
|
+
*
|
|
118
|
+
* `const C` on the return type keeps the callback's object-literal
|
|
119
|
+
* `type:` field narrow (see `when` above for the full rationale).
|
|
120
|
+
*/
|
|
121
|
+
whenDefined: (value, build) => value !== void 0 ? [build(value)] : []
|
|
122
|
+
};
|
|
123
|
+
function subId(s) {
|
|
124
|
+
return s;
|
|
125
|
+
}
|
|
126
|
+
function foldUpdates(machine, initialState, msgs) {
|
|
127
|
+
const updateForm = machine.update;
|
|
128
|
+
const updateMode = formOf(machine);
|
|
129
|
+
let state = initialState;
|
|
130
|
+
const cmds = [];
|
|
131
|
+
for (const msg of msgs) {
|
|
132
|
+
if (__DEV__) deepFreeze(state);
|
|
133
|
+
let result;
|
|
134
|
+
if (updateMode === "reducer") {
|
|
135
|
+
result = updateForm[msg.type](
|
|
136
|
+
state,
|
|
137
|
+
msg
|
|
138
|
+
);
|
|
139
|
+
} else {
|
|
140
|
+
const table = updateForm;
|
|
141
|
+
const stateKey = state.type;
|
|
142
|
+
result = table[stateKey][msg.type](state, msg);
|
|
143
|
+
}
|
|
144
|
+
if (__DEV__) assertPureResult(result, msg.type);
|
|
145
|
+
const [next, emitted] = result;
|
|
146
|
+
state = next;
|
|
147
|
+
cmds.push(...emitted);
|
|
148
|
+
}
|
|
149
|
+
return { state, cmds };
|
|
150
|
+
}
|
|
151
|
+
function foldMsgs(machine, base, msgs) {
|
|
152
|
+
return foldUpdates(machine, base, msgs).state;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { Cmd, __DEV__, assertPureResult, deepFreeze, detectUpdateForm, foldMsgs, foldUpdates, formOf, subId };
|
|
156
|
+
//# sourceMappingURL=chunk-BNJYE4AU.js.map
|
|
157
|
+
//# sourceMappingURL=chunk-BNJYE4AU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pure/core.ts"],"names":[],"mappings":"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","file":"chunk-BNJYE4AU.js","sourcesContent":["/**\n * @demlik/tea — pure-core leaf (ADR 0006).\n *\n * This is the runtime-free pure core of the substrate. It holds the\n * client-prediction fold seam (`foldMsgs`), the form reader\n * (`formOf`/`detectUpdateForm`), and the pure type vocabulary (`Machine`,\n * `Reducer`, `Transitions`, `Cmd`, `Sub`, `Port`, …).\n *\n * **Dependency direction (the actual decoupling):** this module imports\n * NOTHING from the runtime — no `better-result`, no `run`/host/`Store`. The\n * runtime (`run`, the host, interpret, `Store`, subscribe — all in\n * `../index.ts`) imports *from* here; never the reverse. The\n * `@demlik/tea/pure` subpath re-exports this surface, and\n * `pure/import-graph.test.ts` is the regression fence asserting the pure\n * entrypoint's import graph never reaches `run`.\n */\n\n// === Dev-mode invariant enforcement ===\n//\n// Three runtime guards that catch TEA invariant violations during development.\n// Dead-code-eliminated in production by any bundler that replaces\n// `import.meta.env.DEV` (Vite) or `process.env.NODE_ENV` (webpack/esbuild).\n//\n// 1. deepFreeze(state) before passing to update — catches mutation\n// 2. Thenable check on return value — catches async update\n// 3. Walk cmd fields for function types — catches closures-as-data\n//\n// These enforce invariants 1 and 2 at runtime where the type system cannot.\nexport const __DEV__: boolean = (() => {\n try {\n const meta = import.meta as unknown as { env?: { DEV?: boolean } };\n if (meta.env?.DEV !== undefined) {\n return meta.env.DEV;\n }\n } catch {\n // import.meta not available (Node CJS, service workers)\n }\n try {\n return (\n typeof process !== \"undefined\" && process.env?.NODE_ENV !== \"production\"\n );\n } catch {\n return false;\n }\n})();\n\nexport function deepFreeze<T>(obj: T, seen?: WeakSet<object>): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n const proto = Object.getPrototypeOf(obj);\n if (proto !== Object.prototype && proto !== Array.prototype && proto !== null)\n return obj;\n const visited = seen ?? new WeakSet();\n if (visited.has(obj as object)) return obj;\n visited.add(obj as object);\n Object.freeze(obj);\n for (const v of Object.values(obj as Record<string, unknown>))\n deepFreeze(v, visited);\n return obj;\n}\n\nexport function hasFunctionValues(obj: unknown): string | null {\n if (obj === null || typeof obj !== \"object\") return null;\n for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n if (typeof value === \"function\") return key;\n }\n return null;\n}\n\nexport function assertPureResult(result: unknown, msgType: string): void {\n if (result && typeof (result as { then?: unknown }).then === \"function\") {\n throw new Error(\n `@demlik/tea: update cell \"${msgType}\" returned a Promise. ` +\n `update must be synchronous. Move async work to interpret.`,\n );\n }\n if (\n !Array.isArray(result) ||\n result.length < 2 ||\n !Array.isArray(result[1])\n ) {\n throw new Error(\n `@demlik/tea: update cell \"${msgType}\" returned a non-tuple. ` +\n `Expected [State, Cmd[]], got ${typeof result}.`,\n );\n }\n const cmds = result[1] as readonly { type: string }[];\n for (const cmd of cmds) {\n const fnField = hasFunctionValues(cmd);\n if (fnField !== null) {\n throw new Error(\n `@demlik/tea: Cmd \"${cmd.type}\" has function field \"${fnField}\". ` +\n `Cmds must be plain data, not closures.`,\n );\n }\n }\n}\n\n// === Cmd: tagged-union, one-shot effect ===\nexport type Cmd<T extends string = string> = { type: T };\n\n// === SyncReturn<S, C>: the compile-time reentrancy guard (ADR 0003 #5) ===\n//\n// The canon rule (`.patterns/tea-do/reentrancy.md`,\n// `.patterns/tea-do/reentrancy-deadlock.md`): a reducer must never\n// inline-`await` a re-entrant result. \"Non-reentrant by default\" ≡ \"the reducer\n// is pure AND synchronous\": a reducer that returns a `Promise` (because it is\n// `async`, or inline-`await`s) is the suspended-but-still-blocking turn Orleans\n// serializes against — it occupies the single-writer slot across a suspension\n// point and can be re-entered mid-flight → deadlock.\n//\n// So the guard is purely type-level: every reducer/transition cell returns\n// `SyncReturn<S, C>` — the synchronous result tuple, intersected with `{ then?:\n// never }` to make it *non-thenable*. A `Promise<readonly [S, readonly C[]]>`\n// is an object with a `then` method, so it fails to satisfy `then?: never`; an\n// `async (s, m) => [...]` (or one that inline-`await`s) is therefore\n// unrepresentable at compile time — `tsc` rejects it *at the cell*, not at a\n// runtime hang. A plain synchronous tuple `[next, cmds]` has no `then` property,\n// so the optional-absent `then?: never` is satisfied and existing pure reducers\n// compile unchanged. `SyncReturn<S, C>` is also assignable *to* `readonly [S,\n// readonly C[]]` (the intersection is narrower), so the runtime dispatch in\n// `run`/`replay` consumes a cell result exactly as before.\n//\n// This mirrors `retry-backoff`'s `Rng` brand (#63): the obligation lives in the\n// type at the construction boundary (\"parse, don't validate\"), not in a comment.\nexport type SyncReturn<S, C extends Cmd> = readonly [S, readonly C[]] & {\n // A thenable carries a callable `then`; forbidding it rejects every Promise.\n // Optional + `never` means \"absent on a sync tuple, impossible on a Promise\".\n readonly then?: never;\n};\n\n// === update form: Reducer vs Transitions, tagged once ===\n//\n// The substrate accepts two `update` shapes (see the `Machine.update` union and\n// the `defineMachine` overloads):\n//\n// - \"reducer\" — flat record keyed by Msg.type, every cell a function.\n// - \"transitions\" — 2D table keyed by State.type then Msg.type, every cell a\n// record of functions.\n//\n// Which one a given machine is must be known at runtime by `run`, `replay`, and\n// the `withX` wrappers (their dispatch / key-enumeration / reserved-namespace\n// scans differ per form). Rather than re-derive it structurally at every reader\n// — a `typeof update[firstKey] === \"function\"` heuristic that breaks the day a\n// reducer cell is itself an object-with-a-call — `defineMachine` computes the\n// form ONCE at construction and stamps it on the machine as a non-enumerable\n// `__form` (so it never serializes, never collides with a Msg.type key, never\n// shows up in `Object.keys(machine)`). Every reader calls `formOf(machine)`.\nexport type UpdateForm = \"reducer\" | \"transitions\";\n\n// The single structural heuristic, defined ONCE. Used only by `defineMachine`\n// (and `formOf`'s fallback for machines built without it, e.g. a plain object\n// literal annotated as `Machine`). A Reducer's first own value is a function; a\n// Transitions table's first own value is a record (of functions). An empty\n// record (`M` is `never`) can never dispatch a Msg, so the form is irrelevant —\n// \"reducer\" is returned arbitrarily.\nexport function detectUpdateForm(update: object): UpdateForm {\n const firstKey = Object.keys(update)[0];\n if (firstKey === undefined) return \"reducer\";\n const firstValue = (update as Record<string, unknown>)[firstKey];\n return typeof firstValue === \"function\" ? \"reducer\" : \"transitions\";\n}\n\n// The single reader every form-sensitive site goes through. Prefers the\n// `__form` tag stamped by `defineMachine` (authoritative — computed once at the\n// typed construction boundary); falls back to `detectUpdateForm` only for a\n// machine that never passed through `defineMachine`. No reader re-implements the\n// heuristic.\nexport function formOf(machine: {\n update: object;\n __form?: UpdateForm;\n}): UpdateForm {\n return machine.__form ?? detectUpdateForm(machine.update);\n}\n\n// === Reducer<S, M, C>: record-of-handlers form of `update` ===\n//\n// Flat dispatch table keyed by `Msg.type`. Each cell is a pure transition for\n// a single Msg variant, narrowed via `Extract<M, { type: K }>`. The mapped type\n// is *load-bearing*: adding a Msg variant without a matching key in the\n// Reducer record is a compile error — no `absurd()` helper at call sites, no\n// silent fall-through, no default branch hiding impurity.\n//\n// Strengthens invariant 2 (pure transitions — the record form has no\n// fall-through default to hide impurity behind) and invariant 7 (identity is\n// explicit — the Msg variant set is load-bearing at the type level).\n//\n// `defineMachine` accepts the Reducer record form via overload. The runtime\n// dispatches via `update[msg.type](state, msg)`.\nexport type Reducer<S, M extends { type: string }, C extends Cmd> = {\n [K in M[\"type\"]]: (\n state: S,\n msg: Extract<M, { type: K }>,\n // `SyncReturn<S, C>` (not `readonly [S, readonly C[]]`) is the reentrancy\n // guard: a non-thenable return type, so an `async`/inline-`await`ing cell —\n // which returns `Promise<...>` — fails to compile at the cell. See the\n // `SyncReturn` doc above.\n ) => SyncReturn<S, C>;\n};\n\n// === Transitions<S, M, C>: table form of `update` for state-machine-shaped machines ===\n//\n// When `State` is itself a discriminated union (`State.type` is the active\n// phase), the table form makes every (state.type × msg.type) cell explicit at\n// the type level. Missing a cell — e.g. forgetting how `phase: \"settling\"`\n// handles `msg.type: \"tab_stepped\"` — fails to compile. No nested switch, no\n// fall-through, no `_ -> (state, [])` default branch hiding regressions.\n//\n// Each cell receives the narrowed `State` for its phase and the narrowed\n// `Msg` for its variant via two `Extract` lookups. Phantom-narrow at the\n// type level; pure data at runtime.\n//\n// `defineMachine` accepts a third `update` form for `Transitions<S, M, C>` —\n// the runtime dispatches via `update[state.type][msg.type](state, msg)`.\n//\n// Strengthens invariant 2 (table form has no fall-through default),\n// invariant 6 (runtime walks the table predictably, no hidden dispatch\n// fallback), and invariant 7 (both state.type and msg.type are load-bearing\n// at the type level).\nexport type Transitions<\n S extends { type: string },\n M extends { type: string },\n C extends Cmd,\n> = {\n [P in S[\"type\"]]: {\n [K in M[\"type\"]]: (\n state: Extract<S, { type: P }>,\n msg: Extract<M, { type: K }>,\n // Same reentrancy guard as `Reducer`: a transition cell that returns a\n // Promise (async / inline-await) is the suspended-blocking turn the canon\n // forbids, so its return is the non-thenable `SyncReturn<S, C>`.\n ) => SyncReturn<S, C>;\n };\n};\n\n// === Cmd: namespace for conditional Cmd emission ===\n//\n// Two recurring frictions in reducer cells:\n//\n// 1. \"Emit cmd X only when condition Y holds.\"\n// Hand-rolled: `cond ? [{ type: \"x\", ... }] : []`\n// With Cmd: `Cmd.when(cond, { type: \"x\", ... })`\n//\n// 2. \"Emit cmd X carrying a value that may be undefined; if undefined,\n// emit nothing.\"\n// Hand-rolled: `value !== undefined ? [{ type: \"x\", value }] : []`\n// With Cmd: `Cmd.whenDefined(value, (v) => ({ type: \"x\", value: v }))`\n//\n// Both forms return `readonly C[]` so they spread cleanly into the cmds\n// array returned by a Transitions cell:\n//\n// return [next, [\n// ...Cmd.when(state.tabId !== undefined, { type: \"detach_debugger\", tabId: state.tabId }),\n// ...Cmd.whenDefined(state.queueItemId, (id) => ({\n// type: \"queue:complete\", queueItemId: id, status: \"done\",\n// })),\n// ]];\n//\n// `Cmd` as a value namespace coexists with `Cmd<T>` as a type — TypeScript's\n// declaration merging puts them in different name spaces (type vs. value).\n// The reader sees `Cmd<...>` in type position and `Cmd.when(...)` in\n// expression position; no ambiguity at use sites.\n//\n// Why a namespace and not top-level `when` / `whenDefined`: `when` at module\n// scope reads as a generic conditional, but these helpers are specifically\n// for *Cmd emission* (return `readonly C[]`, the cmds-array contract of\n// every Transitions cell). The namespace pins that intent at the call site.\nexport const Cmd = {\n /**\n * The empty Cmd array. Typed `readonly never[]` so it's assignable to any\n * `readonly C[]` for any `C extends Cmd`. Use in `init` returns and\n * Transitions cells that emit zero effects:\n *\n * init: (loaded) => [loaded ?? initial, Cmd.none],\n * tick: (state) => [state, Cmd.none],\n *\n * Elm's `Cmd.none` analogue. Cultural signal alongside the runtime help:\n * `[state, Cmd.none]` reads as intent (\"this transition emits nothing\"),\n * `[state, []]` reads as \"empty array of what\".\n *\n * Frozen at runtime so a downstream consumer can't `.push()` into the\n * shared reference.\n */\n none: Object.freeze([]) as readonly never[],\n\n /**\n * Flat concat of cmd arrays. Use when a cell composes effects from\n * multiple conditional sources:\n *\n * return [next, Cmd.batch(\n * Cmd.whenDefined(state.queueItemId, (id) => ({ type: \"complete\", id })),\n * Cmd.when(state.windowId !== undefined, { type: \"close_window\", windowId }),\n * [{ type: \"detach_debugger\", tabId: state.tabId }],\n * )];\n *\n * `<const C>` keeps inline cmd literals' discriminants narrow (same\n * rationale as `when` / `whenDefined` above).\n */\n batch: <const C extends Cmd>(\n ...arrs: readonly (readonly C[])[]\n ): readonly C[] => {\n const out: C[] = [];\n for (const arr of arrs) out.push(...arr);\n return out;\n },\n\n /**\n * Emit `cmd` wrapped in a single-element array iff `cond` is true.\n * Otherwise return the empty array. Spreads cleanly into the cmds array\n * returned by a Transitions cell.\n *\n * `const C` (TypeScript 5.0+) keeps the inferred `type:` discriminator\n * literal narrow when called with an object literal — so `Cmd.when(b,\n * { type: \"x\", ... })` infers as `{ type: \"x\", ... }`, not `{ type:\n * string, ... }`, and stays assignable to a discriminated-union arm\n * without `as const` at the call site.\n */\n when: <const C extends Cmd>(cond: boolean, cmd: C): readonly C[] =>\n cond ? [cmd] : [],\n\n /**\n * If `value` is defined, call `build(value)` and emit the resulting cmd\n * wrapped in a single-element array. If `value` is `undefined`, return\n * the empty array. `value` is narrowed to `T` inside `build`.\n *\n * `const C` on the return type keeps the callback's object-literal\n * `type:` field narrow (see `when` above for the full rationale).\n */\n whenDefined: <T, const C extends Cmd>(\n value: T | undefined,\n build: (value: T) => C,\n ): readonly C[] => (value !== undefined ? [build(value)] : []),\n} as const;\n\n// === SubId: branded string for Sub identity ===\n//\n// Subs are reconciled by id on every transition (canon §2.5, invariant 7).\n// Before branding, `Sub.id` was a raw `string` — a typo at one call site and a\n// different typo at another would silently produce two distinct subs that\n// looked identical to a human reader. The brand makes id construction explicit\n// (`subId(\"...\")`) and catches accidental string-where-SubId at the type\n// level.\n//\n// The brand is structural: `string & { __brand: \"SubId\" }`. The `subId(s)`\n// constructor is the ONE permitted cast in the substrate — every other call\n// site must go through it.\nexport type SubId = string & { readonly __brand: \"SubId\" };\n\n/**\n * Construct a `SubId` from a string. Use this everywhere a Sub literal is\n * built — including ids derived from runtime data (`subId(\\`ws:${auditId}\\`)`).\n * The brand is structural, not nominal, so dynamic ids are permitted; the\n * constructor's job is to make the identity decision explicit at every call\n * site so the type system can catch accidental raw-string drift.\n *\n * Strengthens invariant 7 (identity is explicit).\n */\nexport function subId(s: string): SubId {\n return s as SubId;\n}\n\n// === Sub: tagged-union, continuous source of msgs; stable id used for diff/reconcile ===\nexport type Sub<T extends string = string> = { id: SubId; type: T };\n\n// === Port<T>: typed escape hatch for \"data leaving the runtime\" ===\n//\n// A Port is a named, typed channel that Cmd handlers can `emit` to via the\n// augmented `ctx.emit(port, value)`. Subscribers attach via\n// `runtime.subscribePort(port, listener)` and receive every emitted value\n// synchronously (same fanout discipline as `runtime.observe`).\n//\n// Why Ports are a substrate primitive, distinct from State and Observe:\n// - **State** is \"the world as the program sees it.\" Folding outgoing\n// announcements into State turns ephemeral signals into persisted facts —\n// the classic anti-pattern that motivated this primitive.\n// - **Observe** sees every transition `(msg, state)`. That's the right channel\n// for devtools and logging, but it forces every consumer to filter the\n// entire firehose for a single signal.\n// - **Ports** are typed and selective: one channel per concept, subscribers\n// only see what the handler chose to emit. Mirrors Elm's outgoing-port\n// semantics — a typed declaration with `port` keyword whose values flow\n// out of the program to the host.\n//\n// Port identity is **by reference** — the returned object IS the identity.\n// Name is metadata for debugging. To prevent the \"two modules definePort the\n// same name expecting to share a channel, silently get distinct ports\" class\n// of bug, `definePort` asserts the name has not been seen in this process —\n// the symmetric runtime check to `SubId` (canon §2.12, invariant 7). Each\n// definePort call must use a unique name; if two modules need the same port,\n// one module exports it and the other imports it.\nexport interface Port<T> {\n readonly __brand: \"port\";\n readonly name: string;\n // Phantom field — never assigned, never read at runtime. Carries `T` for\n // inference at `ctx.emit` and `subscribePort` call sites. Using `?` so\n // `definePort` can produce a Port without constructing a value of `T`.\n readonly __t?: T;\n}\n\n/**\n * Augmentation injected onto `ctx` inside Cmd handlers. Handlers receive\n * `ctx & PortEmitter` so they can call `ctx.emit(port, value)` synchronously.\n * Subscribers registered via `runtime.subscribePort(port, listener)` receive\n * the value immediately.\n *\n * Emitting to a port with no subscribers is a no-op (does not throw).\n */\nexport interface PortEmitter {\n emit<T>(port: Port<T>, value: T): void;\n}\n\n// === NoCtx: the context-free marker ===\n//\n// A handler, Sub, or machine that reads NOTHING from `ctx` says so by using\n// `NoCtx` for its Ctx slot. This is the DELIBERATE absence of context — not\n// the accidental `unknown` looseness that erodes a boundary (see issue #64).\n//\n// `unknown` at a Ctx seam is ambiguous: it can mean \"I intentionally need no\n// context\" OR \"I gave up tightening this type.\" `NoCtx` resolves that\n// ambiguity at the type level. A reader (and the next agent) sees `NoCtx` and\n// knows the context-free-ness is a choice the author made, not a hole.\n//\n// Shape: an empty readonly record. A `Ctx & PortEmitter` is-a `NoCtx`, so a\n// context-free handler still composes at a richer call site. It carries no\n// own fields, so the name's claim (\"reads nothing from ctx\") is what callers\n// read at the seam.\n//\n// Strengthens invariant 6 (no silent looseness — a context-free seam is named,\n// not inferred) and invariant 8 (the boundary is legible; `unknown` stays\n// reserved for genuine wire-edge erasure, not for \"didn't bother\").\nexport type NoCtx = Readonly<Record<never, never>>;\n\n/** Spelled-out alias for `NoCtx`, for sites that prefer the long name. */\nexport type ContextFree = NoCtx;\n\n// === Interpret<M, C, Ctx>: record-of-handlers form of `interpret` ===\n//\n// Flat dispatch table keyed by `Cmd.type`. Each cell receives the narrowed Cmd\n// and the runtime-augmented Ctx (`Ctx & PortEmitter`) and resolves to a\n// follow-up Msg or `void` (fire-and-forget). The mapped type makes a missing\n// handler a compile error — `defineMachine` cannot accept the dictionary until\n// every Cmd variant has one.\n//\n// Hoisted out of `Machine.interpret` so consumers can type a free-standing\n// handler dictionary with `Interpret<MyMsg, MyCmd, MyCtx>` instead of\n// re-declaring the mapped type at every effects module.\n//\n// Strengthens invariant 2 (the record form has no fall-through default to\n// hide impurity behind) and invariant 7 (identity is explicit — the Cmd\n// variant set is load-bearing at the type level).\nexport type Interpret<M extends { type: string }, C extends Cmd, Ctx> = {\n [K in C[\"type\"]]: (\n cmd: Extract<C, { type: K }>,\n ctx: Ctx & PortEmitter,\n // biome-ignore lint/suspicious/noConfusingVoidType: an interpret handler returns a follow-up Msg or nothing; `void` permits no-return bodies that `M | undefined` would reject\n ) => Promise<M | void>;\n};\n\n// === Subscribe<M, U, Ctx>: record-of-handlers form of `subscribe` ===\n//\n// Flat dispatch table keyed by `Sub.type`. Each cell receives the narrowed\n// Sub, the Ctx, and a `dispatch` to fire follow-up Msgs. Returns a cleanup\n// function the substrate calls when the Sub is reconciled out (state\n// transitioned away). The mapped type guarantees every Sub variant has a\n// handler at the type level; runtime dispatch is a single property lookup.\n//\n// Hoisted out of `Machine.subscribe` so consumers can type a free-standing\n// handler dictionary with `Subscribe<MyMsg, MySub, MyCtx>` instead of\n// re-declaring the mapped type at every subs module.\n//\n// Strengthens invariant 7 (identity is explicit — the Sub variant set is\n// load-bearing at the type level).\nexport type Subscribe<M extends { type: string }, U extends Sub, Ctx> = {\n [K in U[\"type\"]]: (\n sub: Extract<U, { type: K }>,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n ) => () => void;\n};\n\n// === Machine: pure data, host-agnostic ===\n//\n// `update` is stored as a union of the two record forms — the runtime branches\n// on shape at dispatch time. The public `defineMachine` overloads enforce one\n// shape per call so consumers never see the union themselves.\n//\n// - Reducer form: `Reducer<S, M, C>` — flat record keyed by Msg.type;\n// exhaustiveness enforced by the mapped type.\n// - Transitions form: `Transitions<S, M, C>` — 2D table keyed by\n// `state.type` then `msg.type`; only available when `S extends { type:\n// string }`. Conditional in the union so machines without a discriminated\n// State don't widen to include this branch.\n//\n// `M` is constrained to `{ type: string }` because both record forms require\n// a string discriminant.\n//\n// `interpret` is conditionally optional: when `C` is `Cmd<never>` the\n// interpret map is keyed by `never`, so the only valid value is `{}`. Forcing\n// every cmdless machine to write `interpret: {} as never` is ceremony for a\n// shape the type already pins. The tuple-wrap (`[C] extends [Cmd<never>]`)\n// disables distributive conditional so a real cmd union (e.g.\n// `Cmd<\"a\"> | Cmd<\"b\">`) doesn't degrade to optional just because one arm\n// happens to be `Cmd<never>`. Same trick the `update` field uses.\nexport type Machine<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n> = {\n /**\n * Boot the runtime. Called once by `run(...)` with whatever `Store.load()`\n * returned: `null` on fresh boot, the persisted state on rehydrate.\n *\n * **Contract:** when `loaded !== null`, init MUST return `[loaded, []]` —\n * no Cmds. Init's rehydrate branch is the migration / parse boundary, not\n * the boot-effect hook. See Invariant 2 in `.patterns/tea/tea-invariants.md`.\n *\n * Boot effects routes:\n * - Stateless infrastructure → host module top, outside TEA.\n * - State-conditional resume → a `boot` Msg the host dispatches once\n * after `run(...)` returns.\n *\n * Violations are caught at runtime by `replay` (which throws with a\n * pointer to the alternatives).\n */\n init: (loaded: S | null, ctx: Ctx) => readonly [S, readonly C[]];\n update:\n | Reducer<S, M, C>\n // Wrap in tuple to disable distributive conditional behavior. Without\n // `[S]`, TS distributes the conditional across `S`'s union members,\n // producing `Transitions<A,...> | Transitions<B,...>` instead of the\n // desired `Transitions<A|B,...>` — the table form needs a single\n // mapping over the full union of state types, not separate tables per\n // member.\n | ([S] extends [{ type: string }] ? Transitions<S, M, C> : never);\n subscriptions?: (state: S) => readonly U[];\n subscribe?: Subscribe<M, U, Ctx>;\n /**\n * The update form (\"reducer\" | \"transitions\"), stamped non-enumerably by\n * `defineMachine` at construction (see `UpdateForm` / `formOf`). Optional in\n * the type so the structural `Machine` annotation form keeps accepting plain\n * object literals; readers go through `formOf`, which falls back to\n * `detectUpdateForm` when the tag is absent. Never written by hand.\n */\n readonly __form?: UpdateForm;\n} & ([C] extends [Cmd<never>]\n ? { interpret?: Interpret<M, C, Ctx> }\n : { interpret: Interpret<M, C, Ctx> });\n\n// === foldUpdates: the single internal fold `replay` and `foldMsgs` share ===\n//\n// Folds `machine.update` over `msgs` from `initialState`, branching on\n// `formOf(machine)` — the same reader `run` uses — so every fold site agrees\n// on the reducer-vs-transitions form by construction (no second copy of the\n// heuristic to drift). Returns the final state plus the Cmds the cells emitted\n// along the way; the caller keeps them (`replay`) or discards them (`foldMsgs`).\n//\n// Touches no `Store`, no `interpret` handler, and starts no subscription — it\n// only calls `update` cells. The two public folds differ only in how they\n// enter and what they return: `replay` enters via `init` and returns\n// `{ state, cmds, subs }`; `foldMsgs` enters from a base state and returns `S`.\nexport function foldUpdates<S, M extends { type: string }, C extends Cmd>(\n machine: { update: object; __form?: UpdateForm },\n initialState: S,\n msgs: readonly M[],\n): { state: S; cmds: C[] } {\n type CellFn = (state: S, msg: M) => readonly [S, readonly C[]];\n type ReducerRecord = Reducer<S, M, C>;\n type TransitionsTable = {\n [stateType: string]: { [msgType: string]: CellFn };\n };\n const updateForm = machine.update;\n const updateMode: UpdateForm = formOf(machine);\n\n let state: S = initialState;\n const cmds: C[] = [];\n\n for (const msg of msgs) {\n if (__DEV__) deepFreeze(state);\n\n let result: readonly [S, readonly C[]];\n if (updateMode === \"reducer\") {\n result = (updateForm as ReducerRecord)[msg.type as M[\"type\"]](\n state,\n msg as Extract<M, { type: M[\"type\"] }>,\n );\n } else {\n const table = updateForm as unknown as TransitionsTable;\n const stateKey = (state as unknown as { type: string }).type;\n // biome-ignore lint/style/noNonNullAssertion: Transitions overload guarantees the (state.type × msg.type) cell exists; `!` required under noUncheckedIndexedAccess\n result = table[stateKey]![msg.type]!(state, msg);\n }\n\n if (__DEV__) assertPureResult(result, msg.type);\n\n const [next, emitted] = result;\n state = next;\n cmds.push(...emitted);\n }\n\n return { state, cmds };\n}\n\n// === foldMsgs: runtime-free client-prediction fold seam (ADR 0006, #211) ===\n//\n// Folds `machine.update` over an ordered `Msg[]` starting from a caller-supplied\n// `base` state and returns the resulting state ONLY. This is the client-side\n// replay primitive a prediction loop needs (the Gambetta/Valve\n// authoritative-server reconcile step): re-simulate a queue of un-acked inputs\n// on top of an authoritative snapshot —\n// `foldMsgs(machine, snapshot, pendingInputs)`.\n//\n// Distinct from `replay` (the test idiom), by design (ADR 0006):\n// - Enters from a direct `base` parameter, NOT via `init`/`loaded` — so\n// reconciliation correctness never depends on the machine author's `init`\n// rehydrate discipline. Takes no `ctx` (the fold calls `update` only).\n// - Returns just the final `S` — not `{ state, cmds, subs }`. During prediction\n// the inputs' effects were already sent to the server; re-emitting their Cmds\n// on the client would double-fire, and `subs` are a runtime concern. Returning\n// only `S` makes \"replay fires no effects\" structural, not caller discipline.\n// A caller that wants the emitted Cmds for assertions uses `replay`.\n//\n// Invokes no `Store`, no `interpret` handler, and starts no subscription — it\n// shares `replay`'s `foldUpdates` fold, which only calls `update` cells, and\n// reads the reducer-vs-transitions form via the same `formOf(machine)` reader\n// `run`/`replay` use, so it agrees with them by construction.\n//\n// Per ADR 0006 the runtime-free *guarantee* lives on a dedicated\n// `@demlik/tea/pure` subpath whose module graph never reaches `run`; that\n// subpath export + import-graph guard are #213's scope. Here `foldMsgs` ships\n// as a reachable public API from root; #213 formalizes the boundary.\nexport function foldMsgs<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(machine: Machine<S, M, C, U, Ctx>, base: S, msgs: readonly M[]): S {\n return foldUpdates<S, M, C>(machine, base, msgs).state;\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"]}
|