@camstack/server 1.1.65 → 1.1.67
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/trpc/generated-cap-routers.js +90 -0
- package/dist/core/addon/addon-registry.service.js +44 -2
- package/dist/core/addon/system-addons-sync.js +121 -0
- package/dist/core/moleculer/agent-readiness-pull.js +40 -0
- package/dist/core/moleculer/moleculer.service.js +65 -0
- package/package.json +14 -14
|
@@ -2243,6 +2243,33 @@ function createCapRouter_deviceExport(getProvider, createRemoteProxy) {
|
|
|
2243
2243
|
}
|
|
2244
2244
|
function createCapRouter_deviceManager(getProvider, createRemoteProxy) {
|
|
2245
2245
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
2246
|
+
getDeviceSettingsContribution: trpc_middleware_js_1.protectedProcedure
|
|
2247
|
+
.input(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceSettingsContribution.input.loose())
|
|
2248
|
+
.output(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceSettingsContribution.output)
|
|
2249
|
+
.query(async ({ input, ctx }) => {
|
|
2250
|
+
const { nodeId, ...methodInput } = input;
|
|
2251
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2252
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2253
|
+
return p.getDeviceSettingsContribution(methodInput);
|
|
2254
|
+
}),
|
|
2255
|
+
getDeviceLiveContribution: trpc_middleware_js_1.protectedProcedure
|
|
2256
|
+
.input(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceLiveContribution.input.loose())
|
|
2257
|
+
.output(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceLiveContribution.output)
|
|
2258
|
+
.query(async ({ input, ctx }) => {
|
|
2259
|
+
const { nodeId, ...methodInput } = input;
|
|
2260
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2261
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2262
|
+
return p.getDeviceLiveContribution(methodInput);
|
|
2263
|
+
}),
|
|
2264
|
+
applyDeviceSettingsPatch: trpc_middleware_js_1.adminProcedure
|
|
2265
|
+
.input(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.applyDeviceSettingsPatch.input.loose())
|
|
2266
|
+
.output(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.applyDeviceSettingsPatch.output)
|
|
2267
|
+
.mutation(async ({ input, ctx }) => {
|
|
2268
|
+
const { nodeId, ...methodInput } = input;
|
|
2269
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2270
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2271
|
+
return p.applyDeviceSettingsPatch(methodInput);
|
|
2272
|
+
}),
|
|
2246
2273
|
allocateDeviceId: trpc_middleware_js_1.protectedProcedure
|
|
2247
2274
|
.input(types_39.deviceManagerCapability.methods.allocateDeviceId.input.loose())
|
|
2248
2275
|
.output(types_39.deviceManagerCapability.methods.allocateDeviceId.output)
|
|
@@ -2511,6 +2538,15 @@ function createCapRouter_deviceManager(getProvider, createRemoteProxy) {
|
|
|
2511
2538
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2512
2539
|
return p.getChildren(methodInput);
|
|
2513
2540
|
}),
|
|
2541
|
+
getLinkedDevices: trpc_middleware_js_1.protectedProcedure
|
|
2542
|
+
.input(types_39.deviceManagerCapability.methods.getLinkedDevices.input.loose())
|
|
2543
|
+
.output(types_39.deviceManagerCapability.methods.getLinkedDevices.output)
|
|
2544
|
+
.query(async ({ input, ctx }) => {
|
|
2545
|
+
const { nodeId, ...methodInput } = input;
|
|
2546
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2547
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2548
|
+
return p.getLinkedDevices(methodInput);
|
|
2549
|
+
}),
|
|
2514
2550
|
getStreamSources: trpc_middleware_js_1.protectedProcedure
|
|
2515
2551
|
.input(types_39.deviceManagerCapability.methods.getStreamSources.input.loose())
|
|
2516
2552
|
.output(types_39.deviceManagerCapability.methods.getStreamSources.output)
|
|
@@ -3117,6 +3153,33 @@ function createCapRouter_doorbell(getProvider, createRemoteProxy) {
|
|
|
3117
3153
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
3118
3154
|
return p.getStatus(methodInput);
|
|
3119
3155
|
}),
|
|
3156
|
+
getDeviceSettingsContribution: trpc_middleware_js_1.protectedProcedure
|
|
3157
|
+
.input(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceSettingsContribution.input.loose())
|
|
3158
|
+
.output(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceSettingsContribution.output)
|
|
3159
|
+
.query(async ({ input, ctx }) => {
|
|
3160
|
+
const { nodeId, ...methodInput } = input;
|
|
3161
|
+
const p = resolveProvider('doorbell', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
3162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
3163
|
+
return p.getDeviceSettingsContribution(methodInput);
|
|
3164
|
+
}),
|
|
3165
|
+
getDeviceLiveContribution: trpc_middleware_js_1.protectedProcedure
|
|
3166
|
+
.input(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceLiveContribution.input.loose())
|
|
3167
|
+
.output(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceLiveContribution.output)
|
|
3168
|
+
.query(async ({ input, ctx }) => {
|
|
3169
|
+
const { nodeId, ...methodInput } = input;
|
|
3170
|
+
const p = resolveProvider('doorbell', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
3171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
3172
|
+
return p.getDeviceLiveContribution(methodInput);
|
|
3173
|
+
}),
|
|
3174
|
+
applyDeviceSettingsPatch: trpc_middleware_js_1.adminProcedure
|
|
3175
|
+
.input(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.applyDeviceSettingsPatch.input.loose())
|
|
3176
|
+
.output(types_1.DEVICE_SETTINGS_CONTRIBUTION_METHODS.applyDeviceSettingsPatch.output)
|
|
3177
|
+
.mutation(async ({ input, ctx }) => {
|
|
3178
|
+
const { nodeId, ...methodInput } = input;
|
|
3179
|
+
const p = resolveProvider('doorbell', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
3180
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
3181
|
+
return p.applyDeviceSettingsPatch(methodInput);
|
|
3182
|
+
}),
|
|
3120
3183
|
});
|
|
3121
3184
|
}
|
|
3122
3185
|
function createCapRouter_embeddingEncoder(getProvider, createRemoteProxy) {
|
|
@@ -5121,6 +5184,15 @@ function createCapRouter_pipelineAnalytics(getProvider, createRemoteProxy) {
|
|
|
5121
5184
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5122
5185
|
return p.listTracks(methodInput);
|
|
5123
5186
|
}),
|
|
5187
|
+
listRecentTracks: trpc_middleware_js_1.protectedProcedure
|
|
5188
|
+
.input(types_78.pipelineAnalyticsCapability.methods.listRecentTracks.input.loose())
|
|
5189
|
+
.output(types_78.pipelineAnalyticsCapability.methods.listRecentTracks.output)
|
|
5190
|
+
.query(async ({ input, ctx }) => {
|
|
5191
|
+
const { nodeId, ...methodInput } = input;
|
|
5192
|
+
const p = resolveProvider('pipeline-analytics', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
5193
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5194
|
+
return p.listRecentTracks(methodInput);
|
|
5195
|
+
}),
|
|
5124
5196
|
clearTracks: trpc_middleware_js_1.adminProcedure
|
|
5125
5197
|
.input(types_78.pipelineAnalyticsCapability.methods.clearTracks.input.loose())
|
|
5126
5198
|
.output(types_78.pipelineAnalyticsCapability.methods.clearTracks.output)
|
|
@@ -5157,6 +5229,24 @@ function createCapRouter_pipelineAnalytics(getProvider, createRemoteProxy) {
|
|
|
5157
5229
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5158
5230
|
return p.getAudioEvents(methodInput);
|
|
5159
5231
|
}),
|
|
5232
|
+
listEventKinds: trpc_middleware_js_1.protectedProcedure
|
|
5233
|
+
.input(types_78.pipelineAnalyticsCapability.methods.listEventKinds.input.loose())
|
|
5234
|
+
.output(types_78.pipelineAnalyticsCapability.methods.listEventKinds.output)
|
|
5235
|
+
.query(async ({ input, ctx }) => {
|
|
5236
|
+
const { nodeId, ...methodInput } = input;
|
|
5237
|
+
const p = resolveProvider('pipeline-analytics', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
5238
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5239
|
+
return p.listEventKinds(methodInput);
|
|
5240
|
+
}),
|
|
5241
|
+
getSensorEvents: trpc_middleware_js_1.protectedProcedure
|
|
5242
|
+
.input(types_78.pipelineAnalyticsCapability.methods.getSensorEvents.input.loose())
|
|
5243
|
+
.output(types_78.pipelineAnalyticsCapability.methods.getSensorEvents.output)
|
|
5244
|
+
.query(async ({ input, ctx }) => {
|
|
5245
|
+
const { nodeId, ...methodInput } = input;
|
|
5246
|
+
const p = resolveProvider('pipeline-analytics', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
5247
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5248
|
+
return p.getSensorEvents(methodInput);
|
|
5249
|
+
}),
|
|
5160
5250
|
getKeyEvents: trpc_middleware_js_1.protectedProcedure
|
|
5161
5251
|
.input(types_78.pipelineAnalyticsCapability.methods.getKeyEvents.input.loose())
|
|
5162
5252
|
.output(types_78.pipelineAnalyticsCapability.methods.getKeyEvents.output)
|
|
@@ -39,6 +39,8 @@ exports.shouldEmitProviderRegisteredReady = shouldEmitProviderRegisteredReady;
|
|
|
39
39
|
const os = __importStar(require("node:os"));
|
|
40
40
|
const addon_row_manifest_1 = require("./addon-row-manifest");
|
|
41
41
|
const runner_convergence_1 = require("./runner-convergence");
|
|
42
|
+
const system_addons_sync_1 = require("./system-addons-sync");
|
|
43
|
+
const package_dir_utils_1 = require("./package-dir-utils");
|
|
42
44
|
const types_1 = require("@camstack/types");
|
|
43
45
|
const system_1 = require("@camstack/system");
|
|
44
46
|
const system_2 = require("@camstack/system");
|
|
@@ -433,6 +435,24 @@ class AddonRegistryService {
|
|
|
433
435
|
// launcher). Defaults to `<dataDir>/addons` for dev.
|
|
434
436
|
const dataDir = path.resolve(process.env.CAMSTACK_DATA ?? 'camstack-data');
|
|
435
437
|
const addonsDir = process.env.CAMSTACK_ADDONS_DIR ?? path.resolve(dataDir, 'addons');
|
|
438
|
+
// Builtins load from the ADDONS-DIR copy of `@camstack/system`, which no
|
|
439
|
+
// update channel refreshes on an image hub (seed excludes it, apply-update
|
|
440
|
+
// swaps only server-root, deploy of system is rejected) — a stale copy
|
|
441
|
+
// silently pins builtin code forever (2026-07-17: snapshot builtin ran
|
|
442
|
+
// 1.1.49 while the closure carried 1.1.51). Reconcile it from the hub's
|
|
443
|
+
// own closure when the closure is NEWER; a newer addons copy (dev
|
|
444
|
+
// live-update flow) is left alone. Never throws.
|
|
445
|
+
const closureSystemDir = (0, package_dir_utils_1.resolveHubClosurePackageDir)('@camstack/system');
|
|
446
|
+
if (closureSystemDir) {
|
|
447
|
+
const outcome = (0, system_addons_sync_1.syncSystemAddonsCopy)({
|
|
448
|
+
addonsDir,
|
|
449
|
+
closureSystemDir,
|
|
450
|
+
log: (msg, meta) => this.logger.info(msg, { meta: meta ?? {} }),
|
|
451
|
+
});
|
|
452
|
+
if (outcome === 'failed') {
|
|
453
|
+
this.logger.warn('system addons-copy sync failed — builtins may run stale code');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
436
456
|
await this.addonLoader.loadFromDirectory(addonsDir);
|
|
437
457
|
// Hand pre-init load failures (broken package.json, missing
|
|
438
458
|
// dist, import-time errors) to the health monitor so its retry
|
|
@@ -1399,6 +1419,16 @@ class AddonRegistryService {
|
|
|
1399
1419
|
// roster from the freshly-swapped on-disk bundle. Updating ONE member of a
|
|
1400
1420
|
// group therefore respawns the ENTIRE group — they share a process.
|
|
1401
1421
|
if (this.isForkedAddonEntry(entry)) {
|
|
1422
|
+
// Snapshot which declared caps are ACTUALLY registered right now —
|
|
1423
|
+
// the post-restart wait below must only gate on these. Device-scoped
|
|
1424
|
+
// caps (vacuum-control, valve, humidifier, …) register only when a
|
|
1425
|
+
// device using them exists: waiting for a cap that was NOT registered
|
|
1426
|
+
// before the restart burns the full 120s timeout for nothing (live:
|
|
1427
|
+
// integrations.create held its mutation — and the creation modal —
|
|
1428
|
+
// for 2 minutes on the HA addon's 8 deviceless caps).
|
|
1429
|
+
const preRegistered = new Set(entry.declaredCapabilities
|
|
1430
|
+
.map((cap) => cap.name)
|
|
1431
|
+
.filter((name) => this.capabilityRegistry.getProviderByAddon(name, addonId) != null));
|
|
1402
1432
|
await this.ensureForkedRunner(addonId, { restart: true });
|
|
1403
1433
|
// The respawn resolves as soon as the child is up, not when its
|
|
1404
1434
|
// capabilities are re-registered. Callers (integrations.create, UI forms) may
|
|
@@ -1430,9 +1460,21 @@ class AddonRegistryService {
|
|
|
1430
1460
|
// *misleading error* on timeout — a warning avoids that while still
|
|
1431
1461
|
// letting the update finalize).
|
|
1432
1462
|
const REGISTER_WAIT_MS = 120_000;
|
|
1433
|
-
|
|
1463
|
+
// Gate ONLY on the caps that were registered BEFORE the restart —
|
|
1464
|
+
// those are the ones a caller could have been routing to. Caps that
|
|
1465
|
+
// were absent (device-scoped, no device yet) re-register
|
|
1466
|
+
// asynchronously whenever their devices appear; waiting for them
|
|
1467
|
+
// here held integrations.create (and its modal) for the full
|
|
1468
|
+
// timeout on every deviceless declared cap.
|
|
1469
|
+
const gated = declared.filter((cap) => preRegistered.has(cap.name));
|
|
1470
|
+
const skipped = declared.filter((cap) => !preRegistered.has(cap.name));
|
|
1471
|
+
if (skipped.length > 0) {
|
|
1472
|
+
this.logger.debug(`Addon "${addonId}" restart: not gating on ${skipped.length} cap(s) that were ` +
|
|
1473
|
+
`unregistered pre-restart (device-scoped, awaiting devices): ${skipped.map((c) => c.name).join(', ')}`, { tags: { addonId } });
|
|
1474
|
+
}
|
|
1475
|
+
const waits = gated.map((cap) => this.capabilityRegistry.waitForProvider(cap.name, addonId, REGISTER_WAIT_MS));
|
|
1434
1476
|
const settled = await Promise.all(waits);
|
|
1435
|
-
const missing =
|
|
1477
|
+
const missing = gated
|
|
1436
1478
|
.map((cap, i) => (settled[i] == null ? cap.name : null))
|
|
1437
1479
|
.filter((name) => name !== null);
|
|
1438
1480
|
if (missing.length > 0) {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isVersionNewer = isVersionNewer;
|
|
37
|
+
exports.syncSystemAddonsCopy = syncSystemAddonsCopy;
|
|
38
|
+
/**
|
|
39
|
+
* system-addons-sync — reconcile the ADDONS-DIR copy of `@camstack/system`
|
|
40
|
+
* with the hub's own (active server-root closure) copy at boot.
|
|
41
|
+
*
|
|
42
|
+
* Builtins are loaded from `CAMSTACK_ADDONS_DIR/@camstack/system` (the addon
|
|
43
|
+
* loader scans only that dir), but NO update channel refreshes that copy on an
|
|
44
|
+
* image hub: the seed-addons stage excludes framework packages, applyServerUpdate
|
|
45
|
+
* swaps only /data/server-root, and `camstack deploy packages/system` is
|
|
46
|
+
* rejected once a release image runs. Live incident 2026-07-17: the snapshot
|
|
47
|
+
* builtin ran system 1.1.49 (no media endpoint) while the active closure
|
|
48
|
+
* carried 1.1.51 — shipped builtin code silently never executed.
|
|
49
|
+
*
|
|
50
|
+
* The sync copies `dist/` + `package.json` from the closure copy over the
|
|
51
|
+
* addons copy when the closure version is NEWER (semver-wise). The addons
|
|
52
|
+
* copy's own `node_modules/` is preserved: third-party deps also resolve via
|
|
53
|
+
* NODE_PATH (the framework dir), and `@camstack/*` deps are host-provided.
|
|
54
|
+
* Staged copy + rename swap so a crash mid-sync never leaves a gutted dist.
|
|
55
|
+
* An addons copy that is newer or equal (dev live-update flow) is left alone.
|
|
56
|
+
*/
|
|
57
|
+
const fs = __importStar(require("node:fs"));
|
|
58
|
+
const path = __importStar(require("node:path"));
|
|
59
|
+
/** Loose semver compare on dotted numeric parts; non-numeric parts compare 0. */
|
|
60
|
+
function isVersionNewer(candidate, current) {
|
|
61
|
+
const a = candidate.split('.').map((p) => Number.parseInt(p, 10) || 0);
|
|
62
|
+
const b = current.split('.').map((p) => Number.parseInt(p, 10) || 0);
|
|
63
|
+
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
64
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
65
|
+
if (d !== 0)
|
|
66
|
+
return d > 0;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function readVersion(pkgDir) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf-8'));
|
|
73
|
+
return typeof raw.version === 'string' ? raw.version : null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reconcile once. Returns what happened (for the caller's log/telemetry).
|
|
81
|
+
* Never throws — a failed sync leaves the previous copy running (same as
|
|
82
|
+
* before this fix existed) and reports `'failed'`.
|
|
83
|
+
*/
|
|
84
|
+
function syncSystemAddonsCopy(input) {
|
|
85
|
+
const { addonsDir, closureSystemDir, log } = input;
|
|
86
|
+
const addonsCopyDir = path.join(addonsDir, '@camstack', 'system');
|
|
87
|
+
if (!fs.existsSync(path.join(addonsCopyDir, 'package.json')))
|
|
88
|
+
return 'absent';
|
|
89
|
+
const closureVersion = readVersion(closureSystemDir);
|
|
90
|
+
const addonsVersion = readVersion(addonsCopyDir);
|
|
91
|
+
if (closureVersion === null || addonsVersion === null)
|
|
92
|
+
return 'failed';
|
|
93
|
+
if (!isVersionNewer(closureVersion, addonsVersion))
|
|
94
|
+
return 'up-to-date';
|
|
95
|
+
try {
|
|
96
|
+
const srcDist = path.join(closureSystemDir, 'dist');
|
|
97
|
+
const nextDist = path.join(addonsCopyDir, `.dist.next-${process.pid}`);
|
|
98
|
+
const oldDist = path.join(addonsCopyDir, `.dist.old-${process.pid}`);
|
|
99
|
+
fs.rmSync(nextDist, { recursive: true, force: true });
|
|
100
|
+
fs.cpSync(srcDist, nextDist, { recursive: true });
|
|
101
|
+
// Swap: rename-aside then rename-in (metadata ops on the same mount).
|
|
102
|
+
fs.rmSync(oldDist, { recursive: true, force: true });
|
|
103
|
+
if (fs.existsSync(path.join(addonsCopyDir, 'dist'))) {
|
|
104
|
+
fs.renameSync(path.join(addonsCopyDir, 'dist'), oldDist);
|
|
105
|
+
}
|
|
106
|
+
fs.renameSync(nextDist, path.join(addonsCopyDir, 'dist'));
|
|
107
|
+
fs.copyFileSync(path.join(closureSystemDir, 'package.json'), path.join(addonsCopyDir, 'package.json'));
|
|
108
|
+
fs.rmSync(oldDist, { recursive: true, force: true });
|
|
109
|
+
log('addons-dir @camstack/system synced from closure', {
|
|
110
|
+
from: addonsVersion,
|
|
111
|
+
to: closureVersion,
|
|
112
|
+
});
|
|
113
|
+
return 'synced';
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
log('addons-dir @camstack/system sync FAILED — stale builtins keep running', {
|
|
117
|
+
error: err instanceof Error ? err.message : String(err),
|
|
118
|
+
});
|
|
119
|
+
return 'failed';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterAgentAuthoritativeRecords = filterAgentAuthoritativeRecords;
|
|
4
|
+
const VALID_STATES = new Set(['starting', 'ready', 'down']);
|
|
5
|
+
const VALID_SCOPE_TYPES = new Set(['global', 'node', 'device']);
|
|
6
|
+
/** Structural validation for a record received from a remote node. */
|
|
7
|
+
function isWellFormedRecord(value) {
|
|
8
|
+
if (typeof value !== 'object' || value === null)
|
|
9
|
+
return false;
|
|
10
|
+
const record = value;
|
|
11
|
+
if (typeof record.capName !== 'string' || record.capName.length === 0)
|
|
12
|
+
return false;
|
|
13
|
+
if (typeof record.state !== 'string' || !VALID_STATES.has(record.state))
|
|
14
|
+
return false;
|
|
15
|
+
if (typeof record.generation !== 'string')
|
|
16
|
+
return false;
|
|
17
|
+
if (typeof record.sourceNodeId !== 'string')
|
|
18
|
+
return false;
|
|
19
|
+
const scope = record.scope;
|
|
20
|
+
if (typeof scope !== 'object' || scope === null)
|
|
21
|
+
return false;
|
|
22
|
+
if (!VALID_SCOPE_TYPES.has(scope.type))
|
|
23
|
+
return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Keep only the records `agentNodeId` is authoritative for, dropping
|
|
28
|
+
* malformed entries. See module doc for why this filter must exist.
|
|
29
|
+
*/
|
|
30
|
+
function filterAgentAuthoritativeRecords(records, agentNodeId) {
|
|
31
|
+
if (!Array.isArray(records))
|
|
32
|
+
return [];
|
|
33
|
+
return records.filter((record) => {
|
|
34
|
+
if (!isWellFormedRecord(record))
|
|
35
|
+
return false;
|
|
36
|
+
if (record.scope.type === 'node')
|
|
37
|
+
return record.scope.nodeId === agentNodeId;
|
|
38
|
+
return record.sourceNodeId === agentNodeId;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -5,6 +5,7 @@ exports.buildChildUdsManifest = buildChildUdsManifest;
|
|
|
5
5
|
const node_crypto_1 = require("node:crypto");
|
|
6
6
|
const system_1 = require("@camstack/system");
|
|
7
7
|
const types_1 = require("@camstack/types");
|
|
8
|
+
const agent_readiness_pull_js_1 = require("./agent-readiness-pull.js");
|
|
8
9
|
const cap_router_runtime_js_1 = require("../../api/trpc/cap-router-runtime.js");
|
|
9
10
|
const core_cap_bridge_js_1 = require("../../api/trpc/core-cap-bridge.js");
|
|
10
11
|
const cap_call_fn_js_1 = require("./cap-call-fn.js");
|
|
@@ -26,6 +27,14 @@ class MoleculerService {
|
|
|
26
27
|
* `$hub.registerNode`. Populated by `onRegisterNode` in `hubDeps`.
|
|
27
28
|
*/
|
|
28
29
|
nodeRegistry = new system_1.HubNodeRegistry();
|
|
30
|
+
/**
|
|
31
|
+
* Fixed-period agent-readiness snapshot sweep (D8 reconcile) — repairs
|
|
32
|
+
* agent-origin readiness deltas lost while the agent stayed connected.
|
|
33
|
+
* Not a retry backoff: a steady-state reconcile against each agent's
|
|
34
|
+
* authoritative snapshot.
|
|
35
|
+
*/
|
|
36
|
+
static AGENT_READINESS_PULL_INTERVAL_MS = 60_000;
|
|
37
|
+
agentReadinessPullTimer = null;
|
|
29
38
|
nodeCallFns = new Map();
|
|
30
39
|
/**
|
|
31
40
|
* D3: callback invoked whenever a bare-ID agent node completes the
|
|
@@ -390,6 +399,30 @@ class MoleculerService {
|
|
|
390
399
|
bridgeBus.localBus.on('$node.disconnected', ({ node }) => {
|
|
391
400
|
this.removeNodeFromRegistry(node.id);
|
|
392
401
|
});
|
|
402
|
+
// D8 reverse-leg snapshot-reconcile: on every agent (re)connect, pull
|
|
403
|
+
// the agent's own readiness snapshot (`$agent-readiness.getSnapshot`)
|
|
404
|
+
// and hydrate the hub-authoritative registry with the records the
|
|
405
|
+
// agent is authoritative for. Cross-node `system.ready-state` deltas
|
|
406
|
+
// are telemetry (lossy) — a lost post-restart `ready` used to leave
|
|
407
|
+
// this registry stuck on the synthesized `down` forever (2026-07-17:
|
|
408
|
+
// pipeline-executor@little-unraid, 3.5h dispatch outage). The
|
|
409
|
+
// periodic sweep below repairs deltas lost while CONNECTED.
|
|
410
|
+
bridgeBus.localBus.on('$node.connected', ({ node }) => {
|
|
411
|
+
const nodeId = node.id;
|
|
412
|
+
if (nodeId === this.brokerSafe.nodeID || nodeId.includes('/'))
|
|
413
|
+
return;
|
|
414
|
+
void this.pullAgentReadinessSnapshot(nodeId);
|
|
415
|
+
});
|
|
416
|
+
this.agentReadinessPullTimer = setInterval(() => {
|
|
417
|
+
for (const nodeId of this.nodeRegistry.listNodeIds()) {
|
|
418
|
+
if (nodeId === this.brokerSafe.nodeID || nodeId.includes('/'))
|
|
419
|
+
continue;
|
|
420
|
+
void this.pullAgentReadinessSnapshot(nodeId);
|
|
421
|
+
}
|
|
422
|
+
}, MoleculerService.AGENT_READINESS_PULL_INTERVAL_MS);
|
|
423
|
+
if (typeof this.agentReadinessPullTimer.unref === 'function') {
|
|
424
|
+
this.agentReadinessPullTimer.unref();
|
|
425
|
+
}
|
|
393
426
|
// Register the $event-bus service BEFORE broker.start(). Moleculer
|
|
394
427
|
// announces service subscriptions to remote nodes only during discovery
|
|
395
428
|
// handshake, not dynamically after post-start `createService()`.
|
|
@@ -955,7 +988,39 @@ class MoleculerService {
|
|
|
955
988
|
return false;
|
|
956
989
|
return registry.setChildLogLevel(childId, level);
|
|
957
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* Pull one agent's own readiness snapshot and hydrate the
|
|
993
|
+
* hub-authoritative registry with the records that agent is
|
|
994
|
+
* authoritative for (see `agent-readiness-pull.ts` for the filter
|
|
995
|
+
* rationale). Best-effort: agents predating `$agent-readiness` (or a
|
|
996
|
+
* service list still propagating right after connect) fail with
|
|
997
|
+
* "not found" — silent; the periodic sweep retries.
|
|
998
|
+
*/
|
|
999
|
+
async pullAgentReadinessSnapshot(nodeId) {
|
|
1000
|
+
try {
|
|
1001
|
+
const snapshot = await this.brokerSafe.call(`${system_1.AGENT_READINESS_SERVICE_NAME}.getSnapshot`, {}, { nodeID: nodeId });
|
|
1002
|
+
if (!Array.isArray(snapshot) || snapshot.length === 0)
|
|
1003
|
+
return;
|
|
1004
|
+
const records = (0, agent_readiness_pull_js_1.filterAgentAuthoritativeRecords)(snapshot, nodeId);
|
|
1005
|
+
if (records.length === 0)
|
|
1006
|
+
return;
|
|
1007
|
+
this.readinessRegistry.hydrate(records);
|
|
1008
|
+
}
|
|
1009
|
+
catch (err) {
|
|
1010
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1011
|
+
if (!msg.includes('not found') && !msg.includes('NOT_FOUND')) {
|
|
1012
|
+
this.logger.debug('agent readiness snapshot pull failed', {
|
|
1013
|
+
tags: { nodeId },
|
|
1014
|
+
meta: { err: msg },
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
958
1019
|
async onModuleDestroy() {
|
|
1020
|
+
if (this.agentReadinessPullTimer !== null) {
|
|
1021
|
+
clearInterval(this.agentReadinessPullTimer);
|
|
1022
|
+
this.agentReadinessPullTimer = null;
|
|
1023
|
+
}
|
|
959
1024
|
this.udsEventBridgeDispose?.();
|
|
960
1025
|
this.udsEventBridgeDispose = null;
|
|
961
1026
|
await this.brokerSafe.stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.67",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
"test:watch": "vitest"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@camstack/addon-admin-ui": "1.1.
|
|
27
|
-
"@camstack/addon-advanced-notifier": "1.1.
|
|
28
|
-
"@camstack/addon-auth": "1.1.
|
|
29
|
-
"@camstack/addon-decoder-nodeav": "1.1.
|
|
30
|
-
"@camstack/addon-notifiers": "1.1.
|
|
31
|
-
"@camstack/addon-pipeline": "1.1.
|
|
32
|
-
"@camstack/addon-pipeline-orchestrator": "1.1.
|
|
33
|
-
"@camstack/addon-post-analysis": "1.1.
|
|
34
|
-
"@camstack/sdk": "1.1.
|
|
35
|
-
"@camstack/shm-ring": "1.0.
|
|
36
|
-
"@camstack/system": "1.1.
|
|
37
|
-
"@camstack/types": "1.1.
|
|
38
|
-
"@camstack/ui-library": "1.1.
|
|
26
|
+
"@camstack/addon-admin-ui": "1.1.55",
|
|
27
|
+
"@camstack/addon-advanced-notifier": "1.1.27",
|
|
28
|
+
"@camstack/addon-auth": "1.1.12",
|
|
29
|
+
"@camstack/addon-decoder-nodeav": "1.1.15",
|
|
30
|
+
"@camstack/addon-notifiers": "1.1.27",
|
|
31
|
+
"@camstack/addon-pipeline": "1.1.60",
|
|
32
|
+
"@camstack/addon-pipeline-orchestrator": "1.1.50",
|
|
33
|
+
"@camstack/addon-post-analysis": "1.1.32",
|
|
34
|
+
"@camstack/sdk": "1.1.27",
|
|
35
|
+
"@camstack/shm-ring": "1.0.26",
|
|
36
|
+
"@camstack/system": "1.1.53",
|
|
37
|
+
"@camstack/types": "1.1.48",
|
|
38
|
+
"@camstack/ui-library": "1.1.39",
|
|
39
39
|
"@fastify/compress": "^9.0.0",
|
|
40
40
|
"@fastify/cookie": "^11.0.2",
|
|
41
41
|
"@fastify/multipart": "^10.0.0",
|