@camstack/system 1.2.72 → 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.
|
|
1037
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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: {
|
|
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.
|
|
1032
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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: {
|
|
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.
|
|
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
|
package/dist/index.js
CHANGED
|
@@ -3330,6 +3330,7 @@ var AddonLoader = class {
|
|
|
3330
3330
|
};
|
|
3331
3331
|
/** Check if a Dirent is a directory (or a symlink pointing to a directory) */
|
|
3332
3332
|
function isDirectoryEntry(entry, parentDir) {
|
|
3333
|
+
if (entry.name.startsWith(".")) return false;
|
|
3333
3334
|
if (entry.isDirectory()) return true;
|
|
3334
3335
|
if (entry.isSymbolicLink()) try {
|
|
3335
3336
|
return node_fs.statSync(node_path.join(parentDir, entry.name)).isDirectory();
|
package/dist/index.mjs
CHANGED
|
@@ -3322,6 +3322,7 @@ var AddonLoader = class {
|
|
|
3322
3322
|
};
|
|
3323
3323
|
/** Check if a Dirent is a directory (or a symlink pointing to a directory) */
|
|
3324
3324
|
function isDirectoryEntry(entry, parentDir) {
|
|
3325
|
+
if (entry.name.startsWith(".")) return false;
|
|
3325
3326
|
if (entry.isDirectory()) return true;
|
|
3326
3327
|
if (entry.isSymbolicLink()) try {
|
|
3327
3328
|
return fs$17.statSync(path$39.join(parentDir, entry.name)).isDirectory();
|