dandelion-mesh 1.0.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/build/main/crypto/CryptoService.d.ts +31 -0
  4. package/build/main/crypto/CryptoService.js +78 -0
  5. package/build/main/index.d.ts +11 -0
  6. package/build/main/index.js +24 -0
  7. package/build/main/mesh/DandelionMesh.d.ts +115 -0
  8. package/build/main/mesh/DandelionMesh.js +368 -0
  9. package/build/main/mesh/types.d.ts +58 -0
  10. package/build/main/mesh/types.js +3 -0
  11. package/build/main/raft/RaftNode.d.ts +87 -0
  12. package/build/main/raft/RaftNode.js +443 -0
  13. package/build/main/raft/log/InMemoryRaftLog.d.ts +20 -0
  14. package/build/main/raft/log/InMemoryRaftLog.js +55 -0
  15. package/build/main/raft/log/LocalStorageRaftLog.d.ts +9 -0
  16. package/build/main/raft/log/LocalStorageRaftLog.js +16 -0
  17. package/build/main/raft/log/RaftLog.d.ts +30 -0
  18. package/build/main/raft/log/RaftLog.js +3 -0
  19. package/build/main/raft/log/RaftLog.test-util.d.ts +3 -0
  20. package/build/main/raft/log/RaftLog.test-util.js +82 -0
  21. package/build/main/raft/log/SessionStorageRaftLog.d.ts +9 -0
  22. package/build/main/raft/log/SessionStorageRaftLog.js +38 -0
  23. package/build/main/raft/log/StorageRaftLog.d.ts +28 -0
  24. package/build/main/raft/log/StorageRaftLog.js +117 -0
  25. package/build/main/raft/log/StorageRaftLog.test-util.d.ts +3 -0
  26. package/build/main/raft/log/StorageRaftLog.test-util.js +54 -0
  27. package/build/main/raft/types.d.ts +53 -0
  28. package/build/main/raft/types.js +3 -0
  29. package/build/main/transport/PeerJSTransport.d.ts +46 -0
  30. package/build/main/transport/PeerJSTransport.js +139 -0
  31. package/build/main/transport/Transport.d.ts +38 -0
  32. package/build/main/transport/Transport.js +8 -0
  33. package/build/module/crypto/CryptoService.d.ts +31 -0
  34. package/build/module/crypto/CryptoService.js +69 -0
  35. package/build/module/index.d.ts +11 -0
  36. package/build/module/index.js +11 -0
  37. package/build/module/mesh/DandelionMesh.d.ts +115 -0
  38. package/build/module/mesh/DandelionMesh.js +364 -0
  39. package/build/module/mesh/types.d.ts +58 -0
  40. package/build/module/mesh/types.js +2 -0
  41. package/build/module/raft/RaftNode.d.ts +87 -0
  42. package/build/module/raft/RaftNode.js +440 -0
  43. package/build/module/raft/log/InMemoryRaftLog.d.ts +20 -0
  44. package/build/module/raft/log/InMemoryRaftLog.js +48 -0
  45. package/build/module/raft/log/LocalStorageRaftLog.d.ts +9 -0
  46. package/build/module/raft/log/LocalStorageRaftLog.js +12 -0
  47. package/build/module/raft/log/RaftLog.d.ts +30 -0
  48. package/build/module/raft/log/RaftLog.js +2 -0
  49. package/build/module/raft/log/RaftLog.test-util.d.ts +3 -0
  50. package/build/module/raft/log/RaftLog.test-util.js +78 -0
  51. package/build/module/raft/log/SessionStorageRaftLog.d.ts +9 -0
  52. package/build/module/raft/log/SessionStorageRaftLog.js +34 -0
  53. package/build/module/raft/log/StorageRaftLog.d.ts +28 -0
  54. package/build/module/raft/log/StorageRaftLog.js +114 -0
  55. package/build/module/raft/log/StorageRaftLog.test-util.d.ts +3 -0
  56. package/build/module/raft/log/StorageRaftLog.test-util.js +50 -0
  57. package/build/module/raft/types.d.ts +53 -0
  58. package/build/module/raft/types.js +2 -0
  59. package/build/module/transport/PeerJSTransport.d.ts +46 -0
  60. package/build/module/transport/PeerJSTransport.js +134 -0
  61. package/build/module/transport/Transport.d.ts +38 -0
  62. package/build/module/transport/Transport.js +7 -0
  63. package/package.json +119 -0
