@camstack/server 1.2.93 → 1.2.95

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.
@@ -1,119 +0,0 @@
1
- "use strict";
2
- /**
3
- * Boot-time framework-swap job resume + health confirm.
4
- *
5
- * After a framework swap reboot, `post-boot.service.ts` calls this once the
6
- * hub is healthy. It:
7
- * 1. Reads `.framework-swap-confirm.json` (written by the launcher on apply).
8
- * 2. Marks the journal task `applied` → `done`, then finalises the job →
9
- * `completed`.
10
- * 3. Calls `confirmFrameworkSwapHealthy` to delete the confirm marker +
11
- * backup dirs (disarms the crash-loop rollback).
12
- *
13
- * Best-effort: a missing/corrupt journal is tolerated — `confirmFrameworkSwapHealthy`
14
- * is still called so the rollback is always disarmed when the hub boots healthy.
15
- */
16
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
- if (k2 === undefined) k2 = k;
18
- var desc = Object.getOwnPropertyDescriptor(m, k);
19
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
- desc = { enumerable: true, get: function() { return m[k]; } };
21
- }
22
- Object.defineProperty(o, k2, desc);
23
- }) : (function(o, m, k, k2) {
24
- if (k2 === undefined) k2 = k;
25
- o[k2] = m[k];
26
- }));
27
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
- Object.defineProperty(o, "default", { enumerable: true, value: v });
29
- }) : function(o, v) {
30
- o["default"] = v;
31
- });
32
- var __importStar = (this && this.__importStar) || (function () {
33
- var ownKeys = function(o) {
34
- ownKeys = Object.getOwnPropertyNames || function (o) {
35
- var ar = [];
36
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
- return ar;
38
- };
39
- return ownKeys(o);
40
- };
41
- return function (mod) {
42
- if (mod && mod.__esModule) return mod;
43
- var result = {};
44
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
- __setModuleDefault(result, mod);
46
- return result;
47
- };
48
- })();
49
- Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.resumeFrameworkSwapJob = resumeFrameworkSwapJob;
51
- const fs = __importStar(require("node:fs"));
52
- const path = __importStar(require("node:path"));
53
- const types_1 = require("@camstack/types");
54
- const system_1 = require("@camstack/system");
55
- const launcher_framework_swap_js_1 = require("../launcher-framework-swap.js");
56
- const lifecycle_journal_path_js_1 = require("../lifecycle-journal-path.js");
57
- const SWAP_CONFIRM_FILE = '.framework-swap-confirm.json';
58
- /**
59
- * Resume a framework-swap journal job to `done`/`completed` and confirm the
60
- * hub is healthy (deletes the confirm marker + backups).
61
- *
62
- * @returns `{ resumed: false }` when no confirm marker exists.
63
- * `{ resumed: true, jobId }` when the marker was found and processed.
64
- * Never throws — errors are swallowed to avoid crashing the post-boot path.
65
- */
66
- async function resumeFrameworkSwapJob(dataDir) {
67
- try {
68
- const confirmMarker = readConfirmMarker(dataDir);
69
- if (confirmMarker === null) {
70
- return { resumed: false };
71
- }
72
- const { jobId, taskId } = confirmMarker;
73
- let journalPatched = false;
74
- try {
75
- const journal = new system_1.JobJournal((0, lifecycle_journal_path_js_1.lifecycleJobsDir)(dataDir));
76
- const job = journal.getJob(jobId);
77
- if (job !== null) {
78
- const task = job.tasks.find((t) => t.taskId === taskId);
79
- if (task !== undefined && task.phase === 'applied') {
80
- journal.patchTask(jobId, taskId, { phase: 'done', finishedAtMs: Date.now() });
81
- // Single-task framework job: if all tasks are now terminal and none
82
- // failed, mark the job completed (mirrors the engine's finalize logic).
83
- const updatedJob = journal.getJob(jobId);
84
- if (updatedJob !== null) {
85
- const allTerminal = updatedJob.tasks.every((t) => t.phase === 'done' || t.phase === 'failed' || t.phase === 'skipped');
86
- const anyFailed = updatedJob.tasks.some((t) => t.phase === 'failed');
87
- if (allTerminal && !anyFailed) {
88
- journal.setJobState(jobId, 'completed');
89
- }
90
- }
91
- journalPatched = true;
92
- }
93
- }
94
- }
95
- catch {
96
- // Journal is missing or corrupt — still clean up the confirm marker so
97
- // the rollback is disarmed on a healthy hub boot.
98
- }
99
- (0, launcher_framework_swap_js_1.confirmFrameworkSwapHealthy)(dataDir);
100
- return journalPatched ? { resumed: true, jobId } : { resumed: false };
101
- }
102
- catch {
103
- // Never crash the caller (post-boot service).
104
- return { resumed: false };
105
- }
106
- }
107
- /** Read and shape-check the confirm marker. Returns null on any error. */
108
- function readConfirmMarker(dataDir) {
109
- try {
110
- const raw = JSON.parse(fs.readFileSync(path.join(dataDir, SWAP_CONFIRM_FILE), 'utf-8'));
111
- const parsed = types_1.frameworkSwapConfirmSchema.safeParse(raw);
112
- if (!parsed.success)
113
- return null;
114
- return { jobId: parsed.data.jobId, taskId: parsed.data.taskId };
115
- }
116
- catch {
117
- return null;
118
- }
119
- }
@@ -1,344 +0,0 @@
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.resolveHubClosurePackageDir = resolveHubClosurePackageDir;
37
- exports.resolveOverlayPackageDir = resolveOverlayPackageDir;
38
- exports.computeDistBuildId = computeDistBuildId;
39
- exports.swapDist = swapDist;
40
- exports.syncFrameworkDist = syncFrameworkDist;
41
- /**
42
- * Framework live-sync — make a `camstack deploy` of a framework package
43
- * (`@camstack/system`, …) actually reach the code the running hub loads.
44
- *
45
- * WHY THIS EXISTS
46
- * ---------------
47
- * `installFromTgz` writes the freshly-built framework tree to
48
- * `CAMSTACK_ADDONS_DIR/<pkg>` (`/data/addons/@camstack/system`). NO runtime
49
- * consumer loads from there:
50
- * • the hub main process resolves `@camstack/system` from its OWN
51
- * `node_modules` closure (Node's `node_modules` walk-up from
52
- * `@camstack/server` finds the co-located copy BEFORE `NODE_PATH` is ever
53
- * consulted — so the `/data/framework` overlay is a no-op for it);
54
- * • the forked addon runners resolve it from `CAMSTACK_FRAMEWORK_DIR`
55
- * (`/data/framework/node_modules/<pkg>`) via the ESM resolver hook.
56
- * So a framework deploy landed on disk in a dead location and the subsequent
57
- * hub restart reloaded the SAME unchanged code.
58
- *
59
- * This module mirrors the freshly-built `dist/` into BOTH live locations with
60
- * an atomic rename-aside swap (mirrors `AddonInstaller.evictInstallDir`), so the
61
- * scheduled restart actually loads the new framework code in the hub main AND
62
- * the addon runners. Only `dist/` is swapped — native prebuilds and sibling
63
- * packages in each closure are left untouched.
64
- *
65
- * All failures are surfaced (logged + returned) but never thrown: a failed
66
- * mirror must not brick the restart, and the rename-aside guarantees a live
67
- * `dist/` is never left half-written (the incoming copy is staged first, and
68
- * the previous `dist/` is restored if the final swap fails).
69
- */
70
- const fs = __importStar(require("node:fs"));
71
- const path = __importStar(require("node:path"));
72
- const node_crypto_1 = require("node:crypto");
73
- const node_module_1 = require("node:module");
74
- // Resolve framework packages the same way the running hub does — from THIS
75
- // module's location, whose walk-up lands on the hub's own `node_modules`
76
- // closure (identical to the hub main's `@camstack/system` resolution).
77
- const requireFromHere = (0, node_module_1.createRequire)(__filename);
78
- // ---------------------------------------------------------------------------
79
- // Tiny cast-free JSON helper (mirror of addon-package.service.readJsonObject).
80
- // ---------------------------------------------------------------------------
81
- function readJsonObjectSafe(filePath) {
82
- try {
83
- const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
84
- if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
85
- return { ...parsed };
86
- }
87
- }
88
- catch {
89
- /* missing / malformed — treat as absent */
90
- }
91
- return null;
92
- }
93
- function packageNameOf(pkgJsonPath) {
94
- const obj = readJsonObjectSafe(pkgJsonPath);
95
- const name = obj?.['name'];
96
- return typeof name === 'string' ? name : null;
97
- }
98
- // ---------------------------------------------------------------------------
99
- // Target resolution
100
- // ---------------------------------------------------------------------------
101
- /**
102
- * The package directory the HUB MAIN process loads `<packageName>` from — its
103
- * own `node_modules` closure. `require.resolve('<pkg>/package.json')` is the
104
- * happy path; packages whose `exports` map omits that subpath fall back to
105
- * resolving the main entry and walking up to the matching `package.json`.
106
- * Returns null when the package is not resolvable.
107
- */
108
- function resolveHubClosurePackageDir(packageName) {
109
- try {
110
- return path.dirname(requireFromHere.resolve(`${packageName}/package.json`));
111
- }
112
- catch {
113
- // `exports` map blocks the package.json subpath — fall through.
114
- }
115
- try {
116
- let dir = path.dirname(requireFromHere.resolve(packageName));
117
- while (dir !== path.dirname(dir)) {
118
- const candidate = path.join(dir, 'package.json');
119
- if (fs.existsSync(candidate) && packageNameOf(candidate) === packageName) {
120
- return dir;
121
- }
122
- dir = path.dirname(dir);
123
- }
124
- }
125
- catch {
126
- // package itself not resolvable — treat as not installed.
127
- }
128
- return null;
129
- }
130
- /**
131
- * The package directory the forked ADDON RUNNERS load `<packageName>` from —
132
- * the `CAMSTACK_FRAMEWORK_DIR` overlay. Returns null when the env is unset
133
- * (dev / no overlay) or the dir does not exist.
134
- */
135
- function resolveOverlayPackageDir(packageName) {
136
- const frameworkDir = process.env['CAMSTACK_FRAMEWORK_DIR'];
137
- if (frameworkDir === undefined || frameworkDir.length === 0)
138
- return null;
139
- const dir = path.join(frameworkDir, 'node_modules', packageName);
140
- return fs.existsSync(dir) ? dir : null;
141
- }
142
- function resolveLiveTargets(packageName) {
143
- const out = [];
144
- const hub = resolveHubClosurePackageDir(packageName);
145
- if (hub !== null)
146
- out.push({ role: 'hub-closure', packageDir: hub });
147
- const overlay = resolveOverlayPackageDir(packageName);
148
- if (overlay !== null)
149
- out.push({ role: 'overlay', packageDir: overlay });
150
- return out;
151
- }
152
- function safeRealpath(p) {
153
- try {
154
- return fs.realpathSync(p);
155
- }
156
- catch {
157
- return path.resolve(p);
158
- }
159
- }
160
- // ---------------------------------------------------------------------------
161
- // Build-id (content hash of a `dist/` tree)
162
- // ---------------------------------------------------------------------------
163
- function collectJsFiles(root, dir, out) {
164
- let entries;
165
- try {
166
- entries = fs.readdirSync(dir, { withFileTypes: true });
167
- }
168
- catch {
169
- return;
170
- }
171
- for (const entry of entries) {
172
- const full = path.join(dir, entry.name);
173
- if (entry.isDirectory()) {
174
- collectJsFiles(root, full, out);
175
- }
176
- else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.mjs'))) {
177
- out.push(path.relative(root, full));
178
- }
179
- }
180
- }
181
- /**
182
- * Content build-id of a `dist/` tree — md5 over every `.js`/`.mjs` file's
183
- * relative path + bytes (sorted for determinism). Analogous to the Docker
184
- * image's `seed-version` fingerprint. Returns null when the dir is missing or
185
- * empty. Used to surface "same semver, new code" instead of trusting `version`.
186
- */
187
- function computeDistBuildId(distDir) {
188
- if (!fs.existsSync(distDir))
189
- return null;
190
- const files = [];
191
- collectJsFiles(distDir, distDir, files);
192
- if (files.length === 0)
193
- return null;
194
- files.sort();
195
- const hash = (0, node_crypto_1.createHash)('md5');
196
- for (const rel of files) {
197
- hash.update(rel);
198
- hash.update('\0');
199
- hash.update(fs.readFileSync(path.join(distDir, rel)));
200
- }
201
- return hash.digest('hex');
202
- }
203
- // ---------------------------------------------------------------------------
204
- // Atomic dist swap (rename-aside)
205
- // ---------------------------------------------------------------------------
206
- function isCrossDeviceError(err) {
207
- return err instanceof Error && err.code === 'EXDEV';
208
- }
209
- /**
210
- * Replace `<targetPkgDir>/dist` with a copy of `sourceDistDir`.
211
- *
212
- * 1. COPY `sourceDistDir` → a staging dir INSIDE the target dir. The live
213
- * `dist/` is untouched, so a mid-copy failure cannot corrupt it.
214
- * 2. Evict the current `dist/` aside (rename), then rename the staging dir into
215
- * place. Both renames are metadata ops WITHIN the target fs (atomic). If the
216
- * final swap fails, the previous `dist/` is restored.
217
- *
218
- * OVERLAYFS FALLBACK: the hub-closure target (`/opt/.../@camstack/system`) lives
219
- * on the container's overlay rootfs. A `dist/` materialized from a LOWER image
220
- * layer cannot be `rename()`d (overlayfs returns `EXDEV: cross-device link`),
221
- * so the atomic aside/swap is impossible there. On EXDEV we fall back to a
222
- * copy-aside restore point + in-place replace. This opens a brief non-atomic
223
- * window, but it is safe here: the running process already holds the old
224
- * modules in memory (nothing reads `dist/` until the scheduled restart), and
225
- * the copy-aside is kept until the replace completes so a failure is
226
- * recoverable.
227
- */
228
- async function swapDist(sourceDistDir, targetPkgDir) {
229
- const targetDist = path.join(targetPkgDir, 'dist');
230
- const stamp = `${process.pid}-${Date.now()}`;
231
- const incoming = path.join(targetPkgDir, `.dist-incoming-${stamp}`);
232
- const aside = path.join(targetPkgDir, `.dist-evicted-${stamp}`);
233
- // Step 1 — stage the incoming dist (cross-device copy is fine here).
234
- try {
235
- await fs.promises.cp(sourceDistDir, incoming, { recursive: true, force: true });
236
- }
237
- catch (err) {
238
- await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
239
- throw err;
240
- }
241
- const hadDist = fs.existsSync(targetDist);
242
- // How the previous dist was preserved: 'rename' (moved aside, same fs),
243
- // 'copy' (overlayfs EXDEV — copied aside then removed in place), or 'none'.
244
- let asideKind = 'none';
245
- const restorePreviousDist = async () => {
246
- if (asideKind === 'none' || fs.existsSync(targetDist))
247
- return;
248
- if (asideKind === 'rename') {
249
- await fs.promises.rename(aside, targetDist).catch(() => undefined);
250
- }
251
- else {
252
- await fs.promises
253
- .cp(aside, targetDist, { recursive: true, force: true })
254
- .catch(() => undefined);
255
- }
256
- };
257
- // Step 2 — evict the current dist. Prefer the atomic rename; fall back to
258
- // copy-aside + in-place remove on overlayfs EXDEV.
259
- if (hadDist) {
260
- try {
261
- await fs.promises.rename(targetDist, aside);
262
- asideKind = 'rename';
263
- }
264
- catch (err) {
265
- if (!isCrossDeviceError(err)) {
266
- await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
267
- throw err;
268
- }
269
- await fs.promises.cp(targetDist, aside, { recursive: true, force: true });
270
- await fs.promises.rm(targetDist, { recursive: true, force: true });
271
- asideKind = 'copy';
272
- }
273
- }
274
- // Step 3 — move the incoming dist into place; fall back to copy on EXDEV.
275
- try {
276
- await fs.promises.rename(incoming, targetDist);
277
- }
278
- catch (err) {
279
- if (isCrossDeviceError(err)) {
280
- try {
281
- await fs.promises.cp(incoming, targetDist, { recursive: true, force: true });
282
- await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
283
- }
284
- catch (copyErr) {
285
- await restorePreviousDist();
286
- await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
287
- throw copyErr;
288
- }
289
- }
290
- else {
291
- await restorePreviousDist();
292
- await fs.promises.rm(incoming, { recursive: true, force: true }).catch(() => undefined);
293
- throw err;
294
- }
295
- }
296
- // Step 4 — best-effort cleanup of the evicted copy.
297
- if (asideKind !== 'none') {
298
- await fs.promises.rm(aside, { recursive: true, force: true }).catch(() => undefined);
299
- }
300
- }
301
- // ---------------------------------------------------------------------------
302
- // Public entry
303
- // ---------------------------------------------------------------------------
304
- /**
305
- * Mirror a freshly-installed framework `dist/` into every live location the
306
- * runtime actually loads it from (hub-main closure + addon-runner overlay).
307
- * Never throws — returns a per-target outcome. Call BEFORE the scheduled hub
308
- * restart so the bounce loads the new code.
309
- */
310
- async function syncFrameworkDist(args) {
311
- const { packageName, sourceDistDir, logger } = args;
312
- if (!fs.existsSync(sourceDistDir)) {
313
- logger.warn('framework sync: source dist missing — nothing to mirror', {
314
- meta: { packageName, sourceDistDir },
315
- });
316
- return { results: [] };
317
- }
318
- const sourcePkgReal = safeRealpath(path.dirname(sourceDistDir));
319
- const seen = new Set();
320
- const results = [];
321
- for (const target of resolveLiveTargets(packageName)) {
322
- const real = safeRealpath(target.packageDir);
323
- // Dedupe (a dev symlink can point both roles at one dir) and never mirror
324
- // a package dir onto its own source (would delete the code mid-swap).
325
- if (seen.has(real) || real === sourcePkgReal)
326
- continue;
327
- seen.add(real);
328
- try {
329
- await swapDist(sourceDistDir, target.packageDir);
330
- results.push({ role: target.role, packageDir: target.packageDir, ok: true });
331
- logger.info('framework sync: dist swapped into live location', {
332
- meta: { packageName, role: target.role, packageDir: target.packageDir },
333
- });
334
- }
335
- catch (err) {
336
- const message = err instanceof Error ? err.message : String(err);
337
- results.push({ role: target.role, packageDir: target.packageDir, ok: false, error: message });
338
- logger.error('framework sync: dist swap FAILED (live dir left intact)', {
339
- meta: { packageName, role: target.role, packageDir: target.packageDir, error: message },
340
- });
341
- }
342
- }
343
- return { results };
344
- }