@camstack/system 1.2.73 → 1.2.74

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.
@@ -788,6 +788,8 @@ function parseVerifiedSnapshotLink(input) {
788
788
  * answers inside it, short enough that a dead camera cannot stall the batch.
789
789
  */
790
790
  var LINK_MINT_DEADLINE_MS = 2500;
791
+ /** Frames older than this are never represented as current by the Viewer. */
792
+ var LINK_CURRENT_MAX_AGE_MS = 15e3;
791
793
  /** Default cache window for non-battery cams (seconds). 10s feels live. */
792
794
  var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
793
795
  /** Default cache window for battery cams (seconds). 1h ≈ "don't wake the cam unless asked". */
@@ -1033,8 +1035,10 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
1033
1035
  *
1034
1036
  * So: note every target (creating a subscription, which `getSnapshotOverview`
1035
1037
  * deliberately cannot), then join the ordinary non-forced capture path and
1036
- * wait a BOUNDED time for it. What comes back is the identity of the frame the
1037
- * link will actually serve.
1038
+ * wait a BOUNDED time for it. The Viewer never presents a frame older than
1039
+ * 15 seconds as current, so this path tightens a looser per-device cache
1040
+ * window to that contract without using `force` (and its battery-wake
1041
+ * semantics). What comes back is the identity of the frame the link serves.
1038
1042
  *
1039
1043
  * The bound matters more than its exact value. A cold fleet is paced by
1040
1044
  * `MAX_CONCURRENT_GRABS`, so sixteen cameras cannot all capture at once and
@@ -1085,19 +1089,25 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
1085
1089
  /**
1086
1090
  * Join the ordinary capture path for one device and wait, bounded, for it.
1087
1091
  *
1088
- * Deliberately `getSnapshot` with `force: false` and nothing else: every gate
1089
- * that already owns this decision the per-device max-age, the single flight,
1090
- * the grab pool, the sleeping-battery refusal — applies unchanged. A fresh
1091
- * cache returns immediately; a stale one starts the refresh. There is no
1092
- * second freshness policy here, which is the mistake D93 called out.
1092
+ * Deliberately `getSnapshot` with `force: false`: the single flight, grab
1093
+ * pool, and sleeping-battery refusal apply unchanged. The only override is
1094
+ * the Viewer's 15-second currency contract and the link-mint wait bound.
1093
1095
  */
1094
1096
  async awaitFreshEnough(deviceId) {
1095
- if (!(await raceForResult(this.getSnapshot({
1097
+ const before = this.cache.latest(deviceId)?.ts ?? null;
1098
+ const alreadyCurrent = before !== null && Date.now() - before <= LINK_CURRENT_MAX_AGE_MS;
1099
+ await this.getSnapshot({
1096
1100
  deviceId,
1097
1101
  force: false
1098
- }).catch(() => null), LINK_MINT_DEADLINE_MS)).settled) this.ctx.logger.debug("snapshot: link mint gave up waiting; serving the older frame", {
1102
+ }, 0, LINK_MINT_DEADLINE_MS, LINK_CURRENT_MAX_AGE_MS).catch(() => null);
1103
+ const current = this.cache.latest(deviceId)?.ts ?? null;
1104
+ if (alreadyCurrent || current !== null && (before === null || current > before)) return;
1105
+ this.ctx.logger.debug("snapshot: link mint gave up waiting; serving the older frame", {
1099
1106
  tags: { deviceId },
1100
- meta: { deadlineMs: LINK_MINT_DEADLINE_MS }
1107
+ meta: {
1108
+ deadlineMs: LINK_MINT_DEADLINE_MS,
1109
+ previousCapturedAt: before
1110
+ }
1101
1111
  });
1102
1112
  }
1103
1113
  /** True only for a BATTERY device that is currently asleep. Kept as one
@@ -1346,7 +1356,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
1346
1356
  })]
1347
1357
  }] });
1348
1358
  }
1349
- async getSnapshot(input, warmAheadMs = 0) {
1359
+ async getSnapshot(input, warmAheadMs = 0, minimumCaptureWaitMs = 0, maximumCacheAgeMs = Number.POSITIVE_INFINITY) {
1350
1360
  const { deviceId } = input;
1351
1361
  const force = input.force === true;
1352
1362
  const meta = await this.lookupDeviceMeta(deviceId);
@@ -1365,7 +1375,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
1365
1375
  meta: { stream: effectiveStreamId ?? "auto" }
1366
1376
  });
1367
1377
  const hit = this.cache.get(deviceId, effectiveStreamId);
1368
- const effectiveMaxAgeMs = Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3 - warmAheadMs);
1378
+ const effectiveMaxAgeMs = Math.min(maximumCacheAgeMs, Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3 - warmAheadMs));
1369
1379
  const decision = decideSnapshotServe({
1370
1380
  now,
1371
1381
  cachedAt: hit?.ts ?? null,
@@ -1402,7 +1412,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
1402
1412
  log
1403
1413
  }));
1404
1414
  flight.catch(() => void 0);
1405
- const raced = await raceForResult(flight, decision.waitMs);
1415
+ const raced = await raceForResult(flight, Math.max(decision.waitMs, minimumCaptureWaitMs));
1406
1416
  if (raced.settled) return this.resolveOutcome(raced.value, deviceId, hit, log);
1407
1417
  if (decision.staleFallback && hit) {
1408
1418
  if (prefs.snapshotDebug) log.debug("snapshot: SWR — returning stale frame; refresh continues in background", {
@@ -783,6 +783,8 @@ function parseVerifiedSnapshotLink(input) {
783
783
  * answers inside it, short enough that a dead camera cannot stall the batch.
784
784
  */
785
785
  var LINK_MINT_DEADLINE_MS = 2500;
786
+ /** Frames older than this are never represented as current by the Viewer. */
787
+ var LINK_CURRENT_MAX_AGE_MS = 15e3;
786
788
  /** Default cache window for non-battery cams (seconds). 10s feels live. */
787
789
  var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
788
790
  /** Default cache window for battery cams (seconds). 1h ≈ "don't wake the cam unless asked". */
@@ -1028,8 +1030,10 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
1028
1030
  *
1029
1031
  * So: note every target (creating a subscription, which `getSnapshotOverview`
1030
1032
  * deliberately cannot), then join the ordinary non-forced capture path and
1031
- * wait a BOUNDED time for it. What comes back is the identity of the frame the
1032
- * link will actually serve.
1033
+ * wait a BOUNDED time for it. The Viewer never presents a frame older than
1034
+ * 15 seconds as current, so this path tightens a looser per-device cache
1035
+ * window to that contract without using `force` (and its battery-wake
1036
+ * semantics). What comes back is the identity of the frame the link serves.
1033
1037
  *
1034
1038
  * The bound matters more than its exact value. A cold fleet is paced by
1035
1039
  * `MAX_CONCURRENT_GRABS`, so sixteen cameras cannot all capture at once and
@@ -1080,19 +1084,25 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
1080
1084
  /**
1081
1085
  * Join the ordinary capture path for one device and wait, bounded, for it.
1082
1086
  *
1083
- * Deliberately `getSnapshot` with `force: false` and nothing else: every gate
1084
- * that already owns this decision the per-device max-age, the single flight,
1085
- * the grab pool, the sleeping-battery refusal — applies unchanged. A fresh
1086
- * cache returns immediately; a stale one starts the refresh. There is no
1087
- * second freshness policy here, which is the mistake D93 called out.
1087
+ * Deliberately `getSnapshot` with `force: false`: the single flight, grab
1088
+ * pool, and sleeping-battery refusal apply unchanged. The only override is
1089
+ * the Viewer's 15-second currency contract and the link-mint wait bound.
1088
1090
  */
1089
1091
  async awaitFreshEnough(deviceId) {
1090
- if (!(await raceForResult(this.getSnapshot({
1092
+ const before = this.cache.latest(deviceId)?.ts ?? null;
1093
+ const alreadyCurrent = before !== null && Date.now() - before <= LINK_CURRENT_MAX_AGE_MS;
1094
+ await this.getSnapshot({
1091
1095
  deviceId,
1092
1096
  force: false
1093
- }).catch(() => null), LINK_MINT_DEADLINE_MS)).settled) this.ctx.logger.debug("snapshot: link mint gave up waiting; serving the older frame", {
1097
+ }, 0, LINK_MINT_DEADLINE_MS, LINK_CURRENT_MAX_AGE_MS).catch(() => null);
1098
+ const current = this.cache.latest(deviceId)?.ts ?? null;
1099
+ if (alreadyCurrent || current !== null && (before === null || current > before)) return;
1100
+ this.ctx.logger.debug("snapshot: link mint gave up waiting; serving the older frame", {
1094
1101
  tags: { deviceId },
1095
- meta: { deadlineMs: LINK_MINT_DEADLINE_MS }
1102
+ meta: {
1103
+ deadlineMs: LINK_MINT_DEADLINE_MS,
1104
+ previousCapturedAt: before
1105
+ }
1096
1106
  });
1097
1107
  }
1098
1108
  /** True only for a BATTERY device that is currently asleep. Kept as one
@@ -1341,7 +1351,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
1341
1351
  })]
1342
1352
  }] });
1343
1353
  }
1344
- async getSnapshot(input, warmAheadMs = 0) {
1354
+ async getSnapshot(input, warmAheadMs = 0, minimumCaptureWaitMs = 0, maximumCacheAgeMs = Number.POSITIVE_INFINITY) {
1345
1355
  const { deviceId } = input;
1346
1356
  const force = input.force === true;
1347
1357
  const meta = await this.lookupDeviceMeta(deviceId);
@@ -1360,7 +1370,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
1360
1370
  meta: { stream: effectiveStreamId ?? "auto" }
1361
1371
  });
1362
1372
  const hit = this.cache.get(deviceId, effectiveStreamId);
1363
- const effectiveMaxAgeMs = Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3 - warmAheadMs);
1373
+ const effectiveMaxAgeMs = Math.min(maximumCacheAgeMs, Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3 - warmAheadMs));
1364
1374
  const decision = decideSnapshotServe({
1365
1375
  now,
1366
1376
  cachedAt: hit?.ts ?? null,
@@ -1397,7 +1407,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
1397
1407
  log
1398
1408
  }));
1399
1409
  flight.catch(() => void 0);
1400
- const raced = await raceForResult(flight, decision.waitMs);
1410
+ const raced = await raceForResult(flight, Math.max(decision.waitMs, minimumCaptureWaitMs));
1401
1411
  if (raced.settled) return this.resolveOutcome(raced.value, deviceId, hit, log);
1402
1412
  if (decision.staleFallback && hit) {
1403
1413
  if (prefs.snapshotDebug) log.debug("snapshot: SWR — returning stale frame; refresh continues in background", {
@@ -146,8 +146,10 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
146
146
  *
147
147
  * So: note every target (creating a subscription, which `getSnapshotOverview`
148
148
  * deliberately cannot), then join the ordinary non-forced capture path and
149
- * wait a BOUNDED time for it. What comes back is the identity of the frame the
150
- * link will actually serve.
149
+ * wait a BOUNDED time for it. The Viewer never presents a frame older than
150
+ * 15 seconds as current, so this path tightens a looser per-device cache
151
+ * window to that contract without using `force` (and its battery-wake
152
+ * semantics). What comes back is the identity of the frame the link serves.
151
153
  *
152
154
  * The bound matters more than its exact value. A cold fleet is paced by
153
155
  * `MAX_CONCURRENT_GRABS`, so sixteen cameras cannot all capture at once and
@@ -160,11 +162,9 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
160
162
  /**
161
163
  * Join the ordinary capture path for one device and wait, bounded, for it.
162
164
  *
163
- * Deliberately `getSnapshot` with `force: false` and nothing else: every gate
164
- * that already owns this decision the per-device max-age, the single flight,
165
- * the grab pool, the sleeping-battery refusal — applies unchanged. A fresh
166
- * cache returns immediately; a stale one starts the refresh. There is no
167
- * second freshness policy here, which is the mistake D93 called out.
165
+ * Deliberately `getSnapshot` with `force: false`: the single flight, grab
166
+ * pool, and sleeping-battery refusal apply unchanged. The only override is
167
+ * the Viewer's 15-second currency contract and the link-mint wait bound.
168
168
  */
169
169
  private awaitFreshEnough;
170
170
  /** True only for a BATTERY device that is currently asleep. Kept as one
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.73",
3
+ "version": "1.2.74",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",