@chidchanun/bcp 0.2.18 → 0.3.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.
- package/README.md +171 -218
- package/docs/README.md +81 -44
- package/docs/api-freeze-snapshot.json +243 -0
- package/docs/api-manifest.json +21 -13
- package/docs/api-reference.md +105 -12
- package/docs/application-platform.md +378 -0
- package/docs/docs-web-manifest.json +9 -4
- package/docs/migration-0.2.md +79 -80
- package/docs/migration-0.3.md +159 -0
- package/docs/platform-contract.md +92 -78
- package/docs/platform-manifest.json +29 -6
- package/docs/releases/0.2.19.md +125 -0
- package/docs/releases/0.3.0.md +129 -0
- package/docs/releasing.md +104 -179
- package/docs/stability-api-freeze.md +150 -0
- package/package.json +8 -2
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/application.mjs +1873 -0
- package/packages/client/src/application.ts +12 -0
- package/packages/server/src/application.ts +845 -0
|
@@ -0,0 +1,1873 @@
|
|
|
1
|
+
// packages/server/src/deployment.ts
|
|
2
|
+
import {
|
|
3
|
+
randomUUID
|
|
4
|
+
} from "node:crypto";
|
|
5
|
+
|
|
6
|
+
// packages/server/src/production-hardening.ts
|
|
7
|
+
var nextShutdownHookId = 1;
|
|
8
|
+
var shutdownHooks = /* @__PURE__ */ new Map();
|
|
9
|
+
function registerShutdownHook(hook, options = {}) {
|
|
10
|
+
if (typeof hook !== "function") {
|
|
11
|
+
throw new TypeError(
|
|
12
|
+
"BCP Framework: shutdown hook must be a function."
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
const id = nextShutdownHookId++;
|
|
16
|
+
const name = normalizeHookName(
|
|
17
|
+
options.name,
|
|
18
|
+
id
|
|
19
|
+
);
|
|
20
|
+
shutdownHooks.set(
|
|
21
|
+
id,
|
|
22
|
+
{
|
|
23
|
+
id,
|
|
24
|
+
name,
|
|
25
|
+
hook
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
return () => {
|
|
29
|
+
shutdownHooks.delete(
|
|
30
|
+
id
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function normalizeHookName(value, id) {
|
|
35
|
+
const normalized = value?.trim();
|
|
36
|
+
if (!normalized) {
|
|
37
|
+
return `hook-${id}`;
|
|
38
|
+
}
|
|
39
|
+
if (normalized.length > 128 || /[\r\n]/.test(
|
|
40
|
+
normalized
|
|
41
|
+
)) {
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
"BCP Framework: shutdown hook name must be at most 128 characters without line breaks."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// packages/server/src/deployment.ts
|
|
50
|
+
var DEFAULT_SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
51
|
+
var DEFAULT_READINESS_TIMEOUT_MS = 5e3;
|
|
52
|
+
var DEFAULT_SIGNALS = [
|
|
53
|
+
"SIGTERM",
|
|
54
|
+
"SIGINT"
|
|
55
|
+
];
|
|
56
|
+
function createDeploymentRuntime(options) {
|
|
57
|
+
const environment = options.environment ?? process.env;
|
|
58
|
+
const now = options.now ?? Date.now;
|
|
59
|
+
const idFactory = options.idFactory ?? randomUUID;
|
|
60
|
+
const shutdownTimeoutMs = positiveInteger(
|
|
61
|
+
options.shutdownTimeoutMs ?? parseOptionalPositiveInteger(
|
|
62
|
+
environment.BCP_SHUTDOWN_TIMEOUT_MS
|
|
63
|
+
) ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
64
|
+
"shutdownTimeoutMs"
|
|
65
|
+
);
|
|
66
|
+
const readinessTimeoutMs = positiveInteger(
|
|
67
|
+
options.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS,
|
|
68
|
+
"readinessTimeoutMs"
|
|
69
|
+
);
|
|
70
|
+
const startedAtMs = now();
|
|
71
|
+
assertTimestamp(
|
|
72
|
+
startedAtMs,
|
|
73
|
+
"runtime start timestamp"
|
|
74
|
+
);
|
|
75
|
+
const metadata = {
|
|
76
|
+
serviceName: normalizeName(
|
|
77
|
+
options.serviceName,
|
|
78
|
+
"serviceName"
|
|
79
|
+
),
|
|
80
|
+
...normalizeOptionalText(
|
|
81
|
+
options.version
|
|
82
|
+
) ? {
|
|
83
|
+
version: normalizeOptionalText(
|
|
84
|
+
options.version
|
|
85
|
+
)
|
|
86
|
+
} : {},
|
|
87
|
+
deploymentId: normalizeName(
|
|
88
|
+
options.deploymentId ?? environment.BCP_DEPLOYMENT_ID ?? idFactory(),
|
|
89
|
+
"deploymentId"
|
|
90
|
+
),
|
|
91
|
+
...normalizeOptionalText(
|
|
92
|
+
options.instanceId ?? environment.BCP_INSTANCE_ID
|
|
93
|
+
) ? {
|
|
94
|
+
instanceId: normalizeOptionalText(
|
|
95
|
+
options.instanceId ?? environment.BCP_INSTANCE_ID
|
|
96
|
+
)
|
|
97
|
+
} : {},
|
|
98
|
+
...normalizeOptionalText(
|
|
99
|
+
options.release ?? environment.BCP_RELEASE
|
|
100
|
+
) ? {
|
|
101
|
+
release: normalizeOptionalText(
|
|
102
|
+
options.release ?? environment.BCP_RELEASE
|
|
103
|
+
)
|
|
104
|
+
} : {},
|
|
105
|
+
...normalizeOptionalText(
|
|
106
|
+
options.environmentName ?? environment.NODE_ENV
|
|
107
|
+
) ? {
|
|
108
|
+
environment: normalizeOptionalText(
|
|
109
|
+
options.environmentName ?? environment.NODE_ENV
|
|
110
|
+
)
|
|
111
|
+
} : {},
|
|
112
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
113
|
+
pid: process.pid,
|
|
114
|
+
nodeVersion: process.version,
|
|
115
|
+
platform: process.platform,
|
|
116
|
+
arch: process.arch
|
|
117
|
+
};
|
|
118
|
+
const abortController = new AbortController();
|
|
119
|
+
const resourceEntries = [];
|
|
120
|
+
const names = /* @__PURE__ */ new Set();
|
|
121
|
+
let state = "idle";
|
|
122
|
+
let startPromise;
|
|
123
|
+
let shutdownPromise;
|
|
124
|
+
const runtime = {
|
|
125
|
+
metadata,
|
|
126
|
+
get state() {
|
|
127
|
+
return state;
|
|
128
|
+
},
|
|
129
|
+
addResource(resource) {
|
|
130
|
+
if (state !== "idle") {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"BCP Deployment: resources can only be registered before runtime start."
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const name = normalizeName(
|
|
136
|
+
resource.name,
|
|
137
|
+
"resource name"
|
|
138
|
+
);
|
|
139
|
+
if (names.has(name)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`BCP Deployment: resource "${name}" is already registered.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const normalized = {
|
|
145
|
+
...resource,
|
|
146
|
+
name
|
|
147
|
+
};
|
|
148
|
+
const entry = {
|
|
149
|
+
resource: normalized,
|
|
150
|
+
status: {
|
|
151
|
+
name,
|
|
152
|
+
state: "registered"
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
names.add(name);
|
|
156
|
+
resourceEntries.push(entry);
|
|
157
|
+
return () => {
|
|
158
|
+
if (state !== "idle") {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const index = resourceEntries.indexOf(
|
|
162
|
+
entry
|
|
163
|
+
);
|
|
164
|
+
if (index >= 0) {
|
|
165
|
+
resourceEntries.splice(
|
|
166
|
+
index,
|
|
167
|
+
1
|
|
168
|
+
);
|
|
169
|
+
names.delete(name);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
resources() {
|
|
174
|
+
return resourceEntries.map(
|
|
175
|
+
(entry) => ({
|
|
176
|
+
...entry.status
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
},
|
|
180
|
+
async start() {
|
|
181
|
+
if (state === "ready") {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (startPromise) {
|
|
185
|
+
return startPromise;
|
|
186
|
+
}
|
|
187
|
+
if (state === "draining" || state === "stopped") {
|
|
188
|
+
throw new Error(
|
|
189
|
+
"BCP Deployment: stopped runtime cannot be started again."
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (state === "failed") {
|
|
193
|
+
throw new Error(
|
|
194
|
+
"BCP Deployment: failed runtime cannot be started again."
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
startPromise = startResources();
|
|
198
|
+
return startPromise;
|
|
199
|
+
},
|
|
200
|
+
async readiness() {
|
|
201
|
+
const checkedAtMs = now();
|
|
202
|
+
assertTimestamp(
|
|
203
|
+
checkedAtMs,
|
|
204
|
+
"readiness timestamp"
|
|
205
|
+
);
|
|
206
|
+
const results = [];
|
|
207
|
+
for (const entry of resourceEntries) {
|
|
208
|
+
if (entry.status.state !== "started") {
|
|
209
|
+
results.push({
|
|
210
|
+
name: entry.resource.name,
|
|
211
|
+
ok: false,
|
|
212
|
+
durationMs: 0,
|
|
213
|
+
detail: `Resource state is ${entry.status.state}.`
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!entry.resource.ready) {
|
|
218
|
+
results.push({
|
|
219
|
+
name: entry.resource.name,
|
|
220
|
+
ok: true,
|
|
221
|
+
durationMs: 0
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const checkStarted = performance.now();
|
|
226
|
+
try {
|
|
227
|
+
const value = await withTimeout(
|
|
228
|
+
Promise.resolve(
|
|
229
|
+
entry.resource.ready(
|
|
230
|
+
createContext()
|
|
231
|
+
)
|
|
232
|
+
),
|
|
233
|
+
readinessTimeoutMs,
|
|
234
|
+
`readiness check for ${entry.resource.name}`
|
|
235
|
+
);
|
|
236
|
+
const normalized = typeof value === "boolean" ? {
|
|
237
|
+
ok: value
|
|
238
|
+
} : value;
|
|
239
|
+
results.push({
|
|
240
|
+
name: entry.resource.name,
|
|
241
|
+
ok: normalized.ok === true,
|
|
242
|
+
durationMs: elapsedMilliseconds(
|
|
243
|
+
checkStarted
|
|
244
|
+
),
|
|
245
|
+
...normalized.detail ? {
|
|
246
|
+
detail: normalized.detail
|
|
247
|
+
} : {}
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
results.push({
|
|
251
|
+
name: entry.resource.name,
|
|
252
|
+
ok: false,
|
|
253
|
+
durationMs: elapsedMilliseconds(
|
|
254
|
+
checkStarted
|
|
255
|
+
),
|
|
256
|
+
detail: errorMessage(error)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
ok: state === "ready" && results.every(
|
|
262
|
+
(item) => item.ok
|
|
263
|
+
),
|
|
264
|
+
state,
|
|
265
|
+
checkedAt: new Date(
|
|
266
|
+
checkedAtMs
|
|
267
|
+
).toISOString(),
|
|
268
|
+
resources: results
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
async diagnostics() {
|
|
272
|
+
const resources = [];
|
|
273
|
+
for (const entry of resourceEntries) {
|
|
274
|
+
let details;
|
|
275
|
+
let error;
|
|
276
|
+
if (entry.resource.diagnostics) {
|
|
277
|
+
try {
|
|
278
|
+
details = await entry.resource.diagnostics(
|
|
279
|
+
createContext()
|
|
280
|
+
);
|
|
281
|
+
} catch (diagnosticError) {
|
|
282
|
+
error = errorMessage(
|
|
283
|
+
diagnosticError
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
resources.push({
|
|
288
|
+
name: entry.resource.name,
|
|
289
|
+
lifecycle: {
|
|
290
|
+
...entry.status
|
|
291
|
+
},
|
|
292
|
+
...details ? {
|
|
293
|
+
details
|
|
294
|
+
} : {},
|
|
295
|
+
...error ? {
|
|
296
|
+
error
|
|
297
|
+
} : {}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
metadata: {
|
|
302
|
+
...metadata
|
|
303
|
+
},
|
|
304
|
+
state,
|
|
305
|
+
uptimeSeconds: Math.max(
|
|
306
|
+
0,
|
|
307
|
+
(now() - startedAtMs) / 1e3
|
|
308
|
+
),
|
|
309
|
+
resources
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
async shutdown(_options = {}) {
|
|
313
|
+
if (shutdownPromise) {
|
|
314
|
+
return shutdownPromise;
|
|
315
|
+
}
|
|
316
|
+
if (state === "stopped") {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
shutdownPromise = stopResources();
|
|
320
|
+
return shutdownPromise;
|
|
321
|
+
},
|
|
322
|
+
installSignalHandlers(signalOptions = {}) {
|
|
323
|
+
const signals = normalizeSignals(
|
|
324
|
+
signalOptions.signals ?? DEFAULT_SIGNALS
|
|
325
|
+
);
|
|
326
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
327
|
+
for (const signal of signals) {
|
|
328
|
+
const handler = () => {
|
|
329
|
+
void runtime.shutdown({
|
|
330
|
+
reason: signal
|
|
331
|
+
}).then(
|
|
332
|
+
() => {
|
|
333
|
+
if (signalOptions.setExitCode !== false) {
|
|
334
|
+
process.exitCode = 0;
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
() => {
|
|
338
|
+
process.exitCode = 1;
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
};
|
|
342
|
+
handlers.set(
|
|
343
|
+
signal,
|
|
344
|
+
handler
|
|
345
|
+
);
|
|
346
|
+
process.on(
|
|
347
|
+
signal,
|
|
348
|
+
handler
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return () => {
|
|
352
|
+
for (const [
|
|
353
|
+
signal,
|
|
354
|
+
handler
|
|
355
|
+
] of handlers) {
|
|
356
|
+
process.off(
|
|
357
|
+
signal,
|
|
358
|
+
handler
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
handlers.clear();
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
registerShutdownHook(name = `deployment:${metadata.serviceName}`) {
|
|
365
|
+
return registerShutdownHook(
|
|
366
|
+
() => runtime.shutdown({
|
|
367
|
+
reason: "framework-shutdown"
|
|
368
|
+
}),
|
|
369
|
+
{
|
|
370
|
+
name
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
for (const resource of options.resources ?? []) {
|
|
376
|
+
runtime.addResource(resource);
|
|
377
|
+
}
|
|
378
|
+
return runtime;
|
|
379
|
+
async function startResources() {
|
|
380
|
+
state = "starting";
|
|
381
|
+
try {
|
|
382
|
+
for (const entry of resourceEntries) {
|
|
383
|
+
entry.status = {
|
|
384
|
+
name: entry.resource.name,
|
|
385
|
+
state: "starting"
|
|
386
|
+
};
|
|
387
|
+
try {
|
|
388
|
+
await entry.resource.start?.(
|
|
389
|
+
createContext()
|
|
390
|
+
);
|
|
391
|
+
entry.status = {
|
|
392
|
+
name: entry.resource.name,
|
|
393
|
+
state: "started",
|
|
394
|
+
startedAt: new Date(
|
|
395
|
+
now()
|
|
396
|
+
).toISOString()
|
|
397
|
+
};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
entry.status = {
|
|
400
|
+
name: entry.resource.name,
|
|
401
|
+
state: "failed",
|
|
402
|
+
error: errorMessage(error)
|
|
403
|
+
};
|
|
404
|
+
throw new Error(
|
|
405
|
+
`BCP Deployment: resource "${entry.resource.name}" failed to start. ${errorMessage(error)}`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
state = "ready";
|
|
410
|
+
} catch (error) {
|
|
411
|
+
state = "failed";
|
|
412
|
+
abortController.abort(error);
|
|
413
|
+
await stopStartedResources(
|
|
414
|
+
shutdownTimeoutMs
|
|
415
|
+
);
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function stopResources() {
|
|
420
|
+
state = "draining";
|
|
421
|
+
if (!abortController.signal.aborted) {
|
|
422
|
+
abortController.abort(
|
|
423
|
+
new Error(
|
|
424
|
+
"BCP Deployment: runtime is shutting down."
|
|
425
|
+
)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const failures = await stopStartedResources(
|
|
429
|
+
shutdownTimeoutMs
|
|
430
|
+
);
|
|
431
|
+
state = failures.length > 0 ? "failed" : "stopped";
|
|
432
|
+
if (failures.length > 0) {
|
|
433
|
+
throw new AggregateError(
|
|
434
|
+
failures,
|
|
435
|
+
"BCP Deployment: one or more resources failed to stop."
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async function stopStartedResources(timeoutMs) {
|
|
440
|
+
const failures = [];
|
|
441
|
+
const deadline = Date.now() + timeoutMs;
|
|
442
|
+
for (const entry of [...resourceEntries].reverse()) {
|
|
443
|
+
if (entry.status.state !== "started") {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
entry.status = {
|
|
447
|
+
...entry.status,
|
|
448
|
+
state: "stopping"
|
|
449
|
+
};
|
|
450
|
+
try {
|
|
451
|
+
const remaining = Math.max(
|
|
452
|
+
1,
|
|
453
|
+
deadline - Date.now()
|
|
454
|
+
);
|
|
455
|
+
await withTimeout(
|
|
456
|
+
Promise.resolve(
|
|
457
|
+
entry.resource.stop?.(
|
|
458
|
+
createContext()
|
|
459
|
+
)
|
|
460
|
+
),
|
|
461
|
+
remaining,
|
|
462
|
+
`shutdown of ${entry.resource.name}`
|
|
463
|
+
);
|
|
464
|
+
entry.status = {
|
|
465
|
+
...entry.status,
|
|
466
|
+
state: "stopped",
|
|
467
|
+
stoppedAt: new Date(
|
|
468
|
+
now()
|
|
469
|
+
).toISOString()
|
|
470
|
+
};
|
|
471
|
+
} catch (error) {
|
|
472
|
+
const message = errorMessage(error);
|
|
473
|
+
entry.status = {
|
|
474
|
+
...entry.status,
|
|
475
|
+
state: "failed",
|
|
476
|
+
error: message
|
|
477
|
+
};
|
|
478
|
+
failures.push(
|
|
479
|
+
new Error(
|
|
480
|
+
`BCP Deployment: resource "${entry.resource.name}" failed to stop. ${message}`
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return failures;
|
|
486
|
+
}
|
|
487
|
+
function createContext() {
|
|
488
|
+
return {
|
|
489
|
+
metadata,
|
|
490
|
+
signal: abortController.signal
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function normalizeSignals(signals) {
|
|
495
|
+
return Array.from(
|
|
496
|
+
new Set(signals)
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
function normalizeName(value, field) {
|
|
500
|
+
const normalized = String(value ?? "").trim();
|
|
501
|
+
if (!normalized) {
|
|
502
|
+
throw new TypeError(
|
|
503
|
+
`BCP Deployment: ${field} must be a non-empty string.`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
if (normalized.length > 256 || /[\r\n]/.test(normalized)) {
|
|
507
|
+
throw new TypeError(
|
|
508
|
+
`BCP Deployment: ${field} must be at most 256 characters without line breaks.`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
return normalized;
|
|
512
|
+
}
|
|
513
|
+
function normalizeOptionalText(value) {
|
|
514
|
+
if (value === void 0) {
|
|
515
|
+
return void 0;
|
|
516
|
+
}
|
|
517
|
+
const normalized = value.trim();
|
|
518
|
+
return normalized || void 0;
|
|
519
|
+
}
|
|
520
|
+
function positiveInteger(value, field) {
|
|
521
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
522
|
+
throw new TypeError(
|
|
523
|
+
`BCP Deployment: ${field} must be a positive safe integer.`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
function parseOptionalPositiveInteger(value) {
|
|
529
|
+
if (value === void 0) {
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
const parsed = Number(value.trim());
|
|
533
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
534
|
+
throw new TypeError(
|
|
535
|
+
"BCP Deployment: BCP_SHUTDOWN_TIMEOUT_MS must be a positive safe integer."
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return parsed;
|
|
539
|
+
}
|
|
540
|
+
function assertTimestamp(value, field) {
|
|
541
|
+
if (!Number.isFinite(value)) {
|
|
542
|
+
throw new TypeError(
|
|
543
|
+
`BCP Deployment: ${field} must be a finite number.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function errorMessage(error) {
|
|
548
|
+
return error instanceof Error ? error.message : String(error);
|
|
549
|
+
}
|
|
550
|
+
function elapsedMilliseconds(startedAt) {
|
|
551
|
+
return Math.max(
|
|
552
|
+
0,
|
|
553
|
+
performance.now() - startedAt
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
557
|
+
return new Promise(
|
|
558
|
+
(resolve, reject) => {
|
|
559
|
+
const timer = setTimeout(
|
|
560
|
+
() => reject(
|
|
561
|
+
new Error(
|
|
562
|
+
`BCP Deployment: ${label} timed out after ${timeoutMs}ms.`
|
|
563
|
+
)
|
|
564
|
+
),
|
|
565
|
+
timeoutMs
|
|
566
|
+
);
|
|
567
|
+
timer.unref?.();
|
|
568
|
+
promise.then(
|
|
569
|
+
(value) => {
|
|
570
|
+
clearTimeout(timer);
|
|
571
|
+
resolve(value);
|
|
572
|
+
},
|
|
573
|
+
(error) => {
|
|
574
|
+
clearTimeout(timer);
|
|
575
|
+
reject(error);
|
|
576
|
+
}
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// packages/server/src/plugins.ts
|
|
583
|
+
var PluginDependencyError = class extends Error {
|
|
584
|
+
constructor(message) {
|
|
585
|
+
super(message);
|
|
586
|
+
this.name = "PluginDependencyError";
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
var PluginLifecycleError = class extends Error {
|
|
590
|
+
plugin;
|
|
591
|
+
phase;
|
|
592
|
+
cause;
|
|
593
|
+
constructor(plugin, phase, cause) {
|
|
594
|
+
super(
|
|
595
|
+
`BCP Plugins: ${phase} failed for plugin "${plugin}": ${formatError(cause)}`
|
|
596
|
+
);
|
|
597
|
+
this.name = "PluginLifecycleError";
|
|
598
|
+
this.plugin = plugin;
|
|
599
|
+
this.phase = phase;
|
|
600
|
+
this.cause = cause;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
function defineModule(module) {
|
|
604
|
+
if (!module || typeof module !== "object") {
|
|
605
|
+
throw new TypeError(
|
|
606
|
+
"BCP Plugins: module must be an object."
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
normalizeName2(
|
|
610
|
+
module.name,
|
|
611
|
+
"module name"
|
|
612
|
+
);
|
|
613
|
+
if (!Array.isArray(module.plugins)) {
|
|
614
|
+
throw new TypeError(
|
|
615
|
+
"BCP Plugins: module plugins must be an array."
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
for (const plugin of module.plugins) {
|
|
619
|
+
validatePluginDefinition(plugin);
|
|
620
|
+
}
|
|
621
|
+
return module;
|
|
622
|
+
}
|
|
623
|
+
function createPluginServiceRegistry(initial) {
|
|
624
|
+
const values = /* @__PURE__ */ new Map();
|
|
625
|
+
if (initial) {
|
|
626
|
+
for (const [key, value] of initial) {
|
|
627
|
+
assertServiceKey(key);
|
|
628
|
+
if (values.has(key)) {
|
|
629
|
+
throw new Error(
|
|
630
|
+
`BCP Plugins: duplicate initial service ${formatServiceKey(key)}.`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
values.set(key, value);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const registry = {
|
|
637
|
+
provide(key, value, options = {}) {
|
|
638
|
+
assertServiceKey(key);
|
|
639
|
+
if (values.has(key) && !options.replace) {
|
|
640
|
+
throw new Error(
|
|
641
|
+
`BCP Plugins: service ${formatServiceKey(key)} is already registered.`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
values.set(key, value);
|
|
645
|
+
},
|
|
646
|
+
get(key) {
|
|
647
|
+
assertServiceKey(key);
|
|
648
|
+
if (!values.has(key)) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`BCP Plugins: service ${formatServiceKey(key)} is not registered.`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
return values.get(key);
|
|
654
|
+
},
|
|
655
|
+
optional(key) {
|
|
656
|
+
assertServiceKey(key);
|
|
657
|
+
return values.get(key);
|
|
658
|
+
},
|
|
659
|
+
has(key) {
|
|
660
|
+
assertServiceKey(key);
|
|
661
|
+
return values.has(key);
|
|
662
|
+
},
|
|
663
|
+
delete(key) {
|
|
664
|
+
assertServiceKey(key);
|
|
665
|
+
return values.delete(key);
|
|
666
|
+
},
|
|
667
|
+
keys() {
|
|
668
|
+
return [
|
|
669
|
+
...values.keys()
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
return registry;
|
|
674
|
+
}
|
|
675
|
+
function createPluginHookBus() {
|
|
676
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
677
|
+
const bus = {
|
|
678
|
+
on(name, handler) {
|
|
679
|
+
const normalized = normalizeName2(
|
|
680
|
+
name,
|
|
681
|
+
"hook name"
|
|
682
|
+
);
|
|
683
|
+
if (typeof handler !== "function") {
|
|
684
|
+
throw new TypeError(
|
|
685
|
+
"BCP Plugins: hook handler must be a function."
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
let group = hooks.get(normalized);
|
|
689
|
+
if (!group) {
|
|
690
|
+
group = /* @__PURE__ */ new Set();
|
|
691
|
+
hooks.set(
|
|
692
|
+
normalized,
|
|
693
|
+
group
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
group.add(
|
|
697
|
+
handler
|
|
698
|
+
);
|
|
699
|
+
return () => {
|
|
700
|
+
group?.delete(
|
|
701
|
+
handler
|
|
702
|
+
);
|
|
703
|
+
if (group?.size === 0) {
|
|
704
|
+
hooks.delete(normalized);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
},
|
|
708
|
+
async emit(name, payload) {
|
|
709
|
+
const normalized = normalizeName2(
|
|
710
|
+
name,
|
|
711
|
+
"hook name"
|
|
712
|
+
);
|
|
713
|
+
const group = hooks.get(normalized);
|
|
714
|
+
if (!group) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
for (const handler of [
|
|
718
|
+
...group
|
|
719
|
+
]) {
|
|
720
|
+
await handler(payload);
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
listenerCount(name) {
|
|
724
|
+
return hooks.get(
|
|
725
|
+
normalizeName2(
|
|
726
|
+
name,
|
|
727
|
+
"hook name"
|
|
728
|
+
)
|
|
729
|
+
)?.size ?? 0;
|
|
730
|
+
},
|
|
731
|
+
clear(name) {
|
|
732
|
+
if (name === void 0) {
|
|
733
|
+
hooks.clear();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
hooks.delete(
|
|
737
|
+
normalizeName2(
|
|
738
|
+
name,
|
|
739
|
+
"hook name"
|
|
740
|
+
)
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
return bus;
|
|
745
|
+
}
|
|
746
|
+
function createPluginHost(options = {}) {
|
|
747
|
+
const now = options.now ?? Date.now;
|
|
748
|
+
const services = createPluginServiceRegistry(
|
|
749
|
+
options.services
|
|
750
|
+
);
|
|
751
|
+
const hooks = createPluginHookBus();
|
|
752
|
+
const entries = /* @__PURE__ */ new Map();
|
|
753
|
+
const configs = {
|
|
754
|
+
...options.configs ?? {}
|
|
755
|
+
};
|
|
756
|
+
let setupCompleted = false;
|
|
757
|
+
let started = false;
|
|
758
|
+
let closed = false;
|
|
759
|
+
let lifecycleActive = false;
|
|
760
|
+
let fatalError = null;
|
|
761
|
+
const host = {
|
|
762
|
+
services,
|
|
763
|
+
hooks,
|
|
764
|
+
get started() {
|
|
765
|
+
return started;
|
|
766
|
+
},
|
|
767
|
+
use(extension) {
|
|
768
|
+
assertMutable();
|
|
769
|
+
if (isPluginModule(extension)) {
|
|
770
|
+
defineModule(extension);
|
|
771
|
+
for (const plugin of extension.plugins) {
|
|
772
|
+
register(plugin);
|
|
773
|
+
}
|
|
774
|
+
return host;
|
|
775
|
+
}
|
|
776
|
+
register(extension);
|
|
777
|
+
return host;
|
|
778
|
+
},
|
|
779
|
+
resolveOrder() {
|
|
780
|
+
return resolvePluginOrder(
|
|
781
|
+
entries
|
|
782
|
+
);
|
|
783
|
+
},
|
|
784
|
+
async setup() {
|
|
785
|
+
assertOpen();
|
|
786
|
+
assertHealthy();
|
|
787
|
+
if (setupCompleted) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
assertNotActive();
|
|
791
|
+
lifecycleActive = true;
|
|
792
|
+
try {
|
|
793
|
+
const order = resolvePluginOrder(
|
|
794
|
+
entries
|
|
795
|
+
);
|
|
796
|
+
for (const name of order) {
|
|
797
|
+
const entry = requireInternal(name);
|
|
798
|
+
if (entry.record.state !== "registered") {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
entry.record.state = "setting-up";
|
|
802
|
+
try {
|
|
803
|
+
const context = createContext(entry);
|
|
804
|
+
entry.context = context;
|
|
805
|
+
await entry.definition.setup?.(
|
|
806
|
+
context
|
|
807
|
+
);
|
|
808
|
+
entry.record.state = "ready";
|
|
809
|
+
entry.record.setupAt = timestamp();
|
|
810
|
+
} catch (error) {
|
|
811
|
+
markFailed(
|
|
812
|
+
entry,
|
|
813
|
+
error
|
|
814
|
+
);
|
|
815
|
+
const lifecycleError = error instanceof PluginLifecycleError ? error : new PluginLifecycleError(
|
|
816
|
+
name,
|
|
817
|
+
"setup",
|
|
818
|
+
error
|
|
819
|
+
);
|
|
820
|
+
fatalError = lifecycleError;
|
|
821
|
+
await disposePrepared(
|
|
822
|
+
order,
|
|
823
|
+
name
|
|
824
|
+
);
|
|
825
|
+
throw lifecycleError;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
setupCompleted = true;
|
|
829
|
+
} finally {
|
|
830
|
+
lifecycleActive = false;
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
async start() {
|
|
834
|
+
assertOpen();
|
|
835
|
+
assertHealthy();
|
|
836
|
+
if (started) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (!setupCompleted) {
|
|
840
|
+
await host.setup();
|
|
841
|
+
}
|
|
842
|
+
assertHealthy();
|
|
843
|
+
assertNotActive();
|
|
844
|
+
lifecycleActive = true;
|
|
845
|
+
const order = resolvePluginOrder(
|
|
846
|
+
entries
|
|
847
|
+
);
|
|
848
|
+
const startedNames = [];
|
|
849
|
+
try {
|
|
850
|
+
for (const name of order) {
|
|
851
|
+
const entry = requireInternal(name);
|
|
852
|
+
if (entry.record.state !== "ready" && entry.record.state !== "stopped") {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
entry.record.state = "starting";
|
|
856
|
+
try {
|
|
857
|
+
await entry.definition.start?.(
|
|
858
|
+
requireContext(entry)
|
|
859
|
+
);
|
|
860
|
+
entry.record.state = "started";
|
|
861
|
+
entry.record.startedAt = timestamp();
|
|
862
|
+
entry.record.error = void 0;
|
|
863
|
+
startedNames.push(name);
|
|
864
|
+
} catch (error) {
|
|
865
|
+
markFailed(
|
|
866
|
+
entry,
|
|
867
|
+
error
|
|
868
|
+
);
|
|
869
|
+
const lifecycleError = new PluginLifecycleError(
|
|
870
|
+
name,
|
|
871
|
+
"start",
|
|
872
|
+
error
|
|
873
|
+
);
|
|
874
|
+
fatalError = lifecycleError;
|
|
875
|
+
await stopNames(
|
|
876
|
+
[
|
|
877
|
+
...startedNames
|
|
878
|
+
].reverse()
|
|
879
|
+
);
|
|
880
|
+
throw lifecycleError;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
started = true;
|
|
884
|
+
} finally {
|
|
885
|
+
lifecycleActive = false;
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
async stop() {
|
|
889
|
+
if (closed || !started) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
assertNotActive();
|
|
893
|
+
lifecycleActive = true;
|
|
894
|
+
try {
|
|
895
|
+
const errors = await stopNames(
|
|
896
|
+
resolvePluginOrder(
|
|
897
|
+
entries
|
|
898
|
+
).reverse()
|
|
899
|
+
);
|
|
900
|
+
started = false;
|
|
901
|
+
if (errors.length > 0) {
|
|
902
|
+
throw new AggregateError(
|
|
903
|
+
errors,
|
|
904
|
+
"BCP Plugins: one or more plugin stop hooks failed."
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
} finally {
|
|
908
|
+
lifecycleActive = false;
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
async close() {
|
|
912
|
+
if (closed) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const errors = [];
|
|
916
|
+
if (started) {
|
|
917
|
+
try {
|
|
918
|
+
await host.stop();
|
|
919
|
+
} catch (error) {
|
|
920
|
+
if (error instanceof AggregateError) {
|
|
921
|
+
errors.push(
|
|
922
|
+
...error.errors
|
|
923
|
+
);
|
|
924
|
+
} else {
|
|
925
|
+
errors.push(error);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
assertNotActive();
|
|
930
|
+
lifecycleActive = true;
|
|
931
|
+
try {
|
|
932
|
+
const order = resolvePluginOrder(
|
|
933
|
+
entries
|
|
934
|
+
).reverse();
|
|
935
|
+
for (const name of order) {
|
|
936
|
+
const entry = requireInternal(name);
|
|
937
|
+
if (entry.disposed || !entry.context) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
await entry.definition.dispose?.(
|
|
942
|
+
entry.context
|
|
943
|
+
);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
errors.push(
|
|
946
|
+
new PluginLifecycleError(
|
|
947
|
+
name,
|
|
948
|
+
"dispose",
|
|
949
|
+
error
|
|
950
|
+
)
|
|
951
|
+
);
|
|
952
|
+
} finally {
|
|
953
|
+
entry.disposed = true;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
hooks.clear();
|
|
957
|
+
closed = true;
|
|
958
|
+
} finally {
|
|
959
|
+
lifecycleActive = false;
|
|
960
|
+
}
|
|
961
|
+
if (errors.length > 0) {
|
|
962
|
+
throw new AggregateError(
|
|
963
|
+
errors,
|
|
964
|
+
"BCP Plugins: one or more plugin shutdown hooks failed."
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
plugin(name) {
|
|
969
|
+
const entry = entries.get(
|
|
970
|
+
normalizeName2(
|
|
971
|
+
name,
|
|
972
|
+
"plugin name"
|
|
973
|
+
)
|
|
974
|
+
);
|
|
975
|
+
return entry ? cloneRecord(
|
|
976
|
+
entry.record
|
|
977
|
+
) : null;
|
|
978
|
+
},
|
|
979
|
+
plugins() {
|
|
980
|
+
return [
|
|
981
|
+
...entries.values()
|
|
982
|
+
].map(
|
|
983
|
+
(entry) => cloneRecord(
|
|
984
|
+
entry.record
|
|
985
|
+
)
|
|
986
|
+
).sort(
|
|
987
|
+
(left, right) => left.registeredAt - right.registeredAt || left.name.localeCompare(
|
|
988
|
+
right.name
|
|
989
|
+
)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
for (const module of options.modules ?? []) {
|
|
994
|
+
host.use(module);
|
|
995
|
+
}
|
|
996
|
+
for (const plugin of options.plugins ?? []) {
|
|
997
|
+
host.use(plugin);
|
|
998
|
+
}
|
|
999
|
+
return host;
|
|
1000
|
+
function register(definition) {
|
|
1001
|
+
validatePluginDefinition(
|
|
1002
|
+
definition
|
|
1003
|
+
);
|
|
1004
|
+
const name = normalizeName2(
|
|
1005
|
+
definition.name,
|
|
1006
|
+
"plugin name"
|
|
1007
|
+
);
|
|
1008
|
+
if (entries.has(name)) {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`BCP Plugins: plugin "${name}" is already registered.`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
const requires = normalizeDependencyList(
|
|
1014
|
+
definition.requires,
|
|
1015
|
+
name,
|
|
1016
|
+
"requires"
|
|
1017
|
+
);
|
|
1018
|
+
const optional = normalizeDependencyList(
|
|
1019
|
+
definition.optional,
|
|
1020
|
+
name,
|
|
1021
|
+
"optional"
|
|
1022
|
+
);
|
|
1023
|
+
entries.set(
|
|
1024
|
+
name,
|
|
1025
|
+
{
|
|
1026
|
+
definition: {
|
|
1027
|
+
...definition,
|
|
1028
|
+
name,
|
|
1029
|
+
requires,
|
|
1030
|
+
optional
|
|
1031
|
+
},
|
|
1032
|
+
record: {
|
|
1033
|
+
name,
|
|
1034
|
+
version: normalizeOptionalVersion(
|
|
1035
|
+
definition.version
|
|
1036
|
+
),
|
|
1037
|
+
state: "registered",
|
|
1038
|
+
requires,
|
|
1039
|
+
optional,
|
|
1040
|
+
registeredAt: timestamp()
|
|
1041
|
+
},
|
|
1042
|
+
disposed: false
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
function createContext(entry) {
|
|
1047
|
+
const rawConfig = Object.prototype.hasOwnProperty.call(
|
|
1048
|
+
configs,
|
|
1049
|
+
entry.record.name
|
|
1050
|
+
) ? configs[entry.record.name] : entry.definition.config;
|
|
1051
|
+
const config = parseConfig(
|
|
1052
|
+
entry.definition.schema,
|
|
1053
|
+
rawConfig,
|
|
1054
|
+
entry.record.name
|
|
1055
|
+
);
|
|
1056
|
+
return {
|
|
1057
|
+
name: entry.record.name,
|
|
1058
|
+
config,
|
|
1059
|
+
services,
|
|
1060
|
+
hooks,
|
|
1061
|
+
host
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
async function stopNames(names) {
|
|
1065
|
+
const errors = [];
|
|
1066
|
+
for (const name of names) {
|
|
1067
|
+
const entry = requireInternal(name);
|
|
1068
|
+
if (entry.record.state !== "started") {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
entry.record.state = "stopping";
|
|
1072
|
+
try {
|
|
1073
|
+
await entry.definition.stop?.(
|
|
1074
|
+
requireContext(entry)
|
|
1075
|
+
);
|
|
1076
|
+
entry.record.state = "stopped";
|
|
1077
|
+
entry.record.stoppedAt = timestamp();
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
markFailed(
|
|
1080
|
+
entry,
|
|
1081
|
+
error
|
|
1082
|
+
);
|
|
1083
|
+
errors.push(
|
|
1084
|
+
new PluginLifecycleError(
|
|
1085
|
+
name,
|
|
1086
|
+
"stop",
|
|
1087
|
+
error
|
|
1088
|
+
)
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return errors;
|
|
1093
|
+
}
|
|
1094
|
+
async function disposePrepared(order, failedName) {
|
|
1095
|
+
const index = order.indexOf(failedName);
|
|
1096
|
+
const names = order.slice(
|
|
1097
|
+
0,
|
|
1098
|
+
Math.max(0, index) + 1
|
|
1099
|
+
).reverse();
|
|
1100
|
+
for (const name of names) {
|
|
1101
|
+
const entry = requireInternal(name);
|
|
1102
|
+
if (entry.disposed || !entry.context) {
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
await entry.definition.dispose?.(
|
|
1107
|
+
entry.context
|
|
1108
|
+
);
|
|
1109
|
+
} catch {
|
|
1110
|
+
} finally {
|
|
1111
|
+
entry.disposed = true;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
function requireInternal(name) {
|
|
1116
|
+
const entry = entries.get(name);
|
|
1117
|
+
if (!entry) {
|
|
1118
|
+
throw new Error(
|
|
1119
|
+
`BCP Plugins: plugin "${name}" is not registered.`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
return entry;
|
|
1123
|
+
}
|
|
1124
|
+
function timestamp() {
|
|
1125
|
+
const value = now();
|
|
1126
|
+
if (!Number.isFinite(value)) {
|
|
1127
|
+
throw new TypeError(
|
|
1128
|
+
"BCP Plugins: now() must return a finite number."
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
return value;
|
|
1132
|
+
}
|
|
1133
|
+
function assertOpen() {
|
|
1134
|
+
if (closed) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
"BCP Plugins: plugin host is closed."
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function assertHealthy() {
|
|
1141
|
+
if (fatalError) {
|
|
1142
|
+
throw fatalError;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function assertMutable() {
|
|
1146
|
+
assertOpen();
|
|
1147
|
+
if (setupCompleted || lifecycleActive || fatalError) {
|
|
1148
|
+
throw new Error(
|
|
1149
|
+
"BCP Plugins: plugins cannot be registered after setup begins or after a lifecycle failure."
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
function assertNotActive() {
|
|
1154
|
+
if (lifecycleActive) {
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
"BCP Plugins: another lifecycle transition is already running."
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function resolvePluginOrder(entries) {
|
|
1162
|
+
for (const entry of entries.values()) {
|
|
1163
|
+
for (const dependency of entry.record.requires) {
|
|
1164
|
+
if (!entries.has(dependency)) {
|
|
1165
|
+
throw new PluginDependencyError(
|
|
1166
|
+
`BCP Plugins: plugin "${entry.record.name}" requires missing plugin "${dependency}".`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1172
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1173
|
+
const order = [];
|
|
1174
|
+
const stack = [];
|
|
1175
|
+
const visit = (name) => {
|
|
1176
|
+
if (visited.has(name)) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (visiting.has(name)) {
|
|
1180
|
+
const start = stack.indexOf(name);
|
|
1181
|
+
const cycle = [
|
|
1182
|
+
...stack.slice(
|
|
1183
|
+
Math.max(0, start)
|
|
1184
|
+
),
|
|
1185
|
+
name
|
|
1186
|
+
];
|
|
1187
|
+
throw new PluginDependencyError(
|
|
1188
|
+
`BCP Plugins: dependency cycle detected: ${cycle.join(" -> ")}.`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const entry = entries.get(name);
|
|
1192
|
+
if (!entry) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
visiting.add(name);
|
|
1196
|
+
stack.push(name);
|
|
1197
|
+
for (const dependency of [
|
|
1198
|
+
...entry.record.requires,
|
|
1199
|
+
...entry.record.optional.filter(
|
|
1200
|
+
(candidate) => entries.has(candidate)
|
|
1201
|
+
)
|
|
1202
|
+
]) {
|
|
1203
|
+
visit(dependency);
|
|
1204
|
+
}
|
|
1205
|
+
stack.pop();
|
|
1206
|
+
visiting.delete(name);
|
|
1207
|
+
visited.add(name);
|
|
1208
|
+
order.push(name);
|
|
1209
|
+
};
|
|
1210
|
+
for (const name of entries.keys()) {
|
|
1211
|
+
visit(name);
|
|
1212
|
+
}
|
|
1213
|
+
return order;
|
|
1214
|
+
}
|
|
1215
|
+
function validatePluginDefinition(definition) {
|
|
1216
|
+
if (!definition || typeof definition !== "object") {
|
|
1217
|
+
throw new TypeError(
|
|
1218
|
+
"BCP Plugins: plugin definition must be an object."
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const name = normalizeName2(
|
|
1222
|
+
definition.name,
|
|
1223
|
+
"plugin name"
|
|
1224
|
+
);
|
|
1225
|
+
normalizeDependencyList(
|
|
1226
|
+
definition.requires,
|
|
1227
|
+
name,
|
|
1228
|
+
"requires"
|
|
1229
|
+
);
|
|
1230
|
+
normalizeDependencyList(
|
|
1231
|
+
definition.optional,
|
|
1232
|
+
name,
|
|
1233
|
+
"optional"
|
|
1234
|
+
);
|
|
1235
|
+
for (const hook of [
|
|
1236
|
+
"setup",
|
|
1237
|
+
"start",
|
|
1238
|
+
"stop",
|
|
1239
|
+
"dispose"
|
|
1240
|
+
]) {
|
|
1241
|
+
const value = definition[hook];
|
|
1242
|
+
if (value !== void 0 && typeof value !== "function") {
|
|
1243
|
+
throw new TypeError(
|
|
1244
|
+
`BCP Plugins: plugin "${name}" ${hook} must be a function.`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (definition.schema !== void 0 && typeof definition.schema !== "function" && (!definition.schema || typeof definition.schema.parse !== "function")) {
|
|
1249
|
+
throw new TypeError(
|
|
1250
|
+
`BCP Plugins: plugin "${name}" schema must be a parser function or object with parse().`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
function normalizeDependencyList(value, plugin, field) {
|
|
1255
|
+
if (value === void 0) {
|
|
1256
|
+
return [];
|
|
1257
|
+
}
|
|
1258
|
+
if (!Array.isArray(value)) {
|
|
1259
|
+
throw new TypeError(
|
|
1260
|
+
`BCP Plugins: plugin "${plugin}" ${field} must be an array.`
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
const normalized = value.map(
|
|
1264
|
+
(item) => normalizeName2(
|
|
1265
|
+
item,
|
|
1266
|
+
`${field} dependency`
|
|
1267
|
+
)
|
|
1268
|
+
);
|
|
1269
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
`BCP Plugins: plugin "${plugin}" ${field} contains duplicate dependencies.`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
if (normalized.includes(plugin)) {
|
|
1275
|
+
throw new PluginDependencyError(
|
|
1276
|
+
`BCP Plugins: plugin "${plugin}" cannot depend on itself.`
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
return normalized;
|
|
1280
|
+
}
|
|
1281
|
+
function parseConfig(parser, raw, plugin) {
|
|
1282
|
+
if (!parser) {
|
|
1283
|
+
return raw;
|
|
1284
|
+
}
|
|
1285
|
+
try {
|
|
1286
|
+
return typeof parser === "function" ? parser(raw) : parser.parse(raw);
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
throw new PluginLifecycleError(
|
|
1289
|
+
plugin,
|
|
1290
|
+
"config",
|
|
1291
|
+
error
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
function requireContext(entry) {
|
|
1296
|
+
if (!entry.context) {
|
|
1297
|
+
throw new Error(
|
|
1298
|
+
`BCP Plugins: plugin "${entry.record.name}" has not been set up.`
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
return entry.context;
|
|
1302
|
+
}
|
|
1303
|
+
function markFailed(entry, error) {
|
|
1304
|
+
entry.record.state = "failed";
|
|
1305
|
+
entry.record.error = formatError(error);
|
|
1306
|
+
}
|
|
1307
|
+
function cloneRecord(record) {
|
|
1308
|
+
return {
|
|
1309
|
+
...record,
|
|
1310
|
+
requires: [
|
|
1311
|
+
...record.requires
|
|
1312
|
+
],
|
|
1313
|
+
optional: [
|
|
1314
|
+
...record.optional
|
|
1315
|
+
]
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function isPluginModule(value) {
|
|
1319
|
+
return Boolean(
|
|
1320
|
+
value && typeof value === "object" && Array.isArray(
|
|
1321
|
+
value.plugins
|
|
1322
|
+
)
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
function normalizeName2(value, field) {
|
|
1326
|
+
const text = String(value ?? "").trim();
|
|
1327
|
+
if (!text) {
|
|
1328
|
+
throw new TypeError(
|
|
1329
|
+
`BCP Plugins: ${field} must be a non-empty string.`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (text.length > 200) {
|
|
1333
|
+
throw new TypeError(
|
|
1334
|
+
`BCP Plugins: ${field} must not exceed 200 characters.`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
return text;
|
|
1338
|
+
}
|
|
1339
|
+
function normalizeOptionalVersion(value) {
|
|
1340
|
+
if (value === void 0) {
|
|
1341
|
+
return void 0;
|
|
1342
|
+
}
|
|
1343
|
+
return normalizeName2(
|
|
1344
|
+
value,
|
|
1345
|
+
"plugin version"
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
function assertServiceKey(key) {
|
|
1349
|
+
if (typeof key === "string") {
|
|
1350
|
+
normalizeName2(
|
|
1351
|
+
key,
|
|
1352
|
+
"service key"
|
|
1353
|
+
);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (typeof key !== "symbol") {
|
|
1357
|
+
throw new TypeError(
|
|
1358
|
+
"BCP Plugins: service key must be a string or symbol."
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function formatServiceKey(key) {
|
|
1363
|
+
return typeof key === "symbol" ? String(key) : `"${key}"`;
|
|
1364
|
+
}
|
|
1365
|
+
function formatError(error) {
|
|
1366
|
+
if (error instanceof Error) {
|
|
1367
|
+
return error.message || error.name;
|
|
1368
|
+
}
|
|
1369
|
+
if (typeof error === "string") {
|
|
1370
|
+
return error;
|
|
1371
|
+
}
|
|
1372
|
+
try {
|
|
1373
|
+
return JSON.stringify(error) ?? String(error);
|
|
1374
|
+
} catch {
|
|
1375
|
+
return String(error);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// packages/server/src/application.ts
|
|
1380
|
+
var DEFAULT_APPLICATION_SIGNALS = [
|
|
1381
|
+
"SIGTERM",
|
|
1382
|
+
"SIGINT"
|
|
1383
|
+
];
|
|
1384
|
+
var ApplicationLifecycleError = class extends Error {
|
|
1385
|
+
phase;
|
|
1386
|
+
cause;
|
|
1387
|
+
constructor(phase, cause) {
|
|
1388
|
+
super(
|
|
1389
|
+
`BCP Application: ${phase} failed: ${formatError2(cause)}`
|
|
1390
|
+
);
|
|
1391
|
+
this.name = "ApplicationLifecycleError";
|
|
1392
|
+
this.phase = phase;
|
|
1393
|
+
this.cause = cause;
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
function defineApp(definition) {
|
|
1397
|
+
validateDefinition(definition);
|
|
1398
|
+
return definition;
|
|
1399
|
+
}
|
|
1400
|
+
function createApp(definition) {
|
|
1401
|
+
defineApp(definition);
|
|
1402
|
+
const name = normalizeName3(
|
|
1403
|
+
definition.name,
|
|
1404
|
+
"application name"
|
|
1405
|
+
);
|
|
1406
|
+
const version = normalizeOptionalText2(
|
|
1407
|
+
definition.version
|
|
1408
|
+
);
|
|
1409
|
+
const config = parseConfig2(
|
|
1410
|
+
definition.schema,
|
|
1411
|
+
definition.config
|
|
1412
|
+
);
|
|
1413
|
+
const plugins = createPluginHost({
|
|
1414
|
+
plugins: definition.plugins,
|
|
1415
|
+
modules: definition.modules,
|
|
1416
|
+
configs: definition.pluginConfigs,
|
|
1417
|
+
services: definition.services
|
|
1418
|
+
});
|
|
1419
|
+
const deployment = createDeploymentRuntime({
|
|
1420
|
+
...definition.deployment ?? {},
|
|
1421
|
+
serviceName: name,
|
|
1422
|
+
...version ? {
|
|
1423
|
+
version
|
|
1424
|
+
} : {}
|
|
1425
|
+
});
|
|
1426
|
+
let state = "created";
|
|
1427
|
+
let startPromise = null;
|
|
1428
|
+
let stopPromise = null;
|
|
1429
|
+
let setupCompleted = false;
|
|
1430
|
+
let applicationResourceRegistered = false;
|
|
1431
|
+
let disposed = false;
|
|
1432
|
+
let failure = null;
|
|
1433
|
+
const context = {
|
|
1434
|
+
name,
|
|
1435
|
+
...version ? {
|
|
1436
|
+
version
|
|
1437
|
+
} : {},
|
|
1438
|
+
config,
|
|
1439
|
+
services: plugins.services,
|
|
1440
|
+
hooks: plugins.hooks,
|
|
1441
|
+
plugins,
|
|
1442
|
+
deployment,
|
|
1443
|
+
get metadata() {
|
|
1444
|
+
return deployment.metadata;
|
|
1445
|
+
},
|
|
1446
|
+
get state() {
|
|
1447
|
+
return state;
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
deployment.addResource({
|
|
1451
|
+
name: "bcp:plugins",
|
|
1452
|
+
async start() {
|
|
1453
|
+
await plugins.start();
|
|
1454
|
+
},
|
|
1455
|
+
ready() {
|
|
1456
|
+
return {
|
|
1457
|
+
ok: plugins.started,
|
|
1458
|
+
detail: plugins.started ? "Plugin host started." : "Plugin host is not started."
|
|
1459
|
+
};
|
|
1460
|
+
},
|
|
1461
|
+
async stop() {
|
|
1462
|
+
await plugins.close();
|
|
1463
|
+
},
|
|
1464
|
+
diagnostics() {
|
|
1465
|
+
return {
|
|
1466
|
+
started: plugins.started,
|
|
1467
|
+
plugins: plugins.plugins()
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
for (const resource of definition.resources ?? []) {
|
|
1472
|
+
deployment.addResource(resource);
|
|
1473
|
+
}
|
|
1474
|
+
const app = {
|
|
1475
|
+
name,
|
|
1476
|
+
...version ? {
|
|
1477
|
+
version
|
|
1478
|
+
} : {},
|
|
1479
|
+
config,
|
|
1480
|
+
context,
|
|
1481
|
+
services: plugins.services,
|
|
1482
|
+
hooks: plugins.hooks,
|
|
1483
|
+
plugins,
|
|
1484
|
+
deployment,
|
|
1485
|
+
get state() {
|
|
1486
|
+
return state;
|
|
1487
|
+
},
|
|
1488
|
+
use(extension) {
|
|
1489
|
+
assertMutable();
|
|
1490
|
+
plugins.use(extension);
|
|
1491
|
+
return app;
|
|
1492
|
+
},
|
|
1493
|
+
provide(key, value, options = {}) {
|
|
1494
|
+
assertMutable();
|
|
1495
|
+
plugins.services.provide(
|
|
1496
|
+
key,
|
|
1497
|
+
value,
|
|
1498
|
+
options
|
|
1499
|
+
);
|
|
1500
|
+
return app;
|
|
1501
|
+
},
|
|
1502
|
+
addResource(resource) {
|
|
1503
|
+
assertMutable();
|
|
1504
|
+
deployment.addResource(
|
|
1505
|
+
resource
|
|
1506
|
+
);
|
|
1507
|
+
return app;
|
|
1508
|
+
},
|
|
1509
|
+
async start() {
|
|
1510
|
+
if (state === "ready") {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (state === "starting") {
|
|
1514
|
+
if (!startPromise) {
|
|
1515
|
+
throw new Error(
|
|
1516
|
+
"BCP Application: startup promise is unavailable while starting."
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
return startPromise;
|
|
1520
|
+
}
|
|
1521
|
+
if (state === "stopping") {
|
|
1522
|
+
throw new Error(
|
|
1523
|
+
"BCP Application: application cannot start while stopping."
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
if (state === "stopped") {
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
"BCP Application: a stopped application cannot be started again."
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
if (state === "failed") {
|
|
1532
|
+
throw failure ?? new Error(
|
|
1533
|
+
"BCP Application: failed application cannot be started again."
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
state = "starting";
|
|
1537
|
+
startPromise = startApplication();
|
|
1538
|
+
return startPromise;
|
|
1539
|
+
},
|
|
1540
|
+
stop(options = {}) {
|
|
1541
|
+
return shutdownApplication(
|
|
1542
|
+
options
|
|
1543
|
+
);
|
|
1544
|
+
},
|
|
1545
|
+
shutdown(options = {}) {
|
|
1546
|
+
return shutdownApplication(
|
|
1547
|
+
options
|
|
1548
|
+
);
|
|
1549
|
+
},
|
|
1550
|
+
close(options = {}) {
|
|
1551
|
+
return shutdownApplication(
|
|
1552
|
+
options
|
|
1553
|
+
);
|
|
1554
|
+
},
|
|
1555
|
+
readiness() {
|
|
1556
|
+
return deployment.readiness();
|
|
1557
|
+
},
|
|
1558
|
+
diagnostics() {
|
|
1559
|
+
return deployment.diagnostics();
|
|
1560
|
+
},
|
|
1561
|
+
installSignalHandlers(signalOptions = {}) {
|
|
1562
|
+
const signals = Array.from(
|
|
1563
|
+
new Set(
|
|
1564
|
+
signalOptions.signals ?? DEFAULT_APPLICATION_SIGNALS
|
|
1565
|
+
)
|
|
1566
|
+
);
|
|
1567
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1568
|
+
for (const signal of signals) {
|
|
1569
|
+
const handler = () => {
|
|
1570
|
+
void app.shutdown({
|
|
1571
|
+
reason: signal
|
|
1572
|
+
}).then(
|
|
1573
|
+
() => {
|
|
1574
|
+
if (signalOptions.setExitCode !== false) {
|
|
1575
|
+
process.exitCode = 0;
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
() => {
|
|
1579
|
+
process.exitCode = 1;
|
|
1580
|
+
}
|
|
1581
|
+
);
|
|
1582
|
+
};
|
|
1583
|
+
handlers.set(
|
|
1584
|
+
signal,
|
|
1585
|
+
handler
|
|
1586
|
+
);
|
|
1587
|
+
process.on(
|
|
1588
|
+
signal,
|
|
1589
|
+
handler
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
return () => {
|
|
1593
|
+
for (const [
|
|
1594
|
+
signal,
|
|
1595
|
+
handler
|
|
1596
|
+
] of handlers) {
|
|
1597
|
+
process.off(
|
|
1598
|
+
signal,
|
|
1599
|
+
handler
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
handlers.clear();
|
|
1603
|
+
};
|
|
1604
|
+
},
|
|
1605
|
+
registerShutdownHook(hookName) {
|
|
1606
|
+
return registerShutdownHook(
|
|
1607
|
+
() => app.shutdown({
|
|
1608
|
+
reason: "framework-shutdown"
|
|
1609
|
+
}),
|
|
1610
|
+
{
|
|
1611
|
+
name: hookName ?? `application:${name}`
|
|
1612
|
+
}
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
return app;
|
|
1617
|
+
async function startApplication() {
|
|
1618
|
+
try {
|
|
1619
|
+
if (!setupCompleted) {
|
|
1620
|
+
await runHook(
|
|
1621
|
+
"setup",
|
|
1622
|
+
definition.setup
|
|
1623
|
+
);
|
|
1624
|
+
setupCompleted = true;
|
|
1625
|
+
}
|
|
1626
|
+
registerApplicationResource();
|
|
1627
|
+
await deployment.start();
|
|
1628
|
+
state = "ready";
|
|
1629
|
+
failure = null;
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
const lifecycleError = error instanceof ApplicationLifecycleError ? error : new ApplicationLifecycleError(
|
|
1632
|
+
"start",
|
|
1633
|
+
error
|
|
1634
|
+
);
|
|
1635
|
+
failure = lifecycleError;
|
|
1636
|
+
state = "failed";
|
|
1637
|
+
await cleanupAfterFailure();
|
|
1638
|
+
throw lifecycleError;
|
|
1639
|
+
} finally {
|
|
1640
|
+
startPromise = null;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
async function shutdownApplication(options) {
|
|
1644
|
+
if (state === "stopped") {
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
if (state === "stopping") {
|
|
1648
|
+
return stopPromise ?? Promise.resolve();
|
|
1649
|
+
}
|
|
1650
|
+
if (state === "starting") {
|
|
1651
|
+
try {
|
|
1652
|
+
await startPromise;
|
|
1653
|
+
} catch {
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
state = "stopping";
|
|
1657
|
+
stopPromise = stopApplication(options);
|
|
1658
|
+
return stopPromise;
|
|
1659
|
+
}
|
|
1660
|
+
async function stopApplication(options) {
|
|
1661
|
+
const errors = [];
|
|
1662
|
+
try {
|
|
1663
|
+
if (deployment.state !== "idle" && deployment.state !== "stopped") {
|
|
1664
|
+
try {
|
|
1665
|
+
await deployment.shutdown(
|
|
1666
|
+
options
|
|
1667
|
+
);
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
errors.push(error);
|
|
1670
|
+
}
|
|
1671
|
+
} else {
|
|
1672
|
+
try {
|
|
1673
|
+
await plugins.close();
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
errors.push(error);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
try {
|
|
1679
|
+
await disposeApplication();
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
errors.push(error);
|
|
1682
|
+
}
|
|
1683
|
+
state = "stopped";
|
|
1684
|
+
} finally {
|
|
1685
|
+
stopPromise = null;
|
|
1686
|
+
}
|
|
1687
|
+
if (errors.length > 0) {
|
|
1688
|
+
throw new AggregateError(
|
|
1689
|
+
errors,
|
|
1690
|
+
"BCP Application: one or more shutdown operations failed."
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
function registerApplicationResource() {
|
|
1695
|
+
if (applicationResourceRegistered) {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
deployment.addResource({
|
|
1699
|
+
name: "bcp:application",
|
|
1700
|
+
async start() {
|
|
1701
|
+
await runHook(
|
|
1702
|
+
"start",
|
|
1703
|
+
definition.start
|
|
1704
|
+
);
|
|
1705
|
+
},
|
|
1706
|
+
ready() {
|
|
1707
|
+
return true;
|
|
1708
|
+
},
|
|
1709
|
+
async stop() {
|
|
1710
|
+
await runHook(
|
|
1711
|
+
"stop",
|
|
1712
|
+
definition.stop
|
|
1713
|
+
);
|
|
1714
|
+
},
|
|
1715
|
+
diagnostics() {
|
|
1716
|
+
return {
|
|
1717
|
+
name,
|
|
1718
|
+
...version ? {
|
|
1719
|
+
version
|
|
1720
|
+
} : {},
|
|
1721
|
+
state,
|
|
1722
|
+
services: plugins.services.keys().map(
|
|
1723
|
+
formatServiceKey2
|
|
1724
|
+
),
|
|
1725
|
+
pluginCount: plugins.plugins().length
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
applicationResourceRegistered = true;
|
|
1730
|
+
}
|
|
1731
|
+
async function cleanupAfterFailure() {
|
|
1732
|
+
if (deployment.state !== "idle" && deployment.state !== "stopped") {
|
|
1733
|
+
try {
|
|
1734
|
+
await deployment.shutdown({
|
|
1735
|
+
reason: "application-start-failed"
|
|
1736
|
+
});
|
|
1737
|
+
} catch {
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
try {
|
|
1741
|
+
await plugins.close();
|
|
1742
|
+
} catch {
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
await disposeApplication();
|
|
1746
|
+
} catch {
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async function disposeApplication() {
|
|
1750
|
+
if (disposed) {
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
disposed = true;
|
|
1754
|
+
await runHook(
|
|
1755
|
+
"dispose",
|
|
1756
|
+
definition.dispose
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
async function runHook(phase, hook) {
|
|
1760
|
+
if (!hook) {
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
try {
|
|
1764
|
+
await hook(context);
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
throw new ApplicationLifecycleError(
|
|
1767
|
+
phase,
|
|
1768
|
+
error
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function assertMutable() {
|
|
1773
|
+
if (state !== "created") {
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
"BCP Application: plugins, services and resources must be registered before start()."
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function parseConfig2(parser, value) {
|
|
1781
|
+
if (!parser) {
|
|
1782
|
+
return value;
|
|
1783
|
+
}
|
|
1784
|
+
if (typeof parser === "function") {
|
|
1785
|
+
return parser(value);
|
|
1786
|
+
}
|
|
1787
|
+
if (parser && typeof parser === "object" && typeof parser.parse === "function") {
|
|
1788
|
+
return parser.parse(value);
|
|
1789
|
+
}
|
|
1790
|
+
throw new TypeError(
|
|
1791
|
+
"BCP Application: config schema must be a function or an object with parse()."
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
function validateDefinition(definition) {
|
|
1795
|
+
if (!definition || typeof definition !== "object") {
|
|
1796
|
+
throw new TypeError(
|
|
1797
|
+
"BCP Application: definition must be an object."
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
normalizeName3(
|
|
1801
|
+
definition.name,
|
|
1802
|
+
"application name"
|
|
1803
|
+
);
|
|
1804
|
+
normalizeOptionalText2(
|
|
1805
|
+
definition.version
|
|
1806
|
+
);
|
|
1807
|
+
if (definition.plugins !== void 0 && !Array.isArray(definition.plugins)) {
|
|
1808
|
+
throw new TypeError(
|
|
1809
|
+
"BCP Application: plugins must be an array."
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
if (definition.modules !== void 0 && !Array.isArray(definition.modules)) {
|
|
1813
|
+
throw new TypeError(
|
|
1814
|
+
"BCP Application: modules must be an array."
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
if (definition.resources !== void 0 && !Array.isArray(definition.resources)) {
|
|
1818
|
+
throw new TypeError(
|
|
1819
|
+
"BCP Application: resources must be an array."
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
for (const [phase, hook] of [
|
|
1823
|
+
["setup", definition.setup],
|
|
1824
|
+
["start", definition.start],
|
|
1825
|
+
["stop", definition.stop],
|
|
1826
|
+
["dispose", definition.dispose]
|
|
1827
|
+
]) {
|
|
1828
|
+
if (hook !== void 0 && typeof hook !== "function") {
|
|
1829
|
+
throw new TypeError(
|
|
1830
|
+
`BCP Application: ${phase} must be a function.`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
function normalizeName3(value, label) {
|
|
1836
|
+
if (typeof value !== "string") {
|
|
1837
|
+
throw new TypeError(
|
|
1838
|
+
`BCP Application: ${label} must be a string.`
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
const normalized = value.trim();
|
|
1842
|
+
if (!normalized) {
|
|
1843
|
+
throw new TypeError(
|
|
1844
|
+
`BCP Application: ${label} cannot be empty.`
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
return normalized;
|
|
1848
|
+
}
|
|
1849
|
+
function normalizeOptionalText2(value) {
|
|
1850
|
+
if (value === void 0) {
|
|
1851
|
+
return void 0;
|
|
1852
|
+
}
|
|
1853
|
+
if (typeof value !== "string") {
|
|
1854
|
+
throw new TypeError(
|
|
1855
|
+
"BCP Application: optional text values must be strings."
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
return value.trim() || void 0;
|
|
1859
|
+
}
|
|
1860
|
+
function formatServiceKey2(key) {
|
|
1861
|
+
return typeof key === "symbol" ? key.description ? `Symbol(${key.description})` : key.toString() : key;
|
|
1862
|
+
}
|
|
1863
|
+
function formatError2(value) {
|
|
1864
|
+
if (value instanceof Error) {
|
|
1865
|
+
return value.message;
|
|
1866
|
+
}
|
|
1867
|
+
return String(value);
|
|
1868
|
+
}
|
|
1869
|
+
export {
|
|
1870
|
+
ApplicationLifecycleError,
|
|
1871
|
+
createApp,
|
|
1872
|
+
defineApp
|
|
1873
|
+
};
|