@camstack/system 1.0.2
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/addon/addon-api-factory.d.ts +35 -0
- package/dist/addon-routes/addon-route-registry.d.ts +37 -0
- package/dist/addon-runner.js +599 -0
- package/dist/addon-runner.mjs +597 -0
- package/dist/auth/api-key-manager.d.ts +26 -0
- package/dist/auth/auth-manager.d.ts +109 -0
- package/dist/auth/parse-record.d.ts +18 -0
- package/dist/auth/scope-matcher.d.ts +7 -0
- package/dist/auth/scoped-token-manager.d.ts +40 -0
- package/dist/auth/totp-manager.d.ts +51 -0
- package/dist/auth/user-manager.d.ts +34 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.d.ts +53 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +259 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +251 -0
- package/dist/builtins/addon-pages-aggregator/dedupe-pages.d.ts +6 -0
- package/dist/builtins/addon-pages-aggregator/index.d.ts +1 -0
- package/dist/builtins/addon-pages-aggregator/index.js +8 -0
- package/dist/builtins/addon-pages-aggregator/index.mjs +2 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +47 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +228 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +220 -0
- package/dist/builtins/addon-widgets-aggregator/index.d.ts +1 -0
- package/dist/builtins/addon-widgets-aggregator/index.js +8 -0
- package/dist/builtins/addon-widgets-aggregator/index.mjs +2 -0
- package/dist/builtins/alerts/alerts.addon.d.ts +81 -0
- package/dist/builtins/alerts/alerts.addon.js +601 -0
- package/dist/builtins/alerts/alerts.addon.mjs +595 -0
- package/dist/builtins/alerts/index.d.ts +1 -0
- package/dist/builtins/alerts/index.js +4 -0
- package/dist/builtins/alerts/index.mjs +2 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +147 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2229 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2220 -0
- package/dist/builtins/backup-orchestrator/cron-helpers.d.ts +23 -0
- package/dist/builtins/backup-orchestrator/destination-policy.d.ts +72 -0
- package/dist/builtins/backup-orchestrator/download-helpers.d.ts +12 -0
- package/dist/builtins/backup-orchestrator/index.d.ts +2 -0
- package/dist/builtins/backup-orchestrator/index.js +8 -0
- package/dist/builtins/backup-orchestrator/index.mjs +2 -0
- package/dist/builtins/backup-orchestrator/manifest-store.d.ts +77 -0
- package/dist/builtins/console-logging/console-destination.d.ts +13 -0
- package/dist/builtins/console-logging/console-logging.addon.d.ts +25 -0
- package/dist/builtins/console-logging/index.d.ts +3 -0
- package/dist/builtins/console-logging/index.js +104 -0
- package/dist/builtins/console-logging/index.mjs +95 -0
- package/dist/builtins/device-manager/device-config-contribution.d.ts +32 -0
- package/dist/builtins/device-manager/device-event-propagator.d.ts +26 -0
- package/dist/builtins/device-manager/device-link-overlay.d.ts +23 -0
- package/dist/builtins/device-manager/device-link-resolver.d.ts +15 -0
- package/dist/builtins/device-manager/device-manager.addon.d.ts +452 -0
- package/dist/builtins/device-manager/device-manager.addon.js +3299 -0
- package/dist/builtins/device-manager/device-manager.addon.mjs +3292 -0
- package/dist/builtins/device-manager/index.d.ts +2 -0
- package/dist/builtins/device-manager/index.js +8 -0
- package/dist/builtins/device-manager/index.mjs +2 -0
- package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +44 -0
- package/dist/builtins/hub-forwarder/hub-forwarder.addon.d.ts +15 -0
- package/dist/builtins/hub-forwarder/index.d.ts +3 -0
- package/dist/builtins/hub-forwarder/index.js +154 -0
- package/dist/builtins/hub-forwarder/index.mjs +145 -0
- package/dist/builtins/local-auth/auth-schema.d.ts +26 -0
- package/dist/builtins/local-auth/index.d.ts +1 -0
- package/dist/builtins/local-auth/index.js +4 -0
- package/dist/builtins/local-auth/index.mjs +2 -0
- package/dist/builtins/local-auth/local-auth.addon.d.ts +18 -0
- package/dist/builtins/local-auth/local-auth.addon.js +8094 -0
- package/dist/builtins/local-auth/local-auth.addon.mjs +8063 -0
- package/dist/builtins/local-auth/oauth-grants.d.ts +45 -0
- package/dist/builtins/local-auth/oauth-session-manager.d.ts +50 -0
- package/dist/builtins/local-network/index.d.ts +2 -0
- package/dist/builtins/local-network/index.js +10 -0
- package/dist/builtins/local-network/index.mjs +2 -0
- package/dist/builtins/local-network/local-network.addon.d.ts +150 -0
- package/dist/builtins/local-network/local-network.addon.js +489 -0
- package/dist/builtins/local-network/local-network.addon.mjs +477 -0
- package/dist/builtins/native-metrics/index.d.ts +2 -0
- package/dist/builtins/native-metrics/native-metrics-provider.d.ts +48 -0
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +73 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js +922 -0
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +914 -0
- package/dist/builtins/platform-probe/hardware-decode-accel-probe.d.ts +37 -0
- package/dist/builtins/platform-probe/hardware-encoder-probe.d.ts +13 -0
- package/dist/builtins/platform-probe/index.d.ts +22 -0
- package/dist/builtins/platform-probe/index.js +834 -0
- package/dist/builtins/platform-probe/index.mjs +822 -0
- package/dist/builtins/platform-probe/inference-config-resolver.d.ts +29 -0
- package/dist/builtins/platform-probe/intel-accelerators.d.ts +11 -0
- package/dist/builtins/platform-probe/platform-scorer.d.ts +30 -0
- package/dist/builtins/platform-probe/runtime-packages.d.ts +6 -0
- package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts +96 -0
- package/dist/builtins/remote-access-orchestrator/index.d.ts +1 -0
- package/dist/builtins/remote-access-orchestrator/index.js +8 -0
- package/dist/builtins/remote-access-orchestrator/index.mjs +2 -0
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +40 -0
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +214 -0
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +208 -0
- package/dist/builtins/shared/settle-sources.d.ts +22 -0
- package/dist/builtins/snapshot/index.d.ts +2 -0
- package/dist/builtins/snapshot/index.js +494 -0
- package/dist/builtins/snapshot/index.mjs +488 -0
- package/dist/builtins/snapshot/snapshot.addon.d.ts +120 -0
- package/dist/builtins/sqlite-storage/config-store.d.ts +8 -0
- package/dist/builtins/sqlite-storage/device-store.d.ts +23 -0
- package/dist/builtins/sqlite-storage/filesystem-browse-provider.d.ts +25 -0
- package/dist/builtins/sqlite-storage/filesystem-storage-provider.d.ts +83 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +32 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +396 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +388 -0
- package/dist/builtins/sqlite-storage/index.d.ts +8 -0
- package/dist/builtins/sqlite-storage/index.js +62 -0
- package/dist/builtins/sqlite-storage/index.mjs +49 -0
- package/dist/builtins/sqlite-storage/integration-registry.d.ts +27 -0
- package/dist/builtins/sqlite-storage/path-guard.d.ts +4 -0
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +102 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +14 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +644 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +636 -0
- package/dist/builtins/storage-orchestrator/index.d.ts +6 -0
- package/dist/builtins/storage-orchestrator/index.js +10 -0
- package/dist/builtins/storage-orchestrator/index.mjs +2 -0
- package/dist/builtins/storage-orchestrator/location-store.d.ts +49 -0
- package/dist/builtins/storage-orchestrator/provider-discovery.d.ts +10 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +103 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1138 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1128 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +236 -0
- package/dist/builtins/storage-orchestrator/storage-pressure-manager.d.ts +38 -0
- package/dist/builtins/system-backup/system-backup.service.d.ts +137 -0
- package/dist/builtins/system-config/index.d.ts +1 -0
- package/dist/builtins/system-config/index.js +8 -0
- package/dist/builtins/system-config/index.mjs +2 -0
- package/dist/builtins/system-config/system-config.addon.d.ts +10 -0
- package/dist/builtins/system-config/system-config.addon.js +232 -0
- package/dist/builtins/system-config/system-config.addon.mjs +226 -0
- package/dist/builtins/winston-logging/index.d.ts +3 -0
- package/dist/builtins/winston-logging/index.js +156 -0
- package/dist/builtins/winston-logging/index.mjs +144 -0
- package/dist/builtins/winston-logging/winston-destination.d.ts +21 -0
- package/dist/builtins/winston-logging/winston-logging.addon.d.ts +19 -0
- package/dist/chunk-CNf5ZN-e.mjs +37 -0
- package/dist/chunk-Cek0wNdY.js +64 -0
- package/dist/download/model-download-service.d.ts +41 -0
- package/dist/download/model-downloader.d.ts +31 -0
- package/dist/events/event-bus.d.ts +10 -0
- package/dist/events/system-event-bus.d.ts +14 -0
- package/dist/feature/feature-manager.d.ts +11 -0
- package/dist/formatter-B7qW8bPJ.mjs +162 -0
- package/dist/formatter-DqAKDlvN.js +167 -0
- package/dist/http/authenticated-file-server.d.ts +53 -0
- package/dist/http/data-plane-registry.d.ts +23 -0
- package/dist/http/file-data-plane.d.ts +10 -0
- package/dist/http/reverse-proxy.d.ts +15 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +93485 -0
- package/dist/index.mjs +93179 -0
- package/dist/intel-accelerators-Gg0P5mnl.js +20 -0
- package/dist/intel-accelerators-hGgpZ0pX.mjs +19 -0
- package/dist/kernel/addon-class-resolver.d.ts +4 -0
- package/dist/kernel/addon-engine-manager.d.ts +22 -0
- package/dist/kernel/addon-health-monitor.d.ts +154 -0
- package/dist/kernel/addon-installer.d.ts +208 -0
- package/dist/kernel/addon-loader.d.ts +106 -0
- package/dist/kernel/addon-manifest.d.ts +77 -0
- package/dist/kernel/capability-handle.d.ts +46 -0
- package/dist/kernel/capability-registry.d.ts +412 -0
- package/dist/kernel/config-manager.d.ts +212 -0
- package/dist/kernel/config-schema.d.ts +93 -0
- package/dist/kernel/custom-action-registry.d.ts +23 -0
- package/dist/kernel/deps/addon-deps-manager.d.ts +19 -0
- package/dist/kernel/deps/manifest-native-deps.d.ts +25 -0
- package/dist/kernel/deps/manifest-python-deps.d.ts +20 -0
- package/dist/kernel/device-registry.d.ts +29 -0
- package/dist/kernel/fs-utils.d.ts +41 -0
- package/dist/kernel/hwaccel/hwaccel-resolver.d.ts +19 -0
- package/dist/kernel/hwaccel/hwaccel-service.d.ts +4 -0
- package/dist/kernel/index.d.ts +74 -0
- package/dist/kernel/infra-capabilities.d.ts +13 -0
- package/dist/kernel/moleculer/addon-context-factory.d.ts +91 -0
- package/dist/kernel/moleculer/addon-data-plane-facility.d.ts +19 -0
- package/dist/kernel/moleculer/addon-runner.d.ts +1 -0
- package/dist/kernel/moleculer/addon-service-factory.d.ts +50 -0
- package/dist/kernel/moleculer/broker-factory.d.ts +50 -0
- package/dist/kernel/moleculer/cap-usage-registry.d.ts +46 -0
- package/dist/kernel/moleculer/capabilities-access.d.ts +21 -0
- package/dist/kernel/moleculer/child-addon-call-dispatch.d.ts +46 -0
- package/dist/kernel/moleculer/child-cap-dispatch.d.ts +20 -0
- package/dist/kernel/moleculer/cluster-secret.d.ts +15 -0
- package/dist/kernel/moleculer/core-cap-service.d.ts +50 -0
- package/dist/kernel/moleculer/crash-supervisor.d.ts +50 -0
- package/dist/kernel/moleculer/device-cap-proxy.d.ts +79 -0
- package/dist/kernel/moleculer/event-bus-core.d.ts +53 -0
- package/dist/kernel/moleculer/event-bus.d.ts +53 -0
- package/dist/kernel/moleculer/hub-log-forwarder.d.ts +36 -0
- package/dist/kernel/moleculer/hub-service.d.ts +35 -0
- package/dist/kernel/moleculer/node-registry.d.ts +126 -0
- package/dist/kernel/moleculer/process-context.d.ts +4 -0
- package/dist/kernel/moleculer/process-service.d.ts +72 -0
- package/dist/kernel/moleculer/provider-registry.d.ts +28 -0
- package/dist/kernel/moleculer/readiness-context.d.ts +62 -0
- package/dist/kernel/moleculer/readiness-service.d.ts +7 -0
- package/dist/kernel/moleculer/register-node-client.d.ts +35 -0
- package/dist/kernel/moleculer/remote-logger.d.ts +43 -0
- package/dist/kernel/moleculer/resilient-cap-call.d.ts +28 -0
- package/dist/kernel/moleculer/stream-probe-service.d.ts +9 -0
- package/dist/kernel/moleculer/trpc-links.d.ts +189 -0
- package/dist/kernel/moleculer/typed-array-serde.d.ts +25 -0
- package/dist/kernel/moleculer/worker-device-restore.d.ts +10 -0
- package/dist/kernel/provider-kind-drift.d.ts +12 -0
- package/dist/kernel/restart-coordinator.d.ts +90 -0
- package/dist/kernel/storage-location-registry.d.ts +40 -0
- package/dist/kernel/transport/cap-action-name.d.ts +100 -0
- package/dist/kernel/transport/cap-route-resolver.d.ts +148 -0
- package/dist/kernel/transport/cap-route.d.ts +148 -0
- package/dist/kernel/transport/child-cap-protocol.d.ts +136 -0
- package/dist/kernel/transport/create-local-transport.d.ts +7 -0
- package/dist/kernel/transport/frame-codec.d.ts +7 -0
- package/dist/kernel/transport/index.d.ts +27 -0
- package/dist/kernel/transport/local-child-client.d.ts +136 -0
- package/dist/kernel/transport/local-child-registry.d.ts +179 -0
- package/dist/kernel/transport/local-endpoint-path.d.ts +6 -0
- package/dist/kernel/transport/local-transport.d.ts +46 -0
- package/dist/kernel/transport/parent-unowned-call.d.ts +75 -0
- package/dist/kernel/transport/socket-channel.d.ts +27 -0
- package/dist/kernel/transport/uds-event-bridge.d.ts +36 -0
- package/dist/kernel/transport/uds-event-bus.d.ts +22 -0
- package/dist/kernel/transport/uds-local-transport.d.ts +18 -0
- package/dist/kernel/transport/uds-log-ingest.d.ts +28 -0
- package/dist/kernel/transport/uds-logger.d.ts +44 -0
- package/dist/kernel/utils/ring-buffer.d.ts +15 -0
- package/dist/kernel/workspace-detect.d.ts +9 -0
- package/dist/lifecycle/lifecycle-state-machine.d.ts +28 -0
- package/dist/logging/formatter.d.ts +30 -0
- package/dist/logging/log-manager.d.ts +54 -0
- package/dist/logging/log-ring-buffer.d.ts +47 -0
- package/dist/logging/partitioned-log-buffer.d.ts +35 -0
- package/dist/logging/scoped-logger.d.ts +17 -0
- package/dist/main-DNnMW7Z2.js +9983 -0
- package/dist/main-rtjOwPBR.mjs +9976 -0
- package/dist/manifest-python-deps-D1DbAQEv.js +6724 -0
- package/dist/manifest-python-deps-DZsKTbs1.mjs +6315 -0
- package/dist/network/network-quality.d.ts +11 -0
- package/dist/notification/notification-service.d.ts +37 -0
- package/dist/notification/toast-service.d.ts +22 -0
- package/dist/pipeline/engine-manager-resolver.d.ts +15 -0
- package/dist/pipeline/pipeline-runner.d.ts +8 -0
- package/dist/pipeline/pipeline-validator.d.ts +13 -0
- package/dist/process/resource-monitor.d.ts +11 -0
- package/dist/python/python-env-manager.d.ts +12 -0
- package/dist/repl/interfaces.d.ts +31 -0
- package/dist/repl/repl-engine.d.ts +8 -0
- package/dist/resource-monitor-ClDGFyf6.mjs +57 -0
- package/dist/resource-monitor-IIEanuJt.js +74 -0
- package/dist/settle-sources-Bhsy57y-.js +38 -0
- package/dist/settle-sources-CDtNC8ub.mjs +33 -0
- package/dist/storage/fs-storage-backend.d.ts +40 -0
- package/dist/storage/storage-location-manager.d.ts +23 -0
- package/dist/storage/storage-manager.d.ts +83 -0
- package/dist/tar-BgAEMRBR.js +5434 -0
- package/dist/tar-ByMOPNM0.mjs +5429 -0
- package/dist/tls/cert-manager.d.ts +26 -0
- package/dist/tls/index.d.ts +1 -0
- package/package.json +343 -0
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
import * as path$1 from "node:path";
|
|
2
|
+
import { BaseAddon, StorageLocationTypeSchema, parseJsonObject, storageCapability } from "@camstack/types";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import { buildStorageLocationRegistry } from "@camstack/system";
|
|
5
|
+
//#region src/builtins/storage-orchestrator/storage-orchestrator.service.ts
|
|
6
|
+
var StorageOrchestratorService = class {
|
|
7
|
+
logger;
|
|
8
|
+
getProviders;
|
|
9
|
+
locationStore;
|
|
10
|
+
locations = /* @__PURE__ */ new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Declaration-driven cardinality source, injected by the addon after
|
|
13
|
+
* the kernel aggregates `storageLocations` declarations. `null` until
|
|
14
|
+
* `setRegistry` runs — `upsertLocation` then treats every type as
|
|
15
|
+
* `'multi'` (no singleton enforcement), the safe default for the
|
|
16
|
+
* in-memory-only / early-boot path.
|
|
17
|
+
*/
|
|
18
|
+
registry = null;
|
|
19
|
+
/**
|
|
20
|
+
* Injected `providerId → nodeLocal` resolver (see {@link NodeLocalResolver}).
|
|
21
|
+
* `null` until {@link setNodeLocalResolver} runs — `upsertLocation` then
|
|
22
|
+
* skips the node-local requirement check entirely (every provider is
|
|
23
|
+
* treated as node-agnostic), the safe default for the in-memory-only /
|
|
24
|
+
* early-boot path that predates the resolver wiring.
|
|
25
|
+
*/
|
|
26
|
+
nodeLocalResolver = null;
|
|
27
|
+
constructor(logger, getProviders, locationStore = null) {
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
this.getProviders = getProviders;
|
|
30
|
+
this.locationStore = locationStore;
|
|
31
|
+
}
|
|
32
|
+
/** True once a persistence store is wired (constructor or {@link attachStore}). */
|
|
33
|
+
hasStore() {
|
|
34
|
+
return this.locationStore !== null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Late-bind the persistence store (production boot: the `settings-store`
|
|
38
|
+
* cap registers after the orchestrator initializes, so the service runs
|
|
39
|
+
* in-memory-only until then). Idempotent — a second call once a store is
|
|
40
|
+
* wired is a no-op.
|
|
41
|
+
*
|
|
42
|
+
* Reconciles both directions so operator edits survive a reboot:
|
|
43
|
+
* 1. hydrate — DB rows win over any early in-memory seed (restores
|
|
44
|
+
* operator config like `minFreePercent` that the pre-store boot
|
|
45
|
+
* can't see), with the same `isSystem` upgrade as {@link initialize};
|
|
46
|
+
* 2. backfill — in-memory locations the DB doesn't have yet (the
|
|
47
|
+
* pre-store seed defaults on a fresh install) are persisted, so the
|
|
48
|
+
* store becomes the durable source of truth from here on.
|
|
49
|
+
*/
|
|
50
|
+
async attachStore(store) {
|
|
51
|
+
if (this.locationStore) return;
|
|
52
|
+
this.locationStore = store;
|
|
53
|
+
const rows = await store.loadAll();
|
|
54
|
+
const dbIds = new Set(rows.map((r) => r.id));
|
|
55
|
+
for (const loc of rows) {
|
|
56
|
+
const upgraded = !loc.isSystem && loc.id === `${loc.type}:default` ? {
|
|
57
|
+
...loc,
|
|
58
|
+
isSystem: true
|
|
59
|
+
} : loc;
|
|
60
|
+
this.locations.set(upgraded.id, upgraded);
|
|
61
|
+
if (upgraded !== loc) store.upsert(upgraded).catch(() => {});
|
|
62
|
+
}
|
|
63
|
+
const backfill = [...this.locations.values()].filter((l) => !dbIds.has(l.id));
|
|
64
|
+
for (const loc of backfill) store.upsert(loc).catch((err) => {
|
|
65
|
+
this.logger.warn("storage-orchestrator: attachStore backfill persist failed", { meta: {
|
|
66
|
+
id: loc.id,
|
|
67
|
+
error: err instanceof Error ? err.message : String(err)
|
|
68
|
+
} });
|
|
69
|
+
});
|
|
70
|
+
this.logger.info("storage-orchestrator: store attached + reconciled", { meta: {
|
|
71
|
+
hydrated: rows.length,
|
|
72
|
+
backfilled: backfill.length,
|
|
73
|
+
total: this.locations.size
|
|
74
|
+
} });
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Inject the declaration-driven cardinality source. Called once by the
|
|
78
|
+
* addon after the kernel aggregates every addon's `storageLocations`
|
|
79
|
+
* declarations into a `StorageLocationRegistry`.
|
|
80
|
+
*/
|
|
81
|
+
setRegistry(registry) {
|
|
82
|
+
this.registry = registry;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Inject the synchronous `providerId → nodeLocal` resolver (SP1). Called
|
|
86
|
+
* by the addon from a cached snapshot of every `storage-provider`'s
|
|
87
|
+
* `getProviderInfo().nodeLocal`, refreshed when the provider collection
|
|
88
|
+
* changes. Powers the node-local upsert guard and the boot backfill.
|
|
89
|
+
*/
|
|
90
|
+
setNodeLocalResolver(resolver) {
|
|
91
|
+
this.nodeLocalResolver = resolver;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The full set of addon-declared storage locations, as aggregated by
|
|
95
|
+
* the kernel and injected via {@link setRegistry}. Powers the admin-UI
|
|
96
|
+
* Data screen's per-declaration grouping. Empty until the registry is
|
|
97
|
+
* injected (early-boot / in-memory-only path).
|
|
98
|
+
*/
|
|
99
|
+
listDeclarations() {
|
|
100
|
+
return this.registry ? this.registry.list() : [];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Hydrate the in-memory map from the persistence layer. Called once
|
|
104
|
+
* by the addon during `onInitialize`, before the eager seed pass.
|
|
105
|
+
* Idempotent — running it twice with the same store contents
|
|
106
|
+
* produces the same map. No-op when the service was constructed
|
|
107
|
+
* without a store.
|
|
108
|
+
*
|
|
109
|
+
* Errors loading individual rows are tolerated: the bad row is
|
|
110
|
+
* skipped and logged so a single corrupted record doesn't lock the
|
|
111
|
+
* whole orchestrator out of boot. (The store-side validation pass
|
|
112
|
+
* before insert means only schema-invalid SQL state can produce
|
|
113
|
+
* such a row.)
|
|
114
|
+
*/
|
|
115
|
+
async initialize() {
|
|
116
|
+
if (!this.locationStore) return;
|
|
117
|
+
const rows = await this.locationStore.loadAll();
|
|
118
|
+
let upgraded = 0;
|
|
119
|
+
for (const loc of rows) if (!loc.isSystem && loc.id === `${loc.type}:default`) {
|
|
120
|
+
const upgradedLoc = {
|
|
121
|
+
...loc,
|
|
122
|
+
isSystem: true
|
|
123
|
+
};
|
|
124
|
+
this.locations.set(upgradedLoc.id, upgradedLoc);
|
|
125
|
+
this.locationStore.upsert(upgradedLoc).catch((err) => {
|
|
126
|
+
this.logger.warn("storage-orchestrator: isSystem upgrade persist failed", { meta: {
|
|
127
|
+
id: upgradedLoc.id,
|
|
128
|
+
error: err instanceof Error ? err.message : String(err)
|
|
129
|
+
} });
|
|
130
|
+
});
|
|
131
|
+
upgraded++;
|
|
132
|
+
} else this.locations.set(loc.id, loc);
|
|
133
|
+
this.logger.info("storage-orchestrator: hydrated locations from store", { meta: {
|
|
134
|
+
loaded: this.locations.size,
|
|
135
|
+
isSystemUpgraded: upgraded
|
|
136
|
+
} });
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Boot backfill (SP1): stamp `nodeId` on every persisted node-local
|
|
140
|
+
* location that lacks one. Single-node deployments seeded their
|
|
141
|
+
* filesystem locations before `nodeId` existed — they live on `hubNodeId`
|
|
142
|
+
* (the hub). Node-agnostic (remote) locations are left untouched: their
|
|
143
|
+
* absent `nodeId` is correct (reachable from any node).
|
|
144
|
+
*
|
|
145
|
+
* Idempotent — a location that already carries a `nodeId` (any value) is
|
|
146
|
+
* never re-touched, so re-running on a populated map (or a re-boot) is a
|
|
147
|
+
* no-op. A plain local update, not versioned-migration machinery: only
|
|
148
|
+
* the rows missing `nodeId` are mutated + persisted.
|
|
149
|
+
*
|
|
150
|
+
* No-op until the node-local resolver is wired (it classifies which
|
|
151
|
+
* providers are node-local).
|
|
152
|
+
*/
|
|
153
|
+
async backfillNodeIds(hubNodeId) {
|
|
154
|
+
const resolver = this.nodeLocalResolver;
|
|
155
|
+
if (!resolver) return;
|
|
156
|
+
let stamped = 0;
|
|
157
|
+
for (const [id, loc] of [...this.locations]) {
|
|
158
|
+
if (loc.nodeId) continue;
|
|
159
|
+
if (resolver(loc.providerId) !== true) continue;
|
|
160
|
+
const updated = {
|
|
161
|
+
...loc,
|
|
162
|
+
nodeId: hubNodeId,
|
|
163
|
+
updatedAt: Date.now()
|
|
164
|
+
};
|
|
165
|
+
this.locations.set(id, updated);
|
|
166
|
+
stamped++;
|
|
167
|
+
if (this.locationStore) try {
|
|
168
|
+
await this.locationStore.upsert(updated);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
this.logger.error("storage-orchestrator: nodeId backfill persist failed", { meta: {
|
|
171
|
+
id,
|
|
172
|
+
error: err instanceof Error ? err.message : String(err)
|
|
173
|
+
} });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (stamped > 0) this.logger.info("storage-orchestrator: backfilled nodeId on node-local locations", { meta: {
|
|
177
|
+
stamped,
|
|
178
|
+
hubNodeId
|
|
179
|
+
} });
|
|
180
|
+
}
|
|
181
|
+
listLocations(filter) {
|
|
182
|
+
const all = [...this.locations.values()];
|
|
183
|
+
return filter?.type ? all.filter((l) => l.type === filter.type) : all;
|
|
184
|
+
}
|
|
185
|
+
getDefaultLocation(type) {
|
|
186
|
+
for (const loc of this.locations.values()) if (loc.type === type && loc.isDefault) return loc;
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Insert or update a location. If `isDefault: true`, atomically
|
|
191
|
+
* demotes any other default for the same type — operators always see
|
|
192
|
+
* exactly one default per type.
|
|
193
|
+
*
|
|
194
|
+
* When a persistence store is wired (Task 6), the new record AND any
|
|
195
|
+
* implicitly-demoted siblings are persisted before the in-memory map
|
|
196
|
+
* mutation returns. Persistence errors propagate to the caller.
|
|
197
|
+
*/
|
|
198
|
+
upsertLocation(input) {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
const existing = this.locations.get(input.id);
|
|
201
|
+
if (!existing) {
|
|
202
|
+
if ((this.registry?.cardinalityOf(input.type) ?? "multi") === "single") {
|
|
203
|
+
const already = [...this.locations.values()].find((l) => l.type === input.type);
|
|
204
|
+
if (already) throw new Error(`Storage type "${input.type}" is single — only one location allowed (existing: "${already.id}"). Edit it instead of adding a new one.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (this.nodeLocalResolver?.(input.providerId) === true && !input.nodeId) input = {
|
|
208
|
+
...input,
|
|
209
|
+
nodeId: "hub"
|
|
210
|
+
};
|
|
211
|
+
if (existing?.isSystem === true) input = {
|
|
212
|
+
...input,
|
|
213
|
+
isSystem: true
|
|
214
|
+
};
|
|
215
|
+
const next = {
|
|
216
|
+
...input,
|
|
217
|
+
createdAt: existing?.createdAt ?? now,
|
|
218
|
+
updatedAt: now
|
|
219
|
+
};
|
|
220
|
+
const demoted = [];
|
|
221
|
+
if (next.isDefault) {
|
|
222
|
+
for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
|
|
223
|
+
const updated = {
|
|
224
|
+
...otherLoc,
|
|
225
|
+
isDefault: false,
|
|
226
|
+
updatedAt: now
|
|
227
|
+
};
|
|
228
|
+
this.locations.set(otherId, updated);
|
|
229
|
+
demoted.push(updated);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
this.locations.set(next.id, next);
|
|
233
|
+
this.logger.debug("storage-orchestrator: upsertLocation", { meta: {
|
|
234
|
+
id: next.id,
|
|
235
|
+
type: next.type,
|
|
236
|
+
providerId: next.providerId,
|
|
237
|
+
isDefault: next.isDefault
|
|
238
|
+
} });
|
|
239
|
+
if (this.locationStore) {
|
|
240
|
+
const store = this.locationStore;
|
|
241
|
+
(async () => {
|
|
242
|
+
try {
|
|
243
|
+
for (const d of demoted) await store.upsert(d);
|
|
244
|
+
await store.upsert(next);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
this.logger.error("storage-orchestrator: upsert persistence failed", { meta: {
|
|
247
|
+
id: next.id,
|
|
248
|
+
error: err instanceof Error ? err.message : String(err)
|
|
249
|
+
} });
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
}
|
|
253
|
+
return next;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Remove a location. Refuses to remove the default of a type unless a
|
|
257
|
+
* sibling default exists (defensive — `upsertLocation` already ensures
|
|
258
|
+
* at most one default per type, but the logic guards against future
|
|
259
|
+
* bypass paths e.g. SQLite migration that imports two defaults).
|
|
260
|
+
*
|
|
261
|
+
* Persistence (Task 6) mirrors the delete asynchronously, with errors
|
|
262
|
+
* routed to the logger — see `upsertLocation` for the rationale.
|
|
263
|
+
*/
|
|
264
|
+
deleteLocation(id) {
|
|
265
|
+
const loc = this.locations.get(id);
|
|
266
|
+
if (!loc) throw new Error(`Storage location "${id}" not found`);
|
|
267
|
+
if (loc.isSystem) throw new Error(`Storage location "${id}" is system-managed and cannot be deleted. Edit its config (path / providerId) instead.`);
|
|
268
|
+
if (loc.isDefault) {
|
|
269
|
+
if (![...this.locations.values()].find((l) => l.type === loc.type && l.id !== id && l.isDefault)) throw new Error(`Cannot delete default location "${id}" for type "${loc.type}" — promote another location to default first`);
|
|
270
|
+
}
|
|
271
|
+
this.locations.delete(id);
|
|
272
|
+
if (this.locationStore) this.locationStore.delete(id).catch((err) => {
|
|
273
|
+
this.logger.error("storage-orchestrator: delete persistence failed", { meta: {
|
|
274
|
+
id,
|
|
275
|
+
error: err instanceof Error ? err.message : String(err)
|
|
276
|
+
} });
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Remove system-seeded locations whose type is no longer declared by any
|
|
281
|
+
* addon (stale defaults from a removed location type). Operator-added
|
|
282
|
+
* (non-system) locations of an undeclared type are KEPT but warned — the
|
|
283
|
+
* operator owns them. Requires the registry to be set first.
|
|
284
|
+
*
|
|
285
|
+
* FAIL-SAFE: if the registry is EMPTY (no addon declared any location), we
|
|
286
|
+
* refuse to prune anything. An empty registry almost always means
|
|
287
|
+
* declarations failed to load (boot ordering, a stale install) — pruning
|
|
288
|
+
* "everything undeclared" in that state would wipe every system location
|
|
289
|
+
* (data/logs/recordings/…). Better to keep stale rows than destroy live ones.
|
|
290
|
+
*/
|
|
291
|
+
pruneUndeclaredSystemLocations() {
|
|
292
|
+
if (!this.registry) return;
|
|
293
|
+
if (this.registry.list().length === 0) {
|
|
294
|
+
this.logger.warn("storage-orchestrator: skipping prune — no location declarations loaded (fail-safe)");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
for (const [id, loc] of [...this.locations]) {
|
|
298
|
+
if (this.registry.cardinalityOf(loc.type) !== null) continue;
|
|
299
|
+
if (loc.isSystem) {
|
|
300
|
+
this.locations.delete(id);
|
|
301
|
+
if (this.locationStore) this.locationStore.delete(id).catch((err) => {
|
|
302
|
+
this.logger.error("storage-orchestrator: prune persistence failed", { meta: {
|
|
303
|
+
id,
|
|
304
|
+
error: err instanceof Error ? err.message : String(err)
|
|
305
|
+
} });
|
|
306
|
+
});
|
|
307
|
+
this.logger.info("storage-orchestrator: pruned stale system location", { meta: {
|
|
308
|
+
id,
|
|
309
|
+
type: loc.type
|
|
310
|
+
} });
|
|
311
|
+
} else this.logger.warn("storage-orchestrator: location of undeclared type kept (operator-owned)", { meta: {
|
|
312
|
+
id,
|
|
313
|
+
type: loc.type
|
|
314
|
+
} });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Resolve a `StorageLocationRef` to a concrete `StorageLocation`.
|
|
319
|
+
* - bare type (e.g. `'backups'`) → default location of that type
|
|
320
|
+
* - fully-qualified id (e.g. `'backups:nas-01'`) → that exact instance
|
|
321
|
+
*
|
|
322
|
+
* Throws an actionable error when nothing matches — every consumer of
|
|
323
|
+
* the singleton `storage` cap funnels through here, so the error
|
|
324
|
+
* message is what operators see when the orchestrator's view of the
|
|
325
|
+
* world doesn't match expectations.
|
|
326
|
+
*/
|
|
327
|
+
resolveRef(ref) {
|
|
328
|
+
if (ref.includes(":")) {
|
|
329
|
+
const loc = this.locations.get(ref);
|
|
330
|
+
if (!loc) throw new Error(`Storage location "${ref}" not found`);
|
|
331
|
+
return loc;
|
|
332
|
+
}
|
|
333
|
+
const def = this.getDefaultLocation(ref);
|
|
334
|
+
if (!def) throw new Error(`No default storage location configured for type "${ref}" — register one or pass a fully-qualified id (\`<type>:<slug>\`)`);
|
|
335
|
+
return def;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Find the storage-provider that backs a given location. Lookup is by
|
|
339
|
+
* `location.providerId` against `getProviderInfo().providerId` from
|
|
340
|
+
* each registered collection provider. Throws with both the missing
|
|
341
|
+
* providerId and the offending location id so the operator can pick
|
|
342
|
+
* the right place to fix the config.
|
|
343
|
+
*/
|
|
344
|
+
async getProviderFor(location) {
|
|
345
|
+
const providers = this.getProviders();
|
|
346
|
+
for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
|
|
347
|
+
throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
|
|
348
|
+
}
|
|
349
|
+
/** Convenience: lookup by id (returns `undefined` if not found). */
|
|
350
|
+
getLocationById(id) {
|
|
351
|
+
return this.locations.get(id);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Seed one default `StorageLocation` per addon-declared location that
|
|
355
|
+
* doesn't already have one. Idempotent — re-running with the same
|
|
356
|
+
* declarations on a populated map is a no-op (each declared `id`
|
|
357
|
+
* already resolves to an existing `<id>:default`).
|
|
358
|
+
*
|
|
359
|
+
* Convention: the bare-type ref `'recordings'` resolves via
|
|
360
|
+
* `getDefaultLocation('recordings')` (walks `isDefault === true`); the
|
|
361
|
+
* actual stored `id` is `'recordings:default'` so it satisfies the
|
|
362
|
+
* `^[a-z][a-z0-9-]*:[a-z0-9-]+$` regex on `StorageLocationSchema.id`.
|
|
363
|
+
*
|
|
364
|
+
* `basePath` is the storage root (e.g. `<CAMSTACK_DATA>`). Each
|
|
365
|
+
* location's `config.basePath` is `<basePath>/<id>`, except when the
|
|
366
|
+
* declaration sets `defaultsTo` — then it inherits the resolved root of
|
|
367
|
+
* the referenced location's default instance (sharing the same root
|
|
368
|
+
* directory for derivative slots like `recordingsLow` → `recordings`).
|
|
369
|
+
*/
|
|
370
|
+
seedDefaults(input) {
|
|
371
|
+
let added = 0;
|
|
372
|
+
const rootOf = (id) => path$1.join(input.basePath, id);
|
|
373
|
+
for (const d of input.declarations) {
|
|
374
|
+
if (this.hasAnyLocationOfType(d.id)) continue;
|
|
375
|
+
const base = d.defaultsTo ? this.getLocationById(`${d.defaultsTo}:default`)?.config["basePath"] ?? rootOf(d.defaultsTo) : rootOf(d.id);
|
|
376
|
+
this.upsertLocation({
|
|
377
|
+
id: `${d.id}:default`,
|
|
378
|
+
type: d.id,
|
|
379
|
+
displayName: d.displayName,
|
|
380
|
+
providerId: input.providerId,
|
|
381
|
+
config: { basePath: base },
|
|
382
|
+
isDefault: true,
|
|
383
|
+
isSystem: true
|
|
384
|
+
});
|
|
385
|
+
added++;
|
|
386
|
+
}
|
|
387
|
+
if (added > 0) this.logger.info("storage-orchestrator: seeded default locations", { meta: {
|
|
388
|
+
added,
|
|
389
|
+
basePath: input.basePath
|
|
390
|
+
} });
|
|
391
|
+
}
|
|
392
|
+
/** Internal: check whether any location of the given type exists. */
|
|
393
|
+
hasAnyLocationOfType(type) {
|
|
394
|
+
for (const loc of this.locations.values()) if (loc.type === type) return true;
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/builtins/storage-orchestrator/location-store.ts
|
|
400
|
+
var STORAGE_LOCATIONS_TABLE = "storage_locations";
|
|
401
|
+
/**
|
|
402
|
+
* Reserved key under which the cluster `nodeId` (SP1) is persisted INSIDE
|
|
403
|
+
* the `config` JSON blob. Storing it in the blob rather than a dedicated
|
|
404
|
+
* column avoids a destructive `ensureTable` DROP+recreate on deploy (a
|
|
405
|
+
* newly-declared column counts as a missing column → the structured
|
|
406
|
+
* backend drops the table, wiping operator location customizations such
|
|
407
|
+
* as `recordings:default`'s `minFreePercent`). The double-underscore
|
|
408
|
+
* prefix keeps it out of the provider-facing config namespace
|
|
409
|
+
* (`basePath`, `minFreePercent`, …) — the mapper strips it on read so
|
|
410
|
+
* API consumers and providers never see it.
|
|
411
|
+
*/
|
|
412
|
+
var NODE_ID_CONFIG_KEY = "__nodeId";
|
|
413
|
+
var STORAGE_LOCATIONS_SCHEMA = {
|
|
414
|
+
columns: [
|
|
415
|
+
{
|
|
416
|
+
name: "id",
|
|
417
|
+
type: "TEXT",
|
|
418
|
+
primaryKey: true
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: "type",
|
|
422
|
+
type: "TEXT",
|
|
423
|
+
notNull: true
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "display_name",
|
|
427
|
+
type: "TEXT",
|
|
428
|
+
notNull: true
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: "provider_id",
|
|
432
|
+
type: "TEXT",
|
|
433
|
+
notNull: true
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: "config",
|
|
437
|
+
type: "TEXT",
|
|
438
|
+
notNull: true,
|
|
439
|
+
defaultValue: "{}"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "is_default",
|
|
443
|
+
type: "INTEGER",
|
|
444
|
+
notNull: true,
|
|
445
|
+
defaultValue: 0
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "is_system",
|
|
449
|
+
type: "INTEGER",
|
|
450
|
+
notNull: true,
|
|
451
|
+
defaultValue: 0
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: "created_at",
|
|
455
|
+
type: "INTEGER",
|
|
456
|
+
notNull: true
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: "updated_at",
|
|
460
|
+
type: "INTEGER",
|
|
461
|
+
notNull: true
|
|
462
|
+
}
|
|
463
|
+
],
|
|
464
|
+
indexes: [{
|
|
465
|
+
name: "idx_storage_locations_type",
|
|
466
|
+
columns: ["type"]
|
|
467
|
+
}]
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* SQLite-backed implementation of `ILocationStore` — uses the
|
|
471
|
+
* `settings-store` cap's structured-table surface (`ensureTable` /
|
|
472
|
+
* `tableInsert` / `tableUpdate` / `tableDelete` / `tableQuery`). Wired
|
|
473
|
+
* to the live `ISettingsBackend` provider obtained from
|
|
474
|
+
* `kernel.capabilityRegistry.getSingleton('settings-store')`.
|
|
475
|
+
*
|
|
476
|
+
* The cap-router surface (`ctx.api.settingsStore.*`) does NOT expose
|
|
477
|
+
* the `tableXxx` methods — they're only on the raw `ISettingsBackend`.
|
|
478
|
+
* Same constraint that drives `IntegrationRegistry` to take a direct
|
|
479
|
+
* provider reference instead of going through the cap.
|
|
480
|
+
*/
|
|
481
|
+
var SqliteLocationStore = class {
|
|
482
|
+
backend;
|
|
483
|
+
tableEnsured = false;
|
|
484
|
+
constructor(backend) {
|
|
485
|
+
this.backend = backend;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Lazy `ensureTable` — keeps the constructor cheap and async-free.
|
|
489
|
+
* Called from every mutation/read path; the backend's `ensureTable`
|
|
490
|
+
* is itself idempotent (additive migration), so repeated calls are
|
|
491
|
+
* effectively no-ops after the first.
|
|
492
|
+
*/
|
|
493
|
+
async ensureTable() {
|
|
494
|
+
if (this.tableEnsured) return;
|
|
495
|
+
if (!this.backend.ensureTable) throw new Error("SqliteLocationStore: settings backend does not implement ensureTable — expected the SQLite settings backend, got a backend without structured-table support");
|
|
496
|
+
await this.backend.ensureTable(STORAGE_LOCATIONS_TABLE, STORAGE_LOCATIONS_SCHEMA);
|
|
497
|
+
this.tableEnsured = true;
|
|
498
|
+
}
|
|
499
|
+
async loadAll() {
|
|
500
|
+
await this.ensureTable();
|
|
501
|
+
if (!this.backend.tableQuery) return [];
|
|
502
|
+
const rows = await this.backend.tableQuery(STORAGE_LOCATIONS_TABLE, { orderBy: {
|
|
503
|
+
field: "created_at",
|
|
504
|
+
direction: "asc"
|
|
505
|
+
} });
|
|
506
|
+
const out = [];
|
|
507
|
+
for (const row of rows) {
|
|
508
|
+
const mapped = mapRowToLocation(row);
|
|
509
|
+
if (mapped) out.push(mapped);
|
|
510
|
+
}
|
|
511
|
+
return out;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Replace-or-insert. Implemented as `delete` + `insert` rather than
|
|
515
|
+
* the conditional `tableUpdate` path because the orchestrator
|
|
516
|
+
* serializes every mutation through the in-memory map before
|
|
517
|
+
* dispatching to the store — there's no contention to lose, and the
|
|
518
|
+
* delete-then-insert path is unconditionally simpler.
|
|
519
|
+
*/
|
|
520
|
+
async upsert(loc) {
|
|
521
|
+
await this.ensureTable();
|
|
522
|
+
if (!this.backend.tableDelete || !this.backend.tableInsert) throw new Error("SqliteLocationStore: backend missing tableDelete/tableInsert");
|
|
523
|
+
await this.backend.tableDelete(STORAGE_LOCATIONS_TABLE, { id: loc.id });
|
|
524
|
+
await this.backend.tableInsert(STORAGE_LOCATIONS_TABLE, mapLocationToRow(loc));
|
|
525
|
+
}
|
|
526
|
+
async delete(id) {
|
|
527
|
+
await this.ensureTable();
|
|
528
|
+
if (!this.backend.tableDelete) throw new Error("SqliteLocationStore: backend missing tableDelete");
|
|
529
|
+
await this.backend.tableDelete(STORAGE_LOCATIONS_TABLE, { id });
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
function mapLocationToRow(loc) {
|
|
533
|
+
const storedConfig = loc.nodeId !== void 0 ? {
|
|
534
|
+
...loc.config,
|
|
535
|
+
[NODE_ID_CONFIG_KEY]: loc.nodeId
|
|
536
|
+
} : { ...loc.config };
|
|
537
|
+
return {
|
|
538
|
+
id: loc.id,
|
|
539
|
+
type: loc.type,
|
|
540
|
+
display_name: loc.displayName,
|
|
541
|
+
provider_id: loc.providerId,
|
|
542
|
+
config: JSON.stringify(storedConfig),
|
|
543
|
+
is_default: loc.isDefault ? 1 : 0,
|
|
544
|
+
is_system: loc.isSystem ? 1 : 0,
|
|
545
|
+
created_at: loc.createdAt,
|
|
546
|
+
updated_at: loc.updatedAt
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Decode a row read from the structured table. Returns `null` when the
|
|
551
|
+
* row's `type` doesn't parse against the Zod enum — the caller skips
|
|
552
|
+
* the row and logs. Defensive against schema drift across upgrades
|
|
553
|
+
* (e.g. an older build wrote a since-removed `recordings-archive`
|
|
554
|
+
* type and the new build no longer recognises it).
|
|
555
|
+
*/
|
|
556
|
+
function mapRowToLocation(row) {
|
|
557
|
+
const typeRaw = row["type"];
|
|
558
|
+
const parsedType = StorageLocationTypeSchema.safeParse(typeRaw);
|
|
559
|
+
if (!parsedType.success) return null;
|
|
560
|
+
const type = parsedType.data;
|
|
561
|
+
const id = String(row["id"] ?? "");
|
|
562
|
+
if (!id) return null;
|
|
563
|
+
const configRaw = row["config"];
|
|
564
|
+
const storedConfig = typeof configRaw === "string" && configRaw.length > 0 ? parseJsonObject(configRaw) ?? {} : {};
|
|
565
|
+
const nodeIdRaw = storedConfig[NODE_ID_CONFIG_KEY];
|
|
566
|
+
const nodeId = typeof nodeIdRaw === "string" && nodeIdRaw.length > 0 ? nodeIdRaw : void 0;
|
|
567
|
+
const config = { ...storedConfig };
|
|
568
|
+
delete config[NODE_ID_CONFIG_KEY];
|
|
569
|
+
return {
|
|
570
|
+
id,
|
|
571
|
+
type,
|
|
572
|
+
displayName: String(row["display_name"] ?? ""),
|
|
573
|
+
providerId: String(row["provider_id"] ?? ""),
|
|
574
|
+
config,
|
|
575
|
+
...nodeId !== void 0 ? { nodeId } : {},
|
|
576
|
+
isDefault: row["is_default"] === 1 || row["is_default"] === true,
|
|
577
|
+
isSystem: row["is_system"] === 1 || row["is_system"] === true,
|
|
578
|
+
createdAt: Number(row["created_at"] ?? 0),
|
|
579
|
+
updatedAt: Number(row["updated_at"] ?? 0)
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/builtins/storage-orchestrator/provider-discovery.ts
|
|
584
|
+
async function collectProviderInfos(providers, onError) {
|
|
585
|
+
const out = [];
|
|
586
|
+
for (const [index, p] of providers.entries()) try {
|
|
587
|
+
const info = await p.getProviderInfo();
|
|
588
|
+
out.push(info);
|
|
589
|
+
} catch (err) {
|
|
590
|
+
onError(p, err, index);
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/builtins/storage-orchestrator/storage-pressure-manager.ts
|
|
596
|
+
var DEFAULT_MAX_ROUNDS = 8;
|
|
597
|
+
var DEFAULT_HYSTERESIS_PERCENT = 1;
|
|
598
|
+
var StoragePressureManager = class {
|
|
599
|
+
deps;
|
|
600
|
+
constructor(deps) {
|
|
601
|
+
this.deps = deps;
|
|
602
|
+
}
|
|
603
|
+
/** Relieve every managed location currently under its threshold. */
|
|
604
|
+
async sweep() {
|
|
605
|
+
const thresholds = await this.deps.getLocationThresholds();
|
|
606
|
+
for (const [locationId, threshold] of thresholds) {
|
|
607
|
+
if (threshold <= 0) continue;
|
|
608
|
+
try {
|
|
609
|
+
await this.relieveLocation(locationId, threshold);
|
|
610
|
+
} catch (err) {
|
|
611
|
+
this.deps.logger.warn("storage pressure: relief failed for location", { meta: {
|
|
612
|
+
locationId,
|
|
613
|
+
error: err instanceof Error ? err.message : String(err)
|
|
614
|
+
} });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async relieveLocation(locationId, threshold) {
|
|
619
|
+
const maxRounds = this.deps.maxRounds ?? DEFAULT_MAX_ROUNDS;
|
|
620
|
+
const hysteresis = this.deps.hysteresisPercent ?? DEFAULT_HYSTERESIS_PERCENT;
|
|
621
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
622
|
+
const free = await this.deps.getFreePercent(locationId);
|
|
623
|
+
if (free >= threshold) return;
|
|
624
|
+
const total = await this.deps.getVolumeTotalBytes(locationId);
|
|
625
|
+
const deficitPercent = threshold - free + hysteresis;
|
|
626
|
+
const targetBytes = Math.ceil(total * deficitPercent / 100);
|
|
627
|
+
if (targetBytes <= 0) return;
|
|
628
|
+
const providers = this.deps.getEvictableProviders();
|
|
629
|
+
const usages = await Promise.all(providers.map(async (p) => ({
|
|
630
|
+
p,
|
|
631
|
+
bytes: (await p.getEvictableUsage({ locationId })).bytes
|
|
632
|
+
})));
|
|
633
|
+
const totalEvictable = usages.reduce((s, u) => s + u.bytes, 0);
|
|
634
|
+
if (totalEvictable <= 0) {
|
|
635
|
+
this.deps.logger.debug("storage pressure: breach but no evictable data on location", { meta: {
|
|
636
|
+
locationId,
|
|
637
|
+
freePercent: free,
|
|
638
|
+
threshold
|
|
639
|
+
} });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
let reclaimed = 0;
|
|
643
|
+
for (const u of usages) {
|
|
644
|
+
if (u.bytes <= 0) continue;
|
|
645
|
+
const share = Math.max(1, Math.ceil(targetBytes * u.bytes / totalEvictable));
|
|
646
|
+
const res = await u.p.evict({
|
|
647
|
+
locationId,
|
|
648
|
+
targetBytes: share
|
|
649
|
+
});
|
|
650
|
+
reclaimed += res.reclaimedBytes;
|
|
651
|
+
}
|
|
652
|
+
this.deps.logger.info("storage pressure: relief round", { meta: {
|
|
653
|
+
locationId,
|
|
654
|
+
round,
|
|
655
|
+
freePercent: free,
|
|
656
|
+
threshold,
|
|
657
|
+
targetBytes,
|
|
658
|
+
reclaimedBytes: reclaimed
|
|
659
|
+
} });
|
|
660
|
+
if (reclaimed <= 0) return;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/builtins/storage-orchestrator/storage-orchestrator.addon.ts
|
|
666
|
+
/**
|
|
667
|
+
* `storage-orchestrator` builtin — singleton owner of the consumer-
|
|
668
|
+
* facing `storage` cap.
|
|
669
|
+
*
|
|
670
|
+
* Responsibilities:
|
|
671
|
+
* - Hold the `locationId → StorageLocation` map (in-memory; Task 6
|
|
672
|
+
* adds persistence).
|
|
673
|
+
* - Resolve `StorageLocationRef` → concrete `StorageLocation`.
|
|
674
|
+
* - Dispatch every cap method to the right `storage-provider`
|
|
675
|
+
* (collection cap, registered by `filesystem-storage` and future
|
|
676
|
+
* SFTP / S3 / WebDAV addons).
|
|
677
|
+
* - Track ownership of upload / download sessions so chunked I/O
|
|
678
|
+
* (which carries no `location` after the first hop) can route
|
|
679
|
+
* subsequent chunks to the same provider.
|
|
680
|
+
* - First-boot seed defaults from addon-declared location types (Task 5).
|
|
681
|
+
* Re-runs on every `capability:provider-registered` event so providers
|
|
682
|
+
* that register after the orchestrator boots also get seeded — the
|
|
683
|
+
* `hasAnyLocationOfType` guard makes the operation idempotent.
|
|
684
|
+
*
|
|
685
|
+
* Task 6 will plug a persistence backend in front of the in-memory map.
|
|
686
|
+
* Task 7 will migrate consumers off the legacy storage shim.
|
|
687
|
+
*/
|
|
688
|
+
var STORAGE_PROVIDER_CAP_NAME = "storage-provider";
|
|
689
|
+
var STORAGE_EVICTABLE_CAP_NAME = "storage-evictable";
|
|
690
|
+
/**
|
|
691
|
+
* Single-node default: existing node-local locations live on the hub.
|
|
692
|
+
* The boot backfill (SP1) stamps this on any node-local location seeded
|
|
693
|
+
* before `nodeId` existed.
|
|
694
|
+
*/
|
|
695
|
+
var HUB_NODE_ID = "hub";
|
|
696
|
+
/** How often the pressure manager re-evaluates free space per location. */
|
|
697
|
+
var PRESSURE_SWEEP_INTERVAL_MS = 6e4;
|
|
698
|
+
/**
|
|
699
|
+
* Raw event category emitted by `CapabilityRegistry.registerProvider` —
|
|
700
|
+
* not in the typed `EventCategory` enum because it lives in the kernel
|
|
701
|
+
* infra layer (every cap-bound event is a custom-shape payload).
|
|
702
|
+
*/
|
|
703
|
+
var CAP_PROVIDER_REGISTERED_CATEGORY = "capability:provider-registered";
|
|
704
|
+
var StorageOrchestratorAddon = class extends BaseAddon {
|
|
705
|
+
service = null;
|
|
706
|
+
pressureManager = null;
|
|
707
|
+
pressureTimer = null;
|
|
708
|
+
pressureSweepInFlight = false;
|
|
709
|
+
/**
|
|
710
|
+
* `uploadId` → providerId. Populated on `beginUpload`, consulted on
|
|
711
|
+
* every subsequent `writeChunk` / `finalizeUpload` / `abortUpload`,
|
|
712
|
+
* cleared on terminal calls. The provider holds the actual session
|
|
713
|
+
* state (open file descriptor, buffered offsets, …) — we just
|
|
714
|
+
* remember which provider owns it.
|
|
715
|
+
*/
|
|
716
|
+
uploadOwners = /* @__PURE__ */ new Map();
|
|
717
|
+
/** Symmetric to `uploadOwners` for download sessions. */
|
|
718
|
+
downloadOwners = /* @__PURE__ */ new Map();
|
|
719
|
+
/**
|
|
720
|
+
* Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
|
|
721
|
+
* synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
|
|
722
|
+
* keep a sync cache and refresh it whenever the provider collection
|
|
723
|
+
* changes (alongside the seed pass). Absent providerId → resolver returns
|
|
724
|
+
* `undefined` (treated as node-agnostic).
|
|
725
|
+
*/
|
|
726
|
+
nodeLocalByProvider = /* @__PURE__ */ new Map();
|
|
727
|
+
/**
|
|
728
|
+
* Disposers run on `onShutdown` — currently the eventBus subscription
|
|
729
|
+
* for `capability:provider-registered` events used by the lazy seed
|
|
730
|
+
* fallback. Stored separately from the `BaseAddon` disposer chain so
|
|
731
|
+
* it can be inspected in tests.
|
|
732
|
+
*/
|
|
733
|
+
seedSubscriptionDisposers = [];
|
|
734
|
+
constructor() {
|
|
735
|
+
super({});
|
|
736
|
+
}
|
|
737
|
+
async onInitialize() {
|
|
738
|
+
const getProviders = () => {
|
|
739
|
+
const reg = this.ctx.kernel.capabilityRegistry;
|
|
740
|
+
if (!reg) return [];
|
|
741
|
+
return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
|
|
742
|
+
};
|
|
743
|
+
const locationStore = this.resolveLocationStore();
|
|
744
|
+
const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore);
|
|
745
|
+
this.service = service;
|
|
746
|
+
service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
|
|
747
|
+
await service.initialize();
|
|
748
|
+
const provider = {
|
|
749
|
+
listLocations: async ({ type }) => {
|
|
750
|
+
return type !== void 0 ? service.listLocations({ type }) : service.listLocations();
|
|
751
|
+
},
|
|
752
|
+
getDefaultLocation: async ({ type }) => service.getDefaultLocation(type),
|
|
753
|
+
listLocationDeclarations: async () => service.listDeclarations(),
|
|
754
|
+
upsertLocation: async (input) => service.upsertLocation(input),
|
|
755
|
+
deleteLocation: async ({ id }) => {
|
|
756
|
+
service.deleteLocation(id);
|
|
757
|
+
},
|
|
758
|
+
testLocation: async ({ id }) => {
|
|
759
|
+
const loc = service.getLocationById(id);
|
|
760
|
+
if (!loc) return {
|
|
761
|
+
ok: false,
|
|
762
|
+
error: `Location "${id}" not found`
|
|
763
|
+
};
|
|
764
|
+
try {
|
|
765
|
+
return (await service.getProviderFor(loc)).testLocation({ config: loc.config });
|
|
766
|
+
} catch (err) {
|
|
767
|
+
return {
|
|
768
|
+
ok: false,
|
|
769
|
+
error: err instanceof Error ? err.message : String(err)
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
listProviders: async () => {
|
|
774
|
+
return collectProviderInfos(getProviders(), (_provider, err, index) => {
|
|
775
|
+
this.ctx.logger.warn("storage-orchestrator: getProviderInfo failed", { meta: {
|
|
776
|
+
providerIndex: index,
|
|
777
|
+
error: err instanceof Error ? err.message : String(err)
|
|
778
|
+
} });
|
|
779
|
+
});
|
|
780
|
+
},
|
|
781
|
+
testConfig: async ({ providerId, config }) => {
|
|
782
|
+
const providers = getProviders();
|
|
783
|
+
for (const p of providers) try {
|
|
784
|
+
if ((await p.getProviderInfo()).providerId !== providerId) continue;
|
|
785
|
+
return p.testLocation({ config });
|
|
786
|
+
} catch (err) {
|
|
787
|
+
return {
|
|
788
|
+
ok: false,
|
|
789
|
+
error: err instanceof Error ? err.message : String(err)
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
ok: false,
|
|
794
|
+
error: `No storage-provider registered for providerId "${providerId}"`
|
|
795
|
+
};
|
|
796
|
+
},
|
|
797
|
+
resolve: async ({ location, relativePath }) => {
|
|
798
|
+
const loc = service.resolveRef(location);
|
|
799
|
+
return (await service.getProviderFor(loc)).resolve({
|
|
800
|
+
location: loc,
|
|
801
|
+
relativePath
|
|
802
|
+
});
|
|
803
|
+
},
|
|
804
|
+
write: async ({ location, relativePath, data }) => {
|
|
805
|
+
const loc = service.resolveRef(location);
|
|
806
|
+
return (await service.getProviderFor(loc)).write({
|
|
807
|
+
location: loc,
|
|
808
|
+
relativePath,
|
|
809
|
+
data
|
|
810
|
+
});
|
|
811
|
+
},
|
|
812
|
+
read: async ({ location, relativePath }) => {
|
|
813
|
+
const loc = service.resolveRef(location);
|
|
814
|
+
return (await service.getProviderFor(loc)).read({
|
|
815
|
+
location: loc,
|
|
816
|
+
relativePath
|
|
817
|
+
});
|
|
818
|
+
},
|
|
819
|
+
exists: async ({ location, relativePath }) => {
|
|
820
|
+
const loc = service.resolveRef(location);
|
|
821
|
+
return (await service.getProviderFor(loc)).exists({
|
|
822
|
+
location: loc,
|
|
823
|
+
relativePath
|
|
824
|
+
});
|
|
825
|
+
},
|
|
826
|
+
list: async ({ location, prefix }) => {
|
|
827
|
+
const loc = service.resolveRef(location);
|
|
828
|
+
const p = await service.getProviderFor(loc);
|
|
829
|
+
return prefix !== void 0 ? p.list({
|
|
830
|
+
location: loc,
|
|
831
|
+
prefix
|
|
832
|
+
}) : p.list({ location: loc });
|
|
833
|
+
},
|
|
834
|
+
delete: async ({ location, relativePath }) => {
|
|
835
|
+
const loc = service.resolveRef(location);
|
|
836
|
+
return (await service.getProviderFor(loc)).delete({
|
|
837
|
+
location: loc,
|
|
838
|
+
relativePath
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
getAvailableSpace: async ({ location }) => {
|
|
842
|
+
const loc = service.resolveRef(location);
|
|
843
|
+
return (await service.getProviderFor(loc)).getAvailableSpace({ location: loc });
|
|
844
|
+
},
|
|
845
|
+
beginUpload: async ({ location, relativePath, sizeBytes }) => {
|
|
846
|
+
const loc = service.resolveRef(location);
|
|
847
|
+
const p = await service.getProviderFor(loc);
|
|
848
|
+
const result = sizeBytes !== void 0 ? await p.beginUpload({
|
|
849
|
+
location: loc,
|
|
850
|
+
relativePath,
|
|
851
|
+
sizeBytes
|
|
852
|
+
}) : await p.beginUpload({
|
|
853
|
+
location: loc,
|
|
854
|
+
relativePath
|
|
855
|
+
});
|
|
856
|
+
this.uploadOwners.set(result.uploadId, loc.providerId);
|
|
857
|
+
return result;
|
|
858
|
+
},
|
|
859
|
+
writeChunk: async (input) => {
|
|
860
|
+
return (await this.resolveByUploadId(input.uploadId)).writeChunk(input);
|
|
861
|
+
},
|
|
862
|
+
finalizeUpload: async (input) => {
|
|
863
|
+
const p = await this.resolveByUploadId(input.uploadId);
|
|
864
|
+
try {
|
|
865
|
+
return await p.finalizeUpload(input);
|
|
866
|
+
} finally {
|
|
867
|
+
this.uploadOwners.delete(input.uploadId);
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
abortUpload: async (input) => {
|
|
871
|
+
const p = await this.resolveByUploadId(input.uploadId);
|
|
872
|
+
try {
|
|
873
|
+
return await p.abortUpload(input);
|
|
874
|
+
} finally {
|
|
875
|
+
this.uploadOwners.delete(input.uploadId);
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
beginDownload: async ({ location, relativePath }) => {
|
|
879
|
+
const loc = service.resolveRef(location);
|
|
880
|
+
const result = await (await service.getProviderFor(loc)).beginDownload({
|
|
881
|
+
location: loc,
|
|
882
|
+
relativePath
|
|
883
|
+
});
|
|
884
|
+
this.downloadOwners.set(result.downloadId, loc.providerId);
|
|
885
|
+
return result;
|
|
886
|
+
},
|
|
887
|
+
readChunk: async (input) => {
|
|
888
|
+
return (await this.resolveByDownloadId(input.downloadId)).readChunk(input);
|
|
889
|
+
},
|
|
890
|
+
endDownload: async (input) => {
|
|
891
|
+
const p = await this.resolveByDownloadId(input.downloadId);
|
|
892
|
+
try {
|
|
893
|
+
return await p.endDownload(input);
|
|
894
|
+
} finally {
|
|
895
|
+
this.downloadOwners.delete(input.downloadId);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
await this.seedFromDeclarations();
|
|
900
|
+
const eventBus = this.ctx.eventBus;
|
|
901
|
+
if (eventBus) {
|
|
902
|
+
const unsub = eventBus.subscribe({ category: CAP_PROVIDER_REGISTERED_CATEGORY }, (event) => {
|
|
903
|
+
this.ensureStoreWired();
|
|
904
|
+
if (event.data?.capability !== STORAGE_PROVIDER_CAP_NAME) return;
|
|
905
|
+
this.seedFromDeclarations();
|
|
906
|
+
});
|
|
907
|
+
this.seedSubscriptionDisposers.push(unsub);
|
|
908
|
+
}
|
|
909
|
+
this.ensureStoreWired();
|
|
910
|
+
this.pressureManager = new StoragePressureManager({
|
|
911
|
+
logger: this.ctx.logger,
|
|
912
|
+
getFreePercent: (id) => this.locationFreePercent(id),
|
|
913
|
+
getVolumeTotalBytes: (id) => this.locationVolumeTotalBytes(id),
|
|
914
|
+
getEvictableProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_EVICTABLE_CAP_NAME) ?? [],
|
|
915
|
+
getLocationThresholds: () => this.resolveLocationThresholds()
|
|
916
|
+
});
|
|
917
|
+
this.pressureTimer = setInterval(() => {
|
|
918
|
+
this.runPressureSweep();
|
|
919
|
+
}, PRESSURE_SWEEP_INTERVAL_MS);
|
|
920
|
+
this.ctx.logger.info("Storage orchestrator initialized");
|
|
921
|
+
return [{
|
|
922
|
+
capability: storageCapability,
|
|
923
|
+
provider
|
|
924
|
+
}];
|
|
925
|
+
}
|
|
926
|
+
async onShutdown() {
|
|
927
|
+
for (const dispose of this.seedSubscriptionDisposers) try {
|
|
928
|
+
dispose();
|
|
929
|
+
} catch {}
|
|
930
|
+
this.seedSubscriptionDisposers.length = 0;
|
|
931
|
+
if (this.pressureTimer) {
|
|
932
|
+
clearInterval(this.pressureTimer);
|
|
933
|
+
this.pressureTimer = null;
|
|
934
|
+
}
|
|
935
|
+
this.pressureManager = null;
|
|
936
|
+
this.uploadOwners.clear();
|
|
937
|
+
this.downloadOwners.clear();
|
|
938
|
+
this.service = null;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Resolve the live `ISettingsBackend` from the capability registry
|
|
942
|
+
* and wrap it in a `SqliteLocationStore`. Returns `null` when the
|
|
943
|
+
* settings-store cap isn't registered yet — the service falls back
|
|
944
|
+
* to in-memory only. Production boot order (sqlite-settings runs
|
|
945
|
+
* before the orchestrator) makes this the rare path.
|
|
946
|
+
*/
|
|
947
|
+
resolveLocationStore(quiet = false) {
|
|
948
|
+
const reg = this.ctx.kernel.capabilityRegistry;
|
|
949
|
+
if (!reg) return null;
|
|
950
|
+
const backend = reg.getSingleton("settings-store");
|
|
951
|
+
if (!backend) {
|
|
952
|
+
if (!quiet) this.ctx.logger.warn("storage-orchestrator: no settings-store provider yet — running in-memory until it registers");
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
if (!backend.ensureTable || !backend.tableInsert) {
|
|
956
|
+
if (!quiet) this.ctx.logger.warn("storage-orchestrator: settings backend lacks structured-table support — running in-memory only");
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
return new SqliteLocationStore(backend);
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Late-wire the persistence store once the `settings-store` cap registers.
|
|
963
|
+
* Production boot order means the store is usually absent at
|
|
964
|
+
* `onInitialize`; this runs on every `capability:provider-registered`
|
|
965
|
+
* event until it succeeds. Single-flighted + idempotent (the service
|
|
966
|
+
* ignores a second attach). Without this, operator edits (e.g. a
|
|
967
|
+
* location's `minFreePercent`) live only in memory and are wiped on every
|
|
968
|
+
* restart — the service never hydrates from / persists to SQLite.
|
|
969
|
+
*/
|
|
970
|
+
storeWireInFlight = null;
|
|
971
|
+
async ensureStoreWired() {
|
|
972
|
+
const service = this.service;
|
|
973
|
+
if (!service || service.hasStore()) return;
|
|
974
|
+
if (this.storeWireInFlight) return this.storeWireInFlight;
|
|
975
|
+
const store = this.resolveLocationStore(true);
|
|
976
|
+
if (!store) return;
|
|
977
|
+
this.ctx.logger.info("storage-orchestrator: settings-store now available — wiring persistence");
|
|
978
|
+
this.storeWireInFlight = service.attachStore(store).then(() => this.seedFromDeclarations()).catch((err) => {
|
|
979
|
+
this.ctx.logger.error("storage-orchestrator: attachStore failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
980
|
+
}).finally(() => {
|
|
981
|
+
this.storeWireInFlight = null;
|
|
982
|
+
});
|
|
983
|
+
return this.storeWireInFlight;
|
|
984
|
+
}
|
|
985
|
+
storageProviders() {
|
|
986
|
+
const reg = this.ctx.kernel.capabilityRegistry;
|
|
987
|
+
if (!reg) return [];
|
|
988
|
+
return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
|
|
989
|
+
}
|
|
990
|
+
/** Single-flighted pressure sweep — never overlaps a slow eviction round. */
|
|
991
|
+
async runPressureSweep() {
|
|
992
|
+
if (this.pressureSweepInFlight || !this.pressureManager) return;
|
|
993
|
+
this.pressureSweepInFlight = true;
|
|
994
|
+
try {
|
|
995
|
+
await this.pressureManager.sweep();
|
|
996
|
+
} catch (err) {
|
|
997
|
+
this.ctx.logger.warn("storage pressure sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
998
|
+
} finally {
|
|
999
|
+
this.pressureSweepInFlight = false;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* locationId → resolved `minFreePercent`. The policy is intrinsic to the
|
|
1004
|
+
* location's provider (`shouldSaveDiskSpace` gates the guard, `minFreePercent`
|
|
1005
|
+
* is the default), overridable per-location via `config.minFreePercent`.
|
|
1006
|
+
* Locations with the guard off (or threshold 0) are omitted. Moved here from
|
|
1007
|
+
* the recorder so disk-pressure is a single core concern.
|
|
1008
|
+
*/
|
|
1009
|
+
async resolveLocationThresholds() {
|
|
1010
|
+
const out = /* @__PURE__ */ new Map();
|
|
1011
|
+
const svc = this.service;
|
|
1012
|
+
if (!svc) return out;
|
|
1013
|
+
const infos = await collectProviderInfos(this.storageProviders(), () => {});
|
|
1014
|
+
const byProvider = new Map(infos.map((i) => [i.providerId, i]));
|
|
1015
|
+
for (const loc of svc.listLocations()) {
|
|
1016
|
+
const info = byProvider.get(loc.providerId);
|
|
1017
|
+
if (!info || !info.shouldSaveDiskSpace) continue;
|
|
1018
|
+
const override = loc.config["minFreePercent"];
|
|
1019
|
+
const threshold = typeof override === "number" && Number.isFinite(override) && override >= 0 ? override : info.minFreePercent ?? 0;
|
|
1020
|
+
if (threshold > 0) out.set(loc.id, threshold);
|
|
1021
|
+
}
|
|
1022
|
+
return out;
|
|
1023
|
+
}
|
|
1024
|
+
locationBasePath(locationId) {
|
|
1025
|
+
const bp = (this.service?.listLocations().find((l) => l.id === locationId))?.config["basePath"];
|
|
1026
|
+
return typeof bp === "string" && bp.length > 0 ? bp : null;
|
|
1027
|
+
}
|
|
1028
|
+
/** Free capacity (%) on a location's volume via `statfs`; 100 (guard inert) when unstattable. */
|
|
1029
|
+
async locationFreePercent(locationId) {
|
|
1030
|
+
const bp = this.locationBasePath(locationId);
|
|
1031
|
+
if (!bp) return 100;
|
|
1032
|
+
try {
|
|
1033
|
+
const st = await fs.statfs(bp);
|
|
1034
|
+
if (st.blocks <= 0) return 100;
|
|
1035
|
+
return st.bavail / st.blocks * 100;
|
|
1036
|
+
} catch {
|
|
1037
|
+
return 100;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/** Total bytes on a location's volume via `statfs`; 0 when unstattable. */
|
|
1041
|
+
async locationVolumeTotalBytes(locationId) {
|
|
1042
|
+
const bp = this.locationBasePath(locationId);
|
|
1043
|
+
if (!bp) return 0;
|
|
1044
|
+
try {
|
|
1045
|
+
const st = await fs.statfs(bp);
|
|
1046
|
+
return st.blocks * st.bsize;
|
|
1047
|
+
} catch {
|
|
1048
|
+
return 0;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Seed default storage locations from the addon-declared
|
|
1053
|
+
* `storageLocations`, aggregated by the kernel into a
|
|
1054
|
+
* `StorageLocationRegistry`. Idempotent — re-running on a populated
|
|
1055
|
+
* map is a no-op (each declared id's `<id>:default` is already in the
|
|
1056
|
+
* service's map).
|
|
1057
|
+
*
|
|
1058
|
+
* `basePath` is the filesystem storage root, falling back to
|
|
1059
|
+
* `process.env.CAMSTACK_DATA ?? path.resolve(process.cwd(), 'camstack-data')`.
|
|
1060
|
+
* Every declared default is attributed to the `filesystem-storage`
|
|
1061
|
+
* provider; operators repoint individual locations (or add remote
|
|
1062
|
+
* SFTP/S3/WebDAV instances) through the admin UI afterwards.
|
|
1063
|
+
*/
|
|
1064
|
+
async seedFromDeclarations() {
|
|
1065
|
+
if (!this.service) return;
|
|
1066
|
+
await this.refreshNodeLocalCache();
|
|
1067
|
+
const registry = buildStorageLocationRegistry(this.ctx.kernel.listStorageLocationDeclarations?.() ?? []);
|
|
1068
|
+
this.service.setRegistry(registry);
|
|
1069
|
+
this.service.pruneUndeclaredSystemLocations();
|
|
1070
|
+
const basePath = process.env["CAMSTACK_DATA"] ?? path$1.resolve(process.cwd(), "camstack-data");
|
|
1071
|
+
this.service.seedDefaults({
|
|
1072
|
+
providerId: "filesystem-storage",
|
|
1073
|
+
basePath,
|
|
1074
|
+
declarations: registry.list()
|
|
1075
|
+
});
|
|
1076
|
+
await this.service.backfillNodeIds(HUB_NODE_ID);
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Rebuild the `providerId → nodeLocal` cache from every registered
|
|
1080
|
+
* `storage-provider`'s `getProviderInfo()`. Cheap registry scan; runs on
|
|
1081
|
+
* boot and on every provider-registered reconcile. Failed probes are
|
|
1082
|
+
* skipped (the provider stays absent → treated as node-agnostic).
|
|
1083
|
+
*/
|
|
1084
|
+
async refreshNodeLocalCache() {
|
|
1085
|
+
const reg = this.ctx.kernel.capabilityRegistry;
|
|
1086
|
+
if (!reg) return;
|
|
1087
|
+
const infos = await collectProviderInfos(reg.getCollection(STORAGE_PROVIDER_CAP_NAME), (_p, err, index) => {
|
|
1088
|
+
this.ctx.logger.warn("storage-orchestrator: getProviderInfo failed (nodeLocal cache)", { meta: {
|
|
1089
|
+
providerIndex: index,
|
|
1090
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1091
|
+
} });
|
|
1092
|
+
});
|
|
1093
|
+
for (const info of infos) this.nodeLocalByProvider.set(info.providerId, info.nodeLocal);
|
|
1094
|
+
}
|
|
1095
|
+
async resolveByUploadId(uploadId) {
|
|
1096
|
+
const providerId = this.uploadOwners.get(uploadId);
|
|
1097
|
+
if (!providerId) throw new Error(`Unknown uploadId "${uploadId}" — orchestrator has no record of beginUpload`);
|
|
1098
|
+
return this.providerByProviderId(providerId, `uploadId "${uploadId}"`);
|
|
1099
|
+
}
|
|
1100
|
+
async resolveByDownloadId(downloadId) {
|
|
1101
|
+
const providerId = this.downloadOwners.get(downloadId);
|
|
1102
|
+
if (!providerId) throw new Error(`Unknown downloadId "${downloadId}" — orchestrator has no record of beginDownload`);
|
|
1103
|
+
return this.providerByProviderId(providerId, `downloadId "${downloadId}"`);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Look up a `storage-provider` by `providerId` only — used for
|
|
1107
|
+
* upload / download dispatch where the original `StorageLocation` is
|
|
1108
|
+
* no longer in scope. Throws if the provider has gone away (e.g.
|
|
1109
|
+
* addon unloaded mid-session).
|
|
1110
|
+
*/
|
|
1111
|
+
async providerByProviderId(providerId, sessionDescriptor) {
|
|
1112
|
+
if (!this.service) throw new Error("storage-orchestrator: service not initialized");
|
|
1113
|
+
const probe = {
|
|
1114
|
+
id: `__session:${sessionDescriptor}`,
|
|
1115
|
+
type: "data",
|
|
1116
|
+
displayName: sessionDescriptor,
|
|
1117
|
+
providerId,
|
|
1118
|
+
config: {},
|
|
1119
|
+
isDefault: false,
|
|
1120
|
+
isSystem: false,
|
|
1121
|
+
createdAt: 0,
|
|
1122
|
+
updatedAt: 0
|
|
1123
|
+
};
|
|
1124
|
+
return this.service.getProviderFor(probe);
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
//#endregion
|
|
1128
|
+
export { StorageOrchestratorAddon, StorageOrchestratorAddon as default, StorageOrchestratorService as n, SqliteLocationStore as t };
|