@camstack/server 1.0.4 → 1.0.6

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.
@@ -75,8 +75,8 @@ const types_1 = require("@camstack/types");
75
75
  const system_1 = require("@camstack/system");
76
76
  const integration_id_backfill_1 = require("../../boot/integration-id-backfill");
77
77
  const collection_preference_js_1 = require("./collection-preference.js");
78
- const bulk_update_coordinator_js_1 = require("./bulk-update-coordinator.js");
79
78
  const addon_package_service_js_1 = require("../../core/addon/addon-package.service.js");
79
+ const lifecycle_runner_singleton_js_1 = require("../../core/lifecycle/lifecycle-runner.singleton.js");
80
80
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
81
81
  // ── system ──────────────────────────────────────────────────────────
82
82
  function getRetention(registry) {
@@ -907,42 +907,14 @@ async function fetchAgentInstalledPackages(broker, nodeId) {
907
907
  }
908
908
  return out;
909
909
  }
910
- function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, eb) {
910
+ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
911
911
  const broker = moleculer.broker;
912
- // Adapt the hub EventBusService (which takes a full SystemEvent object) to
913
- // the IBulkUpdateEventBus interface (which takes (category, payload) pairs).
914
- // Using `import { EventCategory }` from @camstack/types avoids a new import
915
- // — it is already resolved in the generated-cap-routers layer above. The
916
- // `eb.emit` overload that takes a TypedSystemEvent is the type-safe path.
917
- const bulkEventBus = {
918
- emit: (category, payload) => {
919
- eb.emit({
920
- id: (0, node_crypto_1.randomUUID)(),
921
- timestamp: new Date(),
922
- source: { type: 'core', id: 'bulk-update-coordinator' },
923
- category,
924
- data: payload,
925
- });
926
- },
927
- };
928
- const bulkCoordinator = new bulk_update_coordinator_js_1.BulkUpdateCoordinator({
929
- eventBus: bulkEventBus,
930
- updateAddon: async (i) => {
931
- await ps.updatePackage(i.name, i.version);
932
- },
933
- updateFrameworkPackage: async (i) => {
934
- await ps.updateFrameworkPackage({
935
- packageName: i.packageName,
936
- version: i.version,
937
- deferRestart: i.deferRestart,
938
- });
939
- },
940
- restartServer: async () => {
941
- await ps.restartServer(ctx.user?.username ?? ctx.user?.id);
942
- },
943
- logger: ls.createLogger('bulk-update'),
944
- });
945
912
  const frameworkAllowSet = new Set([addon_package_service_js_1.SYSTEM_PACKAGE]);
913
+ // Process-wide runner built once at boot (manual-boot wires its deps from the
914
+ // addon-package service + event bus). `createdBy` travels per `startJob` call.
915
+ const lifecycleRunner = (0, lifecycle_runner_singleton_js_1.getLifecycleRunner)();
916
+ // Who triggered each lifecycle job from this request context.
917
+ const lifecycleCreatedBy = ctx.user?.username ?? ctx.user?.id ?? 'system';
946
918
  return {
947
919
  list: async () => {
948
920
  const rollbackable = ps.getRollbackablePackages();
@@ -986,13 +958,30 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, eb) {
986
958
  updatePackage: async (input) => {
987
959
  const nodeId = input.nodeId;
988
960
  if (nodeId === undefined || isHubNode(nodeId)) {
989
- return ps.updatePackage(input.name, input.version);
961
+ // Hub-single: route through the lifecycle job engine so the fast
962
+ // staged-swap path (applyStagedAddonUpdate) is always used.
963
+ const version = input.version ?? 'latest';
964
+ const { jobId } = await lifecycleRunner.startJob({
965
+ kind: 'update',
966
+ targets: [{ name: input.name, version }],
967
+ createdBy: lifecycleCreatedBy,
968
+ });
969
+ const job = lifecycleRunner.getJob(jobId);
970
+ const task = job?.tasks[0];
971
+ if (task?.phase === 'done') {
972
+ return { success: true, name: input.name, version, jobId };
973
+ }
974
+ return { success: false, error: task?.error ?? 'update failed', jobId };
990
975
  }
991
976
  // Agent target: the hub packs the resolved version and ships the
992
977
  // tarball over `$agent.deploy` — the agent has no npm runtime.
978
+ // The reload re-instantiates the changed package's addons; for a large
979
+ // package (e.g. the whole pipeline stack) that can take well over a
980
+ // minute, so allow generous headroom — a timeout here aborts the call
981
+ // even though the deploy already landed on disk.
993
982
  const packed = await ps.packPackage(input.name, input.version);
994
- await broker.call('$agent.deploy', { addonId: input.name, bundle: packed.buffer }, { nodeID: nodeId, timeout: 120_000 });
995
- await broker.call('$agent.reload', {}, { nodeID: nodeId, timeout: 120_000 });
983
+ await broker.call('$agent.deploy', { addonId: input.name, bundle: packed.buffer }, { nodeID: nodeId, timeout: 300_000 });
984
+ await broker.call('$agent.reload', {}, { nodeID: nodeId, timeout: 300_000 });
996
985
  return { success: true, name: input.name, version: packed.version, nodeId };
997
986
  },
998
987
  rollbackPackage: async (input) => ps.rollbackPackage(input.name),
@@ -1071,6 +1060,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, eb) {
1071
1060
  ? { requestedBy: ctx.user.id }
1072
1061
  : {}),
1073
1062
  ...(input.deferRestart !== undefined ? { deferRestart: input.deferRestart } : {}),
1063
+ runner: lifecycleRunner,
1074
1064
  }),
