@edryslabs/genericprovider 1.0.1

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.
Files changed (63) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +660 -0
  3. package/dist/index.d.ts +323 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +1001 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib.d.ts +36 -0
  8. package/dist/lib.d.ts.map +1 -0
  9. package/dist/lib.js +37 -0
  10. package/dist/lib.js.map +1 -0
  11. package/dist/providers/dummy/index.d.ts +226 -0
  12. package/dist/providers/dummy/index.d.ts.map +1 -0
  13. package/dist/providers/dummy/index.js +326 -0
  14. package/dist/providers/dummy/index.js.map +1 -0
  15. package/dist/providers/gun/index.d.ts +269 -0
  16. package/dist/providers/gun/index.d.ts.map +1 -0
  17. package/dist/providers/gun/index.js +683 -0
  18. package/dist/providers/gun/index.js.map +1 -0
  19. package/dist/providers/indexeddb/index.d.ts +161 -0
  20. package/dist/providers/indexeddb/index.d.ts.map +1 -0
  21. package/dist/providers/indexeddb/index.js +369 -0
  22. package/dist/providers/indexeddb/index.js.map +1 -0
  23. package/dist/providers/matrix/index.d.ts +109 -0
  24. package/dist/providers/matrix/index.d.ts.map +1 -0
  25. package/dist/providers/matrix/index.js +329 -0
  26. package/dist/providers/matrix/index.js.map +1 -0
  27. package/dist/providers/nostr/index.d.ts +172 -0
  28. package/dist/providers/nostr/index.d.ts.map +1 -0
  29. package/dist/providers/nostr/index.js +280 -0
  30. package/dist/providers/nostr/index.js.map +1 -0
  31. package/dist/providers/peerjs/index.d.ts +231 -0
  32. package/dist/providers/peerjs/index.d.ts.map +1 -0
  33. package/dist/providers/peerjs/index.js +1038 -0
  34. package/dist/providers/peerjs/index.js.map +1 -0
  35. package/dist/providers/pubnub/index.d.ts +106 -0
  36. package/dist/providers/pubnub/index.d.ts.map +1 -0
  37. package/dist/providers/pubnub/index.js +357 -0
  38. package/dist/providers/pubnub/index.js.map +1 -0
  39. package/dist/providers/simple-peer/index.d.ts +253 -0
  40. package/dist/providers/simple-peer/index.d.ts.map +1 -0
  41. package/dist/providers/simple-peer/index.js +783 -0
  42. package/dist/providers/simple-peer/index.js.map +1 -0
  43. package/dist/providers/supabase/index.d.ts +80 -0
  44. package/dist/providers/supabase/index.d.ts.map +1 -0
  45. package/dist/providers/supabase/index.js +202 -0
  46. package/dist/providers/supabase/index.js.map +1 -0
  47. package/dist/providers/trystero/index.d.ts +181 -0
  48. package/dist/providers/trystero/index.d.ts.map +1 -0
  49. package/dist/providers/trystero/index.js +187 -0
  50. package/dist/providers/trystero/index.js.map +1 -0
  51. package/dist/providers/websocket/index.d.ts +92 -0
  52. package/dist/providers/websocket/index.d.ts.map +1 -0
  53. package/dist/providers/websocket/index.js +272 -0
  54. package/dist/providers/websocket/index.js.map +1 -0
  55. package/dist/sync-monitor.d.ts +90 -0
  56. package/dist/sync-monitor.d.ts.map +1 -0
  57. package/dist/sync-monitor.js +149 -0
  58. package/dist/sync-monitor.js.map +1 -0
  59. package/dist/transport.d.ts +116 -0
  60. package/dist/transport.d.ts.map +1 -0
  61. package/dist/transport.js +2 -0
  62. package/dist/transport.js.map +1 -0
  63. package/package.json +137 -0
