@demlik/tea 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/agent/index.d.ts +2 -2
  2. package/dist/agent/index.js +8 -8
  3. package/dist/authed-call/index.d.ts +3 -3
  4. package/dist/authed-call/index.js +5 -5
  5. package/dist/await-terminal/index.d.ts +1 -1
  6. package/dist/await-terminal/index.js +2 -2
  7. package/dist/batch-window/index.d.ts +2 -2
  8. package/dist/batch-window/index.js +4 -4
  9. package/dist/cache/index.d.ts +2 -2
  10. package/dist/{chunk-EIZJTYKR.js → chunk-5KVHID6A.js} +4 -4
  11. package/dist/{chunk-EIZJTYKR.js.map → chunk-5KVHID6A.js.map} +1 -1
  12. package/dist/{chunk-RVJFGRGK.js → chunk-6GIGQT6C.js} +4 -4
  13. package/dist/chunk-6GIGQT6C.js.map +1 -0
  14. package/dist/{chunk-FH5RUIIL.js → chunk-6OUDPSSG.js} +3 -3
  15. package/dist/{chunk-FH5RUIIL.js.map → chunk-6OUDPSSG.js.map} +1 -1
  16. package/dist/{chunk-2WZHS4P2.js → chunk-6VDCTR7N.js} +25 -40
  17. package/dist/chunk-6VDCTR7N.js.map +1 -0
  18. package/dist/{chunk-BNJYE4AU.js → chunk-DYRKDCJI.js} +46 -16
  19. package/dist/chunk-DYRKDCJI.js.map +1 -0
  20. package/dist/{chunk-RGTPPF4U.js → chunk-EGWTT5XJ.js} +3 -3
  21. package/dist/{chunk-RGTPPF4U.js.map → chunk-EGWTT5XJ.js.map} +1 -1
  22. package/dist/{chunk-A56GNPI7.js → chunk-EMJTPHI7.js} +5 -23
  23. package/dist/chunk-EMJTPHI7.js.map +1 -0
  24. package/dist/{chunk-IFBQJYGI.js → chunk-FOGL3TC7.js} +3 -3
  25. package/dist/{chunk-IFBQJYGI.js.map → chunk-FOGL3TC7.js.map} +1 -1
  26. package/dist/{chunk-RVU3RBE6.js → chunk-G2U7DJHL.js} +6 -4
  27. package/dist/chunk-G2U7DJHL.js.map +1 -0
  28. package/dist/{chunk-WUV3PSCZ.js → chunk-HCEVHFJ7.js} +2 -2
  29. package/dist/chunk-HCEVHFJ7.js.map +1 -0
  30. package/dist/{chunk-EDGDR5XX.js → chunk-IK4S5DJN.js} +3 -3
  31. package/dist/{chunk-EDGDR5XX.js.map → chunk-IK4S5DJN.js.map} +1 -1
  32. package/dist/{chunk-EQRUAP2A.js → chunk-J6527VRQ.js} +2 -2
  33. package/dist/chunk-J6527VRQ.js.map +1 -0
  34. package/dist/{chunk-TAO4YOLO.js → chunk-M6UZT7HS.js} +3 -3
  35. package/dist/{chunk-TAO4YOLO.js.map → chunk-M6UZT7HS.js.map} +1 -1
  36. package/dist/{chunk-QGZW4W3E.js → chunk-SKAMZKTE.js} +3 -3
  37. package/dist/{chunk-QGZW4W3E.js.map → chunk-SKAMZKTE.js.map} +1 -1
  38. package/dist/{chunk-TC4HWABA.js → chunk-VFGKTGGC.js} +7 -7
  39. package/dist/{chunk-TC4HWABA.js.map → chunk-VFGKTGGC.js.map} +1 -1
  40. package/dist/{chunk-D5EQDLDV.js → chunk-ZDRN3YCE.js} +4 -4
  41. package/dist/{chunk-D5EQDLDV.js.map → chunk-ZDRN3YCE.js.map} +1 -1
  42. package/dist/{core-CymXZCeY.d.ts → core-BmlJOvxM.d.ts} +20 -1
  43. package/dist/deadline/index.d.ts +2 -2
  44. package/dist/deadline/index.js +3 -3
  45. package/dist/debounce/index.d.ts +6 -0
  46. package/dist/debounce/index.js +1 -1
  47. package/dist/do/index.d.ts +422 -315
  48. package/dist/do/index.js +173 -149
  49. package/dist/do/index.js.map +1 -1
  50. package/dist/{durable-effects-2MDxDs45.d.ts → durable-effects-9LxFpmh3.d.ts} +1 -1
  51. package/dist/extension/index.d.ts +1 -1
  52. package/dist/extension/index.js +3 -3
  53. package/dist/extension/react.js +3 -3
  54. package/dist/extension/subs/index.d.ts +2 -2
  55. package/dist/fan-out/index.d.ts +1 -1
  56. package/dist/idempotent-intake/index.d.ts +1 -1
  57. package/dist/index.d.ts +20 -3
  58. package/dist/index.js +2 -2
  59. package/dist/llm-call/index.d.ts +2 -2
  60. package/dist/llm-call/index.js +5 -5
  61. package/dist/machine-viz/index.d.ts +4 -4
  62. package/dist/machine-viz/index.js +4 -8
  63. package/dist/machine-viz/index.js.map +1 -1
  64. package/dist/mem/index.d.ts +1 -1
  65. package/dist/monitored-run/index.d.ts +2 -2
  66. package/dist/monitored-run/index.js +5 -5
  67. package/dist/node/index.d.ts +1 -1
  68. package/dist/paginated-walk/index.d.ts +2 -2
  69. package/dist/paginated-walk/index.js +5 -5
  70. package/dist/parity/index.d.ts +10 -4
  71. package/dist/parity/index.js +6 -5
  72. package/dist/parity/index.js.map +1 -1
  73. package/dist/pbt/arbitraries/index.d.ts +1 -1
  74. package/dist/pbt/index.d.ts +15 -8
  75. package/dist/pbt/index.js +5 -11
  76. package/dist/pbt/index.js.map +1 -1
  77. package/dist/pbt/runners/index.d.ts +1 -1
  78. package/dist/pbt/runners/index.js +3 -3
  79. package/dist/poller/index.d.ts +2 -2
  80. package/dist/poller/index.js +3 -3
  81. package/dist/prediction/index.d.ts +23 -2
  82. package/dist/prediction/index.js +2 -2
  83. package/dist/pure/index.d.ts +2 -2
  84. package/dist/pure/index.js +2 -2
  85. package/dist/react/index.d.ts +1 -1
  86. package/dist/react/index.js +8 -4
  87. package/dist/react/index.js.map +1 -1
  88. package/dist/reconciler/index.d.ts +2 -2
  89. package/dist/reconciler/index.js +5 -5
  90. package/dist/recorder/index.d.ts +1 -1
  91. package/dist/resilient-call/index.d.ts +45 -2
  92. package/dist/resilient-call/index.js +4 -4
  93. package/dist/retry-to-success/index.d.ts +109 -0
  94. package/dist/retry-to-success/index.js +35 -0
  95. package/dist/retry-to-success/index.js.map +1 -0
  96. package/dist/saga/index.d.ts +14 -1
  97. package/dist/saga/index.js.map +1 -1
  98. package/dist/snapshot/index.d.ts +1 -1
  99. package/dist/snapshot/index.js +3 -3
  100. package/dist/subs/index.d.ts +2 -2
  101. package/dist/testing/index.d.ts +1 -1
  102. package/dist/testing/index.js +2 -2
  103. package/dist/throttle/index.d.ts +9 -0
  104. package/dist/throttle/index.js +1 -1
  105. package/dist/throttled-input/index.d.ts +10 -2
  106. package/dist/throttled-input/index.js +6 -6
  107. package/dist/throttled-input/index.js.map +1 -1
  108. package/dist/token-refresh/index.d.ts +1 -1
  109. package/dist/token-refresh/index.js +3 -3
  110. package/dist/trace-replay/index.d.ts +1 -1
  111. package/dist/trace-replay/index.js +3 -3
  112. package/dist/{types-N9LZbdnr.d.ts → types-BoUWgwSm.d.ts} +1 -1
  113. package/dist/with-deadline/index.d.ts +1 -1
  114. package/dist/with-deadline/index.js +5 -24
  115. package/dist/with-deadline/index.js.map +1 -1
  116. package/dist/with-resilience/index.d.ts +2 -2
  117. package/dist/with-resilience/index.js +13 -36
  118. package/dist/with-resilience/index.js.map +1 -1
  119. package/dist/with-telemetry/index.d.ts +1 -1
  120. package/dist/with-telemetry/index.js +4 -29
  121. package/dist/with-telemetry/index.js.map +1 -1
  122. package/dist/work-queue/adapter.d.ts +1 -1
  123. package/dist/work-queue/index.d.ts +1 -1
  124. package/dist/work-queue/ops.d.ts +1 -1
  125. package/dist/workflow/index.d.ts +16 -10
  126. package/dist/workflow/index.js.map +1 -1
  127. package/package.json +8 -20
  128. package/dist/chunk-2WZHS4P2.js.map +0 -1
  129. package/dist/chunk-7VEG4KHB.js +0 -202
  130. package/dist/chunk-7VEG4KHB.js.map +0 -1
  131. package/dist/chunk-A56GNPI7.js.map +0 -1
  132. package/dist/chunk-BNJYE4AU.js.map +0 -1
  133. package/dist/chunk-EQRUAP2A.js.map +0 -1
  134. package/dist/chunk-RVJFGRGK.js.map +0 -1
  135. package/dist/chunk-RVU3RBE6.js.map +0 -1
  136. package/dist/chunk-WUV3PSCZ.js.map +0 -1
  137. package/dist/chunk-XRXKH7KA.js +0 -268
  138. package/dist/chunk-XRXKH7KA.js.map +0 -1
  139. package/dist/raft/demo.d.ts +0 -105
  140. package/dist/raft/demo.js +0 -172
  141. package/dist/raft/demo.js.map +0 -1
  142. package/dist/raft/index.d.ts +0 -548
  143. package/dist/raft/index.js +0 -7
  144. package/dist/raft/index.js.map +0 -1
  145. package/dist/raft/sim.d.ts +0 -222
  146. package/dist/raft/sim.js +0 -8
  147. package/dist/raft/sim.js.map +0 -1
  148. package/dist/raft/timeline.d.ts +0 -46
  149. package/dist/raft/timeline.js +0 -62
  150. package/dist/raft/timeline.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/raft/index.ts"],"names":[],"mappings":"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","file":"chunk-XRXKH7KA.js","sourcesContent":["/**\n * @demlik/tea/raft — Raft (Figure 2) as a pure TEA reducer. Slices #119 (the\n * election half) + #120 (the log-replication half) of the consensus epic\n * (#117): the role state machine (`follower → candidate → leader`), election +\n * heartbeat timers as `DeadlineSub`s, `RequestVote` request/reply handling, and\n * — added in #120 — the real replicated log with the Figure-2 `AppendEntries`\n * consistency check, leader `nextIndex`/`matchIndex` bookkeeping, and\n * `commitIndex` advancement under the Figure-8 current-term safety rule.\n * Persistence/cold-wake (#122) and the safety-property suite + multi-node\n * harness (#121/#123) are LATER siblings — out of scope here.\n *\n * The Raft paper (\"In Search of an Understandable Consensus Algorithm\",\n * raft.github.io) maps onto TEA almost 1:1, and this module is the proof of\n * that mapping:\n *\n * - **Reducer** — the Raft transition function over `RaftState`. PURE: it\n * reads no ambient clock and no ambient RNG. Wall time arrives as the `at`\n * parameter every verb takes (the firing Msg carries it, same discipline\n * as `../poller`); election-timeout jitter comes from the `rng` injected\n * ONCE at `createRaftNode` (default `Math.random` at the effect boundary, a\n * fixed value in tests). No verb body names the global RNG or `Date.now`,\n * so a node driven by a recorded `(timer × message)` schedule re-decides\n * bit-for-bit — the headline replay-testing payoff of the epic.\n * - **Sub** — the election-timeout and heartbeat timers are `DeadlineSub`s\n * (`../deadline`, the same shape `../poller` / the agent layer arm). A\n * fired timer is a `Msg` the consumer routes back into a verb; the verb\n * never owns a `setTimeout`.\n * - **Cmd** — the RPCs (`RequestVote` request + reply, `AppendEntries`\n * request + reply) are `Cmd`s routed through the consumer's `interpret`\n * over an injected transport port. This module emits the Cmd literals; it\n * performs NO networking. Tests assert against the emitted Cmds and fake\n * the transport.\n *\n * Invalid states are unrepresentable: the leader-only volatile state\n * (`nextIndex` / `matchIndex`) lives ONLY on the `leader` variant of the role\n * union, so a follower or candidate cannot carry — or be asked for — replication\n * bookkeeping it has no business holding (#119 acceptance criterion).\n *\n * **Persistence seam (#120 → #122).** The replicated log *is* the event log\n * (epic Approach). The verbs here keep `log` a pure value computed by folding —\n * a client command or a follower-side `AppendEntries` append/truncate is a\n * deterministic function of `(state, msg)`, with no clock/RNG and no mutation.\n * That is exactly the port the event-sourced store under `../do`\n * (`doEventSourcedStore`) folds over: its `replay(machine, { loaded, msgs })`\n * re-runs this same reducer to rebuild `log` byte-identically. #120 therefore\n * routes the log through the pure-reducer seam and defers the *effectful* host\n * wiring (async persist-before-respond, snapshot retention, cold-wake replay)\n * to #122 — see {@link RaftState.log} and {@link RaftNode.onClientCommand}.\n *\n * Generic over the command payload `C` (`unknown` by default): a `LogEntry`\n * carries an opaque `command` the consumer's state machine interprets on\n * commit. Raft itself never inspects it.\n *\n * NOT a substrate primitive: it depends only on sibling subpaths (`../deadline`)\n * and the core `Cmd` / `Sub` types. Consumers reach it via the\n * `@demlik/tea/raft` subpath.\n */\n\nimport { type DeadlineSub, deadlineSub } from \"../deadline\";\nimport { type Cmd, type Sub, subId } from \"../index\";\n\n// ===========================================================================\n// Identity & config\n// ===========================================================================\n\n/**\n * A Raft node identity. Opaque string; the cluster's `peers` set lists every\n * OTHER node (self excluded). Branded-by-convention only — kept a plain string\n * so transport adapters can use it as an address key without unwrapping.\n */\nexport type NodeId = string;\n\n/**\n * A Raft term — a monotonically increasing logical clock. Starts at 0 (no\n * leader elected yet) and bumps by one each time a node starts an election.\n */\nexport type Term = number;\n\n/**\n * The election-timeout window, in milliseconds. Raft randomizes each node's\n * timeout uniformly in `[minMs, maxMs]` so split votes resolve quickly — the\n * single source of liveness in leader election. The concrete duration is drawn\n * from the injected `rng` (never `Math.random` ambiently), so a fixed `rng`\n * pins the timeout and the run replays identically.\n */\nexport interface ElectionTimeout {\n /** Lower bound of the randomized election timeout, inclusive. */\n readonly minMs: number;\n /** Upper bound of the randomized election timeout, inclusive. */\n readonly maxMs: number;\n}\n\n/**\n * Static configuration for one Raft node, injected at construction. Pure data:\n * no node reads its own id or peer set from any ambient source.\n */\nexport interface RaftConfig {\n /** This node's identity. */\n readonly self: NodeId;\n /**\n * Every OTHER node in the cluster (self excluded). Cluster size is\n * `peers.length + 1`; a majority is `floor((peers.length + 1) / 2) + 1`.\n */\n readonly peers: readonly NodeId[];\n /** The randomized election-timeout window (see {@link ElectionTimeout}). */\n readonly electionTimeout: ElectionTimeout;\n /**\n * The leader's heartbeat interval, in milliseconds. A leader re-arms an empty\n * `AppendEntries` to every peer this often; it MUST be comfortably below\n * `electionTimeout.minMs` so followers never time out under a live leader.\n */\n readonly heartbeatMs: number;\n}\n\n// ===========================================================================\n// Log entry — the real { term, index, command } (#120)\n// ===========================================================================\n\n/**\n * One entry in the replicated log (Figure 2). Generic over the command payload\n * `C` (`unknown` by default): Raft itself never inspects `command` — it\n * replicates and orders the opaque blob, and the consumer's state machine\n * applies it once the entry commits.\n *\n * `index` is 1-based (Raft convention: index 0 means \"before the log\"; the\n * first real entry is index 1), so an empty log has `lastLogIndex` 0. It is\n * stored on the entry rather than derived from array position so the\n * consistency check and the leader's per-peer bookkeeping read it directly,\n * and so a persisted/replayed log carries its own indices.\n */\nexport interface LogEntry<C = unknown> {\n /** The term in which the leader created this entry. */\n readonly term: Term;\n /** This entry's 1-based position in the log. */\n readonly index: number;\n /** The opaque command this entry replicates; applied by the consumer on commit. */\n readonly command: C;\n}\n\n// ===========================================================================\n// Role union — invalid states unrepresentable\n// ===========================================================================\n\n/**\n * The role discriminated union. `_tag` is the discriminant (engine convention).\n *\n * The crux of \"make invalid states unrepresentable\": the leader-only volatile\n * state — `nextIndex` / `matchIndex`, the per-peer replication bookkeeping —\n * exists ONLY on the `leader` variant. A follower or candidate value has no\n * such field to read or mis-set; the type makes \"a follower with a matchIndex\n * map\" unspellable rather than merely discouraged (#119 acceptance criterion).\n *\n * `candidate` carries the per-election vote tally (`votesGranted`, including\n * self) so the reducer can decide majority without re-deriving it.\n */\nexport type Role =\n | { readonly _tag: \"follower\" }\n | {\n readonly _tag: \"candidate\";\n /**\n * The set of nodes (self + peers) that have granted this candidate a vote\n * THIS term. A `Set`-by-id semantic kept as a readonly array of unique\n * ids; the reducer dedups on insert so a duplicate reply never\n * double-counts toward the majority.\n */\n readonly votesGranted: readonly NodeId[];\n }\n | {\n readonly _tag: \"leader\";\n /**\n * For each peer, the index of the next log entry to send (Figure 2,\n * leader volatile state). Initialized to the leader's last-log index + 1\n * on election; #120 advances it on `AppendEntries`. Keyed by `NodeId`.\n */\n readonly nextIndex: Readonly<Record<NodeId, number>>;\n /**\n * For each peer, the highest log index known to be replicated (Figure 2,\n * leader volatile state). Initialized to 0; #120 advances it. Keyed by\n * `NodeId`.\n */\n readonly matchIndex: Readonly<Record<NodeId, number>>;\n };\n\n// ===========================================================================\n// RaftState — persistent + volatile, role-discriminated\n// ===========================================================================\n\n/**\n * The whole state of one Raft node, generic over the command payload `C`.\n * Persistent state (`currentTerm`, `votedFor`, `log`) survives crashes in a\n * real deployment (#122); volatile state (`commitIndex`, the leader's\n * `nextIndex`/`matchIndex`) is rebuilt on restart.\n */\nexport interface RaftState<C = unknown> {\n /**\n * Latest term this node has seen (Figure 2, persistent). Init 0; increases\n * monotonically — every higher-term message observed bumps it (and steps the\n * node down to follower).\n */\n readonly currentTerm: Term;\n /**\n * The candidate this node voted for in `currentTerm`, or `null` if it has not\n * voted this term (Figure 2, persistent). Reset to `null` whenever\n * `currentTerm` advances.\n */\n readonly votedFor: NodeId | null;\n /**\n * The replicated log (Figure 2, persistent). 1-based: `log[i]` has\n * `index === i + 1`; `lastLogIndex` is `log.length`. The log *is* the event\n * log (epic Approach): every mutation here — a leader appending a client\n * command, a follower truncating-and-appending on `AppendEntries` — is a pure\n * function of `(state, msg)`, so the event-sourced store under `../do` rebuilds\n * it by folding this same reducer over the recorded Msgs (#122 wires the host;\n * see the module header's persistence-seam note).\n */\n readonly log: readonly LogEntry<C>[];\n /**\n * Highest log index known to be committed (Figure 2, volatile). Init 0. A\n * follower advances it to `min(leaderCommit, lastNewEntryIndex)` on a matching\n * `AppendEntries`; a leader advances it under the Figure-8 current-term\n * majority rule (see {@link RaftNode.onAppendEntriesReply}). Entries up to and\n * including `commitIndex` are durably agreed and safe for the consumer to apply.\n */\n readonly commitIndex: number;\n /** The current role + its role-specific state (see {@link Role}). */\n readonly role: Role;\n}\n\n// ===========================================================================\n// Messages — fired timers + inbound RPCs, the verbs' inputs\n// ===========================================================================\n\n/**\n * Tagged wire shapes the consumer unions into its machine's Msg type and routes\n * into the matching verb. `_tag` is the discriminant. Each carries the sender's\n * `term` (Figure 2: every RPC carries the sender's `currentTerm`) so the\n * \"rules for all servers\" higher-term step-down applies uniformly.\n */\n\n/** Inbound `RequestVote` RPC — a candidate asking this node for its vote. */\nexport interface RequestVoteRequest {\n readonly _tag: \"request_vote_request\";\n /** The candidate's term. */\n readonly term: Term;\n /** The candidate requesting the vote. */\n readonly candidateId: NodeId;\n /**\n * Index of the candidate's last log entry (Figure 2). Read by the up-to-date\n * check — STUBBED in #119 (see {@link logIsUpToDate}); real comparison in #120.\n */\n readonly lastLogIndex: number;\n /** Term of the candidate's last log entry (Figure 2). Stub-read in #119. */\n readonly lastLogTerm: Term;\n}\n\n/** Inbound `RequestVote` reply — a peer answering this node's vote request. */\nexport interface RequestVoteReply {\n readonly _tag: \"request_vote_reply\";\n /** The replier's `currentTerm`, for the candidate to update itself. */\n readonly term: Term;\n /** The peer that replied (so the candidate can dedup its tally). */\n readonly from: NodeId;\n /** Whether the peer granted its vote. */\n readonly voteGranted: boolean;\n}\n\n/**\n * Inbound `AppendEntries` RPC (Figure 2). An empty `entries` array is a\n * heartbeat; a non-empty one replicates log entries. The `prevLog*` pair anchors\n * the consistency check, and `leaderCommit` carries the leader's commit index so\n * the follower can advance its own.\n */\nexport interface AppendEntriesRequest<C = unknown> {\n readonly _tag: \"append_entries_request\";\n /** The leader's term. */\n readonly term: Term;\n /** The leader sending this (so a follower can track the leader). */\n readonly leaderId: NodeId;\n /**\n * Index of the log entry immediately preceding `entries` (0 if `entries`\n * starts at the head of the log). The follower must already hold an entry at\n * this index whose term is `prevLogTerm`, or it rejects (consistency check).\n */\n readonly prevLogIndex: number;\n /** Term of the entry at `prevLogIndex` (0 when `prevLogIndex` is 0). */\n readonly prevLogTerm: Term;\n /** The new entries to store (empty for a heartbeat). Already index-stamped. */\n readonly entries: readonly LogEntry<C>[];\n /** The leader's `commitIndex`, so the follower can advance its own. */\n readonly leaderCommit: number;\n}\n\n/**\n * Inbound `AppendEntries` reply — a follower answering this leader's\n * replication RPC (Figure 2 reply shape). `matchIndex` is the index of the last\n * entry the follower now agrees on (0 if none), used by the leader to advance\n * `matchIndex`/`nextIndex` on success without re-deriving it.\n */\nexport interface AppendEntriesReply {\n readonly _tag: \"append_entries_reply\";\n /** The replier's `currentTerm`, for the leader to update itself / step down. */\n readonly term: Term;\n /** The follower that replied (so the leader can update its per-peer bookkeeping). */\n readonly from: NodeId;\n /** Whether the consistency check passed and the entries were stored. */\n readonly success: boolean;\n /**\n * On success, the index of the follower's last entry covered by this RPC\n * (`prevLogIndex + entries.length`) — the leader's new `matchIndex` for this\n * peer. Meaningless (0) on rejection.\n */\n readonly matchIndex: number;\n}\n\n/**\n * A client command submitted to this node (Figure 2 §5.3: clients send commands\n * to the leader). The leader appends it to its log and replicates; a non-leader\n * rejects it (the consumer would redirect the client to the known leader).\n */\nexport interface ClientCommand<C = unknown> {\n readonly _tag: \"client_command\";\n /** The opaque command to replicate (the consumer's state-machine input). */\n readonly command: C;\n}\n\n/**\n * The two fired-timer Msgs, dispatched by the `DeadlineSub`s this module arms.\n * The consumer maps a `deadline_exceeded` whose id matches the election /\n * heartbeat sub id onto these (`toRaftMsg` does the mapping), then routes them\n * into `onElectionTimeout` / `onHeartbeat`. Each carries the firing `at` (the\n * deadline's `atMs`) so the verb re-arms the next absolute target without\n * reading the clock.\n */\nexport interface ElectionTimeoutFired {\n readonly _tag: \"election_timeout_fired\";\n /** Absolute wall-clock ms the timer fired at (the deadline's target). */\n readonly at: number;\n}\nexport interface HeartbeatFired {\n readonly _tag: \"heartbeat_fired\";\n /** Absolute wall-clock ms the timer fired at (the deadline's target). */\n readonly at: number;\n}\n\n/** The full inbound-Msg union a Raft node's `update` handles. */\nexport type RaftMsg<C = unknown> =\n | RequestVoteRequest\n | RequestVoteReply\n | AppendEntriesRequest<C>\n | AppendEntriesReply\n | ClientCommand<C>\n | ElectionTimeoutFired\n | HeartbeatFired;\n\n// ===========================================================================\n// Cmds — outbound RPCs routed through the injected transport port\n// ===========================================================================\n\n/**\n * Outbound RPCs as `Cmd`s. The consumer's `interpret` performs each over the\n * injected transport port (a fetch binding, an in-memory bus in tests); this\n * module only emits the literals. `to` addresses a peer; a reply addresses the\n * original caller.\n */\n\n/** Send a `RequestVote` request to one peer (fanned out, one Cmd per peer). */\nexport interface SendRequestVote extends Cmd<\"raft:send_request_vote\"> {\n /** The peer to ask. */\n readonly to: NodeId;\n /** This candidate's term. */\n readonly term: Term;\n /** This candidate's id. */\n readonly candidateId: NodeId;\n /** This candidate's last-log index (stub payload until #120). */\n readonly lastLogIndex: number;\n /** This candidate's last-log term (stub payload until #120). */\n readonly lastLogTerm: Term;\n}\n\n/** Reply to a `RequestVote` request the original candidate awaits. */\nexport interface SendRequestVoteReply\n extends Cmd<\"raft:send_request_vote_reply\"> {\n /** The candidate that asked. */\n readonly to: NodeId;\n /** This node's `currentTerm`. */\n readonly term: Term;\n /** Whether this node granted its vote. */\n readonly voteGranted: boolean;\n}\n\n/**\n * Send an `AppendEntries` to one peer. Empty `entries` is a heartbeat; a\n * non-empty one replicates from the leader's `nextIndex` for that peer. Carries\n * the full Figure-2 payload so the follower can run its consistency check.\n */\nexport interface SendAppendEntries<C = unknown>\n extends Cmd<\"raft:send_append_entries\"> {\n /** The peer to send to. */\n readonly to: NodeId;\n /** The leader's term. */\n readonly term: Term;\n /** The leader's id. */\n readonly leaderId: NodeId;\n /** Index of the entry preceding `entries` (0 at the head of the log). */\n readonly prevLogIndex: number;\n /** Term of the entry at `prevLogIndex` (0 when `prevLogIndex` is 0). */\n readonly prevLogTerm: Term;\n /** The entries to replicate (empty for a heartbeat). */\n readonly entries: readonly LogEntry<C>[];\n /** The leader's `commitIndex`. */\n readonly leaderCommit: number;\n}\n\n/** Reply to an `AppendEntries` the original leader awaits. */\nexport interface SendAppendEntriesReply\n extends Cmd<\"raft:send_append_entries_reply\"> {\n /** The leader that asked. */\n readonly to: NodeId;\n /** This node's `currentTerm`. */\n readonly term: Term;\n /** Whether the consistency check passed and the entries were stored. */\n readonly success: boolean;\n /** On success, this node's new last-agreed index (the leader's `matchIndex`). */\n readonly matchIndex: number;\n}\n\n/** The full outbound-Cmd union a Raft node emits. */\nexport type RaftCmd<C = unknown> =\n | SendRequestVote\n | SendRequestVoteReply\n | SendAppendEntries<C>\n | SendAppendEntriesReply;\n\n// ===========================================================================\n// Subs — election + heartbeat DeadlineSubs\n// ===========================================================================\n\n/** Stable sub id for this node's election-timeout deadline. */\nfunction electionSubId(self: NodeId): string {\n return `raft:election:${self}`;\n}\n/** Stable sub id for this node's heartbeat deadline. */\nfunction heartbeatSubId(self: NodeId): string {\n return `raft:heartbeat:${self}`;\n}\n\n/**\n * Map a fired `deadline_exceeded` (from `../deadline`'s subscribe cell) onto the\n * Raft timer Msg it represents, by matching the sub id. Returns `null` if the\n * id belongs to neither Raft timer — so a consumer wiring several deadline\n * families can route ours without a brittle string-prefix check at the call\n * site. The deadline's `atMs` becomes the Msg's `at` (no clock read).\n */\nexport function toRaftMsg(\n self: NodeId,\n fired: { readonly id: string; readonly atMs: number },\n): ElectionTimeoutFired | HeartbeatFired | null {\n if (fired.id === electionSubId(self)) {\n return { _tag: \"election_timeout_fired\", at: fired.atMs };\n }\n if (fired.id === heartbeatSubId(self)) {\n return { _tag: \"heartbeat_fired\", at: fired.atMs };\n }\n return null;\n}\n\n// ===========================================================================\n// The node handle — pure verbs + subs, rng injected once\n// ===========================================================================\n\n/**\n * Result tuple every verb returns: the next state and the Cmds to emit. Matches\n * the `(state, cmds)` shape the substrate's reducer cells return, so a consumer\n * threads these straight through their `update`.\n */\nexport type RaftStep<C = unknown> = readonly [\n RaftState<C>,\n readonly RaftCmd<C>[],\n];\n\n/**\n * The node handle returned by {@link createRaftNode}. Spread its hooks into a\n * machine: `init()` seeds a fresh follower, the verbs (`onElectionTimeout`,\n * `onRequestVote`, `onRequestVoteReply`, `onAppendEntries`, `onHeartbeat`) are\n * the reducer cells, and `subs(state, now)` returns the live `DeadlineSub`s.\n *\n * Every verb is PURE given the injected `rng`: no clock read (time arrives as\n * `at`), no ambient randomness, no mutation, no throw.\n */\nexport interface RaftNode<C = unknown> {\n /** Seed a fresh node: term 0, no vote, empty log, follower. */\n init(): RaftState<C>;\n\n /**\n * The election timeout fired without a heartbeat / granted vote. Convert\n * follower OR candidate → candidate and start a NEW election: bump the term,\n * vote for self, reset the tally to `{self}`, and emit `RequestVote` to every\n * peer (carrying the real last-log index/term). A leader ignores its own\n * election timeout (it has a live mandate).\n *\n * `at` is the firing wall-clock (carried by the Msg) — used only so the\n * caller can re-arm the next election deadline at `at + <jittered timeout>`\n * via `subs`; the verb itself reads no clock.\n *\n * A single-node cluster (`peers` empty) wins the election immediately:\n * one vote (self) is already a majority, so the node converts straight to\n * leader and emits the first heartbeat round (empty here — no peers).\n */\n onElectionTimeout(state: RaftState<C>, at: number): RaftStep<C>;\n\n /**\n * Handle an inbound `RequestVote` request (Figure 2 receiver rule).\n *\n * - Rules for all servers: if `req.term > currentTerm`, adopt the term, clear\n * `votedFor`, and step down to follower BEFORE evaluating the grant.\n * - Reply `false` if `req.term < currentTerm` (stale candidate).\n * - Otherwise grant iff (`votedFor` is null OR already this candidate) AND the\n * candidate's log is at least as up-to-date ({@link logIsUpToDate} — the\n * real last-log term/index comparison, #120). Granting sets\n * `votedFor = candidateId`.\n *\n * Granting a vote (or stepping down) counts as \"heard from a valid leader /\n * candidate\", so the consumer re-arms the election timer off the resulting\n * state (`subs`).\n */\n onRequestVote(state: RaftState<C>, req: RequestVoteRequest): RaftStep<C>;\n\n /**\n * Handle an inbound `RequestVote` reply (the candidate tallying votes).\n *\n * - Rules for all servers: a higher-term reply steps the node down to follower\n * (it lost the election implicitly).\n * - A stale-term reply (`reply.term < currentTerm`) is dropped.\n * - A grant from a peer not yet counted adds to the tally; on reaching a\n * majority the candidate converts to leader (initializing `nextIndex` /\n * `matchIndex`) and emits the first `AppendEntries` round to every peer.\n * - A reply that arrives when the node is no longer a candidate (already\n * leader / stepped down) is a no-op.\n */\n onRequestVoteReply(state: RaftState<C>, reply: RequestVoteReply): RaftStep<C>;\n\n /**\n * Handle an inbound `AppendEntries` — heartbeat or replication (Figure 2\n * receiver rules). Always emits an `AppendEntries` reply Cmd unless the term\n * is stale-and-dropped without a state change.\n *\n * - Reply-side rule: `req.term < currentTerm` is rejected — a stale leader;\n * the node holds its role and replies `success: false`.\n * - `req.term >= currentTerm`: a valid leader for this term. Adopt the term if\n * higher and revert candidate → follower (a candidate that hears from a\n * leader of term ≥ its own steps down). Then run the **consistency check**:\n * if the follower's log has no entry at `prevLogIndex` with term\n * `prevLogTerm`, reply `success: false` (the leader backs off `nextIndex`\n * and retries). On a match, delete any conflicting suffix and append the new\n * `entries`, then advance `commitIndex` to\n * `min(leaderCommit, lastNewEntryIndex)`, and reply `success: true` with the\n * follower's new last-agreed index as `matchIndex`.\n *\n * Hearing a valid `AppendEntries` resets the election timer (the consumer\n * re-arms off the resulting state).\n */\n onAppendEntries(\n state: RaftState<C>,\n req: AppendEntriesRequest<C>,\n ): RaftStep<C>;\n\n /**\n * Handle an inbound `AppendEntries` reply (the leader updating its per-peer\n * bookkeeping, Figure 2 leader rules).\n *\n * - Rules for all servers: a higher-term reply steps the leader down to\n * follower.\n * - A reply received when no longer leader, or a stale-term reply, is a no-op.\n * - On `success`: set `matchIndex[from]` to the reply's `matchIndex` and\n * `nextIndex[from]` to `matchIndex + 1`, then advance `commitIndex` under the\n * **Figure-8 safety rule** — to the highest N such that N is on a majority of\n * `matchIndex`es AND `log[N].term === currentTerm` (a leader NEVER commits a\n * prior-term entry by counting replicas; it commits it only transitively once\n * a current-term entry above it commits).\n * - On rejection: decrement `nextIndex[from]` (floored at 1) and re-emit an\n * `AppendEntries` to that peer from the backed-off index (the retry).\n */\n onAppendEntriesReply(\n state: RaftState<C>,\n reply: AppendEntriesReply,\n ): RaftStep<C>;\n\n /**\n * A client submitted a command. On the leader: append a new entry\n * `{ term: currentTerm, index: lastLogIndex + 1, command }` to the log and\n * emit an `AppendEntries` round replicating it to every peer. On a non-leader:\n * a no-op (no append, no Cmds) — the consumer redirects the client to the\n * leader. This is the entry point that grows the replicated/event log.\n */\n onClientCommand(state: RaftState<C>, msg: ClientCommand<C>): RaftStep<C>;\n\n /**\n * The leader's heartbeat timer fired: emit an `AppendEntries` to every peer\n * (entries beyond that peer's `nextIndex`, empty when the peer is caught up).\n * A no-op (no Cmds) on a non-leader — a stale heartbeat fire racing a\n * step-down the reconcile pass has not yet retired.\n */\n onHeartbeat(state: RaftState<C>, at: number): RaftStep<C>;\n\n /**\n * The live `DeadlineSub`s for `state`, computed against the current clock\n * `now` (the ONLY clock read — at subscribe time, never in a verb; same seam\n * as `../poller`'s `subs`). The election-timeout target is `now + <jittered\n * timeout>` drawn from the injected `rng`; the heartbeat target is\n * `now + heartbeatMs`.\n *\n * - follower / candidate → arm ONLY the election timeout.\n * - leader → arm ONLY the heartbeat (a leader does not time itself out).\n */\n subs(state: RaftState<C>, now: number): readonly DeadlineSub[];\n}\n\n/**\n * The candidate \"log at least as up-to-date\" check (Figure 2 receiver rule,\n * §5.4.1 election restriction). Compares the candidate's last-log\n * (term, index) against ours: term dominates, index breaks a tie.\n *\n * Up-to-date iff the candidate's last-log term is GREATER than ours, OR the\n * terms are EQUAL and the candidate's last-log index is ≥ ours. (Our own\n * last-log term/index are read from the tail of `log`; an empty log is\n * (term 0, index 0), so any candidate is at least as up-to-date as an empty\n * log.) This is the property that keeps a leader's log a superset of every\n * committed entry — a node missing a committed entry can never gather a\n * majority, so it cannot win.\n */\nexport function logIsUpToDate(\n log: readonly LogEntry[],\n candidateLastLogIndex: number,\n candidateLastLogTerm: Term,\n): boolean {\n const last = log.at(-1);\n const ourLastTerm = last?.term ?? 0;\n const ourLastIndex = last?.index ?? 0;\n if (candidateLastLogTerm !== ourLastTerm) {\n return candidateLastLogTerm > ourLastTerm;\n }\n return candidateLastLogIndex >= ourLastIndex;\n}\n\n/**\n * The number of votes that constitutes a majority of the cluster. Cluster size\n * is `peers + self`; a strict majority is `floor(size / 2) + 1`. A single-node\n * cluster (size 1) needs 1 vote — its own.\n */\nfunction majority(peerCount: number): number {\n const clusterSize = peerCount + 1;\n return Math.floor(clusterSize / 2) + 1;\n}\n\n/**\n * Build a Raft node from `config`. Returns the hook bag to spread into a\n * machine. Pure factory — no clock read, no timer; everything happens when the\n * consumer calls the verbs / `subs` with an injected `at` / `now`.\n *\n * `rng` is injected ONCE here (default `Math.random` at the effect boundary, a\n * fixed value in tests) and read ONLY by `subs` to jitter the election timeout.\n * No verb body names the global RNG, so a node driven by a recorded schedule\n * replays bit-for-bit (the epic's headline payoff).\n */\nexport function createRaftNode<C = unknown>(\n config: RaftConfig,\n rng: () => number = Math.random,\n): RaftNode<C> {\n const { self, peers, electionTimeout, heartbeatMs } = config;\n const quorum = majority(peers.length);\n\n /** This node's last-log index (1-based; 0 for an empty log). */\n function lastIndex(state: RaftState<C>): number {\n return state.log.at(-1)?.index ?? 0;\n }\n /** Term of this node's last log entry (0 for an empty log). */\n function lastTerm(state: RaftState<C>): number {\n return state.log.at(-1)?.term ?? 0;\n }\n\n /**\n * Adopt a strictly-higher term: bump `currentTerm`, clear `votedFor`, and\n * step down to follower (Figure 2 \"rules for all servers\"). Pure; the caller\n * folds the rest of its decision on top of the returned state. Returns the\n * input unchanged when `term` is not higher (so callers can apply it\n * unconditionally before evaluating their own rule).\n */\n function adoptHigherTerm(state: RaftState<C>, term: Term): RaftState<C> {\n if (term <= state.currentTerm) return state;\n return {\n ...state,\n currentTerm: term,\n votedFor: null,\n role: { _tag: \"follower\" },\n };\n }\n\n /**\n * Convert to candidate and start a fresh election. Bumps the term, votes for\n * self, resets the tally to `{self}`, and emits `RequestVote` to every peer\n * carrying the real last-log index/term (the §5.4.1 election restriction the\n * receiver enforces via {@link logIsUpToDate}). Shared by `onElectionTimeout`\n * for both follower→candidate and candidate→candidate.\n */\n function startElection(state: RaftState<C>): RaftStep<C> {\n const newTerm = state.currentTerm + 1;\n const lastLogIndex = lastIndex(state);\n const lastLogTerm = lastTerm(state);\n\n const next: RaftState<C> = {\n ...state,\n currentTerm: newTerm,\n votedFor: self,\n role: { _tag: \"candidate\", votesGranted: [self] },\n };\n\n const requests: RaftCmd<C>[] = peers.map((peer) => ({\n type: \"raft:send_request_vote\",\n to: peer,\n term: newTerm,\n candidateId: self,\n lastLogIndex,\n lastLogTerm,\n }));\n\n // A single-node cluster reaches majority with its own vote alone — convert\n // straight to leader and emit the (empty, no-peer) first heartbeat round.\n if (\n next.role._tag === \"candidate\" &&\n next.role.votesGranted.length >= quorum\n ) {\n return becomeLeader(next);\n }\n\n return [next, requests];\n }\n\n /**\n * Convert a winning candidate to leader: initialize per-peer `nextIndex`\n * (leader's last-log index + 1) and `matchIndex` (0), then emit the first\n * `AppendEntries` round to every peer. Figure 2 leader-on-election rules.\n * `nextIndex` / `matchIndex` live only on the leader variant — built here,\n * nowhere else.\n */\n function becomeLeader(state: RaftState<C>): RaftStep<C> {\n const nextIdx = lastIndex(state) + 1;\n const nextIndex: Record<NodeId, number> = {};\n const matchIndex: Record<NodeId, number> = {};\n for (const peer of peers) {\n nextIndex[peer] = nextIdx;\n matchIndex[peer] = 0;\n }\n\n const leader: RaftState<C> = {\n ...state,\n role: { _tag: \"leader\", nextIndex, matchIndex },\n };\n\n return [leader, replicateAll(leader)];\n }\n\n /**\n * Build the `AppendEntries` Cmd for one peer from the leader's `nextIndex`\n * for it: `prevLogIndex` is `nextIndex - 1`, `entries` is everything from\n * `nextIndex` onward (empty when the peer is caught up → a heartbeat). Pure\n * read of the leader's log + per-peer cursor. Caller guarantees `state` is a\n * leader.\n */\n function appendEntriesFor(\n state: RaftState<C>,\n peer: NodeId,\n next: number,\n ): SendAppendEntries<C> {\n const prevLogIndex = next - 1;\n // log is 1-based and contiguous, so index i lives at log[i - 1]; `.at`\n // keeps the access total (0 term at the head / off the end).\n const prevLogTerm =\n prevLogIndex > 0 ? (state.log.at(prevLogIndex - 1)?.term ?? 0) : 0;\n const entries = state.log.slice(prevLogIndex);\n return {\n type: \"raft:send_append_entries\",\n to: peer,\n term: state.currentTerm,\n leaderId: self,\n prevLogIndex,\n prevLogTerm,\n entries,\n leaderCommit: state.commitIndex,\n };\n }\n\n /** The full `AppendEntries` fanout a leader sends (one Cmd per peer). */\n function replicateAll(state: RaftState<C>): RaftCmd<C>[] {\n if (state.role._tag !== \"leader\") return [];\n const { nextIndex } = state.role;\n // Every peer is a key of nextIndex (built in becomeLeader); `?? 1` keeps\n // the read total for the type-checker and floors a missing cursor at the\n // log head.\n return peers.map((peer) =>\n appendEntriesFor(state, peer, nextIndex[peer] ?? 1),\n );\n }\n\n /**\n * Advance a leader's `commitIndex` under the Figure-8 safety rule. Returns the\n * highest N > commitIndex such that (a) a MAJORITY of the cluster — counting\n * the leader's own log plus the peers whose `matchIndex >= N` — has the entry,\n * AND (b) `log[N].term === currentTerm`. Clause (b) is the subtle correctness\n * point: a leader must NOT commit an entry from a PRIOR term merely because it\n * sits on a majority of logs (Figure 8 shows such an entry can still be\n * overwritten). A prior-term entry commits only transitively, once a\n * current-term entry above it reaches a majority. Returns the unchanged\n * `commitIndex` when no such N exists.\n */\n function advanceCommitIndex(state: RaftState<C>): number {\n if (state.role._tag !== \"leader\") return state.commitIndex;\n const { matchIndex } = state.role;\n const lastLogIndex = lastIndex(state);\n let commit = state.commitIndex;\n // Walk candidate indices above the current commit; the highest qualifying N\n // wins (each higher N implies the lower ones, so we keep the max).\n for (let n = state.commitIndex + 1; n <= lastLogIndex; n++) {\n // Figure-8: only an entry from the current term is committable by count.\n if (state.log.at(n - 1)?.term !== state.currentTerm) continue;\n // The leader itself holds entry n (n <= lastLogIndex), so start the count\n // at 1, then add every peer that has replicated up to at least n.\n let replicas = 1;\n for (const peer of peers) {\n if ((matchIndex[peer] ?? 0) >= n) replicas++;\n }\n if (replicas >= quorum) commit = n;\n }\n return commit;\n }\n\n function init(): RaftState<C> {\n return {\n currentTerm: 0,\n votedFor: null,\n log: [],\n commitIndex: 0,\n role: { _tag: \"follower\" },\n };\n }\n\n function onElectionTimeout(state: RaftState<C>, _at: number): RaftStep<C> {\n // A leader has a live mandate; its own election timer is never armed (see\n // `subs`), but a stale fire racing a step-up must be a no-op regardless.\n if (state.role._tag === \"leader\") return [state, []];\n return startElection(state);\n }\n\n function onRequestVote(\n state: RaftState<C>,\n req: RequestVoteRequest,\n ): RaftStep<C> {\n // Rules for all servers: a higher term steps us down BEFORE we decide.\n const base = adoptHigherTerm(state, req.term);\n\n // Stale candidate — reject. Use `base.currentTerm` (already adopted) so the\n // reply carries our up-to-date term.\n if (req.term < base.currentTerm) {\n return [base, [voteReply(req.candidateId, base.currentTerm, false)]];\n }\n\n const free = base.votedFor === null || base.votedFor === req.candidateId;\n const upToDate = logIsUpToDate(base.log, req.lastLogIndex, req.lastLogTerm);\n const grant = free && upToDate;\n\n if (!grant) {\n return [base, [voteReply(req.candidateId, base.currentTerm, false)]];\n }\n\n // Granting records the vote (and resets the election timer via `subs`, off\n // the returned follower state).\n const granted: RaftState<C> = {\n ...base,\n votedFor: req.candidateId,\n role: { _tag: \"follower\" },\n };\n return [granted, [voteReply(req.candidateId, base.currentTerm, true)]];\n }\n\n function onRequestVoteReply(\n state: RaftState<C>,\n reply: RequestVoteReply,\n ): RaftStep<C> {\n // Rules for all servers: a higher-term reply steps us down (we lost).\n if (reply.term > state.currentTerm) {\n return [adoptHigherTerm(state, reply.term), []];\n }\n\n // Drop stale-term replies and replies for a role that is no longer\n // tallying (already leader / stepped down).\n if (reply.term < state.currentTerm) return [state, []];\n if (state.role._tag !== \"candidate\") return [state, []];\n if (!reply.voteGranted) return [state, []];\n\n // Dedup the granter so a duplicate reply never double-counts.\n if (state.role.votesGranted.includes(reply.from)) return [state, []];\n\n const votesGranted = [...state.role.votesGranted, reply.from];\n const tallied: RaftState<C> = {\n ...state,\n role: { _tag: \"candidate\", votesGranted },\n };\n\n if (votesGranted.length >= quorum) return becomeLeader(tallied);\n return [tallied, []];\n }\n\n function onAppendEntries(\n state: RaftState<C>,\n req: AppendEntriesRequest<C>,\n ): RaftStep<C> {\n // Stale leader — reject. Reply with our (higher) term so the sender steps\n // down; no state change.\n if (req.term < state.currentTerm) {\n return [state, [appendReply(req.leaderId, state.currentTerm, false, 0)]];\n }\n\n // Valid leader for term ≥ ours. Adopt a higher term (clears votedFor, steps\n // down); then ensure we are a follower for this term — a candidate that\n // hears from a leader of term ≥ its own reverts (Figure 2). Hearing a valid\n // AppendEntries resets the election timer (consumer re-arms off this state).\n const adopted = adoptHigherTerm(state, req.term);\n const base: RaftState<C> =\n adopted.role._tag === \"follower\"\n ? adopted\n : { ...adopted, role: { _tag: \"follower\" } };\n\n // Consistency check (Figure 2): the follower must hold an entry at\n // `prevLogIndex` whose term is `prevLogTerm` (prevLogIndex 0 always matches\n // — it anchors at the head). Otherwise reject; the leader backs off.\n const prevOk =\n req.prevLogIndex === 0 ||\n base.log[req.prevLogIndex - 1]?.term === req.prevLogTerm;\n if (!prevOk) {\n return [base, [appendReply(req.leaderId, base.currentTerm, false, 0)]];\n }\n\n // Match: keep the log up to prevLogIndex, then delete any conflicting suffix\n // and append the new entries. Splicing at prevLogIndex and concatenating is\n // the truncate-and-append in one step (an entry already present and\n // identical is simply overwritten with itself — idempotent on a retry).\n const kept = base.log.slice(0, req.prevLogIndex);\n const nextLog = [...kept, ...req.entries];\n const lastNew = req.prevLogIndex + req.entries.length;\n\n // Advance commitIndex to min(leaderCommit, index of last new entry). Never\n // run ahead of what this follower actually holds.\n const commitIndex =\n req.leaderCommit > base.commitIndex\n ? Math.min(req.leaderCommit, lastNew)\n : base.commitIndex;\n\n const next: RaftState<C> = { ...base, log: nextLog, commitIndex };\n return [next, [appendReply(req.leaderId, base.currentTerm, true, lastNew)]];\n }\n\n function onAppendEntriesReply(\n state: RaftState<C>,\n reply: AppendEntriesReply,\n ): RaftStep<C> {\n // Rules for all servers: a higher-term reply steps us down.\n if (reply.term > state.currentTerm) {\n return [adoptHigherTerm(state, reply.term), []];\n }\n // Only a leader of the matching term acts on replies; anything else is a\n // stale/misrouted reply and a no-op.\n if (state.role._tag !== \"leader\") return [state, []];\n if (reply.term < state.currentTerm) return [state, []];\n\n const { nextIndex, matchIndex } = state.role;\n\n if (!reply.success) {\n // Consistency check failed at the follower: back off nextIndex (floored\n // at 1) and retry from the lower bound. matchIndex is untouched.\n const backed = Math.max(1, (nextIndex[reply.from] ?? 1) - 1);\n const retried: RaftState<C> = {\n ...state,\n role: {\n _tag: \"leader\",\n nextIndex: { ...nextIndex, [reply.from]: backed },\n matchIndex,\n },\n };\n return [retried, [appendEntriesFor(retried, reply.from, backed)]];\n }\n\n // Success: this peer now agrees up to reply.matchIndex. Advance its cursors\n // monotonically (a stale/duplicated success must never pull them backward),\n // then re-evaluate the commit index under the Figure-8 rule.\n const newMatch = Math.max(matchIndex[reply.from] ?? 0, reply.matchIndex);\n const advanced: RaftState<C> = {\n ...state,\n role: {\n _tag: \"leader\",\n nextIndex: { ...nextIndex, [reply.from]: newMatch + 1 },\n matchIndex: { ...matchIndex, [reply.from]: newMatch },\n },\n };\n const commitIndex = advanceCommitIndex(advanced);\n return [{ ...advanced, commitIndex }, []];\n }\n\n function onClientCommand(\n state: RaftState<C>,\n msg: ClientCommand<C>,\n ): RaftStep<C> {\n // Only the leader accepts client commands; a non-leader rejects (the\n // consumer redirects to the known leader). No append, no Cmds.\n if (state.role._tag !== \"leader\") return [state, []];\n\n const entry: LogEntry<C> = {\n term: state.currentTerm,\n index: lastIndex(state) + 1,\n command: msg.command,\n };\n const appended: RaftState<C> = { ...state, log: [...state.log, entry] };\n return [appended, replicateAll(appended)];\n }\n\n function onHeartbeat(state: RaftState<C>, _at: number): RaftStep<C> {\n // Only a leader heartbeats. A stale fire racing a step-down is a no-op.\n if (state.role._tag !== \"leader\") return [state, []];\n return [state, replicateAll(state)];\n }\n\n function subs(state: RaftState<C>, now: number): readonly DeadlineSub[] {\n if (state.role._tag === \"leader\") {\n // A leader arms ONLY the heartbeat; it never times itself out.\n return [deadlineSub(heartbeatSubId(self), now + heartbeatMs)];\n }\n // Follower / candidate arm ONLY the election timeout, jittered in\n // [minMs, maxMs] from the injected rng (never Math.random ambiently).\n const span = electionTimeout.maxMs - electionTimeout.minMs;\n const timeout = electionTimeout.minMs + Math.floor(rng() * (span + 1));\n return [deadlineSub(electionSubId(self), now + timeout)];\n }\n\n /** Build a `RequestVote` reply Cmd addressed to the asking candidate. */\n function voteReply(\n to: NodeId,\n term: Term,\n voteGranted: boolean,\n ): SendRequestVoteReply {\n return { type: \"raft:send_request_vote_reply\", to, term, voteGranted };\n }\n\n /** Build an `AppendEntries` reply Cmd addressed to the asking leader. */\n function appendReply(\n to: NodeId,\n term: Term,\n success: boolean,\n matchIndex: number,\n ): SendAppendEntriesReply {\n return {\n type: \"raft:send_append_entries_reply\",\n to,\n term,\n success,\n matchIndex,\n };\n }\n\n return {\n init,\n onElectionTimeout,\n onRequestVote,\n onRequestVoteReply,\n onAppendEntries,\n onAppendEntriesReply,\n onClientCommand,\n onHeartbeat,\n subs,\n };\n}\n\n// Re-export the sub-id builders' identities for consumers wiring `subscribe`\n// cells / asserting against the armed sub ids without re-deriving the strings.\nexport { electionSubId, heartbeatSubId };\n\n// `Sub` / `subId` are imported for the `DeadlineSub` return type and id\n// branding inside `../deadline`; re-exported here so a consumer wiring the Raft\n// subs into a machine's `subscribe` map need not also reach into the core.\nexport type { Sub };\nexport { subId };\n"]}
