@camstack/system 1.2.25 → 1.2.26
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/device-manager/device-aggregation.d.ts +18 -0
- package/dist/builtins/device-manager/device-manager.addon.js +47 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +47 -1
- 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/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 +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +0 -19
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +143 -67
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +143 -67
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +67 -7
- 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-CS0pdC8l.js → dist-Cu72wsz5.js} +132 -3
- package/dist/{dist-CIek6F9X.mjs → dist-DimX9eZy.mjs} +132 -3
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -3,18 +3,44 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Cu72wsz5.js");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_chunk.__toESM(node_path);
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
10
10
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
11
11
|
let _camstack_system = require("@camstack/system");
|
|
12
12
|
//#region src/builtins/storage-orchestrator/storage-orchestrator.service.ts
|
|
13
|
+
var SESSION_VOCABULARY = {
|
|
14
|
+
upload: {
|
|
15
|
+
idLabel: "uploadId",
|
|
16
|
+
openMethod: "beginUpload"
|
|
17
|
+
},
|
|
18
|
+
download: {
|
|
19
|
+
idLabel: "downloadId",
|
|
20
|
+
openMethod: "beginDownload"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/** Map key for a tracked chunked session — the id spaces never collide. */
|
|
24
|
+
function sessionKey(kind, sessionId) {
|
|
25
|
+
return `${kind}:${sessionId}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Bare node id for a raw kernel `localNodeId`. Forked children carry a
|
|
29
|
+
* `<node>/<addon>` suffix; routing is per-NODE, so the addon segment is
|
|
30
|
+
* dropped. (Mirrors `normalizeNodeId` in `@camstack/types/addon`, which is a
|
|
31
|
+
* leaf module not re-exported on any subpath this package can import.)
|
|
32
|
+
*/
|
|
33
|
+
function bareNodeId(raw) {
|
|
34
|
+
if (raw === void 0 || raw === "") return "hub";
|
|
35
|
+
const slash = raw.indexOf("/");
|
|
36
|
+
if (slash < 0) return raw;
|
|
37
|
+
const bare = raw.slice(0, slash);
|
|
38
|
+
return bare === "" ? "hub" : bare;
|
|
39
|
+
}
|
|
13
40
|
var StorageOrchestratorService = class {
|
|
14
41
|
logger;
|
|
15
42
|
getProviders;
|
|
16
43
|
locationStore;
|
|
17
|
-
localNodeId;
|
|
18
44
|
getRemoteProvider;
|
|
19
45
|
locations = /* @__PURE__ */ new Map();
|
|
20
46
|
/**
|
|
@@ -33,12 +59,29 @@ var StorageOrchestratorService = class {
|
|
|
33
59
|
* early-boot path that predates the resolver wiring.
|
|
34
60
|
*/
|
|
35
61
|
nodeLocalResolver = null;
|
|
62
|
+
/** `localNodeId` with any forked-child `/addon` suffix stripped. */
|
|
63
|
+
localNode;
|
|
64
|
+
/**
|
|
65
|
+
* `<kind>:<sessionId>` → the dispatch target that OPENED the session.
|
|
66
|
+
* Chunked I/O carries no `StorageLocation` after the first hop, so without
|
|
67
|
+
* this the follow-up chunks fall back to a `providerId`-only lookup — which
|
|
68
|
+
* on a multi-node cluster means the hub's filesystem, not the agent's.
|
|
69
|
+
*/
|
|
70
|
+
sessionTargets = /* @__PURE__ */ new Map();
|
|
36
71
|
constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
|
|
37
72
|
this.logger = logger;
|
|
38
73
|
this.getProviders = getProviders;
|
|
39
74
|
this.locationStore = locationStore;
|
|
40
|
-
this.localNodeId = localNodeId;
|
|
41
75
|
this.getRemoteProvider = getRemoteProvider;
|
|
76
|
+
this.localNode = bareNodeId(localNodeId);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The node this orchestrator runs on, with any forked-child `/addon`
|
|
80
|
+
* suffix stripped. A location whose `nodeId` differs is NOT this node's
|
|
81
|
+
* to touch — its bytes, and its free space, live elsewhere.
|
|
82
|
+
*/
|
|
83
|
+
getLocalNodeId() {
|
|
84
|
+
return this.localNode;
|
|
42
85
|
}
|
|
43
86
|
/** True once a persistence store is wired (constructor or {@link attachStore}). */
|
|
44
87
|
hasStore() {
|
|
@@ -346,22 +389,96 @@ var StorageOrchestratorService = class {
|
|
|
346
389
|
return def;
|
|
347
390
|
}
|
|
348
391
|
/**
|
|
349
|
-
* Find the storage-provider that backs a given location
|
|
350
|
-
* `
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
392
|
+
* Find the storage-provider that backs a given location — the entry point
|
|
393
|
+
* every consumer-facing `storage` cap method funnels through.
|
|
394
|
+
*
|
|
395
|
+
* Routing key is `(providerId, nodeId)`, never `providerId` alone: every
|
|
396
|
+
* node's FilesystemStorageProvider registers under the SAME providerId, so
|
|
397
|
+
* a providerId-only match resolves to whichever node asked. That is how a
|
|
398
|
+
* location created on an agent ended up writing to the HUB's filesystem
|
|
399
|
+
* (root-caused 2026-07-29).
|
|
354
400
|
*/
|
|
355
401
|
async getProviderFor(location) {
|
|
356
|
-
|
|
357
|
-
|
|
402
|
+
return this.getProviderForTarget({
|
|
403
|
+
providerId: location.providerId,
|
|
404
|
+
...location.nodeId !== void 0 && { nodeId: location.nodeId }
|
|
405
|
+
}, `location "${location.id}"`, location.id);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Resolve one dispatch target to the provider that must execute it.
|
|
409
|
+
*
|
|
410
|
+
* `descriptor` is the operator-facing subject of the error message
|
|
411
|
+
* (`location "recordings:disk"`, `uploadId "u-42"`); `locationId` is the
|
|
412
|
+
* structured tag for the log line when there is a location behind the
|
|
413
|
+
* dispatch (chunked sessions carry only the session id).
|
|
414
|
+
*
|
|
415
|
+
* Both failure branches LOG before throwing, and NEITHER falls back to the
|
|
416
|
+
* local provider — a silent fallback is the bug this method exists to fix.
|
|
417
|
+
*/
|
|
418
|
+
async getProviderForTarget(target, descriptor, locationId) {
|
|
419
|
+
const nodeId = target.nodeId;
|
|
420
|
+
if (nodeId !== void 0 && bareNodeId(nodeId) !== this.localNode) {
|
|
358
421
|
const remote = this.getRemoteProvider?.(nodeId);
|
|
359
422
|
if (remote) return remote;
|
|
360
|
-
|
|
423
|
+
this.logger.error("storage-orchestrator: target node has no reachable storage-provider — dispatch refused", { meta: {
|
|
424
|
+
...locationId !== void 0 && { locationId },
|
|
425
|
+
descriptor,
|
|
426
|
+
nodeId,
|
|
427
|
+
providerId: target.providerId,
|
|
428
|
+
localNodeId: this.localNode,
|
|
429
|
+
crossNodeResolverWired: this.getRemoteProvider !== void 0
|
|
430
|
+
} });
|
|
431
|
+
throw new Error(`storage-provider for ${descriptor} lives on node "${nodeId}" which is not reachable`);
|
|
361
432
|
}
|
|
362
433
|
const providers = this.getProviders();
|
|
363
|
-
for (const p of providers) if ((await p.getProviderInfo()).providerId ===
|
|
364
|
-
|
|
434
|
+
for (const p of providers) if ((await p.getProviderInfo()).providerId === target.providerId) return p;
|
|
435
|
+
this.logger.error("storage-orchestrator: no local storage-provider for providerId", { meta: {
|
|
436
|
+
...locationId !== void 0 && { locationId },
|
|
437
|
+
descriptor,
|
|
438
|
+
providerId: target.providerId,
|
|
439
|
+
localNodeId: this.localNode,
|
|
440
|
+
registered: providers.length
|
|
441
|
+
} });
|
|
442
|
+
throw new Error(`No storage-provider registered for providerId "${target.providerId}" (${descriptor})`);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Remember which provider — and on which NODE — opened a chunked session.
|
|
446
|
+
* `beginUpload` / `beginDownload` carry a `StorageLocation`; every later
|
|
447
|
+
* chunk carries only the session id, and a session id issued by an agent
|
|
448
|
+
* means nothing to the hub's provider.
|
|
449
|
+
*/
|
|
450
|
+
rememberSession(kind, sessionId, location) {
|
|
451
|
+
this.sessionTargets.set(sessionKey(kind, sessionId), {
|
|
452
|
+
providerId: location.providerId,
|
|
453
|
+
...location.nodeId !== void 0 && { nodeId: location.nodeId }
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/** Drop a session's routing entry (finalize / abort / end). */
|
|
457
|
+
forgetSession(kind, sessionId) {
|
|
458
|
+
this.sessionTargets.delete(sessionKey(kind, sessionId));
|
|
459
|
+
}
|
|
460
|
+
/** Drop every session entry (addon shutdown). */
|
|
461
|
+
clearSessions() {
|
|
462
|
+
this.sessionTargets.clear();
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Resolve the provider that owns an in-flight chunked session, on the node
|
|
466
|
+
* that opened it. Throws when the orchestrator has no record of the open
|
|
467
|
+
* call (provider gone, or a chunk for a session it never issued).
|
|
468
|
+
*/
|
|
469
|
+
async getProviderForSession(kind, sessionId) {
|
|
470
|
+
const vocabulary = SESSION_VOCABULARY[kind];
|
|
471
|
+
const descriptor = `${vocabulary.idLabel} "${sessionId}"`;
|
|
472
|
+
const target = this.sessionTargets.get(sessionKey(kind, sessionId));
|
|
473
|
+
if (!target) {
|
|
474
|
+
this.logger.error("storage-orchestrator: chunk for an unknown session — dispatch refused", { meta: {
|
|
475
|
+
descriptor,
|
|
476
|
+
kind,
|
|
477
|
+
sessionId
|
|
478
|
+
} });
|
|
479
|
+
throw new Error(`Unknown ${vocabulary.idLabel} "${sessionId}" — orchestrator has no record of ${vocabulary.openMethod}`);
|
|
480
|
+
}
|
|
481
|
+
return this.getProviderForTarget(target, descriptor);
|
|
365
482
|
}
|
|
366
483
|
/** Convenience: lookup by id (returns `undefined` if not found). */
|
|
367
484
|
getLocationById(id) {
|
|
@@ -795,16 +912,6 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
795
912
|
pressureTimer = null;
|
|
796
913
|
pressureSweepInFlight = false;
|
|
797
914
|
/**
|
|
798
|
-
* `uploadId` → providerId. Populated on `beginUpload`, consulted on
|
|
799
|
-
* every subsequent `writeChunk` / `finalizeUpload` / `abortUpload`,
|
|
800
|
-
* cleared on terminal calls. The provider holds the actual session
|
|
801
|
-
* state (open file descriptor, buffered offsets, …) — we just
|
|
802
|
-
* remember which provider owns it.
|
|
803
|
-
*/
|
|
804
|
-
uploadOwners = /* @__PURE__ */ new Map();
|
|
805
|
-
/** Symmetric to `uploadOwners` for download sessions. */
|
|
806
|
-
downloadOwners = /* @__PURE__ */ new Map();
|
|
807
|
-
/**
|
|
808
915
|
* Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
|
|
809
916
|
* synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
|
|
810
917
|
* keep a sync cache and refresh it whenever the provider collection
|
|
@@ -949,26 +1056,26 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
949
1056
|
location: loc,
|
|
950
1057
|
relativePath
|
|
951
1058
|
});
|
|
952
|
-
|
|
1059
|
+
service.rememberSession("upload", result.uploadId, loc);
|
|
953
1060
|
return result;
|
|
954
1061
|
},
|
|
955
1062
|
writeChunk: async (input) => {
|
|
956
|
-
return (await
|
|
1063
|
+
return (await service.getProviderForSession("upload", input.uploadId)).writeChunk(input);
|
|
957
1064
|
},
|
|
958
1065
|
finalizeUpload: async (input) => {
|
|
959
|
-
const p = await
|
|
1066
|
+
const p = await service.getProviderForSession("upload", input.uploadId);
|
|
960
1067
|
try {
|
|
961
1068
|
return await p.finalizeUpload(input);
|
|
962
1069
|
} finally {
|
|
963
|
-
|
|
1070
|
+
service.forgetSession("upload", input.uploadId);
|
|
964
1071
|
}
|
|
965
1072
|
},
|
|
966
1073
|
abortUpload: async (input) => {
|
|
967
|
-
const p = await
|
|
1074
|
+
const p = await service.getProviderForSession("upload", input.uploadId);
|
|
968
1075
|
try {
|
|
969
1076
|
return await p.abortUpload(input);
|
|
970
1077
|
} finally {
|
|
971
|
-
|
|
1078
|
+
service.forgetSession("upload", input.uploadId);
|
|
972
1079
|
}
|
|
973
1080
|
},
|
|
974
1081
|
beginDownload: async ({ location, relativePath }) => {
|
|
@@ -977,18 +1084,18 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
977
1084
|
location: loc,
|
|
978
1085
|
relativePath
|
|
979
1086
|
});
|
|
980
|
-
|
|
1087
|
+
service.rememberSession("download", result.downloadId, loc);
|
|
981
1088
|
return result;
|
|
982
1089
|
},
|
|
983
1090
|
readChunk: async (input) => {
|
|
984
|
-
return (await
|
|
1091
|
+
return (await service.getProviderForSession("download", input.downloadId)).readChunk(input);
|
|
985
1092
|
},
|
|
986
1093
|
endDownload: async (input) => {
|
|
987
|
-
const p = await
|
|
1094
|
+
const p = await service.getProviderForSession("download", input.downloadId);
|
|
988
1095
|
try {
|
|
989
1096
|
return await p.endDownload(input);
|
|
990
1097
|
} finally {
|
|
991
|
-
|
|
1098
|
+
service.forgetSession("download", input.downloadId);
|
|
992
1099
|
}
|
|
993
1100
|
}
|
|
994
1101
|
};
|
|
@@ -1031,8 +1138,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
1031
1138
|
this.pressureTimer = null;
|
|
1032
1139
|
}
|
|
1033
1140
|
this.pressureManager = null;
|
|
1034
|
-
this.
|
|
1035
|
-
this.downloadOwners.clear();
|
|
1141
|
+
this.service?.clearSessions();
|
|
1036
1142
|
this.service = null;
|
|
1037
1143
|
}
|
|
1038
1144
|
/**
|
|
@@ -1122,7 +1228,8 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
1122
1228
|
/** statfs capacity of a location's basePath, walking up to the nearest
|
|
1123
1229
|
* existing ancestor. Null for remote-node locations or unstattable paths. */
|
|
1124
1230
|
async localCapacityOf(loc) {
|
|
1125
|
-
|
|
1231
|
+
const localNode = this.service?.getLocalNodeId() ?? "hub";
|
|
1232
|
+
if (loc.nodeId !== void 0 && loc.nodeId !== localNode) return null;
|
|
1126
1233
|
const bp = loc.config["basePath"];
|
|
1127
1234
|
if (typeof bp !== "string" || bp.length === 0) return null;
|
|
1128
1235
|
try {
|
|
@@ -1251,37 +1358,6 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
1251
1358
|
});
|
|
1252
1359
|
for (const info of infos) this.nodeLocalByProvider.set(info.providerId, info.nodeLocal);
|
|
1253
1360
|
}
|
|
1254
|
-
async resolveByUploadId(uploadId) {
|
|
1255
|
-
const providerId = this.uploadOwners.get(uploadId);
|
|
1256
|
-
if (!providerId) throw new Error(`Unknown uploadId "${uploadId}" — orchestrator has no record of beginUpload`);
|
|
1257
|
-
return this.providerByProviderId(providerId, `uploadId "${uploadId}"`);
|
|
1258
|
-
}
|
|
1259
|
-
async resolveByDownloadId(downloadId) {
|
|
1260
|
-
const providerId = this.downloadOwners.get(downloadId);
|
|
1261
|
-
if (!providerId) throw new Error(`Unknown downloadId "${downloadId}" — orchestrator has no record of beginDownload`);
|
|
1262
|
-
return this.providerByProviderId(providerId, `downloadId "${downloadId}"`);
|
|
1263
|
-
}
|
|
1264
|
-
/**
|
|
1265
|
-
* Look up a `storage-provider` by `providerId` only — used for
|
|
1266
|
-
* upload / download dispatch where the original `StorageLocation` is
|
|
1267
|
-
* no longer in scope. Throws if the provider has gone away (e.g.
|
|
1268
|
-
* addon unloaded mid-session).
|
|
1269
|
-
*/
|
|
1270
|
-
async providerByProviderId(providerId, sessionDescriptor) {
|
|
1271
|
-
if (!this.service) throw new Error("storage-orchestrator: service not initialized");
|
|
1272
|
-
const probe = {
|
|
1273
|
-
id: `__session:${sessionDescriptor}`,
|
|
1274
|
-
type: "data",
|
|
1275
|
-
displayName: sessionDescriptor,
|
|
1276
|
-
providerId,
|
|
1277
|
-
config: {},
|
|
1278
|
-
isDefault: false,
|
|
1279
|
-
isSystem: false,
|
|
1280
|
-
createdAt: 0,
|
|
1281
|
-
updatedAt: 0
|
|
1282
|
-
};
|
|
1283
|
-
return this.service.getProviderFor(probe);
|
|
1284
|
-
}
|
|
1285
1361
|
};
|
|
1286
1362
|
//#endregion
|
|
1287
1363
|
exports.SqliteLocationStore = SqliteLocationStore;
|
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-
|
|
1
|
+
import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-DimX9eZy.mjs";
|
|
2
2
|
import * as path$1 from "node:path";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import { buildStorageLocationRegistry } from "@camstack/system";
|
|
5
5
|
//#region src/builtins/storage-orchestrator/storage-orchestrator.service.ts
|
|
6
|
+
var SESSION_VOCABULARY = {
|
|
7
|
+
upload: {
|
|
8
|
+
idLabel: "uploadId",
|
|
9
|
+
openMethod: "beginUpload"
|
|
10
|
+
},
|
|
11
|
+
download: {
|
|
12
|
+
idLabel: "downloadId",
|
|
13
|
+
openMethod: "beginDownload"
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** Map key for a tracked chunked session — the id spaces never collide. */
|
|
17
|
+
function sessionKey(kind, sessionId) {
|
|
18
|
+
return `${kind}:${sessionId}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Bare node id for a raw kernel `localNodeId`. Forked children carry a
|
|
22
|
+
* `<node>/<addon>` suffix; routing is per-NODE, so the addon segment is
|
|
23
|
+
* dropped. (Mirrors `normalizeNodeId` in `@camstack/types/addon`, which is a
|
|
24
|
+
* leaf module not re-exported on any subpath this package can import.)
|
|
25
|
+
*/
|
|
26
|
+
function bareNodeId(raw) {
|
|
27
|
+
if (raw === void 0 || raw === "") return "hub";
|
|
28
|
+
const slash = raw.indexOf("/");
|
|
29
|
+
if (slash < 0) return raw;
|
|
30
|
+
const bare = raw.slice(0, slash);
|
|
31
|
+
return bare === "" ? "hub" : bare;
|
|
32
|
+
}
|
|
6
33
|
var StorageOrchestratorService = class {
|
|
7
34
|
logger;
|
|
8
35
|
getProviders;
|
|
9
36
|
locationStore;
|
|
10
|
-
localNodeId;
|
|
11
37
|
getRemoteProvider;
|
|
12
38
|
locations = /* @__PURE__ */ new Map();
|
|
13
39
|
/**
|
|
@@ -26,12 +52,29 @@ var StorageOrchestratorService = class {
|
|
|
26
52
|
* early-boot path that predates the resolver wiring.
|
|
27
53
|
*/
|
|
28
54
|
nodeLocalResolver = null;
|
|
55
|
+
/** `localNodeId` with any forked-child `/addon` suffix stripped. */
|
|
56
|
+
localNode;
|
|
57
|
+
/**
|
|
58
|
+
* `<kind>:<sessionId>` → the dispatch target that OPENED the session.
|
|
59
|
+
* Chunked I/O carries no `StorageLocation` after the first hop, so without
|
|
60
|
+
* this the follow-up chunks fall back to a `providerId`-only lookup — which
|
|
61
|
+
* on a multi-node cluster means the hub's filesystem, not the agent's.
|
|
62
|
+
*/
|
|
63
|
+
sessionTargets = /* @__PURE__ */ new Map();
|
|
29
64
|
constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
|
|
30
65
|
this.logger = logger;
|
|
31
66
|
this.getProviders = getProviders;
|
|
32
67
|
this.locationStore = locationStore;
|
|
33
|
-
this.localNodeId = localNodeId;
|
|
34
68
|
this.getRemoteProvider = getRemoteProvider;
|
|
69
|
+
this.localNode = bareNodeId(localNodeId);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The node this orchestrator runs on, with any forked-child `/addon`
|
|
73
|
+
* suffix stripped. A location whose `nodeId` differs is NOT this node's
|
|
74
|
+
* to touch — its bytes, and its free space, live elsewhere.
|
|
75
|
+
*/
|
|
76
|
+
getLocalNodeId() {
|
|
77
|
+
return this.localNode;
|
|
35
78
|
}
|
|
36
79
|
/** True once a persistence store is wired (constructor or {@link attachStore}). */
|
|
37
80
|
hasStore() {
|
|
@@ -339,22 +382,96 @@ var StorageOrchestratorService = class {
|
|
|
339
382
|
return def;
|
|
340
383
|
}
|
|
341
384
|
/**
|
|
342
|
-
* Find the storage-provider that backs a given location
|
|
343
|
-
* `
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
385
|
+
* Find the storage-provider that backs a given location — the entry point
|
|
386
|
+
* every consumer-facing `storage` cap method funnels through.
|
|
387
|
+
*
|
|
388
|
+
* Routing key is `(providerId, nodeId)`, never `providerId` alone: every
|
|
389
|
+
* node's FilesystemStorageProvider registers under the SAME providerId, so
|
|
390
|
+
* a providerId-only match resolves to whichever node asked. That is how a
|
|
391
|
+
* location created on an agent ended up writing to the HUB's filesystem
|
|
392
|
+
* (root-caused 2026-07-29).
|
|
347
393
|
*/
|
|
348
394
|
async getProviderFor(location) {
|
|
349
|
-
|
|
350
|
-
|
|
395
|
+
return this.getProviderForTarget({
|
|
396
|
+
providerId: location.providerId,
|
|
397
|
+
...location.nodeId !== void 0 && { nodeId: location.nodeId }
|
|
398
|
+
}, `location "${location.id}"`, location.id);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Resolve one dispatch target to the provider that must execute it.
|
|
402
|
+
*
|
|
403
|
+
* `descriptor` is the operator-facing subject of the error message
|
|
404
|
+
* (`location "recordings:disk"`, `uploadId "u-42"`); `locationId` is the
|
|
405
|
+
* structured tag for the log line when there is a location behind the
|
|
406
|
+
* dispatch (chunked sessions carry only the session id).
|
|
407
|
+
*
|
|
408
|
+
* Both failure branches LOG before throwing, and NEITHER falls back to the
|
|
409
|
+
* local provider — a silent fallback is the bug this method exists to fix.
|
|
410
|
+
*/
|
|
411
|
+
async getProviderForTarget(target, descriptor, locationId) {
|
|
412
|
+
const nodeId = target.nodeId;
|
|
413
|
+
if (nodeId !== void 0 && bareNodeId(nodeId) !== this.localNode) {
|
|
351
414
|
const remote = this.getRemoteProvider?.(nodeId);
|
|
352
415
|
if (remote) return remote;
|
|
353
|
-
|
|
416
|
+
this.logger.error("storage-orchestrator: target node has no reachable storage-provider — dispatch refused", { meta: {
|
|
417
|
+
...locationId !== void 0 && { locationId },
|
|
418
|
+
descriptor,
|
|
419
|
+
nodeId,
|
|
420
|
+
providerId: target.providerId,
|
|
421
|
+
localNodeId: this.localNode,
|
|
422
|
+
crossNodeResolverWired: this.getRemoteProvider !== void 0
|
|
423
|
+
} });
|
|
424
|
+
throw new Error(`storage-provider for ${descriptor} lives on node "${nodeId}" which is not reachable`);
|
|
354
425
|
}
|
|
355
426
|
const providers = this.getProviders();
|
|
356
|
-
for (const p of providers) if ((await p.getProviderInfo()).providerId ===
|
|
357
|
-
|
|
427
|
+
for (const p of providers) if ((await p.getProviderInfo()).providerId === target.providerId) return p;
|
|
428
|
+
this.logger.error("storage-orchestrator: no local storage-provider for providerId", { meta: {
|
|
429
|
+
...locationId !== void 0 && { locationId },
|
|
430
|
+
descriptor,
|
|
431
|
+
providerId: target.providerId,
|
|
432
|
+
localNodeId: this.localNode,
|
|
433
|
+
registered: providers.length
|
|
434
|
+
} });
|
|
435
|
+
throw new Error(`No storage-provider registered for providerId "${target.providerId}" (${descriptor})`);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Remember which provider — and on which NODE — opened a chunked session.
|
|
439
|
+
* `beginUpload` / `beginDownload` carry a `StorageLocation`; every later
|
|
440
|
+
* chunk carries only the session id, and a session id issued by an agent
|
|
441
|
+
* means nothing to the hub's provider.
|
|
442
|
+
*/
|
|
443
|
+
rememberSession(kind, sessionId, location) {
|
|
444
|
+
this.sessionTargets.set(sessionKey(kind, sessionId), {
|
|
445
|
+
providerId: location.providerId,
|
|
446
|
+
...location.nodeId !== void 0 && { nodeId: location.nodeId }
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/** Drop a session's routing entry (finalize / abort / end). */
|
|
450
|
+
forgetSession(kind, sessionId) {
|
|
451
|
+
this.sessionTargets.delete(sessionKey(kind, sessionId));
|
|
452
|
+
}
|
|
453
|
+
/** Drop every session entry (addon shutdown). */
|
|
454
|
+
clearSessions() {
|
|
455
|
+
this.sessionTargets.clear();
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Resolve the provider that owns an in-flight chunked session, on the node
|
|
459
|
+
* that opened it. Throws when the orchestrator has no record of the open
|
|
460
|
+
* call (provider gone, or a chunk for a session it never issued).
|
|
461
|
+
*/
|
|
462
|
+
async getProviderForSession(kind, sessionId) {
|
|
463
|
+
const vocabulary = SESSION_VOCABULARY[kind];
|
|
464
|
+
const descriptor = `${vocabulary.idLabel} "${sessionId}"`;
|
|
465
|
+
const target = this.sessionTargets.get(sessionKey(kind, sessionId));
|
|
466
|
+
if (!target) {
|
|
467
|
+
this.logger.error("storage-orchestrator: chunk for an unknown session — dispatch refused", { meta: {
|
|
468
|
+
descriptor,
|
|
469
|
+
kind,
|
|
470
|
+
sessionId
|
|
471
|
+
} });
|
|
472
|
+
throw new Error(`Unknown ${vocabulary.idLabel} "${sessionId}" — orchestrator has no record of ${vocabulary.openMethod}`);
|
|
473
|
+
}
|
|
474
|
+
return this.getProviderForTarget(target, descriptor);
|
|
358
475
|
}
|
|
359
476
|
/** Convenience: lookup by id (returns `undefined` if not found). */
|
|
360
477
|
getLocationById(id) {
|
|
@@ -788,16 +905,6 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
788
905
|
pressureTimer = null;
|
|
789
906
|
pressureSweepInFlight = false;
|
|
790
907
|
/**
|
|
791
|
-
* `uploadId` → providerId. Populated on `beginUpload`, consulted on
|
|
792
|
-
* every subsequent `writeChunk` / `finalizeUpload` / `abortUpload`,
|
|
793
|
-
* cleared on terminal calls. The provider holds the actual session
|
|
794
|
-
* state (open file descriptor, buffered offsets, …) — we just
|
|
795
|
-
* remember which provider owns it.
|
|
796
|
-
*/
|
|
797
|
-
uploadOwners = /* @__PURE__ */ new Map();
|
|
798
|
-
/** Symmetric to `uploadOwners` for download sessions. */
|
|
799
|
-
downloadOwners = /* @__PURE__ */ new Map();
|
|
800
|
-
/**
|
|
801
908
|
* Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
|
|
802
909
|
* synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
|
|
803
910
|
* keep a sync cache and refresh it whenever the provider collection
|
|
@@ -942,26 +1049,26 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
942
1049
|
location: loc,
|
|
943
1050
|
relativePath
|
|
944
1051
|
});
|
|
945
|
-
|
|
1052
|
+
service.rememberSession("upload", result.uploadId, loc);
|
|
946
1053
|
return result;
|
|
947
1054
|
},
|
|
948
1055
|
writeChunk: async (input) => {
|
|
949
|
-
return (await
|
|
1056
|
+
return (await service.getProviderForSession("upload", input.uploadId)).writeChunk(input);
|
|
950
1057
|
},
|
|
951
1058
|
finalizeUpload: async (input) => {
|
|
952
|
-
const p = await
|
|
1059
|
+
const p = await service.getProviderForSession("upload", input.uploadId);
|
|
953
1060
|
try {
|
|
954
1061
|
return await p.finalizeUpload(input);
|
|
955
1062
|
} finally {
|
|
956
|
-
|
|
1063
|
+
service.forgetSession("upload", input.uploadId);
|
|
957
1064
|
}
|
|
958
1065
|
},
|
|
959
1066
|
abortUpload: async (input) => {
|
|
960
|
-
const p = await
|
|
1067
|
+
const p = await service.getProviderForSession("upload", input.uploadId);
|
|
961
1068
|
try {
|
|
962
1069
|
return await p.abortUpload(input);
|
|
963
1070
|
} finally {
|
|
964
|
-
|
|
1071
|
+
service.forgetSession("upload", input.uploadId);
|
|
965
1072
|
}
|
|
966
1073
|
},
|
|
967
1074
|
beginDownload: async ({ location, relativePath }) => {
|
|
@@ -970,18 +1077,18 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
970
1077
|
location: loc,
|
|
971
1078
|
relativePath
|
|
972
1079
|
});
|
|
973
|
-
|
|
1080
|
+
service.rememberSession("download", result.downloadId, loc);
|
|
974
1081
|
return result;
|
|
975
1082
|
},
|
|
976
1083
|
readChunk: async (input) => {
|
|
977
|
-
return (await
|
|
1084
|
+
return (await service.getProviderForSession("download", input.downloadId)).readChunk(input);
|
|
978
1085
|
},
|
|
979
1086
|
endDownload: async (input) => {
|
|
980
|
-
const p = await
|
|
1087
|
+
const p = await service.getProviderForSession("download", input.downloadId);
|
|
981
1088
|
try {
|
|
982
1089
|
return await p.endDownload(input);
|
|
983
1090
|
} finally {
|
|
984
|
-
|
|
1091
|
+
service.forgetSession("download", input.downloadId);
|
|
985
1092
|
}
|
|
986
1093
|
}
|
|
987
1094
|
};
|
|
@@ -1024,8 +1131,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
1024
1131
|
this.pressureTimer = null;
|
|
1025
1132
|
}
|
|
1026
1133
|
this.pressureManager = null;
|
|
1027
|
-
this.
|
|
1028
|
-
this.downloadOwners.clear();
|
|
1134
|
+
this.service?.clearSessions();
|
|
1029
1135
|
this.service = null;
|
|
1030
1136
|
}
|
|
1031
1137
|
/**
|
|
@@ -1115,7 +1221,8 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
1115
1221
|
/** statfs capacity of a location's basePath, walking up to the nearest
|
|
1116
1222
|
* existing ancestor. Null for remote-node locations or unstattable paths. */
|
|
1117
1223
|
async localCapacityOf(loc) {
|
|
1118
|
-
|
|
1224
|
+
const localNode = this.service?.getLocalNodeId() ?? "hub";
|
|
1225
|
+
if (loc.nodeId !== void 0 && loc.nodeId !== localNode) return null;
|
|
1119
1226
|
const bp = loc.config["basePath"];
|
|
1120
1227
|
if (typeof bp !== "string" || bp.length === 0) return null;
|
|
1121
1228
|
try {
|
|
@@ -1244,37 +1351,6 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
1244
1351
|
});
|
|
1245
1352
|
for (const info of infos) this.nodeLocalByProvider.set(info.providerId, info.nodeLocal);
|
|
1246
1353
|
}
|
|
1247
|
-
async resolveByUploadId(uploadId) {
|
|
1248
|
-
const providerId = this.uploadOwners.get(uploadId);
|
|
1249
|
-
if (!providerId) throw new Error(`Unknown uploadId "${uploadId}" — orchestrator has no record of beginUpload`);
|
|
1250
|
-
return this.providerByProviderId(providerId, `uploadId "${uploadId}"`);
|
|
1251
|
-
}
|
|
1252
|
-
async resolveByDownloadId(downloadId) {
|
|
1253
|
-
const providerId = this.downloadOwners.get(downloadId);
|
|
1254
|
-
if (!providerId) throw new Error(`Unknown downloadId "${downloadId}" — orchestrator has no record of beginDownload`);
|
|
1255
|
-
return this.providerByProviderId(providerId, `downloadId "${downloadId}"`);
|
|
1256
|
-
}
|
|
1257
|
-
/**
|
|
1258
|
-
* Look up a `storage-provider` by `providerId` only — used for
|
|
1259
|
-
* upload / download dispatch where the original `StorageLocation` is
|
|
1260
|
-
* no longer in scope. Throws if the provider has gone away (e.g.
|
|
1261
|
-
* addon unloaded mid-session).
|
|
1262
|
-
*/
|
|
1263
|
-
async providerByProviderId(providerId, sessionDescriptor) {
|
|
1264
|
-
if (!this.service) throw new Error("storage-orchestrator: service not initialized");
|
|
1265
|
-
const probe = {
|
|
1266
|
-
id: `__session:${sessionDescriptor}`,
|
|
1267
|
-
type: "data",
|
|
1268
|
-
displayName: sessionDescriptor,
|
|
1269
|
-
providerId,
|
|
1270
|
-
config: {},
|
|
1271
|
-
isDefault: false,
|
|
1272
|
-
isSystem: false,
|
|
1273
|
-
createdAt: 0,
|
|
1274
|
-
updatedAt: 0
|
|
1275
|
-
};
|
|
1276
|
-
return this.service.getProviderFor(probe);
|
|
1277
|
-
}
|
|
1278
1354
|
};
|
|
1279
1355
|
//#endregion
|
|
1280
1356
|
export { StorageOrchestratorAddon, StorageOrchestratorAddon as default, StorageOrchestratorService as n, SqliteLocationStore as t };
|