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.
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/build/main/crypto/CryptoService.d.ts +31 -0
- package/build/main/crypto/CryptoService.js +78 -0
- package/build/main/index.d.ts +11 -0
- package/build/main/index.js +24 -0
- package/build/main/mesh/DandelionMesh.d.ts +115 -0
- package/build/main/mesh/DandelionMesh.js +368 -0
- package/build/main/mesh/types.d.ts +58 -0
- package/build/main/mesh/types.js +3 -0
- package/build/main/raft/RaftNode.d.ts +87 -0
- package/build/main/raft/RaftNode.js +443 -0
- package/build/main/raft/log/InMemoryRaftLog.d.ts +20 -0
- package/build/main/raft/log/InMemoryRaftLog.js +55 -0
- package/build/main/raft/log/LocalStorageRaftLog.d.ts +9 -0
- package/build/main/raft/log/LocalStorageRaftLog.js +16 -0
- package/build/main/raft/log/RaftLog.d.ts +30 -0
- package/build/main/raft/log/RaftLog.js +3 -0
- package/build/main/raft/log/RaftLog.test-util.d.ts +3 -0
- package/build/main/raft/log/RaftLog.test-util.js +82 -0
- package/build/main/raft/log/SessionStorageRaftLog.d.ts +9 -0
- package/build/main/raft/log/SessionStorageRaftLog.js +38 -0
- package/build/main/raft/log/StorageRaftLog.d.ts +28 -0
- package/build/main/raft/log/StorageRaftLog.js +117 -0
- package/build/main/raft/log/StorageRaftLog.test-util.d.ts +3 -0
- package/build/main/raft/log/StorageRaftLog.test-util.js +54 -0
- package/build/main/raft/types.d.ts +53 -0
- package/build/main/raft/types.js +3 -0
- package/build/main/transport/PeerJSTransport.d.ts +46 -0
- package/build/main/transport/PeerJSTransport.js +139 -0
- package/build/main/transport/Transport.d.ts +38 -0
- package/build/main/transport/Transport.js +8 -0
- package/build/module/crypto/CryptoService.d.ts +31 -0
- package/build/module/crypto/CryptoService.js +69 -0
- package/build/module/index.d.ts +11 -0
- package/build/module/index.js +11 -0
- package/build/module/mesh/DandelionMesh.d.ts +115 -0
- package/build/module/mesh/DandelionMesh.js +364 -0
- package/build/module/mesh/types.d.ts +58 -0
- package/build/module/mesh/types.js +2 -0
- package/build/module/raft/RaftNode.d.ts +87 -0
- package/build/module/raft/RaftNode.js +440 -0
- package/build/module/raft/log/InMemoryRaftLog.d.ts +20 -0
- package/build/module/raft/log/InMemoryRaftLog.js +48 -0
- package/build/module/raft/log/LocalStorageRaftLog.d.ts +9 -0
- package/build/module/raft/log/LocalStorageRaftLog.js +12 -0
- package/build/module/raft/log/RaftLog.d.ts +30 -0
- package/build/module/raft/log/RaftLog.js +2 -0
- package/build/module/raft/log/RaftLog.test-util.d.ts +3 -0
- package/build/module/raft/log/RaftLog.test-util.js +78 -0
- package/build/module/raft/log/SessionStorageRaftLog.d.ts +9 -0
- package/build/module/raft/log/SessionStorageRaftLog.js +34 -0
- package/build/module/raft/log/StorageRaftLog.d.ts +28 -0
- package/build/module/raft/log/StorageRaftLog.js +114 -0
- package/build/module/raft/log/StorageRaftLog.test-util.d.ts +3 -0
- package/build/module/raft/log/StorageRaftLog.test-util.js +50 -0
- package/build/module/raft/types.d.ts +53 -0
- package/build/module/raft/types.js +2 -0
- package/build/module/transport/PeerJSTransport.d.ts +46 -0
- package/build/module/transport/PeerJSTransport.js +134 -0
- package/build/module/transport/Transport.d.ts +38 -0
- package/build/module/transport/Transport.js +7 -0
- 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,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
|
+
}
|