@@ -1,105 +0,0 @@
1
- import { NodeId, RaftState, RaftConfig } from './index.js';
2
- import { SimTrace, Schedule } from './sim.js';
3
- import '../deadline/index.js';
4
- import '../core-CymXZCeY.js';
5
- import '../types-N9LZbdnr.js';
6
-
7
- /**
8
- * @demlik/tea/raft — a runnable, deterministic multi-node Raft demo (#123,
9
- * stories 7+8 of epic #117). The live demo the docs reference: it boots a
10
- * 3-node cluster and drives the full consensus arc through {@link runSchedule}
11
- * — elect a leader, replicate + commit a client command to a majority, KILL the
12
- * leader (a network partition), watch the surviving majority elect a NEW leader,
13
- * and show the committed log converged across the survivors.
14
- *
15
- * It is a thin, readable composition over the existing simulation harness
16
- * (`./sim`): every phase is just a slice of one fixed {@link Schedule}, so the
17
- * whole run is pure, replayable, and byte-identical on re-run (the determinism
18
- * the safety suite already enforces — here turned into a watchable narrative).
19
- * It owns NO clock, NO RNG, NO IO beyond an optional narration printer.
20
- *
21
- * Two entry points:
22
- * - {@link runDemo} — fold the scripted schedule, return a structured
23
- * {@link DemoResult} (the phases, the per-phase cluster summary, the final
24
- * trace). Pure: it asserts nothing and prints nothing — the test and the
25
- * CLI both read its result.
26
- * - {@link narrateDemo} — render a {@link DemoResult} as readable lines (the
27
- * `pnpm demo:raft` output). Pure string-building; the runner does the IO.
28
- *
29
- * Run it: `pnpm --filter @demlik/tea demo:raft`
30
- * (see `src/raft/README.md`).
31
- */
32
-
33
- /** A compact, JSON-friendly read of one node's state (what the demo reports). */
34
- interface NodeSummary {
35
- /** The node's id. */
36
- readonly id: NodeId;
37
- /** Its role this snapshot: follower / candidate / leader. */
38
- readonly role: RaftState["role"]["_tag"];
39
- /** Its current term. */
40
- readonly term: number;
41
- /** Its commit index (entries at or below this are committed). */
42
- readonly commitIndex: number;
43
- /** The COMMITTED commands, in log order (entries `1..commitIndex`). */
44
- readonly committed: readonly number[];
45
- }
46
- /** Read one node's committed commands (the prefix of its log up to commitIndex). */
47
- declare function committedCommands(state: RaftState<number>): readonly number[];
48
- /** A snapshot of one phase's outcome: its headline + the cluster after it ran. */
49
- interface PhaseOutcome {
50
- /** The phase headline. */
51
- readonly title: string;
52
- /** The phase's one-line note. */
53
- readonly note: string;
54
- /** The single leader after this phase, or `null`. */
55
- readonly leader: NodeId | null;
56
- /** Every node's summary after this phase. */
57
- readonly cluster: readonly NodeSummary[];
58
- }
59
- /** The structured outcome of a full demo run — pure data, asserts nothing. */
60
- interface DemoResult {
61
- /** The cluster node ids, in config order. */
62
- readonly nodes: readonly NodeId[];
63
- /** The nodes partitioned off by the end of the run (the killed leader). */
64
- readonly partitioned: readonly NodeId[];
65
- /** The leader elected in phase 1 (before the kill). */
66
- readonly firstLeader: NodeId | null;
67
- /** The leader elected in phase 4 (after the kill) — a DIFFERENT node. */
68
- readonly secondLeader: NodeId | null;
69
- /** The commands committed across the SURVIVING majority by the end. */
70
- readonly convergedLog: readonly number[];
71
- /** Per-phase outcomes, in order. */
72
- readonly phases: readonly PhaseOutcome[];
73
- /** The full underlying simulation trace (for replay / deep inspection). */
74
- readonly trace: SimTrace;
75
- }
76
- /** The demo cluster: 3 nodes, default election-timeout window + heartbeat. */
77
- declare function demoCluster(): readonly RaftConfig[];
78
- /** The full scripted schedule (every phase's events concatenated, in order). */
79
- declare function demoSchedule(): Schedule;
80
- /**
81
- * The nodes that survive the partition (everyone NOT in the down-set). The demo
82
- * checks convergence over THESE — the partitioned leader is, by construction,
83
- * stuck at its pre-kill log.
84
- */
85
- declare function survivors(nodes: readonly NodeId[], partitioned: readonly NodeId[]): readonly NodeId[];
86
- /**
87
- * Run the scripted demo against a fresh 3-node cluster and return a structured
88
- * {@link DemoResult}. Pure: folds {@link demoSchedule} through {@link runSchedule},
89
- * then reads the cluster at each phase boundary. Deterministic — the same
90
- * schedule yields a byte-identical trace, so {@link runDemo}() === runDemo().
91
- */
92
- declare function runDemo(): DemoResult;
93
- /**
94
- * Render a {@link DemoResult} as a readable, deterministic narrative — the
95
- * `pnpm demo:raft` output. Pure: returns the lines; the runner prints them.
96
- */
97
- declare function narrateDemo(result: DemoResult): string;
98
- /**
99
- * Re-run the demo's schedule from a fresh cluster and report whether the trace
100
- * is byte-identical to the original run (the determinism contract). Used by the
101
- * test and surfaced in the CLI footer.
102
- */
103
- declare function demoIsReproducible(result: DemoResult): boolean;
104
-
105
- export { type DemoResult, type NodeSummary, type PhaseOutcome, committedCommands, demoCluster, demoIsReproducible, demoSchedule, narrateDemo, runDemo, survivors };
package/dist/raft/demo.js DELETED
@@ -1,172 +0,0 @@
1
- import { clusterConfigs, runSchedule, replaySchedule } from '../chunk-7VEG4KHB.js';
2
- import '../chunk-XRXKH7KA.js';
3
- import '../chunk-QGZW4W3E.js';
4
- import '../chunk-2YFLOCWX.js';
5
- import '../chunk-2WZHS4P2.js';
6
- import '../chunk-BNJYE4AU.js';
7
-
8
- // src/raft/demo.ts
9
- var N0 = "n0";
10
- var N1 = "n1";
11
- var FIRST_COMMAND = 42;
12
- var SECOND_COMMAND = 77;
13
- var PHASES = [
14
- {
15
- title: "1. Elect a leader",
16
- note: "n0 times out, runs an election, and wins a majority (term 1).",
17
- events: [
18
- { kind: "timer", node: N0, timer: "election" },
19
- { kind: "settle", bound: 30 }
20
- ]
21
- },
22
- {
23
- title: "2. Replicate + commit a command",
24
- note: `client sends ${FIRST_COMMAND} to the leader; it replicates to a majority, commits, and the next heartbeat carries the commit to the followers.`,
25
- events: [
26
- { kind: "client", node: N0, command: FIRST_COMMAND },
27
- { kind: "settle", bound: 30 },
28
- // The leader's commitIndex advances on the success replies, but followers
29
- // only learn it on the NEXT AppendEntries (Figure 2 `leaderCommit`). One
30
- // heartbeat round propagates the commit so the cluster converges.
31
- { kind: "timer", node: N0, timer: "heartbeat" },
32
- { kind: "settle", bound: 30 }
33
- ]
34
- },
35
- {
36
- title: "3. Kill the leader",
37
- note: "partition isolates n0 \u2014 the transport drops all traffic to/from it.",
38
- events: [{ kind: "partition", down: [N0] }]
39
- },
40
- {
41
- title: "4. A new leader is elected",
42
- note: "a surviving follower (n1) times out and wins the surviving majority {n1,n2} in a higher term.",
43
- events: [
44
- { kind: "timer", node: N1, timer: "election" },
45
- { kind: "settle", bound: 30 }
46
- ]
47
- },
48
- {
49
- title: "5. Replicate on the new leader \u2192 logs converged",
50
- note: `client sends ${SECOND_COMMAND} to the new leader; it commits on the surviving majority, and a heartbeat converges the survivors' committed logs.`,
51
- events: [
52
- { kind: "client", node: N1, command: SECOND_COMMAND },
53
- { kind: "settle", bound: 30 },
54
- // Same as phase 2: a heartbeat round carries the new commitIndex to the
55
- // surviving follower (n2) so every survivor's committed log converges.
56
- { kind: "timer", node: N1, timer: "heartbeat" },
57
- { kind: "settle", bound: 30 }
58
- ]
59
- }
60
- ];
61
- function committedCommands(state) {
62
- return state.log.slice(0, state.commitIndex).map((e) => e.command);
63
- }
64
- function summarizeNode(id, states) {
65
- const state = states[id];
66
- if (!state) {
67
- return { id, role: "follower", term: 0, commitIndex: 0, committed: [] };
68
- }
69
- return {
70
- id,
71
- role: state.role._tag,
72
- term: state.currentTerm,
73
- commitIndex: state.commitIndex,
74
- committed: committedCommands(state)
75
- };
76
- }
77
- function summarizeCluster(nodes, states) {
78
- return nodes.map((id) => summarizeNode(id, states));
79
- }
80
- function leaderOf(nodes, states) {
81
- const leaders = nodes.filter((id) => states[id]?.role._tag === "leader");
82
- if (leaders.length === 0) return null;
83
- const topTerm = Math.max(
84
- ...leaders.map((id) => states[id]?.currentTerm ?? 0)
85
- );
86
- const top = leaders.filter((id) => states[id]?.currentTerm === topTerm);
87
- return top.length === 1 ? top[0] ?? null : null;
88
- }
89
- function demoCluster() {
90
- return clusterConfigs(3);
91
- }
92
- function demoSchedule() {
93
- return PHASES.flatMap((phase) => phase.events);
94
- }
95
- function survivors(nodes, partitioned) {
96
- const down = new Set(partitioned);
97
- return nodes.filter((id) => !down.has(id));
98
- }
99
- function runDemo() {
100
- const configs = demoCluster();
101
- const nodes = configs.map((c) => c.self);
102
- const schedule = demoSchedule();
103
- const trace = runSchedule(configs, schedule);
104
- const phases = [];
105
- for (let i = 0; i < PHASES.length; i++) {
106
- const phase = PHASES[i];
107
- if (!phase) continue;
108
- const prefix = PHASES.slice(0, i + 1).flatMap((p) => p.events);
109
- const states = runSchedule(configs, prefix).final;
110
- phases.push({
111
- title: phase.title,
112
- note: phase.note,
113
- leader: leaderOf(nodes, states),
114
- cluster: summarizeCluster(nodes, states)
115
- });
116
- }
117
- const partitioned = [N0];
118
- const firstLeader = phases[1]?.leader ?? null;
119
- const secondLeader = phases.at(-1)?.leader ?? null;
120
- const leaderState = secondLeader ? trace.final[secondLeader] : void 0;
121
- const convergedLog = leaderState ? committedCommands(leaderState) : [];
122
- return {
123
- nodes,
124
- partitioned,
125
- firstLeader,
126
- secondLeader,
127
- convergedLog,
128
- phases,
129
- trace
130
- };
131
- }
132
- function nodeLine(n) {
133
- const role = n.role.padEnd(9);
134
- return ` ${n.id} ${role} term=${n.term} commit=${n.commitIndex} committed=[${n.committed.join(",")}]`;
135
- }
136
- function narrateDemo(result) {
137
- const lines = [];
138
- lines.push("=".repeat(72));
139
- lines.push(" @demlik/tea/raft \u2014 multi-node consensus demo (deterministic)");
140
- lines.push(` cluster: ${result.nodes.join(", ")} (majority = 2 of 3)`);
141
- lines.push("=".repeat(72));
142
- for (const phase of result.phases) {
143
- lines.push("");
144
- lines.push(phase.title);
145
- lines.push(` ${phase.note}`);
146
- lines.push(` leader: ${phase.leader ?? "(none \u2014 election in progress)"}`);
147
- for (const node of phase.cluster) {
148
- lines.push(nodeLine(node));
149
- }
150
- }
151
- lines.push("");
152
- lines.push("-".repeat(72));
153
- lines.push(" Summary");
154
- lines.push(` first leader (pre-kill): ${result.firstLeader ?? "none"}`);
155
- lines.push(` killed (partitioned): ${result.partitioned.join(", ")}`);
156
- lines.push(` new leader (post-kill): ${result.secondLeader ?? "none"}`);
157
- lines.push(` converged committed log: [${result.convergedLog.join(", ")}]`);
158
- const failedOver = result.firstLeader !== null && result.secondLeader !== null && result.firstLeader !== result.secondLeader;
159
- lines.push(
160
- ` failover: ${failedOver ? "OK \u2014 a NEW leader took over and the log converged" : "NOT observed"}`
161
- );
162
- lines.push("-".repeat(72));
163
- return lines.join("\n");
164
- }
165
- function demoIsReproducible(result) {
166
- const replay = replaySchedule(demoCluster(), demoSchedule());
167
- return JSON.stringify(replay.final) === JSON.stringify(result.trace.final) && JSON.stringify(replay.steps) === JSON.stringify(result.trace.steps);
168
- }
169
-
170
- export { committedCommands, demoCluster, demoIsReproducible, demoSchedule, narrateDemo, runDemo, survivors };
171
- //# sourceMappingURL=demo.js.map
172
- //# sourceMappingURL=demo.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/raft/demo.ts"],"names":[],"mappings":";;;;;;;;AA4CA,IAAM,EAAA,GAAK,IAAA;AACX,IAAM,EAAA,GAAK,IAAA;AAGX,IAAM,aAAA,GAAgB,EAAA;AACtB,IAAM,cAAA,GAAiB,EAAA;AAoCvB,IAAM,MAAA,GAA+B;AAAA,EACnC;AAAA,IACE,KAAA,EAAO,mBAAA;AAAA,IACP,IAAA,EAAM,+DAAA;AAAA,IACN,MAAA,EAAQ;AAAA,MACN,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,EAAA,EAAI,OAAO,UAAA,EAAW;AAAA,MAC7C,EAAE,IAAA,EAAM,QAAA,EAAU,KAAA,EAAO,EAAA;AAAG;AAC9B,GACF;AAAA,EACA;AAAA,IACE,KAAA,EAAO,iCAAA;AAAA,IACP,IAAA,EAAM,gBAAgB,aAAa,CAAA,iHAAA,CAAA;AAAA,IACnC,MAAA,EAAQ;AAAA,MACN,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,EAAA,EAAI,SAAS,aAAA,EAAc;AAAA,MACnD,EAAE,IAAA,EAAM,QAAA,EAAU,KAAA,EAAO,EAAA,EAAG;AAAA;AAAA;AAAA;AAAA,MAI5B,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,EAAA,EAAI,OAAO,WAAA,EAAY;AAAA,MAC9C,EAAE,IAAA,EAAM,QAAA,EAAU,KAAA,EAAO,EAAA;AAAG;AAC9B,GACF;AAAA,EACA;AAAA,IACE,KAAA,EAAO,oBAAA;AAAA,IACP,IAAA,EAAM,0EAAA;AAAA,IACN,MAAA,EAAQ,CAAC,EAAE,IAAA,EAAM,aAAa,IAAA,EAAM,CAAC,EAAE,CAAA,EAAG;AAAA,GAC5C;AAAA,EACA;AAAA,IACE,KAAA,EAAO,4BAAA;AAAA,IACP,IAAA,EAAM,+FAAA;AAAA,IACN,MAAA,EAAQ;AAAA,MACN,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,EAAA,EAAI,OAAO,UAAA,EAAW;AAAA,MAC7C,EAAE,IAAA,EAAM,QAAA,EAAU,KAAA,EAAO,EAAA;AAAG;AAC9B,GACF;AAAA,EACA;AAAA,IACE,KAAA,EAAO,sDAAA;AAAA,IACP,IAAA,EAAM,gBAAgB,cAAc,CAAA,kHAAA,CAAA;AAAA,IACpC,MAAA,EAAQ;AAAA,MACN,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,EAAA,EAAI,SAAS,cAAA,EAAe;AAAA,MACpD,EAAE,IAAA,EAAM,QAAA,EAAU,KAAA,EAAO,EAAA,EAAG;AAAA;AAAA;AAAA,MAG5B,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,EAAA,EAAI,OAAO,WAAA,EAAY;AAAA,MAC9C,EAAE,IAAA,EAAM,QAAA,EAAU,KAAA,EAAO,EAAA;AAAG;AAC9B;AAEJ,CAAA;AAqBO,SAAS,kBAAkB,KAAA,EAA6C;AAC7E,EAAA,OAAO,KAAA,CAAM,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,WAAW,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAA;AACnE;AAGA,SAAS,aAAA,CAAc,IAAY,MAAA,EAAoC;AACrE,EAAA,MAAM,KAAA,GAAQ,OAAO,EAAE,CAAA;AACvB,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,UAAA,EAAY,IAAA,EAAM,GAAG,WAAA,EAAa,CAAA,EAAG,SAAA,EAAW,EAAC,EAAE;AAAA,EACxE;AACA,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,IAAA,EAAM,MAAM,IAAA,CAAK,IAAA;AAAA,IACjB,MAAM,KAAA,CAAM,WAAA;AAAA,IACZ,aAAa,KAAA,CAAM,WAAA;AAAA,IACnB,SAAA,EAAW,kBAAkB,KAAK;AAAA,GACpC;AACF;AAGA,SAAS,gBAAA,CACP,OACA,MAAA,EACwB;AACxB,EAAA,OAAO,MAAM,GAAA,CAAI,CAAC,OAAO,aAAA,CAAc,EAAA,EAAI,MAAM,CAAC,CAAA;AACpD;AAWA,SAAS,QAAA,CACP,OACA,MAAA,EACe;AACf,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,MAAA,CAAO,CAAC,EAAA,KAAO,OAAO,EAAE,CAAA,EAAG,IAAA,CAAK,IAAA,KAAS,QAAQ,CAAA;AACvE,EAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AACjC,EAAA,MAAM,UAAU,IAAA,CAAK,GAAA;AAAA,IACnB,GAAG,QAAQ,GAAA,CAAI,CAAC,OAAO,MAAA,CAAO,EAAE,CAAA,EAAG,WAAA,IAAe,CAAC;AAAA,GACrD;AACA,EAAA,MAAM,GAAA,GAAM,QAAQ,MAAA,CAAO,CAAC,OAAO,MAAA,CAAO,EAAE,CAAA,EAAG,WAAA,KAAgB,OAAO,CAAA;AACtE,EAAA,OAAO,IAAI,MAAA,KAAW,CAAA,GAAK,GAAA,CAAI,CAAC,KAAK,IAAA,GAAQ,IAAA;AAC/C;AAyCO,SAAS,WAAA,GAAqC;AACnD,EAAA,OAAO,eAAe,CAAC,CAAA;AACzB;AAGO,SAAS,YAAA,GAAyB;AACvC,EAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,CAAC,KAAA,KAAU,MAAM,MAAM,CAAA;AAC/C;AAOO,SAAS,SAAA,CACd,OACA,WAAA,EACmB;AACnB,EAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,WAAW,CAAA;AAChC,EAAA,OAAO,KAAA,CAAM,OAAO,CAAC,EAAA,KAAO,CAAC,IAAA,CAAK,GAAA,CAAI,EAAE,CAAC,CAAA;AAC3C;AAQO,SAAS,OAAA,GAAsB;AACpC,EAAA,MAAM,UAAU,WAAA,EAAY;AAC5B,EAAA,MAAM,QAAQ,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AACvC,EAAA,MAAM,WAAW,YAAA,EAAa;AAC9B,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,OAAA,EAAS,QAAQ,CAAA;AAO3C,EAAA,MAAM,SAAyB,EAAC;AAChC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,CAAO,QAAQ,CAAA,EAAA,EAAK;AACtC,IAAA,MAAM,KAAA,GAAQ,OAAO,CAAC,CAAA;AACtB,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,CAAC,CAAA,CAAE,OAAA,CAAQ,CAAC,CAAA,KAAM,CAAA,CAAE,MAAM,CAAA;AAC7D,IAAA,MAAM,MAAA,GAAS,WAAA,CAAY,OAAA,EAAS,MAAM,CAAA,CAAE,KAAA;AAC5C,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,OAAO,KAAA,CAAM,KAAA;AAAA,MACb,MAAM,KAAA,CAAM,IAAA;AAAA,MACZ,MAAA,EAAQ,QAAA,CAAS,KAAA,EAAO,MAAM,CAAA;AAAA,MAC9B,OAAA,EAAS,gBAAA,CAAiB,KAAA,EAAO,MAAM;AAAA,KACxC,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,WAAA,GAAiC,CAAC,EAAE,CAAA;AAC1C,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,CAAC,CAAA,EAAG,MAAA,IAAU,IAAA;AACzC,EAAA,MAAM,YAAA,GAAe,MAAA,CAAO,EAAA,CAAG,EAAE,GAAG,MAAA,IAAU,IAAA;AAK9C,EAAA,MAAM,WAAA,GAAc,YAAA,GAAe,KAAA,CAAM,KAAA,CAAM,YAAY,CAAA,GAAI,MAAA;AAC/D,EAAA,MAAM,YAAA,GAAe,WAAA,GAAc,iBAAA,CAAkB,WAAW,IAAI,EAAC;AAErE,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,WAAA;AAAA,IACA,WAAA;AAAA,IACA,YAAA;AAAA,IACA,YAAA;AAAA,IACA,MAAA;AAAA,IACA;AAAA,GACF;AACF;AAOA,SAAS,SAAS,CAAA,EAAwB;AACxC,EAAA,MAAM,IAAA,GAAO,CAAA,CAAE,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA;AAC5B,EAAA,OAAO,OAAO,CAAA,CAAE,EAAE,CAAA,EAAA,EAAK,IAAI,SAAS,CAAA,CAAE,IAAI,CAAA,QAAA,EAAW,CAAA,CAAE,WAAW,CAAA,YAAA,EAAe,CAAA,CAAE,SAAA,CAAU,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,CAAA;AACxG;AAMO,SAAS,YAAY,MAAA,EAA4B;AACtD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAC,CAAA;AACzB,EAAA,KAAA,CAAM,KAAK,qEAAgE,CAAA;AAC3E,EAAA,KAAA,CAAM,KAAK,CAAA,WAAA,EAAc,MAAA,CAAO,MAAM,IAAA,CAAK,IAAI,CAAC,CAAA,qBAAA,CAAuB,CAAA;AACvE,EAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAC,CAAA;AAEzB,EAAA,KAAA,MAAW,KAAA,IAAS,OAAO,MAAA,EAAQ;AACjC,IAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AACb,IAAA,KAAA,CAAM,IAAA,CAAK,MAAM,KAAK,CAAA;AACtB,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,EAAA,EAAK,KAAA,CAAM,IAAI,CAAA,CAAE,CAAA;AAC5B,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,UAAA,EAAa,KAAA,CAAM,MAAA,IAAU,oCAA+B,CAAA,CAAE,CAAA;AACzE,IAAA,KAAA,MAAW,IAAA,IAAQ,MAAM,OAAA,EAAS;AAChC,MAAA,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,IAAI,CAAC,CAAA;AAAA,IAC3B;AAAA,EACF;AAEA,EAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AACb,EAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAC,CAAA;AACzB,EAAA,KAAA,CAAM,KAAK,WAAW,CAAA;AACtB,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,4BAAA,EAA+B,MAAA,CAAO,WAAA,IAAe,MAAM,CAAA,CAAE,CAAA;AACxE,EAAA,KAAA,CAAM,KAAK,CAAA,4BAAA,EAA+B,MAAA,CAAO,YAAY,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AACzE,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,4BAAA,EAA+B,MAAA,CAAO,YAAA,IAAgB,MAAM,CAAA,CAAE,CAAA;AACzE,EAAA,KAAA,CAAM,KAAK,CAAA,6BAAA,EAAgC,MAAA,CAAO,aAAa,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA;AAC5E,EAAA,MAAM,UAAA,GACJ,OAAO,WAAA,KAAgB,IAAA,IACvB,OAAO,YAAA,KAAiB,IAAA,IACxB,MAAA,CAAO,WAAA,KAAgB,MAAA,CAAO,YAAA;AAChC,EAAA,KAAA,CAAM,IAAA;AAAA,IACJ,CAAA,YAAA,EAAe,UAAA,GAAa,wDAAA,GAAsD,cAAc,CAAA;AAAA,GAClG;AACA,EAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAC,CAAA;AACzB,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB;AAWO,SAAS,mBAAmB,MAAA,EAA6B;AAC9D,EAAA,MAAM,MAAA,GAAS,cAAA,CAAe,WAAA,EAAY,EAAG,cAAc,CAAA;AAC3D,EAAA,OACE,IAAA,CAAK,UAAU,MAAA,CAAO,KAAK,MAAM,IAAA,CAAK,SAAA,CAAU,OAAO,KAAA,CAAM,KAAK,KAClE,IAAA,CAAK,SAAA,CAAU,OAAO,KAAK,CAAA,KAAM,KAAK,SAAA,CAAU,MAAA,CAAO,MAAM,KAAK,CAAA;AAEtE","file":"demo.js","sourcesContent":["/**\n * @demlik/tea/raft — a runnable, deterministic multi-node Raft demo (#123,\n * stories 7+8 of epic #117). The live demo the docs reference: it boots a\n * 3-node cluster and drives the full consensus arc through {@link runSchedule}\n * — elect a leader, replicate + commit a client command to a majority, KILL the\n * leader (a network partition), watch the surviving majority elect a NEW leader,\n * and show the committed log converged across the survivors.\n *\n * It is a thin, readable composition over the existing simulation harness\n * (`./sim`): every phase is just a slice of one fixed {@link Schedule}, so the\n * whole run is pure, replayable, and byte-identical on re-run (the determinism\n * the safety suite already enforces — here turned into a watchable narrative).\n * It owns NO clock, NO RNG, NO IO beyond an optional narration printer.\n *\n * Two entry points:\n * - {@link runDemo} — fold the scripted schedule, return a structured\n * {@link DemoResult} (the phases, the per-phase cluster summary, the final\n * trace). Pure: it asserts nothing and prints nothing — the test and the\n * CLI both read its result.\n * - {@link narrateDemo} — render a {@link DemoResult} as readable lines (the\n * `pnpm demo:raft` output). Pure string-building; the runner does the IO.\n *\n * Run it: `pnpm --filter @demlik/tea demo:raft`\n * (see `src/raft/README.md`).\n */\n\nimport type { NodeId, RaftState } from \"./index\";\nimport {\n type ClusterStates,\n clusterConfigs,\n type RaftConfig,\n replaySchedule,\n runSchedule,\n type Schedule,\n type SimTrace,\n} from \"./sim\";\n\n// ===========================================================================\n// The scripted scenario — one fixed schedule, sliced into named phases\n// ===========================================================================\n\n// The demo cluster is the 3 default nodes n0/n1/n2 (see {@link clusterConfigs}).\n// Only the two nodes the script names explicitly need constants; the rest of the\n// cluster is addressed via `nodes`.\nconst N0 = \"n0\";\nconst N1 = \"n1\";\n\n/** The two client commands the demo replicates (opaque numeric payloads). */\nconst FIRST_COMMAND = 42;\nconst SECOND_COMMAND = 77;\n\n/**\n * One phase of the demo: a human-readable headline plus the slice of the\n * schedule that drives it. Concatenating every phase's `events` in order yields\n * the full {@link Schedule} {@link runDemo} folds — so the phase boundaries are\n * exactly the trace-step boundaries, and narration lines up with the trace.\n */\ninterface DemoPhase {\n /** What this phase demonstrates (printed as the section headline). */\n readonly title: string;\n /** A one-line plain-language description of the move. */\n readonly note: string;\n /** The schedule events this phase contributes (in order). */\n readonly events: Schedule;\n}\n\n/**\n * The scripted arc. Each phase is a deterministic macro over the harness:\n *\n * 1. n0's election timer fires; `settle` drains the RequestVote round → n0\n * wins a majority (n0+n1, or n0+n2) and becomes leader of term 1, then its\n * first heartbeat round drains.\n * 2. A client command hits the leader; `settle` runs the AppendEntries\n * replication + success replies → the entry commits on a majority.\n * 3. `partition` isolates n0 (the leader): the transport now drops every\n * message to/from n0. n0 can no longer heartbeat the cluster.\n * 4. A surviving follower (n1) times out and starts an election; `settle`\n * drains it across the survivors {n1,n2} (a majority of 3) → n1 becomes\n * leader of a HIGHER term.\n * 5. A client command hits the NEW leader; `settle` replicates it → it commits\n * on the surviving majority. The survivors' committed logs have converged.\n *\n * The `bound`s are generous caps (the FIFO drain stops early when the pool is\n * empty); they only guard against a pathological re-emit loop.\n */\nconst PHASES: readonly DemoPhase[] = [\n {\n title: \"1. Elect a leader\",\n note: \"n0 times out, runs an election, and wins a majority (term 1).\",\n events: [\n { kind: \"timer\", node: N0, timer: \"election\" },\n { kind: \"settle\", bound: 30 },\n ],\n },\n {\n title: \"2. Replicate + commit a command\",\n note: `client sends ${FIRST_COMMAND} to the leader; it replicates to a majority, commits, and the next heartbeat carries the commit to the followers.`,\n events: [\n { kind: \"client\", node: N0, command: FIRST_COMMAND },\n { kind: \"settle\", bound: 30 },\n // The leader's commitIndex advances on the success replies, but followers\n // only learn it on the NEXT AppendEntries (Figure 2 `leaderCommit`). One\n // heartbeat round propagates the commit so the cluster converges.\n { kind: \"timer\", node: N0, timer: \"heartbeat\" },\n { kind: \"settle\", bound: 30 },\n ],\n },\n {\n title: \"3. Kill the leader\",\n note: \"partition isolates n0 — the transport drops all traffic to/from it.\",\n events: [{ kind: \"partition\", down: [N0] }],\n },\n {\n title: \"4. A new leader is elected\",\n note: \"a surviving follower (n1) times out and wins the surviving majority {n1,n2} in a higher term.\",\n events: [\n { kind: \"timer\", node: N1, timer: \"election\" },\n { kind: \"settle\", bound: 30 },\n ],\n },\n {\n title: \"5. Replicate on the new leader → logs converged\",\n note: `client sends ${SECOND_COMMAND} to the new leader; it commits on the surviving majority, and a heartbeat converges the survivors' committed logs.`,\n events: [\n { kind: \"client\", node: N1, command: SECOND_COMMAND },\n { kind: \"settle\", bound: 30 },\n // Same as phase 2: a heartbeat round carries the new commitIndex to the\n // surviving follower (n2) so every survivor's committed log converges.\n { kind: \"timer\", node: N1, timer: \"heartbeat\" },\n { kind: \"settle\", bound: 30 },\n ],\n },\n] as const;\n\n// ===========================================================================\n// Cluster-state reading — the demo's view of \"who leads / what committed\"\n// ===========================================================================\n\n/** A compact, JSON-friendly read of one node's state (what the demo reports). */\nexport interface NodeSummary {\n /** The node's id. */\n readonly id: NodeId;\n /** Its role this snapshot: follower / candidate / leader. */\n readonly role: RaftState[\"role\"][\"_tag\"];\n /** Its current term. */\n readonly term: number;\n /** Its commit index (entries at or below this are committed). */\n readonly commitIndex: number;\n /** The COMMITTED commands, in log order (entries `1..commitIndex`). */\n readonly committed: readonly number[];\n}\n\n/** Read one node's committed commands (the prefix of its log up to commitIndex). */\nexport function committedCommands(state: RaftState<number>): readonly number[] {\n return state.log.slice(0, state.commitIndex).map((e) => e.command);\n}\n\n/** Summarize one node from a cluster snapshot. */\nfunction summarizeNode(id: NodeId, states: ClusterStates): NodeSummary {\n const state = states[id];\n if (!state) {\n return { id, role: \"follower\", term: 0, commitIndex: 0, committed: [] };\n }\n return {\n id,\n role: state.role._tag,\n term: state.currentTerm,\n commitIndex: state.commitIndex,\n committed: committedCommands(state),\n };\n}\n\n/** Summarize every node in a cluster snapshot, in the given order. */\nfunction summarizeCluster(\n nodes: readonly NodeId[],\n states: ClusterStates,\n): readonly NodeSummary[] {\n return nodes.map((id) => summarizeNode(id, states));\n}\n\n/**\n * The cluster's *current* leader in a snapshot: the leader of the HIGHEST term,\n * or `null` if no node leads (mid-election) or two nodes tie for the top term (a\n * genuine split). A partitioned OLD leader keeps believing it leads — it cannot\n * learn it was deposed while its traffic is dropped — but it sits at a strictly\n * LOWER term than whoever the surviving majority elected, so the highest-term\n * leader is the legitimate one. (Election Safety guarantees ≤1 leader per term,\n * so a single top-term leader is unambiguous.)\n */\nfunction leaderOf(\n nodes: readonly NodeId[],\n states: ClusterStates,\n): NodeId | null {\n const leaders = nodes.filter((id) => states[id]?.role._tag === \"leader\");\n if (leaders.length === 0) return null;\n const topTerm = Math.max(\n ...leaders.map((id) => states[id]?.currentTerm ?? 0),\n );\n const top = leaders.filter((id) => states[id]?.currentTerm === topTerm);\n return top.length === 1 ? (top[0] ?? null) : null;\n}\n\n// ===========================================================================\n// The demo result — what runDemo returns (the test + the CLI both read this)\n// ===========================================================================\n\n/** A snapshot of one phase's outcome: its headline + the cluster after it ran. */\nexport interface PhaseOutcome {\n /** The phase headline. */\n readonly title: string;\n /** The phase's one-line note. */\n readonly note: string;\n /** The single leader after this phase, or `null`. */\n readonly leader: NodeId | null;\n /** Every node's summary after this phase. */\n readonly cluster: readonly NodeSummary[];\n}\n\n/** The structured outcome of a full demo run — pure data, asserts nothing. */\nexport interface DemoResult {\n /** The cluster node ids, in config order. */\n readonly nodes: readonly NodeId[];\n /** The nodes partitioned off by the end of the run (the killed leader). */\n readonly partitioned: readonly NodeId[];\n /** The leader elected in phase 1 (before the kill). */\n readonly firstLeader: NodeId | null;\n /** The leader elected in phase 4 (after the kill) — a DIFFERENT node. */\n readonly secondLeader: NodeId | null;\n /** The commands committed across the SURVIVING majority by the end. */\n readonly convergedLog: readonly number[];\n /** Per-phase outcomes, in order. */\n readonly phases: readonly PhaseOutcome[];\n /** The full underlying simulation trace (for replay / deep inspection). */\n readonly trace: SimTrace;\n}\n\n// ===========================================================================\n// Running the demo\n// ===========================================================================\n\n/** The demo cluster: 3 nodes, default election-timeout window + heartbeat. */\nexport function demoCluster(): readonly RaftConfig[] {\n return clusterConfigs(3);\n}\n\n/** The full scripted schedule (every phase's events concatenated, in order). */\nexport function demoSchedule(): Schedule {\n return PHASES.flatMap((phase) => phase.events);\n}\n\n/**\n * The nodes that survive the partition (everyone NOT in the down-set). The demo\n * checks convergence over THESE — the partitioned leader is, by construction,\n * stuck at its pre-kill log.\n */\nexport function survivors(\n nodes: readonly NodeId[],\n partitioned: readonly NodeId[],\n): readonly NodeId[] {\n const down = new Set(partitioned);\n return nodes.filter((id) => !down.has(id));\n}\n\n/**\n * Run the scripted demo against a fresh 3-node cluster and return a structured\n * {@link DemoResult}. Pure: folds {@link demoSchedule} through {@link runSchedule},\n * then reads the cluster at each phase boundary. Deterministic — the same\n * schedule yields a byte-identical trace, so {@link runDemo}() === runDemo().\n */\nexport function runDemo(): DemoResult {\n const configs = demoCluster();\n const nodes = configs.map((c) => c.self);\n const schedule = demoSchedule();\n const trace = runSchedule(configs, schedule);\n\n // Walk the trace one phase at a time: each phase consumes as many trace steps\n // as it contributed events — except `settle`, which expands to one step per\n // drained delivery. So we slice by counting steps until the phase's last\n // non-settle event is consumed. Simpler + robust: re-fold each cumulative\n // prefix of phases and read the resulting cluster (the schedule is tiny).\n const phases: PhaseOutcome[] = [];\n for (let i = 0; i < PHASES.length; i++) {\n const phase = PHASES[i];\n if (!phase) continue;\n const prefix = PHASES.slice(0, i + 1).flatMap((p) => p.events);\n const states = runSchedule(configs, prefix).final;\n phases.push({\n title: phase.title,\n note: phase.note,\n leader: leaderOf(nodes, states),\n cluster: summarizeCluster(nodes, states),\n });\n }\n\n const partitioned: readonly NodeId[] = [N0];\n const firstLeader = phases[1]?.leader ?? null;\n const secondLeader = phases.at(-1)?.leader ?? null;\n\n // Converged log = the committed commands the surviving majority agree on. We\n // read the new leader's committed prefix; the test asserts every survivor\n // matches it once the final settle drains.\n const leaderState = secondLeader ? trace.final[secondLeader] : undefined;\n const convergedLog = leaderState ? committedCommands(leaderState) : [];\n\n return {\n nodes,\n partitioned,\n firstLeader,\n secondLeader,\n convergedLog,\n phases,\n trace,\n };\n}\n\n// ===========================================================================\n// Narration — render a DemoResult as readable lines (the CLI output)\n// ===========================================================================\n\n/** One node's summary as a compact line, e.g. `n0 leader term=2 commit=2 [42,77]`. */\nfunction nodeLine(n: NodeSummary): string {\n const role = n.role.padEnd(9);\n return ` ${n.id} ${role} term=${n.term} commit=${n.commitIndex} committed=[${n.committed.join(\",\")}]`;\n}\n\n/**\n * Render a {@link DemoResult} as a readable, deterministic narrative — the\n * `pnpm demo:raft` output. Pure: returns the lines; the runner prints them.\n */\nexport function narrateDemo(result: DemoResult): string {\n const lines: string[] = [];\n lines.push(\"=\".repeat(72));\n lines.push(\" @demlik/tea/raft — multi-node consensus demo (deterministic)\");\n lines.push(` cluster: ${result.nodes.join(\", \")} (majority = 2 of 3)`);\n lines.push(\"=\".repeat(72));\n\n for (const phase of result.phases) {\n lines.push(\"\");\n lines.push(phase.title);\n lines.push(` ${phase.note}`);\n lines.push(` leader: ${phase.leader ?? \"(none — election in progress)\"}`);\n for (const node of phase.cluster) {\n lines.push(nodeLine(node));\n }\n }\n\n lines.push(\"\");\n lines.push(\"-\".repeat(72));\n lines.push(\" Summary\");\n lines.push(` first leader (pre-kill): ${result.firstLeader ?? \"none\"}`);\n lines.push(` killed (partitioned): ${result.partitioned.join(\", \")}`);\n lines.push(` new leader (post-kill): ${result.secondLeader ?? \"none\"}`);\n lines.push(` converged committed log: [${result.convergedLog.join(\", \")}]`);\n const failedOver =\n result.firstLeader !== null &&\n result.secondLeader !== null &&\n result.firstLeader !== result.secondLeader;\n lines.push(\n ` failover: ${failedOver ? \"OK — a NEW leader took over and the log converged\" : \"NOT observed\"}`,\n );\n lines.push(\"-\".repeat(72));\n return lines.join(\"\\n\");\n}\n\n// ===========================================================================\n// Replay helper — prove the demo is byte-identically reproducible\n// ===========================================================================\n\n/**\n * Re-run the demo's schedule from a fresh cluster and report whether the trace\n * is byte-identical to the original run (the determinism contract). Used by the\n * test and surfaced in the CLI footer.\n */\nexport function demoIsReproducible(result: DemoResult): boolean {\n const replay = replaySchedule(demoCluster(), demoSchedule());\n return (\n JSON.stringify(replay.final) === JSON.stringify(result.trace.final) &&\n JSON.stringify(replay.steps) === JSON.stringify(result.trace.steps)\n );\n}\n"]}