@camstack/server 1.2.138 → 1.2.140
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/agent/agent-update-service.js +9 -13
- package/dist/core/addon/addon-package.service.js +19 -26
- package/dist/core/moleculer/moleculer.service.js +81 -12
- package/dist/core/update-availability-emitter.js +36 -26
- package/dist/core/updates/restart-marker.js +38 -0
- package/dist/manual-boot.js +1 -1
- package/dist/server-root/index.js +7 -1
- package/package.json +8 -8
|
@@ -70,6 +70,7 @@ const system_exec_npm_js_1 = require("../core/server-update/system-exec-npm.js")
|
|
|
70
70
|
const update_availability_emitter_js_1 = require("../core/update-availability-emitter.js");
|
|
71
71
|
const update_availability_store_js_1 = require("../core/update-availability-store.js");
|
|
72
72
|
const index_js_1 = require("../server-root/index.js");
|
|
73
|
+
const restart_marker_js_1 = require("../core/updates/restart-marker.js");
|
|
73
74
|
/**
|
|
74
75
|
* The synthetic addonId the agent's OWN runtime registers infra providers
|
|
75
76
|
* under (no addon owns them — the bootstrap does). Appears in the agent's
|
|
@@ -168,8 +169,8 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
|
168
169
|
seedDir: 'CAMSTACK_SEED_SERVER_DIR',
|
|
169
170
|
},
|
|
170
171
|
logger: options.logger,
|
|
171
|
-
restartServer: (requestedBy) => {
|
|
172
|
-
writeAgentRestartMarker(options.dataDir, requestedBy);
|
|
172
|
+
restartServer: (requestedBy, update) => {
|
|
173
|
+
writeAgentRestartMarker(options.dataDir, requestedBy, update);
|
|
173
174
|
restartAgent(requestedBy);
|
|
174
175
|
},
|
|
175
176
|
dataDir: options.dataDir,
|
|
@@ -253,20 +254,15 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
|
253
254
|
}
|
|
254
255
|
}
|
|
255
256
|
exports.AgentUpdateService = AgentUpdateService;
|
|
256
|
-
function writeAgentRestartMarker(dataDir, requestedBy) {
|
|
257
|
-
const toVersion = /@([^@\s]+)$/.exec(requestedBy)?.[1];
|
|
257
|
+
function writeAgentRestartMarker(dataDir, requestedBy, update) {
|
|
258
258
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
// Same builder as the hub: an agent's "Server aggiornato" notification must
|
|
260
|
+
// name both versions too — see `core/updates/restart-marker.ts`.
|
|
261
|
+
(0, system_1.writePendingRestart)(dataDir, (0, restart_marker_js_1.buildRestartMarker)({
|
|
261
262
|
requestedAt: Date.now(),
|
|
262
263
|
requestedBy,
|
|
263
|
-
...(
|
|
264
|
-
|
|
265
|
-
packageName: '@camstack/server',
|
|
266
|
-
...(toVersion !== undefined ? { toVersion } : {}),
|
|
267
|
-
}
|
|
268
|
-
: {}),
|
|
269
|
-
});
|
|
264
|
+
...(update !== undefined ? { update } : {}),
|
|
265
|
+
}));
|
|
270
266
|
}
|
|
271
267
|
catch {
|
|
272
268
|
// Marker write must never block the restart.
|
|
@@ -47,6 +47,7 @@ const system_1 = require("@camstack/system");
|
|
|
47
47
|
const types_1 = require("@camstack/types");
|
|
48
48
|
const update_availability_emitter_js_1 = require("../update-availability-emitter.js");
|
|
49
49
|
const update_check_scheduler_js_1 = require("../updates/update-check-scheduler.js");
|
|
50
|
+
const restart_marker_js_1 = require("../updates/restart-marker.js");
|
|
50
51
|
const package_dir_utils_js_1 = require("./package-dir-utils.js");
|
|
51
52
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
52
53
|
/**
|
|
@@ -1105,28 +1106,25 @@ class AddonPackageService {
|
|
|
1105
1106
|
/**
|
|
1106
1107
|
* Gracefully restart the server process.
|
|
1107
1108
|
*
|
|
1108
|
-
* Writes
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1109
|
+
* Writes the restart marker so the post-boot service can emit
|
|
1110
|
+
* `system.restart-completed` on the next boot (powers the admin-UI
|
|
1111
|
+
* "Server restarted" toast AND the operator's "Server aggiornato"
|
|
1112
|
+
* notification). `update` is passed by `RootUpdateService` for an
|
|
1113
|
+
* update-driven restart and is the ONLY source of the version being left
|
|
1114
|
+
* behind — see `updates/restart-marker.ts`. Emits `system.restarting`
|
|
1111
1115
|
* with the same payload so subscribers can show a reconnect overlay
|
|
1112
1116
|
* immediately. Then delegates to `scheduleSelfRestart` which picks
|
|
1113
1117
|
* the right exit path (Electron `app.relaunch` vs supervisor-driven
|
|
1114
1118
|
* `process.exit`).
|
|
1115
1119
|
*/
|
|
1116
|
-
restartServer(requestedBy) {
|
|
1120
|
+
restartServer(requestedBy, update) {
|
|
1117
1121
|
this.logger.info('Server restart requested -- initiating graceful shutdown');
|
|
1118
|
-
|
|
1119
|
-
const payload = {
|
|
1120
|
-
kind: isServerUpdateRequest(requestedBy) ? 'framework-update' : 'manual',
|
|
1122
|
+
// Both versions, one builder, shared with the agent — see `restart-marker.ts`.
|
|
1123
|
+
const payload = (0, restart_marker_js_1.buildRestartMarker)({
|
|
1121
1124
|
requestedAt: Date.now(),
|
|
1122
1125
|
...(requestedBy !== undefined ? { requestedBy } : {}),
|
|
1123
|
-
...(
|
|
1124
|
-
|
|
1125
|
-
packageName: '@camstack/server',
|
|
1126
|
-
...(toVersion !== undefined ? { toVersion } : {}),
|
|
1127
|
-
}
|
|
1128
|
-
: {}),
|
|
1129
|
-
};
|
|
1126
|
+
...(update !== undefined ? { update } : {}),
|
|
1127
|
+
});
|
|
1130
1128
|
try {
|
|
1131
1129
|
(0, system_1.writePendingRestart)(this.resolveDataDir(), payload);
|
|
1132
1130
|
}
|
|
@@ -1682,22 +1680,26 @@ class AddonPackageService {
|
|
|
1682
1680
|
* re-announces them on the next good poll.
|
|
1683
1681
|
*/
|
|
1684
1682
|
publishAddonAvailability(updates, failures, nodeId) {
|
|
1683
|
+
// The hub's own sweep passes no nodeId — but its rows are about a NODE
|
|
1684
|
+
// like any other, and a list whose lines cannot be attributed is what made
|
|
1685
|
+
// three announcements look like one.
|
|
1686
|
+
const scope = nodeId ?? this.resolveNodeId();
|
|
1685
1687
|
const candidates = updates.map((update) => ({
|
|
1686
1688
|
target: 'addon',
|
|
1687
1689
|
packageName: update.name,
|
|
1688
1690
|
currentVersion: update.currentVersion,
|
|
1689
1691
|
latestVersion: update.latestVersion,
|
|
1690
|
-
|
|
1692
|
+
nodeId: scope,
|
|
1691
1693
|
}));
|
|
1692
1694
|
if (failures > 0) {
|
|
1693
1695
|
this.logger.warn('Addon update check incomplete — availability published as partial', {
|
|
1694
|
-
tags: { nodeId:
|
|
1696
|
+
tags: { nodeId: scope },
|
|
1695
1697
|
meta: { failures, published: candidates.length },
|
|
1696
1698
|
});
|
|
1697
1699
|
this.updateAvailability.publishCandidates(candidates);
|
|
1698
1700
|
return;
|
|
1699
1701
|
}
|
|
1700
|
-
this.updateAvailability.publishSnapshot('addon', candidates,
|
|
1702
|
+
this.updateAvailability.publishSnapshot('addon', candidates, scope);
|
|
1701
1703
|
}
|
|
1702
1704
|
/** Fetch npm search results for camstack addon packages (cached 5 min) */
|
|
1703
1705
|
async fetchSearchFromNpm() {
|
|
@@ -1860,15 +1862,6 @@ class AddonPackageService {
|
|
|
1860
1862
|
}
|
|
1861
1863
|
}
|
|
1862
1864
|
exports.AddonPackageService = AddonPackageService;
|
|
1863
|
-
function isServerUpdateRequest(requestedBy) {
|
|
1864
|
-
return requestedBy !== undefined && requestedBy.startsWith('server-update:');
|
|
1865
|
-
}
|
|
1866
|
-
function toVersionFromRestartReason(requestedBy) {
|
|
1867
|
-
if (requestedBy === undefined)
|
|
1868
|
-
return undefined;
|
|
1869
|
-
const match = /@([^@\s]+)$/.exec(requestedBy);
|
|
1870
|
-
return match?.[1];
|
|
1871
|
-
}
|
|
1872
1865
|
// ---------------------------------------------------------------------------
|
|
1873
1866
|
// Tarball extraction helper (exported for regression tests)
|
|
1874
1867
|
// ---------------------------------------------------------------------------
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MoleculerService = void 0;
|
|
4
|
+
exports.childOwnerToken = childOwnerToken;
|
|
4
5
|
exports.buildChildUdsManifest = buildChildUdsManifest;
|
|
5
6
|
const node_crypto_1 = require("node:crypto");
|
|
6
7
|
const system_1 = require("@camstack/system");
|
|
@@ -27,6 +28,21 @@ class MoleculerService {
|
|
|
27
28
|
* `$hub.registerNode`. Populated by `onRegisterNode` in `hubDeps`.
|
|
28
29
|
*/
|
|
29
30
|
nodeRegistry = new system_1.HubNodeRegistry();
|
|
31
|
+
/**
|
|
32
|
+
* nodeId → the owner token of the INCARNATION whose manifest currently backs
|
|
33
|
+
* that node's capability registrations.
|
|
34
|
+
*
|
|
35
|
+
* A node id is not unique over time: `resolveRunnerId` is deterministic, so a
|
|
36
|
+
* runner replaced by an addon update reconnects as the same `hub/<runner>`
|
|
37
|
+
* node. Without an incarnation the predecessor's teardown and the successor's
|
|
38
|
+
* registration are the same `(cap, addonId)` key, and the teardown wins by
|
|
39
|
+
* arriving last — `recording`, `storage-evictable` and `recording-export`
|
|
40
|
+
* were unregistered from a healthy process for 7 hours on 2026-08-20.
|
|
41
|
+
* Populated only for transports that can observe an incarnation (UDS
|
|
42
|
+
* children); remote nodes stay ownerless and behave exactly as before.
|
|
43
|
+
* See `docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md`.
|
|
44
|
+
*/
|
|
45
|
+
nodeOwners = new Map();
|
|
30
46
|
/**
|
|
31
47
|
* Fixed-period agent-readiness snapshot sweep (D8 reconcile) — repairs
|
|
32
48
|
* agent-origin readiness deltas lost while the agent stayed connected.
|
|
@@ -405,16 +421,23 @@ class MoleculerService {
|
|
|
405
421
|
const hubNodeId = this.brokerSafe.nodeID;
|
|
406
422
|
const childNodeId = `${hubNodeId}/${child.childId}`;
|
|
407
423
|
const params = buildChildUdsManifest(childNodeId, child.childId, child.caps);
|
|
408
|
-
|
|
409
|
-
|
|
424
|
+
// The UDS connection is the only thing that can tell two generations of
|
|
425
|
+
// the same runner apart — carry its incarnation into the registry as
|
|
426
|
+
// the owner of every registration this manifest makes.
|
|
427
|
+
this.onRegisterNode(params, childOwnerToken(childNodeId, child.incarnation));
|
|
428
|
+
logger.info('UDS child registered — manifest applied', {
|
|
429
|
+
meta: { nodeId: childNodeId, incarnation: child.incarnation },
|
|
430
|
+
});
|
|
410
431
|
});
|
|
411
432
|
// E1: cleanup on child disconnect — same effect as `$node.disconnected`
|
|
412
433
|
// for hub-local children. The Moleculer path stays for AGENT nodes.
|
|
413
|
-
registry.onChildGone((childId) => {
|
|
434
|
+
registry.onChildGone((childId, incarnation) => {
|
|
414
435
|
const hubNodeId = this.brokerSafe.nodeID;
|
|
415
436
|
const childNodeId = `${hubNodeId}/${childId}`;
|
|
416
|
-
logger.info('UDS child gone — removing from registry', {
|
|
417
|
-
|
|
437
|
+
logger.info('UDS child gone — removing from registry', {
|
|
438
|
+
meta: { childId, incarnation },
|
|
439
|
+
});
|
|
440
|
+
this.removeNodeFromRegistry(childNodeId, childOwnerToken(childNodeId, incarnation));
|
|
418
441
|
});
|
|
419
442
|
// B2: ingest UDS child logs into the hub's LoggingService so they appear
|
|
420
443
|
// in the LogManager / admin-UI log stream alongside broker-forwarded logs.
|
|
@@ -449,6 +472,10 @@ class MoleculerService {
|
|
|
449
472
|
// sent a $hub.registerNode manifest get cleaned up on disconnect.
|
|
450
473
|
const bridgeBus = this.broker;
|
|
451
474
|
bridgeBus.localBus.on('$node.disconnected', ({ node }) => {
|
|
475
|
+
// No owner token: this listener cannot observe WHICH incarnation died.
|
|
476
|
+
// For a hub-local child the UDS `onChildGone` is the authority (it holds
|
|
477
|
+
// the incarnation) and this call is refused if the id has been reclaimed;
|
|
478
|
+
// remote nodes register no owner at all, so they clean up as before.
|
|
452
479
|
this.removeNodeFromRegistry(node.id);
|
|
453
480
|
});
|
|
454
481
|
// D8 reverse-leg snapshot-reconcile: on every agent (re)connect, pull
|
|
@@ -682,10 +709,15 @@ class MoleculerService {
|
|
|
682
709
|
* CapabilityRegistry update is a diff (atomic replace) rather than an
|
|
683
710
|
* unconditional re-register — see `applyNodeManifest` for the rationale.
|
|
684
711
|
*/
|
|
685
|
-
onRegisterNode(params) {
|
|
712
|
+
onRegisterNode(params, owner) {
|
|
686
713
|
const previousManifest = this.nodeRegistry.getNodeManifest(params.nodeId);
|
|
714
|
+
const previousOwner = this.nodeOwners.get(params.nodeId);
|
|
687
715
|
this.nodeRegistry.registerNode(params);
|
|
688
|
-
|
|
716
|
+
if (owner === undefined)
|
|
717
|
+
this.nodeOwners.delete(params.nodeId);
|
|
718
|
+
else
|
|
719
|
+
this.nodeOwners.set(params.nodeId, owner);
|
|
720
|
+
this.applyNodeManifest(params, previousManifest, owner, previousOwner);
|
|
689
721
|
// Notify AgentRegistryService to reconcile placement for bare-ID
|
|
690
722
|
// agent nodes (no '/' = not a hub child worker, not the hub itself).
|
|
691
723
|
// The handshake is the authoritative completeness signal — the full
|
|
@@ -717,7 +749,7 @@ class MoleculerService {
|
|
|
717
749
|
* already; this method handles only `params.addons` (system caps).
|
|
718
750
|
* Native-cap wiring into device-manager is done in a later task.
|
|
719
751
|
*/
|
|
720
|
-
applyNodeManifest(params, previousManifest) {
|
|
752
|
+
applyNodeManifest(params, previousManifest, owner, previousOwner) {
|
|
721
753
|
const { nodeId, addons } = params;
|
|
722
754
|
const hubNodeId = this.brokerSafe.nodeID;
|
|
723
755
|
const isLocalChild = nodeId.startsWith(hubNodeId + '/');
|
|
@@ -763,9 +795,23 @@ class MoleculerService {
|
|
|
763
795
|
for (const [key, { addonId, capName }] of previous) {
|
|
764
796
|
if (desired.has(key))
|
|
765
797
|
continue;
|
|
766
|
-
|
|
798
|
+
// Present the token the DROPPED registration was made under — this node
|
|
799
|
+
// is dropping its own cap, and the registry refuses anything else.
|
|
800
|
+
registry.unregisterProvider(capName, registryKeyFor(addonId), previousOwner);
|
|
767
801
|
this.nodeCallFns.delete(`${nodeId}::${capName}`);
|
|
768
802
|
}
|
|
803
|
+
// ── TAKE OVER ── caps present in BOTH manifests when the node id has been
|
|
804
|
+
// reclaimed by a NEW incarnation. The registration is correct and stays
|
|
805
|
+
// (zero churn), but it is still stamped with the dead generation's token —
|
|
806
|
+
// leave it there and nothing the live generation can present would ever
|
|
807
|
+
// clean it up when IT dies. Re-stamp instead.
|
|
808
|
+
if (owner !== undefined && previousOwner !== undefined && owner !== previousOwner) {
|
|
809
|
+
for (const [key, { addonId, capName }] of desired) {
|
|
810
|
+
if (!previous.has(key))
|
|
811
|
+
continue;
|
|
812
|
+
registry.reassignProviderOwner(capName, registryKeyFor(addonId), owner);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
769
815
|
// ── REGISTER ── caps the new manifest applies that the previous one
|
|
770
816
|
// did not. Caps present in BOTH sets are left untouched — zero churn,
|
|
771
817
|
// no duplicate `registerProvider`, no spurious page/widget re-emit.
|
|
@@ -810,7 +856,7 @@ class MoleculerService {
|
|
|
810
856
|
for (const methodName of Object.keys((0, system_1.expandCapMethods)(capDef))) {
|
|
811
857
|
proxy[methodName] = (methodParams) => callFn(methodName, methodParams);
|
|
812
858
|
}
|
|
813
|
-
registry.registerProvider(capName, registryKey, proxy);
|
|
859
|
+
registry.registerProvider(capName, registryKey, proxy, owner);
|
|
814
860
|
// Local-first singleton preference (UDS regression fix). A
|
|
815
861
|
// `placement: 'any-node'` singleton (e.g. `pipeline-executor`) can
|
|
816
862
|
// register on BOTH the hub-local forked child and a remote agent.
|
|
@@ -857,7 +903,17 @@ class MoleculerService {
|
|
|
857
903
|
* Unregisters every cap the node's last manifest declared and emits
|
|
858
904
|
* synthetic readiness-down events for each.
|
|
859
905
|
*/
|
|
860
|
-
removeNodeFromRegistry(nodeId) {
|
|
906
|
+
removeNodeFromRegistry(nodeId, owner) {
|
|
907
|
+
const currentOwner = this.nodeOwners.get(nodeId);
|
|
908
|
+
if (currentOwner !== undefined && currentOwner !== owner) {
|
|
909
|
+
// The node id has been reclaimed by a newer incarnation since this
|
|
910
|
+
// teardown was scheduled. Tearing down here would unregister the LIVE
|
|
911
|
+
// process's capabilities and remove a node that is still connected.
|
|
912
|
+
this.logger.info('Node teardown ignored — node id reclaimed by a newer incarnation', {
|
|
913
|
+
meta: { nodeId, currentOwner, presentedOwner: owner ?? null },
|
|
914
|
+
});
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
861
917
|
const manifest = this.nodeRegistry.getNodeManifest(nodeId);
|
|
862
918
|
if (!manifest)
|
|
863
919
|
return; // node never sent a handshake — nothing to do
|
|
@@ -871,7 +927,7 @@ class MoleculerService {
|
|
|
871
927
|
const registryKey = isLocalChild ? addonId : `${addonId}@${nodeId}`;
|
|
872
928
|
if (registry) {
|
|
873
929
|
for (const capName of capabilities) {
|
|
874
|
-
registry.unregisterProvider(capName, registryKey);
|
|
930
|
+
registry.unregisterProvider(capName, registryKey, owner);
|
|
875
931
|
}
|
|
876
932
|
}
|
|
877
933
|
for (const capName of capabilities) {
|
|
@@ -893,6 +949,7 @@ class MoleculerService {
|
|
|
893
949
|
}
|
|
894
950
|
}
|
|
895
951
|
}
|
|
952
|
+
this.nodeOwners.delete(nodeId);
|
|
896
953
|
this.nodeRegistry.removeNode(nodeId);
|
|
897
954
|
}
|
|
898
955
|
findCallFn(nodeId, capabilityName) {
|
|
@@ -1083,6 +1140,18 @@ exports.MoleculerService = MoleculerService;
|
|
|
1083
1140
|
// ---------------------------------------------------------------------------
|
|
1084
1141
|
// Module-level helpers
|
|
1085
1142
|
// ---------------------------------------------------------------------------
|
|
1143
|
+
/**
|
|
1144
|
+
* The owner token a hub-local UDS child's capability registrations carry.
|
|
1145
|
+
*
|
|
1146
|
+
* `nodeId` alone is NOT an identity over time — a runner replaced by an addon
|
|
1147
|
+
* update reconnects under the same deterministic `hub/<runner>` id — so the
|
|
1148
|
+
* connection's incarnation is what makes the pair unique. Exported for the
|
|
1149
|
+
* regression suite: the rule is only worth anything if register and unregister
|
|
1150
|
+
* derive the token the SAME way.
|
|
1151
|
+
*/
|
|
1152
|
+
function childOwnerToken(childNodeId, incarnation) {
|
|
1153
|
+
return `${childNodeId}#${incarnation}`;
|
|
1154
|
+
}
|
|
1086
1155
|
/**
|
|
1087
1156
|
* E1: Adapt a child's UDS `ChildCapDescriptor[]` into a `RegisterNodeParams`
|
|
1088
1157
|
* that `onRegisterNode` / `applyNodeManifest` can consume.
|
|
@@ -7,10 +7,16 @@ const types_1 = require("@camstack/types");
|
|
|
7
7
|
* `latestVersion` (or first-sees a package/node that has one). Repeated polls
|
|
8
8
|
* of the same latests are silent.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* One event carries the entire currently-available list **of one node** — the
|
|
11
|
+
* scope that changed, never the union across nodes. The Notification Center
|
|
12
|
+
* turns that into "2 aggiornamenti addon disponibili su little-unraid" with
|
|
13
|
+
* that node's packages under it.
|
|
14
|
+
*
|
|
15
|
+
* The union was the shape until 2026-08-19, and on a three-node cluster one
|
|
16
|
+
* 6-hourly sweep sent the operator THREE pushes that each listed hub +
|
|
17
|
+
* MinidiGianluca + little-unraid and named no node: every scope's change
|
|
18
|
+
* re-emitted the whole held map, so the notifications differed by one line and
|
|
19
|
+
* read as identical. A list is only actionable if it belongs to somebody.
|
|
14
20
|
*
|
|
15
21
|
* A successful empty snapshot clears prior state for that scope; failed checks
|
|
16
22
|
* should not call this method.
|
|
@@ -67,22 +73,22 @@ class UpdateAvailabilityEmitter {
|
|
|
67
73
|
this.persistIfDirty();
|
|
68
74
|
if (!latestChanged)
|
|
69
75
|
return;
|
|
70
|
-
this.emitList(target);
|
|
76
|
+
this.emitList(target, scope);
|
|
71
77
|
}
|
|
72
78
|
/** Publish a partial candidate set without treating omissions as up-to-date. */
|
|
73
79
|
publishCandidates(candidates) {
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
// Only the (target, node) pairs that actually moved are announced — a
|
|
81
|
+
// partial sweep of one node must not re-announce another node's list.
|
|
82
|
+
const changed = new Map();
|
|
76
83
|
for (const candidate of candidates) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
if (!this.note(candidate))
|
|
85
|
+
continue;
|
|
86
|
+
const scope = scopeOf(candidate);
|
|
87
|
+
changed.set(`${candidate.target}:${scope}`, { target: candidate.target, scope });
|
|
80
88
|
}
|
|
81
89
|
this.persistIfDirty();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const target of targets)
|
|
85
|
-
this.emitList(target);
|
|
90
|
+
for (const { target, scope } of changed.values())
|
|
91
|
+
this.emitList(target, scope);
|
|
86
92
|
}
|
|
87
93
|
note(candidate) {
|
|
88
94
|
const key = this.key(candidate);
|
|
@@ -111,21 +117,24 @@ class UpdateAvailabilityEmitter {
|
|
|
111
117
|
held: Object.fromEntries(this.held),
|
|
112
118
|
});
|
|
113
119
|
}
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
/** Announce ONE scope's full list. `scope` is the node id, `hub` for the hub. */
|
|
121
|
+
emitList(target, scope) {
|
|
122
|
+
const list = [...this.held.values()].filter((candidate) => candidate.target === target && scopeOf(candidate) === scope);
|
|
116
123
|
if (list.length === 0)
|
|
117
124
|
return;
|
|
125
|
+
// Every row names its node, the hub's included: the row a caller published
|
|
126
|
+
// without a nodeId is still ABOUT a node, and an unlabelled line in a
|
|
127
|
+
// cluster-wide list is unreadable.
|
|
118
128
|
const packages = list.map((candidate) => ({
|
|
119
129
|
packageName: candidate.packageName,
|
|
120
130
|
currentVersion: candidate.currentVersion,
|
|
121
131
|
latestVersion: candidate.latestVersion,
|
|
122
|
-
|
|
132
|
+
nodeId: scope,
|
|
123
133
|
}));
|
|
124
|
-
const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== undefined));
|
|
125
134
|
const head = list[0];
|
|
126
135
|
this.eventBus.emit({
|
|
127
|
-
id: `update.available:${target}:${packages
|
|
128
|
-
.map((pkg) => `${pkg.
|
|
136
|
+
id: `update.available:${target}:${scope}:${packages
|
|
137
|
+
.map((pkg) => `${pkg.packageName}@${pkg.latestVersion}`)
|
|
129
138
|
.toSorted()
|
|
130
139
|
.join(',')}`,
|
|
131
140
|
timestamp: new Date(),
|
|
@@ -137,21 +146,22 @@ class UpdateAvailabilityEmitter {
|
|
|
137
146
|
currentVersion: head.currentVersion,
|
|
138
147
|
latestVersion: head.latestVersion,
|
|
139
148
|
packages,
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
nodeId: scope,
|
|
150
|
+
nodeIds: [scope],
|
|
142
151
|
},
|
|
143
152
|
});
|
|
144
153
|
}
|
|
145
154
|
key(candidate) {
|
|
146
|
-
return `${candidate.target}:${candidate
|
|
155
|
+
return `${candidate.target}:${scopeOf(candidate)}:${candidate.packageName}`;
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
exports.UpdateAvailabilityEmitter = UpdateAvailabilityEmitter;
|
|
159
|
+
/** The node an availability row belongs to. A row without one is the hub's. */
|
|
160
|
+
function scopeOf(candidate) {
|
|
161
|
+
return candidate.nodeId ?? 'hub';
|
|
162
|
+
}
|
|
150
163
|
function stampNode(candidate, nodeId) {
|
|
151
164
|
if (nodeId === undefined || candidate.nodeId !== undefined)
|
|
152
165
|
return candidate;
|
|
153
166
|
return { ...candidate, nodeId };
|
|
154
167
|
}
|
|
155
|
-
function unique(values) {
|
|
156
|
-
return [...new Set(values)];
|
|
157
|
-
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildRestartMarker = buildRestartMarker;
|
|
4
|
+
exports.isServerUpdateReason = isServerUpdateReason;
|
|
5
|
+
exports.versionFromReason = versionFromReason;
|
|
6
|
+
/** Reason-string prefix every `RootUpdateService`-driven restart carries. */
|
|
7
|
+
const SERVER_UPDATE_PREFIX = 'server-update:';
|
|
8
|
+
/** The root package a version-less `server-update:` reason is about. */
|
|
9
|
+
const ROOT_PACKAGE_NAME = '@camstack/server';
|
|
10
|
+
function buildRestartMarker(input) {
|
|
11
|
+
const { requestedBy, update, requestedAt } = input;
|
|
12
|
+
const toVersion = update?.toVersion ?? versionFromReason(requestedBy);
|
|
13
|
+
const isUpdate = update !== undefined || (isServerUpdateReason(requestedBy) && toVersion !== undefined);
|
|
14
|
+
if (!isUpdate) {
|
|
15
|
+
return {
|
|
16
|
+
kind: 'manual',
|
|
17
|
+
requestedAt,
|
|
18
|
+
...(requestedBy !== undefined ? { requestedBy } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
kind: 'framework-update',
|
|
23
|
+
requestedAt,
|
|
24
|
+
...(requestedBy !== undefined ? { requestedBy } : {}),
|
|
25
|
+
packageName: update?.packageName ?? ROOT_PACKAGE_NAME,
|
|
26
|
+
...(update?.fromVersion !== undefined ? { fromVersion: update.fromVersion } : {}),
|
|
27
|
+
...(toVersion !== undefined ? { toVersion } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function isServerUpdateReason(requestedBy) {
|
|
31
|
+
return requestedBy !== undefined && requestedBy.startsWith(SERVER_UPDATE_PREFIX);
|
|
32
|
+
}
|
|
33
|
+
/** `server-update: @camstack/server@1.2.138` → `1.2.138`; `… manual restart` → none. */
|
|
34
|
+
function versionFromReason(requestedBy) {
|
|
35
|
+
if (requestedBy === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
return /@([^@\s]+)$/.exec(requestedBy)?.[1];
|
|
38
|
+
}
|
package/dist/manual-boot.js
CHANGED
|
@@ -238,7 +238,7 @@ async function bootManual(opts) {
|
|
|
238
238
|
// AddonPackageService.restartServer for the marker + reconnect toast.
|
|
239
239
|
const serverUpdateService = new server_update_service_1.ServerUpdateService({
|
|
240
240
|
logger: loggingService.createLogger('ServerUpdate'),
|
|
241
|
-
restartServer: (requestedBy) => addonPackageService.restartServer(requestedBy),
|
|
241
|
+
restartServer: (requestedBy, update) => addonPackageService.restartServer(requestedBy, update),
|
|
242
242
|
eventBus: eventBusService,
|
|
243
243
|
updateAvailabilityStore: new update_availability_store_js_1.FileUpdateAvailabilityStore(availabilityDataDir, 'server-update', loggingService.createLogger('UpdateAvailability')),
|
|
244
244
|
});
|
|
@@ -1216,7 +1216,13 @@ var require_dist = __commonJS({
|
|
|
1216
1216
|
fromVersion: runningVersion
|
|
1217
1217
|
}
|
|
1218
1218
|
});
|
|
1219
|
-
this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}
|
|
1219
|
+
this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`, {
|
|
1220
|
+
packageName: this.spec.packageName,
|
|
1221
|
+
...runningVersion !== null ? {
|
|
1222
|
+
fromVersion: runningVersion
|
|
1223
|
+
} : {},
|
|
1224
|
+
toVersion: target
|
|
1225
|
+
});
|
|
1220
1226
|
return {
|
|
1221
1227
|
accepted: true,
|
|
1222
1228
|
targetVersion: target,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.140",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
36
|
+
"@camstack/addon-admin-ui": "1.2.84",
|
|
37
37
|
"@camstack/addon-agent-ui": "1.2.24",
|
|
38
38
|
"@camstack/addon-auth": "1.2.26",
|
|
39
39
|
"@camstack/addon-decoder-nodeav": "1.2.22",
|
|
40
40
|
"@camstack/addon-notifiers": "1.2.27",
|
|
41
|
-
"@camstack/addon-pipeline": "1.2.
|
|
42
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
43
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
41
|
+
"@camstack/addon-pipeline": "1.2.103",
|
|
42
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.85",
|
|
43
|
+
"@camstack/addon-post-analysis": "1.2.103",
|
|
44
44
|
"@camstack/sdk": "1.2.25",
|
|
45
|
-
"@camstack/system": "1.2.
|
|
46
|
-
"@camstack/types": "1.2.
|
|
47
|
-
"@camstack/ui-library": "1.2.
|
|
45
|
+
"@camstack/system": "1.2.111",
|
|
46
|
+
"@camstack/types": "1.2.92",
|
|
47
|
+
"@camstack/ui-library": "1.2.63",
|
|
48
48
|
"@fastify/compress": "^9.0.0",
|
|
49
49
|
"@fastify/cookie": "^11.0.2",
|
|
50
50
|
"@fastify/cors": "^11.2.0",
|