@mindexec/cli 0.2.107 → 0.2.108

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": "@mindexec/cli",
3
- "version": "0.2.107",
3
+ "version": "0.2.108",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/remote-hub.js CHANGED
@@ -15,6 +15,11 @@ const MAX_AGENT_TASK_CHARS = 4000;
15
15
  const MAX_AGENT_TASK_RESULT_CHARS = 3000;
16
16
  const RECENT_TASK_LIMIT = 12;
17
17
  const RECENT_TASK_BATCH_LIMIT = 16;
18
+ const RECENT_FRAME_CACHE_TTL_MS = 5000;
19
+ const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 4;
20
+ const RECENT_LIVE_FRAME_CACHE_LIMIT = 24;
21
+ const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 3 * 1024 * 1024;
22
+ const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
18
23
  const REMOTE_PROTOCOL_VERSION = 1;
19
24
  const MAX_SYNTHETIC_DEVICES = 1000;
20
25
  const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
@@ -374,6 +379,153 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
374
379
  return serialized;
375
380
  }
376
381
 
382
+ function getRecentFrameCache(device, frameKind) {
383
+ if (!device) {
384
+ return null;
385
+ }
386
+
387
+ if (!device.recentFramePayloads) {
388
+ device.recentFramePayloads = {
389
+ thumbnail: [],
390
+ live: []
391
+ };
392
+ }
393
+
394
+ const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
395
+ if (!Array.isArray(device.recentFramePayloads[kind])) {
396
+ device.recentFramePayloads[kind] = [];
397
+ }
398
+
399
+ return device.recentFramePayloads[kind];
400
+ }
401
+
402
+ function getRecentFrameCacheLimit(frameKind) {
403
+ return frameKind === 'thumbnail'
404
+ ? RECENT_THUMBNAIL_FRAME_CACHE_LIMIT
405
+ : RECENT_LIVE_FRAME_CACHE_LIMIT;
406
+ }
407
+
408
+ function getRecentFrameCacheMaxBytes(frameKind) {
409
+ return frameKind === 'thumbnail'
410
+ ? RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES
411
+ : RECENT_LIVE_FRAME_CACHE_MAX_BYTES;
412
+ }
413
+
414
+ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
415
+ if (!Array.isArray(cache)) {
416
+ return;
417
+ }
418
+
419
+ const limit = getRecentFrameCacheLimit(frameKind);
420
+ const maxBytes = getRecentFrameCacheMaxBytes(frameKind);
421
+ let byteTotal = 0;
422
+ for (let index = cache.length - 1; index >= 0; index -= 1) {
423
+ const entry = cache[index];
424
+ const frame = entry?.frame;
425
+ if (!entry
426
+ || entry.expiresAt <= nowMs
427
+ || !frame
428
+ || !Buffer.isBuffer(frame.payload)
429
+ || !Number.isFinite(Number(frame.frameSeq))
430
+ || !safeString(frame.accessToken, 128)) {
431
+ cache.splice(index, 1);
432
+ }
433
+ }
434
+
435
+ for (let index = 0; index < cache.length; index += 1) {
436
+ const entry = cache[index];
437
+ const byteLength = Number(entry?.byteLength || entry?.frame?.payload?.length || 0) || 0;
438
+ byteTotal += byteLength;
439
+ if (index >= limit || byteTotal > maxBytes) {
440
+ cache.splice(index);
441
+ break;
442
+ }
443
+ }
444
+ }
445
+
446
+ function rememberRecentFramePayload(device, frameKind, frame) {
447
+ if (!device || !frame || !Buffer.isBuffer(frame.payload)) {
448
+ return;
449
+ }
450
+
451
+ const frameSeq = Number(frame.frameSeq);
452
+ const accessToken = safeString(frame.accessToken, 128);
453
+ if (!Number.isFinite(frameSeq) || !accessToken) {
454
+ return;
455
+ }
456
+
457
+ const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
458
+ const cache = getRecentFrameCache(device, kind);
459
+ if (!cache) {
460
+ return;
461
+ }
462
+
463
+ const nowMs = Date.now();
464
+ const normalizedSeq = Math.floor(frameSeq);
465
+ for (let index = cache.length - 1; index >= 0; index -= 1) {
466
+ const existing = cache[index]?.frame;
467
+ if (Number(existing?.frameSeq) === normalizedSeq
468
+ || safeString(existing?.accessToken, 128) === accessToken) {
469
+ cache.splice(index, 1);
470
+ }
471
+ }
472
+
473
+ cache.unshift({
474
+ frame,
475
+ byteLength: frame.payload.length,
476
+ cachedAt: nowMs,
477
+ expiresAt: nowMs + RECENT_FRAME_CACHE_TTL_MS
478
+ });
479
+ pruneRecentFrameCache(cache, kind, nowMs);
480
+ }
481
+
482
+ function isFramePayloadRequestMatch(frame, options = {}) {
483
+ if (!frame || !Buffer.isBuffer(frame.payload)) {
484
+ return false;
485
+ }
486
+
487
+ const requestedSeq = Number(options.frameSeq);
488
+ if (Number.isFinite(requestedSeq) && Math.floor(requestedSeq) !== Number(frame.frameSeq)) {
489
+ return false;
490
+ }
491
+
492
+ const token = safeString(options.token, 128);
493
+ if (token && !timingSafeStringEqual(token, frame.accessToken)) {
494
+ return false;
495
+ }
496
+
497
+ if (options.requireToken === true && !token) {
498
+ return false;
499
+ }
500
+
501
+ return true;
502
+ }
503
+
504
+ function findRecentFramePayload(device, frameKind, options = {}) {
505
+ const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
506
+ const latest = kind === 'thumbnail'
507
+ ? device?.latestThumbnail
508
+ : device?.latestLiveFrame;
509
+ if (isFramePayloadRequestMatch(latest, options)) {
510
+ return latest;
511
+ }
512
+
513
+ const token = safeString(options.token, 128);
514
+ const requestedSeq = Number(options.frameSeq);
515
+ if (!token && !Number.isFinite(requestedSeq)) {
516
+ return null;
517
+ }
518
+
519
+ const cache = getRecentFrameCache(device, kind);
520
+ if (!cache) {
521
+ return null;
522
+ }
523
+
524
+ pruneRecentFrameCache(cache, kind);
525
+ const entry = cache.find(item => isFramePayloadRequestMatch(item?.frame, options));
526
+ return entry?.frame || null;
527
+ }
528
+
377
529
  function serializeDevice(device, options = {}) {
378
530
  if (!device) {
379
531
  return null;
@@ -809,6 +961,7 @@ export function createRemoteHub(options = {}) {
809
961
  delete frame.mode;
810
962
  delete frame.fps;
811
963
  device.latestThumbnail = frame;
964
+ rememberRecentFramePayload(device, 'thumbnail', frame);
812
965
  device.lastSeenAt = frame.receivedAt;
813
966
  device.counters.thumbnailFramesReceived += 1;
814
967
  emitRemoteEvent('RemoteFrameReceived', device, {
@@ -831,6 +984,7 @@ export function createRemoteHub(options = {}) {
831
984
  fps: options.fps || device.activeLiveStream.fps
832
985
  });
833
986
  device.latestLiveFrame = frame;
987
+ rememberRecentFramePayload(device, 'live', frame);
834
988
  device.activeLiveStream.lastFrameAt = frame.receivedAt;
835
989
  device.activeLiveStream.lastFrameSeq = frame.frameSeq;
836
990
  device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
@@ -968,6 +1122,10 @@ export function createRemoteHub(options = {}) {
968
1122
  },
969
1123
  latestThumbnail: null,
970
1124
  latestLiveFrame: null,
1125
+ recentFramePayloads: {
1126
+ thumbnail: [],
1127
+ live: []
1128
+ },
971
1129
  activeLiveStream: null,
972
1130
  latestTask,
973
1131
  recentTasks: latestTask ? [latestTask] : [],
@@ -992,6 +1150,7 @@ export function createRemoteHub(options = {}) {
992
1150
  frame.receivedAt = seenAt;
993
1151
  frame.capturedAt = seenAt;
994
1152
  device.latestThumbnail = frame;
1153
+ rememberRecentFramePayload(device, 'thumbnail', frame);
995
1154
  device.counters.thumbnailFramesReceived = 1;
996
1155
  }
997
1156
 
@@ -1074,6 +1233,10 @@ export function createRemoteHub(options = {}) {
1074
1233
  status: {},
1075
1234
  latestThumbnail: null,
1076
1235
  latestLiveFrame: null,
1236
+ recentFramePayloads: {
1237
+ thumbnail: [],
1238
+ live: []
1239
+ },
1077
1240
  activeLiveStream: null,
1078
1241
  latestTask: null,
1079
1242
  recentTasks: [],
@@ -1545,6 +1708,7 @@ export function createRemoteHub(options = {}) {
1545
1708
  payload,
1546
1709
  accessToken: createFrameAccessToken()
1547
1710
  };
1711
+ rememberRecentFramePayload(device, 'thumbnail', device.latestThumbnail);
1548
1712
  device.counters.thumbnailFramesReceived += 1;
1549
1713
  emitRemoteEvent('RemoteFrameReceived', device, {
1550
1714
  streamId: device.latestThumbnail.streamId,
@@ -1615,6 +1779,7 @@ export function createRemoteHub(options = {}) {
1615
1779
  payload,
1616
1780
  accessToken: createFrameAccessToken()
1617
1781
  };
1782
+ rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
1618
1783
  device.activeLiveStream.lastFrameAt = device.lastSeenAt;
1619
1784
  device.activeLiveStream.lastFrameSeq = frameSeq;
1620
1785
  device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
@@ -2376,29 +2541,18 @@ export function createRemoteHub(options = {}) {
2376
2541
 
2377
2542
  function getFramePayload(deviceId, frameKind, options = {}) {
2378
2543
  const device = devices.get(String(deviceId || ''));
2379
- const frame = frameKind === 'thumbnail'
2380
- ? device?.latestThumbnail
2381
- : device?.latestLiveFrame;
2382
- if (!frame || !Buffer.isBuffer(frame.payload)) {
2383
- return null;
2384
- }
2385
-
2386
- const requestedSeq = Number(options.frameSeq);
2387
- if (Number.isFinite(requestedSeq) && Math.floor(requestedSeq) !== frame.frameSeq) {
2388
- return null;
2389
- }
2390
-
2391
- const token = safeString(options.token, 128);
2392
- if (token && !timingSafeStringEqual(token, frame.accessToken)) {
2544
+ if (!device) {
2393
2545
  return null;
2394
2546
  }
2395
2547
 
2396
- if (options.requireToken === true && !token) {
2548
+ const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
2549
+ const frame = findRecentFramePayload(device, kind, options);
2550
+ if (!frame) {
2397
2551
  return null;
2398
2552
  }
2399
2553
 
2400
2554
  return {
2401
- frame: serializeRemoteFrame(frame, device.deviceId, frameKind, { includeDataUrl: false }),
2555
+ frame: serializeRemoteFrame(frame, device.deviceId, kind, { includeDataUrl: false }),
2402
2556
  payload: frame.payload,
2403
2557
  mimeType: safeString(frame.mimeType || frame.format || 'application/octet-stream', 120) || 'application/octet-stream',
2404
2558
  byteLength: frame.payload.length
@@ -143,6 +143,8 @@ try {
143
143
  });
144
144
  assert.equal(thumbnailDevice.latestThumbnail.streamId, 'smoke-thumb');
145
145
  assert.equal(thumbnailDevice.counters.thumbnailFramesReceived, 1);
146
+ const serializedFirstThumbnail = hub.getDeviceThumbnail('smoke-device', { includeDataUrl: false });
147
+ const serializedFirstThumbnailUrl = new URL(serializedFirstThumbnail.framePath, 'http://127.0.0.1');
146
148
 
147
149
  const binaryThumbnailCommand = hub.requestThumbnail('smoke-device', {
148
150
  streamId: 'smoke-thumb-binary',
@@ -190,6 +192,14 @@ try {
190
192
  assert.equal(thumbnailPayload?.mimeType, 'image/png');
191
193
  assert.equal(thumbnailPayload?.byteLength, smokePngFrame.length);
192
194
  assert.equal(Buffer.compare(thumbnailPayload.payload, smokePngFrame), 0);
195
+ const retainedThumbnailPayload = hub.getFramePayload('smoke-device', 'thumbnail', {
196
+ token: serializedFirstThumbnailUrl.searchParams.get('token'),
197
+ frameSeq: serializedFirstThumbnailUrl.searchParams.get('seq'),
198
+ requireToken: true
199
+ });
200
+ assert.equal(retainedThumbnailPayload?.frame?.frameSeq, 1);
201
+ assert.equal(retainedThumbnailPayload?.byteLength, smokePngFrame.length);
202
+ assert.equal(Buffer.compare(retainedThumbnailPayload.payload, smokePngFrame), 0);
193
203
  assert.equal(hub.getFramePayload('smoke-device', 'thumbnail', {
194
204
  token: 'wrong-token',
195
205
  frameSeq: 4,
@@ -225,6 +235,8 @@ try {
225
235
  assert.equal(liveDevice.activeLiveStream.active, true);
226
236
  assert.equal(liveDevice.latestLiveFrame.mode, 'remote-fast');
227
237
  assert.equal(liveDevice.counters.liveFramesReceived, 1);
238
+ const serializedFirstLiveFrame = hub.getDeviceLiveFrame('smoke-device', { includeDataUrl: false });
239
+ const serializedFirstLiveUrl = new URL(serializedFirstLiveFrame.framePath, 'http://127.0.0.1');
228
240
 
229
241
  writeBinaryFrame(socket, {
230
242
  frameKind: 'stream',
@@ -267,6 +279,14 @@ try {
267
279
  assert.equal(livePayload?.mimeType, 'image/png');
268
280
  assert.equal(livePayload?.byteLength, smokePngFrame.length);
269
281
  assert.equal(Buffer.compare(livePayload.payload, smokePngFrame), 0);
282
+ const retainedLivePayload = hub.getFramePayload('smoke-device', 'live', {
283
+ token: serializedFirstLiveUrl.searchParams.get('token'),
284
+ frameSeq: serializedFirstLiveUrl.searchParams.get('seq'),
285
+ requireToken: true
286
+ });
287
+ assert.equal(retainedLivePayload?.frame?.frameSeq, 2);
288
+ assert.equal(retainedLivePayload?.byteLength, smokePngFrame.length);
289
+ assert.equal(Buffer.compare(retainedLivePayload.payload, smokePngFrame), 0);
270
290
  assert.equal(hub.getFramePayload('smoke-device', 'live', {
271
291
  token: serializedLiveUrl.searchParams.get('token'),
272
292
  frameSeq: 4,