@edge-base/web 0.2.5 → 0.2.7
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 +15 -31
- package/dist/auth-refresh.d.ts.map +1 -1
- package/dist/auth-refresh.js +6 -5
- package/dist/auth-refresh.js.map +1 -1
- package/dist/client.d.ts +25 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +23 -1
- package/dist/client.js.map +1 -1
- package/dist/database-live.d.ts.map +1 -1
- package/dist/database-live.js +5 -5
- package/dist/database-live.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/room-cloudflare-media.d.ts +6 -1
- package/dist/room-cloudflare-media.d.ts.map +1 -1
- package/dist/room-cloudflare-media.js +113 -1
- package/dist/room-cloudflare-media.js.map +1 -1
- package/dist/room-p2p-media.d.ts +64 -1
- package/dist/room-p2p-media.d.ts.map +1 -1
- package/dist/room-p2p-media.js +776 -49
- package/dist/room-p2p-media.js.map +1 -1
- package/dist/room.d.ts +59 -260
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +241 -469
- package/dist/room.js.map +1 -1
- package/dist/token-manager.d.ts +5 -1
- package/dist/token-manager.d.ts.map +1 -1
- package/dist/token-manager.js +38 -30
- package/dist/token-manager.js.map +1 -1
- package/llms.txt +9 -57
- package/package.json +2 -3
package/dist/room.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { EdgeBaseError, createSubscription } from '@edge-base/core';
|
|
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(),
|
|
@@ -154,6 +153,7 @@ export class RoomClient {
|
|
|
154
153
|
};
|
|
155
154
|
meta = {
|
|
156
155
|
get: () => this.getMetadata(),
|
|
156
|
+
summary: () => this.getSummary(),
|
|
157
157
|
};
|
|
158
158
|
signals = {
|
|
159
159
|
send: (event, payload, options) => this.sendSignal(event, payload, options),
|
|
@@ -162,7 +162,7 @@ export class RoomClient {
|
|
|
162
162
|
onAny: (handler) => this.onAnySignal(handler),
|
|
163
163
|
};
|
|
164
164
|
members = {
|
|
165
|
-
list: () =>
|
|
165
|
+
list: () => this._members.map((member) => cloneValue(member)),
|
|
166
166
|
current: () => {
|
|
167
167
|
const connectionId = this.currentConnectionId;
|
|
168
168
|
if (connectionId) {
|
|
@@ -178,7 +178,9 @@ export class RoomClient {
|
|
|
178
178
|
const member = this._members.find((entry) => entry.userId === userId) ?? null;
|
|
179
179
|
return member ? cloneValue(member) : null;
|
|
180
180
|
},
|
|
181
|
+
awaitCurrent: (timeoutMs = 10_000) => this.waitForCurrentMember(timeoutMs),
|
|
181
182
|
onSync: (handler) => this.onMembersSync(handler),
|
|
183
|
+
onSnapshot: (handler) => this.onMemberSnapshot(handler),
|
|
182
184
|
onJoin: (handler) => this.onMemberJoin(handler),
|
|
183
185
|
onLeave: (handler) => this.onMemberLeave(handler),
|
|
184
186
|
setState: (state) => this.sendMemberState(state),
|
|
@@ -187,59 +189,16 @@ export class RoomClient {
|
|
|
187
189
|
};
|
|
188
190
|
admin = {
|
|
189
191
|
kick: (memberId) => this.sendAdmin('kick', memberId),
|
|
190
|
-
mute: (memberId) => this.sendAdmin('mute', memberId),
|
|
191
192
|
block: (memberId) => this.sendAdmin('block', memberId),
|
|
192
193
|
setRole: (memberId, role) => this.sendAdmin('setRole', memberId, { role }),
|
|
193
|
-
disableVideo: (memberId) => this.sendAdmin('disableVideo', memberId),
|
|
194
|
-
stopScreenShare: (memberId) => this.sendAdmin('stopScreenShare', memberId),
|
|
195
|
-
};
|
|
196
|
-
media = {
|
|
197
|
-
list: () => cloneValue(this._mediaMembers),
|
|
198
|
-
audio: {
|
|
199
|
-
enable: (payload) => this.sendMedia('publish', 'audio', payload),
|
|
200
|
-
disable: () => this.sendMedia('unpublish', 'audio'),
|
|
201
|
-
setMuted: (muted) => this.sendMedia('mute', 'audio', { muted }),
|
|
202
|
-
},
|
|
203
|
-
video: {
|
|
204
|
-
enable: (payload) => this.sendMedia('publish', 'video', payload),
|
|
205
|
-
disable: () => this.sendMedia('unpublish', 'video'),
|
|
206
|
-
setMuted: (muted) => this.sendMedia('mute', 'video', { muted }),
|
|
207
|
-
},
|
|
208
|
-
screen: {
|
|
209
|
-
start: (payload) => this.sendMedia('publish', 'screen', payload),
|
|
210
|
-
stop: () => this.sendMedia('unpublish', 'screen'),
|
|
211
|
-
},
|
|
212
|
-
devices: {
|
|
213
|
-
switch: (payload) => this.switchMediaDevices(payload),
|
|
214
|
-
},
|
|
215
|
-
realtime: {
|
|
216
|
-
iceServers: (payload) => this.requestRealtimeMedia('turn', 'POST', payload),
|
|
217
|
-
},
|
|
218
|
-
cloudflareRealtimeKit: {
|
|
219
|
-
createSession: (payload) => this.requestCloudflareRealtimeKitMedia('session', 'POST', payload),
|
|
220
|
-
},
|
|
221
|
-
transport: (options) => {
|
|
222
|
-
// Infer provider from options: if cloudflareRealtimeKit config is present, use it;
|
|
223
|
-
// otherwise default to p2p for zero-config local development.
|
|
224
|
-
const hasCloudflareConfig = options && 'cloudflareRealtimeKit' in options && options.cloudflareRealtimeKit != null;
|
|
225
|
-
const provider = options?.provider ?? (hasCloudflareConfig ? 'cloudflare_realtimekit' : 'p2p');
|
|
226
|
-
if (provider === 'p2p') {
|
|
227
|
-
const p2pOptions = options?.p2p;
|
|
228
|
-
return new RoomP2PMediaTransport(this, p2pOptions);
|
|
229
|
-
}
|
|
230
|
-
const cloudflareOptions = options?.cloudflareRealtimeKit;
|
|
231
|
-
return new RoomCloudflareMediaTransport(this, cloudflareOptions);
|
|
232
|
-
},
|
|
233
|
-
onTrack: (handler) => this.onMediaTrack(handler),
|
|
234
|
-
onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
|
|
235
|
-
onStateChange: (handler) => this.onMediaStateChange(handler),
|
|
236
|
-
onDeviceChange: (handler) => this.onMediaDeviceChange(handler),
|
|
237
194
|
};
|
|
238
195
|
session = {
|
|
239
196
|
onError: (handler) => this.onError(handler),
|
|
240
197
|
onKicked: (handler) => this.onKicked(handler),
|
|
241
198
|
onReconnect: (handler) => this.onReconnect(handler),
|
|
242
199
|
onConnectionStateChange: (handler) => this.onConnectionStateChange(handler),
|
|
200
|
+
onRecoveryFailure: (handler) => this.onRecoveryFailure(handler),
|
|
201
|
+
getDebugSnapshot: () => this.getDebugSnapshot(),
|
|
243
202
|
};
|
|
244
203
|
constructor(baseUrl, namespace, roomId, tokenManager, options) {
|
|
245
204
|
this.baseUrl = baseUrl;
|
|
@@ -252,6 +211,11 @@ export class RoomClient {
|
|
|
252
211
|
reconnectBaseDelay: options?.reconnectBaseDelay ?? 1000,
|
|
253
212
|
sendTimeout: options?.sendTimeout ?? 10000,
|
|
254
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,
|
|
255
219
|
};
|
|
256
220
|
this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
|
|
257
221
|
this.handleAuthStateChange(user);
|
|
@@ -267,6 +231,17 @@ export class RoomClient {
|
|
|
267
231
|
getPlayerState() {
|
|
268
232
|
return cloneRecord(this._playerState);
|
|
269
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
|
+
}
|
|
270
245
|
// ─── Metadata (HTTP, no WebSocket needed) ───
|
|
271
246
|
/**
|
|
272
247
|
* Get room metadata without joining (HTTP GET).
|
|
@@ -275,45 +250,91 @@ export class RoomClient {
|
|
|
275
250
|
async getMetadata() {
|
|
276
251
|
return RoomClient.getMetadata(this.baseUrl, this.namespace, this.roomId);
|
|
277
252
|
}
|
|
253
|
+
async getSummary() {
|
|
254
|
+
return RoomClient.getSummary(this.baseUrl, this.namespace, this.roomId);
|
|
255
|
+
}
|
|
256
|
+
async checkConnection() {
|
|
257
|
+
return RoomClient.checkConnection(this.baseUrl, this.namespace, this.roomId);
|
|
258
|
+
}
|
|
278
259
|
/**
|
|
279
260
|
* Static: Get room metadata without creating a RoomClient instance.
|
|
280
261
|
* Useful for lobby screens where you need room info before joining.
|
|
281
262
|
*/
|
|
282
263
|
static async getMetadata(baseUrl, namespace, roomId) {
|
|
283
|
-
|
|
284
|
-
const res = await fetch(url);
|
|
285
|
-
if (!res.ok) {
|
|
286
|
-
throw new EdgeBaseError(res.status, `Failed to get room metadata: ${res.statusText}`);
|
|
287
|
-
}
|
|
288
|
-
return res.json();
|
|
264
|
+
return RoomClient.requestPublicRoomResource(baseUrl, 'metadata', namespace, roomId, 'Failed to get room metadata');
|
|
289
265
|
}
|
|
290
|
-
async
|
|
291
|
-
return
|
|
266
|
+
static async getSummary(baseUrl, namespace, roomId) {
|
|
267
|
+
return RoomClient.requestPublicRoomResource(baseUrl, 'summary', namespace, roomId, 'Failed to get room summary');
|
|
292
268
|
}
|
|
293
|
-
async
|
|
294
|
-
|
|
269
|
+
static async getSummaries(baseUrl, namespace, roomIds) {
|
|
270
|
+
const url = `${baseUrl.replace(/\/$/, '')}/api/room/summaries`;
|
|
271
|
+
let res;
|
|
272
|
+
try {
|
|
273
|
+
res = await fetch(url, {
|
|
274
|
+
method: 'POST',
|
|
275
|
+
headers: { 'Content-Type': 'application/json' },
|
|
276
|
+
body: JSON.stringify({ namespace, ids: roomIds }),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
throw networkError(`Failed to get room summaries. Could not reach ${baseUrl.replace(/\/$/, '')}. Make sure the EdgeBase server is running and reachable.`, { cause: error });
|
|
281
|
+
}
|
|
282
|
+
const data = (await res.json().catch(() => null));
|
|
283
|
+
if (!res.ok) {
|
|
284
|
+
const parsed = parseErrorResponse(res.status, data);
|
|
285
|
+
parsed.message = `Failed to get room summaries: ${parsed.message}`;
|
|
286
|
+
throw parsed;
|
|
287
|
+
}
|
|
288
|
+
return data;
|
|
295
289
|
}
|
|
296
|
-
async
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
headers: {
|
|
307
|
-
Authorization: `Bearer ${token}`,
|
|
308
|
-
'Content-Type': 'application/json',
|
|
309
|
-
},
|
|
310
|
-
body: method === 'GET' ? undefined : JSON.stringify(payload ?? {}),
|
|
311
|
-
});
|
|
312
|
-
const data = (await response.json().catch(() => ({})));
|
|
290
|
+
static async checkConnection(baseUrl, namespace, roomId) {
|
|
291
|
+
const url = `${baseUrl.replace(/\/$/, '')}/api/room/connect-check?namespace=${encodeURIComponent(namespace)}&id=${encodeURIComponent(roomId)}`;
|
|
292
|
+
let response;
|
|
293
|
+
try {
|
|
294
|
+
response = await fetch(url);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
throw networkError(`Room connect-check could not reach ${baseUrl.replace(/\/$/, '')}. Make sure the EdgeBase server is running and reachable.`, { cause: error });
|
|
298
|
+
}
|
|
299
|
+
const data = (await response.json().catch(() => null));
|
|
313
300
|
if (!response.ok) {
|
|
314
|
-
throw
|
|
301
|
+
throw parseErrorResponse(response.status, data);
|
|
315
302
|
}
|
|
316
|
-
|
|
303
|
+
if (typeof data?.ok !== 'boolean'
|
|
304
|
+
|| typeof data?.type !== 'string'
|
|
305
|
+
|| typeof data?.category !== 'string'
|
|
306
|
+
|| typeof data?.message !== 'string') {
|
|
307
|
+
throw new EdgeBaseError(response.status || 500, 'Room connect-check returned an unexpected response. The EdgeBase server and SDK may be out of sync.');
|
|
308
|
+
}
|
|
309
|
+
const diagnostic = data;
|
|
310
|
+
return {
|
|
311
|
+
ok: diagnostic.ok,
|
|
312
|
+
type: diagnostic.type,
|
|
313
|
+
category: diagnostic.category,
|
|
314
|
+
message: diagnostic.message,
|
|
315
|
+
namespace: typeof diagnostic.namespace === 'string' ? diagnostic.namespace : undefined,
|
|
316
|
+
roomId: typeof diagnostic.roomId === 'string' ? diagnostic.roomId : undefined,
|
|
317
|
+
runtime: typeof diagnostic.runtime === 'string' ? diagnostic.runtime : undefined,
|
|
318
|
+
pendingCount: typeof diagnostic.pendingCount === 'number' ? diagnostic.pendingCount : undefined,
|
|
319
|
+
maxPending: typeof diagnostic.maxPending === 'number' ? diagnostic.maxPending : undefined,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
static async requestPublicRoomResource(baseUrl, resource, namespace, roomId, failureMessage) {
|
|
323
|
+
const url = `${baseUrl.replace(/\/$/, '')}/api/room/${resource}?namespace=${encodeURIComponent(namespace)}&id=${encodeURIComponent(roomId)}`;
|
|
324
|
+
let res;
|
|
325
|
+
try {
|
|
326
|
+
res = await fetch(url);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
throw networkError(`${failureMessage}. Could not reach ${baseUrl.replace(/\/$/, '')}. Make sure the EdgeBase server is running and reachable.`, { cause: error });
|
|
330
|
+
}
|
|
331
|
+
if (!res.ok) {
|
|
332
|
+
const data = await res.json().catch(() => null);
|
|
333
|
+
const parsed = parseErrorResponse(res.status, data);
|
|
334
|
+
parsed.message = `${failureMessage}: ${parsed.message}`;
|
|
335
|
+
throw parsed;
|
|
336
|
+
}
|
|
337
|
+
return res.json();
|
|
317
338
|
}
|
|
318
339
|
// ─── Connection Lifecycle ───
|
|
319
340
|
/** Connect to the room, authenticate, and join */
|
|
@@ -332,6 +353,7 @@ export class RoomClient {
|
|
|
332
353
|
this.joinRequested = false;
|
|
333
354
|
this.waitingForAuth = false;
|
|
334
355
|
this.stopHeartbeat();
|
|
356
|
+
this.clearDisconnectResetTimer();
|
|
335
357
|
// Reject all pending send() requests
|
|
336
358
|
this.rejectAllPendingRequests(new EdgeBaseError(499, 'Room left'));
|
|
337
359
|
if (this.ws) {
|
|
@@ -349,12 +371,17 @@ export class RoomClient {
|
|
|
349
371
|
this._playerState = {};
|
|
350
372
|
this._playerVersion = 0;
|
|
351
373
|
this._members = [];
|
|
352
|
-
this.
|
|
374
|
+
this.lastLocalMemberState = null;
|
|
353
375
|
this.currentUserId = null;
|
|
354
376
|
this.currentConnectionId = null;
|
|
355
377
|
this.reconnectInfo = null;
|
|
356
378
|
this.setConnectionState('disconnected');
|
|
357
379
|
}
|
|
380
|
+
assertConnected(action) {
|
|
381
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
382
|
+
throw new EdgeBaseError(400, `Room connection required before ${action}. Call room.join() and wait for the connection to finish.`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
358
385
|
/**
|
|
359
386
|
* Destroy the RoomClient and release all resources.
|
|
360
387
|
* Calls leave() if still connected, unsubscribes from auth state changes,
|
|
@@ -378,10 +405,6 @@ export class RoomClient {
|
|
|
378
405
|
this.memberStateHandlers.length = 0;
|
|
379
406
|
this.signalHandlers.clear();
|
|
380
407
|
this.anySignalHandlers.length = 0;
|
|
381
|
-
this.mediaTrackHandlers.length = 0;
|
|
382
|
-
this.mediaTrackRemovedHandlers.length = 0;
|
|
383
|
-
this.mediaStateHandlers.length = 0;
|
|
384
|
-
this.mediaDeviceHandlers.length = 0;
|
|
385
408
|
this.reconnectHandlers.length = 0;
|
|
386
409
|
this.connectionStateHandlers.length = 0;
|
|
387
410
|
}
|
|
@@ -394,9 +417,7 @@ export class RoomClient {
|
|
|
394
417
|
* const result = await room.send('SET_SCORE', { score: 42 });
|
|
395
418
|
*/
|
|
396
419
|
async send(actionType, payload) {
|
|
397
|
-
|
|
398
|
-
throw new EdgeBaseError(400, 'Not connected to room');
|
|
399
|
-
}
|
|
420
|
+
this.assertConnected(`sending action '${actionType}'`);
|
|
400
421
|
const requestId = generateRequestId();
|
|
401
422
|
return new Promise((resolve, reject) => {
|
|
402
423
|
const timeout = setTimeout(() => {
|
|
@@ -524,6 +545,14 @@ export class RoomClient {
|
|
|
524
545
|
this.memberSyncHandlers.splice(index, 1);
|
|
525
546
|
});
|
|
526
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
|
+
}
|
|
527
556
|
onMemberJoin(handler) {
|
|
528
557
|
this.memberJoinHandlers.push(handler);
|
|
529
558
|
return createSubscription(() => {
|
|
@@ -556,50 +585,24 @@ export class RoomClient {
|
|
|
556
585
|
this.reconnectHandlers.splice(index, 1);
|
|
557
586
|
});
|
|
558
587
|
}
|
|
559
|
-
|
|
560
|
-
this.
|
|
561
|
-
return createSubscription(() => {
|
|
562
|
-
const index = this.connectionStateHandlers.indexOf(handler);
|
|
563
|
-
if (index >= 0)
|
|
564
|
-
this.connectionStateHandlers.splice(index, 1);
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
onMediaTrack(handler) {
|
|
568
|
-
this.mediaTrackHandlers.push(handler);
|
|
569
|
-
return createSubscription(() => {
|
|
570
|
-
const index = this.mediaTrackHandlers.indexOf(handler);
|
|
571
|
-
if (index >= 0)
|
|
572
|
-
this.mediaTrackHandlers.splice(index, 1);
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
onMediaTrackRemoved(handler) {
|
|
576
|
-
this.mediaTrackRemovedHandlers.push(handler);
|
|
577
|
-
return createSubscription(() => {
|
|
578
|
-
const index = this.mediaTrackRemovedHandlers.indexOf(handler);
|
|
579
|
-
if (index >= 0)
|
|
580
|
-
this.mediaTrackRemovedHandlers.splice(index, 1);
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
onMediaStateChange(handler) {
|
|
584
|
-
this.mediaStateHandlers.push(handler);
|
|
588
|
+
onRecoveryFailure(handler) {
|
|
589
|
+
this.recoveryFailureHandlers.push(handler);
|
|
585
590
|
return createSubscription(() => {
|
|
586
|
-
const index = this.
|
|
591
|
+
const index = this.recoveryFailureHandlers.indexOf(handler);
|
|
587
592
|
if (index >= 0)
|
|
588
|
-
this.
|
|
593
|
+
this.recoveryFailureHandlers.splice(index, 1);
|
|
589
594
|
});
|
|
590
595
|
}
|
|
591
|
-
|
|
592
|
-
this.
|
|
596
|
+
onConnectionStateChange(handler) {
|
|
597
|
+
this.connectionStateHandlers.push(handler);
|
|
593
598
|
return createSubscription(() => {
|
|
594
|
-
const index = this.
|
|
599
|
+
const index = this.connectionStateHandlers.indexOf(handler);
|
|
595
600
|
if (index >= 0)
|
|
596
|
-
this.
|
|
601
|
+
this.connectionStateHandlers.splice(index, 1);
|
|
597
602
|
});
|
|
598
603
|
}
|
|
599
604
|
async sendSignal(event, payload, options) {
|
|
600
|
-
|
|
601
|
-
throw new EdgeBaseError(400, 'Not connected to room');
|
|
602
|
-
}
|
|
605
|
+
this.assertConnected(`sending signal '${event}'`);
|
|
603
606
|
const requestId = generateRequestId();
|
|
604
607
|
return new Promise((resolve, reject) => {
|
|
605
608
|
const timeout = setTimeout(() => {
|
|
@@ -618,34 +621,39 @@ export class RoomClient {
|
|
|
618
621
|
});
|
|
619
622
|
}
|
|
620
623
|
async sendMemberState(state) {
|
|
624
|
+
const nextState = {
|
|
625
|
+
...(this.lastLocalMemberState ?? {}),
|
|
626
|
+
...cloneRecord(state),
|
|
627
|
+
};
|
|
621
628
|
return this.sendMemberStateRequest({
|
|
622
629
|
type: 'member_state',
|
|
623
630
|
state,
|
|
631
|
+
}, () => {
|
|
632
|
+
this.lastLocalMemberState = nextState;
|
|
624
633
|
});
|
|
625
634
|
}
|
|
626
635
|
async clearMemberState() {
|
|
636
|
+
const clearedState = {};
|
|
627
637
|
return this.sendMemberStateRequest({
|
|
628
638
|
type: 'member_state_clear',
|
|
639
|
+
}, () => {
|
|
640
|
+
this.lastLocalMemberState = clearedState;
|
|
629
641
|
});
|
|
630
642
|
}
|
|
631
|
-
async sendMemberStateRequest(payload) {
|
|
632
|
-
|
|
633
|
-
throw new EdgeBaseError(400, 'Not connected to room');
|
|
634
|
-
}
|
|
643
|
+
async sendMemberStateRequest(payload, onSuccess) {
|
|
644
|
+
this.assertConnected('updating member state');
|
|
635
645
|
const requestId = generateRequestId();
|
|
636
646
|
return new Promise((resolve, reject) => {
|
|
637
647
|
const timeout = setTimeout(() => {
|
|
638
648
|
this.pendingMemberStateRequests.delete(requestId);
|
|
639
649
|
reject(new EdgeBaseError(408, 'Member state update timed out'));
|
|
640
650
|
}, this.options.sendTimeout);
|
|
641
|
-
this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout });
|
|
651
|
+
this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout, onSuccess });
|
|
642
652
|
this.sendRaw({ ...payload, requestId });
|
|
643
653
|
});
|
|
644
654
|
}
|
|
645
655
|
async sendAdmin(operation, memberId, payload) {
|
|
646
|
-
|
|
647
|
-
throw new EdgeBaseError(400, 'Not connected to room');
|
|
648
|
-
}
|
|
656
|
+
this.assertConnected(`running admin operation '${operation}'`);
|
|
649
657
|
const requestId = generateRequestId();
|
|
650
658
|
return new Promise((resolve, reject) => {
|
|
651
659
|
const timeout = setTimeout(() => {
|
|
@@ -662,39 +670,6 @@ export class RoomClient {
|
|
|
662
670
|
});
|
|
663
671
|
});
|
|
664
672
|
}
|
|
665
|
-
async sendMedia(operation, kind, payload) {
|
|
666
|
-
if (!this.ws || !this.connected || !this.authenticated) {
|
|
667
|
-
throw new EdgeBaseError(400, 'Not connected to room');
|
|
668
|
-
}
|
|
669
|
-
const requestId = generateRequestId();
|
|
670
|
-
return new Promise((resolve, reject) => {
|
|
671
|
-
const timeout = setTimeout(() => {
|
|
672
|
-
this.pendingMediaRequests.delete(requestId);
|
|
673
|
-
reject(new EdgeBaseError(408, `Media operation '${operation}' timed out`));
|
|
674
|
-
}, this.options.sendTimeout);
|
|
675
|
-
this.pendingMediaRequests.set(requestId, { resolve, reject, timeout });
|
|
676
|
-
this.sendRaw({
|
|
677
|
-
type: 'media',
|
|
678
|
-
operation,
|
|
679
|
-
kind,
|
|
680
|
-
payload: payload ?? {},
|
|
681
|
-
requestId,
|
|
682
|
-
});
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
async switchMediaDevices(payload) {
|
|
686
|
-
const operations = [];
|
|
687
|
-
if (payload.audioInputId) {
|
|
688
|
-
operations.push(this.sendMedia('device', 'audio', { deviceId: payload.audioInputId }));
|
|
689
|
-
}
|
|
690
|
-
if (payload.videoInputId) {
|
|
691
|
-
operations.push(this.sendMedia('device', 'video', { deviceId: payload.videoInputId }));
|
|
692
|
-
}
|
|
693
|
-
if (payload.screenInputId) {
|
|
694
|
-
operations.push(this.sendMedia('device', 'screen', { deviceId: payload.screenInputId }));
|
|
695
|
-
}
|
|
696
|
-
await Promise.all(operations);
|
|
697
|
-
}
|
|
698
673
|
// ─── Private: Connection ───
|
|
699
674
|
async establishConnection() {
|
|
700
675
|
return new Promise((resolve, reject) => {
|
|
@@ -794,11 +769,11 @@ export class RoomClient {
|
|
|
794
769
|
async authenticate() {
|
|
795
770
|
const token = await this.tokenManager.getAccessToken((refreshToken) => refreshAccessToken(this.baseUrl, refreshToken));
|
|
796
771
|
if (!token) {
|
|
797
|
-
throw new EdgeBaseError(401, '
|
|
772
|
+
throw new EdgeBaseError(401, 'Room authentication requires a signed-in session. Sign in before joining the room.');
|
|
798
773
|
}
|
|
799
774
|
return new Promise((resolve, reject) => {
|
|
800
775
|
const timeout = setTimeout(() => {
|
|
801
|
-
reject(new EdgeBaseError(401, 'Room auth
|
|
776
|
+
reject(new EdgeBaseError(401, 'Room authentication timed out. Check the server auth response and room connectivity.'));
|
|
802
777
|
}, 10000);
|
|
803
778
|
const originalOnMessage = this.ws?.onmessage;
|
|
804
779
|
if (this.ws) {
|
|
@@ -818,6 +793,7 @@ export class RoomClient {
|
|
|
818
793
|
lastSharedVersion: this._sharedVersion,
|
|
819
794
|
lastPlayerState: this._playerState,
|
|
820
795
|
lastPlayerVersion: this._playerVersion,
|
|
796
|
+
lastMemberState: this.getReconnectMemberState(),
|
|
821
797
|
});
|
|
822
798
|
this.joined = true;
|
|
823
799
|
resolve();
|
|
@@ -876,9 +852,6 @@ export class RoomClient {
|
|
|
876
852
|
case 'members_sync':
|
|
877
853
|
this.handleMembersSync(msg);
|
|
878
854
|
break;
|
|
879
|
-
case 'media_sync':
|
|
880
|
-
this.handleMediaSync(msg);
|
|
881
|
-
break;
|
|
882
855
|
case 'member_join':
|
|
883
856
|
this.handleMemberJoinFrame(msg);
|
|
884
857
|
break;
|
|
@@ -891,24 +864,6 @@ export class RoomClient {
|
|
|
891
864
|
case 'member_state_error':
|
|
892
865
|
this.handleMemberStateError(msg);
|
|
893
866
|
break;
|
|
894
|
-
case 'media_track':
|
|
895
|
-
this.handleMediaTrackFrame(msg);
|
|
896
|
-
break;
|
|
897
|
-
case 'media_track_removed':
|
|
898
|
-
this.handleMediaTrackRemovedFrame(msg);
|
|
899
|
-
break;
|
|
900
|
-
case 'media_state':
|
|
901
|
-
this.handleMediaStateFrame(msg);
|
|
902
|
-
break;
|
|
903
|
-
case 'media_device':
|
|
904
|
-
this.handleMediaDeviceFrame(msg);
|
|
905
|
-
break;
|
|
906
|
-
case 'media_result':
|
|
907
|
-
this.handleMediaResult(msg);
|
|
908
|
-
break;
|
|
909
|
-
case 'media_error':
|
|
910
|
-
this.handleMediaError(msg);
|
|
911
|
-
break;
|
|
912
867
|
case 'admin_result':
|
|
913
868
|
this.handleAdminResult(msg);
|
|
914
869
|
break;
|
|
@@ -923,6 +878,7 @@ export class RoomClient {
|
|
|
923
878
|
break;
|
|
924
879
|
case 'pong':
|
|
925
880
|
// Heartbeat response — no action needed
|
|
881
|
+
this.lastHeartbeatAckAt = Date.now();
|
|
926
882
|
break;
|
|
927
883
|
}
|
|
928
884
|
}
|
|
@@ -1052,23 +1008,19 @@ export class RoomClient {
|
|
|
1052
1008
|
handleMembersSync(msg) {
|
|
1053
1009
|
const members = this.normalizeMembers(msg.members);
|
|
1054
1010
|
this._members = members;
|
|
1055
|
-
|
|
1056
|
-
this.syncMediaMemberInfo(member);
|
|
1057
|
-
}
|
|
1058
|
-
const snapshot = cloneValue(this._members);
|
|
1011
|
+
const snapshot = this._members.map((member) => cloneValue(member));
|
|
1059
1012
|
for (const handler of this.memberSyncHandlers) {
|
|
1060
1013
|
handler(snapshot);
|
|
1061
1014
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1015
|
+
for (const handler of this.memberSnapshotHandlers) {
|
|
1016
|
+
handler(snapshot);
|
|
1017
|
+
}
|
|
1065
1018
|
}
|
|
1066
1019
|
handleMemberJoinFrame(msg) {
|
|
1067
1020
|
const member = this.normalizeMember(msg.member);
|
|
1068
1021
|
if (!member)
|
|
1069
1022
|
return;
|
|
1070
1023
|
this.upsertMember(member);
|
|
1071
|
-
this.syncMediaMemberInfo(member);
|
|
1072
1024
|
const snapshot = cloneValue(member);
|
|
1073
1025
|
for (const handler of this.memberJoinHandlers) {
|
|
1074
1026
|
handler(snapshot);
|
|
@@ -1079,7 +1031,6 @@ export class RoomClient {
|
|
|
1079
1031
|
if (!member)
|
|
1080
1032
|
return;
|
|
1081
1033
|
this.removeMember(member.memberId);
|
|
1082
|
-
this.removeMediaMember(member.memberId);
|
|
1083
1034
|
const reason = this.normalizeLeaveReason(msg.reason);
|
|
1084
1035
|
const snapshot = cloneValue(member);
|
|
1085
1036
|
for (const handler of this.memberLeaveHandlers) {
|
|
@@ -1093,13 +1044,13 @@ export class RoomClient {
|
|
|
1093
1044
|
return;
|
|
1094
1045
|
member.state = state;
|
|
1095
1046
|
this.upsertMember(member);
|
|
1096
|
-
this.syncMediaMemberInfo(member);
|
|
1097
1047
|
const requestId = msg.requestId;
|
|
1098
1048
|
if (requestId) {
|
|
1099
1049
|
const pending = this.pendingMemberStateRequests.get(requestId);
|
|
1100
1050
|
if (pending) {
|
|
1101
1051
|
clearTimeout(pending.timeout);
|
|
1102
1052
|
this.pendingMemberStateRequests.delete(requestId);
|
|
1053
|
+
pending.onSuccess?.();
|
|
1103
1054
|
pending.resolve();
|
|
1104
1055
|
}
|
|
1105
1056
|
}
|
|
@@ -1120,115 +1071,6 @@ export class RoomClient {
|
|
|
1120
1071
|
this.pendingMemberStateRequests.delete(requestId);
|
|
1121
1072
|
pending.reject(new EdgeBaseError(400, msg.message || 'Member state update failed'));
|
|
1122
1073
|
}
|
|
1123
|
-
handleMediaTrackFrame(msg) {
|
|
1124
|
-
const member = this.normalizeMember(msg.member);
|
|
1125
|
-
const track = this.normalizeMediaTrack(msg.track);
|
|
1126
|
-
if (!member || !track)
|
|
1127
|
-
return;
|
|
1128
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
1129
|
-
this.upsertMediaTrack(mediaMember, track);
|
|
1130
|
-
this.mergeMediaState(mediaMember, track.kind, {
|
|
1131
|
-
published: true,
|
|
1132
|
-
muted: track.muted,
|
|
1133
|
-
trackId: track.trackId,
|
|
1134
|
-
deviceId: track.deviceId,
|
|
1135
|
-
publishedAt: track.publishedAt,
|
|
1136
|
-
adminDisabled: track.adminDisabled,
|
|
1137
|
-
providerSessionId: track.providerSessionId,
|
|
1138
|
-
});
|
|
1139
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1140
|
-
const trackSnapshot = cloneValue(track);
|
|
1141
|
-
for (const handler of this.mediaTrackHandlers) {
|
|
1142
|
-
handler(trackSnapshot, memberSnapshot);
|
|
1143
|
-
}
|
|
1144
|
-
const stateSnapshot = cloneValue(mediaMember.state);
|
|
1145
|
-
for (const handler of this.mediaStateHandlers) {
|
|
1146
|
-
handler(memberSnapshot, stateSnapshot);
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
handleMediaTrackRemovedFrame(msg) {
|
|
1150
|
-
const member = this.normalizeMember(msg.member);
|
|
1151
|
-
const track = this.normalizeMediaTrack(msg.track);
|
|
1152
|
-
if (!member || !track)
|
|
1153
|
-
return;
|
|
1154
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
1155
|
-
this.removeMediaTrack(mediaMember, track);
|
|
1156
|
-
mediaMember.state = {
|
|
1157
|
-
...mediaMember.state,
|
|
1158
|
-
[track.kind]: {
|
|
1159
|
-
published: false,
|
|
1160
|
-
muted: false,
|
|
1161
|
-
adminDisabled: false,
|
|
1162
|
-
providerSessionId: undefined,
|
|
1163
|
-
},
|
|
1164
|
-
};
|
|
1165
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1166
|
-
const trackSnapshot = cloneValue(track);
|
|
1167
|
-
for (const handler of this.mediaTrackRemovedHandlers) {
|
|
1168
|
-
handler(trackSnapshot, memberSnapshot);
|
|
1169
|
-
}
|
|
1170
|
-
const stateSnapshot = cloneValue(mediaMember.state);
|
|
1171
|
-
for (const handler of this.mediaStateHandlers) {
|
|
1172
|
-
handler(memberSnapshot, stateSnapshot);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
handleMediaStateFrame(msg) {
|
|
1176
|
-
const member = this.normalizeMember(msg.member);
|
|
1177
|
-
if (!member)
|
|
1178
|
-
return;
|
|
1179
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
1180
|
-
mediaMember.state = this.normalizeMediaState(msg.state);
|
|
1181
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1182
|
-
const stateSnapshot = cloneValue(mediaMember.state);
|
|
1183
|
-
for (const handler of this.mediaStateHandlers) {
|
|
1184
|
-
handler(memberSnapshot, stateSnapshot);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
handleMediaDeviceFrame(msg) {
|
|
1188
|
-
const member = this.normalizeMember(msg.member);
|
|
1189
|
-
const kind = this.normalizeMediaKind(msg.kind);
|
|
1190
|
-
const deviceId = typeof msg.deviceId === 'string' ? msg.deviceId : '';
|
|
1191
|
-
if (!member || !kind || !deviceId)
|
|
1192
|
-
return;
|
|
1193
|
-
const mediaMember = this.ensureMediaMember(member);
|
|
1194
|
-
this.mergeMediaState(mediaMember, kind, { deviceId });
|
|
1195
|
-
for (const track of mediaMember.tracks) {
|
|
1196
|
-
if (track.kind === kind) {
|
|
1197
|
-
track.deviceId = deviceId;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1201
|
-
const change = { kind, deviceId };
|
|
1202
|
-
for (const handler of this.mediaDeviceHandlers) {
|
|
1203
|
-
handler(memberSnapshot, change);
|
|
1204
|
-
}
|
|
1205
|
-
const stateSnapshot = cloneValue(mediaMember.state);
|
|
1206
|
-
for (const handler of this.mediaStateHandlers) {
|
|
1207
|
-
handler(memberSnapshot, stateSnapshot);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
handleMediaResult(msg) {
|
|
1211
|
-
const requestId = msg.requestId;
|
|
1212
|
-
if (!requestId)
|
|
1213
|
-
return;
|
|
1214
|
-
const pending = this.pendingMediaRequests.get(requestId);
|
|
1215
|
-
if (!pending)
|
|
1216
|
-
return;
|
|
1217
|
-
clearTimeout(pending.timeout);
|
|
1218
|
-
this.pendingMediaRequests.delete(requestId);
|
|
1219
|
-
pending.resolve();
|
|
1220
|
-
}
|
|
1221
|
-
handleMediaError(msg) {
|
|
1222
|
-
const requestId = msg.requestId;
|
|
1223
|
-
if (!requestId)
|
|
1224
|
-
return;
|
|
1225
|
-
const pending = this.pendingMediaRequests.get(requestId);
|
|
1226
|
-
if (!pending)
|
|
1227
|
-
return;
|
|
1228
|
-
clearTimeout(pending.timeout);
|
|
1229
|
-
this.pendingMediaRequests.delete(requestId);
|
|
1230
|
-
pending.reject(new EdgeBaseError(400, msg.message || 'Media operation failed'));
|
|
1231
|
-
}
|
|
1232
1074
|
handleAdminResult(msg) {
|
|
1233
1075
|
const requestId = msg.requestId;
|
|
1234
1076
|
if (!requestId)
|
|
@@ -1305,7 +1147,6 @@ export class RoomClient {
|
|
|
1305
1147
|
this.connected = false;
|
|
1306
1148
|
this.authenticated = false;
|
|
1307
1149
|
this.joined = false;
|
|
1308
|
-
this._mediaMembers = [];
|
|
1309
1150
|
this.currentUserId = null;
|
|
1310
1151
|
this.currentConnectionId = null;
|
|
1311
1152
|
try {
|
|
@@ -1319,7 +1160,6 @@ export class RoomClient {
|
|
|
1319
1160
|
this.connected = false;
|
|
1320
1161
|
this.authenticated = false;
|
|
1321
1162
|
this.joined = false;
|
|
1322
|
-
this._mediaMembers = [];
|
|
1323
1163
|
}
|
|
1324
1164
|
handleAuthenticationFailure(error) {
|
|
1325
1165
|
const authError = error instanceof EdgeBaseError
|
|
@@ -1355,8 +1195,7 @@ export class RoomClient {
|
|
|
1355
1195
|
return this.pendingRequests.size > 0
|
|
1356
1196
|
|| this.pendingSignalRequests.size > 0
|
|
1357
1197
|
|| this.pendingAdminRequests.size > 0
|
|
1358
|
-
|| this.pendingMemberStateRequests.size > 0
|
|
1359
|
-
|| this.pendingMediaRequests.size > 0;
|
|
1198
|
+
|| this.pendingMemberStateRequests.size > 0;
|
|
1360
1199
|
}
|
|
1361
1200
|
handleRoomAuthStateLoss(message) {
|
|
1362
1201
|
const detail = message?.trim();
|
|
@@ -1382,14 +1221,6 @@ export class RoomClient {
|
|
|
1382
1221
|
.map((member) => this.normalizeMember(member))
|
|
1383
1222
|
.filter((member) => !!member);
|
|
1384
1223
|
}
|
|
1385
|
-
normalizeMediaMembers(value) {
|
|
1386
|
-
if (!Array.isArray(value)) {
|
|
1387
|
-
return [];
|
|
1388
|
-
}
|
|
1389
|
-
return value
|
|
1390
|
-
.map((member) => this.normalizeMediaMember(member))
|
|
1391
|
-
.filter((member) => !!member);
|
|
1392
|
-
}
|
|
1393
1224
|
normalizeMember(value) {
|
|
1394
1225
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1395
1226
|
return null;
|
|
@@ -1413,84 +1244,6 @@ export class RoomClient {
|
|
|
1413
1244
|
}
|
|
1414
1245
|
return cloneRecord(value);
|
|
1415
1246
|
}
|
|
1416
|
-
normalizeMediaMember(value) {
|
|
1417
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1418
|
-
return null;
|
|
1419
|
-
}
|
|
1420
|
-
const entry = value;
|
|
1421
|
-
const member = this.normalizeMember(entry.member);
|
|
1422
|
-
if (!member) {
|
|
1423
|
-
return null;
|
|
1424
|
-
}
|
|
1425
|
-
return {
|
|
1426
|
-
member,
|
|
1427
|
-
state: this.normalizeMediaState(entry.state),
|
|
1428
|
-
tracks: this.normalizeMediaTracks(entry.tracks),
|
|
1429
|
-
};
|
|
1430
|
-
}
|
|
1431
|
-
normalizeMediaState(value) {
|
|
1432
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1433
|
-
return {};
|
|
1434
|
-
}
|
|
1435
|
-
const state = value;
|
|
1436
|
-
return {
|
|
1437
|
-
audio: this.normalizeMediaKindState(state.audio),
|
|
1438
|
-
video: this.normalizeMediaKindState(state.video),
|
|
1439
|
-
screen: this.normalizeMediaKindState(state.screen),
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
normalizeMediaKindState(value) {
|
|
1443
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1444
|
-
return undefined;
|
|
1445
|
-
}
|
|
1446
|
-
const state = value;
|
|
1447
|
-
return {
|
|
1448
|
-
published: state.published === true,
|
|
1449
|
-
muted: state.muted === true,
|
|
1450
|
-
trackId: typeof state.trackId === 'string' ? state.trackId : undefined,
|
|
1451
|
-
deviceId: typeof state.deviceId === 'string' ? state.deviceId : undefined,
|
|
1452
|
-
publishedAt: typeof state.publishedAt === 'number' ? state.publishedAt : undefined,
|
|
1453
|
-
adminDisabled: state.adminDisabled === true,
|
|
1454
|
-
providerSessionId: typeof state.providerSessionId === 'string' ? state.providerSessionId : undefined,
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
normalizeMediaTracks(value) {
|
|
1458
|
-
if (!Array.isArray(value)) {
|
|
1459
|
-
return [];
|
|
1460
|
-
}
|
|
1461
|
-
return value
|
|
1462
|
-
.map((track) => this.normalizeMediaTrack(track))
|
|
1463
|
-
.filter((track) => !!track);
|
|
1464
|
-
}
|
|
1465
|
-
normalizeMediaTrack(value) {
|
|
1466
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1467
|
-
return null;
|
|
1468
|
-
}
|
|
1469
|
-
const track = value;
|
|
1470
|
-
const kind = this.normalizeMediaKind(track.kind);
|
|
1471
|
-
if (!kind) {
|
|
1472
|
-
return null;
|
|
1473
|
-
}
|
|
1474
|
-
return {
|
|
1475
|
-
kind,
|
|
1476
|
-
trackId: typeof track.trackId === 'string' ? track.trackId : undefined,
|
|
1477
|
-
deviceId: typeof track.deviceId === 'string' ? track.deviceId : undefined,
|
|
1478
|
-
muted: track.muted === true,
|
|
1479
|
-
publishedAt: typeof track.publishedAt === 'number' ? track.publishedAt : undefined,
|
|
1480
|
-
adminDisabled: track.adminDisabled === true,
|
|
1481
|
-
providerSessionId: typeof track.providerSessionId === 'string' ? track.providerSessionId : undefined,
|
|
1482
|
-
};
|
|
1483
|
-
}
|
|
1484
|
-
normalizeMediaKind(value) {
|
|
1485
|
-
switch (value) {
|
|
1486
|
-
case 'audio':
|
|
1487
|
-
case 'video':
|
|
1488
|
-
case 'screen':
|
|
1489
|
-
return value;
|
|
1490
|
-
default:
|
|
1491
|
-
return null;
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
1247
|
normalizeSignalMeta(value) {
|
|
1495
1248
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1496
1249
|
return {};
|
|
@@ -1516,6 +1269,20 @@ export class RoomClient {
|
|
|
1516
1269
|
return 'leave';
|
|
1517
1270
|
}
|
|
1518
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
|
+
}
|
|
1519
1286
|
upsertMember(member) {
|
|
1520
1287
|
const index = this._members.findIndex((entry) => entry.memberId === member.memberId);
|
|
1521
1288
|
if (index >= 0) {
|
|
@@ -1527,64 +1294,6 @@ export class RoomClient {
|
|
|
1527
1294
|
removeMember(memberId) {
|
|
1528
1295
|
this._members = this._members.filter((member) => member.memberId !== memberId);
|
|
1529
1296
|
}
|
|
1530
|
-
syncMediaMemberInfo(member) {
|
|
1531
|
-
const mediaMember = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
1532
|
-
if (!mediaMember) {
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
mediaMember.member = cloneValue(member);
|
|
1536
|
-
}
|
|
1537
|
-
ensureMediaMember(member) {
|
|
1538
|
-
const existing = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
1539
|
-
if (existing) {
|
|
1540
|
-
existing.member = cloneValue(member);
|
|
1541
|
-
return existing;
|
|
1542
|
-
}
|
|
1543
|
-
const created = {
|
|
1544
|
-
member: cloneValue(member),
|
|
1545
|
-
state: {},
|
|
1546
|
-
tracks: [],
|
|
1547
|
-
};
|
|
1548
|
-
this._mediaMembers.push(created);
|
|
1549
|
-
return created;
|
|
1550
|
-
}
|
|
1551
|
-
removeMediaMember(memberId) {
|
|
1552
|
-
this._mediaMembers = this._mediaMembers.filter((member) => member.member.memberId !== memberId);
|
|
1553
|
-
}
|
|
1554
|
-
upsertMediaTrack(mediaMember, track) {
|
|
1555
|
-
const index = mediaMember.tracks.findIndex((entry) => entry.kind === track.kind &&
|
|
1556
|
-
entry.trackId === track.trackId);
|
|
1557
|
-
if (index >= 0) {
|
|
1558
|
-
mediaMember.tracks[index] = cloneValue(track);
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
mediaMember.tracks = mediaMember.tracks
|
|
1562
|
-
.filter((entry) => !(entry.kind === track.kind && !track.trackId))
|
|
1563
|
-
.concat(cloneValue(track));
|
|
1564
|
-
}
|
|
1565
|
-
removeMediaTrack(mediaMember, track) {
|
|
1566
|
-
mediaMember.tracks = mediaMember.tracks.filter((entry) => {
|
|
1567
|
-
if (track.trackId) {
|
|
1568
|
-
return !(entry.kind === track.kind && entry.trackId === track.trackId);
|
|
1569
|
-
}
|
|
1570
|
-
return entry.kind !== track.kind;
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1573
|
-
mergeMediaState(mediaMember, kind, partial) {
|
|
1574
|
-
const next = {
|
|
1575
|
-
published: partial.published ?? mediaMember.state[kind]?.published ?? false,
|
|
1576
|
-
muted: partial.muted ?? mediaMember.state[kind]?.muted ?? false,
|
|
1577
|
-
trackId: partial.trackId ?? mediaMember.state[kind]?.trackId,
|
|
1578
|
-
deviceId: partial.deviceId ?? mediaMember.state[kind]?.deviceId,
|
|
1579
|
-
publishedAt: partial.publishedAt ?? mediaMember.state[kind]?.publishedAt,
|
|
1580
|
-
adminDisabled: partial.adminDisabled ?? mediaMember.state[kind]?.adminDisabled,
|
|
1581
|
-
providerSessionId: partial.providerSessionId ?? mediaMember.state[kind]?.providerSessionId,
|
|
1582
|
-
};
|
|
1583
|
-
mediaMember.state = {
|
|
1584
|
-
...mediaMember.state,
|
|
1585
|
-
[kind]: next,
|
|
1586
|
-
};
|
|
1587
|
-
}
|
|
1588
1297
|
/** Reject all 5 pending request maps at once. */
|
|
1589
1298
|
rejectAllPendingRequests(error) {
|
|
1590
1299
|
for (const [, pending] of this.pendingRequests) {
|
|
@@ -1595,7 +1304,6 @@ export class RoomClient {
|
|
|
1595
1304
|
this.rejectPendingVoidRequests(this.pendingSignalRequests, error);
|
|
1596
1305
|
this.rejectPendingVoidRequests(this.pendingAdminRequests, error);
|
|
1597
1306
|
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, error);
|
|
1598
|
-
this.rejectPendingVoidRequests(this.pendingMediaRequests, error);
|
|
1599
1307
|
}
|
|
1600
1308
|
rejectPendingVoidRequests(pendingRequests, error) {
|
|
1601
1309
|
for (const [, pending] of pendingRequests) {
|
|
@@ -1604,11 +1312,59 @@ export class RoomClient {
|
|
|
1604
1312
|
}
|
|
1605
1313
|
pendingRequests.clear();
|
|
1606
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
|
+
}
|
|
1607
1357
|
setConnectionState(next) {
|
|
1608
1358
|
if (this.connectionState === next) {
|
|
1609
1359
|
return;
|
|
1610
1360
|
}
|
|
1611
1361
|
this.connectionState = next;
|
|
1362
|
+
if (this.shouldScheduleDisconnectReset(next)) {
|
|
1363
|
+
this.scheduleDisconnectReset(next);
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
this.clearDisconnectResetTimer();
|
|
1367
|
+
}
|
|
1612
1368
|
for (const handler of this.connectionStateHandlers) {
|
|
1613
1369
|
handler(next);
|
|
1614
1370
|
}
|
|
@@ -1675,11 +1431,21 @@ export class RoomClient {
|
|
|
1675
1431
|
}
|
|
1676
1432
|
startHeartbeat() {
|
|
1677
1433
|
this.stopHeartbeat();
|
|
1434
|
+
this.lastHeartbeatAckAt = Date.now();
|
|
1678
1435
|
this.heartbeatTimer = setInterval(() => {
|
|
1679
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
|
+
}
|
|
1680
1446
|
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
1681
1447
|
}
|
|
1682
|
-
},
|
|
1448
|
+
}, this.options.heartbeatIntervalMs);
|
|
1683
1449
|
}
|
|
1684
1450
|
stopHeartbeat() {
|
|
1685
1451
|
if (this.heartbeatTimer) {
|
|
@@ -1687,5 +1453,11 @@ export class RoomClient {
|
|
|
1687
1453
|
this.heartbeatTimer = null;
|
|
1688
1454
|
}
|
|
1689
1455
|
}
|
|
1456
|
+
getReconnectMemberState() {
|
|
1457
|
+
if (!this.lastLocalMemberState) {
|
|
1458
|
+
return undefined;
|
|
1459
|
+
}
|
|
1460
|
+
return cloneRecord(this.lastLocalMemberState);
|
|
1461
|
+
}
|
|
1690
1462
|
}
|
|
1691
1463
|
//# sourceMappingURL=room.js.map
|