@camstack/server 1.2.88 → 1.2.90
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/addons-custom.router.js +99 -0
- package/dist/api/core/bulk-update-coordinator.js +229 -0
- package/dist/api/core/cap-providers.js +2 -2
- package/dist/api/core/settings-backend.router.js +121 -0
- package/dist/api/static/spa-static.js +10 -1
- package/dist/boot/resume-framework-swap.js +119 -0
- package/dist/core/addon/addon-package.service.js +30 -3
- package/dist/core/addon/framework-live-sync.js +344 -0
- package/dist/core/server-update/server-update.service.js +25 -0
- package/dist/core/update-availability-emitter.js +57 -0
- package/dist/launcher-framework-swap.js +408 -0
- package/dist/main.js +154 -197
- package/dist/manual-boot.js +1 -0
- package/dist/request-framework-swap.js +41 -0
- package/dist/server-root/boot-plan.js +110 -0
- package/dist/server-root/semver-compare.js +45 -0
- package/dist/server-root/server-root-state.js +220 -0
- package/dist/server-root/workspace-detect.js +73 -0
- package/package.json +14 -14
|
@@ -0,0 +1,408 @@
|
|
|
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.writePendingFrameworkSwap = writePendingFrameworkSwap;
|
|
37
|
+
exports.applyPendingFrameworkSwap = applyPendingFrameworkSwap;
|
|
38
|
+
exports.rollbackUnconfirmedFrameworkSwap = rollbackUnconfirmedFrameworkSwap;
|
|
39
|
+
exports.confirmFrameworkSwapHealthy = confirmFrameworkSwapHealthy;
|
|
40
|
+
/**
|
|
41
|
+
* Launcher zero-dep framework swap/rollback helpers.
|
|
42
|
+
*
|
|
43
|
+
* These run before @camstack/system is imported — zero @camstack imports.
|
|
44
|
+
* Only node:fs + node:path. All writes are atomic (tmpfile+rename).
|
|
45
|
+
* All reads tolerate missing/corrupt files (no-op result).
|
|
46
|
+
*/
|
|
47
|
+
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
49
|
+
const lifecycle_journal_path_js_1 = require("./lifecycle-journal-path.js");
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Marker file names
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
const PENDING_SWAP_MARKER = '.pending-framework-swap.json';
|
|
54
|
+
const SWAP_CONFIRM_MARKER = '.framework-swap-confirm.json';
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Helpers
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/** Write a JSON file atomically via a tmp sibling + rename. */
|
|
59
|
+
function writeJsonAtomic(filePath, value) {
|
|
60
|
+
const tmp = `${filePath}.tmp`;
|
|
61
|
+
fs.writeFileSync(tmp, JSON.stringify(value), 'utf-8');
|
|
62
|
+
fs.renameSync(tmp, filePath);
|
|
63
|
+
}
|
|
64
|
+
/** Read + parse JSON, returning null on any error (missing / corrupt). */
|
|
65
|
+
function readJsonSafe(filePath) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Validate that a parsed value matches the PendingSwapMarker shape. */
|
|
74
|
+
function isPendingSwapMarker(value) {
|
|
75
|
+
if (typeof value !== 'object' || value === null)
|
|
76
|
+
return false;
|
|
77
|
+
const v = value;
|
|
78
|
+
if (typeof v['jobId'] !== 'string')
|
|
79
|
+
return false;
|
|
80
|
+
if (typeof v['taskId'] !== 'string')
|
|
81
|
+
return false;
|
|
82
|
+
if (!Array.isArray(v['packages']))
|
|
83
|
+
return false;
|
|
84
|
+
for (const pkg of v['packages']) {
|
|
85
|
+
if (typeof pkg !== 'object' || pkg === null)
|
|
86
|
+
return false;
|
|
87
|
+
const p = pkg;
|
|
88
|
+
if (typeof p['name'] !== 'string')
|
|
89
|
+
return false;
|
|
90
|
+
if (typeof p['stagedPath'] !== 'string')
|
|
91
|
+
return false;
|
|
92
|
+
if (typeof p['backupPath'] !== 'string')
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (typeof v['requestedAtMs'] !== 'number')
|
|
96
|
+
return false;
|
|
97
|
+
if (v['schemaVersion'] !== 1)
|
|
98
|
+
return false;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/** Validate that a parsed value matches the SwapConfirmMarker shape. */
|
|
102
|
+
function isSwapConfirmMarker(value) {
|
|
103
|
+
if (typeof value !== 'object' || value === null)
|
|
104
|
+
return false;
|
|
105
|
+
const v = value;
|
|
106
|
+
if (typeof v['jobId'] !== 'string')
|
|
107
|
+
return false;
|
|
108
|
+
if (typeof v['taskId'] !== 'string')
|
|
109
|
+
return false;
|
|
110
|
+
if (!Array.isArray(v['backups']))
|
|
111
|
+
return false;
|
|
112
|
+
for (const b of v['backups']) {
|
|
113
|
+
if (typeof b !== 'object' || b === null)
|
|
114
|
+
return false;
|
|
115
|
+
const entry = b;
|
|
116
|
+
if (typeof entry['name'] !== 'string')
|
|
117
|
+
return false;
|
|
118
|
+
if (typeof entry['backupPath'] !== 'string')
|
|
119
|
+
return false;
|
|
120
|
+
if (typeof entry['livePath'] !== 'string')
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (typeof v['appliedAtMs'] !== 'number')
|
|
124
|
+
return false;
|
|
125
|
+
if (typeof v['bootAttempts'] !== 'number')
|
|
126
|
+
return false;
|
|
127
|
+
if (v['schemaVersion'] !== 1)
|
|
128
|
+
return false;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Patch a specific task inside a journal job file atomically.
|
|
133
|
+
* Reads `<dataDir>/lifecycle/jobs/<jobId>.json`, updates tasks[taskId],
|
|
134
|
+
* and writes back atomically. Silently no-ops on any read/write error.
|
|
135
|
+
*/
|
|
136
|
+
function patchJournalTask(dataDir, jobId, taskId, patch) {
|
|
137
|
+
const jobFile = path.join((0, lifecycle_journal_path_js_1.lifecycleJobsDir)(dataDir), `${jobId}.json`);
|
|
138
|
+
const raw = readJsonSafe(jobFile);
|
|
139
|
+
if (typeof raw !== 'object' || raw === null)
|
|
140
|
+
return;
|
|
141
|
+
const job = raw;
|
|
142
|
+
const tasks = job['tasks'];
|
|
143
|
+
if (!Array.isArray(tasks))
|
|
144
|
+
return;
|
|
145
|
+
const updatedTasks = tasks.map((task) => {
|
|
146
|
+
if (typeof task !== 'object' || task === null)
|
|
147
|
+
return task;
|
|
148
|
+
const t = task;
|
|
149
|
+
if (t['taskId'] !== taskId)
|
|
150
|
+
return task;
|
|
151
|
+
const updated = { ...t, phase: patch.phase };
|
|
152
|
+
if (patch.error !== undefined)
|
|
153
|
+
updated['error'] = patch.error;
|
|
154
|
+
if (patch.finishedAtMs !== undefined)
|
|
155
|
+
updated['finishedAtMs'] = patch.finishedAtMs;
|
|
156
|
+
return updated;
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
writeJsonAtomic(jobFile, { ...job, tasks: updatedTasks });
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// journal patch is best-effort — never crash the launcher
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Copy a directory tree cross-device (fallback for EXDEV rename errors). */
|
|
166
|
+
function moveDirCrossDevice(src, dest) {
|
|
167
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
168
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Write the `.pending-framework-swap.json` marker atomically.
|
|
172
|
+
* Co-located with the reader (`applyPendingFrameworkSwap`) for symmetry.
|
|
173
|
+
* Zero @camstack deps — this file runs before @camstack/system is imported.
|
|
174
|
+
*/
|
|
175
|
+
function writePendingFrameworkSwap(dataDir, marker) {
|
|
176
|
+
const markerPath = path.join(dataDir, PENDING_SWAP_MARKER);
|
|
177
|
+
writeJsonAtomic(markerPath, marker);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Apply a pending framework swap if a `.pending-framework-swap.json` marker
|
|
181
|
+
* exists in `dataDir`. Swaps each listed package atomically:
|
|
182
|
+
* 1. Backup: rename live → backupPath
|
|
183
|
+
* 2. Swap: rename staged → live (EXDEV fallback: cpSync + rmSync)
|
|
184
|
+
*
|
|
185
|
+
* On any mid-loop failure: restores already-swapped packages, deletes the
|
|
186
|
+
* marker, returns `{ applied: false }`.
|
|
187
|
+
* On success: writes `.framework-swap-confirm.json`, patches the journal
|
|
188
|
+
* task to `applied`, deletes the swap marker, returns `{ applied: true, jobId }`.
|
|
189
|
+
*/
|
|
190
|
+
function applyPendingFrameworkSwap(dataDir, frameworkDir) {
|
|
191
|
+
if (!frameworkDir)
|
|
192
|
+
return { applied: false };
|
|
193
|
+
const markerPath = path.join(dataDir, PENDING_SWAP_MARKER);
|
|
194
|
+
if (!fs.existsSync(markerPath))
|
|
195
|
+
return { applied: false };
|
|
196
|
+
const raw = readJsonSafe(markerPath);
|
|
197
|
+
if (!isPendingSwapMarker(raw)) {
|
|
198
|
+
// malformed — delete to prevent blocking future updates
|
|
199
|
+
try {
|
|
200
|
+
fs.rmSync(markerPath, { force: true });
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// ignore deletion error
|
|
204
|
+
}
|
|
205
|
+
return { applied: false };
|
|
206
|
+
}
|
|
207
|
+
const marker = raw;
|
|
208
|
+
const swapped = [];
|
|
209
|
+
for (const pkg of marker.packages) {
|
|
210
|
+
const livePath = path.join(frameworkDir, 'node_modules', pkg.name);
|
|
211
|
+
try {
|
|
212
|
+
// Step 1: backup live dir if it exists
|
|
213
|
+
if (fs.existsSync(livePath)) {
|
|
214
|
+
fs.mkdirSync(path.dirname(pkg.backupPath), { recursive: true });
|
|
215
|
+
fs.renameSync(livePath, pkg.backupPath);
|
|
216
|
+
}
|
|
217
|
+
// Step 2: move staged dir to live
|
|
218
|
+
try {
|
|
219
|
+
fs.mkdirSync(path.dirname(livePath), { recursive: true });
|
|
220
|
+
fs.renameSync(pkg.stagedPath, livePath);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (err.code === 'EXDEV') {
|
|
224
|
+
fs.mkdirSync(path.dirname(livePath), { recursive: true });
|
|
225
|
+
moveDirCrossDevice(pkg.stagedPath, livePath);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
swapped.push({ livePath, backupPath: pkg.backupPath });
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Restore all already-swapped packages
|
|
235
|
+
for (const done of swapped) {
|
|
236
|
+
try {
|
|
237
|
+
if (fs.existsSync(done.livePath)) {
|
|
238
|
+
fs.rmSync(done.livePath, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
if (fs.existsSync(done.backupPath)) {
|
|
241
|
+
fs.mkdirSync(path.dirname(done.livePath), { recursive: true });
|
|
242
|
+
fs.renameSync(done.backupPath, done.livePath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// best-effort restore
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Delete the pending marker so the next boot doesn't retry a broken swap
|
|
250
|
+
try {
|
|
251
|
+
fs.rmSync(markerPath, { force: true });
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// ignore
|
|
255
|
+
}
|
|
256
|
+
return { applied: false };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// All packages swapped successfully — write confirm marker
|
|
260
|
+
const backups = marker.packages.map((pkg) => ({
|
|
261
|
+
name: pkg.name,
|
|
262
|
+
backupPath: pkg.backupPath,
|
|
263
|
+
livePath: path.join(frameworkDir, 'node_modules', pkg.name),
|
|
264
|
+
}));
|
|
265
|
+
const confirmMarker = {
|
|
266
|
+
jobId: marker.jobId,
|
|
267
|
+
taskId: marker.taskId,
|
|
268
|
+
backups,
|
|
269
|
+
appliedAtMs: Date.now(),
|
|
270
|
+
bootAttempts: 0,
|
|
271
|
+
schemaVersion: 1,
|
|
272
|
+
};
|
|
273
|
+
// Write confirm marker — if this fails, restore everything and fail the apply
|
|
274
|
+
try {
|
|
275
|
+
writeJsonAtomic(path.join(dataDir, SWAP_CONFIRM_MARKER), confirmMarker);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Confirm marker write failed — restore all swapped packages and fail the apply
|
|
279
|
+
for (const done of swapped) {
|
|
280
|
+
try {
|
|
281
|
+
if (fs.existsSync(done.livePath)) {
|
|
282
|
+
fs.rmSync(done.livePath, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
if (fs.existsSync(done.backupPath)) {
|
|
285
|
+
fs.mkdirSync(path.dirname(done.livePath), { recursive: true });
|
|
286
|
+
fs.renameSync(done.backupPath, done.livePath);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// best-effort restore
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Delete the pending marker so the next boot doesn't retry
|
|
294
|
+
try {
|
|
295
|
+
fs.rmSync(markerPath, { force: true });
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// ignore
|
|
299
|
+
}
|
|
300
|
+
return { applied: false };
|
|
301
|
+
}
|
|
302
|
+
// Patch journal task → applied (best-effort, doesn't fail the apply)
|
|
303
|
+
patchJournalTask(dataDir, marker.jobId, marker.taskId, { phase: 'applied' });
|
|
304
|
+
// Delete the swap marker
|
|
305
|
+
try {
|
|
306
|
+
fs.rmSync(markerPath, { force: true });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// ignore
|
|
310
|
+
}
|
|
311
|
+
return { applied: true, jobId: marker.jobId };
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Check for an unconfirmed framework swap on boot.
|
|
315
|
+
*
|
|
316
|
+
* - `bootAttempts === 0`: this is the probation boot. Increment to 1,
|
|
317
|
+
* rewrite the marker, return `{ rolledBack: false }`.
|
|
318
|
+
* - `bootAttempts >= 1`: the previous probation boot crashed / didn't call
|
|
319
|
+
* `confirmFrameworkSwapHealthy`. Restore all backups, patch the journal
|
|
320
|
+
* task to `failed`, delete the confirm marker, return `{ rolledBack: true }`.
|
|
321
|
+
*/
|
|
322
|
+
function rollbackUnconfirmedFrameworkSwap(dataDir, frameworkDir) {
|
|
323
|
+
if (!frameworkDir)
|
|
324
|
+
return { rolledBack: false };
|
|
325
|
+
const confirmPath = path.join(dataDir, SWAP_CONFIRM_MARKER);
|
|
326
|
+
if (!fs.existsSync(confirmPath))
|
|
327
|
+
return { rolledBack: false };
|
|
328
|
+
const raw = readJsonSafe(confirmPath);
|
|
329
|
+
if (!isSwapConfirmMarker(raw))
|
|
330
|
+
return { rolledBack: false };
|
|
331
|
+
const confirm = raw;
|
|
332
|
+
if (confirm.bootAttempts === 0) {
|
|
333
|
+
// Probation boot — increment attempts and let this boot proceed
|
|
334
|
+
const updated = { ...confirm, bootAttempts: 1 };
|
|
335
|
+
try {
|
|
336
|
+
writeJsonAtomic(confirmPath, updated);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// ignore write failure
|
|
340
|
+
}
|
|
341
|
+
return { rolledBack: false };
|
|
342
|
+
}
|
|
343
|
+
// bootAttempts >= 1: probation boot didn't become healthy — rollback
|
|
344
|
+
for (const backup of confirm.backups) {
|
|
345
|
+
try {
|
|
346
|
+
if (fs.existsSync(backup.livePath)) {
|
|
347
|
+
fs.rmSync(backup.livePath, { recursive: true, force: true });
|
|
348
|
+
}
|
|
349
|
+
if (fs.existsSync(backup.backupPath)) {
|
|
350
|
+
fs.mkdirSync(path.dirname(backup.livePath), { recursive: true });
|
|
351
|
+
fs.renameSync(backup.backupPath, backup.livePath);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// best-effort restore
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
patchJournalTask(dataDir, confirm.jobId, confirm.taskId, {
|
|
359
|
+
phase: 'failed',
|
|
360
|
+
error: 'framework update rolled back — boot did not become healthy',
|
|
361
|
+
finishedAtMs: Date.now(),
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
fs.rmSync(confirmPath, { force: true });
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// ignore
|
|
368
|
+
}
|
|
369
|
+
return { rolledBack: true };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Called by post-boot health check once the hub is confirmed healthy after
|
|
373
|
+
* a framework swap. Deletes all backups and the confirm marker so the old
|
|
374
|
+
* framework packages are cleaned up.
|
|
375
|
+
*/
|
|
376
|
+
function confirmFrameworkSwapHealthy(dataDir) {
|
|
377
|
+
const confirmPath = path.join(dataDir, SWAP_CONFIRM_MARKER);
|
|
378
|
+
if (!fs.existsSync(confirmPath))
|
|
379
|
+
return;
|
|
380
|
+
const raw = readJsonSafe(confirmPath);
|
|
381
|
+
if (!isSwapConfirmMarker(raw)) {
|
|
382
|
+
// corrupt marker — still delete it
|
|
383
|
+
try {
|
|
384
|
+
fs.rmSync(confirmPath, { force: true });
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// ignore
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const confirm = raw;
|
|
392
|
+
// Delete all backup dirs
|
|
393
|
+
for (const backup of confirm.backups) {
|
|
394
|
+
try {
|
|
395
|
+
fs.rmSync(backup.backupPath, { recursive: true, force: true });
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// best-effort cleanup
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Delete the confirm marker
|
|
402
|
+
try {
|
|
403
|
+
fs.rmSync(confirmPath, { force: true });
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// ignore
|
|
407
|
+
}
|
|
408
|
+
}
|