@livedesk/hub 0.1.57 → 0.1.58
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/package.json +3 -3
- package/src/console-direct.js +140 -1
- package/src/console-direct.test.mjs +114 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.58",
|
|
4
4
|
"description": "VuvoDesk local Hub API and browser frame bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "node src/server.js",
|
|
13
13
|
"start": "node src/server.js",
|
|
14
|
-
"check": "node --check src/server.js && node --check src/remote-hub.js && node --check src/remote-clipboard-contract.mjs && node --check src/console-direct.js",
|
|
14
|
+
"check": "node --check src/server.js && node --check src/remote-hub.js && node --check src/remote-clipboard-contract.mjs && node --check src/console-direct.js",
|
|
15
15
|
"prepublishOnly": "node ../../scripts/livedesk-release-git-gate.mjs"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
19
|
-
"@livedesk/runtime-core": "0.1.8",
|
|
19
|
+
"@livedesk/runtime-core": "0.1.8",
|
|
20
20
|
"@openai/codex-sdk": "0.145.0",
|
|
21
21
|
"cors": "^2.8.5",
|
|
22
22
|
"express": "^4.21.2",
|
package/src/console-direct.js
CHANGED
|
@@ -34,6 +34,8 @@ const ICE_CONNECT_TIMEOUT_MS = 10_000;
|
|
|
34
34
|
const PEER_DISCONNECTED_TIMEOUT_MS = 5_000;
|
|
35
35
|
const ACCESS_TOKEN_TIMEOUT_MS = 8_000;
|
|
36
36
|
const SIGNAL_READY_TIMEOUT_MS = 10_000;
|
|
37
|
+
const SIGNAL_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
38
|
+
const SIGNAL_HEARTBEAT_TIMEOUT_MS = 10_000;
|
|
37
39
|
const CONTROL_ASSEMBLY_TIMEOUT_MS = 15_000;
|
|
38
40
|
const MEDIA_ASSEMBLY_TIMEOUT_MS = 500;
|
|
39
41
|
const LOGICAL_BIND_TIMEOUT_MS = 5_000;
|
|
@@ -302,6 +304,20 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
302
304
|
1,
|
|
303
305
|
Number(options.signalReadyTimeoutMs) || SIGNAL_READY_TIMEOUT_MS
|
|
304
306
|
);
|
|
307
|
+
const signalHeartbeatIntervalMs = Math.max(
|
|
308
|
+
1,
|
|
309
|
+
Number(options.signalHeartbeatIntervalMs) || SIGNAL_HEARTBEAT_INTERVAL_MS
|
|
310
|
+
);
|
|
311
|
+
const signalHeartbeatTimeoutMs = Math.max(
|
|
312
|
+
1,
|
|
313
|
+
Number(options.signalHeartbeatTimeoutMs) || SIGNAL_HEARTBEAT_TIMEOUT_MS
|
|
314
|
+
);
|
|
315
|
+
const scheduleSignalHeartbeatTimer = typeof options.scheduleSignalHeartbeatTimer === 'function'
|
|
316
|
+
? options.scheduleSignalHeartbeatTimer
|
|
317
|
+
: setTimeout;
|
|
318
|
+
const cancelSignalHeartbeatTimer = typeof options.cancelSignalHeartbeatTimer === 'function'
|
|
319
|
+
? options.cancelSignalHeartbeatTimer
|
|
320
|
+
: clearTimeout;
|
|
305
321
|
const mediaAssemblyTimeoutMs = Math.max(
|
|
306
322
|
1,
|
|
307
323
|
Number(options.mediaAssemblyTimeoutMs) || MEDIA_ASSEMBLY_TIMEOUT_MS
|
|
@@ -317,6 +333,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
317
333
|
let retryTimer = null;
|
|
318
334
|
let accessTokenAttempt = null;
|
|
319
335
|
let signalReadyDeadline = null;
|
|
336
|
+
let signalHeartbeat = null;
|
|
320
337
|
let retryAttempt = 0;
|
|
321
338
|
let retryNotBeforeAt = 0;
|
|
322
339
|
let nextRetryAt = '';
|
|
@@ -1174,6 +1191,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1174
1191
|
retryAttempt = 0;
|
|
1175
1192
|
retryNotBeforeAt = 0;
|
|
1176
1193
|
nextRetryAt = '';
|
|
1194
|
+
armSignalHeartbeat(ownerSignalGeneration, socket, connect);
|
|
1177
1195
|
return;
|
|
1178
1196
|
}
|
|
1179
1197
|
const consoleId = String(payload.consoleId || '');
|
|
@@ -1284,6 +1302,114 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1284
1302
|
signalReadyDeadline = deadline;
|
|
1285
1303
|
};
|
|
1286
1304
|
|
|
1305
|
+
const clearSignalHeartbeat = (ownerSignalGeneration, socket) => {
|
|
1306
|
+
const heartbeat = signalHeartbeat;
|
|
1307
|
+
if (!heartbeat
|
|
1308
|
+
|| (ownerSignalGeneration !== undefined && heartbeat.generation !== ownerSignalGeneration)
|
|
1309
|
+
|| (socket !== undefined && heartbeat.socket !== socket)) {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
signalHeartbeat = null;
|
|
1313
|
+
cancelSignalHeartbeatTimer(heartbeat.timer);
|
|
1314
|
+
return true;
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
const failSignalHeartbeat = (heartbeat, connect, reason) => {
|
|
1318
|
+
if (signalHeartbeat !== heartbeat
|
|
1319
|
+
|| stopped
|
|
1320
|
+
|| heartbeat.generation !== signalGeneration
|
|
1321
|
+
|| signalSocket !== heartbeat.socket) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
clearSignalHeartbeat(heartbeat.generation, heartbeat.socket);
|
|
1325
|
+
signalSocket = null;
|
|
1326
|
+
lastHttpStatus = 0;
|
|
1327
|
+
lastError = reason;
|
|
1328
|
+
retireNegotiatingPeers(reason);
|
|
1329
|
+
try { heartbeat.socket.terminate?.(); } catch {}
|
|
1330
|
+
scheduleReconnect(connect);
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
const scheduleSignalHeartbeatPing = (heartbeat, connect) => {
|
|
1334
|
+
if (signalHeartbeat !== heartbeat
|
|
1335
|
+
|| stopped
|
|
1336
|
+
|| heartbeat.generation !== signalGeneration
|
|
1337
|
+
|| signalSocket !== heartbeat.socket) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
heartbeat.awaitingPong = false;
|
|
1341
|
+
let pingTimer = null;
|
|
1342
|
+
pingTimer = scheduleSignalHeartbeatTimer(() => {
|
|
1343
|
+
if (signalHeartbeat !== heartbeat
|
|
1344
|
+
|| heartbeat.timer !== pingTimer
|
|
1345
|
+
|| stopped
|
|
1346
|
+
|| heartbeat.generation !== signalGeneration
|
|
1347
|
+
|| signalSocket !== heartbeat.socket) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
heartbeat.awaitingPong = true;
|
|
1351
|
+
let timeoutTimer = null;
|
|
1352
|
+
timeoutTimer = scheduleSignalHeartbeatTimer(() => {
|
|
1353
|
+
if (signalHeartbeat !== heartbeat
|
|
1354
|
+
|| heartbeat.timer !== timeoutTimer
|
|
1355
|
+
|| !heartbeat.awaitingPong
|
|
1356
|
+
|| stopped
|
|
1357
|
+
|| heartbeat.generation !== signalGeneration
|
|
1358
|
+
|| signalSocket !== heartbeat.socket) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
failSignalHeartbeat(
|
|
1362
|
+
heartbeat,
|
|
1363
|
+
connect,
|
|
1364
|
+
'console-direct-signal-heartbeat-timeout'
|
|
1365
|
+
);
|
|
1366
|
+
}, signalHeartbeatTimeoutMs);
|
|
1367
|
+
heartbeat.timer = timeoutTimer;
|
|
1368
|
+
heartbeat.timer.unref?.();
|
|
1369
|
+
try {
|
|
1370
|
+
if (heartbeat.socket.readyState !== WebSocket.OPEN
|
|
1371
|
+
|| typeof heartbeat.socket.ping !== 'function') {
|
|
1372
|
+
throw new Error('console-direct-signal-heartbeat-unavailable');
|
|
1373
|
+
}
|
|
1374
|
+
heartbeat.socket.ping();
|
|
1375
|
+
} catch {
|
|
1376
|
+
failSignalHeartbeat(
|
|
1377
|
+
heartbeat,
|
|
1378
|
+
connect,
|
|
1379
|
+
'console-direct-signal-heartbeat-failed'
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
}, signalHeartbeatIntervalMs);
|
|
1383
|
+
heartbeat.timer = pingTimer;
|
|
1384
|
+
heartbeat.timer.unref?.();
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
const armSignalHeartbeat = (ownerSignalGeneration, socket, connect) => {
|
|
1388
|
+
clearSignalHeartbeat();
|
|
1389
|
+
const heartbeat = {
|
|
1390
|
+
generation: ownerSignalGeneration,
|
|
1391
|
+
socket,
|
|
1392
|
+
timer: null,
|
|
1393
|
+
awaitingPong: false
|
|
1394
|
+
};
|
|
1395
|
+
signalHeartbeat = heartbeat;
|
|
1396
|
+
scheduleSignalHeartbeatPing(heartbeat, connect);
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
const acceptSignalHeartbeatPong = (ownerSignalGeneration, socket, connect) => {
|
|
1400
|
+
const heartbeat = signalHeartbeat;
|
|
1401
|
+
if (!heartbeat
|
|
1402
|
+
|| heartbeat.generation !== ownerSignalGeneration
|
|
1403
|
+
|| heartbeat.socket !== socket
|
|
1404
|
+
|| !heartbeat.awaitingPong) {
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
cancelSignalHeartbeatTimer(heartbeat.timer);
|
|
1408
|
+
heartbeat.timer = null;
|
|
1409
|
+
scheduleSignalHeartbeatPing(heartbeat, connect);
|
|
1410
|
+
return true;
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1287
1413
|
const scheduleReconnect = connect => {
|
|
1288
1414
|
if (stopped || retryTimer || !(signalUrl instanceof URL)) return;
|
|
1289
1415
|
const sequenceDelay = retryDelaysMs[Math.min(retryAttempt, retryDelaysMs.length - 1)];
|
|
@@ -1374,12 +1500,16 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1374
1500
|
handleSignalMessage(raw, ownerSignalGeneration, socket);
|
|
1375
1501
|
}
|
|
1376
1502
|
});
|
|
1503
|
+
socket.on('pong', () => {
|
|
1504
|
+
acceptSignalHeartbeatPong(ownerSignalGeneration, socket, connect);
|
|
1505
|
+
});
|
|
1377
1506
|
socket.once('close', () => {
|
|
1378
1507
|
const wasOwner = signalSocket === socket
|
|
1379
1508
|
&& ownerSignalGeneration === signalGeneration
|
|
1380
1509
|
&& !stopped;
|
|
1381
1510
|
if (signalSocket === socket) signalSocket = null;
|
|
1382
1511
|
clearSignalReadyDeadline(ownerSignalGeneration, socket);
|
|
1512
|
+
clearSignalHeartbeat(ownerSignalGeneration, socket);
|
|
1383
1513
|
if (!wasOwner) return;
|
|
1384
1514
|
retireNegotiatingPeers('console-direct-signaling-closed');
|
|
1385
1515
|
if (!retryTimer) state = 'disconnected';
|
|
@@ -1393,6 +1523,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1393
1523
|
}
|
|
1394
1524
|
signalSocket = null;
|
|
1395
1525
|
clearSignalReadyDeadline(ownerSignalGeneration, socket);
|
|
1526
|
+
clearSignalHeartbeat(ownerSignalGeneration, socket);
|
|
1396
1527
|
retireNegotiatingPeers('console-direct-signaling-error');
|
|
1397
1528
|
try { socket.terminate?.(); } catch {}
|
|
1398
1529
|
scheduleReconnect(connect);
|
|
@@ -1413,6 +1544,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1413
1544
|
signalGeneration += 1;
|
|
1414
1545
|
cancelAccessTokenAttempt();
|
|
1415
1546
|
clearSignalReadyDeadline();
|
|
1547
|
+
clearSignalHeartbeat();
|
|
1416
1548
|
if (retryTimer) clearTimeout(retryTimer);
|
|
1417
1549
|
retryTimer = null;
|
|
1418
1550
|
retryAttempt = 0;
|
|
@@ -1431,6 +1563,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1431
1563
|
signalGeneration += 1;
|
|
1432
1564
|
cancelAccessTokenAttempt();
|
|
1433
1565
|
clearSignalReadyDeadline();
|
|
1566
|
+
clearSignalHeartbeat();
|
|
1434
1567
|
if (retryTimer) clearTimeout(retryTimer);
|
|
1435
1568
|
retryTimer = null;
|
|
1436
1569
|
nextRetryAt = '';
|
|
@@ -1472,6 +1605,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1472
1605
|
const reconnectTimerActive = Boolean(retryTimer);
|
|
1473
1606
|
const accessTokenDeadlineActive = Boolean(accessTokenAttempt);
|
|
1474
1607
|
const signalReadyDeadlineActive = Boolean(signalReadyDeadline);
|
|
1608
|
+
const signalHeartbeatTimerActive = Boolean(signalHeartbeat?.timer);
|
|
1475
1609
|
return Object.freeze({
|
|
1476
1610
|
enabled: signalUrl instanceof URL && Boolean(deviceId),
|
|
1477
1611
|
state,
|
|
@@ -1491,6 +1625,8 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1491
1625
|
reconnectTimerActive,
|
|
1492
1626
|
accessTokenDeadlineActive,
|
|
1493
1627
|
signalReadyDeadlineActive,
|
|
1628
|
+
signalHeartbeatTimerActive,
|
|
1629
|
+
signalHeartbeatAwaitingPong: signalHeartbeat?.awaitingPong === true,
|
|
1494
1630
|
resourceTimers: iceConnectTimers
|
|
1495
1631
|
+ peerDisconnectTimers
|
|
1496
1632
|
+ assemblyDeadlineTimers
|
|
@@ -1498,7 +1634,8 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1498
1634
|
+ pendingLogicalControlRequests
|
|
1499
1635
|
+ Number(reconnectTimerActive)
|
|
1500
1636
|
+ Number(accessTokenDeadlineActive)
|
|
1501
|
-
+ Number(signalReadyDeadlineActive)
|
|
1637
|
+
+ Number(signalReadyDeadlineActive)
|
|
1638
|
+
+ Number(signalHeartbeatTimerActive),
|
|
1502
1639
|
rejectedRelayCandidates,
|
|
1503
1640
|
dataChannelBackpressureCloses,
|
|
1504
1641
|
malformedWireCloses,
|
|
@@ -1530,6 +1667,8 @@ export const consoleDirectContract = Object.freeze({
|
|
|
1530
1667
|
maxDefaultRetryDelayMs: DEFAULT_RETRY_DELAYS_MS.at(-1),
|
|
1531
1668
|
accessTokenTimeoutMs: ACCESS_TOKEN_TIMEOUT_MS,
|
|
1532
1669
|
signalReadyTimeoutMs: SIGNAL_READY_TIMEOUT_MS,
|
|
1670
|
+
signalHeartbeatIntervalMs: SIGNAL_HEARTBEAT_INTERVAL_MS,
|
|
1671
|
+
signalHeartbeatTimeoutMs: SIGNAL_HEARTBEAT_TIMEOUT_MS,
|
|
1533
1672
|
logicalBindTimeoutMs: LOGICAL_BIND_TIMEOUT_MS,
|
|
1534
1673
|
iceConnectTimeoutMs: ICE_CONNECT_TIMEOUT_MS,
|
|
1535
1674
|
peerDisconnectedTimeoutMs: PEER_DISCONNECTED_TIMEOUT_MS,
|
|
@@ -13,6 +13,8 @@ assert.equal(consoleDirectContract.maxDefaultRetryDelayMs, 20_000);
|
|
|
13
13
|
assert.equal(consoleDirectContract.peerDisconnectedTimeoutMs, 5_000);
|
|
14
14
|
assert.equal(consoleDirectContract.maxPendingMediaWireMessages, 8);
|
|
15
15
|
assert.equal(consoleDirectContract.reliableAssemblyTimeoutMs, 15_000);
|
|
16
|
+
assert.equal(consoleDirectContract.signalHeartbeatIntervalMs, 15_000);
|
|
17
|
+
assert.equal(consoleDirectContract.signalHeartbeatTimeoutMs, 10_000);
|
|
16
18
|
|
|
17
19
|
const HUB_EPOCH = '11111111-1111-4111-8111-111111111111';
|
|
18
20
|
const HUB_EPOCH_TWO = '77777777-7777-4777-8777-777777777777';
|
|
@@ -38,6 +40,7 @@ class FakeSocket extends EventEmitter {
|
|
|
38
40
|
this.sent = [];
|
|
39
41
|
this.closeCode = 0;
|
|
40
42
|
this.closeReason = '';
|
|
43
|
+
this.pingCount = 0;
|
|
41
44
|
this.constructor.instances.push(this);
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -55,6 +58,15 @@ class FakeSocket extends EventEmitter {
|
|
|
55
58
|
this.emit('message', value, binary);
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
ping() {
|
|
62
|
+
if (this.readyState !== FakeSocket.OPEN) throw new Error('fake-socket-not-open');
|
|
63
|
+
this.pingCount += 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pong() {
|
|
67
|
+
this.emit('pong');
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
close(code = 1000, reason = '') {
|
|
59
71
|
if (this.readyState === FakeSocket.CLOSED) return;
|
|
60
72
|
this.closeCode = code;
|
|
@@ -222,6 +234,18 @@ async function connectedDirect(options = {}) {
|
|
|
222
234
|
? { peerDisconnectedTimeoutMs: options.peerDisconnectedTimeoutMs }
|
|
223
235
|
: {}),
|
|
224
236
|
...(options.mediaAssemblyTimeoutMs ? { mediaAssemblyTimeoutMs: options.mediaAssemblyTimeoutMs } : {}),
|
|
237
|
+
...(options.signalHeartbeatIntervalMs
|
|
238
|
+
? { signalHeartbeatIntervalMs: options.signalHeartbeatIntervalMs }
|
|
239
|
+
: {}),
|
|
240
|
+
...(options.signalHeartbeatTimeoutMs
|
|
241
|
+
? { signalHeartbeatTimeoutMs: options.signalHeartbeatTimeoutMs }
|
|
242
|
+
: {}),
|
|
243
|
+
...(options.scheduleSignalHeartbeatTimer
|
|
244
|
+
? { scheduleSignalHeartbeatTimer: options.scheduleSignalHeartbeatTimer }
|
|
245
|
+
: {}),
|
|
246
|
+
...(options.cancelSignalHeartbeatTimer
|
|
247
|
+
? { cancelSignalHeartbeatTimer: options.cancelSignalHeartbeatTimer }
|
|
248
|
+
: {}),
|
|
225
249
|
fetchImpl: options.fetchImpl || (async () => new Response('{}', {
|
|
226
250
|
status: 200,
|
|
227
251
|
headers: { 'Content-Type': 'application/json' }
|
|
@@ -319,6 +343,89 @@ test('a signaling reconnect preserves a healthy direct peer until the Hub epoch
|
|
|
319
343
|
direct.close();
|
|
320
344
|
});
|
|
321
345
|
|
|
346
|
+
test('a silent half-open signaling socket is replaced while a healthy direct peer stays alive', async () => {
|
|
347
|
+
const { direct, signal } = await connectedDirect({
|
|
348
|
+
retryDelaysMs: [0],
|
|
349
|
+
signalHeartbeatIntervalMs: 5,
|
|
350
|
+
signalHeartbeatTimeoutMs: 20
|
|
351
|
+
});
|
|
352
|
+
const peer = offer(signal);
|
|
353
|
+
const control = new FakeDataChannel('livedesk-control-v1');
|
|
354
|
+
peer.emitDataChannel(control);
|
|
355
|
+
control.open();
|
|
356
|
+
|
|
357
|
+
assert.equal(direct.inspect().signalHeartbeatTimerActive, true);
|
|
358
|
+
await waitFor(() => signal.pingCount === 1);
|
|
359
|
+
assert.equal(direct.inspect().signalHeartbeatAwaitingPong, true);
|
|
360
|
+
await waitFor(() => FakeSignalSocket.instances.length === 2);
|
|
361
|
+
|
|
362
|
+
const replacementSignal = FakeSignalSocket.instances[1];
|
|
363
|
+
assert.equal(signal.readyState, FakeSignalSocket.CLOSED);
|
|
364
|
+
assert.equal(peer.closed, false);
|
|
365
|
+
assert.equal(direct.inspect().peerConnections, 1);
|
|
366
|
+
assert.equal(direct.inspect().lastError, 'console-direct-signal-heartbeat-timeout');
|
|
367
|
+
replacementSignal.open();
|
|
368
|
+
replacementSignal.receive(JSON.stringify({
|
|
369
|
+
type: 'signal-ready',
|
|
370
|
+
role: 'hub',
|
|
371
|
+
deviceId: 'hub-device',
|
|
372
|
+
hubEpoch: HUB_EPOCH
|
|
373
|
+
}), false);
|
|
374
|
+
assert.equal(peer.closed, false);
|
|
375
|
+
assert.equal(direct.inspect().signalHeartbeatTimerActive, true);
|
|
376
|
+
|
|
377
|
+
await waitFor(() => replacementSignal.pingCount === 1);
|
|
378
|
+
replacementSignal.pong();
|
|
379
|
+
assert.equal(direct.inspect().signalHeartbeatAwaitingPong, false);
|
|
380
|
+
assert.equal(direct.inspect().signalHeartbeatTimerActive, true);
|
|
381
|
+
direct.close();
|
|
382
|
+
assert.equal(direct.inspect().signalHeartbeatTimerActive, false);
|
|
383
|
+
assert.equal(direct.inspect().resourceTimers, 0);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('a cancelled heartbeat timeout cannot retire a healthy signal after pong re-arms it', async () => {
|
|
387
|
+
let nextTimerId = 0;
|
|
388
|
+
const callbacks = new Map();
|
|
389
|
+
const cancelledTimers = new Set();
|
|
390
|
+
const { direct, signal } = await connectedDirect({
|
|
391
|
+
scheduleSignalHeartbeatTimer(callback) {
|
|
392
|
+
const timerId = ++nextTimerId;
|
|
393
|
+
callbacks.set(timerId, callback);
|
|
394
|
+
return timerId;
|
|
395
|
+
},
|
|
396
|
+
cancelSignalHeartbeatTimer(timerId) {
|
|
397
|
+
cancelledTimers.add(timerId);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
const peer = offer(signal);
|
|
401
|
+
const control = new FakeDataChannel('livedesk-control-v1');
|
|
402
|
+
peer.emitDataChannel(control);
|
|
403
|
+
control.open();
|
|
404
|
+
|
|
405
|
+
assert.equal(nextTimerId, 1);
|
|
406
|
+
callbacks.get(1)();
|
|
407
|
+
assert.equal(signal.pingCount, 1);
|
|
408
|
+
assert.equal(direct.inspect().signalHeartbeatAwaitingPong, true);
|
|
409
|
+
assert.equal(nextTimerId, 2);
|
|
410
|
+
|
|
411
|
+
signal.pong();
|
|
412
|
+
assert.equal(cancelledTimers.has(2), true);
|
|
413
|
+
assert.equal(nextTimerId, 3);
|
|
414
|
+
assert.equal(direct.inspect().signalHeartbeatAwaitingPong, false);
|
|
415
|
+
|
|
416
|
+
callbacks.get(2)();
|
|
417
|
+
assert.equal(signal.readyState, FakeSignalSocket.OPEN);
|
|
418
|
+
assert.equal(FakeSignalSocket.instances.length, 1);
|
|
419
|
+
assert.equal(peer.closed, false);
|
|
420
|
+
assert.equal(direct.inspect().peerConnections, 1);
|
|
421
|
+
assert.equal(direct.inspect().signalHeartbeatTimerActive, true);
|
|
422
|
+
assert.equal(direct.inspect().signalHeartbeatAwaitingPong, false);
|
|
423
|
+
|
|
424
|
+
direct.close();
|
|
425
|
+
assert.equal(cancelledTimers.has(3), true);
|
|
426
|
+
assert.equal(direct.inspect().resourceTimers, 0);
|
|
427
|
+
});
|
|
428
|
+
|
|
322
429
|
test('a same-account token refresh reconnects signaling without dropping a healthy direct peer', async () => {
|
|
323
430
|
const { direct, signal } = await connectedDirect();
|
|
324
431
|
const peer = offer(signal);
|
|
@@ -812,8 +919,10 @@ test('a transient disconnected state recovers or retires the exact peer on one b
|
|
|
812
919
|
assert.equal(peer.closed, true);
|
|
813
920
|
assert.equal(direct.inspect().peerConnections, 0);
|
|
814
921
|
assert.equal(direct.inspect().peerDisconnectTimers, 0);
|
|
815
|
-
assert.equal(direct.inspect().
|
|
922
|
+
assert.equal(direct.inspect().signalHeartbeatTimerActive, true);
|
|
923
|
+
assert.equal(direct.inspect().resourceTimers, 1);
|
|
816
924
|
direct.close();
|
|
925
|
+
assert.equal(direct.inspect().resourceTimers, 0);
|
|
817
926
|
});
|
|
818
927
|
|
|
819
928
|
test('an oversized local frame closes only its logical media lane', async () => {
|
|
@@ -851,8 +960,11 @@ test('an unopened control channel cannot suppress the bounded ICE owner timeout'
|
|
|
851
960
|
await new Promise(resolve => setTimeout(resolve, 15));
|
|
852
961
|
assert.equal(peer.closed, true);
|
|
853
962
|
assert.equal(direct.inspect().peerConnections, 0);
|
|
854
|
-
assert.equal(direct.inspect().
|
|
963
|
+
assert.equal(direct.inspect().iceConnectTimers, 0);
|
|
964
|
+
assert.equal(direct.inspect().signalHeartbeatTimerActive, true);
|
|
965
|
+
assert.equal(direct.inspect().resourceTimers, 1);
|
|
855
966
|
direct.close();
|
|
967
|
+
assert.equal(direct.inspect().resourceTimers, 0);
|
|
856
968
|
});
|
|
857
969
|
|
|
858
970
|
test('an incomplete partial-reliable media message owns one deadline and close returns terminal zero', async () => {
|