@ruvector/edge-net 0.1.1 → 0.1.2
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/README.md +119 -0
- package/cli.js +17 -0
- package/join.html +985 -0
- package/join.js +1333 -0
- package/network.js +820 -0
- package/networks.js +817 -0
- package/package.json +15 -3
- package/webrtc.js +964 -0
package/webrtc.js
ADDED
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Edge-Net WebRTC P2P Implementation
|
|
4
|
+
*
|
|
5
|
+
* Real peer-to-peer communication using WebRTC data channels.
|
|
6
|
+
* Replaces simulated P2P with actual network connectivity.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - WebRTC data channels for P2P messaging
|
|
10
|
+
* - ICE candidate handling with STUN/TURN
|
|
11
|
+
* - WebSocket signaling with fallback
|
|
12
|
+
* - Connection quality monitoring
|
|
13
|
+
* - Automatic reconnection
|
|
14
|
+
* - QDAG synchronization over data channels
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { EventEmitter } from 'events';
|
|
18
|
+
import { createHash, randomBytes } from 'crypto';
|
|
19
|
+
|
|
20
|
+
// WebRTC Configuration
|
|
21
|
+
export const WEBRTC_CONFIG = {
|
|
22
|
+
iceServers: [
|
|
23
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
24
|
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
25
|
+
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
26
|
+
{ urls: 'stun:stun.services.mozilla.com:3478' },
|
|
27
|
+
],
|
|
28
|
+
// Signaling server endpoints
|
|
29
|
+
signalingServers: [
|
|
30
|
+
'wss://edge-net-signal.ruvector.dev',
|
|
31
|
+
'wss://signal.edge-net.io',
|
|
32
|
+
],
|
|
33
|
+
// Fallback to local simulation if no signaling available
|
|
34
|
+
fallbackToSimulation: true,
|
|
35
|
+
// Connection timeouts
|
|
36
|
+
connectionTimeout: 30000,
|
|
37
|
+
reconnectDelay: 5000,
|
|
38
|
+
maxReconnectAttempts: 5,
|
|
39
|
+
// Data channel options
|
|
40
|
+
dataChannelOptions: {
|
|
41
|
+
ordered: true,
|
|
42
|
+
maxRetransmits: 3,
|
|
43
|
+
},
|
|
44
|
+
// Heartbeat for connection health
|
|
45
|
+
heartbeatInterval: 5000,
|
|
46
|
+
heartbeatTimeout: 15000,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* WebRTC Peer Connection Manager
|
|
51
|
+
*
|
|
52
|
+
* Manages individual peer connections with ICE handling,
|
|
53
|
+
* data channels, and connection lifecycle.
|
|
54
|
+
*/
|
|
55
|
+
export class WebRTCPeerConnection extends EventEmitter {
|
|
56
|
+
constructor(peerId, localIdentity, isInitiator = false) {
|
|
57
|
+
super();
|
|
58
|
+
this.peerId = peerId;
|
|
59
|
+
this.localIdentity = localIdentity;
|
|
60
|
+
this.isInitiator = isInitiator;
|
|
61
|
+
this.pc = null;
|
|
62
|
+
this.dataChannel = null;
|
|
63
|
+
this.state = 'new';
|
|
64
|
+
this.iceCandidates = [];
|
|
65
|
+
this.pendingCandidates = [];
|
|
66
|
+
this.lastHeartbeat = Date.now();
|
|
67
|
+
this.reconnectAttempts = 0;
|
|
68
|
+
this.metrics = {
|
|
69
|
+
messagesSent: 0,
|
|
70
|
+
messagesReceived: 0,
|
|
71
|
+
bytesTransferred: 0,
|
|
72
|
+
latency: [],
|
|
73
|
+
connectionTime: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Initialize the RTCPeerConnection
|
|
79
|
+
*/
|
|
80
|
+
async initialize() {
|
|
81
|
+
// Use wrtc for Node.js or native WebRTC in browser
|
|
82
|
+
const RTCPeerConnection = globalThis.RTCPeerConnection ||
|
|
83
|
+
(await this.loadNodeWebRTC());
|
|
84
|
+
|
|
85
|
+
if (!RTCPeerConnection) {
|
|
86
|
+
throw new Error('WebRTC not available');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.pc = new RTCPeerConnection({
|
|
90
|
+
iceServers: WEBRTC_CONFIG.iceServers,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.setupEventHandlers();
|
|
94
|
+
|
|
95
|
+
if (this.isInitiator) {
|
|
96
|
+
await this.createDataChannel();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load wrtc for Node.js environment
|
|
104
|
+
*/
|
|
105
|
+
async loadNodeWebRTC() {
|
|
106
|
+
try {
|
|
107
|
+
const wrtc = await import('wrtc');
|
|
108
|
+
return wrtc.RTCPeerConnection;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// wrtc not available, will use simulation
|
|
111
|
+
console.warn('WebRTC not available in Node.js, using simulation');
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Setup RTCPeerConnection event handlers
|
|
118
|
+
*/
|
|
119
|
+
setupEventHandlers() {
|
|
120
|
+
// ICE candidate events
|
|
121
|
+
this.pc.onicecandidate = (event) => {
|
|
122
|
+
if (event.candidate) {
|
|
123
|
+
this.iceCandidates.push(event.candidate);
|
|
124
|
+
this.emit('ice-candidate', {
|
|
125
|
+
peerId: this.peerId,
|
|
126
|
+
candidate: event.candidate,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
this.pc.onicegatheringstatechange = () => {
|
|
132
|
+
this.emit('ice-gathering-state', this.pc.iceGatheringState);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
this.pc.oniceconnectionstatechange = () => {
|
|
136
|
+
const state = this.pc.iceConnectionState;
|
|
137
|
+
this.state = state;
|
|
138
|
+
this.emit('connection-state', state);
|
|
139
|
+
|
|
140
|
+
if (state === 'connected') {
|
|
141
|
+
this.metrics.connectionTime = Date.now();
|
|
142
|
+
this.startHeartbeat();
|
|
143
|
+
} else if (state === 'disconnected' || state === 'failed') {
|
|
144
|
+
this.handleDisconnection();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Data channel events (for non-initiator)
|
|
149
|
+
this.pc.ondatachannel = (event) => {
|
|
150
|
+
this.dataChannel = event.channel;
|
|
151
|
+
this.setupDataChannel();
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create data channel (initiator only)
|
|
157
|
+
*/
|
|
158
|
+
async createDataChannel() {
|
|
159
|
+
this.dataChannel = this.pc.createDataChannel(
|
|
160
|
+
'edge-net',
|
|
161
|
+
WEBRTC_CONFIG.dataChannelOptions
|
|
162
|
+
);
|
|
163
|
+
this.setupDataChannel();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Setup data channel event handlers
|
|
168
|
+
*/
|
|
169
|
+
setupDataChannel() {
|
|
170
|
+
if (!this.dataChannel) return;
|
|
171
|
+
|
|
172
|
+
this.dataChannel.onopen = () => {
|
|
173
|
+
this.emit('channel-open', this.peerId);
|
|
174
|
+
console.log(` 📡 Data channel open with ${this.peerId.slice(0, 8)}...`);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
this.dataChannel.onclose = () => {
|
|
178
|
+
this.emit('channel-close', this.peerId);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.dataChannel.onerror = (error) => {
|
|
182
|
+
this.emit('channel-error', { peerId: this.peerId, error });
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
this.dataChannel.onmessage = (event) => {
|
|
186
|
+
this.metrics.messagesReceived++;
|
|
187
|
+
this.metrics.bytesTransferred += event.data.length;
|
|
188
|
+
this.handleMessage(event.data);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create and return an offer
|
|
194
|
+
*/
|
|
195
|
+
async createOffer() {
|
|
196
|
+
const offer = await this.pc.createOffer();
|
|
197
|
+
await this.pc.setLocalDescription(offer);
|
|
198
|
+
return offer;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Handle incoming offer and create answer
|
|
203
|
+
*/
|
|
204
|
+
async handleOffer(offer) {
|
|
205
|
+
await this.pc.setRemoteDescription(new RTCSessionDescription(offer));
|
|
206
|
+
|
|
207
|
+
// Process any pending ICE candidates
|
|
208
|
+
for (const candidate of this.pendingCandidates) {
|
|
209
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
210
|
+
}
|
|
211
|
+
this.pendingCandidates = [];
|
|
212
|
+
|
|
213
|
+
const answer = await this.pc.createAnswer();
|
|
214
|
+
await this.pc.setLocalDescription(answer);
|
|
215
|
+
return answer;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Handle incoming answer
|
|
220
|
+
*/
|
|
221
|
+
async handleAnswer(answer) {
|
|
222
|
+
await this.pc.setRemoteDescription(new RTCSessionDescription(answer));
|
|
223
|
+
|
|
224
|
+
// Process any pending ICE candidates
|
|
225
|
+
for (const candidate of this.pendingCandidates) {
|
|
226
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
227
|
+
}
|
|
228
|
+
this.pendingCandidates = [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Add ICE candidate
|
|
233
|
+
*/
|
|
234
|
+
async addIceCandidate(candidate) {
|
|
235
|
+
if (this.pc.remoteDescription) {
|
|
236
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
237
|
+
} else {
|
|
238
|
+
// Queue for later
|
|
239
|
+
this.pendingCandidates.push(candidate);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Send message over data channel
|
|
245
|
+
*/
|
|
246
|
+
send(data) {
|
|
247
|
+
if (!this.dataChannel || this.dataChannel.readyState !== 'open') {
|
|
248
|
+
throw new Error('Data channel not ready');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const message = typeof data === 'string' ? data : JSON.stringify(data);
|
|
252
|
+
this.dataChannel.send(message);
|
|
253
|
+
this.metrics.messagesSent++;
|
|
254
|
+
this.metrics.bytesTransferred += message.length;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Handle incoming message
|
|
259
|
+
*/
|
|
260
|
+
handleMessage(data) {
|
|
261
|
+
try {
|
|
262
|
+
const message = JSON.parse(data);
|
|
263
|
+
|
|
264
|
+
// Handle heartbeat
|
|
265
|
+
if (message.type === 'heartbeat') {
|
|
266
|
+
this.lastHeartbeat = Date.now();
|
|
267
|
+
this.send({ type: 'heartbeat-ack', timestamp: message.timestamp });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (message.type === 'heartbeat-ack') {
|
|
272
|
+
const latency = Date.now() - message.timestamp;
|
|
273
|
+
this.metrics.latency.push(latency);
|
|
274
|
+
if (this.metrics.latency.length > 100) {
|
|
275
|
+
this.metrics.latency.shift();
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
this.emit('message', { peerId: this.peerId, message });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
// Raw string message
|
|
283
|
+
this.emit('message', { peerId: this.peerId, message: data });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Start heartbeat monitoring
|
|
289
|
+
*/
|
|
290
|
+
startHeartbeat() {
|
|
291
|
+
this.heartbeatTimer = setInterval(() => {
|
|
292
|
+
if (this.dataChannel?.readyState === 'open') {
|
|
293
|
+
this.send({ type: 'heartbeat', timestamp: Date.now() });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check for timeout
|
|
297
|
+
if (Date.now() - this.lastHeartbeat > WEBRTC_CONFIG.heartbeatTimeout) {
|
|
298
|
+
this.handleDisconnection();
|
|
299
|
+
}
|
|
300
|
+
}, WEBRTC_CONFIG.heartbeatInterval);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Handle disconnection with reconnection logic
|
|
305
|
+
*/
|
|
306
|
+
handleDisconnection() {
|
|
307
|
+
if (this.heartbeatTimer) {
|
|
308
|
+
clearInterval(this.heartbeatTimer);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (this.reconnectAttempts < WEBRTC_CONFIG.maxReconnectAttempts) {
|
|
312
|
+
this.reconnectAttempts++;
|
|
313
|
+
this.emit('reconnecting', {
|
|
314
|
+
peerId: this.peerId,
|
|
315
|
+
attempt: this.reconnectAttempts,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
setTimeout(() => {
|
|
319
|
+
this.emit('reconnect', this.peerId);
|
|
320
|
+
}, WEBRTC_CONFIG.reconnectDelay * this.reconnectAttempts);
|
|
321
|
+
} else {
|
|
322
|
+
this.emit('disconnected', this.peerId);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get connection metrics
|
|
328
|
+
*/
|
|
329
|
+
getMetrics() {
|
|
330
|
+
const avgLatency = this.metrics.latency.length > 0
|
|
331
|
+
? this.metrics.latency.reduce((a, b) => a + b, 0) / this.metrics.latency.length
|
|
332
|
+
: 0;
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
...this.metrics,
|
|
336
|
+
averageLatency: avgLatency,
|
|
337
|
+
state: this.state,
|
|
338
|
+
dataChannelState: this.dataChannel?.readyState || 'closed',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Close the connection
|
|
344
|
+
*/
|
|
345
|
+
close() {
|
|
346
|
+
if (this.heartbeatTimer) {
|
|
347
|
+
clearInterval(this.heartbeatTimer);
|
|
348
|
+
}
|
|
349
|
+
if (this.dataChannel) {
|
|
350
|
+
this.dataChannel.close();
|
|
351
|
+
}
|
|
352
|
+
if (this.pc) {
|
|
353
|
+
this.pc.close();
|
|
354
|
+
}
|
|
355
|
+
this.state = 'closed';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* WebRTC Peer Manager
|
|
361
|
+
*
|
|
362
|
+
* Manages multiple peer connections, signaling, and network topology.
|
|
363
|
+
*/
|
|
364
|
+
export class WebRTCPeerManager extends EventEmitter {
|
|
365
|
+
constructor(localIdentity, options = {}) {
|
|
366
|
+
super();
|
|
367
|
+
this.localIdentity = localIdentity;
|
|
368
|
+
this.options = { ...WEBRTC_CONFIG, ...options };
|
|
369
|
+
this.peers = new Map();
|
|
370
|
+
this.signalingSocket = null;
|
|
371
|
+
this.isConnected = false;
|
|
372
|
+
this.mode = 'initializing'; // 'webrtc', 'simulation', 'hybrid'
|
|
373
|
+
this.stats = {
|
|
374
|
+
totalConnections: 0,
|
|
375
|
+
successfulConnections: 0,
|
|
376
|
+
failedConnections: 0,
|
|
377
|
+
messagesRouted: 0,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Initialize the peer manager and connect to signaling
|
|
383
|
+
*/
|
|
384
|
+
async initialize() {
|
|
385
|
+
console.log('\n🌐 Initializing WebRTC P2P Network...');
|
|
386
|
+
|
|
387
|
+
// Try to connect to signaling server
|
|
388
|
+
const signalingConnected = await this.connectToSignaling();
|
|
389
|
+
|
|
390
|
+
if (signalingConnected) {
|
|
391
|
+
this.mode = 'webrtc';
|
|
392
|
+
console.log(' ✅ WebRTC mode active - real P2P enabled');
|
|
393
|
+
} else if (this.options.fallbackToSimulation) {
|
|
394
|
+
this.mode = 'simulation';
|
|
395
|
+
console.log(' ⚠️ Simulation mode - signaling unavailable');
|
|
396
|
+
} else {
|
|
397
|
+
throw new Error('Could not connect to signaling server');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Announce our presence
|
|
401
|
+
await this.announce();
|
|
402
|
+
|
|
403
|
+
return this;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Connect to WebSocket signaling server
|
|
408
|
+
*/
|
|
409
|
+
async connectToSignaling() {
|
|
410
|
+
// Check if WebSocket is available
|
|
411
|
+
const WebSocket = globalThis.WebSocket ||
|
|
412
|
+
(await this.loadNodeWebSocket());
|
|
413
|
+
|
|
414
|
+
if (!WebSocket) {
|
|
415
|
+
console.log(' ⚠️ WebSocket not available');
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const serverUrl of this.options.signalingServers) {
|
|
420
|
+
try {
|
|
421
|
+
const connected = await this.trySignalingServer(WebSocket, serverUrl);
|
|
422
|
+
if (connected) return true;
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.log(` ⚠️ Signaling server ${serverUrl} unavailable`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Load ws for Node.js environment
|
|
433
|
+
*/
|
|
434
|
+
async loadNodeWebSocket() {
|
|
435
|
+
try {
|
|
436
|
+
const ws = await import('ws');
|
|
437
|
+
return ws.default || ws.WebSocket;
|
|
438
|
+
} catch (err) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Try connecting to a specific signaling server
|
|
445
|
+
*/
|
|
446
|
+
async trySignalingServer(WebSocket, serverUrl) {
|
|
447
|
+
return new Promise((resolve) => {
|
|
448
|
+
const timeout = setTimeout(() => {
|
|
449
|
+
resolve(false);
|
|
450
|
+
}, 5000);
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
this.signalingSocket = new WebSocket(serverUrl);
|
|
454
|
+
|
|
455
|
+
this.signalingSocket.onopen = () => {
|
|
456
|
+
clearTimeout(timeout);
|
|
457
|
+
console.log(` 📡 Connected to signaling: ${serverUrl}`);
|
|
458
|
+
this.setupSignalingHandlers();
|
|
459
|
+
this.isConnected = true;
|
|
460
|
+
resolve(true);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
this.signalingSocket.onerror = () => {
|
|
464
|
+
clearTimeout(timeout);
|
|
465
|
+
resolve(false);
|
|
466
|
+
};
|
|
467
|
+
} catch (err) {
|
|
468
|
+
clearTimeout(timeout);
|
|
469
|
+
resolve(false);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Setup signaling socket event handlers
|
|
476
|
+
*/
|
|
477
|
+
setupSignalingHandlers() {
|
|
478
|
+
this.signalingSocket.onmessage = async (event) => {
|
|
479
|
+
try {
|
|
480
|
+
const message = JSON.parse(event.data);
|
|
481
|
+
await this.handleSignalingMessage(message);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
console.error('Signaling message error:', err);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
this.signalingSocket.onclose = () => {
|
|
488
|
+
this.isConnected = false;
|
|
489
|
+
this.emit('signaling-disconnected');
|
|
490
|
+
|
|
491
|
+
// Attempt reconnection
|
|
492
|
+
setTimeout(() => this.connectToSignaling(), 5000);
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Handle incoming signaling messages
|
|
498
|
+
*/
|
|
499
|
+
async handleSignalingMessage(message) {
|
|
500
|
+
switch (message.type) {
|
|
501
|
+
case 'peer-joined':
|
|
502
|
+
await this.handlePeerJoined(message);
|
|
503
|
+
break;
|
|
504
|
+
|
|
505
|
+
case 'offer':
|
|
506
|
+
await this.handleOffer(message);
|
|
507
|
+
break;
|
|
508
|
+
|
|
509
|
+
case 'answer':
|
|
510
|
+
await this.handleAnswer(message);
|
|
511
|
+
break;
|
|
512
|
+
|
|
513
|
+
case 'ice-candidate':
|
|
514
|
+
await this.handleIceCandidate(message);
|
|
515
|
+
break;
|
|
516
|
+
|
|
517
|
+
case 'peer-list':
|
|
518
|
+
await this.handlePeerList(message.peers);
|
|
519
|
+
break;
|
|
520
|
+
|
|
521
|
+
case 'peer-left':
|
|
522
|
+
this.handlePeerLeft(message.peerId);
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Announce presence to signaling server
|
|
529
|
+
*/
|
|
530
|
+
async announce() {
|
|
531
|
+
if (this.mode === 'simulation') {
|
|
532
|
+
// Simulate some peers
|
|
533
|
+
this.simulatePeers();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (this.signalingSocket?.readyState === 1) {
|
|
538
|
+
this.signalingSocket.send(JSON.stringify({
|
|
539
|
+
type: 'announce',
|
|
540
|
+
piKey: this.localIdentity.piKey,
|
|
541
|
+
publicKey: this.localIdentity.publicKey,
|
|
542
|
+
siteId: this.localIdentity.siteId,
|
|
543
|
+
capabilities: ['compute', 'storage', 'verify'],
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Simulate peers for offline/testing mode
|
|
550
|
+
*/
|
|
551
|
+
simulatePeers() {
|
|
552
|
+
const simulatedPeers = [
|
|
553
|
+
{ piKey: 'sim-peer-1-' + randomBytes(8).toString('hex'), siteId: 'sim-node-1' },
|
|
554
|
+
{ piKey: 'sim-peer-2-' + randomBytes(8).toString('hex'), siteId: 'sim-node-2' },
|
|
555
|
+
{ piKey: 'sim-peer-3-' + randomBytes(8).toString('hex'), siteId: 'sim-node-3' },
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
for (const peer of simulatedPeers) {
|
|
559
|
+
this.peers.set(peer.piKey, {
|
|
560
|
+
piKey: peer.piKey,
|
|
561
|
+
siteId: peer.siteId,
|
|
562
|
+
state: 'simulated',
|
|
563
|
+
lastSeen: Date.now(),
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
console.log(` 📡 Simulated ${simulatedPeers.length} peers`);
|
|
568
|
+
this.emit('peers-updated', this.getPeerList());
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Handle new peer joining
|
|
573
|
+
*/
|
|
574
|
+
async handlePeerJoined(message) {
|
|
575
|
+
const { peerId, publicKey, siteId } = message;
|
|
576
|
+
|
|
577
|
+
// Don't connect to ourselves
|
|
578
|
+
if (peerId === this.localIdentity.piKey) return;
|
|
579
|
+
|
|
580
|
+
console.log(` 🔗 New peer: ${siteId} (${peerId.slice(0, 8)}...)`);
|
|
581
|
+
|
|
582
|
+
// Initiate connection if we have higher ID (simple tiebreaker)
|
|
583
|
+
if (this.localIdentity.piKey > peerId) {
|
|
584
|
+
await this.connectToPeer(peerId);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
this.emit('peer-joined', { peerId, siteId });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Initiate connection to a peer
|
|
592
|
+
*/
|
|
593
|
+
async connectToPeer(peerId) {
|
|
594
|
+
if (this.peers.has(peerId)) return;
|
|
595
|
+
|
|
596
|
+
this.stats.totalConnections++;
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const peerConnection = new WebRTCPeerConnection(
|
|
600
|
+
peerId,
|
|
601
|
+
this.localIdentity,
|
|
602
|
+
true // initiator
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
await peerConnection.initialize();
|
|
606
|
+
this.setupPeerHandlers(peerConnection);
|
|
607
|
+
|
|
608
|
+
const offer = await peerConnection.createOffer();
|
|
609
|
+
|
|
610
|
+
// Send offer via signaling
|
|
611
|
+
this.signalingSocket.send(JSON.stringify({
|
|
612
|
+
type: 'offer',
|
|
613
|
+
to: peerId,
|
|
614
|
+
from: this.localIdentity.piKey,
|
|
615
|
+
offer,
|
|
616
|
+
}));
|
|
617
|
+
|
|
618
|
+
this.peers.set(peerId, peerConnection);
|
|
619
|
+
this.emit('peers-updated', this.getPeerList());
|
|
620
|
+
|
|
621
|
+
} catch (err) {
|
|
622
|
+
this.stats.failedConnections++;
|
|
623
|
+
console.error(`Failed to connect to ${peerId}:`, err.message);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Handle incoming offer
|
|
629
|
+
*/
|
|
630
|
+
async handleOffer(message) {
|
|
631
|
+
const { from, offer } = message;
|
|
632
|
+
|
|
633
|
+
if (this.peers.has(from)) return;
|
|
634
|
+
|
|
635
|
+
this.stats.totalConnections++;
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
const peerConnection = new WebRTCPeerConnection(
|
|
639
|
+
from,
|
|
640
|
+
this.localIdentity,
|
|
641
|
+
false // not initiator
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
await peerConnection.initialize();
|
|
645
|
+
this.setupPeerHandlers(peerConnection);
|
|
646
|
+
|
|
647
|
+
const answer = await peerConnection.handleOffer(offer);
|
|
648
|
+
|
|
649
|
+
// Send answer via signaling
|
|
650
|
+
this.signalingSocket.send(JSON.stringify({
|
|
651
|
+
type: 'answer',
|
|
652
|
+
to: from,
|
|
653
|
+
from: this.localIdentity.piKey,
|
|
654
|
+
answer,
|
|
655
|
+
}));
|
|
656
|
+
|
|
657
|
+
this.peers.set(from, peerConnection);
|
|
658
|
+
this.emit('peers-updated', this.getPeerList());
|
|
659
|
+
|
|
660
|
+
} catch (err) {
|
|
661
|
+
this.stats.failedConnections++;
|
|
662
|
+
console.error(`Failed to handle offer from ${from}:`, err.message);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Handle incoming answer
|
|
668
|
+
*/
|
|
669
|
+
async handleAnswer(message) {
|
|
670
|
+
const { from, answer } = message;
|
|
671
|
+
const peerConnection = this.peers.get(from);
|
|
672
|
+
|
|
673
|
+
if (peerConnection) {
|
|
674
|
+
await peerConnection.handleAnswer(answer);
|
|
675
|
+
this.stats.successfulConnections++;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Handle ICE candidate
|
|
681
|
+
*/
|
|
682
|
+
async handleIceCandidate(message) {
|
|
683
|
+
const { from, candidate } = message;
|
|
684
|
+
const peerConnection = this.peers.get(from);
|
|
685
|
+
|
|
686
|
+
if (peerConnection) {
|
|
687
|
+
await peerConnection.addIceCandidate(candidate);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Handle peer list from server
|
|
693
|
+
*/
|
|
694
|
+
async handlePeerList(peers) {
|
|
695
|
+
for (const peer of peers) {
|
|
696
|
+
if (peer.piKey !== this.localIdentity.piKey && !this.peers.has(peer.piKey)) {
|
|
697
|
+
await this.connectToPeer(peer.piKey);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Handle peer leaving
|
|
704
|
+
*/
|
|
705
|
+
handlePeerLeft(peerId) {
|
|
706
|
+
const peer = this.peers.get(peerId);
|
|
707
|
+
if (peer) {
|
|
708
|
+
if (peer.close) peer.close();
|
|
709
|
+
this.peers.delete(peerId);
|
|
710
|
+
this.emit('peer-left', peerId);
|
|
711
|
+
this.emit('peers-updated', this.getPeerList());
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Setup event handlers for a peer connection
|
|
717
|
+
*/
|
|
718
|
+
setupPeerHandlers(peerConnection) {
|
|
719
|
+
peerConnection.on('ice-candidate', ({ candidate }) => {
|
|
720
|
+
if (this.signalingSocket?.readyState === 1) {
|
|
721
|
+
this.signalingSocket.send(JSON.stringify({
|
|
722
|
+
type: 'ice-candidate',
|
|
723
|
+
to: peerConnection.peerId,
|
|
724
|
+
from: this.localIdentity.piKey,
|
|
725
|
+
candidate,
|
|
726
|
+
}));
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
peerConnection.on('channel-open', () => {
|
|
731
|
+
this.stats.successfulConnections++;
|
|
732
|
+
this.emit('peer-connected', peerConnection.peerId);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
peerConnection.on('message', ({ message }) => {
|
|
736
|
+
this.stats.messagesRouted++;
|
|
737
|
+
this.emit('message', {
|
|
738
|
+
from: peerConnection.peerId,
|
|
739
|
+
message,
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
peerConnection.on('disconnected', () => {
|
|
744
|
+
this.peers.delete(peerConnection.peerId);
|
|
745
|
+
this.emit('peer-disconnected', peerConnection.peerId);
|
|
746
|
+
this.emit('peers-updated', this.getPeerList());
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
peerConnection.on('reconnect', async (peerId) => {
|
|
750
|
+
this.peers.delete(peerId);
|
|
751
|
+
await this.connectToPeer(peerId);
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Send message to a specific peer
|
|
757
|
+
*/
|
|
758
|
+
sendToPeer(peerId, message) {
|
|
759
|
+
const peer = this.peers.get(peerId);
|
|
760
|
+
if (peer && peer.send) {
|
|
761
|
+
peer.send(message);
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Broadcast message to all peers
|
|
769
|
+
*/
|
|
770
|
+
broadcast(message) {
|
|
771
|
+
let sent = 0;
|
|
772
|
+
for (const [peerId, peer] of this.peers) {
|
|
773
|
+
try {
|
|
774
|
+
if (peer.send) {
|
|
775
|
+
peer.send(message);
|
|
776
|
+
sent++;
|
|
777
|
+
}
|
|
778
|
+
} catch (err) {
|
|
779
|
+
// Peer not ready
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return sent;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Get list of connected peers
|
|
787
|
+
*/
|
|
788
|
+
getPeerList() {
|
|
789
|
+
const peers = [];
|
|
790
|
+
for (const [peerId, peer] of this.peers) {
|
|
791
|
+
peers.push({
|
|
792
|
+
peerId,
|
|
793
|
+
state: peer.state || 'simulated',
|
|
794
|
+
siteId: peer.siteId,
|
|
795
|
+
lastSeen: peer.lastSeen || Date.now(),
|
|
796
|
+
metrics: peer.getMetrics ? peer.getMetrics() : null,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
return peers;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Get connection statistics
|
|
804
|
+
*/
|
|
805
|
+
getStats() {
|
|
806
|
+
return {
|
|
807
|
+
...this.stats,
|
|
808
|
+
mode: this.mode,
|
|
809
|
+
connectedPeers: this.peers.size,
|
|
810
|
+
signalingConnected: this.isConnected,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Close all connections
|
|
816
|
+
*/
|
|
817
|
+
close() {
|
|
818
|
+
for (const [, peer] of this.peers) {
|
|
819
|
+
if (peer.close) peer.close();
|
|
820
|
+
}
|
|
821
|
+
this.peers.clear();
|
|
822
|
+
|
|
823
|
+
if (this.signalingSocket) {
|
|
824
|
+
this.signalingSocket.close();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* QDAG Synchronizer
|
|
831
|
+
*
|
|
832
|
+
* Synchronizes QDAG contributions over WebRTC data channels.
|
|
833
|
+
*/
|
|
834
|
+
export class QDAGSynchronizer extends EventEmitter {
|
|
835
|
+
constructor(peerManager, qdag) {
|
|
836
|
+
super();
|
|
837
|
+
this.peerManager = peerManager;
|
|
838
|
+
this.qdag = qdag;
|
|
839
|
+
this.syncState = new Map(); // Track sync state per peer
|
|
840
|
+
this.pendingSync = new Set();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Initialize synchronization
|
|
845
|
+
*/
|
|
846
|
+
initialize() {
|
|
847
|
+
// Listen for new peer connections
|
|
848
|
+
this.peerManager.on('peer-connected', (peerId) => {
|
|
849
|
+
this.requestSync(peerId);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// Listen for sync messages
|
|
853
|
+
this.peerManager.on('message', ({ from, message }) => {
|
|
854
|
+
this.handleSyncMessage(from, message);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// Periodic sync
|
|
858
|
+
setInterval(() => this.syncWithPeers(), 10000);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Request QDAG sync from a peer
|
|
863
|
+
*/
|
|
864
|
+
requestSync(peerId) {
|
|
865
|
+
const lastSync = this.syncState.get(peerId) || 0;
|
|
866
|
+
|
|
867
|
+
this.peerManager.sendToPeer(peerId, {
|
|
868
|
+
type: 'qdag_sync_request',
|
|
869
|
+
since: lastSync,
|
|
870
|
+
myTip: this.qdag?.getLatestHash() || null,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
this.pendingSync.add(peerId);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Handle incoming sync messages
|
|
878
|
+
*/
|
|
879
|
+
handleSyncMessage(from, message) {
|
|
880
|
+
if (message.type === 'qdag_sync_request') {
|
|
881
|
+
this.handleSyncRequest(from, message);
|
|
882
|
+
} else if (message.type === 'qdag_sync_response') {
|
|
883
|
+
this.handleSyncResponse(from, message);
|
|
884
|
+
} else if (message.type === 'qdag_contribution') {
|
|
885
|
+
this.handleNewContribution(from, message);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Handle sync request from peer
|
|
891
|
+
*/
|
|
892
|
+
handleSyncRequest(from, message) {
|
|
893
|
+
const contributions = this.qdag?.getContributionsSince(message.since) || [];
|
|
894
|
+
|
|
895
|
+
this.peerManager.sendToPeer(from, {
|
|
896
|
+
type: 'qdag_sync_response',
|
|
897
|
+
contributions,
|
|
898
|
+
tip: this.qdag?.getLatestHash() || null,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Handle sync response from peer
|
|
904
|
+
*/
|
|
905
|
+
handleSyncResponse(from, message) {
|
|
906
|
+
this.pendingSync.delete(from);
|
|
907
|
+
this.syncState.set(from, Date.now());
|
|
908
|
+
|
|
909
|
+
if (message.contributions && message.contributions.length > 0) {
|
|
910
|
+
let added = 0;
|
|
911
|
+
for (const contrib of message.contributions) {
|
|
912
|
+
if (this.qdag?.addContribution(contrib)) {
|
|
913
|
+
added++;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (added > 0) {
|
|
918
|
+
this.emit('synced', { from, added });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Handle new contribution broadcast
|
|
925
|
+
*/
|
|
926
|
+
handleNewContribution(from, message) {
|
|
927
|
+
if (this.qdag?.addContribution(message.contribution)) {
|
|
928
|
+
this.emit('contribution-received', {
|
|
929
|
+
from,
|
|
930
|
+
contribution: message.contribution,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Broadcast a new contribution to all peers
|
|
937
|
+
*/
|
|
938
|
+
broadcastContribution(contribution) {
|
|
939
|
+
this.peerManager.broadcast({
|
|
940
|
+
type: 'qdag_contribution',
|
|
941
|
+
contribution,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Sync with all connected peers
|
|
947
|
+
*/
|
|
948
|
+
syncWithPeers() {
|
|
949
|
+
const peers = this.peerManager.getPeerList();
|
|
950
|
+
for (const peer of peers) {
|
|
951
|
+
if (!this.pendingSync.has(peer.peerId)) {
|
|
952
|
+
this.requestSync(peer.peerId);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Export default configuration for testing
|
|
959
|
+
export default {
|
|
960
|
+
WebRTCPeerConnection,
|
|
961
|
+
WebRTCPeerManager,
|
|
962
|
+
QDAGSynchronizer,
|
|
963
|
+
WEBRTC_CONFIG,
|
|
964
|
+
};
|