@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { At as storageCapability, E as StorageMigrationJobSchema, L as collectSecretConfigKeys, Ot as settingsStoreCapability, Rt as BaseAddon, T as StorageLocationTypeSchema, g as REDACTED_SECRET, in as parseJsonObject, jt as storageMigrationCapability, m as LegacyStorageLocationDefaultSchema, rt as isLocationEnabled, w as StorageCleanupJobSchema } from "../../dist-CiHYspij.mjs";
|
|
2
2
|
import { t as HUB_CAP_FWD_ACTION } from "../../hub-cap-forward-BdT320dq.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -188,7 +188,7 @@ function mapLocationToRow(loc) {
|
|
|
188
188
|
display_name: loc.displayName,
|
|
189
189
|
provider_id: loc.providerId,
|
|
190
190
|
config: storedConfig,
|
|
191
|
-
is_default:
|
|
191
|
+
is_default: 0,
|
|
192
192
|
is_system: loc.isSystem ? 1 : 0,
|
|
193
193
|
created_at: loc.createdAt,
|
|
194
194
|
updated_at: loc.updatedAt
|
|
@@ -222,7 +222,7 @@ function mapRowToLocation(row) {
|
|
|
222
222
|
config,
|
|
223
223
|
...nodeId !== void 0 ? { nodeId } : {},
|
|
224
224
|
...enabled !== void 0 ? { enabled } : {},
|
|
225
|
-
isDefault: row["is_default"] === 1 || row["is_default"] === true,
|
|
225
|
+
...LegacyStorageLocationDefaultSchema.parse({ isDefault: row["is_default"] === 1 || row["is_default"] === true }),
|
|
226
226
|
isSystem: row["is_system"] === 1 || row["is_system"] === true,
|
|
227
227
|
createdAt: Number(row["created_at"] ?? 0),
|
|
228
228
|
updatedAt: Number(row["updated_at"] ?? 0)
|
|
@@ -290,16 +290,18 @@ var StorageMigrationCoordinator = class {
|
|
|
290
290
|
const targetId = input.destinations[storageClass];
|
|
291
291
|
if (targetId === void 0) continue;
|
|
292
292
|
const namedSourceId = input.sources?.[storageClass];
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
if (!
|
|
293
|
+
const writeSet = this.deps.locations.writeTargets(storageClass);
|
|
294
|
+
const implicitSources = writeSet.filter((loc) => loc.id !== targetId);
|
|
295
|
+
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\`.`);
|
|
296
|
+
const source = namedSourceId ? this.deps.locations.getLocationById(namedSourceId) : implicitSources[0];
|
|
297
|
+
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.`);
|
|
296
298
|
if (source.type !== storageClass) throw new Error(`Storage location "${source.id}" is type "${source.type}", expected "${storageClass}"`);
|
|
297
299
|
const target = this.deps.locations.getLocationById(targetId);
|
|
298
300
|
if (!target) throw new Error(`Storage location "${targetId}" not found`);
|
|
299
301
|
if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
|
|
300
|
-
if (source.id === target.id) throw new Error(namedSourceId ? `Source and destination are the same location ("${targetId}")` : `Storage location "${targetId}" is already the "${storageClass}"
|
|
301
|
-
const freezeSource =
|
|
302
|
-
if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" — the migration can
|
|
302
|
+
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}"`);
|
|
303
|
+
const freezeSource = !writeSet.some((loc) => loc.id === source.id);
|
|
304
|
+
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.`);
|
|
303
305
|
moves.push({
|
|
304
306
|
storageClass,
|
|
305
307
|
fromLocationId: source.id,
|
|
@@ -316,7 +318,7 @@ var StorageMigrationCoordinator = class {
|
|
|
316
318
|
if (moves.some((move) => move.storageClass === "eventMedia")) {
|
|
317
319
|
const unstamped = await this.deps.participants.analytics.countUnstamped();
|
|
318
320
|
if (unstamped === null) {
|
|
319
|
-
if (mode === "nonBlocking") throw new Error("could not determine whether any event-media row still carries no locationId. A non-blocking cutover
|
|
321
|
+
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.");
|
|
320
322
|
findings.push({
|
|
321
323
|
code: "unstampedEventMediaRows",
|
|
322
324
|
storageClass: "eventMedia",
|
|
@@ -325,7 +327,7 @@ var StorageMigrationCoordinator = class {
|
|
|
325
327
|
} else if (unstamped.anyPresent) {
|
|
326
328
|
const scale = unstamped.total === null ? "an unknown number of" : unstamped.total.toString();
|
|
327
329
|
const perLane = `${describeUnstamped(unstamped.media)} media, ${describeUnstamped(unstamped.retrainFrames)} retrain frames`;
|
|
328
|
-
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.
|
|
330
|
+
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"}" }).`);
|
|
329
331
|
findings.push({
|
|
330
332
|
code: "unstampedEventMediaRows",
|
|
331
333
|
storageClass: "eventMedia",
|
|
@@ -464,7 +466,7 @@ var StorageMigrationCoordinator = class {
|
|
|
464
466
|
* remaining" action.
|
|
465
467
|
*
|
|
466
468
|
* A class appears here only when something is (or might be) left on a
|
|
467
|
-
* location
|
|
469
|
+
* location outside its write set. An empty result therefore means exactly
|
|
468
470
|
* "there is nothing to drain", which is what lets the UI offer the action
|
|
469
471
|
* only when it is true, and what lets {@link drain} refuse rather than start
|
|
470
472
|
* a job that would move nothing and report `done` — the failure mode D295
|
|
@@ -478,11 +480,13 @@ var StorageMigrationCoordinator = class {
|
|
|
478
480
|
const out = [];
|
|
479
481
|
for (const storageClass of STORAGE_CLASSES) {
|
|
480
482
|
if (!MOVER_CLASSES.includes(storageClass)) continue;
|
|
481
|
-
const
|
|
483
|
+
const writeSet = this.deps.locations.writeTargets(storageClass);
|
|
484
|
+
const target = writeSet[0];
|
|
482
485
|
if (!target) continue;
|
|
486
|
+
const inWriteSet = new Set(writeSet.map((loc) => loc.id));
|
|
483
487
|
if (laneOf(storageClass) === "media") {
|
|
484
488
|
for (const source of this.deps.locations.listLocations({ type: storageClass })) {
|
|
485
|
-
if (source.id
|
|
489
|
+
if (inWriteSet.has(source.id)) continue;
|
|
486
490
|
const count = await unanswerable(this.deps.participants.analytics.residue({
|
|
487
491
|
toLocationId: target.id,
|
|
488
492
|
fromLocationId: source.id,
|
|
@@ -500,7 +504,7 @@ var StorageMigrationCoordinator = class {
|
|
|
500
504
|
continue;
|
|
501
505
|
}
|
|
502
506
|
for (const source of this.deps.locations.listLocations({ type: storageClass })) {
|
|
503
|
-
if (source.id
|
|
507
|
+
if (inWriteSet.has(source.id)) continue;
|
|
504
508
|
const census = await unanswerable(this.deps.participants.recorder.residue({
|
|
505
509
|
fromLocationId: source.id,
|
|
506
510
|
footageClass: storageClass === "recordingsLow" ? "recordingsLow" : "recordings"
|
|
@@ -518,12 +522,12 @@ var StorageMigrationCoordinator = class {
|
|
|
518
522
|
return out;
|
|
519
523
|
}
|
|
520
524
|
/**
|
|
521
|
-
* Run the DRAIN half alone, against classes whose
|
|
525
|
+
* Run the DRAIN half alone, against classes whose write set has already moved.
|
|
522
526
|
*
|
|
523
527
|
* ## Why this is a second verb rather than a looser `start`
|
|
524
528
|
*
|
|
525
|
-
* `start` refuses a destination that is already the class's
|
|
526
|
-
* (`"recordingsLow:ssd" is already the "recordingsLow"
|
|
529
|
+
* `start` refuses a destination that is already the class's only write target
|
|
530
|
+
* (`"recordingsLow:ssd" is already the only write target for "recordingsLow"`). That refusal
|
|
527
531
|
* is correct and it is load-bearing: there is genuinely nothing left to
|
|
528
532
|
* repoint, and an operator must never be able to re-repoint a migrated class
|
|
529
533
|
* by accident. Making `start` idempotent — "an already-repointed class
|
|
@@ -532,7 +536,7 @@ var StorageMigrationCoordinator = class {
|
|
|
532
536
|
* operator reads ("pauses the writers…") would be a lie half the time.
|
|
533
537
|
*
|
|
534
538
|
* `drain` instead cannot repoint AT ALL: it never touches
|
|
535
|
-
* `
|
|
539
|
+
* `setWriteTargets`, and its job starts in `draining` with `repointed`
|
|
536
540
|
* already true, so the `repointing` / `refreshing` / `resuming` blocks of
|
|
537
541
|
* {@link run} are behind it and unreachable. The two verbs are disjoint, and
|
|
538
542
|
* `start`'s refusal keeps meaning exactly what it meant.
|
|
@@ -542,7 +546,7 @@ var StorageMigrationCoordinator = class {
|
|
|
542
546
|
* The finished job is the audit of what happened; re-opening it destroys
|
|
543
547
|
* that. And a drain is needed in cases where no migration job ever existed
|
|
544
548
|
* (a mover armed by hand, footage stranded on a location an operator added
|
|
545
|
-
* and then
|
|
549
|
+
* and then disabled again). One job = one operation, and the work list comes
|
|
546
550
|
* from {@link residue} — the archive — not from what a previous job believed.
|
|
547
551
|
*/
|
|
548
552
|
async drain(input) {
|
|
@@ -565,9 +569,9 @@ var StorageMigrationCoordinator = class {
|
|
|
565
569
|
const destinations = {};
|
|
566
570
|
for (const storageClass of input.classes) {
|
|
567
571
|
const remaining = residue.filter((entry) => entry.storageClass === storageClass);
|
|
568
|
-
if (remaining.length === 0) throw new Error(`"${storageClass}" has nothing left outside its
|
|
572
|
+
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.`);
|
|
569
573
|
for (const entry of remaining) {
|
|
570
|
-
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
|
|
574
|
+
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.`);
|
|
571
575
|
moves.push({
|
|
572
576
|
storageClass,
|
|
573
577
|
fromLocationId: entry.fromLocationId,
|
|
@@ -685,8 +689,13 @@ var StorageMigrationCoordinator = class {
|
|
|
685
689
|
await this.setPhase(job, "repointing");
|
|
686
690
|
}
|
|
687
691
|
if (job.phase === "repointing") {
|
|
688
|
-
const targets =
|
|
689
|
-
|
|
692
|
+
const targets = /* @__PURE__ */ new Map();
|
|
693
|
+
for (const move of job.moves) {
|
|
694
|
+
if (move.freezeSource === true) continue;
|
|
695
|
+
targets.set(move.toLocationId, true);
|
|
696
|
+
targets.set(move.fromLocationId, false);
|
|
697
|
+
}
|
|
698
|
+
if (targets.size > 0) await this.deps.locations.setWriteTargets(targets);
|
|
690
699
|
job.repointed = true;
|
|
691
700
|
await this.setPhase(job, "refreshing");
|
|
692
701
|
}
|
|
@@ -885,9 +894,9 @@ var StorageMigrationCoordinator = class {
|
|
|
885
894
|
for (const move of job.moves) if (move.state !== "done") throw new Error(`${move.storageClass} move did not complete verification`);
|
|
886
895
|
}
|
|
887
896
|
/**
|
|
888
|
-
* After a from→to copy whose source was NOT
|
|
889
|
-
*
|
|
890
|
-
* naming a
|
|
897
|
+
* After a from→to copy whose source was NOT a write target: stop new writes
|
|
898
|
+
* to `from`. The rest of the write set is unchanged (that is the whole point
|
|
899
|
+
* of naming a source outside it). A location that is already disabled is
|
|
891
900
|
* left alone.
|
|
892
901
|
*/
|
|
893
902
|
freezeDrainedSources(job) {
|
|
@@ -1832,33 +1841,77 @@ var StorageOrchestratorService = class {
|
|
|
1832
1841
|
backfilled: backfill.length,
|
|
1833
1842
|
total: this.locations.size
|
|
1834
1843
|
} });
|
|
1835
|
-
this.
|
|
1844
|
+
this.migrateOntoTheWriteSet(rows);
|
|
1836
1845
|
}
|
|
1837
1846
|
/**
|
|
1838
|
-
*
|
|
1847
|
+
* ONE explicit migration onto the enabled write set (D383), run once per
|
|
1848
|
+
* boot against the rows that came off disk. Idempotent: the second boot finds
|
|
1849
|
+
* nothing to do.
|
|
1850
|
+
*
|
|
1851
|
+
* Two steps, in order, and each one is a WRITE — the point is that "absent"
|
|
1852
|
+
* stops existing rather than being re-interpreted on every read forever:
|
|
1839
1853
|
*
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
1854
|
+
* 1. A flagless row (persisted before `enabled` existed) is stamped
|
|
1855
|
+
* `enabled: true`. Absence already meant active, so this preserves
|
|
1856
|
+
* behaviour exactly; it is the operator's "the initial locations are
|
|
1857
|
+
* enabled" spelled into the store.
|
|
1858
|
+
* 2. A type left with NO enabled row — only reachable from a hand-edited
|
|
1859
|
+
* store or a row an older build wrote — gets one enabled back, chosen as
|
|
1860
|
+
* the row the RETIRED default flag named, else the `:default`-slugged
|
|
1861
|
+
* row, else the lexicographically first. Loudly: a class with no write
|
|
1862
|
+
* target records nowhere, and refusing to repair it here would leave the
|
|
1863
|
+
* hub silently unable to write with no operator action pending.
|
|
1847
1864
|
*
|
|
1848
|
-
*
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1851
|
-
* path is deliberately left working, because narrowing a READ is a worse bug
|
|
1852
|
-
* than the one this line reports.
|
|
1865
|
+
* An operator's explicit `enabled: false` is never reversed by step 1 — only
|
|
1866
|
+
* step 2 touches it, and only when the alternative is a class that cannot
|
|
1867
|
+
* write at all.
|
|
1853
1868
|
*/
|
|
1854
|
-
|
|
1869
|
+
migrateOntoTheWriteSet(rows) {
|
|
1870
|
+
const legacyDefaultOfType = /* @__PURE__ */ new Map();
|
|
1871
|
+
for (const row of rows) if (row.isDefault === true) legacyDefaultOfType.set(row.type, row.id);
|
|
1872
|
+
const stamped = [];
|
|
1855
1873
|
for (const loc of this.locations.values()) {
|
|
1856
|
-
if (
|
|
1857
|
-
this.
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
}
|
|
1874
|
+
if (loc.enabled !== void 0) continue;
|
|
1875
|
+
this.persistMigrated({
|
|
1876
|
+
...loc,
|
|
1877
|
+
enabled: true
|
|
1878
|
+
});
|
|
1879
|
+
stamped.push(loc.id);
|
|
1880
|
+
}
|
|
1881
|
+
const repaired = [];
|
|
1882
|
+
for (const type of new Set([...this.locations.values()].map((l) => l.type))) {
|
|
1883
|
+
const ofType = [...this.locations.values()].filter((l) => l.type === type);
|
|
1884
|
+
if (ofType.some((l) => isLocationEnabled(l))) continue;
|
|
1885
|
+
const legacyDefault = legacyDefaultOfType.get(type);
|
|
1886
|
+
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];
|
|
1887
|
+
if (!pick) continue;
|
|
1888
|
+
this.persistMigrated({
|
|
1889
|
+
...pick,
|
|
1890
|
+
enabled: true
|
|
1891
|
+
});
|
|
1892
|
+
repaired.push(pick.id);
|
|
1861
1893
|
}
|
|
1894
|
+
if (stamped.length > 0 || repaired.length > 0) this.logger.info("storage-orchestrator: migrated locations onto the enabled write set", { meta: {
|
|
1895
|
+
stamped,
|
|
1896
|
+
repaired
|
|
1897
|
+
} });
|
|
1898
|
+
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 } });
|
|
1899
|
+
}
|
|
1900
|
+
/** Apply one migrated row in memory and mirror it to the store. */
|
|
1901
|
+
persistMigrated(next) {
|
|
1902
|
+
const withStamp = {
|
|
1903
|
+
...next,
|
|
1904
|
+
updatedAt: Date.now()
|
|
1905
|
+
};
|
|
1906
|
+
this.locations.set(withStamp.id, withStamp);
|
|
1907
|
+
const store = this.locationStore;
|
|
1908
|
+
if (!store) return;
|
|
1909
|
+
store.upsert(withStamp).catch((err) => {
|
|
1910
|
+
this.logger.error("storage-orchestrator: write-set migration persist failed", { meta: {
|
|
1911
|
+
id: withStamp.id,
|
|
1912
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1913
|
+
} });
|
|
1914
|
+
});
|
|
1862
1915
|
}
|
|
1863
1916
|
/**
|
|
1864
1917
|
* Inject the declaration-driven cardinality source. Called once by the
|
|
@@ -1913,7 +1966,7 @@ var StorageOrchestratorService = class {
|
|
|
1913
1966
|
const rows = await this.locationStore.loadAll();
|
|
1914
1967
|
for (const loc of rows) this.locations.set(loc.id, loc);
|
|
1915
1968
|
this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
|
|
1916
|
-
this.
|
|
1969
|
+
this.migrateOntoTheWriteSet(rows);
|
|
1917
1970
|
}
|
|
1918
1971
|
/**
|
|
1919
1972
|
* Boot backfill (SP1): stamp `nodeId` on every persisted node-local
|
|
@@ -1962,40 +2015,51 @@ var StorageOrchestratorService = class {
|
|
|
1962
2015
|
const all = [...this.locations.values()];
|
|
1963
2016
|
return filter?.type ? all.filter((l) => l.type === filter.type) : all;
|
|
1964
2017
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2018
|
+
/**
|
|
2019
|
+
* The WRITE SET of a type: every enabled location of it, in stable id order.
|
|
2020
|
+
* This replaced `getDefaultLocation` (D383) — a type does not have one write
|
|
2021
|
+
* target any more, it has a pool, and every consumer that must pick one picks
|
|
2022
|
+
* from here by its own rule (the recorder by headroom, a drain by an operator
|
|
2023
|
+
* -named destination).
|
|
2024
|
+
*/
|
|
2025
|
+
writeTargets(type) {
|
|
2026
|
+
return [...this.locations.values()].filter((loc) => loc.type === type && isLocationEnabled(loc)).toSorted((a, b) => a.id.localeCompare(b.id));
|
|
1968
2027
|
}
|
|
1969
2028
|
/**
|
|
1970
|
-
*
|
|
1971
|
-
*
|
|
1972
|
-
*
|
|
1973
|
-
*
|
|
2029
|
+
* Change the WRITE SET as one compensated commit: each named location is
|
|
2030
|
+
* enabled (it joins the pool) or disabled (it becomes read-only — still read,
|
|
2031
|
+
* still played back, still age-swept, still drained).
|
|
2032
|
+
*
|
|
2033
|
+
* This is the cutover verb, and its only caller is the migration coordinator,
|
|
2034
|
+
* which persists its `repointing` phase BEFORE calling so a crash mid-write
|
|
2035
|
+
* is recovered by replaying the same idempotent set before writers resume.
|
|
2036
|
+
*
|
|
2037
|
+
* It moves ONLY the locations it is given: a cutover from A to C on a class
|
|
2038
|
+
* that also writes to B leaves B in the pool, because the operator's pool is
|
|
2039
|
+
* the authority and a migration is not a licence to shrink it. The commit is
|
|
2040
|
+
* refused when it would leave a write-serving type with nothing enabled —
|
|
2041
|
+
* the same refusal `upsertLocation` applies, judged on the RESULT so an
|
|
2042
|
+
* enable and a disable in one commit are weighed together.
|
|
1974
2043
|
*/
|
|
1975
|
-
async
|
|
1976
|
-
if (
|
|
2044
|
+
async setWriteTargets(enabledByLocationId) {
|
|
2045
|
+
if (enabledByLocationId.size === 0) return;
|
|
1977
2046
|
const now = Date.now();
|
|
1978
2047
|
const nextById = new Map(this.locations);
|
|
1979
2048
|
const touched = /* @__PURE__ */ new Map();
|
|
1980
|
-
for (const [
|
|
1981
|
-
const
|
|
1982
|
-
if (!
|
|
1983
|
-
if (
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
isDefault,
|
|
1992
|
-
...enable ? { enabled: true } : {},
|
|
1993
|
-
updatedAt: now
|
|
1994
|
-
};
|
|
1995
|
-
nextById.set(id, updated);
|
|
1996
|
-
touched.set(id, updated);
|
|
1997
|
-
}
|
|
2049
|
+
for (const [id, enabled] of enabledByLocationId) {
|
|
2050
|
+
const location = nextById.get(id);
|
|
2051
|
+
if (!location) throw new Error(`Storage location "${id}" not found`);
|
|
2052
|
+
if (location.enabled === enabled) continue;
|
|
2053
|
+
const updated = {
|
|
2054
|
+
...location,
|
|
2055
|
+
enabled,
|
|
2056
|
+
updatedAt: now
|
|
2057
|
+
};
|
|
2058
|
+
nextById.set(id, updated);
|
|
2059
|
+
touched.set(id, updated);
|
|
1998
2060
|
}
|
|
2061
|
+
if (touched.size === 0) return;
|
|
2062
|
+
refuseTypeWithoutWriteTarget(touched.values(), nextById.values());
|
|
1999
2063
|
if (this.locationStore) {
|
|
2000
2064
|
const previous = /* @__PURE__ */ new Map();
|
|
2001
2065
|
for (const id of touched.keys()) {
|
|
@@ -2015,13 +2079,10 @@ var StorageOrchestratorService = class {
|
|
|
2015
2079
|
for (const [id, location] of nextById) this.locations.set(id, location);
|
|
2016
2080
|
}
|
|
2017
2081
|
/**
|
|
2018
|
-
* Insert or update a location.
|
|
2019
|
-
* demotes any other default for the same type — operators always see
|
|
2020
|
-
* exactly one default per type.
|
|
2082
|
+
* Insert or update a location.
|
|
2021
2083
|
*
|
|
2022
|
-
* When a persistence store is wired (Task 6), the
|
|
2023
|
-
*
|
|
2024
|
-
* mutation returns. Persistence errors propagate to the caller.
|
|
2084
|
+
* When a persistence store is wired (Task 6), the record is mirrored to it;
|
|
2085
|
+
* the mirror is fire-and-forget and its errors reach the logger.
|
|
2025
2086
|
*/
|
|
2026
2087
|
/**
|
|
2027
2088
|
* Refuse a `(location kind, provider)` pair the kind cannot use.
|
|
@@ -2107,30 +2168,17 @@ var StorageOrchestratorService = class {
|
|
|
2107
2168
|
updatedAt: now
|
|
2108
2169
|
};
|
|
2109
2170
|
refuseLastWriteTargetDisabled(existing, next, this.locations.values());
|
|
2110
|
-
const demoted = [];
|
|
2111
|
-
if (next.isDefault) {
|
|
2112
|
-
for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
|
|
2113
|
-
const updated = {
|
|
2114
|
-
...otherLoc,
|
|
2115
|
-
isDefault: false,
|
|
2116
|
-
updatedAt: now
|
|
2117
|
-
};
|
|
2118
|
-
this.locations.set(otherId, updated);
|
|
2119
|
-
demoted.push(updated);
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
2171
|
this.locations.set(next.id, next);
|
|
2123
2172
|
this.logger.debug("storage-orchestrator: upsertLocation", { meta: {
|
|
2124
2173
|
id: next.id,
|
|
2125
2174
|
type: next.type,
|
|
2126
2175
|
providerId: next.providerId,
|
|
2127
|
-
|
|
2176
|
+
enabled: next.enabled
|
|
2128
2177
|
} });
|
|
2129
2178
|
if (this.locationStore) {
|
|
2130
2179
|
const store = this.locationStore;
|
|
2131
2180
|
(async () => {
|
|
2132
2181
|
try {
|
|
2133
|
-
for (const d of demoted) await store.upsert(d);
|
|
2134
2182
|
await store.upsert(next);
|
|
2135
2183
|
} catch (err) {
|
|
2136
2184
|
this.logger.error("storage-orchestrator: upsert persistence failed", { meta: {
|
|
@@ -2145,8 +2193,7 @@ var StorageOrchestratorService = class {
|
|
|
2145
2193
|
/**
|
|
2146
2194
|
* Remove a location. Refuses the last location of a type and the last
|
|
2147
2195
|
* enabled location of a type. `isSystem` is informational — a frozen
|
|
2148
|
-
* recordings `:default` is deletable once a sibling is live.
|
|
2149
|
-
* the flagged default promotes an enabled sibling. Occupancy still
|
|
2196
|
+
* recordings `:default` is deletable once a sibling is live. Occupancy still
|
|
2150
2197
|
* refuses unless `force` is set; force never overrides uniqueness /
|
|
2151
2198
|
* last-enabled.
|
|
2152
2199
|
*/
|
|
@@ -2157,8 +2204,6 @@ var StorageOrchestratorService = class {
|
|
|
2157
2204
|
if (ofType.length <= 1) throw new Error(`Cannot delete "${id}" — it is the only location for type "${loc.type}"`);
|
|
2158
2205
|
const enabledOfType = ofType.filter((l) => l.enabled !== false);
|
|
2159
2206
|
if (loc.enabled !== false && enabledOfType.length <= 1) throw new Error(`Cannot delete "${id}" — it is the only enabled location for type "${loc.type}"`);
|
|
2160
|
-
const successor = loc.isDefault === true ? ofType.find((l) => l.id !== id && l.enabled !== false) : void 0;
|
|
2161
|
-
if (loc.isDefault === true && successor === void 0) throw new Error(`Cannot delete "${id}" — it is the only enabled location for type "${loc.type}"`);
|
|
2162
2207
|
const occupancy = await this.probeOccupancy(loc);
|
|
2163
2208
|
if (occupancy !== "empty") {
|
|
2164
2209
|
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`);
|
|
@@ -2168,21 +2213,6 @@ var StorageOrchestratorService = class {
|
|
|
2168
2213
|
occupancy
|
|
2169
2214
|
} });
|
|
2170
2215
|
}
|
|
2171
|
-
if (successor !== void 0 && successor.isDefault !== true) {
|
|
2172
|
-
const now = Date.now();
|
|
2173
|
-
const promoted = {
|
|
2174
|
-
...successor,
|
|
2175
|
-
isDefault: true,
|
|
2176
|
-
updatedAt: now
|
|
2177
|
-
};
|
|
2178
|
-
this.locations.set(successor.id, promoted);
|
|
2179
|
-
if (this.locationStore) this.locationStore.upsert(promoted).catch((err) => {
|
|
2180
|
-
this.logger.error("storage-orchestrator: default promotion persist failed", { meta: {
|
|
2181
|
-
id: successor.id,
|
|
2182
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2183
|
-
} });
|
|
2184
|
-
});
|
|
2185
|
-
}
|
|
2186
2216
|
this.locations.delete(id);
|
|
2187
2217
|
if (this.locationStore) this.locationStore.delete(id).catch((err) => {
|
|
2188
2218
|
this.logger.error("storage-orchestrator: delete persistence failed", { meta: {
|
|
@@ -2261,9 +2291,17 @@ var StorageOrchestratorService = class {
|
|
|
2261
2291
|
if (!loc) throw new Error(`Storage location "${ref}" not found`);
|
|
2262
2292
|
return loc;
|
|
2263
2293
|
}
|
|
2264
|
-
const
|
|
2265
|
-
if (
|
|
2266
|
-
|
|
2294
|
+
const ofType = [...this.locations.values()].filter((loc) => loc.type === ref);
|
|
2295
|
+
if (ofType.length === 1) return ofType[0];
|
|
2296
|
+
if (ofType.length === 0) throw new Error(`No storage location configured for type "${ref}" — register one or pass a fully-qualified id (\`<type>:<slug>\`)`);
|
|
2297
|
+
const slugged = ofType.find((loc) => loc.id === `${ref}:default`);
|
|
2298
|
+
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>\`)`);
|
|
2299
|
+
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: {
|
|
2300
|
+
ref,
|
|
2301
|
+
resolved: slugged.id,
|
|
2302
|
+
candidates: ofType.length
|
|
2303
|
+
} });
|
|
2304
|
+
return slugged;
|
|
2267
2305
|
}
|
|
2268
2306
|
/**
|
|
2269
2307
|
* Find the storage-provider that backs a given location — the entry point
|
|
@@ -2367,9 +2405,11 @@ var StorageOrchestratorService = class {
|
|
|
2367
2405
|
* declarations on a populated map is a no-op (each declared `id`
|
|
2368
2406
|
* already resolves to an existing `<id>:default`).
|
|
2369
2407
|
*
|
|
2370
|
-
*
|
|
2371
|
-
* `
|
|
2372
|
-
*
|
|
2408
|
+
* The seeded row is born ENABLED — a fresh install must be able to write the
|
|
2409
|
+
* moment it boots, and `enabled` is the only write switch there is (D383).
|
|
2410
|
+
* The `:default` suffix is a NAME kept for idempotency across existing
|
|
2411
|
+
* installs, not a flag: the stored `id` is `'recordings:default'` so it
|
|
2412
|
+
* satisfies the
|
|
2373
2413
|
* `^[a-z][a-z0-9-]*:[a-z0-9-]+$` regex on `StorageLocationSchema.id`.
|
|
2374
2414
|
*
|
|
2375
2415
|
* `basePath` is the storage root (e.g. `<CAMSTACK_DATA>`). Each
|
|
@@ -2394,7 +2434,7 @@ var StorageOrchestratorService = class {
|
|
|
2394
2434
|
displayName: d.displayName,
|
|
2395
2435
|
providerId: input.providerId,
|
|
2396
2436
|
config: { basePath: base },
|
|
2397
|
-
|
|
2437
|
+
enabled: true,
|
|
2398
2438
|
isSystem: false
|
|
2399
2439
|
});
|
|
2400
2440
|
added++;
|
|
@@ -2418,7 +2458,7 @@ function resolveEnabled(input, existing, typeAlreadyHasLocation) {
|
|
|
2418
2458
|
const requested = input.enabled ?? existing?.enabled;
|
|
2419
2459
|
if (requested === void 0) {
|
|
2420
2460
|
if (existing) return {};
|
|
2421
|
-
return { enabled:
|
|
2461
|
+
return { enabled: !typeAlreadyHasLocation };
|
|
2422
2462
|
}
|
|
2423
2463
|
return { enabled: requested };
|
|
2424
2464
|
}
|
|
@@ -2427,8 +2467,7 @@ var OPTIONAL_WRITE_TYPES = new Set(["backups"]);
|
|
|
2427
2467
|
/**
|
|
2428
2468
|
* Refuse an UPDATE that would leave a write-serving type with no enabled
|
|
2429
2469
|
* location. Creates are exempt (an explicit `enabled: false` on the first
|
|
2430
|
-
* location is already a named test).
|
|
2431
|
-
* is the write set.
|
|
2470
|
+
* location is already a named test). Enabled IS the write set.
|
|
2432
2471
|
*/
|
|
2433
2472
|
function refuseLastWriteTargetDisabled(existing, next, locations) {
|
|
2434
2473
|
if (OPTIONAL_WRITE_TYPES.has(next.type)) return;
|
|
@@ -2441,6 +2480,23 @@ function refuseLastWriteTargetDisabled(existing, next, locations) {
|
|
|
2441
2480
|
}
|
|
2442
2481
|
throw new Error(`Cannot disable "${next.id}": it is the last enabled location of type "${next.type}". Enable another location of this type first.`);
|
|
2443
2482
|
}
|
|
2483
|
+
/**
|
|
2484
|
+
* The same refusal, judged on a whole COMMIT rather than one row: a
|
|
2485
|
+
* multi-location change (a cutover enables its destination and disables its
|
|
2486
|
+
* source in one go) must be weighed on its RESULT, or the disable half looks
|
|
2487
|
+
* like the last write target leaving while the enable half is already in the
|
|
2488
|
+
* same map.
|
|
2489
|
+
*/
|
|
2490
|
+
function refuseTypeWithoutWriteTarget(touched, result) {
|
|
2491
|
+
const rows = [...result];
|
|
2492
|
+
for (const type of new Set([...touched].map((loc) => loc.type))) {
|
|
2493
|
+
if (OPTIONAL_WRITE_TYPES.has(type)) continue;
|
|
2494
|
+
const ofType = rows.filter((loc) => loc.type === type);
|
|
2495
|
+
if (ofType.length === 0) continue;
|
|
2496
|
+
if (ofType.some((loc) => isLocationEnabled(loc))) continue;
|
|
2497
|
+
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.`);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2444
2500
|
//#endregion
|
|
2445
2501
|
//#region src/builtins/storage-orchestrator/storage-pressure-manager.ts
|
|
2446
2502
|
var DEFAULT_MAX_ROUNDS = 8;
|
|
@@ -2682,13 +2738,9 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
2682
2738
|
const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
|
|
2683
2739
|
return Promise.all(rows.map(async (loc) => ({
|
|
2684
2740
|
...this.redacted(loc),
|
|
2685
|
-
capacity: loc
|
|
2741
|
+
capacity: isLocationEnabled(loc) ? await this.localCapacityOf(loc) : null
|
|
2686
2742
|
})));
|
|
2687
2743
|
},
|
|
2688
|
-
getDefaultLocation: async ({ type }) => {
|
|
2689
|
-
const loc = service.getDefaultLocation(type);
|
|
2690
|
-
return loc === null ? null : this.redacted(loc);
|
|
2691
|
-
},
|
|
2692
2744
|
listLocationDeclarations: async () => service.listDeclarations(),
|
|
2693
2745
|
upsertLocation: async (input) => {
|
|
2694
2746
|
const stored = service.getLocationById(input.id);
|