@edge-base/web 0.1.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 +352 -0
- package/dist/analytics.d.ts +60 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +146 -0
- package/dist/analytics.js.map +1 -0
- package/dist/auth-refresh.d.ts +5 -0
- package/dist/auth-refresh.d.ts.map +1 -0
- package/dist/auth-refresh.js +26 -0
- package/dist/auth-refresh.js.map +1 -0
- package/dist/auth.d.ts +314 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +518 -0
- package/dist/auth.js.map +1 -0
- package/dist/browser-storage.d.ts +7 -0
- package/dist/browser-storage.d.ts.map +1 -0
- package/dist/browser-storage.js +43 -0
- package/dist/browser-storage.js.map +1 -0
- package/dist/client.d.ts +145 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +310 -0
- package/dist/client.js.map +1 -0
- package/dist/database-live.d.ts +65 -0
- package/dist/database-live.d.ts.map +1 -0
- package/dist/database-live.js +486 -0
- package/dist/database-live.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/match-filter.d.ts +30 -0
- package/dist/match-filter.d.ts.map +1 -0
- package/dist/match-filter.js +86 -0
- package/dist/match-filter.js.map +1 -0
- package/dist/room-realtime-media.d.ts +96 -0
- package/dist/room-realtime-media.d.ts.map +1 -0
- package/dist/room-realtime-media.js +418 -0
- package/dist/room-realtime-media.js.map +1 -0
- package/dist/room.d.ts +450 -0
- package/dist/room.d.ts.map +1 -0
- package/dist/room.js +1506 -0
- package/dist/room.js.map +1 -0
- package/dist/token-manager.d.ts +73 -0
- package/dist/token-manager.d.ts.map +1 -0
- package/dist/token-manager.js +378 -0
- package/dist/token-manager.js.map +1 -0
- package/dist/turnstile.d.ts +56 -0
- package/dist/turnstile.d.ts.map +1 -0
- package/dist/turnstile.js +191 -0
- package/dist/turnstile.js.map +1 -0
- package/llms.txt +549 -0
- package/package.json +50 -0
package/dist/room.js
ADDED
|
@@ -0,0 +1,1506 @@
|
|
|
1
|
+
import { EdgeBaseError } from '@edge-base/core';
|
|
2
|
+
import { refreshAccessToken } from './auth-refresh.js';
|
|
3
|
+
import { RoomRealtimeMediaTransport, } from './room-realtime-media.js';
|
|
4
|
+
// ─── Helpers ───
|
|
5
|
+
function deepSet(obj, path, value) {
|
|
6
|
+
const parts = path.split('.');
|
|
7
|
+
let current = obj;
|
|
8
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
9
|
+
const key = parts[i];
|
|
10
|
+
if (typeof current[key] !== 'object' || current[key] === null) {
|
|
11
|
+
current[key] = {};
|
|
12
|
+
}
|
|
13
|
+
current = current[key];
|
|
14
|
+
}
|
|
15
|
+
const lastKey = parts[parts.length - 1];
|
|
16
|
+
if (value === null) {
|
|
17
|
+
delete current[lastKey];
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
current[lastKey] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function generateRequestId() {
|
|
24
|
+
// Use crypto.randomUUID if available, fallback to simple counter
|
|
25
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
26
|
+
return crypto.randomUUID();
|
|
27
|
+
}
|
|
28
|
+
return `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
29
|
+
}
|
|
30
|
+
function cloneValue(value) {
|
|
31
|
+
if (typeof structuredClone === 'function') {
|
|
32
|
+
return structuredClone(value);
|
|
33
|
+
}
|
|
34
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
35
|
+
}
|
|
36
|
+
function cloneRecord(value) {
|
|
37
|
+
return cloneValue(value);
|
|
38
|
+
}
|
|
39
|
+
const WS_CONNECTING = 0;
|
|
40
|
+
const WS_OPEN = 1;
|
|
41
|
+
const ROOM_EXPLICIT_LEAVE_CLOSE_CODE = 4005;
|
|
42
|
+
const ROOM_EXPLICIT_LEAVE_REASON = 'Client left room';
|
|
43
|
+
const ROOM_EXPLICIT_LEAVE_CLOSE_DELAY_MS = 40;
|
|
44
|
+
function isSocketOpenOrConnecting(socket) {
|
|
45
|
+
return !!socket && (socket.readyState === WS_OPEN || socket.readyState === WS_CONNECTING);
|
|
46
|
+
}
|
|
47
|
+
function closeSocketAfterLeave(socket, reason) {
|
|
48
|
+
globalThis.setTimeout(() => {
|
|
49
|
+
try {
|
|
50
|
+
socket.close(ROOM_EXPLICIT_LEAVE_CLOSE_CODE, reason);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Socket already closed.
|
|
54
|
+
}
|
|
55
|
+
}, ROOM_EXPLICIT_LEAVE_CLOSE_DELAY_MS);
|
|
56
|
+
}
|
|
57
|
+
// ─── RoomClient v2 ───
|
|
58
|
+
export class RoomClient {
|
|
59
|
+
baseUrl;
|
|
60
|
+
tokenManager;
|
|
61
|
+
options;
|
|
62
|
+
/** Room namespace (e.g. 'game', 'chat') */
|
|
63
|
+
namespace;
|
|
64
|
+
/** Room instance ID within the namespace */
|
|
65
|
+
roomId;
|
|
66
|
+
// ─── State ───
|
|
67
|
+
_sharedState = {};
|
|
68
|
+
_sharedVersion = 0;
|
|
69
|
+
_playerState = {};
|
|
70
|
+
_playerVersion = 0;
|
|
71
|
+
_members = [];
|
|
72
|
+
_mediaMembers = [];
|
|
73
|
+
// ─── Connection ───
|
|
74
|
+
ws = null;
|
|
75
|
+
reconnectAttempts = 0;
|
|
76
|
+
connected = false;
|
|
77
|
+
authenticated = false;
|
|
78
|
+
joined = false;
|
|
79
|
+
currentUserId = null;
|
|
80
|
+
currentConnectionId = null;
|
|
81
|
+
connectionState = 'idle';
|
|
82
|
+
reconnectInfo = null;
|
|
83
|
+
connectingPromise = null;
|
|
84
|
+
heartbeatTimer = null;
|
|
85
|
+
intentionallyLeft = false;
|
|
86
|
+
waitingForAuth = false;
|
|
87
|
+
joinRequested = false;
|
|
88
|
+
unsubAuthState = null;
|
|
89
|
+
// ─── Pending send() requests (requestId → { resolve, reject, timeout }) ───
|
|
90
|
+
pendingRequests = new Map();
|
|
91
|
+
pendingSignalRequests = new Map();
|
|
92
|
+
pendingAdminRequests = new Map();
|
|
93
|
+
pendingMemberStateRequests = new Map();
|
|
94
|
+
pendingMediaRequests = new Map();
|
|
95
|
+
// ─── Subscriptions ───
|
|
96
|
+
sharedStateHandlers = [];
|
|
97
|
+
playerStateHandlers = [];
|
|
98
|
+
messageHandlers = new Map(); // messageType → handlers
|
|
99
|
+
allMessageHandlers = [];
|
|
100
|
+
errorHandlers = [];
|
|
101
|
+
kickedHandlers = [];
|
|
102
|
+
memberSyncHandlers = [];
|
|
103
|
+
memberJoinHandlers = [];
|
|
104
|
+
memberLeaveHandlers = [];
|
|
105
|
+
memberStateHandlers = [];
|
|
106
|
+
signalHandlers = new Map();
|
|
107
|
+
anySignalHandlers = [];
|
|
108
|
+
mediaTrackHandlers = [];
|
|
109
|
+
mediaTrackRemovedHandlers = [];
|
|
110
|
+
mediaStateHandlers = [];
|
|
111
|
+
mediaDeviceHandlers = [];
|
|
112
|
+
reconnectHandlers = [];
|
|
113
|
+
connectionStateHandlers = [];
|
|
114
|
+
state = {
|
|
115
|
+
getShared: () => this.getSharedState(),
|
|
116
|
+
getMine: () => this.getPlayerState(),
|
|
117
|
+
onSharedChange: (handler) => this.onSharedState(handler),
|
|
118
|
+
onMineChange: (handler) => this.onPlayerState(handler),
|
|
119
|
+
send: (actionType, payload) => this.send(actionType, payload),
|
|
120
|
+
};
|
|
121
|
+
meta = {
|
|
122
|
+
get: () => this.getMetadata(),
|
|
123
|
+
};
|
|
124
|
+
signals = {
|
|
125
|
+
send: (event, payload, options) => this.sendSignal(event, payload, options),
|
|
126
|
+
sendTo: (memberId, event, payload) => this.sendSignal(event, payload, { memberId }),
|
|
127
|
+
on: (event, handler) => this.onSignal(event, handler),
|
|
128
|
+
onAny: (handler) => this.onAnySignal(handler),
|
|
129
|
+
};
|
|
130
|
+
members = {
|
|
131
|
+
list: () => cloneValue(this._members),
|
|
132
|
+
onSync: (handler) => this.onMembersSync(handler),
|
|
133
|
+
onJoin: (handler) => this.onMemberJoin(handler),
|
|
134
|
+
onLeave: (handler) => this.onMemberLeave(handler),
|
|
135
|
+
setState: (state) => this.sendMemberState(state),
|
|
136
|
+
clearState: () => this.clearMemberState(),
|
|
137
|
+
onStateChange: (handler) => this.onMemberStateChange(handler),
|
|
138
|
+
};
|
|
139
|
+
admin = {
|
|
140
|
+
kick: (memberId) => this.sendAdmin('kick', memberId),
|
|
141
|
+
mute: (memberId) => this.sendAdmin('mute', memberId),
|
|
142
|
+
block: (memberId) => this.sendAdmin('block', memberId),
|
|
143
|
+
setRole: (memberId, role) => this.sendAdmin('setRole', memberId, { role }),
|
|
144
|
+
disableVideo: (memberId) => this.sendAdmin('disableVideo', memberId),
|
|
145
|
+
stopScreenShare: (memberId) => this.sendAdmin('stopScreenShare', memberId),
|
|
146
|
+
};
|
|
147
|
+
media = {
|
|
148
|
+
list: () => cloneValue(this._mediaMembers),
|
|
149
|
+
audio: {
|
|
150
|
+
enable: (payload) => this.sendMedia('publish', 'audio', payload),
|
|
151
|
+
disable: () => this.sendMedia('unpublish', 'audio'),
|
|
152
|
+
setMuted: (muted) => this.sendMedia('mute', 'audio', { muted }),
|
|
153
|
+
},
|
|
154
|
+
video: {
|
|
155
|
+
enable: (payload) => this.sendMedia('publish', 'video', payload),
|
|
156
|
+
disable: () => this.sendMedia('unpublish', 'video'),
|
|
157
|
+
setMuted: (muted) => this.sendMedia('mute', 'video', { muted }),
|
|
158
|
+
},
|
|
159
|
+
screen: {
|
|
160
|
+
start: (payload) => this.sendMedia('publish', 'screen', payload),
|
|
161
|
+
stop: () => this.sendMedia('unpublish', 'screen'),
|
|
162
|
+
},
|
|
163
|
+
devices: {
|
|
164
|
+
switch: (payload) => this.switchMediaDevices(payload),
|
|
165
|
+
},
|
|
166
|
+
realtime: {
|
|
167
|
+
createSession: (payload) => this.requestRealtimeMedia('session', 'POST', payload),
|
|
168
|
+
getIceServers: (payload) => this.requestRealtimeMedia('turn', 'POST', payload),
|
|
169
|
+
addTracks: (payload) => this.requestRealtimeMedia('tracks/new', 'POST', payload),
|
|
170
|
+
renegotiate: (payload) => this.requestRealtimeMedia('renegotiate', 'PUT', payload),
|
|
171
|
+
closeTracks: (payload) => this.requestRealtimeMedia('tracks/close', 'PUT', payload),
|
|
172
|
+
transport: (options) => new RoomRealtimeMediaTransport(this, options),
|
|
173
|
+
},
|
|
174
|
+
onTrack: (handler) => this.onMediaTrack(handler),
|
|
175
|
+
onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
|
|
176
|
+
onStateChange: (handler) => this.onMediaStateChange(handler),
|
|
177
|
+
onDeviceChange: (handler) => this.onMediaDeviceChange(handler),
|
|
178
|
+
};
|
|
179
|
+
session = {
|
|
180
|
+
onError: (handler) => this.onError(handler),
|
|
181
|
+
onKicked: (handler) => this.onKicked(handler),
|
|
182
|
+
onReconnect: (handler) => this.onReconnect(handler),
|
|
183
|
+
onConnectionStateChange: (handler) => this.onConnectionStateChange(handler),
|
|
184
|
+
};
|
|
185
|
+
constructor(baseUrl, namespace, roomId, tokenManager, options) {
|
|
186
|
+
this.baseUrl = baseUrl;
|
|
187
|
+
this.namespace = namespace;
|
|
188
|
+
this.roomId = roomId;
|
|
189
|
+
this.tokenManager = tokenManager;
|
|
190
|
+
this.options = {
|
|
191
|
+
autoReconnect: options?.autoReconnect ?? true,
|
|
192
|
+
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
|
|
193
|
+
reconnectBaseDelay: options?.reconnectBaseDelay ?? 1000,
|
|
194
|
+
sendTimeout: options?.sendTimeout ?? 10000,
|
|
195
|
+
};
|
|
196
|
+
this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
|
|
197
|
+
this.handleAuthStateChange(user);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// ─── State Accessors ───
|
|
201
|
+
/** Get current shared state (read-only snapshot) */
|
|
202
|
+
getSharedState() {
|
|
203
|
+
return cloneRecord(this._sharedState);
|
|
204
|
+
}
|
|
205
|
+
/** Get current player state (read-only snapshot) */
|
|
206
|
+
getPlayerState() {
|
|
207
|
+
return cloneRecord(this._playerState);
|
|
208
|
+
}
|
|
209
|
+
// ─── Metadata (HTTP, no WebSocket needed) ───
|
|
210
|
+
/**
|
|
211
|
+
* Get room metadata without joining (HTTP GET).
|
|
212
|
+
* Returns developer-defined metadata set by room.setMetadata() on the server.
|
|
213
|
+
*/
|
|
214
|
+
async getMetadata() {
|
|
215
|
+
return RoomClient.getMetadata(this.baseUrl, this.namespace, this.roomId);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Static: Get room metadata without creating a RoomClient instance.
|
|
219
|
+
* Useful for lobby screens where you need room info before joining.
|
|
220
|
+
*/
|
|
221
|
+
static async getMetadata(baseUrl, namespace, roomId) {
|
|
222
|
+
const url = `${baseUrl.replace(/\/$/, '')}/api/room/metadata?namespace=${encodeURIComponent(namespace)}&id=${encodeURIComponent(roomId)}`;
|
|
223
|
+
const res = await fetch(url);
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
throw new EdgeBaseError(res.status, `Failed to get room metadata: ${res.statusText}`);
|
|
226
|
+
}
|
|
227
|
+
return res.json();
|
|
228
|
+
}
|
|
229
|
+
async requestRealtimeMedia(path, method, payload) {
|
|
230
|
+
const token = await this.tokenManager.getAccessToken((refreshToken) => refreshAccessToken(this.baseUrl, refreshToken));
|
|
231
|
+
if (!token) {
|
|
232
|
+
throw new EdgeBaseError(401, 'Authentication required');
|
|
233
|
+
}
|
|
234
|
+
const url = new URL(`${this.baseUrl.replace(/\/$/, '')}/api/room/media/realtime/${path}`);
|
|
235
|
+
url.searchParams.set('namespace', this.namespace);
|
|
236
|
+
url.searchParams.set('id', this.roomId);
|
|
237
|
+
const response = await fetch(url.toString(), {
|
|
238
|
+
method,
|
|
239
|
+
headers: {
|
|
240
|
+
Authorization: `Bearer ${token}`,
|
|
241
|
+
'Content-Type': 'application/json',
|
|
242
|
+
},
|
|
243
|
+
body: method === 'GET' ? undefined : JSON.stringify(payload ?? {}),
|
|
244
|
+
});
|
|
245
|
+
const data = (await response.json().catch(() => ({})));
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
throw new EdgeBaseError(response.status, (typeof data.message === 'string' && data.message) || `Realtime media request failed: ${response.statusText}`);
|
|
248
|
+
}
|
|
249
|
+
return data;
|
|
250
|
+
}
|
|
251
|
+
// ─── Connection Lifecycle ───
|
|
252
|
+
/** Connect to the room, authenticate, and join */
|
|
253
|
+
async join() {
|
|
254
|
+
this.intentionallyLeft = false;
|
|
255
|
+
this.joinRequested = true;
|
|
256
|
+
if (isSocketOpenOrConnecting(this.ws)) {
|
|
257
|
+
return this.connectingPromise ?? Promise.resolve();
|
|
258
|
+
}
|
|
259
|
+
this.setConnectionState(this.reconnectInfo ? 'reconnecting' : 'connecting');
|
|
260
|
+
return this.ensureConnection();
|
|
261
|
+
}
|
|
262
|
+
/** Leave the room and disconnect. Cleans up all pending requests. */
|
|
263
|
+
leave() {
|
|
264
|
+
this.intentionallyLeft = true;
|
|
265
|
+
this.joinRequested = false;
|
|
266
|
+
this.waitingForAuth = false;
|
|
267
|
+
this.stopHeartbeat();
|
|
268
|
+
// Reject all pending send() requests
|
|
269
|
+
for (const [reqId, pending] of this.pendingRequests) {
|
|
270
|
+
clearTimeout(pending.timeout);
|
|
271
|
+
pending.reject(new EdgeBaseError(499, 'Room left'));
|
|
272
|
+
}
|
|
273
|
+
this.pendingRequests.clear();
|
|
274
|
+
this.rejectPendingVoidRequests(this.pendingSignalRequests, new EdgeBaseError(499, 'Room left'));
|
|
275
|
+
this.rejectPendingVoidRequests(this.pendingAdminRequests, new EdgeBaseError(499, 'Room left'));
|
|
276
|
+
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, new EdgeBaseError(499, 'Room left'));
|
|
277
|
+
this.rejectPendingVoidRequests(this.pendingMediaRequests, new EdgeBaseError(499, 'Room left'));
|
|
278
|
+
if (this.ws) {
|
|
279
|
+
const socket = this.ws;
|
|
280
|
+
this.sendRaw({ type: 'leave' });
|
|
281
|
+
closeSocketAfterLeave(socket, ROOM_EXPLICIT_LEAVE_REASON);
|
|
282
|
+
this.ws = null;
|
|
283
|
+
}
|
|
284
|
+
this.connected = false;
|
|
285
|
+
this.authenticated = false;
|
|
286
|
+
this.joined = false;
|
|
287
|
+
this.connectingPromise = null;
|
|
288
|
+
this._sharedState = {};
|
|
289
|
+
this._sharedVersion = 0;
|
|
290
|
+
this._playerState = {};
|
|
291
|
+
this._playerVersion = 0;
|
|
292
|
+
this._members = [];
|
|
293
|
+
this._mediaMembers = [];
|
|
294
|
+
this.currentUserId = null;
|
|
295
|
+
this.currentConnectionId = null;
|
|
296
|
+
this.reconnectInfo = null;
|
|
297
|
+
this.setConnectionState('disconnected');
|
|
298
|
+
}
|
|
299
|
+
// ─── Actions ───
|
|
300
|
+
/**
|
|
301
|
+
* Send an action to the server.
|
|
302
|
+
* Returns a Promise that resolves with the action result from the server.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* const result = await room.send('SET_SCORE', { score: 42 });
|
|
306
|
+
*/
|
|
307
|
+
async send(actionType, payload) {
|
|
308
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
309
|
+
throw new EdgeBaseError(400, 'Not connected to room');
|
|
310
|
+
}
|
|
311
|
+
const requestId = generateRequestId();
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const timeout = setTimeout(() => {
|
|
314
|
+
this.pendingRequests.delete(requestId);
|
|
315
|
+
reject(new EdgeBaseError(408, `Action '${actionType}' timed out`));
|
|
316
|
+
}, this.options.sendTimeout);
|
|
317
|
+
this.pendingRequests.set(requestId, { resolve, reject, timeout });
|
|
318
|
+
this.sendRaw({
|
|
319
|
+
type: 'send',
|
|
320
|
+
actionType,
|
|
321
|
+
payload: payload ?? {},
|
|
322
|
+
requestId,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
// ─── Subscriptions (v2 API) ───
|
|
327
|
+
/**
|
|
328
|
+
* Subscribe to shared state changes.
|
|
329
|
+
* Called on full sync and on each shared_delta.
|
|
330
|
+
*
|
|
331
|
+
* @returns Subscription with unsubscribe()
|
|
332
|
+
*/
|
|
333
|
+
onSharedState(handler) {
|
|
334
|
+
this.sharedStateHandlers.push(handler);
|
|
335
|
+
return {
|
|
336
|
+
unsubscribe: () => {
|
|
337
|
+
const idx = this.sharedStateHandlers.indexOf(handler);
|
|
338
|
+
if (idx >= 0)
|
|
339
|
+
this.sharedStateHandlers.splice(idx, 1);
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Subscribe to player state changes.
|
|
345
|
+
* Called on full sync and on each player_delta.
|
|
346
|
+
*
|
|
347
|
+
* @returns Subscription with unsubscribe()
|
|
348
|
+
*/
|
|
349
|
+
onPlayerState(handler) {
|
|
350
|
+
this.playerStateHandlers.push(handler);
|
|
351
|
+
return {
|
|
352
|
+
unsubscribe: () => {
|
|
353
|
+
const idx = this.playerStateHandlers.indexOf(handler);
|
|
354
|
+
if (idx >= 0)
|
|
355
|
+
this.playerStateHandlers.splice(idx, 1);
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Subscribe to messages of a specific type sent by room.sendMessage().
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* room.onMessage('game_over', (data) => { console.log(data.winner); });
|
|
364
|
+
*
|
|
365
|
+
* @returns Subscription with unsubscribe()
|
|
366
|
+
*/
|
|
367
|
+
onMessage(messageType, handler) {
|
|
368
|
+
if (!this.messageHandlers.has(messageType)) {
|
|
369
|
+
this.messageHandlers.set(messageType, []);
|
|
370
|
+
}
|
|
371
|
+
this.messageHandlers.get(messageType).push(handler);
|
|
372
|
+
return {
|
|
373
|
+
unsubscribe: () => {
|
|
374
|
+
const handlers = this.messageHandlers.get(messageType);
|
|
375
|
+
if (handlers) {
|
|
376
|
+
const idx = handlers.indexOf(handler);
|
|
377
|
+
if (idx >= 0)
|
|
378
|
+
handlers.splice(idx, 1);
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Subscribe to ALL messages regardless of type.
|
|
385
|
+
*
|
|
386
|
+
* @returns Subscription with unsubscribe()
|
|
387
|
+
*/
|
|
388
|
+
onAnyMessage(handler) {
|
|
389
|
+
this.allMessageHandlers.push(handler);
|
|
390
|
+
return {
|
|
391
|
+
unsubscribe: () => {
|
|
392
|
+
const idx = this.allMessageHandlers.indexOf(handler);
|
|
393
|
+
if (idx >= 0)
|
|
394
|
+
this.allMessageHandlers.splice(idx, 1);
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/** Subscribe to errors */
|
|
399
|
+
onError(handler) {
|
|
400
|
+
this.errorHandlers.push(handler);
|
|
401
|
+
return {
|
|
402
|
+
unsubscribe: () => {
|
|
403
|
+
const idx = this.errorHandlers.indexOf(handler);
|
|
404
|
+
if (idx >= 0)
|
|
405
|
+
this.errorHandlers.splice(idx, 1);
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/** Subscribe to kick events */
|
|
410
|
+
onKicked(handler) {
|
|
411
|
+
this.kickedHandlers.push(handler);
|
|
412
|
+
return {
|
|
413
|
+
unsubscribe: () => {
|
|
414
|
+
const idx = this.kickedHandlers.indexOf(handler);
|
|
415
|
+
if (idx >= 0)
|
|
416
|
+
this.kickedHandlers.splice(idx, 1);
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
onSignal(event, handler) {
|
|
421
|
+
if (!this.signalHandlers.has(event)) {
|
|
422
|
+
this.signalHandlers.set(event, []);
|
|
423
|
+
}
|
|
424
|
+
this.signalHandlers.get(event).push(handler);
|
|
425
|
+
return {
|
|
426
|
+
unsubscribe: () => {
|
|
427
|
+
const handlers = this.signalHandlers.get(event);
|
|
428
|
+
if (!handlers)
|
|
429
|
+
return;
|
|
430
|
+
const index = handlers.indexOf(handler);
|
|
431
|
+
if (index >= 0)
|
|
432
|
+
handlers.splice(index, 1);
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
onAnySignal(handler) {
|
|
437
|
+
this.anySignalHandlers.push(handler);
|
|
438
|
+
return {
|
|
439
|
+
unsubscribe: () => {
|
|
440
|
+
const index = this.anySignalHandlers.indexOf(handler);
|
|
441
|
+
if (index >= 0)
|
|
442
|
+
this.anySignalHandlers.splice(index, 1);
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
onMembersSync(handler) {
|
|
447
|
+
this.memberSyncHandlers.push(handler);
|
|
448
|
+
return {
|
|
449
|
+
unsubscribe: () => {
|
|
450
|
+
const index = this.memberSyncHandlers.indexOf(handler);
|
|
451
|
+
if (index >= 0)
|
|
452
|
+
this.memberSyncHandlers.splice(index, 1);
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
onMemberJoin(handler) {
|
|
457
|
+
this.memberJoinHandlers.push(handler);
|
|
458
|
+
return {
|
|
459
|
+
unsubscribe: () => {
|
|
460
|
+
const index = this.memberJoinHandlers.indexOf(handler);
|
|
461
|
+
if (index >= 0)
|
|
462
|
+
this.memberJoinHandlers.splice(index, 1);
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
onMemberLeave(handler) {
|
|
467
|
+
this.memberLeaveHandlers.push(handler);
|
|
468
|
+
return {
|
|
469
|
+
unsubscribe: () => {
|
|
470
|
+
const index = this.memberLeaveHandlers.indexOf(handler);
|
|
471
|
+
if (index >= 0)
|
|
472
|
+
this.memberLeaveHandlers.splice(index, 1);
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
onMemberStateChange(handler) {
|
|
477
|
+
this.memberStateHandlers.push(handler);
|
|
478
|
+
return {
|
|
479
|
+
unsubscribe: () => {
|
|
480
|
+
const index = this.memberStateHandlers.indexOf(handler);
|
|
481
|
+
if (index >= 0)
|
|
482
|
+
this.memberStateHandlers.splice(index, 1);
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
onReconnect(handler) {
|
|
487
|
+
this.reconnectHandlers.push(handler);
|
|
488
|
+
return {
|
|
489
|
+
unsubscribe: () => {
|
|
490
|
+
const index = this.reconnectHandlers.indexOf(handler);
|
|
491
|
+
if (index >= 0)
|
|
492
|
+
this.reconnectHandlers.splice(index, 1);
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
onConnectionStateChange(handler) {
|
|
497
|
+
this.connectionStateHandlers.push(handler);
|
|
498
|
+
return {
|
|
499
|
+
unsubscribe: () => {
|
|
500
|
+
const index = this.connectionStateHandlers.indexOf(handler);
|
|
501
|
+
if (index >= 0)
|
|
502
|
+
this.connectionStateHandlers.splice(index, 1);
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
onMediaTrack(handler) {
|
|
507
|
+
this.mediaTrackHandlers.push(handler);
|
|
508
|
+
return {
|
|
509
|
+
unsubscribe: () => {
|
|
510
|
+
const index = this.mediaTrackHandlers.indexOf(handler);
|
|
511
|
+
if (index >= 0)
|
|
512
|
+
this.mediaTrackHandlers.splice(index, 1);
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
onMediaTrackRemoved(handler) {
|
|
517
|
+
this.mediaTrackRemovedHandlers.push(handler);
|
|
518
|
+
return {
|
|
519
|
+
unsubscribe: () => {
|
|
520
|
+
const index = this.mediaTrackRemovedHandlers.indexOf(handler);
|
|
521
|
+
if (index >= 0)
|
|
522
|
+
this.mediaTrackRemovedHandlers.splice(index, 1);
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
onMediaStateChange(handler) {
|
|
527
|
+
this.mediaStateHandlers.push(handler);
|
|
528
|
+
return {
|
|
529
|
+
unsubscribe: () => {
|
|
530
|
+
const index = this.mediaStateHandlers.indexOf(handler);
|
|
531
|
+
if (index >= 0)
|
|
532
|
+
this.mediaStateHandlers.splice(index, 1);
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
onMediaDeviceChange(handler) {
|
|
537
|
+
this.mediaDeviceHandlers.push(handler);
|
|
538
|
+
return {
|
|
539
|
+
unsubscribe: () => {
|
|
540
|
+
const index = this.mediaDeviceHandlers.indexOf(handler);
|
|
541
|
+
if (index >= 0)
|
|
542
|
+
this.mediaDeviceHandlers.splice(index, 1);
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
async sendSignal(event, payload, options) {
|
|
547
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
548
|
+
throw new EdgeBaseError(400, 'Not connected to room');
|
|
549
|
+
}
|
|
550
|
+
const requestId = generateRequestId();
|
|
551
|
+
return new Promise((resolve, reject) => {
|
|
552
|
+
const timeout = setTimeout(() => {
|
|
553
|
+
this.pendingSignalRequests.delete(requestId);
|
|
554
|
+
reject(new EdgeBaseError(408, `Signal '${event}' timed out`));
|
|
555
|
+
}, this.options.sendTimeout);
|
|
556
|
+
this.pendingSignalRequests.set(requestId, { resolve, reject, timeout });
|
|
557
|
+
this.sendRaw({
|
|
558
|
+
type: 'signal',
|
|
559
|
+
event,
|
|
560
|
+
payload: payload ?? {},
|
|
561
|
+
includeSelf: options?.includeSelf === true,
|
|
562
|
+
memberId: options?.memberId,
|
|
563
|
+
requestId,
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async sendMemberState(state) {
|
|
568
|
+
return this.sendMemberStateRequest({
|
|
569
|
+
type: 'member_state',
|
|
570
|
+
state,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
async clearMemberState() {
|
|
574
|
+
return this.sendMemberStateRequest({
|
|
575
|
+
type: 'member_state_clear',
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
async sendMemberStateRequest(payload) {
|
|
579
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
580
|
+
throw new EdgeBaseError(400, 'Not connected to room');
|
|
581
|
+
}
|
|
582
|
+
const requestId = generateRequestId();
|
|
583
|
+
return new Promise((resolve, reject) => {
|
|
584
|
+
const timeout = setTimeout(() => {
|
|
585
|
+
this.pendingMemberStateRequests.delete(requestId);
|
|
586
|
+
reject(new EdgeBaseError(408, 'Member state update timed out'));
|
|
587
|
+
}, this.options.sendTimeout);
|
|
588
|
+
this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout });
|
|
589
|
+
this.sendRaw({ ...payload, requestId });
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
async sendAdmin(operation, memberId, payload) {
|
|
593
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
594
|
+
throw new EdgeBaseError(400, 'Not connected to room');
|
|
595
|
+
}
|
|
596
|
+
const requestId = generateRequestId();
|
|
597
|
+
return new Promise((resolve, reject) => {
|
|
598
|
+
const timeout = setTimeout(() => {
|
|
599
|
+
this.pendingAdminRequests.delete(requestId);
|
|
600
|
+
reject(new EdgeBaseError(408, `Admin operation '${operation}' timed out`));
|
|
601
|
+
}, this.options.sendTimeout);
|
|
602
|
+
this.pendingAdminRequests.set(requestId, { resolve, reject, timeout });
|
|
603
|
+
this.sendRaw({
|
|
604
|
+
type: 'admin',
|
|
605
|
+
operation,
|
|
606
|
+
memberId,
|
|
607
|
+
payload: payload ?? {},
|
|
608
|
+
requestId,
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
async sendMedia(operation, kind, payload) {
|
|
613
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
614
|
+
throw new EdgeBaseError(400, 'Not connected to room');
|
|
615
|
+
}
|
|
616
|
+
const requestId = generateRequestId();
|
|
617
|
+
return new Promise((resolve, reject) => {
|
|
618
|
+
const timeout = setTimeout(() => {
|
|
619
|
+
this.pendingMediaRequests.delete(requestId);
|
|
620
|
+
reject(new EdgeBaseError(408, `Media operation '${operation}' timed out`));
|
|
621
|
+
}, this.options.sendTimeout);
|
|
622
|
+
this.pendingMediaRequests.set(requestId, { resolve, reject, timeout });
|
|
623
|
+
this.sendRaw({
|
|
624
|
+
type: 'media',
|
|
625
|
+
operation,
|
|
626
|
+
kind,
|
|
627
|
+
payload: payload ?? {},
|
|
628
|
+
requestId,
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
async switchMediaDevices(payload) {
|
|
633
|
+
const operations = [];
|
|
634
|
+
if (payload.audioInputId) {
|
|
635
|
+
operations.push(this.sendMedia('device', 'audio', { deviceId: payload.audioInputId }));
|
|
636
|
+
}
|
|
637
|
+
if (payload.videoInputId) {
|
|
638
|
+
operations.push(this.sendMedia('device', 'video', { deviceId: payload.videoInputId }));
|
|
639
|
+
}
|
|
640
|
+
if (payload.screenInputId) {
|
|
641
|
+
operations.push(this.sendMedia('device', 'screen', { deviceId: payload.screenInputId }));
|
|
642
|
+
}
|
|
643
|
+
await Promise.all(operations);
|
|
644
|
+
}
|
|
645
|
+
// ─── Private: Connection ───
|
|
646
|
+
async establishConnection() {
|
|
647
|
+
return new Promise((resolve, reject) => {
|
|
648
|
+
const wsUrl = this.buildWsUrl();
|
|
649
|
+
const ws = new WebSocket(wsUrl);
|
|
650
|
+
this.ws = ws;
|
|
651
|
+
ws.onopen = () => {
|
|
652
|
+
this.connected = true;
|
|
653
|
+
this.reconnectAttempts = 0;
|
|
654
|
+
this.startHeartbeat();
|
|
655
|
+
this.authenticate()
|
|
656
|
+
.then(() => {
|
|
657
|
+
this.waitingForAuth = false;
|
|
658
|
+
resolve();
|
|
659
|
+
})
|
|
660
|
+
.catch((error) => {
|
|
661
|
+
this.handleAuthenticationFailure(error);
|
|
662
|
+
reject(error);
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
ws.onmessage = (event) => {
|
|
666
|
+
this.handleMessage(event.data);
|
|
667
|
+
};
|
|
668
|
+
ws.onclose = (event) => {
|
|
669
|
+
this.connected = false;
|
|
670
|
+
this.authenticated = false;
|
|
671
|
+
this.joined = false;
|
|
672
|
+
this.ws = null;
|
|
673
|
+
this.stopHeartbeat();
|
|
674
|
+
if (event.code === 4004 && this.connectionState !== 'kicked') {
|
|
675
|
+
this.handleKicked();
|
|
676
|
+
}
|
|
677
|
+
if (!this.intentionallyLeft &&
|
|
678
|
+
!this.waitingForAuth &&
|
|
679
|
+
this.options.autoReconnect &&
|
|
680
|
+
this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
681
|
+
this.scheduleReconnect();
|
|
682
|
+
}
|
|
683
|
+
else if (!this.intentionallyLeft && this.connectionState !== 'kicked' && this.connectionState !== 'auth_lost') {
|
|
684
|
+
this.setConnectionState('disconnected');
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
ws.onerror = () => {
|
|
688
|
+
reject(new EdgeBaseError(500, 'Room WebSocket connection error'));
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
ensureConnection() {
|
|
693
|
+
if (this.connectingPromise) {
|
|
694
|
+
return this.connectingPromise;
|
|
695
|
+
}
|
|
696
|
+
const nextPromise = this.establishConnection().finally(() => {
|
|
697
|
+
if (this.connectingPromise === nextPromise) {
|
|
698
|
+
this.connectingPromise = null;
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
this.connectingPromise = nextPromise;
|
|
702
|
+
return nextPromise;
|
|
703
|
+
}
|
|
704
|
+
async authenticate() {
|
|
705
|
+
const token = await this.tokenManager.getAccessToken((refreshToken) => refreshAccessToken(this.baseUrl, refreshToken));
|
|
706
|
+
if (!token) {
|
|
707
|
+
throw new EdgeBaseError(401, 'No access token available. Sign in first.');
|
|
708
|
+
}
|
|
709
|
+
return new Promise((resolve, reject) => {
|
|
710
|
+
const timeout = setTimeout(() => {
|
|
711
|
+
reject(new EdgeBaseError(401, 'Room auth timeout'));
|
|
712
|
+
}, 10000);
|
|
713
|
+
const originalOnMessage = this.ws?.onmessage;
|
|
714
|
+
if (this.ws) {
|
|
715
|
+
this.ws.onmessage = (event) => {
|
|
716
|
+
const msg = JSON.parse(event.data);
|
|
717
|
+
if (msg.type === 'auth_success' || msg.type === 'auth_refreshed') {
|
|
718
|
+
clearTimeout(timeout);
|
|
719
|
+
this.authenticated = true;
|
|
720
|
+
this.currentUserId = typeof msg.userId === 'string' ? msg.userId : this.currentUserId;
|
|
721
|
+
this.currentConnectionId = typeof msg.connectionId === 'string' ? msg.connectionId : this.currentConnectionId;
|
|
722
|
+
if (this.ws)
|
|
723
|
+
this.ws.onmessage = originalOnMessage ?? null;
|
|
724
|
+
// Send join message with last known state for eviction recovery
|
|
725
|
+
this.sendRaw({
|
|
726
|
+
type: 'join',
|
|
727
|
+
lastSharedState: this._sharedState,
|
|
728
|
+
lastSharedVersion: this._sharedVersion,
|
|
729
|
+
lastPlayerState: this._playerState,
|
|
730
|
+
lastPlayerVersion: this._playerVersion,
|
|
731
|
+
});
|
|
732
|
+
this.joined = true;
|
|
733
|
+
resolve();
|
|
734
|
+
}
|
|
735
|
+
else if (msg.type === 'error') {
|
|
736
|
+
clearTimeout(timeout);
|
|
737
|
+
reject(new EdgeBaseError(401, msg.message));
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
this.sendRaw({ type: 'auth', token });
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
// ─── Private: Message Handling ───
|
|
745
|
+
handleMessage(raw) {
|
|
746
|
+
let msg;
|
|
747
|
+
try {
|
|
748
|
+
msg = JSON.parse(raw);
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const type = msg.type;
|
|
754
|
+
switch (type) {
|
|
755
|
+
case 'auth_success':
|
|
756
|
+
case 'auth_refreshed':
|
|
757
|
+
this.handleAuthAck(msg);
|
|
758
|
+
break;
|
|
759
|
+
case 'sync':
|
|
760
|
+
this.handleSync(msg);
|
|
761
|
+
break;
|
|
762
|
+
case 'shared_delta':
|
|
763
|
+
this.handleSharedDelta(msg);
|
|
764
|
+
break;
|
|
765
|
+
case 'player_delta':
|
|
766
|
+
this.handlePlayerDelta(msg);
|
|
767
|
+
break;
|
|
768
|
+
case 'action_result':
|
|
769
|
+
this.handleActionResult(msg);
|
|
770
|
+
break;
|
|
771
|
+
case 'action_error':
|
|
772
|
+
this.handleActionError(msg);
|
|
773
|
+
break;
|
|
774
|
+
case 'message':
|
|
775
|
+
this.handleServerMessage(msg);
|
|
776
|
+
break;
|
|
777
|
+
case 'signal':
|
|
778
|
+
this.handleSignalFrame(msg);
|
|
779
|
+
break;
|
|
780
|
+
case 'signal_sent':
|
|
781
|
+
this.handleSignalSent(msg);
|
|
782
|
+
break;
|
|
783
|
+
case 'signal_error':
|
|
784
|
+
this.handleSignalError(msg);
|
|
785
|
+
break;
|
|
786
|
+
case 'members_sync':
|
|
787
|
+
this.handleMembersSync(msg);
|
|
788
|
+
break;
|
|
789
|
+
case 'media_sync':
|
|
790
|
+
this.handleMediaSync(msg);
|
|
791
|
+
break;
|
|
792
|
+
case 'member_join':
|
|
793
|
+
this.handleMemberJoinFrame(msg);
|
|
794
|
+
break;
|
|
795
|
+
case 'member_leave':
|
|
796
|
+
this.handleMemberLeaveFrame(msg);
|
|
797
|
+
break;
|
|
798
|
+
case 'member_state':
|
|
799
|
+
this.handleMemberStateFrame(msg);
|
|
800
|
+
break;
|
|
801
|
+
case 'member_state_error':
|
|
802
|
+
this.handleMemberStateError(msg);
|
|
803
|
+
break;
|
|
804
|
+
case 'media_track':
|
|
805
|
+
this.handleMediaTrackFrame(msg);
|
|
806
|
+
break;
|
|
807
|
+
case 'media_track_removed':
|
|
808
|
+
this.handleMediaTrackRemovedFrame(msg);
|
|
809
|
+
break;
|
|
810
|
+
case 'media_state':
|
|
811
|
+
this.handleMediaStateFrame(msg);
|
|
812
|
+
break;
|
|
813
|
+
case 'media_device':
|
|
814
|
+
this.handleMediaDeviceFrame(msg);
|
|
815
|
+
break;
|
|
816
|
+
case 'media_result':
|
|
817
|
+
this.handleMediaResult(msg);
|
|
818
|
+
break;
|
|
819
|
+
case 'media_error':
|
|
820
|
+
this.handleMediaError(msg);
|
|
821
|
+
break;
|
|
822
|
+
case 'admin_result':
|
|
823
|
+
this.handleAdminResult(msg);
|
|
824
|
+
break;
|
|
825
|
+
case 'admin_error':
|
|
826
|
+
this.handleAdminError(msg);
|
|
827
|
+
break;
|
|
828
|
+
case 'kicked':
|
|
829
|
+
this.handleKicked();
|
|
830
|
+
break;
|
|
831
|
+
case 'error':
|
|
832
|
+
this.handleError(msg);
|
|
833
|
+
break;
|
|
834
|
+
case 'pong':
|
|
835
|
+
// Heartbeat response — no action needed
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
handleSync(msg) {
|
|
840
|
+
this._sharedState = msg.sharedState;
|
|
841
|
+
this._sharedVersion = msg.sharedVersion;
|
|
842
|
+
this._playerState = msg.playerState;
|
|
843
|
+
this._playerVersion = msg.playerVersion;
|
|
844
|
+
const reconnectInfo = this.reconnectInfo;
|
|
845
|
+
this.reconnectInfo = null;
|
|
846
|
+
this.setConnectionState('connected');
|
|
847
|
+
// Notify handlers with full state as changes
|
|
848
|
+
const sharedSnapshot = cloneRecord(this._sharedState);
|
|
849
|
+
const playerSnapshot = cloneRecord(this._playerState);
|
|
850
|
+
for (const handler of this.sharedStateHandlers) {
|
|
851
|
+
handler(sharedSnapshot, cloneRecord(sharedSnapshot));
|
|
852
|
+
}
|
|
853
|
+
for (const handler of this.playerStateHandlers) {
|
|
854
|
+
handler(playerSnapshot, cloneRecord(playerSnapshot));
|
|
855
|
+
}
|
|
856
|
+
if (reconnectInfo) {
|
|
857
|
+
for (const handler of this.reconnectHandlers) {
|
|
858
|
+
handler(reconnectInfo);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
handleSharedDelta(msg) {
|
|
863
|
+
const delta = msg.delta;
|
|
864
|
+
this._sharedVersion = msg.version;
|
|
865
|
+
// Apply delta to local state
|
|
866
|
+
for (const [path, value] of Object.entries(delta)) {
|
|
867
|
+
deepSet(this._sharedState, path, value);
|
|
868
|
+
}
|
|
869
|
+
const sharedSnapshot = cloneRecord(this._sharedState);
|
|
870
|
+
const deltaSnapshot = cloneRecord(delta);
|
|
871
|
+
for (const handler of this.sharedStateHandlers) {
|
|
872
|
+
handler(sharedSnapshot, deltaSnapshot);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
handlePlayerDelta(msg) {
|
|
876
|
+
const delta = msg.delta;
|
|
877
|
+
this._playerVersion = msg.version;
|
|
878
|
+
// Apply delta to local player state
|
|
879
|
+
for (const [path, value] of Object.entries(delta)) {
|
|
880
|
+
deepSet(this._playerState, path, value);
|
|
881
|
+
}
|
|
882
|
+
const playerSnapshot = cloneRecord(this._playerState);
|
|
883
|
+
const deltaSnapshot = cloneRecord(delta);
|
|
884
|
+
for (const handler of this.playerStateHandlers) {
|
|
885
|
+
handler(playerSnapshot, deltaSnapshot);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
handleActionResult(msg) {
|
|
889
|
+
const requestId = msg.requestId;
|
|
890
|
+
const pending = this.pendingRequests.get(requestId);
|
|
891
|
+
if (pending) {
|
|
892
|
+
clearTimeout(pending.timeout);
|
|
893
|
+
this.pendingRequests.delete(requestId);
|
|
894
|
+
pending.resolve(msg.result);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
handleActionError(msg) {
|
|
898
|
+
const requestId = msg.requestId;
|
|
899
|
+
const pending = this.pendingRequests.get(requestId);
|
|
900
|
+
if (pending) {
|
|
901
|
+
clearTimeout(pending.timeout);
|
|
902
|
+
this.pendingRequests.delete(requestId);
|
|
903
|
+
pending.reject(new EdgeBaseError(400, msg.message));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
handleAuthAck(msg) {
|
|
907
|
+
this.currentUserId = typeof msg.userId === 'string' ? msg.userId : this.currentUserId;
|
|
908
|
+
this.currentConnectionId =
|
|
909
|
+
typeof msg.connectionId === 'string' ? msg.connectionId : this.currentConnectionId;
|
|
910
|
+
}
|
|
911
|
+
handleServerMessage(msg) {
|
|
912
|
+
const messageType = msg.messageType;
|
|
913
|
+
const data = msg.data;
|
|
914
|
+
// Type-specific handlers
|
|
915
|
+
const handlers = this.messageHandlers.get(messageType);
|
|
916
|
+
if (handlers) {
|
|
917
|
+
for (const handler of handlers)
|
|
918
|
+
handler(data);
|
|
919
|
+
}
|
|
920
|
+
// All-message handlers
|
|
921
|
+
for (const handler of this.allMessageHandlers) {
|
|
922
|
+
handler(messageType, data);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
handleSignalFrame(msg) {
|
|
926
|
+
const event = typeof msg.event === 'string' ? msg.event : '';
|
|
927
|
+
if (!event)
|
|
928
|
+
return;
|
|
929
|
+
const meta = this.normalizeSignalMeta(msg.meta);
|
|
930
|
+
const payload = msg.payload;
|
|
931
|
+
const handlers = this.signalHandlers.get(event);
|
|
932
|
+
if (handlers) {
|
|
933
|
+
for (const handler of handlers)
|
|
934
|
+
handler(payload, meta);
|
|
935
|
+
}
|
|
936
|
+
for (const handler of this.anySignalHandlers) {
|
|
937
|
+
handler(event, payload, meta);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
handleSignalSent(msg) {
|
|
941
|
+
const requestId = msg.requestId;
|
|
942
|
+
if (!requestId)
|
|
943
|
+
return;
|
|
944
|
+
const pending = this.pendingSignalRequests.get(requestId);
|
|
945
|
+
if (!pending)
|
|
946
|
+
return;
|
|
947
|
+
clearTimeout(pending.timeout);
|
|
948
|
+
this.pendingSignalRequests.delete(requestId);
|
|
949
|
+
pending.resolve();
|
|
950
|
+
}
|
|
951
|
+
handleSignalError(msg) {
|
|
952
|
+
const requestId = msg.requestId;
|
|
953
|
+
if (!requestId)
|
|
954
|
+
return;
|
|
955
|
+
const pending = this.pendingSignalRequests.get(requestId);
|
|
956
|
+
if (!pending)
|
|
957
|
+
return;
|
|
958
|
+
clearTimeout(pending.timeout);
|
|
959
|
+
this.pendingSignalRequests.delete(requestId);
|
|
960
|
+
pending.reject(new EdgeBaseError(400, msg.message || 'Signal failed'));
|
|
961
|
+
}
|
|
962
|
+
handleMembersSync(msg) {
|
|
963
|
+
const members = this.normalizeMembers(msg.members);
|
|
964
|
+
this._members = members;
|
|
965
|
+
for (const member of members) {
|
|
966
|
+
this.syncMediaMemberInfo(member);
|
|
967
|
+
}
|
|
968
|
+
const snapshot = cloneValue(this._members);
|
|
969
|
+
for (const handler of this.memberSyncHandlers) {
|
|
970
|
+
handler(snapshot);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
handleMediaSync(msg) {
|
|
974
|
+
this._mediaMembers = this.normalizeMediaMembers(msg.members);
|
|
975
|
+
}
|
|
976
|
+
handleMemberJoinFrame(msg) {
|
|
977
|
+
const member = this.normalizeMember(msg.member);
|
|
978
|
+
if (!member)
|
|
979
|
+
return;
|
|
980
|
+
this.upsertMember(member);
|
|
981
|
+
this.syncMediaMemberInfo(member);
|
|
982
|
+
const snapshot = cloneValue(member);
|
|
983
|
+
for (const handler of this.memberJoinHandlers) {
|
|
984
|
+
handler(snapshot);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
handleMemberLeaveFrame(msg) {
|
|
988
|
+
const member = this.normalizeMember(msg.member);
|
|
989
|
+
if (!member)
|
|
990
|
+
return;
|
|
991
|
+
this.removeMember(member.memberId);
|
|
992
|
+
this.removeMediaMember(member.memberId);
|
|
993
|
+
const reason = this.normalizeLeaveReason(msg.reason);
|
|
994
|
+
const snapshot = cloneValue(member);
|
|
995
|
+
for (const handler of this.memberLeaveHandlers) {
|
|
996
|
+
handler(snapshot, reason);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
handleMemberStateFrame(msg) {
|
|
1000
|
+
const member = this.normalizeMember(msg.member);
|
|
1001
|
+
const state = this.normalizeState(msg.state);
|
|
1002
|
+
if (!member)
|
|
1003
|
+
return;
|
|
1004
|
+
member.state = state;
|
|
1005
|
+
this.upsertMember(member);
|
|
1006
|
+
this.syncMediaMemberInfo(member);
|
|
1007
|
+
const requestId = msg.requestId;
|
|
1008
|
+
if (requestId && member.memberId === this.currentUserId) {
|
|
1009
|
+
const pending = this.pendingMemberStateRequests.get(requestId);
|
|
1010
|
+
if (pending) {
|
|
1011
|
+
clearTimeout(pending.timeout);
|
|
1012
|
+
this.pendingMemberStateRequests.delete(requestId);
|
|
1013
|
+
pending.resolve();
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const memberSnapshot = cloneValue(member);
|
|
1017
|
+
const stateSnapshot = cloneRecord(state);
|
|
1018
|
+
for (const handler of this.memberStateHandlers) {
|
|
1019
|
+
handler(memberSnapshot, stateSnapshot);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
handleMemberStateError(msg) {
|
|
1023
|
+
const requestId = msg.requestId;
|
|
1024
|
+
if (!requestId)
|
|
1025
|
+
return;
|
|
1026
|
+
const pending = this.pendingMemberStateRequests.get(requestId);
|
|
1027
|
+
if (!pending)
|
|
1028
|
+
return;
|
|
1029
|
+
clearTimeout(pending.timeout);
|
|
1030
|
+
this.pendingMemberStateRequests.delete(requestId);
|
|
1031
|
+
pending.reject(new EdgeBaseError(400, msg.message || 'Member state update failed'));
|
|
1032
|
+
}
|
|
1033
|
+
handleMediaTrackFrame(msg) {
|
|
1034
|
+
const member = this.normalizeMember(msg.member);
|
|
1035
|
+
const track = this.normalizeMediaTrack(msg.track);
|
|
1036
|
+
if (!member || !track)
|
|
1037
|
+
return;
|
|
1038
|
+
const mediaMember = this.ensureMediaMember(member);
|
|
1039
|
+
this.upsertMediaTrack(mediaMember, track);
|
|
1040
|
+
this.mergeMediaState(mediaMember, track.kind, {
|
|
1041
|
+
published: true,
|
|
1042
|
+
muted: track.muted,
|
|
1043
|
+
trackId: track.trackId,
|
|
1044
|
+
deviceId: track.deviceId,
|
|
1045
|
+
publishedAt: track.publishedAt,
|
|
1046
|
+
adminDisabled: track.adminDisabled,
|
|
1047
|
+
providerSessionId: track.providerSessionId,
|
|
1048
|
+
});
|
|
1049
|
+
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1050
|
+
const trackSnapshot = cloneValue(track);
|
|
1051
|
+
for (const handler of this.mediaTrackHandlers) {
|
|
1052
|
+
handler(trackSnapshot, memberSnapshot);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
handleMediaTrackRemovedFrame(msg) {
|
|
1056
|
+
const member = this.normalizeMember(msg.member);
|
|
1057
|
+
const track = this.normalizeMediaTrack(msg.track);
|
|
1058
|
+
if (!member || !track)
|
|
1059
|
+
return;
|
|
1060
|
+
const mediaMember = this.ensureMediaMember(member);
|
|
1061
|
+
this.removeMediaTrack(mediaMember, track);
|
|
1062
|
+
mediaMember.state = {
|
|
1063
|
+
...mediaMember.state,
|
|
1064
|
+
[track.kind]: {
|
|
1065
|
+
published: false,
|
|
1066
|
+
muted: false,
|
|
1067
|
+
adminDisabled: false,
|
|
1068
|
+
providerSessionId: undefined,
|
|
1069
|
+
},
|
|
1070
|
+
};
|
|
1071
|
+
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1072
|
+
const trackSnapshot = cloneValue(track);
|
|
1073
|
+
for (const handler of this.mediaTrackRemovedHandlers) {
|
|
1074
|
+
handler(trackSnapshot, memberSnapshot);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
handleMediaStateFrame(msg) {
|
|
1078
|
+
const member = this.normalizeMember(msg.member);
|
|
1079
|
+
if (!member)
|
|
1080
|
+
return;
|
|
1081
|
+
const mediaMember = this.ensureMediaMember(member);
|
|
1082
|
+
mediaMember.state = this.normalizeMediaState(msg.state);
|
|
1083
|
+
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1084
|
+
const stateSnapshot = cloneValue(mediaMember.state);
|
|
1085
|
+
for (const handler of this.mediaStateHandlers) {
|
|
1086
|
+
handler(memberSnapshot, stateSnapshot);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
handleMediaDeviceFrame(msg) {
|
|
1090
|
+
const member = this.normalizeMember(msg.member);
|
|
1091
|
+
const kind = this.normalizeMediaKind(msg.kind);
|
|
1092
|
+
const deviceId = typeof msg.deviceId === 'string' ? msg.deviceId : '';
|
|
1093
|
+
if (!member || !kind || !deviceId)
|
|
1094
|
+
return;
|
|
1095
|
+
const mediaMember = this.ensureMediaMember(member);
|
|
1096
|
+
this.mergeMediaState(mediaMember, kind, { deviceId });
|
|
1097
|
+
for (const track of mediaMember.tracks) {
|
|
1098
|
+
if (track.kind === kind) {
|
|
1099
|
+
track.deviceId = deviceId;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1103
|
+
const change = { kind, deviceId };
|
|
1104
|
+
for (const handler of this.mediaDeviceHandlers) {
|
|
1105
|
+
handler(memberSnapshot, change);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
handleMediaResult(msg) {
|
|
1109
|
+
const requestId = msg.requestId;
|
|
1110
|
+
if (!requestId)
|
|
1111
|
+
return;
|
|
1112
|
+
const pending = this.pendingMediaRequests.get(requestId);
|
|
1113
|
+
if (!pending)
|
|
1114
|
+
return;
|
|
1115
|
+
clearTimeout(pending.timeout);
|
|
1116
|
+
this.pendingMediaRequests.delete(requestId);
|
|
1117
|
+
pending.resolve();
|
|
1118
|
+
}
|
|
1119
|
+
handleMediaError(msg) {
|
|
1120
|
+
const requestId = msg.requestId;
|
|
1121
|
+
if (!requestId)
|
|
1122
|
+
return;
|
|
1123
|
+
const pending = this.pendingMediaRequests.get(requestId);
|
|
1124
|
+
if (!pending)
|
|
1125
|
+
return;
|
|
1126
|
+
clearTimeout(pending.timeout);
|
|
1127
|
+
this.pendingMediaRequests.delete(requestId);
|
|
1128
|
+
pending.reject(new EdgeBaseError(400, msg.message || 'Media operation failed'));
|
|
1129
|
+
}
|
|
1130
|
+
handleAdminResult(msg) {
|
|
1131
|
+
const requestId = msg.requestId;
|
|
1132
|
+
if (!requestId)
|
|
1133
|
+
return;
|
|
1134
|
+
const pending = this.pendingAdminRequests.get(requestId);
|
|
1135
|
+
if (!pending)
|
|
1136
|
+
return;
|
|
1137
|
+
clearTimeout(pending.timeout);
|
|
1138
|
+
this.pendingAdminRequests.delete(requestId);
|
|
1139
|
+
pending.resolve();
|
|
1140
|
+
}
|
|
1141
|
+
handleAdminError(msg) {
|
|
1142
|
+
const requestId = msg.requestId;
|
|
1143
|
+
if (!requestId)
|
|
1144
|
+
return;
|
|
1145
|
+
const pending = this.pendingAdminRequests.get(requestId);
|
|
1146
|
+
if (!pending)
|
|
1147
|
+
return;
|
|
1148
|
+
clearTimeout(pending.timeout);
|
|
1149
|
+
this.pendingAdminRequests.delete(requestId);
|
|
1150
|
+
pending.reject(new EdgeBaseError(400, msg.message || 'Admin operation failed'));
|
|
1151
|
+
}
|
|
1152
|
+
handleKicked() {
|
|
1153
|
+
for (const handler of this.kickedHandlers)
|
|
1154
|
+
handler();
|
|
1155
|
+
// Don't auto-reconnect after being kicked
|
|
1156
|
+
this.intentionallyLeft = true;
|
|
1157
|
+
this.reconnectInfo = null;
|
|
1158
|
+
this.setConnectionState('kicked');
|
|
1159
|
+
}
|
|
1160
|
+
handleError(msg) {
|
|
1161
|
+
for (const handler of this.errorHandlers) {
|
|
1162
|
+
handler({ code: msg.code, message: msg.message });
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
refreshAuth() {
|
|
1166
|
+
const token = this.tokenManager.currentAccessToken;
|
|
1167
|
+
if (!token || !this.ws || !this.connected)
|
|
1168
|
+
return;
|
|
1169
|
+
this.sendRaw({ type: 'auth', token });
|
|
1170
|
+
}
|
|
1171
|
+
handleAuthStateChange(user) {
|
|
1172
|
+
if (user) {
|
|
1173
|
+
if (this.ws && this.connected && this.authenticated) {
|
|
1174
|
+
this.refreshAuth();
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
this.waitingForAuth = false;
|
|
1178
|
+
if (this.joinRequested
|
|
1179
|
+
&& !this.connectingPromise
|
|
1180
|
+
&& !isSocketOpenOrConnecting(this.ws)) {
|
|
1181
|
+
this.reconnectAttempts = 0;
|
|
1182
|
+
this.ensureConnection().catch(() => {
|
|
1183
|
+
// Connection errors are surfaced through the normal socket lifecycle.
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
this.waitingForAuth = this.joinRequested;
|
|
1189
|
+
this.reconnectInfo = null;
|
|
1190
|
+
this.setConnectionState('auth_lost');
|
|
1191
|
+
if (this.ws) {
|
|
1192
|
+
const socket = this.ws;
|
|
1193
|
+
this.sendRaw({ type: 'leave' });
|
|
1194
|
+
this.stopHeartbeat();
|
|
1195
|
+
this.ws = null;
|
|
1196
|
+
this.connected = false;
|
|
1197
|
+
this.authenticated = false;
|
|
1198
|
+
this.joined = false;
|
|
1199
|
+
this._mediaMembers = [];
|
|
1200
|
+
this.currentUserId = null;
|
|
1201
|
+
this.currentConnectionId = null;
|
|
1202
|
+
try {
|
|
1203
|
+
closeSocketAfterLeave(socket, 'Signed out');
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
// Ignore close failures — socket is already unusable.
|
|
1207
|
+
}
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
this.connected = false;
|
|
1211
|
+
this.authenticated = false;
|
|
1212
|
+
this.joined = false;
|
|
1213
|
+
this._mediaMembers = [];
|
|
1214
|
+
}
|
|
1215
|
+
handleAuthenticationFailure(error) {
|
|
1216
|
+
const authError = error instanceof EdgeBaseError
|
|
1217
|
+
? error
|
|
1218
|
+
: new EdgeBaseError(500, 'Room authentication failed.');
|
|
1219
|
+
this.waitingForAuth = authError.code === 401 && this.joinRequested;
|
|
1220
|
+
this.stopHeartbeat();
|
|
1221
|
+
this.connected = false;
|
|
1222
|
+
this.authenticated = false;
|
|
1223
|
+
this.joined = false;
|
|
1224
|
+
this.connectingPromise = null;
|
|
1225
|
+
if (this.ws) {
|
|
1226
|
+
const socket = this.ws;
|
|
1227
|
+
this.ws = null;
|
|
1228
|
+
try {
|
|
1229
|
+
socket.close(4001, authError.message);
|
|
1230
|
+
}
|
|
1231
|
+
catch {
|
|
1232
|
+
// Ignore close failures — the server will time out stale sockets.
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
normalizeMembers(value) {
|
|
1237
|
+
if (!Array.isArray(value)) {
|
|
1238
|
+
return [];
|
|
1239
|
+
}
|
|
1240
|
+
return value
|
|
1241
|
+
.map((member) => this.normalizeMember(member))
|
|
1242
|
+
.filter((member) => !!member);
|
|
1243
|
+
}
|
|
1244
|
+
normalizeMediaMembers(value) {
|
|
1245
|
+
if (!Array.isArray(value)) {
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
return value
|
|
1249
|
+
.map((member) => this.normalizeMediaMember(member))
|
|
1250
|
+
.filter((member) => !!member);
|
|
1251
|
+
}
|
|
1252
|
+
normalizeMember(value) {
|
|
1253
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
const member = value;
|
|
1257
|
+
if (typeof member.memberId !== 'string' || typeof member.userId !== 'string') {
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
memberId: member.memberId,
|
|
1262
|
+
userId: member.userId,
|
|
1263
|
+
connectionId: typeof member.connectionId === 'string' ? member.connectionId : undefined,
|
|
1264
|
+
connectionCount: typeof member.connectionCount === 'number' ? member.connectionCount : undefined,
|
|
1265
|
+
role: typeof member.role === 'string' ? member.role : undefined,
|
|
1266
|
+
state: this.normalizeState(member.state),
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
normalizeState(value) {
|
|
1270
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1271
|
+
return {};
|
|
1272
|
+
}
|
|
1273
|
+
return cloneRecord(value);
|
|
1274
|
+
}
|
|
1275
|
+
normalizeMediaMember(value) {
|
|
1276
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
const entry = value;
|
|
1280
|
+
const member = this.normalizeMember(entry.member);
|
|
1281
|
+
if (!member) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
member,
|
|
1286
|
+
state: this.normalizeMediaState(entry.state),
|
|
1287
|
+
tracks: this.normalizeMediaTracks(entry.tracks),
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
normalizeMediaState(value) {
|
|
1291
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1292
|
+
return {};
|
|
1293
|
+
}
|
|
1294
|
+
const state = value;
|
|
1295
|
+
return {
|
|
1296
|
+
audio: this.normalizeMediaKindState(state.audio),
|
|
1297
|
+
video: this.normalizeMediaKindState(state.video),
|
|
1298
|
+
screen: this.normalizeMediaKindState(state.screen),
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
normalizeMediaKindState(value) {
|
|
1302
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1303
|
+
return undefined;
|
|
1304
|
+
}
|
|
1305
|
+
const state = value;
|
|
1306
|
+
return {
|
|
1307
|
+
published: state.published === true,
|
|
1308
|
+
muted: state.muted === true,
|
|
1309
|
+
trackId: typeof state.trackId === 'string' ? state.trackId : undefined,
|
|
1310
|
+
deviceId: typeof state.deviceId === 'string' ? state.deviceId : undefined,
|
|
1311
|
+
publishedAt: typeof state.publishedAt === 'number' ? state.publishedAt : undefined,
|
|
1312
|
+
adminDisabled: state.adminDisabled === true,
|
|
1313
|
+
providerSessionId: typeof state.providerSessionId === 'string' ? state.providerSessionId : undefined,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
normalizeMediaTracks(value) {
|
|
1317
|
+
if (!Array.isArray(value)) {
|
|
1318
|
+
return [];
|
|
1319
|
+
}
|
|
1320
|
+
return value
|
|
1321
|
+
.map((track) => this.normalizeMediaTrack(track))
|
|
1322
|
+
.filter((track) => !!track);
|
|
1323
|
+
}
|
|
1324
|
+
normalizeMediaTrack(value) {
|
|
1325
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
const track = value;
|
|
1329
|
+
const kind = this.normalizeMediaKind(track.kind);
|
|
1330
|
+
if (!kind) {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
kind,
|
|
1335
|
+
trackId: typeof track.trackId === 'string' ? track.trackId : undefined,
|
|
1336
|
+
deviceId: typeof track.deviceId === 'string' ? track.deviceId : undefined,
|
|
1337
|
+
muted: track.muted === true,
|
|
1338
|
+
publishedAt: typeof track.publishedAt === 'number' ? track.publishedAt : undefined,
|
|
1339
|
+
adminDisabled: track.adminDisabled === true,
|
|
1340
|
+
providerSessionId: typeof track.providerSessionId === 'string' ? track.providerSessionId : undefined,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
normalizeMediaKind(value) {
|
|
1344
|
+
switch (value) {
|
|
1345
|
+
case 'audio':
|
|
1346
|
+
case 'video':
|
|
1347
|
+
case 'screen':
|
|
1348
|
+
return value;
|
|
1349
|
+
default:
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
normalizeSignalMeta(value) {
|
|
1354
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1355
|
+
return {};
|
|
1356
|
+
}
|
|
1357
|
+
const meta = value;
|
|
1358
|
+
return {
|
|
1359
|
+
memberId: typeof meta.memberId === 'string' || meta.memberId === null ? meta.memberId : undefined,
|
|
1360
|
+
userId: typeof meta.userId === 'string' || meta.userId === null ? meta.userId : undefined,
|
|
1361
|
+
connectionId: typeof meta.connectionId === 'string' || meta.connectionId === null
|
|
1362
|
+
? meta.connectionId
|
|
1363
|
+
: undefined,
|
|
1364
|
+
sentAt: typeof meta.sentAt === 'number' ? meta.sentAt : undefined,
|
|
1365
|
+
serverSent: meta.serverSent === true,
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
normalizeLeaveReason(value) {
|
|
1369
|
+
switch (value) {
|
|
1370
|
+
case 'leave':
|
|
1371
|
+
case 'timeout':
|
|
1372
|
+
case 'kicked':
|
|
1373
|
+
return value;
|
|
1374
|
+
default:
|
|
1375
|
+
return 'leave';
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
upsertMember(member) {
|
|
1379
|
+
const index = this._members.findIndex((entry) => entry.memberId === member.memberId);
|
|
1380
|
+
if (index >= 0) {
|
|
1381
|
+
this._members[index] = cloneValue(member);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
this._members.push(cloneValue(member));
|
|
1385
|
+
}
|
|
1386
|
+
removeMember(memberId) {
|
|
1387
|
+
this._members = this._members.filter((member) => member.memberId !== memberId);
|
|
1388
|
+
}
|
|
1389
|
+
syncMediaMemberInfo(member) {
|
|
1390
|
+
const mediaMember = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
1391
|
+
if (!mediaMember) {
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
mediaMember.member = cloneValue(member);
|
|
1395
|
+
}
|
|
1396
|
+
ensureMediaMember(member) {
|
|
1397
|
+
const existing = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
1398
|
+
if (existing) {
|
|
1399
|
+
existing.member = cloneValue(member);
|
|
1400
|
+
return existing;
|
|
1401
|
+
}
|
|
1402
|
+
const created = {
|
|
1403
|
+
member: cloneValue(member),
|
|
1404
|
+
state: {},
|
|
1405
|
+
tracks: [],
|
|
1406
|
+
};
|
|
1407
|
+
this._mediaMembers.push(created);
|
|
1408
|
+
return created;
|
|
1409
|
+
}
|
|
1410
|
+
removeMediaMember(memberId) {
|
|
1411
|
+
this._mediaMembers = this._mediaMembers.filter((member) => member.member.memberId !== memberId);
|
|
1412
|
+
}
|
|
1413
|
+
upsertMediaTrack(mediaMember, track) {
|
|
1414
|
+
const index = mediaMember.tracks.findIndex((entry) => entry.kind === track.kind &&
|
|
1415
|
+
entry.trackId === track.trackId);
|
|
1416
|
+
if (index >= 0) {
|
|
1417
|
+
mediaMember.tracks[index] = cloneValue(track);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
mediaMember.tracks = mediaMember.tracks
|
|
1421
|
+
.filter((entry) => !(entry.kind === track.kind && !track.trackId))
|
|
1422
|
+
.concat(cloneValue(track));
|
|
1423
|
+
}
|
|
1424
|
+
removeMediaTrack(mediaMember, track) {
|
|
1425
|
+
mediaMember.tracks = mediaMember.tracks.filter((entry) => {
|
|
1426
|
+
if (track.trackId) {
|
|
1427
|
+
return !(entry.kind === track.kind && entry.trackId === track.trackId);
|
|
1428
|
+
}
|
|
1429
|
+
return entry.kind !== track.kind;
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
mergeMediaState(mediaMember, kind, partial) {
|
|
1433
|
+
const next = {
|
|
1434
|
+
published: partial.published ?? mediaMember.state[kind]?.published ?? false,
|
|
1435
|
+
muted: partial.muted ?? mediaMember.state[kind]?.muted ?? false,
|
|
1436
|
+
trackId: partial.trackId ?? mediaMember.state[kind]?.trackId,
|
|
1437
|
+
deviceId: partial.deviceId ?? mediaMember.state[kind]?.deviceId,
|
|
1438
|
+
publishedAt: partial.publishedAt ?? mediaMember.state[kind]?.publishedAt,
|
|
1439
|
+
adminDisabled: partial.adminDisabled ?? mediaMember.state[kind]?.adminDisabled,
|
|
1440
|
+
providerSessionId: partial.providerSessionId ?? mediaMember.state[kind]?.providerSessionId,
|
|
1441
|
+
};
|
|
1442
|
+
mediaMember.state = {
|
|
1443
|
+
...mediaMember.state,
|
|
1444
|
+
[kind]: next,
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
rejectPendingVoidRequests(pendingRequests, error) {
|
|
1448
|
+
for (const [, pending] of pendingRequests) {
|
|
1449
|
+
clearTimeout(pending.timeout);
|
|
1450
|
+
pending.reject(error);
|
|
1451
|
+
}
|
|
1452
|
+
pendingRequests.clear();
|
|
1453
|
+
}
|
|
1454
|
+
setConnectionState(next) {
|
|
1455
|
+
if (this.connectionState === next) {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
this.connectionState = next;
|
|
1459
|
+
for (const handler of this.connectionStateHandlers) {
|
|
1460
|
+
handler(next);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
// ─── Private: Helpers ───
|
|
1464
|
+
sendRaw(data) {
|
|
1465
|
+
if (this.ws && this.connected) {
|
|
1466
|
+
this.ws.send(JSON.stringify(data));
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
buildWsUrl() {
|
|
1471
|
+
const httpUrl = this.baseUrl.replace(/\/$/, '');
|
|
1472
|
+
const wsUrl = httpUrl.replace(/^http/, 'ws');
|
|
1473
|
+
return `${wsUrl}/api/room?namespace=${encodeURIComponent(this.namespace)}&id=${encodeURIComponent(this.roomId)}`;
|
|
1474
|
+
}
|
|
1475
|
+
scheduleReconnect() {
|
|
1476
|
+
const attempt = this.reconnectAttempts + 1;
|
|
1477
|
+
const delay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
|
|
1478
|
+
this.reconnectAttempts++;
|
|
1479
|
+
this.reconnectInfo = { attempt };
|
|
1480
|
+
this.setConnectionState('reconnecting');
|
|
1481
|
+
setTimeout(() => {
|
|
1482
|
+
if (this.connectingPromise
|
|
1483
|
+
|| !this.joinRequested
|
|
1484
|
+
|| this.waitingForAuth
|
|
1485
|
+
|| isSocketOpenOrConnecting(this.ws)) {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
this.ensureConnection().catch(() => { });
|
|
1489
|
+
}, Math.min(delay, 30000));
|
|
1490
|
+
}
|
|
1491
|
+
startHeartbeat() {
|
|
1492
|
+
this.stopHeartbeat();
|
|
1493
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1494
|
+
if (this.ws && this.connected) {
|
|
1495
|
+
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
1496
|
+
}
|
|
1497
|
+
}, 30000);
|
|
1498
|
+
}
|
|
1499
|
+
stopHeartbeat() {
|
|
1500
|
+
if (this.heartbeatTimer) {
|
|
1501
|
+
clearInterval(this.heartbeatTimer);
|
|
1502
|
+
this.heartbeatTimer = null;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
//# sourceMappingURL=room.js.map
|