@@ -0,0 +1,783 @@
1
+ /**
2
+ * SimplePeer Transport Provider
3
+ *
4
+ * Peer-to-peer transport using WebRTC data channels with simple-peer library.
5
+ * Connects directly to other clients without going through a central server.
6
+ *
7
+ * Features:
8
+ * - Direct peer-to-peer connections
9
+ * - Mesh network (each peer connects to multiple others)
10
+ * - Uses signaling server only for peer discovery (not for data)
11
+ * - Optional encryption
12
+ * - Automatic connection management
13
+ * - Resilient to peer disconnections
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { GenericProvider } from 'y-generic'
18
+ * import { SimplePeerTransport } from 'y-generic/providers/simple-peer'
19
+ * import Peer from 'simple-peer'
20
+ *
21
+ * const doc = new Y.Doc()
22
+ * const transport = new SimplePeerTransport({
23
+ * peer: Peer, // Pass the simple-peer constructor
24
+ * signaling: ['wss://signaling.example.com'],
25
+ * password: 'optional-encryption-key'
26
+ * })
27
+ * const provider = new GenericProvider(doc, transport)
28
+ * await provider.connect({ room: 'my-room' })
29
+ * ```
30
+ */
31
+ /**
32
+ * Maximum chunk size for WebRTC DataChannel messages.
33
+ * Most browsers support up to 256KB, but we use 64KB for safety.
34
+ */
35
+ const CHUNK_SIZE = 64 * 1024;
36
+ /**
37
+ * Maximum buffer size before we pause sending (16KB).
38
+ * WebRTC DataChannel buffers data before sending over the network.
39
+ * If we send too much too fast, the buffer overflows.
40
+ */
41
+ const MAX_BUFFERED_AMOUNT = 16 * 1024;
42
+ /**
43
+ * Message type markers for chunking protocol.
44
+ * - 0x00: Complete message (no chunking, raw data)
45
+ * - 0x01: Chunked message with header
46
+ * - 0x02: Consumer control frame — per-peer side-channel via sendControl()/
47
+ * onControlFrame(), never reaches the provider pipe. Not chunked/encrypted.
48
+ */
49
+ const MSG_TYPE_COMPLETE = 0x00;
50
+ const MSG_TYPE_CHUNKED = 0x01;
51
+ const MSG_TYPE_CONTROL = 0x02;
52
+ /** Counter for generating unique message IDs */
53
+ let messageIdCounter = 0;
54
+ /**
55
+ * SimplePeer transport implementation using simple-peer library.
56
+ * Creates direct peer-to-peer connections for data transfer.
57
+ */
58
+ export class SimplePeerTransport {
59
+ /**
60
+ * Create a new SimplePeer transport.
61
+ *
62
+ * @param options - Configuration options (must include peer constructor)
63
+ */
64
+ constructor(options) {
65
+ this._connected = false;
66
+ this._room = '';
67
+ this._peerConnectCallbacks = new Set();
68
+ this._peerDisconnectCallbacks = new Set();
69
+ this._controlCallbacks = new Set();
70
+ this.peers = new Map();
71
+ this.signalingConns = [];
72
+ this.announcedPeers = new Set();
73
+ if (!options.peer) {
74
+ throw new Error('SimplePeerTransport requires the "peer" option. ' +
75
+ 'Please provide the simple-peer constructor: ' +
76
+ 'import Peer from "simple-peer"; new SimplePeerTransport({ peer: Peer, ... })');
77
+ }
78
+ // Default ICE servers (Google's public STUN server)
79
+ const defaultIceServers = [
80
+ { urls: 'stun:stun.l.google.com:19302' },
81
+ ];
82
+ // Prepare peerOpts with ICE servers
83
+ const peerOpts = { ...options.peerOpts };
84
+ // Merge ICE servers into peerOpts.config
85
+ if (options.iceServers || !peerOpts.config?.iceServers) {
86
+ const iceServers = options.iceServers ?? defaultIceServers;
87
+ peerOpts.config = {
88
+ ...peerOpts.config,
89
+ iceServers: iceServers,
90
+ };
91
+ }
92
+ this.options = {
93
+ peer: options.peer,
94
+ signaling: options.signaling ?? ['wss://signaling.yjs.dev'],
95
+ password: options.password ?? '',
96
+ maxConns: options.maxConns ?? 20 + Math.floor(Math.random() * 15),
97
+ peerOpts,
98
+ debug: options.debug ?? false,
99
+ };
100
+ // Generate unique peer ID
101
+ this.peerId = this.generatePeerId();
102
+ this.log(`Initialized — peerId: ${this.peerId}, maxConns: ${this.options.maxConns}`, `\n signaling: [${this.options.signaling.join(', ')}]`, `\n iceServers: [${(peerOpts.config?.iceServers ?? []).map((s) => (Array.isArray(s.urls) ? s.urls[0] : s.urls)).join(', ')}]`);
103
+ }
104
+ /**
105
+ * Connect to the room via signaling servers and start discovering peers.
106
+ */
107
+ async connect(config) {
108
+ if (this._connected) {
109
+ throw new Error('Already connected');
110
+ }
111
+ this._room = config.room;
112
+ this.log(`🔌 Connecting to room "${config.room}" as ${this.peerId}`);
113
+ // Try to connect to signaling servers
114
+ // Use Promise.allSettled to allow partial success
115
+ const results = await Promise.allSettled(this.options.signaling.map((url) => this.connectSignaling(url)));
116
+ // Count successful connections
117
+ const successCount = results.filter((r) => r.status === 'fulfilled').length;
118
+ const failCount = results.filter((r) => r.status === 'rejected').length;
119
+ if (successCount > 0) {
120
+ this.log(`📡 Signaling: ${successCount}/${this.options.signaling.length} server(s) connected`);
121
+ }
122
+ else {
123
+ console.warn('[SimplePeerTransport] ⚠️ No signaling servers reachable — WebRTC peer discovery disabled. ' +
124
+ 'Cross-tab sync via BroadcastChannel will still work.');
125
+ results.forEach((result, index) => {
126
+ if (result.status === 'rejected') {
127
+ console.warn(`[SimplePeerTransport] ✗ ${this.options.signaling[index]}:`, result.reason?.message ?? result.reason);
128
+ }
129
+ });
130
+ }
131
+ // Still mark as connected even if no signaling servers work
132
+ // This allows BroadcastChannel-only mode for same-browser tabs
133
+ this._connected = true;
134
+ this.log(`✅ Connected to room "${this._room}" — ${this.signalingConns.length}/${this.options.signaling.length} signaling server(s)`);
135
+ // Start periodic re-announce to help late joiners discover us
136
+ this.announceInterval = setInterval(() => {
137
+ if (this.peers.size < this.options.maxConns &&
138
+ this.signalingConns.length > 0) {
139
+ this.log('Re-announcing presence...');
140
+ for (const ws of this.signalingConns) {
141
+ this.sendSignaling(ws, {
142
+ type: 'publish',
143
+ topic: this._room,
144
+ from: this.peerId,
145
+ });
146
+ }
147
+ }
148
+ }, 5000); // Re-announce every 5 seconds for better peer discovery
149
+ }
150
+ /**
151
+ * Disconnect from all peers and signaling servers.
152
+ */
153
+ disconnect() {
154
+ if (!this._connected)
155
+ return;
156
+ this.log(`🔌 Disconnecting — ${this.peers.size} peer(s), ${this.signalingConns.length} signaling server(s)`);
157
+ // Stop re-announce interval
158
+ if (this.announceInterval) {
159
+ clearInterval(this.announceInterval);
160
+ this.announceInterval = undefined;
161
+ }
162
+ // Close all peer connections
163
+ for (const peerConn of this.peers.values()) {
164
+ peerConn.peer.destroy();
165
+ }
166
+ this.peers.clear();
167
+ // Close all signaling connections
168
+ for (const ws of this.signalingConns) {
169
+ ws.close();
170
+ }
171
+ this.signalingConns = [];
172
+ this._connected = false;
173
+ this.announcedPeers.clear();
174
+ }
175
+ /**
176
+ * Send data to all connected peers.
177
+ * Large messages are automatically chunked to fit within WebRTC DataChannel limits.
178
+ */
179
+ send(data) {
180
+ if (!this._connected) {
181
+ this.log('Not connected, cannot send');
182
+ return;
183
+ }
184
+ // Encrypt if password is set
185
+ const dataToSend = this.options.password
186
+ ? this.encrypt(data, this.options.password)
187
+ : data;
188
+ // Send to all connected peers
189
+ let sentCount = 0;
190
+ for (const peerConn of this.peers.values()) {
191
+ if (peerConn.connected) {
192
+ try {
193
+ this.sendToPeer(peerConn, dataToSend);
194
+ sentCount++;
195
+ }
196
+ catch (error) {
197
+ this.log(`❌ Send failed to ${peerConn.peerId}:`, error.message);
198
+ }
199
+ }
200
+ }
201
+ // Only log when the picture is non-trivial (missing peers or no peers at all)
202
+ if (sentCount === 0) {
203
+ this.log(`⚠️ Send: 0 peers connected — ${data.length}B dropped`);
204
+ }
205
+ else if (sentCount < this.peers.size) {
206
+ const skipped = this.peers.size - sentCount;
207
+ this.log(`📤 Sent ${data.length}B to ${sentCount}/${this.peers.size} peer(s) — ${skipped} not yet connected`);
208
+ }
209
+ }
210
+ /**
211
+ * Send data to a single connected peer by ID (targeted delivery).
212
+ */
213
+ sendTo(peerId, data) {
214
+ if (!this._connected) {
215
+ this.log('Not connected, cannot sendTo');
216
+ return;
217
+ }
218
+ const peerConn = this.peers.get(peerId);
219
+ if (!peerConn || !peerConn.connected) {
220
+ this.log(`⚠️ sendTo: peer ${peerId} not connected — ${data.length}B dropped`);
221
+ return;
222
+ }
223
+ const dataToSend = this.options.password
224
+ ? this.encrypt(data, this.options.password)
225
+ : data;
226
+ try {
227
+ this.sendToPeer(peerConn, dataToSend);
228
+ }
229
+ catch (error) {
230
+ this.log(`❌ sendTo failed to ${peerId}:`, error.message);
231
+ }
232
+ }
233
+ /**
234
+ * Send data to a single peer, chunking if necessary.
235
+ * Uses flow control to avoid overwhelming the WebRTC buffer.
236
+ */
237
+ sendToPeer(peerConn, data) {
238
+ // Small messages can be sent directly with type marker
239
+ if (data.length <= CHUNK_SIZE - 1) {
240
+ const msg = new Uint8Array(data.length + 1);
241
+ msg[0] = MSG_TYPE_COMPLETE;
242
+ msg.set(data, 1);
243
+ peerConn.peer.send(msg);
244
+ return;
245
+ }
246
+ // Large messages need chunking with flow control
247
+ const messageId = messageIdCounter++;
248
+ const totalChunks = Math.ceil(data.length / (CHUNK_SIZE - 13));
249
+ this.log(`📦 Chunking ${data.length}B → ${totalChunks} chunks (msgId=${messageId})`);
250
+ // Queue all chunks and send with backpressure handling
251
+ const chunks = [];
252
+ for (let i = 0; i < totalChunks; i++) {
253
+ const start = i * (CHUNK_SIZE - 13);
254
+ const end = Math.min(start + (CHUNK_SIZE - 13), data.length);
255
+ const chunkData = data.slice(start, end);
256
+ // Chunk header: [type:1][messageId:4][chunkIndex:4][totalChunks:4][data]
257
+ const chunk = new Uint8Array(13 + chunkData.length);
258
+ chunk[0] = MSG_TYPE_CHUNKED;
259
+ new DataView(chunk.buffer).setUint32(1, messageId, true);
260
+ new DataView(chunk.buffer).setUint32(5, i, true);
261
+ new DataView(chunk.buffer).setUint32(9, totalChunks, true);
262
+ chunk.set(chunkData, 13);
263
+ chunks.push(chunk);
264
+ }
265
+ // Send chunks with flow control
266
+ this.sendChunksWithFlowControl(peerConn, chunks);
267
+ }
268
+ /**
269
+ * Send chunks with backpressure handling.
270
+ * Waits for buffer to drain before sending more data.
271
+ */
272
+ sendChunksWithFlowControl(peerConn, chunks) {
273
+ let index = 0;
274
+ const peer = peerConn.peer;
275
+ const sendNext = () => {
276
+ while (index < chunks.length) {
277
+ // Check if buffer is too full
278
+ const channel = peer._channel;
279
+ if (channel && channel.bufferedAmount > MAX_BUFFERED_AMOUNT) {
280
+ // Wait for buffer to drain
281
+ this.log(`⏸️ Backpressure on ${peerConn.peerId}: buffered ${channel.bufferedAmount}B, waiting...`);
282
+ channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT / 2;
283
+ channel.onbufferedamountlow = () => {
284
+ channel.onbufferedamountlow = null;
285
+ this.log(`▶️ Buffer drained on ${peerConn.peerId}, resuming chunks`);
286
+ sendNext();
287
+ };
288
+ return;
289
+ }
290
+ // Send next chunk
291
+ try {
292
+ peer.send(chunks[index]);
293
+ index++;
294
+ }
295
+ catch (error) {
296
+ this.log('Error sending chunk:', index, error);
297
+ return;
298
+ }
299
+ }
300
+ };
301
+ sendNext();
302
+ }
303
+ /**
304
+ * Register callback for incoming messages.
305
+ */
306
+ onMessage(callback) {
307
+ this._callback = callback;
308
+ return () => {
309
+ this._callback = undefined;
310
+ };
311
+ }
312
+ /**
313
+ * Register callback for new peer data-channel connections.
314
+ */
315
+ onPeerConnect(callback) {
316
+ this._peerConnectCallbacks.add(callback);
317
+ return () => {
318
+ this._peerConnectCallbacks.delete(callback);
319
+ };
320
+ }
321
+ /**
322
+ * Register callback for peer disconnects (channel close or error). Only fires
323
+ * for peers that had reached the connected state.
324
+ */
325
+ onPeerDisconnect(callback) {
326
+ this._peerDisconnectCallbacks.add(callback);
327
+ return () => {
328
+ this._peerDisconnectCallbacks.delete(callback);
329
+ };
330
+ }
331
+ /**
332
+ * Register callback for consumer control frames (MSG_TYPE_CONTROL).
333
+ * These bypass the provider pipe — use for per-peer handshakes/auth.
334
+ */
335
+ onControlFrame(callback) {
336
+ this._controlCallbacks.add(callback);
337
+ return () => {
338
+ this._controlCallbacks.delete(callback);
339
+ };
340
+ }
341
+ /**
342
+ * Tear down a single peer connection (e.g. to reject a peer that failed an
343
+ * out-of-band handshake). Fires onPeerDisconnect if the peer was connected.
344
+ */
345
+ disconnectPeer(peerId) {
346
+ this.removePeer(peerId);
347
+ }
348
+ /**
349
+ * Send a control frame to a single peer. Not chunked or encrypted — keep
350
+ * payloads small (they must fit one DataChannel message).
351
+ */
352
+ sendControl(peerId, payload) {
353
+ const peerConn = this.peers.get(peerId);
354
+ if (!peerConn || !peerConn.connected) {
355
+ this.log(`⚠️ sendControl: peer ${peerId} not connected — dropped`);
356
+ return;
357
+ }
358
+ const msg = new Uint8Array(payload.length + 1);
359
+ msg[0] = MSG_TYPE_CONTROL;
360
+ msg.set(payload, 1);
361
+ try {
362
+ peerConn.peer.send(msg);
363
+ }
364
+ catch (error) {
365
+ this.log(`❌ sendControl failed to ${peerId}:`, error.message);
366
+ }
367
+ }
368
+ /**
369
+ * Check if connected.
370
+ */
371
+ get isConnected() {
372
+ return this._connected;
373
+ }
374
+ /**
375
+ * Get number of connected peers (for debugging).
376
+ */
377
+ get connectedPeers() {
378
+ return Array.from(this.peers.values()).filter((p) => p.connected).length;
379
+ }
380
+ /**
381
+ * Connect to a signaling server.
382
+ */
383
+ async connectSignaling(url) {
384
+ return new Promise((resolve, reject) => {
385
+ const ws = new WebSocket(url);
386
+ let resolved = false;
387
+ ws.onopen = () => {
388
+ this.log(`🟢 Signaling connected: ${url}`);
389
+ // Subscribe to room
390
+ this.sendSignaling(ws, {
391
+ type: 'subscribe',
392
+ topics: [this._room],
393
+ });
394
+ // Announce presence with topic (room) for y-webrtc protocol
395
+ if (this.peers.size < this.options.maxConns) {
396
+ // y-webrtc uses 'publish' with from field
397
+ this.sendSignaling(ws, {
398
+ type: 'publish',
399
+ topic: this._room,
400
+ from: this.peerId,
401
+ });
402
+ // Also send announce for basic protocol compatibility
403
+ this.sendSignaling(ws, {
404
+ type: 'announce',
405
+ from: this.peerId,
406
+ topic: this._room,
407
+ });
408
+ }
409
+ this.signalingConns.push(ws);
410
+ if (!resolved) {
411
+ resolved = true;
412
+ resolve();
413
+ }
414
+ };
415
+ ws.onmessage = (event) => {
416
+ try {
417
+ const msg = JSON.parse(event.data);
418
+ // Only log signal-bearing messages to avoid flooding with pure topology pings
419
+ if (msg.signal || msg.type === 'announce') {
420
+ this.log(`📩 Signaling ‹${msg.type}›`, msg.from ? `from=${msg.from.slice(0, 8)}` : '', msg.to ? `to=${msg.to.slice(0, 8)}` : '', msg.signal ? `signal=${msg.signal.type ?? 'candidate'}` : '');
421
+ }
422
+ this.handleSignalingMessage(msg);
423
+ }
424
+ catch (error) {
425
+ this.log(`❌ Failed to parse signaling message: ${error.message} raw=${event.data}`);
426
+ }
427
+ };
428
+ ws.onerror = (error) => {
429
+ this.log(`❌ Signaling error: ${url}`, error);
430
+ if (!resolved) {
431
+ resolved = true;
432
+ reject(error);
433
+ }
434
+ };
435
+ ws.onclose = () => {
436
+ this.log(`🔴 Signaling disconnected: ${url}`);
437
+ const index = this.signalingConns.indexOf(ws);
438
+ if (index > -1) {
439
+ this.signalingConns.splice(index, 1);
440
+ }
441
+ };
442
+ // Timeout after 10 seconds
443
+ setTimeout(() => {
444
+ if (!resolved) {
445
+ resolved = true;
446
+ this.log(`⏱️ Signaling connection timeout: ${url}`);
447
+ reject(new Error('Signaling connection timeout'));
448
+ }
449
+ }, 10000);
450
+ });
451
+ }
452
+ /**
453
+ * Handle messages from signaling server.
454
+ */
455
+ handleSignalingMessage(msg) {
456
+ // Skip messages from ourselves
457
+ if (msg.from && msg.from === this.peerId)
458
+ return;
459
+ switch (msg.type) {
460
+ case 'publish':
461
+ // y-webrtc protocol: messages are published to topics
462
+ // This is an envelope, the actual message could be an announce or signal
463
+ if (msg.from) {
464
+ // Treat as announce if it's a publish from another peer
465
+ if (!this.peers.has(msg.from) &&
466
+ this.peers.size < this.options.maxConns &&
467
+ !this.announcedPeers.has(msg.from)) {
468
+ const shouldInitiate = this.peerId > msg.from;
469
+ this.log(`📡 Peer discovered via publish: ${msg.from} — role: ${shouldInitiate ? 'initiator' : 'non-initiator'}`, `(peers: ${this.peers.size + 1}/${this.options.maxConns})`);
470
+ this.announcedPeers.add(msg.from);
471
+ this.createPeerConnection(msg.from, shouldInitiate);
472
+ // If we're NOT the initiator, immediately re-announce so the initiator
473
+ // can discover us (they may have missed our initial publish)
474
+ if (!shouldInitiate) {
475
+ this.log('📢 Re-announcing so initiator can find us...');
476
+ for (const ws of this.signalingConns) {
477
+ this.sendSignaling(ws, {
478
+ type: 'publish',
479
+ topic: this._room,
480
+ from: this.peerId,
481
+ });
482
+ }
483
+ }
484
+ }
485
+ }
486
+ // Handle embedded signal if present
487
+ if (msg.signal && msg.from) {
488
+ if (!msg.to || msg.to === this.peerId) {
489
+ this.handlePeerSignal(msg.from, msg.signal);
490
+ }
491
+ }
492
+ break;
493
+ case 'announce':
494
+ if (!msg.from) {
495
+ this.log('Announce message missing from field');
496
+ return;
497
+ }
498
+ // Another peer announced - connect to them if we have capacity
499
+ if (!this.peers.has(msg.from) &&
500
+ this.peers.size < this.options.maxConns &&
501
+ !this.announcedPeers.has(msg.from)) {
502
+ const shouldInitiate = this.peerId > msg.from;
503
+ this.log(`📡 Peer announced: ${msg.from} — role: ${shouldInitiate ? 'initiator' : 'non-initiator'}`, `(peers: ${this.peers.size + 1}/${this.options.maxConns})`);
504
+ this.announcedPeers.add(msg.from);
505
+ this.createPeerConnection(msg.from, shouldInitiate);
506
+ }
507
+ else if (this.peers.size >= this.options.maxConns) {
508
+ this.log(`⚠️ Peer limit reached (${this.options.maxConns}), ignoring announce from ${msg.from}`);
509
+ }
510
+ break;
511
+ case 'signal':
512
+ if (!msg.from) {
513
+ this.log('Signal message missing from field');
514
+ return;
515
+ }
516
+ // Received WebRTC signal from peer
517
+ if (msg.to === this.peerId && msg.signal) {
518
+ this.handlePeerSignal(msg.from, msg.signal);
519
+ }
520
+ else if (!msg.to && msg.signal) {
521
+ // Signal without explicit target — handle anyway (some servers strip `to`)
522
+ this.handlePeerSignal(msg.from, msg.signal);
523
+ }
524
+ break;
525
+ default:
526
+ this.log(`❓ Unknown signaling message type: ${msg.type}`);
527
+ }
528
+ }
529
+ /**
530
+ * Send message to signaling server.
531
+ */
532
+ sendSignaling(ws, msg) {
533
+ if (ws.readyState === WebSocket.OPEN) {
534
+ ws.send(JSON.stringify(msg));
535
+ }
536
+ }
537
+ /**
538
+ * Broadcast message to all signaling servers.
539
+ */
540
+ broadcastSignaling(msg) {
541
+ for (const ws of this.signalingConns) {
542
+ this.sendSignaling(ws, msg);
543
+ }
544
+ }
545
+ /**
546
+ * Create a WebRTC peer connection.
547
+ */
548
+ createPeerConnection(remotePeerId, initiator) {
549
+ if (this.peers.has(remotePeerId)) {
550
+ this.log(`⏭️ Peer connection already exists: ${remotePeerId}`);
551
+ return;
552
+ }
553
+ this.log(`🤝 Creating ${initiator ? 'outbound (initiator)' : 'inbound (non-initiator)'} connection to ${remotePeerId}`);
554
+ const peer = new this.options.peer({
555
+ initiator,
556
+ ...this.options.peerOpts,
557
+ });
558
+ const peerConn = {
559
+ peer,
560
+ connected: false,
561
+ peerId: remotePeerId,
562
+ chunkBuffers: new Map(),
563
+ };
564
+ this.peers.set(remotePeerId, peerConn);
565
+ // Handle signaling data (ICE candidates and SDP)
566
+ peer.on('signal', (signal) => {
567
+ this.log(`📤 Signal to ${remotePeerId}: ${signal.type ?? 'candidate'}`);
568
+ // Send as 'publish' only — broadcasting as BOTH 'publish' and 'signal' causes the
569
+ // receiving peer to apply the same SDP offer twice, which triggers a WebRTC error
570
+ // and destroys the peer before it can fully connect.
571
+ this.broadcastSignaling({
572
+ type: 'publish',
573
+ from: this.peerId,
574
+ to: remotePeerId,
575
+ signal,
576
+ topic: this._room,
577
+ });
578
+ });
579
+ // Mark channel open and notify provider — called by either 'connect' or the first
580
+ // 'data' event, whichever fires first (WebRTC can deliver data before 'connect' on
581
+ // some browsers, causing replies to be dropped if we wait for 'connect' only).
582
+ const onChannelOpen = (via) => {
583
+ if (peerConn.connected)
584
+ return; // already fired
585
+ peerConn.connected = true;
586
+ const connectedCount = Array.from(this.peers.values()).filter((p) => p.connected).length;
587
+ this.log(`✅ Peer channel open (${via}): ${remotePeerId} — ${connectedCount}/${this.peers.size} peer(s) connected`);
588
+ for (const cb of this._peerConnectCallbacks)
589
+ cb(remotePeerId);
590
+ };
591
+ // Handle connection
592
+ peer.on('connect', () => onChannelOpen('connect'));
593
+ // Handle ICE connection state for debugging
594
+ if (peer._pc) {
595
+ peer._pc.oniceconnectionstatechange = () => {
596
+ this.log(`🧊 ICE ${remotePeerId}: ${peer._pc.iceConnectionState}`);
597
+ };
598
+ peer._pc.onconnectionstatechange = () => {
599
+ this.log(`🔗 Connection ${remotePeerId}: ${peer._pc.connectionState}`);
600
+ };
601
+ }
602
+ // Handle incoming data
603
+ peer.on('data', (data) => {
604
+ // Data flowing proves the channel is open — handle the race where 'data' fires
605
+ // before 'connect' (seen on Chrome when the remote initiator sends immediately).
606
+ onChannelOpen('data');
607
+ let uint8Data;
608
+ try {
609
+ uint8Data = new Uint8Array(data);
610
+ }
611
+ catch (error) {
612
+ this.log('Error handling peer data:', error);
613
+ return;
614
+ }
615
+ if (uint8Data.length === 0)
616
+ return;
617
+ // Control frames bypass the provider pipe (identity/auth handshakes etc.)
618
+ if (uint8Data[0] === MSG_TYPE_CONTROL) {
619
+ const payload = uint8Data.slice(1);
620
+ for (const cb of this._controlCallbacks)
621
+ cb(remotePeerId, payload);
622
+ return;
623
+ }
624
+ if (!this._callback)
625
+ return;
626
+ try {
627
+ const msgType = uint8Data[0];
628
+ if (msgType === MSG_TYPE_COMPLETE) {
629
+ // Complete message, extract payload (skip type byte)
630
+ const payload = uint8Data.slice(1);
631
+ const decryptedData = this.options.password
632
+ ? this.decrypt(payload, this.options.password)
633
+ : payload;
634
+ this._callback(decryptedData);
635
+ }
636
+ else if (msgType === MSG_TYPE_CHUNKED) {
637
+ // Chunked message - reassemble
638
+ const view = new DataView(uint8Data.buffer, uint8Data.byteOffset);
639
+ const messageId = view.getUint32(1, true);
640
+ const chunkIndex = view.getUint32(5, true);
641
+ const totalChunks = view.getUint32(9, true);
642
+ const chunkData = uint8Data.slice(13);
643
+ // Get or create buffer for this message
644
+ let buffer = peerConn.chunkBuffers.get(messageId);
645
+ if (!buffer) {
646
+ buffer = { chunks: new Map(), totalChunks };
647
+ peerConn.chunkBuffers.set(messageId, buffer);
648
+ }
649
+ // Store chunk
650
+ buffer.chunks.set(chunkIndex, chunkData);
651
+ // Check if complete
652
+ if (buffer.chunks.size === totalChunks) {
653
+ // Reassemble message
654
+ let totalLength = 0;
655
+ for (let i = 0; i < totalChunks; i++) {
656
+ totalLength += buffer.chunks.get(i).length;
657
+ }
658
+ const fullMessage = new Uint8Array(totalLength);
659
+ let offset = 0;
660
+ for (let i = 0; i < totalChunks; i++) {
661
+ const chunk = buffer.chunks.get(i);
662
+ fullMessage.set(chunk, offset);
663
+ offset += chunk.length;
664
+ }
665
+ // Clean up buffer
666
+ peerConn.chunkBuffers.delete(messageId);
667
+ // Decrypt and deliver
668
+ const decryptedData = this.options.password
669
+ ? this.decrypt(fullMessage, this.options.password)
670
+ : fullMessage;
671
+ this._callback(decryptedData);
672
+ this.log(`📥 Reassembled ${totalLength}B from ${totalChunks} chunks (msgId=${messageId})`);
673
+ }
674
+ }
675
+ else {
676
+ // Unknown type or legacy message without type marker - try as raw data
677
+ const decryptedData = this.options.password
678
+ ? this.decrypt(uint8Data, this.options.password)
679
+ : uint8Data;
680
+ this._callback(decryptedData);
681
+ }
682
+ }
683
+ catch (error) {
684
+ this.log('Error handling peer data:', error);
685
+ }
686
+ });
687
+ // Handle errors
688
+ peer.on('error', (error) => {
689
+ this.log(`❌ Peer error [${remotePeerId}]: ${error.message ?? error}`);
690
+ this.removePeer(remotePeerId);
691
+ });
692
+ // Handle close
693
+ peer.on('close', () => {
694
+ const connectedCount = Array.from(this.peers.values()).filter((p) => p.connected && p.peerId !== remotePeerId).length;
695
+ this.log(`🔴 Peer channel closed: ${remotePeerId} — ${connectedCount}/${this.peers.size - 1} remaining`);
696
+ this.removePeer(remotePeerId);
697
+ });
698
+ }
699
+ /**
700
+ * Handle WebRTC signal from peer.
701
+ */
702
+ handlePeerSignal(remotePeerId, signal) {
703
+ let peerConn = this.peers.get(remotePeerId);
704
+ if (!peerConn) {
705
+ this.log(`📶 Signal from unknown peer ${remotePeerId} (${signal?.type ?? 'candidate'}) — creating non-initiator connection`);
706
+ this.createPeerConnection(remotePeerId, false);
707
+ peerConn = this.peers.get(remotePeerId);
708
+ }
709
+ if (peerConn) {
710
+ try {
711
+ peerConn.peer.signal(signal);
712
+ }
713
+ catch (error) {
714
+ this.log(`❌ Failed to apply signal from ${remotePeerId}: ${error.message}`);
715
+ }
716
+ }
717
+ else {
718
+ this.log(`❌ Could not create peer connection for signal from ${remotePeerId}`);
719
+ }
720
+ }
721
+ /**
722
+ * Remove and cleanup a peer connection.
723
+ */
724
+ removePeer(peerId) {
725
+ const peerConn = this.peers.get(peerId);
726
+ if (peerConn) {
727
+ const wasConnected = peerConn.connected;
728
+ try {
729
+ peerConn.peer.destroy();
730
+ }
731
+ catch (error) {
732
+ // Ignore errors during cleanup
733
+ }
734
+ this.peers.delete(peerId);
735
+ this.announcedPeers.delete(peerId);
736
+ if (wasConnected)
737
+ for (const cb of this._peerDisconnectCallbacks)
738
+ cb(peerId);
739
+ const connectedCount = Array.from(this.peers.values()).filter((p) => p.connected).length;
740
+ this.log(`🗑️ Removed peer ${peerId} — ${connectedCount} connected / ${this.peers.size} total`);
741
+ }
742
+ }
743
+ /**
744
+ * Generate a unique peer ID.
745
+ */
746
+ generatePeerId() {
747
+ return Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
748
+ }
749
+ /**
750
+ * Simple XOR encryption (not cryptographically secure, just obfuscation).
751
+ */
752
+ encrypt(data, password) {
753
+ const key = this.hashPassword(password);
754
+ const encrypted = new Uint8Array(data.length);
755
+ for (let i = 0; i < data.length; i++) {
756
+ encrypted[i] = data[i] ^ key[i % key.length];
757
+ }
758
+ return encrypted;
759
+ }
760
+ /**
761
+ * Simple XOR decryption.
762
+ */
763
+ decrypt(data, password) {
764
+ // XOR is symmetric, so decrypt is the same as encrypt
765
+ return this.encrypt(data, password);
766
+ }
767
+ /**
768
+ * Hash password to key.
769
+ */
770
+ hashPassword(password) {
771
+ const encoder = new TextEncoder();
772
+ return encoder.encode(password);
773
+ }
774
+ /**
775
+ * Log debug messages if enabled.
776
+ */
777
+ log(...args) {
778
+ if (this.options.debug) {
779
+ console.log('[SimplePeerTransport]', ...args);
780
+ }
781
+ }
782
+ }
783
+ //# sourceMappingURL=index.js.map