@camstack/system 1.2.39 → 1.2.41

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 (59) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/block-package.d.ts +59 -0
  12. package/dist/builtins/core-blocks/block-supervisor.d.ts +86 -0
  13. package/dist/builtins/core-blocks/core-blocks.addon.d.ts +18 -0
  14. package/dist/builtins/core-blocks/core-blocks.addon.js +533 -68
  15. package/dist/builtins/core-blocks/core-blocks.addon.mjs +528 -69
  16. package/dist/builtins/core-blocks/index.d.ts +5 -3
  17. package/dist/builtins/core-blocks/index.js +6 -0
  18. package/dist/builtins/core-blocks/index.mjs +2 -2
  19. package/dist/builtins/device-manager/device-manager.addon.js +14 -6
  20. package/dist/builtins/device-manager/device-manager.addon.mjs +14 -6
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  23. package/dist/builtins/hub-forwarder/index.js +1 -1
  24. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  26. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  28. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  29. package/dist/builtins/local-network/local-network.addon.js +1 -1
  30. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  31. package/dist/builtins/loki-logging/index.js +1 -1
  32. package/dist/builtins/loki-logging/index.mjs +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  34. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  35. package/dist/builtins/platform-probe/index.js +1 -1
  36. package/dist/builtins/platform-probe/index.mjs +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  39. package/dist/builtins/snapshot/index.js +281 -16
  40. package/dist/builtins/snapshot/index.mjs +282 -17
  41. package/dist/builtins/snapshot/snapshot-cache.d.ts +45 -0
  42. package/dist/builtins/snapshot/snapshot-media-handler.d.ts +32 -2
  43. package/dist/builtins/snapshot/snapshot-resize.d.ts +50 -0
  44. package/dist/builtins/snapshot/snapshot.addon.d.ts +13 -0
  45. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  47. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  51. package/dist/builtins/system-config/system-config.addon.js +1 -1
  52. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  53. package/dist/builtins/winston-logging/index.js +1 -1
  54. package/dist/builtins/winston-logging/index.mjs +1 -1
  55. package/dist/{dist-CFinlH7b.js → dist-BECNOXoK.js} +54 -2
  56. package/dist/{dist-DO76YVO2.mjs → dist-rjk6L9_P.mjs} +54 -2
  57. package/dist/index.js +1 -1
  58. package/dist/index.mjs +1 -1
  59. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CFinlH7b.js");
