livedesk 0.1.604 → 0.1.606

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.260",
3
+ "version": "0.1.261",
4
4
  "description": "VuvoDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,10 +42,10 @@
42
42
  "ws": "^8.18.3"
43
43
  },
44
44
  "optionalDependencies": {
45
- "@livedesk/fast-linux-x64": "0.1.457",
46
- "@livedesk/fast-osx-arm64": "0.1.457",
47
- "@livedesk/fast-osx-x64": "0.1.457",
48
- "@livedesk/fast-win-x64": "0.1.457"
45
+ "@livedesk/fast-linux-x64": "0.1.458",
46
+ "@livedesk/fast-osx-arm64": "0.1.458",
47
+ "@livedesk/fast-osx-x64": "0.1.458",
48
+ "@livedesk/fast-win-x64": "0.1.458"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"
@@ -130,7 +130,7 @@ if (!ffmpegStaticPath || !existsSync(ffmpegStaticPath)) {
130
130
  throw new Error('The packaged ffmpeg-static decoder is unavailable.');
131
131
  }
132
132
  let ffmpegPath = ffmpegStaticPath;
133
- try {
133
+ try {
134
134
  ffmpegPath = String(require('@ffmpeg-installer/ffmpeg')?.path || ffmpegStaticPath);
135
135
  } catch {
136
136
  // ffmpeg-static remains the portable decoder and compatibility fallback.
@@ -554,7 +554,7 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
554
554
  const socket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/input/ws`, {
555
555
  perMessageDeflate: false
556
556
  });
557
- const sendInput = async (input, timeoutMs = 5_000) => {
557
+ const sendInput = async (input, timeoutMs = 5_000) => {
558
558
  const outcome = waitForWebSocketJson(
559
559
  socket,
560
560
  message => (
@@ -577,17 +577,101 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
577
577
  captureGeneration: binding.captureGeneration
578
578
  }
579
579
  }));
580
- return outcome;
581
- };
582
-
583
- let resetApplied = false;
584
- try {
585
- const ready = waitForWebSocketJson(socket, message => message.type === 'RemoteInputSocketReady');
586
- await Promise.all([waitForWebSocketOpen(socket), ready]);
587
- const probe = await sendInput({
588
- type: 'keyboard.probe',
589
- inputSeq: 1,
590
- inputEventId: `windows-physical-probe-${Date.now()}`,
580
+ try {
581
+ return await outcome;
582
+ } catch (error) {
583
+ throw new Error(
584
+ `Windows input ${input.type} seq=${input.inputSeq} did not return an outcome: `
585
+ + `${error instanceof Error ? error.message : String(error)}`
586
+ );
587
+ }
588
+ };
589
+
590
+ const dispatchReset = (reason) => {
591
+ if (socket.readyState !== WebSocket.OPEN) {
592
+ return false;
593
+ }
594
+ socket.send(JSON.stringify({
595
+ type: 'input',
596
+ deviceId: binding.deviceId,
597
+ fireAndForget: true,
598
+ input: {
599
+ type: 'keyboard.reset',
600
+ inputSeq: 0,
601
+ requestAck: false,
602
+ pressedCodes: [],
603
+ shiftKey: false,
604
+ ctrlKey: false,
605
+ altKey: false,
606
+ metaKey: false,
607
+ repeat: false,
608
+ reason,
609
+ monitorIndex: binding.monitorIndex,
610
+ controlSessionId: binding.sessionId,
611
+ controlCommandId: binding.commandId,
612
+ captureGeneration: binding.captureGeneration
613
+ }
614
+ }));
615
+ return true;
616
+ };
617
+
618
+ let resetDispatched = false;
619
+ try {
620
+ const ready = waitForWebSocketJson(socket, message => message.type === 'RemoteInputSocketReady');
621
+ await Promise.all([waitForWebSocketOpen(socket), ready]);
622
+ const pointer = await sendInput({
623
+ type: 'pointermove',
624
+ inputSeq: 1,
625
+ inputEventId: `windows-physical-pointer-${Date.now()}`,
626
+ normalizedX: 0.5,
627
+ normalizedY: 0.5
628
+ });
629
+ assert.equal(pointer.type, 'RemoteInputApplied', String(pointer.error || 'Windows pointer input failed.'));
630
+ assert.equal(pointer.duplicate, false, 'Windows pointer input was acknowledged as a duplicate.');
631
+ assert.equal(pointer.nativeInputBackend, 'win32-setcursorpos');
632
+ assert.equal(Number(pointer.nativeEventsRequested), 1);
633
+ assert.equal(Number(pointer.nativeEventsAccepted), 1);
634
+
635
+ const shiftDown = await sendInput({
636
+ type: 'keydown',
637
+ inputSeq: 2,
638
+ inputEventId: `windows-physical-shift-down-${Date.now()}`,
639
+ key: 'Shift',
640
+ code: 'ShiftLeft',
641
+ keyCode: 16,
642
+ location: 1,
643
+ pressedCodes: ['ShiftLeft'],
644
+ shiftKey: true,
645
+ ctrlKey: false,
646
+ altKey: false,
647
+ metaKey: false,
648
+ repeat: false
649
+ });
650
+ assert.equal(shiftDown.type, 'RemoteInputApplied', String(shiftDown.error || 'Windows Shift keydown failed.'));
651
+ assert.equal(shiftDown.duplicate, false, 'Windows Shift keydown was acknowledged as a duplicate.');
652
+
653
+ const shiftUp = await sendInput({
654
+ type: 'keyup',
655
+ inputSeq: 3,
656
+ inputEventId: `windows-physical-shift-up-${Date.now()}`,
657
+ key: 'Shift',
658
+ code: 'ShiftLeft',
659
+ keyCode: 16,
660
+ location: 1,
661
+ pressedCodes: [],
662
+ shiftKey: false,
663
+ ctrlKey: false,
664
+ altKey: false,
665
+ metaKey: false,
666
+ repeat: false
667
+ });
668
+ assert.equal(shiftUp.type, 'RemoteInputApplied', String(shiftUp.error || 'Windows Shift keyup failed.'));
669
+ assert.equal(shiftUp.duplicate, false, 'Windows Shift keyup was acknowledged as a duplicate.');
670
+
671
+ const probe = await sendInput({
672
+ type: 'keyboard.probe',
673
+ inputSeq: 4,
674
+ inputEventId: `windows-physical-probe-${Date.now()}`,
591
675
  pressedCodes: [],
592
676
  shiftKey: false,
593
677
  ctrlKey: false,
@@ -606,26 +690,31 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
606
690
  /diagnostic keyboard isolation hook did not become ready/i,
607
691
  'The forced late hook start did not fail at the bounded ready deadline.'
608
692
  );
609
- const reset = await sendInput({
610
- type: 'keyboard.reset',
611
- inputSeq: 2,
612
- inputEventId: `windows-late-start-reset-${Date.now()}`,
613
- pressedCodes: [],
614
- shiftKey: false,
615
- ctrlKey: false,
616
- altKey: false,
617
- metaKey: false,
618
- repeat: false,
619
- reason: 'windows-hook-late-start-cleanup'
620
- });
621
- assert.equal(reset.type, 'RemoteInputApplied', String(reset.error || 'Windows late-start reset failed.'));
622
- assert.equal(reset.duplicate, false, 'Windows late-start reset was acknowledged as a duplicate.');
623
- resetApplied = true;
624
- return {
625
- lateStartFailure: true,
626
- error: String(probe.error || ''),
627
- resetApplied
628
- };
693
+ const recovery = await sendInput({
694
+ type: 'pointermove',
695
+ inputSeq: 5,
696
+ inputEventId: `windows-late-start-recovery-${Date.now()}`,
697
+ normalizedX: 0.5,
698
+ normalizedY: 0.5
699
+ });
700
+ assert.equal(
701
+ recovery.type,
702
+ 'RemoteInputApplied',
703
+ String(recovery.error || 'Windows input owner did not recover after the bounded hook failure.')
704
+ );
705
+ assert.equal(recovery.nativeInputBackend, 'win32-setcursorpos');
706
+ assert.equal(Number(recovery.nativeEventsRequested), 1);
707
+ assert.equal(Number(recovery.nativeEventsAccepted), 1);
708
+ resetDispatched = dispatchReset('windows-hook-late-start-cleanup');
709
+ assert.equal(resetDispatched, true, 'Windows late-start cleanup reset was not dispatched.');
710
+ return {
711
+ lateStartFailure: true,
712
+ pointerBackend: String(pointer.nativeInputBackend || ''),
713
+ normalKeyAcks: 2,
714
+ error: String(probe.error || ''),
715
+ recoveryApplied: true,
716
+ resetDispatched
717
+ };
629
718
  }
630
719
  assert.equal(probe.type, 'RemoteInputApplied', String(probe.error || 'Windows native input probe failed.'));
631
720
  assert.equal(probe.duplicate, false, 'Windows native input probe was acknowledged as a duplicate.');
@@ -637,57 +726,28 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
637
726
  assert.equal(probe.focusChanged, false, 'Windows native input probe changed the foreground window.');
638
727
  assert.equal(probe.inputDiagnostic, true);
639
728
 
640
- const reset = await sendInput({
641
- type: 'keyboard.reset',
642
- inputSeq: 2,
643
- inputEventId: `windows-physical-reset-${Date.now()}`,
644
- pressedCodes: [],
645
- shiftKey: false,
646
- ctrlKey: false,
647
- altKey: false,
648
- metaKey: false,
649
- repeat: false,
650
- reason: 'windows-native-input-diagnostic-cleanup'
651
- });
652
- assert.equal(reset.type, 'RemoteInputApplied', String(reset.error || 'Windows native input reset failed.'));
653
- assert.equal(reset.duplicate, false, 'Windows native input reset was acknowledged as a duplicate.');
654
- resetApplied = true;
655
- return {
656
- backend: String(probe.nativeInputBackend || ''),
729
+ resetDispatched = dispatchReset('windows-native-input-diagnostic-cleanup');
730
+ assert.equal(resetDispatched, true, 'Windows native input cleanup reset was not dispatched.');
731
+ return {
732
+ pointerBackend: String(pointer.nativeInputBackend || ''),
733
+ pointerRequested: Number(pointer.nativeEventsRequested || 0),
734
+ pointerAccepted: Number(pointer.nativeEventsAccepted || 0),
735
+ normalKeyAcks: 2,
736
+ backend: String(probe.nativeInputBackend || ''),
657
737
  requested: Number(probe.nativeEventsRequested || 0),
658
738
  accepted: Number(probe.nativeEventsAccepted || 0),
659
739
  hookObserved: Number(probe.hookEventsObserved || 0),
660
740
  hookBlocked: Number(probe.hookEventsBlocked || 0),
661
741
  duplicate: probe.duplicate === true,
662
742
  focusChanged: probe.focusChanged === true,
663
- resetApplied
664
- };
665
- } finally {
666
- if (!resetApplied && socket.readyState === WebSocket.OPEN) {
667
- try {
668
- socket.send(JSON.stringify({
669
- type: 'input',
670
- deviceId: binding.deviceId,
671
- fireAndForget: true,
672
- input: {
673
- type: 'keyboard.reset',
674
- inputSeq: 0,
675
- requestAck: false,
676
- pressedCodes: [],
677
- shiftKey: false,
678
- ctrlKey: false,
679
- altKey: false,
680
- metaKey: false,
681
- repeat: false,
682
- reason: 'windows-native-input-diagnostic-finally',
683
- monitorIndex: binding.monitorIndex,
684
- controlSessionId: binding.sessionId,
685
- controlCommandId: binding.commandId,
686
- captureGeneration: binding.captureGeneration
687
- }
688
- }));
689
- } catch {
690
- // Closing the exact input owner also triggers RemoteFast ReleaseAll.
743
+ resetDispatched
744
+ };
745
+ } finally {
746
+ if (!resetDispatched) {
747
+ try {
748
+ dispatchReset('windows-native-input-diagnostic-finally');
749
+ } catch {
750
+ // Closing the exact input owner also triggers RemoteFast ReleaseAll.
691
751
  }
692
752
  }
693
753
  try { socket.close(); } catch {}
@@ -1704,9 +1764,9 @@ function removeMacDiagnosticSignalHandlers() {
1704
1764
  macSignalHandlers.clear();
1705
1765
  }
1706
1766
 
1707
- installMacDiagnosticSignalHandlers();
1708
-
1709
- try {
1767
+ installMacDiagnosticSignalHandlers();
1768
+
1769
+ inputDiagnostic: try {
1710
1770
  macIsolation = await prepareMacDiagnosticIsolation();
1711
1771
  if (isMacPhysicalGate) {
1712
1772
  macDiagnosticAbort.throwIfRequested();
@@ -1924,7 +1984,8 @@ try {
1924
1984
  if (metadata.deviceId !== connectedDeviceId || metadata.frameMode !== 'mode3-h264-hw') return;
1925
1985
  const payload = Buffer.from(data.subarray(4 + metadataLength));
1926
1986
  frames.push({ metadata, receivedAt: performance.now(), bytes: payload.length, payload });
1927
- if (frames.length >= (isMacPhysicalGate ? 75 : 90)) {
1987
+ const requiredFrameCount = windowsInputAck ? 1 : (isMacPhysicalGate ? 75 : 90);
1988
+ if (frames.length >= requiredFrameCount) {
1928
1989
  finish(resolve, frames);
1929
1990
  }
1930
1991
  };
@@ -1953,9 +2014,40 @@ try {
1953
2014
  monitorSelections: { [connectedDeviceId]: requestedMonitorIndex }
1954
2015
  }));
1955
2016
 
1956
- const frames = await withMacDiagnosticAbort(framePromise, cancelFrameWait);
1957
- let activeInputBindingFrame = frames.at(-1);
1958
- const initialCaptureIdentity = switchMonitor && isWindows
2017
+ const frames = await withMacDiagnosticAbort(framePromise, cancelFrameWait);
2018
+ let activeInputBindingFrame = frames.at(-1);
2019
+ if (windowsInputAck) {
2020
+ const inputProbe = await runWindowsInputAckProbe({
2021
+ deviceId: connectedDeviceId,
2022
+ sessionId: String(activeInputBindingFrame.metadata.sessionId || ''),
2023
+ commandId: String(activeInputBindingFrame.metadata.commandId || ''),
2024
+ captureGeneration: Number(activeInputBindingFrame.metadata.captureGeneration || 0),
2025
+ monitorIndex: Number(activeInputBindingFrame.metadata.monitorIndex || 0)
2026
+ }, {
2027
+ expectHookLateStartFailure: windowsInputHookLateStart
2028
+ });
2029
+ if (windowsInputHookLateStart) {
2030
+ console.log(
2031
+ `Windows diagnostic hook late-start cleanup OK: expected timeout, `
2032
+ + `Agent responsive=${inputProbe.recoveryApplied}, reset dispatched=${inputProbe.resetDispatched}.`
2033
+ );
2034
+ } else {
2035
+ console.log(
2036
+ `Windows pointer input ACK: ${inputProbe.pointerAccepted}/${inputProbe.pointerRequested} `
2037
+ + `via ${inputProbe.pointerBackend}; normal key ACK: ${inputProbe.normalKeyAcks}/2; `
2038
+ + `isolated modifier ACK: `
2039
+ + `${inputProbe.accepted}/${inputProbe.requested} via ${inputProbe.backend}, `
2040
+ + `hook=${inputProbe.hookObserved}/${inputProbe.hookBlocked}, `
2041
+ + `duplicate=${inputProbe.duplicate}, `
2042
+ + `focusChanged=${inputProbe.focusChanged}, reset dispatched=${inputProbe.resetDispatched}.`
2043
+ );
2044
+ }
2045
+ // The Windows input gate shares only the minimum current Control binding
2046
+ // setup with the video smoke. Encoder profile and soak assertions belong
2047
+ // to their own commands and must not obscure native input evidence.
2048
+ break inputDiagnostic;
2049
+ }
2050
+ const initialCaptureIdentity = switchMonitor && isWindows
1959
2051
  ? await waitFor(
1960
2052
  () => findWindowsCaptureIdentity(agentLogs, requestedMonitorIndex),
1961
2053
  3_000,
@@ -2426,31 +2518,6 @@ try {
2426
2518
  console.log(`Mode 3 monitor transition OK: ${requestedMonitorIndex} -> ${nextMonitorIndex}, first frame ${switchedFirstFrameMs.toFixed(0)}ms, generation ${previousCaptureGeneration} -> ${firstSwitchedFrame.metadata.captureGeneration}.`);
2427
2519
  }
2428
2520
 
2429
- if (windowsInputAck) {
2430
- const inputProbe = await runWindowsInputAckProbe({
2431
- deviceId: connectedDeviceId,
2432
- sessionId: String(activeInputBindingFrame.metadata.sessionId || ''),
2433
- commandId: String(activeInputBindingFrame.metadata.commandId || ''),
2434
- captureGeneration: Number(activeInputBindingFrame.metadata.captureGeneration || 0),
2435
- monitorIndex: Number(activeInputBindingFrame.metadata.monitorIndex || 0)
2436
- }, {
2437
- expectHookLateStartFailure: windowsInputHookLateStart
2438
- });
2439
- if (windowsInputHookLateStart) {
2440
- console.log(
2441
- `Windows diagnostic hook late-start cleanup OK: expected timeout, `
2442
- + `Agent responsive, reset=${inputProbe.resetApplied}.`
2443
- );
2444
- } else {
2445
- console.log(
2446
- `Windows native modifier input ACK: ${inputProbe.accepted}/${inputProbe.requested} `
2447
- + `via ${inputProbe.backend}, hook=${inputProbe.hookObserved}/${inputProbe.hookBlocked}, `
2448
- + `duplicate=${inputProbe.duplicate}, `
2449
- + `focusChanged=${inputProbe.focusChanged}, reset=${inputProbe.resetApplied}.`
2450
- );
2451
- }
2452
- }
2453
-
2454
2521
  if (transitionToMode5) {
2455
2522
  const previousFrameSocket = frameSocket;
2456
2523
  frameSocket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/frames/ws?devices=${encodeURIComponent(connectedDeviceId)}`);
package/hub/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "description": "VuvoDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -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()) {
@@ -4982,22 +4997,38 @@ export function createRemoteHub(options = {}) {
4982
4997
  && device.inputSocket === socket
4983
4998
  && device.inputSocketConnectionId === inputSocketConnectionId
4984
4999
  && 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 = '';
5000
+ onFailClosed: reason => {
5001
+ if (device.inputWriteOwner !== writeOwner
5002
+ || device.inputSocket !== socket
5003
+ || device.inputSocketConnectionId !== inputSocketConnectionId) {
5004
+ return;
5005
+ }
5006
+ clearPendingDedicatedInputAcks(device);
5007
+ device.inputWriteOwner = null;
5008
+ device.inputSocket = null;
5009
+ device.inputSocketConnectionId = '';
4994
5010
  device.inputOwnerConnectionId = '';
4995
5011
  device.inputOwnerBindingKey = '';
4996
5012
  inputSockets.delete(socket);
4997
- device.inputLastSeenAt = new Date().toISOString();
4998
- emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
4999
- }
5000
- });
5013
+ device.inputLastSeenAt = new Date().toISOString();
5014
+ emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
5015
+ },
5016
+ onAcknowledgedWrite: (message, bindingKey) => {
5017
+ if (device.inputWriteOwner !== writeOwner
5018
+ || device.inputSocket !== socket
5019
+ || device.inputSocketConnectionId !== inputSocketConnectionId) {
5020
+ return;
5021
+ }
5022
+ const pending = message?.payload && typeof message.payload === 'object'
5023
+ ? message.payload
5024
+ : message;
5025
+ schedulePendingDedicatedInputAck(device, {
5026
+ ...pending,
5027
+ inputSocketConnectionId,
5028
+ bindingKey
5029
+ });
5030
+ }
5031
+ });
5001
5032
  device.inputWriteOwner = writeOwner;
