livedesk 0.1.436 → 0.1.438

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/hub/src/server.js CHANGED
@@ -61,7 +61,7 @@ const runtimeManager = createHubRuntime({
61
61
  });
62
62
  runtimeManager.emit('runtime.started', { role: runtimeRole, pid: process.pid });
63
63
  const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 8 * 1024 * 1024);
64
- const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 4);
64
+ const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 8);
65
65
  const controlFrameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_CONTROL_FRAME_WS_QUEUE_PACKETS', 2);
66
66
  const frameStreamStopGraceMs = readPositiveIntegerEnv('LIVEDESK_FRAME_STREAM_STOP_GRACE_MS', 1500);
67
67
  const mode5FrameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_BACKPRESSURE_BYTES', 2 * 1024 * 1024);
@@ -1821,11 +1821,21 @@ function enqueueFramePacketForClient(ws, packet, meta) {
1821
1821
  ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
1822
1822
  }
1823
1823
  }
1824
- while (lane.queue.length >= frameClientQueueLimit(ws)) {
1825
- dropQueuedFrameForLane(lane);
1826
- ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
1827
- }
1828
- lane.queue.push({
1824
+ while (lane.queue.length >= frameClientQueueLimit(ws)) {
1825
+ dropQueuedFrameForLane(lane);
1826
+ ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
1827
+ }
1828
+ // Queue eviction can put this H.264 device into key-frame recovery after
1829
+ // the pre-overflow check above. Never enqueue the current dependent delta
1830
+ // while the decoder is waiting for a fresh recovery key frame.
1831
+ if (meta.isH264 && meta.isKeyFrame) {
1832
+ lane.awaitingKeyFrames.delete(meta.deviceId);
1833
+ } else if (meta.isH264 && lane.awaitingKeyFrames.has(meta.deviceId)) {
1834
+ lane.dropped += 1;
1835
+ ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
1836
+ return false;
1837
+ }
1838
+ lane.queue.push({
1829
1839
  packet,
1830
1840
  deviceId: meta.deviceId,
1831
1841
  frameSeq: meta.frameSeq,
@@ -2043,10 +2053,28 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
2043
2053
  agentPaceMs: Number(frame.agentPaceMs || 0) || 0,
2044
2054
  agentQueueBeforePaceMs: Number(frame.agentQueueBeforePaceMs || 0) || 0,
2045
2055
  agentFrameAgeMs: Number(frame.agentFrameAgeMs || 0) || 0,
2046
- droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
2047
- hubIngressDropped: Number(frame.hubIngressDropped || 0) || 0,
2048
- hubClientDropped: Number(diagnostics.hubClientDropped || 0) || 0,
2049
- hardwareEncoder: frame.hardwareEncoder || '',
2056
+ droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
2057
+ hubIngressDropped: Number(frame.hubIngressDropped || 0) || 0,
2058
+ hubClientDropped: Number(diagnostics.hubClientDropped || 0) || 0,
2059
+ hubClientQueueDepth: Number(diagnostics.hubClientQueueDepth || 0) || 0,
2060
+ hubClientQueueLimit: Number(diagnostics.hubClientQueueLimit || 0) || 0,
2061
+ hubClientBufferedBytes: Number(diagnostics.hubClientBufferedBytes || 0) || 0,
2062
+ udpIncompleteFrames: Number(frame.udpIncompleteFrames || 0) || 0,
2063
+ transport: frame.transport || '',
2064
+ videoTransport: frame.videoTransport || (frame.transport === 'udp-p2p'
2065
+ ? 'p2p-udp'
2066
+ : frame.transport === 'ws-binary'
2067
+ ? 'direct-ws'
2068
+ : 'direct-tcp'),
2069
+ frameTransportActive: frame.frameTransportActive || '',
2070
+ frameTransportProtocol: frame.frameTransportProtocol || '',
2071
+ frameTransportState: frame.frameTransportState || '',
2072
+ frameTransportP2pReady: frame.frameTransportP2pReady === true,
2073
+ frameTransportReason: frame.frameTransportReason || '',
2074
+ frameTransportChangedAt: frame.frameTransportChangedAt || '',
2075
+ frameTransportSwitchCount: Number(frame.frameTransportSwitchCount || 0) || 0,
2076
+ frameTransportTcpFailureCount: Number(frame.frameTransportTcpFailureCount || 0) || 0,
2077
+ hardwareEncoder: frame.hardwareEncoder || '',
2050
2078
  platformProfile: frame.platformProfile || '',
2051
2079
  monitorIndex: Number.isFinite(Number(frame.monitorIndex)) ? Number(frame.monitorIndex) : 0,
2052
2080
  monitorCount: Number.isFinite(Number(frame.monitorCount)) ? Number(frame.monitorCount) : 1,
@@ -2157,7 +2185,6 @@ function broadcastRemoteBinaryFrame(frameEvent) {
2157
2185
  const isMode5 = String(frame.frameMode || frame.mode || '').toLowerCase() === 'mode5-lzo-delta';
2158
2186
  const isKeyFrame = frame.isKeyFrame === true || String(frame.chunkType || '').toLowerCase() === 'key';
2159
2187
  const frameSeq = Number(frame.frameSeq || 0) || 0;
2160
- let sharedPacket = null;
2161
2188
  for (const client of targetClients) {
2162
2189
  if (client.readyState !== client.OPEN) {
2163
2190
  continue;
@@ -2197,13 +2224,18 @@ function broadcastRemoteBinaryFrame(frameEvent) {
2197
2224
  client.liveDeskFrameBackpressureDrops = Number(client.liveDeskFrameBackpressureDrops || 0) + 1;
2198
2225
  continue;
2199
2226
  }
2200
- sharedPacket ||= buildRemoteFrameBinaryPacket(frameEvent);
2201
- if (!sharedPacket) {
2227
+ const packet = buildRemoteFrameBinaryPacket(frameEvent, {
2228
+ hubClientDropped: lane.dropped,
2229
+ hubClientQueueDepth: lane.queue.length,
2230
+ hubClientQueueLimit: frameClientQueueLimit(client),
2231
+ hubClientBufferedBytes: Number(client.bufferedAmount || 0) || 0
2232
+ });
2233
+ if (!packet) {
2202
2234
  continue;
2203
2235
  }
2204
- enqueueFramePacketForClient(client, sharedPacket, { deviceId, frameSeq, isH264, isMode5, isKeyFrame });
2205
- }
2206
- }
2236
+ enqueueFramePacketForClient(client, packet, { deviceId, frameSeq, isH264, isMode5, isKeyFrame });
2237
+ }
2238
+ }
2207
2239
 
2208
2240
  function broadcastRemoteBinaryAudio(audioEvent) {
2209
2241
  const deviceId = String(audioEvent?.deviceId || audioEvent?.frame?.deviceId || '').trim();
@@ -4034,12 +4066,26 @@ app.post('/api/remote/devices/:deviceId/live/start', requireHubFeatureAccess, (r
4034
4066
  res.json(remoteHub.startLiveStream(req.params.deviceId, req.body || {}));
4035
4067
  });
4036
4068
 
4037
- app.post('/api/remote/devices/:deviceId/live/stop', (req, res) => {
4038
- noStore(res);
4039
- res.json(remoteHub.stopLiveStream(req.params.deviceId, req.body || {}));
4040
- });
4041
-
4042
- app.post('/api/remote/devices/:deviceId/audio/start', requireHubFeatureAccess, (req, res) => {
4069
+ app.post('/api/remote/devices/:deviceId/live/stop', (req, res) => {
4070
+ noStore(res);
4071
+ res.json(remoteHub.stopLiveStream(req.params.deviceId, req.body || {}));
4072
+ });
4073
+
4074
+ app.post('/api/remote/devices/:deviceId/live/pause', requireHubFeatureAccess, async (req, res, next) => {
4075
+ noStore(res);
4076
+ try {
4077
+ res.json(await remoteHub.pauseLiveCapture(req.params.deviceId, req.body || {}));
4078
+ } catch (error) {
4079
+ next(error);
4080
+ }
4081
+ });
4082
+
4083
+ app.post('/api/remote/devices/:deviceId/live/resume', requireHubFeatureAccess, (req, res) => {
4084
+ noStore(res);
4085
+ res.json(remoteHub.resumeLiveCapture(req.params.deviceId, req.body || {}));
4086
+ });
4087
+
4088
+ app.post('/api/remote/devices/:deviceId/audio/start', requireHubFeatureAccess, (req, res) => {
4043
4089
  noStore(res);
4044
4090
  res.json(remoteHub.startAudioStream(req.params.deviceId, req.body || {}));
4045
4091
  });
@@ -4203,9 +4249,10 @@ audioWss.on('connection', (ws, req) => {
4203
4249
  });
4204
4250
  });
4205
4251
 
4206
- inputWss.on('connection', ws => {
4207
- inputClients.add(ws);
4208
- ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
4252
+ inputWss.on('connection', ws => {
4253
+ inputClients.add(ws);
4254
+ ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
4255
+ ws.liveDeskInputDeviceIds = new Set();
4209
4256
  try {
4210
4257
  ws._socket?.setNoDelay?.(true);
4211
4258
  } catch {
@@ -4229,6 +4276,9 @@ inputWss.on('connection', ws => {
4229
4276
  const input = payload?.input && typeof payload.input === 'object'
4230
4277
  ? { ...payload.input, inputEventId, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() }
4231
4278
  : { ...payload, inputEventId, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() };
4279
+ if (deviceId) {
4280
+ ws.liveDeskInputDeviceIds.add(deviceId);
4281
+ }
4232
4282
  const result = remoteHub.sendInputControl(deviceId, input);
4233
4283
  const inputType = String(input?.type || '').toLowerCase();
4234
4284
  const fireAndForget = payload?.fireAndForget === true
@@ -4248,8 +4298,15 @@ inputWss.on('connection', ws => {
4248
4298
  timestamp: new Date().toISOString()
4249
4299
  });
4250
4300
  });
4251
- ws.on('close', () => inputClients.delete(ws));
4252
- ws.on('error', () => inputClients.delete(ws));
4301
+ const releaseBrowserInputOwner = reason => {
4302
+ inputClients.delete(ws);
4303
+ for (const deviceId of ws.liveDeskInputDeviceIds || []) {
4304
+ remoteHub.releaseInputOwner(deviceId, ws.liveDeskInputClientId, reason);
4305
+ }
4306
+ ws.liveDeskInputDeviceIds?.clear?.();
4307
+ };
4308
+ ws.on('close', () => releaseBrowserInputOwner('browser-input-websocket-closed'));
4309
+ ws.on('error', () => releaseBrowserInputOwner('browser-input-websocket-error'));
4253
4310
  sendJson(ws, {
4254
4311
  type: 'RemoteInputSocketReady',
4255
4312
  protocol: 'livedesk.remote.input.json.v1',
@@ -30,16 +30,16 @@ function sameEndpoint(left, right) {
30
30
  return !!left && !!right && String(left.address) === String(right.address) && Number(left.port) === Number(right.port);
31
31
  }
32
32
 
33
- export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
34
- const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED, false);
35
- const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P, true);
33
+ export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
34
+ const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED, true);
35
+ const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P, false);
36
36
  const bindHost = safeText(env.LIVEDESK_UDP_BIND_HOST || '0.0.0.0', 128) || '0.0.0.0';
37
37
  const requestedPort = numberEnv(env.LIVEDESK_UDP_PORT, 0, 65535, 0);
38
38
  const advertiseHost = safeText(env.LIVEDESK_UDP_ADVERTISE_HOST || env.LIVEDESK_REMOTE_PUBLIC_HOST || '', 128);
39
- const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || '', 128);
39
+ const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'www.gnsoftech.com', 128);
40
40
  const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
41
41
  const punchTimeoutMs = numberEnv(env.LIVEDESK_UDP_PUNCH_TIMEOUT_MS, 500, 10_000, 3000);
42
- const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000, 80);
42
+ const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000, 500);
43
43
  const maxPendingFrames = numberEnv(env.LIVEDESK_UDP_MAX_PENDING_FRAMES, 1, 16, 2);
44
44
  const trace = isEnabled(env.LIVEDESK_UDP_TRACE, false);
45
45
  const socket = dgram.createSocket('udp4');
@@ -146,15 +146,23 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
146
146
  return;
147
147
  }
148
148
  if (packet.type !== 'frame') return;
149
- const complete = session.reassembler.ingest(packet);
150
- if (!complete) return;
151
- session.framesReceived += 1;
152
- frameHandler({
153
- deviceId: session.deviceId,
154
- sessionId: session.sessionId,
155
- header: complete.header,
156
- payload: complete.payload,
157
- transport: 'udp-p2p'
149
+ const complete = session.reassembler.ingest(packet);
150
+ if (!complete) return;
151
+ session.framesReceived += 1;
152
+ const reassembly = session.reassembler.getStats();
153
+ frameHandler({
154
+ deviceId: session.deviceId,
155
+ sessionId: session.sessionId,
156
+ header: {
157
+ ...complete.header,
158
+ hubIngressDropped: Math.max(
159
+ Number(complete.header?.hubIngressDropped || 0) || 0,
160
+ reassembly.droppedFrames
161
+ ),
162
+ udpIncompleteFrames: reassembly.droppedFrames
163
+ },
164
+ payload: complete.payload,
165
+ transport: 'udp-p2p'
158
166
  });
159
167
  sendPacket(session, 'pong', {
160
168
  state: 'frame-ack',
@@ -171,10 +179,16 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
171
179
  socket.once('error', onError);
172
180
  socket.once('listening', onListening);
173
181
  socket.bind(requestedPort, bindHost);
174
- });
175
- started = true;
176
- boundPort = socket.address().port;
177
- logEvent('udp', `UDP P2P socket listening on udp://${bindHost}:${boundPort}`);
182
+ });
183
+ started = true;
184
+ boundPort = socket.address().port;
185
+ try {
186
+ socket.setRecvBufferSize(8 * 1024 * 1024);
187
+ socket.setSendBufferSize(1024 * 1024);
188
+ } catch (error) {
189
+ logWarn('udp', `UDP socket buffer tuning was unavailable: ${error?.message || error}`);
190
+ }
191
+ logEvent('udp', `UDP P2P socket listening on udp://${bindHost}:${boundPort}`);
178
192
  return getStatus();
179
193
  }
180
194
 
@@ -240,9 +254,10 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
240
254
  state: session.ready ? 'udp-ready' : 'udp-session-created',
241
255
  ready: session.ready,
242
256
  sessionId: session.sessionId,
243
- lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
244
- framesReceived: session.framesReceived
245
- };
257
+ lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
258
+ framesReceived: session.framesReceived,
259
+ ...session.reassembler.getStats()
260
+ };
246
261
  }