6
+ const require_dist = require("../../dist-BECNOXoK.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/builtins/snapshot/snapshot-coalescing.ts
9
9
  /**
@@ -202,6 +202,32 @@ function raceForResult(promise, timeoutMs) {
202
202
  //#endregion
203
203
  //#region src/builtins/snapshot/snapshot-media-handler.ts
204
204
  /**
205
+ * The widths a thumbnail may be served at.
206
+ *
207
+ * A ladder, not a free integer: the width is a cache key AND an ffmpeg run, so
208
+ * honouring `?w=` verbatim would let any caller mint unbounded work and
209
+ * unbounded memory. Twelve tiles measured at 181–240 px collapse onto one
210
+ * variant instead of twelve.
211
+ *
212
+ * Rungs are chosen for the card sizes the viewer actually renders (180–390 px)
213
+ * with headroom for a 2× device pixel ratio.
214
+ */
215
+ var SNAPSHOT_WIDTH_LADDER = [
216
+ 160,
217
+ 240,
218
+ 320,
219
+ 480,
220
+ 640,
221
+ 960
222
+ ];
223
+ /** Snap UP to the next rung — never below what was asked, so the client is not
224
+ * handed an image it has to upscale. Above the ladder, the largest rung: the
225
+ * point is to stop serving the 4K original. */
226
+ function snapSnapshotWidth(requested) {
227
+ for (const rung of SNAPSHOT_WIDTH_LADDER) if (requested <= rung) return rung;
228
+ return SNAPSHOT_WIDTH_LADDER[SNAPSHOT_WIDTH_LADDER.length - 1] ?? requested;
229
+ }
230
+ /**
205
231
  * Parse the handler-relative path (`/<deviceId>.jpg?query`) into a request.
206
232
  * Returns null for a malformed / nested / non-numeric id so the handler answers
207
233
  * 404 without ever reaching `getMedia`.
@@ -220,13 +246,33 @@ function parseSnapshotMediaRequest(url) {
220
246
  const rawStream = params.get("streamId");
221
247
  const streamId = rawStream !== null && rawStream.length > 0 ? rawStream : void 0;
222
248
  const rawForce = params.get("force");
249
+ const force = rawForce === "1" || rawForce === "true";
250
+ const rawWidth = params.get("w");
223
251
  return {
224
252
  deviceId,
225
253
  streamId,
226
- force: rawForce === "1" || rawForce === "true"
254
+ force,
255
+ width: rawWidth !== null && /^\d+$/.test(rawWidth) && Number.parseInt(rawWidth, 10) > 0 ? snapSnapshotWidth(Number.parseInt(rawWidth, 10)) : void 0
227
256
  };
228
257
  }
229
258
  /**
259
+ * The response's entity tag.
260
+ *
261
+ * It has to identify the VARIANT, not just the frame: the same capture can now
262
+ * be served at several widths and from several streams, and a plain
263
+ * `"<device>-<capturedAt>"` would let a client that fetched `?w=320` receive a
264
+ * 304 for `?w=960` and render the small image at full size. The base form is
265
+ * unchanged for a plain request, so the identity
266
+ * `snapshot.getSnapshotOverview` advertises still matches the frame nobody
267
+ * asked to resize.
268
+ */
269
+ function snapshotEtag(request, capturedAt) {
270
+ const parts = [`${String(request.deviceId)}-${String(capturedAt)}`];
271
+ if (request.streamId !== void 0) parts.push(`s${request.streamId}`);
272
+ if (request.width !== void 0) parts.push(`w${String(request.width)}`);
273
+ return `"${parts.join("-")}"`;
274
+ }
275
+ /**
230
276
  * Create a data-plane handler that serves per-device snapshots as JPEG images.
231
277
  * `deps.getMedia` is called once per request; null → 404, throw → 500. A
232
278
  * conditional GET with a matching `If-None-Match` produces a 304.
@@ -244,7 +290,7 @@ function createSnapshotMediaHandler(deps) {
244
290
  }
245
291
  let media;
246
292
  try {
247
- media = await deps.getMedia(parsed.deviceId, parsed.streamId, parsed.force);
293
+ media = await deps.getMedia(parsed.deviceId, parsed.streamId, parsed.force, parsed.width);
248
294
  } catch {
249
295
  const body = "Internal server error";
250
296
  res.writeHead(500, {
@@ -259,7 +305,7 @@ function createSnapshotMediaHandler(deps) {
259
305
  res.writeHead(404).end();
260
306
  return;
261
307
  }
262
- const etag = `"${parsed.deviceId}-${media.capturedAt}"`;
308
+ const etag = snapshotEtag(parsed, media.capturedAt);
263
309
  const cacheControl = `private, max-age=${Math.max(0, Math.floor(media.maxAgeS))}`;
264
310
  if (req.headers["if-none-match"] === etag) {
265
311
  res.writeHead(304, {
@@ -279,6 +325,182 @@ function createSnapshotMediaHandler(deps) {
279
325
  };
280
326
  }
281
327
  //#endregion
328
+ //#region src/builtins/snapshot/snapshot-cache.ts
329
+ /** A request with no explicit stream is its OWN entry, not a wildcard: the
330
+ * per-device preference decides what it captures, and conflating it with an
331
+ * explicit request is how the original bug read. */
332
+ var AUTO = "auto";
333
+ function keyOf(deviceId, streamId) {
334
+ return `${deviceId}:${streamId ?? AUTO}`;
335
+ }
336
+ var SnapshotCache = class {
337
+ byKey = /* @__PURE__ */ new Map();
338
+ /** deviceId → its live keys, so invalidation is O(streams) not O(cache). */
339
+ keysByDevice = /* @__PURE__ */ new Map();
340
+ get(deviceId, streamId) {
341
+ return this.byKey.get(keyOf(deviceId, streamId));
342
+ }
343
+ set(deviceId, streamId, entry) {
344
+ const key = keyOf(deviceId, streamId);
345
+ this.byKey.set(key, entry);
346
+ const keys = this.keysByDevice.get(deviceId) ?? /* @__PURE__ */ new Set();
347
+ keys.add(key);
348
+ this.keysByDevice.set(deviceId, keys);
349
+ }
350
+ /** The newest entry for a device, whichever stream produced it. */
351
+ latest(deviceId) {
352
+ let newest;
353
+ for (const key of this.keysByDevice.get(deviceId) ?? []) {
354
+ const entry = this.byKey.get(key);
355
+ if (entry !== void 0 && (newest === void 0 || entry.ts > newest.ts)) newest = entry;
356
+ }
357
+ return newest;
358
+ }
359
+ /**
360
+ * Drop every stream of one device.
361
+ *
362
+ * Device-wide on purpose: a settings change or an operator refresh
363
+ * invalidates the CAMERA, and leaving a sibling entry behind would keep
364
+ * serving the pre-change frame from the other key.
365
+ */
366
+ deleteDevice(deviceId) {
367
+ for (const key of this.keysByDevice.get(deviceId) ?? []) this.byKey.delete(key);
368
+ this.keysByDevice.delete(deviceId);
369
+ }
370
+ clear() {
371
+ this.byKey.clear();
372
+ this.keysByDevice.clear();
373
+ }
374
+ };
375
+ //#endregion
376
+ //#region src/builtins/snapshot/snapshot-resize.ts
377
+ /**
378
+ * Downscaling a captured frame to a card-sized thumbnail.
379
+ *
380
+ * Applied AFTER capture rather than during it, and that is deliberate: the two
381
+ * capture paths (the vendor's native HTTP snapshot and the ffmpeg keyframe
382
+ * grab) produce a JPEG by different routes, and only one of them has an
383
+ * ffmpeg filter chain to hook into. Resizing the finished bytes gives both the
384
+ * same behaviour with one implementation.
385
+ *
386
+ * The cost is one ffmpeg run per (device, stream, width) per cache window — not
387
+ * one per request. A camera whose card is on screen pays it once every
388
+ * `snapshotMaxAgeS`.
389
+ *
390
+ * `ffmpeg` is already this addon's dependency (the keyframe grab shells out to
391
+ * it), so this adds no new one.
392
+ */
393
+ var RESIZE_TIMEOUT_MS = 1e4;
394
+ /**
395
+ * Scale to `width`, preserving aspect ratio (`-2` keeps the height even, which
396
+ * mjpeg requires).
397
+ *
398
+ * Rejects on a non-zero exit or a timeout. It NEVER falls back to the original
399
+ * silently — a caller that quietly served the 4K frame when the resize failed
400
+ * would reproduce exactly the bug this whole change exists to fix, and nobody
401
+ * would see it. The caller decides, and logs.
402
+ */
403
+ function resizeJpeg(bytes, width, timeoutMs = RESIZE_TIMEOUT_MS) {
404
+ return new Promise((resolve, reject) => {
405
+ const child = (0, node_child_process.spawn)("ffmpeg", [
406
+ "-loglevel",
407
+ "error",
408
+ "-f",
409
+ "image2pipe",
410
+ "-i",
411
+ "pipe:0",
412
+ "-vf",
413
+ `scale=${String(width)}:-2:flags=bilinear`,
414
+ "-frames:v",
415
+ "1",
416
+ "-q:v",
417
+ "5",
418
+ "-f",
419
+ "image2pipe",
420
+ "-vcodec",
421
+ "mjpeg",
422
+ "pipe:1"
423
+ ], { stdio: [
424
+ "pipe",
425
+ "pipe",
426
+ "pipe"
427
+ ] });
428
+ const out = [];
429
+ let err = "";
430
+ let settled = false;
431
+ const timer = setTimeout(() => {
432
+ if (settled) return;
433
+ settled = true;
434
+ child.kill("SIGKILL");
435
+ reject(/* @__PURE__ */ new Error(`snapshot resize timed out after ${String(timeoutMs)}ms`));
436
+ }, timeoutMs);
437
+ child.stdout.on("data", (chunk) => out.push(chunk));
438
+ child.stderr.on("data", (chunk) => {
439
+ err += chunk.toString();
440
+ });
441
+ child.on("error", (e) => {
442
+ if (settled) return;
443
+ settled = true;
444
+ clearTimeout(timer);
445
+ reject(e);
446
+ });
447
+ child.on("close", (code) => {
448
+ if (settled) return;
449
+ settled = true;
450
+ clearTimeout(timer);
451
+ const buf = Buffer.concat(out);
452
+ if (code !== 0 || buf.length === 0) {
453
+ reject(/* @__PURE__ */ new Error(`snapshot resize failed (code ${String(code)}): ${err.trim() || "no output"}`));
454
+ return;
455
+ }
456
+ resolve(buf);
457
+ });
458
+ child.stdin.on("error", () => {});
459
+ child.stdin.end(bytes);
460
+ });
461
+ }
462
+ /**
463
+ * Resized frames, keyed by (device, stream, width) AND validated against the
464
+ * source frame's timestamp.
465
+ *
466
+ * The timestamp is the whole correctness argument: a variant outlives nothing.
467
+ * When the underlying frame is recaptured its `capturedAt` moves, every variant
468
+ * derived from the old one stops matching, and the next request re-derives.
469
+ * Without that check a card would keep showing a thumbnail of a frame the
470
+ * full-size view had already replaced.
471
+ */
472
+ var SnapshotVariantCache = class SnapshotVariantCache {
473
+ byKey = /* @__PURE__ */ new Map();
474
+ keysByDevice = /* @__PURE__ */ new Map();
475
+ static key(deviceId, streamId, width) {
476
+ return `${deviceId}:${streamId ?? "auto"}:${width}`;
477
+ }
478
+ /** The variant for this exact frame, or undefined when it is missing or was
479
+ * derived from an older capture. */
480
+ get(deviceId, streamId, width, sourceTs) {
481
+ const entry = this.byKey.get(SnapshotVariantCache.key(deviceId, streamId, width));
482
+ return entry !== void 0 && entry.sourceTs === sourceTs ? entry.bytes : void 0;
483
+ }
484
+ set(deviceId, streamId, width, sourceTs, bytes) {
485
+ const key = SnapshotVariantCache.key(deviceId, streamId, width);
486
+ this.byKey.set(key, {
487
+ bytes,
488
+ sourceTs
489
+ });
490
+ const keys = this.keysByDevice.get(deviceId) ?? /* @__PURE__ */ new Set();
491
+ keys.add(key);
492
+ this.keysByDevice.set(deviceId, keys);
493
+ }
494
+ deleteDevice(deviceId) {
495
+ for (const key of this.keysByDevice.get(deviceId) ?? []) this.byKey.delete(key);
496
+ this.keysByDevice.delete(deviceId);
497
+ }
498
+ clear() {
499
+ this.byKey.clear();
500
+ this.keysByDevice.clear();
501
+ }
502
+ };
503
+ //#endregion
282
504
  //#region src/builtins/snapshot/snapshot.addon.ts
283
505
  /** Default cache window for non-battery cams (seconds). 10s feels live. */
284
506
  var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
@@ -314,7 +536,10 @@ function effectiveMaxAgeS(prefs, isBattery) {
314
536
  * publishes stream endpoints.
315
537
  */
316
538
  var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
317
- cache = /* @__PURE__ */ new Map();
539
+ cache = new SnapshotCache();
540
+ /** Card-sized derivatives of the frames above, keyed by width and validated
541
+ * against the source frame's timestamp. */
542
+ variants = new SnapshotVariantCache();
318
543
  /**
319
544
  * De-dupes concurrent captures per `${deviceId}:${streamId}` and holds a
320
545
  * settled SUCCESS for COALESCE_MS so a grid-mount burst (and a row of refresh
@@ -372,7 +597,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
372
597
  * base64 path.
373
598
  */
374
599
  async serveMediaDataPlane() {
375
- const handler = createSnapshotMediaHandler({ getMedia: (deviceId, streamId, force) => this.resolveSnapshotMedia(deviceId, streamId, force) });
600
+ const handler = createSnapshotMediaHandler({ getMedia: (deviceId, streamId, force, width) => this.resolveSnapshotMedia(deviceId, streamId, force, width) });
376
601
  try {
377
602
  this.mediaDataPlane = await this.ctx.dataPlane?.serve({
378
603
  prefix: "media",
@@ -393,29 +618,67 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
393
618
  * the cache entry (matching the ETag `getSnapshotOverview` advertises) and the
394
619
  * effective per-device `maxAgeS` for `Cache-Control`. Null → 404.
395
620
  */
396
- async resolveSnapshotMedia(deviceId, streamId, force) {
621
+ async resolveSnapshotMedia(deviceId, streamId, force, width) {
397
622
  const image = await this.getSnapshot({
398
623
  deviceId,
399
624
  ...streamId !== void 0 ? { streamId } : {},
400
625
  force
401
626
  });
402
627
  if (!image) return null;
403
- const capturedAt = this.cache.get(deviceId)?.ts ?? Date.now();
628
+ const capturedAt = this.cache.get(deviceId, streamId)?.ts ?? Date.now();
404
629
  const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
405
630
  const isBattery = (await this.lookupDeviceMeta(deviceId))?.isBattery ?? false;
406
631
  return {
407
- bytes: Buffer.from(image.base64, "base64"),
632
+ bytes: width === void 0 ? Buffer.from(image.base64, "base64") : await this.thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs),
408
633
  contentType: image.contentType,
409
634
  capturedAt,
410
635
  maxAgeS: effectiveMaxAgeS(prefs, isBattery)
411
636
  };
412
637
  }
638
+ /**
639
+ * The frame at a card-sized width, derived once per (device, stream, width)
640
+ * per capture.
641
+ *
642
+ * A failed resize falls back to the FULL frame — a visible card beats a
643
+ * broken one — but never silently: the card would otherwise keep costing a
644
+ * megabyte with nothing anywhere saying why, which is the shape of the bug
645
+ * this whole change came from.
646
+ */
647
+ async thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs) {
648
+ const cached = this.variants.get(deviceId, streamId, width, capturedAt);
649
+ if (cached !== void 0) return cached;
650
+ const full = Buffer.from(image.base64, "base64");
651
+ try {
652
+ const resized = await resizeJpeg(full, width);
653
+ this.variants.set(deviceId, streamId, width, capturedAt, resized);
654
+ if (prefs.snapshotDebug) this.ctx.logger.info("snapshot: thumbnail derived", {
655
+ tags: { deviceId },
656
+ meta: {
657
+ width,
658
+ fullBytes: full.length,
659
+ thumbBytes: resized.length
660
+ }
661
+ });
662
+ return resized;
663
+ } catch (err) {
664
+ this.ctx.logger.warn("snapshot: resize failed — serving the full frame", {
665
+ tags: { deviceId },
666
+ meta: {
667
+ width,
668
+ bytes: full.length,
669
+ error: require_dist.errMsg(err)
670
+ }
671
+ });
672
+ return full;
673
+ }
674
+ }
413
675
  async onShutdown() {
414
676
  if (this.mediaDataPlane) {
415
677
  await this.mediaDataPlane.dispose().catch(() => void 0);
416
678
  this.mediaDataPlane = null;
417
679
  }
418
680
  this.cache.clear();
681
+ this.variants.clear();
419
682
  this.captureFlight.clear();
420
683
  this.ownerCache = null;
421
684
  }
@@ -459,7 +722,6 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
459
722
  ...deviceName ? { deviceName } : {}
460
723
  });
461
724
  const now = Date.now();
462
- const hit = this.cache.get(deviceId);
463
725
  const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
464
726
  const rawPref = prefs.snapshotStreamId;
465
727
  const effectiveStreamId = input.streamId ?? (rawPref && rawPref !== "auto" ? rawPref : void 0);
@@ -467,6 +729,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
467
729
  tags: { deviceId },
468
730
  meta: { stream: effectiveStreamId ?? "auto" }
469
731
  });
732
+ const hit = this.cache.get(deviceId, effectiveStreamId);
470
733
  const effectiveMaxAgeMs = effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3;
471
734
  const decision = decideSnapshotServe({
472
735
  now,
@@ -575,7 +838,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
575
838
  if (native) {
576
839
  const result = await this.nativePool.run(() => native.getSnapshot(input));
577
840
  if (result) {
578
- this.cache.set(deviceId, {
841
+ this.cache.set(deviceId, effectiveStreamId, {
579
842
  data: result,
580
843
  ts: now,
581
844
  streamId: effectiveStreamId ?? null
@@ -609,7 +872,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
609
872
  if (!(isBatteryDevice && nativeAbsent && !await this.hasStreamingBrokerForDevice(deviceId))) try {
610
873
  const fallback = await this.grabFrameFromBroker(deviceId, effectiveStreamId);
611
874
  if (fallback) {
612
- this.cache.set(deviceId, {
875
+ this.cache.set(deviceId, effectiveStreamId, {
613
876
  data: fallback,
614
877
  ts: now,
615
878
  streamId: effectiveStreamId ?? null
@@ -755,7 +1018,8 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
755
1018
  };
756
1019
  }
757
1020
  async invalidateCache(input) {
758
- this.cache.delete(input.deviceId);
1021
+ this.cache.deleteDevice(input.deviceId);
1022
+ this.variants.deleteDevice(input.deviceId);
759
1023
  this.captureFlight.invalidatePrefix(`${input.deviceId}:`);
760
1024
  }
761
1025
  /**
@@ -829,7 +1093,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
829
1093
  getSnapshotOverview(input) {
830
1094
  const now = Date.now();
831
1095
  const rows = input.deviceIds.map((deviceId) => {
832
- const hit = this.cache.get(deviceId);
1096
+ const hit = this.cache.latest(deviceId);
833
1097
  if (!hit) return {
834
1098
  deviceId,
835
1099
  lastCapturedAt: null,
@@ -853,7 +1117,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
853
1117
  * (cache miss) since the addon started.
854
1118
  */
855
1119
  async getStatus(deviceId) {
856
- const hit = this.cache.get(deviceId);
1120
+ const hit = this.cache.latest(deviceId);
857
1121
  if (!hit) return null;
858
1122
  return {
859
1123
  lastCapturedAt: hit.ts,
@@ -1001,7 +1265,8 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
1001
1265
  else delete next["snapshotMaxAgeS"];
1002
1266
  }
1003
1267
  await this.ctx.settings.writeDeviceStore(deviceId, next);
1004
- this.cache.delete(deviceId);
1268
+ this.cache.deleteDevice(deviceId);
1269
+ this.variants.deleteDevice(deviceId);
1005
1270
  this.captureFlight.invalidatePrefix(`${deviceId}:`);
1006
1271
  return { success: true };
1007
1272
  }