1075
1065
  getVersions: async (input) => ps.getPackageVersions(input.name),
1076
1066
  restartAddon: async (input) => ar.restartAddon(input.addonId),
@@ -1089,10 +1079,16 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, eb) {
1089
1079
  }
1090
1080
  return { success: true };
1091
1081
  },
1092
- startBulkUpdate: async (input) => bulkCoordinator.start(input),
1093
- getBulkUpdateState: async ({ id }) => bulkCoordinator.get(id),
1094
- cancelBulkUpdate: async ({ id }) => bulkCoordinator.cancel(id),
1095
- listActiveBulkUpdates: async ({ nodeId }) => bulkCoordinator.list(nodeId),
1082
+ // ── Lifecycle job engine ─────────────────────────────────────────
1083
+ startJob: async (input) => lifecycleRunner.startJob({
1084
+ kind: input.kind,
1085
+ targets: input.targets,
1086
+ nodeIds: input.nodeIds,
1087
+ createdBy: lifecycleCreatedBy,
1088
+ }),
1089
+ getJob: async (input) => lifecycleRunner.getJob(input.jobId),
1090
+ listJobs: async (input) => lifecycleRunner.listJobs({ activeOnly: input.activeOnly }),
1091
+ cancelJob: async (input) => lifecycleRunner.cancelJob(input.jobId),
1096
1092
  custom: async (input) => {
1097
1093
  const registry = ar.getCustomActionRegistry();
1098
1094
  const entry = registry.resolve(input.addonId, input.action);
@@ -0,0 +1,157 @@
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.createLifecycleJobRunner = createLifecycleJobRunner;
37
+ const path = __importStar(require("node:path"));
38
+ const node_crypto_1 = require("node:crypto");
39
+ const system_1 = require("@camstack/system");
40
+ const addon_package_service_js_1 = require("../../core/addon/addon-package.service.js");
41
+ const lifecycle_journal_path_js_1 = require("../../lifecycle-journal-path.js");
42
+ /**
43
+ * Resolve a target's `TaskTarget` discriminant.
44
+ * Explicit `target` field wins; otherwise auto-detected from package name.
45
+ */
46
+ function resolveTaskTarget(t) {
47
+ if (t.target !== undefined)
48
+ return t.target;
49
+ return t.name === addon_package_service_js_1.SYSTEM_PACKAGE ? 'framework' : 'addon';
50
+ }
51
+ /**
52
+ * Factory that wires `JobJournal` + `StagingArea` + `LifecycleJobEngine`
53
+ * into a thin façade that cap-providers delegates to.
54
+ *
55
+ * Each server boot creates a fresh instance (journals persist on disk; the
56
+ * in-memory running-set does not).
57
+ */
58
+ function createLifecycleJobRunner(deps) {
59
+ const nowFn = deps.now ?? (() => Date.now());
60
+ const journalDir = (0, lifecycle_journal_path_js_1.lifecycleJobsDir)(deps.dataDir);
61
+ const stagingDir = path.join(deps.dataDir, 'lifecycle', 'staging');
62
+ const journal = new system_1.JobJournal(journalDir);
63
+ const staging = new system_1.StagingArea(stagingDir, {
64
+ fetchTarball: deps.fetchTarball,
65
+ extract: deps.extract,
66
+ });
67
+ const engine = new system_1.LifecycleJobEngine({
68
+ journal,
69
+ staging,
70
+ applyAddonUpdate: deps.applyAddonUpdate,
71
+ emit: deps.emit,
72
+ now: nowFn,
73
+ stageFramework: deps.stageFramework,
74
+ requestFrameworkSwap: deps.requestFrameworkSwap,
75
+ });
76
+ /**
77
+ * Set of jobIds currently executing — used to guard cancellation.
78
+ * We only block cancellation of running jobs (cannot abort mid-flight).
79
+ */
80
+ const runningJobs = new Set();
81
+ return {
82
+ async startJob(input) {
83
+ const jobId = (0, node_crypto_1.randomUUID)();
84
+ const job = {
85
+ jobId,
86
+ kind: input.kind,
87
+ createdAtMs: nowFn(),
88
+ createdBy: input.createdBy,
89
+ scope: input.targets.length === 1 ? 'single' : 'bulk',
90
+ schemaVersion: 1,
91
+ state: 'running',
92
+ tasks: input.targets.map((t) => ({
93
+ taskId: (0, node_crypto_1.randomUUID)(),
94
+ nodeId: 'hub',
95
+ packageName: t.name,
96
+ fromVersion: null,
97
+ toVersion: t.version,
98
+ target: resolveTaskTarget(t),
99
+ phase: 'queued',
100
+ stagedPath: null,
101
+ attempts: 0,
102
+ steps: [],
103
+ error: null,
104
+ startedAtMs: null,
105
+ finishedAtMs: null,
106
+ })),
107
+ };
108
+ journal.createJob(job);
109
+ runningJobs.add(jobId);
110
+ try {
111
+ await engine.runJob(job);
112
+ }
113
+ finally {
114
+ runningJobs.delete(jobId);
115
+ }
116
+ return { jobId };
117
+ },
118
+ getJob(jobId) {
119
+ return journal.getJob(jobId);
120
+ },
121
+ listJobs(opts) {
122
+ return journal.listJobs(opts);
123
+ },
124
+ cancelJob(jobId) {
125
+ if (runningJobs.has(jobId)) {
126
+ // Cannot cancel a job that is actively running.
127
+ return { cancelled: false };
128
+ }
129
+ const job = journal.getJob(jobId);
130
+ if (!job || job.state !== 'running') {
131
+ return { cancelled: false };
132
+ }
133
+ journal.setJobState(jobId, 'cancelled');
134
+ return { cancelled: true };
135
+ },
136
+ async reconcile() {
137
+ // §10 boot reconcile: re-drive every active (non-terminal) job from its
138
+ // on-disk checkpoint. `resumed` = jobs that progressed without ending in a
139
+ // `failed` final state (a job left `running` because a framework task is
140
+ // still pending its reboot resume counts as resumed — it made progress and
141
+ // did not fail). `failed` = jobs whose final state is `failed`.
142
+ const jobs = journal.listJobs({ activeOnly: true });
143
+ let resumed = 0;
144
+ let failed = 0;
145
+ for (const job of jobs) {
146
+ const result = await engine.reconcileJob(job);
147
+ if (result.state === 'failed') {
148
+ failed += 1;
149
+ }
150
+ else {
151
+ resumed += 1;
152
+ }
153
+ }
154
+ return { resumed, failed };
155
+ },
156
+ };
157
+ }
@@ -595,38 +595,6 @@ function createCapRouter_addons(getProvider, _createRemoteProxy) {
595
595
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
596
596
  return p.updateFrameworkPackage(input);
597
597
  }),
