@camstack/system 1.2.73 → 1.2.75
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,15 @@ 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
|
+
/**
|
|
792
|
+
* A links RPC is one Viewer-wide batch. Never let one slow camera hold every
|
|
793
|
+
* fast camera behind its individual capture deadline: wait briefly for the
|
|
794
|
+
* common fast path, then mint from the cache as it stands. Slow captures keep
|
|
795
|
+
* running and their new generation is returned by the next 5 s poll.
|
|
796
|
+
*/
|
|
797
|
+
var LINK_BATCH_WAIT_MS = 750;
|
|
798
|
+
/** Frames older than this are never represented as current by the Viewer. */
|
|
799
|
+
var LINK_CURRENT_MAX_AGE_MS = 15e3;
|
|
791
800
|
/** Default cache window for non-battery cams (seconds). 10s feels live. */
|
|
792
801
|
var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
|
|
793
802
|
/** Default cache window for battery cams (seconds). 1h ≈ "don't wake the cam unless asked". */
|
|
@@ -1033,8 +1042,10 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
1033
1042
|
*
|
|
1034
1043
|
* So: note every target (creating a subscription, which `getSnapshotOverview`
|
|
1035
1044
|
* deliberately cannot), then join the ordinary non-forced capture path and
|
|
1036
|
-
* wait a BOUNDED time for it.
|
|
1037
|
-
*
|
|
1045
|
+
* wait a BOUNDED time for it. The Viewer never presents a frame older than
|
|
1046
|
+
* 15 seconds as current, so this path tightens a looser per-device cache
|
|
1047
|
+
* window to that contract without using `force` (and its battery-wake
|
|
1048
|
+
* semantics). What comes back is the identity of the frame the link serves.
|
|
1038
1049
|
*
|
|
1039
1050
|
* The bound matters more than its exact value. A cold fleet is paced by
|
|
1040
1051
|
* `MAX_CONCURRENT_GRABS`, so sixteen cameras cannot all capture at once and
|
|
@@ -1049,12 +1060,18 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
1049
1060
|
const expMs = snapshotLinkExpiry(now, SNAPSHOT_LINK_TTL_MS);
|
|
1050
1061
|
const served = this.linkDataPlane !== null && secret !== null;
|
|
1051
1062
|
const prefix = this.linkRoutePrefix();
|
|
1052
|
-
const
|
|
1063
|
+
const sleepingByDevice = /* @__PURE__ */ new Map();
|
|
1064
|
+
const refreshes = input.targets.map(async (target) => {
|
|
1065
|
+
const width = target.width === void 0 ? void 0 : snapSnapshotWidth(target.width);
|
|
1066
|
+
this.subscriptions.note(target.deviceId, void 0, width, now);
|
|
1067
|
+
const sleeping = await this.isSleepingBatteryDevice(target.deviceId);
|
|
1068
|
+
sleepingByDevice.set(target.deviceId, sleeping);
|
|
1069
|
+
if (!sleeping) await this.awaitFreshEnough(target.deviceId);
|
|
1070
|
+
});
|
|
1071
|
+
await raceForResult(Promise.allSettled(refreshes), LINK_BATCH_WAIT_MS);
|
|
1072
|
+
const rows = input.targets.map((target) => {
|
|
1053
1073
|
const { deviceId } = target;
|
|
1054
1074
|
const width = target.width === void 0 ? void 0 : snapSnapshotWidth(target.width);
|
|
1055
|
-
this.subscriptions.note(deviceId, void 0, width, now);
|
|
1056
|
-
const sleeping = await this.isSleepingBatteryDevice(deviceId);
|
|
1057
|
-
if (!sleeping) await this.awaitFreshEnough(deviceId);
|
|
1058
1075
|
const capturedAt = this.cache.latest(deviceId)?.ts ?? null;
|
|
1059
1076
|
const answeredAt = Date.now();
|
|
1060
1077
|
return {
|
|
@@ -1072,9 +1089,9 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
1072
1089
|
expiresAt: served ? expMs : null,
|
|
1073
1090
|
width: width ?? null,
|
|
1074
1091
|
neverCaptured: capturedAt === null,
|
|
1075
|
-
sleeping
|
|
1092
|
+
sleeping: sleepingByDevice.get(deviceId) ?? false
|
|
1076
1093
|
};
|
|
1077
|
-
})
|
|
1094
|
+
});
|
|
1078
1095
|
this.ctx.logger.debug("snapshot: minted links", { meta: {
|
|
1079
1096
|
targets: input.targets.length,
|
|
1080
1097
|
served,
|
|
@@ -1085,19 +1102,25 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
1085
1102
|
/**
|
|
1086
1103
|
* Join the ordinary capture path for one device and wait, bounded, for it.
|
|
1087
1104
|
*
|
|
1088
|
-
* Deliberately `getSnapshot` with `force: false
|
|
1089
|
-
*
|
|
1090
|
-
* the
|
|
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.
|
|
1105
|
+
* Deliberately `getSnapshot` with `force: false`: the single flight, grab
|
|
1106
|
+
* pool, and sleeping-battery refusal apply unchanged. The only override is
|
|
1107
|
+
* the Viewer's 15-second currency contract and the link-mint wait bound.
|
|
1093
1108
|
*/
|
|
1094
1109
|
async awaitFreshEnough(deviceId) {
|
|
1095
|
-
|
|
1110
|
+
const before = this.cache.latest(deviceId)?.ts ?? null;
|
|
1111
|
+
const alreadyCurrent = before !== null && Date.now() - before <= LINK_CURRENT_MAX_AGE_MS;
|
|
1112
|
+
await this.getSnapshot({
|
|
1096
1113
|
deviceId,
|
|
1097
1114
|
force: false
|
|
1098
|
-
}).catch(() => null)
|
|
1115
|
+
}, 0, LINK_MINT_DEADLINE_MS, LINK_CURRENT_MAX_AGE_MS).catch(() => null);
|
|
1116
|
+
const current = this.cache.latest(deviceId)?.ts ?? null;
|
|
1117
|
+
if (alreadyCurrent || current !== null && (before === null || current > before)) return;
|
|
1118
|
+
this.ctx.logger.debug("snapshot: link mint gave up waiting; serving the older frame", {
|
|
1099
1119
|
tags: { deviceId },
|
|
1100
|
-
meta: {
|
|
1120
|
+
meta: {
|
|
1121
|
+
deadlineMs: LINK_MINT_DEADLINE_MS,
|
|
1122
|
+
previousCapturedAt: before
|
|
1123
|
+
}
|
|
1101
1124
|
});
|
|
1102
1125
|
}
|
|
1103
1126
|
/** True only for a BATTERY device that is currently asleep. Kept as one
|
|
@@ -1346,7 +1369,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
1346
1369
|
})]
|
|
1347
1370
|
}] });
|
|
1348
1371
|
}
|
|
1349
|
-
async getSnapshot(input, warmAheadMs = 0) {
|
|
1372
|
+
async getSnapshot(input, warmAheadMs = 0, minimumCaptureWaitMs = 0, maximumCacheAgeMs = Number.POSITIVE_INFINITY) {
|
|
1350
1373
|
const { deviceId } = input;
|
|
1351
1374
|
const force = input.force === true;
|
|
1352
1375
|
const meta = await this.lookupDeviceMeta(deviceId);
|
|
@@ -1365,7 +1388,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
1365
1388
|
meta: { stream: effectiveStreamId ?? "auto" }
|
|
1366
1389
|
});
|
|
1367
1390
|
const hit = this.cache.get(deviceId, effectiveStreamId);
|
|
1368
|
-
const effectiveMaxAgeMs = Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3 - warmAheadMs);
|
|
1391
|
+
const effectiveMaxAgeMs = Math.min(maximumCacheAgeMs, Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3 - warmAheadMs));
|
|
1369
1392
|
const decision = decideSnapshotServe({
|
|
1370
1393
|
now,
|
|
1371
1394
|
cachedAt: hit?.ts ?? null,
|
|
@@ -1402,7 +1425,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
1402
1425
|
log
|
|
1403
1426
|
}));
|
|
1404
1427
|
flight.catch(() => void 0);
|
|
1405
|
-
const raced = await raceForResult(flight, decision.waitMs);
|
|
1428
|
+
const raced = await raceForResult(flight, Math.max(decision.waitMs, minimumCaptureWaitMs));
|
|
1406
1429
|
if (raced.settled) return this.resolveOutcome(raced.value, deviceId, hit, log);
|
|
1407
1430
|
if (decision.staleFallback && hit) {
|
|
1408
1431
|
if (prefs.snapshotDebug) log.debug("snapshot: SWR — returning stale frame; refresh continues in background", {
|
|
@@ -783,6 +783,15 @@ 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
|
+
/**
|
|
787
|
+
* A links RPC is one Viewer-wide batch. Never let one slow camera hold every
|
|
788
|
+
* fast camera behind its individual capture deadline: wait briefly for the
|
|
789
|
+
* common fast path, then mint from the cache as it stands. Slow captures keep
|
|
790
|
+
* running and their new generation is returned by the next 5 s poll.
|
|
791
|
+
*/
|
|
792
|
+
var LINK_BATCH_WAIT_MS = 750;
|
|
793
|
+
/** Frames older than this are never represented as current by the Viewer. */
|
|
794
|
+
var LINK_CURRENT_MAX_AGE_MS = 15e3;
|
|
786
795
|
/** Default cache window for non-battery cams (seconds). 10s feels live. */
|
|
787
796
|
var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
|
|
788
797
|
/** Default cache window for battery cams (seconds). 1h ≈ "don't wake the cam unless asked". */
|
|
@@ -1028,8 +1037,10 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1028
1037
|
*
|
|
1029
1038
|
* So: note every target (creating a subscription, which `getSnapshotOverview`
|
|
1030
1039
|
* deliberately cannot), then join the ordinary non-forced capture path and
|
|
1031
|
-
* wait a BOUNDED time for it.
|
|
1032
|
-
*
|
|
1040
|
+
* wait a BOUNDED time for it. The Viewer never presents a frame older than
|
|
1041
|
+
* 15 seconds as current, so this path tightens a looser per-device cache
|
|
1042
|
+
* window to that contract without using `force` (and its battery-wake
|
|
1043
|
+
* semantics). What comes back is the identity of the frame the link serves.
|
|
1033
1044
|
*
|
|
1034
1045
|
* The bound matters more than its exact value. A cold fleet is paced by
|
|
1035
1046
|
* `MAX_CONCURRENT_GRABS`, so sixteen cameras cannot all capture at once and
|
|
@@ -1044,12 +1055,18 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1044
1055
|
const expMs = snapshotLinkExpiry(now, SNAPSHOT_LINK_TTL_MS);
|
|
1045
1056
|
const served = this.linkDataPlane !== null && secret !== null;
|
|
1046
1057
|
const prefix = this.linkRoutePrefix();
|
|
1047
|
-
const
|
|
1058
|
+
const sleepingByDevice = /* @__PURE__ */ new Map();
|
|
1059
|
+
const refreshes = input.targets.map(async (target) => {
|
|
1060
|
+
const width = target.width === void 0 ? void 0 : snapSnapshotWidth(target.width);
|
|
1061
|
+
this.subscriptions.note(target.deviceId, void 0, width, now);
|
|
1062
|
+
const sleeping = await this.isSleepingBatteryDevice(target.deviceId);
|
|
1063
|
+
sleepingByDevice.set(target.deviceId, sleeping);
|
|
1064
|
+
if (!sleeping) await this.awaitFreshEnough(target.deviceId);
|
|
1065
|
+
});
|
|
1066
|
+
await raceForResult(Promise.allSettled(refreshes), LINK_BATCH_WAIT_MS);
|
|
1067
|
+
const rows = input.targets.map((target) => {
|
|
1048
1068
|
const { deviceId } = target;
|
|
1049
1069
|
const width = target.width === void 0 ? void 0 : snapSnapshotWidth(target.width);
|
|
1050
|
-
this.subscriptions.note(deviceId, void 0, width, now);
|
|
1051
|
-
const sleeping = await this.isSleepingBatteryDevice(deviceId);
|
|
1052
|
-
if (!sleeping) await this.awaitFreshEnough(deviceId);
|
|
1053
1070
|
const capturedAt = this.cache.latest(deviceId)?.ts ?? null;
|
|
1054
1071
|
const answeredAt = Date.now();
|
|
1055
1072
|
return {
|
|
@@ -1067,9 +1084,9 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1067
1084
|
expiresAt: served ? expMs : null,
|
|
1068
1085
|
width: width ?? null,
|
|
1069
1086
|
neverCaptured: capturedAt === null,
|
|
1070
|
-
sleeping
|
|
1087
|
+
sleeping: sleepingByDevice.get(deviceId) ?? false
|
|
1071
1088
|
};
|
|
1072
|
-
})
|
|
1089
|
+
});
|
|
1073
1090
|
this.ctx.logger.debug("snapshot: minted links", { meta: {
|
|
1074
1091
|
targets: input.targets.length,
|
|
1075
1092
|
served,
|
|
@@ -1080,19 +1097,25 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1080
1097
|
/**
|
|
1081
1098
|
* Join the ordinary capture path for one device and wait, bounded, for it.
|
|
1082
1099
|
*
|
|
1083
|
-
* Deliberately `getSnapshot` with `force: false
|
|
1084
|
-
*
|
|
1085
|
-
* the
|
|
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.
|
|
1100
|
+
* Deliberately `getSnapshot` with `force: false`: the single flight, grab
|
|
1101
|
+
* pool, and sleeping-battery refusal apply unchanged. The only override is
|
|
1102
|
+
* the Viewer's 15-second currency contract and the link-mint wait bound.
|
|
1088
1103
|
*/
|
|
1089
1104
|
async awaitFreshEnough(deviceId) {
|
|
1090
|
-
|
|
1105
|
+
const before = this.cache.latest(deviceId)?.ts ?? null;
|
|
1106
|
+
const alreadyCurrent = before !== null && Date.now() - before <= LINK_CURRENT_MAX_AGE_MS;
|
|
1107
|
+
await this.getSnapshot({
|
|
1091
1108
|
deviceId,
|
|
1092
1109
|
force: false
|
|
1093
|
-
}).catch(() => null)
|
|
1110
|
+
}, 0, LINK_MINT_DEADLINE_MS, LINK_CURRENT_MAX_AGE_MS).catch(() => null);
|
|
1111
|
+
const current = this.cache.latest(deviceId)?.ts ?? null;
|
|
1112
|
+
if (alreadyCurrent || current !== null && (before === null || current > before)) return;
|
|
1113
|
+
this.ctx.logger.debug("snapshot: link mint gave up waiting; serving the older frame", {
|
|
1094
1114
|
tags: { deviceId },
|
|
1095
|
-
meta: {
|
|
1115
|
+
meta: {
|
|
1116
|
+
deadlineMs: LINK_MINT_DEADLINE_MS,
|
|
1117
|
+
previousCapturedAt: before
|
|
1118
|
+
}
|
|
1096
1119
|
});
|
|
1097
1120
|
}
|
|
1098
1121
|
/** True only for a BATTERY device that is currently asleep. Kept as one
|
|
@@ -1341,7 +1364,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1341
1364
|
})]
|
|
1342
1365
|
}] });
|
|
1343
1366
|
}
|
|
1344
|
-
async getSnapshot(input, warmAheadMs = 0) {
|
|
1367
|
+
async getSnapshot(input, warmAheadMs = 0, minimumCaptureWaitMs = 0, maximumCacheAgeMs = Number.POSITIVE_INFINITY) {
|
|
1345
1368
|
const { deviceId } = input;
|
|
1346
1369
|
const force = input.force === true;
|
|
1347
1370
|
const meta = await this.lookupDeviceMeta(deviceId);
|
|
@@ -1360,7 +1383,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1360
1383
|
meta: { stream: effectiveStreamId ?? "auto" }
|
|
1361
1384
|
});
|
|
1362
1385
|
const hit = this.cache.get(deviceId, effectiveStreamId);
|
|
1363
|
-
const effectiveMaxAgeMs = Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3 - warmAheadMs);
|
|
1386
|
+
const effectiveMaxAgeMs = Math.min(maximumCacheAgeMs, Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3 - warmAheadMs));
|
|
1364
1387
|
const decision = decideSnapshotServe({
|
|
1365
1388
|
now,
|
|
1366
1389
|
cachedAt: hit?.ts ?? null,
|
|
@@ -1397,7 +1420,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1397
1420
|
log
|
|
1398
1421
|
}));
|
|
1399
1422
|
flight.catch(() => void 0);
|
|
1400
|
-
const raced = await raceForResult(flight, decision.waitMs);
|
|
1423
|
+
const raced = await raceForResult(flight, Math.max(decision.waitMs, minimumCaptureWaitMs));
|
|
1401
1424
|
if (raced.settled) return this.resolveOutcome(raced.value, deviceId, hit, log);
|
|
1402
1425
|
if (decision.staleFallback && hit) {
|
|
1403
1426
|
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.
|
|
150
|
-
*
|
|
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
|
|
164
|
-
*
|
|
165
|
-
* the
|
|
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
|