@livedesk/hub 0.1.51 → 0.1.52

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/remote-hub.js +183 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.51",
3
+ "version": "0.1.52",
4
4
  "description": "VuvoDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
package/src/remote-hub.js CHANGED
@@ -4288,13 +4288,14 @@ export function createRemoteHub(options = {}) {
4288
4288
  }
4289
4289
  }
4290
4290
 
4291
- function closeInputSocket(device, reason = 'input-socket-closed') {
4291
+ function closeInputSocket(device, reason = 'input-socket-closed') {
4292
4292
  const socket = device?.inputSocket;
4293
4293
  if (!socket) {
4294
4294
  return;
4295
4295
  }
4296
4296
 
4297
- const writeOwner = device.inputWriteOwner;
4297
+ const writeOwner = device.inputWriteOwner;
4298
+ clearPendingDedicatedInputAcks(device);
4298
4299
  clearClipboardOperationsForDevice(device, reason);
4299
4300
  device.inputSocket = null;
4300
4301
  device.inputWriteOwner = null;
@@ -4317,7 +4318,7 @@ export function createRemoteHub(options = {}) {
4317
4318
  }
4318
4319
  }
4319
4320
 
4320
- function detachInputSocket(socket, reason = 'input-socket-closed') {
4321
+ function detachInputSocket(socket, reason = 'input-socket-closed') {
4321
4322
  const deviceId = inputSockets.get(socket);
4322
4323
  inputSockets.delete(socket);
4323
4324
  if (!deviceId) {
@@ -4329,7 +4330,8 @@ export function createRemoteHub(options = {}) {
4329
4330
  return;
4330
4331
  }
4331
4332
 
4332
- const writeOwner = device.inputWriteOwner;
4333
+ const writeOwner = device.inputWriteOwner;
4334
+ clearPendingDedicatedInputAcks(device);
4333
4335
  clearClipboardOperationsForDevice(device, reason);
4334
4336
  device.inputSocket = null;
4335
4337
  device.inputWriteOwner = null;
@@ -4948,7 +4950,8 @@ export function createRemoteHub(options = {}) {
4948
4950
  return null;
4949
4951
  }
4950
4952
 
4951
- closeInputSocket(device, 'replaced-by-new-input-socket');
4953
+ closeInputSocket(device, 'replaced-by-new-input-socket');
4954
+ clearPendingDedicatedInputAcks(device);
4952
4955
 
4953
4956
  const now = new Date().toISOString();
4954
4957
  const inputSocketConnectionId = crypto.randomUUID();
@@ -5392,7 +5395,7 @@ export function createRemoteHub(options = {}) {
5392
5395
  return failed;
5393
5396
  }
5394
5397
 
5395
- function ensurePendingInputFallbacks(device) {
5398
+ function ensurePendingInputFallbacks(device) {
5396
5399
  if (!(device?.pendingInputFallbacks instanceof Map)) {
5397
5400
  Object.defineProperty(device, 'pendingInputFallbacks', {
5398
5401
  value: new Map(),
@@ -5409,8 +5412,151 @@ export function createRemoteHub(options = {}) {
5409
5412
  writable: true
5410
5413
  });
5411
5414
  }
5412
- return device.pendingInputFallbacks;
5413
- }
5415
+ return device.pendingInputFallbacks;
5416
+ }
5417
+
5418
+ function ensurePendingDedicatedInputAckState(device) {
5419
+ if (!device?.pendingDedicatedInputAckState) {
5420
+ Object.defineProperty(device, 'pendingDedicatedInputAckState', {
5421
+ value: { current: null, next: null, timeoutTimer: null },
5422
+ enumerable: false,
5423
+ configurable: true,
5424
+ writable: true
5425
+ });
5426
+ }
5427
+ return device.pendingDedicatedInputAckState;
5428
+ }
5429
+
5430
+ function clearPendingDedicatedInputAcks(device) {
5431
+ if (!device?.pendingDedicatedInputAckState) {
5432
+ return;
5433
+ }
5434
+ const state = device.pendingDedicatedInputAckState;
5435
+ if (state.timeoutTimer) {
5436
+ clearTimeout(state.timeoutTimer);
5437
+ }
5438
+ state.timeoutTimer = null;
5439
+ state.current = null;
5440
+ state.next = null;
5441
+ }
5442
+
5443
+ function dedicatedInputAckMatches(pending, message = {}) {
5444
+ if (!pending) {
5445
+ return false;
5446
+ }
5447
+ const result = message?.result && typeof message.result === 'object'
5448
+ ? message.result
5449
+ : {};
5450
+ const inputEventId = safeString(
5451
+ message.inputEventId
5452
+ || message.InputEventId
5453
+ || result.inputEventId
5454
+ || result.InputEventId,
5455
+ 128);
5456
+ const hubConnectionId = safeString(
5457
+ message.hubConnectionId
5458
+ || message.HubConnectionId
5459
+ || result.hubConnectionId
5460
+ || result.HubConnectionId,
5461
+ 128);
5462
+ const inputSeq = Number(
5463
+ message.inputSeq
5464
+ ?? message.InputSeq
5465
+ ?? result.inputSeq
5466
+ ?? result.InputSeq
5467
+ ?? 0) || 0;
5468
+ if (pending.inputEventId && inputEventId) {
5469
+ return pending.inputEventId === inputEventId
5470
+ && pending.hubConnectionId === hubConnectionId;
5471
+ }
5472
+ return pending.inputSeq > 0
5473
+ && pending.inputSeq === inputSeq
5474
+ && pending.hubConnectionId === hubConnectionId;
5475
+ }
5476
+
5477
+ function armPendingDedicatedInputAck(device) {
5478
+ const state = ensurePendingDedicatedInputAckState(device);
5479
+ if (state.timeoutTimer) {
5480
+ clearTimeout(state.timeoutTimer);
5481
+ state.timeoutTimer = null;
5482
+ }
5483
+ const pending = state.current;
5484
+ if (!pending) {
5485
+ return;
5486
+ }
5487
+ const remainingMs = Math.max(0, pending.deadlineAtEpochMs - Date.now());
5488
+ state.timeoutTimer = setTimeout(() => {
5489
+ state.timeoutTimer = null;
5490
+ if (state.current !== pending) {
5491
+ return;
5492
+ }
5493
+ state.current = null;
5494
+ state.next = null;
5495
+ if (device.inputSocketConnectionId !== pending.inputSocketConnectionId) {
5496
+ return;
5497
+ }
5498
+ emitRemoteInputError(
5499
+ device,
5500
+ { error: 'remote-input-timeout' },
5501
+ pending,
5502
+ 'remote-input-timeout');
5503
+ closeInputSocket(device, 'input-ack-timeout');
5504
+ }, remainingMs);
5505
+ state.timeoutTimer.unref?.();
5506
+ }
5507
+
5508
+ function schedulePendingDedicatedInputAck(device, pending) {
5509
+ if (pending?.requestAck !== true
5510
+ || !Number.isSafeInteger(pending.inputSeq)
5511
+ || pending.inputSeq <= 0) {
5512
+ return;
5513
+ }
5514
+ const state = ensurePendingDedicatedInputAckState(device);
5515
+ const record = {
5516
+ ...pending,
5517
+ deadlineAtEpochMs: Date.now() + inputAckTimeoutMs
5518
+ };
5519
+ if (!state.current) {
5520
+ state.current = record;
5521
+ state.next = null;
5522
+ armPendingDedicatedInputAck(device);
5523
+ return;
5524
+ }
5525
+ if (state.current.inputSocketConnectionId !== record.inputSocketConnectionId
5526
+ || state.current.bindingKey !== record.bindingKey) {
5527
+ clearPendingDedicatedInputAcks(device);
5528
+ state.current = record;
5529
+ armPendingDedicatedInputAck(device);
5530
+ return;
5531
+ }
5532
+ // Keep only one later checkpoint. If the current acknowledgement
5533
+ // arrives, this latest sampled input becomes the next exact deadline.
5534
+ state.next = record;
5535
+ }
5536
+
5537
+ function acknowledgePendingDedicatedInput(device, message = {}) {
5538
+ const state = device?.pendingDedicatedInputAckState;
5539
+ if (!state?.current) {
5540
+ return false;
5541
+ }
5542
+ if (dedicatedInputAckMatches(state.current, message)) {
5543
+ if (state.timeoutTimer) {
5544
+ clearTimeout(state.timeoutTimer);
5545
+ state.timeoutTimer = null;
5546
+ }
5547
+ state.current = state.next;
5548
+ state.next = null;
5549
+ armPendingDedicatedInputAck(device);
5550
+ return true;
5551
+ }
5552
+ if (dedicatedInputAckMatches(state.next, message)) {
5553
+ // A later ordered acknowledgement proves that the input reader is
5554
+ // still draining, even if an earlier diagnostic response vanished.
5555
+ clearPendingDedicatedInputAcks(device);
5556
+ return true;
5557
+ }
5558
+ return false;
5559
+ }
5414
5560
 
5415
5561
  function pruneCompletedInputFallbacks(device, now = Date.now()) {
5416
5562
  ensurePendingInputFallbacks(device);
@@ -7866,10 +8012,14 @@ export function createRemoteHub(options = {}) {
7866
8012
  return;
7867
8013
  }
7868
8014
 
7869
- if (state.inputOnly) {
7870
- device.inputLastSeenAt = new Date().toISOString();
7871
- if (message.type === 'input.applied' || message.type === 'input.error') {
7872
- handleRemoteInputOutcome(device, {
8015
+ if (state.inputOnly) {
8016
+ if (device.inputSocket !== socket) {
8017
+ return;
8018
+ }
8019
+ device.inputLastSeenAt = new Date().toISOString();
8020
+ if (message.type === 'input.applied' || message.type === 'input.error') {
8021
+ acknowledgePendingDedicatedInput(device, message);
8022
+ handleRemoteInputOutcome(device, {
7873
8023
  ...message,
7874
8024
  agentApplyMs: Number(message.agentApplyMs || 0) || 0
7875
8025
  }, 'input');
@@ -9265,9 +9415,10 @@ export function createRemoteHub(options = {}) {
9265
9415
  && normalized.hubConnectionId !== previousOwnerConnectionId;
9266
9416
  const bindingChanged = previousInputBindingKey
9267
9417
  && previousInputBindingKey !== activeInputBindingKey;
9268
- const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
9269
- if (ownerChanged || bindingChanged || writeBindingChanged) {
9270
- if (ownerChanged) {
9418
+ const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
9419
+ if (ownerChanged || bindingChanged || writeBindingChanged) {
9420
+ clearPendingDedicatedInputAcks(device);
9421
+ if (ownerChanged) {
9271
9422
  clearClipboardOperationsForDevice(
9272
9423
  device,
9273
9424
  'browser-input-owner-changed',
@@ -9292,11 +9443,12 @@ export function createRemoteHub(options = {}) {
9292
9443
  return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
9293
9444
  }
9294
9445
  }
9295
- const sent = inputWriteOwner.enqueue({
9296
- type: 'input.control',
9297
- payload: {
9298
- ...normalized,
9299
- hubForwardedAtEpochMs: Date.now(),
9446
+ const hubForwardedAtEpochMs = Date.now();
9447
+ const sent = inputWriteOwner.enqueue({
9448
+ type: 'input.control',
9449
+ payload: {
9450
+ ...normalized,
9451
+ hubForwardedAtEpochMs,
9300
9452
  issuedAt: normalized.issuedAt || new Date().toISOString()
9301
9453
  }
9302
9454
  }, activeInputBindingKey);
@@ -9304,9 +9456,15 @@ export function createRemoteHub(options = {}) {
9304
9456
  if (normalized.hubConnectionId) {
9305
9457
  device.inputOwnerConnectionId = normalized.hubConnectionId;
9306
9458
  }
9307
- device.inputOwnerBindingKey = activeInputBindingKey;
9308
- device.counters.commandsSent += 1;
9309
- device.inputLastSeenAt = new Date().toISOString();
9459
+ device.inputOwnerBindingKey = activeInputBindingKey;
9460
+ device.counters.commandsSent += 1;
9461
+ device.inputLastSeenAt = new Date().toISOString();
9462
+ schedulePendingDedicatedInputAck(device, {
9463
+ ...normalized,
9464
+ inputSocketConnectionId: device.inputSocketConnectionId,
9465
+ bindingKey: activeInputBindingKey,
9466
+ hubForwardedAtEpochMs
9467
+ });
9310
9468
  if (clipboardOperationKey) {
9311
9469
  const operation = clipboardOperations.get(clipboardOperationKey);
9312
9470
  if (operation) {
@@ -9397,7 +9555,8 @@ export function createRemoteHub(options = {}) {
9397
9555
  return { ok: false, error: 'input-owner-not-current' };
9398
9556
  }
9399
9557
 
9400
- clearClipboardOperationsForDevice(device, reason, owner);
9558
+ clearClipboardOperationsForDevice(device, reason, owner);
9559
+ clearPendingDedicatedInputAcks(device);
9401
9560
  device.inputOwnerConnectionId = '';
9402
9561
  device.inputOwnerBindingKey = '';
9403
9562
  const inputSocket = device.inputSocket;