@agatx/serenada-core 0.6.10
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/dist/ConsoleLogger.d.ts +6 -0
- package/dist/ConsoleLogger.d.ts.map +1 -0
- package/dist/ConsoleLogger.js +21 -0
- package/dist/ConsoleLogger.js.map +1 -0
- package/dist/RoomWatcher.d.ts +34 -0
- package/dist/RoomWatcher.d.ts.map +1 -0
- package/dist/RoomWatcher.js +103 -0
- package/dist/RoomWatcher.js.map +1 -0
- package/dist/SerenadaCore.d.ts +47 -0
- package/dist/SerenadaCore.d.ts.map +1 -0
- package/dist/SerenadaCore.js +141 -0
- package/dist/SerenadaCore.js.map +1 -0
- package/dist/SerenadaDiagnostics.d.ts +49 -0
- package/dist/SerenadaDiagnostics.d.ts.map +1 -0
- package/dist/SerenadaDiagnostics.js +421 -0
- package/dist/SerenadaDiagnostics.js.map +1 -0
- package/dist/SerenadaServerProvider.d.ts +48 -0
- package/dist/SerenadaServerProvider.d.ts.map +1 -0
- package/dist/SerenadaServerProvider.js +296 -0
- package/dist/SerenadaServerProvider.js.map +1 -0
- package/dist/SerenadaSession.d.ts +180 -0
- package/dist/SerenadaSession.d.ts.map +1 -0
- package/dist/SerenadaSession.js +1082 -0
- package/dist/SerenadaSession.js.map +1 -0
- package/dist/SignalingProvider.d.ts +132 -0
- package/dist/SignalingProvider.d.ts.map +1 -0
- package/dist/SignalingProvider.js +50 -0
- package/dist/SignalingProvider.js.map +1 -0
- package/dist/api/roomApi.d.ts +2 -0
- package/dist/api/roomApi.d.ts.map +1 -0
- package/dist/api/roomApi.js +14 -0
- package/dist/api/roomApi.js.map +1 -0
- package/dist/cameraModes.d.ts +13 -0
- package/dist/cameraModes.d.ts.map +1 -0
- package/dist/cameraModes.js +35 -0
- package/dist/cameraModes.js.map +1 -0
- package/dist/configValidation.d.ts +10 -0
- package/dist/configValidation.d.ts.map +1 -0
- package/dist/configValidation.js +24 -0
- package/dist/configValidation.js.map +1 -0
- package/dist/constants.d.ts +33 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +65 -0
- package/dist/constants.js.map +1 -0
- package/dist/formatError.d.ts +3 -0
- package/dist/formatError.d.ts.map +1 -0
- package/dist/formatError.js +7 -0
- package/dist/formatError.js.map +1 -0
- package/dist/iceServers.d.ts +2 -0
- package/dist/iceServers.d.ts.map +1 -0
- package/dist/iceServers.js +21 -0
- package/dist/iceServers.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/computeLayout.d.ts +81 -0
- package/dist/layout/computeLayout.d.ts.map +1 -0
- package/dist/layout/computeLayout.js +380 -0
- package/dist/layout/computeLayout.js.map +1 -0
- package/dist/media/AudioLevelMonitor.d.ts +51 -0
- package/dist/media/AudioLevelMonitor.d.ts.map +1 -0
- package/dist/media/AudioLevelMonitor.js +179 -0
- package/dist/media/AudioLevelMonitor.js.map +1 -0
- package/dist/media/MediaEngine.d.ts +137 -0
- package/dist/media/MediaEngine.d.ts.map +1 -0
- package/dist/media/MediaEngine.js +1224 -0
- package/dist/media/MediaEngine.js.map +1 -0
- package/dist/media/callStats.d.ts +16 -0
- package/dist/media/callStats.d.ts.map +1 -0
- package/dist/media/callStats.js +214 -0
- package/dist/media/callStats.js.map +1 -0
- package/dist/media/localVideoRecovery.d.ts +16 -0
- package/dist/media/localVideoRecovery.d.ts.map +1 -0
- package/dist/media/localVideoRecovery.js +14 -0
- package/dist/media/localVideoRecovery.js.map +1 -0
- package/dist/recoveryStorage.d.ts +33 -0
- package/dist/recoveryStorage.d.ts.map +1 -0
- package/dist/recoveryStorage.js +88 -0
- package/dist/recoveryStorage.js.map +1 -0
- package/dist/serverUrls.d.ts +8 -0
- package/dist/serverUrls.d.ts.map +1 -0
- package/dist/serverUrls.js +65 -0
- package/dist/serverUrls.js.map +1 -0
- package/dist/signaling/SignalingEngine.d.ts +126 -0
- package/dist/signaling/SignalingEngine.d.ts.map +1 -0
- package/dist/signaling/SignalingEngine.js +720 -0
- package/dist/signaling/SignalingEngine.js.map +1 -0
- package/dist/signaling/payloads.d.ts +76 -0
- package/dist/signaling/payloads.d.ts.map +1 -0
- package/dist/signaling/payloads.js +160 -0
- package/dist/signaling/payloads.js.map +1 -0
- package/dist/signaling/roomStatuses.d.ts +9 -0
- package/dist/signaling/roomStatuses.d.ts.map +1 -0
- package/dist/signaling/roomStatuses.js +71 -0
- package/dist/signaling/roomStatuses.js.map +1 -0
- package/dist/signaling/transportConfig.d.ts +3 -0
- package/dist/signaling/transportConfig.d.ts.map +1 -0
- package/dist/signaling/transportConfig.js +27 -0
- package/dist/signaling/transportConfig.js.map +1 -0
- package/dist/signaling/transports/index.d.ts +13 -0
- package/dist/signaling/transports/index.d.ts.map +1 -0
- package/dist/signaling/transports/index.js +11 -0
- package/dist/signaling/transports/index.js.map +1 -0
- package/dist/signaling/transports/sse.d.ts +26 -0
- package/dist/signaling/transports/sse.d.ts.map +1 -0
- package/dist/signaling/transports/sse.js +131 -0
- package/dist/signaling/transports/sse.js.map +1 -0
- package/dist/signaling/transports/types.d.ts +17 -0
- package/dist/signaling/transports/types.d.ts.map +1 -0
- package/dist/signaling/transports/types.js +2 -0
- package/dist/signaling/transports/types.js.map +1 -0
- package/dist/signaling/transports/ws.d.ts +21 -0
- package/dist/signaling/transports/ws.d.ts.map +1 -0
- package/dist/signaling/transports/ws.js +93 -0
- package/dist/signaling/transports/ws.js.map +1 -0
- package/dist/signaling/types.d.ts +53 -0
- package/dist/signaling/types.d.ts.map +1 -0
- package/dist/signaling/types.js +2 -0
- package/dist/signaling/types.js.map +1 -0
- package/dist/types.d.ts +279 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/ConsoleLogger.ts +14 -0
- package/src/RoomWatcher.ts +127 -0
- package/src/SerenadaCore.ts +163 -0
- package/src/SerenadaDiagnostics.ts +485 -0
- package/src/SerenadaServerProvider.ts +362 -0
- package/src/SerenadaSession.ts +1258 -0
- package/src/SignalingProvider.ts +207 -0
- package/src/api/roomApi.ts +16 -0
- package/src/cameraModes.ts +34 -0
- package/src/configValidation.ts +35 -0
- package/src/constants.ts +77 -0
- package/src/formatError.ts +5 -0
- package/src/iceServers.ts +20 -0
- package/src/index.ts +155 -0
- package/src/layout/computeLayout.ts +639 -0
- package/src/media/AudioLevelMonitor.ts +190 -0
- package/src/media/MediaEngine.ts +1183 -0
- package/src/media/callStats.ts +260 -0
- package/src/media/localVideoRecovery.ts +39 -0
- package/src/recoveryStorage.ts +101 -0
- package/src/serverUrls.ts +69 -0
- package/src/signaling/SignalingEngine.ts +762 -0
- package/src/signaling/payloads.ts +215 -0
- package/src/signaling/roomStatuses.ts +89 -0
- package/src/signaling/transportConfig.ts +30 -0
- package/src/signaling/transports/index.ts +26 -0
- package/src/signaling/transports/sse.ts +146 -0
- package/src/signaling/transports/types.ts +19 -0
- package/src/signaling/transports/ws.ts +108 -0
- package/src/signaling/types.ts +68 -0
- package/src/types.ts +299 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import { createSignalingTransport } from './transports/index.js';
|
|
2
|
+
import { mergeRoomStatusesPayload, mergeRoomStatusUpdatePayload } from './roomStatuses.js';
|
|
3
|
+
import { parseJoinedPayload, parseRoomStatePayload, parseErrorPayload, parseTurnRefreshedPayload, parseRelayFailedPayload, parseNegotiationDirtyPayload, } from './payloads.js';
|
|
4
|
+
import { formatError } from '../formatError.js';
|
|
5
|
+
import { saveRecoveryRecord, clearRecoveryRecord } from '../recoveryStorage.js';
|
|
6
|
+
import { RECONNECT_BACKOFF_BASE_MS, RECONNECT_BACKOFF_CAP_MS, PING_INTERVAL_MS, PONG_MISS_THRESHOLD, WS_FALLBACK_CONSECUTIVE_FAILURES, JOIN_CONNECT_KICKSTART_MS, JOIN_RECOVERY_MS, JOIN_HARD_TIMEOUT_MS, TURN_REFRESH_TRIGGER_RATIO, SUSPEND_HARD_EVICTION_TIMEOUT_MS, } from '../constants.js';
|
|
7
|
+
export class SignalingEngine {
|
|
8
|
+
// Public state
|
|
9
|
+
isConnected = false;
|
|
10
|
+
activeTransport = null;
|
|
11
|
+
clientId = null;
|
|
12
|
+
roomState = null;
|
|
13
|
+
turnToken = null;
|
|
14
|
+
turnTokenTTLMs = null;
|
|
15
|
+
error = null;
|
|
16
|
+
roomStatuses = {};
|
|
17
|
+
/** Most-recent reconnect outcome reported by the server in `joined`. */
|
|
18
|
+
lastReconnectOutcome = null;
|
|
19
|
+
/**
|
|
20
|
+
* Highest room-state epoch observed so far. Advances monotonically (per
|
|
21
|
+
* room) on every membership change. Used by MediaEngine to gate ICE
|
|
22
|
+
* restart on an authoritative post-reconnect snapshot.
|
|
23
|
+
*/
|
|
24
|
+
lastEpoch = null;
|
|
25
|
+
/** Epoch at the moment the transport was last observed disconnected. */
|
|
26
|
+
epochAtDisconnect = null;
|
|
27
|
+
/**
|
|
28
|
+
* Unix-ms timestamp of the first successful `joined` for the current
|
|
29
|
+
* session. Stable across reconnects so the persisted recovery record
|
|
30
|
+
* carries the original join time, not the latest reattach time.
|
|
31
|
+
*/
|
|
32
|
+
sessionStartTs = null;
|
|
33
|
+
/**
|
|
34
|
+
* True from disconnect until we've seen a fresh authoritative snapshot
|
|
35
|
+
* for the current room on the new transport. Consumers should suppress
|
|
36
|
+
* ICE restart while this flag is set.
|
|
37
|
+
*/
|
|
38
|
+
awaitingPostReconnectSnapshot = false;
|
|
39
|
+
// Config
|
|
40
|
+
wsUrl;
|
|
41
|
+
httpBaseUrl;
|
|
42
|
+
transportOrder;
|
|
43
|
+
// Internal state
|
|
44
|
+
transport = null;
|
|
45
|
+
transportIndex = 0;
|
|
46
|
+
transportConnectedOnce = { ws: false, sse: false };
|
|
47
|
+
transportId = 0;
|
|
48
|
+
currentRoomId = null;
|
|
49
|
+
pendingJoin = null;
|
|
50
|
+
lastClientId = null;
|
|
51
|
+
needsRejoin = false;
|
|
52
|
+
reconnectToken = null;
|
|
53
|
+
reconnectTokenRoomId = null;
|
|
54
|
+
lastPongAt = Date.now();
|
|
55
|
+
missedPongs = 0;
|
|
56
|
+
wsConsecutiveFailures = 0;
|
|
57
|
+
sseSid = null;
|
|
58
|
+
joinAttemptId = 0;
|
|
59
|
+
joinAcked = false;
|
|
60
|
+
joinKickstartTimer = null;
|
|
61
|
+
joinRecoveryTimer = null;
|
|
62
|
+
joinHardTimeout = null;
|
|
63
|
+
turnRefreshTimer = null;
|
|
64
|
+
pingInterval = null;
|
|
65
|
+
reconnectTimeout = null;
|
|
66
|
+
reconnectAttempts = 0;
|
|
67
|
+
closedByDestroy = false;
|
|
68
|
+
connecting = false;
|
|
69
|
+
lastCreateMaxParticipants = undefined;
|
|
70
|
+
lastDisplayName = undefined;
|
|
71
|
+
lastPeerId = undefined;
|
|
72
|
+
// Logger
|
|
73
|
+
logger;
|
|
74
|
+
// Listeners
|
|
75
|
+
messageListeners = [];
|
|
76
|
+
stateListeners = [];
|
|
77
|
+
constructor(config) {
|
|
78
|
+
this.wsUrl = config.wsUrl;
|
|
79
|
+
this.httpBaseUrl = config.httpBaseUrl;
|
|
80
|
+
this.transportOrder = config.transports ?? ['ws', 'sse'];
|
|
81
|
+
this.logger = config.logger;
|
|
82
|
+
this.loadReconnectStorage();
|
|
83
|
+
}
|
|
84
|
+
connect() {
|
|
85
|
+
this.closedByDestroy = false;
|
|
86
|
+
this.transportIndex = 0;
|
|
87
|
+
this.transportConnectedOnce = { ws: false, sse: false };
|
|
88
|
+
this.doConnect(0);
|
|
89
|
+
}
|
|
90
|
+
destroy() {
|
|
91
|
+
this.closedByDestroy = true;
|
|
92
|
+
this.clearReconnectTimeout();
|
|
93
|
+
this.clearJoinTimers();
|
|
94
|
+
this.clearPingInterval();
|
|
95
|
+
this.clearTurnRefreshTimer();
|
|
96
|
+
this.awaitingPostReconnectSnapshot = false;
|
|
97
|
+
this.epochAtDisconnect = null;
|
|
98
|
+
if (this.transport) {
|
|
99
|
+
this.transport.close();
|
|
100
|
+
this.transport = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
sendMessage(type, payload, to) {
|
|
104
|
+
if (this.transport && this.transport.isOpen()) {
|
|
105
|
+
const msg = {
|
|
106
|
+
v: 1,
|
|
107
|
+
type,
|
|
108
|
+
rid: this.currentRoomId || undefined,
|
|
109
|
+
cid: this.clientId || undefined,
|
|
110
|
+
to,
|
|
111
|
+
payload
|
|
112
|
+
};
|
|
113
|
+
this.transport.send(msg);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
this.logger?.log('warning', 'Signaling', 'Transport not connected');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
joinRoom(roomId, options) {
|
|
120
|
+
this.logger?.log('debug', 'Signaling', `joinRoom call for ${roomId}`);
|
|
121
|
+
this.error = null;
|
|
122
|
+
this.clearJoinTimers();
|
|
123
|
+
this.needsRejoin = false;
|
|
124
|
+
this.currentRoomId = roomId;
|
|
125
|
+
this.joinAttemptId += 1;
|
|
126
|
+
const attemptId = this.joinAttemptId;
|
|
127
|
+
this.joinAcked = false;
|
|
128
|
+
if (options?.createMaxParticipants !== undefined) {
|
|
129
|
+
this.lastCreateMaxParticipants = options.createMaxParticipants;
|
|
130
|
+
}
|
|
131
|
+
if (options?.displayName !== undefined) {
|
|
132
|
+
this.lastDisplayName = options.displayName;
|
|
133
|
+
}
|
|
134
|
+
if (options?.peerId !== undefined) {
|
|
135
|
+
this.lastPeerId = options.peerId;
|
|
136
|
+
}
|
|
137
|
+
if (this.transport && this.transport.isOpen()) {
|
|
138
|
+
const payload = {
|
|
139
|
+
capabilities: { trickleIce: true, maxParticipants: 4 },
|
|
140
|
+
createMaxParticipants: options?.createMaxParticipants ?? this.lastCreateMaxParticipants ?? 4,
|
|
141
|
+
};
|
|
142
|
+
const displayName = options?.displayName ?? this.lastDisplayName;
|
|
143
|
+
if (displayName !== undefined) {
|
|
144
|
+
payload.displayName = displayName;
|
|
145
|
+
}
|
|
146
|
+
const peerId = options?.peerId ?? this.lastPeerId;
|
|
147
|
+
if (peerId !== undefined) {
|
|
148
|
+
payload.peerId = peerId;
|
|
149
|
+
}
|
|
150
|
+
const reconnectCid = this.clientId || this.lastClientId;
|
|
151
|
+
if (reconnectCid) {
|
|
152
|
+
payload.reconnectCid = reconnectCid;
|
|
153
|
+
if (this.reconnectToken && this.reconnectTokenRoomId === roomId) {
|
|
154
|
+
payload.reconnectToken = this.reconnectToken;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const doSendJoin = () => {
|
|
158
|
+
if (this.joinAttemptId !== attemptId)
|
|
159
|
+
return;
|
|
160
|
+
this.sendMessage('join', payload);
|
|
161
|
+
};
|
|
162
|
+
doSendJoin();
|
|
163
|
+
this.joinKickstartTimer = window.setTimeout(() => {
|
|
164
|
+
this.joinKickstartTimer = null;
|
|
165
|
+
if (this.joinAttemptId !== attemptId || this.joinAcked)
|
|
166
|
+
return;
|
|
167
|
+
this.logger?.log('debug', 'Signaling', 'Join kickstart: re-sending join');
|
|
168
|
+
doSendJoin();
|
|
169
|
+
}, JOIN_CONNECT_KICKSTART_MS);
|
|
170
|
+
this.joinRecoveryTimer = window.setTimeout(() => {
|
|
171
|
+
this.joinRecoveryTimer = null;
|
|
172
|
+
if (this.joinAttemptId !== attemptId || this.joinAcked)
|
|
173
|
+
return;
|
|
174
|
+
this.logger?.log('debug', 'Signaling', 'Join recovery: re-sending join');
|
|
175
|
+
doSendJoin();
|
|
176
|
+
}, JOIN_RECOVERY_MS);
|
|
177
|
+
this.joinHardTimeout = window.setTimeout(() => {
|
|
178
|
+
this.joinHardTimeout = null;
|
|
179
|
+
if (this.joinAttemptId !== attemptId || this.joinAcked)
|
|
180
|
+
return;
|
|
181
|
+
this.logger?.log('error', 'Signaling', 'Join hard timeout reached');
|
|
182
|
+
this.clearJoinTimers();
|
|
183
|
+
this.error = { code: 'JOIN_TIMEOUT', message: 'Join timed out' };
|
|
184
|
+
this.notifyStateChange();
|
|
185
|
+
}, JOIN_HARD_TIMEOUT_MS);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
this.logger?.log('debug', 'Signaling', 'Transport not ready, buffering join');
|
|
189
|
+
this.pendingJoin = roomId;
|
|
190
|
+
}
|
|
191
|
+
this.notifyStateChange();
|
|
192
|
+
}
|
|
193
|
+
leaveRoom(options) {
|
|
194
|
+
const preserveReconnectState = options?.preserveReconnectState === true;
|
|
195
|
+
this.clearJoinTimers();
|
|
196
|
+
this.sendMessage('leave');
|
|
197
|
+
this.currentRoomId = null;
|
|
198
|
+
this.needsRejoin = false;
|
|
199
|
+
if (preserveReconnectState) {
|
|
200
|
+
this.lastClientId = this.clientId;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
this.lastClientId = null;
|
|
204
|
+
this.clearReconnectStorage();
|
|
205
|
+
}
|
|
206
|
+
this.clientId = null;
|
|
207
|
+
this.roomState = null;
|
|
208
|
+
this.turnToken = null;
|
|
209
|
+
this.turnTokenTTLMs = null;
|
|
210
|
+
this.turnTokenExpiresAtMs = null;
|
|
211
|
+
this.lastReconnectOutcome = null;
|
|
212
|
+
this.lastEpoch = null;
|
|
213
|
+
this.awaitingPostReconnectSnapshot = false;
|
|
214
|
+
this.epochAtDisconnect = null;
|
|
215
|
+
this.notifyStateChange();
|
|
216
|
+
}
|
|
217
|
+
endRoom() {
|
|
218
|
+
this.clearJoinTimers();
|
|
219
|
+
this.sendMessage('end_room');
|
|
220
|
+
}
|
|
221
|
+
watchRooms(rids) {
|
|
222
|
+
this.sendMessage('watch_rooms', { rids });
|
|
223
|
+
}
|
|
224
|
+
clearError() {
|
|
225
|
+
this.error = null;
|
|
226
|
+
this.notifyStateChange();
|
|
227
|
+
}
|
|
228
|
+
subscribeToMessages(cb) {
|
|
229
|
+
this.messageListeners.push(cb);
|
|
230
|
+
return () => {
|
|
231
|
+
this.messageListeners = this.messageListeners.filter(l => l !== cb);
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
onStateChange(cb) {
|
|
235
|
+
this.stateListeners.push(cb);
|
|
236
|
+
return () => {
|
|
237
|
+
this.stateListeners = this.stateListeners.filter(l => l !== cb);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
get currentRoom() {
|
|
241
|
+
return this.currentRoomId;
|
|
242
|
+
}
|
|
243
|
+
// --- Private methods ---
|
|
244
|
+
handleIncomingMessage(msg) {
|
|
245
|
+
switch (msg.type) {
|
|
246
|
+
case 'joined': {
|
|
247
|
+
if (msg.cid)
|
|
248
|
+
this.clientId = msg.cid;
|
|
249
|
+
const joined = parseJoinedPayload(msg.payload);
|
|
250
|
+
if (!joined)
|
|
251
|
+
break;
|
|
252
|
+
this.clearJoinTimers();
|
|
253
|
+
this.joinAcked = true;
|
|
254
|
+
this.lastReconnectOutcome = joined.reconnect ?? null;
|
|
255
|
+
if (joined.epoch !== undefined) {
|
|
256
|
+
this.lastEpoch = joined.epoch;
|
|
257
|
+
}
|
|
258
|
+
this.roomState = {
|
|
259
|
+
hostCid: joined.hostCid,
|
|
260
|
+
participants: joined.participants,
|
|
261
|
+
maxParticipants: joined.maxParticipants,
|
|
262
|
+
epoch: joined.epoch,
|
|
263
|
+
};
|
|
264
|
+
if (joined.turnToken) {
|
|
265
|
+
this.turnToken = joined.turnToken;
|
|
266
|
+
}
|
|
267
|
+
if (joined.turnTokenTTLMs) {
|
|
268
|
+
this.turnTokenTTLMs = joined.turnTokenTTLMs;
|
|
269
|
+
this.scheduleTurnRefresh();
|
|
270
|
+
}
|
|
271
|
+
if (joined.reconnectToken) {
|
|
272
|
+
this.reconnectToken = joined.reconnectToken;
|
|
273
|
+
this.reconnectTokenRoomId = msg.rid || this.currentRoomId;
|
|
274
|
+
this.persistReconnectStorage();
|
|
275
|
+
}
|
|
276
|
+
this.persistClientId();
|
|
277
|
+
if (this.sessionStartTs === null) {
|
|
278
|
+
this.sessionStartTs = Date.now();
|
|
279
|
+
}
|
|
280
|
+
this.persistRecoveryRecord(joined.reconnectTokenTTLMs);
|
|
281
|
+
this.logger?.log('debug', 'Signaling', `joined outcome=${joined.reconnect ?? 'fresh'} epoch=${joined.epoch ?? 'n/a'}`);
|
|
282
|
+
// joined alone is not the authoritative post-reconnect
|
|
283
|
+
// snapshot — wait for the dedicated room_state that the
|
|
284
|
+
// server emits immediately after.
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case 'turn-refreshed': {
|
|
288
|
+
const turnRefreshed = parseTurnRefreshedPayload(msg.payload);
|
|
289
|
+
if (turnRefreshed) {
|
|
290
|
+
this.turnToken = turnRefreshed.turnToken;
|
|
291
|
+
if (turnRefreshed.turnTokenTTLMs) {
|
|
292
|
+
this.turnTokenTTLMs = turnRefreshed.turnTokenTTLMs;
|
|
293
|
+
this.scheduleTurnRefresh();
|
|
294
|
+
}
|
|
295
|
+
this.logger?.log('debug', 'Signaling', 'TURN credentials refreshed');
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case 'pong':
|
|
300
|
+
this.lastPongAt = Date.now();
|
|
301
|
+
this.missedPongs = 0;
|
|
302
|
+
// Pong is internal bookkeeping — skip notifyStateChange to avoid unnecessary rebuilds
|
|
303
|
+
[...this.messageListeners].forEach(listener => listener(msg));
|
|
304
|
+
return;
|
|
305
|
+
case 'room_state': {
|
|
306
|
+
const roomState = parseRoomStatePayload(msg.payload);
|
|
307
|
+
if (roomState) {
|
|
308
|
+
this.roomState = roomState;
|
|
309
|
+
if (roomState.epoch !== undefined) {
|
|
310
|
+
this.lastEpoch = roomState.epoch;
|
|
311
|
+
}
|
|
312
|
+
// First room_state seen after a transport reconnect is the
|
|
313
|
+
// authoritative sync point. Clear the gate so MediaEngine
|
|
314
|
+
// can proceed with renegotiation against confirmed state.
|
|
315
|
+
if (this.awaitingPostReconnectSnapshot) {
|
|
316
|
+
this.awaitingPostReconnectSnapshot = false;
|
|
317
|
+
this.epochAtDisconnect = null;
|
|
318
|
+
this.logger?.log('debug', 'Signaling', `Post-reconnect snapshot received (epoch=${roomState.epoch ?? 'n/a'})`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case 'room_ended':
|
|
324
|
+
this.resetForTerminal();
|
|
325
|
+
break;
|
|
326
|
+
case 'negotiation_dirty':
|
|
327
|
+
case 'relay_failed': {
|
|
328
|
+
// Validate payload shape and drop anything malformed before
|
|
329
|
+
// it reaches downstream listeners. MediaEngine handles the
|
|
330
|
+
// actual renegotiation/back-off behavior off the listener
|
|
331
|
+
// stream.
|
|
332
|
+
const parsed = msg.type === 'relay_failed'
|
|
333
|
+
? parseRelayFailedPayload(msg.payload)
|
|
334
|
+
: parseNegotiationDirtyPayload(msg.payload);
|
|
335
|
+
if (!parsed) {
|
|
336
|
+
this.logger?.log('warning', 'Signaling', `Ignoring malformed ${msg.type} payload`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 'room_statuses':
|
|
342
|
+
if (msg.payload) {
|
|
343
|
+
this.roomStatuses = mergeRoomStatusesPayload(this.roomStatuses, msg.payload);
|
|
344
|
+
}
|
|
345
|
+
break;
|
|
346
|
+
case 'room_status_update':
|
|
347
|
+
if (msg.payload) {
|
|
348
|
+
this.roomStatuses = mergeRoomStatusUpdatePayload(this.roomStatuses, msg.payload);
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
case 'error': {
|
|
352
|
+
const errorPayload = parseErrorPayload(msg.payload);
|
|
353
|
+
if (errorPayload) {
|
|
354
|
+
this.error = errorPayload;
|
|
355
|
+
// Terminal errors that invalidate persisted reconnect
|
|
356
|
+
// state. The session is over for this CID — clear the
|
|
357
|
+
// token so a future join can't try to reclaim it.
|
|
358
|
+
if (errorPayload.code === 'ROOM_ENDED' ||
|
|
359
|
+
errorPayload.code === 'INVALID_RECONNECT_TOKEN') {
|
|
360
|
+
this.resetForTerminal();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this.notifyStateChange();
|
|
367
|
+
[...this.messageListeners].forEach(listener => listener(msg));
|
|
368
|
+
}
|
|
369
|
+
doConnect(index) {
|
|
370
|
+
if (this.closedByDestroy)
|
|
371
|
+
return;
|
|
372
|
+
if (this.connecting)
|
|
373
|
+
return;
|
|
374
|
+
const targetIndex = index ?? this.transportIndex;
|
|
375
|
+
const targetKind = this.transportOrder[targetIndex];
|
|
376
|
+
if (!targetKind)
|
|
377
|
+
return;
|
|
378
|
+
this.transportIndex = targetIndex;
|
|
379
|
+
this.connecting = true;
|
|
380
|
+
if (this.transport) {
|
|
381
|
+
if (this.transport.getSessionId) {
|
|
382
|
+
this.sseSid = this.transport.getSessionId();
|
|
383
|
+
}
|
|
384
|
+
this.transport.close();
|
|
385
|
+
}
|
|
386
|
+
const connectionId = this.transportId + 1;
|
|
387
|
+
this.transportId = connectionId;
|
|
388
|
+
const transport = createSignalingTransport(targetKind, {
|
|
389
|
+
onOpen: () => {
|
|
390
|
+
if (connectionId !== this.transportId)
|
|
391
|
+
return;
|
|
392
|
+
this.connecting = false;
|
|
393
|
+
this.reconnectAttempts = 0;
|
|
394
|
+
if (targetKind === 'ws') {
|
|
395
|
+
this.wsConsecutiveFailures = 0;
|
|
396
|
+
}
|
|
397
|
+
const wasConnected = this.isConnected;
|
|
398
|
+
this.isConnected = true;
|
|
399
|
+
this.activeTransport = targetKind;
|
|
400
|
+
this.transportConnectedOnce[targetKind] = true;
|
|
401
|
+
this.startPingInterval();
|
|
402
|
+
if (!wasConnected) {
|
|
403
|
+
if (this.pendingJoin) {
|
|
404
|
+
const roomId = this.pendingJoin;
|
|
405
|
+
this.pendingJoin = null;
|
|
406
|
+
this.joinRoom(roomId);
|
|
407
|
+
}
|
|
408
|
+
else if (this.needsRejoin && this.currentRoomId) {
|
|
409
|
+
this.logger?.log('debug', 'Signaling', `Auto-rejoining room ${this.currentRoomId}`);
|
|
410
|
+
this.needsRejoin = false;
|
|
411
|
+
this.joinRoom(this.currentRoomId);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
this.notifyStateChange();
|
|
415
|
+
},
|
|
416
|
+
onClose: (reason, err) => {
|
|
417
|
+
if (connectionId !== this.transportId)
|
|
418
|
+
return;
|
|
419
|
+
this.connecting = false;
|
|
420
|
+
if (this.closedByDestroy)
|
|
421
|
+
return;
|
|
422
|
+
this.logger?.log('error', 'Signaling', `Disconnected via ${reason}${err ? `: ${formatError(err)}` : ''}`);
|
|
423
|
+
this.isConnected = false;
|
|
424
|
+
this.activeTransport = null;
|
|
425
|
+
this.clearPingInterval();
|
|
426
|
+
if (targetKind === 'ws') {
|
|
427
|
+
this.wsConsecutiveFailures++;
|
|
428
|
+
}
|
|
429
|
+
if (this.clientId) {
|
|
430
|
+
this.lastClientId = this.clientId;
|
|
431
|
+
}
|
|
432
|
+
this.transport = null;
|
|
433
|
+
this.needsRejoin = !!this.currentRoomId;
|
|
434
|
+
// We may miss membership transitions while disconnected. Set
|
|
435
|
+
// the gate so consumers wait for an authoritative
|
|
436
|
+
// post-reconnect snapshot before scheduling renegotiation.
|
|
437
|
+
if (this.currentRoomId) {
|
|
438
|
+
this.epochAtDisconnect = this.lastEpoch;
|
|
439
|
+
this.awaitingPostReconnectSnapshot = true;
|
|
440
|
+
}
|
|
441
|
+
if (this.shouldFallback(targetKind, reason) && this.tryNextTransport(reason)) {
|
|
442
|
+
this.notifyStateChange();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
this.scheduleReconnect();
|
|
446
|
+
this.notifyStateChange();
|
|
447
|
+
},
|
|
448
|
+
onMessage: (msg) => {
|
|
449
|
+
if (connectionId !== this.transportId)
|
|
450
|
+
return;
|
|
451
|
+
this.handleIncomingMessage(msg);
|
|
452
|
+
}
|
|
453
|
+
}, {
|
|
454
|
+
wsUrl: this.wsUrl,
|
|
455
|
+
httpBaseUrl: this.httpBaseUrl,
|
|
456
|
+
sseSid: this.sseSid || undefined,
|
|
457
|
+
logger: this.logger,
|
|
458
|
+
});
|
|
459
|
+
this.transport = transport;
|
|
460
|
+
try {
|
|
461
|
+
transport.connect();
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
this.connecting = false;
|
|
465
|
+
this.logger?.log('error', 'Signaling', `Transport connect() threw: ${formatError(err)}`);
|
|
466
|
+
this.scheduleReconnect();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
shouldFallback(kind, reason) {
|
|
470
|
+
if (this.transportOrder.length <= 1)
|
|
471
|
+
return false;
|
|
472
|
+
if (this.transportIndex >= this.transportOrder.length - 1)
|
|
473
|
+
return false;
|
|
474
|
+
if (reason === 'unsupported' || reason === 'timeout')
|
|
475
|
+
return true;
|
|
476
|
+
if (!this.transportConnectedOnce[kind])
|
|
477
|
+
return true;
|
|
478
|
+
if (kind === 'ws' && this.wsConsecutiveFailures >= WS_FALLBACK_CONSECUTIVE_FAILURES) {
|
|
479
|
+
this.logger?.log('warning', 'Signaling', `${this.wsConsecutiveFailures} consecutive WS failures, allowing SSE fallback`);
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
tryNextTransport(reason) {
|
|
485
|
+
const nextIndex = this.transportIndex + 1;
|
|
486
|
+
if (nextIndex >= this.transportOrder.length)
|
|
487
|
+
return false;
|
|
488
|
+
this.logger?.log('warning', 'Signaling', `${this.transportOrder[this.transportIndex]} failed (${reason}), trying ${this.transportOrder[nextIndex]}`);
|
|
489
|
+
this.reconnectAttempts = 0;
|
|
490
|
+
this.doConnect(nextIndex);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
scheduleReconnect() {
|
|
494
|
+
if (this.closedByDestroy)
|
|
495
|
+
return;
|
|
496
|
+
if (this.reconnectTimeout !== null)
|
|
497
|
+
return;
|
|
498
|
+
const attempt = this.reconnectAttempts + 1;
|
|
499
|
+
this.reconnectAttempts = attempt;
|
|
500
|
+
const backoff = Math.min(RECONNECT_BACKOFF_BASE_MS * Math.pow(2, attempt - 1), RECONNECT_BACKOFF_CAP_MS);
|
|
501
|
+
this.reconnectTimeout = window.setTimeout(() => {
|
|
502
|
+
this.reconnectTimeout = null;
|
|
503
|
+
this.transportIndex = 0;
|
|
504
|
+
this.transportConnectedOnce = { ws: false, sse: false };
|
|
505
|
+
this.doConnect(0);
|
|
506
|
+
}, backoff);
|
|
507
|
+
}
|
|
508
|
+
startPingInterval() {
|
|
509
|
+
this.clearPingInterval();
|
|
510
|
+
this.lastPongAt = Date.now();
|
|
511
|
+
this.missedPongs = 0;
|
|
512
|
+
this.pingInterval = window.setInterval(() => {
|
|
513
|
+
const elapsed = Date.now() - this.lastPongAt;
|
|
514
|
+
if (elapsed > PING_INTERVAL_MS) {
|
|
515
|
+
this.missedPongs++;
|
|
516
|
+
if (this.missedPongs >= PONG_MISS_THRESHOLD) {
|
|
517
|
+
this.logger?.log('warning', 'Signaling', `${this.missedPongs} missed pongs, treating connection as dead`);
|
|
518
|
+
this.missedPongs = 0;
|
|
519
|
+
if (this.transport) {
|
|
520
|
+
if (this.transport.forceClose) {
|
|
521
|
+
this.transport.forceClose('ping-timeout');
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
this.transport.close();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
this.sendMessage('ping', { ts: Date.now() });
|
|
531
|
+
}, PING_INTERVAL_MS);
|
|
532
|
+
}
|
|
533
|
+
turnRefreshGate = null;
|
|
534
|
+
// Absolute timestamp (epoch ms) at which the current TURN credential expires.
|
|
535
|
+
// Set when TTL is installed; used to compute "remaining until expiry" so the
|
|
536
|
+
// skip-path reschedule has a real safety buffer on repeat skips.
|
|
537
|
+
turnTokenExpiresAtMs = null;
|
|
538
|
+
setTurnRefreshGate(gate) {
|
|
539
|
+
this.turnRefreshGate = gate;
|
|
540
|
+
}
|
|
541
|
+
scheduleTurnRefresh(delayOverrideMs) {
|
|
542
|
+
this.clearTurnRefreshTimer();
|
|
543
|
+
if (!this.isConnected || !this.turnTokenTTLMs || !this.currentRoomId)
|
|
544
|
+
return;
|
|
545
|
+
if (delayOverrideMs === undefined) {
|
|
546
|
+
// Initial schedule after fresh creds: trigger at `ratio * TTL`.
|
|
547
|
+
this.turnTokenExpiresAtMs = Date.now() + this.turnTokenTTLMs;
|
|
548
|
+
}
|
|
549
|
+
const refreshDelay = delayOverrideMs ?? this.turnTokenTTLMs * TURN_REFRESH_TRIGGER_RATIO;
|
|
550
|
+
this.logger?.log('debug', 'Signaling', `Scheduling TURN refresh in ${Math.round(refreshDelay / 1000)}s`);
|
|
551
|
+
this.turnRefreshTimer = window.setTimeout(() => {
|
|
552
|
+
this.turnRefreshTimer = null;
|
|
553
|
+
void this.maybeSendTurnRefresh();
|
|
554
|
+
}, refreshDelay);
|
|
555
|
+
}
|
|
556
|
+
async maybeSendTurnRefresh() {
|
|
557
|
+
if (!this.isConnected || !this.currentRoomId)
|
|
558
|
+
return;
|
|
559
|
+
if (this.turnRefreshGate) {
|
|
560
|
+
let shouldRefresh = true;
|
|
561
|
+
try {
|
|
562
|
+
shouldRefresh = await this.turnRefreshGate();
|
|
563
|
+
}
|
|
564
|
+
catch { /* gate failure → default to refreshing */ }
|
|
565
|
+
if (!shouldRefresh) {
|
|
566
|
+
this.logger?.log('debug', 'Signaling', 'Skipping turn-refresh: all peer paths direct');
|
|
567
|
+
// Reschedule at a fraction of the remaining lifetime so a late
|
|
568
|
+
// path failover to relay still has time to refresh before the
|
|
569
|
+
// current credentials expire. Using `remaining * ratio` gives
|
|
570
|
+
// an exponential approach to expiry on repeat skips.
|
|
571
|
+
if (this.turnTokenExpiresAtMs !== null) {
|
|
572
|
+
const remainingMs = this.turnTokenExpiresAtMs - Date.now();
|
|
573
|
+
if (remainingMs > 0) {
|
|
574
|
+
this.scheduleTurnRefresh(remainingMs * TURN_REFRESH_TRIGGER_RATIO);
|
|
575
|
+
}
|
|
576
|
+
// remainingMs <= 0 → creds already expired; stop polling.
|
|
577
|
+
// A later relay transition is out of our hands; the call
|
|
578
|
+
// was direct when signaling gave us a chance to refresh.
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
this.logger?.log('debug', 'Signaling', 'Sending turn-refresh request');
|
|
584
|
+
this.sendMessage('turn-refresh');
|
|
585
|
+
}
|
|
586
|
+
clearJoinTimers() {
|
|
587
|
+
if (this.joinKickstartTimer !== null) {
|
|
588
|
+
window.clearTimeout(this.joinKickstartTimer);
|
|
589
|
+
this.joinKickstartTimer = null;
|
|
590
|
+
}
|
|
591
|
+
if (this.joinRecoveryTimer !== null) {
|
|
592
|
+
window.clearTimeout(this.joinRecoveryTimer);
|
|
593
|
+
this.joinRecoveryTimer = null;
|
|
594
|
+
}
|
|
595
|
+
if (this.joinHardTimeout !== null) {
|
|
596
|
+
window.clearTimeout(this.joinHardTimeout);
|
|
597
|
+
this.joinHardTimeout = null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
clearPingInterval() {
|
|
601
|
+
if (this.pingInterval !== null) {
|
|
602
|
+
window.clearInterval(this.pingInterval);
|
|
603
|
+
this.pingInterval = null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
clearTurnRefreshTimer() {
|
|
607
|
+
if (this.turnRefreshTimer !== null) {
|
|
608
|
+
window.clearTimeout(this.turnRefreshTimer);
|
|
609
|
+
this.turnRefreshTimer = null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
clearReconnectTimeout() {
|
|
613
|
+
if (this.reconnectTimeout !== null) {
|
|
614
|
+
window.clearTimeout(this.reconnectTimeout);
|
|
615
|
+
this.reconnectTimeout = null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
notifyStateChange() {
|
|
619
|
+
[...this.stateListeners].forEach(l => l());
|
|
620
|
+
}
|
|
621
|
+
// Drops in-room state, persisted reconnect authority, and the
|
|
622
|
+
// post-reconnect gate. Called for any terminal event that means the
|
|
623
|
+
// current session is over and should not influence a future join:
|
|
624
|
+
// server `room_ended`, terminal error codes, etc.
|
|
625
|
+
resetForTerminal() {
|
|
626
|
+
this.clearJoinTimers();
|
|
627
|
+
this.roomState = null;
|
|
628
|
+
this.currentRoomId = null;
|
|
629
|
+
this.needsRejoin = false;
|
|
630
|
+
this.clearReconnectStorage();
|
|
631
|
+
this.lastReconnectOutcome = null;
|
|
632
|
+
this.lastEpoch = null;
|
|
633
|
+
this.awaitingPostReconnectSnapshot = false;
|
|
634
|
+
this.epochAtDisconnect = null;
|
|
635
|
+
}
|
|
636
|
+
// Session storage helpers
|
|
637
|
+
storageKeyClientId = 'serenada.reconnectCid';
|
|
638
|
+
storageKeyReconnectToken = 'serenada.reconnectToken';
|
|
639
|
+
storageKeyReconnectTokenRoom = 'serenada.reconnectTokenRoom';
|
|
640
|
+
loadReconnectStorage() {
|
|
641
|
+
try {
|
|
642
|
+
const stored = window.sessionStorage.getItem(this.storageKeyClientId);
|
|
643
|
+
if (stored && !this.lastClientId)
|
|
644
|
+
this.lastClientId = stored;
|
|
645
|
+
const storedToken = window.sessionStorage.getItem(this.storageKeyReconnectToken);
|
|
646
|
+
if (storedToken && !this.reconnectToken)
|
|
647
|
+
this.reconnectToken = storedToken;
|
|
648
|
+
const storedTokenRoom = window.sessionStorage.getItem(this.storageKeyReconnectTokenRoom);
|
|
649
|
+
if (storedTokenRoom && !this.reconnectTokenRoomId)
|
|
650
|
+
this.reconnectTokenRoomId = storedTokenRoom;
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
this.logger?.log('warning', 'Signaling', `Failed to load reconnectCid: ${err}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
persistClientId() {
|
|
657
|
+
if (this.clientId) {
|
|
658
|
+
try {
|
|
659
|
+
window.sessionStorage.setItem(this.storageKeyClientId, this.clientId);
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
this.logger?.log('warning', 'Signaling', `Failed to persist reconnectCid: ${err}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
persistReconnectStorage() {
|
|
667
|
+
try {
|
|
668
|
+
if (this.reconnectToken) {
|
|
669
|
+
window.sessionStorage.setItem(this.storageKeyReconnectToken, this.reconnectToken);
|
|
670
|
+
}
|
|
671
|
+
if (this.reconnectTokenRoomId) {
|
|
672
|
+
window.sessionStorage.setItem(this.storageKeyReconnectTokenRoom, this.reconnectTokenRoomId);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
this.logger?.log('warning', 'Signaling', `Failed to persist reconnectToken: ${err}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
clearReconnectStorage() {
|
|
680
|
+
try {
|
|
681
|
+
window.sessionStorage.removeItem(this.storageKeyClientId);
|
|
682
|
+
window.sessionStorage.removeItem(this.storageKeyReconnectToken);
|
|
683
|
+
window.sessionStorage.removeItem(this.storageKeyReconnectTokenRoom);
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
this.logger?.log('warning', 'Signaling', `Failed to clear reconnectCid: ${err}`);
|
|
687
|
+
}
|
|
688
|
+
this.reconnectToken = null;
|
|
689
|
+
this.reconnectTokenRoomId = null;
|
|
690
|
+
this.sessionStartTs = null;
|
|
691
|
+
clearRecoveryRecord();
|
|
692
|
+
}
|
|
693
|
+
// Snapshots the in-memory reconnect state into the cross-launch
|
|
694
|
+
// recovery store so a relaunched tab can offer a "Rejoin call?" prompt.
|
|
695
|
+
// No-op when we don't have full credentials yet (e.g. first transport
|
|
696
|
+
// open before the server has answered with `joined`).
|
|
697
|
+
persistRecoveryRecord(reconnectTokenTTLMs) {
|
|
698
|
+
if (!this.currentRoomId || !this.clientId || !this.reconnectToken || !this.sessionStartTs) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
// Fall back to the cross-platform suspendHardEvictionTimeout when
|
|
702
|
+
// the server didn't surface a token TTL. The Go server's reconnect
|
|
703
|
+
// token TTL is bound to suspendHardEvictionTimeout (see
|
|
704
|
+
// signaling.go: reconnectTokenTTL = suspendHardEvictionTimeout), so
|
|
705
|
+
// mirroring that constant keeps Web aligned with iOS/Android and
|
|
706
|
+
// the server without local drift.
|
|
707
|
+
const ttl = reconnectTokenTTLMs && reconnectTokenTTLMs > 0
|
|
708
|
+
? reconnectTokenTTLMs
|
|
709
|
+
: SUSPEND_HARD_EVICTION_TIMEOUT_MS;
|
|
710
|
+
saveRecoveryRecord({
|
|
711
|
+
roomId: this.currentRoomId,
|
|
712
|
+
cid: this.clientId,
|
|
713
|
+
reconnectToken: this.reconnectToken,
|
|
714
|
+
lastEpoch: this.lastEpoch,
|
|
715
|
+
sessionStartTs: this.sessionStartTs,
|
|
716
|
+
expiresAtMs: Date.now() + ttl,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
//# sourceMappingURL=SignalingEngine.js.map
|