@edge-base/web 0.2.6 → 0.2.8
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 +3 -34
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/database-live.js +2 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/room-collab-core.d.ts +124 -0
- package/dist/room-collab-core.d.ts.map +1 -0
- package/dist/room-collab-core.js +675 -0
- package/dist/room-collab-core.js.map +1 -0
- package/dist/room-p2p-media.d.ts +60 -0
- package/dist/room-p2p-media.d.ts.map +1 -1
- package/dist/room-p2p-media.js +640 -49
- package/dist/room-p2p-media.js.map +1 -1
- package/dist/room.d.ts +28 -295
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +153 -463
- package/dist/room.js.map +1 -1
- package/llms.txt +0 -55
- package/package.json +2 -3
package/dist/room.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { EdgeBaseError, createSubscription, networkError, parseErrorResponse, } from '@edge-base/core';
|
|
2
2
|
import { refreshAccessToken } from './auth-refresh.js';
|
|
3
|
-
import { RoomCloudflareMediaTransport, } from './room-cloudflare-media.js';
|
|
4
|
-
import { RoomP2PMediaTransport, } from './room-p2p-media.js';
|
|
5
3
|
export { createSubscription };
|
|
6
4
|
// ─── Helpers ───
|
|
7
5
|
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
@@ -46,19 +44,21 @@ const WS_OPEN = 1;
|
|
|
46
44
|
const ROOM_EXPLICIT_LEAVE_CLOSE_CODE = 4005;
|
|
47
45
|
const ROOM_AUTH_STATE_LOST_CLOSE_CODE = 4006;
|
|
48
46
|
const ROOM_EXPLICIT_LEAVE_REASON = 'Client left room';
|
|
49
|
-
const
|
|
47
|
+
const ROOM_HEARTBEAT_INTERVAL_MS = 8000;
|
|
48
|
+
const ROOM_HEARTBEAT_STALE_TIMEOUT_MS = 20_000;
|
|
50
49
|
function isSocketOpenOrConnecting(socket) {
|
|
51
50
|
return !!socket && (socket.readyState === WS_OPEN || socket.readyState === WS_CONNECTING);
|
|
52
51
|
}
|
|
53
52
|
function closeSocketAfterLeave(socket, reason) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
try {
|
|
54
|
+
socket.close(ROOM_EXPLICIT_LEAVE_CLOSE_CODE, reason);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Socket already closed.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function getDefaultHeartbeatStaleTimeoutMs(heartbeatIntervalMs) {
|
|
61
|
+
return Math.max(Math.floor(heartbeatIntervalMs * 2.5), ROOM_HEARTBEAT_STALE_TIMEOUT_MS);
|
|
62
62
|
}
|
|
63
63
|
// ─── RoomClient v2 ───
|
|
64
64
|
export class RoomClient {
|
|
@@ -75,7 +75,7 @@ export class RoomClient {
|
|
|
75
75
|
_playerState = {};
|
|
76
76
|
_playerVersion = 0;
|
|
77
77
|
_members = [];
|
|
78
|
-
|
|
78
|
+
lastLocalMemberState = null;
|
|
79
79
|
// ─── Connection ───
|
|
80
80
|
ws = null;
|
|
81
81
|
reconnectAttempts = 0;
|
|
@@ -88,6 +88,8 @@ export class RoomClient {
|
|
|
88
88
|
reconnectInfo = null;
|
|
89
89
|
connectingPromise = null;
|
|
90
90
|
heartbeatTimer = null;
|
|
91
|
+
lastHeartbeatAckAt = Date.now();
|
|
92
|
+
disconnectResetTimer = null;
|
|
91
93
|
intentionallyLeft = false;
|
|
92
94
|
waitingForAuth = false;
|
|
93
95
|
joinRequested = false;
|
|
@@ -125,7 +127,6 @@ export class RoomClient {
|
|
|
125
127
|
pendingSignalRequests = new Map();
|
|
126
128
|
pendingAdminRequests = new Map();
|
|
127
129
|
pendingMemberStateRequests = new Map();
|
|
128
|
-
pendingMediaRequests = new Map();
|
|
129
130
|
// ─── Subscriptions ───
|
|
130
131
|
sharedStateHandlers = [];
|
|
131
132
|
playerStateHandlers = [];
|
|
@@ -134,16 +135,14 @@ export class RoomClient {
|
|
|
134
135
|
errorHandlers = [];
|
|
135
136
|
kickedHandlers = [];
|
|
136
137
|
memberSyncHandlers = [];
|
|
138
|
+
memberSnapshotHandlers = [];
|
|
137
139
|
memberJoinHandlers = [];
|
|
138
140
|
memberLeaveHandlers = [];
|
|
139
141
|
memberStateHandlers = [];
|
|
140
142
|
signalHandlers = new Map();
|
|
141
143
|
anySignalHandlers = [];
|
|
142
|
-
mediaTrackHandlers = [];
|
|
143
|
-
mediaTrackRemovedHandlers = [];
|
|
144
|
-
mediaStateHandlers = [];
|
|
145
|
-
mediaDeviceHandlers = [];
|
|
146
144
|
reconnectHandlers = [];
|
|
145
|
+
recoveryFailureHandlers = [];
|
|
147
146
|
connectionStateHandlers = [];
|
|
148
147
|
state = {
|
|
149
148
|
getShared: () => this.getSharedState(),
|
|
@@ -163,7 +162,7 @@ export class RoomClient {
|
|
|
163
162
|
onAny: (handler) => this.onAnySignal(handler),
|
|
164
163
|
};
|
|
165
164
|
members = {
|
|
166
|
-
list: () =>
|
|
165
|
+
list: () => this._members.map((member) => cloneValue(member)),
|
|
167
166
|
current: () => {
|
|
168
167
|
const connectionId = this.currentConnectionId;
|
|
169
168
|
if (connectionId) {
|
|
@@ -179,7 +178,9 @@ export class RoomClient {
|
|
|
179
178
|
const member = this._members.find((entry) => entry.userId === userId) ?? null;
|
|
180
179
|
return member ? cloneValue(member) : null;
|
|
181
180
|
},
|
|
181
|
+
awaitCurrent: (timeoutMs = 10_000) => this.waitForCurrentMember(timeoutMs),
|
|
182
182
|
onSync: (handler) => this.onMembersSync(handler),
|
|
183
|
+
onSnapshot: (handler) => this.onMemberSnapshot(handler),
|
|
183
184
|
onJoin: (handler) => this.onMemberJoin(handler),
|
|
184
185
|
onLeave: (handler) => this.onMemberLeave(handler),
|
|
185
186
|
setState: (state) => this.sendMemberState(state),
|
|
@@ -188,63 +189,16 @@ export class RoomClient {
|
|
|
188
189
|
};
|
|
189
190
|
admin = {
|
|
190
191
|
kick: (memberId) => this.sendAdmin('kick', memberId),
|
|
191
|
-
mute: (memberId) => this.sendAdmin('mute', memberId),
|
|
192
192
|
block: (memberId) => this.sendAdmin('block', memberId),
|
|
193
193
|
setRole: (memberId, role) => this.sendAdmin('setRole', memberId, { role }),
|
|
194
|
-
disableVideo: (memberId) => this.sendAdmin('disableVideo', memberId),
|
|
195
|
-
stopScreenShare: (memberId) => this.sendAdmin('stopScreenShare', memberId),
|
|
196
|
-
};
|
|
197
|
-
media = {
|
|
198
|
-
list: () => cloneValue(this._mediaMembers),
|
|
199
|
-
audio: {
|
|
200
|
-
enable: (payload) => this.sendMedia('publish', 'audio', payload),
|
|
201
|
-
disable: () => this.sendMedia('unpublish', 'audio'),
|
|
202
|
-
setMuted: (muted) => this.sendMedia('mute', 'audio', { muted }),
|
|
203
|
-
},
|
|
204
|
-
video: {
|
|
205
|
-
enable: (payload) => this.sendMedia('publish', 'video', payload),
|
|
206
|
-
disable: () => this.sendMedia('unpublish', 'video'),
|
|
207
|
-
setMuted: (muted) => this.sendMedia('mute', 'video', { muted }),
|
|
208
|
-
},
|
|
209
|
-
screen: {
|
|
210
|
-
start: (payload) => this.sendMedia('publish', 'screen', payload),
|
|
211
|
-
stop: () => this.sendMedia('unpublish', 'screen'),
|
|
212
|
-
},
|
|
213
|
-
devices: {
|
|
214
|
-
switch: (payload) => this.switchMediaDevices(payload),
|
|
215
|
-
},
|
|
216
|
-
realtime: {
|
|
217
|
-
iceServers: (payload) => this.requestRealtimeMedia('turn', 'POST', payload),
|
|
218
|
-
},
|
|
219
|
-
cloudflareRealtimeKit: {
|
|
220
|
-
createSession: (payload) => this.requestCloudflareRealtimeKitMedia('session', 'POST', payload),
|
|
221
|
-
},
|
|
222
|
-
checkReadiness: async (options) => {
|
|
223
|
-
const transport = this.media.transport(options);
|
|
224
|
-
return transport.getCapabilities();
|
|
225
|
-
},
|
|
226
|
-
transport: (options) => {
|
|
227
|
-
// Infer provider from options: if cloudflareRealtimeKit config is present, use it;
|
|
228
|
-
// otherwise default to p2p for zero-config local development.
|
|
229
|
-
const hasCloudflareConfig = options && 'cloudflareRealtimeKit' in options && options.cloudflareRealtimeKit != null;
|
|
230
|
-
const provider = options?.provider ?? (hasCloudflareConfig ? 'cloudflare_realtimekit' : 'p2p');
|
|
231
|
-
if (provider === 'p2p') {
|
|
232
|
-
const p2pOptions = options?.p2p;
|
|
233
|
-
return new RoomP2PMediaTransport(this, p2pOptions);
|
|
234
|
-
}
|
|
235
|
-
const cloudflareOptions = options?.cloudflareRealtimeKit;
|
|
236
|
-
return new RoomCloudflareMediaTransport(this, cloudflareOptions);
|
|
237
|
-
},
|
|
238
|
-
onTrack: (handler) => this.onMediaTrack(handler),
|
|
239
|
-
onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
|
|
240
|
-
onStateChange: (handler) => this.onMediaStateChange(handler),
|
|
241
|
-
onDeviceChange: (handler) => this.onMediaDeviceChange(handler),
|
|
242
194
|
};
|
|
243
195
|
session = {
|
|
244
196
|
onError: (handler) => this.onError(handler),
|
|
245
197
|
onKicked: (handler) => this.onKicked(handler),
|
|
246
198
|
onReconnect: (handler) => this.onReconnect(handler),
|
|
247
199
|
onConnectionStateChange: (handler) => this.onConnectionStateChange(handler),
|
|
200
|
+
onRecoveryFailure: (handler) => this.onRecoveryFailure(handler),
|
|
201
|
+
getDebugSnapshot: () => this.getDebugSnapshot(),
|
|
248
202
|
};
|
|
249
203
|
constructor(baseUrl, namespace, roomId, tokenManager, options) {
|
|
250
204
|
this.baseUrl = baseUrl;
|
|
@@ -257,6 +211,11 @@ export class RoomClient {
|
|
|
257
211
|
reconnectBaseDelay: options?.reconnectBaseDelay ?? 1000,
|
|
258
212
|
sendTimeout: options?.sendTimeout ?? 10000,
|
|
259
213
|
connectionTimeout: options?.connectionTimeout ?? 15000,
|
|
214
|
+
heartbeatIntervalMs: options?.heartbeatIntervalMs ?? ROOM_HEARTBEAT_INTERVAL_MS,
|
|
215
|
+
heartbeatStaleTimeoutMs: options?.heartbeatStaleTimeoutMs
|
|
216
|
+
?? getDefaultHeartbeatStaleTimeoutMs(options?.heartbeatIntervalMs ?? ROOM_HEARTBEAT_INTERVAL_MS),
|
|
217
|
+
networkRecoveryGraceMs: options?.networkRecoveryGraceMs ?? 3500,
|
|
218
|
+
disconnectResetTimeoutMs: options?.disconnectResetTimeoutMs ?? 8000,
|
|
260
219
|
};
|
|
261
220
|
this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
|
|
262
221
|
this.handleAuthStateChange(user);
|
|
@@ -272,6 +231,17 @@ export class RoomClient {
|
|
|
272
231
|
getPlayerState() {
|
|
273
232
|
return cloneRecord(this._playerState);
|
|
274
233
|
}
|
|
234
|
+
async waitForCurrentMember(timeoutMs = 10_000) {
|
|
235
|
+
const startedAt = Date.now();
|
|
236
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
237
|
+
const member = this.members.current();
|
|
238
|
+
if (member) {
|
|
239
|
+
return member;
|
|
240
|
+
}
|
|
241
|
+
await new Promise((resolve) => globalThis.setTimeout(resolve, 50));
|
|
242
|
+
}
|
|
243
|
+
return this.members.current();
|
|
244
|
+
}
|
|
275
245
|
// ─── Metadata (HTTP, no WebSocket needed) ───
|
|
276
246
|
/**
|
|
277
247
|
* Get room metadata without joining (HTTP GET).
|
|
@@ -366,42 +336,6 @@ export class RoomClient {
|
|
|
366
336
|
}
|
|
367
337
|
return res.json();
|
|
368
338
|
}
|
|
369
|
-
async requestCloudflareRealtimeKitMedia(path, method, payload) {
|
|
370
|
-
return this.requestRoomMedia('cloudflare_realtimekit', path, method, payload);
|
|
371
|
-
}
|
|
372
|
-
async requestRealtimeMedia(path, method, payload) {
|
|
373
|
-
return this.requestRoomMedia('realtime', path, method, payload);
|
|
374
|
-
}
|
|
375
|
-
async requestRoomMedia(providerPath, path, method, payload) {
|
|
376
|
-
const token = await this.tokenManager.getAccessToken((refreshToken) => refreshAccessToken(this.baseUrl, refreshToken));
|
|
377
|
-
if (!token) {
|
|
378
|
-
throw new EdgeBaseError(401, 'Authentication required before calling room media APIs. Sign in and join the room first.');
|
|
379
|
-
}
|
|
380
|
-
const url = new URL(`${this.baseUrl.replace(/\/$/, '')}/api/room/media/${providerPath}/${path}`);
|
|
381
|
-
url.searchParams.set('namespace', this.namespace);
|
|
382
|
-
url.searchParams.set('id', this.roomId);
|
|
383
|
-
let response;
|
|
384
|
-
try {
|
|
385
|
-
response = await fetch(url.toString(), {
|
|
386
|
-
method,
|
|
387
|
-
headers: {
|
|
388
|
-
Authorization: `Bearer ${token}`,
|
|
389
|
-
'Content-Type': 'application/json',
|
|
390
|
-
},
|
|
391
|
-
body: method === 'GET' ? undefined : JSON.stringify(payload ?? {}),
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
catch (error) {
|
|
395
|
-
throw networkError(`Room media request ${providerPath}/${path} could not reach ${this.baseUrl.replace(/\/$/, '')}. Make sure the EdgeBase server is running and reachable.`, { cause: error });
|
|
396
|
-
}
|
|
397
|
-
const data = (await response.json().catch(() => null));
|
|
398
|
-
if (!response.ok) {
|
|
399
|
-
const parsed = parseErrorResponse(response.status, data);
|
|
400
|
-
parsed.message = `Room media request ${providerPath}/${path} failed: ${parsed.message}`;
|
|
401
|
-
throw parsed;
|
|
402
|
-
}
|
|
403
|
-
return (data ?? {});
|
|
404
|
-
}
|
|
405
339
|
// ─── Connection Lifecycle ───
|
|
406
340
|
/** Connect to the room, authenticate, and join */
|
|
407
341
|
async join() {
|
|
@@ -419,6 +353,7 @@ export class RoomClient {
|
|
|
419
353
|
this.joinRequested = false;
|
|
420
354
|
this.waitingForAuth = false;
|
|
421
355
|
this.stopHeartbeat();
|
|
356
|
+
this.clearDisconnectResetTimer();
|
|
422
357
|
// Reject all pending send() requests
|
|
423
358
|
this.rejectAllPendingRequests(new EdgeBaseError(499, 'Room left'));
|
|
424
359
|
if (this.ws) {
|
|
@@ -436,7 +371,7 @@ export class RoomClient {
|
|
|
436
371
|
this._playerState = {};
|
|
437
372
|
this._playerVersion = 0;
|
|
438
373
|
this._members = [];
|
|
439
|
-
this.
|
|
374
|
+
this.lastLocalMemberState = null;
|
|
440
375
|
this.currentUserId = null;
|
|
441
376
|
this.currentConnectionId = null;
|
|
442
377
|
this.reconnectInfo = null;
|
|
@@ -470,10 +405,6 @@ export class RoomClient {
|
|
|
470
405
|
this.memberStateHandlers.length = 0;
|
|
471
406
|
this.signalHandlers.clear();
|
|
472
407
|
this.anySignalHandlers.length = 0;
|
|
473
|
-
this.mediaTrackHandlers.length = 0;
|
|
474
|
-
this.mediaTrackRemovedHandlers.length = 0;
|
|
475
|
-
this.mediaStateHandlers.length = 0;
|
|
476
|
-
this.mediaDeviceHandlers.length = 0;
|
|
477
408
|
this.reconnectHandlers.length = 0;
|
|
478
409
|
this.connectionStateHandlers.length = 0;
|
|
479
410
|
}
|
|
@@ -614,6 +545,14 @@ export class RoomClient {
|
|
|
614
545
|
this.memberSyncHandlers.splice(index, 1);
|
|
615
546
|
});
|
|
616
547
|
}
|
|
548
|
+
onMemberSnapshot(handler) {
|
|
549
|
+
this.memberSnapshotHandlers.push(handler);
|
|
550
|
+
return createSubscription(() => {
|
|
551
|
+
const index = this.memberSnapshotHandlers.indexOf(handler);
|
|
552
|
+
if (index >= 0)
|
|
553
|
+
this.memberSnapshotHandlers.splice(index, 1);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
617
556
|
onMemberJoin(handler) {
|
|
618
557
|
this.memberJoinHandlers.push(handler);
|
|
619
558
|
return createSubscription(() => {
|
|
@@ -646,44 +585,20 @@ export class RoomClient {
|
|
|
646
585
|
this.reconnectHandlers.splice(index, 1);
|
|
647
586
|
});
|
|
648
587
|
}
|
|
649
|
-
|
|
650
|
-
this.
|
|
651
|
-
return createSubscription(() => {
|
|
652
|
-
const index = this.connectionStateHandlers.indexOf(handler);
|
|
653
|
-
if (index >= 0)
|
|
654
|
-
this.connectionStateHandlers.splice(index, 1);
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
onMediaTrack(handler) {
|
|
658
|
-
this.mediaTrackHandlers.push(handler);
|
|
659
|
-
return createSubscription(() => {
|
|
660
|
-
const index = this.mediaTrackHandlers.indexOf(handler);
|
|
661
|
-
if (index >= 0)
|
|
662
|
-
this.mediaTrackHandlers.splice(index, 1);
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
onMediaTrackRemoved(handler) {
|
|
666
|
-
this.mediaTrackRemovedHandlers.push(handler);
|
|
667
|
-
return createSubscription(() => {
|
|
668
|
-
const index = this.mediaTrackRemovedHandlers.indexOf(handler);
|
|
669
|
-
if (index >= 0)
|
|
670
|
-
this.mediaTrackRemovedHandlers.splice(index, 1);
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
onMediaStateChange(handler) {
|
|
674
|
-
this.mediaStateHandlers.push(handler);
|
|
588
|
+
onRecoveryFailure(handler) {
|
|
589
|
+
this.recoveryFailureHandlers.push(handler);
|
|
675
590
|
return createSubscription(() => {
|
|
676
|
-
const index = this.
|
|
591
|
+
const index = this.recoveryFailureHandlers.indexOf(handler);
|
|
677
592
|
if (index >= 0)
|
|
678
|
-
this.
|
|
593
|
+
this.recoveryFailureHandlers.splice(index, 1);
|
|
679
594
|
});
|
|
680
595
|
}
|
|
681
|
-
|
|
682
|
-
this.
|
|
596
|
+
onConnectionStateChange(handler) {
|
|
597
|
+
this.connectionStateHandlers.push(handler);
|
|
683
598
|
return createSubscription(() => {
|
|
684
|
-
const index = this.
|
|
599
|
+
const index = this.connectionStateHandlers.indexOf(handler);
|
|
685
600
|
if (index >= 0)
|
|
686
|
-
this.
|
|
601
|
+
this.connectionStateHandlers.splice(index, 1);
|
|
687
602
|
});
|
|
688
603
|
}
|
|
689
604
|
async sendSignal(event, payload, options) {
|
|
@@ -706,17 +621,26 @@ export class RoomClient {
|
|
|
706
621
|
});
|
|
707
622
|
}
|
|
708
623
|
async sendMemberState(state) {
|
|
624
|
+
const nextState = {
|
|
625
|
+
...(this.lastLocalMemberState ?? {}),
|
|
626
|
+
...cloneRecord(state),
|
|
627
|
+
};
|
|
709
628
|
return this.sendMemberStateRequest({
|
|
710
629
|
type: 'member_state',
|
|
711
630
|
state,
|
|
631
|
+
}, () => {
|
|
632
|
+
this.lastLocalMemberState = nextState;
|
|
712
633
|
});
|
|
713
634
|
}
|
|
714
635
|
async clearMemberState() {
|
|
636
|
+
const clearedState = {};
|
|
715
637
|
return this.sendMemberStateRequest({
|
|
716
638
|
type: 'member_state_clear',
|
|
639
|
+
}, () => {
|
|
640
|
+
this.lastLocalMemberState = clearedState;
|
|
717
641
|
});
|
|
718
642
|
}
|
|
719
|
-
async sendMemberStateRequest(payload) {
|
|
643
|
+
async sendMemberStateRequest(payload, onSuccess) {
|
|
720
644
|
this.assertConnected('updating member state');
|
|
721
645
|
const requestId = generateRequestId();
|
|
722
646
|
return new Promise((resolve, reject) => {
|
|
@@ -724,7 +648,7 @@ export class RoomClient {
|
|
|
724
648
|
this.pendingMemberStateRequests.delete(requestId);
|
|
725
649
|
reject(new EdgeBaseError(408, 'Member state update timed out'));
|
|
726
650
|
}, this.options.sendTimeout);
|
|
727
|
-
this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout });
|
|
651
|
+
this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout, onSuccess });
|
|
728
652
|
this.sendRaw({ ...payload, requestId });
|
|
729
653
|
});
|
|
730
654
|
}
|
|
@@ -746,37 +670,6 @@ export class RoomClient {
|
|
|
746
670
|
});
|
|
747
671
|
});
|
|
748
672
|
}
|
|
749
|
-
async sendMedia(operation, kind, payload) {
|
|
750
|
-
this.assertConnected(`running media operation '${operation}' for '${kind}'`);
|
|
751
|
-
const requestId = generateRequestId();
|
|
752
|
-
return new Promise((resolve, reject) => {
|
|
753
|
-
const timeout = setTimeout(() => {
|
|
754
|
-
this.pendingMediaRequests.delete(requestId);
|
|
755
|
-
reject(new EdgeBaseError(408, `Media operation '${operation}' timed out`));
|
|
756
|
-
}, this.options.sendTimeout);
|
|
757
|
-
this.pendingMediaRequests.set(requestId, { resolve, reject, timeout });
|
|
758
|
-
this.sendRaw({
|
|
759
|
-
type: 'media',
|
|
760
|
-
operation,
|
|
761
|
-
kind,
|
|
762
|
-
payload: payload ?? {},
|
|
763
|
-
requestId,
|
|
764
|
-
});
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
async switchMediaDevices(payload) {
|
|
768
|
-
const operations = [];
|
|
769
|
-
if (payload.audioInputId) {
|
|
770
|
-
operations.push(this.sendMedia('device', 'audio', { deviceId: payload.audioInputId }));
|
|
771
|
-
}
|
|
772
|
-
if (payload.videoInputId) {
|
|
773
|
-
operations.push(this.sendMedia('device', 'video', { deviceId: payload.videoInputId }));
|
|
774
|
-
}
|
|
775
|
-
if (payload.screenInputId) {
|
|
776
|
-
operations.push(this.sendMedia('device', 'screen', { deviceId: payload.screenInputId }));
|
|
777
|
-
}
|
|
778
|
-
await Promise.all(operations);
|
|
779
|
-
}
|
|
780
673
|
// ─── Private: Connection ───
|
|
781
674
|
async establishConnection() {
|
|
782
675
|
return new Promise((resolve, reject) => {
|
|
@@ -900,6 +793,7 @@ export class RoomClient {
|
|
|
900
793
|
lastSharedVersion: this._sharedVersion,
|
|
901
794
|
lastPlayerState: this._playerState,
|
|
902
795
|
lastPlayerVersion: this._playerVersion,
|
|
796
|
+
lastMemberState: this.getReconnectMemberState(),
|
|
903
797
|
});
|
|
904
798
|
this.joined = true;
|
|
905
799
|
resolve();
|
|
@@ -958,9 +852,6 @@ export class RoomClient {
|
|
|
958
852
|
case 'members_sync':
|
|
959
853
|
this.handleMembersSync(msg);
|
|
960
854
|
break;
|
|
961
|
-
case 'media_sync':
|
|
962
|
-
this.handleMediaSync(msg);
|
|
963
|
-
break;
|
|
964
855
|
case 'member_join':
|
|
965
856
|
this.handleMemberJoinFrame(msg);
|
|
966
857
|
break;
|
|
@@ -973,24 +864,6 @@ export class RoomClient {
|
|
|
973
864
|
case 'member_state_error':
|
|
974
865
|
this.handleMemberStateError(msg);
|
|
975
866
|
break;
|
|
976
|
-
case 'media_track':
|
|
977
|
-
this.handleMediaTrackFrame(msg);
|
|
978
|
-
break;
|
|
979
|
-
case 'media_track_removed':
|
|
980
|
-
this.handleMediaTrackRemovedFrame(msg);
|
|
981
|
-
break;
|
|
982
|
-
case 'media_state':
|
|
983
|
-
this.handleMediaStateFrame(msg);
|
|
984
|
-
break;
|
|
985
|
-
case 'media_device':
|
|
986
|
-
this.handleMediaDeviceFrame(msg);
|
|
987
|
-
break;
|
|
988
|
-
case 'media_result':
|
|
989
|
-
this.handleMediaResult(msg);
|
|
990
|
-
break;
|
|
991
|
-
case 'media_error':
|
|
992
|
-
this.handleMediaError(msg);
|
|
993
|
-
break;
|
|
994
867
|
case 'admin_result':
|
|
995
868
|
this.handleAdminResult(msg);
|
|
996
869
|
break;
|
|
@@ -1005,6 +878,7 @@ export class RoomClient {
|
|
|
1005
878
|
break;
|
|
1006
879
|
case 'pong':
|
|
1007
880
|
// Heartbeat response — no action needed
|
|
881
|
+
this.lastHeartbeatAckAt = Date.now();
|
|
1008
882
|
break;
|
|
1009
883
|
}
|
|
1010
884
|
}
|
|
@@ -1134,23 +1008,19 @@ export class RoomClient {
|
|
|
1134
1008
|
handleMembersSync(msg) {
|
|
1135
1009
|
const members = this.normalizeMembers(msg.members);
|
|
1136
1010
|
this._members = members;
|
|
1137
|
-
|
|
1138
|
-
this.syncMediaMemberInfo(member);
|
|
1139
|
-
}
|
|
1140
|
-
const snapshot = cloneValue(this._members);
|
|
1011
|
+
const snapshot = this._members.map((member) => cloneValue(member));
|
|
1141
1012
|
for (const handler of this.memberSyncHandlers) {
|
|
1142
1013
|
handler(snapshot);
|
|
1143
1014
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1015
|
+
for (const handler of this.memberSnapshotHandlers) {
|
|
1016
|
+
handler(snapshot);
|
|
1017
|
+
}
|
|
1147
1018
|
}
|
|
1148
1019
|
handleMemberJoinFrame(msg) {
|
|
1149
1020
|
const member = this.normalizeMember(msg.member);
|
|
1150
1021
|
if (!member)
|
|
1151
1022
|
return;
|
|
1152
1023
|
this.upsertMember(member);
|
|
1153
|
-
this.syncMediaMemberInfo(member);
|
|
1154
1024
|
const snapshot = cloneValue(member);
|
|
1155
1025
|
for (const handler of this.memberJoinHandlers) {
|
|
1156
1026
|
handler(snapshot);
|
|
@@ -1161,7 +1031,6 @@ export class RoomClient {
|
|
|
1161
1031
|
if (!member)
|
|
1162
1032
|
return;
|
|
1163
1033
|
this.removeMember(member.memberId);
|
|
1164
|
-
this.removeMediaMember(member.memberId);
|
|
1165
1034
|
const reason = this.normalizeLeaveReason(msg.reason);
|
|
1166
1035
|
const snapshot = cloneValue(member);
|
|
1167
1036
|
for (const handler of this.memberLeaveHandlers) {
|
|
@@ -1175,13 +1044,13 @@ export class RoomClient {
|
|
|
1175
1044
|
return;
|
|
1176
1045
|
member.state = state;
|
|
1177
1046
|
this.upsertMember(member);
|
|
1178
|
-
this.syncMediaMemberInfo(member);
|
|
1179
1047
|
const requestId = msg.requestId;
|
|
1180
1048
|
if (requestId) {
|
|
1181
1049
|
const pending = this.pendingMemberStateRequests.get(requestId);
|
|
1182
1050
|
if (pending) {
|
|
1183
1051
|
clearTimeout(pending.timeout);
|
|
1184
1052
|
this.pendingMemberStateRequests.delete(requestId);
|
|
1053
|
+
pending.onSuccess?.();
|
|
1185
1054
|
pending.resolve();
|
|
1186
1055
|
}
|
|
1187
1056
|
}
|
|
@@ -1202,115 +1071,6 @@ export class RoomClient {
|
|
|
1202
1071
|
this.pendingMemberStateRequests.delete(requestId);
|
|
1203
1072
|
pending.reject(new EdgeBaseError(400, msg.message || 'Member state update failed'));
|
|
1204
1073
|
}
|
|
1205
|
-
handleMediaTrackFrame(msg) {
|
|
1206
|
-
const member = this.normalizeMember(msg.member);
|
|
1207
|
-
const track = this.normalizeMediaTrack(msg.track);
|
|
1208
|
-
if (!member || !track)
|
|
1209
|
-
return;
|
|
1210
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
1211
|
-
this.upsertMediaTrack(mediaMember, track);
|
|
1212
|
-
this.mergeMediaState(mediaMember, track.kind, {
|
|
1213
|
-
published: true,
|
|
1214
|
-
muted: track.muted,
|
|
1215
|
-
trackId: track.trackId,
|
|
1216
|
-
deviceId: track.deviceId,
|
|
1217
|
-
publishedAt: track.publishedAt,
|
|
1218
|
-
adminDisabled: track.adminDisabled,
|
|
1219
|
-
providerSessionId: track.providerSessionId,
|
|
1220
|
-
});
|
|
1221
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1222
|
-
const trackSnapshot = cloneValue(track);
|
|
1223
|
-
for (const handler of this.mediaTrackHandlers) {
|
|
1224
|
-
handler(trackSnapshot, memberSnapshot);
|
|
1225
|
-
}
|
|
1226
|
-
const stateSnapshot = cloneValue(mediaMember.state);
|
|
1227
|
-
for (const handler of this.mediaStateHandlers) {
|
|
1228
|
-
handler(memberSnapshot, stateSnapshot);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
handleMediaTrackRemovedFrame(msg) {
|
|
1232
|
-
const member = this.normalizeMember(msg.member);
|
|
1233
|
-
const track = this.normalizeMediaTrack(msg.track);
|
|
1234
|
-
if (!member || !track)
|
|
1235
|
-
return;
|
|
1236
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
1237
|
-
this.removeMediaTrack(mediaMember, track);
|
|
1238
|
-
mediaMember.state = {
|
|
1239
|
-
...mediaMember.state,
|
|
1240
|
-
[track.kind]: {
|
|
1241
|
-
published: false,
|
|
1242
|
-
muted: false,
|
|
1243
|
-
adminDisabled: false,
|
|
1244
|
-
providerSessionId: undefined,
|
|
1245
|
-
},
|
|
1246
|
-
};
|
|
1247
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1248
|
-
const trackSnapshot = cloneValue(track);
|
|
1249
|
-
for (const handler of this.mediaTrackRemovedHandlers) {
|
|
1250
|
-
handler(trackSnapshot, memberSnapshot);
|
|
1251
|
-
}
|
|
1252
|
-
const stateSnapshot = cloneValue(mediaMember.state);
|
|
1253
|
-
for (const handler of this.mediaStateHandlers) {
|
|
1254
|
-
handler(memberSnapshot, stateSnapshot);
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
handleMediaStateFrame(msg) {
|
|
1258
|
-
const member = this.normalizeMember(msg.member);
|
|
1259
|
-
if (!member)
|
|
1260
|
-
return;
|
|
1261
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
1262
|
-
mediaMember.state = this.normalizeMediaState(msg.state);
|
|
1263
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1264
|
-
const stateSnapshot = cloneValue(mediaMember.state);
|
|
1265
|
-
for (const handler of this.mediaStateHandlers) {
|
|
1266
|
-
handler(memberSnapshot, stateSnapshot);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
handleMediaDeviceFrame(msg) {
|
|
1270
|
-
const member = this.normalizeMember(msg.member);
|
|
1271
|
-
const kind = this.normalizeMediaKind(msg.kind);
|
|
1272
|
-
const deviceId = typeof msg.deviceId === 'string' ? msg.deviceId : '';
|
|
1273
|
-
if (!member || !kind || !deviceId)
|
|
1274
|
-
return;
|
|
1275
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
1276
|
-
this.mergeMediaState(mediaMember, kind, { deviceId });
|
|
1277
|
-
for (const track of mediaMember.tracks) {
|
|
1278
|
-
if (track.kind === kind) {
|
|
1279
|
-
track.deviceId = deviceId;
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1283
|
-
const change = { kind, deviceId };
|
|
1284
|
-
for (const handler of this.mediaDeviceHandlers) {
|
|
1285
|
-
handler(memberSnapshot, change);
|
|
1286
|
-
}
|
|
1287
|
-
const stateSnapshot = cloneValue(mediaMember.state);
|
|
1288
|
-
for (const handler of this.mediaStateHandlers) {
|
|
1289
|
-
handler(memberSnapshot, stateSnapshot);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
handleMediaResult(msg) {
|
|
1293
|
-
const requestId = msg.requestId;
|
|
1294
|
-
if (!requestId)
|
|
1295
|
-
return;
|
|
1296
|
-
const pending = this.pendingMediaRequests.get(requestId);
|
|
1297
|
-
if (!pending)
|
|
1298
|
-
return;
|
|
1299
|
-
clearTimeout(pending.timeout);
|
|
1300
|
-
this.pendingMediaRequests.delete(requestId);
|
|
1301
|
-
pending.resolve();
|
|
1302
|
-
}
|
|
1303
|
-
handleMediaError(msg) {
|
|
1304
|
-
const requestId = msg.requestId;
|
|
1305
|
-
if (!requestId)
|
|
1306
|
-
return;
|
|
1307
|
-
const pending = this.pendingMediaRequests.get(requestId);
|
|
1308
|
-
if (!pending)
|
|
1309
|
-
return;
|
|
1310
|
-
clearTimeout(pending.timeout);
|
|
1311
|
-
this.pendingMediaRequests.delete(requestId);
|
|
1312
|
-
pending.reject(new EdgeBaseError(400, msg.message || 'Media operation failed'));
|
|
1313
|
-
}
|
|
1314
1074
|
handleAdminResult(msg) {
|
|
1315
1075
|
const requestId = msg.requestId;
|
|
1316
1076
|
if (!requestId)
|
|
@@ -1387,7 +1147,6 @@ export class RoomClient {
|
|
|
1387
1147
|
this.connected = false;
|
|
1388
1148
|
this.authenticated = false;
|
|
1389
1149
|
this.joined = false;
|
|
1390
|
-
this._mediaMembers = [];
|
|
1391
1150
|
this.currentUserId = null;
|
|
1392
1151
|
this.currentConnectionId = null;
|
|
1393
1152
|
try {
|
|
@@ -1401,7 +1160,6 @@ export class RoomClient {
|
|
|
1401
1160
|
this.connected = false;
|
|
1402
1161
|
this.authenticated = false;
|
|
1403
1162
|
this.joined = false;
|
|
1404
|
-
this._mediaMembers = [];
|
|
1405
1163
|
}
|
|
1406
1164
|
handleAuthenticationFailure(error) {
|
|
1407
1165
|
const authError = error instanceof EdgeBaseError
|
|
@@ -1437,8 +1195,7 @@ export class RoomClient {
|
|
|
1437
1195
|
return this.pendingRequests.size > 0
|
|
1438
1196
|
|| this.pendingSignalRequests.size > 0
|
|
1439
1197
|
|| this.pendingAdminRequests.size > 0
|
|
1440
|
-
|| this.pendingMemberStateRequests.size > 0
|
|
1441
|
-
|| this.pendingMediaRequests.size > 0;
|
|
1198
|
+
|| this.pendingMemberStateRequests.size > 0;
|
|
1442
1199
|
}
|
|
1443
1200
|
handleRoomAuthStateLoss(message) {
|
|
1444
1201
|
const detail = message?.trim();
|
|
@@ -1464,14 +1221,6 @@ export class RoomClient {
|
|
|
1464
1221
|
.map((member) => this.normalizeMember(member))
|
|
1465
1222
|
.filter((member) => !!member);
|
|
1466
1223
|
}
|
|
1467
|
-
normalizeMediaMembers(value) {
|
|
1468
|
-
if (!Array.isArray(value)) {
|
|
1469
|
-
return [];
|
|
1470
|
-
}
|
|
1471
|
-
return value
|
|
1472
|
-
.map((member) => this.normalizeMediaMember(member))
|
|
1473
|
-
.filter((member) => !!member);
|
|
1474
|
-
}
|
|
1475
1224
|
normalizeMember(value) {
|
|
1476
1225
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1477
1226
|
return null;
|
|
@@ -1495,84 +1244,6 @@ export class RoomClient {
|
|
|
1495
1244
|
}
|
|
1496
1245
|
return cloneRecord(value);
|
|
1497
1246
|
}
|
|
1498
|
-
normalizeMediaMember(value) {
|
|
1499
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1500
|
-
return null;
|
|
1501
|
-
}
|
|
1502
|
-
const entry = value;
|
|
1503
|
-
const member = this.normalizeMember(entry.member);
|
|
1504
|
-
if (!member) {
|
|
1505
|
-
return null;
|
|
1506
|
-
}
|
|
1507
|
-
return {
|
|
1508
|
-
member,
|
|
1509
|
-
state: this.normalizeMediaState(entry.state),
|
|
1510
|
-
tracks: this.normalizeMediaTracks(entry.tracks),
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
normalizeMediaState(value) {
|
|
1514
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1515
|
-
return {};
|
|
1516
|
-
}
|
|
1517
|
-
const state = value;
|
|
1518
|
-
return {
|
|
1519
|
-
audio: this.normalizeMediaKindState(state.audio),
|
|
1520
|
-
video: this.normalizeMediaKindState(state.video),
|
|
1521
|
-
screen: this.normalizeMediaKindState(state.screen),
|
|
1522
|
-
};
|
|
1523
|
-
}
|
|
1524
|
-
normalizeMediaKindState(value) {
|
|
1525
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1526
|
-
return undefined;
|
|
1527
|
-
}
|
|
1528
|
-
const state = value;
|
|
1529
|
-
return {
|
|
1530
|
-
published: state.published === true,
|
|
1531
|
-
muted: state.muted === true,
|
|
1532
|
-
trackId: typeof state.trackId === 'string' ? state.trackId : undefined,
|
|
1533
|
-
deviceId: typeof state.deviceId === 'string' ? state.deviceId : undefined,
|
|
1534
|
-
publishedAt: typeof state.publishedAt === 'number' ? state.publishedAt : undefined,
|
|
1535
|
-
adminDisabled: state.adminDisabled === true,
|
|
1536
|
-
providerSessionId: typeof state.providerSessionId === 'string' ? state.providerSessionId : undefined,
|
|
1537
|
-
};
|
|
1538
|
-
}
|
|
1539
|
-
normalizeMediaTracks(value) {
|
|
1540
|
-
if (!Array.isArray(value)) {
|
|
1541
|
-
return [];
|
|
1542
|
-
}
|
|
1543
|
-
return value
|
|
1544
|
-
.map((track) => this.normalizeMediaTrack(track))
|
|
1545
|
-
.filter((track) => !!track);
|
|
1546
|
-
}
|
|
1547
|
-
normalizeMediaTrack(value) {
|
|
1548
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1549
|
-
return null;
|
|
1550
|
-
}
|
|
1551
|
-
const track = value;
|
|
1552
|
-
const kind = this.normalizeMediaKind(track.kind);
|
|
1553
|
-
if (!kind) {
|
|
1554
|
-
return null;
|
|
1555
|
-
}
|
|
1556
|
-
return {
|
|
1557
|
-
kind,
|
|
1558
|
-
trackId: typeof track.trackId === 'string' ? track.trackId : undefined,
|
|
1559
|
-
deviceId: typeof track.deviceId === 'string' ? track.deviceId : undefined,
|
|
1560
|
-
muted: track.muted === true,
|
|
1561
|
-
publishedAt: typeof track.publishedAt === 'number' ? track.publishedAt : undefined,
|
|
1562
|
-
adminDisabled: track.adminDisabled === true,
|
|
1563
|
-
providerSessionId: typeof track.providerSessionId === 'string' ? track.providerSessionId : undefined,
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
normalizeMediaKind(value) {
|
|
1567
|
-
switch (value) {
|
|
1568
|
-
case 'audio':
|
|
1569
|
-
case 'video':
|
|
1570
|
-
case 'screen':
|
|
1571
|
-
return value;
|
|
1572
|
-
default:
|
|
1573
|
-
return null;
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
1247
|
normalizeSignalMeta(value) {
|
|
1577
1248
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1578
1249
|
return {};
|
|
@@ -1598,6 +1269,20 @@ export class RoomClient {
|
|
|
1598
1269
|
return 'leave';
|
|
1599
1270
|
}
|
|
1600
1271
|
}
|
|
1272
|
+
getDebugSnapshot() {
|
|
1273
|
+
return {
|
|
1274
|
+
connectionState: this.connectionState,
|
|
1275
|
+
connected: this.connected,
|
|
1276
|
+
authenticated: this.authenticated,
|
|
1277
|
+
joined: this.joined,
|
|
1278
|
+
currentUserId: this.currentUserId,
|
|
1279
|
+
currentConnectionId: this.currentConnectionId,
|
|
1280
|
+
membersCount: this._members.length,
|
|
1281
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
1282
|
+
joinRequested: this.joinRequested,
|
|
1283
|
+
waitingForAuth: this.waitingForAuth,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1601
1286
|
upsertMember(member) {
|
|
1602
1287
|
const index = this._members.findIndex((entry) => entry.memberId === member.memberId);
|
|
1603
1288
|
if (index >= 0) {
|
|
@@ -1609,64 +1294,6 @@ export class RoomClient {
|
|
|
1609
1294
|
removeMember(memberId) {
|
|
1610
1295
|
this._members = this._members.filter((member) => member.memberId !== memberId);
|
|
1611
1296
|
}
|
|
1612
|
-
syncMediaMemberInfo(member) {
|
|
1613
|
-
const mediaMember = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
1614
|
-
if (!mediaMember) {
|
|
1615
|
-
return;
|
|
1616
|
-
}
|
|
1617
|
-
mediaMember.member = cloneValue(member);
|
|
1618
|
-
}
|
|
1619
|
-
ensureMediaMember(member) {
|
|
1620
|
-
const existing = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
1621
|
-
if (existing) {
|
|
1622
|
-
existing.member = cloneValue(member);
|
|
1623
|
-
return existing;
|
|
1624
|
-
}
|
|
1625
|
-
const created = {
|
|
1626
|
-
member: cloneValue(member),
|
|
1627
|
-
state: {},
|
|
1628
|
-
tracks: [],
|
|
1629
|
-
};
|
|
1630
|
-
this._mediaMembers.push(created);
|
|
1631
|
-
return created;
|
|
1632
|
-
}
|
|
1633
|
-
removeMediaMember(memberId) {
|
|
1634
|
-
this._mediaMembers = this._mediaMembers.filter((member) => member.member.memberId !== memberId);
|
|
1635
|
-
}
|
|
1636
|
-
upsertMediaTrack(mediaMember, track) {
|
|
1637
|
-
const index = mediaMember.tracks.findIndex((entry) => entry.kind === track.kind &&
|
|
1638
|
-
entry.trackId === track.trackId);
|
|
1639
|
-
if (index >= 0) {
|
|
1640
|
-
mediaMember.tracks[index] = cloneValue(track);
|
|
1641
|
-
return;
|
|
1642
|
-
}
|
|
1643
|
-
mediaMember.tracks = mediaMember.tracks
|
|
1644
|
-
.filter((entry) => !(entry.kind === track.kind && !track.trackId))
|
|
1645
|
-
.concat(cloneValue(track));
|
|
1646
|
-
}
|
|
1647
|
-
removeMediaTrack(mediaMember, track) {
|
|
1648
|
-
mediaMember.tracks = mediaMember.tracks.filter((entry) => {
|
|
1649
|
-
if (track.trackId) {
|
|
1650
|
-
return !(entry.kind === track.kind && entry.trackId === track.trackId);
|
|
1651
|
-
}
|
|
1652
|
-
return entry.kind !== track.kind;
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
mergeMediaState(mediaMember, kind, partial) {
|
|
1656
|
-
const next = {
|
|
1657
|
-
published: partial.published ?? mediaMember.state[kind]?.published ?? false,
|
|
1658
|
-
muted: partial.muted ?? mediaMember.state[kind]?.muted ?? false,
|
|
1659
|
-
trackId: partial.trackId ?? mediaMember.state[kind]?.trackId,
|
|
1660
|
-
deviceId: partial.deviceId ?? mediaMember.state[kind]?.deviceId,
|
|
1661
|
-
publishedAt: partial.publishedAt ?? mediaMember.state[kind]?.publishedAt,
|
|
1662
|
-
adminDisabled: partial.adminDisabled ?? mediaMember.state[kind]?.adminDisabled,
|
|
1663
|
-
providerSessionId: partial.providerSessionId ?? mediaMember.state[kind]?.providerSessionId,
|
|
1664
|
-
};
|
|
1665
|
-
mediaMember.state = {
|
|
1666
|
-
...mediaMember.state,
|
|
1667
|
-
[kind]: next,
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
1297
|
/** Reject all 5 pending request maps at once. */
|
|
1671
1298
|
rejectAllPendingRequests(error) {
|
|
1672
1299
|
for (const [, pending] of this.pendingRequests) {
|
|
@@ -1677,7 +1304,6 @@ export class RoomClient {
|
|
|
1677
1304
|
this.rejectPendingVoidRequests(this.pendingSignalRequests, error);
|
|
1678
1305
|
this.rejectPendingVoidRequests(this.pendingAdminRequests, error);
|
|
1679
1306
|
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, error);
|
|
1680
|
-
this.rejectPendingVoidRequests(this.pendingMediaRequests, error);
|
|
1681
1307
|
}
|
|
1682
1308
|
rejectPendingVoidRequests(pendingRequests, error) {
|
|
1683
1309
|
for (const [, pending] of pendingRequests) {
|
|
@@ -1686,11 +1312,59 @@ export class RoomClient {
|
|
|
1686
1312
|
}
|
|
1687
1313
|
pendingRequests.clear();
|
|
1688
1314
|
}
|
|
1315
|
+
shouldScheduleDisconnectReset(next) {
|
|
1316
|
+
if (this.intentionallyLeft || !this.joinRequested) {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
return next === 'disconnected';
|
|
1320
|
+
}
|
|
1321
|
+
clearDisconnectResetTimer() {
|
|
1322
|
+
if (this.disconnectResetTimer) {
|
|
1323
|
+
clearTimeout(this.disconnectResetTimer);
|
|
1324
|
+
this.disconnectResetTimer = null;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
scheduleDisconnectReset(stateAtSchedule) {
|
|
1328
|
+
this.clearDisconnectResetTimer();
|
|
1329
|
+
const timeoutMs = this.options.disconnectResetTimeoutMs;
|
|
1330
|
+
if (!(timeoutMs > 0)) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
this.disconnectResetTimer = setTimeout(() => {
|
|
1334
|
+
this.disconnectResetTimer = null;
|
|
1335
|
+
if (this.intentionallyLeft || !this.joinRequested) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (this.connectionState !== stateAtSchedule) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (this.connectionState === 'connected') {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
for (const handler of this.recoveryFailureHandlers) {
|
|
1345
|
+
try {
|
|
1346
|
+
handler({
|
|
1347
|
+
state: this.connectionState,
|
|
1348
|
+
timeoutMs,
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
catch {
|
|
1352
|
+
// Ignore recovery failure handler errors.
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}, timeoutMs);
|
|
1356
|
+
}
|
|
1689
1357
|
setConnectionState(next) {
|
|
1690
1358
|
if (this.connectionState === next) {
|
|
1691
1359
|
return;
|
|
1692
1360
|
}
|
|
1693
1361
|
this.connectionState = next;
|
|
1362
|
+
if (this.shouldScheduleDisconnectReset(next)) {
|
|
1363
|
+
this.scheduleDisconnectReset(next);
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
this.clearDisconnectResetTimer();
|
|
1367
|
+
}
|
|
1694
1368
|
for (const handler of this.connectionStateHandlers) {
|
|
1695
1369
|
handler(next);
|
|
1696
1370
|
}
|
|
@@ -1757,11 +1431,21 @@ export class RoomClient {
|
|
|
1757
1431
|
}
|
|
1758
1432
|
startHeartbeat() {
|
|
1759
1433
|
this.stopHeartbeat();
|
|
1434
|
+
this.lastHeartbeatAckAt = Date.now();
|
|
1760
1435
|
this.heartbeatTimer = setInterval(() => {
|
|
1761
1436
|
if (this.ws && this.connected) {
|
|
1437
|
+
if (Date.now() - this.lastHeartbeatAckAt > this.options.heartbeatStaleTimeoutMs) {
|
|
1438
|
+
try {
|
|
1439
|
+
this.ws.close();
|
|
1440
|
+
}
|
|
1441
|
+
catch {
|
|
1442
|
+
// Socket may already be closing.
|
|
1443
|
+
}
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1762
1446
|
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
1763
1447
|
}
|
|
1764
|
-
},
|
|
1448
|
+
}, this.options.heartbeatIntervalMs);
|
|
1765
1449
|
}
|
|
1766
1450
|
stopHeartbeat() {
|
|
1767
1451
|
if (this.heartbeatTimer) {
|
|
@@ -1769,5 +1453,11 @@ export class RoomClient {
|
|
|
1769
1453
|
this.heartbeatTimer = null;
|
|
1770
1454
|
}
|
|
1771
1455
|
}
|
|
1456
|
+
getReconnectMemberState() {
|
|
1457
|
+
if (!this.lastLocalMemberState) {
|
|
1458
|
+
return undefined;
|
|
1459
|
+
}
|
|
1460
|
+
return cloneRecord(this.lastLocalMemberState);
|
|
1461
|
+
}
|
|
1772
1462
|
}
|
|
1773
1463
|
//# sourceMappingURL=room.js.map
|