@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
|
@@ -12,6 +12,7 @@ const hwaccel_router_js_1 = require("../core/hwaccel.router.js");
|
|
|
12
12
|
const live_events_router_js_1 = require("../core/live-events.router.js");
|
|
13
13
|
const logs_router_js_1 = require("../core/logs.router.js");
|
|
14
14
|
const notifications_router_js_1 = require("../core/notifications.router.js");
|
|
15
|
+
const cluster_nodes_router_js_1 = require("../core/cluster-nodes.router.js");
|
|
15
16
|
const repl_router_js_1 = require("../core/repl.router.js");
|
|
16
17
|
const settings_backend_router_js_1 = require("../core/settings-backend.router.js");
|
|
17
18
|
const stream_probe_router_js_1 = require("../core/stream-probe.router.js");
|
|
@@ -143,10 +144,14 @@ function buildCapabilityRouters(services) {
|
|
|
143
144
|
live: (0, live_events_router_js_1.createLiveEventsRouter)(services.eventBus, services.addonRegistry),
|
|
144
145
|
// stream-probe — fixed core API (ffprobe wrapper), not a cap.
|
|
145
146
|
streamProbe: (0, stream_probe_router_js_1.createStreamProbeRouter)(services.streamProbe),
|
|
146
|
-
// hwaccel — fixed core API,
|
|
147
|
-
//
|
|
148
|
-
// which hardware backend each
|
|
149
|
-
|
|
147
|
+
// hwaccel — fixed core API, repointed onto the `platform-probe` cap
|
|
148
|
+
// (the sole cross-node-pinnable hwaccel surface). UI pipeline /
|
|
149
|
+
// NodeDetail pages query per-node to show which hardware backend each
|
|
150
|
+
// agent will use.
|
|
151
|
+
hwaccel: (0, hwaccel_router_js_1.createHwAccelRouter)(services.capabilityRegistry, services.moleculer),
|
|
152
|
+
// clusterNodes — fixed core API. Write-side purge for the durable
|
|
153
|
+
// offline-node history (Track A "Forget node"); read side is push-only.
|
|
154
|
+
clusterNodes: (0, cluster_nodes_router_js_1.createClusterNodesRouter)(services.agentRegistry),
|
|
150
155
|
auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry),
|
|
151
156
|
// ── Cap overrides: `mount: { kind: 'custom' }` ──────────────────
|
|
152
157
|
// `snapshot-provider.supportsDevice` is an OR across providers;
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.AddonRegistryService = void 0;
|
|
37
|
+
exports.shouldEmitProviderRegisteredReady = shouldEmitProviderRegisteredReady;
|
|
37
38
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access -- pre-existing lint debt across this 2200-line orchestration class. The flagged sites (StorageService.setLocationManager / setSettingsBackend, LoggingService.addDestination, RouteRegistry, etc.) are typed as `unknown` by their owning services to break circular construction-order dependencies; runtime contracts are validated structurally. Tracked separately; do not amend in unrelated edits. */
|
|
38
39
|
const os = __importStar(require("node:os"));
|
|
39
40
|
const addon_row_manifest_1 = require("./addon-row-manifest");
|
|
@@ -159,6 +160,19 @@ function parseDataPlaneEndpoints(raw) {
|
|
|
159
160
|
// shim that consumed it were deleted. All settings flow through the
|
|
160
161
|
// new `getAddonSettings / getGlobalSettings / getDeviceSettings`
|
|
161
162
|
// endpoints on the `addon-settings` singleton capability.
|
|
163
|
+
/**
|
|
164
|
+
* Whether the `capability:provider-registered` hook may emit `ready` for a
|
|
165
|
+
* capability. `platform-probe` OWNS its readiness protocol: its provider
|
|
166
|
+
* registers synchronously from `onInitialize` while the REAL probe
|
|
167
|
+
* (hardware + embedded-Python EP scoring) is still running async — the
|
|
168
|
+
* registration-time emit would flip it `ready` prematurely and probe-gated
|
|
169
|
+
* consumers (detection-pipeline engine auto-pick) would read
|
|
170
|
+
* accelerator-blind results. The addon emits `starting`/`ready`/`down`
|
|
171
|
+
* itself (`autoEmitReadiness=false`, ready on probe-done).
|
|
172
|
+
*/
|
|
173
|
+
function shouldEmitProviderRegisteredReady(capability) {
|
|
174
|
+
return capability !== 'platform-probe';
|
|
175
|
+
}
|
|
162
176
|
class AddonRegistryService {
|
|
163
177
|
loggingService;
|
|
164
178
|
eventBusService;
|
|
@@ -2048,13 +2062,18 @@ class AddonRegistryService {
|
|
|
2048
2062
|
// loops). The node-scoped emit is sufficient: the hydrate
|
|
2049
2063
|
// path on subprocess brokers replays whichever records the
|
|
2050
2064
|
// hub's `$readiness.getSnapshot` returns, scope and all.
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
}
|
|
2065
|
+
// Caps that OWN their readiness (platform-probe: registration
|
|
2066
|
+
// happens while its async probe still runs) are excluded —
|
|
2067
|
+
// this emit would flip them `ready` prematurely.
|
|
2068
|
+
if (shouldEmitProviderRegisteredReady(capability)) {
|
|
2069
|
+
try {
|
|
2070
|
+
this.moleculer.readinessRegistry.emitReady(capability, { type: 'node', nodeId: 'hub' });
|
|
2071
|
+
}
|
|
2072
|
+
catch (err) {
|
|
2073
|
+
this.logger.warn('emitReady failed', {
|
|
2074
|
+
meta: { capability, addonId, error: (0, types_1.errMsg)(err) },
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2058
2077
|
}
|
|
2059
2078
|
switch (capability) {
|
|
2060
2079
|
case 'storage': {
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.AgentRegistryService = void 0;
|
|
37
37
|
exports.toNodeLiveness = toNodeLiveness;
|
|
38
|
+
exports.toDescriptor = toDescriptor;
|
|
38
39
|
const node_crypto_1 = require("node:crypto");
|
|
39
40
|
const os = __importStar(require("node:os"));
|
|
40
41
|
const system_1 = require("@camstack/system");
|
|
@@ -81,6 +82,28 @@ function toNodeLiveness(nodes) {
|
|
|
81
82
|
.filter((node) => !node.id.includes('/'))
|
|
82
83
|
.map((node) => ({ id: node.id, isHub: node.id === 'hub', isOnline: node.available }));
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Project a live `AgentListItem` into the durable history descriptor. Field
|
|
87
|
+
* derivations mirror `computeTopology` exactly (engines from `pythonRuntimes`,
|
|
88
|
+
* cpuModel `undefined`→`null`, addonIds from the reported `agentAddons`) so an
|
|
89
|
+
* offline row rendered from history is indistinguishable from the live shape.
|
|
90
|
+
*/
|
|
91
|
+
function toDescriptor(entry) {
|
|
92
|
+
return {
|
|
93
|
+
id: entry.info.id,
|
|
94
|
+
name: entry.info.name,
|
|
95
|
+
hostname: entry.info.hostname ?? entry.info.id,
|
|
96
|
+
platform: entry.info.platform ?? 'unknown',
|
|
97
|
+
arch: entry.info.arch ?? 'unknown',
|
|
98
|
+
cpuModel: entry.info.cpuModel ?? null,
|
|
99
|
+
cpuCores: entry.info.cpuCores ?? 0,
|
|
100
|
+
memoryMB: entry.info.memoryMB ?? 0,
|
|
101
|
+
engines: [...(entry.info.pythonRuntimes ?? [])],
|
|
102
|
+
isHub: entry.isHub,
|
|
103
|
+
localIps: [...(entry.localIps ?? [])],
|
|
104
|
+
addonIds: [...(entry.agentAddons ?? [])],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
84
107
|
class AgentRegistryService {
|
|
85
108
|
eventBus;
|
|
86
109
|
moleculer;
|
|
@@ -96,6 +119,13 @@ class AgentRegistryService {
|
|
|
96
119
|
* order (see `manual-boot.ts`).
|
|
97
120
|
*/
|
|
98
121
|
addonRegistry = null;
|
|
122
|
+
/**
|
|
123
|
+
* Durable, routing-blind offline-node history (A1). Separate from
|
|
124
|
+
* `HubNodeRegistry` on purpose — see `cluster-node-history-store.ts`. Wired
|
|
125
|
+
* post-construction from manual-boot so the 3-arg constructor (and its unit
|
|
126
|
+
* fakes) stay untouched; a null store means "history disabled" (best-effort).
|
|
127
|
+
*/
|
|
128
|
+
historyStore = null;
|
|
99
129
|
constructor(eventBus, moleculer, capabilityService) {
|
|
100
130
|
this.eventBus = eventBus;
|
|
101
131
|
this.moleculer = moleculer;
|
|
@@ -105,6 +135,22 @@ class AgentRegistryService {
|
|
|
105
135
|
setAddonRegistry(addonRegistry) {
|
|
106
136
|
this.addonRegistry = addonRegistry;
|
|
107
137
|
}
|
|
138
|
+
/** Wire the durable offline-node history store (A1). */
|
|
139
|
+
setClusterNodeHistoryStore(store) {
|
|
140
|
+
this.historyStore = store;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Read the durable offline-node history — consumed by `computeTopology`
|
|
144
|
+
* to render OFFLINE rows for nodes no longer in the live Moleculer window.
|
|
145
|
+
* Returns `[]` when no history store is wired (unit fakes).
|
|
146
|
+
*/
|
|
147
|
+
async getClusterNodeHistory() {
|
|
148
|
+
return this.historyStore ? this.historyStore.getAll() : [];
|
|
149
|
+
}
|
|
150
|
+
/** Permanently forget a node's persisted history (inline "Forget node"). */
|
|
151
|
+
async forgetClusterNode(nodeId) {
|
|
152
|
+
await this.historyStore?.forget(nodeId);
|
|
153
|
+
}
|
|
108
154
|
/** Typed view of the Moleculer broker — single documented cast. */
|
|
109
155
|
get broker() {
|
|
110
156
|
return this.moleculer.broker;
|
|
@@ -154,6 +200,10 @@ class AgentRegistryService {
|
|
|
154
200
|
return;
|
|
155
201
|
if (kind === 'agent') {
|
|
156
202
|
console.log(`[agent-registry] Agent disconnected: ${node.id}`);
|
|
203
|
+
// A1: stamp the disconnect time as the node's `lastActive` so its
|
|
204
|
+
// offline row reports an honest "last seen". Best-effort, no-op if the
|
|
205
|
+
// node was never snapshotted while online.
|
|
206
|
+
void this.historyStore?.touch(node.id);
|
|
157
207
|
this.eventBus.emit({
|
|
158
208
|
id: (0, node_crypto_1.randomUUID)(),
|
|
159
209
|
timestamp: new Date(),
|
|
@@ -407,12 +457,27 @@ class AgentRegistryService {
|
|
|
407
457
|
const hubEntry = await this.buildHubEntry(hubProcesses);
|
|
408
458
|
const remoteEntries = [];
|
|
409
459
|
const registry = this.moleculer.broker.registry;
|
|
410
|
-
|
|
460
|
+
// `onlyAvailable: false` intentionally includes nodes Moleculer still
|
|
461
|
+
// remembers as disconnected (`available: false`) — the same registry
|
|
462
|
+
// read `listNodeLiveness()` uses for `/health`. This surfaces recently
|
|
463
|
+
// offline agents inline instead of silently omitting them; it is NOT a
|
|
464
|
+
// new shadow map (no persistence, no timestamp field added anywhere) —
|
|
465
|
+
// once Moleculer's own `cleanOfflineNodesTimeout` purges the entry, the
|
|
466
|
+
// row disappears again. A hub-restart-surviving offline history is a
|
|
467
|
+
// separate, deliberately deferred piece of work.
|
|
468
|
+
const nodes = registry?.getNodeList?.({ onlyAvailable: false }) ?? [];
|
|
411
469
|
for (const node of nodes) {
|
|
412
470
|
const nodeId = node.id;
|
|
413
471
|
// Skip hub (already included) and child processes (contain '/')
|
|
414
472
|
if (nodeId === 'hub' || nodeId.includes('/'))
|
|
415
473
|
continue;
|
|
474
|
+
if (!node.available) {
|
|
475
|
+
// Offline: skip the `$agent.status`/`$process.list` RPC fan-out
|
|
476
|
+
// entirely (the node is unreachable — those calls would just time
|
|
477
|
+
// out) and surface a minimal, honestly-degraded row instead.
|
|
478
|
+
remoteEntries.push(this.buildOfflineEntry(node));
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
416
481
|
try {
|
|
417
482
|
const status = (await this.broker.call('$agent.status', {}, {
|
|
418
483
|
nodeID: nodeId,
|
|
@@ -486,12 +551,64 @@ class AgentRegistryService {
|
|
|
486
551
|
// Skip nodes without $agent service
|
|
487
552
|
}
|
|
488
553
|
}
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
554
|
+
// A1 (R2 capture point): snapshot every ONLINE node's static descriptor +
|
|
555
|
+
// last-known addon roster into the durable, routing-blind history store on
|
|
556
|
+
// each refresh. This is the ONLY place cpuModel/cores/memory/localIps are
|
|
557
|
+
// resolved from live `$agent.status`, so it is where an offline row's
|
|
558
|
+
// descriptor must be captured while the node is still reachable. Offline
|
|
559
|
+
// entries (the `!node.available` branch above) are NOT re-snapshotted —
|
|
560
|
+
// their `lastActive` is stamped on `$node.disconnected` instead. This is a
|
|
561
|
+
// separate durable store, never a `knownAgents`-style shadow of
|
|
562
|
+
// `HubNodeRegistry` (which stays the ephemeral live cap authority).
|
|
563
|
+
await this.snapshotOnlineNodes([hubEntry, ...remoteEntries]);
|
|
493
564
|
return [hubEntry, ...remoteEntries];
|
|
494
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Best-effort upsert of every online entry's descriptor into the history
|
|
568
|
+
* store. Never throws — the store swallows its own errors and this awaits
|
|
569
|
+
* `allSettled` so a storage hiccup can never fail `listNodes()`.
|
|
570
|
+
*/
|
|
571
|
+
async snapshotOnlineNodes(entries) {
|
|
572
|
+
if (!this.historyStore)
|
|
573
|
+
return;
|
|
574
|
+
const online = entries.filter((e) => e.isOnline !== false);
|
|
575
|
+
await Promise.allSettled(online.map((e) => this.historyStore.snapshot(toDescriptor(e))));
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Minimal, honestly-degraded entry for a node Moleculer's registry still
|
|
579
|
+
* remembers but marks `available: false`. No RPC round-trip is attempted
|
|
580
|
+
* (the node is unreachable by definition) so every live-only field is
|
|
581
|
+
* zeroed/defaulted rather than guessed. `connectedSince: Date.now()`
|
|
582
|
+
* keeps the downstream `uptime = Date.now() - connectedSince` topology
|
|
583
|
+
* projection at ~0 instead of asserting a stale/undefined value.
|
|
584
|
+
*/
|
|
585
|
+
buildOfflineEntry(node) {
|
|
586
|
+
return {
|
|
587
|
+
info: {
|
|
588
|
+
id: node.id,
|
|
589
|
+
name: node.id,
|
|
590
|
+
hostname: node.hostname ?? node.id,
|
|
591
|
+
capabilities: [],
|
|
592
|
+
platform: 'unknown',
|
|
593
|
+
arch: 'unknown',
|
|
594
|
+
cpuCores: 0,
|
|
595
|
+
memoryMB: 0,
|
|
596
|
+
},
|
|
597
|
+
localIps: [],
|
|
598
|
+
status: {
|
|
599
|
+
activeCameras: 0,
|
|
600
|
+
cpuPercent: 0,
|
|
601
|
+
memoryPercent: 0,
|
|
602
|
+
fps: {},
|
|
603
|
+
errors: [],
|
|
604
|
+
},
|
|
605
|
+
connectedSince: Date.now(),
|
|
606
|
+
isHub: false,
|
|
607
|
+
isOnline: false,
|
|
608
|
+
subProcesses: [],
|
|
609
|
+
agentAddons: [],
|
|
610
|
+
};
|
|
611
|
+
}
|
|
495
612
|
async buildHubEntry(subProcesses = []) {
|
|
496
613
|
const cpus = os.cpus();
|
|
497
614
|
// Get live metrics from the metrics-provider capability (NativeMetricsProvider).
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClusterNodeHistoryStore = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const types_1 = require("@camstack/types");
|
|
6
|
+
const DescriptorSchema = zod_1.z.object({
|
|
7
|
+
id: zod_1.z.string(),
|
|
8
|
+
name: zod_1.z.string(),
|
|
9
|
+
hostname: zod_1.z.string(),
|
|
10
|
+
platform: zod_1.z.string(),
|
|
11
|
+
arch: zod_1.z.string(),
|
|
12
|
+
cpuModel: zod_1.z.string().nullable(),
|
|
13
|
+
cpuCores: zod_1.z.number(),
|
|
14
|
+
memoryMB: zod_1.z.number(),
|
|
15
|
+
engines: zod_1.z.array(zod_1.z.string()).readonly(),
|
|
16
|
+
isHub: zod_1.z.boolean(),
|
|
17
|
+
localIps: zod_1.z.array(zod_1.z.string()).readonly(),
|
|
18
|
+
addonIds: zod_1.z.array(zod_1.z.string()).readonly(),
|
|
19
|
+
});
|
|
20
|
+
/** Shape of a `settings-store.query` record's `data` for this collection. */
|
|
21
|
+
const RowDataSchema = zod_1.z.object({
|
|
22
|
+
lastActive: zod_1.z.number(),
|
|
23
|
+
descriptor: DescriptorSchema,
|
|
24
|
+
});
|
|
25
|
+
const COLLECTION = 'cluster-node-history';
|
|
26
|
+
class ClusterNodeHistoryStore {
|
|
27
|
+
resolveSettingsStore;
|
|
28
|
+
logger;
|
|
29
|
+
declared = false;
|
|
30
|
+
constructor(resolveSettingsStore, logger) {
|
|
31
|
+
this.resolveSettingsStore = resolveSettingsStore;
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Idempotently declare the typed collection. Returns the resolved provider
|
|
36
|
+
* or null if the settings-store isn't available yet (early boot / hub
|
|
37
|
+
* unreachable) — callers treat null as "skip, best-effort".
|
|
38
|
+
*/
|
|
39
|
+
async ensureDeclared() {
|
|
40
|
+
const store = this.resolveSettingsStore();
|
|
41
|
+
if (!store)
|
|
42
|
+
return null;
|
|
43
|
+
if (this.declared)
|
|
44
|
+
return store;
|
|
45
|
+
try {
|
|
46
|
+
await store.declareCollection({
|
|
47
|
+
collection: COLLECTION,
|
|
48
|
+
columns: [
|
|
49
|
+
{ name: 'id', type: 'TEXT', primaryKey: true, notNull: true },
|
|
50
|
+
{ name: 'lastActive', type: 'INTEGER', notNull: true },
|
|
51
|
+
{ name: 'descriptor', type: 'JSON', notNull: true },
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
this.declared = true;
|
|
55
|
+
return store;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
this.logger.warn('declareCollection failed (best-effort)', {
|
|
59
|
+
meta: { error: (0, types_1.errMsg)(err) },
|
|
60
|
+
});
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Upsert a node's descriptor + addon roster while it is ONLINE, stamping
|
|
66
|
+
* `lastActive = Date.now()`. Called on each `listNodes()` refresh for every
|
|
67
|
+
* live node (R2 capture point).
|
|
68
|
+
*/
|
|
69
|
+
async snapshot(descriptor) {
|
|
70
|
+
const store = await this.ensureDeclared();
|
|
71
|
+
if (!store)
|
|
72
|
+
return;
|
|
73
|
+
try {
|
|
74
|
+
await store.set({
|
|
75
|
+
collection: COLLECTION,
|
|
76
|
+
key: descriptor.id,
|
|
77
|
+
value: { lastActive: Date.now(), descriptor },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
this.logger.warn('snapshot upsert failed (best-effort)', {
|
|
82
|
+
tags: { nodeId: descriptor.id },
|
|
83
|
+
meta: { error: (0, types_1.errMsg)(err) },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Touch `lastActive` for a node without rewriting its descriptor — used on
|
|
89
|
+
* `$node.disconnected` so an offline node's "last seen" is the disconnect
|
|
90
|
+
* time. A no-op if the node was never snapshotted (nothing to update).
|
|
91
|
+
*/
|
|
92
|
+
async touch(nodeId) {
|
|
93
|
+
const store = await this.ensureDeclared();
|
|
94
|
+
if (!store)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
await store.update({
|
|
98
|
+
collection: COLLECTION,
|
|
99
|
+
id: nodeId,
|
|
100
|
+
data: { lastActive: Date.now() },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
this.logger.warn('touch failed (best-effort)', {
|
|
105
|
+
tags: { nodeId },
|
|
106
|
+
meta: { error: (0, types_1.errMsg)(err) },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Read the full persisted history, validated. Malformed rows are dropped. */
|
|
111
|
+
async getAll() {
|
|
112
|
+
const store = await this.ensureDeclared();
|
|
113
|
+
if (!store)
|
|
114
|
+
return [];
|
|
115
|
+
try {
|
|
116
|
+
const rows = await store.query({ collection: COLLECTION });
|
|
117
|
+
const result = [];
|
|
118
|
+
for (const row of rows) {
|
|
119
|
+
const parsed = RowDataSchema.safeParse(row.data);
|
|
120
|
+
if (!parsed.success) {
|
|
121
|
+
this.logger.warn('dropping malformed cluster-node-history row', {
|
|
122
|
+
tags: { nodeId: row.id },
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
result.push({ ...parsed.data.descriptor, lastActive: parsed.data.lastActive });
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
this.logger.warn('getAll failed (best-effort)', { meta: { error: (0, types_1.errMsg)(err) } });
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Permanently forget a node's history row (the inline "Forget node" action). */
|
|
136
|
+
async forget(nodeId) {
|
|
137
|
+
const store = await this.ensureDeclared();
|
|
138
|
+
if (!store)
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
await store.delete({ collection: COLLECTION, key: nodeId });
|
|
142
|
+
this.logger.info('forgot cluster node', { tags: { nodeId } });
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
this.logger.warn('forget failed (best-effort)', {
|
|
146
|
+
tags: { nodeId },
|
|
147
|
+
meta: { error: (0, types_1.errMsg)(err) },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.ClusterNodeHistoryStore = ClusterNodeHistoryStore;
|
|
@@ -251,6 +251,14 @@ class MoleculerService {
|
|
|
251
251
|
// handler retries the hub-local route then throws a precise error rather
|
|
252
252
|
// than the unroutable broker fallback (`switch.switch.getStatus`).
|
|
253
253
|
isDeviceNativeCap: (capName) => this.capabilityService.getRegistry()?.getDefinition(capName)?.deviceNative === true,
|
|
254
|
+
// `nodeIdMode: 'data'` signal: for these caps (`addon-settings`,
|
|
255
|
+
// `addons`, `nodes`, `pipeline-orchestrator`) an inline `args.nodeId`
|
|
256
|
+
// is provider DATA — the hub singleton dispatches internally — never a
|
|
257
|
+
// routing pin. Without this a forked child's call carrying `nodeId`
|
|
258
|
+
// gets pinned to a node with no provider (dead broker fallback →
|
|
259
|
+
// ServiceNotFoundError). The kernel layer has no cap registry, so feed
|
|
260
|
+
// it the flag from the cap definition.
|
|
261
|
+
isDataNodeIdCap: (capName) => this.capabilityService.getRegistry()?.getDefinition(capName)?.nodeIdMode === 'data',
|
|
254
262
|
// Empty-collection fast-path: a `collection` cap's array-output method
|
|
255
263
|
// with ZERO providers registered cluster-wide IS the empty aggregate —
|
|
256
264
|
// return [] instead of routing (resolver miss → broker fallback → ~30s
|
|
@@ -388,12 +396,6 @@ class MoleculerService {
|
|
|
388
396
|
probe: (url, options) => this.streamProbe.probe(url, options),
|
|
389
397
|
probeField: (key, value) => this.streamProbe.probeField(key, value),
|
|
390
398
|
}));
|
|
391
|
-
// Register `$hwaccel` on hub — every node in the cluster does the
|
|
392
|
-
// same so `broker.call('$hwaccel.resolve', params, { nodeID })`
|
|
393
|
-
// returns the backend list for whichever host the caller targets.
|
|
394
|
-
// Admin UI uses this to show per-agent hwaccel info on the
|
|
395
|
-
// pipeline / NodeDetail pages.
|
|
396
|
-
this.brokerSafe.createService((0, system_1.createHwAccelService)((0, system_1.createKernelHwAccel)()));
|
|
397
399
|
await this.brokerSafe.start();
|
|
398
400
|
logger.info('Moleculer broker started (TCP transport)');
|
|
399
401
|
// Construct the CapRouteResolver now that both the broker and
|
package/dist/manual-boot.js
CHANGED
|
@@ -67,6 +67,7 @@ const addon_widgets_service_1 = require("./core/addon-widgets/addon-widgets.serv
|
|
|
67
67
|
const addon_bridge_service_1 = require("./core/addon-bridge/addon-bridge.service");
|
|
68
68
|
const moleculer_service_1 = require("./core/moleculer/moleculer.service");
|
|
69
69
|
const agent_registry_service_1 = require("./core/agent/agent-registry.service");
|
|
70
|
+
const cluster_node_history_store_1 = require("./core/agent/cluster-node-history-store");
|
|
70
71
|
const addon_registry_service_1 = require("./core/addon/addon-registry.service");
|
|
71
72
|
const addon_search_service_1 = require("./core/addon/addon-search.service");
|
|
72
73
|
const addon_package_service_1 = require("./core/addon/addon-package.service");
|
|
@@ -128,6 +129,11 @@ async function bootManual(opts) {
|
|
|
128
129
|
// reconciliation, but is constructed first (it has no other dependency
|
|
129
130
|
// on AddonRegistryService) — wire it post-construction.
|
|
130
131
|
agentRegistryService.setAddonRegistry(addonRegistryService);
|
|
132
|
+
// A1: durable, routing-blind offline-node history — persisted through the
|
|
133
|
+
// hub-resident `settings-store` cap (resolved lazily; null until it boots).
|
|
134
|
+
// Wired post-construction so the service's 3-arg constructor stays intact.
|
|
135
|
+
agentRegistryService.setClusterNodeHistoryStore(new cluster_node_history_store_1.ClusterNodeHistoryStore(() => capabilityService.getRegistry()?.getSingleton('settings-store') ??
|
|
136
|
+
null, loggingService.createLogger('cluster-node-history')));
|
|
131
137
|
// ---- Layer 4: need AddonRegistry ---------------------------------------
|
|
132
138
|
// AddonWidgetsService — needs AddonRegistryService for the bundled-addon
|
|
133
139
|
// dist sub-folder lookup (see service docstring).
|