247
262
 
248
263
  async function close() {
@@ -1,4 +1,5 @@
1
- import crypto from 'node:crypto';
1
+ import crypto from 'node:crypto';
2
+ import { inflateRawSync } from 'node:zlib';
2
3
 
3
4
  export const UDP_P2P_PROTOCOL = 'livedesk.udp.p2p.v1';
4
5
  export const UDP_P2P_VERSION = 1;
@@ -17,8 +18,10 @@ const UDP_FIXED_HEADER_BYTES = 15;
17
18
  const UDP_NONCE_BYTES = 12;
18
19
  const UDP_TAG_BYTES = 16;
19
20
  const UDP_MAX_SESSION_ID_BYTES = 96;
20
- const UDP_MAX_HEADER_BYTES = 4096;
21
- const UDP_MAX_PAYLOAD_BYTES = UDP_MAX_DATAGRAM_BYTES - UDP_FIXED_HEADER_BYTES - UDP_NONCE_BYTES - UDP_TAG_BYTES - 1;
21
+ const UDP_MAX_HEADER_BYTES = 4096;
22
+ const UDP_MAX_PAYLOAD_BYTES = UDP_MAX_DATAGRAM_BYTES - UDP_FIXED_HEADER_BYTES - UDP_NONCE_BYTES - UDP_TAG_BYTES - 1;
23
+ const UDP_FRAME_PAYLOAD_MAGIC = Buffer.from('LDF1', 'ascii');
24
+ const UDP_MAX_FRAME_HEADER_BYTES = 64 * 1024;
22
25
 
23
26
  function asBuffer(value) {
24
27
  if (Buffer.isBuffer(value)) return value;
@@ -32,13 +35,53 @@ function normalizeKey(key) {
32
35
  return value.length === 32 ? value : crypto.createHash('sha256').update(value).digest();
33
36
  }
34
37
 
35
- function normalizeSessionId(value) {
38
+ function normalizeSessionId(value) {
36
39
  const sessionId = String(value || '').trim();
37
40
  if (!sessionId || Buffer.byteLength(sessionId, 'utf8') > UDP_MAX_SESSION_ID_BYTES) {
38
41
  throw new Error('invalid-udp-session-id');
39
42
  }
40
43
  return sessionId;
41
- }
44
+ }
45
+
46
+ function decodeFrameHeader(header) {
47
+ if (header?.frameHeader && typeof header.frameHeader === 'object') {
48
+ return header.frameHeader;
49
+ }
50
+ const encoded = String(header?.frameHeaderDeflate || '');
51
+ if (!encoded || encoded.length > 8192 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
52
+ return null;
53
+ }
54
+ try {
55
+ const json = inflateRawSync(Buffer.from(encoded, 'base64'), { maxOutputLength: 64 * 1024 }).toString('utf8');
56
+ const value = JSON.parse(json);
57
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function decodeFramedPayload(payload, legacyHeader = null) {
64
+ const bytes = asBuffer(payload);
65
+ if (bytes.length < 8 || !bytes.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
66
+ return { header: legacyHeader || {}, payload: bytes };
67
+ }
68
+ const compressedHeaderLength = bytes.readUInt32BE(4);
69
+ if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
70
+ || 8 + compressedHeaderLength > bytes.length) return null;
71
+ try {
72
+ const json = inflateRawSync(bytes.subarray(8, 8 + compressedHeaderLength), {
73
+ maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
74
+ }).toString('utf8');
75
+ const header = JSON.parse(json);
76
+ if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
77
+ return {
78
+ header,
79
+ payload: bytes.subarray(8 + compressedHeaderLength)
80
+ };
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
42
85
 
43
86
  function packetTypeCode(type) {
44
87
  const code = typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()];
@@ -147,12 +190,15 @@ export class UdpReplayWindow {
147
190
  }
148
191
  }
149
192
 
150
- export class UdpFrameReassembler {
151
- constructor({ maxPending = 2, timeoutMs = 80, maxFrameBytes = 8 * 1024 * 1024 } = {}) {
152
- this.maxPending = Math.max(1, Math.min(16, Math.floor(Number(maxPending) || 2)));
153
- this.timeoutMs = Math.max(20, Math.min(2000, Math.floor(Number(timeoutMs) || 80)));
154
- this.maxFrameBytes = Math.max(64 * 1024, Math.min(16 * 1024 * 1024, Math.floor(Number(maxFrameBytes) || 8 * 1024 * 1024)));
155
- this.pending = new Map();
193
+ export class UdpFrameReassembler {
194
+ constructor({ maxPending = 2, timeoutMs = 500, maxFrameBytes = 8 * 1024 * 1024 } = {}) {
195
+ this.maxPending = Math.max(1, Math.min(16, Math.floor(Number(maxPending) || 2)));
196
+ this.timeoutMs = Math.max(20, Math.min(2000, Math.floor(Number(timeoutMs) || 500)));
197
+ this.maxFrameBytes = Math.max(64 * 1024, Math.min(16 * 1024 * 1024, Math.floor(Number(maxFrameBytes) || 8 * 1024 * 1024)));
198
+ this.pending = new Map();
199
+ this.droppedFrames = 0;
200
+ this.expiredFrames = 0;
201
+ this.evictedFrames = 0;
156
202
  }
157
203
 
158
204
  ingest(packet, now = Date.now()) {
@@ -166,11 +212,14 @@ export class UdpFrameReassembler {
166
212
  || chunkIndex < 0 || chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
167
213
  let state = this.pending.get(frameId);
168
214
  if (!state) {
169
- while (this.pending.size >= this.maxPending) {
170
- const oldest = this.pending.keys().next().value;
171
- if (oldest === undefined) break;
172
- this.pending.delete(oldest);
173
- }
215
+ while (this.pending.size >= this.maxPending) {
216
+ const oldest = this.pending.keys().next().value;
217
+ if (oldest === undefined) break;
218
+ if (this.pending.delete(oldest)) {
219
+ this.droppedFrames += 1;
220
+ this.evictedFrames += 1;
221
+ }
222
+ }
174
223
  state = { createdAt: now, chunkCount, chunks: new Array(chunkCount), received: 0, totalBytes: 0, frameHeader: null };
175
224
  this.pending.set(frameId, state);
176
225
  }
@@ -179,26 +228,47 @@ export class UdpFrameReassembler {
179
228
  state.chunks[chunkIndex] = payload;
180
229
  state.received += 1;
181
230
  state.totalBytes += payload.length;
182
- if (header.frameHeader && typeof header.frameHeader === 'object') state.frameHeader = header.frameHeader;
183
- if (state.totalBytes > this.maxFrameBytes) {
184
- this.pending.delete(frameId);
185
- return null;
186
- }
187
- if (state.received !== state.chunkCount) return null;
188
- this.pending.delete(frameId);
189
- return {
190
- frameId,
191
- header: state.frameHeader || header.frameHeader || {},
192
- payload: Buffer.concat(state.chunks, state.totalBytes)
193
- };
194
- }
231
+ const decodedFrameHeader = decodeFrameHeader(header);
232
+ if (decodedFrameHeader) state.frameHeader = decodedFrameHeader;
233
+ if (state.totalBytes > this.maxFrameBytes) {
234
+ if (this.pending.delete(frameId)) this.droppedFrames += 1;
235
+ return null;
236
+ }
237
+ if (state.received !== state.chunkCount) return null;
238
+ this.pending.delete(frameId);
239
+ const complete = decodeFramedPayload(
240
+ Buffer.concat(state.chunks, state.totalBytes),
241
+ state.frameHeader || header.frameHeader || {}
242
+ );
243
+ if (!complete) {
244
+ this.droppedFrames += 1;
245
+ return null;
246
+ }
247
+ return {
248
+ frameId,
249
+ header: complete.header,
250
+ payload: complete.payload
251
+ };
252
+ }
195
253
 
196
- expire(now = Date.now()) {
197
- for (const [frameId, state] of this.pending) {
198
- if (now - state.createdAt > this.timeoutMs) this.pending.delete(frameId);
199
- }
200
- }
201
- }
254
+ expire(now = Date.now()) {
255
+ for (const [frameId, state] of this.pending) {
256
+ if (now - state.createdAt > this.timeoutMs && this.pending.delete(frameId)) {
257
+ this.droppedFrames += 1;
258
+ this.expiredFrames += 1;
259
+ }
260
+ }
261
+ }
262
+
263
+ getStats() {
264
+ return {
265
+ pendingFrames: this.pending.size,
266
+ droppedFrames: this.droppedFrames,
267
+ expiredFrames: this.expiredFrames,
268
+ evictedFrames: this.evictedFrames
269
+ };
270
+ }
271
+ }
202
272
 
203
273
  export function encodeRendezvousMessage(message) {
204
274
  const payload = Buffer.from(JSON.stringify(message && typeof message === 'object' ? message : {}), 'utf8');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.436",
4
- "livedeskClientVersion": "0.1.192",
3
+ "version": "0.1.438",
4
+ "livedeskClientVersion": "0.1.194",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -50,10 +50,10 @@
50
50
  "ws": "^8.18.3"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@livedesk/fast-linux-x64": "0.1.399",
54
- "@livedesk/fast-osx-arm64": "0.1.399",
55
- "@livedesk/fast-osx-x64": "0.1.399",
56
- "@livedesk/fast-win-x64": "0.1.399"
53
+ "@livedesk/fast-linux-x64": "0.1.401",
54
+ "@livedesk/fast-osx-arm64": "0.1.401",
55
+ "@livedesk/fast-osx-x64": "0.1.401",
56
+ "@livedesk/fast-win-x64": "0.1.401"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"