@claude-flow/swarm 3.0.0-alpha.7 → 3.0.0-alpha.9
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/dist/agent-pool.d.ts.map +1 -1
- package/dist/agent-pool.js +10 -2
- package/dist/agent-pool.js.map +1 -1
- package/dist/attention-coordinator.d.ts +5 -4
- package/dist/attention-coordinator.d.ts.map +1 -1
- package/dist/attention-coordinator.js +17 -10
- package/dist/attention-coordinator.js.map +1 -1
- package/dist/consensus/byzantine.d.ts +2 -1
- package/dist/consensus/byzantine.d.ts.map +1 -1
- package/dist/consensus/byzantine.js +34 -8
- package/dist/consensus/byzantine.js.map +1 -1
- package/dist/consensus/gossip.d.ts +2 -1
- package/dist/consensus/gossip.d.ts.map +1 -1
- package/dist/consensus/gossip.js +27 -8
- package/dist/consensus/gossip.js.map +1 -1
- package/dist/consensus/index.d.ts +1 -1
- package/dist/consensus/index.d.ts.map +1 -1
- package/dist/consensus/index.js +2 -2
- package/dist/consensus/index.js.map +1 -1
- package/dist/consensus/raft.d.ts +2 -1
- package/dist/consensus/raft.d.ts.map +1 -1
- package/dist/consensus/raft.js +25 -9
- package/dist/consensus/raft.js.map +1 -1
- package/dist/coordination/agent-registry.d.ts +2 -2
- package/dist/coordination/agent-registry.d.ts.map +1 -1
- package/dist/coordination/agent-registry.js +1 -1
- package/dist/coordination/agent-registry.js.map +1 -1
- package/dist/coordination/swarm-hub.d.ts +5 -5
- package/dist/coordination/swarm-hub.d.ts.map +1 -1
- package/dist/coordination/swarm-hub.js +5 -5
- package/dist/coordination/swarm-hub.js.map +1 -1
- package/dist/coordination/task-orchestrator.d.ts +3 -3
- package/dist/coordination/task-orchestrator.d.ts.map +1 -1
- package/dist/coordination/task-orchestrator.js +1 -1
- package/dist/coordination/task-orchestrator.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/message-bus.d.ts.map +1 -1
- package/dist/message-bus.js +13 -4
- package/dist/message-bus.js.map +1 -1
- package/dist/queen-coordinator.d.ts +1 -1
- package/dist/queen-coordinator.d.ts.map +1 -1
- package/dist/queen-coordinator.js +10 -3
- package/dist/queen-coordinator.js.map +1 -1
- package/dist/topology-manager.d.ts.map +1 -1
- package/dist/topology-manager.js +26 -1
- package/dist/topology-manager.js.map +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/unified-coordinator.d.ts +1 -1
- package/dist/unified-coordinator.d.ts.map +1 -1
- package/dist/unified-coordinator.js +85 -29
- package/dist/unified-coordinator.js.map +1 -1
- package/package.json +7 -2
- package/.claude-flow/data/pending-insights.jsonl +0 -12
- package/__tests__/byzantine-transport.test.ts +0 -147
- package/__tests__/consensus-failure-injection.test.ts +0 -161
- package/__tests__/consensus-transport.test.ts +0 -200
- package/__tests__/consensus.test.ts +0 -577
- package/__tests__/coordinator.test.ts +0 -501
- package/__tests__/federation-transport.test.ts +0 -145
- package/__tests__/gossip-transport.test.ts +0 -81
- package/__tests__/queen-coordinator.test.ts +0 -1335
- package/__tests__/raft-transport.test.ts +0 -121
- package/__tests__/topology.test.ts +0 -621
- package/ruvector.db +0 -0
- package/src/agent-pool.ts +0 -476
- package/src/application/commands/create-task.command.ts +0 -124
- package/src/application/commands/spawn-agent.command.ts +0 -122
- package/src/application/index.ts +0 -30
- package/src/application/services/swarm-application-service.ts +0 -200
- package/src/attention-coordinator.ts +0 -1000
- package/src/consensus/byzantine.ts +0 -514
- package/src/consensus/federation-transport.ts +0 -185
- package/src/consensus/gossip.ts +0 -599
- package/src/consensus/index.ts +0 -296
- package/src/consensus/raft.ts +0 -561
- package/src/consensus/transport.ts +0 -284
- package/src/coordination/agent-registry.ts +0 -544
- package/src/coordination/index.ts +0 -23
- package/src/coordination/swarm-hub.ts +0 -776
- package/src/coordination/task-orchestrator.ts +0 -605
- package/src/domain/entities/agent.d.ts +0 -151
- package/src/domain/entities/agent.d.ts.map +0 -1
- package/src/domain/entities/agent.js +0 -280
- package/src/domain/entities/agent.js.map +0 -1
- package/src/domain/entities/agent.ts +0 -370
- package/src/domain/entities/task.d.ts +0 -133
- package/src/domain/entities/task.d.ts.map +0 -1
- package/src/domain/entities/task.js +0 -261
- package/src/domain/entities/task.js.map +0 -1
- package/src/domain/entities/task.ts +0 -319
- package/src/domain/index.ts +0 -41
- package/src/domain/repositories/agent-repository.interface.d.ts +0 -57
- package/src/domain/repositories/agent-repository.interface.d.ts.map +0 -1
- package/src/domain/repositories/agent-repository.interface.js +0 -9
- package/src/domain/repositories/agent-repository.interface.js.map +0 -1
- package/src/domain/repositories/agent-repository.interface.ts +0 -69
- package/src/domain/repositories/task-repository.interface.d.ts +0 -61
- package/src/domain/repositories/task-repository.interface.d.ts.map +0 -1
- package/src/domain/repositories/task-repository.interface.js +0 -9
- package/src/domain/repositories/task-repository.interface.js.map +0 -1
- package/src/domain/repositories/task-repository.interface.ts +0 -75
- package/src/domain/services/coordination-service.ts +0 -320
- package/src/federation-hub.d.ts +0 -284
- package/src/federation-hub.d.ts.map +0 -1
- package/src/federation-hub.js +0 -692
- package/src/federation-hub.js.map +0 -1
- package/src/federation-hub.ts +0 -979
- package/src/index.ts +0 -348
- package/src/message-bus.ts +0 -607
- package/src/queen-coordinator.ts +0 -2025
- package/src/shared/events.ts +0 -285
- package/src/shared/types.ts +0 -389
- package/src/topology-manager.ts +0 -656
- package/src/types.ts +0 -545
- package/src/unified-coordinator.ts +0 -1844
- package/src/workers/index.ts +0 -65
- package/src/workers/worker-dispatch.d.ts +0 -234
- package/src/workers/worker-dispatch.d.ts.map +0 -1
- package/src/workers/worker-dispatch.js +0 -842
- package/src/workers/worker-dispatch.js.map +0 -1
- package/src/workers/worker-dispatch.ts +0 -1076
- package/tmp.json +0 -0
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -20
package/src/consensus/raft.ts
DELETED
|
@@ -1,561 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* V3 Raft Consensus Implementation
|
|
3
|
-
* Leader election and log replication for distributed coordination
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { EventEmitter } from 'events';
|
|
7
|
-
import type { ConsensusTransport, ConsensusMessage } from './transport.js';
|
|
8
|
-
import {
|
|
9
|
-
ConsensusProposal,
|
|
10
|
-
ConsensusVote,
|
|
11
|
-
ConsensusResult,
|
|
12
|
-
ConsensusConfig,
|
|
13
|
-
SWARM_CONSTANTS,
|
|
14
|
-
} from '../types.js';
|
|
15
|
-
|
|
16
|
-
export type RaftState = 'follower' | 'candidate' | 'leader';
|
|
17
|
-
|
|
18
|
-
export interface RaftNode {
|
|
19
|
-
id: string;
|
|
20
|
-
state: RaftState;
|
|
21
|
-
currentTerm: number;
|
|
22
|
-
votedFor?: string;
|
|
23
|
-
log: RaftLogEntry[];
|
|
24
|
-
commitIndex: number;
|
|
25
|
-
lastApplied: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface RaftLogEntry {
|
|
29
|
-
term: number;
|
|
30
|
-
index: number;
|
|
31
|
-
command: unknown;
|
|
32
|
-
timestamp: Date;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface RaftConfig extends Partial<ConsensusConfig> {
|
|
36
|
-
electionTimeoutMinMs?: number;
|
|
37
|
-
electionTimeoutMaxMs?: number;
|
|
38
|
-
heartbeatIntervalMs?: number;
|
|
39
|
-
/**
|
|
40
|
-
* ADR-095 G2 — optional pluggable transport. When set, RequestVote and
|
|
41
|
-
* AppendEntries RPCs go over it (request-response) and this node also
|
|
42
|
-
* answers inbound RequestVote / AppendEntries from peers using proper
|
|
43
|
-
* Raft receiver rules (term comparison, vote-once-per-term, log-up-to-date
|
|
44
|
-
* check). When unset, behavior is unchanged: the legacy in-process path
|
|
45
|
-
* mutates the local `peers` map directly (single-process).
|
|
46
|
-
*/
|
|
47
|
-
transport?: ConsensusTransport;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class RaftConsensus extends EventEmitter {
|
|
51
|
-
private config: RaftConfig;
|
|
52
|
-
private node: RaftNode;
|
|
53
|
-
private peers: Map<string, RaftNode> = new Map();
|
|
54
|
-
private proposals: Map<string, ConsensusProposal> = new Map();
|
|
55
|
-
private electionTimeout?: NodeJS.Timeout;
|
|
56
|
-
private heartbeatInterval?: NodeJS.Timeout;
|
|
57
|
-
private proposalCounter: number = 0;
|
|
58
|
-
private readonly transport?: ConsensusTransport;
|
|
59
|
-
|
|
60
|
-
constructor(nodeId: string, config: RaftConfig = {}) {
|
|
61
|
-
super();
|
|
62
|
-
this.config = {
|
|
63
|
-
threshold: config.threshold ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_THRESHOLD,
|
|
64
|
-
timeoutMs: config.timeoutMs ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_TIMEOUT_MS,
|
|
65
|
-
maxRounds: config.maxRounds ?? 10,
|
|
66
|
-
requireQuorum: config.requireQuorum ?? true,
|
|
67
|
-
electionTimeoutMinMs: config.electionTimeoutMinMs ?? 150,
|
|
68
|
-
electionTimeoutMaxMs: config.electionTimeoutMaxMs ?? 300,
|
|
69
|
-
heartbeatIntervalMs: config.heartbeatIntervalMs ?? 50,
|
|
70
|
-
transport: config.transport,
|
|
71
|
-
};
|
|
72
|
-
this.transport = config.transport;
|
|
73
|
-
|
|
74
|
-
this.node = {
|
|
75
|
-
id: nodeId,
|
|
76
|
-
state: 'follower',
|
|
77
|
-
currentTerm: 0,
|
|
78
|
-
log: [],
|
|
79
|
-
commitIndex: 0,
|
|
80
|
-
lastApplied: 0,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
if (this.transport) {
|
|
84
|
-
this.transport.onMessage(async (msg: ConsensusMessage) => this.handleInboundRaftMessage(msg));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Index/term of the last entry in this node's log (Raft "up-to-date" comparison). */
|
|
89
|
-
private lastLogInfo(): { index: number; term: number } {
|
|
90
|
-
const last = this.node.log[this.node.log.length - 1];
|
|
91
|
-
return last ? { index: last.index, term: last.term } : { index: 0, term: 0 };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* ADR-095 G2 — Raft receiver. Answers inbound RequestVote / AppendEntries
|
|
96
|
-
* with proper Raft rules. Returns the RPC response the transport relays
|
|
97
|
-
* back to the caller (the transport's `send` resolves with it).
|
|
98
|
-
*/
|
|
99
|
-
private async handleInboundRaftMessage(msg: ConsensusMessage): Promise<ConsensusMessage | void> {
|
|
100
|
-
const p = (msg.payload ?? {}) as Record<string, unknown>;
|
|
101
|
-
const term = typeof p.term === 'number' ? p.term : 0;
|
|
102
|
-
|
|
103
|
-
// §5.1 — any RPC with a higher term makes us a follower and adopts it.
|
|
104
|
-
if (term > this.node.currentTerm) {
|
|
105
|
-
this.node.currentTerm = term;
|
|
106
|
-
this.node.votedFor = undefined;
|
|
107
|
-
this.node.state = 'follower';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (msg.type === 'request-vote') {
|
|
111
|
-
const candidateId = String(p.candidateId ?? msg.from);
|
|
112
|
-
const candLastIndex = typeof p.lastLogIndex === 'number' ? p.lastLogIndex : 0;
|
|
113
|
-
const candLastTerm = typeof p.lastLogTerm === 'number' ? p.lastLogTerm : 0;
|
|
114
|
-
const my = this.lastLogInfo();
|
|
115
|
-
const logOk = candLastTerm > my.term || (candLastTerm === my.term && candLastIndex >= my.index);
|
|
116
|
-
const termOk = term >= this.node.currentTerm;
|
|
117
|
-
const notVotedOrSame = this.node.votedFor === undefined || this.node.votedFor === candidateId;
|
|
118
|
-
const granted = termOk && notVotedOrSame && logOk;
|
|
119
|
-
if (granted) {
|
|
120
|
-
this.node.votedFor = candidateId;
|
|
121
|
-
this.resetElectionTimeout();
|
|
122
|
-
}
|
|
123
|
-
return { type: 'vote-response', from: this.node.id, to: msg.from, payload: { term: this.node.currentTerm, granted }, term: this.node.currentTerm };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (msg.type === 'append-entries') {
|
|
127
|
-
// §5.2/5.3 — reject if leader's term is stale.
|
|
128
|
-
if (term < this.node.currentTerm) {
|
|
129
|
-
return { type: 'append-entries-response', from: this.node.id, to: msg.from, payload: { term: this.node.currentTerm, success: false }, term: this.node.currentTerm };
|
|
130
|
-
}
|
|
131
|
-
// Valid leader heartbeat/append → become/stay follower, reset election timer.
|
|
132
|
-
this.node.state = 'follower';
|
|
133
|
-
this.resetElectionTimeout();
|
|
134
|
-
const entries = Array.isArray(p.entries) ? (p.entries as RaftLogEntry[]) : [];
|
|
135
|
-
// (Simplified log matching: append entries not already present by index.
|
|
136
|
-
// Full prevLogIndex/prevLogTerm conflict resolution is the next refinement.)
|
|
137
|
-
for (const e of entries) {
|
|
138
|
-
if (!this.node.log.some(x => x.index === e.index)) this.node.log.push(e);
|
|
139
|
-
}
|
|
140
|
-
const leaderCommit = typeof p.leaderCommit === 'number' ? p.leaderCommit : this.node.commitIndex;
|
|
141
|
-
if (leaderCommit > this.node.commitIndex) {
|
|
142
|
-
this.node.commitIndex = Math.min(leaderCommit, this.lastLogInfo().index);
|
|
143
|
-
}
|
|
144
|
-
return { type: 'append-entries-response', from: this.node.id, to: msg.from, payload: { term: this.node.currentTerm, success: true }, term: this.node.currentTerm };
|
|
145
|
-
}
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async initialize(): Promise<void> {
|
|
150
|
-
this.resetElectionTimeout();
|
|
151
|
-
this.emit('initialized', { nodeId: this.node.id });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async shutdown(): Promise<void> {
|
|
155
|
-
if (this.electionTimeout) {
|
|
156
|
-
clearTimeout(this.electionTimeout);
|
|
157
|
-
}
|
|
158
|
-
if (this.heartbeatInterval) {
|
|
159
|
-
clearInterval(this.heartbeatInterval);
|
|
160
|
-
}
|
|
161
|
-
this.emit('shutdown');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
addPeer(peerId: string): void {
|
|
165
|
-
this.peers.set(peerId, {
|
|
166
|
-
id: peerId,
|
|
167
|
-
state: 'follower',
|
|
168
|
-
currentTerm: 0,
|
|
169
|
-
log: [],
|
|
170
|
-
commitIndex: 0,
|
|
171
|
-
lastApplied: 0,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
removePeer(peerId: string): void {
|
|
176
|
-
this.peers.delete(peerId);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async propose(value: unknown): Promise<ConsensusProposal> {
|
|
180
|
-
if (this.node.state !== 'leader') {
|
|
181
|
-
throw new Error('Only leader can propose values');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
this.proposalCounter++;
|
|
185
|
-
const proposalId = `raft_${this.node.id}_${this.proposalCounter}`;
|
|
186
|
-
|
|
187
|
-
const proposal: ConsensusProposal = {
|
|
188
|
-
id: proposalId,
|
|
189
|
-
proposerId: this.node.id,
|
|
190
|
-
value,
|
|
191
|
-
term: this.node.currentTerm,
|
|
192
|
-
timestamp: new Date(),
|
|
193
|
-
votes: new Map(),
|
|
194
|
-
status: 'pending',
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
// Add to local log
|
|
198
|
-
const logEntry: RaftLogEntry = {
|
|
199
|
-
term: this.node.currentTerm,
|
|
200
|
-
index: this.node.log.length + 1,
|
|
201
|
-
command: { proposalId, value },
|
|
202
|
-
timestamp: new Date(),
|
|
203
|
-
};
|
|
204
|
-
this.node.log.push(logEntry);
|
|
205
|
-
|
|
206
|
-
this.proposals.set(proposalId, proposal);
|
|
207
|
-
|
|
208
|
-
// Leader votes for itself
|
|
209
|
-
proposal.votes.set(this.node.id, {
|
|
210
|
-
voterId: this.node.id,
|
|
211
|
-
approve: true,
|
|
212
|
-
confidence: 1.0,
|
|
213
|
-
timestamp: new Date(),
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Replicate to followers
|
|
217
|
-
await this.replicateToFollowers(logEntry);
|
|
218
|
-
|
|
219
|
-
return proposal;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async vote(proposalId: string, vote: ConsensusVote): Promise<void> {
|
|
223
|
-
const proposal = this.proposals.get(proposalId);
|
|
224
|
-
if (!proposal) {
|
|
225
|
-
throw new Error(`Proposal ${proposalId} not found`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (proposal.status !== 'pending') {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
proposal.votes.set(vote.voterId, vote);
|
|
233
|
-
|
|
234
|
-
// Check if we have consensus
|
|
235
|
-
await this.checkConsensus(proposalId);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async awaitConsensus(proposalId: string): Promise<ConsensusResult> {
|
|
239
|
-
const startTime = Date.now();
|
|
240
|
-
|
|
241
|
-
return new Promise((resolve, reject) => {
|
|
242
|
-
const checkInterval = setInterval(() => {
|
|
243
|
-
const proposal = this.proposals.get(proposalId);
|
|
244
|
-
if (!proposal) {
|
|
245
|
-
clearInterval(checkInterval);
|
|
246
|
-
reject(new Error(`Proposal ${proposalId} not found`));
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (proposal.status !== 'pending') {
|
|
251
|
-
clearInterval(checkInterval);
|
|
252
|
-
resolve(this.createResult(proposal, Date.now() - startTime));
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (Date.now() - startTime > (this.config.timeoutMs ?? 30000)) {
|
|
257
|
-
clearInterval(checkInterval);
|
|
258
|
-
proposal.status = 'expired';
|
|
259
|
-
resolve(this.createResult(proposal, Date.now() - startTime));
|
|
260
|
-
}
|
|
261
|
-
}, 10);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
getState(): RaftState {
|
|
266
|
-
return this.node.state;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
getTerm(): number {
|
|
270
|
-
return this.node.currentTerm;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
isLeader(): boolean {
|
|
274
|
-
return this.node.state === 'leader';
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
getLeaderId(): string | undefined {
|
|
278
|
-
if (this.node.state === 'leader') {
|
|
279
|
-
return this.node.id;
|
|
280
|
-
}
|
|
281
|
-
return this.node.votedFor;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ===== PRIVATE METHODS =====
|
|
285
|
-
|
|
286
|
-
private resetElectionTimeout(): void {
|
|
287
|
-
if (this.electionTimeout) {
|
|
288
|
-
clearTimeout(this.electionTimeout);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const timeout = this.randomElectionTimeout();
|
|
292
|
-
this.electionTimeout = setTimeout(() => {
|
|
293
|
-
this.startElection();
|
|
294
|
-
}, timeout);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
private randomElectionTimeout(): number {
|
|
298
|
-
const min = this.config.electionTimeoutMinMs ?? 150;
|
|
299
|
-
const max = this.config.electionTimeoutMaxMs ?? 300;
|
|
300
|
-
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
private async startElection(): Promise<void> {
|
|
304
|
-
this.node.state = 'candidate';
|
|
305
|
-
this.node.currentTerm++;
|
|
306
|
-
this.node.votedFor = this.node.id;
|
|
307
|
-
|
|
308
|
-
this.emit('election.started', {
|
|
309
|
-
term: this.node.currentTerm,
|
|
310
|
-
candidateId: this.node.id
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Vote for self
|
|
314
|
-
let votesReceived = 1;
|
|
315
|
-
const votesNeeded = Math.floor((this.peers.size + 1) / 2) + 1;
|
|
316
|
-
|
|
317
|
-
// Request votes from peers
|
|
318
|
-
for (const [peerId, peer] of this.peers) {
|
|
319
|
-
const granted = await this.requestVote(peerId);
|
|
320
|
-
if (granted) {
|
|
321
|
-
votesReceived++;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (votesReceived >= votesNeeded) {
|
|
325
|
-
this.becomeLeader();
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Election failed, reset to follower
|
|
331
|
-
this.node.state = 'follower';
|
|
332
|
-
this.resetElectionTimeout();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
private async requestVote(peerId: string): Promise<boolean> {
|
|
336
|
-
// ADR-095 G2 — over the transport when wired: real RequestVote RPC.
|
|
337
|
-
if (this.transport) {
|
|
338
|
-
const my = this.lastLogInfo();
|
|
339
|
-
try {
|
|
340
|
-
const reply = await this.transport.send(peerId, {
|
|
341
|
-
type: 'request-vote',
|
|
342
|
-
payload: { term: this.node.currentTerm, candidateId: this.node.id, lastLogIndex: my.index, lastLogTerm: my.term },
|
|
343
|
-
term: this.node.currentTerm,
|
|
344
|
-
});
|
|
345
|
-
const rp = (reply?.payload ?? {}) as Record<string, unknown>;
|
|
346
|
-
// §5.1 — if the responder's term is higher, step down.
|
|
347
|
-
if (typeof rp.term === 'number' && rp.term > this.node.currentTerm) {
|
|
348
|
-
this.node.currentTerm = rp.term;
|
|
349
|
-
this.node.votedFor = undefined;
|
|
350
|
-
this.node.state = 'follower';
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
return rp.granted === true;
|
|
354
|
-
} catch {
|
|
355
|
-
return false; // unreachable peer / timeout — counts as no vote.
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Legacy in-process path — mutates the local fake peer state.
|
|
360
|
-
const peer = this.peers.get(peerId);
|
|
361
|
-
if (!peer) return false;
|
|
362
|
-
if (this.node.currentTerm > peer.currentTerm) {
|
|
363
|
-
peer.votedFor = this.node.id;
|
|
364
|
-
peer.currentTerm = this.node.currentTerm;
|
|
365
|
-
return true;
|
|
366
|
-
}
|
|
367
|
-
return false;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
private becomeLeader(): void {
|
|
371
|
-
this.node.state = 'leader';
|
|
372
|
-
|
|
373
|
-
if (this.electionTimeout) {
|
|
374
|
-
clearTimeout(this.electionTimeout);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Start sending heartbeats
|
|
378
|
-
this.heartbeatInterval = setInterval(() => {
|
|
379
|
-
this.sendHeartbeats();
|
|
380
|
-
}, this.config.heartbeatIntervalMs ?? 50);
|
|
381
|
-
|
|
382
|
-
this.emit('leader.elected', {
|
|
383
|
-
term: this.node.currentTerm,
|
|
384
|
-
leaderId: this.node.id
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
private async sendHeartbeats(): Promise<void> {
|
|
389
|
-
for (const [peerId, peer] of this.peers) {
|
|
390
|
-
await this.appendEntries(peerId, []);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
private async appendEntries(peerId: string, entries: RaftLogEntry[]): Promise<boolean> {
|
|
395
|
-
// ADR-095 G2 — over the transport when wired: real AppendEntries RPC.
|
|
396
|
-
if (this.transport) {
|
|
397
|
-
try {
|
|
398
|
-
const reply = await this.transport.send(peerId, {
|
|
399
|
-
type: 'append-entries',
|
|
400
|
-
payload: { term: this.node.currentTerm, leaderId: this.node.id, entries, leaderCommit: this.node.commitIndex },
|
|
401
|
-
term: this.node.currentTerm,
|
|
402
|
-
});
|
|
403
|
-
const rp = (reply?.payload ?? {}) as Record<string, unknown>;
|
|
404
|
-
if (typeof rp.term === 'number' && rp.term > this.node.currentTerm) {
|
|
405
|
-
this.node.currentTerm = rp.term;
|
|
406
|
-
this.node.votedFor = undefined;
|
|
407
|
-
this.node.state = 'follower';
|
|
408
|
-
if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = undefined; }
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
return rp.success === true;
|
|
412
|
-
} catch {
|
|
413
|
-
return false; // unreachable peer / timeout.
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Legacy in-process path.
|
|
418
|
-
const peer = this.peers.get(peerId);
|
|
419
|
-
if (!peer) return false;
|
|
420
|
-
if (this.node.currentTerm >= peer.currentTerm) {
|
|
421
|
-
peer.currentTerm = this.node.currentTerm;
|
|
422
|
-
peer.state = 'follower';
|
|
423
|
-
peer.log.push(...entries);
|
|
424
|
-
return true;
|
|
425
|
-
}
|
|
426
|
-
return false;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
private async replicateToFollowers(entry: RaftLogEntry): Promise<void> {
|
|
430
|
-
const replicationPromises = Array.from(this.peers.keys()).map(
|
|
431
|
-
peerId => this.appendEntries(peerId, [entry])
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
const results = await Promise.allSettled(replicationPromises);
|
|
435
|
-
const successCount = results.filter(
|
|
436
|
-
r => r.status === 'fulfilled' && r.value
|
|
437
|
-
).length;
|
|
438
|
-
|
|
439
|
-
// Check if majority replicated
|
|
440
|
-
const majority = Math.floor((this.peers.size + 1) / 2) + 1;
|
|
441
|
-
if (successCount + 1 >= majority) {
|
|
442
|
-
this.node.commitIndex = entry.index;
|
|
443
|
-
this.emit('log.committed', { index: entry.index });
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
private async checkConsensus(proposalId: string): Promise<void> {
|
|
448
|
-
const proposal = this.proposals.get(proposalId);
|
|
449
|
-
if (!proposal || proposal.status !== 'pending') {
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const totalVoters = this.peers.size + 1;
|
|
454
|
-
const votesReceived = proposal.votes.size;
|
|
455
|
-
const approvingVotes = Array.from(proposal.votes.values()).filter(
|
|
456
|
-
v => v.approve
|
|
457
|
-
).length;
|
|
458
|
-
|
|
459
|
-
const threshold = this.config.threshold ?? 0.66;
|
|
460
|
-
const quorum = Math.floor(totalVoters * threshold);
|
|
461
|
-
|
|
462
|
-
if (approvingVotes >= quorum) {
|
|
463
|
-
proposal.status = 'accepted';
|
|
464
|
-
this.emit('consensus.achieved', { proposalId, approved: true });
|
|
465
|
-
} else if (votesReceived - approvingVotes > totalVoters - quorum) {
|
|
466
|
-
proposal.status = 'rejected';
|
|
467
|
-
this.emit('consensus.achieved', { proposalId, approved: false });
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
private createResult(proposal: ConsensusProposal, durationMs: number): ConsensusResult {
|
|
472
|
-
const totalVoters = this.peers.size + 1;
|
|
473
|
-
const approvingVotes = Array.from(proposal.votes.values()).filter(
|
|
474
|
-
v => v.approve
|
|
475
|
-
).length;
|
|
476
|
-
|
|
477
|
-
return {
|
|
478
|
-
proposalId: proposal.id,
|
|
479
|
-
approved: proposal.status === 'accepted',
|
|
480
|
-
approvalRate: proposal.votes.size > 0
|
|
481
|
-
? approvingVotes / proposal.votes.size
|
|
482
|
-
: 0,
|
|
483
|
-
participationRate: proposal.votes.size / totalVoters,
|
|
484
|
-
finalValue: proposal.value,
|
|
485
|
-
rounds: 1,
|
|
486
|
-
durationMs,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Handle vote request from another candidate
|
|
491
|
-
handleVoteRequest(
|
|
492
|
-
candidateId: string,
|
|
493
|
-
term: number,
|
|
494
|
-
lastLogIndex: number,
|
|
495
|
-
lastLogTerm: number
|
|
496
|
-
): boolean {
|
|
497
|
-
if (term < this.node.currentTerm) {
|
|
498
|
-
return false;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (term > this.node.currentTerm) {
|
|
502
|
-
this.node.currentTerm = term;
|
|
503
|
-
this.node.state = 'follower';
|
|
504
|
-
this.node.votedFor = undefined;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (this.node.votedFor === undefined || this.node.votedFor === candidateId) {
|
|
508
|
-
// Check log is at least as up-to-date
|
|
509
|
-
const lastEntry = this.node.log[this.node.log.length - 1];
|
|
510
|
-
const myLastTerm = lastEntry?.term ?? 0;
|
|
511
|
-
const myLastIndex = lastEntry?.index ?? 0;
|
|
512
|
-
|
|
513
|
-
if (lastLogTerm > myLastTerm ||
|
|
514
|
-
(lastLogTerm === myLastTerm && lastLogIndex >= myLastIndex)) {
|
|
515
|
-
this.node.votedFor = candidateId;
|
|
516
|
-
this.resetElectionTimeout();
|
|
517
|
-
return true;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Handle append entries from leader
|
|
525
|
-
handleAppendEntries(
|
|
526
|
-
leaderId: string,
|
|
527
|
-
term: number,
|
|
528
|
-
entries: RaftLogEntry[],
|
|
529
|
-
leaderCommit: number
|
|
530
|
-
): boolean {
|
|
531
|
-
if (term < this.node.currentTerm) {
|
|
532
|
-
return false;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
this.resetElectionTimeout();
|
|
536
|
-
|
|
537
|
-
if (term > this.node.currentTerm) {
|
|
538
|
-
this.node.currentTerm = term;
|
|
539
|
-
this.node.state = 'follower';
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
this.node.votedFor = leaderId;
|
|
543
|
-
|
|
544
|
-
// Append entries
|
|
545
|
-
this.node.log.push(...entries);
|
|
546
|
-
|
|
547
|
-
// Update commit index
|
|
548
|
-
if (leaderCommit > this.node.commitIndex) {
|
|
549
|
-
this.node.commitIndex = Math.min(
|
|
550
|
-
leaderCommit,
|
|
551
|
-
this.node.log.length
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return true;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
export function createRaftConsensus(nodeId: string, config?: RaftConfig): RaftConsensus {
|
|
560
|
-
return new RaftConsensus(nodeId, config);
|
|
561
|
-
}
|