@camstack/server 1.2.137 → 1.2.139

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.
@@ -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
- (0, system_1.writePendingRestart)(dataDir, {
260
- kind: requestedBy.startsWith('server-update:') ? 'framework-update' : 'manual',
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
- ...(requestedBy.startsWith('server-update:')
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 a `manual` restart marker so the post-boot service can
1109
- * emit `system.restart-completed` on the next boot (powers the
1110
- * admin-UI "Server restarted" toast). Emits `system.restarting`
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
- const toVersion = toVersionFromRestartReason(requestedBy);
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
- ...(isServerUpdateRequest(requestedBy)
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
- ...(nodeId !== undefined ? { nodeId } : {}),
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: nodeId ?? 'hub' },
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, nodeId);
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
  // ---------------------------------------------------------------------------
@@ -42,17 +42,6 @@ function createFilteredIntegrationRegistry(deps) {
42
42
  // key is bare (D72). Normalise on the way in, or the create guard rejects
43
43
  // exactly the builtins that legitimately declare an integration.
44
44
  const isInstalled = (addonId) => deps.isAddonInstalled((0, types_1.bareAddonId)(addonId));
45
- // Integration ids whose addon IS installed. Cached per wrapper — these
46
- // wrappers are built per `getIntegrationRegistry()` call, so the cache never
47
- // outlives a request path.
48
- let activeIntegrationIds = null;
49
- const ensureActiveIds = async () => {
50
- if (activeIntegrationIds)
51
- return activeIntegrationIds;
52
- const all = await raw.listIntegrations();
53
- activeIntegrationIds = new Set(all.filter((i) => isInstalled(i.addonId)).map((i) => i.id));
54
- return activeIntegrationIds;
55
- };
56
45
  /** The suppression log line. Only ever called when the row EXISTS. */
57
46
  const logHidden = (integration, phase) => {
58
47
  logger.warn('integration hidden — its addon is not installed, so this read answers null even though the row exists', {
@@ -106,37 +95,5 @@ function createFilteredIntegrationRegistry(deps) {
106
95
  getIntegrationSettings: (id) => raw.getIntegrationSettings(id),
107
96
  setIntegrationSetting: (id, key, value) => raw.setIntegrationSetting(id, key, value),
108
97
  setIntegrationSettings: (id, settings) => raw.setIntegrationSettings(id, settings),
109
- // Devices: filter out devices belonging to hidden integrations
110
- createDevice: (input) => raw.createDevice(input),
111
- getDevice: async (id) => {
112
- const d = await raw.getDevice(id);
113
- if (!d)
114
- return null;
115
- const ids = await ensureActiveIds();
116
- return ids.has(d.integrationId) ? d : null;
117
- },
118
- getDeviceByStableId: async (stableId) => {
119
- const d = await raw.getDeviceByStableId(stableId);
120
- if (!d)
121
- return null;
122
- const ids = await ensureActiveIds();
123
- return ids.has(d.integrationId) ? d : null;
124
- },
125
- listDevices: async (integrationId) => {
126
- const devices = await raw.listDevices(integrationId);
127
- const ids = await ensureActiveIds();
128
- return devices.filter((d) => ids.has(d.integrationId));
129
- },
130
- listCameras: async () => {
131
- const cameras = await raw.listCameras();
132
- const ids = await ensureActiveIds();
133
- return cameras.filter((d) => ids.has(d.integrationId));
134
- },
135
- updateDevice: (id, updates) => raw.updateDevice(id, updates),
136
- deleteDevice: (id) => raw.deleteDevice(id),
137
- // Device settings: passthrough
138
- getDeviceSettings: (id) => raw.getDeviceSettings(id),
139
- setDeviceSetting: (id, key, value) => raw.setDeviceSetting(id, key, value),
140
- setDeviceSettings: (id, settings) => raw.setDeviceSettings(id, settings),
141
98
  };
142
99
  }
@@ -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
- * Addon snapshots emit ONE event carrying the **entire** currently-available
11
- * list. Server snapshots do the same across nodes, so the Notification Center
12
- * can say "1.0.3 is available on hub and little-unraid" instead of one push
13
- * per package or per node.
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
- let latestChanged = false;
75
- const targets = new Set();
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
- targets.add(candidate.target);
78
- if (this.note(candidate))
79
- latestChanged = true;
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
- if (!latestChanged)
83
- return;
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
- emitList(target) {
115
- const list = [...this.held.values()].filter((candidate) => candidate.target === target);
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
- ...(candidate.nodeId !== undefined ? { nodeId: candidate.nodeId } : {}),
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.nodeId ?? 'hub'}:${pkg.packageName}@${pkg.latestVersion}`)
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
- ...(head.nodeId !== undefined ? { nodeId: head.nodeId } : {}),
141
- ...(nodeIds.length > 0 ? { nodeIds } : {}),
149
+ nodeId: scope,
150
+ nodeIds: [scope],
142
151
  },
143
152
  });
144
153
  }
145
154
  key(candidate) {
146
- return `${candidate.target}:${candidate.nodeId ?? 'hub'}:${candidate.packageName}`;
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
+ }
@@ -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.137",
3
+ "version": "1.2.139",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -33,19 +33,18 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@camstack/addon-admin-ui": "1.2.83",
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.100",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.83",
43
- "@camstack/addon-post-analysis": "1.2.100",
41
+ "@camstack/addon-pipeline": "1.2.102",
42
+ "@camstack/addon-pipeline-orchestrator": "1.2.84",
43
+ "@camstack/addon-post-analysis": "1.2.102",
44
44
  "@camstack/sdk": "1.2.25",
45
- "@camstack/shm-ring": "1.1.22",
46
- "@camstack/system": "1.2.108",
47
- "@camstack/types": "1.2.90",
48
- "@camstack/ui-library": "1.2.62",
45
+ "@camstack/system": "1.2.110",
46
+ "@camstack/types": "1.2.92",
47
+ "@camstack/ui-library": "1.2.63",
49
48
  "@fastify/compress": "^9.0.0",
50
49
  "@fastify/cookie": "^11.0.2",
51
50
  "@fastify/cors": "^11.2.0",