@@ -0,0 +1,58 @@
1
+ import { EncryptedPayload } from '../crypto/CryptoService';
2
+ export interface PublicMessage<T = unknown> {
3
+ type: 'public';
4
+ sender: string;
5
+ data: T;
6
+ }
7
+ export interface PrivateMessage<T = unknown> {
8
+ type: 'private';
9
+ sender: string;
10
+ recipient: string;
11
+ data: T;
12
+ }
13
+ export type MeshMessage<T = unknown> = PublicMessage<T> | PrivateMessage<T>;
14
+ /** Broadcast of a peer's RSA public key */
15
+ export interface PublicKeyAnnouncement {
16
+ _meshType: 'publicKey';
17
+ peerId: string;
18
+ jwk: JsonWebKey;
19
+ }
20
+ /** An encrypted private message (the Raft log entry payload for private messages) */
21
+ export interface EncryptedPrivateMessage {
22
+ _meshType: 'encrypted';
23
+ sender: string;
24
+ recipient: string;
25
+ payload: EncryptedPayload;
26
+ }
27
+ /** A public message log entry */
28
+ export interface PublicMessageEntry<T = unknown> {
29
+ _meshType: 'public';
30
+ sender: string;
31
+ data: T;
32
+ }
33
+ /** Union of all Raft log command types */
34
+ export type MeshLogCommand<T = unknown> = PublicMessageEntry<T> | EncryptedPrivateMessage | PublicKeyAnnouncement;
35
+ /** A proposal forwarded from a non-leader to the leader */
36
+ export interface ProposeMessage<T = unknown> {
37
+ _meshType: 'propose';
38
+ command: MeshLogCommand<T>;
39
+ }
40
+ /** Internal transport-level messages (not replicated via Raft) */
41
+ export type MeshControlMessage<T = unknown> = ProposeMessage<T>;
42
+ /** Wrapper to distinguish Raft messages from mesh control messages on the wire */
43
+ export interface WireMessage {
44
+ channel: 'raft' | 'control';
45
+ payload: unknown;
46
+ }
47
+ export interface DandelionMeshEvents<T = unknown> {
48
+ /** Local peer is ready */
49
+ ready: (localPeerId: string) => void;
50
+ /** A message was committed and delivered */
51
+ message: (message: MeshMessage<T>, replay: boolean) => void;
52
+ /** The set of connected peer IDs changed */
53
+ peersChanged: (peers: string[]) => void;
54
+ /** The cluster leader changed */
55
+ leaderChanged: (leaderId: string | null) => void;
56
+ /** An error occurred */
57
+ error: (error: Error) => void;
58
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvbWVzaC90eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIn0=
@@ -0,0 +1,87 @@
1
+ import { RaftLog } from './log/RaftLog';
2
+ import { RaftMessage, RaftNodeEvents, RaftRole } from './types';
3
+ export interface RaftNodeOptions {
4
+ /** Minimum election timeout in ms (default 2000) */
5
+ electionTimeoutMin?: number;
6
+ /** Maximum election timeout in ms (default 4000) */
7
+ electionTimeoutMax?: number;
8
+ /** Heartbeat interval in ms (default 400) */
9
+ heartbeatInterval?: number;
10
+ }
11
+ /**
12
+ * Core Raft consensus node.
13
+ *
14
+ * This class implements the Raft algorithm for leader election and log
15
+ * replication. It is transport-agnostic — the caller must wire up
16
+ * `sendMessage` to deliver RPCs, and call `handleMessage` when RPCs arrive.
17
+ *
18
+ * Log indices are 1-based per the Raft paper.
19
+ */
20
+ export declare class RaftNode<T = unknown> {
21
+ readonly id: string;
22
+ private role;
23
+ private currentTerm;
24
+ private votedFor;
25
+ private leaderId;
26
+ private commitIndex;
27
+ private lastApplied;
28
+ private nextIndex;
29
+ private matchIndex;
30
+ private electionTimer;
31
+ private heartbeatTimer;
32
+ private readonly electionTimeoutMin;
33
+ private readonly electionTimeoutMax;
34
+ private readonly heartbeatInterval;
35
+ private peers;
36
+ private votesReceived;
37
+ private readonly log;
38
+ /** Called by the node to send an RPC to a peer — must be wired up externally */
39
+ sendMessage: (toPeerId: string, message: RaftMessage<T>) => void;
40
+ private readonly eventListeners;
41
+ private destroyed;
42
+ constructor(id: string, log: RaftLog<T>, options?: RaftNodeOptions);
43
+ /** Start the node (begin election timer as follower) */
44
+ start(peerIds: string[]): void;
45
+ /** Update the cluster membership dynamically (e.g. when peers join/leave) */
46
+ updatePeers(peerIds: string[]): void;
47
+ /** Submit a command to be replicated. Only succeeds on the leader. */
48
+ propose(command: T): boolean;
49
+ /** Handle an incoming Raft RPC message from a peer */
50
+ handleMessage(fromPeerId: string, message: RaftMessage<T>): void;
51
+ /** Get current role */
52
+ getRole(): RaftRole;
53
+ /** Get current term */
54
+ getCurrentTerm(): number;
55
+ /** Get the current leader ID (null if unknown) */
56
+ getLeaderId(): string | null;
57
+ /** Get the commit index */
58
+ getCommitIndex(): number;
59
+ /** Check if this node is the leader */
60
+ isLeader(): boolean;
61
+ /** Register an event listener */
62
+ on<E extends keyof RaftNodeEvents<T>>(event: E, listener: RaftNodeEvents<T>[E]): void;
63
+ /** Remove an event listener */
64
+ off<E extends keyof RaftNodeEvents<T>>(event: E, listener: RaftNodeEvents<T>[E]): void;
65
+ /** Stop all timers and clean up */
66
+ destroy(): void;
67
+ private becomeFollower;
68
+ private becomeCandidate;
69
+ private becomeLeader;
70
+ private handleRequestVote;
71
+ private handleRequestVoteResult;
72
+ /** Check if a candidate's log is at least as up-to-date as ours */
73
+ private isLogUpToDate;
74
+ private handleAppendEntries;
75
+ private handleAppendEntriesResult;
76
+ private sendAppendEntriesToAll;
77
+ private sendAppendEntriesTo;
78
+ /** Advance commitIndex if a majority of matchIndex values allow it */
79
+ private advanceCommitIndex;
80
+ /** Apply all committed but not-yet-applied entries */
81
+ private applyCommitted;
82
+ private resetElectionTimer;
83
+ private clearElectionTimer;
84
+ private clearHeartbeatTimer;
85
+ private persistState;
86
+ private emit;
87
+ }
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Core Raft consensus node.
3
+ *
4
+ * This class implements the Raft algorithm for leader election and log
5
+ * replication. It is transport-agnostic — the caller must wire up
6
+ * `sendMessage` to deliver RPCs, and call `handleMessage` when RPCs arrive.
7
+ *
8
+ * Log indices are 1-based per the Raft paper.
9
+ */
10
+ export class RaftNode {
11
+ id;
12
+ role = 'follower';
13
+ currentTerm = 0;
14
+ votedFor = null;
15
+ leaderId = null;
16
+ // Volatile state (all servers)
17
+ commitIndex = 0;
18
+ lastApplied = 0;
19
+ // Volatile state (leader only)
20
+ nextIndex = new Map();
21
+ matchIndex = new Map();
22
+ // Election / heartbeat timers
23
+ electionTimer = null;
24
+ heartbeatTimer = null;
25
+ electionTimeoutMin;
26
+ electionTimeoutMax;
27
+ heartbeatInterval;
28
+ // Cluster membership (peer IDs excluding self)
29
+ peers = [];
30
+ // Votes received in current election
31
+ votesReceived = new Set();
32
+ log;
33
+ /** Called by the node to send an RPC to a peer — must be wired up externally */
34
+ sendMessage = () => { };
35
+ eventListeners = {
36
+ leaderChanged: new Set(),
37
+ committed: new Set(),
38
+ roleChanged: new Set(),
39
+ };
40
+ destroyed = false;
41
+ constructor(id, log, options) {
42
+ this.id = id;
43
+ this.log = log;
44
+ this.electionTimeoutMin = options?.electionTimeoutMin ?? 2000;
45
+ this.electionTimeoutMax = options?.electionTimeoutMax ?? 4000;
46
+ this.heartbeatInterval = options?.heartbeatInterval ?? 400;
47
+ // Restore persistent state
48
+ const state = this.log.loadState();
49
+ this.currentTerm = state.currentTerm;
50
+ this.votedFor = state.votedFor;
51
+ }
52
+ // --- Public API ---
53
+ /** Start the node (begin election timer as follower) */
54
+ start(peerIds) {
55
+ this.peers = peerIds.filter((p) => p !== this.id);
56
+ this.resetElectionTimer();
57
+ }
58
+ /** Update the cluster membership dynamically (e.g. when peers join/leave) */
59
+ updatePeers(peerIds) {
60
+ this.peers = peerIds.filter((p) => p !== this.id);
61
+ // Reinitialize leader volatile state for new peers
62
+ if (this.role === 'leader') {
63
+ for (const peer of this.peers) {
64
+ if (!this.nextIndex.has(peer)) {
65
+ this.nextIndex.set(peer, this.log.lastIndex() + 1);
66
+ this.matchIndex.set(peer, 0);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ /** Submit a command to be replicated. Only succeeds on the leader. */
72
+ propose(command) {
73
+ if (this.role !== 'leader') {
74
+ return false;
75
+ }
76
+ const entry = { term: this.currentTerm, command };
77
+ this.log.append([entry]);
78
+ this.persistState();
79
+ // Immediately replicate to followers
80
+ this.sendAppendEntriesToAll();
81
+ // Check if we can commit (single-node cluster)
82
+ this.advanceCommitIndex();
83
+ return true;
84
+ }
85
+ /** Handle an incoming Raft RPC message from a peer */
86
+ handleMessage(fromPeerId, message) {
87
+ if (this.destroyed)
88
+ return;
89
+ // If we see a higher term, step down
90
+ const msgTerm = 'term' in message ? message.term : 0;
91
+ if (msgTerm > this.currentTerm) {
92
+ this.currentTerm = msgTerm;
93
+ this.votedFor = null;
94
+ this.persistState();
95
+ this.becomeFollower();
96
+ }
97
+ switch (message.type) {
98
+ case 'RequestVote':
99
+ this.handleRequestVote(fromPeerId, message);
100
+ break;
101
+ case 'RequestVoteResult':
102
+ this.handleRequestVoteResult(fromPeerId, message);
103
+ break;
104
+ case 'AppendEntries':
105
+ this.handleAppendEntries(fromPeerId, message);
106
+ break;
107
+ case 'AppendEntriesResult':
108
+ this.handleAppendEntriesResult(message);
109
+ break;
110
+ }
111
+ }
112
+ /** Get current role */
113
+ getRole() {
114
+ return this.role;
115
+ }
116
+ /** Get current term */
117
+ getCurrentTerm() {
118
+ return this.currentTerm;
119
+ }
120
+ /** Get the current leader ID (null if unknown) */
121
+ getLeaderId() {
122
+ return this.leaderId;
123
+ }
124
+ /** Get the commit index */
125
+ getCommitIndex() {
126
+ return this.commitIndex;
127
+ }
128
+ /** Check if this node is the leader */
129
+ isLeader() {
130
+ return this.role === 'leader';
131
+ }
132
+ /** Register an event listener */
133
+ on(event, listener) {
134
+ this.eventListeners[event].add(listener);
135
+ }
136
+ /** Remove an event listener */
137
+ off(event, listener) {
138
+ this.eventListeners[event].delete(listener);
139
+ }
140
+ /** Stop all timers and clean up */
141
+ destroy() {
142
+ this.destroyed = true;
143
+ this.clearElectionTimer();
144
+ this.clearHeartbeatTimer();
145
+ }
146
+ // --- Election ---
147
+ becomeFollower() {
148
+ const wasLeader = this.role === 'leader';
149
+ this.role = 'follower';
150
+ this.clearHeartbeatTimer();
151
+ this.resetElectionTimer();
152
+ this.emit('roleChanged', 'follower');
153
+ if (wasLeader) {
154
+ // Leader identity is unknown until we hear from the new leader
155
+ }
156
+ }
157
+ becomeCandidate() {
158
+ this.role = 'candidate';
159
+ this.currentTerm++;
160
+ this.votedFor = this.id;
161
+ this.persistState();
162
+ this.votesReceived.clear();
163
+ this.votesReceived.add(this.id); // vote for self
164
+ this.leaderId = null;
165
+ this.emit('roleChanged', 'candidate');
166
+ this.emit('leaderChanged', null);
167
+ this.resetElectionTimer();
168
+ // Single-node cluster: become leader immediately
169
+ if (this.peers.length === 0) {
170
+ this.becomeLeader();
171
+ return;
172
+ }
173
+ // Send RequestVote to all peers
174
+ const request = {
175
+ type: 'RequestVote',
176
+ term: this.currentTerm,
177
+ candidateId: this.id,
178
+ lastLogIndex: this.log.lastIndex(),
179
+ lastLogTerm: this.log.lastTerm(),
180
+ };
181
+ for (const peer of this.peers) {
182
+ this.sendMessage(peer, request);
183
+ }
184
+ }
185
+ becomeLeader() {
186
+ this.role = 'leader';
187
+ this.leaderId = this.id;
188
+ this.clearElectionTimer();
189
+ this.emit('roleChanged', 'leader');
190
+ this.emit('leaderChanged', this.id);
191
+ // Initialize nextIndex and matchIndex for all peers
192
+ const lastLogIndex = this.log.lastIndex();
193
+ this.nextIndex.clear();
194
+ this.matchIndex.clear();
195
+ for (const peer of this.peers) {
196
+ this.nextIndex.set(peer, lastLogIndex + 1);
197
+ this.matchIndex.set(peer, 0);
198
+ }
199
+ // Send initial heartbeats
200
+ this.sendAppendEntriesToAll();
201
+ // Start heartbeat timer
202
+ this.clearHeartbeatTimer();
203
+ this.heartbeatTimer = setInterval(() => {
204
+ if (this.role === 'leader' && !this.destroyed) {
205
+ this.sendAppendEntriesToAll();
206
+ }
207
+ }, this.heartbeatInterval);
208
+ }
209
+ // --- RequestVote handling ---
210
+ handleRequestVote(_fromPeerId, args) {
211
+ const reply = {
212
+ type: 'RequestVoteResult',
213
+ term: this.currentTerm,
214
+ voteGranted: false,
215
+ };
216
+ // Reject if candidate's term is behind
217
+ if (args.term < this.currentTerm) {
218
+ this.sendMessage(args.candidateId, reply);
219
+ return;
220
+ }
221
+ // Grant vote if we haven't voted for someone else, and candidate's log is up-to-date
222
+ const canVote = this.votedFor === null || this.votedFor === args.candidateId;
223
+ const candidateLogUpToDate = this.isLogUpToDate(args.lastLogTerm, args.lastLogIndex);
224
+ if (canVote && candidateLogUpToDate) {
225
+ this.votedFor = args.candidateId;
226
+ this.persistState();
227
+ reply.voteGranted = true;
228
+ this.resetElectionTimer(); // reset timer on granting vote
229
+ }
230
+ this.sendMessage(args.candidateId, reply);
231
+ }
232
+ handleRequestVoteResult(fromPeerId, result) {
233
+ if (this.role !== 'candidate')
234
+ return;
235
+ if (result.term !== this.currentTerm)
236
+ return;
237
+ if (result.voteGranted) {
238
+ this.votesReceived.add(fromPeerId);
239
+ // Check majority (votesReceived includes self)
240
+ const totalCluster = this.peers.length + 1;
241
+ if (this.votesReceived.size >= Math.floor(totalCluster / 2) + 1) {
242
+ this.becomeLeader();
243
+ }
244
+ }
245
+ }
246
+ /** Check if a candidate's log is at least as up-to-date as ours */
247
+ isLogUpToDate(candidateLastTerm, candidateLastIndex) {
248
+ const myLastTerm = this.log.lastTerm();
249
+ if (candidateLastTerm !== myLastTerm) {
250
+ return candidateLastTerm > myLastTerm;
251
+ }
252
+ return candidateLastIndex >= this.log.lastIndex();
253
+ }
254
+ // --- AppendEntries handling ---
255
+ handleAppendEntries(_fromPeerId, args) {
256
+ const reply = {
257
+ type: 'AppendEntriesResult',
258
+ term: this.currentTerm,
259
+ success: false,
260
+ responderId: this.id,
261
+ matchIndex: 0,
262
+ };
263
+ // Reject if leader's term is behind
264
+ if (args.term < this.currentTerm) {
265
+ this.sendMessage(args.leaderId, reply);
266
+ return;
267
+ }
268
+ // Valid leader — become follower if not already
269
+ if (this.role !== 'follower') {
270
+ this.becomeFollower();
271
+ }
272
+ else {
273
+ this.resetElectionTimer();
274
+ }
275
+ if (this.leaderId !== args.leaderId) {
276
+ this.leaderId = args.leaderId;
277
+ this.emit('leaderChanged', args.leaderId);
278
+ }
279
+ // Check log consistency
280
+ if (args.prevLogIndex > 0) {
281
+ const prevTerm = this.log.getTerm(args.prevLogIndex);
282
+ if (prevTerm === 0 && args.prevLogIndex > this.log.lastIndex()) {
283
+ // We don't have an entry at prevLogIndex
284
+ this.resetElectionTimer();
285
+ this.sendMessage(args.leaderId, reply);
286
+ return;
287
+ }
288
+ if (prevTerm !== args.prevLogTerm) {
289
+ // Conflict: delete this entry and everything after
290
+ this.log.truncateFrom(args.prevLogIndex);
291
+ this.resetElectionTimer();
292
+ this.sendMessage(args.leaderId, reply);
293
+ return;
294
+ }
295
+ }
296
+ // Append new entries (handle conflicts and skip duplicates)
297
+ if (args.entries.length > 0) {
298
+ for (let i = 0; i < args.entries.length; i++) {
299
+ const entryIndex = args.prevLogIndex + 1 + i;
300
+ const existing = this.log.getEntry(entryIndex);
301
+ if (existing) {
302
+ if (existing.term !== args.entries[i].term) {
303
+ // Conflict: truncate from here and append the rest
304
+ this.log.truncateFrom(entryIndex);
305
+ this.log.append(args.entries.slice(i));
306
+ break;
307
+ }
308
+ // Same term — entry already present, skip
309
+ }
310
+ else {
311
+ // No existing entry — append remaining
312
+ this.log.append(args.entries.slice(i));
313
+ break;
314
+ }
315
+ }
316
+ }
317
+ // Update commit index
318
+ if (args.leaderCommit > this.commitIndex) {
319
+ this.commitIndex = Math.min(args.leaderCommit, this.log.lastIndex());
320
+ this.applyCommitted();
321
+ }
322
+ reply.success = true;
323
+ reply.matchIndex = args.prevLogIndex + args.entries.length;
324
+ this.resetElectionTimer();
325
+ this.sendMessage(args.leaderId, reply);
326
+ }
327
+ handleAppendEntriesResult(result) {
328
+ if (this.role !== 'leader')
329
+ return;
330
+ if (result.term !== this.currentTerm)
331
+ return;
332
+ const peer = result.responderId;
333
+ if (result.success) {
334
+ // Update nextIndex and matchIndex
335
+ if (result.matchIndex > 0) {
336
+ this.nextIndex.set(peer, result.matchIndex + 1);
337
+ this.matchIndex.set(peer, result.matchIndex);
338
+ }
339
+ this.advanceCommitIndex();
340
+ }
341
+ else {
342
+ // Decrement nextIndex and retry
343
+ const ni = this.nextIndex.get(peer) ?? 1;
344
+ this.nextIndex.set(peer, Math.max(1, ni - 1));
345
+ this.sendAppendEntriesTo(peer);
346
+ }
347
+ }
348
+ // --- Log replication ---
349
+ sendAppendEntriesToAll() {
350
+ for (const peer of this.peers) {
351
+ this.sendAppendEntriesTo(peer);
352
+ }
353
+ }
354
+ sendAppendEntriesTo(peer) {
355
+ const ni = this.nextIndex.get(peer) ?? this.log.lastIndex() + 1;
356
+ const prevLogIndex = ni - 1;
357
+ const prevLogTerm = this.log.getTerm(prevLogIndex);
358
+ const lastIndex = this.log.lastIndex();
359
+ const entries = ni <= lastIndex ? this.log.getEntries(ni, lastIndex) : [];
360
+ const args = {
361
+ type: 'AppendEntries',
362
+ term: this.currentTerm,
363
+ leaderId: this.id,
364
+ prevLogIndex,
365
+ prevLogTerm,
366
+ entries,
367
+ leaderCommit: this.commitIndex,
368
+ };
369
+ this.sendMessage(peer, args);
370
+ }
371
+ /** Advance commitIndex if a majority of matchIndex values allow it */
372
+ advanceCommitIndex() {
373
+ const lastIndex = this.log.lastIndex();
374
+ for (let n = lastIndex; n > this.commitIndex; n--) {
375
+ // Only commit entries from the current term
376
+ if (this.log.getTerm(n) !== this.currentTerm)
377
+ continue;
378
+ // Count replicas (including self)
379
+ let replicaCount = 1; // self
380
+ for (const peer of this.peers) {
381
+ if ((this.matchIndex.get(peer) ?? 0) >= n) {
382
+ replicaCount++;
383
+ }
384
+ }
385
+ const majority = Math.floor((this.peers.length + 1) / 2) + 1;
386
+ if (replicaCount >= majority) {
387
+ this.commitIndex = n;
388
+ this.applyCommitted();
389
+ break;
390
+ }
391
+ }
392
+ }
393
+ /** Apply all committed but not-yet-applied entries */
394
+ applyCommitted() {
395
+ while (this.lastApplied < this.commitIndex) {
396
+ this.lastApplied++;
397
+ const entry = this.log.getEntry(this.lastApplied);
398
+ if (entry) {
399
+ this.emit('committed', entry, this.lastApplied);
400
+ }
401
+ }
402
+ }
403
+ // --- Timer management ---
404
+ resetElectionTimer() {
405
+ this.clearElectionTimer();
406
+ const timeout = this.electionTimeoutMin +
407
+ Math.random() * (this.electionTimeoutMax - this.electionTimeoutMin);
408
+ this.electionTimer = setTimeout(() => {
409
+ if (!this.destroyed) {
410
+ this.becomeCandidate();
411
+ }
412
+ }, timeout);
413
+ }
414
+ clearElectionTimer() {
415
+ if (this.electionTimer !== null) {
416
+ clearTimeout(this.electionTimer);
417
+ this.electionTimer = null;
418
+ }
419
+ }
420
+ clearHeartbeatTimer() {
421
+ if (this.heartbeatTimer !== null) {
422
+ clearInterval(this.heartbeatTimer);
423
+ this.heartbeatTimer = null;
424
+ }
425
+ }
426
+ // --- Persistence ---
427
+ persistState() {
428
+ this.log.saveState({
429
+ currentTerm: this.currentTerm,
430
+ votedFor: this.votedFor,
431
+ });
432
+ }
433
+ // --- Event emission ---
434
+ emit(event, ...args) {
435
+ for (const listener of this.eventListeners[event]) {
436
+ listener(...args);
437
+ }
438
+ }
439
+ }
440
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUmFmdE5vZGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvcmFmdC9SYWZ0Tm9kZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUF5QkE7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFNLE9BQU8sUUFBUTtJQUNWLEVBQUUsQ0FBUztJQUVaLElBQUksR0FBYSxVQUFVLENBQUM7SUFDNUIsV0FBVyxHQUFHLENBQUMsQ0FBQztJQUNoQixRQUFRLEdBQWtCLElBQUksQ0FBQztJQUMvQixRQUFRLEdBQWtCLElBQUksQ0FBQztJQUV2QywrQkFBK0I7SUFDdkIsV0FBVyxHQUFHLENBQUMsQ0FBQztJQUNoQixXQUFXLEdBQUcsQ0FBQyxDQUFDO0lBRXhCLCtCQUErQjtJQUN2QixTQUFTLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUM7SUFDdEMsVUFBVSxHQUFHLElBQUksR0FBRyxFQUFrQixDQUFDO0lBRS9DLDhCQUE4QjtJQUN0QixhQUFhLEdBQXlDLElBQUksQ0FBQztJQUMzRCxjQUFjLEdBQTBDLElBQUksQ0FBQztJQUVwRCxrQkFBa0IsQ0FBUztJQUMzQixrQkFBa0IsQ0FBUztJQUMzQixpQkFBaUIsQ0FBUztJQUUzQywrQ0FBK0M7SUFDdkMsS0FBSyxHQUFhLEVBQUUsQ0FBQztJQUU3QixxQ0FBcUM7SUFDN0IsYUFBYSxHQUFHLElBQUksR0FBRyxFQUFVLENBQUM7SUFFekIsR0FBRyxDQUFhO0lBRWpDLGdGQUFnRjtJQUN6RSxXQUFXLEdBQ2hCLEdBQUcsRUFBRSxHQUFFLENBQUMsQ0FBQztJQUVNLGNBQWMsR0FBd0I7UUFDckQsYUFBYSxFQUFFLElBQUksR0FBRyxFQUFFO1FBQ3hCLFNBQVMsRUFBRSxJQUFJLEdBQUcsRUFBRTtRQUNwQixXQUFXLEVBQUUsSUFBSSxHQUFHLEVBQUU7S0FDdkIsQ0FBQztJQUVNLFNBQVMsR0FBRyxLQUFLLENBQUM7SUFFMUIsWUFBWSxFQUFVLEVBQUUsR0FBZSxFQUFFLE9BQXlCO1FBQ2hFLElBQUks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
@@ -0,0 +1,20 @@
1
+ import { LogEntry, RaftPersistentState } from '../types';
2
+ import { RaftLog } from './RaftLog';
3
+ /**
4
+ * In-memory Raft log implementation.
5
+ * All state is lost on page refresh — suitable for ephemeral sessions.
6
+ */
7
+ export declare class InMemoryRaftLog<T = unknown> implements RaftLog<T> {
8
+ private entries;
9
+ private state;
10
+ loadState(): RaftPersistentState;
11
+ saveState(state: RaftPersistentState): void;
12
+ length(): number;
13
+ getEntry(index: number): LogEntry<T> | undefined;
14
+ getTerm(index: number): number;
15
+ append(entries: LogEntry<T>[]): void;
16
+ truncateFrom(index: number): void;
17
+ getEntries(startIndex: number, endIndex: number): LogEntry<T>[];
18
+ lastIndex(): number;
19
+ lastTerm(): number;
20
+ }