@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,1038 @@
1
+ /**
2
+ * PeerJS Transport Provider
3
+ *
4
+ * Peer-to-peer transport using WebRTC data channels with PeerJS library.
5
+ * PeerJS provides a simpler API and built-in signaling infrastructure.
6
+ *
7
+ * Features:
8
+ * - Direct peer-to-peer connections
9
+ * - Built-in signaling servers (PeerJS Cloud)
10
+ * - Automatic peer ID management
11
+ * - Simple connection API
12
+ * - Optional encryption
13
+ * - Automatic reconnection
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { GenericProvider } from 'y-generic'
18
+ * import { PeerJSTransport } from 'y-generic/providers/peerjs'
19
+ * import Peer from 'peerjs'
20
+ *
21
+ * const doc = new Y.Doc()
22
+ * const transport = new PeerJSTransport({
23
+ * peer: Peer, // Pass the PeerJS constructor
24
+ * password: 'optional-encryption-key'
25
+ * })
26
+ * const provider = new GenericProvider(doc, transport)
27
+ * await provider.connect({ room: 'my-room' })
28
+ * ```
29
+ */
30
+ /**
31
+ * PeerJS transport implementation.
32
+ * Creates direct peer-to-peer connections using PeerJS library.
33
+ */
34
+ export class PeerJSTransport {
35
+ /**
36
+ * Create a new PeerJS transport.
37
+ *
38
+ * @param options - Configuration options (must include peer constructor)
39
+ */
40
+ constructor(options) {
41
+ this._connected = false;
42
+ this._room = '';
43
+ this.peer = null; // PeerJS Peer instance
44
+ this.peerId = '';
45
+ this.peers = new Map();
46
+ this.knownPeers = new Set();
47
+ // Cross-browser coordination
48
+ this.isCoordinator = false;
49
+ this.coordinatorPeerId = '';
50
+ this.roomPeers = new Set(); // All peers in room (coordinator tracks this)
51
+ this.reElectionInProgress = false; // Prevent duplicate re-elections
52
+ this._destroying = false; // Set during disconnect() to suppress reconnects
53
+ if (!options.peer) {
54
+ throw new Error('PeerJSTransport requires the "peer" option. ' +
55
+ 'Please provide the PeerJS constructor: ' +
56
+ 'import Peer from "peerjs"; new PeerJSTransport({ peer: Peer, ... })');
57
+ }
58
+ this.options = {
59
+ peer: options.peer,
60
+ peerOptions: options.peerOptions ?? {},
61
+ password: options.password ?? '',
62
+ maxConns: options.maxConns ?? 20 + Math.floor(Math.random() * 15),
63
+ debug: options.debug ?? false,
64
+ };
65
+ }
66
+ /**
67
+ * Connect to the room and start discovering peers.
68
+ */
69
+ async connect(config) {
70
+ if (this._connected) {
71
+ throw new Error('Already connected');
72
+ }
73
+ this._room = config.room;
74
+ // Deterministic coordinator ID for this room
75
+ this.coordinatorPeerId = `yjs-coordinator-${this._room}`;
76
+ // Attempting to join room
77
+ // Strategy: Try to claim the coordinator ID first
78
+ // If taken, we'll get an error and become a regular peer
79
+ return new Promise((resolve, reject) => {
80
+ try {
81
+ // First, try to create ourselves as the coordinator
82
+ // Attempting to claim coordinator ID
83
+ let coordinatorAttempt = new this.options.peer(this.coordinatorPeerId, {
84
+ debug: this.options.debug ? 3 : 0,
85
+ ...this.options.peerOptions,
86
+ });
87
+ // Timeout in case PeerJS doesn't respond
88
+ const coordinatorTimeout = setTimeout(() => {
89
+ // Coordinator claim timeout
90
+ coordinatorAttempt.destroy();
91
+ this.createRegularPeer(resolve, reject);
92
+ // Cross-browser discovery setup complete
93
+ }, 3000);
94
+ coordinatorAttempt.on('open', (id) => {
95
+ clearTimeout(coordinatorTimeout);
96
+ if (id === this.coordinatorPeerId) {
97
+ // Successfully claimed coordinator ID!
98
+ this.log('Coordinator: claimed ID');
99
+ this.peer = coordinatorAttempt;
100
+ this.peerId = id;
101
+ this.isCoordinator = true;
102
+ this.roomPeers.add(this.peerId);
103
+ this._connected = true;
104
+ // Setup peer discovery
105
+ this.setupPeerDiscovery();
106
+ // Listen for incoming connections
107
+ this.peer.on('connection', (conn) => {
108
+ this.handleIncomingConnection(conn);
109
+ });
110
+ this.peer.on('disconnected', () => {
111
+ this.log('Peer disconnected from PeerJS server, attempting reconnect...');
112
+ this._handlePeerServerDisconnect();
113
+ });
114
+ resolve();
115
+ }
116
+ });
117
+ coordinatorAttempt.on('error', (error) => {
118
+ clearTimeout(coordinatorTimeout);
119
+ // Check if error is because ID is taken
120
+ if (error.message.includes('ID is taken') ||
121
+ error.message.includes('taken') ||
122
+ error.message.includes('already') ||
123
+ error.type === 'unavailable-id') {
124
+ // Coordinator already exists
125
+ coordinatorAttempt.destroy();
126
+ this.createRegularPeer(resolve, reject);
127
+ }
128
+ else {
129
+ this.log('❌ Unexpected error claiming coordinator:', error);
130
+ coordinatorAttempt.destroy();
131
+ this.createRegularPeer(resolve, reject);
132
+ }
133
+ });
134
+ }
135
+ catch (error) {
136
+ reject(error);
137
+ }
138
+ });
139
+ }
140
+ /**
141
+ * Create a regular (non-coordinator) peer and connect to coordinator.
142
+ */
143
+ createRegularPeer(resolve, reject) {
144
+ const peerIdSuffix = Math.random().toString(36).substring(7);
145
+ this.peerId = `yjs-${this._room}-${peerIdSuffix}`;
146
+ // Creating regular peer
147
+ this.peer = new this.options.peer(this.peerId, {
148
+ debug: this.options.debug ? 3 : 0,
149
+ ...this.options.peerOptions,
150
+ });
151
+ this.peer.on('open', (id) => {
152
+ this.peerId = id;
153
+ this._connected = true;
154
+ // Setup peer discovery via BroadcastChannel (same-browser tabs)
155
+ this.setupPeerDiscovery();
156
+ // Listen for incoming connections
157
+ this.peer.on('connection', (conn) => {
158
+ this.handleIncomingConnection(conn);
159
+ });
160
+ // Setup cross-browser discovery via coordinator pattern
161
+ this.setupCrossBrowserDiscovery()
162
+ .then(() => {
163
+ // Cross-browser discovery setup complete
164
+ resolve();
165
+ })
166
+ .catch((err) => {
167
+ this.log('⚠️ Cross-browser discovery failed, continuing with BroadcastChannel only:', err);
168
+ // Still resolve - BroadcastChannel will work for same-browser tabs
169
+ resolve();
170
+ });
171
+ });
172
+ this.peer.on('error', (error) => {
173
+ this.log('Peer error:', error);
174
+ if (!this._connected) {
175
+ reject(error);
176
+ }
177
+ });
178
+ this.peer.on('disconnected', () => {
179
+ this.log('Peer disconnected from PeerJS server, attempting reconnect...');
180
+ this._handlePeerServerDisconnect();
181
+ });
182
+ }
183
+ /**
184
+ * Disconnect from all peers and cleanup.
185
+ */
186
+ disconnect() {
187
+ if (!this._connected)
188
+ return;
189
+ this._destroying = true;
190
+ // Clear discovery interval
191
+ if (this.discoveryInterval) {
192
+ clearInterval(this.discoveryInterval);
193
+ this.discoveryInterval = undefined;
194
+ }
195
+ // Close BroadcastChannel
196
+ if (this.broadcastChannel) {
197
+ // Announce disconnection
198
+ this.broadcastChannel.postMessage({
199
+ type: 'leave',
200
+ peerId: this.peerId,
201
+ });
202
+ this.broadcastChannel.close();
203
+ this.broadcastChannel = undefined;
204
+ }
205
+ // Disconnect from coordinator if connected
206
+ if (this.coordinatorConn) {
207
+ try {
208
+ this.sendCoordinationMessage(this.coordinatorConn, {
209
+ type: 'leave',
210
+ peerId: this.peerId,
211
+ });
212
+ this.coordinatorConn.close();
213
+ }
214
+ catch (err) {
215
+ // Ignore errors during cleanup
216
+ }
217
+ this.coordinatorConn = undefined;
218
+ }
219
+ // If we're coordinator, notify all peers we're leaving
220
+ if (this.isCoordinator) {
221
+ for (const peerId of this.roomPeers) {
222
+ if (peerId !== this.peerId) {
223
+ const peerConn = this.peers.get(peerId);
224
+ if (peerConn && peerConn.connected) {
225
+ try {
226
+ this.sendCoordinationMessage(peerConn.conn, {
227
+ type: 'coordinator-leaving',
228
+ peerId: this.peerId,
229
+ });
230
+ }
231
+ catch (err) {
232
+ // Ignore
233
+ }
234
+ }
235
+ }
236
+ }
237
+ this.isCoordinator = false;
238
+ this.roomPeers.clear();
239
+ }
240
+ // Close all peer connections
241
+ for (const peerConn of this.peers.values()) {
242
+ try {
243
+ peerConn.conn.close();
244
+ }
245
+ catch (error) {
246
+ // Ignore errors during cleanup
247
+ }
248
+ }
249
+ this.peers.clear();
250
+ this.knownPeers.clear();
251
+ this.reElectionInProgress = false;
252
+ // Destroy peer
253
+ if (this.peer) {
254
+ this.peer.destroy();
255
+ this.peer = null;
256
+ }
257
+ this._connected = false;
258
+ this._destroying = false;
259
+ }
260
+ /**
261
+ * Handle unexpected disconnect from the PeerJS signaling server.
262
+ * Happens on network changes (WiFi ↔ mobile), server restarts, etc.
263
+ * PeerJS keeps the same Peer ID — we call reconnect() then re-open
264
+ * DataConnections to all peers we already knew about.
265
+ */
266
+ _handlePeerServerDisconnect() {
267
+ if (this._destroying)
268
+ return;
269
+ // Defer until network is back if we are offline
270
+ if (typeof navigator !== 'undefined' && !navigator.onLine) {
271
+ this.log('📴 Offline – deferring PeerJS reconnect until network returns');
272
+ window.addEventListener('online', () => {
273
+ this.log('🌐 Network restored – reconnecting to PeerJS server');
274
+ this._handlePeerServerDisconnect();
275
+ }, { once: true });
276
+ return;
277
+ }
278
+ // peer.reconnect() re-registers the same ID with the PeerJS server
279
+ try {
280
+ this.peer.reconnect();
281
+ this.log(`🔄 PeerJS reconnect initiated (peer ID: ${this.peerId})`);
282
+ }
283
+ catch (err) {
284
+ this.log('❌ peer.reconnect() failed:', err);
285
+ return;
286
+ }
287
+ // Once the server accepts us again, re-open DataConnections to known peers
288
+ this.peer.once('open', () => {
289
+ this.log(`✅ Reconnected to PeerJS server — re-connecting to ${this.knownPeers.size} known peer(s)`);
290
+ // Drop stale (disconnected) DataConnection entries from before the network change
291
+ for (const [peerId, peerConn] of this.peers.entries()) {
292
+ if (!peerConn.connected) {
293
+ try {
294
+ peerConn.conn.close();
295
+ }
296
+ catch (_) { }
297
+ this.peers.delete(peerId);
298
+ }
299
+ }
300
+ // Re-establish connections to all known peers
301
+ for (const peerId of this.knownPeers) {
302
+ if (!this.peers.has(peerId) && peerId !== this.peerId) {
303
+ this.log(`🔌 Re-connecting to known peer: ${peerId}`);
304
+ this.connectToPeer(peerId);
305
+ }
306
+ }
307
+ // Regular peer: if coordinator connection was lost, reconnect to coordinator
308
+ if (!this.isCoordinator && this.coordinatorConn == null) {
309
+ setTimeout(() => this.attemptCoordinatorConnection(), 1000);
310
+ }
311
+ });
312
+ }
313
+ /**
314
+ * Register callback for new peer data-channel connections.
315
+ */
316
+ onPeerConnect(callback) {
317
+ this._peerConnectCallback = callback;
318
+ return () => {
319
+ this._peerConnectCallback = undefined;
320
+ };
321
+ }
322
+ /**
323
+ * Send data to all connected peers.
324
+ */
325
+ send(data) {
326
+ if (!this._connected) {
327
+ this.log('Not connected, cannot send');
328
+ return;
329
+ }
330
+ // Encrypt if password is set
331
+ const dataToSend = this.options.password
332
+ ? this.encrypt(data, this.options.password)
333
+ : data;
334
+ // Send to all connected peers
335
+ let sentCount = 0;
336
+ for (const peerConn of this.peers.values()) {
337
+ if (peerConn.connected) {
338
+ try {
339
+ peerConn.conn.send(dataToSend);
340
+ sentCount++;
341
+ }
342
+ catch (error) {
343
+ this.log('Error sending to peer:', peerConn.peerId, error);
344
+ }
345
+ }
346
+ }
347
+ // Sent to peers
348
+ }
349
+ /**
350
+ * Register callback for incoming messages.
351
+ */
352
+ onMessage(callback) {
353
+ this._callback = callback;
354
+ return () => {
355
+ this._callback = undefined;
356
+ };
357
+ }
358
+ /**
359
+ * Check if connected.
360
+ */
361
+ get isConnected() {
362
+ return this._connected;
363
+ }
364
+ /**
365
+ * Get number of connected peers (for debugging).
366
+ */
367
+ get connectedPeers() {
368
+ return Array.from(this.peers.values()).filter((p) => p.connected).length;
369
+ }
370
+ /**
371
+ * Setup peer discovery using BroadcastChannel for same-browser tabs.
372
+ */
373
+ setupPeerDiscovery() {
374
+ if (typeof BroadcastChannel === 'undefined') {
375
+ this.log('❌ BroadcastChannel not available in this browser');
376
+ return;
377
+ }
378
+ const channelName = `yjs-peerjs-${this._room}`;
379
+ // Creating BroadcastChannel
380
+ this.broadcastChannel = new BroadcastChannel(channelName);
381
+ this.broadcastChannel.onmessage = (event) => {
382
+ const msg = event.data;
383
+ if (msg.peerId === this.peerId) {
384
+ return; // Ignore own messages
385
+ }
386
+ if (msg.type === 'announce') {
387
+ // Another peer announced
388
+ // Discovered peer via BroadcastChannel
389
+ this.knownPeers.add(msg.peerId);
390
+ this.connectToPeer(msg.peerId);
391
+ }
392
+ else if (msg.type === 'leave') {
393
+ // Peer left
394
+ // Peer left
395
+ this.knownPeers.delete(msg.peerId);
396
+ this.removePeer(msg.peerId);
397
+ }
398
+ };
399
+ // Announce presence
400
+ this.announcePeer();
401
+ // Periodic announcements for new peers that join later
402
+ this.discoveryInterval = setInterval(() => {
403
+ if (this._connected && this.peers.size < this.options.maxConns) {
404
+ this.announcePeer();
405
+ }
406
+ }, 5000);
407
+ }
408
+ /**
409
+ * Announce presence to other peers.
410
+ */
411
+ announcePeer() {
412
+ if (this.broadcastChannel) {
413
+ const message = {
414
+ type: 'announce',
415
+ peerId: this.peerId,
416
+ };
417
+ this.broadcastChannel.postMessage(message);
418
+ }
419
+ }
420
+ /**
421
+ * Setup cross-browser peer discovery by connecting to the coordinator.
422
+ *
423
+ * Note: This method is only called for regular (non-coordinator) peers.
424
+ * The coordinator already exists because we failed to claim its ID.
425
+ */
426
+ async setupCrossBrowserDiscovery() {
427
+ // Connecting to coordinator
428
+ // Wait a bit for PeerJS server registration to complete
429
+ await new Promise((resolve) => setTimeout(resolve, 500));
430
+ return new Promise((resolve, reject) => {
431
+ let connectionAttemptDone = false;
432
+ // Set timeout for network issues
433
+ const timeout = setTimeout(() => {
434
+ if (!connectionAttemptDone) {
435
+ connectionAttemptDone = true;
436
+ const error = new Error('Timeout connecting to coordinator');
437
+ this.log('⏱️ Timeout connecting to coordinator');
438
+ reject(error);
439
+ }
440
+ }, 5000);
441
+ try {
442
+ // Connect to the coordinator (we know it exists)
443
+ const conn = this.peer.connect(this.coordinatorPeerId, {
444
+ reliable: true,
445
+ serialization: 'binary',
446
+ metadata: { role: 'peer', peerId: this.peerId },
447
+ });
448
+ conn.on('open', () => {
449
+ if (connectionAttemptDone)
450
+ return;
451
+ connectionAttemptDone = true;
452
+ clearTimeout(timeout);
453
+ this.log('Connected to coordinator');
454
+ this.coordinatorConn = conn;
455
+ // IMPORTANT: Add coordinator to peers map for Yjs sync
456
+ const peerConn = {
457
+ conn,
458
+ connected: true,
459
+ peerId: this.coordinatorPeerId,
460
+ };
461
+ this.peers.set(this.coordinatorPeerId, peerConn);
462
+ this.knownPeers.add(this.coordinatorPeerId);
463
+ // Notify provider of the coordinator channel opening
464
+ this._peerConnectCallback?.(this.coordinatorPeerId);
465
+ // Request peer list from coordinator (send as JSON-encoded binary)
466
+ this.sendCoordinationMessage(conn, {
467
+ type: 'join',
468
+ peerId: this.peerId,
469
+ });
470
+ // Handle messages from coordinator
471
+ conn.on('data', (data) => {
472
+ // Try to decode as coordination message first
473
+ const uint8Data = data instanceof Uint8Array ? data : new Uint8Array(data);
474
+ const coordMessage = this.tryDecodeCoordinationMessage(uint8Data);
475
+ if (coordMessage) {
476
+ // Handle coordination messages
477
+ this.handleCoordinatorMessage(coordMessage);
478
+ }
479
+ else {
480
+ // It's Yjs sync data - process it
481
+ if (this._callback) {
482
+ try {
483
+ // Decrypt if password is set
484
+ const decryptedData = this.options.password
485
+ ? this.decrypt(uint8Data, this.options.password)
486
+ : uint8Data;
487
+ this._callback(decryptedData);
488
+ }
489
+ catch (error) {
490
+ this.log('Error handling coordinator data:', error);
491
+ }
492
+ }
493
+ }
494
+ });
495
+ conn.on('close', () => {
496
+ this.log('Coordinator disconnected');
497
+ this.peers.delete(this.coordinatorPeerId);
498
+ this.knownPeers.delete(this.coordinatorPeerId);
499
+ this.handleCoordinatorDisconnect();
500
+ });
501
+ resolve();
502
+ });
503
+ conn.on('error', (err) => {
504
+ if (connectionAttemptDone)
505
+ return;
506
+ connectionAttemptDone = true;
507
+ clearTimeout(timeout);
508
+ this.log('❌ Failed to connect to coordinator:', err.message);
509
+ reject(err);
510
+ });
511
+ }
512
+ catch (err) {
513
+ if (!connectionAttemptDone) {
514
+ connectionAttemptDone = true;
515
+ clearTimeout(timeout);
516
+ this.log('⚠️ Error connecting to coordinator:', err);
517
+ reject(err);
518
+ }
519
+ }
520
+ });
521
+ }
522
+ /**
523
+ * Become the coordinator for this room.
524
+ */
525
+ becomeCoordinator() {
526
+ this.isCoordinator = true;
527
+ this.roomPeers.add(this.peerId);
528
+ this.log('👑 I am now the coordinator. Room peers:', Array.from(this.roomPeers));
529
+ // Periodically check if we should still be coordinator (have lowest ID)
530
+ setInterval(() => {
531
+ if (this.isCoordinator) {
532
+ const allPeerIds = Array.from(this.roomPeers).sort();
533
+ if (allPeerIds.length > 0 && allPeerIds[0] !== this.peerId) {
534
+ this.log('⚠️ I am no longer the lowest ID peer, stepping down as coordinator');
535
+ this.isCoordinator = false;
536
+ }
537
+ }
538
+ }, 10000);
539
+ }
540
+ /**
541
+ * Handle messages from coordinator (when we're a regular peer).
542
+ */
543
+ handleCoordinatorMessage(data) {
544
+ switch (data.type) {
545
+ case 'peer-list':
546
+ // Received list of all peers in room from coordinator
547
+ // Received peer list
548
+ for (const peerId of data.peers) {
549
+ if (peerId !== this.peerId && !this.peers.has(peerId)) {
550
+ this.knownPeers.add(peerId);
551
+ this.connectToPeer(peerId);
552
+ }
553
+ }
554
+ break;
555
+ case 'peer-joined':
556
+ // A new peer joined the room
557
+ // New peer joined
558
+ if (data.peerId !== this.peerId && !this.peers.has(data.peerId)) {
559
+ this.knownPeers.add(data.peerId);
560
+ this.connectToPeer(data.peerId);
561
+ }
562
+ break;
563
+ case 'peer-left':
564
+ // A peer left the room
565
+ // Peer left
566
+ this.knownPeers.delete(data.peerId);
567
+ this.removePeer(data.peerId);
568
+ break;
569
+ case 'coordinator-change':
570
+ // Coordinator changed
571
+ // Coordinator changed
572
+ this.coordinatorPeerId = data.coordinatorId;
573
+ // We might need to reconnect to the new coordinator
574
+ break;
575
+ case 'coordinator-leaving':
576
+ // Coordinator is leaving
577
+ this.log('Coordinator leaving, re-election starting');
578
+ this.handleCoordinatorDisconnect();
579
+ break;
580
+ }
581
+ }
582
+ /**
583
+ * Handle coordinator disconnect - start re-election.
584
+ */
585
+ handleCoordinatorDisconnect() {
586
+ // Prevent duplicate re-elections
587
+ if (this.reElectionInProgress) {
588
+ this.log('⏭️ Re-election already in progress, skipping');
589
+ return;
590
+ }
591
+ // If we are currently offline the coordinator disconnected because WE lost
592
+ // the network, not because the coordinator disappeared. Any attempt to
593
+ // create a new PeerJS peer will fail immediately with "Lost connection to
594
+ // server". Defer the whole re-election until we are back online.
595
+ if (typeof navigator !== 'undefined' && !navigator.onLine) {
596
+ this.log('📴 Offline – deferring re-election until network returns');
597
+ const handler = () => {
598
+ this.log('🌐 Network restored – resuming re-election');
599
+ this.handleCoordinatorDisconnect();
600
+ };
601
+ window.addEventListener('online', handler, { once: true });
602
+ return;
603
+ }
604
+ this.log('🗳️ Coordinator disconnected, starting re-election');
605
+ this.reElectionInProgress = true;
606
+ this.coordinatorConn = undefined;
607
+ // Gather all known peer IDs
608
+ const knownPeerIds = [
609
+ this.peerId,
610
+ ...Array.from(this.knownPeers),
611
+ ...Array.from(this.peers.keys()),
612
+ ].filter((id) => id !== this.coordinatorPeerId); // Exclude the old coordinator
613
+ // Sort and pick lowest ID
614
+ knownPeerIds.sort();
615
+ const lowestId = knownPeerIds[0];
616
+ this.log('🗳️ Coordinator election: Lowest peer ID is', lowestId);
617
+ if (lowestId === this.peerId) {
618
+ this.log('👑 I have the lowest ID, attempting to claim coordinator role');
619
+ this.transitionToCoordinator();
620
+ }
621
+ else {
622
+ // Waiting for new coordinator
623
+ this.log('⏳ Waiting for new coordinator:', lowestId);
624
+ // Wait a bit for the new coordinator to claim the ID, then try to connect
625
+ setTimeout(() => {
626
+ this.attemptCoordinatorConnection();
627
+ }, 2000);
628
+ }
629
+ }
630
+ /**
631
+ * Attempt to connect to the coordinator.
632
+ * If coordinator doesn't exist yet, retry a few times.
633
+ */
634
+ attemptCoordinatorConnection(retryCount = 0) {
635
+ const maxRetries = 5;
636
+ if (retryCount >= maxRetries) {
637
+ this.log('❌ Failed to connect to coordinator after', maxRetries, 'attempts');
638
+ // Try to become coordinator ourselves as fallback
639
+ this.log('🔄 Attempting to become coordinator as fallback');
640
+ this.transitionToCoordinator();
641
+ return;
642
+ }
643
+ this.log(`🔌 Attempting to connect to coordinator (attempt ${retryCount + 1}/${maxRetries})`);
644
+ this.setupCrossBrowserDiscovery()
645
+ .then(() => {
646
+ this.log('✅ Successfully connected to new coordinator');
647
+ this.reElectionInProgress = false; // Re-election complete
648
+ })
649
+ .catch((err) => {
650
+ this.log(`⚠️ Failed to connect to coordinator (attempt ${retryCount + 1}):`, err);
651
+ // Retry with exponential backoff
652
+ const delay = 1000 * Math.pow(1.5, retryCount);
653
+ setTimeout(() => {
654
+ this.attemptCoordinatorConnection(retryCount + 1);
655
+ }, delay);
656
+ });
657
+ }
658
+ /**
659
+ * Transition from regular peer to coordinator by reconnecting with coordinator ID.
660
+ */
661
+ async transitionToCoordinator() {
662
+ this.log('🔄 Transitioning to coordinator role...');
663
+ // Save current state
664
+ const currentConnections = Array.from(this.peers.entries());
665
+ const callback = this._callback;
666
+ const room = this._room;
667
+ // Close current peer connection
668
+ if (this.peer) {
669
+ this.peer.destroy();
670
+ }
671
+ // Clear state but keep connections info
672
+ this.peers.clear();
673
+ this._connected = false;
674
+ try {
675
+ // Attempt to claim coordinator ID
676
+ await new Promise((resolve, reject) => {
677
+ const coordinatorPeer = new this.options.peer(this.coordinatorPeerId, {
678
+ debug: this.options.debug ? 3 : 0,
679
+ ...this.options.peerOptions,
680
+ });
681
+ const timeout = setTimeout(() => {
682
+ this.log('❌ Timeout claiming coordinator ID');
683
+ coordinatorPeer.destroy();
684
+ reject(new Error('Timeout claiming coordinator ID'));
685
+ }, 5000);
686
+ coordinatorPeer.on('open', (id) => {
687
+ clearTimeout(timeout);
688
+ if (id === this.coordinatorPeerId) {
689
+ this.log('✅ Successfully claimed coordinator ID:', id);
690
+ this.peer = coordinatorPeer;
691
+ this.peerId = id;
692
+ this.isCoordinator = true;
693
+ this.roomPeers.clear();
694
+ this.roomPeers.add(this.peerId);
695
+ this._connected = true;
696
+ this._callback = callback;
697
+ this.reElectionInProgress = false; // Re-election complete
698
+ // Setup peer discovery as coordinator
699
+ this.setupPeerDiscovery();
700
+ // Listen for incoming connections
701
+ this.peer.on('connection', (conn) => {
702
+ this.handleIncomingConnection(conn);
703
+ });
704
+ // Re-establish connections with known peers
705
+ this.log('🔗 Re-establishing connections with', currentConnections.length, 'known peers');
706
+ for (const [peerId, _] of currentConnections) {
707
+ if (peerId !== this.coordinatorPeerId) {
708
+ this.knownPeers.add(peerId);
709
+ this.roomPeers.add(peerId);
710
+ }
711
+ }
712
+ resolve();
713
+ }
714
+ });
715
+ coordinatorPeer.on('error', (error) => {
716
+ clearTimeout(timeout);
717
+ this.log('❌ Error claiming coordinator ID:', error);
718
+ coordinatorPeer.destroy();
719
+ reject(error);
720
+ });
721
+ });
722
+ }
723
+ catch (error) {
724
+ this.log('❌ Failed to transition to coordinator:', error);
725
+ this.reElectionInProgress = false; // Re-election failed
726
+ // Fallback: recreate as regular peer and connect to whoever won the election
727
+ this.log('🔄 Falling back to regular peer');
728
+ this.reestablishAsRegularPeer();
729
+ }
730
+ }
731
+ /**
732
+ * Recreate the local peer with a fresh random ID and reconnect to the
733
+ * coordinator (whoever won the election while we were transitioning).
734
+ * Called when transitionToCoordinator() fails.
735
+ */
736
+ reestablishAsRegularPeer() {
737
+ const peerIdSuffix = Math.random().toString(36).substring(7);
738
+ this.peerId = `yjs-${this._room}-${peerIdSuffix}`;
739
+ this.isCoordinator = false;
740
+ this.peer = new this.options.peer(this.peerId, {
741
+ debug: this.options.debug ? 3 : 0,
742
+ ...this.options.peerOptions,
743
+ });
744
+ this.peer.on('open', (id) => {
745
+ this.peerId = id;
746
+ this._connected = true;
747
+ this.log('✅ Reestablished as regular peer:', id);
748
+ this.setupPeerDiscovery();
749
+ this.peer.on('connection', (conn) => {
750
+ this.handleIncomingConnection(conn);
751
+ });
752
+ this.peer.on('disconnected', () => {
753
+ this.log('Peer disconnected from PeerJS server, attempting reconnect...');
754
+ this._handlePeerServerDisconnect();
755
+ });
756
+ // Connect to whoever is now coordinator
757
+ this.attemptCoordinatorConnection();
758
+ });
759
+ this.peer.on('error', (error) => {
760
+ this.log('❌ Error reestablishing as regular peer:', error);
761
+ // Destroy the dead peer object so we don't hold a stale reference
762
+ try {
763
+ this.peer?.destroy();
764
+ }
765
+ catch (_) { }
766
+ this.peer = null;
767
+ this._connected = false;
768
+ // If the failure is due to being offline, wait for the network to return
769
+ // and then try again from scratch.
770
+ const retryOnline = () => {
771
+ this.log('🌐 Network restored – retrying reestablish as regular peer');
772
+ this.reestablishAsRegularPeer();
773
+ };
774
+ if (typeof navigator !== 'undefined' && !navigator.onLine) {
775
+ window.addEventListener('online', retryOnline, { once: true });
776
+ }
777
+ else {
778
+ // We appear online but still failed – back off briefly and retry
779
+ setTimeout(retryOnline, 3000);
780
+ }
781
+ });
782
+ }
783
+ /**
784
+ * Connect to a peer by ID.
785
+ * To avoid race conditions, only the peer with the lower ID initiates the connection.
786
+ */
787
+ connectToPeer(remotePeerId) {
788
+ if (remotePeerId === this.peerId) {
789
+ // Skip self-connection
790
+ return;
791
+ }
792
+ if (this.peers.has(remotePeerId)) {
793
+ return;
794
+ }
795
+ if (this.peers.size >= this.options.maxConns) {
796
+ // Max connections reached
797
+ return;
798
+ }
799
+ // Only initiate if our peer ID is lower (to prevent dual-initiation race condition)
800
+ if (this.peerId && this.peerId > remotePeerId) {
801
+ // Waiting for peer to initiate
802
+ return;
803
+ }
804
+ // Initiating connection
805
+ try {
806
+ const conn = this.peer.connect(remotePeerId, {
807
+ reliable: true,
808
+ serialization: 'binary',
809
+ });
810
+ if (!conn) {
811
+ this.log('Failed to create connection object for:', remotePeerId);
812
+ return;
813
+ }
814
+ // Connection object created
815
+ this.setupConnection(conn, remotePeerId);
816
+ }
817
+ catch (error) {
818
+ this.log('Error connecting to peer:', remotePeerId, error);
819
+ }
820
+ }
821
+ /**
822
+ * Handle incoming connection from another peer.
823
+ */
824
+ handleIncomingConnection(conn) {
825
+ const remotePeerId = conn.peer;
826
+ // Incoming connection
827
+ if (this.peers.has(remotePeerId)) {
828
+ // Already connected
829
+ conn.close();
830
+ return;
831
+ }
832
+ if (this.peers.size >= this.options.maxConns) {
833
+ this.log('Max connections reached, rejecting peer:', remotePeerId);
834
+ conn.close();
835
+ return;
836
+ }
837
+ // If we're the coordinator, handle coordination duties
838
+ if (this.isCoordinator) {
839
+ // Add to room peers immediately
840
+ this.roomPeers.add(remotePeerId);
841
+ // Setup close handler to remove from room peers
842
+ conn.on('close', () => {
843
+ // Coordinator: peer left
844
+ this.roomPeers.delete(remotePeerId);
845
+ // Notify all other peers
846
+ for (const peerId of this.roomPeers) {
847
+ if (peerId !== this.peerId) {
848
+ const peerConn = this.peers.get(peerId);
849
+ if (peerConn && peerConn.connected) {
850
+ try {
851
+ this.sendCoordinationMessage(peerConn.conn, {
852
+ type: 'peer-left',
853
+ peerId: remotePeerId,
854
+ });
855
+ }
856
+ catch (err) {
857
+ this.log('⚠️ Failed to notify peer of departure:', peerId);
858
+ }
859
+ }
860
+ }
861
+ }
862
+ });
863
+ }
864
+ // Setup the connection for Yjs sync (this handles all data including join messages)
865
+ this.setupConnection(conn, remotePeerId);
866
+ }
867
+ /**
868
+ * Setup a peer connection with event handlers.
869
+ */
870
+ setupConnection(conn, remotePeerId) {
871
+ // Setting up connection
872
+ const peerConn = {
873
+ conn,
874
+ connected: false,
875
+ peerId: remotePeerId,
876
+ };
877
+ this.peers.set(remotePeerId, peerConn);
878
+ conn.on('open', () => {
879
+ peerConn.connected = true;
880
+ this.knownPeers.add(remotePeerId);
881
+ // Notify provider so it can push its local state to this new peer
882
+ this._peerConnectCallback?.(remotePeerId);
883
+ });
884
+ conn.on('data', (data) => {
885
+ // Convert to Uint8Array if needed
886
+ const uint8Data = data instanceof Uint8Array ? data : new Uint8Array(data);
887
+ // Try to decode as JSON coordination message
888
+ const coordMessage = this.tryDecodeCoordinationMessage(uint8Data);
889
+ if (coordMessage) {
890
+ // Handle coordination messages
891
+ if (coordMessage.type === 'coordinator-change' ||
892
+ coordMessage.type === 'coordinator-leaving' ||
893
+ coordMessage.type === 'peer-joined' ||
894
+ coordMessage.type === 'peer-left' ||
895
+ coordMessage.type === 'peer-list') {
896
+ this.handleCoordinatorMessage(coordMessage);
897
+ return;
898
+ }
899
+ else if (coordMessage.type === 'join' && this.isCoordinator) {
900
+ // Handle join message if we're the coordinator
901
+ // Coordinator: peer joined
902
+ // Send current peer list (excluding coordinator and the new peer)
903
+ const peerList = Array.from(this.roomPeers).filter((id) => id !== this.peerId && id !== remotePeerId);
904
+ this.sendCoordinationMessage(conn, {
905
+ type: 'peer-list',
906
+ peers: peerList,
907
+ });
908
+ // Notify all other peers about the new peer
909
+ for (const peerId of this.roomPeers) {
910
+ if (peerId !== this.peerId && peerId !== remotePeerId) {
911
+ const peerConn = this.peers.get(peerId);
912
+ if (peerConn && peerConn.connected) {
913
+ try {
914
+ this.sendCoordinationMessage(peerConn.conn, {
915
+ type: 'peer-joined',
916
+ peerId: remotePeerId,
917
+ });
918
+ }
919
+ catch (err) {
920
+ this.log('⚠️ Failed to notify peer of new joiner:', peerId);
921
+ }
922
+ }
923
+ }
924
+ }
925
+ return;
926
+ }
927
+ }
928
+ // Otherwise, it's Yjs sync data
929
+ if (this._callback) {
930
+ try {
931
+ // Decrypt if password is set
932
+ const decryptedData = this.options.password
933
+ ? this.decrypt(uint8Data, this.options.password)
934
+ : uint8Data;
935
+ this._callback(decryptedData);
936
+ }
937
+ catch (error) {
938
+ this.log('Error handling peer data:', error);
939
+ }
940
+ }
941
+ });
942
+ conn.on('close', () => {
943
+ this.removePeer(remotePeerId);
944
+ });
945
+ conn.on('error', (error) => {
946
+ this.log('⚠️ Connection error:', remotePeerId, error);
947
+ this.removePeer(remotePeerId);
948
+ });
949
+ // Log connection state after a delay to debug
950
+ setTimeout(() => { }, 1000);
951
+ }
952
+ /**
953
+ * Remove and cleanup a peer connection.
954
+ */
955
+ removePeer(peerId) {
956
+ const peerConn = this.peers.get(peerId);
957
+ if (peerConn) {
958
+ try {
959
+ peerConn.conn.close();
960
+ }
961
+ catch (error) {
962
+ // Ignore errors during cleanup
963
+ }
964
+ this.peers.delete(peerId);
965
+ }
966
+ }
967
+ /**
968
+ * Send a coordination message (JSON encoded as binary).
969
+ */
970
+ sendCoordinationMessage(conn, message) {
971
+ try {
972
+ const jsonStr = JSON.stringify(message);
973
+ const encoder = new TextEncoder();
974
+ const encoded = encoder.encode(jsonStr);
975
+ conn.send(encoded);
976
+ }
977
+ catch (error) {
978
+ this.log('Error encoding coordination message:', error);
979
+ }
980
+ }
981
+ /**
982
+ * Try to decode data as a coordination message.
983
+ * Returns the parsed message if successful, null otherwise.
984
+ */
985
+ tryDecodeCoordinationMessage(data) {
986
+ try {
987
+ const decoder = new TextDecoder();
988
+ const jsonStr = decoder.decode(data);
989
+ const parsed = JSON.parse(jsonStr);
990
+ // Check if it looks like a coordination message (has a type field)
991
+ if (parsed &&
992
+ typeof parsed === 'object' &&
993
+ parsed.type &&
994
+ typeof parsed.type === 'string') {
995
+ return parsed;
996
+ }
997
+ return null;
998
+ }
999
+ catch (error) {
1000
+ // Not JSON, must be Yjs data
1001
+ return null;
1002
+ }
1003
+ }
1004
+ /**
1005
+ * Simple XOR encryption (not cryptographically secure, just obfuscation).
1006
+ */
1007
+ encrypt(data, password) {
1008
+ const key = this.hashPassword(password);
1009
+ const encrypted = new Uint8Array(data.length);
1010
+ for (let i = 0; i < data.length; i++) {
1011
+ encrypted[i] = data[i] ^ key[i % key.length];
1012
+ }
1013
+ return encrypted;
1014
+ }
1015
+ /**
1016
+ * Simple XOR decryption.
1017
+ */
1018
+ decrypt(data, password) {
1019
+ // XOR is symmetric, so decrypt is the same as encrypt
1020
+ return this.encrypt(data, password);
1021
+ }
1022
+ /**
1023
+ * Hash password to key.
1024
+ */
1025
+ hashPassword(password) {
1026
+ const encoder = new TextEncoder();
1027
+ return encoder.encode(password);
1028
+ }
1029
+ /**
1030
+ * Log debug messages if enabled.
1031
+ */
1032
+ log(...args) {
1033
+ if (this.options.debug) {
1034
+ console.log('[PeerJSTransport]', ...args);
1035
+ }
1036
+ }
1037
+ }
1038
+ //# sourceMappingURL=index.js.map