598
- startBulkUpdate: trpc_middleware_js_1.adminProcedure
599
- .input(types_10.addonsCapability.methods.startBulkUpdate.input.loose())
600
- .output(types_10.addonsCapability.methods.startBulkUpdate.output)
601
- .mutation(async ({ input, ctx }) => {
602
- const p = requireCapProvider('addons', () => getProvider(ctx));
603
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
604
- return p.startBulkUpdate(input);
605
- }),
606
- getBulkUpdateState: trpc_middleware_js_1.adminProcedure
607
- .input(types_10.addonsCapability.methods.getBulkUpdateState.input.loose())
608
- .output(types_10.addonsCapability.methods.getBulkUpdateState.output)
609
- .query(async ({ input, ctx }) => {
610
- const p = requireCapProvider('addons', () => getProvider(ctx));
611
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
612
- return p.getBulkUpdateState(input);
613
- }),
614
- cancelBulkUpdate: trpc_middleware_js_1.adminProcedure
615
- .input(types_10.addonsCapability.methods.cancelBulkUpdate.input.loose())
616
- .output(types_10.addonsCapability.methods.cancelBulkUpdate.output)
617
- .mutation(async ({ input, ctx }) => {
618
- const p = requireCapProvider('addons', () => getProvider(ctx));
619
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
620
- return p.cancelBulkUpdate(input);
621
- }),
622
- listActiveBulkUpdates: trpc_middleware_js_1.adminProcedure
623
- .input(types_10.addonsCapability.methods.listActiveBulkUpdates.input.loose())
624
- .output(types_10.addonsCapability.methods.listActiveBulkUpdates.output)
625
- .query(async ({ input, ctx }) => {
626
- const p = requireCapProvider('addons', () => getProvider(ctx));
627
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
628
- return p.listActiveBulkUpdates(input);
629
- }),
630
598
  getVersions: trpc_middleware_js_1.protectedProcedure
