@livedesk/hub 0.1.69 → 0.1.71

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.69",
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.71",
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
+ }
@@ -0,0 +1,80 @@
1
+ export const CONSOLE_MEDIA_ADMISSION = Object.freeze({
2
+ laneBufferedBytes: 512 * 1024,
3
+ peerBufferedBytes: 1024 * 1024,
4
+ bufferedAgeMs: 120,
5
+ maxStreams: 64,
6
+ maxMetadataBytes: 64 * 1024
7
+ });
8
+
9
+ function h264Identity(packet) {
10
+ if (!Buffer.isBuffer(packet) || packet.length < 5) return null;
11
+ const size = packet.readUInt32BE(0);
12
+ if (size < 2 || size > CONSOLE_MEDIA_ADMISSION.maxMetadataBytes || 4 + size >= packet.length) return null;
13
+ let frame;
14
+ try { frame = JSON.parse(packet.toString('utf8', 4, 4 + size)); } catch { return null; }
15
+ if (frame?.frameMode !== 'mode3-h264-hw'
16
+ || !['wall', 'control'].includes(frame.streamPurpose)
17
+ || !Number.isSafeInteger(frame.captureGeneration) || frame.captureGeneration <= 0
18
+ || !Number.isSafeInteger(frame.monitorIndex) || frame.monitorIndex < 0
19
+ || !['deviceId', 'sessionId', 'streamId', 'commandId'].every(key => (
20
+ typeof frame[key] === 'string' && frame[key].length > 0 && frame[key].length <= 256
21
+ ))) return null;
22
+ return {
23
+ stream: JSON.stringify([frame.deviceId, frame.streamPurpose]),
24
+ owner: JSON.stringify([frame.sessionId, frame.streamId, frame.commandId, frame.captureGeneration, frame.monitorIndex]),
25
+ key: frame.isKeyFrame === true || frame.chunkType === 'key'
26
+ };
27
+ }
28
+
29
+ // This is send admission, not authentication or presentation authority. Those
30
+ // exact binding gates still run at the Hub and browser. Retain no payload,
31
+ // timer, task, or outbound queue; every decision precedes wire fragmentation.
32
+ export function createConsoleFrameAdmission() {
33
+ const streams = new Map();
34
+ let bufferedSince = null;
35
+ let droppedFrames = 0;
36
+ let recoveredStreams = 0;
37
+ return {
38
+ admit(packet, { laneBufferedBytes, peerBufferedBytes, now }) {
39
+ const identity = h264Identity(packet);
40
+ if (!identity) return 'unmanaged';
41
+ let state = streams.get(identity.stream);
42
+ if (!state) {
43
+ if (streams.size >= CONSOLE_MEDIA_ADMISSION.maxStreams) return 'capacity';
44
+ state = { owner: identity.owner, awaitingKey: false };
45
+ streams.set(identity.stream, state);
46
+ } else if (state.owner !== identity.owner) {
47
+ state.owner = identity.owner;
48
+ state.awaitingKey = true;
49
+ }
50
+ if (laneBufferedBytes <= 0) bufferedSince = null;
51
+ else if (bufferedSince === null) bufferedSince = now;
52
+ const pressured = (laneBufferedBytes > 0 && (
53
+ laneBufferedBytes + packet.length > CONSOLE_MEDIA_ADMISSION.laneBufferedBytes
54
+ || now - bufferedSince >= CONSOLE_MEDIA_ADMISSION.bufferedAgeMs
55
+ )) || (peerBufferedBytes > 0
56
+ && peerBufferedBytes + packet.length > CONSOLE_MEDIA_ADMISSION.peerBufferedBytes);
57
+ if (pressured || (state.awaitingKey && !identity.key)) {
58
+ state.awaitingKey = true;
59
+ droppedFrames += 1;
60
+ return 'drop';
61
+ }
62
+ if (state.awaitingKey) recoveredStreams += 1;
63
+ state.awaitingKey = false;
64
+ return 'send';
65
+ },
66
+ inspect() {
67
+ return {
68
+ streamOwners: streams.size,
69
+ awaitingKeyStreams: [...streams.values()].filter(state => state.awaitingKey).length,
70
+ droppedFrames,
71
+ recoveredStreams,
72
+ retainedPayloadBytes: 0
73
+ };
74
+ },
75
+ clear() {
76
+ streams.clear();
77
+ bufferedSince = null;
78
+ }
79
+ };
80
+ }
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { CONSOLE_MEDIA_ADMISSION, createConsoleFrameAdmission } from './console-direct-frame-admission.mjs';
4
+ import { buildImmutableRemoteFramePacket } from './frame-packet-contract.mjs';
5
+
6
+ function packet({ key = false, device = 'a', generation = 1, monitor = 0, bytes = 1024 } = {}) {
7
+ return buildImmutableRemoteFramePacket({
8
+ deviceId: device, sessionId: 'session', streamId: 'stream', commandId: 'command',
9
+ captureGeneration: generation, monitorIndex: monitor, streamPurpose: 'wall',
10
+ frameMode: 'mode3-h264-hw', isKeyFrame: key
11
+ }, Buffer.alloc(bytes)).packet;
12
+ }
13
+ const empty = { laneBufferedBytes: 0, peerBufferedBytes: 0, now: 0 };
14
+
15
+ test('a small persistent backlog is bounded by age without a timer or retained frame', () => {
16
+ const gate = createConsoleFrameAdmission();
17
+ assert.equal(gate.admit(packet({ key: true }), empty), 'send');
18
+ const pending = { laneBufferedBytes: 1024, peerBufferedBytes: 1024, now: 1 };
19
+ assert.equal(gate.admit(packet(), pending), 'send');
20
+ assert.equal(gate.admit(packet(), { ...pending, now: 1 + CONSOLE_MEDIA_ADMISSION.bufferedAgeMs }), 'drop');
21
+ assert.equal(gate.admit(packet(), { ...empty, now: 200 }), 'drop');
22
+ assert.equal(gate.admit(packet({ key: true }), { ...empty, now: 201 }), 'send');
23
+ assert.equal(gate.inspect().retainedPayloadBytes, 0);
24
+ assert.equal(gate.inspect().recoveredStreams, 1);
25
+ gate.clear();
26
+ assert.equal(gate.inspect().streamOwners, 0);
27
+ assert.equal(gate.inspect().awaitingKeyStreams, 0);
28
+ });
29
+
30
+ test('a pressured viewer cannot poison another viewer, device, or replacement binding', () => {
31
+ const phone = createConsoleFrameAdmission();
32
+ const desktop = createConsoleFrameAdmission();
33
+ const pressure = { ...empty, laneBufferedBytes: CONSOLE_MEDIA_ADMISSION.laneBufferedBytes, peerBufferedBytes: CONSOLE_MEDIA_ADMISSION.laneBufferedBytes };
34
+ assert.equal(phone.admit(packet(), pressure), 'drop');
35
+ assert.equal(desktop.admit(packet(), empty), 'send');
36
+ assert.equal(phone.admit(packet({ device: 'b', key: true }), empty), 'send');
37
+ assert.equal(phone.admit(packet(), empty), 'drop');
38
+ assert.equal(phone.admit(packet({ generation: 2, monitor: 1 }), empty), 'drop');
39
+ assert.equal(phone.admit(packet({ generation: 2, monitor: 1, key: true }), empty), 'send');
40
+ assert.equal(phone.inspect().awaitingKeyStreams, 0);
41
+ });
42
+
43
+ test('one complete key may exceed the soft watermark on an empty peer without admitting a second backlog', () => {
44
+ const gate = createConsoleFrameAdmission();
45
+ const key = packet({ key: true, bytes: 700 * 1024 });
46
+ assert.equal(gate.admit(key, empty), 'send');
47
+ assert.equal(gate.admit(key, { ...empty, laneBufferedBytes: key.length, peerBufferedBytes: key.length }), 'drop');
48
+ assert.equal(gate.admit(key, empty), 'send');
49
+ assert.equal(gate.admit(Buffer.from('legacy-binary'), empty), 'unmanaged');
50
+ });
51
+
52
+ test('a multiplexed lane has a fixed owner bound and releases it on close', () => {
53
+ const gate = createConsoleFrameAdmission();
54
+ for (let n = 0; n < CONSOLE_MEDIA_ADMISSION.maxStreams; n++) {
55
+ assert.equal(gate.admit(packet({ device: `device-${n}`, key: true }), empty), 'send');
56
+ }
57
+ assert.equal(gate.admit(packet({ device: 'overflow', key: true }), empty), 'capacity');
58
+ gate.clear();
59
+ assert.equal(gate.admit(packet({ device: 'replacement', key: true }), empty), 'send');
60
+ });
@@ -9,6 +9,7 @@ import {
9
9
  encodeDirectConsoleWireMessage
10
10
  } from '../../runtime-core/src/console-direct-wire.js';