5002
5033
 
5003
5034
  emitRemoteEvent('RemoteInputSocketConnected', device);
@@ -5757,11 +5788,17 @@ export function createRemoteHub(options = {}) {
5757
5788
  });
5758
5789
  }
5759
5790
 
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(
5791
+ function emitRemoteInputError(
5792
+ device,
5793
+ message = {},
5794
+ pending = null,
5795
+ fallbackError = 'remote-input-failed',
5796
+ errorKind = 'route') {
5797
+ const outcome = buildRemoteInputOutcome(device, message, pending);
5798
+ emitEvent('RemoteInputError', {
5799
+ ...outcome,
5800
+ errorKind: errorKind === 'native-apply' ? 'native-apply' : 'route',
5801
+ error: safeString(
5765
5802
  message.error
5766
5803
  || message.Error
5767
5804
  || message.result?.error
@@ -5791,9 +5828,9 @@ export function createRemoteHub(options = {}) {
5791
5828
  // session cannot acknowledge input from the current control session.
5792
5829
  if (!pending && source === 'main') {
5793
5830
  return false;
5794
- }
5795
- if (message.type === 'input.error') {
5796
- emitRemoteInputError(device, message, pending);
5831
+ }
5832
+ if (message.type === 'input.error') {
5833
+ emitRemoteInputError(device, message, pending, 'remote-input-failed', 'native-apply');
5797
5834
  } else {
5798
5835
  emitRemoteInputApplied(device, message, pending);
5799
5836
  }
@@ -5813,8 +5850,13 @@ export function createRemoteHub(options = {}) {
5813
5850
  || !!error
5814
5851
  || result.ok === false
5815
5852
  || safeString(result.status, 40).toLowerCase() === 'failed';
5816
- if (failed) {
5817
- emitRemoteInputError(device, { ...message, error: error || 'remote-input-failed' }, completed);
5853
+ if (failed) {
5854
+ emitRemoteInputError(
5855
+ device,
5856
+ { ...message, error: error || 'remote-input-failed' },
5857
+ completed,
5858
+ 'remote-input-failed',
5859
+ 'native-apply');
5818
5860
  } else {
5819
5861
  emitRemoteInputApplied(device, message, completed);
5820
5862
  }
@@ -9459,13 +9501,7 @@ export function createRemoteHub(options = {}) {
9459
9501
  device.inputOwnerBindingKey = activeInputBindingKey;
9460
9502
  device.counters.commandsSent += 1;
9461
9503
  device.inputLastSeenAt = new Date().toISOString();
9462
- schedulePendingDedicatedInputAck(device, {
9463
- ...normalized,
9464
- inputSocketConnectionId: device.inputSocketConnectionId,
9465
- bindingKey: activeInputBindingKey,
9466
- hubForwardedAtEpochMs
9467
- });
9468
- if (clipboardOperationKey) {
9504
+ if (clipboardOperationKey) {
9469
9505
  const operation = clipboardOperations.get(clipboardOperationKey);
9470
9506
  if (operation) {
9471
9507
  operation.status = 'pasting';
package/hub/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,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.604",
4
- "livedeskClientVersion": "0.1.260",
3
+ "version": "0.1.606",
4
+ "livedeskClientVersion": "0.1.261",
5
5
  "buildFlavor": "production",
6
6
  "description": "VuvoDesk Hub and client launcher",
7
7
  "type": "module",
@@ -51,10 +51,10 @@
51
51
  "ws": "^8.18.3"
52
52
  },
53
53
  "optionalDependencies": {
54
- "@livedesk/fast-linux-x64": "0.1.457",
55
- "@livedesk/fast-osx-arm64": "0.1.457",
56
- "@livedesk/fast-osx-x64": "0.1.457",
57
- "@livedesk/fast-win-x64": "0.1.457"
54
+ "@livedesk/fast-linux-x64": "0.1.458",
55
+ "@livedesk/fast-osx-arm64": "0.1.458",
56
+ "@livedesk/fast-osx-x64": "0.1.458",
57
+ "@livedesk/fast-win-x64": "0.1.458"
58
58
  },
59
59
  "publishConfig": {
60
60
  "access": "public"
@@ -4,7 +4,7 @@
4
4
  "short_name": "VuvoDesk",
5
5
  "description": "Monitor and control your VuvoDesk computers from your phone.",
6
6
  "lang": "en",
7
- "start_url": "/app?pwa=0.1.604",
7
+ "start_url": "/app?pwa=0.1.606",
8
8
  "scope": "/",
9
9
  "display": "standalone",
10
10
  "orientation": "any",