@camstack/addon-pipeline 1.2.257 → 1.2.259
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-BTzxxm-z.mjs → default-detection-model-BZ54sUY2.mjs} +1 -1
- package/dist/{default-detection-model-C8CHpxQl.js → default-detection-model-vVWcdv0g.js} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-BMARLLlX.js → dist-CsJJPDQW.js} +154 -1
- package/dist/{dist-xsaGDCOu.mjs → dist-D3w2fwD4.mjs} +154 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-Ca-vE0xX.js → node-1vbEWzPZ.js} +1 -1
- package/dist/{node-Fv4ByT36.mjs → node-IHGNrWH9.mjs} +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-C3Wjmvm0.js → process-memory-DynU4177.js} +1 -1
- package/dist/{process-memory-BgtQADAY.mjs → process-memory-jhOBJEil.mjs} +1 -1
- package/dist/recorder/index.js +293 -19
- package/dist/recorder/index.mjs +293 -19
- package/dist/{restream-intent-D2VT97-U.mjs → restream-intent-CW79dvFV.mjs} +118 -2
- package/dist/{restream-intent-VVJiUAkh.js → restream-intent-iKGCB9IR.js} +135 -1
- package/dist/{segment-demux-js-C7C6c-g2.mjs → segment-demux-js-BZAiu_2h.mjs} +1 -1
- package/dist/{segment-demux-js-DHV7EOQf.js → segment-demux-js-OQT6wqXH.js} +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CJWLoj3Z.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CFcO9OoY.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CTHexfai.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-BzBcvawL.mjs → hostInit-rU23F6nl.mjs} +3 -3
- package/dist/stream-broker/index.js +476 -17
- package/dist/stream-broker/index.mjs +476 -17
- 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_types__loadShare__.js-ReCGmeOG.mjs +0 -26
package/dist/recorder/index.js
CHANGED
|
@@ -3,10 +3,10 @@ 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-CsJJPDQW.js");
|
|
7
|
+
const require_node = require("../node-1vbEWzPZ.js");
|
|
8
8
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
9
|
-
const require_restream_intent = require("../restream-intent-
|
|
9
|
+
const require_restream_intent = require("../restream-intent-iKGCB9IR.js");
|
|
10
10
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
11
11
|
let node_crypto = require("node:crypto");
|
|
12
12
|
let node_fs = require("node:fs");
|
|
@@ -820,14 +820,22 @@ function parseSegmentPath(relPath) {
|
|
|
820
820
|
/** Hour bucket, the granularity the path convention (`.../YYYY/MM/DD/HH/`)
|
|
821
821
|
* already gives us — so "have I walked this?" costs one Set lookup. */
|
|
822
822
|
var HOUR_MS$9 = 36e5;
|
|
823
|
-
/** A device walked end-to-end. Cheaper and clearer than inserting every hour of
|
|
824
|
-
* the retention window into the set. */
|
|
825
|
-
var ALL_HOURS = Symbol("all-hours");
|
|
826
823
|
/** The hour buckets a half-open span touches. */
|
|
827
824
|
function* hourBuckets(fromMs, toMs) {
|
|
828
825
|
const first = Math.floor(fromMs / HOUR_MS$9) * HOUR_MS$9;
|
|
829
826
|
for (let h = first; h < toMs; h += HOUR_MS$9) yield h;
|
|
830
827
|
}
|
|
828
|
+
/** The hour bucket a segment belongs to — the same one its path encodes. */
|
|
829
|
+
function hourOf$1(startMs) {
|
|
830
|
+
return Math.floor(startMs / HOUR_MS$9) * HOUR_MS$9;
|
|
831
|
+
}
|
|
832
|
+
/** The production bound. See {@link RecordingIndexBudget} for the measurement. */
|
|
833
|
+
var DEFAULT_RECORDING_INDEX_BUDGET = {
|
|
834
|
+
residentRowBudget: 25e4,
|
|
835
|
+
residentHourBudget: 2e4,
|
|
836
|
+
evictToFraction: .8,
|
|
837
|
+
pinnedTailHours: 2
|
|
838
|
+
};
|
|
831
839
|
/**
|
|
832
840
|
* Index of the first element whose `startMs` is GREATER than `startMs` (upper
|
|
833
841
|
* bound). Inserting there keeps equal-start rows in arrival order — exactly
|
|
@@ -875,10 +883,35 @@ var RecordingIndex = class {
|
|
|
875
883
|
/** See {@link HydrateReuseStats}. */
|
|
876
884
|
rowsReused = 0;
|
|
877
885
|
rowsAllocated = 0;
|
|
878
|
-
/**
|
|
879
|
-
*
|
|
880
|
-
*
|
|
886
|
+
/**
|
|
887
|
+
* Hour buckets this device has been walked for → when anything last ASKED
|
|
888
|
+
* about that hour ({@link useTick}, 0 = never since the walk). Empty =
|
|
889
|
+
* nothing has been looked at, which is NOT the same as "there is nothing" —
|
|
890
|
+
* see {@link hydrationOf}.
|
|
891
|
+
*
|
|
892
|
+
* The tick is the eviction order, and it lives here rather than in a side
|
|
893
|
+
* map because an evicted hour must lose its marker in the SAME operation
|
|
894
|
+
* that loses its rows: a walked marker outliving its rows would report
|
|
895
|
+
* `hydrated` over an hour the index no longer holds, which is the one
|
|
896
|
+
* failure a forgetting index must not have.
|
|
897
|
+
*
|
|
898
|
+
* There used to be an `ALL_HOURS` sentinel here for a device walked
|
|
899
|
+
* end-to-end. It had no production caller (2026-08-30 census) and a bounded
|
|
900
|
+
* index can never truthfully claim it, so it is gone.
|
|
901
|
+
*/
|
|
881
902
|
walked = /* @__PURE__ */ new Map();
|
|
903
|
+
/** Monotonic "somebody asked" counter. Only ever compared, never read out. */
|
|
904
|
+
useTick = 0;
|
|
905
|
+
/** `null` until {@link armResidency}. An unarmed index never evicts. */
|
|
906
|
+
residencyDeps = null;
|
|
907
|
+
evictedRows = 0;
|
|
908
|
+
evictedHours = 0;
|
|
909
|
+
evictionPasses = 0;
|
|
910
|
+
/** Hours dropped per device, for the life of the process. One number per
|
|
911
|
+
* camera — the input to {@link droppedDuringWalk}, nothing else. */
|
|
912
|
+
evictedHoursByDevice = /* @__PURE__ */ new Map();
|
|
913
|
+
/** {@link evictedHoursByDevice} as it stood when a whole-device walk began. */
|
|
914
|
+
walkBaseline = /* @__PURE__ */ new Map();
|
|
882
915
|
/**
|
|
883
916
|
* Upper bound on the `durMs` of any row the index holds — the ONLY thing that
|
|
884
917
|
* turns "walk the whole sorted view" into "walk backwards a bounded number of
|
|
@@ -1052,7 +1085,9 @@ var RecordingIndex = class {
|
|
|
1052
1085
|
* Deliberately NOT delete-all-then-reinsert: the archive walk re-reports rows
|
|
1053
1086
|
* this index already holds, with freshly built path strings, and dropping the
|
|
1054
1087
|
* held rows first would make those fresh strings the permanent ones — a
|
|
1055
|
-
* second copy of the whole archive's paths. See {@link
|
|
1088
|
+
* second copy of the whole archive's paths. See {@link RecordingIndex.rowFor}
|
|
1089
|
+
* for the measurement (`retainedPathStrings`, which this used to point at,
|
|
1090
|
+
* never existed — a leftover that read as a verification, removed 2026-09-17).
|
|
1056
1091
|
* @residency mutation
|
|
1057
1092
|
*/
|
|
1058
1093
|
hydrateDevice(deviceId, locationId, relPaths) {
|
|
@@ -1066,6 +1101,7 @@ var RecordingIndex = class {
|
|
|
1066
1101
|
}
|
|
1067
1102
|
for (const [path, existing] of m) if (existing.locationId === locationId && !seen.has(path)) m.delete(path);
|
|
1068
1103
|
this.invalidateSorted(deviceId);
|
|
1104
|
+
this.enforceResidency();
|
|
1069
1105
|
}
|
|
1070
1106
|
/**
|
|
1071
1107
|
* Whether the index has WALKED `[fromMs, toMs)` for a device.
|
|
@@ -1079,7 +1115,6 @@ var RecordingIndex = class {
|
|
|
1079
1115
|
hydrationOf(deviceId, fromMs, toMs) {
|
|
1080
1116
|
const w = this.walked.get(deviceId);
|
|
1081
1117
|
if (w === void 0) return "unknown";
|
|
1082
|
-
if (w === ALL_HOURS) return "hydrated";
|
|
1083
1118
|
let seen = 0;
|
|
1084
1119
|
let total = 0;
|
|
1085
1120
|
for (const h of hourBuckets(fromMs, toMs)) {
|
|
@@ -1091,15 +1126,225 @@ var RecordingIndex = class {
|
|
|
1091
1126
|
}
|
|
1092
1127
|
/** Record that `[fromMs, toMs)` has been walked, whatever it contained. @residency mutation */
|
|
1093
1128
|
markHydrated(deviceId, fromMs, toMs) {
|
|
1129
|
+
let w = this.walked.get(deviceId);
|
|
1130
|
+
if (w === void 0) {
|
|
1131
|
+
w = /* @__PURE__ */ new Map();
|
|
1132
|
+
this.walked.set(deviceId, w);
|
|
1133
|
+
}
|
|
1134
|
+
const tick = ++this.useTick;
|
|
1135
|
+
for (const h of hourBuckets(fromMs, toMs)) w.set(h, tick);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Bound this index, and give it somewhere to SAY what it dropped.
|
|
1139
|
+
*
|
|
1140
|
+
* Called once, from the recorder addon's initialize — `ctx.logger` does not
|
|
1141
|
+
* exist when the field is constructed. Until it runs the index never evicts,
|
|
1142
|
+
* which is honest for the pre-init window (it holds nothing) and is the one
|
|
1143
|
+
* state in which {@link ResidencyCensus.budget} is `null`.
|
|
1144
|
+
* @residency mutation
|
|
1145
|
+
*/
|
|
1146
|
+
armResidency(deps) {
|
|
1147
|
+
this.residencyDeps = deps;
|
|
1148
|
+
this.enforceResidency();
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* A whole-device walk is starting: remember how much this index had already
|
|
1152
|
+
* forgotten about that camera, so {@link droppedDuringWalk} can say whether
|
|
1153
|
+
* the walk's result is a COMPLETE view of what it read.
|
|
1154
|
+
* @residency mutation
|
|
1155
|
+
*/
|
|
1156
|
+
beginWalk(deviceId) {
|
|
1157
|
+
this.walkBaseline.set(deviceId, this.evictedHoursByDevice.get(deviceId) ?? 0);
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Did this index drop anything for `deviceId` since {@link beginWalk}?
|
|
1161
|
+
*
|
|
1162
|
+
* The one question a PRUNE has to ask. An hour the index dropped is
|
|
1163
|
+
* indistinguishable, from the outside, from an hour disk no longer has — so
|
|
1164
|
+
* a consumer that deletes durable state on the index's silence must refuse
|
|
1165
|
+
* when the answer is `true`. `false` when nobody called `beginWalk`: an
|
|
1166
|
+
* unarmed index never drops, and a caller that never announced a walk is
|
|
1167
|
+
* asking about one that did not happen.
|
|
1168
|
+
* @residency resident-scoped
|
|
1169
|
+
*/
|
|
1170
|
+
droppedDuringWalk(deviceId) {
|
|
1171
|
+
const baseline = this.walkBaseline.get(deviceId);
|
|
1172
|
+
if (baseline === void 0) return false;
|
|
1173
|
+
return (this.evictedHoursByDevice.get(deviceId) ?? 0) > baseline;
|
|
1174
|
+
}
|
|
1175
|
+
/** What is held, what was let go, and under which bound. @residency resident-scoped */
|
|
1176
|
+
residency() {
|
|
1177
|
+
return {
|
|
1178
|
+
residentRows: this.residentRowCount(),
|
|
1179
|
+
residentHours: this.residentHourCount(),
|
|
1180
|
+
evictedRows: this.evictedRows,
|
|
1181
|
+
evictedHours: this.evictedHours,
|
|
1182
|
+
evictionPasses: this.evictionPasses,
|
|
1183
|
+
budget: this.residencyDeps?.budget ?? null
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
residentRowCount() {
|
|
1187
|
+
let n = 0;
|
|
1188
|
+
for (const m of this.byDevice.values()) n += m.size;
|
|
1189
|
+
return n;
|
|
1190
|
+
}
|
|
1191
|
+
residentHourCount() {
|
|
1192
|
+
let n = 0;
|
|
1193
|
+
for (const w of this.walked.values()) n += w.size;
|
|
1194
|
+
return n;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Note that something ASKED about `[fromMs, toMs)` — the only input to the
|
|
1198
|
+
* eviction order.
|
|
1199
|
+
*
|
|
1200
|
+
* Walks whichever side is smaller: a read whose span is wider than the
|
|
1201
|
+
* device's whole walked map (a month-wide `ranges`, an availability sweep)
|
|
1202
|
+
* would otherwise iterate one hour at a time across the span.
|
|
1203
|
+
*/
|
|
1204
|
+
touch(deviceId, fromMs, toMs) {
|
|
1094
1205
|
const w = this.walked.get(deviceId);
|
|
1095
|
-
if (w ===
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1206
|
+
if (w === void 0 || w.size === 0) return;
|
|
1207
|
+
const tick = ++this.useTick;
|
|
1208
|
+
if (Math.ceil(Math.max(0, toMs - fromMs) / HOUR_MS$9) > w.size) {
|
|
1209
|
+
for (const h of w.keys()) if (h < toMs && h + HOUR_MS$9 > fromMs) w.set(h, tick);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
for (const h of hourBuckets(fromMs, toMs)) if (w.has(h)) w.set(h, tick);
|
|
1099
1213
|
}
|
|
1100
|
-
/**
|
|
1101
|
-
|
|
1102
|
-
|
|
1214
|
+
/**
|
|
1215
|
+
* Drop the least-recently-asked-about hours until the index is back inside
|
|
1216
|
+
* its budget.
|
|
1217
|
+
*
|
|
1218
|
+
* ## What a dropped hour costs, and why that is safe
|
|
1219
|
+
*
|
|
1220
|
+
* The directory is the authority for which segments exist — every row here
|
|
1221
|
+
* is derived from a path the walk read, and `hydrateHour` /
|
|
1222
|
+
* `hydrateWindowFromStorage` rebuild it from one shallow `readdir` (or one
|
|
1223
|
+
* ledger point read). So a drop costs a re-read, never a segment. The
|
|
1224
|
+
* property that makes it costs-a-read rather than loses-a-segment is that
|
|
1225
|
+
* the walked marker goes with the rows: `hydrationOf` then answers `unknown`
|
|
1226
|
+
* and the reader walks. A marker left behind would answer `hydrated` over an
|
|
1227
|
+
* hour holding nothing, which is exactly the silent gap this class spent
|
|
1228
|
+
* D295 learning not to report.
|
|
1229
|
+
*
|
|
1230
|
+
* ## What is never a candidate
|
|
1231
|
+
*
|
|
1232
|
+
* The live tail ({@link RecordingIndexBudget.pinnedTailHours}). Those hours
|
|
1233
|
+
* are write-through from the writer (`addSegment`), not walked, and a
|
|
1234
|
+
* `readdir` of an hour a writer is still filling cannot give back what it
|
|
1235
|
+
* had — D445's lost tail, from the other direction.
|
|
1236
|
+
*/
|
|
1237
|
+
enforceResidency() {
|
|
1238
|
+
const deps = this.residencyDeps;
|
|
1239
|
+
if (deps === null) return;
|
|
1240
|
+
const { budget } = deps;
|
|
1241
|
+
let rows = this.residentRowCount();
|
|
1242
|
+
let hours = this.residentHourCount();
|
|
1243
|
+
if (rows <= budget.residentRowBudget && hours <= budget.residentHourBudget) return;
|
|
1244
|
+
const rowTarget = Math.floor(budget.residentRowBudget * budget.evictToFraction);
|
|
1245
|
+
const hourTarget = Math.floor(budget.residentHourBudget * budget.evictToFraction);
|
|
1246
|
+
const pinnedFrom = hourOf$1(deps.now()) - Math.max(0, budget.pinnedTailHours - 1) * HOUR_MS$9;
|
|
1247
|
+
const counts = this.hourHistogram();
|
|
1248
|
+
const candidates = [];
|
|
1249
|
+
for (const [deviceId, byHour] of counts) {
|
|
1250
|
+
const walkedHours = this.walked.get(deviceId);
|
|
1251
|
+
for (const [hourStartMs, count] of byHour) {
|
|
1252
|
+
if (hourStartMs >= pinnedFrom) continue;
|
|
1253
|
+
candidates.push({
|
|
1254
|
+
deviceId,
|
|
1255
|
+
hourStartMs,
|
|
1256
|
+
rows: count,
|
|
1257
|
+
lastUse: walkedHours?.get(hourStartMs) ?? 0
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
candidates.sort((a, b) => a.lastUse - b.lastUse || a.hourStartMs - b.hourStartMs);
|
|
1262
|
+
const victims = /* @__PURE__ */ new Map();
|
|
1263
|
+
let droppedRows = 0;
|
|
1264
|
+
let droppedHours = 0;
|
|
1265
|
+
for (const c of candidates) {
|
|
1266
|
+
if (rows <= rowTarget && hours <= hourTarget) break;
|
|
1267
|
+
let forDevice = victims.get(c.deviceId);
|
|
1268
|
+
if (!forDevice) {
|
|
1269
|
+
forDevice = /* @__PURE__ */ new Set();
|
|
1270
|
+
victims.set(c.deviceId, forDevice);
|
|
1271
|
+
}
|
|
1272
|
+
forDevice.add(c.hourStartMs);
|
|
1273
|
+
rows -= c.rows;
|
|
1274
|
+
droppedRows += c.rows;
|
|
1275
|
+
if (this.walked.get(c.deviceId)?.has(c.hourStartMs) === true) {
|
|
1276
|
+
hours -= 1;
|
|
1277
|
+
droppedHours += 1;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (victims.size === 0) return;
|
|
1281
|
+
this.evictionPasses += 1;
|
|
1282
|
+
this.evictedRows += droppedRows;
|
|
1283
|
+
this.evictedHours += droppedHours;
|
|
1284
|
+
for (const [deviceId, hoursToDrop] of victims) this.dropHours(deviceId, hoursToDrop, rows, budget, deps.logger);
|
|
1285
|
+
}
|
|
1286
|
+
/** `deviceId → hourStartMs → resident rows`, including hours walked and found
|
|
1287
|
+
* empty (count 0) — those hold no rows and would otherwise never be dropped,
|
|
1288
|
+
* leaving the walked map to grow without bound on its own. */
|
|
1289
|
+
hourHistogram() {
|
|
1290
|
+
const out = /* @__PURE__ */ new Map();
|
|
1291
|
+
for (const [deviceId, m] of this.byDevice) {
|
|
1292
|
+
const byHour = /* @__PURE__ */ new Map();
|
|
1293
|
+
for (const s of m.values()) {
|
|
1294
|
+
const h = hourOf$1(s.startMs);
|
|
1295
|
+
byHour.set(h, (byHour.get(h) ?? 0) + 1);
|
|
1296
|
+
}
|
|
1297
|
+
out.set(deviceId, byHour);
|
|
1298
|
+
}
|
|
1299
|
+
for (const [deviceId, w] of this.walked) {
|
|
1300
|
+
let byHour = out.get(deviceId);
|
|
1301
|
+
if (!byHour) {
|
|
1302
|
+
byHour = /* @__PURE__ */ new Map();
|
|
1303
|
+
out.set(deviceId, byHour);
|
|
1304
|
+
}
|
|
1305
|
+
for (const h of w.keys()) if (!byHour.has(h)) byHour.set(h, 0);
|
|
1306
|
+
}
|
|
1307
|
+
return out;
|
|
1308
|
+
}
|
|
1309
|
+
/** Delete one device's victim hours — rows and walked markers together — and
|
|
1310
|
+
* say so. A structure that silently forgets is how this class of bug hides. */
|
|
1311
|
+
dropHours(deviceId, hoursToDrop, residentRowsAfter, budget, logger) {
|
|
1312
|
+
const m = this.byDevice.get(deviceId);
|
|
1313
|
+
let droppedRows = 0;
|
|
1314
|
+
if (m) {
|
|
1315
|
+
for (const [path, s] of m) {
|
|
1316
|
+
if (!hoursToDrop.has(hourOf$1(s.startMs))) continue;
|
|
1317
|
+
m.delete(path);
|
|
1318
|
+
droppedRows += 1;
|
|
1319
|
+
}
|
|
1320
|
+
if (m.size === 0) this.byDevice.delete(deviceId);
|
|
1321
|
+
}
|
|
1322
|
+
const w = this.walked.get(deviceId);
|
|
1323
|
+
if (w) {
|
|
1324
|
+
for (const h of hoursToDrop) w.delete(h);
|
|
1325
|
+
if (w.size === 0) this.walked.delete(deviceId);
|
|
1326
|
+
}
|
|
1327
|
+
this.invalidateSorted(deviceId);
|
|
1328
|
+
this.evictedHoursByDevice.set(deviceId, (this.evictedHoursByDevice.get(deviceId) ?? 0) + hoursToDrop.size);
|
|
1329
|
+
let oldestHourMs = Infinity;
|
|
1330
|
+
let newestHourMs = -Infinity;
|
|
1331
|
+
for (const h of hoursToDrop) {
|
|
1332
|
+
if (h < oldestHourMs) oldestHourMs = h;
|
|
1333
|
+
if (h > newestHourMs) newestHourMs = h;
|
|
1334
|
+
}
|
|
1335
|
+
logger.info("recorder: index dropped resident hours — over the residency budget", {
|
|
1336
|
+
tags: { deviceId },
|
|
1337
|
+
meta: {
|
|
1338
|
+
hours: hoursToDrop.size,
|
|
1339
|
+
rows: droppedRows,
|
|
1340
|
+
oldestHourMs,
|
|
1341
|
+
newestHourMs,
|
|
1342
|
+
residentRowsAfter,
|
|
1343
|
+
rowBudget: budget.residentRowBudget,
|
|
1344
|
+
hourBudget: budget.residentHourBudget,
|
|
1345
|
+
costOfRecovery: "readdir-per-hour"
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1103
1348
|
}
|
|
1104
1349
|
/**
|
|
1105
1350
|
* Merge one hour's segments for a location, and mark that hour walked.
|
|
@@ -1121,6 +1366,7 @@ var RecordingIndex = class {
|
|
|
1121
1366
|
}
|
|
1122
1367
|
if (changed) this.invalidateSorted(deviceId);
|
|
1123
1368
|
this.markHydrated(deviceId, hourStartMs, hourStartMs + HOUR_MS$9);
|
|
1369
|
+
this.enforceResidency();
|
|
1124
1370
|
}
|
|
1125
1371
|
/**
|
|
1126
1372
|
* Make one hour of ONE location exactly `relPaths` — merge what is there and
|
|
@@ -1151,6 +1397,7 @@ var RecordingIndex = class {
|
|
|
1151
1397
|
for (const path of dropped) m.delete(path);
|
|
1152
1398
|
if (dropped.length > 0) this.invalidateSorted(deviceId);
|
|
1153
1399
|
this.hydrateHour(deviceId, locationId, hourStartMs, relPaths);
|
|
1400
|
+
this.enforceResidency();
|
|
1154
1401
|
return dropped;
|
|
1155
1402
|
}
|
|
1156
1403
|
/**
|
|
@@ -1175,6 +1422,7 @@ var RecordingIndex = class {
|
|
|
1175
1422
|
return;
|
|
1176
1423
|
}
|
|
1177
1424
|
this.insertIntoSortedViews(s);
|
|
1425
|
+
this.enforceResidency();
|
|
1178
1426
|
}
|
|
1179
1427
|
/** Remove segments by relative path, routing each path to the correct device map via its encoded deviceId. @residency mutation */
|
|
1180
1428
|
removeSegments(paths) {
|
|
@@ -1202,6 +1450,7 @@ var RecordingIndex = class {
|
|
|
1202
1450
|
* @residency resident-scoped
|
|
1203
1451
|
*/
|
|
1204
1452
|
segmentsStartingIn(deviceId, profile, fromMs, toMs) {
|
|
1453
|
+
this.touch(deviceId, fromMs, toMs);
|
|
1205
1454
|
const segs = this.sortedSegments(deviceId, profile);
|
|
1206
1455
|
let lo = 0;
|
|
1207
1456
|
let hi = segs.length;
|
|
@@ -1230,6 +1479,7 @@ var RecordingIndex = class {
|
|
|
1230
1479
|
* @residency resident-scoped
|
|
1231
1480
|
*/
|
|
1232
1481
|
segmentStartingAt(deviceId, profile, startMs) {
|
|
1482
|
+
this.touch(deviceId, startMs, startMs + 1);
|
|
1233
1483
|
const segs = this.sortedSegments(deviceId, profile);
|
|
1234
1484
|
const at = segs[lowerBoundByStart(segs, startMs)];
|
|
1235
1485
|
return at !== void 0 && at.startMs === startMs ? at : null;
|
|
@@ -1253,6 +1503,7 @@ var RecordingIndex = class {
|
|
|
1253
1503
|
* @residency resident-scoped
|
|
1254
1504
|
*/
|
|
1255
1505
|
segmentsOverlapping(deviceId, profile, fromMs, toMs) {
|
|
1506
|
+
this.touch(deviceId, fromMs - this.maxDurMs, toMs);
|
|
1256
1507
|
const segs = this.sortedSegments(deviceId, profile);
|
|
1257
1508
|
const end = lowerBoundByStart(segs, toMs);
|
|
1258
1509
|
let head = lowerBoundByStart(segs, fromMs);
|
|
@@ -1414,6 +1665,7 @@ var RecordingIndex = class {
|
|
|
1414
1665
|
}
|
|
1415
1666
|
/** The segment whose [startMs, startMs+durMs) window contains `epochMs`, or null if `epochMs` falls in a gap / there is no footage. @residency resident-scoped */
|
|
1416
1667
|
segmentAt(deviceId, profile, epochMs) {
|
|
1668
|
+
this.touch(deviceId, epochMs - this.maxDurMs, epochMs + 1);
|
|
1417
1669
|
return this.segmentAtIn(this.sortedSegments(deviceId, profile), epochMs);
|
|
1418
1670
|
}
|
|
1419
1671
|
/**
|
|
@@ -1423,6 +1675,7 @@ var RecordingIndex = class {
|
|
|
1423
1675
|
* @residency resident-scoped
|
|
1424
1676
|
*/
|
|
1425
1677
|
nearestCoveredEdge(deviceId, profile, epochMs) {
|
|
1678
|
+
this.touch(deviceId, epochMs - this.maxDurMs, epochMs + 1);
|
|
1426
1679
|
const segs = this.sortedSegments(deviceId, profile);
|
|
1427
1680
|
if (segs.length === 0) return null;
|
|
1428
1681
|
if (this.segmentAtIn(segs, epochMs) !== null) return epochMs;
|
|
@@ -1476,6 +1729,7 @@ var RecordingIndex = class {
|
|
|
1476
1729
|
* @residency resident-scoped
|
|
1477
1730
|
*/
|
|
1478
1731
|
coveredEdgeBefore(deviceId, profile, epochMs) {
|
|
1732
|
+
this.touch(deviceId, epochMs - this.maxDurMs, epochMs + 1);
|
|
1479
1733
|
const segs = this.sortedSegments(deviceId, profile);
|
|
1480
1734
|
let best = null;
|
|
1481
1735
|
for (let i = upperBoundByStart(segs, epochMs) - 1; i >= 0; i--) {
|
|
@@ -1488,6 +1742,7 @@ var RecordingIndex = class {
|
|
|
1488
1742
|
}
|
|
1489
1743
|
/** @residency resident-scoped */
|
|
1490
1744
|
ranges(deviceId, profile, fromMs, toMs, gapToleranceMs) {
|
|
1745
|
+
this.touch(deviceId, fromMs, toMs);
|
|
1491
1746
|
return mergeRanges(this.sortedSegments(deviceId, profile).filter((s) => s.startMs + s.durMs > fromMs && s.startMs < toMs), gapToleranceMs).map((r) => ({
|
|
1492
1747
|
startMs: Math.max(r.startMs, fromMs),
|
|
1493
1748
|
endMs: Math.min(r.endMs, toMs)
|
|
@@ -1722,6 +1977,7 @@ async function resolveRecordingsLocations(api, logger) {
|
|
|
1722
1977
|
async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger, options = {}) {
|
|
1723
1978
|
const yieldBetween = options.yieldBetween ?? macrotask$3;
|
|
1724
1979
|
const chunkSize = options.chunkSize ?? HYDRATE_CHUNK;
|
|
1980
|
+
index.beginWalk(deviceId);
|
|
1725
1981
|
for (const [root, aliases] of aliasesByRoot(locations)) {
|
|
1726
1982
|
const deviceDir = node_path.default.join(root, String(deviceId));
|
|
1727
1983
|
const startedMs = Date.now();
|
|
@@ -10056,10 +10312,16 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
10056
10312
|
else fromIndex.set(key, appendSegmentPath(existing, seg.path));
|
|
10057
10313
|
}
|
|
10058
10314
|
const refused = this.refusedPrunes(walked, fromIndex);
|
|
10315
|
+
const trimmed = /* @__PURE__ */ new Set();
|
|
10316
|
+
for (const deviceId of walked) {
|
|
10317
|
+
if (!index.droppedDuringWalk(deviceId)) continue;
|
|
10318
|
+
trimmed.add(deviceId);
|
|
10319
|
+
this.logger.warn("recorder: ledger prune REFUSED — the RAM index dropped hours during this walk, so its silence is not evidence", { tags: { deviceId } });
|
|
10320
|
+
}
|
|
10059
10321
|
for (const row of this.ledger.snapshot()) {
|
|
10060
10322
|
if (!walked.has(row.deviceId)) continue;
|
|
10061
10323
|
const disk = fromIndex.get(row.key);
|
|
10062
|
-
const additiveOnly = row.hourStartMs === currentHour || refused.devices.has(row.deviceId) || refused.locations.has(locationKey(row.deviceId, row.locationId));
|
|
10324
|
+
const additiveOnly = row.hourStartMs === currentHour || trimmed.has(row.deviceId) || refused.devices.has(row.deviceId) || refused.locations.has(locationKey(row.deviceId, row.locationId));
|
|
10063
10325
|
if (disk === void 0) {
|
|
10064
10326
|
if (additiveOnly) continue;
|
|
10065
10327
|
await this.ledger.forget(row.key);
|
|
@@ -17797,6 +18059,11 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
17797
18059
|
}
|
|
17798
18060
|
async onInitialize() {
|
|
17799
18061
|
this.stopStallMonitor = (0, _camstack_system_addon_utils.startEventLoopStallMonitor)(this.ctx.logger);
|
|
18062
|
+
this.index.armResidency({
|
|
18063
|
+
logger: this.ctx.logger,
|
|
18064
|
+
now: () => Date.now(),
|
|
18065
|
+
budget: DEFAULT_RECORDING_INDEX_BUDGET
|
|
18066
|
+
});
|
|
17800
18067
|
const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
17801
18068
|
this.nodeId = raw.includes("/") ? raw.split("/")[0] : raw;
|
|
17802
18069
|
const configuredNode = this.config.recordingNodeId;
|
|
@@ -18792,9 +19059,16 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
18792
19059
|
const census = this.index.sortedViewCensus();
|
|
18793
19060
|
const views = this.index.sortedViewStats();
|
|
18794
19061
|
const reuse = this.index.hydrateReuseStats();
|
|
19062
|
+
const residency = this.index.residency();
|
|
18795
19063
|
this.ctx.logger.info("recorder: index census", {
|
|
18796
19064
|
...census.largestDeviceId > 0 ? { tags: { deviceId: census.largestDeviceId } } : {},
|
|
18797
19065
|
meta: {
|
|
19066
|
+
residentRows: residency.residentRows,
|
|
19067
|
+
residentHours: residency.residentHours,
|
|
19068
|
+
rowBudget: residency.budget?.residentRowBudget ?? null,
|
|
19069
|
+
evictedRows: residency.evictedRows,
|
|
19070
|
+
evictedHours: residency.evictedHours,
|
|
19071
|
+
evictionPasses: residency.evictionPasses,
|
|
18798
19072
|
sortedViews: census.views,
|
|
18799
19073
|
rowSlots: census.rowSlots,
|
|
18800
19074
|
largestViewRows: census.largestRows,
|