@camstack/server 1.1.26 → 1.1.28
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/api/core/cap-providers.js +43 -2
- package/dist/api/core/cluster-nodes.router.js +32 -0
- package/dist/api/core/hwaccel.router.js +63 -40
- package/dist/api/trpc/generated-cap-mounts.js +2 -1
- package/dist/api/trpc/generated-cap-routers.js +687 -582
- package/dist/api/trpc/trpc.router.js +9 -4
- package/dist/core/addon/addon-registry.service.js +26 -7
- package/dist/core/agent/agent-registry.service.js +122 -5
- package/dist/core/agent/cluster-node-history-store.js +152 -0
- package/dist/core/moleculer/moleculer.service.js +8 -6
- package/dist/manual-boot.js +6 -0
- package/package.json +1 -1
|
@@ -167,6 +167,12 @@ function getLocalIps() {
|
|
|
167
167
|
*/
|
|
168
168
|
async function computeTopology(agentRegistry, addonRegistry) {
|
|
169
169
|
const nodes = await agentRegistry.listNodes();
|
|
170
|
+
// A2: durable offline-node history. `listNodes()` (above) has just
|
|
171
|
+
// snapshotted every ONLINE node into this store, so `lastActive` is fresh
|
|
172
|
+
// for live rows and preserved (at disconnect time) for offline ones.
|
|
173
|
+
const history = await agentRegistry.getClusterNodeHistory();
|
|
174
|
+
const historyById = new Map(history.map((h) => [h.id, h]));
|
|
175
|
+
const liveIds = new Set(nodes.map((n) => n.info.id));
|
|
170
176
|
const allAddons = addonRegistry?.listAddons() ?? [];
|
|
171
177
|
const getInGroupAddonIds = (node) => {
|
|
172
178
|
const subs = (node.subProcesses ?? []);
|
|
@@ -184,7 +190,7 @@ async function computeTopology(agentRegistry, addonRegistry) {
|
|
|
184
190
|
const category = a.declaration?.category ?? 'system';
|
|
185
191
|
addonCategory.set(id, category);
|
|
186
192
|
}
|
|
187
|
-
|
|
193
|
+
const liveNodes = nodes.map((node) => {
|
|
188
194
|
const inGroupAddonIds = new Set(getInGroupAddonIds(node));
|
|
189
195
|
const agentAddonIds = node.agentAddons ?? [];
|
|
190
196
|
const allNodeAddons = node.isHub
|
|
@@ -286,13 +292,48 @@ async function computeTopology(agentRegistry, addonRegistry) {
|
|
|
286
292
|
cpuPercent: node.status?.cpuPercent ?? 0,
|
|
287
293
|
memoryPercent: node.status?.memoryPercent ?? 0,
|
|
288
294
|
uptime: Date.now() - node.connectedSince,
|
|
289
|
-
|
|
295
|
+
// Fix (was hardcoded `new Date().toISOString()` — always "now", a bug
|
|
296
|
+
// even for online nodes): report the persisted `lastActive` when we have
|
|
297
|
+
// it, falling back to now only for a node with no history row yet.
|
|
298
|
+
lastSeen: new Date(historyById.get(node.info.id)?.lastActive ?? Date.now()).toISOString(),
|
|
290
299
|
localIps: node.isHub ? getLocalIps() : (node.localIps ?? []),
|
|
291
300
|
addons: allNodeAddons,
|
|
292
301
|
processes: [mainProcess, ...childProcesses],
|
|
293
302
|
categories: categoriesProjection,
|
|
294
303
|
};
|
|
295
304
|
});
|
|
305
|
+
// A2: union in OFFLINE rows for every persisted node no longer present in the
|
|
306
|
+
// live Moleculer window (or gone entirely after a hub restart). These render
|
|
307
|
+
// straight from the last-known descriptor with live metrics zeroed. Nothing
|
|
308
|
+
// here participates in capability routing — it is purely a topology row.
|
|
309
|
+
const offlineNodes = history
|
|
310
|
+
.filter((persisted) => !liveIds.has(persisted.id))
|
|
311
|
+
.map((persisted) => ({
|
|
312
|
+
id: persisted.id,
|
|
313
|
+
name: persisted.name,
|
|
314
|
+
hostname: persisted.hostname,
|
|
315
|
+
platform: persisted.platform,
|
|
316
|
+
arch: persisted.arch,
|
|
317
|
+
cpuModel: persisted.cpuModel,
|
|
318
|
+
cpuCores: persisted.cpuCores,
|
|
319
|
+
memoryMB: persisted.memoryMB,
|
|
320
|
+
engines: [...persisted.engines],
|
|
321
|
+
isHub: persisted.isHub,
|
|
322
|
+
isOnline: false,
|
|
323
|
+
cpuPercent: 0,
|
|
324
|
+
memoryPercent: 0,
|
|
325
|
+
uptime: 0,
|
|
326
|
+
lastSeen: new Date(persisted.lastActive).toISOString(),
|
|
327
|
+
localIps: [...persisted.localIps],
|
|
328
|
+
addons: persisted.addonIds.map((id) => ({
|
|
329
|
+
id,
|
|
330
|
+
capabilities: [...(addonCaps.get(id) ?? [])],
|
|
331
|
+
status: 'stopped',
|
|
332
|
+
})),
|
|
333
|
+
processes: [],
|
|
334
|
+
categories: [],
|
|
335
|
+
}));
|
|
336
|
+
return [...liveNodes, ...offlineNodes];
|
|
296
337
|
}
|
|
297
338
|
function buildNodesProvider(agentRegistry, moleculer, addonRegistry) {
|
|
298
339
|
const broker = moleculer.broker;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createClusterNodesRouter = createClusterNodesRouter;
|
|
4
|
+
/**
|
|
5
|
+
* Cluster-nodes router — fixed core API (not a capability).
|
|
6
|
+
*
|
|
7
|
+
* Exposes the durable offline-node history's write-side cleanup so the admin
|
|
8
|
+
* UI's inline "Forget node" action can purge a node the operator never expects
|
|
9
|
+
* to see again. Hand-written (not a `*.cap.ts`) on purpose: it needs no
|
|
10
|
+
* per-provider routing and adding a cap method would force a full codegen pass.
|
|
11
|
+
*
|
|
12
|
+
* The read side (`getClusterNodeHistory`) is consumed server-side by
|
|
13
|
+
* `computeTopology` and is intentionally NOT exposed here — the admin UI reads
|
|
14
|
+
* offline rows off the pushed `cluster.topology-snapshot`, never by polling.
|
|
15
|
+
*/
|
|
16
|
+
const zod_1 = require("zod");
|
|
17
|
+
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
18
|
+
const ForgetNodeInputSchema = zod_1.z.object({ nodeId: zod_1.z.string().min(1) });
|
|
19
|
+
function createClusterNodesRouter(agentRegistry) {
|
|
20
|
+
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
21
|
+
// Purge one node's persisted offline history. Pairs with
|
|
22
|
+
// `pipelineOrchestrator.removeAgentSettings` on the frontend to fully
|
|
23
|
+
// forget an offline node (history row + per-node pipeline assignments).
|
|
24
|
+
forgetNode: trpc_middleware_js_1.adminProcedure
|
|
25
|
+
.input(ForgetNodeInputSchema)
|
|
26
|
+
.output(zod_1.z.object({ success: zod_1.z.boolean() }))
|
|
27
|
+
.mutation(async ({ input }) => {
|
|
28
|
+
await agentRegistry.forgetClusterNode(input.nodeId);
|
|
29
|
+
return { success: true };
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -4,16 +4,16 @@ exports.createHwAccelRouter = createHwAccelRouter;
|
|
|
4
4
|
/**
|
|
5
5
|
* Hwaccel router — fixed core API (not a capability).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Repointed (S3 consolidation) onto the `platform-probe` capability — the SOLE
|
|
8
|
+
* cross-node-pinnable hwaccel surface — instead of the retired `$hwaccel`
|
|
9
|
+
* Moleculer service. The hub-local instance is resolved via
|
|
10
|
+
* `capabilityRegistry.getSingleton('platform-probe')`; any other cluster node
|
|
11
|
+
* via `moleculer.createCapabilityProxy('platform-probe', nodeId)` (the same
|
|
12
|
+
* node-routing the codegen'd cap routers use). Used by the admin UI pipeline /
|
|
13
|
+
* NodeDetail pages to show which hardware backend each node will use.
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
15
|
-
* (hub + forked worker + remote agent) registers `$hwaccel` at
|
|
16
|
-
* bootstrap, so any reachable nodeId works.
|
|
15
|
+
* The tRPC input/output shapes are UNCHANGED for clients (`{prefer}` in,
|
|
16
|
+
* `{preferred, rationale}` out) — no SDK break.
|
|
17
17
|
*/
|
|
18
18
|
const zod_1 = require("zod");
|
|
19
19
|
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
@@ -33,33 +33,69 @@ const HwAccelPreferSchema = zod_1.z
|
|
|
33
33
|
.union([HwAccelBackendSchema, zod_1.z.literal('none')])
|
|
34
34
|
.nullable()
|
|
35
35
|
.optional();
|
|
36
|
+
// `preferred` is a plain string array (not the decode-backend enum) to match
|
|
37
|
+
// the `platform-probe` cap's own resolution schema: the resolver only ever
|
|
38
|
+
// emits decode backends, but a narrow enum would reject any future addition and
|
|
39
|
+
// force a cast at the cap boundary.
|
|
36
40
|
const HwAccelResolutionSchema = zod_1.z.object({
|
|
37
|
-
preferred: zod_1.z.array(
|
|
41
|
+
preferred: zod_1.z.array(zod_1.z.string()).readonly(),
|
|
38
42
|
rationale: zod_1.z.string(),
|
|
39
43
|
});
|
|
40
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Resolve hwaccel on a specific cluster node through the `platform-probe` cap
|
|
46
|
+
* proxy. `createCapabilityProxy` returns `Record<string, (params) =>
|
|
47
|
+
* Promise<unknown>> | null`; rather than casting that to the provider type
|
|
48
|
+
* (the webrtc-session mount does — repo rule forbids it), we call the proxy's
|
|
49
|
+
* `resolveHwAccel` and narrow its result through the cap's Zod OUTPUT schema.
|
|
50
|
+
* A missing proxy (node unreachable), a missing method, a malformed response,
|
|
51
|
+
* or a thrown call all degrade to an EXPLAINED empty list rather than
|
|
52
|
+
* corrupting the typed API surface.
|
|
53
|
+
*/
|
|
54
|
+
async function resolveHwAccelOnNode(moleculer, nodeId, prefer) {
|
|
55
|
+
const proxy = moleculer?.createCapabilityProxy('platform-probe', nodeId) ?? null;
|
|
56
|
+
const resolveFn = proxy?.['resolveHwAccel'];
|
|
57
|
+
if (!resolveFn) {
|
|
58
|
+
return { preferred: [], rationale: `node ${nodeId} unreachable` };
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const raw = await resolveFn({ prefer });
|
|
62
|
+
const parsed = HwAccelResolutionSchema.safeParse(raw);
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
return { preferred: [], rationale: `invalid probe response from ${nodeId}` };
|
|
65
|
+
}
|
|
66
|
+
return parsed.data;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
70
|
+
return { preferred: [], rationale: `resolve failed: ${msg}` };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function listAvailableNodeIds(moleculer) {
|
|
74
|
+
const broker = moleculer?.broker ?? null;
|
|
75
|
+
if (!broker)
|
|
76
|
+
return [];
|
|
77
|
+
const withRegistry = broker;
|
|
78
|
+
return withRegistry.registry.getNodeList({ onlyAvailable: true }).map((n) => n.id);
|
|
79
|
+
}
|
|
80
|
+
function createHwAccelRouter(capabilityRegistry, moleculer) {
|
|
41
81
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
42
|
-
/** Probe the current hub process. */
|
|
82
|
+
/** Probe the current hub process via the local `platform-probe` singleton. */
|
|
43
83
|
resolve: trpc_middleware_js_1.adminProcedure
|
|
44
84
|
.input(zod_1.z.object({ prefer: HwAccelPreferSchema }).optional())
|
|
45
85
|
.output(HwAccelResolutionSchema)
|
|
46
86
|
.query(async ({ input }) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
const provider = capabilityRegistry?.getSingleton('platform-probe') ?? null;
|
|
88
|
+
if (!provider) {
|
|
89
|
+
return { preferred: [], rationale: 'platform-probe capability not available' };
|
|
90
|
+
}
|
|
91
|
+
return provider.resolveHwAccel({ prefer: input?.prefer ?? null });
|
|
51
92
|
}),
|
|
52
93
|
/** Probe a specific node in the cluster — used by per-agent UI cards. */
|
|
53
94
|
resolveForNode: trpc_middleware_js_1.adminProcedure
|
|
54
95
|
.input(zod_1.z.object({ nodeId: zod_1.z.string(), prefer: HwAccelPreferSchema }))
|
|
55
96
|
.output(HwAccelResolutionSchema)
|
|
56
97
|
.query(async ({ input }) => {
|
|
57
|
-
|
|
58
|
-
throw new Error('Moleculer broker not available');
|
|
59
|
-
const params = { prefer: input.prefer ?? null };
|
|
60
|
-
return broker.call('$hwaccel.resolve', params, {
|
|
61
|
-
nodeID: input.nodeId,
|
|
62
|
-
});
|
|
98
|
+
return resolveHwAccelOnNode(moleculer, input.nodeId, input.prefer ?? null);
|
|
63
99
|
}),
|
|
64
100
|
/** List every node currently reachable and the hwaccel each resolves to. */
|
|
65
101
|
resolveAll: trpc_middleware_js_1.adminProcedure
|
|
@@ -68,24 +104,11 @@ function createHwAccelRouter(broker) {
|
|
|
68
104
|
resolution: HwAccelResolutionSchema,
|
|
69
105
|
})))
|
|
70
106
|
.query(async () => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const resolution = (await broker.call('$hwaccel.resolve', { prefer: null }, { nodeID: n.id }));
|
|
78
|
-
return { nodeId: n.id, resolution };
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
82
|
-
return {
|
|
83
|
-
nodeId: n.id,
|
|
84
|
-
resolution: { preferred: [], rationale: `resolve failed: ${msg}` },
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}));
|
|
88
|
-
return results;
|
|
107
|
+
const nodeIds = listAvailableNodeIds(moleculer);
|
|
108
|
+
return Promise.all(nodeIds.map(async (nodeId) => ({
|
|
109
|
+
nodeId,
|
|
110
|
+
resolution: await resolveHwAccelOnNode(moleculer, nodeId, null),
|
|
111
|
+
})));
|
|
89
112
|
}),
|
|
90
113
|
});
|
|
91
114
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
|
|
3
3
|
// Re-run: npx tsx scripts/generate-cap-mounts.ts
|
|
4
4
|
//
|
|
5
|
-
// Mounted:
|
|
5
|
+
// Mounted: 131 Skipped (legacy): 6
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.LEGACY_SHAPE_SKIP = void 0;
|
|
8
8
|
exports.mountAllCaps = mountAllCaps;
|
|
@@ -294,6 +294,7 @@ function mountAllCaps(services) {
|
|
|
294
294
|
return entries[0]?.[1] ?? null;
|
|
295
295
|
}, remoteCapProxy),
|
|
296
296
|
osd: (0, generated_cap_routers_js_1.createCapRouter_osd)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'osd'), remoteCapProxy),
|
|
297
|
+
petFeeder: (0, generated_cap_routers_js_1.createCapRouter_petFeeder)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pet-feeder'), remoteCapProxy),
|
|
297
298
|
pipelineAnalytics: (0, generated_cap_routers_js_1.createCapRouter_pipelineAnalytics)((_ctx) => reg?.getSingleton('pipeline-analytics') ?? null, remoteCapProxy),
|
|
298
299
|
pipelineExecutor: (0, generated_cap_routers_js_1.createCapRouter_pipelineExecutor)((_ctx) => reg?.getSingleton('pipeline-executor') ??
|
|
299
300
|
null, remoteCapProxy),
|