@camstack/server 1.2.353 → 1.2.354

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.
@@ -83,9 +83,13 @@ var require_dist = __commonJS({
83
83
  PENDING_ROOT_SWAP_FILE: /* @__PURE__ */ __name(() => PENDING_ROOT_SWAP_FILE, "PENDING_ROOT_SWAP_FILE"),
84
84
  REQUIRED_NATIVE_PACKAGES: /* @__PURE__ */ __name(() => REQUIRED_NATIVE_PACKAGES, "REQUIRED_NATIVE_PACKAGES"),
85
85
  RESTART_INTENT_FILE: /* @__PURE__ */ __name(() => RESTART_INTENT_FILE, "RESTART_INTENT_FILE"),
86
+ ROLLBACK_UNSUPPORTED_REASON: /* @__PURE__ */ __name(() => ROLLBACK_UNSUPPORTED_REASON, "ROLLBACK_UNSUPPORTED_REASON"),
86
87
  RootUpdateService: /* @__PURE__ */ __name(() => RootUpdateService, "RootUpdateService"),
87
88
  SERVER_ROOT_DIRNAME: /* @__PURE__ */ __name(() => SERVER_ROOT_DIRNAME, "SERVER_ROOT_DIRNAME"),
88
89
  SERVER_ROOT_STATE_FILE: /* @__PURE__ */ __name(() => SERVER_ROOT_STATE_FILE, "SERVER_ROOT_STATE_FILE"),
90
+ STAGING_ATTEMPTS: /* @__PURE__ */ __name(() => STAGING_ATTEMPTS, "STAGING_ATTEMPTS"),
91
+ STAGING_FAILED_REASON: /* @__PURE__ */ __name(() => STAGING_FAILED_REASON, "STAGING_FAILED_REASON"),
92
+ STAGING_RETRY_DELAYS_MS: /* @__PURE__ */ __name(() => STAGING_RETRY_DELAYS_MS, "STAGING_RETRY_DELAYS_MS"),
89
93
  applyPendingRootSwap: /* @__PURE__ */ __name(() => applyPendingRootSwap, "applyPendingRootSwap"),
90
94
  assessImageContract: /* @__PURE__ */ __name(() => assessImageContract, "assessImageContract"),
91
95
  clearPendingRootSwap: /* @__PURE__ */ __name(() => clearPendingRootSwap, "clearPendingRootSwap"),
@@ -105,6 +109,7 @@ var require_dist = __commonJS({
105
109
  isDevChannelVersion: /* @__PURE__ */ __name(() => isDevChannelVersion, "isDevChannelVersion"),
106
110
  isHostExternal: /* @__PURE__ */ __name(() => isHostExternal, "isHostExternal"),
107
111
  isServerRootState: /* @__PURE__ */ __name(() => isServerRootState, "isServerRootState"),
112
+ isTransientNpmNetworkError: /* @__PURE__ */ __name(() => isTransientNpmNetworkError, "isTransientNpmNetworkError"),
108
113
  migrateToSingleCopy: /* @__PURE__ */ __name(() => migrateToSingleCopy, "migrateToSingleCopy"),
109
114
  minNodeMajorOf: /* @__PURE__ */ __name(() => minNodeMajorOf, "minNodeMajorOf"),
110
115
  pendingRootSwapPath: /* @__PURE__ */ __name(() => pendingRootSwapPath, "pendingRootSwapPath"),
@@ -1071,6 +1076,18 @@ var require_dist = __commonJS({
1071
1076
  __name(buildNpmRegistryArgs, "buildNpmRegistryArgs");
1072
1077
  var NPM_VIEW_TIMEOUT_MS = 2e4;
1073
1078
  var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
1079
+ var STAGING_ATTEMPTS = 3;
1080
+ var STAGING_RETRY_DELAYS_MS = [
1081
+ 5e3,
1082
+ 15e3
1083
+ ];
1084
+ var TRANSIENT_NPM_ERROR_RE = /npm error code (?:ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|EPIPE|E50[234]|E429)\b|npm error network/i;
1085
+ function isTransientNpmNetworkError(message) {
1086
+ return TRANSIENT_NPM_ERROR_RE.test(message);
1087
+ }
1088
+ __name(isTransientNpmNetworkError, "isTransientNpmNetworkError");
1089
+ var STAGING_FAILED_REASON = "staging-failed";
1090
+ var ROLLBACK_UNSUPPORTED_REASON = "rollback-unsupported";
1074
1091
  var CLOSURE_ALLOW_SCRIPTS = {
1075
1092
  "@camstack/server": true,
1076
1093
  "better-sqlite3": true,
@@ -1131,8 +1148,20 @@ var require_dist = __commonJS({
1131
1148
  env;
1132
1149
  readWrapperProbesFn;
1133
1150
  now;
1151
+ sleep;
1134
1152
  runningPackageJsonPath;
1135
1153
  workspaceProbeDir;
1154
+ /**
1155
+ * The version THIS PROCESS loaded — read once, here, from the package.json
1156
+ * of the copy the code came from, and never again. The file is the only
1157
+ * place the fact exists in a form the process can read, and the moment of
1158
+ * construction is the only moment it is guaranteed to describe what is in
1159
+ * memory: the starter swaps `current/` BEFORE loading the entry, and nothing
1160
+ * else may move it, so a later read that disagrees is a disk that changed
1161
+ * under a live process — reported through `installedVersion`, never folded
1162
+ * into this number.
1163
+ */
1164
+ loadedVersion;
1136
1165
  checkCache = null;
1137
1166
  inFlight = "idle";
1138
1167
  constructor(options) {
@@ -1160,8 +1189,22 @@ var require_dist = __commonJS({
1160
1189
  this.env = options.env ?? process.env;
1161
1190
  this.readWrapperProbesFn = options.readWrapperProbes ?? readWrapperProbes;
1162
1191
  this.now = options.now ?? Date.now;
1192
+ this.sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
1193
+ setTimeout(resolve3, ms);
1194
+ }));
1163
1195
  this.runningPackageJsonPath = options.runningPackageJsonPath;
1164
1196
  this.workspaceProbeDir = options.workspaceProbeDir;
1197
+ this.loadedVersion = readPackageVersion(this.runningPackageJsonPath);
1198
+ const bootedVersion = this.env[this.envNames.activeVersion];
1199
+ if (bootedVersion !== void 0 && this.loadedVersion !== null && bootedVersion !== this.loadedVersion) {
1200
+ this.logger.warn("running root package disagrees with the version the starter booted", {
1201
+ tags: this.logTags,
1202
+ meta: {
1203
+ loadedVersion: this.loadedVersion,
1204
+ bootedVersion
1205
+ }
1206
+ });
1207
+ }
1165
1208
  }
1166
1209
  // ── Status ────────────────────────────────────────────────────────────
1167
1210
  resolveBootMode() {
@@ -1169,8 +1212,36 @@ var require_dist = __commonJS({
1169
1212
  if (raw === "workspace" || raw === "baked" || raw === "data-root") return raw;
1170
1213
  return detectWorkspaceRoot(this.workspaceProbeDir) !== null ? "workspace" : "baked";
1171
1214
  }
1215
+ /** What THIS process loaded — see {@link loadedVersion}. Never a disk read. */
1172
1216
  runningVersion() {
1173
- return readPackageVersion(this.runningPackageJsonPath);
1217
+ return this.loadedVersion;
1218
+ }
1219
+ /**
1220
+ * What is in the data-root `current/` copy RIGHT NOW — the version the next
1221
+ * boot would load if nothing is staged. Only answered on a `data-root` boot:
1222
+ * on a workspace boot there is no such copy, and on a `baked` boot the
1223
+ * starter already judged `current/` unloadable (or absent), so a version
1224
+ * read from it would claim "runs after restart" about code that will not.
1225
+ */
1226
+ installedVersion() {
1227
+ if (this.resolveBootMode() !== "data-root") return null;
1228
+ return readPackageVersion(path6.join(rootPackageDir2(currentDir(this.rootDir()), this.spec), "package.json"));
1229
+ }
1230
+ /**
1231
+ * The three facts a restart question needs, read together so no surface can
1232
+ * quote one without the others: what is staged, what is on disk, and whether
1233
+ * the code that would run after a restart differs from the code running now.
1234
+ */
1235
+ restartFacts() {
1236
+ const pendingVersion = readPendingRootSwap(this.rootDir())?.version ?? null;
1237
+ const installedVersion = this.installedVersion();
1238
+ const running = this.runningVersion();
1239
+ const restartRequired = pendingVersion !== null || installedVersion !== null && running !== null && installedVersion !== running;
1240
+ return {
1241
+ pendingVersion,
1242
+ installedVersion,
1243
+ restartRequired
1244
+ };
1174
1245
  }
1175
1246
  /** The running root package's identity — feeds registerNode / topology rows. */
1176
1247
  getRunningRootPackage() {
@@ -1207,16 +1278,26 @@ var require_dist = __commonJS({
1207
1278
  if (readPendingRootSwap(this.rootDir()) !== null) return "pending-restart";
1208
1279
  return "idle";
1209
1280
  }
1281
+ /**
1282
+ * `latest` is an update this node can still TAKE: newer than what runs, and
1283
+ * not the version already staged here — that one is a restart away, and
1284
+ * re-applying it is refused (`already-staged`).
1285
+ */
1286
+ isUpdateAvailable(latestVersion, runningVersion, pendingVersion) {
1287
+ return latestVersion !== null && runningVersion !== null && latestVersion !== pendingVersion && compareSemver(latestVersion, runningVersion) > 0;
1288
+ }
1210
1289
  async getServerPackageStatus() {
1211
1290
  const runningVersion = this.runningVersion();
1212
1291
  const latestVersion = this.checkCache?.latestVersion ?? null;
1213
- const pending = readPendingRootSwap(this.rootDir());
1214
- const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
1292
+ const { pendingVersion, installedVersion, restartRequired } = this.restartFacts();
1293
+ const updateAvailable = this.isUpdateAvailable(latestVersion, runningVersion, pendingVersion);
1215
1294
  return {
1216
1295
  packageName: this.spec.packageName,
1217
1296
  runningVersion,
1218
1297
  nodeRuntimeVersion: process.versions.node,
1219
1298
  activeVersion: this.env[this.envNames.activeVersion] ?? runningVersion,
1299
+ installedVersion,
1300
+ restartRequired,
1220
1301
  // Single-copy model: no N-1 retention, no rollback record.
1221
1302
  previousVersion: null,
1222
1303
  seedVersion: this.seedVersion(),
@@ -1224,7 +1305,7 @@ var require_dist = __commonJS({
1224
1305
  updateAvailable,
1225
1306
  bootMode: this.resolveBootMode(),
1226
1307
  updateState: this.updateState(),
1227
- pendingVersion: pending?.version ?? null,
1308
+ pendingVersion,
1228
1309
  rolledBack: null,
1229
1310
  stateFileCorrupt: false,
1230
1311
  lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null,
@@ -1246,6 +1327,7 @@ var require_dist = __commonJS({
1246
1327
  // ── Check ─────────────────────────────────────────────────────────────
1247
1328
  async checkServerUpdate() {
1248
1329
  const runningVersion = this.runningVersion();
1330
+ const { pendingVersion, restartRequired } = this.restartFacts();
1249
1331
  if (this.inFlight !== "idle") {
1250
1332
  return {
1251
1333
  packageName: this.spec.packageName,
@@ -1253,7 +1335,9 @@ var require_dist = __commonJS({
1253
1335
  latestVersion: this.checkCache?.latestVersion ?? null,
1254
1336
  updateAvailable: false,
1255
1337
  checkedAtMs: this.checkCache?.checkedAtMs ?? this.now(),
1256
- error: `busy: ${this.inFlight}`
1338
+ error: `busy: ${this.inFlight}`,
1339
+ pendingVersion,
1340
+ restartRequired
1257
1341
  };
1258
1342
  }
1259
1343
  this.inFlight = "checking";
@@ -1288,14 +1372,16 @@ var require_dist = __commonJS({
1288
1372
  }
1289
1373
  });
1290
1374
  }
1291
- const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
1375
+ const updateAvailable = this.isUpdateAvailable(latestVersion, runningVersion, pendingVersion);
1292
1376
  return {
1293
1377
  packageName: this.spec.packageName,
1294
1378
  runningVersion,
1295
1379
  latestVersion,
1296
1380
  updateAvailable,
1297
1381
  checkedAtMs: this.checkCache.checkedAtMs,
1298
- error: null
1382
+ error: null,
1383
+ pendingVersion,
1384
+ restartRequired
1299
1385
  };
1300
1386
  } catch (err) {
1301
1387
  const message = err instanceof Error ? err.message : String(err);
@@ -1315,7 +1401,9 @@ var require_dist = __commonJS({
1315
1401
  latestVersion: this.checkCache.latestVersion,
1316
1402
  updateAvailable: false,
1317
1403
  checkedAtMs: this.checkCache.checkedAtMs,
1318
- error: message
1404
+ error: message,
1405
+ pendingVersion,
1406
+ restartRequired
1319
1407
  };
1320
1408
  } finally {
1321
1409
  this.inFlight = "idle";
@@ -1345,7 +1433,12 @@ var require_dist = __commonJS({
1345
1433
  accepted: false,
1346
1434
  targetVersion,
1347
1435
  restarting: false,
1348
- message
1436
+ message,
1437
+ // "Already running" is not a gate saying no — it is the work already
1438
+ // done. Every other reason is a refusal the caller may need to clear.
1439
+ outcome: reason === "already-running" ? "already-current" : "refused",
1440
+ reason,
1441
+ error: null
1349
1442
  };
1350
1443
  }
1351
1444
  async applyServerUpdate(input) {
@@ -1388,21 +1481,17 @@ var require_dist = __commonJS({
1388
1481
  this.inFlight = "staging";
1389
1482
  let stagingPath;
1390
1483
  try {
1391
- stagingPath = await this.stageClosure(target);
1484
+ stagingPath = await this.stageClosureWithRetry(target);
1392
1485
  } catch (err) {
1393
1486
  const message = err instanceof Error ? err.message : String(err);
1394
- this.logger.error("root package update staging failed", {
1395
- tags: this.logTags,
1396
- meta: {
1397
- targetVersion: target,
1398
- error: message
1399
- }
1400
- });
1401
1487
  return {
1402
1488
  accepted: false,
1403
1489
  targetVersion: target,
1404
1490
  restarting: false,
1405
- message: `Staging failed: ${message}`
1491
+ message: `Staging failed: ${message}`,
1492
+ outcome: "failed",
1493
+ reason: STAGING_FAILED_REASON,
1494
+ error: message
1406
1495
  };
1407
1496
  } finally {
1408
1497
  this.inFlight = "idle";
@@ -1431,10 +1520,59 @@ var require_dist = __commonJS({
1431
1520
  accepted: true,
1432
1521
  targetVersion: target,
1433
1522
  restarting: true,
1434
- message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (single automatic reboot).`
1523
+ message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (single automatic reboot). Until that restart completes the node still runs ${runningVersion ?? "the previous version"}.`,
1524
+ outcome: "staged",
1525
+ reason: null,
1526
+ error: null
1435
1527
  };
1436
1528
  }
1437
1529
  /**
1530
+ * {@link stageClosure}, attempted up to {@link STAGING_ATTEMPTS} times when
1531
+ * the failure is a transient registry network error and ONLY then. Every
1532
+ * attempt is logged with its ordinal; the final failure is logged at ERROR
1533
+ * with the attempt count, so a log that shows one line shows the whole
1534
+ * story. Throws the last error when every attempt failed.
1535
+ */
1536
+ async stageClosureWithRetry(target) {
1537
+ let lastError = new Error("staging never ran");
1538
+ for (let attempt = 1; attempt <= STAGING_ATTEMPTS; attempt += 1) {
1539
+ try {
1540
+ return await this.stageClosure(target);
1541
+ } catch (err) {
1542
+ lastError = err;
1543
+ const message = err instanceof Error ? err.message : String(err);
1544
+ const transient = isTransientNpmNetworkError(message);
1545
+ const delayMs = STAGING_RETRY_DELAYS_MS[attempt - 1];
1546
+ const willRetry = transient && attempt < STAGING_ATTEMPTS && delayMs !== void 0;
1547
+ if (!willRetry) {
1548
+ this.logger.error("root package update staging failed", {
1549
+ tags: this.logTags,
1550
+ meta: {
1551
+ targetVersion: target,
1552
+ attempt,
1553
+ attempts: STAGING_ATTEMPTS,
1554
+ transient,
1555
+ error: message
1556
+ }
1557
+ });
1558
+ break;
1559
+ }
1560
+ this.logger.warn("root package update staging failed \u2014 transient network error, retrying", {
1561
+ tags: this.logTags,
1562
+ meta: {
1563
+ targetVersion: target,
1564
+ attempt,
1565
+ attempts: STAGING_ATTEMPTS,
1566
+ delayMs,
1567
+ error: message
1568
+ }
1569
+ });
1570
+ await this.sleep(delayMs);
1571
+ }
1572
+ }
1573
+ throw lastError;
1574
+ }
1575
+ /**
1438
1576
  * npm-install the target closure into a SAME-FS `.staging-*` dir, then
1439
1577
  * validate it (entry + version + native prebuilds). Returns the staging dir
1440
1578
  * path for the pending-root-swap marker; the STARTER swaps it into `current`
@@ -1583,7 +1721,10 @@ var require_dist = __commonJS({
1583
1721
  accepted: false,
1584
1722
  targetVersion: null,
1585
1723
  restarting: false,
1586
- message: "Rollback is not supported in the single-copy model (no retained previous version). Recover by re-applying a known-good version via applyServerUpdate, or reinstall the image seed."
1724
+ message: "Rollback is not supported in the single-copy model (no retained previous version). Recover by re-applying a known-good version via applyServerUpdate, or reinstall the image seed.",
1725
+ outcome: "refused",
1726
+ reason: ROLLBACK_UNSUPPORTED_REASON,
1727
+ error: null
1587
1728
  };
1588
1729
  }
1589
1730
  // ── Plain restart ─────────────────────────────────────────────────────
@@ -1608,7 +1749,10 @@ var require_dist = __commonJS({
1608
1749
  accepted: false,
1609
1750
  targetVersion,
1610
1751
  restarting: false,
1611
- message
1752
+ message,
1753
+ outcome: "refused",
1754
+ reason,
1755
+ error: null
1612
1756
  };
1613
1757
  }
1614
1758
  restartNode() {
@@ -1630,7 +1774,10 @@ var require_dist = __commonJS({
1630
1774
  accepted: true,
1631
1775
  targetVersion: runningVersion,
1632
1776
  restarting: true,
1633
- message: "Restarting the node \u2014 the supervisor relaunches it on the same version."
1777
+ message: "Restarting the node \u2014 the supervisor relaunches it on the same version.",
1778
+ outcome: "restart-scheduled",
1779
+ reason: null,
1780
+ error: null
1634
1781
  };
1635
1782
  }
1636
1783
  // ── Boot health confirmation ──────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.353",
3
+ "version": "1.2.354",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -35,17 +35,17 @@
35
35
  ]
36
36
  },
37
37
  "dependencies": {
38
- "@camstack/addon-admin-ui": "1.2.242",
38
+ "@camstack/addon-admin-ui": "1.2.243",
39
39
  "@camstack/addon-agent-ui": "1.2.131",
40
40
  "@camstack/addon-auth": "1.2.123",
41
41
  "@camstack/addon-decoder-nodeav": "1.2.115",
42
42
  "@camstack/addon-notifiers": "1.2.119",
43
- "@camstack/addon-pipeline": "1.2.271",
44
- "@camstack/addon-pipeline-orchestrator": "1.2.230",
45
- "@camstack/addon-post-analysis": "1.2.270",
43
+ "@camstack/addon-pipeline": "1.2.272",
44
+ "@camstack/addon-pipeline-orchestrator": "1.2.231",
45
+ "@camstack/addon-post-analysis": "1.2.271",
46
46
  "@camstack/sdk": "1.2.118",
47
- "@camstack/system": "1.2.274",
48
- "@camstack/types": "1.2.232",
47
+ "@camstack/system": "1.2.275",
48
+ "@camstack/types": "1.2.233",
49
49
  "@camstack/ui-library": "1.2.189",
50
50
  "@fastify/compress": "^9.0.0",
51
51
  "@fastify/cookie": "^11.0.2",