@sparkleideas/swarm 3.0.0-alpha.7
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/MIGRATION.md +472 -0
- package/README.md +634 -0
- package/__tests__/consensus.test.ts +577 -0
- package/__tests__/coordinator.test.ts +501 -0
- package/__tests__/queen-coordinator.test.ts +1335 -0
- package/__tests__/topology.test.ts +621 -0
- package/package.json +32 -0
- package/src/agent-pool.ts +476 -0
- package/src/application/commands/create-task.command.ts +124 -0
- package/src/application/commands/spawn-agent.command.ts +122 -0
- package/src/application/index.ts +30 -0
- package/src/application/services/swarm-application-service.ts +200 -0
- package/src/attention-coordinator.ts +1000 -0
- package/src/consensus/byzantine.ts +431 -0
- package/src/consensus/gossip.ts +513 -0
- package/src/consensus/index.ts +267 -0
- package/src/consensus/raft.ts +443 -0
- package/src/coordination/agent-registry.ts +544 -0
- package/src/coordination/index.ts +23 -0
- package/src/coordination/swarm-hub.ts +776 -0
- package/src/coordination/task-orchestrator.ts +605 -0
- package/src/domain/entities/agent.d.ts +151 -0
- package/src/domain/entities/agent.d.ts.map +1 -0
- package/src/domain/entities/agent.js +280 -0
- package/src/domain/entities/agent.js.map +1 -0
- package/src/domain/entities/agent.ts +370 -0
- package/src/domain/entities/task.d.ts +133 -0
- package/src/domain/entities/task.d.ts.map +1 -0
- package/src/domain/entities/task.js +261 -0
- package/src/domain/entities/task.js.map +1 -0
- package/src/domain/entities/task.ts +319 -0
- package/src/domain/index.ts +41 -0
- package/src/domain/repositories/agent-repository.interface.d.ts +57 -0
- package/src/domain/repositories/agent-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/agent-repository.interface.js +9 -0
- package/src/domain/repositories/agent-repository.interface.js.map +1 -0
- package/src/domain/repositories/agent-repository.interface.ts +69 -0
- package/src/domain/repositories/task-repository.interface.d.ts +61 -0
- package/src/domain/repositories/task-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/task-repository.interface.js +9 -0
- package/src/domain/repositories/task-repository.interface.js.map +1 -0
- package/src/domain/repositories/task-repository.interface.ts +75 -0
- package/src/domain/services/coordination-service.ts +320 -0
- package/src/federation-hub.d.ts +284 -0
- package/src/federation-hub.d.ts.map +1 -0
- package/src/federation-hub.js +692 -0
- package/src/federation-hub.js.map +1 -0
- package/src/federation-hub.ts +979 -0
- package/src/index.ts +348 -0
- package/src/message-bus.ts +607 -0
- package/src/queen-coordinator.ts +2025 -0
- package/src/shared/events.ts +285 -0
- package/src/shared/types.ts +389 -0
- package/src/topology-manager.ts +656 -0
- package/src/types.ts +545 -0
- package/src/unified-coordinator.ts +1844 -0
- package/src/workers/index.ts +65 -0
- package/src/workers/worker-dispatch.d.ts +234 -0
- package/src/workers/worker-dispatch.d.ts.map +1 -0
- package/src/workers/worker-dispatch.js +842 -0
- package/src/workers/worker-dispatch.js.map +1 -0
- package/src/workers/worker-dispatch.ts +1076 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 Gossip Protocol Consensus
|
|
3
|
+
* Eventually consistent consensus for large-scale distributed systems
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import {
|
|
8
|
+
ConsensusProposal,
|
|
9
|
+
ConsensusVote,
|
|
10
|
+
ConsensusResult,
|
|
11
|
+
ConsensusConfig,
|
|
12
|
+
SWARM_CONSTANTS,
|
|
13
|
+
} from '../types.js';
|
|
14
|
+
|
|
15
|
+
export interface GossipMessage {
|
|
16
|
+
id: string;
|
|
17
|
+
type: 'proposal' | 'vote' | 'state' | 'ack';
|
|
18
|
+
senderId: string;
|
|
19
|
+
version: number;
|
|
20
|
+
payload: unknown;
|
|
21
|
+
timestamp: Date;
|
|
22
|
+
ttl: number;
|
|
23
|
+
hops: number;
|
|
24
|
+
path: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GossipNode {
|
|
28
|
+
id: string;
|
|
29
|
+
state: Map<string, unknown>;
|
|
30
|
+
version: number;
|
|
31
|
+
neighbors: Set<string>;
|
|
32
|
+
seenMessages: Set<string>;
|
|
33
|
+
lastSync: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface GossipConfig extends Partial<ConsensusConfig> {
|
|
37
|
+
fanout?: number;
|
|
38
|
+
gossipIntervalMs?: number;
|
|
39
|
+
maxHops?: number;
|
|
40
|
+
convergenceThreshold?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class GossipConsensus extends EventEmitter {
|
|
44
|
+
private config: GossipConfig;
|
|
45
|
+
private node: GossipNode;
|
|
46
|
+
private nodes: Map<string, GossipNode> = new Map();
|
|
47
|
+
private proposals: Map<string, ConsensusProposal> = new Map();
|
|
48
|
+
private messageQueue: GossipMessage[] = [];
|
|
49
|
+
private gossipInterval?: NodeJS.Timeout;
|
|
50
|
+
private proposalCounter: number = 0;
|
|
51
|
+
|
|
52
|
+
constructor(nodeId: string, config: GossipConfig = {}) {
|
|
53
|
+
super();
|
|
54
|
+
this.config = {
|
|
55
|
+
threshold: config.threshold ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_THRESHOLD,
|
|
56
|
+
timeoutMs: config.timeoutMs ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_TIMEOUT_MS,
|
|
57
|
+
maxRounds: config.maxRounds ?? 10,
|
|
58
|
+
requireQuorum: config.requireQuorum ?? false, // Gossip is eventually consistent
|
|
59
|
+
fanout: config.fanout ?? 3,
|
|
60
|
+
gossipIntervalMs: config.gossipIntervalMs ?? 100,
|
|
61
|
+
maxHops: config.maxHops ?? 10,
|
|
62
|
+
convergenceThreshold: config.convergenceThreshold ?? 0.9,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.node = {
|
|
66
|
+
id: nodeId,
|
|
67
|
+
state: new Map(),
|
|
68
|
+
version: 0,
|
|
69
|
+
neighbors: new Set(),
|
|
70
|
+
seenMessages: new Set(),
|
|
71
|
+
lastSync: new Date(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async initialize(): Promise<void> {
|
|
76
|
+
this.startGossipLoop();
|
|
77
|
+
this.emit('initialized', { nodeId: this.node.id });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async shutdown(): Promise<void> {
|
|
81
|
+
if (this.gossipInterval) {
|
|
82
|
+
clearInterval(this.gossipInterval);
|
|
83
|
+
}
|
|
84
|
+
this.emit('shutdown');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
addNode(nodeId: string): void {
|
|
88
|
+
this.nodes.set(nodeId, {
|
|
89
|
+
id: nodeId,
|
|
90
|
+
state: new Map(),
|
|
91
|
+
version: 0,
|
|
92
|
+
neighbors: new Set(),
|
|
93
|
+
seenMessages: new Set(),
|
|
94
|
+
lastSync: new Date(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Add as neighbor with some probability (random mesh)
|
|
98
|
+
if (Math.random() < 0.5) {
|
|
99
|
+
this.node.neighbors.add(nodeId);
|
|
100
|
+
this.nodes.get(nodeId)!.neighbors.add(this.node.id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
removeNode(nodeId: string): void {
|
|
105
|
+
this.nodes.delete(nodeId);
|
|
106
|
+
this.node.neighbors.delete(nodeId);
|
|
107
|
+
|
|
108
|
+
for (const node of this.nodes.values()) {
|
|
109
|
+
node.neighbors.delete(nodeId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
addNeighbor(nodeId: string): void {
|
|
114
|
+
if (this.nodes.has(nodeId)) {
|
|
115
|
+
this.node.neighbors.add(nodeId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
removeNeighbor(nodeId: string): void {
|
|
120
|
+
this.node.neighbors.delete(nodeId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async propose(value: unknown): Promise<ConsensusProposal> {
|
|
124
|
+
this.proposalCounter++;
|
|
125
|
+
const proposalId = `gossip_${this.node.id}_${this.proposalCounter}`;
|
|
126
|
+
|
|
127
|
+
const proposal: ConsensusProposal = {
|
|
128
|
+
id: proposalId,
|
|
129
|
+
proposerId: this.node.id,
|
|
130
|
+
value,
|
|
131
|
+
term: this.node.version,
|
|
132
|
+
timestamp: new Date(),
|
|
133
|
+
votes: new Map(),
|
|
134
|
+
status: 'pending',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
this.proposals.set(proposalId, proposal);
|
|
138
|
+
|
|
139
|
+
// Self-vote
|
|
140
|
+
proposal.votes.set(this.node.id, {
|
|
141
|
+
voterId: this.node.id,
|
|
142
|
+
approve: true,
|
|
143
|
+
confidence: 1.0,
|
|
144
|
+
timestamp: new Date(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Create gossip message
|
|
148
|
+
const message: GossipMessage = {
|
|
149
|
+
id: `msg_${proposalId}`,
|
|
150
|
+
type: 'proposal',
|
|
151
|
+
senderId: this.node.id,
|
|
152
|
+
version: ++this.node.version,
|
|
153
|
+
payload: { proposalId, value },
|
|
154
|
+
timestamp: new Date(),
|
|
155
|
+
ttl: this.config.maxHops ?? 10,
|
|
156
|
+
hops: 0,
|
|
157
|
+
path: [this.node.id],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Queue for gossip
|
|
161
|
+
this.queueMessage(message);
|
|
162
|
+
|
|
163
|
+
return proposal;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async vote(proposalId: string, vote: ConsensusVote): Promise<void> {
|
|
167
|
+
const proposal = this.proposals.get(proposalId);
|
|
168
|
+
if (!proposal) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
proposal.votes.set(vote.voterId, vote);
|
|
173
|
+
|
|
174
|
+
// Create vote gossip message
|
|
175
|
+
const message: GossipMessage = {
|
|
176
|
+
id: `vote_${proposalId}_${vote.voterId}`,
|
|
177
|
+
type: 'vote',
|
|
178
|
+
senderId: this.node.id,
|
|
179
|
+
version: ++this.node.version,
|
|
180
|
+
payload: { proposalId, vote },
|
|
181
|
+
timestamp: new Date(),
|
|
182
|
+
ttl: this.config.maxHops ?? 10,
|
|
183
|
+
hops: 0,
|
|
184
|
+
path: [this.node.id],
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
this.queueMessage(message);
|
|
188
|
+
|
|
189
|
+
// Check convergence
|
|
190
|
+
await this.checkConvergence(proposalId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async awaitConsensus(proposalId: string): Promise<ConsensusResult> {
|
|
194
|
+
const startTime = Date.now();
|
|
195
|
+
const maxWait = this.config.timeoutMs ?? 30000;
|
|
196
|
+
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
const checkInterval = setInterval(() => {
|
|
199
|
+
const proposal = this.proposals.get(proposalId);
|
|
200
|
+
if (!proposal) {
|
|
201
|
+
clearInterval(checkInterval);
|
|
202
|
+
reject(new Error(`Proposal ${proposalId} not found`));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (proposal.status !== 'pending') {
|
|
207
|
+
clearInterval(checkInterval);
|
|
208
|
+
resolve(this.createResult(proposal, Date.now() - startTime));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check convergence
|
|
213
|
+
this.checkConvergence(proposalId);
|
|
214
|
+
|
|
215
|
+
if (Date.now() - startTime > maxWait) {
|
|
216
|
+
clearInterval(checkInterval);
|
|
217
|
+
// Gossip is eventually consistent, so mark as accepted if threshold met
|
|
218
|
+
const totalNodes = this.nodes.size + 1;
|
|
219
|
+
const votes = proposal.votes.size;
|
|
220
|
+
const threshold = this.config.convergenceThreshold ?? 0.9;
|
|
221
|
+
|
|
222
|
+
if (votes / totalNodes >= threshold) {
|
|
223
|
+
proposal.status = 'accepted';
|
|
224
|
+
} else {
|
|
225
|
+
proposal.status = 'expired';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
resolve(this.createResult(proposal, Date.now() - startTime));
|
|
229
|
+
}
|
|
230
|
+
}, 50);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ===== GOSSIP PROTOCOL =====
|
|
235
|
+
|
|
236
|
+
private startGossipLoop(): void {
|
|
237
|
+
this.gossipInterval = setInterval(() => {
|
|
238
|
+
this.gossipRound();
|
|
239
|
+
}, this.config.gossipIntervalMs ?? 100);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async gossipRound(): Promise<void> {
|
|
243
|
+
if (this.messageQueue.length === 0) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Select random neighbors (fanout)
|
|
248
|
+
const fanout = Math.min(
|
|
249
|
+
this.config.fanout ?? 3,
|
|
250
|
+
this.node.neighbors.size
|
|
251
|
+
);
|
|
252
|
+
const neighbors = this.selectRandomNeighbors(fanout);
|
|
253
|
+
|
|
254
|
+
// Send queued messages to selected neighbors
|
|
255
|
+
const messages = this.messageQueue.splice(0, 10); // Process up to 10 per round
|
|
256
|
+
|
|
257
|
+
for (const message of messages) {
|
|
258
|
+
for (const neighborId of neighbors) {
|
|
259
|
+
await this.sendToNeighbor(neighborId, message);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.node.lastSync = new Date();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private selectRandomNeighbors(count: number): string[] {
|
|
267
|
+
const neighbors = Array.from(this.node.neighbors);
|
|
268
|
+
const selected: string[] = [];
|
|
269
|
+
|
|
270
|
+
while (selected.length < count && neighbors.length > 0) {
|
|
271
|
+
const idx = Math.floor(Math.random() * neighbors.length);
|
|
272
|
+
selected.push(neighbors.splice(idx, 1)[0]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return selected;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private async sendToNeighbor(neighborId: string, message: GossipMessage): Promise<void> {
|
|
279
|
+
const neighbor = this.nodes.get(neighborId);
|
|
280
|
+
if (!neighbor) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check if already seen
|
|
285
|
+
if (neighbor.seenMessages.has(message.id)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Deliver message to neighbor node
|
|
290
|
+
const deliveredMessage: GossipMessage = {
|
|
291
|
+
...message,
|
|
292
|
+
hops: message.hops + 1,
|
|
293
|
+
path: [...message.path, neighborId],
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Process at neighbor
|
|
297
|
+
await this.processReceivedMessage(neighbor, deliveredMessage);
|
|
298
|
+
|
|
299
|
+
this.emit('message.sent', { to: neighborId, message: deliveredMessage });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async processReceivedMessage(
|
|
303
|
+
node: GossipNode,
|
|
304
|
+
message: GossipMessage
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
// Mark as seen
|
|
307
|
+
node.seenMessages.add(message.id);
|
|
308
|
+
|
|
309
|
+
// Check TTL
|
|
310
|
+
if (message.ttl <= 0 || message.hops >= (this.config.maxHops ?? 10)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
switch (message.type) {
|
|
315
|
+
case 'proposal':
|
|
316
|
+
await this.handleProposalMessage(node, message);
|
|
317
|
+
break;
|
|
318
|
+
case 'vote':
|
|
319
|
+
await this.handleVoteMessage(node, message);
|
|
320
|
+
break;
|
|
321
|
+
case 'state':
|
|
322
|
+
await this.handleStateMessage(node, message);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Propagate to neighbors (gossip)
|
|
327
|
+
if (message.hops < (this.config.maxHops ?? 10)) {
|
|
328
|
+
const propagateMessage: GossipMessage = {
|
|
329
|
+
...message,
|
|
330
|
+
ttl: message.ttl - 1,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Add to queue if this is our node
|
|
334
|
+
if (node.id === this.node.id) {
|
|
335
|
+
this.queueMessage(propagateMessage);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private async handleProposalMessage(
|
|
341
|
+
node: GossipNode,
|
|
342
|
+
message: GossipMessage
|
|
343
|
+
): Promise<void> {
|
|
344
|
+
const { proposalId, value } = message.payload as {
|
|
345
|
+
proposalId: string;
|
|
346
|
+
value: unknown;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
if (!this.proposals.has(proposalId)) {
|
|
350
|
+
const proposal: ConsensusProposal = {
|
|
351
|
+
id: proposalId,
|
|
352
|
+
proposerId: message.senderId,
|
|
353
|
+
value,
|
|
354
|
+
term: message.version,
|
|
355
|
+
timestamp: message.timestamp,
|
|
356
|
+
votes: new Map(),
|
|
357
|
+
status: 'pending',
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
this.proposals.set(proposalId, proposal);
|
|
361
|
+
|
|
362
|
+
// Auto-vote (simplified)
|
|
363
|
+
if (node.id === this.node.id) {
|
|
364
|
+
await this.vote(proposalId, {
|
|
365
|
+
voterId: this.node.id,
|
|
366
|
+
approve: true,
|
|
367
|
+
confidence: 0.9,
|
|
368
|
+
timestamp: new Date(),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private async handleVoteMessage(
|
|
375
|
+
node: GossipNode,
|
|
376
|
+
message: GossipMessage
|
|
377
|
+
): Promise<void> {
|
|
378
|
+
const { proposalId, vote } = message.payload as {
|
|
379
|
+
proposalId: string;
|
|
380
|
+
vote: ConsensusVote;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const proposal = this.proposals.get(proposalId);
|
|
384
|
+
if (proposal && !proposal.votes.has(vote.voterId)) {
|
|
385
|
+
proposal.votes.set(vote.voterId, vote);
|
|
386
|
+
await this.checkConvergence(proposalId);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async handleStateMessage(
|
|
391
|
+
node: GossipNode,
|
|
392
|
+
message: GossipMessage
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
const state = message.payload as Record<string, unknown>;
|
|
395
|
+
|
|
396
|
+
// Merge state (last-writer-wins)
|
|
397
|
+
if (message.version > node.version) {
|
|
398
|
+
for (const [key, value] of Object.entries(state)) {
|
|
399
|
+
node.state.set(key, value);
|
|
400
|
+
}
|
|
401
|
+
node.version = message.version;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private queueMessage(message: GossipMessage): void {
|
|
406
|
+
// Avoid duplicates
|
|
407
|
+
if (!this.node.seenMessages.has(message.id)) {
|
|
408
|
+
this.node.seenMessages.add(message.id);
|
|
409
|
+
this.messageQueue.push(message);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private async checkConvergence(proposalId: string): Promise<void> {
|
|
414
|
+
const proposal = this.proposals.get(proposalId);
|
|
415
|
+
if (!proposal || proposal.status !== 'pending') {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const totalNodes = this.nodes.size + 1;
|
|
420
|
+
const votes = proposal.votes.size;
|
|
421
|
+
const threshold = this.config.convergenceThreshold ?? 0.9;
|
|
422
|
+
const approvalThreshold = this.config.threshold ?? 0.66;
|
|
423
|
+
|
|
424
|
+
// Check if we've converged (enough nodes have voted)
|
|
425
|
+
if (votes / totalNodes >= threshold) {
|
|
426
|
+
const approvingVotes = Array.from(proposal.votes.values()).filter(
|
|
427
|
+
v => v.approve
|
|
428
|
+
).length;
|
|
429
|
+
|
|
430
|
+
if (approvingVotes / votes >= approvalThreshold) {
|
|
431
|
+
proposal.status = 'accepted';
|
|
432
|
+
this.emit('consensus.achieved', { proposalId, approved: true });
|
|
433
|
+
} else {
|
|
434
|
+
proposal.status = 'rejected';
|
|
435
|
+
this.emit('consensus.achieved', { proposalId, approved: false });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private createResult(proposal: ConsensusProposal, durationMs: number): ConsensusResult {
|
|
441
|
+
const totalNodes = this.nodes.size + 1;
|
|
442
|
+
const approvingVotes = Array.from(proposal.votes.values()).filter(
|
|
443
|
+
v => v.approve
|
|
444
|
+
).length;
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
proposalId: proposal.id,
|
|
448
|
+
approved: proposal.status === 'accepted',
|
|
449
|
+
approvalRate: proposal.votes.size > 0
|
|
450
|
+
? approvingVotes / proposal.votes.size
|
|
451
|
+
: 0,
|
|
452
|
+
participationRate: proposal.votes.size / totalNodes,
|
|
453
|
+
finalValue: proposal.value,
|
|
454
|
+
rounds: this.node.version,
|
|
455
|
+
durationMs,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ===== STATE QUERIES =====
|
|
460
|
+
|
|
461
|
+
getConvergence(proposalId: string): number {
|
|
462
|
+
const proposal = this.proposals.get(proposalId);
|
|
463
|
+
if (!proposal) return 0;
|
|
464
|
+
|
|
465
|
+
const totalNodes = this.nodes.size + 1;
|
|
466
|
+
return proposal.votes.size / totalNodes;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
getVersion(): number {
|
|
470
|
+
return this.node.version;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
getNeighborCount(): number {
|
|
474
|
+
return this.node.neighbors.size;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
getSeenMessageCount(): number {
|
|
478
|
+
return this.node.seenMessages.size;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
getQueueDepth(): number {
|
|
482
|
+
return this.messageQueue.length;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Anti-entropy: sync full state with a random neighbor
|
|
486
|
+
async antiEntropy(): Promise<void> {
|
|
487
|
+
if (this.node.neighbors.size === 0) return;
|
|
488
|
+
|
|
489
|
+
const neighbors = Array.from(this.node.neighbors);
|
|
490
|
+
const randomNeighbor = neighbors[Math.floor(Math.random() * neighbors.length)];
|
|
491
|
+
|
|
492
|
+
const stateMessage: GossipMessage = {
|
|
493
|
+
id: `state_${this.node.id}_${Date.now()}`,
|
|
494
|
+
type: 'state',
|
|
495
|
+
senderId: this.node.id,
|
|
496
|
+
version: this.node.version,
|
|
497
|
+
payload: Object.fromEntries(this.node.state),
|
|
498
|
+
timestamp: new Date(),
|
|
499
|
+
ttl: 1,
|
|
500
|
+
hops: 0,
|
|
501
|
+
path: [this.node.id],
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
await this.sendToNeighbor(randomNeighbor, stateMessage);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function createGossipConsensus(
|
|
509
|
+
nodeId: string,
|
|
510
|
+
config?: GossipConfig
|
|
511
|
+
): GossipConsensus {
|
|
512
|
+
return new GossipConsensus(nodeId, config);
|
|
513
|
+
}
|