@carverjs/multiplayer 0.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 (43) hide show
  1. package/dist/NetworkManager-DrKM2tEx.d.mts +369 -0
  2. package/dist/NetworkManager-nvVAOr1O.d.ts +369 -0
  3. package/dist/chunk-3KT73N2S.mjs +655 -0
  4. package/dist/chunk-3KT73N2S.mjs.map +1 -0
  5. package/dist/chunk-EO3YNPRQ.mjs +817 -0
  6. package/dist/chunk-EO3YNPRQ.mjs.map +1 -0
  7. package/dist/chunk-UD6FDZMX.mjs +581 -0
  8. package/dist/chunk-UD6FDZMX.mjs.map +1 -0
  9. package/dist/firebase-CPu87KA0.d.ts +100 -0
  10. package/dist/firebase-PE6MxGdJ.d.mts +100 -0
  11. package/dist/index.d.mts +316 -0
  12. package/dist/index.d.ts +316 -0
  13. package/dist/index.js +3817 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/index.mjs +1743 -0
  16. package/dist/index.mjs.map +1 -0
  17. package/dist/strategy.d.mts +7 -0
  18. package/dist/strategy.d.ts +7 -0
  19. package/dist/strategy.js +619 -0
  20. package/dist/strategy.js.map +1 -0
  21. package/dist/strategy.mjs +11 -0
  22. package/dist/strategy.mjs.map +1 -0
  23. package/dist/sync.d.mts +212 -0
  24. package/dist/sync.d.ts +212 -0
  25. package/dist/sync.js +845 -0
  26. package/dist/sync.js.map +1 -0
  27. package/dist/sync.mjs +11 -0
  28. package/dist/sync.mjs.map +1 -0
  29. package/dist/transport.d.mts +159 -0
  30. package/dist/transport.d.ts +159 -0
  31. package/dist/transport.js +1274 -0
  32. package/dist/transport.js.map +1 -0
  33. package/dist/transport.mjs +19 -0
  34. package/dist/transport.mjs.map +1 -0
  35. package/dist/types-5LHBOW08.d.mts +74 -0
  36. package/dist/types-5LHBOW08.d.ts +74 -0
  37. package/dist/types.d.mts +2 -0
  38. package/dist/types.d.ts +2 -0
  39. package/dist/types.js +19 -0
  40. package/dist/types.js.map +1 -0
  41. package/dist/types.mjs +1 -0
  42. package/dist/types.mjs.map +1 -0
  43. package/package.json +73 -0
