@camstack/system 1.2.201 → 1.2.202
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/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/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- 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 +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.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/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 +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- 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 +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- 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/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/location-store.d.ts +12 -3
- package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +9 -9
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +181 -129
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +181 -129
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +54 -31
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-BmJNnb_s.mjs → dist-CiHYspij.mjs} +202 -185
- package/dist/{dist-CcV3k-hD.js → dist-jZb-HKI5.js} +213 -184
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-nLVKlhe-.mjs → retired-settings-keys-DeRxo3vg.mjs} +1 -1
- package/dist/{retired-settings-keys-Bd8fuwO1.js → retired-settings-keys-Dxv9S_lO.js} +1 -1
- package/package.json +1 -1
|
@@ -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-jZb-HKI5.js");
|
|
7
7
|
const require_hub_cap_forward = require("../../hub-cap-forward-DmHNjbB0.js");
|
|
8
8
|
let zod = require("zod");
|
|
9
9
|
let node_crypto = require("node:crypto");
|
|
@@ -193,7 +193,7 @@ function mapLocationToRow(loc) {
|
|
|
193
193
|
display_name: loc.displayName,
|
|
194
194
|
provider_id: loc.providerId,
|
|
195
195
|
config: storedConfig,
|
|
196
|
-
is_default:
|
|
196
|
+
is_default: 0,
|
|
197
197
|
is_system: loc.isSystem ? 1 : 0,
|
|
198
198
|
created_at: loc.createdAt,
|
|
199
199
|
updated_at: loc.updatedAt
|
|
@@ -227,7 +227,7 @@ function mapRowToLocation(row) {
|
|
|
227
227
|
config,
|
|
228
228
|
...nodeId !== void 0 ? { nodeId } : {},
|
|
229
229
|
...enabled !== void 0 ? { enabled } : {},
|
|
230
|
-
isDefault: row["is_default"] === 1 || row["is_default"] === true,
|
|
230
|
+
...require_dist.LegacyStorageLocationDefaultSchema.parse({ isDefault: row["is_default"] === 1 || row["is_default"] === true }),
|
|
231
231
|
isSystem: row["is_system"] === 1 || row["is_system"] === true,
|
|
232
232
|
createdAt: Number(row["created_at"] ?? 0),
|
|
233
233
|
updatedAt: Number(row["updated_at"] ?? 0)
|
|
@@ -295,16 +295,18 @@ var StorageMigrationCoordinator = class {
|
|
|
295
295
|
const targetId = input.destinations[storageClass];
|
|
296
296
|
if (targetId === void 0) continue;
|
|
297
297
|
const namedSourceId = input.sources?.[storageClass];
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
298
|
+
const writeSet = this.deps.locations.writeTargets(storageClass);
|
|
299
|
+
const implicitSources = writeSet.filter((loc) => loc.id !== targetId);
|
|
300
|
+
if (!namedSourceId && implicitSources.length > 1) throw new Error(`"${storageClass}" writes to ${String(implicitSources.length)} locations (${implicitSources.map((loc) => loc.id).join(", ")}) — name the one to cut over explicitly with \`sources\`.`);
|
|
301
|
+
const source = namedSourceId ? this.deps.locations.getLocationById(namedSourceId) : implicitSources[0];
|
|
302
|
+
if (!source) throw new Error(namedSourceId ? `Storage location "${namedSourceId}" not found` : `Storage location "${targetId}" is already the only write target for "${storageClass}" — there is nothing to cut over. Use storageMigration.drain to move what is left behind.`);
|
|
301
303
|
if (source.type !== storageClass) throw new Error(`Storage location "${source.id}" is type "${source.type}", expected "${storageClass}"`);
|
|
302
304
|
const target = this.deps.locations.getLocationById(targetId);
|
|
303
305
|
if (!target) throw new Error(`Storage location "${targetId}" not found`);
|
|
304
306
|
if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
|
|
305
|
-
if (source.id === target.id) throw new Error(namedSourceId ? `Source and destination are the same location ("${targetId}")` : `Storage location "${targetId}" is already the "${storageClass}"
|
|
306
|
-
const freezeSource =
|
|
307
|
-
if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" — the migration can
|
|
307
|
+
if (source.id === target.id) throw new Error(namedSourceId ? `Source and destination are the same location ("${targetId}")` : `Storage location "${targetId}" is already the only write target for "${storageClass}"`);
|
|
308
|
+
const freezeSource = !writeSet.some((loc) => loc.id === source.id);
|
|
309
|
+
if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" — the migration can move its write set but cannot move its bytes. Move it by hand, then enable the destination (and disable the source) with upsertLocation.`);
|
|
308
310
|
moves.push({
|
|
309
311
|
storageClass,
|
|
310
312
|
fromLocationId: source.id,
|
|
@@ -321,7 +323,7 @@ var StorageMigrationCoordinator = class {
|
|
|
321
323
|
if (moves.some((move) => move.storageClass === "eventMedia")) {
|
|
322
324
|
const unstamped = await this.deps.participants.analytics.countUnstamped();
|
|
323
325
|
if (unstamped === null) {
|
|
324
|
-
if (mode === "nonBlocking") throw new Error("could not determine whether any event-media row still carries no locationId. A non-blocking cutover
|
|
326
|
+
if (mode === "nonBlocking") throw new Error("could not determine whether any event-media row still carries no locationId. A non-blocking cutover moves the write set and would silently orphan every such row, so an unmeasured collection is refused exactly like a non-empty one. Retry, or run the blocking mode, which stamps rows as it moves them.");
|
|
325
327
|
findings.push({
|
|
326
328
|
code: "unstampedEventMediaRows",
|
|
327
329
|
storageClass: "eventMedia",
|
|
@@ -330,7 +332,7 @@ var StorageMigrationCoordinator = class {
|
|
|
330
332
|
} else if (unstamped.anyPresent) {
|
|
331
333
|
const scale = unstamped.total === null ? "an unknown number of" : unstamped.total.toString();
|
|
332
334
|
const perLane = `${describeUnstamped(unstamped.media)} media, ${describeUnstamped(unstamped.retrainFrames)} retrain frames`;
|
|
333
|
-
if (mode === "nonBlocking") throw new Error(`${scale} event-media row(s) still carry no locationId (${perLane}). A non-blocking cutover would silently orphan them. Run the seal first: pipelineAnalytics.relocateMedia({ mode: "seal", toLocationId: "${this.deps.locations.
|
|
335
|
+
if (mode === "nonBlocking") throw new Error(`${scale} event-media row(s) still carry no locationId (${perLane}). A non-blocking cutover would silently orphan them. Run the seal first: pipelineAnalytics.relocateMedia({ mode: "seal", toLocationId: "${this.deps.locations.writeTargets("eventMedia")[0]?.id ?? "eventMedia"}" }).`);
|
|
334
336
|
findings.push({
|
|
335
337
|
code: "unstampedEventMediaRows",
|
|
336
338
|
storageClass: "eventMedia",
|
|
@@ -469,7 +471,7 @@ var StorageMigrationCoordinator = class {
|
|
|
469
471
|
* remaining" action.
|
|
470
472
|
*
|
|
471
473
|
* A class appears here only when something is (or might be) left on a
|
|
472
|
-
* location
|
|
474
|
+
* location outside its write set. An empty result therefore means exactly
|
|
473
475
|
* "there is nothing to drain", which is what lets the UI offer the action
|
|
474
476
|
* only when it is true, and what lets {@link drain} refuse rather than start
|
|
475
477
|
* a job that would move nothing and report `done` — the failure mode D295
|
|
@@ -483,11 +485,13 @@ var StorageMigrationCoordinator = class {
|
|
|
483
485
|
const out = [];
|
|
484
486
|
for (const storageClass of STORAGE_CLASSES) {
|
|
485
487
|
if (!MOVER_CLASSES.includes(storageClass)) continue;
|
|
486
|
-
const
|
|
488
|
+
const writeSet = this.deps.locations.writeTargets(storageClass);
|
|
489
|
+
const target = writeSet[0];
|
|
487
490
|
if (!target) continue;
|
|
491
|
+
const inWriteSet = new Set(writeSet.map((loc) => loc.id));
|
|
488
492
|
if (laneOf(storageClass) === "media") {
|
|
489
493
|
for (const source of this.deps.locations.listLocations({ type: storageClass })) {
|
|
490
|
-
if (source.id
|
|
494
|
+
if (inWriteSet.has(source.id)) continue;
|
|
491
495
|
const count = await unanswerable(this.deps.participants.analytics.residue({
|
|
492
496
|
toLocationId: target.id,
|
|
493
497
|
fromLocationId: source.id,
|
|
@@ -505,7 +509,7 @@ var StorageMigrationCoordinator = class {
|
|
|
505
509
|
continue;
|
|
506
510
|
}
|
|
507
511
|
for (const source of this.deps.locations.listLocations({ type: storageClass })) {
|
|
508
|
-
if (source.id
|
|
512
|
+
if (inWriteSet.has(source.id)) continue;
|
|
509
513
|
const census = await unanswerable(this.deps.participants.recorder.residue({
|
|
510
514
|
fromLocationId: source.id,
|
|
511
515
|
footageClass: storageClass === "recordingsLow" ? "recordingsLow" : "recordings"
|
|
@@ -523,12 +527,12 @@ var StorageMigrationCoordinator = class {
|
|
|
523
527
|
return out;
|
|
524
528
|
}
|
|
525
529
|
/**
|
|
526
|
-
* Run the DRAIN half alone, against classes whose
|
|
530
|
+
* Run the DRAIN half alone, against classes whose write set has already moved.
|
|
527
531
|
*
|
|
528
532
|
* ## Why this is a second verb rather than a looser `start`
|
|
529
533
|
*
|
|
530
|
-
* `start` refuses a destination that is already the class's
|
|
531
|
-
* (`"recordingsLow:ssd" is already the "recordingsLow"
|
|
534
|
+
* `start` refuses a destination that is already the class's only write target
|
|
535
|
+
* (`"recordingsLow:ssd" is already the only write target for "recordingsLow"`). That refusal
|
|
532
536
|
* is correct and it is load-bearing: there is genuinely nothing left to
|
|
533
537
|
* repoint, and an operator must never be able to re-repoint a migrated class
|
|
534
538
|
* by accident. Making `start` idempotent — "an already-repointed class
|
|
@@ -537,7 +541,7 @@ var StorageMigrationCoordinator = class {
|
|
|
537
541
|
* operator reads ("pauses the writers…") would be a lie half the time.
|
|
538
542
|
*
|
|
539
543
|
* `drain` instead cannot repoint AT ALL: it never touches
|
|
540
|
-
* `
|
|
544
|
+
* `setWriteTargets`, and its job starts in `draining` with `repointed`
|
|
541
545
|
* already true, so the `repointing` / `refreshing` / `resuming` blocks of
|
|
542
546
|
* {@link run} are behind it and unreachable. The two verbs are disjoint, and
|
|
543
547
|
* `start`'s refusal keeps meaning exactly what it meant.
|
|
@@ -547,7 +551,7 @@ var StorageMigrationCoordinator = class {
|
|
|
547
551
|
* The finished job is the audit of what happened; re-opening it destroys
|
|
548
552
|
* that. And a drain is needed in cases where no migration job ever existed
|
|
549
553
|
* (a mover armed by hand, footage stranded on a location an operator added
|
|
550
|
-
* and then
|
|
554
|
+
* and then disabled again). One job = one operation, and the work list comes
|
|
551
555
|
* from {@link residue} — the archive — not from what a previous job believed.
|
|
552
556
|
*/
|
|
553
557
|
async drain(input) {
|
|
@@ -570,9 +574,9 @@ var StorageMigrationCoordinator = class {
|
|
|
570
574
|
const destinations = {};
|
|
571
575
|
for (const storageClass of input.classes) {
|
|
572
576
|
const remaining = residue.filter((entry) => entry.storageClass === storageClass);
|
|
573
|
-
if (remaining.length === 0) throw new Error(`"${storageClass}" has nothing left outside its
|
|
577
|
+
if (remaining.length === 0) throw new Error(`"${storageClass}" has nothing left outside its write set — there is nothing to drain. To CHANGE where it writes, use storageMigration.start with a new destination.`);
|
|
574
578
|
for (const entry of remaining) {
|
|
575
|
-
if (this.deps.locations.getLocationById(entry.toLocationId)?.enabled === false) throw new Error(`The "${storageClass}" drain destination "${entry.toLocationId}" is DISABLED — a disabled location takes no writes, and a drain is the largest write there is. Enable it on the Storage screen
|
|
579
|
+
if (this.deps.locations.getLocationById(entry.toLocationId)?.enabled === false) throw new Error(`The "${storageClass}" drain destination "${entry.toLocationId}" is DISABLED — a disabled location takes no writes, and a drain is the largest write there is. Enable it on the Storage screen first.`);
|
|
576
580
|
moves.push({
|
|
577
581
|
storageClass,
|
|
578
582
|
fromLocationId: entry.fromLocationId,
|
|
@@ -690,8 +694,13 @@ var StorageMigrationCoordinator = class {
|
|
|
690
694
|
await this.setPhase(job, "repointing");
|
|
691
695
|
}
|
|
692
696
|
if (job.phase === "repointing") {
|
|
693
|
-
const targets =
|
|
694
|
-
|
|
697
|
+
const targets = /* @__PURE__ */ new Map();
|
|
698
|
+
for (const move of job.moves) {
|
|
699
|
+
if (move.freezeSource === true) continue;
|
|
700
|
+
targets.set(move.toLocationId, true);
|
|
701
|
+
targets.set(move.fromLocationId, false);
|
|
702
|
+
}
|
|
703
|
+
if (targets.size > 0) await this.deps.locations.setWriteTargets(targets);
|
|
695
704
|
job.repointed = true;
|
|
696
705
|
await this.setPhase(job, "refreshing");
|
|
697
706
|
}
|
|
@@ -890,9 +899,9 @@ var StorageMigrationCoordinator = class {
|
|
|
890
899
|
for (const move of job.moves) if (move.state !== "done") throw new Error(`${move.storageClass} move did not complete verification`);
|
|
891
900
|
}
|
|
892
901
|
/**
|
|
893
|
-
* After a from→to copy whose source was NOT
|
|
894
|
-
*
|
|
895
|
-
* naming a
|
|
902
|
+
* After a from→to copy whose source was NOT a write target: stop new writes
|
|
903
|
+
* to `from`. The rest of the write set is unchanged (that is the whole point
|
|
904
|
+
* of naming a source outside it). A location that is already disabled is
|
|
896
905
|
* left alone.
|
|
897
906
|
*/
|
|
898
907
|
freezeDrainedSources(job) {
|
|
@@ -1837,33 +1846,77 @@ var StorageOrchestratorService = class {
|
|
|
1837
1846
|
backfilled: backfill.length,
|
|
1838
1847
|
total: this.locations.size
|
|
1839
1848
|
} });
|
|
1840
|
-
this.
|
|
1849
|
+
this.migrateOntoTheWriteSet(rows);
|
|
1841
1850
|
}
|
|
1842
1851
|
/**
|
|
1843
|
-
*
|
|
1852
|
+
* ONE explicit migration onto the enabled write set (D383), run once per
|
|
1853
|
+
* boot against the rows that came off disk. Idempotent: the second boot finds
|
|
1854
|
+
* nothing to do.
|
|
1855
|
+
*
|
|
1856
|
+
* Two steps, in order, and each one is a WRITE — the point is that "absent"
|
|
1857
|
+
* stops existing rather than being re-interpreted on every read forever:
|
|
1844
1858
|
*
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1848
|
-
*
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1859
|
+
* 1. A flagless row (persisted before `enabled` existed) is stamped
|
|
1860
|
+
* `enabled: true`. Absence already meant active, so this preserves
|
|
1861
|
+
* behaviour exactly; it is the operator's "the initial locations are
|
|
1862
|
+
* enabled" spelled into the store.
|
|
1863
|
+
* 2. A type left with NO enabled row — only reachable from a hand-edited
|
|
1864
|
+
* store or a row an older build wrote — gets one enabled back, chosen as
|
|
1865
|
+
* the row the RETIRED default flag named, else the `:default`-slugged
|
|
1866
|
+
* row, else the lexicographically first. Loudly: a class with no write
|
|
1867
|
+
* target records nowhere, and refusing to repair it here would leave the
|
|
1868
|
+
* hub silently unable to write with no operator action pending.
|
|
1852
1869
|
*
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
1855
|
-
*
|
|
1856
|
-
* path is deliberately left working, because narrowing a READ is a worse bug
|
|
1857
|
-
* than the one this line reports.
|
|
1870
|
+
* An operator's explicit `enabled: false` is never reversed by step 1 — only
|
|
1871
|
+
* step 2 touches it, and only when the alternative is a class that cannot
|
|
1872
|
+
* write at all.
|
|
1858
1873
|
*/
|
|
1859
|
-
|
|
1874
|
+
migrateOntoTheWriteSet(rows) {
|
|
1875
|
+
const legacyDefaultOfType = /* @__PURE__ */ new Map();
|
|
1876
|
+
for (const row of rows) if (row.isDefault === true) legacyDefaultOfType.set(row.type, row.id);
|
|
1877
|
+
const stamped = [];
|
|
1860
1878
|
for (const loc of this.locations.values()) {
|
|
1861
|
-
if (
|
|
1862
|
-
this.
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
}
|
|
1879
|
+
if (loc.enabled !== void 0) continue;
|
|
1880
|
+
this.persistMigrated({
|
|
1881
|
+
...loc,
|
|
1882
|
+
enabled: true
|
|
1883
|
+
});
|
|
1884
|
+
stamped.push(loc.id);
|
|
1885
|
+
}
|
|
1886
|
+
const repaired = [];
|
|
1887
|
+
for (const type of new Set([...this.locations.values()].map((l) => l.type))) {
|
|
1888
|
+
const ofType = [...this.locations.values()].filter((l) => l.type === type);
|
|
1889
|
+
if (ofType.some((l) => require_dist.isLocationEnabled(l))) continue;
|
|
1890
|
+
const legacyDefault = legacyDefaultOfType.get(type);
|
|
1891
|
+
const pick = ofType.find((l) => l.id === legacyDefault) ?? ofType.find((l) => l.id === `${type}:default`) ?? ofType.toSorted((a, b) => a.id.localeCompare(b.id))[0];
|
|
1892
|
+
if (!pick) continue;
|
|
1893
|
+
this.persistMigrated({
|
|
1894
|
+
...pick,
|
|
1895
|
+
enabled: true
|
|
1896
|
+
});
|
|
1897
|
+
repaired.push(pick.id);
|
|
1866
1898
|
}
|
|
1899
|
+
if (stamped.length > 0 || repaired.length > 0) this.logger.info("storage-orchestrator: migrated locations onto the enabled write set", { meta: {
|
|
1900
|
+
stamped,
|
|
1901
|
+
repaired
|
|
1902
|
+
} });
|
|
1903
|
+
for (const id of repaired) this.logger.warn("storage-orchestrator: a storage type had NO enabled location — re-enabled one so it can write. Every other location of that type stays read-only until you enable it.", { meta: { id } });
|
|
1904
|
+
}
|
|
1905
|
+
/** Apply one migrated row in memory and mirror it to the store. */
|
|
1906
|
+
persistMigrated(next) {
|
|
1907
|
+
const withStamp = {
|
|
1908
|
+
...next,
|
|
1909
|
+
updatedAt: Date.now()
|
|
1910
|
+
};
|
|
1911
|
+
this.locations.set(withStamp.id, withStamp);
|
|
1912
|
+
const store = this.locationStore;
|
|
1913
|
+
if (!store) return;
|
|
1914
|
+
store.upsert(withStamp).catch((err) => {
|
|
1915
|
+
this.logger.error("storage-orchestrator: write-set migration persist failed", { meta: {
|
|
1916
|
+
id: withStamp.id,
|
|
1917
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1918
|
+
} });
|
|
1919
|
+
});
|
|
1867
1920
|
}
|
|
1868
1921
|
/**
|
|
1869
1922
|
* Inject the declaration-driven cardinality source. Called once by the
|
|
@@ -1918,7 +1971,7 @@ var StorageOrchestratorService = class {
|
|
|
1918
1971
|
const rows = await this.locationStore.loadAll();
|
|
1919
1972
|
for (const loc of rows) this.locations.set(loc.id, loc);
|
|
1920
1973
|
this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
|
|
1921
|
-
this.
|
|
1974
|
+
this.migrateOntoTheWriteSet(rows);
|
|
1922
1975
|
}
|
|
1923
1976
|
/**
|
|
1924
1977
|
* Boot backfill (SP1): stamp `nodeId` on every persisted node-local
|
|
@@ -1967,40 +2020,51 @@ var StorageOrchestratorService = class {
|
|
|
1967
2020
|
const all = [...this.locations.values()];
|
|
1968
2021
|
return filter?.type ? all.filter((l) => l.type === filter.type) : all;
|
|
1969
2022
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2023
|
+
/**
|
|
2024
|
+
* The WRITE SET of a type: every enabled location of it, in stable id order.
|
|
2025
|
+
* This replaced `getDefaultLocation` (D383) — a type does not have one write
|
|
2026
|
+
* target any more, it has a pool, and every consumer that must pick one picks
|
|
2027
|
+
* from here by its own rule (the recorder by headroom, a drain by an operator
|
|
2028
|
+
* -named destination).
|
|
2029
|
+
*/
|
|
2030
|
+
writeTargets(type) {
|
|
2031
|
+
return [...this.locations.values()].filter((loc) => loc.type === type && require_dist.isLocationEnabled(loc)).toSorted((a, b) => a.id.localeCompare(b.id));
|
|
1973
2032
|
}
|
|
1974
2033
|
/**
|
|
1975
|
-
*
|
|
1976
|
-
*
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
2034
|
+
* Change the WRITE SET as one compensated commit: each named location is
|
|
2035
|
+
* enabled (it joins the pool) or disabled (it becomes read-only — still read,
|
|
2036
|
+
* still played back, still age-swept, still drained).
|
|
2037
|
+
*
|
|
2038
|
+
* This is the cutover verb, and its only caller is the migration coordinator,
|
|
2039
|
+
* which persists its `repointing` phase BEFORE calling so a crash mid-write
|
|
2040
|
+
* is recovered by replaying the same idempotent set before writers resume.
|
|
2041
|
+
*
|
|
2042
|
+
* It moves ONLY the locations it is given: a cutover from A to C on a class
|
|
2043
|
+
* that also writes to B leaves B in the pool, because the operator's pool is
|
|
2044
|
+
* the authority and a migration is not a licence to shrink it. The commit is
|
|
2045
|
+
* refused when it would leave a write-serving type with nothing enabled —
|
|
2046
|
+
* the same refusal `upsertLocation` applies, judged on the RESULT so an
|
|
2047
|
+
* enable and a disable in one commit are weighed together.
|
|
1979
2048
|
*/
|
|
1980
|
-
async
|
|
1981
|
-
if (
|
|
2049
|
+
async setWriteTargets(enabledByLocationId) {
|
|
2050
|
+
if (enabledByLocationId.size === 0) return;
|
|
1982
2051
|
const now = Date.now();
|
|
1983
2052
|
const nextById = new Map(this.locations);
|
|
1984
2053
|
const touched = /* @__PURE__ */ new Map();
|
|
1985
|
-
for (const [
|
|
1986
|
-
const
|
|
1987
|
-
if (!
|
|
1988
|
-
if (
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
isDefault,
|
|
1997
|
-
...enable ? { enabled: true } : {},
|
|
1998
|
-
updatedAt: now
|
|
1999
|
-
};
|
|
2000
|
-
nextById.set(id, updated);
|
|
2001
|
-
touched.set(id, updated);
|
|
2002
|
-
}
|
|
2054
|
+
for (const [id, enabled] of enabledByLocationId) {
|
|
2055
|
+
const location = nextById.get(id);
|
|
2056
|
+
if (!location) throw new Error(`Storage location "${id}" not found`);
|
|
2057
|
+
if (location.enabled === enabled) continue;
|
|
2058
|
+
const updated = {
|
|
2059
|
+
...location,
|
|
2060
|
+
enabled,
|
|
2061
|
+
updatedAt: now
|
|
2062
|
+
};
|
|
2063
|
+
nextById.set(id, updated);
|
|
2064
|
+
touched.set(id, updated);
|
|
2003
2065
|
}
|
|
2066
|
+
if (touched.size === 0) return;
|
|
2067
|
+
refuseTypeWithoutWriteTarget(touched.values(), nextById.values());
|
|
2004
2068
|
if (this.locationStore) {
|
|
2005
2069
|
const previous = /* @__PURE__ */ new Map();
|
|
2006
2070
|
for (const id of touched.keys()) {
|
|
@@ -2020,13 +2084,10 @@ var StorageOrchestratorService = class {
|
|
|
2020
2084
|
for (const [id, location] of nextById) this.locations.set(id, location);
|
|
2021
2085
|
}
|
|
2022
2086
|
/**
|
|
2023
|
-
* Insert or update a location.
|
|
2024
|
-
* demotes any other default for the same type — operators always see
|
|
2025
|
-
* exactly one default per type.
|
|
2087
|
+
* Insert or update a location.
|
|
2026
2088
|
*
|
|
2027
|
-
* When a persistence store is wired (Task 6), the
|
|
2028
|
-
*
|
|
2029
|
-
* mutation returns. Persistence errors propagate to the caller.
|
|
2089
|
+
* When a persistence store is wired (Task 6), the record is mirrored to it;
|
|
2090
|
+
* the mirror is fire-and-forget and its errors reach the logger.
|
|
2030
2091
|
*/
|
|
2031
2092
|
/**
|
|
2032
2093
|
* Refuse a `(location kind, provider)` pair the kind cannot use.
|
|
@@ -2112,30 +2173,17 @@ var StorageOrchestratorService = class {
|
|
|
2112
2173
|
updatedAt: now
|
|
2113
2174
|
};
|
|
2114
2175
|
refuseLastWriteTargetDisabled(existing, next, this.locations.values());
|
|
2115
|
-
const demoted = [];
|
|
2116
|
-
if (next.isDefault) {
|
|
2117
|
-
for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
|
|
2118
|
-
const updated = {
|
|
2119
|
-
...otherLoc,
|
|
2120
|
-
isDefault: false,
|
|
2121
|
-
updatedAt: now
|
|
2122
|
-
};
|
|
2123
|
-
this.locations.set(otherId, updated);
|
|
2124
|
-
demoted.push(updated);
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
2176
|
this.locations.set(next.id, next);
|
|
2128
2177
|
this.logger.debug("storage-orchestrator: upsertLocation", { meta: {
|
|
2129
2178
|
id: next.id,
|
|
2130
2179
|
type: next.type,
|
|
2131
2180
|
providerId: next.providerId,
|
|
2132
|
-
|
|
2181
|
+
enabled: next.enabled
|
|
2133
2182
|
} });
|
|
2134
2183
|
if (this.locationStore) {
|
|
2135
2184
|
const store = this.locationStore;
|
|
2136
2185
|
(async () => {
|
|
2137
2186
|
try {
|
|
2138
|
-
for (const d of demoted) await store.upsert(d);
|
|
2139
2187
|
await store.upsert(next);
|
|
2140
2188
|
} catch (err) {
|
|
2141
2189
|
this.logger.error("storage-orchestrator: upsert persistence failed", { meta: {
|
|
@@ -2150,8 +2198,7 @@ var StorageOrchestratorService = class {
|
|
|
2150
2198
|
/**
|
|
2151
2199
|
* Remove a location. Refuses the last location of a type and the last
|
|
2152
2200
|
* enabled location of a type. `isSystem` is informational — a frozen
|
|
2153
|
-
* recordings `:default` is deletable once a sibling is live.
|
|
2154
|
-
* the flagged default promotes an enabled sibling. Occupancy still
|
|
2201
|
+
* recordings `:default` is deletable once a sibling is live. Occupancy still
|
|
2155
2202
|
* refuses unless `force` is set; force never overrides uniqueness /
|
|
2156
2203
|
* last-enabled.
|
|
2157
2204
|
*/
|
|
@@ -2162,8 +2209,6 @@ var StorageOrchestratorService = class {
|
|
|
2162
2209
|
if (ofType.length <= 1) throw new Error(`Cannot delete "${id}" — it is the only location for type "${loc.type}"`);
|
|
2163
2210
|
const enabledOfType = ofType.filter((l) => l.enabled !== false);
|
|
2164
2211
|
if (loc.enabled !== false && enabledOfType.length <= 1) throw new Error(`Cannot delete "${id}" — it is the only enabled location for type "${loc.type}"`);
|
|
2165
|
-
const successor = loc.isDefault === true ? ofType.find((l) => l.id !== id && l.enabled !== false) : void 0;
|
|
2166
|
-
if (loc.isDefault === true && successor === void 0) throw new Error(`Cannot delete "${id}" — it is the only enabled location for type "${loc.type}"`);
|
|
2167
2212
|
const occupancy = await this.probeOccupancy(loc);
|
|
2168
2213
|
if (occupancy !== "empty") {
|
|
2169
2214
|
if (options?.force !== true) throw new Error(occupancy === "occupied" ? `Storage location "${id}" still holds data — drain it first (storage migration / relocate), or pass force to delete the record anyway and strand what is on it` : `Storage location "${id}" could not be checked for remaining data (unreachable provider, another node, or an unmounted root) — pass force to delete the record anyway`);
|
|
@@ -2173,21 +2218,6 @@ var StorageOrchestratorService = class {
|
|
|
2173
2218
|
occupancy
|
|
2174
2219
|
} });
|
|
2175
2220
|
}
|
|
2176
|
-
if (successor !== void 0 && successor.isDefault !== true) {
|
|
2177
|
-
const now = Date.now();
|
|
2178
|
-
const promoted = {
|
|
2179
|
-
...successor,
|
|
2180
|
-
isDefault: true,
|
|
2181
|
-
updatedAt: now
|
|
2182
|
-
};
|
|
2183
|
-
this.locations.set(successor.id, promoted);
|
|
2184
|
-
if (this.locationStore) this.locationStore.upsert(promoted).catch((err) => {
|
|
2185
|
-
this.logger.error("storage-orchestrator: default promotion persist failed", { meta: {
|
|
2186
|
-
id: successor.id,
|
|
2187
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2188
|
-
} });
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
2221
|
this.locations.delete(id);
|
|
2192
2222
|
if (this.locationStore) this.locationStore.delete(id).catch((err) => {
|
|
2193
2223
|
this.logger.error("storage-orchestrator: delete persistence failed", { meta: {
|
|
@@ -2266,9 +2296,17 @@ var StorageOrchestratorService = class {
|
|
|
2266
2296
|
if (!loc) throw new Error(`Storage location "${ref}" not found`);
|
|
2267
2297
|
return loc;
|
|
2268
2298
|
}
|
|
2269
|
-
const
|
|
2270
|
-
if (
|
|
2271
|
-
|
|
2299
|
+
const ofType = [...this.locations.values()].filter((loc) => loc.type === ref);
|
|
2300
|
+
if (ofType.length === 1) return ofType[0];
|
|
2301
|
+
if (ofType.length === 0) throw new Error(`No storage location configured for type "${ref}" — register one or pass a fully-qualified id (\`<type>:<slug>\`)`);
|
|
2302
|
+
const slugged = ofType.find((loc) => loc.id === `${ref}:default`);
|
|
2303
|
+
if (!slugged) throw new Error(`Storage type "${ref}" has ${String(ofType.length)} locations (${ofType.map((loc) => loc.id).toSorted().join(", ")}) — a bare type ref is ambiguous, pass a fully-qualified id (\`<type>:<slug>\`)`);
|
|
2304
|
+
this.logger.warn("storage-orchestrator: DEPRECATED bare-type ref on a type with several locations — resolved to the legacy `:default`-slugged row. Pass a fully-qualified location id.", { meta: {
|
|
2305
|
+
ref,
|
|
2306
|
+
resolved: slugged.id,
|
|
2307
|
+
candidates: ofType.length
|
|
2308
|
+
} });
|
|
2309
|
+
return slugged;
|
|
2272
2310
|
}
|
|
2273
2311
|
/**
|
|
2274
2312
|
* Find the storage-provider that backs a given location — the entry point
|
|
@@ -2372,9 +2410,11 @@ var StorageOrchestratorService = class {
|
|
|
2372
2410
|
* declarations on a populated map is a no-op (each declared `id`
|
|
2373
2411
|
* already resolves to an existing `<id>:default`).
|
|
2374
2412
|
*
|
|
2375
|
-
*
|
|
2376
|
-
* `
|
|
2377
|
-
*
|
|
2413
|
+
* The seeded row is born ENABLED — a fresh install must be able to write the
|
|
2414
|
+
* moment it boots, and `enabled` is the only write switch there is (D383).
|
|
2415
|
+
* The `:default` suffix is a NAME kept for idempotency across existing
|
|
2416
|
+
* installs, not a flag: the stored `id` is `'recordings:default'` so it
|
|
2417
|
+
* satisfies the
|
|
2378
2418
|
* `^[a-z][a-z0-9-]*:[a-z0-9-]+$` regex on `StorageLocationSchema.id`.
|
|
2379
2419
|
*
|
|
2380
2420
|
* `basePath` is the storage root (e.g. `<CAMSTACK_DATA>`). Each
|
|
@@ -2399,7 +2439,7 @@ var StorageOrchestratorService = class {
|
|
|
2399
2439
|
displayName: d.displayName,
|
|
2400
2440
|
providerId: input.providerId,
|
|
2401
2441
|
config: { basePath: base },
|
|
2402
|
-
|
|
2442
|
+
enabled: true,
|
|
2403
2443
|
isSystem: false
|
|
2404
2444
|
});
|
|
2405
2445
|
added++;
|
|
@@ -2423,7 +2463,7 @@ function resolveEnabled(input, existing, typeAlreadyHasLocation) {
|
|
|
2423
2463
|
const requested = input.enabled ?? existing?.enabled;
|
|
2424
2464
|
if (requested === void 0) {
|
|
2425
2465
|
if (existing) return {};
|
|
2426
|
-
return { enabled:
|
|
2466
|
+
return { enabled: !typeAlreadyHasLocation };
|
|
2427
2467
|
}
|
|
2428
2468
|
return { enabled: requested };
|
|
2429
2469
|
}
|
|
@@ -2432,8 +2472,7 @@ var OPTIONAL_WRITE_TYPES = new Set(["backups"]);
|
|
|
2432
2472
|
/**
|
|
2433
2473
|
* Refuse an UPDATE that would leave a write-serving type with no enabled
|
|
2434
2474
|
* location. Creates are exempt (an explicit `enabled: false` on the first
|
|
2435
|
-
* location is already a named test).
|
|
2436
|
-
* is the write set.
|
|
2475
|
+
* location is already a named test). Enabled IS the write set.
|
|
2437
2476
|
*/
|
|
2438
2477
|
function refuseLastWriteTargetDisabled(existing, next, locations) {
|
|
2439
2478
|
if (OPTIONAL_WRITE_TYPES.has(next.type)) return;
|
|
@@ -2446,6 +2485,23 @@ function refuseLastWriteTargetDisabled(existing, next, locations) {
|
|
|
2446
2485
|
}
|
|
2447
2486
|
throw new Error(`Cannot disable "${next.id}": it is the last enabled location of type "${next.type}". Enable another location of this type first.`);
|
|
2448
2487
|
}
|
|
2488
|
+
/**
|
|
2489
|
+
* The same refusal, judged on a whole COMMIT rather than one row: a
|
|
2490
|
+
* multi-location change (a cutover enables its destination and disables its
|
|
2491
|
+
* source in one go) must be weighed on its RESULT, or the disable half looks
|
|
2492
|
+
* like the last write target leaving while the enable half is already in the
|
|
2493
|
+
* same map.
|
|
2494
|
+
*/
|
|
2495
|
+
function refuseTypeWithoutWriteTarget(touched, result) {
|
|
2496
|
+
const rows = [...result];
|
|
2497
|
+
for (const type of new Set([...touched].map((loc) => loc.type))) {
|
|
2498
|
+
if (OPTIONAL_WRITE_TYPES.has(type)) continue;
|
|
2499
|
+
const ofType = rows.filter((loc) => loc.type === type);
|
|
2500
|
+
if (ofType.length === 0) continue;
|
|
2501
|
+
if (ofType.some((loc) => require_dist.isLocationEnabled(loc))) continue;
|
|
2502
|
+
throw new Error(`Refusing the write-set change: it would leave type "${type}" with no enabled location. Enable one of ${ofType.map((loc) => loc.id).toSorted().join(", ")} first.`);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2449
2505
|
//#endregion
|
|
2450
2506
|
//#region src/builtins/storage-orchestrator/storage-pressure-manager.ts
|
|
2451
2507
|
var DEFAULT_MAX_ROUNDS = 8;
|
|
@@ -2687,13 +2743,9 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
2687
2743
|
const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
|
|
2688
2744
|
return Promise.all(rows.map(async (loc) => ({
|
|
2689
2745
|
...this.redacted(loc),
|
|
2690
|
-
capacity: loc
|
|
2746
|
+
capacity: require_dist.isLocationEnabled(loc) ? await this.localCapacityOf(loc) : null
|
|
2691
2747
|
})));
|
|
2692
2748
|
},
|
|
2693
|
-
getDefaultLocation: async ({ type }) => {
|
|
2694
|
-
const loc = service.getDefaultLocation(type);
|
|
2695
|
-
return loc === null ? null : this.redacted(loc);
|
|
2696
|
-
},
|
|
2697
2749
|
listLocationDeclarations: async () => service.listDeclarations(),
|
|
2698
2750
|
upsertLocation: async (input) => {
|
|
2699
2751
|
const stored = service.getLocationById(input.id);
|