@hypequery/deployment 0.2.0 → 0.4.0

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.
@@ -0,0 +1,187 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, open } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { prepareProtocolDeploymentReleaseEnvelope } from '@hypequery/protocol';
6
+ import { validateDeploymentActivationRecord } from './activation.js';
7
+ import { verifyDeploymentBundle, } from './bundle.js';
8
+ const DEFAULT_STABILITY_ATTEMPTS = 4;
9
+ const MAX_STABILITY_ATTEMPTS = 16;
10
+ export class DeploymentRuntimeMaterializationError extends Error {
11
+ code;
12
+ constructor(code, message, options = {}) {
13
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
14
+ this.name = 'DeploymentRuntimeMaterializationError';
15
+ this.code = code;
16
+ }
17
+ }
18
+ function materializationError(code, message, cause) {
19
+ return new DeploymentRuntimeMaterializationError(code, message, { cause });
20
+ }
21
+ function sameTarget(left, right) {
22
+ return left.project === right.project && left.environment === right.environment;
23
+ }
24
+ function sha256(bytes) {
25
+ return createHash('sha256').update(bytes).digest('hex');
26
+ }
27
+ async function readArtifact(bundleDirectory, artifact) {
28
+ const artifactPath = path.join(bundleDirectory, ...artifact.path.split('/'));
29
+ let handle;
30
+ try {
31
+ const initial = await lstat(artifactPath);
32
+ if (initial.isSymbolicLink() || !initial.isFile() || initial.size !== artifact.byteLength) {
33
+ throw new Error('Runtime artifact is not the declared bounded regular file.');
34
+ }
35
+ handle = await open(artifactPath, constants.O_RDONLY | constants.O_NOFOLLOW);
36
+ const stat = await handle.stat();
37
+ if (!stat.isFile() || stat.size !== artifact.byteLength) {
38
+ throw new Error('Runtime artifact changed before materialization.');
39
+ }
40
+ const bytes = await handle.readFile();
41
+ if (bytes.byteLength !== artifact.byteLength || sha256(bytes) !== artifact.sha256) {
42
+ throw new Error('Runtime artifact bytes do not match the closed bundle manifest.');
43
+ }
44
+ await handle.close();
45
+ handle = undefined;
46
+ return Uint8Array.from(bytes);
47
+ }
48
+ catch (error) {
49
+ try {
50
+ await handle?.close();
51
+ }
52
+ catch {
53
+ // Preserve the validation failure that made this artifact unusable.
54
+ }
55
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_ARTIFACT_INVALID', 'A deployment runtime artifact could not be materialized.', error);
56
+ }
57
+ }
58
+ function artifactSnapshot(input, bytes) {
59
+ const materialized = Uint8Array.from(bytes);
60
+ return Object.freeze({
61
+ runtime: input.runtime,
62
+ artifactSha256: input.artifactSha256,
63
+ byteLength: materialized.byteLength,
64
+ entrypoints: Object.freeze([...input.entrypoints]),
65
+ read: () => Uint8Array.from(materialized),
66
+ });
67
+ }
68
+ async function materializeActivation(activation, releases) {
69
+ let stored;
70
+ try {
71
+ stored = await releases.read(activation.releaseIdentity);
72
+ }
73
+ catch (error) {
74
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_RELEASE_INVALID', 'The active deployment release could not be revalidated.', error);
75
+ }
76
+ if (!stored) {
77
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_RELEASE_NOT_FOUND', 'The active deployment release was not found.');
78
+ }
79
+ let release;
80
+ let releaseIdentity;
81
+ try {
82
+ const prepared = prepareProtocolDeploymentReleaseEnvelope(stored.release);
83
+ release = prepared.release;
84
+ releaseIdentity = prepared.identity;
85
+ }
86
+ catch (error) {
87
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_RELEASE_INVALID', 'The active deployment release envelope is invalid.', error);
88
+ }
89
+ if (stored.releaseIdentity !== activation.releaseIdentity
90
+ || releaseIdentity !== activation.releaseIdentity
91
+ || !sameTarget(release.target, activation.target)
92
+ || release.bundleIdentity !== stored.bundle.identity) {
93
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_RELEASE_INVALID', 'The active deployment release is inconsistent with its activation or bundle.');
94
+ }
95
+ let bundle;
96
+ try {
97
+ bundle = await verifyDeploymentBundle(stored.bundle.directory);
98
+ }
99
+ catch (error) {
100
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_RELEASE_INVALID', 'The active deployment bundle could not be revalidated.', error);
101
+ }
102
+ if (bundle.identity !== stored.bundle.identity
103
+ || bundle.identity !== release.bundleIdentity) {
104
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_RELEASE_INVALID', 'The active deployment bundle identity is inconsistent.');
105
+ }
106
+ const entrypoints = new Map();
107
+ const queries = [];
108
+ for (const query of bundle.contract.queries) {
109
+ const implementation = query.implementation;
110
+ if (implementation.kind !== 'runtime-reference')
111
+ continue;
112
+ const names = entrypoints.get(implementation.artifactSha256) ?? [];
113
+ names.push(implementation.entrypoint);
114
+ entrypoints.set(implementation.artifactSha256, names);
115
+ queries.push(Object.freeze({
116
+ query: query.name,
117
+ runtime: implementation.runtime,
118
+ artifactSha256: implementation.artifactSha256,
119
+ entrypoint: implementation.entrypoint,
120
+ }));
121
+ }
122
+ const artifacts = [];
123
+ for (const artifact of bundle.manifest.artifacts) {
124
+ const bytes = await readArtifact(bundle.directory, artifact);
125
+ artifacts.push(artifactSnapshot({
126
+ runtime: artifact.runtime,
127
+ artifactSha256: artifact.sha256,
128
+ entrypoints: [...new Set(entrypoints.get(artifact.sha256) ?? [])].sort(),
129
+ }, bytes));
130
+ }
131
+ artifacts.sort((left, right) => left.artifactSha256.localeCompare(right.artifactSha256));
132
+ queries.sort((left, right) => left.query.localeCompare(right.query));
133
+ return Object.freeze({
134
+ target: activation.target,
135
+ activation,
136
+ release,
137
+ releaseIdentity,
138
+ bundleIdentity: bundle.identity,
139
+ deployment: bundle.contract,
140
+ artifacts: Object.freeze(artifacts),
141
+ queries: Object.freeze(queries),
142
+ });
143
+ }
144
+ function stabilityAttempts(input) {
145
+ const value = input ?? DEFAULT_STABILITY_ATTEMPTS;
146
+ if (!Number.isSafeInteger(value) || value < 1 || value > MAX_STABILITY_ATTEMPTS) {
147
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_CONFIGURATION', `maxStabilityAttempts must be between 1 and ${MAX_STABILITY_ATTEMPTS}.`);
148
+ }
149
+ return value;
150
+ }
151
+ export function createDeploymentRuntimeMaterializer(options) {
152
+ const maximumAttempts = stabilityAttempts(options.maxStabilityAttempts);
153
+ if (!constants.O_NOFOLLOW) {
154
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_CONFIGURATION', 'Runtime materialization requires filesystem no-follow support.');
155
+ }
156
+ async function currentActivation(target) {
157
+ try {
158
+ const activation = await options.activations.current(target);
159
+ if (!activation)
160
+ return undefined;
161
+ const validated = validateDeploymentActivationRecord(activation);
162
+ if (!sameTarget(validated.target, target)) {
163
+ throw new Error('Deployment activation target does not match the requested target.');
164
+ }
165
+ return validated;
166
+ }
167
+ catch (error) {
168
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_ACTIVATION_UNAVAILABLE', 'Deployment activation state could not be read.', error);
169
+ }
170
+ }
171
+ return Object.freeze({
172
+ async current(target) {
173
+ for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {
174
+ const activation = await currentActivation(target);
175
+ if (!activation)
176
+ return undefined;
177
+ const snapshot = await materializeActivation(activation, options.releases);
178
+ const confirmed = await currentActivation(target);
179
+ if (!confirmed)
180
+ return undefined;
181
+ if (confirmed.revision === activation.revision)
182
+ return snapshot;
183
+ }
184
+ throw materializationError('HQ_RUNTIME_MATERIALIZATION_UNSTABLE_ACTIVATION', 'Deployment activation changed repeatedly during runtime materialization.');
185
+ },
186
+ });
187
+ }
@@ -0,0 +1,72 @@
1
+ import type { ProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
2
+ import type { DeploymentRuntimeMaterializer, DeploymentRuntimeQueryBinding, DeploymentRuntimeSnapshot } from './runtime-materialization.js';
3
+ export type DeploymentRuntimeSupervisorErrorCode = 'HQ_RUNTIME_SUPERVISOR_CONFIGURATION' | 'HQ_RUNTIME_SUPERVISOR_CLOSED' | 'HQ_RUNTIME_NOT_READY' | 'HQ_RUNTIME_QUERY_NOT_FOUND' | 'HQ_RUNTIME_QUERY_NOT_EXECUTABLE' | 'HQ_RUNTIME_START_FAILED' | 'HQ_RUNTIME_HEALTH_FAILED' | 'HQ_RUNTIME_INVOCATION_FAILED' | 'HQ_RUNTIME_RECONCILE_UNSTABLE' | 'HQ_RUNTIME_ABORTED';
4
+ export declare class DeploymentRuntimeSupervisorError extends Error {
5
+ readonly code: DeploymentRuntimeSupervisorErrorCode;
6
+ constructor(code: DeploymentRuntimeSupervisorErrorCode, message: string, options?: {
7
+ readonly cause?: unknown;
8
+ });
9
+ }
10
+ export interface DeploymentRuntimeInstanceInvocation {
11
+ readonly query: string;
12
+ readonly binding: DeploymentRuntimeQueryBinding;
13
+ readonly argument: unknown;
14
+ readonly signal?: AbortSignal;
15
+ }
16
+ export interface DeploymentRuntimeInstance {
17
+ healthCheck(input: {
18
+ readonly signal?: AbortSignal;
19
+ }): Promise<void>;
20
+ invoke(input: DeploymentRuntimeInstanceInvocation): Promise<unknown>;
21
+ close(): Promise<void>;
22
+ }
23
+ export interface DeploymentRuntimeFactory {
24
+ start(snapshot: DeploymentRuntimeSnapshot, input: {
25
+ readonly signal?: AbortSignal;
26
+ }): Promise<DeploymentRuntimeInstance>;
27
+ }
28
+ export interface DeploymentRuntimeInvocation {
29
+ readonly target: ProtocolDeploymentReleaseTarget;
30
+ /** Reject the invocation unless this exact activation generation is active. */
31
+ readonly activationRevision?: string;
32
+ readonly query: string;
33
+ readonly argument: unknown;
34
+ readonly signal?: AbortSignal;
35
+ }
36
+ export interface DeploymentRuntimeStatus {
37
+ readonly target: ProtocolDeploymentReleaseTarget;
38
+ readonly activationRevision: string;
39
+ readonly releaseIdentity: string;
40
+ readonly bundleIdentity: string;
41
+ }
42
+ export type DeploymentRuntimeReconcileResult = {
43
+ readonly status: 'activated';
44
+ readonly runtime: DeploymentRuntimeStatus;
45
+ } | {
46
+ readonly status: 'already-current';
47
+ readonly runtime: DeploymentRuntimeStatus;
48
+ } | {
49
+ readonly status: 'deactivated';
50
+ readonly previous: DeploymentRuntimeStatus;
51
+ } | {
52
+ readonly status: 'no-active-release';
53
+ };
54
+ export interface DeploymentRuntimeSupervisor {
55
+ reconcile(target: ProtocolDeploymentReleaseTarget, options?: {
56
+ readonly signal?: AbortSignal;
57
+ }): Promise<DeploymentRuntimeReconcileResult>;
58
+ invoke(input: DeploymentRuntimeInvocation): Promise<unknown>;
59
+ status(target: ProtocolDeploymentReleaseTarget): DeploymentRuntimeStatus | undefined;
60
+ close(): Promise<void>;
61
+ }
62
+ export interface DeploymentRuntimeSupervisorOptions {
63
+ readonly materializer: DeploymentRuntimeMaterializer;
64
+ readonly factory: DeploymentRuntimeFactory;
65
+ /** In-flight drain deadline from 0 through 300,000 milliseconds. */
66
+ readonly drainTimeoutMs?: number;
67
+ /** Activation-stability attempts from 1 through 16. */
68
+ readonly maxReconcileAttempts?: number;
69
+ readonly onBackgroundError?: (error: unknown) => void;
70
+ }
71
+ export declare function createDeploymentRuntimeSupervisor(options: DeploymentRuntimeSupervisorOptions): DeploymentRuntimeSupervisor;
72
+ //# sourceMappingURL=runtime-supervisor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-supervisor.d.ts","sourceRoot":"","sources":["../src/runtime-supervisor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,258 @@
1
+ const DEFAULT_DRAIN_TIMEOUT_MS = 30_000;
2
+ const DEFAULT_RECONCILE_ATTEMPTS = 4;
3
+ const MAX_DRAIN_TIMEOUT_MS = 5 * 60_000;
4
+ const MAX_RECONCILE_ATTEMPTS = 16;
5
+ export class DeploymentRuntimeSupervisorError extends Error {
6
+ code;
7
+ constructor(code, message, options = {}) {
8
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
9
+ this.name = 'DeploymentRuntimeSupervisorError';
10
+ this.code = code;
11
+ }
12
+ }
13
+ function supervisorError(code, message, cause) {
14
+ return new DeploymentRuntimeSupervisorError(code, message, { cause });
15
+ }
16
+ function boundedInteger(input, fallback, maximum, name, allowZero = false) {
17
+ const value = input ?? fallback;
18
+ if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1) || value > maximum) {
19
+ throw supervisorError('HQ_RUNTIME_SUPERVISOR_CONFIGURATION', `${name} must be an integer between ${allowZero ? 0 : 1} and ${maximum}.`);
20
+ }
21
+ return value;
22
+ }
23
+ function targetKey(target) {
24
+ return JSON.stringify([target.project, target.environment]);
25
+ }
26
+ function sameTarget(left, right) {
27
+ return left.project === right.project && left.environment === right.environment;
28
+ }
29
+ function status(generation) {
30
+ const snapshot = generation.snapshot;
31
+ return Object.freeze({
32
+ target: snapshot.target,
33
+ activationRevision: snapshot.activation.revision,
34
+ releaseIdentity: snapshot.releaseIdentity,
35
+ bundleIdentity: snapshot.bundleIdentity,
36
+ });
37
+ }
38
+ function throwIfAborted(signal) {
39
+ if (signal?.aborted) {
40
+ throw supervisorError('HQ_RUNTIME_ABORTED', 'The runtime operation was aborted.', signal.reason);
41
+ }
42
+ }
43
+ function binding(snapshot, queryName) {
44
+ const query = snapshot.deployment.queries.find(candidate => candidate.name === queryName);
45
+ if (!query) {
46
+ throw supervisorError('HQ_RUNTIME_QUERY_NOT_FOUND', `Runtime query not found: ${queryName}`);
47
+ }
48
+ if (query.implementation.kind !== 'runtime-reference') {
49
+ throw supervisorError('HQ_RUNTIME_QUERY_NOT_EXECUTABLE', `Query is portable and does not use a supervised runtime: ${queryName}`);
50
+ }
51
+ const resolved = snapshot.queries.find(candidate => candidate.query === queryName);
52
+ if (!resolved) {
53
+ throw supervisorError('HQ_RUNTIME_QUERY_NOT_EXECUTABLE', `Runtime query binding is unavailable: ${queryName}`);
54
+ }
55
+ return resolved;
56
+ }
57
+ export function createDeploymentRuntimeSupervisor(options) {
58
+ const drainTimeoutMs = boundedInteger(options.drainTimeoutMs, DEFAULT_DRAIN_TIMEOUT_MS, MAX_DRAIN_TIMEOUT_MS, 'drainTimeoutMs', true);
59
+ const maxReconcileAttempts = boundedInteger(options.maxReconcileAttempts, DEFAULT_RECONCILE_ATTEMPTS, MAX_RECONCILE_ATTEMPTS, 'maxReconcileAttempts');
60
+ const active = new Map();
61
+ const updates = new Map();
62
+ const background = new Set();
63
+ const backgroundFailures = [];
64
+ let closed = false;
65
+ let closePromise;
66
+ function ensureOpen() {
67
+ if (closed)
68
+ throw supervisorError('HQ_RUNTIME_SUPERVISOR_CLOSED', 'Runtime supervisor is closed.');
69
+ }
70
+ async function drain(generation) {
71
+ if (generation.draining)
72
+ return;
73
+ generation.draining = true;
74
+ if (generation.inFlight > 0) {
75
+ await new Promise(resolve => {
76
+ let settled = false;
77
+ const finish = () => {
78
+ if (settled)
79
+ return;
80
+ settled = true;
81
+ clearTimeout(timer);
82
+ generation.drained = undefined;
83
+ resolve();
84
+ };
85
+ generation.drained = finish;
86
+ const timer = setTimeout(finish, drainTimeoutMs);
87
+ timer.unref();
88
+ });
89
+ }
90
+ await generation.instance.close();
91
+ }
92
+ function drainInBackground(generation) {
93
+ const operation = drain(generation).catch(error => {
94
+ backgroundFailures.push(error);
95
+ options.onBackgroundError?.(error);
96
+ });
97
+ background.add(operation);
98
+ void operation.finally(() => background.delete(operation));
99
+ }
100
+ async function safeClose(instance) {
101
+ try {
102
+ await instance.close();
103
+ }
104
+ catch (error) {
105
+ backgroundFailures.push(error);
106
+ options.onBackgroundError?.(error);
107
+ }
108
+ }
109
+ async function performReconcile(target, signal) {
110
+ for (let attempt = 0; attempt < maxReconcileAttempts; attempt += 1) {
111
+ ensureOpen();
112
+ throwIfAborted(signal);
113
+ const snapshot = await options.materializer.current(target);
114
+ ensureOpen();
115
+ throwIfAborted(signal);
116
+ const key = targetKey(target);
117
+ const existing = active.get(key);
118
+ if (!snapshot) {
119
+ if (!existing)
120
+ return Object.freeze({ status: 'no-active-release' });
121
+ active.delete(key);
122
+ drainInBackground(existing);
123
+ return Object.freeze({ status: 'deactivated', previous: status(existing) });
124
+ }
125
+ if (!sameTarget(snapshot.target, target)) {
126
+ throw supervisorError('HQ_RUNTIME_START_FAILED', 'The materialized runtime snapshot does not match the requested target.');
127
+ }
128
+ if (existing?.snapshot.activation.revision === snapshot.activation.revision) {
129
+ return Object.freeze({ status: 'already-current', runtime: status(existing) });
130
+ }
131
+ let candidate;
132
+ try {
133
+ candidate = await options.factory.start(snapshot, { signal });
134
+ }
135
+ catch (error) {
136
+ if (signal?.aborted) {
137
+ throw supervisorError('HQ_RUNTIME_ABORTED', 'The runtime operation was aborted while starting a candidate.', error);
138
+ }
139
+ if (error instanceof DeploymentRuntimeSupervisorError)
140
+ throw error;
141
+ throw supervisorError('HQ_RUNTIME_START_FAILED', 'The candidate deployment runtime could not be started.', error);
142
+ }
143
+ try {
144
+ ensureOpen();
145
+ throwIfAborted(signal);
146
+ await candidate.healthCheck({ signal });
147
+ }
148
+ catch (error) {
149
+ await safeClose(candidate);
150
+ if (error instanceof DeploymentRuntimeSupervisorError)
151
+ throw error;
152
+ throw supervisorError('HQ_RUNTIME_HEALTH_FAILED', 'The candidate deployment runtime did not become ready.', error);
153
+ }
154
+ let confirmed;
155
+ try {
156
+ ensureOpen();
157
+ throwIfAborted(signal);
158
+ confirmed = await options.materializer.current(target);
159
+ ensureOpen();
160
+ throwIfAborted(signal);
161
+ }
162
+ catch (error) {
163
+ await safeClose(candidate);
164
+ throw error;
165
+ }
166
+ if (confirmed?.activation.revision !== snapshot.activation.revision) {
167
+ await safeClose(candidate);
168
+ continue;
169
+ }
170
+ const generation = {
171
+ snapshot,
172
+ instance: candidate,
173
+ inFlight: 0,
174
+ draining: false,
175
+ };
176
+ const previous = active.get(key);
177
+ active.set(key, generation);
178
+ if (previous)
179
+ drainInBackground(previous);
180
+ return Object.freeze({ status: 'activated', runtime: status(generation) });
181
+ }
182
+ throw supervisorError('HQ_RUNTIME_RECONCILE_UNSTABLE', 'Deployment activation changed repeatedly while starting a runtime.');
183
+ }
184
+ function serialize(key, operation) {
185
+ const previous = updates.get(key) ?? Promise.resolve();
186
+ const current = previous.catch(() => undefined).then(operation);
187
+ const tracked = current.finally(() => {
188
+ if (updates.get(key) === tracked)
189
+ updates.delete(key);
190
+ });
191
+ updates.set(key, tracked);
192
+ return tracked;
193
+ }
194
+ return Object.freeze({
195
+ async reconcile(target, reconcileOptions = {}) {
196
+ ensureOpen();
197
+ return await serialize(targetKey(target), () => performReconcile(target, reconcileOptions.signal));
198
+ },
199
+ async invoke(input) {
200
+ ensureOpen();
201
+ throwIfAborted(input.signal);
202
+ const generation = active.get(targetKey(input.target));
203
+ if (!generation) {
204
+ throw supervisorError('HQ_RUNTIME_NOT_READY', 'No deployment runtime is ready for target.');
205
+ }
206
+ if (input.activationRevision !== undefined
207
+ && generation.snapshot.activation.revision !== input.activationRevision) {
208
+ throw supervisorError('HQ_RUNTIME_NOT_READY', 'The requested deployment runtime generation is not ready for target.');
209
+ }
210
+ const resolved = binding(generation.snapshot, input.query);
211
+ generation.inFlight += 1;
212
+ try {
213
+ return await generation.instance.invoke({
214
+ query: input.query,
215
+ binding: resolved,
216
+ argument: input.argument,
217
+ signal: input.signal,
218
+ });
219
+ }
220
+ catch (error) {
221
+ if (error instanceof DeploymentRuntimeSupervisorError)
222
+ throw error;
223
+ throw supervisorError('HQ_RUNTIME_INVOCATION_FAILED', 'The deployment runtime invocation failed.', error);
224
+ }
225
+ finally {
226
+ generation.inFlight -= 1;
227
+ if (generation.draining && generation.inFlight === 0)
228
+ generation.drained?.();
229
+ }
230
+ },
231
+ status(target) {
232
+ const generation = active.get(targetKey(target));
233
+ return generation ? status(generation) : undefined;
234
+ },
235
+ close() {
236
+ if (closePromise)
237
+ return closePromise;
238
+ closed = true;
239
+ closePromise = (async () => {
240
+ await Promise.allSettled([...updates.values()]);
241
+ const generations = [...active.values()];
242
+ active.clear();
243
+ const results = await Promise.allSettled(generations.map(generation => drain(generation)));
244
+ await Promise.allSettled([...background]);
245
+ const failures = [
246
+ ...backgroundFailures,
247
+ ...results
248
+ .filter((result) => result.status === 'rejected')
249
+ .map(result => result.reason),
250
+ ];
251
+ if (failures.length > 0) {
252
+ throw new AggregateError(failures, 'One or more deployment runtimes could not be closed.');
253
+ }
254
+ })();
255
+ return closePromise;
256
+ },
257
+ });
258
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/deployment",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Provider-neutral deployment verification and intake for Hypequery",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@hypequery/protocol": "0.6.0"
21
+ "@hypequery/protocol": "0.7.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^22.5.0",
@@ -41,6 +41,6 @@
41
41
  "dev": "tsc --project tsconfig.json --watch",
42
42
  "lint": "eslint --ext .ts \"src/**/*.ts\"",
43
43
  "test": "vitest run",
44
- "types": "tsc --project tsconfig.json --noEmit"
44
+ "types": "tsc --project tsconfig.json --noEmit && tsc --project tsconfig.test.json"
45
45
  }
46
46
  }