@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/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
+ };