631
599
  .input(types_10.addonsCapability.methods.getVersions.input.loose())
632
600
  .output(types_10.addonsCapability.methods.getVersions.output)
@@ -699,6 +667,38 @@ function createCapRouter_addons(getProvider, _createRemoteProxy) {
699
667
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
700
668
  return p.custom(input);
701
669
  }),
670
+ startJob: trpc_middleware_js_1.adminProcedure
671
+ .input(types_10.addonsCapability.methods.startJob.input.loose())
672
+ .output(types_10.addonsCapability.methods.startJob.output)
673
+ .mutation(async ({ input, ctx }) => {
674
+ const p = requireCapProvider('addons', () => getProvider(ctx));
675
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
676
+ return p.startJob(input);
677
+ }),
678
+ getJob: trpc_middleware_js_1.adminProcedure
679
+ .input(types_10.addonsCapability.methods.getJob.input.loose())
680
+ .output(types_10.addonsCapability.methods.getJob.output)
681
+ .query(async ({ input, ctx }) => {
682
+ const p = requireCapProvider('addons', () => getProvider(ctx));
683
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
684
+ return p.getJob(input);
685
+ }),
686
+ listJobs: trpc_middleware_js_1.adminProcedure
687
+ .input(types_10.addonsCapability.methods.listJobs.input.loose())
688
+ .output(types_10.addonsCapability.methods.listJobs.output)
689
+ .query(async ({ input, ctx }) => {
690
+ const p = requireCapProvider('addons', () => getProvider(ctx));
691
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
692
+ return p.listJobs(input);
693
+ }),
694
+ cancelJob: trpc_middleware_js_1.adminProcedure
695
+ .input(types_10.addonsCapability.methods.cancelJob.input.loose())
696
+ .output(types_10.addonsCapability.methods.cancelJob.output)
697
+ .mutation(async ({ input, ctx }) => {
698
+ const p = requireCapProvider('addons', () => getProvider(ctx));
699
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
700
+ return p.cancelJob(input);
701
+ }),
702
702
  onAddonLogs: trpc_middleware_js_1.protectedProcedure
703
703
  .input(types_10.addonsCapability.methods.onAddonLogs.input)
