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,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DandelionMesh = void 0;
|
|
4
|
+
const CryptoService_1 = require("../crypto/CryptoService");
|
|
5
|
+
const RaftNode_1 = require("../raft/RaftNode");
|
|
6
|
+
const InMemoryRaftLog_1 = require("../raft/log/InMemoryRaftLog");
|
|
7
|
+
/**
|
|
8
|
+
* DandelionMesh — a fault-tolerant P2P mesh network for browser applications.
|
|
9
|
+
*
|
|
10
|
+
* Combines three layers:
|
|
11
|
+
* - **Transport** (default: PeerJS) — handles peer-to-peer WebRTC connections.
|
|
12
|
+
* - **Crypto** — RSA-OAEP + AES-256-GCM hybrid encryption for private messages.
|
|
13
|
+
* - **Raft consensus** — leader election and totally-ordered log replication.
|
|
14
|
+
*
|
|
15
|
+
* All application messages (public broadcasts and encrypted private messages)
|
|
16
|
+
* flow through the Raft log, guaranteeing every peer sees the same events in
|
|
17
|
+
* the same order. Non-leader peers forward proposals to the current leader.
|
|
18
|
+
*
|
|
19
|
+
* ## Events
|
|
20
|
+
*
|
|
21
|
+
* | Event | Payload | When |
|
|
22
|
+
* |------------------|--------------------------------------|---------------------------------------------|
|
|
23
|
+
* | `ready` | `localPeerId: string` | Transport is open and Raft has started |
|
|
24
|
+
* | `message` | `MeshMessage<T>, replay: boolean` | A committed log entry is delivered |
|
|
25
|
+
* | `peersChanged` | `peers: string[]` | A peer connects or disconnects |
|
|
26
|
+
* | `leaderChanged` | `leaderId: string \| null` | Raft leader changes |
|
|
27
|
+
* | `error` | `Error` | Transport-level error |
|
|
28
|
+
*
|
|
29
|
+
* @typeParam T - The application-level payload type for messages.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const transport = new PeerJSTransport({ peerId: 'alice' });
|
|
34
|
+
* const mesh = new DandelionMesh<GameAction>(transport, {
|
|
35
|
+
* bootstrapPeers: ['bob', 'charlie'],
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* mesh.on('ready', (id) => console.log('My ID:', id));
|
|
39
|
+
* mesh.on('message', (msg) => {
|
|
40
|
+
* if (msg.type === 'public') console.log(msg.sender, msg.data);
|
|
41
|
+
* if (msg.type === 'private') console.log('secret from', msg.sender);
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* await mesh.sendPublic({ action: 'bet', amount: 100 });
|
|
45
|
+
* await mesh.sendPrivate('bob', { cards: ['Ah', 'Kd'] });
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example Providing a pre-generated crypto key bundle
|
|
49
|
+
* ```ts
|
|
50
|
+
* const bundle = await generateKeyBundle(2048);
|
|
51
|
+
* const mesh = new DandelionMesh(transport, { cryptoKeyBundle: bundle });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
class DandelionMesh {
|
|
55
|
+
constructor(transport, options) {
|
|
56
|
+
var _a, _b, _c, _d;
|
|
57
|
+
this.raftNode = null;
|
|
58
|
+
this.cryptoBundle = null;
|
|
59
|
+
this.peerPublicKeys = new Map();
|
|
60
|
+
this.pendingKeyWaiters = new Map();
|
|
61
|
+
this.lastAppliedIndex = 0;
|
|
62
|
+
this.listeners = {
|
|
63
|
+
ready: new Set(),
|
|
64
|
+
message: new Set(),
|
|
65
|
+
peersChanged: new Set(),
|
|
66
|
+
leaderChanged: new Set(),
|
|
67
|
+
error: new Set(),
|
|
68
|
+
};
|
|
69
|
+
// --- Transport event handlers ---
|
|
70
|
+
this.onTransportOpen = (peerId) => {
|
|
71
|
+
this.localPeerId = peerId;
|
|
72
|
+
// Connect to bootstrap peers
|
|
73
|
+
for (const bp of this.bootstrapPeers) {
|
|
74
|
+
if (bp !== peerId) {
|
|
75
|
+
this.transport.connect(bp);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Initialize Raft node
|
|
79
|
+
this.raftNode = new RaftNode_1.RaftNode(peerId, this.raftLog, this.raftOptions);
|
|
80
|
+
this.raftNode.sendMessage = (toPeerId, message) => {
|
|
81
|
+
this.transport
|
|
82
|
+
.send(toPeerId, this.wireMessage('raft', message))
|
|
83
|
+
.catch(() => {
|
|
84
|
+
// Peer may have disconnected; non-fatal
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
this.raftNode.on('committed', this.onRaftCommitted);
|
|
88
|
+
this.raftNode.on('leaderChanged', (leaderId) => {
|
|
89
|
+
this.emit('leaderChanged', leaderId);
|
|
90
|
+
});
|
|
91
|
+
// Start Raft with currently connected peers
|
|
92
|
+
this.raftNode.start(this.transport.connectedPeers);
|
|
93
|
+
this.emit('ready', peerId);
|
|
94
|
+
// Propose our public key through Raft so all peers (including
|
|
95
|
+
// those joining later) receive it via log replay.
|
|
96
|
+
this.proposePublicKey();
|
|
97
|
+
};
|
|
98
|
+
this.onPeerConnected = (_remotePeerId) => {
|
|
99
|
+
var _a;
|
|
100
|
+
// Update Raft cluster membership
|
|
101
|
+
(_a = this.raftNode) === null || _a === void 0 ? void 0 : _a.updatePeers(this.transport.connectedPeers);
|
|
102
|
+
this.emit('peersChanged', this.peers);
|
|
103
|
+
};
|
|
104
|
+
this.onPeerDisconnected = (_remotePeerId) => {
|
|
105
|
+
var _a;
|
|
106
|
+
(_a = this.raftNode) === null || _a === void 0 ? void 0 : _a.updatePeers(this.transport.connectedPeers);
|
|
107
|
+
this.emit('peersChanged', this.peers);
|
|
108
|
+
};
|
|
109
|
+
this.onTransportMessage = (fromPeerId, data) => {
|
|
110
|
+
var _a;
|
|
111
|
+
const wire = data;
|
|
112
|
+
if (!wire || !wire.channel)
|
|
113
|
+
return;
|
|
114
|
+
if (wire.channel === 'raft') {
|
|
115
|
+
(_a = this.raftNode) === null || _a === void 0 ? void 0 : _a.handleMessage(fromPeerId, wire.payload);
|
|
116
|
+
}
|
|
117
|
+
else if (wire.channel === 'control') {
|
|
118
|
+
this.handleControlMessage(wire.payload);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
this.onTransportError = (error) => {
|
|
122
|
+
this.emit('error', error);
|
|
123
|
+
};
|
|
124
|
+
// --- Raft commit handler ---
|
|
125
|
+
this.onRaftCommitted = (entry, index) => {
|
|
126
|
+
const isReplay = index <= this.lastAppliedIndex;
|
|
127
|
+
const cmd = entry.command;
|
|
128
|
+
switch (cmd._meshType) {
|
|
129
|
+
case 'public':
|
|
130
|
+
this.emit('message', {
|
|
131
|
+
type: 'public',
|
|
132
|
+
sender: cmd.sender,
|
|
133
|
+
data: cmd.data,
|
|
134
|
+
}, isReplay);
|
|
135
|
+
break;
|
|
136
|
+
case 'encrypted':
|
|
137
|
+
this.handleEncryptedCommit(cmd, isReplay);
|
|
138
|
+
break;
|
|
139
|
+
case 'publicKey': {
|
|
140
|
+
if (!this.peerPublicKeys.has(cmd.peerId)) {
|
|
141
|
+
const keyPromise = (0, CryptoService_1.importPublicKey)(cmd.jwk);
|
|
142
|
+
this.peerPublicKeys.set(cmd.peerId, keyPromise);
|
|
143
|
+
// Notify any pending sendPrivate calls waiting for this key
|
|
144
|
+
const waiters = this.pendingKeyWaiters.get(cmd.peerId);
|
|
145
|
+
if (waiters) {
|
|
146
|
+
for (const resolve of waiters) {
|
|
147
|
+
resolve(keyPromise);
|
|
148
|
+
}
|
|
149
|
+
this.pendingKeyWaiters.delete(cmd.peerId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!isReplay) {
|
|
156
|
+
this.lastAppliedIndex = index;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
this.transport = transport;
|
|
160
|
+
this.modulusLength = (_a = options === null || options === void 0 ? void 0 : options.modulusLength) !== null && _a !== void 0 ? _a : 4096;
|
|
161
|
+
this.raftOptions = (_b = options === null || options === void 0 ? void 0 : options.raft) !== null && _b !== void 0 ? _b : {};
|
|
162
|
+
this.raftLog =
|
|
163
|
+
(_c = options === null || options === void 0 ? void 0 : options.raftLog) !== null && _c !== void 0 ? _c : new InMemoryRaftLog_1.InMemoryRaftLog();
|
|
164
|
+
this.bootstrapPeers = (_d = options === null || options === void 0 ? void 0 : options.bootstrapPeers) !== null && _d !== void 0 ? _d : [];
|
|
165
|
+
this.lastAppliedIndex = this.raftLog.length();
|
|
166
|
+
// Use provided key bundle or generate a new one
|
|
167
|
+
this.cryptoReady = ((options === null || options === void 0 ? void 0 : options.cryptoKeyBundle)
|
|
168
|
+
? Promise.resolve(options.cryptoKeyBundle)
|
|
169
|
+
: (0, CryptoService_1.generateKeyBundle)(this.modulusLength)).then((bundle) => {
|
|
170
|
+
this.cryptoBundle = bundle;
|
|
171
|
+
return bundle;
|
|
172
|
+
});
|
|
173
|
+
// Wire up transport events
|
|
174
|
+
this.transport.on('open', this.onTransportOpen);
|
|
175
|
+
this.transport.on('peerConnected', this.onPeerConnected);
|
|
176
|
+
this.transport.on('peerDisconnected', this.onPeerDisconnected);
|
|
177
|
+
this.transport.on('message', this.onTransportMessage);
|
|
178
|
+
this.transport.on('error', this.onTransportError);
|
|
179
|
+
}
|
|
180
|
+
// --- Public API ---
|
|
181
|
+
/** Send a public (broadcast) message through the Raft cluster */
|
|
182
|
+
async sendPublic(data) {
|
|
183
|
+
if (!this.raftNode || !this.localPeerId)
|
|
184
|
+
return false;
|
|
185
|
+
const entry = {
|
|
186
|
+
_meshType: 'public',
|
|
187
|
+
sender: this.localPeerId,
|
|
188
|
+
data,
|
|
189
|
+
};
|
|
190
|
+
if (this.raftNode.isLeader()) {
|
|
191
|
+
return this.raftNode.propose(entry);
|
|
192
|
+
}
|
|
193
|
+
// Forward to leader
|
|
194
|
+
const leaderId = this.raftNode.getLeaderId();
|
|
195
|
+
if (!leaderId)
|
|
196
|
+
return false;
|
|
197
|
+
const propose = { _meshType: 'propose', command: entry };
|
|
198
|
+
await this.transport.send(leaderId, this.wireMessage('control', propose));
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
/** Send an encrypted private message through the Raft cluster */
|
|
202
|
+
async sendPrivate(recipientPeerId, data) {
|
|
203
|
+
if (!this.raftNode || !this.localPeerId)
|
|
204
|
+
return false;
|
|
205
|
+
const recipientKey = await this.getOrAwaitPublicKey(recipientPeerId);
|
|
206
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
207
|
+
const payload = await (0, CryptoService_1.encrypt)(plaintext, recipientKey);
|
|
208
|
+
const entry = {
|
|
209
|
+
_meshType: 'encrypted',
|
|
210
|
+
sender: this.localPeerId,
|
|
211
|
+
recipient: recipientPeerId,
|
|
212
|
+
payload,
|
|
213
|
+
};
|
|
214
|
+
if (this.raftNode.isLeader()) {
|
|
215
|
+
return this.raftNode.propose(entry);
|
|
216
|
+
}
|
|
217
|
+
// Forward to leader
|
|
218
|
+
const leaderId = this.raftNode.getLeaderId();
|
|
219
|
+
if (!leaderId)
|
|
220
|
+
return false;
|
|
221
|
+
const propose = { _meshType: 'propose', command: entry };
|
|
222
|
+
await this.transport.send(leaderId, this.wireMessage('control', propose));
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
/** Get all connected peer IDs (including self) */
|
|
226
|
+
get peers() {
|
|
227
|
+
const connected = [...this.transport.connectedPeers];
|
|
228
|
+
if (this.localPeerId)
|
|
229
|
+
connected.unshift(this.localPeerId);
|
|
230
|
+
return connected;
|
|
231
|
+
}
|
|
232
|
+
/** Get the current Raft leader ID */
|
|
233
|
+
get leaderId() {
|
|
234
|
+
var _a, _b;
|
|
235
|
+
return (_b = (_a = this.raftNode) === null || _a === void 0 ? void 0 : _a.getLeaderId()) !== null && _b !== void 0 ? _b : null;
|
|
236
|
+
}
|
|
237
|
+
/** Whether this node is the Raft leader */
|
|
238
|
+
get isLeader() {
|
|
239
|
+
var _a, _b;
|
|
240
|
+
return (_b = (_a = this.raftNode) === null || _a === void 0 ? void 0 : _a.isLeader()) !== null && _b !== void 0 ? _b : false;
|
|
241
|
+
}
|
|
242
|
+
/** The local peer ID (available after 'ready') */
|
|
243
|
+
get peerId() {
|
|
244
|
+
return this.localPeerId;
|
|
245
|
+
}
|
|
246
|
+
/** Register an event listener */
|
|
247
|
+
on(event, listener) {
|
|
248
|
+
this.listeners[event].add(listener);
|
|
249
|
+
}
|
|
250
|
+
/** Remove an event listener */
|
|
251
|
+
off(event, listener) {
|
|
252
|
+
this.listeners[event].delete(listener);
|
|
253
|
+
}
|
|
254
|
+
/** Shut down the mesh: stop Raft, close transport */
|
|
255
|
+
close() {
|
|
256
|
+
var _a;
|
|
257
|
+
(_a = this.raftNode) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
258
|
+
this.transport.off('open', this.onTransportOpen);
|
|
259
|
+
this.transport.off('peerConnected', this.onPeerConnected);
|
|
260
|
+
this.transport.off('peerDisconnected', this.onPeerDisconnected);
|
|
261
|
+
this.transport.off('message', this.onTransportMessage);
|
|
262
|
+
this.transport.off('error', this.onTransportError);
|
|
263
|
+
this.transport.close();
|
|
264
|
+
}
|
|
265
|
+
// --- Control message handling ---
|
|
266
|
+
handleControlMessage(msg) {
|
|
267
|
+
var _a;
|
|
268
|
+
switch (msg._meshType) {
|
|
269
|
+
case 'propose': {
|
|
270
|
+
// A non-leader peer is forwarding a command to us (the leader)
|
|
271
|
+
if ((_a = this.raftNode) === null || _a === void 0 ? void 0 : _a.isLeader()) {
|
|
272
|
+
this.raftNode.propose(msg.command);
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async handleEncryptedCommit(cmd, isReplay) {
|
|
279
|
+
if (!this.localPeerId)
|
|
280
|
+
return;
|
|
281
|
+
// Only the intended recipient decrypts
|
|
282
|
+
if (cmd.recipient !== this.localPeerId)
|
|
283
|
+
return;
|
|
284
|
+
if (!this.cryptoBundle) {
|
|
285
|
+
await this.cryptoReady;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const plaintext = await (0, CryptoService_1.decrypt)(cmd.payload, this.cryptoBundle.privateKey);
|
|
289
|
+
const data = JSON.parse(new TextDecoder().decode(plaintext));
|
|
290
|
+
this.emit('message', {
|
|
291
|
+
type: 'private',
|
|
292
|
+
sender: cmd.sender,
|
|
293
|
+
recipient: cmd.recipient,
|
|
294
|
+
data,
|
|
295
|
+
}, isReplay);
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
// Not for us or decryption failed — this is expected for other recipients
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// --- Key management ---
|
|
302
|
+
async proposePublicKey() {
|
|
303
|
+
if (!this.cryptoBundle) {
|
|
304
|
+
await this.cryptoReady;
|
|
305
|
+
}
|
|
306
|
+
if (!this.localPeerId || !this.cryptoBundle || !this.raftNode)
|
|
307
|
+
return;
|
|
308
|
+
// Wait until a leader is available
|
|
309
|
+
if (!this.raftNode.isLeader() && !this.raftNode.getLeaderId()) {
|
|
310
|
+
await new Promise((resolve) => {
|
|
311
|
+
const onLeader = (leaderId) => {
|
|
312
|
+
if (leaderId) {
|
|
313
|
+
this.off('leaderChanged', onLeader);
|
|
314
|
+
resolve();
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
this.on('leaderChanged', onLeader);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (!this.raftNode)
|
|
321
|
+
return;
|
|
322
|
+
const announcement = {
|
|
323
|
+
_meshType: 'publicKey',
|
|
324
|
+
peerId: this.localPeerId,
|
|
325
|
+
jwk: this.cryptoBundle.publicKeyJwk,
|
|
326
|
+
};
|
|
327
|
+
if (this.raftNode.isLeader()) {
|
|
328
|
+
this.raftNode.propose(announcement);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const leaderId = this.raftNode.getLeaderId();
|
|
332
|
+
if (!leaderId)
|
|
333
|
+
return;
|
|
334
|
+
const propose = {
|
|
335
|
+
_meshType: 'propose',
|
|
336
|
+
command: announcement,
|
|
337
|
+
};
|
|
338
|
+
this.transport
|
|
339
|
+
.send(leaderId, this.wireMessage('control', propose))
|
|
340
|
+
.catch(() => { });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async getOrAwaitPublicKey(peerId) {
|
|
344
|
+
const existing = this.peerPublicKeys.get(peerId);
|
|
345
|
+
if (existing)
|
|
346
|
+
return existing;
|
|
347
|
+
// Wait for the key to arrive via Raft commit
|
|
348
|
+
return new Promise((resolve) => {
|
|
349
|
+
let waiters = this.pendingKeyWaiters.get(peerId);
|
|
350
|
+
if (!waiters) {
|
|
351
|
+
waiters = [];
|
|
352
|
+
this.pendingKeyWaiters.set(peerId, waiters);
|
|
353
|
+
}
|
|
354
|
+
waiters.push((keyPromise) => resolve(keyPromise));
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
// --- Helpers ---
|
|
358
|
+
wireMessage(channel, payload) {
|
|
359
|
+
return { channel, payload };
|
|
360
|
+
}
|
|
361
|
+
emit(event, ...args) {
|
|
362
|
+
for (const listener of this.listeners[event]) {
|
|
363
|
+
listener(...args);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
exports.DandelionMesh = DandelionMesh;
|
|
368
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRGFuZGVsaW9uTWVzaC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9tZXNoL0RhbmRlbGlvbk1lc2gudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsMkRBTWlDO0FBQ2pDLCtDQUE2RDtBQUM3RCxpRUFBOEQ7QUF1QzlEOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBOENHO0FBQ0gsTUFBYSxhQUFhO0lBMkJ4QixZQUFZLFNBQW9CLEVBQUUsT0FBOEI7O1FBeEJ4RCxhQUFRLEdBQXVDLElBQUksQ0FBQztRQUlwRCxpQkFBWSxHQUEyQixJQUFJLENBQUM7UUFDbkMsbUJBQWMsR0FBRyxJQUFJLEdBQUcsRUFBOEIsQ0FBQztRQUN2RCxzQkFBaUIsR0FBRyxJQUFJLEdBQUcsRUFHekMsQ0FBQztRQUtJLHFCQUFnQixHQUFHLENBQUMsQ0FBQztRQUVaLGNBQVMsR0FBbUI7WUFDM0MsS0FBSyxFQUFFLElBQUksR0FBRyxFQUFFO1lBQ2hCLE9BQU8sRUFBRSxJQUFJLEdBQUcsRUFBRTtZQUNsQixZQUFZLEVBQUUsSUFBSSxHQUFHLEVBQUU7WUFDdkIsYUFBYSxFQUFFLElBQUksR0FBRyxFQUFFO1lBQ3hCLEtBQUssRUFBRSxJQUFJLEdBQUcsRUFBRTtTQUNqQixDQUFDO1FBb0lGLG1DQUFtQztRQUUzQixvQkFBZSxHQUFHLENBQUMsTUFBYyxFQUFRLEVBQUU7WUFDakQsSUFBSSxDQUFDLFdBQVcsR0FBRyxNQUFNLENBQUM7WUFFMUIsNkJBQTZCO1lBQzdCLEtBQUssTUFBTSxFQUFFLElBQUksSUFBSSxDQUFDLGNBQWMsRUFBRTtnQkFDcEMsSUFBSSxFQUFFLEtBQUssTUFBTSxFQUFFO29CQUNqQixJQUFJLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsQ0FBQztpQkFDNUI7YUFDRjtZQUVELHVCQUF1QjtZQUN2QixJQUFJLENBQUMsUUFBUSxHQUFHLElBQUksbUJB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
|
|
@@ -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,3 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
//# 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
|
+
}
|