@camstack/addon-pipeline 1.2.84 → 1.2.85
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/dist/recorder/index.js +94 -34
- package/dist/recorder/index.mjs +94 -34
- package/package.json +1 -1
package/dist/recorder/index.js
CHANGED
|
@@ -7684,6 +7684,22 @@ var EXPORT_SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
|
7684
7684
|
* hourly cadence keeps the storage.list/evict cost negligible while honouring
|
|
7685
7685
|
* the recording-spec §6 periodic retention. */
|
|
7686
7686
|
var RETENTION_SWEEP_INTERVAL_MS = 60 * 6e4;
|
|
7687
|
+
/**
|
|
7688
|
+
* How long after boot the FULL-archive index walk starts.
|
|
7689
|
+
*
|
|
7690
|
+
* The walk is a recursive `readdir` per device over the whole recordings
|
|
7691
|
+
* subtree — minutes of network-FS latency on a real archive (measured: 240 s
|
|
7692
|
+
* for ONE 43 017-entry device, 2026-08-15) and it is the read model for
|
|
7693
|
+
* playback/retention/footprint, NOT the write path. Every read self-hydrates
|
|
7694
|
+
* the window it is asked about, and the whole-archive consumers
|
|
7695
|
+
* (retention sweep, redundancy janitor, disk-pressure eviction) only ever
|
|
7696
|
+
* under-report while the index is partial — so the walk waits out the boot
|
|
7697
|
+
* storm (writers attaching, staging reconcile relocating, first viewer paint)
|
|
7698
|
+
* instead of competing with it. Five minutes because the storm is measured in
|
|
7699
|
+
* seconds-to-a-minute and the consumers re-run on their own cadences anyway
|
|
7700
|
+
* (retention hourly, eviction on pressure).
|
|
7701
|
+
*/
|
|
7702
|
+
var FULL_WALK_DEFER_MS = 5 * 6e4;
|
|
7687
7703
|
/** Grace after a completed download before a `deleteAfterDownload` file is removed. */
|
|
7688
7704
|
var EXPORT_DELETE_GRACE_MS = 6e4;
|
|
7689
7705
|
/** Only `.mp4` export ids matching this shape are servable (traversal guard). */
|
|
@@ -7749,6 +7765,11 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
7749
7765
|
exportEngine = null;
|
|
7750
7766
|
/** Periodic export-expiry sweep timer. Cleared on shutdown. */
|
|
7751
7767
|
exportSweepTimer = null;
|
|
7768
|
+
/** Timer arming the deferred full-archive index walk. Null once fired. */
|
|
7769
|
+
fullWalkTimer = null;
|
|
7770
|
+
/** Single-flight guard for the full-archive walk (onHubReachable can re-enter
|
|
7771
|
+
* on a hub reconnect while a deferred walk is still running). */
|
|
7772
|
+
fullWalkInFlight = false;
|
|
7752
7773
|
/** The `recording` cap provider — retained so the periodic retention sweep can
|
|
7753
7774
|
* call its `pruneFootage`. Null until built in `onInitialize`. */
|
|
7754
7775
|
recordingProvider = null;
|
|
@@ -8121,6 +8142,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
8121
8142
|
}
|
|
8122
8143
|
this.retentionSweeper?.stop();
|
|
8123
8144
|
this.retentionSweeper = null;
|
|
8145
|
+
if (this.fullWalkTimer !== null) {
|
|
8146
|
+
clearTimeout(this.fullWalkTimer);
|
|
8147
|
+
this.fullWalkTimer = null;
|
|
8148
|
+
}
|
|
8124
8149
|
this.exportEngine = null;
|
|
8125
8150
|
this.recordingProvider = null;
|
|
8126
8151
|
}
|
|
@@ -8190,41 +8215,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
8190
8215
|
return;
|
|
8191
8216
|
}
|
|
8192
8217
|
this.warmToday([...configs.keys()]);
|
|
8193
|
-
|
|
8194
|
-
await hydrateDevicesFromStorage(this.ctx.api, this.index, [...configs.keys()], this.resolvedLocations, this.ctx.logger);
|
|
8195
|
-
this.ctx.logger.info("recorder: hydrated index from storage", { meta: {
|
|
8196
|
-
deviceCount: configs.size,
|
|
8197
|
-
locationCount: this.resolvedLocations.length,
|
|
8198
|
-
ms: Date.now() - hydrateStartedMs
|
|
8199
|
-
} });
|
|
8200
|
-
const loggedDomains = /* @__PURE__ */ new Set();
|
|
8201
|
-
for (const loc of this.resolvedLocations) {
|
|
8202
|
-
const domain = this.locationDomain(loc.id);
|
|
8203
|
-
const domainKey = [...domain].toSorted().join("+");
|
|
8204
|
-
if (loggedDomains.has(domainKey)) continue;
|
|
8205
|
-
loggedDomains.add(domainKey);
|
|
8206
|
-
const acct = this.index.accountingForLocations(domain);
|
|
8207
|
-
this.ctx.logger.info("recorder: eviction domain footage span at hydrate", { meta: {
|
|
8208
|
-
locationIds: [...domain].toSorted(),
|
|
8209
|
-
root: loc.root,
|
|
8210
|
-
count: acct.count,
|
|
8211
|
-
bytes: acct.bytes,
|
|
8212
|
-
oldestMs: acct.oldestMs,
|
|
8213
|
-
newestMs: acct.newestMs
|
|
8214
|
-
} });
|
|
8215
|
-
}
|
|
8218
|
+
this.scheduleDeferredFullWalk([...configs.keys()]);
|
|
8216
8219
|
this.cleanupLegacyThumbsDirs();
|
|
8217
|
-
const store = this.segmentStore;
|
|
8218
|
-
if (store) try {
|
|
8219
|
-
await runRedundancyJanitor({
|
|
8220
|
-
deviceIds: [...configs.keys()],
|
|
8221
|
-
segmentsFor: (deviceId, profile) => this.index.segments(deviceId, profile),
|
|
8222
|
-
evict: (location, rows) => store.evict(location, rows),
|
|
8223
|
-
logger: this.ctx.logger
|
|
8224
|
-
});
|
|
8225
|
-
} catch (err) {
|
|
8226
|
-
this.ctx.logger.warn("recorder: redundancy janitor failed", { meta: { error: require_dist.errMsg(err) } });
|
|
8227
|
-
}
|
|
8228
8220
|
await this.bootstrapExports();
|
|
8229
8221
|
const provider = this.recordingProvider;
|
|
8230
8222
|
if (provider && this.retentionSweeper === null) this.retentionSweeper = new RetentionSweeper({
|
|
@@ -8667,6 +8659,74 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
8667
8659
|
ms: Date.now() - started
|
|
8668
8660
|
} });
|
|
8669
8661
|
}
|
|
8662
|
+
/**
|
|
8663
|
+
* Arm the deferred full-archive index walk (see {@link FULL_WALK_DEFER_MS}).
|
|
8664
|
+
* Idempotent across `onHubReachable` re-entries: an armed timer is not
|
|
8665
|
+
* re-armed, and a walk already running is not duplicated.
|
|
8666
|
+
*/
|
|
8667
|
+
scheduleDeferredFullWalk(deviceIds) {
|
|
8668
|
+
if (this.fullWalkTimer !== null || this.fullWalkInFlight) return;
|
|
8669
|
+
this.fullWalkTimer = setTimeout(() => {
|
|
8670
|
+
this.fullWalkTimer = null;
|
|
8671
|
+
this.runFullArchiveWalk(deviceIds);
|
|
8672
|
+
}, FULL_WALK_DEFER_MS);
|
|
8673
|
+
this.fullWalkTimer.unref?.();
|
|
8674
|
+
this.ctx.logger.info("recorder: full-archive index walk deferred", { meta: {
|
|
8675
|
+
deferMs: FULL_WALK_DEFER_MS,
|
|
8676
|
+
deviceCount: deviceIds.length
|
|
8677
|
+
} });
|
|
8678
|
+
}
|
|
8679
|
+
/**
|
|
8680
|
+
* The deferred boot walk itself: hydrate every configured device from the
|
|
8681
|
+
* recordings roots, then run the two consumers that need the COMPLETE index
|
|
8682
|
+
* (the eviction-domain span log and the redundancy janitor). Bounded-parallel
|
|
8683
|
+
* per device, and each camera is marked hydrated at ITS OWN completion — not
|
|
8684
|
+
* after the fleet (D167). Never overlaps itself.
|
|
8685
|
+
*/
|
|
8686
|
+
async runFullArchiveWalk(deviceIds) {
|
|
8687
|
+
if (this.fullWalkInFlight) return;
|
|
8688
|
+
this.fullWalkInFlight = true;
|
|
8689
|
+
const hydrateStartedMs = Date.now();
|
|
8690
|
+
try {
|
|
8691
|
+
await hydrateDevicesFromStorage(this.ctx.api, this.index, deviceIds, this.resolvedLocations, this.ctx.logger);
|
|
8692
|
+
this.ctx.logger.info("recorder: hydrated index from storage", { meta: {
|
|
8693
|
+
deviceCount: deviceIds.length,
|
|
8694
|
+
locationCount: this.resolvedLocations.length,
|
|
8695
|
+
ms: Date.now() - hydrateStartedMs
|
|
8696
|
+
} });
|
|
8697
|
+
const loggedDomains = /* @__PURE__ */ new Set();
|
|
8698
|
+
for (const loc of this.resolvedLocations) {
|
|
8699
|
+
const domain = this.locationDomain(loc.id);
|
|
8700
|
+
const domainKey = [...domain].toSorted().join("+");
|
|
8701
|
+
if (loggedDomains.has(domainKey)) continue;
|
|
8702
|
+
loggedDomains.add(domainKey);
|
|
8703
|
+
const acct = this.index.accountingForLocations(domain);
|
|
8704
|
+
this.ctx.logger.info("recorder: eviction domain footage span at hydrate", { meta: {
|
|
8705
|
+
locationIds: [...domain].toSorted(),
|
|
8706
|
+
root: loc.root,
|
|
8707
|
+
count: acct.count,
|
|
8708
|
+
bytes: acct.bytes,
|
|
8709
|
+
oldestMs: acct.oldestMs,
|
|
8710
|
+
newestMs: acct.newestMs
|
|
8711
|
+
} });
|
|
8712
|
+
}
|
|
8713
|
+
const store = this.segmentStore;
|
|
8714
|
+
if (store) try {
|
|
8715
|
+
await runRedundancyJanitor({
|
|
8716
|
+
deviceIds,
|
|
8717
|
+
segmentsFor: (deviceId, profile) => this.index.segments(deviceId, profile),
|
|
8718
|
+
evict: (location, rows) => store.evict(location, rows),
|
|
8719
|
+
logger: this.ctx.logger
|
|
8720
|
+
});
|
|
8721
|
+
} catch (err) {
|
|
8722
|
+
this.ctx.logger.warn("recorder: redundancy janitor failed", { meta: { error: require_dist.errMsg(err) } });
|
|
8723
|
+
}
|
|
8724
|
+
} catch (err) {
|
|
8725
|
+
this.ctx.logger.warn("recorder: full-archive walk failed", { meta: { error: require_dist.errMsg(err) } });
|
|
8726
|
+
} finally {
|
|
8727
|
+
this.fullWalkInFlight = false;
|
|
8728
|
+
}
|
|
8729
|
+
}
|
|
8670
8730
|
configStore() {
|
|
8671
8731
|
return this.state(RECORDING_CONFIGS_KEY, RecordingConfigsBlobSchema, {});
|
|
8672
8732
|
}
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -7683,6 +7683,22 @@ var EXPORT_SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
|
7683
7683
|
* hourly cadence keeps the storage.list/evict cost negligible while honouring
|
|
7684
7684
|
* the recording-spec §6 periodic retention. */
|
|
7685
7685
|
var RETENTION_SWEEP_INTERVAL_MS = 60 * 6e4;
|
|
7686
|
+
/**
|
|
7687
|
+
* How long after boot the FULL-archive index walk starts.
|
|
7688
|
+
*
|
|
7689
|
+
* The walk is a recursive `readdir` per device over the whole recordings
|
|
7690
|
+
* subtree — minutes of network-FS latency on a real archive (measured: 240 s
|
|
7691
|
+
* for ONE 43 017-entry device, 2026-08-15) and it is the read model for
|
|
7692
|
+
* playback/retention/footprint, NOT the write path. Every read self-hydrates
|
|
7693
|
+
* the window it is asked about, and the whole-archive consumers
|
|
7694
|
+
* (retention sweep, redundancy janitor, disk-pressure eviction) only ever
|
|
7695
|
+
* under-report while the index is partial — so the walk waits out the boot
|
|
7696
|
+
* storm (writers attaching, staging reconcile relocating, first viewer paint)
|
|
7697
|
+
* instead of competing with it. Five minutes because the storm is measured in
|
|
7698
|
+
* seconds-to-a-minute and the consumers re-run on their own cadences anyway
|
|
7699
|
+
* (retention hourly, eviction on pressure).
|
|
7700
|
+
*/
|
|
7701
|
+
var FULL_WALK_DEFER_MS = 5 * 6e4;
|
|
7686
7702
|
/** Grace after a completed download before a `deleteAfterDownload` file is removed. */
|
|
7687
7703
|
var EXPORT_DELETE_GRACE_MS = 6e4;
|
|
7688
7704
|
/** Only `.mp4` export ids matching this shape are servable (traversal guard). */
|
|
@@ -7748,6 +7764,11 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
7748
7764
|
exportEngine = null;
|
|
7749
7765
|
/** Periodic export-expiry sweep timer. Cleared on shutdown. */
|
|
7750
7766
|
exportSweepTimer = null;
|
|
7767
|
+
/** Timer arming the deferred full-archive index walk. Null once fired. */
|
|
7768
|
+
fullWalkTimer = null;
|
|
7769
|
+
/** Single-flight guard for the full-archive walk (onHubReachable can re-enter
|
|
7770
|
+
* on a hub reconnect while a deferred walk is still running). */
|
|
7771
|
+
fullWalkInFlight = false;
|
|
7751
7772
|
/** The `recording` cap provider — retained so the periodic retention sweep can
|
|
7752
7773
|
* call its `pruneFootage`. Null until built in `onInitialize`. */
|
|
7753
7774
|
recordingProvider = null;
|
|
@@ -8120,6 +8141,10 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
8120
8141
|
}
|
|
8121
8142
|
this.retentionSweeper?.stop();
|
|
8122
8143
|
this.retentionSweeper = null;
|
|
8144
|
+
if (this.fullWalkTimer !== null) {
|
|
8145
|
+
clearTimeout(this.fullWalkTimer);
|
|
8146
|
+
this.fullWalkTimer = null;
|
|
8147
|
+
}
|
|
8123
8148
|
this.exportEngine = null;
|
|
8124
8149
|
this.recordingProvider = null;
|
|
8125
8150
|
}
|
|
@@ -8189,41 +8214,8 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
8189
8214
|
return;
|
|
8190
8215
|
}
|
|
8191
8216
|
this.warmToday([...configs.keys()]);
|
|
8192
|
-
|
|
8193
|
-
await hydrateDevicesFromStorage(this.ctx.api, this.index, [...configs.keys()], this.resolvedLocations, this.ctx.logger);
|
|
8194
|
-
this.ctx.logger.info("recorder: hydrated index from storage", { meta: {
|
|
8195
|
-
deviceCount: configs.size,
|
|
8196
|
-
locationCount: this.resolvedLocations.length,
|
|
8197
|
-
ms: Date.now() - hydrateStartedMs
|
|
8198
|
-
} });
|
|
8199
|
-
const loggedDomains = /* @__PURE__ */ new Set();
|
|
8200
|
-
for (const loc of this.resolvedLocations) {
|
|
8201
|
-
const domain = this.locationDomain(loc.id);
|
|
8202
|
-
const domainKey = [...domain].toSorted().join("+");
|
|
8203
|
-
if (loggedDomains.has(domainKey)) continue;
|
|
8204
|
-
loggedDomains.add(domainKey);
|
|
8205
|
-
const acct = this.index.accountingForLocations(domain);
|
|
8206
|
-
this.ctx.logger.info("recorder: eviction domain footage span at hydrate", { meta: {
|
|
8207
|
-
locationIds: [...domain].toSorted(),
|
|
8208
|
-
root: loc.root,
|
|
8209
|
-
count: acct.count,
|
|
8210
|
-
bytes: acct.bytes,
|
|
8211
|
-
oldestMs: acct.oldestMs,
|
|
8212
|
-
newestMs: acct.newestMs
|
|
8213
|
-
} });
|
|
8214
|
-
}
|
|
8217
|
+
this.scheduleDeferredFullWalk([...configs.keys()]);
|
|
8215
8218
|
this.cleanupLegacyThumbsDirs();
|
|
8216
|
-
const store = this.segmentStore;
|
|
8217
|
-
if (store) try {
|
|
8218
|
-
await runRedundancyJanitor({
|
|
8219
|
-
deviceIds: [...configs.keys()],
|
|
8220
|
-
segmentsFor: (deviceId, profile) => this.index.segments(deviceId, profile),
|
|
8221
|
-
evict: (location, rows) => store.evict(location, rows),
|
|
8222
|
-
logger: this.ctx.logger
|
|
8223
|
-
});
|
|
8224
|
-
} catch (err) {
|
|
8225
|
-
this.ctx.logger.warn("recorder: redundancy janitor failed", { meta: { error: errMsg(err) } });
|
|
8226
|
-
}
|
|
8227
8219
|
await this.bootstrapExports();
|
|
8228
8220
|
const provider = this.recordingProvider;
|
|
8229
8221
|
if (provider && this.retentionSweeper === null) this.retentionSweeper = new RetentionSweeper({
|
|
@@ -8666,6 +8658,74 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
8666
8658
|
ms: Date.now() - started
|
|
8667
8659
|
} });
|
|
8668
8660
|
}
|
|
8661
|
+
/**
|
|
8662
|
+
* Arm the deferred full-archive index walk (see {@link FULL_WALK_DEFER_MS}).
|
|
8663
|
+
* Idempotent across `onHubReachable` re-entries: an armed timer is not
|
|
8664
|
+
* re-armed, and a walk already running is not duplicated.
|
|
8665
|
+
*/
|
|
8666
|
+
scheduleDeferredFullWalk(deviceIds) {
|
|
8667
|
+
if (this.fullWalkTimer !== null || this.fullWalkInFlight) return;
|
|
8668
|
+
this.fullWalkTimer = setTimeout(() => {
|
|
8669
|
+
this.fullWalkTimer = null;
|
|
8670
|
+
this.runFullArchiveWalk(deviceIds);
|
|
8671
|
+
}, FULL_WALK_DEFER_MS);
|
|
8672
|
+
this.fullWalkTimer.unref?.();
|
|
8673
|
+
this.ctx.logger.info("recorder: full-archive index walk deferred", { meta: {
|
|
8674
|
+
deferMs: FULL_WALK_DEFER_MS,
|
|
8675
|
+
deviceCount: deviceIds.length
|
|
8676
|
+
} });
|
|
8677
|
+
}
|
|
8678
|
+
/**
|
|
8679
|
+
* The deferred boot walk itself: hydrate every configured device from the
|
|
8680
|
+
* recordings roots, then run the two consumers that need the COMPLETE index
|
|
8681
|
+
* (the eviction-domain span log and the redundancy janitor). Bounded-parallel
|
|
8682
|
+
* per device, and each camera is marked hydrated at ITS OWN completion — not
|
|
8683
|
+
* after the fleet (D167). Never overlaps itself.
|
|
8684
|
+
*/
|
|
8685
|
+
async runFullArchiveWalk(deviceIds) {
|
|
8686
|
+
if (this.fullWalkInFlight) return;
|
|
8687
|
+
this.fullWalkInFlight = true;
|
|
8688
|
+
const hydrateStartedMs = Date.now();
|
|
8689
|
+
try {
|
|
8690
|
+
await hydrateDevicesFromStorage(this.ctx.api, this.index, deviceIds, this.resolvedLocations, this.ctx.logger);
|
|
8691
|
+
this.ctx.logger.info("recorder: hydrated index from storage", { meta: {
|
|
8692
|
+
deviceCount: deviceIds.length,
|
|
8693
|
+
locationCount: this.resolvedLocations.length,
|
|
8694
|
+
ms: Date.now() - hydrateStartedMs
|
|
8695
|
+
} });
|
|
8696
|
+
const loggedDomains = /* @__PURE__ */ new Set();
|
|
8697
|
+
for (const loc of this.resolvedLocations) {
|
|
8698
|
+
const domain = this.locationDomain(loc.id);
|
|
8699
|
+
const domainKey = [...domain].toSorted().join("+");
|
|
8700
|
+
if (loggedDomains.has(domainKey)) continue;
|
|
8701
|
+
loggedDomains.add(domainKey);
|
|
8702
|
+
const acct = this.index.accountingForLocations(domain);
|
|
8703
|
+
this.ctx.logger.info("recorder: eviction domain footage span at hydrate", { meta: {
|
|
8704
|
+
locationIds: [...domain].toSorted(),
|
|
8705
|
+
root: loc.root,
|
|
8706
|
+
count: acct.count,
|
|
8707
|
+
bytes: acct.bytes,
|
|
8708
|
+
oldestMs: acct.oldestMs,
|
|
8709
|
+
newestMs: acct.newestMs
|
|
8710
|
+
} });
|
|
8711
|
+
}
|
|
8712
|
+
const store = this.segmentStore;
|
|
8713
|
+
if (store) try {
|
|
8714
|
+
await runRedundancyJanitor({
|
|
8715
|
+
deviceIds,
|
|
8716
|
+
segmentsFor: (deviceId, profile) => this.index.segments(deviceId, profile),
|
|
8717
|
+
evict: (location, rows) => store.evict(location, rows),
|
|
8718
|
+
logger: this.ctx.logger
|
|
8719
|
+
});
|
|
8720
|
+
} catch (err) {
|
|
8721
|
+
this.ctx.logger.warn("recorder: redundancy janitor failed", { meta: { error: errMsg(err) } });
|
|
8722
|
+
}
|
|
8723
|
+
} catch (err) {
|
|
8724
|
+
this.ctx.logger.warn("recorder: full-archive walk failed", { meta: { error: errMsg(err) } });
|
|
8725
|
+
} finally {
|
|
8726
|
+
this.fullWalkInFlight = false;
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8669
8729
|
configStore() {
|
|
8670
8730
|
return this.state(RECORDING_CONFIGS_KEY, RecordingConfigsBlobSchema, {});
|
|
8671
8731
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.85",
|
|
4
4
|
"description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|