@camstack/addon-pipeline 1.2.254 → 1.2.256
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/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-BegEf29d.mjs → default-detection-model-A5DRjEr3.mjs} +1 -1
- package/dist/{default-detection-model-DQj3yiG8.js → default-detection-model-Bbw4nC11.js} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-CkGvx3Fy.mjs → dist-MJ4ji7um.mjs} +284 -25
- package/dist/{dist-DQsOgAJp.js → dist-lqqUXDfs.js} +284 -25
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-Bg7LGOQ3.js → node-80-uOMr8.js} +1 -1
- package/dist/{node-Dwlty0jj.mjs → node-DdXmHLK4.mjs} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-CzjYHqAz.mjs → process-memory-Cl3ubMxY.mjs} +1 -1
- package/dist/{process-memory-COrQw_pf.js → process-memory-Dac9TXlX.js} +1 -1
- package/dist/recorder/index.js +472 -51
- package/dist/recorder/index.mjs +472 -51
- package/dist/{segment-demux-js-BIrnzZiP.js → segment-demux-js-CWwlKQ3M.js} +1 -1
- package/dist/{segment-demux-js-BAoEDsxT.mjs → segment-demux-js-gIj82O9k.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-rumvXmC-.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-pOLAb3B1.mjs} +3 -3
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-k114uFcJ.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CPFjd94C.mjs} +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BXK7jbJm.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-CFFft0gC.mjs → hostInit-BRnsXyTM.mjs} +3 -3
- package/dist/stream-broker/index.js +3 -3
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BHtcf-Mo.mjs +0 -26
package/dist/recorder/index.js
CHANGED
|
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_node = require("../node-
|
|
6
|
+
const require_dist = require("../dist-lqqUXDfs.js");
|
|
7
|
+
const require_node = require("../node-80-uOMr8.js");
|
|
8
8
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
9
9
|
const require_restream_intent = require("../restream-intent-VVJiUAkh.js");
|
|
10
10
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
@@ -820,14 +820,14 @@ function parseSegmentPath(relPath) {
|
|
|
820
820
|
//#region src/recorder/recording-index.ts
|
|
821
821
|
/** Hour bucket, the granularity the path convention (`.../YYYY/MM/DD/HH/`)
|
|
822
822
|
* already gives us — so "have I walked this?" costs one Set lookup. */
|
|
823
|
-
var HOUR_MS$
|
|
823
|
+
var HOUR_MS$9 = 36e5;
|
|
824
824
|
/** A device walked end-to-end. Cheaper and clearer than inserting every hour of
|
|
825
825
|
* the retention window into the set. */
|
|
826
826
|
var ALL_HOURS = Symbol("all-hours");
|
|
827
827
|
/** The hour buckets a half-open span touches. */
|
|
828
828
|
function* hourBuckets(fromMs, toMs) {
|
|
829
|
-
const first = Math.floor(fromMs / HOUR_MS$
|
|
830
|
-
for (let h = first; h < toMs; h += HOUR_MS$
|
|
829
|
+
const first = Math.floor(fromMs / HOUR_MS$9) * HOUR_MS$9;
|
|
830
|
+
for (let h = first; h < toMs; h += HOUR_MS$9) yield h;
|
|
831
831
|
}
|
|
832
832
|
/**
|
|
833
833
|
* Index of the first element whose `startMs` is GREATER than `startMs` (upper
|
|
@@ -1121,7 +1121,7 @@ var RecordingIndex = class {
|
|
|
1121
1121
|
changed = true;
|
|
1122
1122
|
}
|
|
1123
1123
|
if (changed) this.invalidateSorted(deviceId);
|
|
1124
|
-
this.markHydrated(deviceId, hourStartMs, hourStartMs + HOUR_MS$
|
|
1124
|
+
this.markHydrated(deviceId, hourStartMs, hourStartMs + HOUR_MS$9);
|
|
1125
1125
|
}
|
|
1126
1126
|
/**
|
|
1127
1127
|
* Insert/replace one finalized segment (idempotent by path).
|
|
@@ -1767,11 +1767,11 @@ function hourDirRelPath(deviceId, profile, hourStartMs) {
|
|
|
1767
1767
|
const p2 = (n) => String(n).padStart(2, "0");
|
|
1768
1768
|
return `${deviceId}/${profile}/${d.getUTCFullYear()}/${p2(d.getUTCMonth() + 1)}/${p2(d.getUTCDate())}/${p2(d.getUTCHours())}`;
|
|
1769
1769
|
}
|
|
1770
|
-
var HOUR_MS$
|
|
1770
|
+
var HOUR_MS$8 = 36e5;
|
|
1771
1771
|
async function hydrateWindowFromStorage(index, deviceId, fromMs, toMs, locations, logger, ledger, options) {
|
|
1772
1772
|
const verify = options?.verifyClosedHours === true;
|
|
1773
1773
|
if (!verify && index.hydrationOf(deviceId, fromMs, toMs) === "hydrated") return;
|
|
1774
|
-
const liveHour = Math.floor(Date.now() / HOUR_MS$
|
|
1774
|
+
const liveHour = Math.floor(Date.now() / HOUR_MS$8) * HOUR_MS$8;
|
|
1775
1775
|
/** Per (locationId, profile, hour): what the LEDGER claimed, for the diff. */
|
|
1776
1776
|
const claimed = /* @__PURE__ */ new Map();
|
|
1777
1777
|
const claimKey = (locationId, hour) => `${locationId}:${hour}`;
|
|
@@ -1821,15 +1821,15 @@ async function hydrateWindowFromStorage(index, deviceId, fromMs, toMs, locations
|
|
|
1821
1821
|
});
|
|
1822
1822
|
continue;
|
|
1823
1823
|
}
|
|
1824
|
-
const first = Math.floor(fromMs / HOUR_MS$
|
|
1824
|
+
const first = Math.floor(fromMs / HOUR_MS$8) * HOUR_MS$8;
|
|
1825
1825
|
const reuseBefore = index.hydrateReuseStats();
|
|
1826
1826
|
let hoursRead = 0;
|
|
1827
1827
|
let repaired = 0;
|
|
1828
1828
|
let forgottenSegments = 0;
|
|
1829
1829
|
let forgottenHours = 0;
|
|
1830
1830
|
let sealed = 0;
|
|
1831
|
-
for (let hour = first; hour < toMs; hour += HOUR_MS$
|
|
1832
|
-
if (!verify && hour < liveHour && !unsealed.has(hour) && index.hydrationOf(deviceId, hour, hour + HOUR_MS$
|
|
1831
|
+
for (let hour = first; hour < toMs; hour += HOUR_MS$8) {
|
|
1832
|
+
if (!verify && hour < liveHour && !unsealed.has(hour) && index.hydrationOf(deviceId, hour, hour + HOUR_MS$8) === "hydrated") continue;
|
|
1833
1833
|
for (const profile of profiles) {
|
|
1834
1834
|
const rel = hourDirRelPath(deviceId, profile, hour);
|
|
1835
1835
|
let files;
|
|
@@ -1842,7 +1842,7 @@ async function hydrateWindowFromStorage(index, deviceId, fromMs, toMs, locations
|
|
|
1842
1842
|
const locationId = locationForHydratedProfile(aliases, profile).id;
|
|
1843
1843
|
const relPaths = files.filter((f) => f.endsWith(".m4s")).map((f) => `${rel}/${f}`);
|
|
1844
1844
|
index.hydrateHour(deviceId, locationId, hour, relPaths);
|
|
1845
|
-
if (ledger === void 0 || relPaths.length === 0 || hour + HOUR_MS$
|
|
1845
|
+
if (ledger === void 0 || relPaths.length === 0 || hour + HOUR_MS$8 > Date.now()) continue;
|
|
1846
1846
|
const claimedHere = claimed.get(claimKey(locationId, hour));
|
|
1847
1847
|
const outcome = await ledger.adoptWalkedHour(deviceId, profile, locationId, hour, relPaths);
|
|
1848
1848
|
if (outcome === null) continue;
|
|
@@ -1925,7 +1925,7 @@ async function hydrateWindowFromStorage(index, deviceId, fromMs, toMs, locations
|
|
|
1925
1925
|
*/
|
|
1926
1926
|
var DEFAULT_THROTTLE_MBPS = 40;
|
|
1927
1927
|
/** The durable index's bucket, and therefore this mover's batch. */
|
|
1928
|
-
var HOUR_MS$
|
|
1928
|
+
var HOUR_MS$7 = 36e5;
|
|
1929
1929
|
/** Progress callback cadence (files) — cheap enough to fire often. */
|
|
1930
1930
|
var PROGRESS_EVERY_FILES = 25;
|
|
1931
1931
|
/** Free space a destination must keep when it declares no `config.minFreePercent`
|
|
@@ -2622,7 +2622,7 @@ function profilesOfClass(footageClass) {
|
|
|
2622
2622
|
function groupByHour(rows) {
|
|
2623
2623
|
const out = /* @__PURE__ */ new Map();
|
|
2624
2624
|
for (const row of rows) {
|
|
2625
|
-
const key = `${row.deviceId}|${row.profile}|${Math.floor(row.startMs / HOUR_MS$
|
|
2625
|
+
const key = `${row.deviceId}|${row.profile}|${Math.floor(row.startMs / HOUR_MS$7)}`;
|
|
2626
2626
|
const found = out.get(key);
|
|
2627
2627
|
if (found) found.push(row);
|
|
2628
2628
|
else out.set(key, [row]);
|
|
@@ -3850,7 +3850,7 @@ async function profileHasFootage(root, deviceId, profile) {
|
|
|
3850
3850
|
//#endregion
|
|
3851
3851
|
//#region src/recorder/addon/calendar-index.ts
|
|
3852
3852
|
var DAY_MS$3 = 864e5;
|
|
3853
|
-
var HOUR_MS$
|
|
3853
|
+
var HOUR_MS$6 = 36e5;
|
|
3854
3854
|
/**
|
|
3855
3855
|
* UTC ms → the UTC epoch of LOCAL midnight of the day it falls in.
|
|
3856
3856
|
*
|
|
@@ -3888,8 +3888,8 @@ var RANGE_MERGE_GAP_MS$1 = 5e3;
|
|
|
3888
3888
|
* deploy gap). Unknown hours still fall through to one directory read.
|
|
3889
3889
|
*/
|
|
3890
3890
|
function hourSegmentsFromIndex(index, deviceId, profile, hourStartMs) {
|
|
3891
|
-
const hour = Math.floor(hourStartMs / HOUR_MS$
|
|
3892
|
-
const hourEnd = hour + HOUR_MS$
|
|
3891
|
+
const hour = Math.floor(hourStartMs / HOUR_MS$6) * HOUR_MS$6;
|
|
3892
|
+
const hourEnd = hour + HOUR_MS$6;
|
|
3893
3893
|
if (index.hydrationOf(deviceId, hour, hourEnd) !== "hydrated") return null;
|
|
3894
3894
|
const rows = [];
|
|
3895
3895
|
for (const s of index.segmentsStartingIn(deviceId, profile, hour, hourEnd)) rows.push({
|
|
@@ -4048,7 +4048,7 @@ var CalendarIndex = class {
|
|
|
4048
4048
|
}));
|
|
4049
4049
|
const days = /* @__PURE__ */ new Set();
|
|
4050
4050
|
for (const { day, hours } of perDay) for (const h of hours) {
|
|
4051
|
-
const at = day + h * HOUR_MS$
|
|
4051
|
+
const at = day + h * HOUR_MS$6;
|
|
4052
4052
|
if (at < fromMs || at >= toMs) continue;
|
|
4053
4053
|
days.add(localDayStart(at, tzOffsetMinutes));
|
|
4054
4054
|
}
|
|
@@ -4110,7 +4110,7 @@ var CalendarIndex = class {
|
|
|
4110
4110
|
const day = utcDayOf(utcDayMs);
|
|
4111
4111
|
for (let h = 0; h < 24; h++) {
|
|
4112
4112
|
await this.awaitQuiet();
|
|
4113
|
-
await this.segmentsOf(deviceId, profile, day + h * HOUR_MS$
|
|
4113
|
+
await this.segmentsOf(deviceId, profile, day + h * HOUR_MS$6, true);
|
|
4114
4114
|
}
|
|
4115
4115
|
}
|
|
4116
4116
|
/**
|
|
@@ -4131,7 +4131,7 @@ var CalendarIndex = class {
|
|
|
4131
4131
|
*/
|
|
4132
4132
|
async segmentsOfChecked(deviceId, profile, hourStartMs, background = false) {
|
|
4133
4133
|
if (!background) this.lastForegroundAt = this.deps.now?.() ?? Date.now();
|
|
4134
|
-
const hour = Math.floor(hourStartMs / HOUR_MS$
|
|
4134
|
+
const hour = Math.floor(hourStartMs / HOUR_MS$6) * HOUR_MS$6;
|
|
4135
4135
|
const fromIndex = this.deps.indexHour?.(deviceId, profile, hour);
|
|
4136
4136
|
if (fromIndex !== null && fromIndex !== void 0) return {
|
|
4137
4137
|
segments: fromIndex,
|
|
@@ -4139,7 +4139,7 @@ var CalendarIndex = class {
|
|
|
4139
4139
|
};
|
|
4140
4140
|
const key = `${deviceId}:${profile}:${hour}`;
|
|
4141
4141
|
const now = this.deps.now?.() ?? Date.now();
|
|
4142
|
-
const isCurrentHour = hour === Math.floor(now / HOUR_MS$
|
|
4142
|
+
const isCurrentHour = hour === Math.floor(now / HOUR_MS$6) * HOUR_MS$6;
|
|
4143
4143
|
const cached = this.hourSegs.get(key);
|
|
4144
4144
|
if (cached !== void 0) {
|
|
4145
4145
|
if (!isCurrentHour) return {
|
|
@@ -4203,7 +4203,7 @@ var CalendarIndex = class {
|
|
|
4203
4203
|
*/
|
|
4204
4204
|
async dayRanges(deviceId, profile, utcDayMs) {
|
|
4205
4205
|
const day = utcDayOf(utcDayMs);
|
|
4206
|
-
return mergeRanges((await mapBounded(Array.from({ length: 24 }, (_, h) => day + h * HOUR_MS$
|
|
4206
|
+
return mergeRanges((await mapBounded(Array.from({ length: 24 }, (_, h) => day + h * HOUR_MS$6), READ_CONCURRENCY, (hour) => this.segmentsOf(deviceId, profile, hour))).flat(), RANGE_MERGE_GAP_MS$1);
|
|
4207
4207
|
}
|
|
4208
4208
|
/**
|
|
4209
4209
|
* Exact ranges of `[fromMs, toMs)`, scanning ONLY the hours the window
|
|
@@ -4217,9 +4217,9 @@ var CalendarIndex = class {
|
|
|
4217
4217
|
*/
|
|
4218
4218
|
async rangesIn(deviceId, profile, fromMs, toMs) {
|
|
4219
4219
|
if (toMs <= fromMs) return [];
|
|
4220
|
-
const first = Math.floor(fromMs / HOUR_MS$
|
|
4220
|
+
const first = Math.floor(fromMs / HOUR_MS$6) * HOUR_MS$6;
|
|
4221
4221
|
const hours = [];
|
|
4222
|
-
for (let hour = first; hour < toMs; hour += HOUR_MS$
|
|
4222
|
+
for (let hour = first; hour < toMs; hour += HOUR_MS$6) hours.push(hour);
|
|
4223
4223
|
const perHour = await mapBounded(hours, READ_CONCURRENCY, (hour) => this.segmentsOf(deviceId, profile, hour));
|
|
4224
4224
|
const ranges = [];
|
|
4225
4225
|
for (const r of mergeRanges(perHour.flat(), RANGE_MERGE_GAP_MS$1)) {
|
|
@@ -4772,7 +4772,7 @@ function handleStoreCensusAction(census, logger, input) {
|
|
|
4772
4772
|
}
|
|
4773
4773
|
//#endregion
|
|
4774
4774
|
//#region src/recorder/addon/directory-handler.ts
|
|
4775
|
-
var HOUR_MS$
|
|
4775
|
+
var HOUR_MS$5 = 36e5;
|
|
4776
4776
|
/** A window wider than this is refused: it is a client bug, and answering it
|
|
4777
4777
|
* would walk the archive on the read path — the thing this replaces. */
|
|
4778
4778
|
var MAX_WINDOW_MS = 32 * 864e5;
|
|
@@ -4819,9 +4819,9 @@ function withDirectoryRoute(deps, next) {
|
|
|
4819
4819
|
return;
|
|
4820
4820
|
}
|
|
4821
4821
|
const rows = [];
|
|
4822
|
-
const firstHour = Math.floor(fromMs / HOUR_MS$
|
|
4822
|
+
const firstHour = Math.floor(fromMs / HOUR_MS$5) * HOUR_MS$5;
|
|
4823
4823
|
let hydrated = true;
|
|
4824
|
-
for (let hour = firstHour; hour < toMs; hour += HOUR_MS$
|
|
4824
|
+
for (let hour = firstHour; hour < toMs; hour += HOUR_MS$5) {
|
|
4825
4825
|
const { segments, complete } = await deps.source.segmentsOfChecked(deviceId, profile, hour);
|
|
4826
4826
|
if (!complete) hydrated = false;
|
|
4827
4827
|
for (const s of segments) {
|
|
@@ -8572,7 +8572,7 @@ var DurableLedger = class DurableLedger {
|
|
|
8572
8572
|
};
|
|
8573
8573
|
//#endregion
|
|
8574
8574
|
//#region src/recorder/addon/segment-hour-ledger.ts
|
|
8575
|
-
var HOUR_MS$
|
|
8575
|
+
var HOUR_MS$4 = 36e5;
|
|
8576
8576
|
/** `Math.min` where a missing left operand means "nothing yet", not `NaN`. */
|
|
8577
8577
|
function minOf(a, b) {
|
|
8578
8578
|
return a === null ? b : Math.min(a, b);
|
|
@@ -8711,6 +8711,27 @@ var RECORDING_SEGMENT_HOURS_INDEXES = [
|
|
|
8711
8711
|
]
|
|
8712
8712
|
})
|
|
8713
8713
|
];
|
|
8714
|
+
/**
|
|
8715
|
+
* How far into an hour a writer's first finalize may land and the writer still
|
|
8716
|
+
* be the party that KNOWS what the hour holds (D521).
|
|
8717
|
+
*
|
|
8718
|
+
* A seal is a claim of completeness. A writer that was running across the hour
|
|
8719
|
+
* boundary produces its first in-hour segment within ONE segment length of the
|
|
8720
|
+
* boundary — the segment that straddles it belongs to the previous hour by its
|
|
8721
|
+
* `startMs`. So the grace is one segment length at the longest anyone
|
|
8722
|
+
* configures here (60 s), plus 30 s for a keyframe-cut segment overrunning its
|
|
8723
|
+
* nominal `-segment_time` (ffmpeg cuts on the next GOP, never on the clock).
|
|
8724
|
+
*
|
|
8725
|
+
* It is a guess, and this is the shape of being wrong in each direction.
|
|
8726
|
+
* TIGHTER: a normal rollover stops sealing, every hour of the archive is walked
|
|
8727
|
+
* once by its first reader, and D451's "at rest nothing is unsealed" is undone
|
|
8728
|
+
* — at ~3.6 ms per hour directory cold, affordable but pointless. LOOSER: a
|
|
8729
|
+
* runner respawn inside the window seals an hour it entered late, which is
|
|
8730
|
+
* exactly the 2026-09-17 defect; the observed hole there was ~29 minutes, so
|
|
8731
|
+
* anything under a few minutes would have caught it and 90 s is the smallest
|
|
8732
|
+
* number that does not fire on a legitimate rollover.
|
|
8733
|
+
*/
|
|
8734
|
+
var WRITER_HOUR_ENTRY_GRACE_MS = 9e4;
|
|
8714
8735
|
/** The identity of the {@link hourAggregate} fold — an hour holding nothing. */
|
|
8715
8736
|
var EMPTY_HOUR_AGGREGATE = {
|
|
8716
8737
|
bytes: 0,
|
|
@@ -8931,7 +8952,7 @@ var EMPTY_ACCOUNTING = {
|
|
|
8931
8952
|
newestMs: null
|
|
8932
8953
|
};
|
|
8933
8954
|
function hourStartMs(startMs) {
|
|
8934
|
-
return Math.floor(startMs / HOUR_MS$
|
|
8955
|
+
return Math.floor(startMs / HOUR_MS$4) * HOUR_MS$4;
|
|
8935
8956
|
}
|
|
8936
8957
|
function segmentHourKey(deviceId, profile, locationId, hour) {
|
|
8937
8958
|
return `${deviceId}|${profile}|${locationId}|${hour}`;
|
|
@@ -8948,6 +8969,11 @@ function hourOf(row) {
|
|
|
8948
8969
|
function writerKey(deviceId, profile) {
|
|
8949
8970
|
return `${deviceId}|${profile}`;
|
|
8950
8971
|
}
|
|
8972
|
+
/** One writer's hour, WITHOUT the location — the identity a loss is reported
|
|
8973
|
+
* against by a party that only knows the camera, the profile and the clock. */
|
|
8974
|
+
function writerHourKey(deviceId, profile, hour) {
|
|
8975
|
+
return `${deviceId}|${profile}|${hour}`;
|
|
8976
|
+
}
|
|
8951
8977
|
function detailOf(row) {
|
|
8952
8978
|
return {
|
|
8953
8979
|
locationId: row.locationId,
|
|
@@ -9094,6 +9120,13 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9094
9120
|
writerHours = /* @__PURE__ */ new Map();
|
|
9095
9121
|
/** Hour keys a write was REFUSED for in this process — never sealed by it. */
|
|
9096
9122
|
refused = /* @__PURE__ */ new Set();
|
|
9123
|
+
/**
|
|
9124
|
+
* `${deviceId}|${profile}|${hourStartMs}` a LOSS was reported for, location
|
|
9125
|
+
* left out on purpose: the party that sees the loss (the playlist watcher)
|
|
9126
|
+
* knows the camera and the profile and has no business knowing which disk
|
|
9127
|
+
* the recorder happens to be placing that hour on.
|
|
9128
|
+
*/
|
|
9129
|
+
refusedWriterHours = /* @__PURE__ */ new Set();
|
|
9097
9130
|
constructor(deps) {
|
|
9098
9131
|
this.logger = deps.logger;
|
|
9099
9132
|
this.store = deps.store;
|
|
@@ -9304,6 +9337,10 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9304
9337
|
*
|
|
9305
9338
|
* `null` when the row is known and its detail could not be read: nothing
|
|
9306
9339
|
* is written from a row whose paths are not resident.
|
|
9340
|
+
*
|
|
9341
|
+
* @seal-authority closed-hour-listing — a directory read of an hour that is
|
|
9342
|
+
* OVER is the one complete answer there is; the caller guarantees the hour is
|
|
9343
|
+
* closed (D451, D521).
|
|
9307
9344
|
*/
|
|
9308
9345
|
async adoptWalkedHour(deviceId, profile, locationId, hour, paths) {
|
|
9309
9346
|
const key = segmentHourKey(deviceId, profile, locationId, hour);
|
|
@@ -9388,11 +9425,65 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9388
9425
|
markRefused(row) {
|
|
9389
9426
|
this.refused.add(hourOf(row).key);
|
|
9390
9427
|
}
|
|
9428
|
+
/**
|
|
9429
|
+
* Segments a writer produced that never reached {@link recordSegment} — the
|
|
9430
|
+
* watcher's `rolledOff`, the one moment the system KNOWS work was skipped.
|
|
9431
|
+
*
|
|
9432
|
+
* Every hour the loss spans is refused the seal, whatever else this process
|
|
9433
|
+
* believes about them: the ownership grace answers "was I here", and this
|
|
9434
|
+
* answers "did I get everything while I was". Camera 615's respawned writer
|
|
9435
|
+
* passed the first question for the hour it entered at the top and still held
|
|
9436
|
+
* a row short by 175 segments.
|
|
9437
|
+
*
|
|
9438
|
+
* Returns how many hours were marked, so the caller's line reports what its
|
|
9439
|
+
* report bought rather than that it was made.
|
|
9440
|
+
*/
|
|
9441
|
+
markHoursRefused(span) {
|
|
9442
|
+
if (!Number.isFinite(span.fromMs) || !Number.isFinite(span.toMs)) return 0;
|
|
9443
|
+
const first = hourStartMs(Math.min(span.fromMs, span.toMs));
|
|
9444
|
+
const last = hourStartMs(Math.max(span.fromMs, span.toMs));
|
|
9445
|
+
let marked = 0;
|
|
9446
|
+
for (let hour = first; hour <= last && marked < 24; hour += HOUR_MS$4) {
|
|
9447
|
+
this.refusedWriterHours.add(writerHourKey(span.deviceId, span.profile, hour));
|
|
9448
|
+
marked += 1;
|
|
9449
|
+
}
|
|
9450
|
+
if (marked === 0) return 0;
|
|
9451
|
+
this.logger.warn("recorder: segments this writer produced never reached the hour ledger — these hours stay UNSEALED and a reader walks them", {
|
|
9452
|
+
tags: { deviceId: span.deviceId },
|
|
9453
|
+
meta: {
|
|
9454
|
+
profile: span.profile,
|
|
9455
|
+
fromMs: first,
|
|
9456
|
+
toMs: last,
|
|
9457
|
+
hours: marked,
|
|
9458
|
+
segments: span.count,
|
|
9459
|
+
boundHours: 24
|
|
9460
|
+
}
|
|
9461
|
+
});
|
|
9462
|
+
return marked;
|
|
9463
|
+
}
|
|
9391
9464
|
/** One write: the row as held, flagged sealed. Serialised on its key so it
|
|
9392
|
-
* cannot land over an append that read the row before it.
|
|
9465
|
+
* cannot land over an append that read the row before it.
|
|
9466
|
+
*
|
|
9467
|
+
* @seal-authority writer-owned-hour — the writer that LEAVES the hour, and
|
|
9468
|
+
* only when it entered within {@link WRITER_HOUR_ENTRY_GRACE_MS} of the
|
|
9469
|
+
* hour's start and no loss was reported against it (D521). */
|
|
9393
9470
|
async sealHour(target) {
|
|
9394
9471
|
await this.serialized(target.key, async () => {
|
|
9395
|
-
|
|
9472
|
+
const enteredAfterMs = target.entryMs - target.hourStartMs;
|
|
9473
|
+
if (enteredAfterMs > 9e4) {
|
|
9474
|
+
this.logger.warn("recorder: hour left unsealed — this writer entered it late, so it cannot claim the hour is complete", {
|
|
9475
|
+
tags: { deviceId: target.deviceId },
|
|
9476
|
+
meta: {
|
|
9477
|
+
profile: target.profile,
|
|
9478
|
+
locationId: target.locationId,
|
|
9479
|
+
hourStartMs: target.hourStartMs,
|
|
9480
|
+
enteredAfterMs,
|
|
9481
|
+
graceMs: WRITER_HOUR_ENTRY_GRACE_MS
|
|
9482
|
+
}
|
|
9483
|
+
});
|
|
9484
|
+
return;
|
|
9485
|
+
}
|
|
9486
|
+
if (this.refused.has(target.key) || this.refusedWriterHours.has(writerHourKey(target.deviceId, target.profile, target.hourStartMs))) {
|
|
9396
9487
|
this.logger.warn("recorder: hour left unsealed — a ledger write for it was refused, so its row is short and a reader must walk it", {
|
|
9397
9488
|
tags: { deviceId: target.deviceId },
|
|
9398
9489
|
meta: {
|
|
@@ -9412,9 +9503,15 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9412
9503
|
});
|
|
9413
9504
|
}
|
|
9414
9505
|
/**
|
|
9415
|
-
* Remember which hour this writer is on
|
|
9416
|
-
*
|
|
9417
|
-
*
|
|
9506
|
+
* Remember which hour this writer is on — and how far into it this process
|
|
9507
|
+
* ARRIVED ({@link WriterHour.entryMs}, the first finalize's own start) —
|
|
9508
|
+
* and when it MOVES FORWARD, seal the hour it left. Backwards is a late
|
|
9509
|
+
* finalize (a recovered orphan, a replay) into an hour already accounted for,
|
|
9510
|
+
* and seals nothing.
|
|
9511
|
+
*
|
|
9512
|
+
* The entry stamp is kept from the FIRST finalize into the hour and never
|
|
9513
|
+
* refreshed: the question the seal has to answer is "was this writer here
|
|
9514
|
+
* when the hour began", and the last segment of the hour cannot answer it.
|
|
9418
9515
|
*/
|
|
9419
9516
|
noteWriter(next) {
|
|
9420
9517
|
const id = writerKey(next.deviceId, next.profile);
|
|
@@ -9531,7 +9628,7 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9531
9628
|
/** The hour a write path may touch: current + previous. Materialised at boot
|
|
9532
9629
|
* so {@link recordSegment}'s dedup gate answers from RAM (D49). */
|
|
9533
9630
|
async materializeLiveTail() {
|
|
9534
|
-
const oldest = hourStartMs(this.now()) - (LIVE_TAIL_HOURS - 1) * HOUR_MS$
|
|
9631
|
+
const oldest = hourStartMs(this.now()) - (LIVE_TAIL_HOURS - 1) * HOUR_MS$4;
|
|
9535
9632
|
const keys = [];
|
|
9536
9633
|
for (const row of this.ledger.snapshot()) if (row.hourStartMs >= oldest && !row.pathsResident) keys.push(row.key);
|
|
9537
9634
|
return (await this.materialize(keys)).length;
|
|
@@ -10039,7 +10136,8 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
10039
10136
|
deviceId: row.deviceId,
|
|
10040
10137
|
profile: row.profile,
|
|
10041
10138
|
locationId: row.locationId,
|
|
10042
|
-
hourStartMs: hour
|
|
10139
|
+
hourStartMs: hour,
|
|
10140
|
+
entryMs: row.startMs
|
|
10043
10141
|
});
|
|
10044
10142
|
await this.serialized(key, async () => {
|
|
10045
10143
|
if (!this.ledger.has(key)) {
|
|
@@ -10425,7 +10523,7 @@ function locationKey(deviceId, locationId) {
|
|
|
10425
10523
|
* Work it does not finish is left for the next pass and REPORTED, never
|
|
10426
10524
|
* silently dropped.
|
|
10427
10525
|
*/
|
|
10428
|
-
var HOUR_MS$
|
|
10526
|
+
var HOUR_MS$3 = 36e5;
|
|
10429
10527
|
/**
|
|
10430
10528
|
* Group resolved locations by PHYSICAL root, so two aliases of one disk are
|
|
10431
10529
|
* probed once and a `true` from each is not mistaken for the file existing in
|
|
@@ -10534,7 +10632,7 @@ var GhostLocationSweeper = class {
|
|
|
10534
10632
|
refused: null
|
|
10535
10633
|
};
|
|
10536
10634
|
}
|
|
10537
|
-
const liveTailFrom = this.deps.now() - HOUR_MS$
|
|
10635
|
+
const liveTailFrom = this.deps.now() - HOUR_MS$3;
|
|
10538
10636
|
let examined = 0;
|
|
10539
10637
|
let probes = 0;
|
|
10540
10638
|
let repointedHours = 0;
|
|
@@ -11116,7 +11214,7 @@ function createRecordingConfigMirror(deps) {
|
|
|
11116
11214
|
}
|
|
11117
11215
|
//#endregion
|
|
11118
11216
|
//#region src/recorder/addon/ledger-walk-reconcile.ts
|
|
11119
|
-
var HOUR_MS$
|
|
11217
|
+
var HOUR_MS$2 = 36e5;
|
|
11120
11218
|
/** Every reason at zero, so a reason that never fired reports as zero rather
|
|
11121
11219
|
* than absent and the summary shape is constant between passes. */
|
|
11122
11220
|
function zeroReasons() {
|
|
@@ -11212,7 +11310,7 @@ var LedgerWalkReconciler = class {
|
|
|
11212
11310
|
ledger,
|
|
11213
11311
|
apply,
|
|
11214
11312
|
anchor: anchor.volumeId,
|
|
11215
|
-
liveTailFrom: hourStartMs(this.deps.now()) - 1 * HOUR_MS$
|
|
11313
|
+
liveTailFrom: hourStartMs(this.deps.now()) - 1 * HOUR_MS$2,
|
|
11216
11314
|
maxHours: this.deps.maxHours ?? 4096,
|
|
11217
11315
|
byDevice: /* @__PURE__ */ new Map(),
|
|
11218
11316
|
skippedByReason: zeroReasons(),
|
|
@@ -11845,6 +11943,240 @@ function pendingMovesSignature(moves) {
|
|
|
11845
11943
|
return moves.map((m) => `${String(m.deviceId)}:${m.profile}>${m.plannedFor}`).join(",");
|
|
11846
11944
|
}
|
|
11847
11945
|
//#endregion
|
|
11946
|
+
//#region src/recorder/addon/hour-adoption-sweep.ts
|
|
11947
|
+
var HOUR_MS$1 = 36e5;
|
|
11948
|
+
/** The profile trees a camera directory is allowed to have. Everything else
|
|
11949
|
+
* under it (`events/`, the retired `strips/`) has no hour tree worth probing. */
|
|
11950
|
+
var PROFILES = [
|
|
11951
|
+
"high",
|
|
11952
|
+
"mid",
|
|
11953
|
+
"low"
|
|
11954
|
+
];
|
|
11955
|
+
function isTwoDigit(name) {
|
|
11956
|
+
return /^\d{2}$/.test(name);
|
|
11957
|
+
}
|
|
11958
|
+
/** One directory level in a stable order. Lexicographic, not numeric: it is the
|
|
11959
|
+
* CURSOR's order, and zero-padded path components (`09`, `17`) already sort
|
|
11960
|
+
* chronologically — device ids do not, and do not need to. */
|
|
11961
|
+
function inCursorOrder(names) {
|
|
11962
|
+
return [...names].toSorted();
|
|
11963
|
+
}
|
|
11964
|
+
/**
|
|
11965
|
+
* One add-only pass over one location's hour directories.
|
|
11966
|
+
*
|
|
11967
|
+
* Never throws: a directory that cannot be read is counted and the pass
|
|
11968
|
+
* continues; a precondition failure is a named refusal and the pass does
|
|
11969
|
+
* nothing at all.
|
|
11970
|
+
*/
|
|
11971
|
+
var HourAdoptionSweeper = class {
|
|
11972
|
+
deps;
|
|
11973
|
+
/** Where the last bounded pass stopped (`rel` of the last hour it walked).
|
|
11974
|
+
* In RAM: a restart re-walks from the top, which costs listings and can
|
|
11975
|
+
* never cost footage. */
|
|
11976
|
+
cursor = null;
|
|
11977
|
+
constructor(deps) {
|
|
11978
|
+
this.deps = deps;
|
|
11979
|
+
}
|
|
11980
|
+
async sweep(request) {
|
|
11981
|
+
const location = this.deps.locations().find((candidate) => candidate.id === request.locationId);
|
|
11982
|
+
if (location === void 0) return this.refusedReport(request.locationId, "location-unknown");
|
|
11983
|
+
if (!locationMayBeRead(location)) return this.refusedReport(location.id, "location-unreadable");
|
|
11984
|
+
const ledger = this.deps.ledger();
|
|
11985
|
+
if (ledger === null) {
|
|
11986
|
+
this.deps.logger.warn("recorder: hour adoption REFUSED — this node holds no durable hour ledger, so there is nothing to adopt into", { meta: { locationId: location.id } });
|
|
11987
|
+
return this.refusedReport(location.id, "no-ledger");
|
|
11988
|
+
}
|
|
11989
|
+
const liveTailFrom = hourStartMs(this.deps.now()) - 1 * HOUR_MS$1;
|
|
11990
|
+
const maxHours = this.deps.maxHours ?? 512;
|
|
11991
|
+
const resume = this.cursor;
|
|
11992
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
11993
|
+
let listings = 0;
|
|
11994
|
+
let hoursAdopted = 0;
|
|
11995
|
+
let hoursRefusedByLedger = 0;
|
|
11996
|
+
let hoursSkippedLive = 0;
|
|
11997
|
+
let hoursSkippedEmpty = 0;
|
|
11998
|
+
let listingErrors = 0;
|
|
11999
|
+
let segmentsAdded = 0;
|
|
12000
|
+
let hoursCreated = 0;
|
|
12001
|
+
let bounded = false;
|
|
12002
|
+
let last = null;
|
|
12003
|
+
for await (const candidate of this.candidates(location, () => {
|
|
12004
|
+
listingErrors += 1;
|
|
12005
|
+
})) {
|
|
12006
|
+
if (resume !== null && candidate.rel <= resume) continue;
|
|
12007
|
+
if (candidate.hourStartMs >= liveTailFrom) {
|
|
12008
|
+
hoursSkippedLive += 1;
|
|
12009
|
+
continue;
|
|
12010
|
+
}
|
|
12011
|
+
if (listings >= maxHours) {
|
|
12012
|
+
bounded = true;
|
|
12013
|
+
break;
|
|
12014
|
+
}
|
|
12015
|
+
let names;
|
|
12016
|
+
try {
|
|
12017
|
+
names = await this.deps.listDir(`${location.root}/${candidate.rel}`);
|
|
12018
|
+
} catch (err) {
|
|
12019
|
+
listingErrors += 1;
|
|
12020
|
+
this.deps.logger.debug("recorder: hour adoption could not list an hour directory", {
|
|
12021
|
+
tags: { deviceId: candidate.deviceId },
|
|
12022
|
+
meta: {
|
|
12023
|
+
locationId: location.id,
|
|
12024
|
+
rel: candidate.rel,
|
|
12025
|
+
error: require_dist.errMsg(err)
|
|
12026
|
+
}
|
|
12027
|
+
});
|
|
12028
|
+
continue;
|
|
12029
|
+
}
|
|
12030
|
+
listings += 1;
|
|
12031
|
+
last = candidate.rel;
|
|
12032
|
+
if (listings % 8 === 0) await this.deps.pace();
|
|
12033
|
+
const paths = (names ?? []).filter((name) => name.endsWith(".m4s")).map((name) => `${candidate.rel}/${name}`);
|
|
12034
|
+
if (paths.length === 0) {
|
|
12035
|
+
hoursSkippedEmpty += 1;
|
|
12036
|
+
continue;
|
|
12037
|
+
}
|
|
12038
|
+
const outcome = await ledger.adoptWalkedHour(candidate.deviceId, candidate.profile, location.id, candidate.hourStartMs, paths);
|
|
12039
|
+
if (outcome === null) {
|
|
12040
|
+
hoursRefusedByLedger += 1;
|
|
12041
|
+
continue;
|
|
12042
|
+
}
|
|
12043
|
+
hoursAdopted += 1;
|
|
12044
|
+
segmentsAdded += outcome.added;
|
|
12045
|
+
if (outcome.created) hoursCreated += 1;
|
|
12046
|
+
const tally = byDevice.get(candidate.deviceId) ?? {
|
|
12047
|
+
hoursAdopted: 0,
|
|
12048
|
+
segmentsAdded: 0,
|
|
12049
|
+
hoursCreated: 0
|
|
12050
|
+
};
|
|
12051
|
+
tally.hoursAdopted += 1;
|
|
12052
|
+
tally.segmentsAdded += outcome.added;
|
|
12053
|
+
if (outcome.created) tally.hoursCreated += 1;
|
|
12054
|
+
byDevice.set(candidate.deviceId, tally);
|
|
12055
|
+
if (outcome.added > 0) this.deps.logger.warn("recorder: adopted segments the durable hour row did not name — this hour was under-reported to every reader", {
|
|
12056
|
+
tags: { deviceId: candidate.deviceId },
|
|
12057
|
+
meta: {
|
|
12058
|
+
locationId: location.id,
|
|
12059
|
+
profile: candidate.profile,
|
|
12060
|
+
hourStartMs: candidate.hourStartMs,
|
|
12061
|
+
added: outcome.added,
|
|
12062
|
+
created: outcome.created,
|
|
12063
|
+
onDisk: paths.length
|
|
12064
|
+
}
|
|
12065
|
+
});
|
|
12066
|
+
}
|
|
12067
|
+
this.cursor = bounded ? last ?? resume : null;
|
|
12068
|
+
const report = {
|
|
12069
|
+
locationId: location.id,
|
|
12070
|
+
refused: null,
|
|
12071
|
+
listings,
|
|
12072
|
+
hoursAdopted,
|
|
12073
|
+
hoursRefusedByLedger,
|
|
12074
|
+
hoursSkippedLive,
|
|
12075
|
+
hoursSkippedEmpty,
|
|
12076
|
+
listingErrors,
|
|
12077
|
+
segmentsAdded,
|
|
12078
|
+
hoursCreated,
|
|
12079
|
+
bounded,
|
|
12080
|
+
byDevice: [...byDevice.entries()].map(([deviceId, tally]) => ({
|
|
12081
|
+
deviceId,
|
|
12082
|
+
...tally
|
|
12083
|
+
})).toSorted((a, b) => a.deviceId - b.deviceId)
|
|
12084
|
+
};
|
|
12085
|
+
this.report(report);
|
|
12086
|
+
return report;
|
|
12087
|
+
}
|
|
12088
|
+
/**
|
|
12089
|
+
* Every hour directory under this location, in cursor order.
|
|
12090
|
+
*
|
|
12091
|
+
* Sorted at each level so the cursor is a total order over a tree two passes
|
|
12092
|
+
* see differently: a camera added between passes appears in its place rather
|
|
12093
|
+
* than at the end, and is picked up by the pass after the cursor resets.
|
|
12094
|
+
*/
|
|
12095
|
+
async *candidates(location, onError) {
|
|
12096
|
+
const devices = await this.list(location.root, onError);
|
|
12097
|
+
for (const device of inCursorOrder(devices.filter((name) => /^\d+$/.test(name)))) {
|
|
12098
|
+
const deviceId = Number(device);
|
|
12099
|
+
if (!Number.isSafeInteger(deviceId)) continue;
|
|
12100
|
+
const present = new Set(await this.list(`${location.root}/${device}`, onError));
|
|
12101
|
+
for (const profile of PROFILES) {
|
|
12102
|
+
if (!present.has(profile)) continue;
|
|
12103
|
+
const base = `${device}/${profile}`;
|
|
12104
|
+
for (const year of inCursorOrder((await this.list(`${location.root}/${base}`, onError)).filter((n) => /^\d{4}$/.test(n)))) for (const month of inCursorOrder((await this.list(`${location.root}/${base}/${year}`, onError)).filter(isTwoDigit))) for (const day of inCursorOrder((await this.list(`${location.root}/${base}/${year}/${month}`, onError)).filter(isTwoDigit))) {
|
|
12105
|
+
const dayDir = `${base}/${year}/${month}/${day}`;
|
|
12106
|
+
for (const hour of inCursorOrder((await this.list(`${location.root}/${dayDir}`, onError)).filter(isTwoDigit))) {
|
|
12107
|
+
const startMs = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour));
|
|
12108
|
+
if (!Number.isFinite(startMs)) continue;
|
|
12109
|
+
yield {
|
|
12110
|
+
deviceId,
|
|
12111
|
+
profile,
|
|
12112
|
+
hourStartMs: startMs,
|
|
12113
|
+
rel: `${dayDir}/${hour}`
|
|
12114
|
+
};
|
|
12115
|
+
}
|
|
12116
|
+
}
|
|
12117
|
+
}
|
|
12118
|
+
}
|
|
12119
|
+
}
|
|
12120
|
+
/** One directory's names; `[]` for a directory that is not there, and `[]`
|
|
12121
|
+
* plus a counted error for one that could not be read. */
|
|
12122
|
+
async list(absDir, onError) {
|
|
12123
|
+
try {
|
|
12124
|
+
return await this.deps.listDir(absDir) ?? [];
|
|
12125
|
+
} catch (err) {
|
|
12126
|
+
onError();
|
|
12127
|
+
this.deps.logger.debug("recorder: hour adoption could not list a directory", { meta: {
|
|
12128
|
+
dir: absDir,
|
|
12129
|
+
error: require_dist.errMsg(err)
|
|
12130
|
+
} });
|
|
12131
|
+
return [];
|
|
12132
|
+
}
|
|
12133
|
+
}
|
|
12134
|
+
refusedReport(locationId, refused) {
|
|
12135
|
+
return {
|
|
12136
|
+
locationId,
|
|
12137
|
+
refused,
|
|
12138
|
+
listings: 0,
|
|
12139
|
+
hoursAdopted: 0,
|
|
12140
|
+
hoursRefusedByLedger: 0,
|
|
12141
|
+
hoursSkippedLive: 0,
|
|
12142
|
+
hoursSkippedEmpty: 0,
|
|
12143
|
+
listingErrors: 0,
|
|
12144
|
+
segmentsAdded: 0,
|
|
12145
|
+
hoursCreated: 0,
|
|
12146
|
+
bounded: false,
|
|
12147
|
+
byDevice: []
|
|
12148
|
+
};
|
|
12149
|
+
}
|
|
12150
|
+
/** One line per camera that gained something, then one for the pass — a pass
|
|
12151
|
+
* that found nothing must still be distinguishable from one that never ran. */
|
|
12152
|
+
report(report) {
|
|
12153
|
+
for (const device of report.byDevice) {
|
|
12154
|
+
if (device.segmentsAdded === 0 && device.hoursCreated === 0) continue;
|
|
12155
|
+
this.deps.logger.info("recorder: hour adoption — camera summary", {
|
|
12156
|
+
tags: { deviceId: device.deviceId },
|
|
12157
|
+
meta: {
|
|
12158
|
+
locationId: report.locationId,
|
|
12159
|
+
hoursAdopted: device.hoursAdopted,
|
|
12160
|
+
segmentsAdded: device.segmentsAdded,
|
|
12161
|
+
hoursCreated: device.hoursCreated
|
|
12162
|
+
}
|
|
12163
|
+
});
|
|
12164
|
+
}
|
|
12165
|
+
this.deps.logger.info("recorder: hour adoption pass complete", { meta: {
|
|
12166
|
+
locationId: report.locationId,
|
|
12167
|
+
listings: report.listings,
|
|
12168
|
+
hoursAdopted: report.hoursAdopted,
|
|
12169
|
+
hoursRefusedByLedger: report.hoursRefusedByLedger,
|
|
12170
|
+
hoursSkippedLive: report.hoursSkippedLive,
|
|
12171
|
+
hoursSkippedEmpty: report.hoursSkippedEmpty,
|
|
12172
|
+
listingErrors: report.listingErrors,
|
|
12173
|
+
segmentsAdded: report.segmentsAdded,
|
|
12174
|
+
hoursCreated: report.hoursCreated,
|
|
12175
|
+
bounded: report.bounded
|
|
12176
|
+
} });
|
|
12177
|
+
}
|
|
12178
|
+
};
|
|
12179
|
+
//#endregion
|
|
11848
12180
|
//#region src/recorder/addon/periodic-pass.ts
|
|
11849
12181
|
function startPeriodicPass(deps) {
|
|
11850
12182
|
const now = () => deps.now?.() ?? Date.now();
|
|
@@ -14204,11 +14536,16 @@ function planFinalizations(body, processed, forceFinal = false) {
|
|
|
14204
14536
|
durMs
|
|
14205
14537
|
});
|
|
14206
14538
|
}
|
|
14539
|
+
const front = entries.length > 0 ? parseEpochStartMs(entries[0].segPath) : null;
|
|
14540
|
+
const longestDurMs = entries.reduce((longest, entry) => Math.max(longest, Math.round(entry.durSeconds * 1e3)), 0);
|
|
14541
|
+
const spanKnown = rolledOff > 0 && front !== null && longestDurMs > 0;
|
|
14207
14542
|
return {
|
|
14208
14543
|
toHandle,
|
|
14209
14544
|
from: hw,
|
|
14210
14545
|
processed: Math.max(hw, finalizable),
|
|
14211
|
-
rolledOff
|
|
14546
|
+
rolledOff,
|
|
14547
|
+
rolledOffFromMs: spanKnown ? front - rolledOff * longestDurMs : null,
|
|
14548
|
+
rolledOffToMs: spanKnown ? front : null
|
|
14212
14549
|
};
|
|
14213
14550
|
}
|
|
14214
14551
|
/**
|
|
@@ -14236,11 +14573,24 @@ function startSegmentWatcher(deps) {
|
|
|
14236
14573
|
return;
|
|
14237
14574
|
}
|
|
14238
14575
|
const plan = planFinalizations(body, processed, forceFinal);
|
|
14239
|
-
if (plan.rolledOff > 0)
|
|
14240
|
-
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
|
|
14576
|
+
if (plan.rolledOff > 0) {
|
|
14577
|
+
deps.logger.warn("segment playlist rolled past entries this watcher had not handled", {
|
|
14578
|
+
tags: { deviceId: deps.deviceId },
|
|
14579
|
+
meta: {
|
|
14580
|
+
rolledOff: plan.rolledOff,
|
|
14581
|
+
processed,
|
|
14582
|
+
from: plan.from,
|
|
14583
|
+
fromMs: plan.rolledOffFromMs,
|
|
14584
|
+
toMs: plan.rolledOffToMs
|
|
14585
|
+
}
|
|
14586
|
+
});
|
|
14587
|
+
deps.onSegmentsMissed?.({
|
|
14588
|
+
deviceId: deps.deviceId,
|
|
14589
|
+
count: plan.rolledOff,
|
|
14590
|
+
fromMs: plan.rolledOffFromMs,
|
|
14591
|
+
toMs: plan.rolledOffToMs
|
|
14592
|
+
});
|
|
14593
|
+
}
|
|
14244
14594
|
let index = plan.from;
|
|
14245
14595
|
for (const { entry, durMs } of plan.toHandle) {
|
|
14246
14596
|
if (await handleSegmentEntry(deps.outDir, entry, durMs, deps.onFinalized, deps.logger, deps.writerStartedMs) === "pending") {
|
|
@@ -14250,11 +14600,14 @@ function startSegmentWatcher(deps) {
|
|
|
14250
14600
|
waitedMs,
|
|
14251
14601
|
boundMs: 3e4
|
|
14252
14602
|
})) {
|
|
14253
|
-
deps.logger.warn("segment file never landed — skipping", {
|
|
14254
|
-
|
|
14255
|
-
|
|
14256
|
-
|
|
14257
|
-
|
|
14603
|
+
deps.logger.warn("segment file never landed — skipping", {
|
|
14604
|
+
tags: { deviceId: deps.deviceId },
|
|
14605
|
+
meta: {
|
|
14606
|
+
seg: entry.segPath,
|
|
14607
|
+
waitedMs,
|
|
14608
|
+
boundMs: PENDING_GRACE_MS
|
|
14609
|
+
}
|
|
14610
|
+
});
|
|
14258
14611
|
pendingIndex = -1;
|
|
14259
14612
|
pendingSinceMs = 0;
|
|
14260
14613
|
index++;
|
|
@@ -16092,8 +16445,12 @@ var RecordingController = class {
|
|
|
16092
16445
|
writer,
|
|
16093
16446
|
watcher: startSegmentWatcher({
|
|
16094
16447
|
outDir,
|
|
16448
|
+
deviceId,
|
|
16095
16449
|
writerStartedMs,
|
|
16096
16450
|
now: () => this.now(),
|
|
16451
|
+
onSegmentsMissed: (missed) => {
|
|
16452
|
+
this.deps.onSegmentsMissed?.(missed, profile);
|
|
16453
|
+
},
|
|
16097
16454
|
intervalMs: this.deps.watchIntervalMs > 0 ? this.deps.watchIntervalMs : DEFAULT_WATCH_INTERVAL_MS,
|
|
16098
16455
|
onFinalized: async (startMs, durMs, flatAbsPath) => {
|
|
16099
16456
|
this.lastSegmentAt.set(idleKey(deviceId, profile), this.now());
|
|
@@ -17435,6 +17792,20 @@ var GHOST_LOCATION_PROBE_PAUSE_MS = 50;
|
|
|
17435
17792
|
* hung an SSH session on 2026-08-29.
|
|
17436
17793
|
*/
|
|
17437
17794
|
var LEDGER_WALK_PAUSE_MS = 100;
|
|
17795
|
+
/**
|
|
17796
|
+
* How often the ADD-only hour adoption pass runs (D521).
|
|
17797
|
+
*
|
|
17798
|
+
* Daily, for the same reason the ghost sweep is daily: it spends `readdir`
|
|
17799
|
+
* syscalls on the array every live read competes for, and the damage it repairs
|
|
17800
|
+
* is made by a writer teardown — an event measured in days, not minutes. It
|
|
17801
|
+
* does NOT run at boot (`startPeriodicPass` fires on the interval only), which
|
|
17802
|
+
* is when the staging reconcile, the ledger load and the first retention sweep
|
|
17803
|
+
* are already contending for that array.
|
|
17804
|
+
*/
|
|
17805
|
+
var HOUR_ADOPTION_INTERVAL_MS = 1440 * 6e4;
|
|
17806
|
+
/** An adoption pass longer than this is reported with its duration. Bounded
|
|
17807
|
+
* work (512 hour listings, paced) — minutes is normal, an hour is not. */
|
|
17808
|
+
var HOUR_ADOPTION_SLOW_AFTER_MS = 10 * 6e4;
|
|
17438
17809
|
/** What the per-volume usage row reports when the hour ledger is unavailable:
|
|
17439
17810
|
* nothing measured, rather than a total counted off a partial RAM index. */
|
|
17440
17811
|
var EMPTY_ARCHIVE_ACCOUNTING = {
|
|
@@ -17659,6 +18030,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
17659
18030
|
/** Daily ghost-location reconcile: durable hour rows stamped with a location
|
|
17660
18031
|
* the storage record no longer has. Armed in `onHubReachable`. */
|
|
17661
18032
|
ghostLocationPass = null;
|
|
18033
|
+
hourAdoptionPass = null;
|
|
17662
18034
|
/** Reversible migration pause lease; never reflected in RecordingConfig. */
|
|
17663
18035
|
storageMigrationLeaseId = null;
|
|
17664
18036
|
/**
|
|
@@ -17900,6 +18272,27 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
17900
18272
|
onWriterClosed: async (deviceId, profile) => {
|
|
17901
18273
|
await this.segmentHours?.sealWriter(deviceId, profile);
|
|
17902
18274
|
},
|
|
18275
|
+
onSegmentsMissed: (missed, profile) => {
|
|
18276
|
+
const ledger = this.segmentHours;
|
|
18277
|
+
if (ledger === null) return;
|
|
18278
|
+
if (missed.fromMs === null || missed.toMs === null) {
|
|
18279
|
+
this.ctx.logger.warn("recorder: segments were missed but the playlist named no span — NO hour could be held unsealed for them", {
|
|
18280
|
+
tags: { deviceId: missed.deviceId },
|
|
18281
|
+
meta: {
|
|
18282
|
+
profile,
|
|
18283
|
+
segments: missed.count
|
|
18284
|
+
}
|
|
18285
|
+
});
|
|
18286
|
+
return;
|
|
18287
|
+
}
|
|
18288
|
+
ledger.markHoursRefused({
|
|
18289
|
+
deviceId: missed.deviceId,
|
|
18290
|
+
profile,
|
|
18291
|
+
fromMs: missed.fromMs,
|
|
18292
|
+
toMs: missed.toMs,
|
|
18293
|
+
count: missed.count
|
|
18294
|
+
});
|
|
18295
|
+
},
|
|
17903
18296
|
spawn: node_child_process.spawn,
|
|
17904
18297
|
costRegistry: this.costRegistry,
|
|
17905
18298
|
segmentSeconds: this.config.segmentSeconds,
|
|
@@ -18265,6 +18658,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
18265
18658
|
this.redundancySweeper = null;
|
|
18266
18659
|
this.ghostLocationPass?.stop();
|
|
18267
18660
|
this.ghostLocationPass = null;
|
|
18661
|
+
this.hourAdoptionPass?.stop();
|
|
18662
|
+
this.hourAdoptionPass = null;
|
|
18268
18663
|
this.exportEngine = null;
|
|
18269
18664
|
this.recordingProvider = null;
|
|
18270
18665
|
}
|
|
@@ -18516,6 +18911,32 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
18516
18911
|
logger: this.ctx.logger
|
|
18517
18912
|
});
|
|
18518
18913
|
}
|
|
18914
|
+
if (this.hourAdoptionPass === null) {
|
|
18915
|
+
const adopter = new HourAdoptionSweeper({
|
|
18916
|
+
ledger: () => this.segmentHours,
|
|
18917
|
+
locations: () => this.resolvedLocations,
|
|
18918
|
+
listDir: async (absDir) => {
|
|
18919
|
+
try {
|
|
18920
|
+
return await node_fs.promises.readdir(absDir);
|
|
18921
|
+
} catch (err) {
|
|
18922
|
+
if (err.code === "ENOENT") return null;
|
|
18923
|
+
throw err;
|
|
18924
|
+
}
|
|
18925
|
+
},
|
|
18926
|
+
logger: this.ctx.logger,
|
|
18927
|
+
now: () => Date.now(),
|
|
18928
|
+
pace: () => require_dist.sleep(LEDGER_WALK_PAUSE_MS)
|
|
18929
|
+
});
|
|
18930
|
+
this.hourAdoptionPass = startPeriodicPass({
|
|
18931
|
+
name: "hour-adoption",
|
|
18932
|
+
intervalMs: HOUR_ADOPTION_INTERVAL_MS,
|
|
18933
|
+
slowAfterMs: HOUR_ADOPTION_SLOW_AFTER_MS,
|
|
18934
|
+
run: async () => {
|
|
18935
|
+
for (const location of this.resolvedLocations) await adopter.sweep({ locationId: location.id });
|
|
18936
|
+
},
|
|
18937
|
+
logger: this.ctx.logger
|
|
18938
|
+
});
|
|
18939
|
+
}
|
|
18519
18940
|
}
|
|
18520
18941
|
/**
|
|
18521
18942
|
* Print one window of detection-trigger health.
|