@chidchanun/bcp 0.2.17 → 0.2.18
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/README.md +153 -292
- package/docs/README.md +33 -41
- package/docs/api-manifest.json +24 -16
- package/docs/api-reference.md +150 -140
- package/docs/deployment-platform-v2.md +449 -0
- package/docs/docs-web-manifest.json +5 -3
- package/docs/platform-manifest.json +18 -4
- package/docs/releases/0.2.18.md +136 -0
- package/package.json +10 -5
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/auth.mjs +1391 -0
- package/packages/client/src/config.mjs +1132 -0
- package/packages/client/src/deployment.mjs +609 -0
- package/packages/client/src/deployment.ts +20 -0
- package/packages/client/src/server.mjs +5615 -0
- package/packages/server/src/deployment.ts +936 -0
- package/packages/server/src/middleware.mjs +631 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
import {
|
|
2
|
+
randomUUID,
|
|
3
|
+
} from "node:crypto";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
registerShutdownHook,
|
|
7
|
+
} from "./production-hardening.js";
|
|
8
|
+
|
|
9
|
+
export type DeploymentRuntimeState =
|
|
10
|
+
| "idle"
|
|
11
|
+
| "starting"
|
|
12
|
+
| "ready"
|
|
13
|
+
| "draining"
|
|
14
|
+
| "stopped"
|
|
15
|
+
| "failed";
|
|
16
|
+
|
|
17
|
+
export interface DeploymentMetadata {
|
|
18
|
+
serviceName: string;
|
|
19
|
+
version?: string;
|
|
20
|
+
deploymentId: string;
|
|
21
|
+
instanceId?: string;
|
|
22
|
+
release?: string;
|
|
23
|
+
environment?: string;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
pid: number;
|
|
26
|
+
nodeVersion: string;
|
|
27
|
+
platform: NodeJS.Platform;
|
|
28
|
+
arch: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DeploymentResourceContext {
|
|
32
|
+
readonly metadata: DeploymentMetadata;
|
|
33
|
+
readonly signal: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type DeploymentReadinessResult =
|
|
37
|
+
| boolean
|
|
38
|
+
| {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
detail?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface DeploymentResource {
|
|
44
|
+
name: string;
|
|
45
|
+
start?: (
|
|
46
|
+
context: DeploymentResourceContext
|
|
47
|
+
) => void | Promise<void>;
|
|
48
|
+
ready?: (
|
|
49
|
+
context: DeploymentResourceContext
|
|
50
|
+
) =>
|
|
51
|
+
| DeploymentReadinessResult
|
|
52
|
+
| Promise<DeploymentReadinessResult>;
|
|
53
|
+
stop?: (
|
|
54
|
+
context: DeploymentResourceContext
|
|
55
|
+
) => void | Promise<void>;
|
|
56
|
+
diagnostics?: (
|
|
57
|
+
context: DeploymentResourceContext
|
|
58
|
+
) =>
|
|
59
|
+
| Readonly<Record<string, unknown>>
|
|
60
|
+
| Promise<Readonly<Record<string, unknown>>>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DeploymentResourceStatus {
|
|
64
|
+
name: string;
|
|
65
|
+
state:
|
|
66
|
+
| "registered"
|
|
67
|
+
| "starting"
|
|
68
|
+
| "started"
|
|
69
|
+
| "stopping"
|
|
70
|
+
| "stopped"
|
|
71
|
+
| "failed";
|
|
72
|
+
startedAt?: string;
|
|
73
|
+
stoppedAt?: string;
|
|
74
|
+
error?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DeploymentReadinessItem {
|
|
78
|
+
name: string;
|
|
79
|
+
ok: boolean;
|
|
80
|
+
durationMs: number;
|
|
81
|
+
detail?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface DeploymentReadinessReport {
|
|
85
|
+
ok: boolean;
|
|
86
|
+
state: DeploymentRuntimeState;
|
|
87
|
+
checkedAt: string;
|
|
88
|
+
resources: DeploymentReadinessItem[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface DeploymentDiagnosticsResource {
|
|
92
|
+
name: string;
|
|
93
|
+
lifecycle: DeploymentResourceStatus;
|
|
94
|
+
details?: Readonly<Record<string, unknown>>;
|
|
95
|
+
error?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface DeploymentDiagnosticsReport {
|
|
99
|
+
metadata: DeploymentMetadata;
|
|
100
|
+
state: DeploymentRuntimeState;
|
|
101
|
+
uptimeSeconds: number;
|
|
102
|
+
resources: DeploymentDiagnosticsResource[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface DeploymentRuntimeOptions {
|
|
106
|
+
serviceName: string;
|
|
107
|
+
version?: string;
|
|
108
|
+
deploymentId?: string;
|
|
109
|
+
instanceId?: string;
|
|
110
|
+
release?: string;
|
|
111
|
+
environmentName?: string;
|
|
112
|
+
environment?: NodeJS.ProcessEnv;
|
|
113
|
+
shutdownTimeoutMs?: number;
|
|
114
|
+
readinessTimeoutMs?: number;
|
|
115
|
+
now?: () => number;
|
|
116
|
+
idFactory?: () => string;
|
|
117
|
+
resources?: readonly DeploymentResource[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface DeploymentShutdownOptions {
|
|
121
|
+
reason?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface DeploymentSignalOptions {
|
|
125
|
+
signals?: readonly NodeJS.Signals[];
|
|
126
|
+
setExitCode?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface DeploymentRuntime {
|
|
130
|
+
readonly metadata: DeploymentMetadata;
|
|
131
|
+
readonly state: DeploymentRuntimeState;
|
|
132
|
+
addResource(resource: DeploymentResource): () => void;
|
|
133
|
+
resources(): DeploymentResourceStatus[];
|
|
134
|
+
start(): Promise<void>;
|
|
135
|
+
readiness(): Promise<DeploymentReadinessReport>;
|
|
136
|
+
diagnostics(): Promise<DeploymentDiagnosticsReport>;
|
|
137
|
+
shutdown(options?: DeploymentShutdownOptions): Promise<void>;
|
|
138
|
+
installSignalHandlers(options?: DeploymentSignalOptions): () => void;
|
|
139
|
+
registerShutdownHook(name?: string): () => void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS =
|
|
143
|
+
10_000;
|
|
144
|
+
const DEFAULT_READINESS_TIMEOUT_MS =
|
|
145
|
+
5_000;
|
|
146
|
+
const DEFAULT_SIGNALS:
|
|
147
|
+
readonly NodeJS.Signals[] = [
|
|
148
|
+
"SIGTERM",
|
|
149
|
+
"SIGINT",
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
export function createDeploymentRuntime(
|
|
153
|
+
options: DeploymentRuntimeOptions
|
|
154
|
+
): DeploymentRuntime {
|
|
155
|
+
const environment =
|
|
156
|
+
options.environment ?? process.env;
|
|
157
|
+
const now =
|
|
158
|
+
options.now ?? Date.now;
|
|
159
|
+
const idFactory =
|
|
160
|
+
options.idFactory ?? randomUUID;
|
|
161
|
+
const shutdownTimeoutMs =
|
|
162
|
+
positiveInteger(
|
|
163
|
+
options.shutdownTimeoutMs ??
|
|
164
|
+
parseOptionalPositiveInteger(
|
|
165
|
+
environment.BCP_SHUTDOWN_TIMEOUT_MS
|
|
166
|
+
) ??
|
|
167
|
+
DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
168
|
+
"shutdownTimeoutMs"
|
|
169
|
+
);
|
|
170
|
+
const readinessTimeoutMs =
|
|
171
|
+
positiveInteger(
|
|
172
|
+
options.readinessTimeoutMs ??
|
|
173
|
+
DEFAULT_READINESS_TIMEOUT_MS,
|
|
174
|
+
"readinessTimeoutMs"
|
|
175
|
+
);
|
|
176
|
+
const startedAtMs = now();
|
|
177
|
+
assertTimestamp(
|
|
178
|
+
startedAtMs,
|
|
179
|
+
"runtime start timestamp"
|
|
180
|
+
);
|
|
181
|
+
const metadata:
|
|
182
|
+
DeploymentMetadata = {
|
|
183
|
+
serviceName:
|
|
184
|
+
normalizeName(
|
|
185
|
+
options.serviceName,
|
|
186
|
+
"serviceName"
|
|
187
|
+
),
|
|
188
|
+
...(normalizeOptionalText(
|
|
189
|
+
options.version
|
|
190
|
+
)
|
|
191
|
+
? {
|
|
192
|
+
version:
|
|
193
|
+
normalizeOptionalText(
|
|
194
|
+
options.version
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
: {}),
|
|
198
|
+
deploymentId:
|
|
199
|
+
normalizeName(
|
|
200
|
+
options.deploymentId ??
|
|
201
|
+
environment.BCP_DEPLOYMENT_ID ??
|
|
202
|
+
idFactory(),
|
|
203
|
+
"deploymentId"
|
|
204
|
+
),
|
|
205
|
+
...(normalizeOptionalText(
|
|
206
|
+
options.instanceId ??
|
|
207
|
+
environment.BCP_INSTANCE_ID
|
|
208
|
+
)
|
|
209
|
+
? {
|
|
210
|
+
instanceId:
|
|
211
|
+
normalizeOptionalText(
|
|
212
|
+
options.instanceId ??
|
|
213
|
+
environment.BCP_INSTANCE_ID
|
|
214
|
+
),
|
|
215
|
+
}
|
|
216
|
+
: {}),
|
|
217
|
+
...(normalizeOptionalText(
|
|
218
|
+
options.release ??
|
|
219
|
+
environment.BCP_RELEASE
|
|
220
|
+
)
|
|
221
|
+
? {
|
|
222
|
+
release:
|
|
223
|
+
normalizeOptionalText(
|
|
224
|
+
options.release ??
|
|
225
|
+
environment.BCP_RELEASE
|
|
226
|
+
),
|
|
227
|
+
}
|
|
228
|
+
: {}),
|
|
229
|
+
...(normalizeOptionalText(
|
|
230
|
+
options.environmentName ??
|
|
231
|
+
environment.NODE_ENV
|
|
232
|
+
)
|
|
233
|
+
? {
|
|
234
|
+
environment:
|
|
235
|
+
normalizeOptionalText(
|
|
236
|
+
options.environmentName ??
|
|
237
|
+
environment.NODE_ENV
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
: {}),
|
|
241
|
+
startedAt:
|
|
242
|
+
new Date(startedAtMs)
|
|
243
|
+
.toISOString(),
|
|
244
|
+
pid: process.pid,
|
|
245
|
+
nodeVersion:
|
|
246
|
+
process.version,
|
|
247
|
+
platform:
|
|
248
|
+
process.platform,
|
|
249
|
+
arch:
|
|
250
|
+
process.arch,
|
|
251
|
+
};
|
|
252
|
+
const abortController =
|
|
253
|
+
new AbortController();
|
|
254
|
+
const resourceEntries:
|
|
255
|
+
Array<{
|
|
256
|
+
resource: DeploymentResource;
|
|
257
|
+
status: DeploymentResourceStatus;
|
|
258
|
+
}> = [];
|
|
259
|
+
const names =
|
|
260
|
+
new Set<string>();
|
|
261
|
+
let state:
|
|
262
|
+
DeploymentRuntimeState =
|
|
263
|
+
"idle";
|
|
264
|
+
let startPromise:
|
|
265
|
+
Promise<void> | undefined;
|
|
266
|
+
let shutdownPromise:
|
|
267
|
+
Promise<void> | undefined;
|
|
268
|
+
|
|
269
|
+
const runtime:
|
|
270
|
+
DeploymentRuntime = {
|
|
271
|
+
metadata,
|
|
272
|
+
get state(): DeploymentRuntimeState {
|
|
273
|
+
return state;
|
|
274
|
+
},
|
|
275
|
+
addResource(
|
|
276
|
+
resource: DeploymentResource
|
|
277
|
+
): () => void {
|
|
278
|
+
if (
|
|
279
|
+
state !== "idle"
|
|
280
|
+
) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"BCP Deployment: resources can only be registered before runtime start."
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const name =
|
|
286
|
+
normalizeName(
|
|
287
|
+
resource.name,
|
|
288
|
+
"resource name"
|
|
289
|
+
);
|
|
290
|
+
if (names.has(name)) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`BCP Deployment: resource "${name}" is already registered.`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const normalized:
|
|
296
|
+
DeploymentResource = {
|
|
297
|
+
...resource,
|
|
298
|
+
name,
|
|
299
|
+
};
|
|
300
|
+
const entry = {
|
|
301
|
+
resource: normalized,
|
|
302
|
+
status: {
|
|
303
|
+
name,
|
|
304
|
+
state:
|
|
305
|
+
"registered" as const,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
names.add(name);
|
|
309
|
+
resourceEntries.push(entry);
|
|
310
|
+
return () => {
|
|
311
|
+
if (state !== "idle") {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const index =
|
|
315
|
+
resourceEntries.indexOf(
|
|
316
|
+
entry
|
|
317
|
+
);
|
|
318
|
+
if (index >= 0) {
|
|
319
|
+
resourceEntries.splice(
|
|
320
|
+
index,
|
|
321
|
+
1
|
|
322
|
+
);
|
|
323
|
+
names.delete(name);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
resources(): DeploymentResourceStatus[] {
|
|
328
|
+
return resourceEntries.map(
|
|
329
|
+
entry => ({
|
|
330
|
+
...entry.status,
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
async start(): Promise<void> {
|
|
335
|
+
if (state === "ready") {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (startPromise) {
|
|
339
|
+
return startPromise;
|
|
340
|
+
}
|
|
341
|
+
if (
|
|
342
|
+
state === "draining" ||
|
|
343
|
+
state === "stopped"
|
|
344
|
+
) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
"BCP Deployment: stopped runtime cannot be started again."
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (state === "failed") {
|
|
350
|
+
throw new Error(
|
|
351
|
+
"BCP Deployment: failed runtime cannot be started again."
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
startPromise =
|
|
356
|
+
startResources();
|
|
357
|
+
return startPromise;
|
|
358
|
+
},
|
|
359
|
+
async readiness(): Promise<DeploymentReadinessReport> {
|
|
360
|
+
const checkedAtMs = now();
|
|
361
|
+
assertTimestamp(
|
|
362
|
+
checkedAtMs,
|
|
363
|
+
"readiness timestamp"
|
|
364
|
+
);
|
|
365
|
+
const results:
|
|
366
|
+
DeploymentReadinessItem[] = [];
|
|
367
|
+
|
|
368
|
+
for (const entry of resourceEntries) {
|
|
369
|
+
if (
|
|
370
|
+
entry.status.state !==
|
|
371
|
+
"started"
|
|
372
|
+
) {
|
|
373
|
+
results.push({
|
|
374
|
+
name:
|
|
375
|
+
entry.resource.name,
|
|
376
|
+
ok: false,
|
|
377
|
+
durationMs: 0,
|
|
378
|
+
detail:
|
|
379
|
+
`Resource state is ${entry.status.state}.`,
|
|
380
|
+
});
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!entry.resource.ready) {
|
|
385
|
+
results.push({
|
|
386
|
+
name:
|
|
387
|
+
entry.resource.name,
|
|
388
|
+
ok: true,
|
|
389
|
+
durationMs: 0,
|
|
390
|
+
});
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const checkStarted =
|
|
395
|
+
performance.now();
|
|
396
|
+
try {
|
|
397
|
+
const value =
|
|
398
|
+
await withTimeout(
|
|
399
|
+
Promise.resolve(
|
|
400
|
+
entry.resource.ready(
|
|
401
|
+
createContext()
|
|
402
|
+
)
|
|
403
|
+
),
|
|
404
|
+
readinessTimeoutMs,
|
|
405
|
+
`readiness check for ${entry.resource.name}`
|
|
406
|
+
);
|
|
407
|
+
const normalized =
|
|
408
|
+
typeof value ===
|
|
409
|
+
"boolean"
|
|
410
|
+
? {
|
|
411
|
+
ok: value,
|
|
412
|
+
}
|
|
413
|
+
: value;
|
|
414
|
+
results.push({
|
|
415
|
+
name:
|
|
416
|
+
entry.resource.name,
|
|
417
|
+
ok:
|
|
418
|
+
normalized.ok ===
|
|
419
|
+
true,
|
|
420
|
+
durationMs:
|
|
421
|
+
elapsedMilliseconds(
|
|
422
|
+
checkStarted
|
|
423
|
+
),
|
|
424
|
+
...(normalized.detail
|
|
425
|
+
? {
|
|
426
|
+
detail:
|
|
427
|
+
normalized.detail,
|
|
428
|
+
}
|
|
429
|
+
: {}),
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
results.push({
|
|
433
|
+
name:
|
|
434
|
+
entry.resource.name,
|
|
435
|
+
ok: false,
|
|
436
|
+
durationMs:
|
|
437
|
+
elapsedMilliseconds(
|
|
438
|
+
checkStarted
|
|
439
|
+
),
|
|
440
|
+
detail:
|
|
441
|
+
errorMessage(error),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
ok:
|
|
448
|
+
state === "ready" &&
|
|
449
|
+
results.every(
|
|
450
|
+
item => item.ok
|
|
451
|
+
),
|
|
452
|
+
state,
|
|
453
|
+
checkedAt:
|
|
454
|
+
new Date(
|
|
455
|
+
checkedAtMs
|
|
456
|
+
).toISOString(),
|
|
457
|
+
resources:
|
|
458
|
+
results,
|
|
459
|
+
};
|
|
460
|
+
},
|
|
461
|
+
async diagnostics(): Promise<DeploymentDiagnosticsReport> {
|
|
462
|
+
const resources:
|
|
463
|
+
DeploymentDiagnosticsResource[] = [];
|
|
464
|
+
|
|
465
|
+
for (const entry of resourceEntries) {
|
|
466
|
+
let details:
|
|
467
|
+
Readonly<Record<string, unknown>> |
|
|
468
|
+
undefined;
|
|
469
|
+
let error:
|
|
470
|
+
string | undefined;
|
|
471
|
+
if (entry.resource.diagnostics) {
|
|
472
|
+
try {
|
|
473
|
+
details =
|
|
474
|
+
await entry.resource.diagnostics(
|
|
475
|
+
createContext()
|
|
476
|
+
);
|
|
477
|
+
} catch (diagnosticError) {
|
|
478
|
+
error =
|
|
479
|
+
errorMessage(
|
|
480
|
+
diagnosticError
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
resources.push({
|
|
485
|
+
name:
|
|
486
|
+
entry.resource.name,
|
|
487
|
+
lifecycle: {
|
|
488
|
+
...entry.status,
|
|
489
|
+
},
|
|
490
|
+
...(details
|
|
491
|
+
? {
|
|
492
|
+
details,
|
|
493
|
+
}
|
|
494
|
+
: {}),
|
|
495
|
+
...(error
|
|
496
|
+
? {
|
|
497
|
+
error,
|
|
498
|
+
}
|
|
499
|
+
: {}),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
metadata: {
|
|
505
|
+
...metadata,
|
|
506
|
+
},
|
|
507
|
+
state,
|
|
508
|
+
uptimeSeconds:
|
|
509
|
+
Math.max(
|
|
510
|
+
0,
|
|
511
|
+
(
|
|
512
|
+
now() -
|
|
513
|
+
startedAtMs
|
|
514
|
+
) /
|
|
515
|
+
1000
|
|
516
|
+
),
|
|
517
|
+
resources,
|
|
518
|
+
};
|
|
519
|
+
},
|
|
520
|
+
async shutdown(
|
|
521
|
+
_options: DeploymentShutdownOptions = {}
|
|
522
|
+
): Promise<void> {
|
|
523
|
+
if (shutdownPromise) {
|
|
524
|
+
return shutdownPromise;
|
|
525
|
+
}
|
|
526
|
+
if (state === "stopped") {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
shutdownPromise =
|
|
530
|
+
stopResources();
|
|
531
|
+
return shutdownPromise;
|
|
532
|
+
},
|
|
533
|
+
installSignalHandlers(
|
|
534
|
+
signalOptions:
|
|
535
|
+
DeploymentSignalOptions = {}
|
|
536
|
+
): () => void {
|
|
537
|
+
const signals =
|
|
538
|
+
normalizeSignals(
|
|
539
|
+
signalOptions.signals ??
|
|
540
|
+
DEFAULT_SIGNALS
|
|
541
|
+
);
|
|
542
|
+
const handlers =
|
|
543
|
+
new Map<
|
|
544
|
+
NodeJS.Signals,
|
|
545
|
+
() => void
|
|
546
|
+
>();
|
|
547
|
+
|
|
548
|
+
for (const signal of signals) {
|
|
549
|
+
const handler = () => {
|
|
550
|
+
void runtime.shutdown({
|
|
551
|
+
reason: signal,
|
|
552
|
+
}).then(
|
|
553
|
+
() => {
|
|
554
|
+
if (
|
|
555
|
+
signalOptions.setExitCode !==
|
|
556
|
+
false
|
|
557
|
+
) {
|
|
558
|
+
process.exitCode = 0;
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
() => {
|
|
562
|
+
process.exitCode = 1;
|
|
563
|
+
}
|
|
564
|
+
);
|
|
565
|
+
};
|
|
566
|
+
handlers.set(
|
|
567
|
+
signal,
|
|
568
|
+
handler
|
|
569
|
+
);
|
|
570
|
+
process.on(
|
|
571
|
+
signal,
|
|
572
|
+
handler
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return () => {
|
|
577
|
+
for (
|
|
578
|
+
const [
|
|
579
|
+
signal,
|
|
580
|
+
handler,
|
|
581
|
+
]
|
|
582
|
+
of handlers
|
|
583
|
+
) {
|
|
584
|
+
process.off(
|
|
585
|
+
signal,
|
|
586
|
+
handler
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
handlers.clear();
|
|
590
|
+
};
|
|
591
|
+
},
|
|
592
|
+
registerShutdownHook(
|
|
593
|
+
name =
|
|
594
|
+
`deployment:${metadata.serviceName}`
|
|
595
|
+
): () => void {
|
|
596
|
+
return registerShutdownHook(
|
|
597
|
+
() =>
|
|
598
|
+
runtime.shutdown({
|
|
599
|
+
reason:
|
|
600
|
+
"framework-shutdown",
|
|
601
|
+
}),
|
|
602
|
+
{
|
|
603
|
+
name,
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
for (
|
|
610
|
+
const resource
|
|
611
|
+
of options.resources ?? []
|
|
612
|
+
) {
|
|
613
|
+
runtime.addResource(resource);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return runtime;
|
|
617
|
+
|
|
618
|
+
async function startResources(): Promise<void> {
|
|
619
|
+
state = "starting";
|
|
620
|
+
try {
|
|
621
|
+
for (const entry of resourceEntries) {
|
|
622
|
+
entry.status = {
|
|
623
|
+
name:
|
|
624
|
+
entry.resource.name,
|
|
625
|
+
state: "starting",
|
|
626
|
+
};
|
|
627
|
+
try {
|
|
628
|
+
await entry.resource.start?.(
|
|
629
|
+
createContext()
|
|
630
|
+
);
|
|
631
|
+
entry.status = {
|
|
632
|
+
name:
|
|
633
|
+
entry.resource.name,
|
|
634
|
+
state: "started",
|
|
635
|
+
startedAt:
|
|
636
|
+
new Date(
|
|
637
|
+
now()
|
|
638
|
+
).toISOString(),
|
|
639
|
+
};
|
|
640
|
+
} catch (error) {
|
|
641
|
+
entry.status = {
|
|
642
|
+
name:
|
|
643
|
+
entry.resource.name,
|
|
644
|
+
state: "failed",
|
|
645
|
+
error:
|
|
646
|
+
errorMessage(error),
|
|
647
|
+
};
|
|
648
|
+
throw new Error(
|
|
649
|
+
`BCP Deployment: resource "${entry.resource.name}" failed to start. ${errorMessage(error)}`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
state = "ready";
|
|
654
|
+
} catch (error) {
|
|
655
|
+
state = "failed";
|
|
656
|
+
abortController.abort(error);
|
|
657
|
+
await stopStartedResources(
|
|
658
|
+
shutdownTimeoutMs
|
|
659
|
+
);
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function stopResources(): Promise<void> {
|
|
665
|
+
state = "draining";
|
|
666
|
+
if (!abortController.signal.aborted) {
|
|
667
|
+
abortController.abort(
|
|
668
|
+
new Error(
|
|
669
|
+
"BCP Deployment: runtime is shutting down."
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const failures =
|
|
674
|
+
await stopStartedResources(
|
|
675
|
+
shutdownTimeoutMs
|
|
676
|
+
);
|
|
677
|
+
state =
|
|
678
|
+
failures.length > 0
|
|
679
|
+
? "failed"
|
|
680
|
+
: "stopped";
|
|
681
|
+
if (failures.length > 0) {
|
|
682
|
+
throw new AggregateError(
|
|
683
|
+
failures,
|
|
684
|
+
"BCP Deployment: one or more resources failed to stop."
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function stopStartedResources(
|
|
690
|
+
timeoutMs: number
|
|
691
|
+
): Promise<Error[]> {
|
|
692
|
+
const failures:
|
|
693
|
+
Error[] = [];
|
|
694
|
+
const deadline =
|
|
695
|
+
Date.now() + timeoutMs;
|
|
696
|
+
|
|
697
|
+
for (
|
|
698
|
+
const entry
|
|
699
|
+
of [...resourceEntries].reverse()
|
|
700
|
+
) {
|
|
701
|
+
if (
|
|
702
|
+
entry.status.state !==
|
|
703
|
+
"started"
|
|
704
|
+
) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
entry.status = {
|
|
708
|
+
...entry.status,
|
|
709
|
+
state: "stopping",
|
|
710
|
+
};
|
|
711
|
+
try {
|
|
712
|
+
const remaining =
|
|
713
|
+
Math.max(
|
|
714
|
+
1,
|
|
715
|
+
deadline -
|
|
716
|
+
Date.now()
|
|
717
|
+
);
|
|
718
|
+
await withTimeout(
|
|
719
|
+
Promise.resolve(
|
|
720
|
+
entry.resource.stop?.(
|
|
721
|
+
createContext()
|
|
722
|
+
)
|
|
723
|
+
),
|
|
724
|
+
remaining,
|
|
725
|
+
`shutdown of ${entry.resource.name}`
|
|
726
|
+
);
|
|
727
|
+
entry.status = {
|
|
728
|
+
...entry.status,
|
|
729
|
+
state: "stopped",
|
|
730
|
+
stoppedAt:
|
|
731
|
+
new Date(
|
|
732
|
+
now()
|
|
733
|
+
).toISOString(),
|
|
734
|
+
};
|
|
735
|
+
} catch (error) {
|
|
736
|
+
const message =
|
|
737
|
+
errorMessage(error);
|
|
738
|
+
entry.status = {
|
|
739
|
+
...entry.status,
|
|
740
|
+
state: "failed",
|
|
741
|
+
error: message,
|
|
742
|
+
};
|
|
743
|
+
failures.push(
|
|
744
|
+
new Error(
|
|
745
|
+
`BCP Deployment: resource "${entry.resource.name}" failed to stop. ${message}`
|
|
746
|
+
)
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return failures;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function createContext(): DeploymentResourceContext {
|
|
755
|
+
return {
|
|
756
|
+
metadata,
|
|
757
|
+
signal:
|
|
758
|
+
abortController.signal,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export function createDeploymentReadinessResponse(
|
|
764
|
+
runtime: DeploymentRuntime
|
|
765
|
+
): Promise<Response> {
|
|
766
|
+
return runtime.readiness()
|
|
767
|
+
.then(
|
|
768
|
+
report =>
|
|
769
|
+
Response.json(
|
|
770
|
+
report,
|
|
771
|
+
{
|
|
772
|
+
status:
|
|
773
|
+
report.ok
|
|
774
|
+
? 200
|
|
775
|
+
: 503,
|
|
776
|
+
headers: {
|
|
777
|
+
"cache-control":
|
|
778
|
+
"no-store",
|
|
779
|
+
},
|
|
780
|
+
}
|
|
781
|
+
)
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export async function createDeploymentDiagnosticsResponse(
|
|
786
|
+
runtime: DeploymentRuntime
|
|
787
|
+
): Promise<Response> {
|
|
788
|
+
return Response.json(
|
|
789
|
+
await runtime.diagnostics(),
|
|
790
|
+
{
|
|
791
|
+
status: 200,
|
|
792
|
+
headers: {
|
|
793
|
+
"cache-control":
|
|
794
|
+
"no-store",
|
|
795
|
+
},
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function normalizeSignals(
|
|
801
|
+
signals: readonly NodeJS.Signals[]
|
|
802
|
+
): NodeJS.Signals[] {
|
|
803
|
+
return Array.from(
|
|
804
|
+
new Set(signals)
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function normalizeName(
|
|
809
|
+
value: string,
|
|
810
|
+
field: string
|
|
811
|
+
): string {
|
|
812
|
+
const normalized =
|
|
813
|
+
String(value ?? "").trim();
|
|
814
|
+
if (!normalized) {
|
|
815
|
+
throw new TypeError(
|
|
816
|
+
`BCP Deployment: ${field} must be a non-empty string.`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
if (
|
|
820
|
+
normalized.length > 256 ||
|
|
821
|
+
/[\r\n]/.test(normalized)
|
|
822
|
+
) {
|
|
823
|
+
throw new TypeError(
|
|
824
|
+
`BCP Deployment: ${field} must be at most 256 characters without line breaks.`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
return normalized;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function normalizeOptionalText(
|
|
831
|
+
value: string | undefined
|
|
832
|
+
): string | undefined {
|
|
833
|
+
if (value === undefined) {
|
|
834
|
+
return undefined;
|
|
835
|
+
}
|
|
836
|
+
const normalized =
|
|
837
|
+
value.trim();
|
|
838
|
+
return normalized || undefined;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function positiveInteger(
|
|
842
|
+
value: number,
|
|
843
|
+
field: string
|
|
844
|
+
): number {
|
|
845
|
+
if (
|
|
846
|
+
!Number.isSafeInteger(value) ||
|
|
847
|
+
value <= 0
|
|
848
|
+
) {
|
|
849
|
+
throw new TypeError(
|
|
850
|
+
`BCP Deployment: ${field} must be a positive safe integer.`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
return value;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function parseOptionalPositiveInteger(
|
|
857
|
+
value: string | undefined
|
|
858
|
+
): number | undefined {
|
|
859
|
+
if (value === undefined) {
|
|
860
|
+
return undefined;
|
|
861
|
+
}
|
|
862
|
+
const parsed =
|
|
863
|
+
Number(value.trim());
|
|
864
|
+
if (
|
|
865
|
+
!Number.isSafeInteger(parsed) ||
|
|
866
|
+
parsed <= 0
|
|
867
|
+
) {
|
|
868
|
+
throw new TypeError(
|
|
869
|
+
"BCP Deployment: BCP_SHUTDOWN_TIMEOUT_MS must be a positive safe integer."
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
return parsed;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function assertTimestamp(
|
|
876
|
+
value: number,
|
|
877
|
+
field: string
|
|
878
|
+
): void {
|
|
879
|
+
if (!Number.isFinite(value)) {
|
|
880
|
+
throw new TypeError(
|
|
881
|
+
`BCP Deployment: ${field} must be a finite number.`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function errorMessage(
|
|
887
|
+
error: unknown
|
|
888
|
+
): string {
|
|
889
|
+
return error instanceof Error
|
|
890
|
+
? error.message
|
|
891
|
+
: String(error);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function elapsedMilliseconds(
|
|
895
|
+
startedAt: number
|
|
896
|
+
): number {
|
|
897
|
+
return Math.max(
|
|
898
|
+
0,
|
|
899
|
+
performance.now() - startedAt
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function withTimeout<T>(
|
|
904
|
+
promise: Promise<T>,
|
|
905
|
+
timeoutMs: number,
|
|
906
|
+
label: string
|
|
907
|
+
): Promise<T> {
|
|
908
|
+
return new Promise<T>(
|
|
909
|
+
(
|
|
910
|
+
resolve,
|
|
911
|
+
reject
|
|
912
|
+
) => {
|
|
913
|
+
const timer =
|
|
914
|
+
setTimeout(
|
|
915
|
+
() =>
|
|
916
|
+
reject(
|
|
917
|
+
new Error(
|
|
918
|
+
`BCP Deployment: ${label} timed out after ${timeoutMs}ms.`
|
|
919
|
+
)
|
|
920
|
+
),
|
|
921
|
+
timeoutMs
|
|
922
|
+
);
|
|
923
|
+
timer.unref?.();
|
|
924
|
+
promise.then(
|
|
925
|
+
value => {
|
|
926
|
+
clearTimeout(timer);
|
|
927
|
+
resolve(value);
|
|
928
|
+
},
|
|
929
|
+
error => {
|
|
930
|
+
clearTimeout(timer);
|
|
931
|
+
reject(error);
|
|
932
|
+
}
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
);
|
|
936
|
+
}
|