11
11
  import { workspaceRoleCanControl, workspaceRoleCanRequest } from './auth/workspace-access.js';
12
+ import { createConsoleFrameAdmission } from './console-direct-frame-admission.mjs';
12
13
 
13
14
  const DEFAULT_SIGNAL_URL = 'https://livedesk-wake.lovecrdm.workers.dev';
14
15
  const DEFAULT_STUN_URLS = Object.freeze(['stun:stun.cloudflare.com:3478']);
@@ -87,6 +88,34 @@ function byteLength(value) {
87
88
  return Buffer.byteLength(String(value || ''), 'utf8');
88
89
  }
89
90
 
91
+ function optionalPeerMetric(peer, method) {
92
+ try {
93
+ const value = peer[method]?.();
94
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
95
+ } catch { return null; }
96
+ }
97
+
98
+ function candidateAddressScope(value) {
99
+ if (typeof value !== 'string' || !value || value === '?') return 'unknown';
100
+ if (/^(127\.|::1$)/i.test(value)) return 'loopback';
101
+ if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|f[cd][0-9a-f]{2}:|fe80:)/i.test(value)) return 'private';
102
+ return 'public';
103
+ }
104
+
105
+ function inspectPeerPath(peer) {
106
+ try {
107
+ const pair = peer.getSelectedCandidatePair?.();
108
+ if (!pair?.local || !pair?.remote) return null;
109
+ return {
110
+ localType: String(pair.local.type || 'unknown').slice(0, 24),
111
+ remoteType: String(pair.remote.type || 'unknown').slice(0, 24),
112
+ localScope: candidateAddressScope(pair.local.address),
113
+ remoteScope: candidateAddressScope(pair.remote.address),
114
+ transport: String(pair.remote.transportType || 'unknown').slice(0, 24)
115
+ };
116
+ } catch { return null; }
117
+ }
118
+
90
119
  function parseJson(value) {
91
120
  try {
92
121
  return JSON.parse(String(value || ''));
@@ -527,6 +556,7 @@ export function createHubConsoleDirect(options = {}) {
527
556
  disposePendingLogicalControl(owner, channelState.channelId);
528
557
  disposeAssemblerTimer(channelState);
529
558
  channelState.assembler.dispose();
559
+ channelState.frameAdmission?.clear();
530
560
  closeLocalSocket(
531
561
  owner,
532
562
  channelState,
@@ -667,6 +697,18 @@ export function createHubConsoleDirect(options = {}) {
667
697
 
668
698
  function sendWire(owner, channelState, kind, value, maxMessageBytes, options = {}) {
669
699
  if (!isOwnerActive(owner) || channelState.closed || !channelIsOpen(channelState.channel)) return false;
700
+ if (kind === 'binary' && channelState.frameAdmission && value.byteLength <= maxMessageBytes) {
701
+ const admission = channelState.frameAdmission.admit(value, {
702
+ laneBufferedBytes: channelBufferedAmount(channelState),
703
+ peerBufferedBytes: ownerBufferedAmount(owner),
704
+ now: performance.now()
705
+ });
706
+ if (admission === 'drop') return true;
707
+ if (admission === 'capacity') {
708
+ closeLogicalChannel(owner, channelState, { code: 1009, reason: 'console-direct-frame-owner-capacity' });
709
+ return false;
710
+ }
711
+ }
670
712
  let chunks;
671
713
  try {
672
714
  const messageId = channelState.nextSendMessageId >>> 0;
@@ -739,7 +781,10 @@ export function createHubConsoleDirect(options = {}) {
739
781
  // Node Buffer shape. Share the exact encoded bytes without copying or
740
782
  // retaining a second application queue.
741
783
  const nativeChunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
742
- if (channelState.channel.sendMessageBinary(nativeChunk) !== true) throw new Error('send-rejected');
784
+ // libdatachannel returns false when it accepted the chunk into its
785
+ // bounded send buffer. Only an exception means this send failed.
786
+ // Retrying duplicates a fragment; closing here truncates the frame.
787
+ channelState.channel.sendMessageBinary(nativeChunk);
743
788
  } catch (error) {
744
789
  dataChannelSendFailures += 1;
745
790
  lastDataChannelSendFailure = Object.freeze({
@@ -970,7 +1015,7 @@ export function createHubConsoleDirect(options = {}) {
970
1015
  socket.on('message', (data, isBinary) => {
971
1016
  if (!isOwnerActive(owner) || owner.channels.get(channelId) !== channelState || channelState.localSocket !== socket) return;
972
1017
  const kind = isBinary ? 'binary' : 'text';
973
- const value = isBinary ? new Uint8Array(Buffer.from(data)) : String(data);
1018
+ const value = isBinary ? (Buffer.isBuffer(data) ? data : Buffer.from(data)) : String(data);
974
1019
  if (!sendWire(owner, channelState, kind, value, channelState.maxMessageBytes)) {
975
1020
  closeLogicalChannel(owner, channelState, {
976
1021
  code: 1013,
@@ -1179,6 +1224,7 @@ export function createHubConsoleDirect(options = {}) {
1179
1224
  nextSendMessageId: 1,
1180
1225
  openRequested: false,
1181
1226
  localSocket: null,
1227
+ frameAdmission: identity.purpose === 'frame' ? createConsoleFrameAdmission() : null,
1182
1228
  closed: false
1183
1229
  };
1184
1230
  owner.channels.set(identity.channelId, channelState);
@@ -1935,6 +1981,7 @@ export function createHubConsoleDirect(options = {}) {
1935
1981
  };
1936
1982
 
1937
1983
  const inspect = () => {
1984
+ const peerDiagnostics = [];
1938
1985
  let logicalWebSocketChannels = 0;
1939
1986
  let localWebSocketChannels = 0;
1940
1987
  let pendingHttpRequests = 0;
@@ -1961,6 +2008,30 @@ export function createHubConsoleDirect(options = {}) {
1961
2008
  }
1962
2009
  retainedAssemblyBytes += Number(owner.wireBudget.inspect().retainedBytes || 0);
1963
2010
  bufferedSendBytes += ownerBufferedAmount(owner);
2011
+ peerDiagnostics.push({
2012
+ consoleId: owner.consoleId,
2013
+ connectionId: owner.connectionId,
2014
+ generation: owner.generation,
2015
+ peerState: owner.peerState,
2016
+ iceState: owner.iceState,
2017
+ rttMs: optionalPeerMetric(owner.peer, 'rtt'),
2018
+ bytesSent: optionalPeerMetric(owner.peer, 'bytesSent'),
2019
+ bytesReceived: optionalPeerMetric(owner.peer, 'bytesReceived'),
2020
+ selectedPath: inspectPeerPath(owner.peer),
2021
+ bufferedSendBytes: (() => {
2022
+ const values = [owner.control, ...owner.channels.values()]
2023
+ .filter(lane => lane && !lane.closed)
2024
+ .map(lane => optionalPeerMetric(lane.channel, 'bufferedAmount'));
2025
+ return values.some(value => value === null) ? null : values.reduce((sum, value) => sum + value, 0);
2026
+ })(),
2027
+ pendingHttpRequests: owner.pendingHttp.size,
2028
+ lanes: [...owner.channels.values()].map(lane => ({
2029
+ channelId: lane.channelId,
2030
+ purpose: lane.purpose,
2031
+ bufferedBytes: optionalPeerMetric(lane.channel, 'bufferedAmount'),
2032
+ frameAdmission: lane.frameAdmission?.inspect() ?? null
2033
+ }))
2034
+ });
1964
2035
  }
1965
2036
  const reconnectTimerActive = Boolean(retryTimer);
1966
2037
  const accessTokenDeadlineActive = Boolean(accessTokenAttempt);
@@ -1979,6 +2050,7 @@ export function createHubConsoleDirect(options = {}) {
1979
2050
  pendingLogicalControlRequests,
1980
2051
  retainedAssemblyBytes,
1981
2052
  bufferedSendBytes,
2053
+ peerDiagnostics,
1982
2054
  iceConnectTimers,
1983
2055
  peerDisconnectTimers,
1984
2056
  assemblyDeadlineTimers,
@@ -9,6 +9,7 @@ import {
9
9
  } from '../../runtime-core/src/console-direct-wire.js';
10
10
  import { consoleDirectContract, createHubConsoleDirect } from './console-direct.js';
11
11
  import { workspaceRoleCanRequest } from './auth/workspace-access.js';
12
+ import { buildImmutableRemoteFramePacket } from './frame-packet-contract.mjs';
12
13
 
13
14
  assert.equal(consoleDirectContract.maxDefaultRetryDelayMs, 20_000);
14
15
  assert.equal(consoleDirectContract.peerDisconnectedTimeoutMs, 5_000);
@@ -293,6 +294,64 @@ function offer(signal, connectionId = CONNECTION_ONE, sdp = 'v=0\r\na=fake-offer
293
294
  return FakePeerConnection.instances.at(-1);
294
295
  }
295
296
 
297
+ test('native buffered sends complete each media message without closing input or control', async () => {
298
+ const { direct, signal } = await connectedDirect();
299
+ try {
300
+ const peer = offer(signal);
301
+ const control = new FakeDataChannel('livedesk-control-v1');
302
+ const nativeSend = FakeDataChannel.prototype.sendMessageBinary;
303
+ const acceptBuffered = function (value) {
304
+ nativeSend.call(this, value);
305
+ this.buffered += value.byteLength;
306
+ return false; // libdatachannel accepted and buffered this exact chunk.
307
+ };
308
+ control.sendMessageBinary = acceptBuffered;
309
+ peer.emitDataChannel(control);
310
+ control.open();
311
+ assert.equal(peer.closed, false, 'buffered direct-ready is not a send failure');
312
+ assert.equal(JSON.parse(decodeWireMessages(control.sent)[0].data).type, 'direct-ready');
313
+ const input = new FakeDataChannel(`livedesk-ws-v1:${CHANNEL_ID_TWO}:input`);
314
+ peer.emitDataChannel(input);
315
+ input.open();
316
+ const lane = new FakeDataChannel(`livedesk-ws-v1:${CHANNEL_ID}:frame`);
317
+ lane.sendMessageBinary = acceptBuffered;
318
+ peer.emitDataChannel(lane);
319
+ lane.open();
320
+ sendWire(control, {
321
+ type: 'ws-open', connectionId: CONNECTION_ONE, hubEpoch: HUB_EPOCH,
322
+ channelId: CHANNEL_ID, purpose: 'frame', path: '/api/remote/frames/ws'
323
+ });
324
+ const local = FakeLocalSocket.instances[0];
325
+ local.open();
326
+ for (let frame = 0; frame < 30; frame += 1) {
327
+ lane.buffered = 0;
328
+ local.receive(Buffer.alloc(74 * 1024, frame), true);
329
+ assert.equal(lane.closed, false, `transient congestion must preserve frame ${frame}`);
330
+ }
331
+ const received = decodeWireMessages(lane.sent);
332
+ assert.equal(received.length, 30);
333
+ for (let frame = 0; frame < 30; frame += 1) {
334
+ assert.deepEqual(Buffer.from(received[frame].data), Buffer.alloc(74 * 1024, frame));
335
+ }
336
+ assert.equal(local.readyState, FakeSocket.OPEN);
337
+ assert.equal(input.closed, false);
338
+ assert.equal(control.closed, false);
339
+ assert.equal(peer.closed, false);
340
+ assert.equal(direct.inspect().dataChannelSendFailures, 0);
341
+ assert.equal(direct.inspect().dataChannelBackpressureCloses, 0);
342
+ lane.sendMessageBinary = () => { throw new Error('native-send-failed'); };
343
+ local.receive(Buffer.alloc(10), true);
344
+ assert.equal(lane.closed, true, 'an actual native exception still retires the exact lane');
345
+ assert.equal(control.closed, false);
346
+ assert.equal(input.closed, false);
347
+ assert.equal(direct.inspect().dataChannelSendFailures, 1);
348
+ } finally {
349
+ direct.close();
350
+ assert.equal(direct.inspect().resourceTimers, 0);
351
+ assert.equal(direct.inspect().retainedAssemblyBytes, 0);
352
+ }
353
+ });
354
+
296
355
  test('STUN-only peer answers and rejects stale owner callbacks after replacement', async () => {
297
356
  const { direct, signal } = await connectedDirect();
298
357
  const firstPeer = offer(signal, CONNECTION_ONE);
@@ -1278,6 +1337,64 @@ test('media pressure retires only its stale logical lane while preserving the co
1278
1337
  direct.close();
1279
1338
  });
1280
1339
 
1340
+ test('a pressured H.264 viewer drops whole dependent frames and resumes on a key without reconnecting', async () => {
1341
+ const { direct, signal } = await connectedDirect();
1342
+ const peer = offer(signal);
1343
+ const control = new FakeDataChannel('livedesk-control-v1');
1344
+ peer.emitDataChannel(control);
1345
+ control.open();
1346
+ const lane = new FakeDataChannel(`livedesk-ws-v1:${CHANNEL_ID}:frame`);
1347
+ peer.emitDataChannel(lane);
1348
+ lane.open();
1349
+ sendWire(control, {
1350
+ type: 'ws-open', connectionId: CONNECTION_ONE, hubEpoch: HUB_EPOCH,
1351
+ channelId: CHANNEL_ID, purpose: 'frame', path: '/api/remote/frames/ws'
1352
+ });
1353
+ const local = FakeLocalSocket.instances[0];
1354
+ local.open();
1355
+ const frame = (sequence, key, generation = 1) => buildImmutableRemoteFramePacket({
1356
+ deviceId: 'device-a', sessionId: 'session-a', streamId: 'wall-a', commandId: 'command-a',
1357
+ captureGeneration: generation, monitorIndex: 0, streamPurpose: 'wall',
1358
+ frameMode: 'mode3-h264-hw', chunkType: key ? 'key' : 'delta',
1359
+ isKeyFrame: key, streamFrameSeq: sequence
1360
+ }, Buffer.alloc(32 * 1024)).packet;
1361
+ local.receive(frame(1, true));
1362
+ const initialChunks = lane.sent.length;
1363
+ lane.buffered = consoleDirectContract.controlHeadroomBufferedBytes;
1364
+ local.receive(frame(2, false));
1365
+ assert.equal(lane.closed, false, 'congestion must not create another logical handshake or key-frame burst');
1366
+ assert.equal(lane.sent.length, initialChunks, 'no fragment of a dropped frame enters SCTP');
1367
+ lane.buffered = 0;
1368
+ local.receive(frame(3, false));
1369
+ assert.equal(lane.sent.length, initialChunks, 'a dependent delta cannot follow a discarded delta');
1370
+ local.receive(frame(4, true));
1371
+ assert.ok(lane.sent.length > initialChunks);
1372
+ const recoveredChunks = lane.sent.length;
1373
+ local.receive(frame(5, false));
1374
+ assert.ok(lane.sent.length > recoveredChunks);
1375
+ assert.equal(control.closed, false);
1376
+ assert.equal(peer.closed, false);
1377
+ assert.equal(direct.inspect().dataChannelBackpressureCloses, 0);
1378
+ const diagnostics = direct.inspect().peerDiagnostics[0];
1379
+ assert.equal(diagnostics.rttMs, null, 'missing native RTT is unknown');
1380
+ assert.equal(diagnostics.bytesSent, null, 'missing native byte counters are unknown');
1381
+ assert.equal(diagnostics.lanes[0].frameAdmission.droppedFrames, 2);
1382
+ assert.equal(diagnostics.lanes[0].frameAdmission.recoveredStreams, 1);
1383
+ peer.rtt = () => 12;
1384
+ peer.bytesSent = () => 1234;
1385
+ peer.selectedPair = {
1386
+ local: { type: 'host', address: '192.168.0.4', candidate: 'must-not-leak' },
1387
+ remote: { type: 'host', address: '192.168.0.8', transportType: 'Udp' }
1388
+ };
1389
+ const measured = direct.inspect().peerDiagnostics[0];
1390
+ assert.equal(measured.rttMs, 12);
1391
+ assert.equal(measured.bytesSent, 1234);
1392
+ assert.equal(measured.selectedPath.remoteScope, 'private');
1393
+ assert.doesNotMatch(JSON.stringify(measured), /192\.168\.|must-not-leak/);
1394
+ direct.close();
1395
+ assert.deepEqual(direct.inspect().peerDiagnostics, []);
1396
+ });
1397
+
1281
1398
  test('a reliable control response drops stale media backlog without retiring the peer', async () => {
1282
1399
  const { direct, signal } = await connectedDirect({
1283
1400
  fetchImpl: async () => new Response('{"online":true}', {
@@ -1449,7 +1566,7 @@ test('server session refresh preserves peers while logout uses the revoking clos
1449
1566
  assert.doesNotMatch(logoutBlock, /hubConsoleDirect\?\.refresh\(\)/);
1450
1567
  });
1451
1568
 
1452
- test('native node-datachannel peers carry the fragmented control wire over loopback', { timeout: 8_000 }, async () => {
1569
+ test('native node-datachannel buffered sends deliver the complete fragmented wire over loopback', { timeout: 8_000 }, async t => {
1453
1570
  let offerer = null;
1454
1571
  let answerer = null;
1455
1572
  let offerChannel = null;
@@ -1458,6 +1575,7 @@ test('native node-datachannel peers carry the fragmented control wire over loopb
1458
1575
  let openTimer = null;
1459
1576
  let messageTimer = null;
1460
1577
  try {
1578
+ nodeDataChannel.setSctpSettings({ sendBufferSize: 64 * 1024 });
1461
1579
  offerer = new nodeDataChannel.PeerConnection('console-direct-native-offerer', { iceServers: [] });
1462
1580
  answerer = new nodeDataChannel.PeerConnection('console-direct-native-answerer', { iceServers: [] });
1463
1581
  offerer.onLocalDescription((sdp, type) => {
@@ -1494,17 +1612,23 @@ test('native node-datachannel peers carry the fragmented control wire over loopb
1494
1612
  clearTimeout(openTimer);
1495
1613
  openTimer = null;
1496
1614
  const payload = JSON.stringify({ type: 'direct-ready', connectionId: CONNECTION_ONE, hubEpoch: HUB_EPOCH });
1497
- const chunks = encodeDirectConsoleWireMessage(payload.repeat(400), {
1615
+ const expected = payload.repeat(20_000);
1616
+ const chunks = encodeDirectConsoleWireMessage(expected, {
1498
1617
  messageId: 77,
1499
1618
  kind: 'control'
1500
1619
  });
1501
1620
  assert.ok(chunks.length > 1);
1502
- for (const chunk of chunks) assert.equal(offerChannel.sendMessageBinary(Buffer.from(chunk)), true);
1621
+ let bufferedSends = 0;
1622
+ for (const chunk of chunks) {
1623
+ if (offerChannel.sendMessageBinary(Buffer.from(chunk)) === false) bufferedSends += 1;
1624
+ }
1625
+ assert.ok(bufferedSends > 0, 'the native congestion path must actually be exercised');
1503
1626
  const message = await received;
1504
1627
  clearTimeout(messageTimer);
1505
1628
  messageTimer = null;
1506
1629
  assert.equal(message.kind, 'control');
1507
- assert.equal(message.data, payload.repeat(400));
1630
+ assert.equal(message.data, expected);
1631
+ t.diagnostic(`native buffered sends=${bufferedSends}; complete bytes=${message.byteLength}; chunks=${chunks.length}`);
1508
1632
  } finally {
1509
1633
  if (openTimer) clearTimeout(openTimer);
1510
1634
  if (messageTimer) clearTimeout(messageTimer);
@@ -1515,5 +1639,6 @@ test('native node-datachannel peers carry the fragmented control wire over loopb
1515
1639
  try { answerer?.close(); } catch {}
1516
1640
  await new Promise(resolve => setTimeout(resolve, 100));
1517
1641
  nodeDataChannel.cleanup();
1642
+ nodeDataChannel.setSctpSettings({});
1518
1643
  }
1519
1644
  });
@@ -479,7 +479,7 @@ function parseBorrowLifecycleFramePacket(data) {
479
479
  }
480
480
  }
481
481
 
482
- test('the browser Wall borrower follows exact Control key frames without owning native capture', {
482
+ test('Hub and phone Wall borrowers follow Control without stopping or retaining each other', {
483
483
  timeout: 20_000
484
484
  }, async () => {
485
485
  const {
@@ -563,7 +563,7 @@ test('the browser Wall borrower follows exact Control key frames without owning
563
563
  ).toString('base64')
564
564
  });
565
565
 
566
- const connectBorrower = async label => {
566
+ const connectBorrower = async (label, options = {}) => {
567
567
  const socket = new WebSocket(
568
568
  `ws://127.0.0.1:${httpPort}/api/remote/frames/ws?devices=${encodeURIComponent(deviceId)}`,
569
569
  { headers: { Origin: baseUrl }, perMessageDeflate: false }
@@ -603,7 +603,8 @@ test('the browser Wall borrower follows exact Control key frames without owning
603
603
  maxWidth: 960,
604
604
  maxHeight: 540,
605
605
  quality: 55,
606
- monitorIndex: 0
606
+ monitorIndex: 0,
607
+ ...options
607
608
  }));
608
609
  return { socket, observed, subscribeJsonIndex };
609
610
  };
@@ -799,6 +800,20 @@ test('the browser Wall borrower follows exact Control key frames without owning
799
800
  assert.equal(firstFrame.streamPurpose, 'control');
800
801
  assert.equal(firstFrame.monitorIndex, 0);
801
802
 
803
+ const hubBorrower = await connectBorrower('desktop Hub', {
804
+ fps: 30, maxWidth: 1920, maxHeight: 1080
805
+ });
806
+ await waitForBorrowStart(hubBorrower.observed, initialControl.commandId,
807
+ hubBorrower.subscribeJsonIndex, 'desktop Hub borrows the same Control owner');
808
+ sendControlFrame(initialControl, 4, true);
809
+ await waitForExactReady(hubBorrower.observed, initialControl, 0, 'desktop Hub key readiness');
810
+ const otherMonitor = await connectBorrower('other monitor', { monitorIndex: 1 });
811
+ await waitForBorrowLifecycle(() => otherMonitor.observed.json.some(message =>
812
+ JSON.stringify(message).includes('CONTROL_CAPTURE_OWNS_DEVICE')),
813
+ 'different monitor reports ownership conflict');
814
+ assert.equal(otherMonitor.observed.frames.length, 0, 'never substitute a different monitor');
815
+ otherMonitor.socket.close();
816
+
802
817
  const firstStopCommandIndex = agentMessages.length;
803
818
  const stopInitialPromise = postJson(
804
819
  `/api/remote/devices/${encodeURIComponent(deviceId)}/live/stop`,
@@ -855,6 +870,8 @@ test('the browser Wall borrower follows exact Control key frames without owning
855
870
  const replacementFrameIndex = firstBorrower.observed.frames.length;
856
871
  sendOpen(replacementControl);
857
872
  sendControlFrame(replacementControl, 1, true);
873
+ await waitForBorrowStart(hubBorrower.observed, replacementControl.commandId,
874
+ 0, 'desktop Hub follows the replacement generation');
858
875
  const replacementBorrowStart = await waitForBorrowStart(
859
876
  firstBorrower.observed,
860
877
  replacementControl.commandId,
@@ -878,6 +895,8 @@ test('the browser Wall borrower follows exact Control key frames without owning
878
895
  await new Promise(resolveWait => setTimeout(resolveWait, 60));
879
896
  assert.equal(firstBorrower.observed.frames.length, replacementFrameIndex);
880
897
  sendControlFrame(replacementControl, 3, true);
898
+ await waitForExactReady(hubBorrower.observed, replacementControl, 0,
899
+ 'desktop Hub replacement key readiness');
881
900
  await waitForExactReady(
882
901
  firstBorrower.observed,
883
902
  replacementControl,
@@ -915,6 +934,10 @@ test('the browser Wall borrower follows exact Control key frames without owning
915
934
  stopCountBeforeBorrowerClose,
916
935
  'Closing a read-only borrower must never stop the native Control owner.'
917
936
  );
937
+ sendControlFrame(replacementControl, 4, true);
938
+ await waitForBorrowLifecycle(() => hubBorrower.observed.frames.some(frame =>
939
+ frame.commandId === replacementControl.commandId && frame.frameSeq === 4),
940
+ 'desktop Hub keeps receiving after phone borrower closes');
918
941
 
919
942
  const fallbackBorrower = await connectBorrower('fallback borrower');
920
943
  await waitForBorrowStart(
@@ -924,7 +947,7 @@ test('the browser Wall borrower follows exact Control key frames without owning
924
947
  'fallback borrower Control binding'
925
948
  );
926
949
  const fallbackReadyIndex = fallbackBorrower.observed.json.length;
927
- sendControlFrame(replacementControl, 4, true);
950
+ sendControlFrame(replacementControl, 5, true);
928
951
  await waitForExactReady(
929
952
  fallbackBorrower.observed,
930
953
  replacementControl,
@@ -984,6 +1007,11 @@ test('the browser Wall borrower follows exact Control key frames without owning
984
1007
  'ordinary Wall browser binding after confirmed Control stop'
985
1008
  );
986
1009
  assert.equal(fallbackWallStart.started[0].presentationPurpose, '');
1010
+ await waitForBorrowLifecycle(() => hubBorrower.observed.json.some(message =>
1011
+ message.type === 'RemoteFrameLiveAutoStart'
1012
+ && message.started?.some(item => item.commandId === fallbackWallCommand.message.commandId
1013
+ && item.streamPurpose === 'wall' && item.readOnlyControlBorrow !== true)),
1014
+ 'desktop Hub and phone both restore the ordinary Wall owner');
987
1015
  } catch (error) {
988
1016
  error.message += hubOutput.length > 0
989
1017
  ? `\nIsolated Hub output:\n${hubOutput.join('\n')}`