704
704
  .subscription(({ input, ctx }) => {
@@ -116,7 +116,7 @@ function buildCapabilityRouters(services) {
116
116
  toast: (0, generated_cap_routers_1.createCapRouter_toast)((ctx) => (0, cap_providers_js_1.buildToastProvider)(services.toastService, ctx)),
117
117
  integrations: (0, generated_cap_routers_1.createCapRouter_integrations)((_ctx) => (0, cap_providers_js_1.buildIntegrationsProvider)(services.addonRegistry, services.eventBus, services.loggingService, services.capabilityRegistry)),
118
118
  nodes: (0, generated_cap_routers_1.createCapRouter_nodes)((_ctx) => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry)),
119
- addons: (0, generated_cap_routers_1.createCapRouter_addons)((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx, services.eventBus)),
119
+ addons: (0, generated_cap_routers_1.createCapRouter_addons)((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx)),
120
120
  // ── Cap overrides: cross-node remote-proxy cast ─────────────────
121
121
  // These caps' providers have manual interface types that pre-date
122
122
  // `InferProvider<typeof xCap>` — structurally identical, nominally
@@ -4,6 +4,8 @@ exports.PostBootService = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const system_1 = require("@camstack/system");
6
6
  const types_1 = require("@camstack/types");
7
+ const resume_framework_swap_js_1 = require("./resume-framework-swap.js");
8
+ const reconcile_lifecycle_jobs_js_1 = require("./reconcile-lifecycle-jobs.js");
7
9
  class PostBootService {
8
10
  eventBus;
9
11
  logger;
@@ -55,6 +57,29 @@ class PostBootService {
55
57
  // restart, …). `readPendingRestart` clears the marker atomically so
56
58
  // we never re-fire on a crash-loop boot.
57
59
  this.emitRestartCompletedIfPending(dataPath);
60
+ // If a framework swap was applied on the previous boot and left a
61
+ // `.framework-swap-confirm.json` marker, mark the journal job done and
62
+ // delete the marker + backups now that the hub is healthy. This also
63
+ // disarms the crash-loop rollback (the NEXT boot won't roll back a
64
+ // healthy update).
65
+ await this.resumeFrameworkSwapIfPending(dataPath);
66
+ // Then re-drive every other non-terminal lifecycle job (addon task phases)
67
+ // from its on-disk checkpoint. Order matters: framework resume FIRST (marks
68
+ // the framework task `applied`→`done`), THEN addon reconcile (which leaves
69
+ // framework tasks untouched).
70
+ await this.reconcileLifecycleJobsIfAny();
71
+ }
72
+ async resumeFrameworkSwapIfPending(dataDir) {
73
+ const result = await (0, resume_framework_swap_js_1.resumeFrameworkSwapJob)(dataDir);
74
+ if (result.resumed) {
75
+ this.logger.info('Framework update completed', { meta: { jobId: result.jobId } });
76
+ }
77
+ }
78
+ async reconcileLifecycleJobsIfAny() {
79
+ const { resumed, failed } = await (0, reconcile_lifecycle_jobs_js_1.reconcileLifecycleJobsAtBoot)();
80
+ if (resumed + failed > 0) {
81
+ this.logger.info('Lifecycle jobs reconciled at boot', { meta: { resumed, failed } });
82
+ }
58
83
  }
59
84
  emitRestartCompletedIfPending(dataDir) {
60
85
  const marker = (0, system_1.readPendingRestart)(dataDir);
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ /**
3
+ * Boot-time §10 reconcile of all non-terminal lifecycle jobs.
4
+ *
5
+ * After a crash mid-job (an "Update all" bulk job, a single addon update, …)
6
+ * the on-disk journal holds tasks parked in a non-terminal phase. This boot
7
+ * entry re-drives every active job through the engine so each addon task
8
+ * resumes from its checkpoint (reusing a still-valid `stagedPath` or
9
+ * re-fetching, then applying).
10
+ *
11
+ * Best-effort, mirrors `resume-framework-swap.ts`: this NEVER throws — any error
12
+ * (runner not initialized, journal corruption, …) is swallowed and reported as
13
+ * `{ resumed: 0, failed: 0 }` so the post-boot path can never crash the hub.
14
+ *
15
+ * Ordering note: the framework `applied`→`done` resume (`resumeFrameworkSwapJob`)
16
+ * MUST run BEFORE this — reconcile deliberately leaves framework tasks untouched.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.reconcileLifecycleJobsAtBoot = reconcileLifecycleJobsAtBoot;
20
+ const lifecycle_runner_singleton_js_1 = require("../core/lifecycle/lifecycle-runner.singleton.js");
21
+ async function reconcileLifecycleJobsAtBoot() {
22
+ try {
23
+ return await (0, lifecycle_runner_singleton_js_1.getLifecycleRunner)().reconcile();
24
+ }
25
+ catch {
26
+ // Never crash the post-boot path.
27
+ return { resumed: 0, failed: 0 };
28
+ }
29
+ }
@@ -0,0 +1,119 @@
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
+ }