@@ -0,0 +1,581 @@
1
+ // src/transport/strategy/utils.ts
2
+ function generatePeerId() {
3
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
4
+ const bytes = new Uint8Array(20);
5
+ crypto.getRandomValues(bytes);
6
+ let id = "";
7
+ for (let i = 0; i < 20; i++) {
8
+ id += chars[bytes[i] % chars.length];
9
+ }
10
+ return id;
11
+ }
12
+ function mqttTopics(appId, roomId, peerId) {
13
+ const base = `carver/${appId}`;
14
+ return {
15
+ /** Lobby wildcard: subscribe to discover all room announcements */
16
+ lobbyWildcard: `${base}/lobby/+`,
17
+ /** Single room lobby entry */
18
+ roomLobbyEntry: `${base}/lobby/${roomId}`,
19
+ /** Wildcard for all peer presence in a room */
20
+ roomPresenceWildcard: `${base}/room/${roomId}/presence/+`,
21
+ /** This peer's presence topic */
22
+ peerPresence: peerId ? `${base}/room/${roomId}/presence/${peerId}` : "",
23
+ /** This peer's signal inbox */
24
+ peerSignalInbox: peerId ? `${base}/room/${roomId}/signal/${peerId}` : ""
25
+ };
26
+ }
27
+ function firebasePaths(appId, roomId, peerId) {
28
+ const base = `${appId}/__carver__`;
29
+ return {
30
+ lobby: `${base}/lobby`,
31
+ roomLobbyEntry: `${base}/lobby/${roomId}`,
32
+ peers: `${base}/rooms/${roomId}/peers`,
33
+ peerPresence: peerId ? `${base}/rooms/${roomId}/peers/${peerId}` : "",
34
+ peerSignalInbox: peerId ? `${base}/rooms/${roomId}/signals/${peerId}` : ""
35
+ };
36
+ }
37
+ var DEFAULT_MQTT_BROKERS = [
38
+ "wss://broker.emqx.io:8084/mqtt",
39
+ "wss://test.mosquitto.org:8081/mqtt"
40
+ ];
41
+ var ROOM_ANNOUNCE_EXPIRY_MS = 3e4;
42
+ var ROOM_ANNOUNCE_INTERVAL_MS = 1e4;
43
+ var PRESENCE_HEARTBEAT_MS = 5e3;
44
+ var PEER_EXPIRY_MS = PRESENCE_HEARTBEAT_MS * 3;
45
+ var PRESENCE_WARMUP_DELAYS_MS = [200, 500, 1500];
46
+ function removeFromArray(arr, item) {
47
+ const idx = arr.indexOf(item);
48
+ if (idx >= 0) {
49
+ arr.splice(idx, 1);
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ // src/transport/strategy/mqtt.ts
56
+ var MqttStrategy = class {
57
+ constructor(appId, config = { type: "mqtt" }) {
58
+ this._client = null;
59
+ this._roomId = null;
60
+ this._peerMeta = {};
61
+ /** Monotonic counter to detect stale leaveRoom completions */
62
+ this._joinGeneration = 0;
63
+ // Lazy init
64
+ this._initPromise = null;
65
+ // Callbacks
66
+ this._onPeerDiscovered = [];
67
+ this._onPeerLeft = [];
68
+ this._onSignal = [];
69
+ this._onLobby = [];
70
+ // State
71
+ this._knownPeers = /* @__PURE__ */ new Map();
72
+ this._lobbyRooms = /* @__PURE__ */ new Map();
73
+ this._presenceTimer = null;
74
+ this._warmupTimers = [];
75
+ this._lobbyAnnounceTimer = null;
76
+ this._peerExpiryTimer = null;
77
+ this._lobbySubscribed = false;
78
+ this._destroyed = false;
79
+ this.selfId = generatePeerId();
80
+ this._appId = appId;
81
+ this._config = config;
82
+ }
83
+ // ── Public API ──
84
+ async init() {
85
+ return this._ensureInit();
86
+ }
87
+ async joinRoom(roomId, peerMeta) {
88
+ await this._ensureInit();
89
+ if (!this._client) throw new Error("MQTT client not available");
90
+ this._joinGeneration++;
91
+ this._roomId = roomId;
92
+ this._peerMeta = peerMeta;
93
+ const topics = mqttTopics(this._appId, roomId, this.selfId);
94
+ await new Promise((resolve, reject) => {
95
+ this._client.subscribe(
96
+ [topics.roomPresenceWildcard, topics.peerSignalInbox],
97
+ { qos: 1 },
98
+ (err) => err ? reject(err) : resolve()
99
+ );
100
+ });
101
+ this._publishPresence();
102
+ for (const delay of PRESENCE_WARMUP_DELAYS_MS) {
103
+ this._warmupTimers.push(setTimeout(() => this._publishPresence(), delay));
104
+ }
105
+ this._presenceTimer = setInterval(() => this._publishPresence(), PRESENCE_HEARTBEAT_MS);
106
+ this._peerExpiryTimer = setInterval(() => this._checkPeerExpiry(), PRESENCE_HEARTBEAT_MS);
107
+ }
108
+ async leaveRoom() {
109
+ if (!this._client || !this._roomId) return;
110
+ const generation = this._joinGeneration;
111
+ const topics = mqttTopics(this._appId, this._roomId, this.selfId);
112
+ this._clearRoomTimers();
113
+ this._client.publish(topics.peerPresence, "", { retain: true, qos: 1 });
114
+ this._client.unsubscribe([topics.roomPresenceWildcard, topics.peerSignalInbox]);
115
+ this._knownPeers.clear();
116
+ if (this._joinGeneration === generation) {
117
+ this._roomId = null;
118
+ }
119
+ }
120
+ signal(targetPeerId, data) {
121
+ if (!this._client || !this._roomId) return;
122
+ const targetTopic = `carver/${this._appId}/room/${this._roomId}/signal/${targetPeerId}`;
123
+ this._client.publish(
124
+ targetTopic,
125
+ JSON.stringify({ from: this.selfId, data, ts: Date.now() }),
126
+ { qos: 1 }
127
+ );
128
+ }
129
+ subscribeToLobby(cb) {
130
+ this._onLobby.push(cb);
131
+ if (!this._lobbySubscribed) {
132
+ this._lobbySubscribed = true;
133
+ this._ensureInit().then(() => {
134
+ if (this._client && !this._destroyed) {
135
+ const lobbyTopic = mqttTopics(this._appId, "", "").lobbyWildcard;
136
+ this._client.subscribe(lobbyTopic, { qos: 0 });
137
+ }
138
+ });
139
+ }
140
+ return () => {
141
+ removeFromArray(this._onLobby, cb);
142
+ };
143
+ }
144
+ announceRoom(announcement) {
145
+ if (!this._client) return;
146
+ const topic = mqttTopics(this._appId, announcement.roomId, "").roomLobbyEntry;
147
+ announcement.lastSeen = Date.now();
148
+ this._client.publish(topic, JSON.stringify(announcement), { retain: true, qos: 1 });
149
+ if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);
150
+ this._lobbyAnnounceTimer = setInterval(() => {
151
+ announcement.lastSeen = Date.now();
152
+ this._client?.publish(topic, JSON.stringify(announcement), { retain: true, qos: 1 });
153
+ }, ROOM_ANNOUNCE_INTERVAL_MS);
154
+ }
155
+ removeRoomAnnouncement(roomId) {
156
+ if (!this._client) return;
157
+ const topic = mqttTopics(this._appId, roomId, "").roomLobbyEntry;
158
+ this._client.publish(topic, "", { retain: true, qos: 1 });
159
+ if (this._lobbyAnnounceTimer) {
160
+ clearInterval(this._lobbyAnnounceTimer);
161
+ this._lobbyAnnounceTimer = null;
162
+ }
163
+ }
164
+ onPeerDiscovered(cb) {
165
+ this._onPeerDiscovered.push(cb);
166
+ return () => {
167
+ removeFromArray(this._onPeerDiscovered, cb);
168
+ };
169
+ }
170
+ onPeerLeft(cb) {
171
+ this._onPeerLeft.push(cb);
172
+ return () => {
173
+ removeFromArray(this._onPeerLeft, cb);
174
+ };
175
+ }
176
+ onSignal(cb) {
177
+ this._onSignal.push(cb);
178
+ return () => {
179
+ removeFromArray(this._onSignal, cb);
180
+ };
181
+ }
182
+ destroy() {
183
+ this._destroyed = true;
184
+ this._clearRoomTimers();
185
+ if (this._client && this._roomId) {
186
+ const topics = mqttTopics(this._appId, this._roomId, this.selfId);
187
+ this._client.publish(topics.peerPresence, "", { retain: true, qos: 1 });
188
+ }
189
+ this._client?.end(true);
190
+ this._client = null;
191
+ this._knownPeers.clear();
192
+ this._lobbyRooms.clear();
193
+ this._onPeerDiscovered = [];
194
+ this._onPeerLeft = [];
195
+ this._onSignal = [];
196
+ this._onLobby = [];
197
+ }
198
+ // ── Private ──
199
+ _ensureInit() {
200
+ if (!this._initPromise) {
201
+ this._initPromise = this._doInit();
202
+ }
203
+ return this._initPromise;
204
+ }
205
+ async _doInit() {
206
+ const mqtt = await import("mqtt");
207
+ const brokers = this._config.brokerUrls ?? DEFAULT_MQTT_BROKERS;
208
+ const brokerUrl = brokers[Math.floor(Math.random() * brokers.length)];
209
+ return new Promise((resolve, reject) => {
210
+ const connectFn = mqtt.default?.connect ?? mqtt.connect;
211
+ this._client = connectFn(brokerUrl, {
212
+ clientId: `carver_${this.selfId}`,
213
+ clean: true,
214
+ connectTimeout: 1e4,
215
+ keepalive: 30
216
+ });
217
+ this._client.on("connect", () => {
218
+ if (!this._destroyed) resolve();
219
+ });
220
+ this._client.on("error", (err) => {
221
+ if (!this._initPromise) return;
222
+ reject(err);
223
+ });
224
+ this._client.on("message", (topic, payload) => {
225
+ this._handleMessage(topic, payload);
226
+ });
227
+ });
228
+ }
229
+ _publishPresence() {
230
+ if (!this._client || !this._roomId) return;
231
+ const topics = mqttTopics(this._appId, this._roomId, this.selfId);
232
+ this._client.publish(
233
+ topics.peerPresence,
234
+ JSON.stringify({ peerId: this.selfId, meta: this._peerMeta, ts: Date.now() }),
235
+ { retain: true, qos: 1 }
236
+ );
237
+ }
238
+ _handleMessage(topic, payload) {
239
+ const raw = payload.toString();
240
+ const presenceMatch = topic.match(/\/room\/[^/]+\/presence\/([^/]+)$/);
241
+ if (presenceMatch) {
242
+ const peerId = presenceMatch[1];
243
+ if (peerId === this.selfId) return;
244
+ if (!raw) {
245
+ if (this._knownPeers.has(peerId)) {
246
+ this._knownPeers.delete(peerId);
247
+ for (const cb of this._onPeerLeft) cb(peerId);
248
+ }
249
+ return;
250
+ }
251
+ try {
252
+ const msg = JSON.parse(raw);
253
+ const isNew = !this._knownPeers.has(peerId);
254
+ this._knownPeers.set(peerId, { meta: msg.meta ?? {}, lastSeen: msg.ts ?? Date.now() });
255
+ if (isNew) {
256
+ for (const cb of this._onPeerDiscovered) cb(peerId, msg.meta ?? {});
257
+ }
258
+ } catch {
259
+ }
260
+ return;
261
+ }
262
+ const signalMatch = topic.match(/\/room\/[^/]+\/signal\/([^/]+)$/);
263
+ if (signalMatch) {
264
+ try {
265
+ const msg = JSON.parse(raw);
266
+ if (msg.from && msg.from !== this.selfId) {
267
+ for (const cb of this._onSignal) cb(msg.from, msg.data);
268
+ }
269
+ } catch {
270
+ }
271
+ return;
272
+ }
273
+ const lobbyMatch = topic.match(/\/lobby\/([^/]+)$/);
274
+ if (lobbyMatch) {
275
+ const roomId = lobbyMatch[1];
276
+ if (!raw) {
277
+ this._lobbyRooms.delete(roomId);
278
+ } else {
279
+ try {
280
+ const ann = JSON.parse(raw);
281
+ if (Date.now() - ann.lastSeen < ROOM_ANNOUNCE_EXPIRY_MS) {
282
+ this._lobbyRooms.set(roomId, ann);
283
+ } else {
284
+ this._lobbyRooms.delete(roomId);
285
+ }
286
+ } catch {
287
+ }
288
+ }
289
+ const rooms = Array.from(this._lobbyRooms.values());
290
+ for (const cb of this._onLobby) cb(rooms);
291
+ }
292
+ }
293
+ _checkPeerExpiry() {
294
+ const now = Date.now();
295
+ for (const [peerId, data] of this._knownPeers) {
296
+ if (now - data.lastSeen > PEER_EXPIRY_MS) {
297
+ this._knownPeers.delete(peerId);
298
+ for (const cb of this._onPeerLeft) cb(peerId);
299
+ }
300
+ }
301
+ }
302
+ _clearRoomTimers() {
303
+ if (this._presenceTimer) {
304
+ clearInterval(this._presenceTimer);
305
+ this._presenceTimer = null;
306
+ }
307
+ for (const t of this._warmupTimers) clearTimeout(t);
308
+ this._warmupTimers = [];
309
+ if (this._lobbyAnnounceTimer) {
310
+ clearInterval(this._lobbyAnnounceTimer);
311
+ this._lobbyAnnounceTimer = null;
312
+ }
313
+ if (this._peerExpiryTimer) {
314
+ clearInterval(this._peerExpiryTimer);
315
+ this._peerExpiryTimer = null;
316
+ }
317
+ }
318
+ };
319
+
320
+ // src/transport/strategy/firebase.ts
321
+ var FirebaseStrategy = class {
322
+ constructor(appId, config) {
323
+ this._db = null;
324
+ this._firebaseApp = null;
325
+ this._ownApp = false;
326
+ this._roomId = null;
327
+ this._peerMeta = {};
328
+ /** Monotonic counter to detect stale leaveRoom completions */
329
+ this._joinGeneration = 0;
330
+ // Lazy init
331
+ this._initPromise = null;
332
+ // Firebase module references (filled after dynamic import)
333
+ this._fb = null;
334
+ // Unsubscribe handles for Firebase listeners
335
+ this._listeners = [];
336
+ // Callbacks
337
+ this._onPeerDiscovered = [];
338
+ this._onPeerLeft = [];
339
+ this._onSignal = [];
340
+ this._onLobby = [];
341
+ // State
342
+ this._knownPeers = /* @__PURE__ */ new Set();
343
+ this._lobbyAnnounceTimer = null;
344
+ this._destroyed = false;
345
+ this.selfId = generatePeerId();
346
+ this._appId = appId;
347
+ this._config = config;
348
+ }
349
+ // ── Public API ──
350
+ async init() {
351
+ return this._ensureInit();
352
+ }
353
+ async joinRoom(roomId, peerMeta) {
354
+ await this._ensureInit();
355
+ if (!this._db || !this._fb) throw new Error("Firebase not initialized");
356
+ this._joinGeneration++;
357
+ this._roomId = roomId;
358
+ this._peerMeta = peerMeta;
359
+ const { ref, set, onChildAdded, onChildRemoved, onDisconnect, remove } = this._fb;
360
+ const paths = firebasePaths(this._appId, roomId, this.selfId);
361
+ await remove(ref(this._db, paths.peerSignalInbox)).catch(() => {
362
+ });
363
+ const presenceRef = ref(this._db, paths.peerPresence);
364
+ await set(presenceRef, {
365
+ peerId: this.selfId,
366
+ meta: peerMeta,
367
+ ts: Date.now()
368
+ });
369
+ onDisconnect(presenceRef).remove();
370
+ const peersRef = ref(this._db, paths.peers);
371
+ const addedUnsub = onChildAdded(peersRef, (snapshot) => {
372
+ const data = snapshot.val();
373
+ if (!data || data.peerId === this.selfId) return;
374
+ if (!this._knownPeers.has(data.peerId)) {
375
+ this._knownPeers.add(data.peerId);
376
+ for (const cb of this._onPeerDiscovered) cb(data.peerId, data.meta ?? {});
377
+ }
378
+ });
379
+ this._listeners.push(() => addedUnsub());
380
+ const removedUnsub = onChildRemoved(peersRef, (snapshot) => {
381
+ const data = snapshot.val();
382
+ const peerId = data?.peerId ?? snapshot.key;
383
+ if (peerId && this._knownPeers.has(peerId)) {
384
+ this._knownPeers.delete(peerId);
385
+ for (const cb of this._onPeerLeft) cb(peerId);
386
+ }
387
+ });
388
+ this._listeners.push(() => removedUnsub());
389
+ const signalRef = ref(this._db, paths.peerSignalInbox);
390
+ const signalUnsub = onChildAdded(signalRef, (snapshot) => {
391
+ const msg = snapshot.val();
392
+ if (!msg || msg.from === this.selfId) return;
393
+ for (const cb of this._onSignal) cb(msg.from, msg.data);
394
+ remove(snapshot.ref);
395
+ });
396
+ this._listeners.push(() => signalUnsub());
397
+ }
398
+ async leaveRoom() {
399
+ if (!this._db || !this._fb || !this._roomId) return;
400
+ const leavingRoomId = this._roomId;
401
+ const generation = this._joinGeneration;
402
+ const { ref, remove } = this._fb;
403
+ const paths = firebasePaths(this._appId, leavingRoomId, this.selfId);
404
+ for (const unsub of this._listeners) unsub();
405
+ this._listeners = [];
406
+ await Promise.all([
407
+ remove(ref(this._db, paths.peerPresence)),
408
+ remove(ref(this._db, paths.peerSignalInbox))
409
+ ]).catch(() => {
410
+ });
411
+ if (this._lobbyAnnounceTimer) {
412
+ clearInterval(this._lobbyAnnounceTimer);
413
+ this._lobbyAnnounceTimer = null;
414
+ }
415
+ this._knownPeers.clear();
416
+ if (this._joinGeneration === generation) {
417
+ this._roomId = null;
418
+ }
419
+ }
420
+ signal(targetPeerId, data) {
421
+ if (!this._db || !this._fb || !this._roomId) return;
422
+ const { ref, push } = this._fb;
423
+ const inboxPath = firebasePaths(this._appId, this._roomId, targetPeerId).peerSignalInbox;
424
+ push(ref(this._db, inboxPath), {
425
+ from: this.selfId,
426
+ data: sanitizeForFirebase(data),
427
+ ts: Date.now()
428
+ });
429
+ }
430
+ subscribeToLobby(cb) {
431
+ this._onLobby.push(cb);
432
+ this._ensureInit().then(() => {
433
+ if (!this._db || !this._fb || this._destroyed) return;
434
+ const { ref, onValue } = this._fb;
435
+ const paths = firebasePaths(this._appId, "", "");
436
+ const lobbyRef = ref(this._db, paths.lobby);
437
+ const unsub = onValue(lobbyRef, (snapshot) => {
438
+ const data = snapshot.val();
439
+ if (!data) {
440
+ for (const lcb of this._onLobby) lcb([]);
441
+ return;
442
+ }
443
+ const now = Date.now();
444
+ const rooms = Object.values(data).filter(
445
+ (r) => r && now - (r.lastSeen ?? 0) < ROOM_ANNOUNCE_EXPIRY_MS
446
+ );
447
+ for (const lcb of this._onLobby) lcb(rooms);
448
+ });
449
+ this._listeners.push(() => unsub());
450
+ });
451
+ return () => {
452
+ removeFromArray(this._onLobby, cb);
453
+ };
454
+ }
455
+ announceRoom(announcement) {
456
+ if (!this._db || !this._fb) return;
457
+ const { ref, set } = this._fb;
458
+ const paths = firebasePaths(this._appId, announcement.roomId, "");
459
+ announcement.lastSeen = Date.now();
460
+ set(ref(this._db, paths.roomLobbyEntry), announcement);
461
+ if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);
462
+ this._lobbyAnnounceTimer = setInterval(() => {
463
+ announcement.lastSeen = Date.now();
464
+ set(ref(this._db, paths.roomLobbyEntry), announcement);
465
+ }, ROOM_ANNOUNCE_INTERVAL_MS);
466
+ }
467
+ removeRoomAnnouncement(roomId) {
468
+ if (!this._db || !this._fb) return;
469
+ const { ref, remove } = this._fb;
470
+ remove(ref(this._db, firebasePaths(this._appId, roomId, "").roomLobbyEntry));
471
+ if (this._lobbyAnnounceTimer) {
472
+ clearInterval(this._lobbyAnnounceTimer);
473
+ this._lobbyAnnounceTimer = null;
474
+ }
475
+ }
476
+ onPeerDiscovered(cb) {
477
+ this._onPeerDiscovered.push(cb);
478
+ return () => {
479
+ removeFromArray(this._onPeerDiscovered, cb);
480
+ };
481
+ }
482
+ onPeerLeft(cb) {
483
+ this._onPeerLeft.push(cb);
484
+ return () => {
485
+ removeFromArray(this._onPeerLeft, cb);
486
+ };
487
+ }
488
+ onSignal(cb) {
489
+ this._onSignal.push(cb);
490
+ return () => {
491
+ removeFromArray(this._onSignal, cb);
492
+ };
493
+ }
494
+ destroy() {
495
+ this._destroyed = true;
496
+ for (const unsub of this._listeners) unsub();
497
+ this._listeners = [];
498
+ if (this._lobbyAnnounceTimer) {
499
+ clearInterval(this._lobbyAnnounceTimer);
500
+ this._lobbyAnnounceTimer = null;
501
+ }
502
+ if (this._db && this._fb && this._roomId) {
503
+ const { ref, remove } = this._fb;
504
+ const paths = firebasePaths(this._appId, this._roomId, this.selfId);
505
+ remove(ref(this._db, paths.peerPresence)).catch(() => {
506
+ });
507
+ remove(ref(this._db, paths.peerSignalInbox)).catch(() => {
508
+ });
509
+ }
510
+ if (this._ownApp && this._firebaseApp) {
511
+ import("firebase/app").then(({ deleteApp }) => {
512
+ deleteApp(this._firebaseApp).catch(() => {
513
+ });
514
+ });
515
+ }
516
+ this._db = null;
517
+ this._firebaseApp = null;
518
+ this._fb = null;
519
+ this._knownPeers.clear();
520
+ this._onPeerDiscovered = [];
521
+ this._onPeerLeft = [];
522
+ this._onSignal = [];
523
+ this._onLobby = [];
524
+ }
525
+ // ── Private ──
526
+ _ensureInit() {
527
+ if (!this._initPromise) {
528
+ this._initPromise = this._doInit();
529
+ }
530
+ return this._initPromise;
531
+ }
532
+ async _doInit() {
533
+ const { initializeApp, getApps } = await import("firebase/app");
534
+ const {
535
+ getDatabase,
536
+ ref,
537
+ set,
538
+ push,
539
+ remove,
540
+ onValue,
541
+ onChildAdded,
542
+ onChildRemoved,
543
+ onDisconnect
544
+ } = await import("firebase/database");
545
+ this._fb = { ref, set, push, remove, onValue, onChildAdded, onChildRemoved, onDisconnect };
546
+ if (this._config.firebaseApp) {
547
+ this._firebaseApp = this._config.firebaseApp;
548
+ this._ownApp = false;
549
+ } else {
550
+ const appName = `carver_${this._appId}`;
551
+ const existing = getApps().find((a) => a.name === appName);
552
+ if (existing) {
553
+ this._firebaseApp = existing;
554
+ this._ownApp = false;
555
+ } else {
556
+ this._firebaseApp = initializeApp({ databaseURL: this._config.databaseURL }, appName);
557
+ this._ownApp = true;
558
+ }
559
+ }
560
+ this._db = getDatabase(this._firebaseApp);
561
+ }
562
+ };
563
+ function sanitizeForFirebase(obj) {
564
+ if (obj === null) return "__null__";
565
+ if (Array.isArray(obj)) return obj.map(sanitizeForFirebase);
566
+ if (typeof obj === "object" && obj !== null) {
567
+ const result = {};
568
+ for (const [key, value] of Object.entries(obj)) {
569
+ result[key] = value === null ? "__null__" : sanitizeForFirebase(value);
570
+ }
571
+ return result;
572
+ }
573
+ return obj;
574
+ }
575
+
576
+ export {
577
+ generatePeerId,
578
+ MqttStrategy,
579
+ FirebaseStrategy
580
+ };
581
+ //# sourceMappingURL=chunk-UD6FDZMX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/strategy/utils.ts","../src/transport/strategy/mqtt.ts","../src/transport/strategy/firebase.ts"],"sourcesContent":["/** Generate a random peer ID (20 chars, URL-safe) */\nexport function generatePeerId(): string {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n const bytes = new Uint8Array(20);\n crypto.getRandomValues(bytes);\n let id = '';\n for (let i = 0; i < 20; i++) {\n id += chars[bytes[i] % chars.length];\n }\n return id;\n}\n\n/** Build MQTT topic paths for a given appId + roomId */\nexport function mqttTopics(appId: string, roomId: string, peerId?: string) {\n const base = `carver/${appId}`;\n return {\n /** Lobby wildcard: subscribe to discover all room announcements */\n lobbyWildcard: `${base}/lobby/+`,\n /** Single room lobby entry */\n roomLobbyEntry: `${base}/lobby/${roomId}`,\n /** Wildcard for all peer presence in a room */\n roomPresenceWildcard: `${base}/room/${roomId}/presence/+`,\n /** This peer's presence topic */\n peerPresence: peerId ? `${base}/room/${roomId}/presence/${peerId}` : '',\n /** This peer's signal inbox */\n peerSignalInbox: peerId ? `${base}/room/${roomId}/signal/${peerId}` : '',\n };\n}\n\n/** Build Firebase RTDB paths for a given appId + roomId */\nexport function firebasePaths(appId: string, roomId: string, peerId?: string) {\n const base = `${appId}/__carver__`;\n return {\n lobby: `${base}/lobby`,\n roomLobbyEntry: `${base}/lobby/${roomId}`,\n peers: `${base}/rooms/${roomId}/peers`,\n peerPresence: peerId ? `${base}/rooms/${roomId}/peers/${peerId}` : '',\n peerSignalInbox: peerId ? `${base}/rooms/${roomId}/signals/${peerId}` : '',\n };\n}\n\n/** Default MQTT brokers (WebSocket endpoints, free/public) */\nexport const DEFAULT_MQTT_BROKERS = [\n 'wss://broker.emqx.io:8084/mqtt',\n 'wss://test.mosquitto.org:8081/mqtt',\n];\n\n/** Room announcement expiry time (30s without heartbeat = stale) */\nexport const ROOM_ANNOUNCE_EXPIRY_MS = 30_000;\n\n/** Room announcement heartbeat interval */\nexport const ROOM_ANNOUNCE_INTERVAL_MS = 10_000;\n\n/** Peer presence heartbeat interval */\nexport const PRESENCE_HEARTBEAT_MS = 5_000;\n\n/** Peer expiry: 3 missed heartbeats */\nexport const PEER_EXPIRY_MS = PRESENCE_HEARTBEAT_MS * 3;\n\n/** Rapid warmup announces for faster initial peer discovery */\nexport const PRESENCE_WARMUP_DELAYS_MS = [200, 500, 1500];\n\n/** Remove an item from an array by reference. Returns true if found. */\nexport function removeFromArray<T>(arr: T[], item: T): boolean {\n const idx = arr.indexOf(item);\n if (idx >= 0) {\n arr.splice(idx, 1);\n return true;\n }\n return false;\n}\n","import type {\n SignalingStrategy,\n PeerMetadata,\n RoomAnnouncement,\n MqttStrategyConfig,\n} from \"./types\";\nimport {\n generatePeerId,\n mqttTopics,\n removeFromArray,\n DEFAULT_MQTT_BROKERS,\n ROOM_ANNOUNCE_EXPIRY_MS,\n ROOM_ANNOUNCE_INTERVAL_MS,\n PRESENCE_HEARTBEAT_MS,\n PEER_EXPIRY_MS,\n PRESENCE_WARMUP_DELAYS_MS,\n} from \"./utils\";\n\n// mqtt.MqttClient type (avoid hard import at module level)\ntype MqttClient = {\n on(event: string, cb: (...args: any[]) => void): void;\n subscribe(topic: string | string[], opts: Record<string, unknown>, cb?: (err: Error | null) => void): void;\n unsubscribe(topic: string | string[]): void;\n publish(topic: string, payload: string | Buffer, opts?: Record<string, unknown>): void;\n end(force?: boolean): void;\n};\n\n/**\n * MQTT-based signaling strategy.\n *\n * Connects to public MQTT brokers over WebSocket. Peer discovery uses\n * retained presence messages with periodic heartbeats. SDP/ICE relay\n * uses per-peer signal topics. Room discovery uses retained lobby topics.\n *\n * Zero infrastructure cost -- uses free public brokers by default.\n */\nexport class MqttStrategy implements SignalingStrategy {\n readonly selfId: string;\n\n private _appId: string;\n private _config: MqttStrategyConfig;\n private _client: MqttClient | null = null;\n private _roomId: string | null = null;\n private _peerMeta: PeerMetadata = {};\n /** Monotonic counter to detect stale leaveRoom completions */\n private _joinGeneration = 0;\n\n // Lazy init\n private _initPromise: Promise<void> | null = null;\n\n // Callbacks\n private _onPeerDiscovered: ((peerId: string, meta: PeerMetadata) => void)[] = [];\n private _onPeerLeft: ((peerId: string) => void)[] = [];\n private _onSignal: ((fromPeerId: string, data: unknown) => void)[] = [];\n private _onLobby: ((rooms: RoomAnnouncement[]) => void)[] = [];\n\n // State\n private _knownPeers = new Map<string, { meta: PeerMetadata; lastSeen: number }>();\n private _lobbyRooms = new Map<string, RoomAnnouncement>();\n private _presenceTimer: ReturnType<typeof setInterval> | null = null;\n private _warmupTimers: ReturnType<typeof setTimeout>[] = [];\n private _lobbyAnnounceTimer: ReturnType<typeof setInterval> | null = null;\n private _peerExpiryTimer: ReturnType<typeof setInterval> | null = null;\n private _lobbySubscribed = false;\n private _destroyed = false;\n\n constructor(appId: string, config: MqttStrategyConfig = { type: 'mqtt' }) {\n this.selfId = generatePeerId();\n this._appId = appId;\n this._config = config;\n }\n\n // ── Public API ──\n\n async init(): Promise<void> {\n return this._ensureInit();\n }\n\n async joinRoom(roomId: string, peerMeta: PeerMetadata): Promise<void> {\n await this._ensureInit();\n if (!this._client) throw new Error('MQTT client not available');\n\n this._joinGeneration++;\n this._roomId = roomId;\n this._peerMeta = peerMeta;\n\n const topics = mqttTopics(this._appId, roomId, this.selfId);\n\n // Subscribe to room presence (discover peers) and own signal inbox\n await new Promise<void>((resolve, reject) => {\n this._client!.subscribe(\n [topics.roomPresenceWildcard, topics.peerSignalInbox],\n { qos: 1 },\n (err: Error | null) => (err ? reject(err) : resolve()),\n );\n });\n\n // Publish retained presence\n this._publishPresence();\n\n // Rapid warmup announces for fast peer discovery\n for (const delay of PRESENCE_WARMUP_DELAYS_MS) {\n this._warmupTimers.push(setTimeout(() => this._publishPresence(), delay));\n }\n\n // Periodic heartbeat\n this._presenceTimer = setInterval(() => this._publishPresence(), PRESENCE_HEARTBEAT_MS);\n\n // Periodic peer expiry check\n this._peerExpiryTimer = setInterval(() => this._checkPeerExpiry(), PRESENCE_HEARTBEAT_MS);\n }\n\n async leaveRoom(): Promise<void> {\n if (!this._client || !this._roomId) return;\n\n const generation = this._joinGeneration;\n const topics = mqttTopics(this._appId, this._roomId, this.selfId);\n this._clearRoomTimers();\n\n // Clear retained presence\n this._client.publish(topics.peerPresence, '', { retain: true, qos: 1 });\n\n // Unsubscribe from room topics\n this._client.unsubscribe([topics.roomPresenceWildcard, topics.peerSignalInbox]);\n\n this._knownPeers.clear();\n\n // Only null out _roomId if no new joinRoom() has run since we started.\n if (this._joinGeneration === generation) {\n this._roomId = null;\n }\n }\n\n signal(targetPeerId: string, data: unknown): void {\n if (!this._client || !this._roomId) return;\n const targetTopic = `carver/${this._appId}/room/${this._roomId}/signal/${targetPeerId}`;\n this._client.publish(\n targetTopic,\n JSON.stringify({ from: this.selfId, data, ts: Date.now() }),\n { qos: 1 },\n );\n }\n\n subscribeToLobby(cb: (rooms: RoomAnnouncement[]) => void): () => void {\n this._onLobby.push(cb);\n\n // Subscribe to lobby topic (lazy -- waits for init)\n if (!this._lobbySubscribed) {\n this._lobbySubscribed = true;\n this._ensureInit().then(() => {\n if (this._client && !this._destroyed) {\n const lobbyTopic = mqttTopics(this._appId, '', '').lobbyWildcard;\n this._client.subscribe(lobbyTopic, { qos: 0 });\n }\n });\n }\n\n return () => {\n removeFromArray(this._onLobby, cb);\n };\n }\n\n announceRoom(announcement: RoomAnnouncement): void {\n if (!this._client) return;\n const topic = mqttTopics(this._appId, announcement.roomId, '').roomLobbyEntry;\n\n announcement.lastSeen = Date.now();\n this._client.publish(topic, JSON.stringify(announcement), { retain: true, qos: 1 });\n\n // Periodic heartbeat\n if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = setInterval(() => {\n announcement.lastSeen = Date.now();\n this._client?.publish(topic, JSON.stringify(announcement), { retain: true, qos: 1 });\n }, ROOM_ANNOUNCE_INTERVAL_MS);\n }\n\n removeRoomAnnouncement(roomId: string): void {\n if (!this._client) return;\n const topic = mqttTopics(this._appId, roomId, '').roomLobbyEntry;\n this._client.publish(topic, '', { retain: true, qos: 1 });\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n }\n\n onPeerDiscovered(cb: (peerId: string, meta: PeerMetadata) => void): () => void {\n this._onPeerDiscovered.push(cb);\n return () => { removeFromArray(this._onPeerDiscovered, cb); };\n }\n\n onPeerLeft(cb: (peerId: string) => void): () => void {\n this._onPeerLeft.push(cb);\n return () => { removeFromArray(this._onPeerLeft, cb); };\n }\n\n onSignal(cb: (fromPeerId: string, data: unknown) => void): () => void {\n this._onSignal.push(cb);\n return () => { removeFromArray(this._onSignal, cb); };\n }\n\n destroy(): void {\n this._destroyed = true;\n this._clearRoomTimers();\n\n // Clean up retained presence\n if (this._client && this._roomId) {\n const topics = mqttTopics(this._appId, this._roomId, this.selfId);\n this._client.publish(topics.peerPresence, '', { retain: true, qos: 1 });\n }\n\n this._client?.end(true);\n this._client = null;\n this._knownPeers.clear();\n this._lobbyRooms.clear();\n this._onPeerDiscovered = [];\n this._onPeerLeft = [];\n this._onSignal = [];\n this._onLobby = [];\n }\n\n // ── Private ──\n\n private _ensureInit(): Promise<void> {\n if (!this._initPromise) {\n this._initPromise = this._doInit();\n }\n return this._initPromise;\n }\n\n private async _doInit(): Promise<void> {\n const mqtt = await import('mqtt');\n const brokers = this._config.brokerUrls ?? DEFAULT_MQTT_BROKERS;\n // Pick a random broker from the available pool\n const brokerUrl = brokers[Math.floor(Math.random() * brokers.length)];\n\n return new Promise<void>((resolve, reject) => {\n const connectFn = mqtt.default?.connect ?? mqtt.connect;\n this._client = connectFn(brokerUrl, {\n clientId: `carver_${this.selfId}`,\n clean: true,\n connectTimeout: 10_000,\n keepalive: 30,\n }) as MqttClient;\n\n this._client.on('connect', () => {\n if (!this._destroyed) resolve();\n });\n\n this._client.on('error', (err: Error) => {\n if (!this._initPromise) return; // already resolved\n reject(err);\n });\n\n this._client.on('message', (topic: string, payload: Buffer) => {\n this._handleMessage(topic, payload);\n });\n });\n }\n\n private _publishPresence(): void {\n if (!this._client || !this._roomId) return;\n const topics = mqttTopics(this._appId, this._roomId, this.selfId);\n this._client.publish(\n topics.peerPresence,\n JSON.stringify({ peerId: this.selfId, meta: this._peerMeta, ts: Date.now() }),\n { retain: true, qos: 1 },\n );\n }\n\n private _handleMessage(topic: string, payload: Buffer): void {\n const raw = payload.toString();\n\n // ── Presence message ──\n const presenceMatch = topic.match(/\\/room\\/[^/]+\\/presence\\/([^/]+)$/);\n if (presenceMatch) {\n const peerId = presenceMatch[1];\n if (peerId === this.selfId) return;\n\n if (!raw) {\n // Empty retained = peer left\n if (this._knownPeers.has(peerId)) {\n this._knownPeers.delete(peerId);\n for (const cb of this._onPeerLeft) cb(peerId);\n }\n return;\n }\n try {\n const msg = JSON.parse(raw);\n const isNew = !this._knownPeers.has(peerId);\n this._knownPeers.set(peerId, { meta: msg.meta ?? {}, lastSeen: msg.ts ?? Date.now() });\n if (isNew) {\n for (const cb of this._onPeerDiscovered) cb(peerId, msg.meta ?? {});\n }\n } catch { /* ignore malformed */ }\n return;\n }\n\n // ── Signal message (SDP / ICE) ──\n const signalMatch = topic.match(/\\/room\\/[^/]+\\/signal\\/([^/]+)$/);\n if (signalMatch) {\n try {\n const msg = JSON.parse(raw);\n if (msg.from && msg.from !== this.selfId) {\n for (const cb of this._onSignal) cb(msg.from, msg.data);\n }\n } catch { /* ignore malformed */ }\n return;\n }\n\n // ── Lobby announcement ──\n const lobbyMatch = topic.match(/\\/lobby\\/([^/]+)$/);\n if (lobbyMatch) {\n const roomId = lobbyMatch[1];\n if (!raw) {\n this._lobbyRooms.delete(roomId);\n } else {\n try {\n const ann = JSON.parse(raw) as RoomAnnouncement;\n if (Date.now() - ann.lastSeen < ROOM_ANNOUNCE_EXPIRY_MS) {\n this._lobbyRooms.set(roomId, ann);\n } else {\n this._lobbyRooms.delete(roomId);\n }\n } catch { /* ignore */ }\n }\n const rooms = Array.from(this._lobbyRooms.values());\n for (const cb of this._onLobby) cb(rooms);\n }\n }\n\n private _checkPeerExpiry(): void {\n const now = Date.now();\n for (const [peerId, data] of this._knownPeers) {\n if (now - data.lastSeen > PEER_EXPIRY_MS) {\n this._knownPeers.delete(peerId);\n for (const cb of this._onPeerLeft) cb(peerId);\n }\n }\n }\n\n private _clearRoomTimers(): void {\n if (this._presenceTimer) { clearInterval(this._presenceTimer); this._presenceTimer = null; }\n for (const t of this._warmupTimers) clearTimeout(t);\n this._warmupTimers = [];\n if (this._lobbyAnnounceTimer) { clearInterval(this._lobbyAnnounceTimer); this._lobbyAnnounceTimer = null; }\n if (this._peerExpiryTimer) { clearInterval(this._peerExpiryTimer); this._peerExpiryTimer = null; }\n }\n}\n","import type {\n SignalingStrategy,\n PeerMetadata,\n RoomAnnouncement,\n FirebaseStrategyConfig,\n} from \"./types\";\nimport {\n generatePeerId,\n firebasePaths,\n removeFromArray,\n ROOM_ANNOUNCE_EXPIRY_MS,\n ROOM_ANNOUNCE_INTERVAL_MS,\n} from \"./utils\";\n\n/**\n * Firebase Realtime Database signaling strategy.\n *\n * Requires the `firebase` package as a peer dependency.\n * Pass either a `databaseURL` (auto-creates a namespaced Firebase app)\n * or an existing `firebaseApp` instance.\n *\n * Presence cleanup is automatic via Firebase onDisconnect().\n */\nexport class FirebaseStrategy implements SignalingStrategy {\n readonly selfId: string;\n\n private _appId: string;\n private _config: FirebaseStrategyConfig;\n private _db: any = null;\n private _firebaseApp: any = null;\n private _ownApp = false;\n private _roomId: string | null = null;\n private _peerMeta: PeerMetadata = {};\n /** Monotonic counter to detect stale leaveRoom completions */\n private _joinGeneration = 0;\n\n // Lazy init\n private _initPromise: Promise<void> | null = null;\n\n // Firebase module references (filled after dynamic import)\n private _fb: {\n ref: any;\n set: any;\n push: any;\n remove: any;\n onValue: any;\n onChildAdded: any;\n onChildRemoved: any;\n onDisconnect: any;\n } | null = null;\n\n // Unsubscribe handles for Firebase listeners\n private _listeners: (() => void)[] = [];\n\n // Callbacks\n private _onPeerDiscovered: ((peerId: string, meta: PeerMetadata) => void)[] = [];\n private _onPeerLeft: ((peerId: string) => void)[] = [];\n private _onSignal: ((fromPeerId: string, data: unknown) => void)[] = [];\n private _onLobby: ((rooms: RoomAnnouncement[]) => void)[] = [];\n\n // State\n private _knownPeers = new Set<string>();\n private _lobbyAnnounceTimer: ReturnType<typeof setInterval> | null = null;\n private _destroyed = false;\n\n constructor(appId: string, config: FirebaseStrategyConfig) {\n this.selfId = generatePeerId();\n this._appId = appId;\n this._config = config;\n }\n\n // ── Public API ──\n\n async init(): Promise<void> {\n return this._ensureInit();\n }\n\n async joinRoom(roomId: string, peerMeta: PeerMetadata): Promise<void> {\n await this._ensureInit();\n if (!this._db || !this._fb) throw new Error('Firebase not initialized');\n\n // Bump generation so any in-flight leaveRoom from a prior call won't\n // null out _roomId after we set it here (React StrictMode race fix).\n this._joinGeneration++;\n\n this._roomId = roomId;\n this._peerMeta = peerMeta;\n const { ref, set, onChildAdded, onChildRemoved, onDisconnect, remove } = this._fb;\n const paths = firebasePaths(this._appId, roomId, this.selfId);\n\n // Clean stale signals from our inbox before listening (prevents\n // replaying SDP from a previous session that wasn't cleaned up).\n await remove(ref(this._db, paths.peerSignalInbox)).catch(() => {});\n\n // 1. Write presence with auto-cleanup on disconnect\n const presenceRef = ref(this._db, paths.peerPresence);\n await set(presenceRef, {\n peerId: this.selfId,\n meta: peerMeta,\n ts: Date.now(),\n });\n onDisconnect(presenceRef).remove();\n\n // 2. Listen for peers joining\n const peersRef = ref(this._db, paths.peers);\n const addedUnsub = onChildAdded(peersRef, (snapshot: any) => {\n const data = snapshot.val();\n if (!data || data.peerId === this.selfId) return;\n if (!this._knownPeers.has(data.peerId)) {\n this._knownPeers.add(data.peerId);\n for (const cb of this._onPeerDiscovered) cb(data.peerId, data.meta ?? {});\n }\n });\n this._listeners.push(() => addedUnsub());\n\n // 3. Listen for peers leaving\n const removedUnsub = onChildRemoved(peersRef, (snapshot: any) => {\n const data = snapshot.val();\n const peerId = data?.peerId ?? snapshot.key;\n if (peerId && this._knownPeers.has(peerId)) {\n this._knownPeers.delete(peerId);\n for (const cb of this._onPeerLeft) cb(peerId);\n }\n });\n this._listeners.push(() => removedUnsub());\n\n // 4. Listen for signals addressed to us\n const signalRef = ref(this._db, paths.peerSignalInbox);\n const signalUnsub = onChildAdded(signalRef, (snapshot: any) => {\n const msg = snapshot.val();\n if (!msg || msg.from === this.selfId) return;\n for (const cb of this._onSignal) cb(msg.from, msg.data);\n // Remove processed signal to keep the inbox clean\n remove(snapshot.ref);\n });\n this._listeners.push(() => signalUnsub());\n }\n\n async leaveRoom(): Promise<void> {\n if (!this._db || !this._fb || !this._roomId) return;\n\n // Capture current state so async cleanup targets the correct room\n // even if joinRoom() is called concurrently (React StrictMode).\n const leavingRoomId = this._roomId;\n const generation = this._joinGeneration;\n\n const { ref, remove } = this._fb;\n const paths = firebasePaths(this._appId, leavingRoomId, this.selfId);\n\n // Detach listeners\n for (const unsub of this._listeners) unsub();\n this._listeners = [];\n\n // Remove presence and signal inbox\n await Promise.all([\n remove(ref(this._db, paths.peerPresence)),\n remove(ref(this._db, paths.peerSignalInbox)),\n ]).catch(() => {});\n\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n\n this._knownPeers.clear();\n\n // Only null out _roomId if no new joinRoom() has run since we started.\n // This prevents the StrictMode race: old leaveRoom completing after\n // new joinRoom already set _roomId to the fresh value.\n if (this._joinGeneration === generation) {\n this._roomId = null;\n }\n }\n\n signal(targetPeerId: string, data: unknown): void {\n if (!this._db || !this._fb || !this._roomId) return;\n const { ref, push } = this._fb;\n\n // Atomic push: single operation writes the key + data together.\n // Avoids the push() + set() two-step that can cause onChildAdded to\n // fire with null if the listener catches the intermediate state.\n const inboxPath = firebasePaths(this._appId, this._roomId, targetPeerId).peerSignalInbox;\n push(ref(this._db, inboxPath), {\n from: this.selfId,\n data: sanitizeForFirebase(data),\n ts: Date.now(),\n });\n }\n\n subscribeToLobby(cb: (rooms: RoomAnnouncement[]) => void): () => void {\n this._onLobby.push(cb);\n\n this._ensureInit().then(() => {\n if (!this._db || !this._fb || this._destroyed) return;\n const { ref, onValue } = this._fb;\n const paths = firebasePaths(this._appId, '', '');\n const lobbyRef = ref(this._db, paths.lobby);\n\n const unsub = onValue(lobbyRef, (snapshot: any) => {\n const data = snapshot.val();\n if (!data) {\n for (const lcb of this._onLobby) lcb([]);\n return;\n }\n const now = Date.now();\n const rooms: RoomAnnouncement[] = Object.values(data).filter(\n (r: any) => r && now - (r.lastSeen ?? 0) < ROOM_ANNOUNCE_EXPIRY_MS,\n ) as RoomAnnouncement[];\n for (const lcb of this._onLobby) lcb(rooms);\n });\n this._listeners.push(() => unsub());\n });\n\n return () => {\n removeFromArray(this._onLobby, cb);\n };\n }\n\n announceRoom(announcement: RoomAnnouncement): void {\n if (!this._db || !this._fb) return;\n const { ref, set } = this._fb;\n const paths = firebasePaths(this._appId, announcement.roomId, '');\n\n announcement.lastSeen = Date.now();\n set(ref(this._db, paths.roomLobbyEntry), announcement);\n\n // Periodic heartbeat\n if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = setInterval(() => {\n announcement.lastSeen = Date.now();\n set(ref(this._db, paths.roomLobbyEntry), announcement);\n }, ROOM_ANNOUNCE_INTERVAL_MS);\n }\n\n removeRoomAnnouncement(roomId: string): void {\n if (!this._db || !this._fb) return;\n const { ref, remove } = this._fb;\n remove(ref(this._db, firebasePaths(this._appId, roomId, '').roomLobbyEntry));\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n }\n\n onPeerDiscovered(cb: (peerId: string, meta: PeerMetadata) => void): () => void {\n this._onPeerDiscovered.push(cb);\n return () => { removeFromArray(this._onPeerDiscovered, cb); };\n }\n\n onPeerLeft(cb: (peerId: string) => void): () => void {\n this._onPeerLeft.push(cb);\n return () => { removeFromArray(this._onPeerLeft, cb); };\n }\n\n onSignal(cb: (fromPeerId: string, data: unknown) => void): () => void {\n this._onSignal.push(cb);\n return () => { removeFromArray(this._onSignal, cb); };\n }\n\n destroy(): void {\n this._destroyed = true;\n for (const unsub of this._listeners) unsub();\n this._listeners = [];\n\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n\n // Best-effort cleanup\n if (this._db && this._fb && this._roomId) {\n const { ref, remove } = this._fb;\n const paths = firebasePaths(this._appId, this._roomId, this.selfId);\n remove(ref(this._db, paths.peerPresence)).catch(() => {});\n remove(ref(this._db, paths.peerSignalInbox)).catch(() => {});\n }\n\n // Delete own Firebase app if we created it\n if (this._ownApp && this._firebaseApp) {\n import('firebase/app').then(({ deleteApp }) => {\n deleteApp(this._firebaseApp).catch(() => {});\n });\n }\n\n this._db = null;\n this._firebaseApp = null;\n this._fb = null;\n this._knownPeers.clear();\n this._onPeerDiscovered = [];\n this._onPeerLeft = [];\n this._onSignal = [];\n this._onLobby = [];\n }\n\n // ── Private ──\n\n private _ensureInit(): Promise<void> {\n if (!this._initPromise) {\n this._initPromise = this._doInit();\n }\n return this._initPromise;\n }\n\n private async _doInit(): Promise<void> {\n const { initializeApp, getApps } = await import('firebase/app');\n const {\n getDatabase,\n ref,\n set,\n push,\n remove,\n onValue,\n onChildAdded,\n onChildRemoved,\n onDisconnect,\n } = await import('firebase/database');\n\n this._fb = { ref, set, push, remove, onValue, onChildAdded, onChildRemoved, onDisconnect };\n\n if (this._config.firebaseApp) {\n this._firebaseApp = this._config.firebaseApp;\n this._ownApp = false;\n } else {\n const appName = `carver_${this._appId}`;\n const existing = getApps().find((a: any) => a.name === appName);\n if (existing) {\n this._firebaseApp = existing;\n this._ownApp = false;\n } else {\n this._firebaseApp = initializeApp({ databaseURL: this._config.databaseURL }, appName);\n this._ownApp = true;\n }\n }\n\n this._db = getDatabase(this._firebaseApp);\n }\n}\n\n/**\n * Firebase RTDB deletes any key whose value is `null` (treats null as \"remove\").\n * ICE candidates from `toJSON()` can contain `null` fields (e.g. usernameFragment).\n * We recursively replace `null` with a sentinel so Firebase preserves the key.\n * On the receiving end, `_handleSignal` doesn't need to reverse this because\n * `new RTCIceCandidate()` / `new RTCSessionDescription()` accept missing fields.\n */\nfunction sanitizeForFirebase(obj: unknown): unknown {\n if (obj === null) return '__null__';\n if (Array.isArray(obj)) return obj.map(sanitizeForFirebase);\n if (typeof obj === 'object' && obj !== null) {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[key] = value === null ? '__null__' : sanitizeForFirebase(value);\n }\n return result;\n }\n return obj;\n}\n"],"mappings":"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","names":[]}