@livedesk/hub 0.1.68 → 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 CHANGED
@@ -1,33 +1,33 @@
1
- {
2
- "name": "@livedesk/hub",
3
- "version": "0.1.68",
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
+ }
@@ -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
- if (channelState.channel.sendMessageBinary(nativeChunk) !== true) throw new Error('send-rejected');
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 peers carry the fragmented control wire over loopback', { timeout: 8_000 }, async () => {
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 chunks = encodeDirectConsoleWireMessage(payload.repeat(400), {
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
- for (const chunk of chunks) assert.equal(offerChannel.sendMessageBinary(Buffer.from(chunk)), true);
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, payload.repeat(400));
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
  });
@@ -420,7 +420,7 @@ export function createHubRelayControl({
420
420
  const host = safeText(
421
421
  env.LIVEDESK_RELAY_HOST
422
422
  || env.LIVEDESK_UDP_RENDEZVOUS_HOST
423
- || 'www.gnsoftech.com',
423
+ || 'p2p.gnsoftech.com',
424
424
  253);
425
425
  const port = boundedInteger(
426
426
  env.LIVEDESK_RELAY_PORT || env.LIVEDESK_UDP_RENDEZVOUS_PORT,
@@ -67,7 +67,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
67
67
  const bindHost = safeText(env.LIVEDESK_UDP_BIND_HOST || '0.0.0.0', 128) || '0.0.0.0';
68
68
  const requestedPort = numberEnv(env.LIVEDESK_UDP_PORT, 0, 65535, 0);
69
69
  const advertiseHost = safeText(env.LIVEDESK_UDP_ADVERTISE_HOST || env.LIVEDESK_REMOTE_PUBLIC_HOST || '', 128);
70
- const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'www.gnsoftech.com', 128);
70
+ const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'p2p.gnsoftech.com', 128);
71
71
  const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
72
72
  const rendezvousRefreshMs = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_REFRESH_MS, 1000, 50_000, 25_000);
73
73
  const punchTimeoutMs = numberEnv(env.LIVEDESK_UDP_PUNCH_TIMEOUT_MS, 500, 10_000, 3000);