@demlik/tea 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/agent/index.d.ts +16 -770
- package/dist/agent/index.js +18 -16
- package/dist/authed-call/index.d.ts +3 -3
- package/dist/authed-call/index.js +22 -36
- package/dist/authed-call/index.js.map +1 -1
- package/dist/await-terminal/index.d.ts +2 -2
- package/dist/await-terminal/index.js +2 -2
- package/dist/batch-window/index.d.ts +3 -3
- package/dist/batch-window/index.js +3 -3
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +5 -2
- package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
- package/dist/chunk-25LBDCBC.js.map +1 -0
- package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
- package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
- package/dist/{chunk-MMH7CW5K.js → chunk-BP2R7TPP.js} +216 -157
- package/dist/chunk-BP2R7TPP.js.map +1 -0
- package/dist/chunk-C6QC273X.js +71 -0
- package/dist/chunk-C6QC273X.js.map +1 -0
- package/dist/{chunk-7W4LMPUW.js → chunk-CAXMQJJ3.js} +17 -24
- package/dist/chunk-CAXMQJJ3.js.map +1 -0
- package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
- package/dist/chunk-D736QUGF.js.map +1 -0
- package/dist/chunk-EWJP3YYP.js +21 -0
- package/dist/chunk-EWJP3YYP.js.map +1 -0
- package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
- package/dist/chunk-FV3Z5MBE.js.map +1 -0
- package/dist/chunk-G5E74IJB.js +8 -0
- package/dist/chunk-G5E74IJB.js.map +1 -0
- package/dist/{chunk-27N2SPTN.js → chunk-IXBKFQH5.js} +12 -11
- package/dist/chunk-IXBKFQH5.js.map +1 -0
- package/dist/{chunk-SQTCS44W.js → chunk-JFFVBEDI.js} +3 -3
- package/dist/{chunk-SQTCS44W.js.map → chunk-JFFVBEDI.js.map} +1 -1
- package/dist/chunk-LYLDCIPH.js +365 -0
- package/dist/chunk-LYLDCIPH.js.map +1 -0
- package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
- package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
- package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
- package/dist/chunk-MUUROGIH.js.map +1 -0
- package/dist/{chunk-EHIX7NYZ.js → chunk-MVY5DAMF.js} +4 -4
- package/dist/{chunk-EHIX7NYZ.js.map → chunk-MVY5DAMF.js.map} +1 -1
- package/dist/{chunk-LP7VQBRG.js → chunk-NPERUUUE.js} +76 -95
- package/dist/chunk-NPERUUUE.js.map +1 -0
- package/dist/{chunk-QLA76GIT.js → chunk-PBDYW2TU.js} +68 -9
- package/dist/chunk-PBDYW2TU.js.map +1 -0
- package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
- package/dist/chunk-Q2UM7BXU.js.map +1 -0
- package/dist/chunk-RMX4EOQK.js +26 -0
- package/dist/chunk-RMX4EOQK.js.map +1 -0
- package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
- package/dist/chunk-S2EGRPCM.js.map +1 -0
- package/dist/{chunk-ON24RKSE.js → chunk-S4O26D7B.js} +7 -14
- package/dist/chunk-S4O26D7B.js.map +1 -0
- package/dist/{chunk-3XJWC7VW.js → chunk-SK5JGUY7.js} +3 -3
- package/dist/{chunk-3XJWC7VW.js.map → chunk-SK5JGUY7.js.map} +1 -1
- package/dist/{chunk-5DEQ5I3U.js → chunk-TPGASTFF.js} +8 -10
- package/dist/chunk-TPGASTFF.js.map +1 -0
- package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
- package/dist/chunk-UF4MZSLT.js.map +1 -0
- package/dist/{chunk-PS23H6KQ.js → chunk-VBUCHVCV.js} +6 -5
- package/dist/chunk-VBUCHVCV.js.map +1 -0
- package/dist/{chunk-S3FQN4SL.js → chunk-WGGAMW7Y.js} +191 -178
- package/dist/chunk-WGGAMW7Y.js.map +1 -0
- package/dist/chunk-ZJ5AOYBH.js +8 -0
- package/dist/chunk-ZJ5AOYBH.js.map +1 -0
- package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
- package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
- package/dist/circuit-breaker/index.d.ts +1 -0
- package/dist/circuit-breaker/index.js +1 -1
- package/dist/{core-BoB6t7dB.d.ts → core-42kapW0n.d.ts} +105 -6
- package/dist/deadline/index.d.ts +77 -11
- package/dist/deadline/index.js +3 -3
- package/dist/devtools/index.js +8 -28
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +483 -339
- package/dist/do/index.js +244 -214
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-CO9Uh971.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
- package/dist/extension/index.d.ts +25 -7
- package/dist/extension/index.js +4 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +4 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js +7 -10
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/fan-out/index.d.ts +10 -10
- package/dist/fan-out/index.js +3 -3
- package/dist/idempotent-intake/index.d.ts +2 -2
- package/dist/idempotent-intake/index.js +3 -3
- package/dist/index.d.ts +50 -472
- package/dist/index.js +2 -2
- package/dist/llm-call/index.d.ts +3 -2
- package/dist/llm-call/index.js +12 -10
- package/dist/machine-B_BF4jij.d.ts +804 -0
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/mem/index.d.ts +3 -2
- package/dist/mem/index.js.map +1 -1
- package/dist/monitored-run/index.d.ts +54 -30
- package/dist/monitored-run/index.js +7 -7
- package/dist/node/index.d.ts +3 -2
- package/dist/node/index.js +2 -3
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +2 -2
- package/dist/paginated-walk/index.js +11 -10
- package/dist/parity/index.d.ts +6 -5
- package/dist/parity/index.js +5 -4
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +3 -3
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -3
- package/dist/poller/index.d.ts +59 -17
- package/dist/poller/index.js +56 -20
- package/dist/poller/index.js.map +1 -1
- package/dist/prediction/index.d.ts +1 -1
- package/dist/prediction/index.js +2 -2
- package/dist/pure/index.d.ts +1 -1
- package/dist/pure/index.js +2 -2
- package/dist/rate-limit/index.d.ts +2 -2
- package/dist/rate-limit/index.js +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +2 -2
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +2 -2
- package/dist/reconciler/index.js +13 -15
- package/dist/reconciler/index.js.map +1 -1
- package/dist/recorder/index.d.ts +20 -20
- package/dist/recorder/index.js +1 -1
- package/dist/resilient-call/index.d.ts +31 -6
- package/dist/resilient-call/index.js +10 -9
- package/dist/retry-backoff/index.d.ts +191 -14
- package/dist/retry-backoff/index.js +1 -1
- package/dist/retry-to-success/index.js +1 -1
- package/dist/runtime-types-gsog27b6.d.ts +337 -0
- package/dist/saga/index.d.ts +3 -2
- package/dist/saga/index.js +2 -2
- package/dist/saga/index.js.map +1 -1
- package/dist/snapshot/index.d.ts +1 -1
- package/dist/snapshot/index.js +3 -3
- package/dist/subs/index.d.ts +372 -4
- package/dist/subs/index.js +4 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/throttled-input/index.d.ts +2 -2
- package/dist/throttled-input/index.js +14 -13
- package/dist/throttled-input/index.js.map +1 -1
- package/dist/token-refresh/index.d.ts +1 -1
- package/dist/token-refresh/index.js +3 -3
- package/dist/trace-replay/index.d.ts +12 -7
- package/dist/trace-replay/index.js +4 -3
- package/dist/{types-B-8bcAp8.d.ts → types-KbL3W1mj.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +1 -1
- package/dist/with-deadline/index.js +2 -2
- package/dist/with-resilience/index.d.ts +21 -2
- package/dist/with-resilience/index.js +12 -23
- package/dist/with-resilience/index.js.map +1 -1
- package/dist/with-telemetry/index.d.ts +1 -1
- package/dist/with-telemetry/index.js +2 -2
- package/dist/work-queue/adapter.d.ts +2 -2
- package/dist/work-queue/adapter.js +2 -2
- package/dist/work-queue/index.d.ts +3 -2
- package/dist/work-queue/index.js +1 -1
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +2 -2
- package/dist/work-queue/ops.js +1 -1
- package/dist/workflow/index.d.ts +51 -19
- package/dist/workflow/index.js +71 -52
- package/dist/workflow/index.js.map +1 -1
- package/package.json +4 -19
- package/dist/chunk-27N2SPTN.js.map +0 -1
- package/dist/chunk-2YOE5PIS.js.map +0 -1
- package/dist/chunk-3545BOYL.js.map +0 -1
- package/dist/chunk-5DEQ5I3U.js.map +0 -1
- package/dist/chunk-5VNWZADP.js.map +0 -1
- package/dist/chunk-6RGBKODV.js +0 -268
- package/dist/chunk-6RGBKODV.js.map +0 -1
- package/dist/chunk-7W4LMPUW.js.map +0 -1
- package/dist/chunk-DILMKOZ7.js +0 -91
- package/dist/chunk-DILMKOZ7.js.map +0 -1
- package/dist/chunk-K7Q23EQ4.js +0 -20
- package/dist/chunk-K7Q23EQ4.js.map +0 -1
- package/dist/chunk-LP7VQBRG.js.map +0 -1
- package/dist/chunk-MMH7CW5K.js.map +0 -1
- package/dist/chunk-NSDFA6M6.js +0 -198
- package/dist/chunk-NSDFA6M6.js.map +0 -1
- package/dist/chunk-ON24RKSE.js.map +0 -1
- package/dist/chunk-PS23H6KQ.js.map +0 -1
- package/dist/chunk-QLA76GIT.js.map +0 -1
- package/dist/chunk-S3FQN4SL.js.map +0 -1
- package/dist/chunk-SC3FAFZN.js.map +0 -1
- package/dist/chunk-TGG26XMK.js.map +0 -1
- package/dist/chunk-UV6UWGEO.js.map +0 -1
- package/dist/chunk-UYWUVEP6.js.map +0 -1
- package/dist/chunk-VLGARN6H.js +0 -202
- package/dist/chunk-VLGARN6H.js.map +0 -1
- package/dist/raft/demo.d.ts +0 -105
- package/dist/raft/demo.js +0 -172
- package/dist/raft/demo.js.map +0 -1
- package/dist/raft/index.d.ts +0 -548
- package/dist/raft/index.js +0 -7
- package/dist/raft/index.js.map +0 -1
- package/dist/raft/sim.d.ts +0 -222
- package/dist/raft/sim.js +0 -8
- package/dist/raft/sim.js.map +0 -1
- package/dist/raft/timeline.d.ts +0 -46
- package/dist/raft/timeline.js +0 -62
- package/dist/raft/timeline.js.map +0 -1
package/dist/raft/sim.d.ts
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { NodeId, RaftCmd, RaftState, RaftConfig } from './index.js';
|
|
2
|
-
export { LogEntry } from './index.js';
|
|
3
|
-
import '../deadline/index.js';
|
|
4
|
-
import '../core-BoB6t7dB.js';
|
|
5
|
-
import '../types-B-8bcAp8.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @demlik/tea/raft — a deterministic, in-memory multi-node Raft simulation
|
|
9
|
-
* driver. Slice #121 (the safety-property half) of the consensus epic (#117):
|
|
10
|
-
* the headline payoff is consensus verified by **deterministic replay**, not by
|
|
11
|
-
* flaky wall-clock timing.
|
|
12
|
-
*
|
|
13
|
-
* This module owns NO wall clock, NO real timers, and NO networking. Time and
|
|
14
|
-
* message ordering are entirely the *schedule* — a finite list of {@link SimEvent}s
|
|
15
|
-
* the caller (a fast-check generator or an explicit fixture) hands the driver.
|
|
16
|
-
* The driver folds the schedule one event at a time over a cluster of pure
|
|
17
|
-
* {@link createRaftNode}s, routing every emitted `Cmd` into a pending-message
|
|
18
|
-
* pool and recording the full `(event → resulting cluster states)` trace.
|
|
19
|
-
*
|
|
20
|
-
* Because the underlying reducer is pure (no ambient clock — `at` rides on the
|
|
21
|
-
* Msg; no ambient RNG — `rng` injected once per node), the same schedule re-run
|
|
22
|
-
* yields **byte-identical** cluster state. That replay identity is the property
|
|
23
|
-
* the safety suite (`safety.test.ts`) leans on: a violated invariant shrinks to
|
|
24
|
-
* a *replayable schedule*, not an irreproducible timing fluke (errors are data).
|
|
25
|
-
*
|
|
26
|
-
* ## The schedule model
|
|
27
|
-
*
|
|
28
|
-
* The cluster is N nodes plus a pending-message pool. Each {@link SimEvent} is
|
|
29
|
-
* one of two moves:
|
|
30
|
-
*
|
|
31
|
-
* - `{ kind: "timer", node, timer }` — fire node's election OR heartbeat timer.
|
|
32
|
-
* Drives an `onElectionTimeout` / `onHeartbeat` verb on that node. The
|
|
33
|
-
* firing `at` is the driver's monotonic logical clock (one tick per event),
|
|
34
|
-
* so no verb reads a real clock.
|
|
35
|
-
* - `{ kind: "deliver", index }` — deliver the in-flight message at `index` in
|
|
36
|
-
* the pending pool to its target node, as the matching inbound `RaftMsg`.
|
|
37
|
-
* An out-of-range `index` is taken modulo the pool size (so a fast-check
|
|
38
|
-
* integer always names *some* deliverable message when the pool is
|
|
39
|
-
* non-empty); a `deliver` against an empty pool is a no-op event (recorded,
|
|
40
|
-
* advances the clock, changes nothing) — the schedule stays total.
|
|
41
|
-
*
|
|
42
|
-
* Delivering a message removes it from the pool and feeds it to the target's
|
|
43
|
-
* verb; any Cmds that verb emits are appended to the pool (each Cmd carries its
|
|
44
|
-
* own `to`, so routing is just a lookup). This is the in-memory transport: a
|
|
45
|
-
* `SendRequestVote{to}` delivered becomes a `RequestVoteRequest` on `to`; the
|
|
46
|
-
* `SendRequestVoteReply{to}` it emits becomes a `RequestVoteReply` on the
|
|
47
|
-
* original candidate; and so on around the RPC pairs.
|
|
48
|
-
*
|
|
49
|
-
* ## Record / replay
|
|
50
|
-
*
|
|
51
|
-
* {@link runSchedule} returns a {@link SimTrace}: the ordered list of steps, each
|
|
52
|
-
* the event applied and the resulting per-node {@link RaftState} snapshot, plus
|
|
53
|
-
* the final cluster. {@link replaySchedule} re-runs the identical schedule from a
|
|
54
|
-
* fresh cluster; {@link clusterStatesEqual} compares two cluster snapshots
|
|
55
|
-
* structurally. The safety suite asserts `runSchedule(s)` and
|
|
56
|
-
* `replaySchedule(s)` agree byte-for-byte.
|
|
57
|
-
*
|
|
58
|
-
* NOT a substrate primitive and NOT shipped in the node's public verb surface:
|
|
59
|
-
* it depends only on `./index` (the pure node) read-only, and lives beside the
|
|
60
|
-
* node as a test/replay harness.
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
/** Which of a node's two timers a `timer` event fires. */
|
|
64
|
-
type TimerKind = "election" | "heartbeat";
|
|
65
|
-
/**
|
|
66
|
-
* One move in a schedule. The whole simulation is a fold of these — there is no
|
|
67
|
-
* other source of time or ordering. `_tag`-style `kind` discriminant matches the
|
|
68
|
-
* engine's tagged-union convention.
|
|
69
|
-
*/
|
|
70
|
-
type SimEvent = {
|
|
71
|
-
/** Fire a node's timer (drives `onElectionTimeout` / `onHeartbeat`). */
|
|
72
|
-
readonly kind: "timer";
|
|
73
|
-
/** The node whose timer fires. */
|
|
74
|
-
readonly node: NodeId;
|
|
75
|
-
/** Which timer. */
|
|
76
|
-
readonly timer: TimerKind;
|
|
77
|
-
} | {
|
|
78
|
-
/**
|
|
79
|
-
* Deliver an in-flight message from the pending pool to its target. The
|
|
80
|
-
* index is taken modulo the pool size, so any integer names a deliverable
|
|
81
|
-
* message when the pool is non-empty; a deliver against an empty pool is a
|
|
82
|
-
* recorded no-op (the clock still advances).
|
|
83
|
-
*/
|
|
84
|
-
readonly kind: "deliver";
|
|
85
|
-
/** Which pending message to deliver (modulo pool size). */
|
|
86
|
-
readonly index: number;
|
|
87
|
-
} | {
|
|
88
|
-
/**
|
|
89
|
-
* A client submits a command to a node (Figure 2 §5.3). On the leader it
|
|
90
|
-
* appends + replicates; on a non-leader it is a no-op. This is how the
|
|
91
|
-
* replicated log grows in a simulation.
|
|
92
|
-
*/
|
|
93
|
-
readonly kind: "client";
|
|
94
|
-
/** The node the client contacts. */
|
|
95
|
-
readonly node: NodeId;
|
|
96
|
-
/** The opaque command to submit. */
|
|
97
|
-
readonly command: number;
|
|
98
|
-
} | {
|
|
99
|
-
/**
|
|
100
|
-
* Drain the pending pool to quiescence: deliver messages FIFO (each new
|
|
101
|
-
* Cmd a delivery emits is appended and itself delivered) until the pool is
|
|
102
|
-
* empty or `bound` deliveries have run. This is the "let the network
|
|
103
|
-
* settle" move — it lets a started election or a submitted client command
|
|
104
|
-
* run its RPC round(s) to completion, which is what actually produces
|
|
105
|
-
* leaders and committed entries. Without it, a purely-random `deliver`
|
|
106
|
-
* index almost never threads the exact sequence an election + replication
|
|
107
|
-
* needs (measured: 0 commits in 2000 random schedules vs. 529 with settle),
|
|
108
|
-
* so the safety properties would pass *vacuously* over empty logs.
|
|
109
|
-
*
|
|
110
|
-
* The `bound` caps the drain so a pathological re-emit loop cannot diverge;
|
|
111
|
-
* every step is still recorded (one trace step per delivery), so a settle
|
|
112
|
-
* is a deterministic, replayable macro — not a black box.
|
|
113
|
-
*/
|
|
114
|
-
readonly kind: "settle";
|
|
115
|
-
/** Max deliveries to run (the drain is FIFO; stops early when empty). */
|
|
116
|
-
readonly bound: number;
|
|
117
|
-
} | {
|
|
118
|
-
/**
|
|
119
|
-
* Set the cluster's *partitioned* node set (a network fault, NOT a state
|
|
120
|
-
* change). A node in `down` is isolated: the transport DROPS every message
|
|
121
|
-
* to OR from it (both directions, as a real partition does) while its
|
|
122
|
-
* timers still fire — so a crashed/partitioned leader stops being able to
|
|
123
|
-
* heartbeat and the surviving majority elects a new leader. This is the
|
|
124
|
-
* "kill the leader" / "heal the partition" move the failover demo needs.
|
|
125
|
-
*
|
|
126
|
-
* It names the WHOLE down-set absolutely (not a toggle): `{ down: [n0] }`
|
|
127
|
-
* isolates n0; a later `{ down: [] }` heals every partition. Recorded as
|
|
128
|
-
* one step (clock advances, no message moves), so it replays deterministically.
|
|
129
|
-
*/
|
|
130
|
-
readonly kind: "partition";
|
|
131
|
-
/** The nodes that are currently isolated (drop all their traffic). */
|
|
132
|
-
readonly down: readonly NodeId[];
|
|
133
|
-
};
|
|
134
|
-
/** A whole schedule: the finite event stream the driver folds. */
|
|
135
|
-
type Schedule = readonly SimEvent[];
|
|
136
|
-
/**
|
|
137
|
-
* A Cmd a node emitted, parked until a `deliver` event routes it. We keep the
|
|
138
|
-
* raw {@link RaftCmd} (it already carries `to`) plus the sender, so a delivery
|
|
139
|
-
* can build the correct inbound {@link RaftMsg} on the target. Commands over a
|
|
140
|
-
* numeric payload (the simulation's `C = number`).
|
|
141
|
-
*/
|
|
142
|
-
interface InFlight {
|
|
143
|
-
/** The node that emitted this Cmd (the inbound Msg's `from` / sender). */
|
|
144
|
-
readonly from: NodeId;
|
|
145
|
-
/** The emitted Cmd, addressed to `cmd.to`. */
|
|
146
|
-
readonly cmd: RaftCmd<number>;
|
|
147
|
-
}
|
|
148
|
-
/** A snapshot of every node's state, keyed by id. */
|
|
149
|
-
type ClusterStates = Readonly<Record<NodeId, RaftState<number>>>;
|
|
150
|
-
/**
|
|
151
|
-
* A read-off of the message a `deliver` step actually pulled off the pending
|
|
152
|
-
* pool — sender, target, and the {@link RaftCmd} discriminant (`cmd.type`). This
|
|
153
|
-
* is a pure projection of what the fold *already did* (the InFlight that
|
|
154
|
-
* {@link deliverOne} consumed), recorded so a renderer can draw the RPC flow.
|
|
155
|
-
* It is NOT a new input and changes NO simulation behavior. Recorded even when
|
|
156
|
-
* the packet is dropped by a partition (the RPC was still attempted), so the
|
|
157
|
-
* arrow shows the intent while the greyed node shows the drop.
|
|
158
|
-
*/
|
|
159
|
-
interface DeliveredMessage {
|
|
160
|
-
/** The node that emitted the delivered Cmd (the inbound Msg's sender). */
|
|
161
|
-
readonly from: NodeId;
|
|
162
|
-
/** The node the Cmd was addressed to (`cmd.to`). */
|
|
163
|
-
readonly to: NodeId;
|
|
164
|
-
/** The delivered Cmd's discriminant (`RaftCmd["type"]`). */
|
|
165
|
-
readonly kind: RaftCmd<number>["type"];
|
|
166
|
-
}
|
|
167
|
-
/** One recorded step: the event applied and the cluster state it produced. */
|
|
168
|
-
interface SimStep {
|
|
169
|
-
/** The event applied at this step. */
|
|
170
|
-
readonly event: SimEvent;
|
|
171
|
-
/** The pending-pool size AFTER applying the event. */
|
|
172
|
-
readonly pending: number;
|
|
173
|
-
/** Every node's state after the event. */
|
|
174
|
-
readonly states: ClusterStates;
|
|
175
|
-
/**
|
|
176
|
-
* On a `deliver` step (incl. each delivery a `settle` drains), the message
|
|
177
|
-
* that was actually pulled off the pool. ABSENT on every other step kind and
|
|
178
|
-
* on a `deliver` against an empty pool — additive + backward-compatible, so a
|
|
179
|
-
* trace recorded before this field still folds and renders identically (field
|
|
180
|
-
* absent ⇒ no arrow). Purely a read-off of {@link deliverOne}'s work.
|
|
181
|
-
*/
|
|
182
|
-
readonly delivered?: DeliveredMessage;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* The full record of a run: the per-node configs, the ordered steps, and the
|
|
186
|
-
* final cluster. Replaying the same schedule reproduces this byte-for-byte.
|
|
187
|
-
*/
|
|
188
|
-
interface SimTrace {
|
|
189
|
-
/** The cluster's node ids, in config order. */
|
|
190
|
-
readonly nodes: readonly NodeId[];
|
|
191
|
-
/** One step per event in the schedule (in order). */
|
|
192
|
-
readonly steps: readonly SimStep[];
|
|
193
|
-
/** The final per-node states (== `steps.at(-1)?.states` when non-empty). */
|
|
194
|
-
readonly final: ClusterStates;
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Build N node configs sharing one election-timeout window + heartbeat. Each
|
|
198
|
-
* node's `peers` is every OTHER id. Ids are `n0..n{N-1}`.
|
|
199
|
-
*/
|
|
200
|
-
declare function clusterConfigs(n: number, over?: Partial<Omit<RaftConfig, "self" | "peers">>): readonly RaftConfig[];
|
|
201
|
-
/**
|
|
202
|
-
* Run a schedule against a fresh N-node cluster, recording the full trace. Pure
|
|
203
|
-
* given `(configs, schedule)`: no clock, no RNG, no IO — the only "time" is the
|
|
204
|
-
* driver's logical clock, advanced one tick per event. Each node gets a
|
|
205
|
-
* fixed-jitter rng derived from its index (distinct but deterministic timeouts).
|
|
206
|
-
*/
|
|
207
|
-
declare function runSchedule(configs: readonly RaftConfig[], schedule: Schedule): SimTrace;
|
|
208
|
-
/**
|
|
209
|
-
* Replay a schedule from a fresh cluster. Identical to {@link runSchedule} (it
|
|
210
|
-
* IS `runSchedule`) — exposed under a name that documents intent at the call
|
|
211
|
-
* site: a replay is meant to reproduce a prior run's trace byte-for-byte.
|
|
212
|
-
*/
|
|
213
|
-
declare function replaySchedule(configs: readonly RaftConfig[], schedule: Schedule): SimTrace;
|
|
214
|
-
/**
|
|
215
|
-
* Structural equality of two cluster snapshots. Used by the determinism
|
|
216
|
-
* property: a run and its replay must produce identical per-node state for every
|
|
217
|
-
* node. (Tests use vitest `toEqual` for the same comparison; this is the
|
|
218
|
-
* dependency-free version the driver can expose for ad-hoc checks.)
|
|
219
|
-
*/
|
|
220
|
-
declare function clusterStatesEqual(a: ClusterStates, b: ClusterStates): boolean;
|
|
221
|
-
|
|
222
|
-
export { type ClusterStates, type DeliveredMessage, type InFlight, NodeId, RaftConfig, RaftState, type Schedule, type SimEvent, type SimStep, type SimTrace, type TimerKind, clusterConfigs, clusterStatesEqual, replaySchedule, runSchedule };
|
package/dist/raft/sim.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { clusterConfigs, clusterStatesEqual, replaySchedule, runSchedule } from '../chunk-VLGARN6H.js';
|
|
2
|
-
import '../chunk-6RGBKODV.js';
|
|
3
|
-
import '../chunk-K7Q23EQ4.js';
|
|
4
|
-
import '../chunk-2YFLOCWX.js';
|
|
5
|
-
import '../chunk-MMH7CW5K.js';
|
|
6
|
-
import '../chunk-QLA76GIT.js';
|
|
7
|
-
//# sourceMappingURL=sim.js.map
|
|
8
|
-
//# sourceMappingURL=sim.js.map
|
package/dist/raft/sim.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"sim.js"}
|
package/dist/raft/timeline.d.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { SimTrace } from './sim.js';
|
|
2
|
-
import './index.js';
|
|
3
|
-
import '../deadline/index.js';
|
|
4
|
-
import '../core-BoB6t7dB.js';
|
|
5
|
-
import '../types-B-8bcAp8.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @demlik/tea/raft — an ASCII step-timeline renderer for a {@link SimTrace}.
|
|
9
|
-
*
|
|
10
|
-
* PURE. `renderTimeline(trace)` folds a recorded trace into a deterministic,
|
|
11
|
-
* paced-friendly string: a header, then one block per step — the {@link SimEvent}
|
|
12
|
-
* that drove it, followed by one row per node (role · term · commit · committed
|
|
13
|
-
* log), with the current leader marked and partitioned nodes flagged.
|
|
14
|
-
*
|
|
15
|
-
* It is a *projection of the trace*, not an instrumentation of the engine: the
|
|
16
|
-
* trace already carries every node's full {@link RaftState} at every step
|
|
17
|
-
* (`step.states`), so this reads that data and never touches the reducer. The
|
|
18
|
-
* same renderer works for ANY trace `runSchedule` produces — the canned
|
|
19
|
-
* {@link runDemo} failover, or an author's own schedule.
|
|
20
|
-
*
|
|
21
|
-
* Plain ASCII + a little box-drawing, no ANSI — so it renders identically in a
|
|
22
|
-
* terminal, a captured asciinema cast, and a golden test assertion (the
|
|
23
|
-
* deterministic-output property the rest of the raft module leans on).
|
|
24
|
-
*
|
|
25
|
-
* The partition set is not on `SimStep` (it lives in the sim's internal world),
|
|
26
|
-
* so the renderer RECONSTRUCTS it by folding the `partition` events as it walks
|
|
27
|
-
* the steps — a `{ kind: "partition", down }` sets the down-set absolutely,
|
|
28
|
-
* `{ down: [] }` heals (exactly the harness's own semantics).
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
/** Options for {@link renderTimeline}. */
|
|
32
|
-
interface TimelineOptions {
|
|
33
|
-
/**
|
|
34
|
-
* Drop the header banner (the `@demlik/tea/raft …` title + the column key).
|
|
35
|
-
* Default `false` — the banner is shown. A cast that frames its own title
|
|
36
|
-
* passes `true`.
|
|
37
|
-
*/
|
|
38
|
-
readonly noHeader?: boolean;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Render a whole {@link SimTrace} as an ASCII step timeline. PURE +
|
|
42
|
-
* deterministic — the same trace renders byte-identically every time.
|
|
43
|
-
*/
|
|
44
|
-
declare function renderTimeline(trace: SimTrace, opts?: TimelineOptions): string;
|
|
45
|
-
|
|
46
|
-
export { type TimelineOptions, renderTimeline };
|
package/dist/raft/timeline.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// src/raft/timeline.ts
|
|
2
|
-
function committedCommands(state) {
|
|
3
|
-
return state.log.slice(0, state.commitIndex).map((entry) => entry.command);
|
|
4
|
-
}
|
|
5
|
-
function summarizeEvent(event) {
|
|
6
|
-
switch (event.kind) {
|
|
7
|
-
case "timer":
|
|
8
|
-
return `timer \xB7 ${event.node} ${event.timer}`;
|
|
9
|
-
case "deliver":
|
|
10
|
-
return `deliver \xB7 in-flight msg #${event.index}`;
|
|
11
|
-
case "client":
|
|
12
|
-
return `client \xB7 ${event.node} \u27F5 cmd ${event.command}`;
|
|
13
|
-
case "settle":
|
|
14
|
-
return `settle \xB7 drain the network (\u2264 ${event.bound})`;
|
|
15
|
-
case "partition":
|
|
16
|
-
return event.down.length === 0 ? "partition \xB7 HEAL \u2014 every node reachable" : `partition \xB7 DOWN [${[...event.down].join(", ")}] (traffic dropped)`;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function pad(s, width) {
|
|
20
|
-
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
21
|
-
}
|
|
22
|
-
function renderNodeRow(id, state, down) {
|
|
23
|
-
const isLeader = state.role._tag === "leader";
|
|
24
|
-
const isDown = down.has(id);
|
|
25
|
-
const marker = isDown ? "\u2573" : isLeader ? "\u25B8" : " ";
|
|
26
|
-
const committed = committedCommands(state);
|
|
27
|
-
const logSize = state.log.length;
|
|
28
|
-
const logCell = `[${committed.join(", ")}]${logSize > committed.length ? ` (+${logSize - committed.length} uncommitted)` : ""}`;
|
|
29
|
-
return ` ${marker} ${pad(id, 4)} ${pad(state.role._tag, 10)} term ${pad(String(state.currentTerm), 3)} commit ${pad(String(state.commitIndex), 3)} log ${logCell}${isDown ? " (down)" : ""}`;
|
|
30
|
-
}
|
|
31
|
-
function renderStep(index, event, states, nodes, down) {
|
|
32
|
-
const header = `step ${pad(String(index), 3)}\u2502 ${summarizeEvent(event)}`;
|
|
33
|
-
const rows = nodes.map((id) => {
|
|
34
|
-
const state = states[id];
|
|
35
|
-
return state === void 0 ? ` \xB7 ${pad(id, 4)} (absent)` : renderNodeRow(id, state, down);
|
|
36
|
-
});
|
|
37
|
-
return [header, ...rows].join("\n");
|
|
38
|
-
}
|
|
39
|
-
function renderTimeline(trace, opts = {}) {
|
|
40
|
-
const blocks = [];
|
|
41
|
-
if (!opts.noHeader) {
|
|
42
|
-
const majority = Math.floor(trace.nodes.length / 2) + 1;
|
|
43
|
-
blocks.push(
|
|
44
|
-
"\u2550".repeat(64),
|
|
45
|
-
` @demlik/tea/raft \u2014 consensus timeline (${trace.nodes.length} nodes, majority ${majority})`,
|
|
46
|
-
` \u25B8 leader \u2573 partitioned \xB7 log = [committed]`,
|
|
47
|
-
"\u2550".repeat(64)
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
let down = /* @__PURE__ */ new Set();
|
|
51
|
-
trace.steps.forEach((step, i) => {
|
|
52
|
-
if (step.event.kind === "partition") {
|
|
53
|
-
down = new Set(step.event.down);
|
|
54
|
-
}
|
|
55
|
-
blocks.push(renderStep(i + 1, step.event, step.states, trace.nodes, down));
|
|
56
|
-
});
|
|
57
|
-
return blocks.join("\n\n");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export { renderTimeline };
|
|
61
|
-
//# sourceMappingURL=timeline.js.map
|
|
62
|
-
//# sourceMappingURL=timeline.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/raft/timeline.ts"],"names":[],"mappings":"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","file":"timeline.js","sourcesContent":["/**\n * @demlik/tea/raft — an ASCII step-timeline renderer for a {@link SimTrace}.\n *\n * PURE. `renderTimeline(trace)` folds a recorded trace into a deterministic,\n * paced-friendly string: a header, then one block per step — the {@link SimEvent}\n * that drove it, followed by one row per node (role · term · commit · committed\n * log), with the current leader marked and partitioned nodes flagged.\n *\n * It is a *projection of the trace*, not an instrumentation of the engine: the\n * trace already carries every node's full {@link RaftState} at every step\n * (`step.states`), so this reads that data and never touches the reducer. The\n * same renderer works for ANY trace `runSchedule` produces — the canned\n * {@link runDemo} failover, or an author's own schedule.\n *\n * Plain ASCII + a little box-drawing, no ANSI — so it renders identically in a\n * terminal, a captured asciinema cast, and a golden test assertion (the\n * deterministic-output property the rest of the raft module leans on).\n *\n * The partition set is not on `SimStep` (it lives in the sim's internal world),\n * so the renderer RECONSTRUCTS it by folding the `partition` events as it walks\n * the steps — a `{ kind: \"partition\", down }` sets the down-set absolutely,\n * `{ down: [] }` heals (exactly the harness's own semantics).\n */\n\nimport type { NodeId, RaftState } from \"./index\";\nimport type { ClusterStates, SimEvent, SimTrace } from \"./sim\";\n\n/** Options for {@link renderTimeline}. */\nexport interface TimelineOptions {\n /**\n * Drop the header banner (the `@demlik/tea/raft …` title + the column key).\n * Default `false` — the banner is shown. A cast that frames its own title\n * passes `true`.\n */\n readonly noHeader?: boolean;\n}\n\n/** The committed commands of a node — `log[0 .. commitIndex)`, in order. */\nfunction committedCommands(state: RaftState<number>): number[] {\n return state.log.slice(0, state.commitIndex).map((entry) => entry.command);\n}\n\n/** One-line summary of the move a step applied. */\nfunction summarizeEvent(event: SimEvent): string {\n switch (event.kind) {\n case \"timer\":\n return `timer · ${event.node} ${event.timer}`;\n case \"deliver\":\n return `deliver · in-flight msg #${event.index}`;\n case \"client\":\n return `client · ${event.node} ⟵ cmd ${event.command}`;\n case \"settle\":\n return `settle · drain the network (≤ ${event.bound})`;\n case \"partition\":\n return event.down.length === 0\n ? \"partition · HEAL — every node reachable\"\n : `partition · DOWN [${[...event.down].join(\", \")}] (traffic dropped)`;\n }\n}\n\n/** Pad `s` to `width` with trailing spaces (left-aligned column). */\nfunction pad(s: string, width: number): string {\n return s.length >= width ? s : s + \" \".repeat(width - s.length);\n}\n\n/**\n * Render one node's row. The leader of the cluster is marked with `▸`; a\n * partitioned (down) node is dimmed with `╳` and a `(down)` tag — it keeps\n * showing its last-known state because, isolated, it cannot learn otherwise.\n */\nfunction renderNodeRow(\n id: NodeId,\n state: RaftState<number>,\n down: ReadonlySet<NodeId>,\n): string {\n const isLeader = state.role._tag === \"leader\";\n const isDown = down.has(id);\n const marker = isDown ? \"╳\" : isLeader ? \"▸\" : \" \";\n const committed = committedCommands(state);\n const logSize = state.log.length;\n // committed/total so a row shows replication progress (committed ≤ total).\n const logCell = `[${committed.join(\", \")}]${\n logSize > committed.length\n ? ` (+${logSize - committed.length} uncommitted)`\n : \"\"\n }`;\n return (\n ` ${marker} ${pad(id, 4)} ${pad(state.role._tag, 10)} ` +\n `term ${pad(String(state.currentTerm), 3)} ` +\n `commit ${pad(String(state.commitIndex), 3)} ` +\n `log ${logCell}${isDown ? \" (down)\" : \"\"}`\n );\n}\n\n/** Render one step block: the event header + every node's row. */\nfunction renderStep(\n index: number,\n event: SimEvent,\n states: ClusterStates,\n nodes: readonly NodeId[],\n down: ReadonlySet<NodeId>,\n): string {\n const header = `step ${pad(String(index), 3)}│ ${summarizeEvent(event)}`;\n const rows = nodes.map((id) => {\n const state = states[id];\n return state === undefined\n ? ` · ${pad(id, 4)} (absent)`\n : renderNodeRow(id, state, down);\n });\n return [header, ...rows].join(\"\\n\");\n}\n\n/**\n * Render a whole {@link SimTrace} as an ASCII step timeline. PURE +\n * deterministic — the same trace renders byte-identically every time.\n */\nexport function renderTimeline(\n trace: SimTrace,\n opts: TimelineOptions = {},\n): string {\n const blocks: string[] = [];\n if (!opts.noHeader) {\n const majority = Math.floor(trace.nodes.length / 2) + 1;\n blocks.push(\n \"═\".repeat(64),\n ` @demlik/tea/raft — consensus timeline` +\n ` (${trace.nodes.length} nodes, majority ${majority})`,\n ` ▸ leader ╳ partitioned · log = [committed]`,\n \"═\".repeat(64),\n );\n }\n // Fold the partition set as we walk — a `partition` event sets it absolutely.\n let down: ReadonlySet<NodeId> = new Set();\n trace.steps.forEach((step, i) => {\n if (step.event.kind === \"partition\") {\n down = new Set(step.event.down);\n }\n blocks.push(renderStep(i + 1, step.event, step.states, trace.nodes, down));\n });\n return blocks.join(\"\\n\\n\");\n}\n"]}
|