@livedesk/hub 0.1.52 → 0.1.54

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "description": "VuvoDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -465,8 +465,10 @@ export function normalizeRemoteClipboardCommandResult(action, operationId, value
465
465
  response.manifest = normalizeRemoteClipboardManifest(result.manifest, operationId);
466
466
  response.kind = response.manifest.contentKind;
467
467
  } else {
468
- const kind = result.contentKind || result.kind;
469
- if (kind) response.kind = normalizeContentKind(kind);
468
+ const explicitContentKind = String(result.contentKind || '').trim().toLowerCase();
469
+ const legacyKind = String(result.kind || '').trim().toLowerCase();
470
+ if (explicitContentKind) response.kind = normalizeContentKind(explicitContentKind);
471
+ else if (['text', 'image', 'files'].includes(legacyKind)) response.kind = legacyKind;
470
472
  }
471
473
  for (const key of ['receivedBytes', 'totalBytes']) {
472
474
  if (result[key] !== undefined) {
package/src/remote-hub.js CHANGED
@@ -320,11 +320,14 @@ function serializeRemoteInputWrite(socket, payload) {
320
320
  * - Ordered transitions: keyboard and pointer-button events are never
321
321
  * coalesced. If their bounded queue cannot retain order, the input socket is
322
322
  * closed so the Agent releases native pressed state.
323
- * - High-rate input: only adjacent pointermove/mousemove/wheel samples from the
324
- * same binding may replace an older queued sample.
325
- * - Bounds: one drain listener, one drain deadline, at most 16 userland items,
326
- * 32 KiB queued bytes, and 64 KiB socket writableLength.
327
- */
323
+ * - High-rate input: only adjacent pointermove/mousemove/wheel samples from the
324
+ * same binding may replace an older queued sample.
325
+ * - Acknowledgement health: an acknowledged sample becomes a deadline
326
+ * checkpoint only after that exact message is accepted by the socket write.
327
+ * A sample replaced or dropped in the userland queue owns no timer.
328
+ * - Bounds: one drain listener, one drain deadline, at most 16 userland items,
329
+ * 32 KiB queued bytes, and 64 KiB socket writableLength.
330
+ */
328
331
  export function createBoundedRemoteInputWriter(options = {}) {
329
332
  const socket = options.socket;
330
333
  const connectionId = safeString(options.connectionId, 128);
@@ -357,9 +360,12 @@ export function createBoundedRemoteInputWriter(options = {}) {
357
360
  const isOwnerCurrent = typeof options.isOwnerCurrent === 'function'
358
361
  ? options.isOwnerCurrent
359
362
  : () => true;
360
- const onFailClosed = typeof options.onFailClosed === 'function'
361
- ? options.onFailClosed
362
- : () => {};
363
+ const onFailClosed = typeof options.onFailClosed === 'function'
364
+ ? options.onFailClosed
365
+ : () => {};
366
+ const onAcknowledgedWrite = typeof options.onAcknowledgedWrite === 'function'
367
+ ? options.onAcknowledgedWrite
368
+ : () => {};
363
369
 
364
370
  let closed = false;
365
371
  let closeReason = '';
@@ -501,16 +507,23 @@ export function createBoundedRemoteInputWriter(options = {}) {
501
507
  if (writableLength() > maxWritableBytes) {
502
508
  return failClosed('input-writable-length-exceeded');
503
509
  }
504
- if (writable === false && !armDrainOwner()) {
505
- return {
506
- ok: false,
507
- error: closeReason || 'INPUT_CHANNEL_RECONNECTING',
508
- failClosed: true
509
- };
510
- }
511
- return {
512
- ok: true,
513
- queued: false,
510
+ if (writable === false && !armDrainOwner()) {
511
+ return {
512
+ ok: false,
513
+ error: closeReason || 'INPUT_CHANNEL_RECONNECTING',
514
+ failClosed: true
515
+ };
516
+ }
517
+ if (item.ackRequested) {
518
+ try {
519
+ onAcknowledgedWrite(item.payload, item.bindingKey);
520
+ } catch {
521
+ return failClosed('input-ack-write-observer-failed');
522
+ }
523
+ }
524
+ return {
525
+ ok: true,
526
+ queued: false,
514
527
  backpressured: writable === false
515
528
  };
516
529
  }
@@ -613,12 +626,14 @@ export function createBoundedRemoteInputWriter(options = {}) {
613
626
  return failClosed('input-transition-message-too-large');
614
627
  }
615
628
 
616
- const item = {
617
- payload,
618
- bindingKey: normalizedBindingKey,
619
- serialized,
620
- coalescible
621
- };
629
+ const item = {
630
+ payload,
631
+ bindingKey: normalizedBindingKey,
632
+ serialized,
633
+ coalescible,
634
+ ackRequested: payload?.payload?.requestAck === true
635
+ || payload?.requestAck === true
636
+ };
622
637
  if (blocked || queue.length > 0 || writableLength() >= maxWritableBytes) {
623
638
  const queued = queueItem(item);
624
639
  if (queued.ok && !blocked && !armDrainOwner()) {
@@ -1319,9 +1334,10 @@ function normalizeRemoteInputEvent(value = {}) {
1319
1334
  // input must prove the exact monitor owned by its capture generation.
1320
1335
  monitorIndex,
1321
1336
  normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
1322
- normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
1323
- button: safeString(input.button || input.Button, 24),
1324
- deltaX: Number.isFinite(deltaX) ? Math.max(-4096, Math.min(4096, deltaX)) : 0,
1337
+ normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
1338
+ button: safeString(input.button || input.Button, 24),
1339
+ inputSource: safeString(input.inputSource || input.InputSource, 48).toLowerCase(),
1340
+ deltaX: Number.isFinite(deltaX) ? Math.max(-4096, Math.min(4096, deltaX)) : 0,
1325
1341
  deltaY: Number.isFinite(deltaY) ? Math.max(-4096, Math.min(4096, deltaY)) : 0,
1326
1342
  key,
1327
1343
  code,
@@ -4982,22 +4998,38 @@ export function createRemoteHub(options = {}) {
4982
4998
  && device.inputSocket === socket
4983
4999
  && device.inputSocketConnectionId === inputSocketConnectionId
4984
5000
  && device.inputWriteOwner === writeOwner,
4985
- onFailClosed: reason => {
4986
- if (device.inputWriteOwner !== writeOwner
4987
- || device.inputSocket !== socket
4988
- || device.inputSocketConnectionId !== inputSocketConnectionId) {
4989
- return;
4990
- }
4991
- device.inputWriteOwner = null;
4992
- device.inputSocket = null;
4993
- device.inputSocketConnectionId = '';
5001
+ onFailClosed: reason => {
5002
+ if (device.inputWriteOwner !== writeOwner
5003
+ || device.inputSocket !== socket
5004
+ || device.inputSocketConnectionId !== inputSocketConnectionId) {
5005
+ return;
5006
+ }
5007
+ clearPendingDedicatedInputAcks(device);
5008
+ device.inputWriteOwner = null;
5009
+ device.inputSocket = null;
5010
+ device.inputSocketConnectionId = '';
4994
5011
  device.inputOwnerConnectionId = '';
4995
5012
  device.inputOwnerBindingKey = '';
4996
5013
  inputSockets.delete(socket);
4997
- device.inputLastSeenAt = new Date().toISOString();
4998
- emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
4999
- }
5000
- });
5014
+ device.inputLastSeenAt = new Date().toISOString();
5015
+ emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
5016
+ },
5017
+ onAcknowledgedWrite: (message, bindingKey) => {
5018
+ if (device.inputWriteOwner !== writeOwner
5019
+ || device.inputSocket !== socket
5020
+ || device.inputSocketConnectionId !== inputSocketConnectionId) {
5021
+ return;
5022
+ }
5023
+ const pending = message?.payload && typeof message.payload === 'object'
5024
+ ? message.payload
5025
+ : message;
5026
+ schedulePendingDedicatedInputAck(device, {
5027
+ ...pending,
5028
+ inputSocketConnectionId,
5029
+ bindingKey
5030
+ });
5031
+ }
5032
+ });
5001
5033
  device.inputWriteOwner = writeOwner;
5002
5034
 
5003
5035
  emitRemoteEvent('RemoteInputSocketConnected', device);
@@ -5757,11 +5789,17 @@ export function createRemoteHub(options = {}) {
5757
5789
  });
5758
5790
  }
5759
5791
 
5760
- function emitRemoteInputError(device, message = {}, pending = null, fallbackError = 'remote-input-failed') {
5761
- const outcome = buildRemoteInputOutcome(device, message, pending);
5762
- emitEvent('RemoteInputError', {
5763
- ...outcome,
5764
- error: safeString(
5792
+ function emitRemoteInputError(
5793
+ device,
5794
+ message = {},
5795
+ pending = null,
5796
+ fallbackError = 'remote-input-failed',
5797
+ errorKind = 'route') {
5798
+ const outcome = buildRemoteInputOutcome(device, message, pending);
5799
+ emitEvent('RemoteInputError', {
5800
+ ...outcome,
5801
+ errorKind: errorKind === 'native-apply' ? 'native-apply' : 'route',
5802
+ error: safeString(
5765
5803
  message.error
5766
5804
  || message.Error
5767
5805
  || message.result?.error
@@ -5791,9 +5829,9 @@ export function createRemoteHub(options = {}) {
5791
5829
  // session cannot acknowledge input from the current control session.
5792
5830
  if (!pending && source === 'main') {
5793
5831
  return false;
5794
- }
5795
- if (message.type === 'input.error') {
5796
- emitRemoteInputError(device, message, pending);
5832
+ }
5833
+ if (message.type === 'input.error') {
5834
+ emitRemoteInputError(device, message, pending, 'remote-input-failed', 'native-apply');
5797
5835
  } else {
5798
5836
  emitRemoteInputApplied(device, message, pending);
5799
5837
  }
@@ -5813,8 +5851,13 @@ export function createRemoteHub(options = {}) {
5813
5851
  || !!error
5814
5852
  || result.ok === false
5815
5853
  || safeString(result.status, 40).toLowerCase() === 'failed';
5816
- if (failed) {
5817
- emitRemoteInputError(device, { ...message, error: error || 'remote-input-failed' }, completed);
5854
+ if (failed) {
5855
+ emitRemoteInputError(
5856
+ device,
5857
+ { ...message, error: error || 'remote-input-failed' },
5858
+ completed,
5859
+ 'remote-input-failed',
5860
+ 'native-apply');
5818
5861
  } else {
5819
5862
  emitRemoteInputApplied(device, message, completed);
5820
5863
  }
@@ -9459,13 +9502,7 @@ export function createRemoteHub(options = {}) {
9459
9502
  device.inputOwnerBindingKey = activeInputBindingKey;
9460
9503
  device.counters.commandsSent += 1;
9461
9504
  device.inputLastSeenAt = new Date().toISOString();
9462
- schedulePendingDedicatedInputAck(device, {
9463
- ...normalized,
9464
- inputSocketConnectionId: device.inputSocketConnectionId,
9465
- bindingKey: activeInputBindingKey,
9466
- hubForwardedAtEpochMs
9467
- });
9468
- if (clipboardOperationKey) {
9505
+ if (clipboardOperationKey) {
9469
9506
  const operation = clipboardOperations.get(clipboardOperationKey);
9470
9507
  if (operation) {
9471
9508
  operation.status = 'pasting';
package/src/server.js CHANGED
@@ -5927,13 +5927,13 @@ inputWss.on('connection', ws => {
5927
5927
  try {
5928
5928
  payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
5929
5929
  } catch {
5930
- sendJson(ws, { type: 'RemoteInputError', error: 'invalid-json' });
5930
+ sendJson(ws, { type: 'RemoteInputError', errorKind: 'route', error: 'invalid-json' });
5931
5931
  return;
5932
5932
  }
5933
5933
  const deviceId = String(payload?.deviceId || '').trim();
5934
5934
  if (payload?.type === 'watch') {
5935
5935
  if (!deviceId) {
5936
- sendJson(ws, { type: 'RemoteInputError', error: 'device-id-required' });
5936
+ sendJson(ws, { type: 'RemoteInputError', errorKind: 'route', error: 'device-id-required' });
5937
5937
  return;
5938
5938
  }
5939
5939
  for (const previousDeviceId of ws.liveDeskInputDeviceIds) {
@@ -5970,7 +5970,8 @@ inputWss.on('connection', ws => {
5970
5970
  return;
5971
5971
  }
5972
5972
  sendJson(ws, {
5973
- type: result?.ok ? 'RemoteInputQueued' : 'RemoteInputError',
5973
+ type: result?.ok ? 'RemoteInputQueued' : 'RemoteInputError',
5974
+ errorKind: result?.ok ? '' : 'route',
5974
5975
  requestId: payload?.requestId || '',
5975
5976
  deviceId,
5976
5977
  inputSeq: Number(input?.inputSeq || 0) || 0,