@demlik/tea 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/agent/index.d.ts +907 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/authed-call/index.d.ts +119 -0
- package/dist/authed-call/index.js +148 -0
- package/dist/authed-call/index.js.map +1 -0
- package/dist/await-terminal/index.d.ts +99 -0
- package/dist/await-terminal/index.js +75 -0
- package/dist/await-terminal/index.js.map +1 -0
- package/dist/batch-window/index.d.ts +259 -0
- package/dist/batch-window/index.js +43 -0
- package/dist/batch-window/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -50
- package/dist/cache/index.js.map +1 -1
- package/dist/chunk-27N2SPTN.js +222 -0
- package/dist/chunk-27N2SPTN.js.map +1 -0
- package/dist/chunk-2YOE5PIS.js +45 -0
- package/dist/chunk-2YOE5PIS.js.map +1 -0
- package/dist/chunk-3545BOYL.js +65 -0
- package/dist/chunk-3545BOYL.js.map +1 -0
- package/dist/chunk-3XJWC7VW.js +61 -0
- package/dist/chunk-3XJWC7VW.js.map +1 -0
- package/dist/chunk-5DEQ5I3U.js +132 -0
- package/dist/chunk-5DEQ5I3U.js.map +1 -0
- package/dist/chunk-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.js.map +1 -0
- package/dist/chunk-6RGBKODV.js +268 -0
- package/dist/chunk-6RGBKODV.js.map +1 -0
- package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
- package/dist/chunk-7FYQGDMF.js.map +1 -0
- package/dist/chunk-7W4LMPUW.js +186 -0
- package/dist/chunk-7W4LMPUW.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
- package/dist/chunk-DILMKOZ7.js.map +1 -0
- package/dist/chunk-EHIX7NYZ.js +93 -0
- package/dist/chunk-EHIX7NYZ.js.map +1 -0
- package/dist/chunk-HCEVHFJ7.js +54 -0
- package/dist/chunk-HCEVHFJ7.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.js.map +1 -0
- package/dist/chunk-J6527VRQ.js +56 -0
- package/dist/chunk-J6527VRQ.js.map +1 -0
- package/dist/chunk-JELSZN25.js +47 -0
- package/dist/chunk-JELSZN25.js.map +1 -0
- package/dist/chunk-K575PT4S.js +58 -0
- package/dist/chunk-K575PT4S.js.map +1 -0
- package/dist/chunk-K7Q23EQ4.js +20 -0
- package/dist/chunk-K7Q23EQ4.js.map +1 -0
- package/dist/chunk-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
- package/dist/chunk-LP7VQBRG.js.map +1 -0
- package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
- package/dist/chunk-MMH7CW5K.js.map +1 -0
- package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
- package/dist/chunk-NSDFA6M6.js.map +1 -0
- package/dist/chunk-ON24RKSE.js +60 -0
- package/dist/chunk-ON24RKSE.js.map +1 -0
- package/dist/chunk-PS23H6KQ.js +87 -0
- package/dist/chunk-PS23H6KQ.js.map +1 -0
- package/dist/chunk-QLA76GIT.js +162 -0
- package/dist/chunk-QLA76GIT.js.map +1 -0
- package/dist/chunk-S3FQN4SL.js +506 -0
- package/dist/chunk-S3FQN4SL.js.map +1 -0
- package/dist/chunk-SC3FAFZN.js +53 -0
- package/dist/chunk-SC3FAFZN.js.map +1 -0
- package/dist/chunk-SINXE47Y.js +55 -0
- package/dist/chunk-SINXE47Y.js.map +1 -0
- package/dist/chunk-SQTCS44W.js +33 -0
- package/dist/chunk-SQTCS44W.js.map +1 -0
- package/dist/chunk-TGG26XMK.js +52 -0
- package/dist/chunk-TGG26XMK.js.map +1 -0
- package/dist/chunk-UV6UWGEO.js +147 -0
- package/dist/chunk-UV6UWGEO.js.map +1 -0
- package/dist/chunk-UYWUVEP6.js +158 -0
- package/dist/chunk-UYWUVEP6.js.map +1 -0
- package/dist/chunk-VLGARN6H.js +202 -0
- package/dist/chunk-VLGARN6H.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-BoB6t7dB.d.ts +188 -0
- package/dist/deadline/index.d.ts +24 -5
- package/dist/deadline/index.js +4 -18
- package/dist/deadline/index.js.map +1 -1
- package/dist/debounce/index.d.ts +6 -0
- package/dist/debounce/index.js +1 -54
- package/dist/debounce/index.js.map +1 -1
- package/dist/devtools/index.d.ts +1 -1
- package/dist/devtools/index.js +18 -6
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +1723 -92
- package/dist/do/index.js +673 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-CO9Uh971.d.ts +180 -0
- package/dist/extension/index.d.ts +31 -13
- package/dist/extension/index.js +5 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +8 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/extension/test-utils.js +11 -3
- package/dist/extension/test-utils.js.map +1 -1
- package/dist/fan-out/index.d.ts +239 -0
- package/dist/fan-out/index.js +5 -0
- package/dist/fan-out/index.js.map +1 -0
- package/dist/idempotency/adapter.d.ts +62 -0
- package/dist/idempotency/adapter.js +4 -0
- package/dist/idempotency/adapter.js.map +1 -0
- package/dist/idempotency/index.js +1 -56
- package/dist/idempotency/index.js.map +1 -1
- package/dist/idempotent-intake/index.d.ts +186 -0
- package/dist/idempotent-intake/index.js +7 -0
- package/dist/idempotent-intake/index.js.map +1 -0
- package/dist/index.d.ts +341 -191
- package/dist/index.js +2 -1
- package/dist/llm-call/index.d.ts +309 -0
- package/dist/llm-call/index.js +14 -0
- package/dist/llm-call/index.js.map +1 -0
- package/dist/machine-viz/index.d.ts +4 -4
- package/dist/machine-viz/index.js +5 -9
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -0
- package/dist/monitored-run/index.d.ts +253 -0
- package/dist/monitored-run/index.js +10 -0
- package/dist/monitored-run/index.js.map +1 -0
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +234 -0
- package/dist/paginated-walk/index.js +15 -0
- package/dist/paginated-walk/index.js.map +1 -0
- package/dist/paginator/index.d.ts +271 -0
- package/dist/paginator/index.js +3 -0
- package/dist/paginator/index.js.map +1 -0
- package/dist/parity/index.d.ts +92 -0
- package/dist/parity/index.js +78 -0
- package/dist/parity/index.js.map +1 -0
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/arbitraries/index.js +1 -1
- package/dist/pbt/index.d.ts +15 -8
- package/dist/pbt/index.js +6 -11
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -2
- package/dist/poller/index.d.ts +288 -0
- package/dist/poller/index.js +79 -0
- package/dist/poller/index.js.map +1 -0
- package/dist/prediction/index.d.ts +130 -0
- package/dist/prediction/index.js +4 -0
- package/dist/prediction/index.js.map +1 -0
- package/dist/protocol-B5YmJr0d.d.ts +45 -0
- package/dist/pure/index.d.ts +2 -0
- package/dist/pure/index.js +4 -0
- package/dist/pure/index.js.map +1 -0
- package/dist/raft/demo.d.ts +105 -0
- package/dist/raft/demo.js +172 -0
- package/dist/raft/demo.js.map +1 -0
- package/dist/raft/index.d.ts +548 -0
- package/dist/raft/index.js +7 -0
- package/dist/raft/index.js.map +1 -0
- package/dist/raft/sim.d.ts +222 -0
- package/dist/raft/sim.js +8 -0
- package/dist/raft/sim.js.map +1 -0
- package/dist/raft/timeline.d.ts +46 -0
- package/dist/raft/timeline.js +62 -0
- package/dist/raft/timeline.js.map +1 -0
- package/dist/rate-limit/index.js +1 -43
- package/dist/rate-limit/index.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +43 -20
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +192 -0
- package/dist/reconciler/index.js +166 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/recorder/index.d.ts +7 -3
- package/dist/recorder/index.js +1 -155
- package/dist/recorder/index.js.map +1 -1
- package/dist/resilient-call/index.d.ts +294 -0
- package/dist/resilient-call/index.js +13 -0
- package/dist/resilient-call/index.js.map +1 -0
- package/dist/retry-backoff/index.d.ts +42 -5
- package/dist/retry-backoff/index.js +1 -35
- package/dist/retry-backoff/index.js.map +1 -1
- package/dist/retry-to-success/index.d.ts +109 -0
- package/dist/retry-to-success/index.js +35 -0
- package/dist/retry-to-success/index.js.map +1 -0
- package/dist/saga/index.d.ts +238 -0
- package/dist/saga/index.js +150 -0
- package/dist/saga/index.js.map +1 -0
- package/dist/snapshot/index.d.ts +334 -0
- package/dist/snapshot/index.js +5 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/subs/index.d.ts +48 -10
- package/dist/subs/index.js +1 -1
- package/dist/testing/index.d.ts +107 -2
- package/dist/testing/index.js +204 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/throttle/index.d.ts +9 -0
- package/dist/throttle/index.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +380 -0
- package/dist/throttled-input/index.js +88 -0
- package/dist/throttled-input/index.js.map +1 -0
- package/dist/token-refresh/index.d.ts +215 -0
- package/dist/token-refresh/index.js +6 -0
- package/dist/token-refresh/index.js.map +1 -0
- package/dist/trace-replay/index.d.ts +11 -2
- package/dist/trace-replay/index.js +3 -55
- package/dist/trace-replay/index.js.map +1 -1
- package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +111 -0
- package/dist/with-deadline/index.js.map +1 -0
- package/dist/with-resilience/index.d.ts +205 -0
- package/dist/with-resilience/index.js +251 -0
- package/dist/with-resilience/index.js.map +1 -0
- package/dist/with-telemetry/index.d.ts +149 -0
- package/dist/with-telemetry/index.js +70 -0
- package/dist/with-telemetry/index.js.map +1 -0
- package/dist/work-queue/adapter.d.ts +82 -0
- package/dist/work-queue/adapter.js +4 -0
- package/dist/work-queue/adapter.js.map +1 -0
- package/dist/work-queue/index.d.ts +25 -11
- package/dist/work-queue/index.js +1 -46
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +69 -0
- package/dist/work-queue/ops.js +3 -0
- package/dist/work-queue/ops.js.map +1 -0
- package/dist/workflow/index.d.ts +450 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +168 -25
- package/dist/chunk-CMG2KMWX.js.map +0 -1
- package/dist/chunk-PYBQREFU.js.map +0 -1
- package/dist/chunk-PYOCUBQJ.js.map +0 -1
- package/dist/chunk-Y5JM3UUH.js.map +0 -1
- package/dist/chunk-ZRUST6BK.js.map +0 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { createRaftNode } from './chunk-6RGBKODV.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-VLGARN6H.js.map
|
|
202
|
+
//# sourceMappingURL=chunk-VLGARN6H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/raft/sim.ts"],"names":[],"mappings":"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","file":"chunk-VLGARN6H.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,53 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
var defaultCircuitPolicy = {
|
|
3
|
-
failureThreshold: 5,
|
|
4
|
-
cooldownMs: 3e4,
|
|
5
|
-
halfOpenMaxProbes: 1
|
|
6
|
-
};
|
|
7
|
-
function initCircuit() {
|
|
8
|
-
return { phase: "closed", failures: 0 };
|
|
9
|
-
}
|
|
10
|
-
function onSuccess(state, _policy) {
|
|
11
|
-
switch (state.phase) {
|
|
12
|
-
case "closed":
|
|
13
|
-
return state.failures === 0 ? state : { phase: "closed", failures: 0 };
|
|
14
|
-
case "half_open":
|
|
15
|
-
return { phase: "closed", failures: 0 };
|
|
16
|
-
case "open":
|
|
17
|
-
return state;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function onFailure(state, policy, nowMs) {
|
|
21
|
-
switch (state.phase) {
|
|
22
|
-
case "closed": {
|
|
23
|
-
const failures = state.failures + 1;
|
|
24
|
-
return failures >= policy.failureThreshold ? { phase: "open", openedAtMs: nowMs } : { phase: "closed", failures };
|
|
25
|
-
}
|
|
26
|
-
case "half_open":
|
|
27
|
-
return { phase: "open", openedAtMs: nowMs };
|
|
28
|
-
case "open":
|
|
29
|
-
return state;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function canPass(state, policy, nowMs) {
|
|
33
|
-
switch (state.phase) {
|
|
34
|
-
case "closed":
|
|
35
|
-
return [state, true];
|
|
36
|
-
case "open": {
|
|
37
|
-
const elapsed = nowMs - state.openedAtMs;
|
|
38
|
-
if (elapsed >= policy.cooldownMs) {
|
|
39
|
-
return [{ phase: "half_open", probes: 1 }, true];
|
|
40
|
-
}
|
|
41
|
-
return [state, false];
|
|
42
|
-
}
|
|
43
|
-
case "half_open":
|
|
44
|
-
if (state.probes < policy.halfOpenMaxProbes) {
|
|
45
|
-
return [{ phase: "half_open", probes: state.probes + 1 }, true];
|
|
46
|
-
}
|
|
47
|
-
return [state, false];
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export { canPass, defaultCircuitPolicy, initCircuit, onFailure, onSuccess };
|
|
1
|
+
export { canPass, defaultCircuitPolicy, initCircuit, onFailure, onSuccess } from '../chunk-SC3FAFZN.js';
|
|
52
2
|
//# sourceMappingURL=index.js.map
|
|
53
3
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/circuit-breaker/index.ts"],"names":[],"mappings":";AAwHO,IAAM,oBAAA,GAAsC;AAAA,EACjD,gBAAA,EAAkB,CAAA;AAAA,EAClB,UAAA,EAAY,GAAA;AAAA,EACZ,iBAAA,EAAmB;AACrB;AAoBO,SAAS,WAAA,GAA4B;AAC1C,EAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,QAAA,EAAU,CAAA,EAAE;AACxC;AAYO,SAAS,SAAA,CACd,OACA,OAAA,EACc;AACd,EAAA,QAAQ,MAAM,KAAA;AAAO,IACnB,KAAK,QAAA;AAGH,MAAA,OAAO,KAAA,CAAM,aAAa,CAAA,GAAI,KAAA,GAAQ,EAAE,KAAA,EAAO,QAAA,EAAU,UAAU,CAAA,EAAE;AAAA,IACvE,KAAK,WAAA;AACH,MAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,QAAA,EAAU,CAAA,EAAE;AAAA,IACxC,KAAK,MAAA;AACH,MAAA,OAAO,KAAA;AAAA;AAEb;AAgBO,SAAS,SAAA,CACd,KAAA,EACA,MAAA,EACA,KAAA,EACc;AACd,EAAA,QAAQ,MAAM,KAAA;AAAO,IACnB,KAAK,QAAA,EAAU;AACb,MAAA,MAAM,QAAA,GAAW,MAAM,QAAA,GAAW,CAAA;AAGlC,MAAA,OAAO,QAAA,IAAY,MAAA,CAAO,gBAAA,GACtB,EAAE,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAM,GACnC,EAAE,KAAA,EAAO,QAAA,EAAU,QAAA,EAAS;AAAA,IAClC;AAAA,IACA,KAAK,WAAA;AAEH,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAM;AAAA,IAC5C,KAAK,MAAA;AACH,MAAA,OAAO,KAAA;AAAA;AAEb;AA0BO,SAAS,OAAA,CACd,KAAA,EACA,MAAA,EACA,KAAA,EACkC;AAClC,EAAA,QAAQ,MAAM,KAAA;AAAO,IACnB,KAAK,QAAA;AACH,MAAA,OAAO,CAAC,OAAO,IAAI,CAAA;AAAA,IACrB,KAAK,MAAA,EAAQ;AACX,MAAA,MAAM,OAAA,GAAU,QAAQ,KAAA,CAAM,UAAA;AAG9B,MAAA,IAAI,OAAA,IAAW,OAAO,UAAA,EAAY;AAChC,QAAA,OAAO,CAAC,EAAE,KAAA,EAAO,aAAa,MAAA,EAAQ,CAAA,IAAK,IAAI,CAAA;AAAA,MACjD;AACA,MAAA,OAAO,CAAC,OAAO,KAAK,CAAA;AAAA,IACtB;AAAA,IACA,KAAK,WAAA;AACH,MAAA,IAAI,KAAA,CAAM,MAAA,GAAS,MAAA,CAAO,iBAAA,EAAmB;AAC3C,QAAA,OAAO,CAAC,EAAE,KAAA,EAAO,WAAA,EAAa,QAAQ,KAAA,CAAM,MAAA,GAAS,CAAA,EAAE,EAAG,IAAI,CAAA;AAAA,MAChE;AACA,MAAA,OAAO,CAAC,OAAO,KAAK,CAAA;AAAA;AAE1B","file":"index.js","sourcesContent":["/**\n * @demlik/tea/circuit-breaker — per-target failure tracking as pure state + ops.\n *\n * Same shape as `@demlik/tea/work-queue`'s `ops.ts`, `@demlik/tea/rate-limit`,\n * and `@demlik/tea/idempotency`: a state type plus pure transition functions.\n * Host-agnostic by construction — no `Date.now()`, no timers, no I/O. The\n * caller injects the clock (`nowMs`) and wires the result into its own reducer\n * / interpret. This keeps the breaker inside TEA's invariant 2 (transitions are\n * pure: same inputs → same outputs, no `Date.now`) so a consumer can fold it\n * through `update` and exercise it with `replay` / the pbt fold without a fake\n * clock.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). `canPass` returns a `[next, allowed]` tuple so\n * the caller branches on the decision while threading the advanced state forward\n * — same convention as `rate-limit`'s `tryConsume` / `record`. `onSuccess` /\n * `onFailure` are the outcome-recording transitions the caller folds in once the\n * guarded call resolves.\n *\n * ## The phase machine\n *\n * A circuit breaker guards a fallible downstream target (an HTTP backend, a DB,\n * a flaky tool call). Instead of hammering a target that is already failing, it\n * trips OPEN after enough failures and fast-fails subsequent calls for a\n * cooldown, then probes with a few trial calls before fully closing again.\n * Three phases, each carrying ONLY its own data so impossible states are\n * unrepresentable (pattern 11):\n *\n * - **closed** — the healthy steady state. Calls pass through. Carries\n * `failures`: the count of *consecutive* failures since the last success.\n * A success resets it to 0; the `failureThreshold`-th failure trips the\n * breaker OPEN. Only `closed` has a failure counter — `open` and\n * `half_open` don't, because in those phases the count is irrelevant\n * (the breaker has already tripped / is already probing).\n *\n * - **open** — the tripped state. All calls fast-fail (no downstream load).\n * Carries `openedAtMs`: the clock reading at which the breaker tripped.\n * `canPass` compares `nowMs - openedAtMs` against `cooldownMs` to decide\n * when to start probing. Only `open` carries a timestamp — neither other\n * phase is timing anything.\n *\n * - **half_open** — the probing state. A limited number of trial calls are\n * allowed through to test whether the target has recovered. Carries\n * `probes`: the count of probes admitted so far this probing round.\n * `canPass` admits while `probes < halfOpenMaxProbes`, then fast-fails the\n * rest (so a slow target can't be flooded by every queued caller at once).\n * A probe success closes the breaker; a probe failure re-opens it (and\n * resets the cooldown clock). Only `half_open` counts in-flight probes.\n *\n * ### Transition table\n *\n * Rows are the current phase; columns are the event. `→` denotes the next\n * phase; `self` means unchanged. `nowMs` is the injected clock.\n *\n * | from \\ event | onSuccess | onFailure | canPass (→ allowed?) |\n * | ------------ | -------------- | -------------------------------------- | ------------------------------------------------------------------------------------ |\n * | closed | closed(0) | failures+1; trip → open(nowMs) at thr. | self → true (always) |\n * | open | self *(¹)* | self | elapsed ≥ cooldown → half_open(1), true; else self → false |\n * | half_open | → closed(0) | → open(nowMs) (probe failed) | probes < max → half_open(probes+1), true; else self → false |\n *\n * *(¹)* `onSuccess` on `open` \"shouldn't happen\": no call was admitted while\n * open, so no success can be reported for one. We leave the state unchanged\n * rather than throw — the breaker is a coordination aid, not a correctness\n * gate, and a spurious success report (e.g. a late-arriving result from before\n * the trip) must not silently un-trip the breaker. `onFailure` on `open` is\n * likewise a no-op: the breaker is already tripped; the `open` clock is anchored\n * at the trip, not bumped by late failures.\n *\n * ### The probe-admission asymmetry\n *\n * Entering `half_open` happens inside `canPass`, not `onSuccess` / `onFailure`,\n * because the cooldown is a *time* condition and only `canPass` reads the clock\n * against `openedAtMs`. The first `canPass` after the cooldown elapses both\n * transitions the phase AND admits the probe (`[half_open(1), true]`) — one\n * caller drives the recovery attempt. Subsequent `canPass` calls in the same\n * round admit up to `halfOpenMaxProbes` total, then fast-fail until a probe\n * resolves and `onSuccess` / `onFailure` moves the phase out of `half_open`.\n *\n * This module is a textbook three-state machine — a clean `/machine-viz` demo.\n *\n * NOT exported from the package root — reached via the\n * `@demlik/tea/circuit-breaker` subpath, same one-shape-per-package rule as\n * `work-queue`, `rate-limit`, and `idempotency`.\n */\n\n/**\n * Circuit-breaker policy — pure configuration, no mutable state. One policy is\n * shared across all the calls guarded by one logical breaker (often a\n * module-scope const). `CircuitState` carries the mutable counts; this carries\n * the thresholds those counts are compared against.\n */\nexport interface CircuitPolicy {\n /**\n * Consecutive failures in `closed` that trip the breaker `open`. The\n * `failureThreshold`-th failure is the one that trips — with a threshold of\n * 5, the breaker survives 4 failures and opens on the 5th.\n */\n readonly failureThreshold: number;\n /**\n * How long the breaker stays `open` before `canPass` starts admitting\n * probes, in milliseconds. Measured from `openedAtMs`; the cutoff is\n * half-open (`elapsed >= cooldownMs` admits), matching `rate-limit` /\n * `idempotency` window edges — a breaker opened at `t` first probes at\n * `t + cooldownMs`.\n */\n readonly cooldownMs: number;\n /**\n * How many probe calls `half_open` admits before fast-failing the rest of\n * the round. Caps the load on a recovering target — a single slow probe\n * can't be joined by every queued caller. The round ends when a probe\n * resolves: success closes the breaker, failure re-opens it.\n */\n readonly halfOpenMaxProbes: number;\n}\n\n/**\n * Sensible defaults: trip after 5 consecutive failures, cool down for 30s,\n * admit a single probe before deciding. Tuned for guarding a network backend;\n * override per call site rather than mutating this shared const.\n */\nexport const defaultCircuitPolicy: CircuitPolicy = {\n failureThreshold: 5,\n cooldownMs: 30_000,\n halfOpenMaxProbes: 1,\n};\n\n/**\n * The breaker's phase. A discriminated union on `phase` so each phase carries\n * ONLY its own data and impossible combinations are unrepresentable (pattern\n * 11): there is no state with both a failure count AND an open timestamp,\n * because such a state is meaningless. `phase` is the discriminant; every field\n * is `readonly` so the whole value is trivially JSON-serializable into a\n * consumer's `Store<S>` and safe to thread through a pure reducer.\n *\n * - `closed` carries `failures` — consecutive failures since last success.\n * - `open` carries `openedAtMs` — clock reading when it tripped.\n * - `half_open` carries `probes` — probes admitted this round.\n */\nexport type CircuitState =\n | { readonly phase: \"closed\"; readonly failures: number }\n | { readonly phase: \"open\"; readonly openedAtMs: number }\n | { readonly phase: \"half_open\"; readonly probes: number };\n\n/** The starting state: closed with zero recorded failures. */\nexport function initCircuit(): CircuitState {\n return { phase: \"closed\", failures: 0 };\n}\n\n/**\n * Record a successful guarded call. PURE — returns a new state; the input is\n * never mutated.\n *\n * - `closed` → reset `failures` to 0 (a success breaks the consecutive run).\n * - `half_open` → the probe succeeded; the target has recovered → `closed`.\n * - `open` → unchanged. A success \"shouldn't happen\" while open (no call\n * was admitted to produce one); a late/spurious success must\n * not silently un-trip the breaker. See module header note ¹.\n */\nexport function onSuccess(\n state: CircuitState,\n _policy: CircuitPolicy,\n): CircuitState {\n switch (state.phase) {\n case \"closed\":\n // Already closed at 0 → return the same shape (cheap; the consumer's\n // store only re-persists on a real change if it dedupes).\n return state.failures === 0 ? state : { phase: \"closed\", failures: 0 };\n case \"half_open\":\n return { phase: \"closed\", failures: 0 };\n case \"open\":\n return state;\n }\n}\n\n/**\n * Record a failed guarded call. PURE — returns a new state; the input is never\n * mutated. `nowMs` is the injected clock, stamped onto the `open` phase when the\n * breaker trips so `canPass` can measure the cooldown later.\n *\n * - `closed` → `failures + 1`; if that reaches `failureThreshold`, trip to\n * `open(nowMs)`. The threshold-th failure is the one that trips.\n * - `half_open` → the probe failed; the target is still unhealthy →\n * `open(nowMs)`, restarting the cooldown clock so a fresh\n * cooldown elapses before the next probe round.\n * - `open` → unchanged. The breaker is already tripped; the `open` clock\n * is anchored at the original trip, not bumped by late\n * failures. See module header note ¹.\n */\nexport function onFailure(\n state: CircuitState,\n policy: CircuitPolicy,\n nowMs: number,\n): CircuitState {\n switch (state.phase) {\n case \"closed\": {\n const failures = state.failures + 1;\n // `>=` not `===`: a threshold of 0 (or any already-exceeded count from a\n // policy change) trips immediately rather than over-counting forever.\n return failures >= policy.failureThreshold\n ? { phase: \"open\", openedAtMs: nowMs }\n : { phase: \"closed\", failures };\n }\n case \"half_open\":\n // A failed probe re-opens and resets the cooldown clock to nowMs.\n return { phase: \"open\", openedAtMs: nowMs };\n case \"open\":\n return state;\n }\n}\n\n/**\n * Decide whether a call may pass, advancing the phase as a side effect of the\n * decision. PURE — returns `[next, allowed]`; the input is never mutated.\n * Branch on the boolean, thread the state forward regardless:\n *\n * const [c1, allowed] = canPass(state.circuit, policy, nowMs);\n * if (!allowed) return [{ ...state, circuit: c1 }, [fastFailCmd]];\n * return [{ ...state, circuit: c1 }, [doGuardedCallCmd]];\n *\n * - `closed` → `[state, true]` — always passes; failures are recorded by\n * `onFailure` once the call resolves, not gated here.\n * - `open` → if `nowMs - openedAtMs >= cooldownMs` the cooldown has\n * elapsed → enter `half_open` and admit this caller as the\n * first probe: `[half_open(1), true]`. Otherwise still cooling\n * down → `[state, false]` (fast-fail, no downstream load).\n * - `half_open` → admit while `probes < halfOpenMaxProbes`, incrementing the\n * count: `[half_open(probes+1), true]`. Once the cap is hit →\n * `[state, false]` until a probe resolves and `onSuccess` /\n * `onFailure` moves the phase out of `half_open`.\n *\n * Entering `half_open` lives here (not in `onSuccess` / `onFailure`) because the\n * cooldown is a time condition and only `canPass` reads `nowMs` against\n * `openedAtMs`. See the module header's probe-admission note.\n */\nexport function canPass(\n state: CircuitState,\n policy: CircuitPolicy,\n nowMs: number,\n): readonly [CircuitState, boolean] {\n switch (state.phase) {\n case \"closed\":\n return [state, true];\n case \"open\": {\n const elapsed = nowMs - state.openedAtMs;\n // Half-open cutoff: elapsed exactly == cooldownMs admits the first probe,\n // matching the >= edge `rate-limit` / `idempotency` use for their windows.\n if (elapsed >= policy.cooldownMs) {\n return [{ phase: \"half_open\", probes: 1 }, true];\n }\n return [state, false];\n }\n case \"half_open\":\n if (state.probes < policy.halfOpenMaxProbes) {\n return [{ phase: \"half_open\", probes: state.probes + 1 }, true];\n }\n return [state, false];\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
type SyncReturn<S, C extends Cmd> = readonly [S, readonly C[]] & {
|
|
2
|
+
readonly then?: never;
|
|
3
|
+
};
|
|
4
|
+
type UpdateForm = "reducer" | "transitions";
|
|
5
|
+
declare function detectUpdateForm(update: object): UpdateForm;
|
|
6
|
+
declare function formOf(machine: {
|
|
7
|
+
update: object;
|
|
8
|
+
__form?: UpdateForm;
|
|
9
|
+
}): UpdateForm;
|
|
10
|
+
declare function applyCell<S, M extends {
|
|
11
|
+
type: string;
|
|
12
|
+
}, C extends Cmd>(machine: {
|
|
13
|
+
update: object;
|
|
14
|
+
__form?: UpdateForm;
|
|
15
|
+
}, state: S, msg: M): readonly [S, readonly C[]];
|
|
16
|
+
declare function msgKeysOf(machine: {
|
|
17
|
+
update: object;
|
|
18
|
+
__form?: UpdateForm;
|
|
19
|
+
}): readonly string[];
|
|
20
|
+
type Reducer<S, M extends {
|
|
21
|
+
type: string;
|
|
22
|
+
}, C extends Cmd> = {
|
|
23
|
+
[K in M["type"]]: (state: S, msg: Extract<M, {
|
|
24
|
+
type: K;
|
|
25
|
+
}>) => SyncReturn<S, C>;
|
|
26
|
+
};
|
|
27
|
+
type Transitions<S extends {
|
|
28
|
+
type: string;
|
|
29
|
+
}, M extends {
|
|
30
|
+
type: string;
|
|
31
|
+
}, C extends Cmd> = {
|
|
32
|
+
[P in S["type"]]: {
|
|
33
|
+
[K in M["type"]]: (state: Extract<S, {
|
|
34
|
+
type: P;
|
|
35
|
+
}>, msg: Extract<M, {
|
|
36
|
+
type: K;
|
|
37
|
+
}>) => SyncReturn<S, C>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
type Cmd<T extends string = string> = {
|
|
41
|
+
type: T;
|
|
42
|
+
};
|
|
43
|
+
declare const Cmd: {
|
|
44
|
+
/**
|
|
45
|
+
* The empty Cmd array. Typed `readonly never[]` so it's assignable to any
|
|
46
|
+
* `readonly C[]` for any `C extends Cmd`. Use in `init` returns and
|
|
47
|
+
* Transitions cells that emit zero effects:
|
|
48
|
+
*
|
|
49
|
+
* init: (loaded) => [loaded ?? initial, Cmd.none],
|
|
50
|
+
* tick: (state) => [state, Cmd.none],
|
|
51
|
+
*
|
|
52
|
+
* Elm's `Cmd.none` analogue. Cultural signal alongside the runtime help:
|
|
53
|
+
* `[state, Cmd.none]` reads as intent ("this transition emits nothing"),
|
|
54
|
+
* `[state, []]` reads as "empty array of what".
|
|
55
|
+
*
|
|
56
|
+
* Frozen at runtime so a downstream consumer can't `.push()` into the
|
|
57
|
+
* shared reference.
|
|
58
|
+
*/
|
|
59
|
+
readonly none: readonly never[];
|
|
60
|
+
/**
|
|
61
|
+
* Flat concat of cmd arrays. Use when a cell composes effects from
|
|
62
|
+
* multiple conditional sources:
|
|
63
|
+
*
|
|
64
|
+
* return [next, Cmd.batch(
|
|
65
|
+
* Cmd.whenDefined(state.queueItemId, (id) => ({ type: "complete", id })),
|
|
66
|
+
* Cmd.when(state.windowId !== undefined, { type: "close_window", windowId }),
|
|
67
|
+
* [{ type: "detach_debugger", tabId: state.tabId }],
|
|
68
|
+
* )];
|
|
69
|
+
*
|
|
70
|
+
* `<const C>` keeps inline cmd literals' discriminants narrow (same
|
|
71
|
+
* rationale as `when` / `whenDefined` above).
|
|
72
|
+
*/
|
|
73
|
+
readonly batch: <const C extends Cmd>(...arrs: readonly (readonly C[])[]) => readonly C[];
|
|
74
|
+
/**
|
|
75
|
+
* Emit `cmd` wrapped in a single-element array iff `cond` is true.
|
|
76
|
+
* Otherwise return the empty array. Spreads cleanly into the cmds array
|
|
77
|
+
* returned by a Transitions cell.
|
|
78
|
+
*
|
|
79
|
+
* `const C` (TypeScript 5.0+) keeps the inferred `type:` discriminator
|
|
80
|
+
* literal narrow when called with an object literal — so `Cmd.when(b,
|
|
81
|
+
* { type: "x", ... })` infers as `{ type: "x", ... }`, not `{ type:
|
|
82
|
+
* string, ... }`, and stays assignable to a discriminated-union arm
|
|
83
|
+
* without `as const` at the call site.
|
|
84
|
+
*/
|
|
85
|
+
readonly when: <const C extends Cmd>(cond: boolean, cmd: C) => readonly C[];
|
|
86
|
+
/**
|
|
87
|
+
* If `value` is defined, call `build(value)` and emit the resulting cmd
|
|
88
|
+
* wrapped in a single-element array. If `value` is `undefined`, return
|
|
89
|
+
* the empty array. `value` is narrowed to `T` inside `build`.
|
|
90
|
+
*
|
|
91
|
+
* `const C` on the return type keeps the callback's object-literal
|
|
92
|
+
* `type:` field narrow (see `when` above for the full rationale).
|
|
93
|
+
*/
|
|
94
|
+
readonly whenDefined: <T, const C extends Cmd>(value: T | undefined, build: (value: T) => C) => readonly C[];
|
|
95
|
+
};
|
|
96
|
+
type SubId = string & {
|
|
97
|
+
readonly __brand: "SubId";
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Construct a `SubId` from a string. Use this everywhere a Sub literal is
|
|
101
|
+
* built — including ids derived from runtime data (`subId(\`ws:${auditId}\`)`).
|
|
102
|
+
* The brand is structural, not nominal, so dynamic ids are permitted; the
|
|
103
|
+
* constructor's job is to make the identity decision explicit at every call
|
|
104
|
+
* site so the type system can catch accidental raw-string drift.
|
|
105
|
+
*
|
|
106
|
+
* Strengthens invariant 7 (identity is explicit).
|
|
107
|
+
*/
|
|
108
|
+
declare function subId(s: string): SubId;
|
|
109
|
+
type Sub<T extends string = string> = {
|
|
110
|
+
id: SubId;
|
|
111
|
+
type: T;
|
|
112
|
+
};
|
|
113
|
+
interface Port<T> {
|
|
114
|
+
readonly __brand: "port";
|
|
115
|
+
readonly name: string;
|
|
116
|
+
readonly __t?: T;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Augmentation injected onto `ctx` inside Cmd handlers. Handlers receive
|
|
120
|
+
* `ctx & PortEmitter` so they can call `ctx.emit(port, value)` synchronously.
|
|
121
|
+
* Subscribers registered via `runtime.subscribePort(port, listener)` receive
|
|
122
|
+
* the value immediately.
|
|
123
|
+
*
|
|
124
|
+
* Emitting to a port with no subscribers is a no-op (does not throw).
|
|
125
|
+
*/
|
|
126
|
+
interface PortEmitter {
|
|
127
|
+
emit<T>(port: Port<T>, value: T): void;
|
|
128
|
+
}
|
|
129
|
+
type NoCtx = Readonly<Record<never, never>>;
|
|
130
|
+
/** Spelled-out alias for `NoCtx`, for sites that prefer the long name. */
|
|
131
|
+
type ContextFree = NoCtx;
|
|
132
|
+
type Interpret<M extends {
|
|
133
|
+
type: string;
|
|
134
|
+
}, C extends Cmd, Ctx> = {
|
|
135
|
+
[K in C["type"]]: (cmd: Extract<C, {
|
|
136
|
+
type: K;
|
|
137
|
+
}>, ctx: Ctx & PortEmitter) => Promise<M | void>;
|
|
138
|
+
};
|
|
139
|
+
type Subscribe<M extends {
|
|
140
|
+
type: string;
|
|
141
|
+
}, U extends Sub, Ctx> = {
|
|
142
|
+
[K in U["type"]]: (sub: Extract<U, {
|
|
143
|
+
type: K;
|
|
144
|
+
}>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
|
|
145
|
+
};
|
|
146
|
+
type Machine<S, M extends {
|
|
147
|
+
type: string;
|
|
148
|
+
}, C extends Cmd, U extends Sub, Ctx> = {
|
|
149
|
+
/**
|
|
150
|
+
* Boot the runtime. Called once by `run(...)` with whatever `Store.load()`
|
|
151
|
+
* returned: `null` on fresh boot, the persisted state on rehydrate.
|
|
152
|
+
*
|
|
153
|
+
* **Contract:** when `loaded !== null`, init MUST return `[loaded, []]` —
|
|
154
|
+
* no Cmds. Init's rehydrate branch is the migration / parse boundary, not
|
|
155
|
+
* the boot-effect hook. See Invariant 2 in `.patterns/tea/tea-invariants.md`.
|
|
156
|
+
*
|
|
157
|
+
* Boot effects routes:
|
|
158
|
+
* - Stateless infrastructure → host module top, outside TEA.
|
|
159
|
+
* - State-conditional resume → a `boot` Msg the host dispatches once
|
|
160
|
+
* after `run(...)` returns.
|
|
161
|
+
*
|
|
162
|
+
* Violations are caught at runtime by `replay` (which throws with a
|
|
163
|
+
* pointer to the alternatives).
|
|
164
|
+
*/
|
|
165
|
+
init: (loaded: S | null, ctx: Ctx) => readonly [S, readonly C[]];
|
|
166
|
+
update: Reducer<S, M, C> | ([S] extends [{
|
|
167
|
+
type: string;
|
|
168
|
+
}] ? Transitions<S, M, C> : never);
|
|
169
|
+
subscriptions?: (state: S) => readonly U[];
|
|
170
|
+
subscribe?: Subscribe<M, U, Ctx>;
|
|
171
|
+
/**
|
|
172
|
+
* The update form ("reducer" | "transitions"), stamped non-enumerably by
|
|
173
|
+
* `defineMachine` at construction (see `UpdateForm` / `formOf`). Optional in
|
|
174
|
+
* the type so the structural `Machine` annotation form keeps accepting plain
|
|
175
|
+
* object literals; readers go through `formOf`, which falls back to
|
|
176
|
+
* `detectUpdateForm` when the tag is absent. Never written by hand.
|
|
177
|
+
*/
|
|
178
|
+
readonly __form?: UpdateForm;
|
|
179
|
+
} & ([C] extends [Cmd<never>] ? {
|
|
180
|
+
interpret?: Interpret<M, C, Ctx>;
|
|
181
|
+
} : {
|
|
182
|
+
interpret: Interpret<M, C, Ctx>;
|
|
183
|
+
});
|
|
184
|
+
declare function foldMsgs<S, M extends {
|
|
185
|
+
type: string;
|
|
186
|
+
}, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, base: S, msgs: readonly M[]): S;
|
|
187
|
+
|
|
188
|
+
export { Cmd as C, type Interpret as I, type Machine as M, type NoCtx as N, type Port as P, type Reducer as R, type Sub as S, type Transitions as T, type UpdateForm as U, type SubId as a, type ContextFree as b, type PortEmitter as c, type Subscribe as d, type SyncReturn as e, applyCell as f, detectUpdateForm as g, foldMsgs as h, formOf as i, msgKeysOf as m, subId as s };
|
package/dist/deadline/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Sub } from '../
|
|
2
|
-
import { S as SubscribeHandler } from '../types-
|
|
1
|
+
import { S as Sub } from '../core-BoB6t7dB.js';
|
|
2
|
+
import { S as SubscribeHandler } from '../types-B-8bcAp8.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @demlik/tea/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock
|
|
@@ -48,7 +48,21 @@ import { S as SubscribeHandler } from '../types-CwBC5wMn.js';
|
|
|
48
48
|
*/
|
|
49
49
|
type DeadlineSub = Sub<"deadline"> & {
|
|
50
50
|
readonly atMs: number;
|
|
51
|
-
};
|
|
51
|
+
} & DeadlineOpts;
|
|
52
|
+
/**
|
|
53
|
+
* Additive options the `deadlineSub` factory folds onto the Sub literal. This
|
|
54
|
+
* is the extension seam (invariant 5 — composition by reduction): future fields
|
|
55
|
+
* — `repeatMs` for a re-arming deadline, `jitterMs` for spread, etc. — land HERE
|
|
56
|
+
* as optional members, and every existing call site keeps compiling because the
|
|
57
|
+
* factory's third parameter is optional and the fields are `readonly` optionals
|
|
58
|
+
* on `DeadlineSub`. Callers are insulated from the concrete `DeadlineSub` shape:
|
|
59
|
+
* they ask for what they need by name, never positionally, so a shape extension
|
|
60
|
+
* never rewrites the ~13 `deadlineSub(id, atMs)` call sites.
|
|
61
|
+
*
|
|
62
|
+
* Empty today on purpose — the seam exists before the first extension does, so
|
|
63
|
+
* the first field is a one-line additive change, not a 13-site migration.
|
|
64
|
+
*/
|
|
65
|
+
type DeadlineOpts = {};
|
|
52
66
|
/**
|
|
53
67
|
* The Msg the deadline dispatches when the wall clock crosses `atMs`. Exported
|
|
54
68
|
* as a tagged shape so consumers can union it into their machine's Msg type and
|
|
@@ -83,8 +97,13 @@ type DeadlineExceeded = {
|
|
|
83
97
|
* deadlines on the same machine.
|
|
84
98
|
* @param atMs Absolute target in epoch milliseconds (e.g. `Date.now() + 900_000`
|
|
85
99
|
* for a 15-minute guard, or a persisted `expiresAt`).
|
|
100
|
+
* @param opts Optional additive fields folded onto the Sub literal (see
|
|
101
|
+
* {@link DeadlineOpts}). Omit it for the common case; pass it when a
|
|
102
|
+
* future field (e.g. `repeatMs`) needs to ride on the deadline. The
|
|
103
|
+
* optionality is the future-proofing seam — extending the shape
|
|
104
|
+
* never breaks an existing `deadlineSub(id, atMs)` call site.
|
|
86
105
|
*/
|
|
87
|
-
declare function deadlineSub(id: string, atMs: number): DeadlineSub;
|
|
106
|
+
declare function deadlineSub(id: string, atMs: number, opts?: DeadlineOpts): DeadlineSub;
|
|
88
107
|
/**
|
|
89
108
|
* The `subscribe["deadline"]` cell. Arms a one-shot timer for the remaining
|
|
90
109
|
* delay `max(0, atMs - Date.now())` and dispatches `deadlineExceeded(...)` when
|
|
@@ -110,4 +129,4 @@ declare const subscribeDeadline: SubscribeHandler<DeadlineSub, DeadlineExceeded,
|
|
|
110
129
|
*/
|
|
111
130
|
declare function deadlineExceeded(id: string, atMs: number): DeadlineExceeded;
|
|
112
131
|
|
|
113
|
-
export { type DeadlineExceeded, type DeadlineSub, deadlineExceeded, deadlineSub, subscribeDeadline };
|
|
132
|
+
export { type DeadlineExceeded, type DeadlineOpts, type DeadlineSub, deadlineExceeded, deadlineSub, subscribeDeadline };
|
package/dist/deadline/index.js
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function deadlineSub(id, atMs) {
|
|
6
|
-
return { id: subId(id), type: "deadline", atMs };
|
|
7
|
-
}
|
|
8
|
-
var subscribeDeadline = (sub, ctx, dispatch) => {
|
|
9
|
-
const remainingMs = Math.max(0, sub.atMs - Date.now());
|
|
10
|
-
return fromTimeout(
|
|
11
|
-
(armed) => deadlineExceeded(armed.id, armed.atMs)
|
|
12
|
-
)({ ...sub, delayMs: remainingMs }, ctx, dispatch);
|
|
13
|
-
};
|
|
14
|
-
function deadlineExceeded(id, atMs) {
|
|
15
|
-
return { type: "deadline_exceeded", id, atMs };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export { deadlineExceeded, deadlineSub, subscribeDeadline };
|
|
1
|
+
export { deadlineExceeded, deadlineSub, subscribeDeadline } from '../chunk-K7Q23EQ4.js';
|
|
2
|
+
import '../chunk-2YFLOCWX.js';
|
|
3
|
+
import '../chunk-MMH7CW5K.js';
|
|
4
|
+
import '../chunk-QLA76GIT.js';
|
|
19
5
|
//# sourceMappingURL=index.js.map
|
|
20
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|