@camstack/server 1.0.5 → 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.
- package/dist/api/core/cap-providers.js +38 -42
- package/dist/api/core/lifecycle-job-runner.js +157 -0
- package/dist/api/trpc/generated-cap-routers.js +32 -32
- package/dist/api/trpc/trpc.router.js +1 -1
- package/dist/boot/post-boot.service.js +25 -0
- package/dist/boot/reconcile-lifecycle-jobs.js +29 -0
- package/dist/boot/resume-framework-swap.js +119 -0
- package/dist/core/addon/addon-package.service.js +255 -17
- package/dist/core/addon/addon-registry.service.js +16 -2
- package/dist/core/agent/agent-registry.service.js +43 -1
- package/dist/core/lifecycle/lifecycle-runner.singleton.js +40 -0
- package/dist/framework-nodepath.js +49 -0
- package/dist/launcher-framework-swap.js +408 -0
- package/dist/launcher.js +75 -18
- package/dist/lifecycle-journal-path.js +41 -0
- package/dist/manual-boot.js +93 -0
- package/dist/request-framework-swap.js +41 -0
- package/package.json +1 -1
- package/dist/api/core/bulk-update-coordinator.js +0 -229
package/dist/manual-boot.js
CHANGED
|
@@ -6,12 +6,51 @@
|
|
|
6
6
|
* removed to drop the accidental DI layer on top of an already-explicit
|
|
7
7
|
* dependency graph.
|
|
8
8
|
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
9
42
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
43
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
44
|
};
|
|
12
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
46
|
exports.bootManual = bootManual;
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
49
|
const fastify_1 = __importDefault(require("fastify"));
|
|
50
|
+
const system_1 = require("@camstack/system");
|
|
51
|
+
const types_1 = require("@camstack/types");
|
|
52
|
+
const lifecycle_runner_singleton_1 = require("./core/lifecycle/lifecycle-runner.singleton");
|
|
53
|
+
const request_framework_swap_1 = require("./request-framework-swap");
|
|
15
54
|
const config_service_1 = require("./core/config/config.service");
|
|
16
55
|
const logging_service_1 = require("./core/logging/logging.service");
|
|
17
56
|
const event_bus_service_1 = require("./core/events/event-bus.service");
|
|
@@ -94,6 +133,60 @@ async function bootManual(opts) {
|
|
|
94
133
|
// dist sub-folder lookup (see service docstring).
|
|
95
134
|
const addonWidgetsService = new addon_widgets_service_1.AddonWidgetsService(loggingService, capabilityService, addonRegistryService);
|
|
96
135
|
const addonPackageService = new addon_package_service_1.AddonPackageService(loggingService, eventBusService, configService, addonRegistryService, notificationWrapper, toastWrapper);
|
|
136
|
+
// ---- Lifecycle job runner (boot singleton) -----------------------------
|
|
137
|
+
// The runner used to be built per-tRPC-request inside the `addons` cap
|
|
138
|
+
// factory. Boot-reconcile (F3 Task 4) and the auto-update scheduler (F3
|
|
139
|
+
// Task 5) need a stable instance that exists before any request, so we build
|
|
140
|
+
// it ONCE here from the addon-package service + event bus. `createdBy` is no
|
|
141
|
+
// longer a construction dep — it travels on each `startJob` call.
|
|
142
|
+
const lifecycleDataDir = path.resolve(process.env['CAMSTACK_DATA'] ?? 'camstack-data');
|
|
143
|
+
(0, lifecycle_runner_singleton_1.initLifecycleRunner)({
|
|
144
|
+
dataDir: lifecycleDataDir,
|
|
145
|
+
fetchTarball: (name, version, signal) => addonPackageService.fetchAddonTarball(name, version, signal),
|
|
146
|
+
extract: (tgz, destDir) => addonPackageService.extractTarball(tgz, destDir),
|
|
147
|
+
applyAddonUpdate: async ({ name, version, stagedPath }) => {
|
|
148
|
+
await addonPackageService.applyStagedAddonUpdate(name, version, stagedPath);
|
|
149
|
+
},
|
|
150
|
+
emit: (kind, job, entry) => {
|
|
151
|
+
if (kind === 'progress') {
|
|
152
|
+
eventBusService.emit({
|
|
153
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
154
|
+
timestamp: new Date(),
|
|
155
|
+
source: { type: 'core', id: 'lifecycle-job-engine' },
|
|
156
|
+
category: types_1.EventCategory.AddonsJobProgress,
|
|
157
|
+
data: { job },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
eventBusService.emit({
|
|
162
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
163
|
+
timestamp: new Date(),
|
|
164
|
+
source: { type: 'core', id: 'lifecycle-job-engine' },
|
|
165
|
+
category: types_1.EventCategory.AddonsJobLog,
|
|
166
|
+
data: { jobId: job.jobId, entry },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
// ── Framework swap deps ──────────────────────────────────────────
|
|
171
|
+
stageFramework: (task, signal) => (0, system_1.stageFrameworkLockstep)({
|
|
172
|
+
jobId: task.taskId,
|
|
173
|
+
toVersion: task.toVersion,
|
|
174
|
+
frameworkDir: process.env['CAMSTACK_FRAMEWORK_DIR'] ?? '',
|
|
175
|
+
stagingDir: path.join(lifecycleDataDir, 'lifecycle', 'staging'),
|
|
176
|
+
fetchTarball: (name, version, sig) => addonPackageService.fetchAddonTarball(name, version, sig),
|
|
177
|
+
extract: (tgz, destDir) => addonPackageService.extractTarball(tgz, destDir),
|
|
178
|
+
resolveVersion: (name) => addonPackageService.resolveFrameworkVersion(name, task.toVersion),
|
|
179
|
+
currentVersionOf: (name) => addonPackageService.currentFrameworkVersionOf(name),
|
|
180
|
+
signal,
|
|
181
|
+
}),
|
|
182
|
+
requestFrameworkSwap: ({ jobId, taskId, packages }) => {
|
|
183
|
+
// Write the pending-swap marker AND schedule the reboot that applies it.
|
|
184
|
+
// The engine assumes the process exits after this returns; the restart is
|
|
185
|
+
// owned here (the single seam for every engine path: single update,
|
|
186
|
+
// "Update all" bulk, and the auto-update scheduler).
|
|
187
|
+
(0, request_framework_swap_1.requestFrameworkSwapAndRestart)(lifecycleDataDir, { jobId, taskId, packages });
|
|
188
|
+
},
|
|
189
|
+
});
|
|
97
190
|
const replEngineService = new repl_engine_service_1.ReplEngineService(addonRegistryService, eventBusService, loggingService);
|
|
98
191
|
const topologyEmitterService = new topology_emitter_service_1.TopologyEmitterService(eventBusService, agentRegistryService, addonRegistryService);
|
|
99
192
|
const postBootService = new post_boot_service_1.PostBootService(addonRegistryService, eventBusService, loggingService);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestFrameworkSwapAndRestart = requestFrameworkSwapAndRestart;
|
|
4
|
+
/**
|
|
5
|
+
* The single seam that joins the two halves of a framework-swap request:
|
|
6
|
+
* 1. write the durable `.pending-framework-swap.json` marker (consumed by the
|
|
7
|
+
* zero-dep launcher on the NEXT boot), and
|
|
8
|
+
* 2. schedule the self-restart that makes the launcher actually run.
|
|
9
|
+
*
|
|
10
|
+
* The launcher (`launcher-framework-swap.ts`) runs before `@camstack/system`
|
|
11
|
+
* is imported and is deliberately zero-dep, so it cannot own the restart — that
|
|
12
|
+
* lives in `@camstack/system`. This module is the ONLY owner of "stage a
|
|
13
|
+
* framework swap → reboot": every engine path that stages the framework (single
|
|
14
|
+
* `updateFrameworkPackage`, the "Update all" bulk job, and the auto-update
|
|
15
|
+
* scheduler) wires its `requestFrameworkSwap` dep to this helper and gets the
|
|
16
|
+
* reboot for free. The lifecycle engine assumes the process exits after
|
|
17
|
+
* `requestFrameworkSwap` returns; without the restart here the marker is written
|
|
18
|
+
* but never applied, silently stranding the update on disk.
|
|
19
|
+
*/
|
|
20
|
+
const system_1 = require("@camstack/system");
|
|
21
|
+
const launcher_framework_swap_js_1 = require("./launcher-framework-swap.js");
|
|
22
|
+
/** Delay before the hub exits — lets the in-flight startJob reply drain first. */
|
|
23
|
+
const FRAMEWORK_SWAP_RESTART_DELAY_MS = 500;
|
|
24
|
+
/**
|
|
25
|
+
* Write the pending-swap marker, then schedule the self-restart. The restart is
|
|
26
|
+
* UNCONDITIONAL: a staged framework swap is inert until the launcher applies it
|
|
27
|
+
* on reboot, so staging without restarting would silently lose the update.
|
|
28
|
+
*/
|
|
29
|
+
function requestFrameworkSwapAndRestart(dataDir, input, deps = {}) {
|
|
30
|
+
const writeMarker = deps.writeMarker ?? launcher_framework_swap_js_1.writePendingFrameworkSwap;
|
|
31
|
+
const scheduleRestart = deps.scheduleRestart ?? system_1.scheduleSelfRestart;
|
|
32
|
+
const now = deps.now ?? Date.now;
|
|
33
|
+
writeMarker(dataDir, {
|
|
34
|
+
jobId: input.jobId,
|
|
35
|
+
taskId: input.taskId,
|
|
36
|
+
packages: input.packages,
|
|
37
|
+
requestedAtMs: now(),
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
});
|
|
40
|
+
scheduleRestart({ delayMs: FRAMEWORK_SWAP_RESTART_DELAY_MS });
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BulkUpdateCoordinator = void 0;
|
|
4
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument -- The server installs @camstack/types@0.1.38 (last published) in server/backend/node_modules, while the workspace has 0.1.39 with the new BulkUpdate* types. ESLint's type-checker resolves against 0.1.38 and treats the new imports as `any`. Runtime is correct because Node module resolution walks up to root node_modules → workspace symlink. This disable mirrors the pattern in cap-providers.ts (same root cause). Will resolve when 0.1.39 is published and the local dist is synced. */
|
|
5
|
-
const node_crypto_1 = require("node:crypto");
|
|
6
|
-
const types_1 = require("@camstack/types");
|
|
7
|
-
const DEFAULT_CLEANUP_AFTER_MS = 5 * 60 * 1_000;
|
|
8
|
-
class BulkUpdateCoordinator {
|
|
9
|
-
deps;
|
|
10
|
-
states = new Map();
|
|
11
|
-
cancelFlags = new Map();
|
|
12
|
-
/**
|
|
13
|
-
* Tracks wall-clock time (ms) when each bulk completed. Used for lazy
|
|
14
|
-
* cleanup in `get()` — avoids scheduling a fake-timer `setTimeout` that
|
|
15
|
-
* would be eagerly fired by `vi.runAllTimersAsync()` in tests.
|
|
16
|
-
*/
|
|
17
|
-
completedWallMs = new Map();
|
|
18
|
-
/** Tracks which nodeIds currently have an active (non-completed) bulk update. */
|
|
19
|
-
activeNodeIds = new Set();
|
|
20
|
-
now;
|
|
21
|
-
cleanupAfterMs;
|
|
22
|
-
/** Wall-clock source. Fake timers intercept `Date.now`, so tests can advance via `advanceTimersByTimeAsync`. */
|
|
23
|
-
wallNow;
|
|
24
|
-
constructor(deps) {
|
|
25
|
-
this.deps = deps;
|
|
26
|
-
this.now = deps.clock ?? (() => Date.now());
|
|
27
|
-
this.cleanupAfterMs = deps.cleanupAfterMs ?? DEFAULT_CLEANUP_AFTER_MS;
|
|
28
|
-
this.wallNow = () => Date.now();
|
|
29
|
-
}
|
|
30
|
-
// ── Public API ────────────────────────────────────────────────────
|
|
31
|
-
start(input) {
|
|
32
|
-
if (this.activeNodeIds.has(input.nodeId)) {
|
|
33
|
-
throw new Error(`Bulk update already in progress for node ${input.nodeId}`);
|
|
34
|
-
}
|
|
35
|
-
const id = (0, node_crypto_1.randomUUID)();
|
|
36
|
-
const items = input.items.map((i) => ({
|
|
37
|
-
name: i.name,
|
|
38
|
-
isSystem: i.isSystem,
|
|
39
|
-
// fromVersion: the cap interface receives name+version+isSystem only;
|
|
40
|
-
// the caller (cap-providers.ts) may enrich this with the current version
|
|
41
|
-
// if available. Empty string is acceptable per plan spec.
|
|
42
|
-
fromVersion: '',
|
|
43
|
-
toVersion: i.version,
|
|
44
|
-
status: 'queued',
|
|
45
|
-
}));
|
|
46
|
-
const state = {
|
|
47
|
-
id,
|
|
48
|
-
nodeId: input.nodeId,
|
|
49
|
-
startedAtMs: this.now(),
|
|
50
|
-
total: items.length,
|
|
51
|
-
completed: 0,
|
|
52
|
-
failed: 0,
|
|
53
|
-
current: null,
|
|
54
|
-
phase: 'regular',
|
|
55
|
-
cancelled: false,
|
|
56
|
-
items,
|
|
57
|
-
};
|
|
58
|
-
this.states.set(id, state);
|
|
59
|
-
this.activeNodeIds.add(input.nodeId);
|
|
60
|
-
const cancelFlag = { cancelled: false };
|
|
61
|
-
this.cancelFlags.set(id, cancelFlag);
|
|
62
|
-
// Emit initial state so clients see the bulk as started immediately
|
|
63
|
-
this.emit(state);
|
|
64
|
-
void this.runLoop(id, cancelFlag).catch((err) => {
|
|
65
|
-
this.deps.logger.error('BulkUpdateCoordinator: loop crashed unexpectedly', err);
|
|
66
|
-
});
|
|
67
|
-
return { id };
|
|
68
|
-
}
|
|
69
|
-
get(id) {
|
|
70
|
-
const state = this.states.get(id);
|
|
71
|
-
if (state === undefined)
|
|
72
|
-
return null;
|
|
73
|
-
// Lazy cleanup: purge if the wall-clock elapsed since completion exceeds threshold.
|
|
74
|
-
// This avoids scheduling a long-lived setTimeout that would be eagerly fired
|
|
75
|
-
// by vi.runAllTimersAsync() in tests.
|
|
76
|
-
const completedWall = this.completedWallMs.get(id);
|
|
77
|
-
if (completedWall !== undefined && this.wallNow() - completedWall >= this.cleanupAfterMs) {
|
|
78
|
-
this.purge(id);
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
return state;
|
|
82
|
-
}
|
|
83
|
-
list(nodeId) {
|
|
84
|
-
const all = [...this.states.keys()]
|
|
85
|
-
.map((id) => this.get(id)) // get() applies lazy-cleanup
|
|
86
|
-
.filter((s) => s !== null);
|
|
87
|
-
return nodeId === undefined ? all : all.filter((s) => s.nodeId === nodeId);
|
|
88
|
-
}
|
|
89
|
-
cancel(id) {
|
|
90
|
-
const state = this.states.get(id);
|
|
91
|
-
const flag = this.cancelFlags.get(id);
|
|
92
|
-
if (state === undefined || flag === undefined)
|
|
93
|
-
return { cancelled: false };
|
|
94
|
-
// Once restarting, the hub restart is committed — cancel has no effect.
|
|
95
|
-
if (state.phase === 'restarting')
|
|
96
|
-
return { cancelled: false };
|
|
97
|
-
// Already completed.
|
|
98
|
-
if (state.completedAtMs !== undefined)
|
|
99
|
-
return { cancelled: false };
|
|
100
|
-
flag.cancelled = true;
|
|
101
|
-
this.mutate(id, (s) => ({ ...s, cancelled: true }));
|
|
102
|
-
return { cancelled: true };
|
|
103
|
-
}
|
|
104
|
-
// ── Internal loop ─────────────────────────────────────────────────
|
|
105
|
-
async runLoop(id, cancelFlag) {
|
|
106
|
-
const initial = this.states.get(id);
|
|
107
|
-
// ── Phase 1: regular addons ──────────────────────────────────────
|
|
108
|
-
this.transitionPhase(id, 'regular');
|
|
109
|
-
for (const item of initial.items.filter((i) => !i.isSystem)) {
|
|
110
|
-
if (cancelFlag.cancelled)
|
|
111
|
-
break;
|
|
112
|
-
await this.processItem(id, item, false);
|
|
113
|
-
}
|
|
114
|
-
// ── Phase 2: system packages (deferRestart: true) ────────────────
|
|
115
|
-
if (!cancelFlag.cancelled && initial.items.some((i) => i.isSystem)) {
|
|
116
|
-
this.transitionPhase(id, 'system');
|
|
117
|
-
for (const item of initial.items.filter((i) => i.isSystem)) {
|
|
118
|
-
if (cancelFlag.cancelled)
|
|
119
|
-
break;
|
|
120
|
-
await this.processItem(id, item, true);
|
|
121
|
-
}
|
|
122
|
-
// ── Phase 3: single restart ──────────────────────────────────
|
|
123
|
-
const anySystemPendingRestart = this.states
|
|
124
|
-
.get(id)
|
|
125
|
-
.items.some((i) => i.isSystem && i.status === 'done-pending-restart');
|
|
126
|
-
if (anySystemPendingRestart && !cancelFlag.cancelled) {
|
|
127
|
-
this.transitionPhase(id, 'restarting');
|
|
128
|
-
try {
|
|
129
|
-
await this.deps.restartServer({ confirm: true });
|
|
130
|
-
// NOTE: In production, restartServer kills+respawns the hub process.
|
|
131
|
-
// Code below this point will not execute in that scenario.
|
|
132
|
-
// If the mock/stub returns (e.g. in tests), we fall through to finalizing.
|
|
133
|
-
}
|
|
134
|
-
catch (err) {
|
|
135
|
-
// Restart failed but the npm installs already completed. Promote all
|
|
136
|
-
// done-pending-restart items to done with a caveat error so the UI
|
|
137
|
-
// can inform the user that a manual restart is needed.
|
|
138
|
-
this.deps.logger.error('BulkUpdateCoordinator: restart failed', err);
|
|
139
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
140
|
-
for (const it of this.states.get(id).items) {
|
|
141
|
-
if (it.status === 'done-pending-restart') {
|
|
142
|
-
this.setItemStatus(id, it.name, 'done', {
|
|
143
|
-
error: `Restart failed; manual restart required (${errMsg})`,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// ── Phase 4: finalize ────────────────────────────────────────────
|
|
151
|
-
// Reached when:
|
|
152
|
-
// a) no system packages at all, OR
|
|
153
|
-
// b) restart failed (process continued), OR
|
|
154
|
-
// c) cancelled before the restart phase.
|
|
155
|
-
this.transitionPhase(id, 'finalizing');
|
|
156
|
-
this.completeBulk(id);
|
|
157
|
-
}
|
|
158
|
-
async processItem(id, item, isSystem) {
|
|
159
|
-
this.setItemStatus(id, item.name, 'updating', { startedAtMs: this.now() });
|
|
160
|
-
this.mutate(id, (s) => ({ ...s, current: item.name }));
|
|
161
|
-
this.emit(this.states.get(id));
|
|
162
|
-
try {
|
|
163
|
-
if (isSystem) {
|
|
164
|
-
await this.deps.updateFrameworkPackage({
|
|
165
|
-
packageName: item.name,
|
|
166
|
-
version: item.toVersion,
|
|
167
|
-
deferRestart: true,
|
|
168
|
-
});
|
|
169
|
-
this.setItemStatus(id, item.name, 'done-pending-restart', { completedAtMs: this.now() });
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
await this.deps.updateAddon({ name: item.name, version: item.toVersion });
|
|
173
|
-
this.setItemStatus(id, item.name, 'done', { completedAtMs: this.now() });
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
catch (err) {
|
|
177
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
178
|
-
this.setItemStatus(id, item.name, 'failed', { error: msg, completedAtMs: this.now() });
|
|
179
|
-
}
|
|
180
|
-
this.mutate(id, (s) => ({ ...s, current: null }));
|
|
181
|
-
this.emit(this.states.get(id));
|
|
182
|
-
}
|
|
183
|
-
// ── State mutation helpers ────────────────────────────────────────
|
|
184
|
-
setItemStatus(id, name, status, fields = {}) {
|
|
185
|
-
this.mutate(id, (s) => {
|
|
186
|
-
const items = s.items.map((it) => (it.name === name ? { ...it, status, ...fields } : it));
|
|
187
|
-
// completed = all terminal states: done | done-pending-restart | failed
|
|
188
|
-
const completed = items.filter((it) => it.status === 'done' || it.status === 'done-pending-restart' || it.status === 'failed').length;
|
|
189
|
-
const failed = items.filter((it) => it.status === 'failed').length;
|
|
190
|
-
return { ...s, items, completed, failed };
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
transitionPhase(id, phase) {
|
|
194
|
-
this.mutate(id, (s) => ({ ...s, phase }));
|
|
195
|
-
this.emit(this.states.get(id));
|
|
196
|
-
}
|
|
197
|
-
completeBulk(id) {
|
|
198
|
-
this.mutate(id, (s) => ({ ...s, completedAtMs: this.now(), current: null }));
|
|
199
|
-
this.emit(this.states.get(id));
|
|
200
|
-
// Free the nodeId slot so a new bulk for the same node can be started
|
|
201
|
-
const nodeId = this.states.get(id).nodeId;
|
|
202
|
-
this.activeNodeIds.delete(nodeId);
|
|
203
|
-
// Record wall-clock completion time for lazy cleanup in `get()`.
|
|
204
|
-
// We intentionally avoid scheduling a setTimeout here: a long-lived
|
|
205
|
-
// setTimeout (5 min) would be eagerly fired by vi.runAllTimersAsync()
|
|
206
|
-
// in tests, causing `get()` to return null immediately after the run.
|
|
207
|
-
// Instead, `get()` lazily checks whether the cleanup threshold has
|
|
208
|
-
// elapsed using Date.now() — which fake timers DO advance via
|
|
209
|
-
// advanceTimersByTimeAsync(), making the cleanup testable without
|
|
210
|
-
// a long-running timer.
|
|
211
|
-
this.completedWallMs.set(id, this.wallNow());
|
|
212
|
-
}
|
|
213
|
-
purge(id) {
|
|
214
|
-
this.states.delete(id);
|
|
215
|
-
this.cancelFlags.delete(id);
|
|
216
|
-
this.completedWallMs.delete(id);
|
|
217
|
-
}
|
|
218
|
-
/** Immutably update the state for the given id. No-op if id is unknown. */
|
|
219
|
-
mutate(id, update) {
|
|
220
|
-
const current = this.states.get(id);
|
|
221
|
-
if (current === undefined)
|
|
222
|
-
return;
|
|
223
|
-
this.states.set(id, update(current));
|
|
224
|
-
}
|
|
225
|
-
emit(state) {
|
|
226
|
-
this.deps.eventBus.emit(types_1.EventCategory.AddonsBulkUpdateProgress, state);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
exports.BulkUpdateCoordinator = BulkUpdateCoordinator;
|