@livedesk/hub 0.1.69 → 0.1.70
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 +32 -32
- package/src/console-direct.js +4 -1
- package/src/console-direct.test.mjs +70 -4
package/package.json
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@livedesk/hub",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "VuvoDesk local Hub API and browser frame bridge",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "src/server.js",
|
|
7
|
-
"files": [
|
|
8
|
-
"src/",
|
|
9
|
-
"README.md"
|
|
10
|
-
],
|
|
11
|
-
"scripts": {
|
|
12
|
-
"dev": "node src/server.js",
|
|
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",
|
|
15
|
-
"prepublishOnly": "node ../../scripts/vuvodesk-release-git-gate.mjs"
|
|
16
|
-
},
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
1
|
+
{
|
|
2
|
+
"name": "@livedesk/hub",
|
|
3
|
+
"version": "0.1.70",
|
|
4
|
+
"description": "VuvoDesk local Hub API and browser frame bridge",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/server.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "node src/server.js",
|
|
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",
|
|
15
|
+
"prepublishOnly": "node ../../scripts/vuvodesk-release-git-gate.mjs"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
19
19
|
"@livedesk/runtime-core": "0.1.9",
|
|
20
|
-
"@openai/codex-sdk": "0.145.0",
|
|
21
|
-
"cors": "^2.8.5",
|
|
22
|
-
"express": "^4.21.2",
|
|
23
|
-
"node-datachannel": "0.33.0",
|
|
24
|
-
"path-to-regexp": "0.1.13",
|
|
25
|
-
"ws": "^8.18.3"
|
|
26
|
-
},
|
|
27
|
-
"engines": {
|
|
28
|
-
"node": ">=20"
|
|
29
|
-
},
|
|
30
|
-
"publishConfig": {
|
|
31
|
-
"access": "public"
|
|
32
|
-
}
|
|
33
|
-
}
|
|
20
|
+
"@openai/codex-sdk": "0.145.0",
|
|
21
|
+
"cors": "^2.8.5",
|
|
22
|
+
"express": "^4.21.2",
|
|
23
|
+
"node-datachannel": "0.33.0",
|
|
24
|
+
"path-to-regexp": "0.1.13",
|
|
25
|
+
"ws": "^8.18.3"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/console-direct.js
CHANGED
|
@@ -739,7 +739,10 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
739
739
|
// Node Buffer shape. Share the exact encoded bytes without copying or
|
|
740
740
|
// retaining a second application queue.
|
|
741
741
|
const nativeChunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
742
|
-
|
|
742
|
+
// libdatachannel returns false when it accepted the chunk into its
|
|
743
|
+
// bounded send buffer. Only an exception means this send failed.
|
|
744
|
+
// Retrying duplicates a fragment; closing here truncates the frame.
|
|
745
|
+
channelState.channel.sendMessageBinary(nativeChunk);
|
|
743
746
|
} catch (error) {
|
|
744
747
|
dataChannelSendFailures += 1;
|
|
745
748
|
lastDataChannelSendFailure = Object.freeze({
|
|
@@ -293,6 +293,64 @@ function offer(signal, connectionId = CONNECTION_ONE, sdp = 'v=0\r\na=fake-offer
|
|
|
293
293
|
return FakePeerConnection.instances.at(-1);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
test('native buffered sends complete each media message without closing input or control', async () => {
|
|
297
|
+
const { direct, signal } = await connectedDirect();
|
|
298
|
+
try {
|
|
299
|
+
const peer = offer(signal);
|
|
300
|
+
const control = new FakeDataChannel('livedesk-control-v1');
|
|
301
|
+
const nativeSend = FakeDataChannel.prototype.sendMessageBinary;
|
|
302
|
+
const acceptBuffered = function (value) {
|
|
303
|
+
nativeSend.call(this, value);
|
|
304
|
+
this.buffered += value.byteLength;
|
|
305
|
+
return false; // libdatachannel accepted and buffered this exact chunk.
|
|
306
|
+
};
|
|
307
|
+
control.sendMessageBinary = acceptBuffered;
|
|
308
|
+
peer.emitDataChannel(control);
|
|
309
|
+
control.open();
|
|
310
|
+
assert.equal(peer.closed, false, 'buffered direct-ready is not a send failure');
|
|
311
|
+
assert.equal(JSON.parse(decodeWireMessages(control.sent)[0].data).type, 'direct-ready');
|
|
312
|
+
const input = new FakeDataChannel(`livedesk-ws-v1:${CHANNEL_ID_TWO}:input`);
|
|
313
|
+
peer.emitDataChannel(input);
|
|
314
|
+
input.open();
|
|
315
|
+
const lane = new FakeDataChannel(`livedesk-ws-v1:${CHANNEL_ID}:frame`);
|
|
316
|
+
lane.sendMessageBinary = acceptBuffered;
|
|
317
|
+
peer.emitDataChannel(lane);
|
|
318
|
+
lane.open();
|
|
319
|
+
sendWire(control, {
|
|
320
|
+
type: 'ws-open', connectionId: CONNECTION_ONE, hubEpoch: HUB_EPOCH,
|
|
321
|
+
channelId: CHANNEL_ID, purpose: 'frame', path: '/api/remote/frames/ws'
|
|
322
|
+
});
|
|
323
|
+
const local = FakeLocalSocket.instances[0];
|
|
324
|
+
local.open();
|
|
325
|
+
for (let frame = 0; frame < 30; frame += 1) {
|
|
326
|
+
lane.buffered = 0;
|
|
327
|
+
local.receive(Buffer.alloc(74 * 1024, frame), true);
|
|
328
|
+
assert.equal(lane.closed, false, `transient congestion must preserve frame ${frame}`);
|
|
329
|
+
}
|
|
330
|
+
const received = decodeWireMessages(lane.sent);
|
|
331
|
+
assert.equal(received.length, 30);
|
|
332
|
+
for (let frame = 0; frame < 30; frame += 1) {
|
|
333
|
+
assert.deepEqual(Buffer.from(received[frame].data), Buffer.alloc(74 * 1024, frame));
|
|
334
|
+
}
|
|
335
|
+
assert.equal(local.readyState, FakeSocket.OPEN);
|
|
336
|
+
assert.equal(input.closed, false);
|
|
337
|
+
assert.equal(control.closed, false);
|
|
338
|
+
assert.equal(peer.closed, false);
|
|
339
|
+
assert.equal(direct.inspect().dataChannelSendFailures, 0);
|
|
340
|
+
assert.equal(direct.inspect().dataChannelBackpressureCloses, 0);
|
|
341
|
+
lane.sendMessageBinary = () => { throw new Error('native-send-failed'); };
|
|
342
|
+
local.receive(Buffer.alloc(10), true);
|
|
343
|
+
assert.equal(lane.closed, true, 'an actual native exception still retires the exact lane');
|
|
344
|
+
assert.equal(control.closed, false);
|
|
345
|
+
assert.equal(input.closed, false);
|
|
346
|
+
assert.equal(direct.inspect().dataChannelSendFailures, 1);
|
|
347
|
+
} finally {
|
|
348
|
+
direct.close();
|
|
349
|
+
assert.equal(direct.inspect().resourceTimers, 0);
|
|
350
|
+
assert.equal(direct.inspect().retainedAssemblyBytes, 0);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
296
354
|
test('STUN-only peer answers and rejects stale owner callbacks after replacement', async () => {
|
|
297
355
|
const { direct, signal } = await connectedDirect();
|
|
298
356
|
const firstPeer = offer(signal, CONNECTION_ONE);
|
|
@@ -1449,7 +1507,7 @@ test('server session refresh preserves peers while logout uses the revoking clos
|
|
|
1449
1507
|
assert.doesNotMatch(logoutBlock, /hubConsoleDirect\?\.refresh\(\)/);
|
|
1450
1508
|
});
|
|
1451
1509
|
|
|
1452
|
-
test('native node-datachannel
|
|
1510
|
+
test('native node-datachannel buffered sends deliver the complete fragmented wire over loopback', { timeout: 8_000 }, async t => {
|
|
1453
1511
|
let offerer = null;
|
|
1454
1512
|
let answerer = null;
|
|
1455
1513
|
let offerChannel = null;
|
|
@@ -1458,6 +1516,7 @@ test('native node-datachannel peers carry the fragmented control wire over loopb
|
|
|
1458
1516
|
let openTimer = null;
|
|
1459
1517
|
let messageTimer = null;
|
|
1460
1518
|
try {
|
|
1519
|
+
nodeDataChannel.setSctpSettings({ sendBufferSize: 64 * 1024 });
|
|
1461
1520
|
offerer = new nodeDataChannel.PeerConnection('console-direct-native-offerer', { iceServers: [] });
|
|
1462
1521
|
answerer = new nodeDataChannel.PeerConnection('console-direct-native-answerer', { iceServers: [] });
|
|
1463
1522
|
offerer.onLocalDescription((sdp, type) => {
|
|
@@ -1494,17 +1553,23 @@ test('native node-datachannel peers carry the fragmented control wire over loopb
|
|
|
1494
1553
|
clearTimeout(openTimer);
|
|
1495
1554
|
openTimer = null;
|
|
1496
1555
|
const payload = JSON.stringify({ type: 'direct-ready', connectionId: CONNECTION_ONE, hubEpoch: HUB_EPOCH });
|
|
1497
|
-
const
|
|
1556
|
+
const expected = payload.repeat(20_000);
|
|
1557
|
+
const chunks = encodeDirectConsoleWireMessage(expected, {
|
|
1498
1558
|
messageId: 77,
|
|
1499
1559
|
kind: 'control'
|
|
1500
1560
|
});
|
|
1501
1561
|
assert.ok(chunks.length > 1);
|
|
1502
|
-
|
|
1562
|
+
let bufferedSends = 0;
|
|
1563
|
+
for (const chunk of chunks) {
|
|
1564
|
+
if (offerChannel.sendMessageBinary(Buffer.from(chunk)) === false) bufferedSends += 1;
|
|
1565
|
+
}
|
|
1566
|
+
assert.ok(bufferedSends > 0, 'the native congestion path must actually be exercised');
|
|
1503
1567
|
const message = await received;
|
|
1504
1568
|
clearTimeout(messageTimer);
|
|
1505
1569
|
messageTimer = null;
|
|
1506
1570
|
assert.equal(message.kind, 'control');
|
|
1507
|
-
assert.equal(message.data,
|
|
1571
|
+
assert.equal(message.data, expected);
|
|
1572
|
+
t.diagnostic(`native buffered sends=${bufferedSends}; complete bytes=${message.byteLength}; chunks=${chunks.length}`);
|
|
1508
1573
|
} finally {
|
|
1509
1574
|
if (openTimer) clearTimeout(openTimer);
|
|
1510
1575
|
if (messageTimer) clearTimeout(messageTimer);
|
|
@@ -1515,5 +1580,6 @@ test('native node-datachannel peers carry the fragmented control wire over loopb
|
|
|
1515
1580
|
try { answerer?.close(); } catch {}
|
|
1516
1581
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1517
1582
|
nodeDataChannel.cleanup();
|
|
1583
|
+
nodeDataChannel.setSctpSettings({});
|
|
1518
1584
|
}
|
|
1519
1585
|
});
|