@livedesk/hub 0.1.67 → 0.1.69

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.67",
3
+ "version": "0.1.69",
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
@@ -75,6 +75,8 @@ const LIVE_STREAM_MIN_FRESH_MS = 3000;
75
75
  const LIVE_STREAM_MAX_FRESH_MS = 12000;
76
76
  const TASK_BATCH_OWNER = Symbol('remote-hub-task-batch-owner');
77
77
  const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
78
+ const REMOTE_INPUT_FAILURE_LOG_INTERVAL_MS = 5000;
79
+ const REMOTE_INPUT_FAILURE_LOG_KEY_LIMIT = 32;
78
80
  // RemoteFast owns a shared 7s native stop deadline. Keep four seconds for a
79
81
  // priority command.result to cross either Direct or encrypted Relay transport.
80
82
  const DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS = 11000;
@@ -1531,7 +1533,7 @@ function readCapabilityFlag(capabilities, key) {
1531
1533
  return /^(1|true|yes|on)$/i.test(String(value || '').trim());
1532
1534
  }
1533
1535
 
1534
- function createDeviceCounters(overrides = {}) {
1536
+ function createDeviceCounters(overrides = {}) {
1535
1537
  return {
1536
1538
  messagesReceived: 0,
1537
1539
  statusReceived: 0,
@@ -1547,8 +1549,10 @@ function createDeviceCounters(overrides = {}) {
1547
1549
  audioStreamsStopped: 0,
1548
1550
  audioFramesReceived: 0,
1549
1551
  audioFramesDropped: 0,
1550
- audioStatusReceived: 0,
1551
- tasksQueued: 0,
1552
+ audioStatusReceived: 0,
1553
+ remoteInputsApplied: 0,
1554
+ remoteInputsRejected: 0,
1555
+ tasksQueued: 0,
1552
1556
  taskResultsReceived: 0,
1553
1557
  taskResultsFailed: 0,
1554
1558
  taskResultsTimedOut: 0,
@@ -1816,6 +1820,38 @@ function serializeDeviceTask(task) {
1816
1820
  };
1817
1821
  }
1818
1822
 
1823
+ function createRemoteInputDiagnostics() {
1824
+ return {
1825
+ appliedCount: 0,
1826
+ rejectedCount: 0,
1827
+ lastOutcome: null,
1828
+ lastApplied: null,
1829
+ lastFailure: null
1830
+ };
1831
+ }
1832
+
1833
+ function serializeRemoteInputDiagnostics(diagnostics) {
1834
+ const source = diagnostics && typeof diagnostics === 'object'
1835
+ ? diagnostics
1836
+ : createRemoteInputDiagnostics();
1837
+ return {
1838
+ appliedCount: Math.max(0, Number(source.appliedCount) || 0),
1839
+ rejectedCount: Math.max(0, Number(source.rejectedCount) || 0),
1840
+ lastOutcome: source.lastOutcome ? { ...source.lastOutcome } : null,
1841
+ lastApplied: source.lastApplied ? { ...source.lastApplied } : null,
1842
+ lastFailure: source.lastFailure ? { ...source.lastFailure } : null
1843
+ };
1844
+ }
1845
+
1846
+ function firstOptionalBoolean(...values) {
1847
+ for (const value of values) {
1848
+ if (typeof value === 'boolean') {
1849
+ return value;
1850
+ }
1851
+ }
1852
+ return null;
1853
+ }
1854
+
1819
1855
  function serializeDevice(device, options = {}) {
1820
1856
  if (!device) {
1821
1857
  return null;
@@ -1896,8 +1932,9 @@ function serializeDevice(device, options = {}) {
1896
1932
  recentTasks: Array.isArray(device.recentTasks)
1897
1933
  ? device.recentTasks.map(serializeDeviceTask).filter(Boolean)
1898
1934
  : [],
1899
- status: { ...device.status },
1900
- counters: { ...device.counters }
1935
+ status: { ...device.status },
1936
+ remoteInputDiagnostics: serializeRemoteInputDiagnostics(device.remoteInputDiagnostics),
1937
+ counters: { ...device.counters }
1901
1938
  };
1902
1939
  }
1903
1940
 
@@ -4943,9 +4980,11 @@ export function createRemoteHub(options = {}) {
4943
4980
  latestAudioStatus: null,
4944
4981
  latestTask,
4945
4982
  recentTasks: latestTask ? [latestTask] : [],
4946
- pendingTaskCommands: new Map(),
4947
- pendingTaskTimers: new Map(),
4948
- counters: createDeviceCounters({
4983
+ pendingTaskCommands: new Map(),
4984
+ pendingTaskTimers: new Map(),
4985
+ remoteInputDiagnostics: createRemoteInputDiagnostics(),
4986
+ remoteInputFailureLogAt: new Map(),
4987
+ counters: createDeviceCounters({
4949
4988
  messagesReceived: 1,
4950
4989
  statusReceived: 1,
4951
4990
  tasksQueued: latestTask ? 1 : 0,
@@ -5240,9 +5279,11 @@ export function createRemoteHub(options = {}) {
5240
5279
  },
5241
5280
  latestTask: null,
5242
5281
  recentTasks: [],
5243
- pendingTaskCommands: new Map(),
5244
- pendingTaskTimers: new Map(),
5245
- synthetic: false,
5282
+ pendingTaskCommands: new Map(),
5283
+ pendingTaskTimers: new Map(),
5284
+ remoteInputDiagnostics: createRemoteInputDiagnostics(),
5285
+ remoteInputFailureLogAt: new Map(),
5286
+ synthetic: false,
5246
5287
  counters: createDeviceCounters({ messagesReceived: 1 })
5247
5288
  };
5248
5289
 
@@ -5980,9 +6021,10 @@ export function createRemoteHub(options = {}) {
5980
6021
  function acknowledgePendingDedicatedInput(device, message = {}) {
5981
6022
  const state = device?.pendingDedicatedInputAckState;
5982
6023
  if (!state?.current) {
5983
- return false;
6024
+ return null;
5984
6025
  }
5985
6026
  if (dedicatedInputAckMatches(state.current, message)) {
6027
+ const acknowledged = state.current;
5986
6028
  if (state.timeoutTimer) {
5987
6029
  clearTimeout(state.timeoutTimer);
5988
6030
  state.timeoutTimer = null;
@@ -5990,15 +6032,16 @@ export function createRemoteHub(options = {}) {
5990
6032
  state.current = state.next;
5991
6033
  state.next = null;
5992
6034
  armPendingDedicatedInputAck(device);
5993
- return true;
6035
+ return acknowledged;
5994
6036
  }
5995
6037
  if (dedicatedInputAckMatches(state.next, message)) {
5996
6038
  // A later ordered acknowledgement proves that the input reader is
5997
6039
  // still draining, even if an earlier diagnostic response vanished.
6040
+ const acknowledged = state.next;
5998
6041
  clearPendingDedicatedInputAcks(device);
5999
- return true;
6042
+ return acknowledged;
6000
6043
  }
6001
- return false;
6044
+ return null;
6002
6045
  }
6003
6046
 
6004
6047
  function pruneCompletedInputFallbacks(device, now = Date.now()) {
@@ -6105,14 +6148,39 @@ export function createRemoteHub(options = {}) {
6105
6148
  || pending?.inputEventId,
6106
6149
  128),
6107
6150
  inputSeq,
6108
- inputType: safeString(
6151
+ inputType: safeString(
6109
6152
  message.inputType
6110
6153
  || message.InputType
6111
6154
  || result.inputType
6112
6155
  || result.InputType
6113
6156
  || pending?.inputType,
6114
- 48),
6115
- monitorIndex: Number(
6157
+ 48),
6158
+ controlSessionId: safeString(
6159
+ message.controlSessionId
6160
+ || message.ControlSessionId
6161
+ || result.controlSessionId
6162
+ || result.ControlSessionId
6163
+ || pending?.controlSessionId
6164
+ || device.sessionId,
6165
+ 160),
6166
+ controlCommandId: safeString(
6167
+ message.controlCommandId
6168
+ || message.ControlCommandId
6169
+ || result.controlCommandId
6170
+ || result.ControlCommandId
6171
+ || pending?.controlCommandId,
6172
+ 128),
6173
+ captureGeneration: Math.max(0, Number(
6174
+ message.captureGeneration
6175
+ ?? message.CaptureGeneration
6176
+ ?? result.captureGeneration
6177
+ ?? result.CaptureGeneration
6178
+ ?? pending?.captureGeneration
6179
+ ?? 0) || 0),
6180
+ inputSocketConnectionId: safeString(
6181
+ pending?.inputSocketConnectionId,
6182
+ 128),
6183
+ monitorIndex: Number(
6116
6184
  message.monitorIndex
6117
6185
  ?? message.MonitorIndex
6118
6186
  ?? result.monitorIndex
@@ -6178,27 +6246,144 @@ export function createRemoteHub(options = {}) {
6178
6246
  || message.FocusChanged === true
6179
6247
  || result.focusChanged === true
6180
6248
  || result.FocusChanged === true,
6181
- inputDiagnostic: message.inputDiagnostic === true
6249
+ inputDiagnostic: message.inputDiagnostic === true
6182
6250
  || message.InputDiagnostic === true
6183
- || result.inputDiagnostic === true
6184
- || result.InputDiagnostic === true,
6185
- fallback: pending?.fallback === true,
6186
- hubAcknowledgedAtEpochMs: Date.now()
6187
- };
6188
- }
6189
-
6190
- function emitRemoteInputApplied(device, message = {}, pending = null) {
6191
- const outcome = buildRemoteInputOutcome(device, message, pending);
6192
- emitEvent('RemoteInputApplied', {
6193
- ...outcome,
6194
- agentApplyMs: Number(
6195
- message.agentApplyMs
6196
- ?? message.AgentApplyMs
6197
- ?? message.result?.agentApplyMs
6198
- ?? message.result?.AgentApplyMs
6199
- ?? 0) || 0
6200
- });
6201
- }
6251
+ || result.inputDiagnostic === true
6252
+ || result.InputDiagnostic === true,
6253
+ failureStage: safeString(
6254
+ message.failureStage
6255
+ || message.FailureStage
6256
+ || result.failureStage
6257
+ || result.FailureStage,
6258
+ 40),
6259
+ exceptionType: safeString(
6260
+ message.exceptionType
6261
+ || message.ExceptionType
6262
+ || result.exceptionType
6263
+ || result.ExceptionType,
6264
+ 80),
6265
+ permissionSupported: firstOptionalBoolean(
6266
+ message.permissionSupported,
6267
+ message.PermissionSupported,
6268
+ result.permissionSupported,
6269
+ result.PermissionSupported),
6270
+ permissionGranted: firstOptionalBoolean(
6271
+ message.permissionGranted,
6272
+ message.PermissionGranted,
6273
+ result.permissionGranted,
6274
+ result.PermissionGranted),
6275
+ macAccessibilityTrusted: firstOptionalBoolean(
6276
+ message.macAccessibilityTrusted,
6277
+ message.MacAccessibilityTrusted,
6278
+ result.macAccessibilityTrusted,
6279
+ result.MacAccessibilityTrusted),
6280
+ macPostEventAccess: firstOptionalBoolean(
6281
+ message.macPostEventAccess,
6282
+ message.MacPostEventAccess,
6283
+ result.macPostEventAccess,
6284
+ result.MacPostEventAccess),
6285
+ fallback: pending?.fallback === true,
6286
+ hubAcknowledgedAtEpochMs: Date.now()
6287
+ };
6288
+ }
6289
+
6290
+ function rememberRemoteInputOutcome(device, outcome, state) {
6291
+ const diagnostics = device.remoteInputDiagnostics
6292
+ || (device.remoteInputDiagnostics = createRemoteInputDiagnostics());
6293
+ const record = {
6294
+ state,
6295
+ receivedAt: new Date(outcome.hubAcknowledgedAtEpochMs).toISOString(),
6296
+ inputType: safeString(outcome.inputType, 48),
6297
+ inputSeq: Math.max(0, Number(outcome.inputSeq) || 0),
6298
+ monitorIndex: Math.max(0, Number(outcome.monitorIndex) || 0),
6299
+ sessionId: safeString(outcome.controlSessionId || device.sessionId, 160),
6300
+ controlCommandId: safeString(outcome.controlCommandId, 128),
6301
+ captureGeneration: Math.max(0, Number(outcome.captureGeneration) || 0),
6302
+ inputSocketConnectionId: safeString(outcome.inputSocketConnectionId, 128),
6303
+ fallback: outcome.fallback === true
6304
+ };
6305
+ if (state === 'applied') {
6306
+ diagnostics.appliedCount += 1;
6307
+ device.counters.remoteInputsApplied += 1;
6308
+ const applied = {
6309
+ ...record,
6310
+ duplicate: outcome.duplicate === true,
6311
+ agentApplyMs: Math.max(0, Number(outcome.agentApplyMs) || 0),
6312
+ nativeInputBackend: safeString(outcome.nativeInputBackend, 80)
6313
+ };
6314
+ diagnostics.lastApplied = applied;
6315
+ diagnostics.lastOutcome = applied;
6316
+ return applied;
6317
+ }
6318
+
6319
+ diagnostics.rejectedCount += 1;
6320
+ device.counters.remoteInputsRejected += 1;
6321
+ const failed = {
6322
+ ...record,
6323
+ errorKind: outcome.errorKind === 'native-apply' ? 'native-apply' : 'route',
6324
+ failureStage: safeString(outcome.failureStage, 40) || 'unknown',
6325
+ exceptionType: safeString(outcome.exceptionType, 80),
6326
+ permissionSupported: firstOptionalBoolean(outcome.permissionSupported),
6327
+ permissionGranted: firstOptionalBoolean(outcome.permissionGranted),
6328
+ macAccessibilityTrusted: firstOptionalBoolean(outcome.macAccessibilityTrusted),
6329
+ macPostEventAccess: firstOptionalBoolean(outcome.macPostEventAccess),
6330
+ error: safeDiagnosticText(outcome.error, 600) || 'remote-input-failed',
6331
+ failedAtEpochMs: Math.max(0, Number(outcome.failedAtEpochMs) || 0)
6332
+ };
6333
+ diagnostics.lastFailure = failed;
6334
+ diagnostics.lastOutcome = failed;
6335
+ return failed;
6336
+ }
6337
+
6338
+ function logRemoteInputFailure(device, failure) {
6339
+ const rateLimits = device.remoteInputFailureLogAt
6340
+ || (device.remoteInputFailureLogAt = new Map());
6341
+ const key = `${failure.errorKind}:${failure.failureStage}:${failure.exceptionType}:${failure.error}`;
6342
+ const now = Date.now();
6343
+ const lastAt = Number(rateLimits.get(key) || 0);
6344
+ if (lastAt > 0 && now - lastAt < REMOTE_INPUT_FAILURE_LOG_INTERVAL_MS) {
6345
+ return;
6346
+ }
6347
+ rateLimits.delete(key);
6348
+ rateLimits.set(key, now);
6349
+ while (rateLimits.size > REMOTE_INPUT_FAILURE_LOG_KEY_LIMIT) {
6350
+ const oldestKey = rateLimits.keys().next().value;
6351
+ if (!oldestKey) break;
6352
+ rateLimits.delete(oldestKey);
6353
+ }
6354
+ const shortId = value => safeString(value, 128).slice(0, 12) || 'unknown';
6355
+ const optionalFlag = value => typeof value === 'boolean' ? String(value) : 'unknown';
6356
+ logWarn(
6357
+ 'remote',
6358
+ `input rejected device=${device.deviceName} (${device.deviceId})`
6359
+ + ` session=${shortId(failure.sessionId)}`
6360
+ + ` command=${shortId(failure.controlCommandId)}`
6361
+ + ` generation=${failure.captureGeneration}`
6362
+ + ` monitor=${failure.monitorIndex}`
6363
+ + ` type=${failure.inputType || 'unknown'}`
6364
+ + ` seq=${failure.inputSeq}`
6365
+ + ` kind=${failure.errorKind}`
6366
+ + ` stage=${failure.failureStage}`
6367
+ + ` exception=${failure.exceptionType || 'unknown'}`
6368
+ + ` permission=${optionalFlag(failure.permissionGranted)}`
6369
+ + ` accessibility=${optionalFlag(failure.macAccessibilityTrusted)}`
6370
+ + ` postEvent=${optionalFlag(failure.macPostEventAccess)}`
6371
+ + ` error=${failure.error}`);
6372
+ }
6373
+
6374
+ function emitRemoteInputApplied(device, message = {}, pending = null) {
6375
+ const outcome = {
6376
+ ...buildRemoteInputOutcome(device, message, pending),
6377
+ agentApplyMs: Number(
6378
+ message.agentApplyMs
6379
+ ?? message.AgentApplyMs
6380
+ ?? message.result?.agentApplyMs
6381
+ ?? message.result?.AgentApplyMs
6382
+ ?? 0) || 0
6383
+ };
6384
+ rememberRemoteInputOutcome(device, outcome, 'applied');
6385
+ emitEvent('RemoteInputApplied', outcome);
6386
+ }
6202
6387
 
6203
6388
  function emitRemoteInputError(
6204
6389
  device,
@@ -6207,29 +6392,37 @@ export function createRemoteHub(options = {}) {
6207
6392
  fallbackError = 'remote-input-failed',
6208
6393
  errorKind = 'route') {
6209
6394
  const outcome = buildRemoteInputOutcome(device, message, pending);
6210
- emitEvent('RemoteInputError', {
6395
+ const failure = {
6211
6396
  ...outcome,
6212
6397
  errorKind: errorKind === 'native-apply' ? 'native-apply' : 'route',
6213
- error: safeString(
6214
- message.error
6215
- || message.Error
6216
- || message.result?.error
6217
- || message.result?.Error
6218
- || fallbackError,
6398
+ error: safeDiagnosticText(
6399
+ message.error
6400
+ || message.Error
6401
+ || message.result?.error
6402
+ || message.result?.Error
6403
+ || fallbackError,
6219
6404
  600) || fallbackError,
6220
6405
  failedAtEpochMs: Number(
6221
6406
  message.failedAtEpochMs
6222
6407
  ?? message.FailedAtEpochMs
6223
6408
  ?? message.result?.failedAtEpochMs
6224
- ?? message.result?.FailedAtEpochMs
6225
- ?? 0) || 0
6226
- });
6227
- }
6228
-
6229
- function handleRemoteInputOutcome(device, message = {}, source = 'input') {
6409
+ ?? message.result?.FailedAtEpochMs
6410
+ ?? 0) || 0
6411
+ };
6412
+ const persistentFailure = rememberRemoteInputOutcome(device, failure, 'rejected');
6413
+ logRemoteInputFailure(device, persistentFailure);
6414
+ emitEvent('RemoteInputError', failure);
6415
+ }
6416
+
6417
+ function handleRemoteInputOutcome(
6418
+ device,
6419
+ message = {},
6420
+ source = 'input',
6421
+ acknowledgedPending = null) {
6230
6422
  const commandId = safeString(message.commandId || message.CommandId, 128);
6231
6423
  const inputSeq = Number(message.inputSeq ?? message.InputSeq ?? 0) || 0;
6232
- const pending = takePendingInputFallback(device, commandId, inputSeq);
6424
+ const pending = acknowledgedPending
6425
+ || takePendingInputFallback(device, commandId, inputSeq);
6233
6426
  if (!pending
6234
6427
  && source === 'main'
6235
6428
  && wasInputFallbackCompleted(device, commandId, inputSeq)) {
@@ -8572,12 +8765,12 @@ export function createRemoteHub(options = {}) {
8572
8765
  if (state.inputOnly) {
8573
8766
  device.inputLastSeenAt = new Date().toISOString();
8574
8767
  if (message.type === 'input.applied' || message.type === 'input.error') {
8575
- acknowledgePendingDedicatedInput(device, message);
8768
+ const acknowledgedPending = acknowledgePendingDedicatedInput(device, message);
8576
8769
  handleRemoteInputOutcome(device, {
8577
- ...message,
8578
- agentApplyMs: Number(message.agentApplyMs || 0) || 0
8579
- }, 'input');
8580
- }
8770
+ ...message,
8771
+ agentApplyMs: Number(message.agentApplyMs || 0) || 0
8772
+ }, 'input', acknowledgedPending);
8773
+ }
8581
8774
  return;
8582
8775
  }
8583
8776
  if (state.frameOnly) {
@@ -420,7 +420,7 @@ export function createHubRelayControl({
420
420
  const host = safeText(
421
421
  env.LIVEDESK_RELAY_HOST
422
422
  || env.LIVEDESK_UDP_RENDEZVOUS_HOST
423
- || 'www.gnsoftech.com',
423
+ || 'p2p.gnsoftech.com',
424
424
  253);
425
425
  const port = boundedInteger(
426
426
  env.LIVEDESK_RELAY_PORT || env.LIVEDESK_UDP_RENDEZVOUS_PORT,
@@ -67,7 +67,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
67
67
  const bindHost = safeText(env.LIVEDESK_UDP_BIND_HOST || '0.0.0.0', 128) || '0.0.0.0';
68
68
  const requestedPort = numberEnv(env.LIVEDESK_UDP_PORT, 0, 65535, 0);
69
69
  const advertiseHost = safeText(env.LIVEDESK_UDP_ADVERTISE_HOST || env.LIVEDESK_REMOTE_PUBLIC_HOST || '', 128);
70
- const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'www.gnsoftech.com', 128);
70
+ const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'p2p.gnsoftech.com', 128);
71
71
  const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
72
72
  const rendezvousRefreshMs = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_REFRESH_MS, 1000, 50_000, 25_000);
73
73
  const punchTimeoutMs = numberEnv(env.LIVEDESK_UDP_PUNCH_TIMEOUT_MS, 500, 10_000, 3000);