@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ii as defineRecordKinds, In as modeMayWrite, Ki as isSecretMarker, Kr as BaseAddon, Mn as mayReadLocation, Nn as mayWriteToLocation, Oi as collectSecretConfigKeys, Qi as normalizeNodeId, Tn as legacyModeOf, W as LegacyStorageLocationDefaultSchema, _r as storageMigrationCapability, at as StorageLocationModeSchema, ca as resolveSecretMarkers, ea as parseJsonObject, gr as storageCapability, ha as transformSecretWrite, it as StorageCleanupJobSchema, kr as withLocationMode, li as REDACTED_SECRET, nn as evictionPolicyOfLocation, ot as StorageLocationTypeSchema, rr as resolveLocationMode, st as StorageMigrationJobSchema, ua as secretTreeFromPaths, un as gbToBytes, ur as settingsStoreCapability, va as EventCategory, vn as isLocationEnabled } from "../../dist-7ZciHCIp.mjs";
|
|
2
2
|
import { t as require_read_all_rows } from "../../read-all-rows-TaPqdWNF.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -26,6 +26,7 @@ var StorageLocationBodySchema = z.object({
|
|
|
26
26
|
mode: StorageLocationModeSchema.optional(),
|
|
27
27
|
isDefault: z.boolean().optional()
|
|
28
28
|
});
|
|
29
|
+
var StorageSeedMarkerBodySchema = z.object({ seededAt: z.number() });
|
|
29
30
|
var StorageDrainStateBodySchema = z.object({
|
|
30
31
|
locationId: z.string(),
|
|
31
32
|
startedAtMs: z.number(),
|
|
@@ -50,6 +51,14 @@ var _storageOrchestratorRecordKinds = defineRecordKinds({
|
|
|
50
51
|
retention: { none: "operator delete or declaration prune only" },
|
|
51
52
|
adoptsFrom: STORAGE_LOCATIONS
|
|
52
53
|
},
|
|
54
|
+
"seed-markers": {
|
|
55
|
+
schema: StorageSeedMarkerBodySchema,
|
|
56
|
+
class: "config",
|
|
57
|
+
writeTrigger: "a declared location type was seeded (or found already present on an existing install); the id is the type",
|
|
58
|
+
device: "none",
|
|
59
|
+
at: "none",
|
|
60
|
+
retention: { none: "one row per location type ever seeded — what makes an operator delete stick" }
|
|
61
|
+
},
|
|
53
62
|
"drain-state": {
|
|
54
63
|
schema: StorageDrainStateBodySchema,
|
|
55
64
|
class: "config",
|
|
@@ -183,9 +192,27 @@ var SqliteLocationStore = class {
|
|
|
183
192
|
store;
|
|
184
193
|
declared = false;
|
|
185
194
|
locationsKind;
|
|
186
|
-
constructor(store, locationsKind = null) {
|
|
195
|
+
constructor(store, locationsKind = null, seedMarkersKind = null) {
|
|
187
196
|
this.store = store;
|
|
188
197
|
this.locationsKind = locationsKind;
|
|
198
|
+
this.seedMarkersKind = seedMarkersKind;
|
|
199
|
+
}
|
|
200
|
+
seedMarkersKind;
|
|
201
|
+
static seedMarkersOf(records) {
|
|
202
|
+
return records.kind(storageOrchestratorRecordKinds["seed-markers"]);
|
|
203
|
+
}
|
|
204
|
+
async loadSeededTypes() {
|
|
205
|
+
const kind = this.seedMarkersKind;
|
|
206
|
+
if (kind === null) return [];
|
|
207
|
+
return (await (0, import_read_all_rows.readAllRows)(kind)).map((r) => r.id);
|
|
208
|
+
}
|
|
209
|
+
async markTypeSeeded(type) {
|
|
210
|
+
const kind = this.seedMarkersKind;
|
|
211
|
+
if (kind === null) return;
|
|
212
|
+
await kind.put({
|
|
213
|
+
id: type,
|
|
214
|
+
data: { seededAt: Date.now() }
|
|
215
|
+
});
|
|
189
216
|
}
|
|
190
217
|
static recordStoresOf(records) {
|
|
191
218
|
return records.kind(storageOrchestratorRecordKinds.locations);
|
|
@@ -1227,6 +1254,111 @@ function buildStorageLocationRegistry(perAddon) {
|
|
|
1227
1254
|
};
|
|
1228
1255
|
}
|
|
1229
1256
|
//#endregion
|
|
1257
|
+
//#region src/builtins/storage-orchestrator/location-occupancy.ts
|
|
1258
|
+
/**
|
|
1259
|
+
* Delete-time occupancy of a location's on-disk root.
|
|
1260
|
+
*
|
|
1261
|
+
* A recordings disk may host several classes at one `basePath`
|
|
1262
|
+
* (`<camera>/high|mid`, `<camera>/low`, `<camera>/events`). Occupancy for
|
|
1263
|
+
* a class is therefore the class subtree, not the shared root — otherwise a
|
|
1264
|
+
* drained `eventMedia:default` still looks full because high footage is
|
|
1265
|
+
* writing next to it (observed 2026-09-01).
|
|
1266
|
+
*
|
|
1267
|
+
* The walk is shallow: one `readdir` of the root (one dirent per camera)
|
|
1268
|
+
* plus `opendir`+first-entry of each named subtree. No recursive find of
|
|
1269
|
+
* segments — that is the walk that saturates a USB disk.
|
|
1270
|
+
*/
|
|
1271
|
+
/**
|
|
1272
|
+
* Class → directories under `<root>/<camera>/`. Same map as the discard
|
|
1273
|
+
* wipe (`DISCARD_SUBTREES` in the recorder / pipeline-analytics addons).
|
|
1274
|
+
* Types not listed keep the legacy "any non-dot entry at the root" rule
|
|
1275
|
+
* (backups, addon-data, …).
|
|
1276
|
+
*/
|
|
1277
|
+
var STORAGE_CLASS_SUBTREES = {
|
|
1278
|
+
eventMedia: ["events"],
|
|
1279
|
+
recordingsLow: ["low"],
|
|
1280
|
+
recordings: ["high", "mid"]
|
|
1281
|
+
};
|
|
1282
|
+
function isDotEntry(name) {
|
|
1283
|
+
return name.startsWith(".");
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* True when `dir` exists and contains at least one dirent. Missing → false.
|
|
1287
|
+
* Any other errno throws so the caller can answer `unknown` (D49).
|
|
1288
|
+
*/
|
|
1289
|
+
async function dirHasAnyEntry(dir) {
|
|
1290
|
+
let handle;
|
|
1291
|
+
try {
|
|
1292
|
+
handle = await opendir(dir);
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
if (err.code === "ENOENT") return false;
|
|
1295
|
+
throw err;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
return await handle.read() !== null;
|
|
1299
|
+
} finally {
|
|
1300
|
+
await handle.close();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Occupancy of `basePath` for a location of `type`.
|
|
1305
|
+
*
|
|
1306
|
+
* - missing root → `empty` (nothing this delete can strand)
|
|
1307
|
+
* - class-mapped type → occupied iff any `<camera>/<subtree>` has an entry
|
|
1308
|
+
* - other types → occupied iff the root has a non-dot entry
|
|
1309
|
+
*/
|
|
1310
|
+
async function probeLocalDirectoryOccupancy(input) {
|
|
1311
|
+
let names;
|
|
1312
|
+
try {
|
|
1313
|
+
names = await readdir(input.basePath);
|
|
1314
|
+
} catch (err) {
|
|
1315
|
+
if (err.code === "ENOENT") return "empty";
|
|
1316
|
+
return "unknown";
|
|
1317
|
+
}
|
|
1318
|
+
const visible = names.filter((name) => !isDotEntry(name));
|
|
1319
|
+
const subtrees = STORAGE_CLASS_SUBTREES[input.type];
|
|
1320
|
+
if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
|
|
1321
|
+
try {
|
|
1322
|
+
for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(path.join(input.basePath, camera, subtree))) return "occupied";
|
|
1323
|
+
} catch {
|
|
1324
|
+
return "unknown";
|
|
1325
|
+
}
|
|
1326
|
+
return "empty";
|
|
1327
|
+
}
|
|
1328
|
+
//#endregion
|
|
1329
|
+
//#region src/builtins/storage-orchestrator/location-class-paths.ts
|
|
1330
|
+
/**
|
|
1331
|
+
* Which paths under a shared root BELONG to a storage class (D327).
|
|
1332
|
+
*
|
|
1333
|
+
* A recordings disk hosts several classes at one `basePath`:
|
|
1334
|
+
* `<camera>/high|mid` (recordings), `<camera>/low` (recordingsLow),
|
|
1335
|
+
* `<camera>/events` (eventMedia) and `homekit-clips/…` (homekitClips). A
|
|
1336
|
+
* location may touch only its own subtree; the rest of the root is another
|
|
1337
|
+
* location's data. On 2026-10-20 a repoint of `homekitClips:default` walked the
|
|
1338
|
+
* WHOLE shared root and deleted ~8.6k event-media files that were not its own.
|
|
1339
|
+
*
|
|
1340
|
+
* The per-camera map is {@link STORAGE_CLASS_SUBTREES} (the one source, shared
|
|
1341
|
+
* with occupancy scoring). A class whose subtree sits at the ROOT, not under a
|
|
1342
|
+
* camera, is listed here. A type in neither map owns its whole root (backups,
|
|
1343
|
+
* artifacts, …): there is no subtree to scope to.
|
|
1344
|
+
*/
|
|
1345
|
+
/** Class → top-level directories directly under the root (no camera segment). */
|
|
1346
|
+
var STORAGE_CLASS_ROOT_SUBTREES = { homekitClips: ["homekit-clips"] };
|
|
1347
|
+
/** Classify a root-relative path (`/` separators) for a location of `type`. */
|
|
1348
|
+
function classifyPath(type, relativePath) {
|
|
1349
|
+
const rootSubtrees = STORAGE_CLASS_ROOT_SUBTREES[type];
|
|
1350
|
+
const cameraSubtrees = STORAGE_CLASS_SUBTREES[type];
|
|
1351
|
+
if (rootSubtrees === void 0 && cameraSubtrees === void 0) return "own";
|
|
1352
|
+
const segments = relativePath.split("/").filter((segment) => segment.length > 0);
|
|
1353
|
+
if (rootSubtrees !== void 0) {
|
|
1354
|
+
const top = segments[0];
|
|
1355
|
+
return top !== void 0 && rootSubtrees.includes(top) ? "own" : "foreign";
|
|
1356
|
+
}
|
|
1357
|
+
if (segments.length < 2) return "descend";
|
|
1358
|
+
const subtree = segments[1];
|
|
1359
|
+
return subtree !== void 0 && cameraSubtrees?.includes(subtree) === true ? "own" : "foreign";
|
|
1360
|
+
}
|
|
1361
|
+
//#endregion
|
|
1230
1362
|
//#region src/builtins/storage-orchestrator/location-repoint-move.ts
|
|
1231
1363
|
/**
|
|
1232
1364
|
* Walk the source root and collect FILES.
|
|
@@ -1237,24 +1369,28 @@ function buildStorageLocationRegistry(perAddon) {
|
|
|
1237
1369
|
* (an empty directory) is skipped by the copy loop, which is the only place
|
|
1238
1370
|
* that can tell — and skipping it is right, since there is nothing to move.
|
|
1239
1371
|
*/
|
|
1240
|
-
async function collectFiles(io, prefix) {
|
|
1372
|
+
async function collectFiles(io, scope, prefix) {
|
|
1241
1373
|
const entries = await io.list(prefix);
|
|
1242
1374
|
const files = [];
|
|
1243
1375
|
for (const entry of entries) {
|
|
1376
|
+
const verdict = scope(entry);
|
|
1377
|
+
if (verdict === "foreign") continue;
|
|
1244
1378
|
if ((await io.list(entry)).length > 0) {
|
|
1245
|
-
files.push(...await collectFiles(io, entry));
|
|
1379
|
+
files.push(...await collectFiles(io, scope, entry));
|
|
1246
1380
|
continue;
|
|
1247
1381
|
}
|
|
1248
|
-
files.push(entry);
|
|
1382
|
+
if (verdict === "own") files.push(entry);
|
|
1249
1383
|
}
|
|
1250
1384
|
return files;
|
|
1251
1385
|
}
|
|
1386
|
+
/** The whole root — only correct for a class that owns all of it. */
|
|
1387
|
+
var WHOLE_ROOT = () => "own";
|
|
1252
1388
|
/**
|
|
1253
1389
|
* Copy every file from the source root to the destination, verify each, then
|
|
1254
1390
|
* delete the originals. Throws with NOTHING deleted if any copy fails.
|
|
1255
1391
|
*/
|
|
1256
|
-
async function moveLocationTree(io) {
|
|
1257
|
-
const candidates = await collectFiles(io);
|
|
1392
|
+
async function moveLocationTree(io, scope = WHOLE_ROOT) {
|
|
1393
|
+
const candidates = await collectFiles(io, scope);
|
|
1258
1394
|
const copied = [];
|
|
1259
1395
|
let bytes = 0;
|
|
1260
1396
|
for (const relativePath of candidates) {
|
|
@@ -1277,6 +1413,121 @@ async function moveLocationTree(io) {
|
|
|
1277
1413
|
};
|
|
1278
1414
|
}
|
|
1279
1415
|
//#endregion
|
|
1416
|
+
//#region src/builtins/storage-orchestrator/location-repointer.ts
|
|
1417
|
+
var RepointRefusedError = class extends Error {
|
|
1418
|
+
reason;
|
|
1419
|
+
name = "RepointRefusedError";
|
|
1420
|
+
constructor(reason, message) {
|
|
1421
|
+
super(message);
|
|
1422
|
+
this.reason = reason;
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
/** Does `a` equal `b`, or sit inside it, or contain it? Segment-wise, never by raw prefix. */
|
|
1426
|
+
function rootsOverlap(a, b) {
|
|
1427
|
+
if (a === b) return true;
|
|
1428
|
+
const within = (inner, outer) => inner.startsWith(outer.endsWith("/") ? outer : `${outer}/`);
|
|
1429
|
+
return within(a, b) || within(b, a);
|
|
1430
|
+
}
|
|
1431
|
+
function basePathOf$1(config) {
|
|
1432
|
+
const raw = config["basePath"];
|
|
1433
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
1434
|
+
}
|
|
1435
|
+
var LocationRepointer = class {
|
|
1436
|
+
deps;
|
|
1437
|
+
claims = /* @__PURE__ */ new Set();
|
|
1438
|
+
constructor(deps) {
|
|
1439
|
+
this.deps = deps;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Move `stored`'s own subtree to `next`'s root, then run `commit` (the row
|
|
1443
|
+
* save) while the claim is still held. Throws {@link RepointRefusedError}
|
|
1444
|
+
* with NOTHING copied or deleted when a mover is on the root.
|
|
1445
|
+
*/
|
|
1446
|
+
async run(stored, next, commit) {
|
|
1447
|
+
const claim = await this.claim(stored, next);
|
|
1448
|
+
try {
|
|
1449
|
+
await this.refuseWhileMovers(claim, stored);
|
|
1450
|
+
const moved = await moveLocationTree(await this.deps.ioFor(stored, next), (path) => classifyPath(stored.type, path));
|
|
1451
|
+
this.logMoved(stored, next, moved);
|
|
1452
|
+
return await commit();
|
|
1453
|
+
} finally {
|
|
1454
|
+
this.claims.delete(claim);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* May a mover that touches `locationIds` run? Refuses while a repoint holds
|
|
1459
|
+
* an overlapping root. Called by the owning addon AFTER its job is
|
|
1460
|
+
* registered, so the repoint's mover listing and this check cannot both miss.
|
|
1461
|
+
*/
|
|
1462
|
+
async clearance(locationIds) {
|
|
1463
|
+
const keys = await this.keysOf(locationIds);
|
|
1464
|
+
for (const claim of this.claims) if (claim.keys.some((held) => keys.some((key) => rootsOverlap(held, key)))) return {
|
|
1465
|
+
clear: false,
|
|
1466
|
+
reason: "repoint-active",
|
|
1467
|
+
repointLocationId: claim.locationId
|
|
1468
|
+
};
|
|
1469
|
+
return { clear: true };
|
|
1470
|
+
}
|
|
1471
|
+
async claim(stored, next) {
|
|
1472
|
+
const paths = [basePathOf$1(stored.config), basePathOf$1(next.config)].filter((path) => path !== null);
|
|
1473
|
+
const keys = await Promise.all(paths.map((path) => this.deps.resolveKey(path)));
|
|
1474
|
+
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`);
|
|
1475
|
+
const claim = {
|
|
1476
|
+
locationId: stored.id,
|
|
1477
|
+
keys
|
|
1478
|
+
};
|
|
1479
|
+
this.claims.add(claim);
|
|
1480
|
+
return claim;
|
|
1481
|
+
}
|
|
1482
|
+
async refuseWhileMovers(claim, stored) {
|
|
1483
|
+
let movers;
|
|
1484
|
+
try {
|
|
1485
|
+
movers = await this.deps.listMovers();
|
|
1486
|
+
} catch (err) {
|
|
1487
|
+
this.claims.delete(claim);
|
|
1488
|
+
this.refuse(stored, "movers-unreadable", `the active movers could not be listed (${err instanceof Error ? err.message : String(err)})`);
|
|
1489
|
+
}
|
|
1490
|
+
for (const mover of movers) {
|
|
1491
|
+
if (mover.job.state !== "queued" && mover.job.state !== "running") continue;
|
|
1492
|
+
const touched = await this.keysOf(this.locationsTouchedBy(mover));
|
|
1493
|
+
if (claim.keys.some((held) => touched.some((key) => rootsOverlap(held, key)))) {
|
|
1494
|
+
this.claims.delete(claim);
|
|
1495
|
+
this.refuse(stored, "mover-active", `${mover.lane} mover ${mover.job.jobId} (${mover.job.fromLocationId} -> ${mover.job.toLocationId}) touches the same physical root`, {
|
|
1496
|
+
jobId: mover.job.jobId,
|
|
1497
|
+
lane: mover.lane
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
locationsTouchedBy(mover) {
|
|
1503
|
+
return [mover.job.fromLocationId, mover.job.toLocationId];
|
|
1504
|
+
}
|
|
1505
|
+
/** `*` is the media mover's "from wherever the rows are": every eventMedia location. */
|
|
1506
|
+
async keysOf(locationIds) {
|
|
1507
|
+
const paths = locationIds.flatMap((id) => id === "*" ? this.deps.locationIdsOfType("eventMedia") : [id]).map((id) => this.deps.locationBasePath(id)).filter((path) => path !== null);
|
|
1508
|
+
return Promise.all(paths.map((path) => this.deps.resolveKey(path)));
|
|
1509
|
+
}
|
|
1510
|
+
refuse(stored, reason, detail, meta = {}) {
|
|
1511
|
+
this.deps.logger.warn("storage-orchestrator: REFUSING to re-point a location — nothing was moved", { meta: {
|
|
1512
|
+
id: stored.id,
|
|
1513
|
+
type: stored.type,
|
|
1514
|
+
reason,
|
|
1515
|
+
detail,
|
|
1516
|
+
...meta
|
|
1517
|
+
} });
|
|
1518
|
+
throw new RepointRefusedError(reason, `re-point of "${stored.id}" refused (${reason}): ${detail}`);
|
|
1519
|
+
}
|
|
1520
|
+
logMoved(stored, next, moved) {
|
|
1521
|
+
this.deps.logger.info("storage-orchestrator: singleton location re-pointed — its data was carried over", { meta: {
|
|
1522
|
+
id: stored.id,
|
|
1523
|
+
type: stored.type,
|
|
1524
|
+
from: basePathOf$1(stored.config),
|
|
1525
|
+
to: basePathOf$1(next.config),
|
|
1526
|
+
...moved
|
|
1527
|
+
} });
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
//#endregion
|
|
1280
1531
|
//#region src/builtins/storage-orchestrator/access-guards.ts
|
|
1281
1532
|
/**
|
|
1282
1533
|
* May `storage.resolve` return a path for this location?
|
|
@@ -1776,78 +2027,6 @@ function resolveHubSettingsForward(options) {
|
|
|
1776
2027
|
}, { timeout: HUB_SETTINGS_FORWARD_TIMEOUT_MS }));
|
|
1777
2028
|
}
|
|
1778
2029
|
//#endregion
|
|
1779
|
-
//#region src/builtins/storage-orchestrator/location-occupancy.ts
|
|
1780
|
-
/**
|
|
1781
|
-
* Delete-time occupancy of a location's on-disk root.
|
|
1782
|
-
*
|
|
1783
|
-
* A recordings disk may host several classes at one `basePath`
|
|
1784
|
-
* (`<camera>/high|mid`, `<camera>/low`, `<camera>/events`). Occupancy for
|
|
1785
|
-
* a class is therefore the class subtree, not the shared root — otherwise a
|
|
1786
|
-
* drained `eventMedia:default` still looks full because high footage is
|
|
1787
|
-
* writing next to it (observed 2026-09-01).
|
|
1788
|
-
*
|
|
1789
|
-
* The walk is shallow: one `readdir` of the root (one dirent per camera)
|
|
1790
|
-
* plus `opendir`+first-entry of each named subtree. No recursive find of
|
|
1791
|
-
* segments — that is the walk that saturates a USB disk.
|
|
1792
|
-
*/
|
|
1793
|
-
/**
|
|
1794
|
-
* Class → directories under `<root>/<camera>/`. Same map as the discard
|
|
1795
|
-
* wipe (`DISCARD_SUBTREES` in the recorder / pipeline-analytics addons).
|
|
1796
|
-
* Types not listed keep the legacy "any non-dot entry at the root" rule
|
|
1797
|
-
* (backups, addon-data, …).
|
|
1798
|
-
*/
|
|
1799
|
-
var STORAGE_CLASS_SUBTREES = {
|
|
1800
|
-
eventMedia: ["events"],
|
|
1801
|
-
recordingsLow: ["low"],
|
|
1802
|
-
recordings: ["high", "mid"]
|
|
1803
|
-
};
|
|
1804
|
-
function isDotEntry(name) {
|
|
1805
|
-
return name.startsWith(".");
|
|
1806
|
-
}
|
|
1807
|
-
/**
|
|
1808
|
-
* True when `dir` exists and contains at least one dirent. Missing → false.
|
|
1809
|
-
* Any other errno throws so the caller can answer `unknown` (D49).
|
|
1810
|
-
*/
|
|
1811
|
-
async function dirHasAnyEntry(dir) {
|
|
1812
|
-
let handle;
|
|
1813
|
-
try {
|
|
1814
|
-
handle = await opendir(dir);
|
|
1815
|
-
} catch (err) {
|
|
1816
|
-
if (err.code === "ENOENT") return false;
|
|
1817
|
-
throw err;
|
|
1818
|
-
}
|
|
1819
|
-
try {
|
|
1820
|
-
return await handle.read() !== null;
|
|
1821
|
-
} finally {
|
|
1822
|
-
await handle.close();
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
/**
|
|
1826
|
-
* Occupancy of `basePath` for a location of `type`.
|
|
1827
|
-
*
|
|
1828
|
-
* - missing root → `empty` (nothing this delete can strand)
|
|
1829
|
-
* - class-mapped type → occupied iff any `<camera>/<subtree>` has an entry
|
|
1830
|
-
* - other types → occupied iff the root has a non-dot entry
|
|
1831
|
-
*/
|
|
1832
|
-
async function probeLocalDirectoryOccupancy(input) {
|
|
1833
|
-
let names;
|
|
1834
|
-
try {
|
|
1835
|
-
names = await readdir(input.basePath);
|
|
1836
|
-
} catch (err) {
|
|
1837
|
-
if (err.code === "ENOENT") return "empty";
|
|
1838
|
-
return "unknown";
|
|
1839
|
-
}
|
|
1840
|
-
const visible = names.filter((name) => !isDotEntry(name));
|
|
1841
|
-
const subtrees = STORAGE_CLASS_SUBTREES[input.type];
|
|
1842
|
-
if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
|
|
1843
|
-
try {
|
|
1844
|
-
for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(path.join(input.basePath, camera, subtree))) return "occupied";
|
|
1845
|
-
} catch {
|
|
1846
|
-
return "unknown";
|
|
1847
|
-
}
|
|
1848
|
-
return "empty";
|
|
1849
|
-
}
|
|
1850
|
-
//#endregion
|
|
1851
2030
|
//#region src/builtins/storage-orchestrator/location-occupancy-slice.ts
|
|
1852
2031
|
/** One minute: the same order as the pressure sweep, and far longer than an
|
|
1853
2032
|
* admin-UI repaint. */
|
|
@@ -2583,6 +2762,44 @@ var StorageOrchestratorService = class {
|
|
|
2583
2762
|
getLocalNodeId() {
|
|
2584
2763
|
return this.localNode;
|
|
2585
2764
|
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Whether a seed must wait for an ANSWERED read of the store (D49). True on a
|
|
2767
|
+
* node whose locations live in a store: seeding before that read lands writes
|
|
2768
|
+
* defaults the store's own rows (not yet adopted, or not yet read) then
|
|
2769
|
+
* contradict. False on a node that has no store by design — it seeds in memory
|
|
2770
|
+
* at every boot.
|
|
2771
|
+
*/
|
|
2772
|
+
storeRequiredForSeed = false;
|
|
2773
|
+
setStoreRequiredForSeed(required) {
|
|
2774
|
+
this.storeRequiredForSeed = required;
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* The location types a default was ever seeded for — null when the store
|
|
2778
|
+
* cannot remember (then a type with no row is seeded, as before). What makes
|
|
2779
|
+
* an operator's delete of a seeded default STICK: zero rows today is not
|
|
2780
|
+
* "fresh install", it is only fresh when nothing was ever seeded.
|
|
2781
|
+
*/
|
|
2782
|
+
seededTypes = null;
|
|
2783
|
+
/** The store held locations but no seed memory: an install older than the memory. */
|
|
2784
|
+
installPredatesSeedMemory = false;
|
|
2785
|
+
async loadSeedMemory(store, rowCount) {
|
|
2786
|
+
if (!store.loadSeededTypes) return;
|
|
2787
|
+
const types = await store.loadSeededTypes();
|
|
2788
|
+
this.seededTypes = new Set(types);
|
|
2789
|
+
this.installPredatesSeedMemory = types.length === 0 && rowCount > 0;
|
|
2790
|
+
}
|
|
2791
|
+
rememberSeeded(type) {
|
|
2792
|
+
const seeded = this.seededTypes;
|
|
2793
|
+
const store = this.locationStore;
|
|
2794
|
+
if (seeded === null || seeded.has(type) || !store?.markTypeSeeded) return;
|
|
2795
|
+
seeded.add(type);
|
|
2796
|
+
store.markTypeSeeded(type).catch((err) => {
|
|
2797
|
+
this.logger.error("storage-orchestrator: persisting the seed memory failed", { meta: {
|
|
2798
|
+
type,
|
|
2799
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2800
|
+
} });
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2586
2803
|
/** True once a persistence store is wired (constructor or {@link attachStore}). */
|
|
2587
2804
|
hasStore() {
|
|
2588
2805
|
return this.locationStore !== null;
|
|
@@ -2616,6 +2833,7 @@ var StorageOrchestratorService = class {
|
|
|
2616
2833
|
async attachStore(store) {
|
|
2617
2834
|
if (this.locationStore) return;
|
|
2618
2835
|
const rows = await store.loadAll();
|
|
2836
|
+
await this.loadSeedMemory(store, rows.length);
|
|
2619
2837
|
this.locationStore = store;
|
|
2620
2838
|
const dbIds = new Set(rows.map((r) => r.id));
|
|
2621
2839
|
for (const loc of rows) this.locations.set(loc.id, loc);
|
|
@@ -2754,6 +2972,7 @@ var StorageOrchestratorService = class {
|
|
|
2754
2972
|
async initialize() {
|
|
2755
2973
|
if (!this.locationStore) return;
|
|
2756
2974
|
const rows = await this.locationStore.loadAll();
|
|
2975
|
+
await this.loadSeedMemory(this.locationStore, rows.length);
|
|
2757
2976
|
for (const loc of rows) this.locations.set(loc.id, loc);
|
|
2758
2977
|
this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
|
|
2759
2978
|
this.migrateOntoTheWriteSet(rows);
|
|
@@ -3205,14 +3424,35 @@ var StorageOrchestratorService = class {
|
|
|
3205
3424
|
* directory for derivative slots like `recordingsLow` → `recordings`).
|
|
3206
3425
|
*/
|
|
3207
3426
|
seedDefaults(input) {
|
|
3427
|
+
if (this.storeRequiredForSeed && this.locationStore === null) {
|
|
3428
|
+
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");
|
|
3429
|
+
return {
|
|
3430
|
+
added: 0,
|
|
3431
|
+
deferred: "store-not-answered"
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3208
3434
|
let added = 0;
|
|
3435
|
+
let deferredProvider = false;
|
|
3209
3436
|
const declById = new Map(input.declarations.map((d) => [d.id, d]));
|
|
3210
3437
|
const rootFor = (d) => {
|
|
3211
3438
|
const dedicated = d.defaultRoot === "media" ? input.mediaBasePath : d.defaultRoot === "backup" ? input.backupBasePath : void 0;
|
|
3212
3439
|
return path$1.join(dedicated ?? input.basePath, d.id);
|
|
3213
3440
|
};
|
|
3214
3441
|
for (const d of input.declarations) {
|
|
3215
|
-
if (this.hasAnyLocationOfType(d.id))
|
|
3442
|
+
if (this.hasAnyLocationOfType(d.id)) {
|
|
3443
|
+
this.rememberSeeded(d.id);
|
|
3444
|
+
continue;
|
|
3445
|
+
}
|
|
3446
|
+
if (this.seededTypes?.has(d.id)) continue;
|
|
3447
|
+
if (this.installPredatesSeedMemory) {
|
|
3448
|
+
this.logger.info("storage-orchestrator: existing install — not re-creating a default the operator may have removed", { meta: { type: d.id } });
|
|
3449
|
+
this.rememberSeeded(d.id);
|
|
3450
|
+
continue;
|
|
3451
|
+
}
|
|
3452
|
+
if (this.nodeLocalResolver !== null && this.nodeLocalResolver(input.providerId) === void 0) {
|
|
3453
|
+
deferredProvider = true;
|
|
3454
|
+
continue;
|
|
3455
|
+
}
|
|
3216
3456
|
const base = d.defaultsTo ? this.getLocationById(`${d.defaultsTo}:default`)?.config["basePath"] ?? rootFor(declById.get(d.defaultsTo) ?? d) : rootFor(d);
|
|
3217
3457
|
this.upsertLocation({
|
|
3218
3458
|
id: `${d.id}:default`,
|
|
@@ -3223,12 +3463,18 @@ var StorageOrchestratorService = class {
|
|
|
3223
3463
|
enabled: true,
|
|
3224
3464
|
isSystem: false
|
|
3225
3465
|
});
|
|
3466
|
+
this.rememberSeeded(d.id);
|
|
3226
3467
|
added++;
|
|
3227
3468
|
}
|
|
3228
3469
|
if (added > 0) this.logger.info("storage-orchestrator: seeded default locations", { meta: {
|
|
3229
3470
|
added,
|
|
3230
3471
|
basePath: input.basePath
|
|
3231
3472
|
} });
|
|
3473
|
+
if (deferredProvider) this.logger.info("storage-orchestrator: seed deferred for some types — provider not classified yet", { meta: { providerId: input.providerId } });
|
|
3474
|
+
return deferredProvider ? {
|
|
3475
|
+
added,
|
|
3476
|
+
deferred: "provider-unclassified"
|
|
3477
|
+
} : { added };
|
|
3232
3478
|
}
|
|
3233
3479
|
/**
|
|
3234
3480
|
* The same-type physical-root refusal (D389).
|
|
@@ -3603,29 +3849,72 @@ var DEFAULT_STORAGE_LOCATION_BAND_CONFIG = {
|
|
|
3603
3849
|
lowPercent: 15,
|
|
3604
3850
|
criticalPercent: 5
|
|
3605
3851
|
};
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3852
|
+
/** Margin, in percentage points, between the enter and exit of an unmanaged edge. */
|
|
3853
|
+
var UNMANAGED_HYSTERESIS_POINTS = 2;
|
|
3854
|
+
/** Managed: fractions of the floor. */
|
|
3855
|
+
var MANAGED_LOW_ENTER = .8;
|
|
3856
|
+
var MANAGED_LOW_EXIT = .9;
|
|
3857
|
+
var MANAGED_CRITICAL_ENTER = .5;
|
|
3858
|
+
var MANAGED_CRITICAL_EXIT = .6;
|
|
3859
|
+
/** How long the evictor may sit below the low margin before it counts as behind. */
|
|
3860
|
+
var MANAGED_LOW_GRACE_MS = 10 * 6e4;
|
|
3861
|
+
function boundsOf(floorPercent, config) {
|
|
3862
|
+
if (floorPercent === void 0) return {
|
|
3863
|
+
lowEnter: config.lowPercent,
|
|
3864
|
+
lowExit: config.lowPercent + UNMANAGED_HYSTERESIS_POINTS,
|
|
3865
|
+
criticalEnter: config.criticalPercent,
|
|
3866
|
+
criticalExit: config.criticalPercent + UNMANAGED_HYSTERESIS_POINTS,
|
|
3867
|
+
lowGraceMs: 0,
|
|
3868
|
+
lowReason: "free-below-warn",
|
|
3869
|
+
criticalReason: "free-below-critical"
|
|
3618
3870
|
};
|
|
3619
3871
|
return {
|
|
3620
|
-
|
|
3621
|
-
|
|
3872
|
+
lowEnter: floorPercent * MANAGED_LOW_ENTER,
|
|
3873
|
+
lowExit: floorPercent * MANAGED_LOW_EXIT,
|
|
3874
|
+
criticalEnter: floorPercent * MANAGED_CRITICAL_ENTER,
|
|
3875
|
+
criticalExit: floorPercent * MANAGED_CRITICAL_EXIT,
|
|
3876
|
+
lowGraceMs: MANAGED_LOW_GRACE_MS,
|
|
3877
|
+
lowReason: "free-below-floor",
|
|
3878
|
+
criticalReason: "free-below-hard-bound"
|
|
3622
3879
|
};
|
|
3623
3880
|
}
|
|
3624
3881
|
var StorageLocationBandMonitor = class {
|
|
3625
3882
|
config;
|
|
3883
|
+
now;
|
|
3626
3884
|
band = /* @__PURE__ */ new Map();
|
|
3627
|
-
|
|
3885
|
+
/** When free first went under `lowEnter` and has stayed there. */
|
|
3886
|
+
belowLowSince = /* @__PURE__ */ new Map();
|
|
3887
|
+
constructor(config = DEFAULT_STORAGE_LOCATION_BAND_CONFIG, now = Date.now) {
|
|
3628
3888
|
this.config = config;
|
|
3889
|
+
this.now = now;
|
|
3890
|
+
}
|
|
3891
|
+
nextBand(locationId, previous, free, b) {
|
|
3892
|
+
if (free < b.criticalEnter || previous === "critical" && free < b.criticalExit) return {
|
|
3893
|
+
band: "critical",
|
|
3894
|
+
reason: b.criticalReason
|
|
3895
|
+
};
|
|
3896
|
+
const lowActive = previous === "low" || previous === "critical";
|
|
3897
|
+
if (free < b.lowEnter || lowActive && free < b.lowExit) {
|
|
3898
|
+
if (lowActive) return {
|
|
3899
|
+
band: "low",
|
|
3900
|
+
reason: b.lowReason
|
|
3901
|
+
};
|
|
3902
|
+
const since = this.belowLowSince.get(locationId) ?? this.now();
|
|
3903
|
+
this.belowLowSince.set(locationId, since);
|
|
3904
|
+
if (this.now() - since >= b.lowGraceMs) return {
|
|
3905
|
+
band: "low",
|
|
3906
|
+
reason: b.lowReason
|
|
3907
|
+
};
|
|
3908
|
+
return {
|
|
3909
|
+
band: previous ?? "ok",
|
|
3910
|
+
reason: "free-ok"
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3913
|
+
this.belowLowSince.delete(locationId);
|
|
3914
|
+
return {
|
|
3915
|
+
band: "ok",
|
|
3916
|
+
reason: "free-ok"
|
|
3917
|
+
};
|
|
3629
3918
|
}
|
|
3630
3919
|
/**
|
|
3631
3920
|
* Observe one sweep of managed locations. Locations missing from `samples`
|
|
@@ -3636,8 +3925,9 @@ var StorageLocationBandMonitor = class {
|
|
|
3636
3925
|
const out = [];
|
|
3637
3926
|
for (const sample of samples) {
|
|
3638
3927
|
seen.add(sample.locationId);
|
|
3639
|
-
|
|
3928
|
+
if (sample.freePercent === null) continue;
|
|
3640
3929
|
const previous = this.band.get(sample.locationId) ?? null;
|
|
3930
|
+
const { band, reason } = this.nextBand(sample.locationId, previous, sample.freePercent, boundsOf(sample.floorPercent, this.config));
|
|
3641
3931
|
if (previous === band) continue;
|
|
3642
3932
|
this.band.set(sample.locationId, band);
|
|
3643
3933
|
if (previous === null && band === "ok") continue;
|
|
@@ -3647,11 +3937,17 @@ var StorageLocationBandMonitor = class {
|
|
|
3647
3937
|
...sample.locationName !== void 0 ? { locationName: sample.locationName } : {},
|
|
3648
3938
|
band,
|
|
3649
3939
|
freePercent: sample.freePercent,
|
|
3940
|
+
...sample.floorPercent !== void 0 ? { floorPercent: sample.floorPercent } : {},
|
|
3941
|
+
...sample.freeBytes !== void 0 ? { freeBytes: sample.freeBytes } : {},
|
|
3942
|
+
...sample.floorBytes !== void 0 ? { floorBytes: sample.floorBytes } : {},
|
|
3650
3943
|
reason,
|
|
3651
3944
|
previous
|
|
3652
3945
|
});
|
|
3653
3946
|
}
|
|
3654
|
-
for (const id of [...this.band.keys()]) if (!seen.has(id))
|
|
3947
|
+
for (const id of [...this.band.keys()]) if (!seen.has(id)) {
|
|
3948
|
+
this.band.delete(id);
|
|
3949
|
+
this.belowLowSince.delete(id);
|
|
3950
|
+
}
|
|
3655
3951
|
return out;
|
|
3656
3952
|
}
|
|
3657
3953
|
};
|
|
@@ -3699,6 +3995,7 @@ var PRESSURE_SWEEP_INTERVAL_MS = 6e4;
|
|
|
3699
3995
|
var CAP_PROVIDER_REGISTERED_CATEGORY = "capability:provider-registered";
|
|
3700
3996
|
var StorageOrchestratorAddon = class extends BaseAddon {
|
|
3701
3997
|
locationsKind = null;
|
|
3998
|
+
seedMarkersKind = null;
|
|
3702
3999
|
drainStateKind = null;
|
|
3703
4000
|
service = null;
|
|
3704
4001
|
pressureManager = null;
|
|
@@ -3724,6 +4021,8 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
3724
4021
|
pressureTimer = null;
|
|
3725
4022
|
pressureSweepInFlight = false;
|
|
3726
4023
|
migration = null;
|
|
4024
|
+
/** Guards every single-location repoint and arbitrates movers per physical root. */
|
|
4025
|
+
repointer = null;
|
|
3727
4026
|
/**
|
|
3728
4027
|
* Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
|
|
3729
4028
|
* synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
|
|
@@ -3760,6 +4059,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
3760
4059
|
if (records !== void 0) {
|
|
3761
4060
|
await records.declare(storageOrchestratorRecordKinds);
|
|
3762
4061
|
this.locationsKind = SqliteLocationStore.recordStoresOf(records);
|
|
4062
|
+
this.seedMarkersKind = SqliteLocationStore.seedMarkersOf(records);
|
|
3763
4063
|
this.drainStateKind = SqliteDrainStateStore.recordStoresOf(records);
|
|
3764
4064
|
}
|
|
3765
4065
|
const getProviders = () => {
|
|
@@ -3779,6 +4079,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
3779
4079
|
return proxy;
|
|
3780
4080
|
});
|
|
3781
4081
|
this.service = service;
|
|
4082
|
+
service.setStoreRequiredForSeed(this.locationsKind !== null);
|
|
3782
4083
|
service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
|
|
3783
4084
|
service.setOccupancyProbe((location) => this.locationOccupancy(location));
|
|
3784
4085
|
await service.initialize();
|
|
@@ -3802,8 +4103,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
3802
4103
|
...input,
|
|
3803
4104
|
config: keychained
|
|
3804
4105
|
};
|
|
3805
|
-
await this.
|
|
3806
|
-
const saved = service.upsertLocation(next);
|
|
4106
|
+
const saved = await this.upsertCarryingData(stored, next, () => service.upsertLocation(next));
|
|
3807
4107
|
this.announceLocationsChanged(saved.id, saved.type, "upsert");
|
|
3808
4108
|
return this.redacted(saved);
|
|
3809
4109
|
},
|
|
@@ -4013,6 +4313,49 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
4013
4313
|
deviceKeyOf: (locationId) => this.locationDeviceKey(locationId)
|
|
4014
4314
|
});
|
|
4015
4315
|
this.migration = migration;
|
|
4316
|
+
this.repointer = new LocationRepointer({
|
|
4317
|
+
logger: this.ctx.logger,
|
|
4318
|
+
resolveKey: async (basePath) => {
|
|
4319
|
+
try {
|
|
4320
|
+
return await fs.realpath(basePath);
|
|
4321
|
+
} catch {
|
|
4322
|
+
return path$1.resolve(basePath);
|
|
4323
|
+
}
|
|
4324
|
+
},
|
|
4325
|
+
locationBasePath: (id) => this.locationBasePath(id),
|
|
4326
|
+
locationIdsOfType: (type) => service.listLocations({ type }).map((loc) => loc.id),
|
|
4327
|
+
listMovers: () => migration.movers(),
|
|
4328
|
+
ioFor: async (stored, next) => {
|
|
4329
|
+
const target = {
|
|
4330
|
+
...stored,
|
|
4331
|
+
config: next.config
|
|
4332
|
+
};
|
|
4333
|
+
const [source, destination] = await Promise.all([service.getProviderFor(stored), service.getProviderFor(target)]);
|
|
4334
|
+
return {
|
|
4335
|
+
list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
|
|
4336
|
+
location: stored,
|
|
4337
|
+
prefix
|
|
4338
|
+
}),
|
|
4339
|
+
read: (relativePath) => source.read({
|
|
4340
|
+
location: stored,
|
|
4341
|
+
relativePath
|
|
4342
|
+
}),
|
|
4343
|
+
write: (relativePath, data) => destination.write({
|
|
4344
|
+
location: target,
|
|
4345
|
+
relativePath,
|
|
4346
|
+
data
|
|
4347
|
+
}),
|
|
4348
|
+
readBack: (relativePath) => destination.read({
|
|
4349
|
+
location: target,
|
|
4350
|
+
relativePath
|
|
4351
|
+
}),
|
|
4352
|
+
deleteSource: (relativePath) => source.delete({
|
|
4353
|
+
location: stored,
|
|
4354
|
+
relativePath
|
|
4355
|
+
})
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4358
|
+
});
|
|
4016
4359
|
const cleanup = new StorageCleanupCoordinator({
|
|
4017
4360
|
locations: service,
|
|
4018
4361
|
state: this.state("storage-cleanup", StorageCleanupJobSchema.nullable(), null),
|
|
@@ -4048,6 +4391,13 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
4048
4391
|
status: ({ jobId }) => migration.status(jobId),
|
|
4049
4392
|
cancel: async ({ jobId }) => ({ cancelled: await migration.cancel(jobId) }),
|
|
4050
4393
|
movers: () => migration.movers(),
|
|
4394
|
+
moverClearance: async ({ locationIds }) => {
|
|
4395
|
+
const verdict = await this.requireRepointer().clearance(locationIds);
|
|
4396
|
+
return verdict.clear ? { clear: true } : {
|
|
4397
|
+
clear: false,
|
|
4398
|
+
repointLocationId: verdict.repointLocationId
|
|
4399
|
+
};
|
|
4400
|
+
},
|
|
4051
4401
|
residue: () => migration.residue(),
|
|
4052
4402
|
drain: async (input) => ({ jobId: await migration.drain(input) }),
|
|
4053
4403
|
cleanupStart: async (input) => ({ jobId: await cleanup.start(input) }),
|
|
@@ -4174,7 +4524,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
4174
4524
|
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");
|
|
4175
4525
|
return null;
|
|
4176
4526
|
}
|
|
4177
|
-
return new SqliteLocationStore(engine, this.locationsKind);
|
|
4527
|
+
return new SqliteLocationStore(engine, this.locationsKind, this.seedMarkersKind);
|
|
4178
4528
|
}
|
|
4179
4529
|
/**
|
|
4180
4530
|
* Late-wire the persistence store once a `data-store-provider` engine
|
|
@@ -4186,12 +4536,26 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
4186
4536
|
* restart — the service never hydrates from / persists to SQLite.
|
|
4187
4537
|
*/
|
|
4188
4538
|
storeWireInFlight = null;
|
|
4539
|
+
/** True while the locations kind is empty and its legacy table still holds rows. */
|
|
4540
|
+
async adoptionIsPending() {
|
|
4541
|
+
const records = this.ctx.records;
|
|
4542
|
+
if (records === void 0 || this.locationsKind === null) return false;
|
|
4543
|
+
try {
|
|
4544
|
+
await records.declare(storageOrchestratorRecordKinds);
|
|
4545
|
+
return false;
|
|
4546
|
+
} catch (err) {
|
|
4547
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4548
|
+
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 } });
|
|
4549
|
+
return true;
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4189
4552
|
async ensureStoreWired() {
|
|
4190
4553
|
const service = this.service;
|
|
4191
4554
|
if (!service || service.hasStore()) return;
|
|
4192
4555
|
if (this.storeWireInFlight) return this.storeWireInFlight;
|
|
4193
4556
|
const store = this.resolveLocationStore(true);
|
|
4194
4557
|
if (!store) return;
|
|
4558
|
+
if (await this.adoptionIsPending()) return;
|
|
4195
4559
|
this.ctx.logger.info("storage-orchestrator: data-store engine now available — wiring persistence");
|
|
4196
4560
|
this.storeWireInFlight = service.attachStore(store).then(() => this.seedFromDeclarations()).catch((err) => {
|
|
4197
4561
|
this.ctx.logger.error("storage-orchestrator: attachStore failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -4220,7 +4584,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
4220
4584
|
}
|
|
4221
4585
|
/**
|
|
4222
4586
|
* Sample free space for every managed location and emit NC band edges.
|
|
4223
|
-
* A failed capacity read stays `null`
|
|
4587
|
+
* A failed capacity read stays `null` and changes no band — never folded to 0 (D393).
|
|
4224
4588
|
*/
|
|
4225
4589
|
async emitLocationBandEvents() {
|
|
4226
4590
|
const svc = this.service;
|
|
@@ -4236,11 +4600,18 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
4236
4600
|
};
|
|
4237
4601
|
const capacity = await this.volumeCapacityOf(locationId).catch(() => null);
|
|
4238
4602
|
const freePercent = capacity === null ? null : freePercentOf(capacity);
|
|
4603
|
+
if (capacity === null) ctx.logger.debug("storage: location capacity unreadable — band unchanged", { meta: { locationId } });
|
|
4604
|
+
const floorPercent = thresholds.get(locationId);
|
|
4239
4605
|
return {
|
|
4240
4606
|
locationId,
|
|
4241
4607
|
nodeId: loc.nodeId,
|
|
4242
4608
|
locationName: loc.displayName,
|
|
4243
|
-
freePercent
|
|
4609
|
+
freePercent,
|
|
4610
|
+
...floorPercent !== void 0 ? { floorPercent } : {},
|
|
4611
|
+
...capacity !== null ? {
|
|
4612
|
+
freeBytes: capacity.availableBytes,
|
|
4613
|
+
...floorPercent !== void 0 ? { floorBytes: Math.round(capacity.totalBytes * floorPercent / 100) } : {}
|
|
4614
|
+
} : {}
|
|
4244
4615
|
};
|
|
4245
4616
|
}));
|
|
4246
4617
|
for (const edge of this.locationBandMonitor.observe(samples)) {
|
|
@@ -4249,6 +4620,9 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
4249
4620
|
locationId: edge.locationId,
|
|
4250
4621
|
band: edge.band,
|
|
4251
4622
|
freePercent: edge.freePercent,
|
|
4623
|
+
floorPercent: edge.floorPercent ?? null,
|
|
4624
|
+
freeBytes: edge.freeBytes ?? null,
|
|
4625
|
+
floorBytes: edge.floorBytes ?? null,
|
|
4252
4626
|
reason: edge.reason
|
|
4253
4627
|
} });
|
|
4254
4628
|
ctx.eventBus.emit({
|
|
@@ -4362,50 +4736,19 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
4362
4736
|
* A no-op for everything else — a rename, a mode change, a `multi` kind, or a
|
|
4363
4737
|
* location being created for the first time.
|
|
4364
4738
|
*/
|
|
4365
|
-
async
|
|
4366
|
-
if (stored === void 0) return;
|
|
4739
|
+
async upsertCarryingData(stored, next, commit) {
|
|
4740
|
+
if (stored === void 0) return commit();
|
|
4367
4741
|
const service = this.service;
|
|
4368
|
-
if (service === null) return;
|
|
4369
|
-
if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return;
|
|
4742
|
+
if (service === null) return commit();
|
|
4743
|
+
if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return commit();
|
|
4370
4744
|
const from = stored.config["basePath"];
|
|
4371
4745
|
const to = next.config["basePath"];
|
|
4372
|
-
if (typeof from !== "string" || typeof to !== "string" || from === to) return;
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
const destination = await service.getProviderFor(target);
|
|
4379
|
-
const moved = await moveLocationTree({
|
|
4380
|
-
list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
|
|
4381
|
-
location: stored,
|
|
4382
|
-
prefix
|
|
4383
|
-
}),
|
|
4384
|
-
read: (relativePath) => source.read({
|
|
4385
|
-
location: stored,
|
|
4386
|
-
relativePath
|
|
4387
|
-
}),
|
|
4388
|
-
write: (relativePath, data) => destination.write({
|
|
4389
|
-
location: target,
|
|
4390
|
-
relativePath,
|
|
4391
|
-
data
|
|
4392
|
-
}),
|
|
4393
|
-
readBack: (relativePath) => destination.read({
|
|
4394
|
-
location: target,
|
|
4395
|
-
relativePath
|
|
4396
|
-
}),
|
|
4397
|
-
deleteSource: (relativePath) => source.delete({
|
|
4398
|
-
location: stored,
|
|
4399
|
-
relativePath
|
|
4400
|
-
})
|
|
4401
|
-
});
|
|
4402
|
-
this.ctx.logger.info("storage-orchestrator: singleton location re-pointed — its data was carried over", { meta: {
|
|
4403
|
-
id: stored.id,
|
|
4404
|
-
type: stored.type,
|
|
4405
|
-
from,
|
|
4406
|
-
to,
|
|
4407
|
-
...moved
|
|
4408
|
-
} });
|
|
4746
|
+
if (typeof from !== "string" || typeof to !== "string" || from === to) return commit();
|
|
4747
|
+
return this.requireRepointer().run(stored, next, commit);
|
|
4748
|
+
}
|
|
4749
|
+
requireRepointer() {
|
|
4750
|
+
if (this.repointer === null) throw new Error("storage-orchestrator: not initialized");
|
|
4751
|
+
return this.repointer;
|
|
4409
4752
|
}
|
|
4410
4753
|
secretKeysFor(providerId, config) {
|
|
4411
4754
|
return secretKeysForConfig(config, this.declaredSecretKeys(providerId));
|