@empire-builder-kit/nx 1.0.0-rc.44 → 1.0.0-rc.45
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/package.json +3 -3
- package/src/blueprints/external-sandbox.d.ts +1 -1
- package/src/cli/main.d.ts +11 -5
- package/src/cli/main.js +88 -29
- package/src/container-images.d.ts +1 -1
- package/src/container-images.js +1 -1
- package/src/executors/deployment-plan/deployment-plan.js +5 -1
- package/src/executors/deployment-plan/schema.json +1 -1
- package/src/executors/fleet/fleet.js +4 -0
- package/src/executors/fleet/schema.json +1 -1
- package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
- package/src/executors/local-deploy/federated-credential-transport.js +15 -5
- package/src/foundation/cost-estimate.d.ts +8 -3
- package/src/foundation/cost-estimate.js +42 -19
- package/src/foundation/index.d.ts +1 -0
- package/src/foundation/index.js +1 -0
- package/src/foundation/local-private-smoke-access.js +6 -0
- package/src/foundation/logging.d.ts +3 -1
- package/src/foundation/logging.js +8 -1
- package/src/foundation/observability.d.ts +42 -0
- package/src/foundation/observability.js +181 -0
- package/src/foundation/preflight.js +1 -1
- package/src/generators/function/function.js +5 -2
- package/src/generators/observability-infrastructure.d.ts +2 -0
- package/src/generators/observability-infrastructure.js +31 -0
- package/src/generators/observability-policy.d.ts +4 -0
- package/src/generators/observability-policy.js +23 -0
- package/src/generators/preset/dependency-overrides.d.ts +17 -6
- package/src/generators/preset/dependency-overrides.js +17 -6
- package/src/generators/preset/tool-versions.d.ts +2 -2
- package/src/generators/preset/tool-versions.js +2 -2
- package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
- package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
- package/src/generators/preset/workspace-files-agent-development.js +42 -0
- package/src/generators/preset/workspace-files-thin-deploy.js +12 -3
- package/src/generators/preset/workspace-files-thin-docs.js +10 -22
- package/src/generators/preset/workspace-files.d.ts +1 -0
- package/src/generators/preset/workspace-files.js +89 -7
- package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
- package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
- package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
- package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
- package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
- package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
- package/src/generators/slice/built-in-plan-metadata-files.js +33 -1
- package/src/generators/slice/built-in-plan-operations-files.js +106 -6
- package/src/generators/slice/built-in-plan-project-files.js +1 -0
- package/src/generators/slice/built-in-plan-release-files.js +2 -43
- package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
- package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
- package/src/generators/slice/built-in-plan-shared.js +29 -2
- package/src/generators/slice/built-in-plan-web-files.js +18 -1
- package/src/generators/slice/built-in-plan.js +1 -0
- package/src/generators/slice/slice.js +2 -0
- package/src/generators/upgrade/upgrade.d.ts +4 -3
- package/src/generators/upgrade/upgrade.js +50 -26
- package/src/generators/workload/frozen-container.d.ts +1 -1
- package/src/generators/workload/workload.js +5 -1
- package/src/local-dev/dev-command-core.d.ts +24 -3
- package/src/local-dev/dev-command-core.js +124 -12
- package/src/local-dev/errors.d.ts +19 -1
- package/src/local-dev/errors.js +18 -1
- package/src/local-dev/fleet-client.d.ts +64 -6
- package/src/local-dev/fleet-client.js +189 -15
- package/src/local-dev/fleet-process.d.ts +47 -2
- package/src/local-dev/fleet-process.js +248 -31
- package/src/local-dev/fleet-runtime.d.ts +37 -0
- package/src/local-dev/fleet-runtime.js +127 -69
- package/src/local-dev/fleet-session.d.ts +1 -0
- package/src/local-dev/fleet-session.js +42 -3
- package/src/local-dev/fleet-state.d.ts +5 -0
- package/src/local-dev/fleet-state.js +11 -2
- package/src/local-dev/fleet-supervisor.d.ts +6 -0
- package/src/local-dev/fleet-supervisor.js +49 -1
- package/src/local-dev/slice-runner.d.ts +19 -1
- package/src/local-dev/slice-runner.js +290 -91
- package/src/repository-policy/deployment-plan.d.ts +1 -1
- package/src/repository-policy/deployment-plan.js +16 -1
- package/src/secrets/generated-aws-control-plane.d.ts +11 -1
- package/src/secrets/generated-aws-control-plane.js +92 -29
- package/src/secrets/operator.d.ts +3 -2
- package/src/secrets/operator.js +27 -5
- package/src/supply-chain/generated-files.js +9 -0
|
@@ -1,12 +1,59 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FLEET_OPERATION_TARGET_MS = void 0;
|
|
4
|
+
exports.fleetOperationProgressReporter = fleetOperationProgressReporter;
|
|
5
|
+
exports.fleetOperationFailureDetail = fleetOperationFailureDetail;
|
|
6
|
+
exports.foregroundUpSettlement = foregroundUpSettlement;
|
|
3
7
|
exports.executeFleetUpCore = executeFleetUpCore;
|
|
4
8
|
const node_crypto_1 = require("node:crypto");
|
|
5
9
|
const environment_inspector_js_1 = require("./environment-inspector.js");
|
|
6
10
|
const errors_js_1 = require("./errors.js");
|
|
7
11
|
const fleet_launcher_js_1 = require("./fleet-launcher.js");
|
|
8
12
|
const nx_workspace_js_1 = require("./nx-workspace.js");
|
|
9
|
-
|
|
13
|
+
/** Aspirational duration targets per Fleet command, taken from the measured
|
|
14
|
+
* two-Slice figures in docs/local-development.md (fresh up 334-442 s, down
|
|
15
|
+
* 242-345 s, restart about 70-85 s). Exceeding one prints a "slower than
|
|
16
|
+
* target" line; it never fails or stops observing an operation. */
|
|
17
|
+
exports.FLEET_OPERATION_TARGET_MS = Object.freeze({
|
|
18
|
+
down: 350_000,
|
|
19
|
+
restart: 90_000,
|
|
20
|
+
up: 450_000,
|
|
21
|
+
});
|
|
22
|
+
function progressClock(milliseconds) {
|
|
23
|
+
const seconds = Math.max(0, Math.floor(milliseconds / 1_000));
|
|
24
|
+
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
|
|
25
|
+
}
|
|
26
|
+
function describeSliceProgress(slice) {
|
|
27
|
+
const details = [
|
|
28
|
+
...(slice.cleanup === 'failed' ? ['cleanup failed'] : []),
|
|
29
|
+
...(slice.restartAttempts > 0
|
|
30
|
+
? [`restart attempt ${slice.restartAttempts}`]
|
|
31
|
+
: []),
|
|
32
|
+
];
|
|
33
|
+
return `${slice.name} ${slice.routeState}${details.length > 0 ? ` (${details.join(', ')})` : ''}`;
|
|
34
|
+
}
|
|
35
|
+
/** Progress lines for a watched Fleet operation; the first report past the
|
|
36
|
+
* command's target says so once, and observation continues. */
|
|
37
|
+
function fleetOperationProgressReporter(command, report) {
|
|
38
|
+
const targetMs = exports.FLEET_OPERATION_TARGET_MS[command];
|
|
39
|
+
let exceeded = false;
|
|
40
|
+
return ({ elapsedMs, operation, slices }) => {
|
|
41
|
+
const summary = slices.length > 0
|
|
42
|
+
? slices.map(describeSliceProgress).join(', ')
|
|
43
|
+
: `operation ${operation.state}`;
|
|
44
|
+
if (!exceeded && elapsedMs > targetMs) {
|
|
45
|
+
exceeded = true;
|
|
46
|
+
report(`[ebk dev ${command}] ${progressClock(elapsedMs)} slower than the ${progressClock(targetMs)} target for ${command}; still running: ${summary}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
report(`[ebk dev ${command}] ${progressClock(elapsedMs)} ${summary}`);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Human cause for a terminal failed operation record. */
|
|
53
|
+
function fleetOperationFailureDetail(operation) {
|
|
54
|
+
return operation.failureMessage ? ` Cause: ${operation.failureMessage}` : '';
|
|
55
|
+
}
|
|
56
|
+
function foregroundWait(launch, command, signal, idempotencyKey, reportCleanupFailure, reportProgress) {
|
|
10
57
|
if (!command.foreground || !launch.runtime)
|
|
11
58
|
return undefined;
|
|
12
59
|
const stop = async () => {
|
|
@@ -24,11 +71,15 @@ function foregroundWait(launch, command, signal, idempotencyKey, reportCleanupFa
|
|
|
24
71
|
}
|
|
25
72
|
}
|
|
26
73
|
if (down) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
74
|
+
// The supervisor lives in this process: a capped wait could only
|
|
75
|
+
// abandon its shutdown, so observe the down until it settles.
|
|
76
|
+
const operation = await launch.client.waitForOperationOutcome(down.operationId, reportProgress
|
|
77
|
+
? {
|
|
78
|
+
onProgress: fleetOperationProgressReporter('down', reportProgress),
|
|
79
|
+
}
|
|
80
|
+
: {});
|
|
30
81
|
if (operation.state !== 'succeeded') {
|
|
31
|
-
throw new errors_js_1.LocalDevContractError(operation.failureClassification ?? 'operation-conflict',
|
|
82
|
+
throw new errors_js_1.LocalDevContractError(operation.failureClassification ?? 'operation-conflict', `Foreground Fleet down did not complete successfully; ownership was retained.${fleetOperationFailureDetail(operation)}`);
|
|
32
83
|
}
|
|
33
84
|
}
|
|
34
85
|
await launch.client.shutdown(idempotencyKey());
|
|
@@ -46,6 +97,14 @@ function foregroundWait(launch, command, signal, idempotencyKey, reportCleanupFa
|
|
|
46
97
|
signal.removeEventListener('abort', onAbort);
|
|
47
98
|
});
|
|
48
99
|
}
|
|
100
|
+
const foregroundSettlements = new WeakMap();
|
|
101
|
+
/** Owned-cleanup settlement attached to an error from a foreground `up` whose
|
|
102
|
+
* in-process supervisor can outlive the failure. Callers report the error
|
|
103
|
+
* first, then keep interrupt handling installed until this settles, exactly as
|
|
104
|
+
* they do with `afterWrite` after a successful foreground `up`. */
|
|
105
|
+
function foregroundUpSettlement(error) {
|
|
106
|
+
return error instanceof Error ? foregroundSettlements.get(error) : undefined;
|
|
107
|
+
}
|
|
49
108
|
async function executeFleetUpCore(command, input, effects = {}) {
|
|
50
109
|
const slices = (0, nx_workspace_js_1.selectWorkspaceSlices)(input.plan, command.selector);
|
|
51
110
|
const inspect = effects.inspectEnvironment ?? environment_inspector_js_1.inspectDevelopmentEnvironment;
|
|
@@ -78,7 +137,8 @@ async function executeFleetUpCore(command, input, effects = {}) {
|
|
|
78
137
|
}
|
|
79
138
|
throw error;
|
|
80
139
|
}
|
|
81
|
-
const
|
|
140
|
+
const reportCleanupFailure = effects.reportCleanupFailure ?? (() => undefined);
|
|
141
|
+
const afterWrite = foregroundWait(launch, command, input.signal, idempotencyKey, reportCleanupFailure, effects.reportProgress);
|
|
82
142
|
if (command.noWait) {
|
|
83
143
|
return {
|
|
84
144
|
...(afterWrite ? { afterWrite } : {}),
|
|
@@ -88,21 +148,73 @@ async function executeFleetUpCore(command, input, effects = {}) {
|
|
|
88
148
|
operationId: submission.operationId,
|
|
89
149
|
};
|
|
90
150
|
}
|
|
91
|
-
|
|
151
|
+
if (command.foreground && command.timeoutMs !== undefined) {
|
|
152
|
+
effects.reportProgress?.('[ebk dev up] --timeout does not apply to a foreground up: its supervisor runs in this process, so the command watches until the outcome.');
|
|
153
|
+
}
|
|
154
|
+
let observed;
|
|
155
|
+
try {
|
|
156
|
+
// No default deadline: watch the supervised up until it settles. Only an
|
|
157
|
+
// explicit --timeout caps a detached wait, and in detached mode an
|
|
158
|
+
// interrupt ends observation; neither cancels the operation. A foreground
|
|
159
|
+
// up takes neither: its supervisor lives in this process, so a cap could
|
|
160
|
+
// only abandon it, and an interrupt instead drives the owned down above,
|
|
161
|
+
// which this wait then observes fail.
|
|
162
|
+
const operation = await launch.client.waitForOperationOutcome(submission.operationId, {
|
|
163
|
+
...(command.foreground || command.timeoutMs === undefined
|
|
164
|
+
? {}
|
|
165
|
+
: { timeoutMs: command.timeoutMs }),
|
|
166
|
+
...(command.foreground ? {} : { signal: input.signal }),
|
|
167
|
+
...(effects.reportProgress
|
|
168
|
+
? {
|
|
169
|
+
onProgress: fleetOperationProgressReporter('up', effects.reportProgress),
|
|
170
|
+
}
|
|
171
|
+
: {}),
|
|
172
|
+
});
|
|
173
|
+
observed =
|
|
174
|
+
operation.state === 'failed'
|
|
175
|
+
? { operation }
|
|
176
|
+
: { operation, slices: (await launch.client.status()).slices };
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (afterWrite && !input.signal.aborted) {
|
|
180
|
+
effects.reportProgress?.('[ebk dev up] The foreground Fleet supervisor keeps running in this process. Interrupt to run owned cleanup, or run ebk dev down --all from another terminal.');
|
|
181
|
+
}
|
|
182
|
+
throw holdForegroundSettlement(error, afterWrite);
|
|
183
|
+
}
|
|
184
|
+
const { operation } = observed;
|
|
92
185
|
if (operation.state === 'failed') {
|
|
93
|
-
|
|
94
|
-
|
|
186
|
+
// After a foreground interrupt the owned down and shutdown already run; a
|
|
187
|
+
// second shutdown here would only be refused while that down runs.
|
|
188
|
+
if (!launch.attached && !(afterWrite && input.signal.aborted)) {
|
|
189
|
+
await launch.client.shutdown(idempotencyKey()).catch((error) => {
|
|
190
|
+
reportCleanupFailure(`Fleet up failed and the supervisor it launched refused to shut down, so it keeps running and keeps its claims: ${error instanceof Error ? error.message : String(error)} ${afterWrite
|
|
191
|
+
? 'It runs in this process: interrupt to retry owned cleanup, or run ebk dev down --all from another terminal.'
|
|
192
|
+
: 'Check ebk dev status, then run ebk dev down --all to retry cleanup.'}`);
|
|
193
|
+
});
|
|
95
194
|
}
|
|
96
|
-
throw new errors_js_1.LocalDevContractError(operation.failureClassification ?? 'internal-error', `Fleet up failed after transactional rollback (${operation.failureClassification ?? 'internal-error'})
|
|
195
|
+
throw holdForegroundSettlement(new errors_js_1.LocalDevContractError(operation.failureClassification ?? 'internal-error', `Fleet up failed after transactional rollback (${operation.failureClassification ?? 'internal-error'}).${fleetOperationFailureDetail(operation)}`), afterWrite);
|
|
97
196
|
}
|
|
98
|
-
const state = await launch.client.status();
|
|
99
197
|
return {
|
|
100
198
|
...(afterWrite ? { afterWrite } : {}),
|
|
101
199
|
attached: launch.attached,
|
|
102
200
|
fleetId: launch.fleetId,
|
|
103
201
|
operation,
|
|
104
202
|
operationId: submission.operationId,
|
|
105
|
-
slices:
|
|
203
|
+
slices: observed.slices,
|
|
106
204
|
};
|
|
107
205
|
}
|
|
206
|
+
/** A foreground supervisor lives in this process and can outlive a failed
|
|
207
|
+
* wait: a refused shutdown keeps it, and an interrupt's owned down may still
|
|
208
|
+
* run. Attach its settlement so the caller reports the failure, then holds
|
|
209
|
+
* interrupt handling until owned cleanup settles instead of leaving Node's
|
|
210
|
+
* default handler to kill the process. */
|
|
211
|
+
function holdForegroundSettlement(error, afterWrite) {
|
|
212
|
+
if (!afterWrite)
|
|
213
|
+
return error;
|
|
214
|
+
const failure = error instanceof Error
|
|
215
|
+
? error
|
|
216
|
+
: new errors_js_1.LocalDevContractError('internal-error', String(error));
|
|
217
|
+
foregroundSettlements.set(failure, afterWrite);
|
|
218
|
+
return failure;
|
|
219
|
+
}
|
|
108
220
|
//# sourceMappingURL=dev-command-core.js.map
|
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
export declare const LOCAL_DEV_FAILURE_CLASSIFICATIONS: readonly ["invalid-input", "personal-identity-unavailable", "fleet-in-use", "fleet-ownership-ambiguous", "local-context-missing", "local-context-invalid", "local-context-conflict", "unknown-slice", "invalid-adapter", "slice-active-elsewhere", "slice-claim-ambiguous", "not-running", "invalid-state", "configuration-mismatch", "protocol-mismatch", "environment-deficient", "prerequisite-unavailable", "foundation-unavailable", "operation-conflict", "readiness-timeout", "provisioning-timeout", "startup-interrupted", "cleanup-incomplete", "supervisor-unreachable", "stale-state", "stale-state-ambiguous", "log-query-invalid", "internal-error"];
|
|
1
|
+
export declare const LOCAL_DEV_FAILURE_CLASSIFICATIONS: readonly ["invalid-input", "personal-identity-unavailable", "fleet-in-use", "fleet-ownership-ambiguous", "local-context-missing", "local-context-invalid", "local-context-conflict", "unknown-slice", "invalid-adapter", "slice-active-elsewhere", "slice-claim-ambiguous", "not-running", "invalid-state", "configuration-mismatch", "protocol-mismatch", "environment-deficient", "prerequisite-unavailable", "foundation-unavailable", "operation-conflict", "readiness-timeout", "readiness-stalled", "health-check-failing", "provisioning-timeout", "process-exited", "startup-interrupted", "observation-timeout", "observation-interrupted", "cleanup-incomplete", "supervisor-unreachable", "stale-state", "stale-state-ambiguous", "log-query-invalid", "internal-error"];
|
|
2
2
|
export type LocalDevFailureClassification = (typeof LOCAL_DEV_FAILURE_CLASSIFICATIONS)[number];
|
|
3
3
|
export declare class LocalDevContractError extends Error {
|
|
4
4
|
readonly classification: LocalDevFailureClassification;
|
|
5
5
|
constructor(classification: LocalDevFailureClassification, message: string);
|
|
6
6
|
}
|
|
7
|
+
/** What the client knew when it stopped watching an accepted operation that
|
|
8
|
+
* had not settled. The operation itself may still be running. */
|
|
9
|
+
export interface FleetObservationDetail {
|
|
10
|
+
elapsedMs: number;
|
|
11
|
+
/** The last non-terminal state read; absent when no read succeeded. */
|
|
12
|
+
lastObservedState?: 'accepted' | 'running';
|
|
13
|
+
operationId: string;
|
|
14
|
+
/** The explicit cap, present only for `observation-timeout`. */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Observation of an accepted operation ended without a terminal record: at an
|
|
18
|
+
* explicit cap, on interruption, or because the supervisor stopped answering.
|
|
19
|
+
* The CLI renders `detail` in the failure envelope, with the operation ID at
|
|
20
|
+
* the envelope level, so a script can keep following the same operation. */
|
|
21
|
+
export declare class FleetObservationError extends LocalDevContractError {
|
|
22
|
+
readonly detail: FleetObservationDetail;
|
|
23
|
+
constructor(classification: 'observation-interrupted' | 'observation-timeout' | 'supervisor-unreachable', message: string, detail: FleetObservationDetail);
|
|
24
|
+
}
|
package/src/local-dev/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LocalDevContractError = exports.LOCAL_DEV_FAILURE_CLASSIFICATIONS = void 0;
|
|
3
|
+
exports.FleetObservationError = exports.LocalDevContractError = exports.LOCAL_DEV_FAILURE_CLASSIFICATIONS = void 0;
|
|
4
4
|
exports.LOCAL_DEV_FAILURE_CLASSIFICATIONS = [
|
|
5
5
|
'invalid-input',
|
|
6
6
|
'personal-identity-unavailable',
|
|
@@ -22,8 +22,13 @@ exports.LOCAL_DEV_FAILURE_CLASSIFICATIONS = [
|
|
|
22
22
|
'foundation-unavailable',
|
|
23
23
|
'operation-conflict',
|
|
24
24
|
'readiness-timeout',
|
|
25
|
+
'readiness-stalled',
|
|
26
|
+
'health-check-failing',
|
|
25
27
|
'provisioning-timeout',
|
|
28
|
+
'process-exited',
|
|
26
29
|
'startup-interrupted',
|
|
30
|
+
'observation-timeout',
|
|
31
|
+
'observation-interrupted',
|
|
27
32
|
'cleanup-incomplete',
|
|
28
33
|
'supervisor-unreachable',
|
|
29
34
|
'stale-state',
|
|
@@ -40,4 +45,16 @@ class LocalDevContractError extends Error {
|
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
exports.LocalDevContractError = LocalDevContractError;
|
|
48
|
+
/** Observation of an accepted operation ended without a terminal record: at an
|
|
49
|
+
* explicit cap, on interruption, or because the supervisor stopped answering.
|
|
50
|
+
* The CLI renders `detail` in the failure envelope, with the operation ID at
|
|
51
|
+
* the envelope level, so a script can keep following the same operation. */
|
|
52
|
+
class FleetObservationError extends LocalDevContractError {
|
|
53
|
+
detail;
|
|
54
|
+
constructor(classification, message, detail) {
|
|
55
|
+
super(classification, message);
|
|
56
|
+
this.detail = detail;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.FleetObservationError = FleetObservationError;
|
|
43
60
|
//# sourceMappingURL=errors.js.map
|
|
@@ -1,19 +1,67 @@
|
|
|
1
1
|
import { type FleetMutationCommand } from './control-contract.js';
|
|
2
|
+
import { LocalDevContractError } from './errors.js';
|
|
2
3
|
import { sendFleetControlRequest } from './fleet-ipc.js';
|
|
3
|
-
import { type FleetOperationStateRecord, type FleetStateRecord } from './fleet-state.js';
|
|
4
|
+
import { type FleetOperationStateRecord, type FleetSliceState, type FleetStateRecord } from './fleet-state.js';
|
|
4
5
|
import { type PrivateStateSecurity } from './private-state.js';
|
|
5
6
|
import type { FleetRuntimePaths } from './runtime-paths.js';
|
|
6
7
|
export interface FleetClientEffects {
|
|
8
|
+
/** Monotonic-enough wall clock for observation bookkeeping (tests inject). */
|
|
9
|
+
now?: () => number;
|
|
7
10
|
requestId?: () => string;
|
|
8
11
|
send?: typeof sendFleetControlRequest;
|
|
12
|
+
/** Poll pause; resolves early when the signal aborts (tests inject). */
|
|
13
|
+
sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
|
|
9
14
|
}
|
|
15
|
+
/** A supervisor whose authenticated status keeps answering with the same
|
|
16
|
+
* heartbeat for this long is not making progress. The supervisor writes its
|
|
17
|
+
* heartbeat every 5 s; Windows identity captures can block its loop longer.
|
|
18
|
+
* The window must also span enough consecutive status reads (see
|
|
19
|
+
* `waitForOperationOutcome`), so a host sleep that jumps the clock cannot
|
|
20
|
+
* trip it before the supervisor's next heartbeat. */
|
|
21
|
+
export declare const FLEET_SUPERVISOR_HEARTBEAT_STALE_MS: Readonly<{
|
|
22
|
+
default: 30000;
|
|
23
|
+
win32: 60000;
|
|
24
|
+
}>;
|
|
25
|
+
/** Defaults for watching a supervised operation. None of them is an operation
|
|
26
|
+
* deadline: without an explicit cap the client waits for the real outcome. */
|
|
27
|
+
export declare const FLEET_OPERATION_OBSERVATION: Readonly<{
|
|
28
|
+
pollMs: 200;
|
|
29
|
+
/** Progress is reported on every Slice change and at least this often. */
|
|
30
|
+
progressIntervalMs: 30000;
|
|
31
|
+
/** Status (heartbeat and Slice progress) is read at most this often. */
|
|
32
|
+
statusIntervalMs: 2000;
|
|
33
|
+
/** Consecutive unreachable polls tolerated before the wait fails. */
|
|
34
|
+
unreachableLimit: 6;
|
|
35
|
+
}>;
|
|
36
|
+
export interface FleetOperationProgress {
|
|
37
|
+
elapsedMs: number;
|
|
38
|
+
operation: FleetOperationStateRecord;
|
|
39
|
+
/** Live state of the operation's Slices from the latest status read. */
|
|
40
|
+
slices: FleetSliceState[];
|
|
41
|
+
}
|
|
42
|
+
export interface FleetOperationWaitOptions {
|
|
43
|
+
onProgress?: (progress: FleetOperationProgress) => void;
|
|
44
|
+
pollMs?: number;
|
|
45
|
+
/** Ends observation (never the operation) when aborted. */
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
/** Explicit observation cap. Absent means wait for the terminal state. */
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
}
|
|
50
|
+
export interface FleetOperationObservationTuning {
|
|
51
|
+
progressIntervalMs?: number;
|
|
52
|
+
staleHeartbeatMs?: number;
|
|
53
|
+
statusIntervalMs?: number;
|
|
54
|
+
unreachableLimit?: number;
|
|
55
|
+
}
|
|
56
|
+
/** Terminal failure as an error that keeps the supervisor's persisted cause. */
|
|
57
|
+
export declare function fleetOperationFailure(operation: FleetOperationStateRecord): LocalDevContractError;
|
|
10
58
|
export interface FleetOperationSubmission {
|
|
11
59
|
operationId: string;
|
|
12
60
|
state: 'accepted';
|
|
13
61
|
}
|
|
14
62
|
export declare class FleetClient {
|
|
15
63
|
#private;
|
|
16
|
-
constructor(paths: FleetRuntimePaths, security?: PrivateStateSecurity, effects?: FleetClientEffects);
|
|
64
|
+
constructor(paths: FleetRuntimePaths, security?: PrivateStateSecurity, effects?: FleetClientEffects, observation?: FleetOperationObservationTuning);
|
|
17
65
|
ping(): Promise<{
|
|
18
66
|
fleetId: string;
|
|
19
67
|
protocolVersion: number;
|
|
@@ -25,9 +73,19 @@ export declare class FleetClient {
|
|
|
25
73
|
slices: readonly string[];
|
|
26
74
|
}, idempotencyKey?: `${string}-${string}-${string}-${string}-${string}`): Promise<FleetOperationSubmission>;
|
|
27
75
|
operationStatus(operationId: string): Promise<FleetOperationStateRecord>;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
76
|
+
/** Wait for a terminal operation record, `succeeded` or `failed`.
|
|
77
|
+
*
|
|
78
|
+
* There is no default deadline: an operation that is still progressing is
|
|
79
|
+
* observed until it settles. The wait ends early only on an explicit
|
|
80
|
+
* `timeoutMs` cap (`observation-timeout`), on `signal` abort
|
|
81
|
+
* (`observation-interrupted`), or when the supervisor itself stops making
|
|
82
|
+
* progress (`supervisor-unreachable`: repeated unreachable polls or a
|
|
83
|
+
* heartbeat that no longer advances). Ending observation never cancels the
|
|
84
|
+
* supervised operation.
|
|
85
|
+
*/
|
|
86
|
+
waitForOperationOutcome(operationId: string, options?: FleetOperationWaitOptions): Promise<FleetOperationStateRecord>;
|
|
87
|
+
/** Throwing form: resolves only on `succeeded`; a terminal `failed` record
|
|
88
|
+
* becomes an error carrying its persisted classification and cause. */
|
|
89
|
+
waitForOperation(operationId: string, options?: FleetOperationWaitOptions): Promise<FleetOperationStateRecord>;
|
|
32
90
|
shutdown(idempotencyKey?: `${string}-${string}-${string}-${string}-${string}`): Promise<void>;
|
|
33
91
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FleetClient = void 0;
|
|
3
|
+
exports.FleetClient = exports.FLEET_OPERATION_OBSERVATION = exports.FLEET_SUPERVISOR_HEARTBEAT_STALE_MS = void 0;
|
|
4
|
+
exports.fleetOperationFailure = fleetOperationFailure;
|
|
4
5
|
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
const control_contract_js_1 = require("./control-contract.js");
|
|
6
7
|
const contract_validation_js_1 = require("./contract-validation.js");
|
|
@@ -8,6 +9,56 @@ const errors_js_1 = require("./errors.js");
|
|
|
8
9
|
const fleet_ipc_js_1 = require("./fleet-ipc.js");
|
|
9
10
|
const fleet_state_js_1 = require("./fleet-state.js");
|
|
10
11
|
const private_state_js_1 = require("./private-state.js");
|
|
12
|
+
/** A supervisor whose authenticated status keeps answering with the same
|
|
13
|
+
* heartbeat for this long is not making progress. The supervisor writes its
|
|
14
|
+
* heartbeat every 5 s; Windows identity captures can block its loop longer.
|
|
15
|
+
* The window must also span enough consecutive status reads (see
|
|
16
|
+
* `waitForOperationOutcome`), so a host sleep that jumps the clock cannot
|
|
17
|
+
* trip it before the supervisor's next heartbeat. */
|
|
18
|
+
exports.FLEET_SUPERVISOR_HEARTBEAT_STALE_MS = Object.freeze({
|
|
19
|
+
default: 30_000,
|
|
20
|
+
win32: 60_000,
|
|
21
|
+
});
|
|
22
|
+
/** Defaults for watching a supervised operation. None of them is an operation
|
|
23
|
+
* deadline: without an explicit cap the client waits for the real outcome. */
|
|
24
|
+
exports.FLEET_OPERATION_OBSERVATION = Object.freeze({
|
|
25
|
+
pollMs: 200,
|
|
26
|
+
/** Progress is reported on every Slice change and at least this often. */
|
|
27
|
+
progressIntervalMs: 30_000,
|
|
28
|
+
/** Status (heartbeat and Slice progress) is read at most this often. */
|
|
29
|
+
statusIntervalMs: 2_000,
|
|
30
|
+
/** Consecutive unreachable polls tolerated before the wait fails. */
|
|
31
|
+
unreachableLimit: 6,
|
|
32
|
+
});
|
|
33
|
+
function defaultSleep(milliseconds, signal) {
|
|
34
|
+
return new Promise((resolvePromise) => {
|
|
35
|
+
if (signal?.aborted) {
|
|
36
|
+
resolvePromise();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const settle = () => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
signal?.removeEventListener('abort', settle);
|
|
42
|
+
resolvePromise();
|
|
43
|
+
};
|
|
44
|
+
const timer = setTimeout(settle, milliseconds);
|
|
45
|
+
signal?.addEventListener('abort', settle, { once: true });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function formatObservedDuration(milliseconds) {
|
|
49
|
+
if (milliseconds < 1_000)
|
|
50
|
+
return `${milliseconds}ms`;
|
|
51
|
+
const seconds = Math.round(milliseconds / 1_000);
|
|
52
|
+
const minutes = Math.floor(seconds / 60);
|
|
53
|
+
return minutes === 0
|
|
54
|
+
? `${seconds}s`
|
|
55
|
+
: `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;
|
|
56
|
+
}
|
|
57
|
+
/** Terminal failure as an error that keeps the supervisor's persisted cause. */
|
|
58
|
+
function fleetOperationFailure(operation) {
|
|
59
|
+
const classification = operation.failureClassification ?? 'internal-error';
|
|
60
|
+
return new errors_js_1.LocalDevContractError(classification, `Fleet ${operation.command} operation ${operation.id} failed (${classification})${operation.failureMessage ? `: ${operation.failureMessage}` : '.'}`);
|
|
61
|
+
}
|
|
11
62
|
function clientFailure(response) {
|
|
12
63
|
if (!('error' in response)) {
|
|
13
64
|
throw new errors_js_1.LocalDevContractError('invalid-state', 'Expected a Fleet control failure response.');
|
|
@@ -67,16 +118,33 @@ function validateOperation(value) {
|
|
|
67
118
|
}
|
|
68
119
|
class FleetClient {
|
|
69
120
|
#capability;
|
|
121
|
+
#now;
|
|
122
|
+
#observation;
|
|
70
123
|
#paths;
|
|
71
124
|
#requestId;
|
|
72
125
|
#send;
|
|
73
|
-
|
|
126
|
+
#sleep;
|
|
127
|
+
constructor(paths, security = {}, effects = {}, observation = {}) {
|
|
74
128
|
this.#paths = paths;
|
|
75
129
|
this.#capability = (0, private_state_js_1.readPrivateFile)(paths.capability, security)
|
|
76
130
|
.toString('utf8')
|
|
77
131
|
.trim();
|
|
132
|
+
this.#now = effects.now ?? (() => Date.now());
|
|
78
133
|
this.#requestId = effects.requestId ?? (() => (0, node_crypto_1.randomUUID)());
|
|
79
134
|
this.#send = effects.send ?? fleet_ipc_js_1.sendFleetControlRequest;
|
|
135
|
+
this.#sleep = effects.sleep ?? defaultSleep;
|
|
136
|
+
this.#observation = {
|
|
137
|
+
progressIntervalMs: observation.progressIntervalMs ??
|
|
138
|
+
exports.FLEET_OPERATION_OBSERVATION.progressIntervalMs,
|
|
139
|
+
staleHeartbeatMs: observation.staleHeartbeatMs ??
|
|
140
|
+
(process.platform === 'win32'
|
|
141
|
+
? exports.FLEET_SUPERVISOR_HEARTBEAT_STALE_MS.win32
|
|
142
|
+
: exports.FLEET_SUPERVISOR_HEARTBEAT_STALE_MS.default),
|
|
143
|
+
statusIntervalMs: observation.statusIntervalMs ??
|
|
144
|
+
exports.FLEET_OPERATION_OBSERVATION.statusIntervalMs,
|
|
145
|
+
unreachableLimit: observation.unreachableLimit ??
|
|
146
|
+
exports.FLEET_OPERATION_OBSERVATION.unreachableLimit,
|
|
147
|
+
};
|
|
80
148
|
}
|
|
81
149
|
async #request(request) {
|
|
82
150
|
const response = await this.#send(this.#paths.controlSocket, {
|
|
@@ -128,22 +196,128 @@ class FleetClient {
|
|
|
128
196
|
payload: { operationId },
|
|
129
197
|
}));
|
|
130
198
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
199
|
+
/** Wait for a terminal operation record, `succeeded` or `failed`.
|
|
200
|
+
*
|
|
201
|
+
* There is no default deadline: an operation that is still progressing is
|
|
202
|
+
* observed until it settles. The wait ends early only on an explicit
|
|
203
|
+
* `timeoutMs` cap (`observation-timeout`), on `signal` abort
|
|
204
|
+
* (`observation-interrupted`), or when the supervisor itself stops making
|
|
205
|
+
* progress (`supervisor-unreachable`: repeated unreachable polls or a
|
|
206
|
+
* heartbeat that no longer advances). Ending observation never cancels the
|
|
207
|
+
* supervised operation.
|
|
208
|
+
*/
|
|
209
|
+
async waitForOperationOutcome(operationId, options = {}) {
|
|
210
|
+
const pollMs = options.pollMs ?? exports.FLEET_OPERATION_OBSERVATION.pollMs;
|
|
211
|
+
const tuning = this.#observation;
|
|
212
|
+
const startedAt = this.#now();
|
|
213
|
+
const deadline = options.timeoutMs === undefined
|
|
214
|
+
? undefined
|
|
215
|
+
: startedAt + options.timeoutMs;
|
|
216
|
+
let latest;
|
|
217
|
+
let unreachable = 0;
|
|
218
|
+
let nextStatusAt = startedAt + tuning.statusIntervalMs;
|
|
219
|
+
// A heartbeat is stale only when it stays unchanged both for the stale
|
|
220
|
+
// window by the clock and across enough consecutive status reads to cover
|
|
221
|
+
// that window at the status interval. After a host sleep the clock jumps
|
|
222
|
+
// at once, but the reads still have to accumulate, which gives the
|
|
223
|
+
// supervisor time for its next heartbeat.
|
|
224
|
+
const staleReadLimit = Math.max(1, Math.ceil(tuning.staleHeartbeatMs / Math.max(1, tuning.statusIntervalMs)));
|
|
225
|
+
let heartbeat;
|
|
226
|
+
let reportedSummary;
|
|
227
|
+
let reportedAt = startedAt;
|
|
228
|
+
const label = () => `Fleet ${latest ? `${latest.command} ` : ''}operation ${operationId}`;
|
|
229
|
+
const ended = (classification, message, elapsedMs = this.#now() - startedAt) => new errors_js_1.FleetObservationError(classification, message, {
|
|
230
|
+
elapsedMs,
|
|
231
|
+
...(latest
|
|
232
|
+
? {
|
|
233
|
+
lastObservedState: latest.state,
|
|
234
|
+
}
|
|
235
|
+
: {}),
|
|
236
|
+
operationId,
|
|
237
|
+
...(classification === 'observation-timeout'
|
|
238
|
+
? { timeoutMs: options.timeoutMs }
|
|
239
|
+
: {}),
|
|
240
|
+
});
|
|
241
|
+
while (true) {
|
|
242
|
+
if (options.signal?.aborted) {
|
|
243
|
+
const elapsedMs = this.#now() - startedAt;
|
|
244
|
+
throw ended('observation-interrupted', `Stopped watching ${label()} after ${formatObservedDuration(elapsedMs)} on interruption; the supervised operation continues. Check ebk dev status and ebk dev logs before retrying.`, elapsedMs);
|
|
245
|
+
}
|
|
246
|
+
if (deadline !== undefined && this.#now() >= deadline) {
|
|
247
|
+
throw ended('observation-timeout', `Stopped watching ${label()} after the explicit ${formatObservedDuration(options.timeoutMs)} --timeout cap; it was last ${latest?.state ?? 'unobserved'} and continues in the supervisor. Check ebk dev status and ebk dev logs before retrying.`);
|
|
248
|
+
}
|
|
249
|
+
let operation;
|
|
250
|
+
try {
|
|
251
|
+
operation = await this.operationStatus(operationId);
|
|
252
|
+
unreachable = 0;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (!(error instanceof errors_js_1.LocalDevContractError) ||
|
|
256
|
+
error.classification !== 'supervisor-unreachable')
|
|
257
|
+
throw error;
|
|
258
|
+
unreachable += 1;
|
|
259
|
+
if (unreachable >= tuning.unreachableLimit) {
|
|
260
|
+
throw ended('supervisor-unreachable', `${label()} could not be observed: the supervisor did not answer ${unreachable} consecutive polls (${error.message}). Check ebk dev status and ebk dev ps before retrying.`);
|
|
261
|
+
}
|
|
262
|
+
await this.#pause(Math.min(1_000 * 2 ** (unreachable - 1), 5_000), deadline, options.signal);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (operation.state === 'succeeded' || operation.state === 'failed')
|
|
138
266
|
return operation;
|
|
139
|
-
|
|
140
|
-
|
|
267
|
+
latest = operation;
|
|
268
|
+
const now = this.#now();
|
|
269
|
+
if (now >= nextStatusAt) {
|
|
270
|
+
nextStatusAt = now + tuning.statusIntervalMs;
|
|
271
|
+
// Status is advisory: a refused or malformed read (for example while
|
|
272
|
+
// the supervisor is shutting down) never ends the wait by itself.
|
|
273
|
+
const state = await this.status().catch(() => undefined);
|
|
274
|
+
if (state) {
|
|
275
|
+
const observedAt = this.#now();
|
|
276
|
+
if (heartbeat?.value !== state.heartbeatAt) {
|
|
277
|
+
heartbeat = { observedAt, staleReads: 0, value: state.heartbeatAt };
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
heartbeat.staleReads += 1;
|
|
281
|
+
if (heartbeat.staleReads >= staleReadLimit &&
|
|
282
|
+
observedAt - heartbeat.observedAt > tuning.staleHeartbeatMs) {
|
|
283
|
+
throw ended('supervisor-unreachable', `${label()} could not be observed: the supervisor heartbeat has not advanced since ${state.heartbeatAt} (${formatObservedDuration(observedAt - heartbeat.observedAt)}, ${heartbeat.staleReads} consecutive status reads). Check ebk dev status and ebk dev ps before retrying.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (options.onProgress) {
|
|
287
|
+
const slices = state.slices.filter(({ name }) => operation.slices.includes(name));
|
|
288
|
+
const summary = JSON.stringify(slices.map(({ cleanup, name, restartAttempts, routeState }) => [
|
|
289
|
+
name,
|
|
290
|
+
routeState,
|
|
291
|
+
cleanup,
|
|
292
|
+
restartAttempts,
|
|
293
|
+
]));
|
|
294
|
+
if (summary !== reportedSummary ||
|
|
295
|
+
observedAt - reportedAt >= tuning.progressIntervalMs) {
|
|
296
|
+
reportedSummary = summary;
|
|
297
|
+
reportedAt = observedAt;
|
|
298
|
+
options.onProgress({
|
|
299
|
+
elapsedMs: observedAt - startedAt,
|
|
300
|
+
operation,
|
|
301
|
+
slices,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
141
306
|
}
|
|
142
|
-
await
|
|
143
|
-
setTimeout(resolvePromise, pollMs);
|
|
144
|
-
});
|
|
307
|
+
await this.#pause(pollMs, deadline, options.signal);
|
|
145
308
|
}
|
|
146
|
-
|
|
309
|
+
}
|
|
310
|
+
/** Throwing form: resolves only on `succeeded`; a terminal `failed` record
|
|
311
|
+
* becomes an error carrying its persisted classification and cause. */
|
|
312
|
+
async waitForOperation(operationId, options = {}) {
|
|
313
|
+
const operation = await this.waitForOperationOutcome(operationId, options);
|
|
314
|
+
if (operation.state === 'failed')
|
|
315
|
+
throw fleetOperationFailure(operation);
|
|
316
|
+
return operation;
|
|
317
|
+
}
|
|
318
|
+
async #pause(milliseconds, deadline, signal) {
|
|
319
|
+
const remaining = deadline === undefined ? milliseconds : deadline - this.#now();
|
|
320
|
+
await this.#sleep(Math.max(0, Math.min(milliseconds, remaining)), signal);
|
|
147
321
|
}
|
|
148
322
|
async shutdown(idempotencyKey = (0, node_crypto_1.randomUUID)()) {
|
|
149
323
|
await this.#request({
|