@edge-base/web 0.1.5 → 0.2.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.
- package/README.md +25 -0
- package/dist/analytics.d.ts +2 -0
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +9 -2
- package/dist/analytics.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +11 -2
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +9 -2
- package/dist/client.js.map +1 -1
- package/dist/database-live.d.ts +10 -3
- package/dist/database-live.d.ts.map +1 -1
- package/dist/database-live.js +68 -17
- package/dist/database-live.js.map +1 -1
- package/dist/room-cloudflare-media.d.ts +111 -0
- package/dist/room-cloudflare-media.d.ts.map +1 -0
- package/dist/room-cloudflare-media.js +405 -0
- package/dist/room-cloudflare-media.js.map +1 -0
- package/dist/room-p2p-media.d.ts +116 -0
- package/dist/room-p2p-media.d.ts.map +1 -0
- package/dist/room-p2p-media.js +690 -0
- package/dist/room-p2p-media.js.map +1 -0
- package/dist/room-realtime-media.d.ts.map +1 -1
- package/dist/room-realtime-media.js +7 -8
- package/dist/room-realtime-media.js.map +1 -1
- package/dist/room.d.ts +93 -16
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +281 -164
- package/dist/room.js.map +1 -1
- package/dist/token-manager.d.ts.map +1 -1
- package/dist/token-manager.js +3 -0
- package/dist/token-manager.js.map +1 -1
- package/dist/turnstile.d.ts.map +1 -1
- package/dist/turnstile.js +17 -18
- package/dist/turnstile.js.map +1 -1
- package/llms.txt +20 -2
- package/package.json +3 -2
package/dist/room.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { EdgeBaseError } from '@edge-base/core';
|
|
1
|
+
import { EdgeBaseError, createSubscription } from '@edge-base/core';
|
|
2
2
|
import { refreshAccessToken } from './auth-refresh.js';
|
|
3
|
-
import {
|
|
3
|
+
import { RoomCloudflareMediaTransport, } from './room-cloudflare-media.js';
|
|
4
|
+
import { RoomP2PMediaTransport, } from './room-p2p-media.js';
|
|
5
|
+
export { createSubscription };
|
|
4
6
|
// ─── Helpers ───
|
|
5
7
|
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
6
8
|
function deepSet(obj, path, value) {
|
|
@@ -89,6 +91,34 @@ export class RoomClient {
|
|
|
89
91
|
waitingForAuth = false;
|
|
90
92
|
joinRequested = false;
|
|
91
93
|
unsubAuthState = null;
|
|
94
|
+
browserNetworkListenersAttached = false;
|
|
95
|
+
browserOfflineHandler = () => {
|
|
96
|
+
if (this.intentionallyLeft || !this.joinRequested) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (this.connectionState === 'connected') {
|
|
100
|
+
this.setConnectionState('reconnecting');
|
|
101
|
+
}
|
|
102
|
+
if (isSocketOpenOrConnecting(this.ws)) {
|
|
103
|
+
try {
|
|
104
|
+
this.ws?.close();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Socket may already be closing.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
browserOnlineHandler = () => {
|
|
112
|
+
if (this.intentionallyLeft
|
|
113
|
+
|| !this.joinRequested
|
|
114
|
+
|| this.connectingPromise
|
|
115
|
+
|| isSocketOpenOrConnecting(this.ws)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (this.connectionState === 'reconnecting' || this.connectionState === 'disconnected') {
|
|
119
|
+
this.ensureConnection().catch(() => { });
|
|
120
|
+
}
|
|
121
|
+
};
|
|
92
122
|
// ─── Pending send() requests (requestId → { resolve, reject, timeout }) ───
|
|
93
123
|
pendingRequests = new Map();
|
|
94
124
|
pendingSignalRequests = new Map();
|
|
@@ -132,6 +162,21 @@ export class RoomClient {
|
|
|
132
162
|
};
|
|
133
163
|
members = {
|
|
134
164
|
list: () => cloneValue(this._members),
|
|
165
|
+
current: () => {
|
|
166
|
+
const connectionId = this.currentConnectionId;
|
|
167
|
+
if (connectionId) {
|
|
168
|
+
const byConnection = this._members.find((member) => member.connectionId === connectionId);
|
|
169
|
+
if (byConnection) {
|
|
170
|
+
return cloneValue(byConnection);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const userId = this.currentUserId;
|
|
174
|
+
if (!userId) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const member = this._members.find((entry) => entry.userId === userId) ?? null;
|
|
178
|
+
return member ? cloneValue(member) : null;
|
|
179
|
+
},
|
|
135
180
|
onSync: (handler) => this.onMembersSync(handler),
|
|
136
181
|
onJoin: (handler) => this.onMemberJoin(handler),
|
|
137
182
|
onLeave: (handler) => this.onMemberLeave(handler),
|
|
@@ -166,13 +211,20 @@ export class RoomClient {
|
|
|
166
211
|
devices: {
|
|
167
212
|
switch: (payload) => this.switchMediaDevices(payload),
|
|
168
213
|
},
|
|
169
|
-
|
|
170
|
-
createSession: (payload) => this.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
214
|
+
cloudflareRealtimeKit: {
|
|
215
|
+
createSession: (payload) => this.requestCloudflareRealtimeKitMedia('session', 'POST', payload),
|
|
216
|
+
},
|
|
217
|
+
transport: (options) => {
|
|
218
|
+
// Infer provider from options: if cloudflareRealtimeKit config is present, use it;
|
|
219
|
+
// otherwise default to p2p for zero-config local development.
|
|
220
|
+
const hasCloudflareConfig = options && 'cloudflareRealtimeKit' in options && options.cloudflareRealtimeKit != null;
|
|
221
|
+
const provider = options?.provider ?? (hasCloudflareConfig ? 'cloudflare_realtimekit' : 'p2p');
|
|
222
|
+
if (provider === 'p2p') {
|
|
223
|
+
const p2pOptions = options?.p2p;
|
|
224
|
+
return new RoomP2PMediaTransport(this, p2pOptions);
|
|
225
|
+
}
|
|
226
|
+
const cloudflareOptions = options?.cloudflareRealtimeKit;
|
|
227
|
+
return new RoomCloudflareMediaTransport(this, cloudflareOptions);
|
|
176
228
|
},
|
|
177
229
|
onTrack: (handler) => this.onMediaTrack(handler),
|
|
178
230
|
onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
|
|
@@ -195,10 +247,12 @@ export class RoomClient {
|
|
|
195
247
|
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
|
|
196
248
|
reconnectBaseDelay: options?.reconnectBaseDelay ?? 1000,
|
|
197
249
|
sendTimeout: options?.sendTimeout ?? 10000,
|
|
250
|
+
connectionTimeout: options?.connectionTimeout ?? 15000,
|
|
198
251
|
};
|
|
199
252
|
this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
|
|
200
253
|
this.handleAuthStateChange(user);
|
|
201
254
|
});
|
|
255
|
+
this.attachBrowserNetworkListeners();
|
|
202
256
|
}
|
|
203
257
|
// ─── State Accessors ───
|
|
204
258
|
/** Get current shared state (read-only snapshot) */
|
|
@@ -229,12 +283,15 @@ export class RoomClient {
|
|
|
229
283
|
}
|
|
230
284
|
return res.json();
|
|
231
285
|
}
|
|
232
|
-
async
|
|
286
|
+
async requestCloudflareRealtimeKitMedia(path, method, payload) {
|
|
287
|
+
return this.requestRoomMedia('cloudflare_realtimekit', path, method, payload);
|
|
288
|
+
}
|
|
289
|
+
async requestRoomMedia(providerPath, path, method, payload) {
|
|
233
290
|
const token = await this.tokenManager.getAccessToken((refreshToken) => refreshAccessToken(this.baseUrl, refreshToken));
|
|
234
291
|
if (!token) {
|
|
235
292
|
throw new EdgeBaseError(401, 'Authentication required');
|
|
236
293
|
}
|
|
237
|
-
const url = new URL(`${this.baseUrl.replace(/\/$/, '')}/api/room/media
|
|
294
|
+
const url = new URL(`${this.baseUrl.replace(/\/$/, '')}/api/room/media/${providerPath}/${path}`);
|
|
238
295
|
url.searchParams.set('namespace', this.namespace);
|
|
239
296
|
url.searchParams.set('id', this.roomId);
|
|
240
297
|
const response = await fetch(url.toString(), {
|
|
@@ -247,7 +304,7 @@ export class RoomClient {
|
|
|
247
304
|
});
|
|
248
305
|
const data = (await response.json().catch(() => ({})));
|
|
249
306
|
if (!response.ok) {
|
|
250
|
-
throw new EdgeBaseError(response.status, (typeof data.message === 'string' && data.message) || `
|
|
307
|
+
throw new EdgeBaseError(response.status, (typeof data.message === 'string' && data.message) || `Room media request failed: ${response.statusText}`);
|
|
251
308
|
}
|
|
252
309
|
return data;
|
|
253
310
|
}
|
|
@@ -269,15 +326,7 @@ export class RoomClient {
|
|
|
269
326
|
this.waitingForAuth = false;
|
|
270
327
|
this.stopHeartbeat();
|
|
271
328
|
// Reject all pending send() requests
|
|
272
|
-
|
|
273
|
-
clearTimeout(pending.timeout);
|
|
274
|
-
pending.reject(new EdgeBaseError(499, 'Room left'));
|
|
275
|
-
}
|
|
276
|
-
this.pendingRequests.clear();
|
|
277
|
-
this.rejectPendingVoidRequests(this.pendingSignalRequests, new EdgeBaseError(499, 'Room left'));
|
|
278
|
-
this.rejectPendingVoidRequests(this.pendingAdminRequests, new EdgeBaseError(499, 'Room left'));
|
|
279
|
-
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, new EdgeBaseError(499, 'Room left'));
|
|
280
|
-
this.rejectPendingVoidRequests(this.pendingMediaRequests, new EdgeBaseError(499, 'Room left'));
|
|
329
|
+
this.rejectAllPendingRequests(new EdgeBaseError(499, 'Room left'));
|
|
281
330
|
if (this.ws) {
|
|
282
331
|
const socket = this.ws;
|
|
283
332
|
this.sendRaw({ type: 'leave' });
|
|
@@ -299,6 +348,36 @@ export class RoomClient {
|
|
|
299
348
|
this.reconnectInfo = null;
|
|
300
349
|
this.setConnectionState('disconnected');
|
|
301
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Destroy the RoomClient and release all resources.
|
|
353
|
+
* Calls leave() if still connected, unsubscribes from auth state changes,
|
|
354
|
+
* and clears all handler arrays to allow garbage collection.
|
|
355
|
+
*/
|
|
356
|
+
destroy() {
|
|
357
|
+
this.leave();
|
|
358
|
+
this.detachBrowserNetworkListeners();
|
|
359
|
+
this.unsubAuthState?.();
|
|
360
|
+
this.unsubAuthState = null;
|
|
361
|
+
// Clear all handler arrays to break references
|
|
362
|
+
this.sharedStateHandlers.length = 0;
|
|
363
|
+
this.playerStateHandlers.length = 0;
|
|
364
|
+
this.messageHandlers.clear();
|
|
365
|
+
this.allMessageHandlers.length = 0;
|
|
366
|
+
this.errorHandlers.length = 0;
|
|
367
|
+
this.kickedHandlers.length = 0;
|
|
368
|
+
this.memberSyncHandlers.length = 0;
|
|
369
|
+
this.memberJoinHandlers.length = 0;
|
|
370
|
+
this.memberLeaveHandlers.length = 0;
|
|
371
|
+
this.memberStateHandlers.length = 0;
|
|
372
|
+
this.signalHandlers.clear();
|
|
373
|
+
this.anySignalHandlers.length = 0;
|
|
374
|
+
this.mediaTrackHandlers.length = 0;
|
|
375
|
+
this.mediaTrackRemovedHandlers.length = 0;
|
|
376
|
+
this.mediaStateHandlers.length = 0;
|
|
377
|
+
this.mediaDeviceHandlers.length = 0;
|
|
378
|
+
this.reconnectHandlers.length = 0;
|
|
379
|
+
this.connectionStateHandlers.length = 0;
|
|
380
|
+
}
|
|
302
381
|
// ─── Actions ───
|
|
303
382
|
/**
|
|
304
383
|
* Send an action to the server.
|
|
@@ -331,33 +410,29 @@ export class RoomClient {
|
|
|
331
410
|
* Subscribe to shared state changes.
|
|
332
411
|
* Called on full sync and on each shared_delta.
|
|
333
412
|
*
|
|
334
|
-
* @returns Subscription
|
|
413
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
335
414
|
*/
|
|
336
415
|
onSharedState(handler) {
|
|
337
416
|
this.sharedStateHandlers.push(handler);
|
|
338
|
-
return {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
},
|
|
344
|
-
};
|
|
417
|
+
return createSubscription(() => {
|
|
418
|
+
const idx = this.sharedStateHandlers.indexOf(handler);
|
|
419
|
+
if (idx >= 0)
|
|
420
|
+
this.sharedStateHandlers.splice(idx, 1);
|
|
421
|
+
});
|
|
345
422
|
}
|
|
346
423
|
/**
|
|
347
424
|
* Subscribe to player state changes.
|
|
348
425
|
* Called on full sync and on each player_delta.
|
|
349
426
|
*
|
|
350
|
-
* @returns Subscription
|
|
427
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
351
428
|
*/
|
|
352
429
|
onPlayerState(handler) {
|
|
353
430
|
this.playerStateHandlers.push(handler);
|
|
354
|
-
return {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
},
|
|
360
|
-
};
|
|
431
|
+
return createSubscription(() => {
|
|
432
|
+
const idx = this.playerStateHandlers.indexOf(handler);
|
|
433
|
+
if (idx >= 0)
|
|
434
|
+
this.playerStateHandlers.splice(idx, 1);
|
|
435
|
+
});
|
|
361
436
|
}
|
|
362
437
|
/**
|
|
363
438
|
* Subscribe to messages of a specific type sent by room.sendMessage().
|
|
@@ -365,186 +440,154 @@ export class RoomClient {
|
|
|
365
440
|
* @example
|
|
366
441
|
* room.onMessage('game_over', (data) => { console.log(data.winner); });
|
|
367
442
|
*
|
|
368
|
-
* @returns Subscription
|
|
443
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
369
444
|
*/
|
|
370
445
|
onMessage(messageType, handler) {
|
|
371
446
|
if (!this.messageHandlers.has(messageType)) {
|
|
372
447
|
this.messageHandlers.set(messageType, []);
|
|
373
448
|
}
|
|
374
449
|
this.messageHandlers.get(messageType).push(handler);
|
|
375
|
-
return {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
},
|
|
384
|
-
};
|
|
450
|
+
return createSubscription(() => {
|
|
451
|
+
const handlers = this.messageHandlers.get(messageType);
|
|
452
|
+
if (handlers) {
|
|
453
|
+
const idx = handlers.indexOf(handler);
|
|
454
|
+
if (idx >= 0)
|
|
455
|
+
handlers.splice(idx, 1);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
385
458
|
}
|
|
386
459
|
/**
|
|
387
460
|
* Subscribe to ALL messages regardless of type.
|
|
388
461
|
*
|
|
389
|
-
* @returns Subscription
|
|
462
|
+
* @returns Subscription (callable & .unsubscribe())
|
|
390
463
|
*/
|
|
391
464
|
onAnyMessage(handler) {
|
|
392
465
|
this.allMessageHandlers.push(handler);
|
|
393
|
-
return {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
},
|
|
399
|
-
};
|
|
466
|
+
return createSubscription(() => {
|
|
467
|
+
const idx = this.allMessageHandlers.indexOf(handler);
|
|
468
|
+
if (idx >= 0)
|
|
469
|
+
this.allMessageHandlers.splice(idx, 1);
|
|
470
|
+
});
|
|
400
471
|
}
|
|
401
472
|
/** Subscribe to errors */
|
|
402
473
|
onError(handler) {
|
|
403
474
|
this.errorHandlers.push(handler);
|
|
404
|
-
return {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
},
|
|
410
|
-
};
|
|
475
|
+
return createSubscription(() => {
|
|
476
|
+
const idx = this.errorHandlers.indexOf(handler);
|
|
477
|
+
if (idx >= 0)
|
|
478
|
+
this.errorHandlers.splice(idx, 1);
|
|
479
|
+
});
|
|
411
480
|
}
|
|
412
481
|
/** Subscribe to kick events */
|
|
413
482
|
onKicked(handler) {
|
|
414
483
|
this.kickedHandlers.push(handler);
|
|
415
|
-
return {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
},
|
|
421
|
-
};
|
|
484
|
+
return createSubscription(() => {
|
|
485
|
+
const idx = this.kickedHandlers.indexOf(handler);
|
|
486
|
+
if (idx >= 0)
|
|
487
|
+
this.kickedHandlers.splice(idx, 1);
|
|
488
|
+
});
|
|
422
489
|
}
|
|
423
490
|
onSignal(event, handler) {
|
|
424
491
|
if (!this.signalHandlers.has(event)) {
|
|
425
492
|
this.signalHandlers.set(event, []);
|
|
426
493
|
}
|
|
427
494
|
this.signalHandlers.get(event).push(handler);
|
|
428
|
-
return {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
},
|
|
437
|
-
};
|
|
495
|
+
return createSubscription(() => {
|
|
496
|
+
const handlers = this.signalHandlers.get(event);
|
|
497
|
+
if (!handlers)
|
|
498
|
+
return;
|
|
499
|
+
const index = handlers.indexOf(handler);
|
|
500
|
+
if (index >= 0)
|
|
501
|
+
handlers.splice(index, 1);
|
|
502
|
+
});
|
|
438
503
|
}
|
|
439
504
|
onAnySignal(handler) {
|
|
440
505
|
this.anySignalHandlers.push(handler);
|
|
441
|
-
return {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
},
|
|
447
|
-
};
|
|
506
|
+
return createSubscription(() => {
|
|
507
|
+
const index = this.anySignalHandlers.indexOf(handler);
|
|
508
|
+
if (index >= 0)
|
|
509
|
+
this.anySignalHandlers.splice(index, 1);
|
|
510
|
+
});
|
|
448
511
|
}
|
|
449
512
|
onMembersSync(handler) {
|
|
450
513
|
this.memberSyncHandlers.push(handler);
|
|
451
|
-
return {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
},
|
|
457
|
-
};
|
|
514
|
+
return createSubscription(() => {
|
|
515
|
+
const index = this.memberSyncHandlers.indexOf(handler);
|
|
516
|
+
if (index >= 0)
|
|
517
|
+
this.memberSyncHandlers.splice(index, 1);
|
|
518
|
+
});
|
|
458
519
|
}
|
|
459
520
|
onMemberJoin(handler) {
|
|
460
521
|
this.memberJoinHandlers.push(handler);
|
|
461
|
-
return {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
},
|
|
467
|
-
};
|
|
522
|
+
return createSubscription(() => {
|
|
523
|
+
const index = this.memberJoinHandlers.indexOf(handler);
|
|
524
|
+
if (index >= 0)
|
|
525
|
+
this.memberJoinHandlers.splice(index, 1);
|
|
526
|
+
});
|
|
468
527
|
}
|
|
469
528
|
onMemberLeave(handler) {
|
|
470
529
|
this.memberLeaveHandlers.push(handler);
|
|
471
|
-
return {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
},
|
|
477
|
-
};
|
|
530
|
+
return createSubscription(() => {
|
|
531
|
+
const index = this.memberLeaveHandlers.indexOf(handler);
|
|
532
|
+
if (index >= 0)
|
|
533
|
+
this.memberLeaveHandlers.splice(index, 1);
|
|
534
|
+
});
|
|
478
535
|
}
|
|
479
536
|
onMemberStateChange(handler) {
|
|
480
537
|
this.memberStateHandlers.push(handler);
|
|
481
|
-
return {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
},
|
|
487
|
-
};
|
|
538
|
+
return createSubscription(() => {
|
|
539
|
+
const index = this.memberStateHandlers.indexOf(handler);
|
|
540
|
+
if (index >= 0)
|
|
541
|
+
this.memberStateHandlers.splice(index, 1);
|
|
542
|
+
});
|
|
488
543
|
}
|
|
489
544
|
onReconnect(handler) {
|
|
490
545
|
this.reconnectHandlers.push(handler);
|
|
491
|
-
return {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
},
|
|
497
|
-
};
|
|
546
|
+
return createSubscription(() => {
|
|
547
|
+
const index = this.reconnectHandlers.indexOf(handler);
|
|
548
|
+
if (index >= 0)
|
|
549
|
+
this.reconnectHandlers.splice(index, 1);
|
|
550
|
+
});
|
|
498
551
|
}
|
|
499
552
|
onConnectionStateChange(handler) {
|
|
500
553
|
this.connectionStateHandlers.push(handler);
|
|
501
|
-
return {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
},
|
|
507
|
-
};
|
|
554
|
+
return createSubscription(() => {
|
|
555
|
+
const index = this.connectionStateHandlers.indexOf(handler);
|
|
556
|
+
if (index >= 0)
|
|
557
|
+
this.connectionStateHandlers.splice(index, 1);
|
|
558
|
+
});
|
|
508
559
|
}
|
|
509
560
|
onMediaTrack(handler) {
|
|
510
561
|
this.mediaTrackHandlers.push(handler);
|
|
511
|
-
return {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
},
|
|
517
|
-
};
|
|
562
|
+
return createSubscription(() => {
|
|
563
|
+
const index = this.mediaTrackHandlers.indexOf(handler);
|
|
564
|
+
if (index >= 0)
|
|
565
|
+
this.mediaTrackHandlers.splice(index, 1);
|
|
566
|
+
});
|
|
518
567
|
}
|
|
519
568
|
onMediaTrackRemoved(handler) {
|
|
520
569
|
this.mediaTrackRemovedHandlers.push(handler);
|
|
521
|
-
return {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
},
|
|
527
|
-
};
|
|
570
|
+
return createSubscription(() => {
|
|
571
|
+
const index = this.mediaTrackRemovedHandlers.indexOf(handler);
|
|
572
|
+
if (index >= 0)
|
|
573
|
+
this.mediaTrackRemovedHandlers.splice(index, 1);
|
|
574
|
+
});
|
|
528
575
|
}
|
|
529
576
|
onMediaStateChange(handler) {
|
|
530
577
|
this.mediaStateHandlers.push(handler);
|
|
531
|
-
return {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
},
|
|
537
|
-
};
|
|
578
|
+
return createSubscription(() => {
|
|
579
|
+
const index = this.mediaStateHandlers.indexOf(handler);
|
|
580
|
+
if (index >= 0)
|
|
581
|
+
this.mediaStateHandlers.splice(index, 1);
|
|
582
|
+
});
|
|
538
583
|
}
|
|
539
584
|
onMediaDeviceChange(handler) {
|
|
540
585
|
this.mediaDeviceHandlers.push(handler);
|
|
541
|
-
return {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
},
|
|
547
|
-
};
|
|
586
|
+
return createSubscription(() => {
|
|
587
|
+
const index = this.mediaDeviceHandlers.indexOf(handler);
|
|
588
|
+
if (index >= 0)
|
|
589
|
+
this.mediaDeviceHandlers.splice(index, 1);
|
|
590
|
+
});
|
|
548
591
|
}
|
|
549
592
|
async sendSignal(event, payload, options) {
|
|
550
593
|
if (!this.ws || !this.connected || !this.authenticated) {
|
|
@@ -651,29 +694,53 @@ export class RoomClient {
|
|
|
651
694
|
const wsUrl = this.buildWsUrl();
|
|
652
695
|
const ws = new WebSocket(wsUrl);
|
|
653
696
|
this.ws = ws;
|
|
697
|
+
let settled = false;
|
|
698
|
+
const connectionTimer = setTimeout(() => {
|
|
699
|
+
if (!settled) {
|
|
700
|
+
settled = true;
|
|
701
|
+
try {
|
|
702
|
+
ws.close();
|
|
703
|
+
}
|
|
704
|
+
catch (_) { /* ignore */ }
|
|
705
|
+
this.ws = null;
|
|
706
|
+
reject(new EdgeBaseError(408, `Room WebSocket connection timed out after ${this.options.connectionTimeout}ms. Is the server running?`));
|
|
707
|
+
}
|
|
708
|
+
}, this.options.connectionTimeout);
|
|
654
709
|
ws.onopen = () => {
|
|
710
|
+
clearTimeout(connectionTimer);
|
|
655
711
|
this.connected = true;
|
|
656
712
|
this.reconnectAttempts = 0;
|
|
657
713
|
this.startHeartbeat();
|
|
658
714
|
this.authenticate()
|
|
659
715
|
.then(() => {
|
|
660
|
-
|
|
661
|
-
|
|
716
|
+
if (!settled) {
|
|
717
|
+
settled = true;
|
|
718
|
+
this.waitingForAuth = false;
|
|
719
|
+
resolve();
|
|
720
|
+
}
|
|
662
721
|
})
|
|
663
722
|
.catch((error) => {
|
|
664
|
-
|
|
665
|
-
|
|
723
|
+
if (!settled) {
|
|
724
|
+
settled = true;
|
|
725
|
+
this.handleAuthenticationFailure(error);
|
|
726
|
+
reject(error);
|
|
727
|
+
}
|
|
666
728
|
});
|
|
667
729
|
};
|
|
668
730
|
ws.onmessage = (event) => {
|
|
669
731
|
this.handleMessage(event.data);
|
|
670
732
|
};
|
|
671
733
|
ws.onclose = (event) => {
|
|
734
|
+
clearTimeout(connectionTimer);
|
|
672
735
|
this.connected = false;
|
|
673
736
|
this.authenticated = false;
|
|
674
737
|
this.joined = false;
|
|
675
738
|
this.ws = null;
|
|
676
739
|
this.stopHeartbeat();
|
|
740
|
+
// Reject pending requests immediately so callers don't hang until timeout
|
|
741
|
+
if (!this.intentionallyLeft) {
|
|
742
|
+
this.rejectAllPendingRequests(new EdgeBaseError(499, 'WebSocket connection lost'));
|
|
743
|
+
}
|
|
677
744
|
if (event.code === 4004 && this.connectionState !== 'kicked') {
|
|
678
745
|
this.handleKicked();
|
|
679
746
|
}
|
|
@@ -688,7 +755,11 @@ export class RoomClient {
|
|
|
688
755
|
}
|
|
689
756
|
};
|
|
690
757
|
ws.onerror = () => {
|
|
691
|
-
|
|
758
|
+
clearTimeout(connectionTimer);
|
|
759
|
+
if (!settled) {
|
|
760
|
+
settled = true;
|
|
761
|
+
reject(new EdgeBaseError(500, 'Room WebSocket connection error'));
|
|
762
|
+
}
|
|
692
763
|
};
|
|
693
764
|
});
|
|
694
765
|
}
|
|
@@ -1008,7 +1079,7 @@ export class RoomClient {
|
|
|
1008
1079
|
this.upsertMember(member);
|
|
1009
1080
|
this.syncMediaMemberInfo(member);
|
|
1010
1081
|
const requestId = msg.requestId;
|
|
1011
|
-
if (requestId
|
|
1082
|
+
if (requestId) {
|
|
1012
1083
|
const pending = this.pendingMemberStateRequests.get(requestId);
|
|
1013
1084
|
if (pending) {
|
|
1014
1085
|
clearTimeout(pending.timeout);
|
|
@@ -1191,6 +1262,8 @@ export class RoomClient {
|
|
|
1191
1262
|
this.waitingForAuth = this.joinRequested;
|
|
1192
1263
|
this.reconnectInfo = null;
|
|
1193
1264
|
this.setConnectionState('auth_lost');
|
|
1265
|
+
// Reject pending requests — auth is gone, server won't respond
|
|
1266
|
+
this.rejectAllPendingRequests(new EdgeBaseError(401, 'Auth state lost'));
|
|
1194
1267
|
if (this.ws) {
|
|
1195
1268
|
const socket = this.ws;
|
|
1196
1269
|
this.sendRaw({ type: 'leave' });
|
|
@@ -1447,6 +1520,18 @@ export class RoomClient {
|
|
|
1447
1520
|
[kind]: next,
|
|
1448
1521
|
};
|
|
1449
1522
|
}
|
|
1523
|
+
/** Reject all 5 pending request maps at once. */
|
|
1524
|
+
rejectAllPendingRequests(error) {
|
|
1525
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1526
|
+
clearTimeout(pending.timeout);
|
|
1527
|
+
pending.reject(error);
|
|
1528
|
+
}
|
|
1529
|
+
this.pendingRequests.clear();
|
|
1530
|
+
this.rejectPendingVoidRequests(this.pendingSignalRequests, error);
|
|
1531
|
+
this.rejectPendingVoidRequests(this.pendingAdminRequests, error);
|
|
1532
|
+
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, error);
|
|
1533
|
+
this.rejectPendingVoidRequests(this.pendingMediaRequests, error);
|
|
1534
|
+
}
|
|
1450
1535
|
rejectPendingVoidRequests(pendingRequests, error) {
|
|
1451
1536
|
for (const [, pending] of pendingRequests) {
|
|
1452
1537
|
clearTimeout(pending.timeout);
|
|
@@ -1475,9 +1560,41 @@ export class RoomClient {
|
|
|
1475
1560
|
const wsUrl = httpUrl.replace(/^http/, 'ws');
|
|
1476
1561
|
return `${wsUrl}/api/room?namespace=${encodeURIComponent(this.namespace)}&id=${encodeURIComponent(this.roomId)}`;
|
|
1477
1562
|
}
|
|
1563
|
+
attachBrowserNetworkListeners() {
|
|
1564
|
+
if (this.browserNetworkListenersAttached) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const eventTarget = typeof globalThis !== 'undefined'
|
|
1568
|
+
&& typeof globalThis.addEventListener === 'function'
|
|
1569
|
+
? globalThis
|
|
1570
|
+
: null;
|
|
1571
|
+
if (!eventTarget) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
eventTarget.addEventListener('offline', this.browserOfflineHandler);
|
|
1575
|
+
eventTarget.addEventListener('online', this.browserOnlineHandler);
|
|
1576
|
+
this.browserNetworkListenersAttached = true;
|
|
1577
|
+
}
|
|
1578
|
+
detachBrowserNetworkListeners() {
|
|
1579
|
+
if (!this.browserNetworkListenersAttached) {
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const eventTarget = typeof globalThis !== 'undefined'
|
|
1583
|
+
&& typeof globalThis.removeEventListener === 'function'
|
|
1584
|
+
? globalThis
|
|
1585
|
+
: null;
|
|
1586
|
+
if (!eventTarget) {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
eventTarget.removeEventListener('offline', this.browserOfflineHandler);
|
|
1590
|
+
eventTarget.removeEventListener('online', this.browserOnlineHandler);
|
|
1591
|
+
this.browserNetworkListenersAttached = false;
|
|
1592
|
+
}
|
|
1478
1593
|
scheduleReconnect() {
|
|
1479
1594
|
const attempt = this.reconnectAttempts + 1;
|
|
1480
|
-
const
|
|
1595
|
+
const baseDelay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
|
|
1596
|
+
const jitter = Math.random() * baseDelay * 0.25;
|
|
1597
|
+
const delay = baseDelay + jitter;
|
|
1481
1598
|
this.reconnectAttempts++;
|
|
1482
1599
|
this.reconnectInfo = { attempt };
|
|
1483
1600
|
this.setConnectionState('reconnecting');
|