@camstack/system 1.2.452 → 1.2.454
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/{agent-https-CMz4S56_.js → agent-https-B7IjRRzT.js} +2 -2
- package/dist/{agent-https-D6lRxHma.mjs → agent-https-oXsR24R2.mjs} +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alert-episode-policy.d.ts +39 -0
- package/dist/builtins/alerts/alerts.addon.d.ts +25 -38
- package/dist/builtins/alerts/alerts.addon.js +178 -423
- package/dist/builtins/alerts/alerts.addon.mjs +178 -423
- package/dist/builtins/alerts/emit-alert-policy.d.ts +3 -16
- package/dist/builtins/alerts/runner-alert.d.ts +0 -13
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-history/device-history.addon.js +1 -1
- package/dist/builtins/device-history/device-history.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +4 -4
- package/dist/builtins/keychain/index.mjs +4 -4
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +3 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +15 -2
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +15 -2
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/record-entries/index.js +22 -2
- package/dist/builtins/record-entries/index.mjs +22 -2
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/records/records-engine.d.ts +13 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- package/dist/builtins/storage-orchestrator/location-class-paths.d.ts +13 -0
- package/dist/builtins/storage-orchestrator/location-repoint-move.d.ts +6 -1
- package/dist/builtins/storage-orchestrator/location-repointer.d.ts +97 -0
- package/dist/builtins/storage-orchestrator/location-store.d.ts +13 -2
- package/dist/builtins/storage-orchestrator/record-kinds.d.ts +5 -0
- package/dist/builtins/storage-orchestrator/storage-location-band-monitor.d.ts +38 -4
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +8 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +486 -143
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +486 -143
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +26 -1
- package/dist/builtins/system-config/system-config.addon.js +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +3 -3
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-BQmhc3se.js → composition-sources-Cukc78eu.js} +1 -1
- package/dist/{composition-sources-DzuruDVY.mjs → composition-sources-bt6IQDLu.mjs} +1 -1
- package/dist/{constants-BPWA-r-Y.js → constants-DBGG6Zu5.js} +1 -1
- package/dist/{constants-gzGZbnwO.mjs → constants-DQcVrhVI.mjs} +1 -1
- package/dist/{device-ca-75rt3U_U.js → device-ca-B3hUVfYk.js} +1 -1
- package/dist/{device-ca-D05nqNXE.mjs → device-ca-Utfvsx5x.mjs} +1 -1
- package/dist/{dist-DV2ll9Hi.mjs → dist-7ZciHCIp.mjs} +394 -38
- package/dist/{dist-Ct88catN.js → dist-Bi4Ioxop.js} +417 -37
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-DAU1WttG.js → hub-mdns-ciao-2FzLWfbD.js} +2 -2
- package/dist/{hub-mdns-ciao-BPOg2iyc.mjs → hub-mdns-ciao-BRs3DUT-.mjs} +2 -2
- package/dist/index.js +17 -9
- package/dist/index.mjs +17 -9
- package/dist/kernel/cap-router-builder.d.ts +9 -0
- package/dist/{phase6-declare-gate-BJplW6tX.mjs → phase6-declare-gate-D-2LMw9o.mjs} +1 -1
- package/dist/{phase6-declare-gate-C6WqagkC.js → phase6-declare-gate-DwYAep7y.js} +1 -1
- package/dist/{restore-archive-Cprdh3P9.js → restore-archive-C0ymYQk7.js} +1 -1
- package/dist/{restore-archive-Jiz63mVB.mjs → restore-archive-UVZv_hmr.mjs} +1 -1
- package/dist/{wal-maintenance-BspX29jz.js → wal-maintenance-Bo_lo3Dh.js} +0 -0
- package/dist/{wal-maintenance-BNIaf3QP.mjs → wal-maintenance-Df89xxrK.mjs} +0 -0
- package/package.json +1 -1
- package/dist/builtins/alerts/critical-push.d.ts +0 -90
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Bi4Ioxop.js");
|
|
7
7
|
const require_read_all_rows$1 = require("../../read-all-rows-CVoO1ONM.js");
|
|
8
8
|
let zod = require("zod");
|
|
9
9
|
let node_crypto = require("node:crypto");
|
|
@@ -31,6 +31,7 @@ var StorageLocationBodySchema = zod.z.object({
|
|
|
31
31
|
mode: require_dist.StorageLocationModeSchema.optional(),
|
|
32
32
|
isDefault: zod.z.boolean().optional()
|
|
33
33
|
});
|
|
34
|
+
var StorageSeedMarkerBodySchema = zod.z.object({ seededAt: zod.z.number() });
|
|
34
35
|
var StorageDrainStateBodySchema = zod.z.object({
|
|
35
36
|
locationId: zod.z.string(),
|
|
36
37
|
startedAtMs: zod.z.number(),
|
|
@@ -55,6 +56,14 @@ var _storageOrchestratorRecordKinds = require_dist.defineRecordKinds({
|
|
|
55
56
|
retention: { none: "operator delete or declaration prune only" },
|
|
56
57
|
adoptsFrom: STORAGE_LOCATIONS
|
|
57
58
|
},
|
|
59
|
+
"seed-markers": {
|
|
60
|
+
schema: StorageSeedMarkerBodySchema,
|
|
61
|
+
class: "config",
|
|
62
|
+
writeTrigger: "a declared location type was seeded (or found already present on an existing install); the id is the type",
|
|
63
|
+
device: "none",
|
|
64
|
+
at: "none",
|
|
65
|
+
retention: { none: "one row per location type ever seeded — what makes an operator delete stick" }
|
|
66
|
+
},
|
|
58
67
|
"drain-state": {
|
|
59
68
|
schema: StorageDrainStateBodySchema,
|
|
60
69
|
class: "config",
|
|
@@ -188,9 +197,27 @@ var SqliteLocationStore = class {
|
|
|
188
197
|
store;
|
|
189
198
|
declared = false;
|
|
190
199
|
locationsKind;
|
|
191
|
-
constructor(store, locationsKind = null) {
|
|
200
|
+
constructor(store, locationsKind = null, seedMarkersKind = null) {
|
|
192
201
|
this.store = store;
|
|
193
202
|
this.locationsKind = locationsKind;
|
|
203
|
+
this.seedMarkersKind = seedMarkersKind;
|
|
204
|
+
}
|
|
205
|
+
seedMarkersKind;
|
|
206
|
+
static seedMarkersOf(records) {
|
|
207
|
+
return records.kind(storageOrchestratorRecordKinds["seed-markers"]);
|
|
208
|
+
}
|
|
209
|
+
async loadSeededTypes() {
|
|
210
|
+
const kind = this.seedMarkersKind;
|
|
211
|
+
if (kind === null) return [];
|
|
212
|
+
return (await (0, import_read_all_rows.readAllRows)(kind)).map((r) => r.id);
|
|
213
|
+
}
|
|
214
|
+
async markTypeSeeded(type) {
|
|
215
|
+
const kind = this.seedMarkersKind;
|
|
216
|
+
if (kind === null) return;
|
|
217
|
+
await kind.put({
|
|
218
|
+
id: type,
|
|
219
|
+
data: { seededAt: Date.now() }
|
|
220
|
+
});
|
|
194
221
|
}
|
|
195
222
|
static recordStoresOf(records) {
|
|
196
223
|
return records.kind(storageOrchestratorRecordKinds.locations);
|
|
@@ -1232,6 +1259,111 @@ function buildStorageLocationRegistry(perAddon) {
|
|
|
1232
1259
|
};
|
|
1233
1260
|
}
|
|
1234
1261
|
//#endregion
|
|
1262
|
+
//#region src/builtins/storage-orchestrator/location-occupancy.ts
|
|
1263
|
+
/**
|
|
1264
|
+
* Delete-time occupancy of a location's on-disk root.
|
|
1265
|
+
*
|
|
1266
|
+
* A recordings disk may host several classes at one `basePath`
|
|
1267
|
+
* (`<camera>/high|mid`, `<camera>/low`, `<camera>/events`). Occupancy for
|
|
1268
|
+
* a class is therefore the class subtree, not the shared root — otherwise a
|
|
1269
|
+
* drained `eventMedia:default` still looks full because high footage is
|
|
1270
|
+
* writing next to it (observed 2026-09-01).
|
|
1271
|
+
*
|
|
1272
|
+
* The walk is shallow: one `readdir` of the root (one dirent per camera)
|
|
1273
|
+
* plus `opendir`+first-entry of each named subtree. No recursive find of
|
|
1274
|
+
* segments — that is the walk that saturates a USB disk.
|
|
1275
|
+
*/
|
|
1276
|
+
/**
|
|
1277
|
+
* Class → directories under `<root>/<camera>/`. Same map as the discard
|
|
1278
|
+
* wipe (`DISCARD_SUBTREES` in the recorder / pipeline-analytics addons).
|
|
1279
|
+
* Types not listed keep the legacy "any non-dot entry at the root" rule
|
|
1280
|
+
* (backups, addon-data, …).
|
|
1281
|
+
*/
|
|
1282
|
+
var STORAGE_CLASS_SUBTREES = {
|
|
1283
|
+
eventMedia: ["events"],
|
|
1284
|
+
recordingsLow: ["low"],
|
|
1285
|
+
recordings: ["high", "mid"]
|
|
1286
|
+
};
|
|
1287
|
+
function isDotEntry(name) {
|
|
1288
|
+
return name.startsWith(".");
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* True when `dir` exists and contains at least one dirent. Missing → false.
|
|
1292
|
+
* Any other errno throws so the caller can answer `unknown` (D49).
|
|
1293
|
+
*/
|
|
1294
|
+
async function dirHasAnyEntry(dir) {
|
|
1295
|
+
let handle;
|
|
1296
|
+
try {
|
|
1297
|
+
handle = await (0, node_fs_promises.opendir)(dir);
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
if (err.code === "ENOENT") return false;
|
|
1300
|
+
throw err;
|
|
1301
|
+
}
|
|
1302
|
+
try {
|
|
1303
|
+
return await handle.read() !== null;
|
|
1304
|
+
} finally {
|
|
1305
|
+
await handle.close();
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Occupancy of `basePath` for a location of `type`.
|
|
1310
|
+
*
|
|
1311
|
+
* - missing root → `empty` (nothing this delete can strand)
|
|
1312
|
+
* - class-mapped type → occupied iff any `<camera>/<subtree>` has an entry
|
|
1313
|
+
* - other types → occupied iff the root has a non-dot entry
|
|
1314
|
+
*/
|
|
1315
|
+
async function probeLocalDirectoryOccupancy(input) {
|
|
1316
|
+
let names;
|
|
1317
|
+
try {
|
|
1318
|
+
names = await (0, node_fs_promises.readdir)(input.basePath);
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
if (err.code === "ENOENT") return "empty";
|
|
1321
|
+
return "unknown";
|
|
1322
|
+
}
|
|
1323
|
+
const visible = names.filter((name) => !isDotEntry(name));
|
|
1324
|
+
const subtrees = STORAGE_CLASS_SUBTREES[input.type];
|
|
1325
|
+
if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
|
|
1326
|
+
try {
|
|
1327
|
+
for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(node_path.default.join(input.basePath, camera, subtree))) return "occupied";
|
|
1328
|
+
} catch {
|
|
1329
|
+
return "unknown";
|
|
1330
|
+
}
|
|
1331
|
+
return "empty";
|
|
1332
|
+
}
|
|
1333
|
+
//#endregion
|
|
1334
|
+
//#region src/builtins/storage-orchestrator/location-class-paths.ts
|
|
1335
|
+
/**
|
|
1336
|
+
* Which paths under a shared root BELONG to a storage class (D327).
|
|
1337
|
+
*
|
|
1338
|
+
* A recordings disk hosts several classes at one `basePath`:
|
|
1339
|
+
* `<camera>/high|mid` (recordings), `<camera>/low` (recordingsLow),
|
|
1340
|
+
* `<camera>/events` (eventMedia) and `homekit-clips/…` (homekitClips). A
|
|
1341
|
+
* location may touch only its own subtree; the rest of the root is another
|
|
1342
|
+
* location's data. On 2026-10-20 a repoint of `homekitClips:default` walked the
|
|
1343
|
+
* WHOLE shared root and deleted ~8.6k event-media files that were not its own.
|
|
1344
|
+
*
|
|
1345
|
+
* The per-camera map is {@link STORAGE_CLASS_SUBTREES} (the one source, shared
|
|
1346
|
+
* with occupancy scoring). A class whose subtree sits at the ROOT, not under a
|
|
1347
|
+
* camera, is listed here. A type in neither map owns its whole root (backups,
|
|
1348
|
+
* artifacts, …): there is no subtree to scope to.
|
|
1349
|
+
*/
|
|
1350
|
+
/** Class → top-level directories directly under the root (no camera segment). */
|
|
1351
|
+
var STORAGE_CLASS_ROOT_SUBTREES = { homekitClips: ["homekit-clips"] };
|
|
1352
|
+
/** Classify a root-relative path (`/` separators) for a location of `type`. */
|
|
1353
|
+
function classifyPath(type, relativePath) {
|
|
1354
|
+
const rootSubtrees = STORAGE_CLASS_ROOT_SUBTREES[type];
|
|
1355
|
+
const cameraSubtrees = STORAGE_CLASS_SUBTREES[type];
|
|
1356
|
+
if (rootSubtrees === void 0 && cameraSubtrees === void 0) return "own";
|
|
1357
|
+
const segments = relativePath.split("/").filter((segment) => segment.length > 0);
|
|
1358
|
+
if (rootSubtrees !== void 0) {
|
|
1359
|
+
const top = segments[0];
|
|
1360
|
+
return top !== void 0 && rootSubtrees.includes(top) ? "own" : "foreign";
|
|
1361
|
+
}
|
|
1362
|
+
if (segments.length < 2) return "descend";
|
|
1363
|
+
const subtree = segments[1];
|
|
1364
|
+
return subtree !== void 0 && cameraSubtrees?.includes(subtree) === true ? "own" : "foreign";
|
|
1365
|
+
}
|
|
1366
|
+
//#endregion
|
|
1235
1367
|
//#region src/builtins/storage-orchestrator/location-repoint-move.ts
|
|
1236
1368
|
/**
|
|
1237
1369
|
* Walk the source root and collect FILES.
|
|
@@ -1242,24 +1374,28 @@ function buildStorageLocationRegistry(perAddon) {
|
|
|
1242
1374
|
* (an empty directory) is skipped by the copy loop, which is the only place
|
|
1243
1375
|
* that can tell — and skipping it is right, since there is nothing to move.
|
|
1244
1376
|
*/
|
|
1245
|
-
async function collectFiles(io, prefix) {
|
|
1377
|
+
async function collectFiles(io, scope, prefix) {
|
|
1246
1378
|
const entries = await io.list(prefix);
|
|
1247
1379
|
const files = [];
|
|
1248
1380
|
for (const entry of entries) {
|
|
1381
|
+
const verdict = scope(entry);
|
|
1382
|
+
if (verdict === "foreign") continue;
|
|
1249
1383
|
if ((await io.list(entry)).length > 0) {
|
|
1250
|
-
files.push(...await collectFiles(io, entry));
|
|
1384
|
+
files.push(...await collectFiles(io, scope, entry));
|
|
1251
1385
|
continue;
|
|
1252
1386
|
}
|
|
1253
|
-
files.push(entry);
|
|
1387
|
+
if (verdict === "own") files.push(entry);
|
|
1254
1388
|
}
|
|
1255
1389
|
return files;
|
|
1256
1390
|
}
|
|
1391
|
+
/** The whole root — only correct for a class that owns all of it. */
|
|
1392
|
+
var WHOLE_ROOT = () => "own";
|
|
1257
1393
|
/**
|
|
1258
1394
|
* Copy every file from the source root to the destination, verify each, then
|
|
1259
1395
|
* delete the originals. Throws with NOTHING deleted if any copy fails.
|
|
1260
1396
|
*/
|
|
1261
|
-
async function moveLocationTree(io) {
|
|
1262
|
-
const candidates = await collectFiles(io);
|
|
1397
|
+
async function moveLocationTree(io, scope = WHOLE_ROOT) {
|
|
1398
|
+
const candidates = await collectFiles(io, scope);
|
|
1263
1399
|
const copied = [];
|
|
1264
1400
|
let bytes = 0;
|
|
1265
1401
|
for (const relativePath of candidates) {
|
|
@@ -1282,6 +1418,121 @@ async function moveLocationTree(io) {
|
|
|
1282
1418
|
};
|
|
1283
1419
|
}
|
|
1284
1420
|
//#endregion
|
|
1421
|
+
//#region src/builtins/storage-orchestrator/location-repointer.ts
|
|
1422
|
+
var RepointRefusedError = class extends Error {
|
|
1423
|
+
reason;
|
|
1424
|
+
name = "RepointRefusedError";
|
|
1425
|
+
constructor(reason, message) {
|
|
1426
|
+
super(message);
|
|
1427
|
+
this.reason = reason;
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
/** Does `a` equal `b`, or sit inside it, or contain it? Segment-wise, never by raw prefix. */
|
|
1431
|
+
function rootsOverlap(a, b) {
|
|
1432
|
+
if (a === b) return true;
|
|
1433
|
+
const within = (inner, outer) => inner.startsWith(outer.endsWith("/") ? outer : `${outer}/`);
|
|
1434
|
+
return within(a, b) || within(b, a);
|
|
1435
|
+
}
|
|
1436
|
+
function basePathOf$1(config) {
|
|
1437
|
+
const raw = config["basePath"];
|
|
1438
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
1439
|
+
}
|
|
1440
|
+
var LocationRepointer = class {
|
|
1441
|
+
deps;
|
|
1442
|
+
claims = /* @__PURE__ */ new Set();
|
|
1443
|
+
constructor(deps) {
|
|
1444
|
+
this.deps = deps;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Move `stored`'s own subtree to `next`'s root, then run `commit` (the row
|
|
1448
|
+
* save) while the claim is still held. Throws {@link RepointRefusedError}
|
|
1449
|
+
* with NOTHING copied or deleted when a mover is on the root.
|
|
1450
|
+
*/
|
|
1451
|
+
async run(stored, next, commit) {
|
|
1452
|
+
const claim = await this.claim(stored, next);
|
|
1453
|
+
try {
|
|
1454
|
+
await this.refuseWhileMovers(claim, stored);
|
|
1455
|
+
const moved = await moveLocationTree(await this.deps.ioFor(stored, next), (path) => classifyPath(stored.type, path));
|
|
1456
|
+
this.logMoved(stored, next, moved);
|
|
1457
|
+
return await commit();
|
|
1458
|
+
} finally {
|
|
1459
|
+
this.claims.delete(claim);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* May a mover that touches `locationIds` run? Refuses while a repoint holds
|
|
1464
|
+
* an overlapping root. Called by the owning addon AFTER its job is
|
|
1465
|
+
* registered, so the repoint's mover listing and this check cannot both miss.
|
|
1466
|
+
*/
|
|
1467
|
+
async clearance(locationIds) {
|
|
1468
|
+
const keys = await this.keysOf(locationIds);
|
|
1469
|
+
for (const claim of this.claims) if (claim.keys.some((held) => keys.some((key) => rootsOverlap(held, key)))) return {
|
|
1470
|
+
clear: false,
|
|
1471
|
+
reason: "repoint-active",
|
|
1472
|
+
repointLocationId: claim.locationId
|
|
1473
|
+
};
|
|
1474
|
+
return { clear: true };
|
|
1475
|
+
}
|
|
1476
|
+
async claim(stored, next) {
|
|
1477
|
+
const paths = [basePathOf$1(stored.config), basePathOf$1(next.config)].filter((path) => path !== null);
|
|
1478
|
+
const keys = await Promise.all(paths.map((path) => this.deps.resolveKey(path)));
|
|
1479
|
+
for (const other of this.claims) if (other.keys.some((held) => keys.some((key) => rootsOverlap(held, key)))) this.refuse(stored, "repoint-active", `a repoint of "${other.locationId}" holds the same root`);
|
|
1480
|
+
const claim = {
|
|
1481
|
+
locationId: stored.id,
|
|
1482
|
+
keys
|
|
1483
|
+
};
|
|
1484
|
+
this.claims.add(claim);
|
|
1485
|
+
return claim;
|
|
1486
|
+
}
|
|
1487
|
+
async refuseWhileMovers(claim, stored) {
|
|
1488
|
+
let movers;
|
|
1489
|
+
try {
|
|
1490
|
+
movers = await this.deps.listMovers();
|
|
1491
|
+
} catch (err) {
|
|
1492
|
+
this.claims.delete(claim);
|
|
1493
|
+
this.refuse(stored, "movers-unreadable", `the active movers could not be listed (${err instanceof Error ? err.message : String(err)})`);
|
|
1494
|
+
}
|
|
1495
|
+
for (const mover of movers) {
|
|
1496
|
+
if (mover.job.state !== "queued" && mover.job.state !== "running") continue;
|
|
1497
|
+
const touched = await this.keysOf(this.locationsTouchedBy(mover));
|
|
1498
|
+
if (claim.keys.some((held) => touched.some((key) => rootsOverlap(held, key)))) {
|
|
1499
|
+
this.claims.delete(claim);
|
|
1500
|
+
this.refuse(stored, "mover-active", `${mover.lane} mover ${mover.job.jobId} (${mover.job.fromLocationId} -> ${mover.job.toLocationId}) touches the same physical root`, {
|
|
1501
|
+
jobId: mover.job.jobId,
|
|
1502
|
+
lane: mover.lane
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
locationsTouchedBy(mover) {
|
|
1508
|
+
return [mover.job.fromLocationId, mover.job.toLocationId];
|
|
1509
|
+
}
|
|
1510
|
+
/** `*` is the media mover's "from wherever the rows are": every eventMedia location. */
|
|
1511
|
+
async keysOf(locationIds) {
|
|
1512
|
+
const paths = locationIds.flatMap((id) => id === "*" ? this.deps.locationIdsOfType("eventMedia") : [id]).map((id) => this.deps.locationBasePath(id)).filter((path) => path !== null);
|
|
1513
|
+
return Promise.all(paths.map((path) => this.deps.resolveKey(path)));
|
|
1514
|
+
}
|
|
1515
|
+
refuse(stored, reason, detail, meta = {}) {
|
|
1516
|
+
this.deps.logger.warn("storage-orchestrator: REFUSING to re-point a location — nothing was moved", { meta: {
|
|
1517
|
+
id: stored.id,
|
|
1518
|
+
type: stored.type,
|
|
1519
|
+
reason,
|
|
1520
|
+
detail,
|
|
1521
|
+
...meta
|
|
1522
|
+
} });
|
|
1523
|
+
throw new RepointRefusedError(reason, `re-point of "${stored.id}" refused (${reason}): ${detail}`);
|
|
1524
|
+
}
|
|
1525
|
+
logMoved(stored, next, moved) {
|
|
1526
|
+
this.deps.logger.info("storage-orchestrator: singleton location re-pointed — its data was carried over", { meta: {
|
|
1527
|
+
id: stored.id,
|
|
1528
|
+
type: stored.type,
|
|
1529
|
+
from: basePathOf$1(stored.config),
|
|
1530
|
+
to: basePathOf$1(next.config),
|
|
1531
|
+
...moved
|
|
1532
|
+
} });
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
//#endregion
|
|
1285
1536
|
//#region src/builtins/storage-orchestrator/access-guards.ts
|
|
1286
1537
|
/**
|
|
1287
1538
|
* May `storage.resolve` return a path for this location?
|
|
@@ -1781,78 +2032,6 @@ function resolveHubSettingsForward(options) {
|
|
|
1781
2032
|
}, { timeout: HUB_SETTINGS_FORWARD_TIMEOUT_MS }));
|
|
1782
2033
|
}
|
|
1783
2034
|
//#endregion
|
|
1784
|
-
//#region src/builtins/storage-orchestrator/location-occupancy.ts
|
|
1785
|
-
/**
|
|
1786
|
-
* Delete-time occupancy of a location's on-disk root.
|
|
1787
|
-
*
|
|
1788
|
-
* A recordings disk may host several classes at one `basePath`
|
|
1789
|
-
* (`<camera>/high|mid`, `<camera>/low`, `<camera>/events`). Occupancy for
|
|
1790
|
-
* a class is therefore the class subtree, not the shared root — otherwise a
|
|
1791
|
-
* drained `eventMedia:default` still looks full because high footage is
|
|
1792
|
-
* writing next to it (observed 2026-09-01).
|
|
1793
|
-
*
|
|
1794
|
-
* The walk is shallow: one `readdir` of the root (one dirent per camera)
|
|
1795
|
-
* plus `opendir`+first-entry of each named subtree. No recursive find of
|
|
1796
|
-
* segments — that is the walk that saturates a USB disk.
|
|
1797
|
-
*/
|
|
1798
|
-
/**
|
|
1799
|
-
* Class → directories under `<root>/<camera>/`. Same map as the discard
|
|
1800
|
-
* wipe (`DISCARD_SUBTREES` in the recorder / pipeline-analytics addons).
|
|
1801
|
-
* Types not listed keep the legacy "any non-dot entry at the root" rule
|
|
1802
|
-
* (backups, addon-data, …).
|
|
1803
|
-
*/
|
|
1804
|
-
var STORAGE_CLASS_SUBTREES = {
|
|
1805
|
-
eventMedia: ["events"],
|
|
1806
|
-
recordingsLow: ["low"],
|
|
1807
|
-
recordings: ["high", "mid"]
|
|
1808
|
-
};
|
|
1809
|
-
function isDotEntry(name) {
|
|
1810
|
-
return name.startsWith(".");
|
|
1811
|
-
}
|
|
1812
|
-
/**
|
|
1813
|
-
* True when `dir` exists and contains at least one dirent. Missing → false.
|
|
1814
|
-
* Any other errno throws so the caller can answer `unknown` (D49).
|
|
1815
|
-
*/
|
|
1816
|
-
async function dirHasAnyEntry(dir) {
|
|
1817
|
-
let handle;
|
|
1818
|
-
try {
|
|
1819
|
-
handle = await (0, node_fs_promises.opendir)(dir);
|
|
1820
|
-
} catch (err) {
|
|
1821
|
-
if (err.code === "ENOENT") return false;
|
|
1822
|
-
throw err;
|
|
1823
|
-
}
|
|
1824
|
-
try {
|
|
1825
|
-
return await handle.read() !== null;
|
|
1826
|
-
} finally {
|
|
1827
|
-
await handle.close();
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
/**
|
|
1831
|
-
* Occupancy of `basePath` for a location of `type`.
|
|
1832
|
-
*
|
|
1833
|
-
* - missing root → `empty` (nothing this delete can strand)
|
|
1834
|
-
* - class-mapped type → occupied iff any `<camera>/<subtree>` has an entry
|
|
1835
|
-
* - other types → occupied iff the root has a non-dot entry
|
|
1836
|
-
*/
|
|
1837
|
-
async function probeLocalDirectoryOccupancy(input) {
|
|
1838
|
-
let names;
|
|
1839
|
-
try {
|
|
1840
|
-
names = await (0, node_fs_promises.readdir)(input.basePath);
|
|
1841
|
-
} catch (err) {
|
|
1842
|
-
if (err.code === "ENOENT") return "empty";
|
|
1843
|
-
return "unknown";
|
|
1844
|
-
}
|
|
1845
|
-
const visible = names.filter((name) => !isDotEntry(name));
|
|
1846
|
-
const subtrees = STORAGE_CLASS_SUBTREES[input.type];
|
|
1847
|
-
if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
|
|
1848
|
-
try {
|
|
1849
|
-
for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(node_path.default.join(input.basePath, camera, subtree))) return "occupied";
|
|
1850
|
-
} catch {
|
|
1851
|
-
return "unknown";
|
|
1852
|
-
}
|
|
1853
|
-
return "empty";
|
|
1854
|
-
}
|
|
1855
|
-
//#endregion
|
|
1856
2035
|
//#region src/builtins/storage-orchestrator/location-occupancy-slice.ts
|
|
1857
2036
|
/** One minute: the same order as the pressure sweep, and far longer than an
|
|
1858
2037
|
* admin-UI repaint. */
|
|
@@ -2588,6 +2767,44 @@ var StorageOrchestratorService = class {
|
|
|
2588
2767
|
getLocalNodeId() {
|
|
2589
2768
|
return this.localNode;
|
|
2590
2769
|
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Whether a seed must wait for an ANSWERED read of the store (D49). True on a
|
|
2772
|
+
* node whose locations live in a store: seeding before that read lands writes
|
|
2773
|
+
* defaults the store's own rows (not yet adopted, or not yet read) then
|
|
2774
|
+
* contradict. False on a node that has no store by design — it seeds in memory
|
|
2775
|
+
* at every boot.
|
|
2776
|
+
*/
|
|
2777
|
+
storeRequiredForSeed = false;
|
|
2778
|
+
setStoreRequiredForSeed(required) {
|
|
2779
|
+
this.storeRequiredForSeed = required;
|
|
2780
|
+
}
|
|
2781
|
+
/**
|
|
2782
|
+
* The location types a default was ever seeded for — null when the store
|
|
2783
|
+
* cannot remember (then a type with no row is seeded, as before). What makes
|
|
2784
|
+
* an operator's delete of a seeded default STICK: zero rows today is not
|
|
2785
|
+
* "fresh install", it is only fresh when nothing was ever seeded.
|
|
2786
|
+
*/
|
|
2787
|
+
seededTypes = null;
|
|
2788
|
+
/** The store held locations but no seed memory: an install older than the memory. */
|
|
2789
|
+
installPredatesSeedMemory = false;
|
|
2790
|
+
async loadSeedMemory(store, rowCount) {
|
|
2791
|
+
if (!store.loadSeededTypes) return;
|
|
2792
|
+
const types = await store.loadSeededTypes();
|
|
2793
|
+
this.seededTypes = new Set(types);
|
|
2794
|
+
this.installPredatesSeedMemory = types.length === 0 && rowCount > 0;
|
|
2795
|
+
}
|
|
2796
|
+
rememberSeeded(type) {
|
|
2797
|
+
const seeded = this.seededTypes;
|
|
2798
|
+
const store = this.locationStore;
|
|
2799
|
+
if (seeded === null || seeded.has(type) || !store?.markTypeSeeded) return;
|
|
2800
|
+
seeded.add(type);
|
|
2801
|
+
store.markTypeSeeded(type).catch((err) => {
|
|
2802
|
+
this.logger.error("storage-orchestrator: persisting the seed memory failed", { meta: {
|
|
2803
|
+
type,
|
|
2804
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2805
|
+
} });
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2591
2808
|
/** True once a persistence store is wired (constructor or {@link attachStore}). */
|
|
2592
2809
|
hasStore() {
|
|
2593
2810
|
return this.locationStore !== null;
|
|
@@ -2621,6 +2838,7 @@ var StorageOrchestratorService = class {
|
|
|
2621
2838
|
async attachStore(store) {
|
|
2622
2839
|
if (this.locationStore) return;
|
|
2623
2840
|
const rows = await store.loadAll();
|
|
2841
|
+
await this.loadSeedMemory(store, rows.length);
|
|
2624
2842
|
this.locationStore = store;
|
|
2625
2843
|
const dbIds = new Set(rows.map((r) => r.id));
|
|
2626
2844
|
for (const loc of rows) this.locations.set(loc.id, loc);
|
|
@@ -2759,6 +2977,7 @@ var StorageOrchestratorService = class {
|
|
|
2759
2977
|
async initialize() {
|
|
2760
2978
|
if (!this.locationStore) return;
|
|
2761
2979
|
const rows = await this.locationStore.loadAll();
|
|
2980
|
+
await this.loadSeedMemory(this.locationStore, rows.length);
|
|
2762
2981
|
for (const loc of rows) this.locations.set(loc.id, loc);
|
|
2763
2982
|
this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
|
|
2764
2983
|
this.migrateOntoTheWriteSet(rows);
|
|
@@ -3210,14 +3429,35 @@ var StorageOrchestratorService = class {
|
|
|
3210
3429
|
* directory for derivative slots like `recordingsLow` → `recordings`).
|
|
3211
3430
|
*/
|
|
3212
3431
|
seedDefaults(input) {
|
|
3432
|
+
if (this.storeRequiredForSeed && this.locationStore === null) {
|
|
3433
|
+
this.logger.warn("storage-orchestrator: seed deferred — the location store has not answered a read yet (not wired, adoption pending, or the read failed); seeding now could shadow the operator's existing locations");
|
|
3434
|
+
return {
|
|
3435
|
+
added: 0,
|
|
3436
|
+
deferred: "store-not-answered"
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3213
3439
|
let added = 0;
|
|
3440
|
+
let deferredProvider = false;
|
|
3214
3441
|
const declById = new Map(input.declarations.map((d) => [d.id, d]));
|
|
3215
3442
|
const rootFor = (d) => {
|
|
3216
3443
|
const dedicated = d.defaultRoot === "media" ? input.mediaBasePath : d.defaultRoot === "backup" ? input.backupBasePath : void 0;
|
|
3217
3444
|
return node_path.join(dedicated ?? input.basePath, d.id);
|
|
3218
3445
|
};
|
|
3219
3446
|
for (const d of input.declarations) {
|
|
3220
|
-
if (this.hasAnyLocationOfType(d.id))
|
|
3447
|
+
if (this.hasAnyLocationOfType(d.id)) {
|
|
3448
|
+
this.rememberSeeded(d.id);
|
|
3449
|
+
continue;
|
|
3450
|
+
}
|
|
3451
|
+
if (this.seededTypes?.has(d.id)) continue;
|
|
3452
|
+
if (this.installPredatesSeedMemory) {
|
|
3453
|
+
this.logger.info("storage-orchestrator: existing install — not re-creating a default the operator may have removed", { meta: { type: d.id } });
|
|
3454
|
+
this.rememberSeeded(d.id);
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
if (this.nodeLocalResolver !== null && this.nodeLocalResolver(input.providerId) === void 0) {
|
|
3458
|
+
deferredProvider = true;
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3221
3461
|
const base = d.defaultsTo ? this.getLocationById(`${d.defaultsTo}:default`)?.config["basePath"] ?? rootFor(declById.get(d.defaultsTo) ?? d) : rootFor(d);
|
|
3222
3462
|
this.upsertLocation({
|
|
3223
3463
|
id: `${d.id}:default`,
|
|
@@ -3228,12 +3468,18 @@ var StorageOrchestratorService = class {
|
|
|
3228
3468
|
enabled: true,
|
|
3229
3469
|
isSystem: false
|
|
3230
3470
|
});
|
|
3471
|
+
this.rememberSeeded(d.id);
|
|
3231
3472
|
added++;
|
|
3232
3473
|
}
|
|
3233
3474
|
if (added > 0) this.logger.info("storage-orchestrator: seeded default locations", { meta: {
|
|
3234
3475
|
added,
|
|
3235
3476
|
basePath: input.basePath
|
|
3236
3477
|
} });
|
|
3478
|
+
if (deferredProvider) this.logger.info("storage-orchestrator: seed deferred for some types — provider not classified yet", { meta: { providerId: input.providerId } });
|
|
3479
|
+
return deferredProvider ? {
|
|
3480
|
+
added,
|
|
3481
|
+
deferred: "provider-unclassified"
|
|
3482
|
+
} : { added };
|
|
3237
3483
|
}
|
|
3238
3484
|
/**
|
|
3239
3485
|
* The same-type physical-root refusal (D389).
|
|
@@ -3608,29 +3854,72 @@ var DEFAULT_STORAGE_LOCATION_BAND_CONFIG = {
|
|
|
3608
3854
|
lowPercent: 15,
|
|
3609
3855
|
criticalPercent: 5
|
|
3610
3856
|
};
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3857
|
+
/** Margin, in percentage points, between the enter and exit of an unmanaged edge. */
|
|
3858
|
+
var UNMANAGED_HYSTERESIS_POINTS = 2;
|
|
3859
|
+
/** Managed: fractions of the floor. */
|
|
3860
|
+
var MANAGED_LOW_ENTER = .8;
|
|
3861
|
+
var MANAGED_LOW_EXIT = .9;
|
|
3862
|
+
var MANAGED_CRITICAL_ENTER = .5;
|
|
3863
|
+
var MANAGED_CRITICAL_EXIT = .6;
|
|
3864
|
+
/** How long the evictor may sit below the low margin before it counts as behind. */
|
|
3865
|
+
var MANAGED_LOW_GRACE_MS = 10 * 6e4;
|
|
3866
|
+
function boundsOf(floorPercent, config) {
|
|
3867
|
+
if (floorPercent === void 0) return {
|
|
3868
|
+
lowEnter: config.lowPercent,
|
|
3869
|
+
lowExit: config.lowPercent + UNMANAGED_HYSTERESIS_POINTS,
|
|
3870
|
+
criticalEnter: config.criticalPercent,
|
|
3871
|
+
criticalExit: config.criticalPercent + UNMANAGED_HYSTERESIS_POINTS,
|
|
3872
|
+
lowGraceMs: 0,
|
|
3873
|
+
lowReason: "free-below-warn",
|
|
3874
|
+
criticalReason: "free-below-critical"
|
|
3623
3875
|
};
|
|
3624
3876
|
return {
|
|
3625
|
-
|
|
3626
|
-
|
|
3877
|
+
lowEnter: floorPercent * MANAGED_LOW_ENTER,
|
|
3878
|
+
lowExit: floorPercent * MANAGED_LOW_EXIT,
|
|
3879
|
+
criticalEnter: floorPercent * MANAGED_CRITICAL_ENTER,
|
|
3880
|
+
criticalExit: floorPercent * MANAGED_CRITICAL_EXIT,
|
|
3881
|
+
lowGraceMs: MANAGED_LOW_GRACE_MS,
|
|
3882
|
+
lowReason: "free-below-floor",
|
|
3883
|
+
criticalReason: "free-below-hard-bound"
|
|
3627
3884
|
};
|
|
3628
3885
|
}
|
|
3629
3886
|
var StorageLocationBandMonitor = class {
|
|
3630
3887
|
config;
|
|
3888
|
+
now;
|
|
3631
3889
|
band = /* @__PURE__ */ new Map();
|
|
3632
|
-
|
|
3890
|
+
/** When free first went under `lowEnter` and has stayed there. */
|
|
3891
|
+
belowLowSince = /* @__PURE__ */ new Map();
|
|
3892
|
+
constructor(config = DEFAULT_STORAGE_LOCATION_BAND_CONFIG, now = Date.now) {
|
|
3633
3893
|
this.config = config;
|
|
3894
|
+
this.now = now;
|
|
3895
|
+
}
|
|
3896
|
+
nextBand(locationId, previous, free, b) {
|
|
3897
|
+
if (free < b.criticalEnter || previous === "critical" && free < b.criticalExit) return {
|
|
3898
|
+
band: "critical",
|
|
3899
|
+
reason: b.criticalReason
|
|
3900
|
+
};
|
|
3901
|
+
const lowActive = previous === "low" || previous === "critical";
|
|
3902
|
+
if (free < b.lowEnter || lowActive && free < b.lowExit) {
|
|
3903
|
+
if (lowActive) return {
|
|
3904
|
+
band: "low",
|
|
3905
|
+
reason: b.lowReason
|
|
3906
|
+
};
|
|
3907
|
+
const since = this.belowLowSince.get(locationId) ?? this.now();
|
|
3908
|
+
this.belowLowSince.set(locationId, since);
|
|
3909
|
+
if (this.now() - since >= b.lowGraceMs) return {
|
|
3910
|
+
band: "low",
|
|
3911
|
+
reason: b.lowReason
|
|
3912
|
+
};
|
|
3913
|
+
return {
|
|
3914
|
+
band: previous ?? "ok",
|
|
3915
|
+
reason: "free-ok"
|
|
3916
|
+
};
|
|
3917
|
+
}
|
|
3918
|
+
this.belowLowSince.delete(locationId);
|
|
3919
|
+
return {
|
|
3920
|
+
band: "ok",
|
|
3921
|
+
reason: "free-ok"
|
|
3922
|
+
};
|
|
3634
3923
|
}
|
|
3635
3924
|
/**
|
|
3636
3925
|
* Observe one sweep of managed locations. Locations missing from `samples`
|
|
@@ -3641,8 +3930,9 @@ var StorageLocationBandMonitor = class {
|
|
|
3641
3930
|
const out = [];
|
|
3642
3931
|
for (const sample of samples) {
|
|
3643
3932
|
seen.add(sample.locationId);
|
|
3644
|
-
|
|
3933
|
+
if (sample.freePercent === null) continue;
|
|
3645
3934
|
const previous = this.band.get(sample.locationId) ?? null;
|
|
3935
|
+
const { band, reason } = this.nextBand(sample.locationId, previous, sample.freePercent, boundsOf(sample.floorPercent, this.config));
|
|
3646
3936
|
if (previous === band) continue;
|
|
3647
3937
|
this.band.set(sample.locationId, band);
|
|
3648
3938
|
if (previous === null && band === "ok") continue;
|
|
@@ -3652,11 +3942,17 @@ var StorageLocationBandMonitor = class {
|
|
|
3652
3942
|
...sample.locationName !== void 0 ? { locationName: sample.locationName } : {},
|
|
3653
3943
|
band,
|
|
3654
3944
|
freePercent: sample.freePercent,
|
|
3945
|
+
...sample.floorPercent !== void 0 ? { floorPercent: sample.floorPercent } : {},
|
|
3946
|
+
...sample.freeBytes !== void 0 ? { freeBytes: sample.freeBytes } : {},
|
|
3947
|
+
...sample.floorBytes !== void 0 ? { floorBytes: sample.floorBytes } : {},
|
|
3655
3948
|
reason,
|
|
3656
3949
|
previous
|
|
3657
3950
|
});
|
|
3658
3951
|
}
|
|
3659
|
-
for (const id of [...this.band.keys()]) if (!seen.has(id))
|
|
3952
|
+
for (const id of [...this.band.keys()]) if (!seen.has(id)) {
|
|
3953
|
+
this.band.delete(id);
|
|
3954
|
+
this.belowLowSince.delete(id);
|
|
3955
|
+
}
|
|
3660
3956
|
return out;
|
|
3661
3957
|
}
|
|
3662
3958
|
};
|
|
@@ -3704,6 +4000,7 @@ var PRESSURE_SWEEP_INTERVAL_MS = 6e4;
|
|
|
3704
4000
|
var CAP_PROVIDER_REGISTERED_CATEGORY = "capability:provider-registered";
|
|
3705
4001
|
var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
3706
4002
|
locationsKind = null;
|
|
4003
|
+
seedMarkersKind = null;
|
|
3707
4004
|
drainStateKind = null;
|
|
3708
4005
|
service = null;
|
|
3709
4006
|
pressureManager = null;
|
|
@@ -3729,6 +4026,8 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
3729
4026
|
pressureTimer = null;
|
|
3730
4027
|
pressureSweepInFlight = false;
|
|
3731
4028
|
migration = null;
|
|
4029
|
+
/** Guards every single-location repoint and arbitrates movers per physical root. */
|
|
4030
|
+
repointer = null;
|
|
3732
4031
|
/**
|
|
3733
4032
|
* Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
|
|
3734
4033
|
* synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
|
|
@@ -3765,6 +4064,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
3765
4064
|
if (records !== void 0) {
|
|
3766
4065
|
await records.declare(storageOrchestratorRecordKinds);
|
|
3767
4066
|
this.locationsKind = SqliteLocationStore.recordStoresOf(records);
|
|
4067
|
+
this.seedMarkersKind = SqliteLocationStore.seedMarkersOf(records);
|
|
3768
4068
|
this.drainStateKind = SqliteDrainStateStore.recordStoresOf(records);
|
|
3769
4069
|
}
|
|
3770
4070
|
const getProviders = () => {
|
|
@@ -3784,6 +4084,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
3784
4084
|
return proxy;
|
|
3785
4085
|
});
|
|
3786
4086
|
this.service = service;
|
|
4087
|
+
service.setStoreRequiredForSeed(this.locationsKind !== null);
|
|
3787
4088
|
service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
|
|
3788
4089
|
service.setOccupancyProbe((location) => this.locationOccupancy(location));
|
|
3789
4090
|
await service.initialize();
|
|
@@ -3807,8 +4108,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
3807
4108
|
...input,
|
|
3808
4109
|
config: keychained
|
|
3809
4110
|
};
|
|
3810
|
-
await this.
|
|
3811
|
-
const saved = service.upsertLocation(next);
|
|
4111
|
+
const saved = await this.upsertCarryingData(stored, next, () => service.upsertLocation(next));
|
|
3812
4112
|
this.announceLocationsChanged(saved.id, saved.type, "upsert");
|
|
3813
4113
|
return this.redacted(saved);
|
|
3814
4114
|
},
|
|
@@ -4018,6 +4318,49 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4018
4318
|
deviceKeyOf: (locationId) => this.locationDeviceKey(locationId)
|
|
4019
4319
|
});
|
|
4020
4320
|
this.migration = migration;
|
|
4321
|
+
this.repointer = new LocationRepointer({
|
|
4322
|
+
logger: this.ctx.logger,
|
|
4323
|
+
resolveKey: async (basePath) => {
|
|
4324
|
+
try {
|
|
4325
|
+
return await node_fs_promises.realpath(basePath);
|
|
4326
|
+
} catch {
|
|
4327
|
+
return node_path.resolve(basePath);
|
|
4328
|
+
}
|
|
4329
|
+
},
|
|
4330
|
+
locationBasePath: (id) => this.locationBasePath(id),
|
|
4331
|
+
locationIdsOfType: (type) => service.listLocations({ type }).map((loc) => loc.id),
|
|
4332
|
+
listMovers: () => migration.movers(),
|
|
4333
|
+
ioFor: async (stored, next) => {
|
|
4334
|
+
const target = {
|
|
4335
|
+
...stored,
|
|
4336
|
+
config: next.config
|
|
4337
|
+
};
|
|
4338
|
+
const [source, destination] = await Promise.all([service.getProviderFor(stored), service.getProviderFor(target)]);
|
|
4339
|
+
return {
|
|
4340
|
+
list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
|
|
4341
|
+
location: stored,
|
|
4342
|
+
prefix
|
|
4343
|
+
}),
|
|
4344
|
+
read: (relativePath) => source.read({
|
|
4345
|
+
location: stored,
|
|
4346
|
+
relativePath
|
|
4347
|
+
}),
|
|
4348
|
+
write: (relativePath, data) => destination.write({
|
|
4349
|
+
location: target,
|
|
4350
|
+
relativePath,
|
|
4351
|
+
data
|
|
4352
|
+
}),
|
|
4353
|
+
readBack: (relativePath) => destination.read({
|
|
4354
|
+
location: target,
|
|
4355
|
+
relativePath
|
|
4356
|
+
}),
|
|
4357
|
+
deleteSource: (relativePath) => source.delete({
|
|
4358
|
+
location: stored,
|
|
4359
|
+
relativePath
|
|
4360
|
+
})
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
});
|
|
4021
4364
|
const cleanup = new StorageCleanupCoordinator({
|
|
4022
4365
|
locations: service,
|
|
4023
4366
|
state: this.state("storage-cleanup", require_dist.StorageCleanupJobSchema.nullable(), null),
|
|
@@ -4053,6 +4396,13 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4053
4396
|
status: ({ jobId }) => migration.status(jobId),
|
|
4054
4397
|
cancel: async ({ jobId }) => ({ cancelled: await migration.cancel(jobId) }),
|
|
4055
4398
|
movers: () => migration.movers(),
|
|
4399
|
+
moverClearance: async ({ locationIds }) => {
|
|
4400
|
+
const verdict = await this.requireRepointer().clearance(locationIds);
|
|
4401
|
+
return verdict.clear ? { clear: true } : {
|
|
4402
|
+
clear: false,
|
|
4403
|
+
repointLocationId: verdict.repointLocationId
|
|
4404
|
+
};
|
|
4405
|
+
},
|
|
4056
4406
|
residue: () => migration.residue(),
|
|
4057
4407
|
drain: async (input) => ({ jobId: await migration.drain(input) }),
|
|
4058
4408
|
cleanupStart: async (input) => ({ jobId: await cleanup.start(input) }),
|
|
@@ -4179,7 +4529,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4179
4529
|
else this.ctx.logger.info("storage-orchestrator: this node boots no data-store engine — storage locations are seeded from addon declarations at every boot, and node-pinned locations live on the hub");
|
|
4180
4530
|
return null;
|
|
4181
4531
|
}
|
|
4182
|
-
return new SqliteLocationStore(engine, this.locationsKind);
|
|
4532
|
+
return new SqliteLocationStore(engine, this.locationsKind, this.seedMarkersKind);
|
|
4183
4533
|
}
|
|
4184
4534
|
/**
|
|
4185
4535
|
* Late-wire the persistence store once a `data-store-provider` engine
|
|
@@ -4191,12 +4541,26 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4191
4541
|
* restart — the service never hydrates from / persists to SQLite.
|
|
4192
4542
|
*/
|
|
4193
4543
|
storeWireInFlight = null;
|
|
4544
|
+
/** True while the locations kind is empty and its legacy table still holds rows. */
|
|
4545
|
+
async adoptionIsPending() {
|
|
4546
|
+
const records = this.ctx.records;
|
|
4547
|
+
if (records === void 0 || this.locationsKind === null) return false;
|
|
4548
|
+
try {
|
|
4549
|
+
await records.declare(storageOrchestratorRecordKinds);
|
|
4550
|
+
return false;
|
|
4551
|
+
} catch (err) {
|
|
4552
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4553
|
+
this.ctx.logger.warn("storage-orchestrator: persistence NOT wired — the storage-locations kind is not authoritative yet (adoption pending or the check failed); seeding is deferred", { meta: { error: message } });
|
|
4554
|
+
return true;
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4194
4557
|
async ensureStoreWired() {
|
|
4195
4558
|
const service = this.service;
|
|
4196
4559
|
if (!service || service.hasStore()) return;
|
|
4197
4560
|
if (this.storeWireInFlight) return this.storeWireInFlight;
|
|
4198
4561
|
const store = this.resolveLocationStore(true);
|
|
4199
4562
|
if (!store) return;
|
|
4563
|
+
if (await this.adoptionIsPending()) return;
|
|
4200
4564
|
this.ctx.logger.info("storage-orchestrator: data-store engine now available — wiring persistence");
|
|
4201
4565
|
this.storeWireInFlight = service.attachStore(store).then(() => this.seedFromDeclarations()).catch((err) => {
|
|
4202
4566
|
this.ctx.logger.error("storage-orchestrator: attachStore failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -4225,7 +4589,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4225
4589
|
}
|
|
4226
4590
|
/**
|
|
4227
4591
|
* Sample free space for every managed location and emit NC band edges.
|
|
4228
|
-
* A failed capacity read stays `null`
|
|
4592
|
+
* A failed capacity read stays `null` and changes no band — never folded to 0 (D393).
|
|
4229
4593
|
*/
|
|
4230
4594
|
async emitLocationBandEvents() {
|
|
4231
4595
|
const svc = this.service;
|
|
@@ -4241,11 +4605,18 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4241
4605
|
};
|
|
4242
4606
|
const capacity = await this.volumeCapacityOf(locationId).catch(() => null);
|
|
4243
4607
|
const freePercent = capacity === null ? null : freePercentOf(capacity);
|
|
4608
|
+
if (capacity === null) ctx.logger.debug("storage: location capacity unreadable — band unchanged", { meta: { locationId } });
|
|
4609
|
+
const floorPercent = thresholds.get(locationId);
|
|
4244
4610
|
return {
|
|
4245
4611
|
locationId,
|
|
4246
4612
|
nodeId: loc.nodeId,
|
|
4247
4613
|
locationName: loc.displayName,
|
|
4248
|
-
freePercent
|
|
4614
|
+
freePercent,
|
|
4615
|
+
...floorPercent !== void 0 ? { floorPercent } : {},
|
|
4616
|
+
...capacity !== null ? {
|
|
4617
|
+
freeBytes: capacity.availableBytes,
|
|
4618
|
+
...floorPercent !== void 0 ? { floorBytes: Math.round(capacity.totalBytes * floorPercent / 100) } : {}
|
|
4619
|
+
} : {}
|
|
4249
4620
|
};
|
|
4250
4621
|
}));
|
|
4251
4622
|
for (const edge of this.locationBandMonitor.observe(samples)) {
|
|
@@ -4254,6 +4625,9 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4254
4625
|
locationId: edge.locationId,
|
|
4255
4626
|
band: edge.band,
|
|
4256
4627
|
freePercent: edge.freePercent,
|
|
4628
|
+
floorPercent: edge.floorPercent ?? null,
|
|
4629
|
+
freeBytes: edge.freeBytes ?? null,
|
|
4630
|
+
floorBytes: edge.floorBytes ?? null,
|
|
4257
4631
|
reason: edge.reason
|
|
4258
4632
|
} });
|
|
4259
4633
|
ctx.eventBus.emit({
|
|
@@ -4367,50 +4741,19 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4367
4741
|
* A no-op for everything else — a rename, a mode change, a `multi` kind, or a
|
|
4368
4742
|
* location being created for the first time.
|
|
4369
4743
|
*/
|
|
4370
|
-
async
|
|
4371
|
-
if (stored === void 0) return;
|
|
4744
|
+
async upsertCarryingData(stored, next, commit) {
|
|
4745
|
+
if (stored === void 0) return commit();
|
|
4372
4746
|
const service = this.service;
|
|
4373
|
-
if (service === null) return;
|
|
4374
|
-
if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return;
|
|
4747
|
+
if (service === null) return commit();
|
|
4748
|
+
if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return commit();
|
|
4375
4749
|
const from = stored.config["basePath"];
|
|
4376
4750
|
const to = next.config["basePath"];
|
|
4377
|
-
if (typeof from !== "string" || typeof to !== "string" || from === to) return;
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
const destination = await service.getProviderFor(target);
|
|
4384
|
-
const moved = await moveLocationTree({
|
|
4385
|
-
list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
|
|
4386
|
-
location: stored,
|
|
4387
|
-
prefix
|
|
4388
|
-
}),
|
|
4389
|
-
read: (relativePath) => source.read({
|
|
4390
|
-
location: stored,
|
|
4391
|
-
relativePath
|
|
4392
|
-
}),
|
|
4393
|
-
write: (relativePath, data) => destination.write({
|
|
4394
|
-
location: target,
|
|
4395
|
-
relativePath,
|
|
4396
|
-
data
|
|
4397
|
-
}),
|
|
4398
|
-
readBack: (relativePath) => destination.read({
|
|
4399
|
-
location: target,
|
|
4400
|
-
relativePath
|
|
4401
|
-
}),
|
|
4402
|
-
deleteSource: (relativePath) => source.delete({
|
|
4403
|
-
location: stored,
|
|
4404
|
-
relativePath
|
|
4405
|
-
})
|
|
4406
|
-
});
|
|
4407
|
-
this.ctx.logger.info("storage-orchestrator: singleton location re-pointed — its data was carried over", { meta: {
|
|
4408
|
-
id: stored.id,
|
|
4409
|
-
type: stored.type,
|
|
4410
|
-
from,
|
|
4411
|
-
to,
|
|
4412
|
-
...moved
|
|
4413
|
-
} });
|
|
4751
|
+
if (typeof from !== "string" || typeof to !== "string" || from === to) return commit();
|
|
4752
|
+
return this.requireRepointer().run(stored, next, commit);
|
|
4753
|
+
}
|
|
4754
|
+
requireRepointer() {
|
|
4755
|
+
if (this.repointer === null) throw new Error("storage-orchestrator: not initialized");
|
|
4756
|
+
return this.repointer;
|
|
4414
4757
|
}
|
|
4415
4758
|
secretKeysFor(providerId, config) {
|
|
4416
4759
|
return secretKeysForConfig(config, this.declaredSecretKeys(providerId));
|