@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,845 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDeploymentRuntime,
|
|
3
|
+
|
|
4
|
+
type DeploymentDiagnosticsReport,
|
|
5
|
+
type DeploymentMetadata,
|
|
6
|
+
type DeploymentReadinessReport,
|
|
7
|
+
type DeploymentResource,
|
|
8
|
+
type DeploymentRuntime,
|
|
9
|
+
type DeploymentRuntimeOptions,
|
|
10
|
+
type DeploymentShutdownOptions,
|
|
11
|
+
type DeploymentSignalOptions,
|
|
12
|
+
} from "./deployment.js";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
createPluginHost,
|
|
16
|
+
|
|
17
|
+
type PluginDefinition,
|
|
18
|
+
type PluginHookBus,
|
|
19
|
+
type PluginHost,
|
|
20
|
+
type PluginModule,
|
|
21
|
+
type PluginServiceKey,
|
|
22
|
+
type PluginServiceRegistry,
|
|
23
|
+
} from "./plugins.js";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
registerShutdownHook as registerProductionShutdownHook,
|
|
27
|
+
} from "./production-hardening.js";
|
|
28
|
+
|
|
29
|
+
const DEFAULT_APPLICATION_SIGNALS:
|
|
30
|
+
readonly NodeJS.Signals[] = [
|
|
31
|
+
"SIGTERM",
|
|
32
|
+
"SIGINT",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export type ApplicationState =
|
|
36
|
+
| "created"
|
|
37
|
+
| "starting"
|
|
38
|
+
| "ready"
|
|
39
|
+
| "stopping"
|
|
40
|
+
| "stopped"
|
|
41
|
+
| "failed";
|
|
42
|
+
|
|
43
|
+
export interface ApplicationConfigSchema<TConfig> {
|
|
44
|
+
parse(value: unknown): TConfig;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type ApplicationConfigParser<TConfig> =
|
|
48
|
+
| ApplicationConfigSchema<TConfig>
|
|
49
|
+
| ((value: unknown) => TConfig);
|
|
50
|
+
|
|
51
|
+
export interface ApplicationContext<TConfig = unknown> {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly version?: string;
|
|
54
|
+
readonly config: TConfig;
|
|
55
|
+
readonly services: PluginServiceRegistry;
|
|
56
|
+
readonly hooks: PluginHookBus;
|
|
57
|
+
readonly plugins: PluginHost;
|
|
58
|
+
readonly deployment: DeploymentRuntime;
|
|
59
|
+
readonly metadata: DeploymentMetadata;
|
|
60
|
+
readonly state: ApplicationState;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ApplicationDefinition<TConfig = unknown> {
|
|
64
|
+
name: string;
|
|
65
|
+
version?: string;
|
|
66
|
+
config?: unknown;
|
|
67
|
+
schema?: ApplicationConfigParser<TConfig>;
|
|
68
|
+
plugins?: readonly PluginDefinition<any>[];
|
|
69
|
+
modules?: readonly PluginModule[];
|
|
70
|
+
pluginConfigs?: Record<string, unknown>;
|
|
71
|
+
services?: Iterable<
|
|
72
|
+
readonly [PluginServiceKey, unknown]
|
|
73
|
+
>;
|
|
74
|
+
resources?: readonly DeploymentResource[];
|
|
75
|
+
deployment?: Omit<
|
|
76
|
+
DeploymentRuntimeOptions,
|
|
77
|
+
"serviceName" | "version" | "resources"
|
|
78
|
+
>;
|
|
79
|
+
setup?(
|
|
80
|
+
context: ApplicationContext<TConfig>
|
|
81
|
+
): void | Promise<void>;
|
|
82
|
+
start?(
|
|
83
|
+
context: ApplicationContext<TConfig>
|
|
84
|
+
): void | Promise<void>;
|
|
85
|
+
stop?(
|
|
86
|
+
context: ApplicationContext<TConfig>
|
|
87
|
+
): void | Promise<void>;
|
|
88
|
+
dispose?(
|
|
89
|
+
context: ApplicationContext<TConfig>
|
|
90
|
+
): void | Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface Application<TConfig = unknown> {
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly version?: string;
|
|
96
|
+
readonly state: ApplicationState;
|
|
97
|
+
readonly config: TConfig;
|
|
98
|
+
readonly context: ApplicationContext<TConfig>;
|
|
99
|
+
readonly services: PluginServiceRegistry;
|
|
100
|
+
readonly hooks: PluginHookBus;
|
|
101
|
+
readonly plugins: PluginHost;
|
|
102
|
+
readonly deployment: DeploymentRuntime;
|
|
103
|
+
use(
|
|
104
|
+
extension:
|
|
105
|
+
| PluginDefinition<any>
|
|
106
|
+
| PluginModule
|
|
107
|
+
): Application<TConfig>;
|
|
108
|
+
provide<T>(
|
|
109
|
+
key: PluginServiceKey<T>,
|
|
110
|
+
value: T,
|
|
111
|
+
options?: {
|
|
112
|
+
replace?: boolean;
|
|
113
|
+
}
|
|
114
|
+
): Application<TConfig>;
|
|
115
|
+
addResource(
|
|
116
|
+
resource: DeploymentResource
|
|
117
|
+
): Application<TConfig>;
|
|
118
|
+
start(): Promise<void>;
|
|
119
|
+
stop(
|
|
120
|
+
options?: DeploymentShutdownOptions
|
|
121
|
+
): Promise<void>;
|
|
122
|
+
shutdown(
|
|
123
|
+
options?: DeploymentShutdownOptions
|
|
124
|
+
): Promise<void>;
|
|
125
|
+
close(
|
|
126
|
+
options?: DeploymentShutdownOptions
|
|
127
|
+
): Promise<void>;
|
|
128
|
+
readiness(): Promise<DeploymentReadinessReport>;
|
|
129
|
+
diagnostics(): Promise<DeploymentDiagnosticsReport>;
|
|
130
|
+
installSignalHandlers(
|
|
131
|
+
options?: DeploymentSignalOptions
|
|
132
|
+
): () => void;
|
|
133
|
+
registerShutdownHook(
|
|
134
|
+
name?: string
|
|
135
|
+
): () => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class ApplicationLifecycleError
|
|
139
|
+
extends Error {
|
|
140
|
+
readonly phase: string;
|
|
141
|
+
readonly cause: unknown;
|
|
142
|
+
|
|
143
|
+
constructor(
|
|
144
|
+
phase: string,
|
|
145
|
+
cause: unknown
|
|
146
|
+
) {
|
|
147
|
+
super(
|
|
148
|
+
`BCP Application: ${phase} failed: ${formatError(cause)}`
|
|
149
|
+
);
|
|
150
|
+
this.name =
|
|
151
|
+
"ApplicationLifecycleError";
|
|
152
|
+
this.phase = phase;
|
|
153
|
+
this.cause = cause;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function defineApp<TConfig = unknown>(
|
|
158
|
+
definition: ApplicationDefinition<TConfig>
|
|
159
|
+
): ApplicationDefinition<TConfig> {
|
|
160
|
+
validateDefinition(definition);
|
|
161
|
+
return definition;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function createApp<TConfig = unknown>(
|
|
165
|
+
definition: ApplicationDefinition<TConfig>
|
|
166
|
+
): Application<TConfig> {
|
|
167
|
+
defineApp(definition);
|
|
168
|
+
|
|
169
|
+
const name =
|
|
170
|
+
normalizeName(
|
|
171
|
+
definition.name,
|
|
172
|
+
"application name"
|
|
173
|
+
);
|
|
174
|
+
const version =
|
|
175
|
+
normalizeOptionalText(
|
|
176
|
+
definition.version
|
|
177
|
+
);
|
|
178
|
+
const config =
|
|
179
|
+
parseConfig(
|
|
180
|
+
definition.schema,
|
|
181
|
+
definition.config
|
|
182
|
+
);
|
|
183
|
+
const plugins =
|
|
184
|
+
createPluginHost({
|
|
185
|
+
plugins:
|
|
186
|
+
definition.plugins,
|
|
187
|
+
modules:
|
|
188
|
+
definition.modules,
|
|
189
|
+
configs:
|
|
190
|
+
definition.pluginConfigs,
|
|
191
|
+
services:
|
|
192
|
+
definition.services,
|
|
193
|
+
});
|
|
194
|
+
const deployment =
|
|
195
|
+
createDeploymentRuntime({
|
|
196
|
+
...(definition.deployment ?? {}),
|
|
197
|
+
serviceName:
|
|
198
|
+
name,
|
|
199
|
+
...(version
|
|
200
|
+
? {
|
|
201
|
+
version,
|
|
202
|
+
}
|
|
203
|
+
: {}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
let state:
|
|
207
|
+
ApplicationState =
|
|
208
|
+
"created";
|
|
209
|
+
let startPromise:
|
|
210
|
+
Promise<void> | null =
|
|
211
|
+
null;
|
|
212
|
+
let stopPromise:
|
|
213
|
+
Promise<void> | null =
|
|
214
|
+
null;
|
|
215
|
+
let setupCompleted = false;
|
|
216
|
+
let applicationResourceRegistered =
|
|
217
|
+
false;
|
|
218
|
+
let disposed = false;
|
|
219
|
+
let failure:
|
|
220
|
+
ApplicationLifecycleError | null =
|
|
221
|
+
null;
|
|
222
|
+
|
|
223
|
+
const context:
|
|
224
|
+
ApplicationContext<TConfig> = {
|
|
225
|
+
name,
|
|
226
|
+
...(version
|
|
227
|
+
? {
|
|
228
|
+
version,
|
|
229
|
+
}
|
|
230
|
+
: {}),
|
|
231
|
+
config,
|
|
232
|
+
services:
|
|
233
|
+
plugins.services,
|
|
234
|
+
hooks:
|
|
235
|
+
plugins.hooks,
|
|
236
|
+
plugins,
|
|
237
|
+
deployment,
|
|
238
|
+
get metadata() {
|
|
239
|
+
return deployment.metadata;
|
|
240
|
+
},
|
|
241
|
+
get state() {
|
|
242
|
+
return state;
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
deployment.addResource({
|
|
247
|
+
name:
|
|
248
|
+
"bcp:plugins",
|
|
249
|
+
async start() {
|
|
250
|
+
await plugins.start();
|
|
251
|
+
},
|
|
252
|
+
ready() {
|
|
253
|
+
return {
|
|
254
|
+
ok:
|
|
255
|
+
plugins.started,
|
|
256
|
+
detail:
|
|
257
|
+
plugins.started
|
|
258
|
+
? "Plugin host started."
|
|
259
|
+
: "Plugin host is not started.",
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
async stop() {
|
|
263
|
+
await plugins.close();
|
|
264
|
+
},
|
|
265
|
+
diagnostics() {
|
|
266
|
+
return {
|
|
267
|
+
started:
|
|
268
|
+
plugins.started,
|
|
269
|
+
plugins:
|
|
270
|
+
plugins.plugins(),
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
for (
|
|
276
|
+
const resource
|
|
277
|
+
of definition.resources ?? []
|
|
278
|
+
) {
|
|
279
|
+
deployment.addResource(resource);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const app:
|
|
283
|
+
Application<TConfig> = {
|
|
284
|
+
name,
|
|
285
|
+
...(version
|
|
286
|
+
? {
|
|
287
|
+
version,
|
|
288
|
+
}
|
|
289
|
+
: {}),
|
|
290
|
+
config,
|
|
291
|
+
context,
|
|
292
|
+
services:
|
|
293
|
+
plugins.services,
|
|
294
|
+
hooks:
|
|
295
|
+
plugins.hooks,
|
|
296
|
+
plugins,
|
|
297
|
+
deployment,
|
|
298
|
+
|
|
299
|
+
get state() {
|
|
300
|
+
return state;
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
use(extension) {
|
|
304
|
+
assertMutable();
|
|
305
|
+
plugins.use(extension);
|
|
306
|
+
return app;
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
provide<T>(
|
|
310
|
+
key: PluginServiceKey<T>,
|
|
311
|
+
value: T,
|
|
312
|
+
options: {
|
|
313
|
+
replace?: boolean;
|
|
314
|
+
} = {}
|
|
315
|
+
) {
|
|
316
|
+
assertMutable();
|
|
317
|
+
plugins.services.provide(
|
|
318
|
+
key,
|
|
319
|
+
value,
|
|
320
|
+
options
|
|
321
|
+
);
|
|
322
|
+
return app;
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
addResource(resource) {
|
|
326
|
+
assertMutable();
|
|
327
|
+
deployment.addResource(
|
|
328
|
+
resource
|
|
329
|
+
);
|
|
330
|
+
return app;
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
async start() {
|
|
334
|
+
if (state === "ready") {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (state === "starting") {
|
|
338
|
+
if (!startPromise) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
"BCP Application: startup promise is unavailable while starting."
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return startPromise;
|
|
344
|
+
}
|
|
345
|
+
if (state === "stopping") {
|
|
346
|
+
throw new Error(
|
|
347
|
+
"BCP Application: application cannot start while stopping."
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (state === "stopped") {
|
|
351
|
+
throw new Error(
|
|
352
|
+
"BCP Application: a stopped application cannot be started again."
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (state === "failed") {
|
|
356
|
+
throw failure ??
|
|
357
|
+
new Error(
|
|
358
|
+
"BCP Application: failed application cannot be started again."
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
state =
|
|
363
|
+
"starting";
|
|
364
|
+
startPromise =
|
|
365
|
+
startApplication();
|
|
366
|
+
return startPromise;
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
stop(options = {}) {
|
|
370
|
+
return shutdownApplication(
|
|
371
|
+
options
|
|
372
|
+
);
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
shutdown(options = {}) {
|
|
376
|
+
return shutdownApplication(
|
|
377
|
+
options
|
|
378
|
+
);
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
close(options = {}) {
|
|
382
|
+
return shutdownApplication(
|
|
383
|
+
options
|
|
384
|
+
);
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
readiness() {
|
|
388
|
+
return deployment.readiness();
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
diagnostics() {
|
|
392
|
+
return deployment.diagnostics();
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
installSignalHandlers(
|
|
396
|
+
signalOptions:
|
|
397
|
+
DeploymentSignalOptions = {}
|
|
398
|
+
) {
|
|
399
|
+
const signals =
|
|
400
|
+
Array.from(
|
|
401
|
+
new Set(
|
|
402
|
+
signalOptions.signals ??
|
|
403
|
+
DEFAULT_APPLICATION_SIGNALS
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
const handlers =
|
|
407
|
+
new Map<
|
|
408
|
+
NodeJS.Signals,
|
|
409
|
+
() => void
|
|
410
|
+
>();
|
|
411
|
+
|
|
412
|
+
for (const signal of signals) {
|
|
413
|
+
const handler = () => {
|
|
414
|
+
void app.shutdown({
|
|
415
|
+
reason:
|
|
416
|
+
signal,
|
|
417
|
+
}).then(
|
|
418
|
+
() => {
|
|
419
|
+
if (
|
|
420
|
+
signalOptions.setExitCode !==
|
|
421
|
+
false
|
|
422
|
+
) {
|
|
423
|
+
process.exitCode = 0;
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
() => {
|
|
427
|
+
process.exitCode = 1;
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
};
|
|
431
|
+
handlers.set(
|
|
432
|
+
signal,
|
|
433
|
+
handler
|
|
434
|
+
);
|
|
435
|
+
process.on(
|
|
436
|
+
signal,
|
|
437
|
+
handler
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return () => {
|
|
442
|
+
for (
|
|
443
|
+
const [
|
|
444
|
+
signal,
|
|
445
|
+
handler,
|
|
446
|
+
]
|
|
447
|
+
of handlers
|
|
448
|
+
) {
|
|
449
|
+
process.off(
|
|
450
|
+
signal,
|
|
451
|
+
handler
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
handlers.clear();
|
|
455
|
+
};
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
registerShutdownHook(hookName) {
|
|
459
|
+
return registerProductionShutdownHook(
|
|
460
|
+
() =>
|
|
461
|
+
app.shutdown({
|
|
462
|
+
reason:
|
|
463
|
+
"framework-shutdown",
|
|
464
|
+
}),
|
|
465
|
+
{
|
|
466
|
+
name:
|
|
467
|
+
hookName ??
|
|
468
|
+
`application:${name}`,
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return app;
|
|
475
|
+
|
|
476
|
+
async function startApplication():
|
|
477
|
+
Promise<void> {
|
|
478
|
+
try {
|
|
479
|
+
if (!setupCompleted) {
|
|
480
|
+
await runHook(
|
|
481
|
+
"setup",
|
|
482
|
+
definition.setup
|
|
483
|
+
);
|
|
484
|
+
setupCompleted = true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
registerApplicationResource();
|
|
488
|
+
await deployment.start();
|
|
489
|
+
state =
|
|
490
|
+
"ready";
|
|
491
|
+
failure =
|
|
492
|
+
null;
|
|
493
|
+
} catch (error) {
|
|
494
|
+
const lifecycleError =
|
|
495
|
+
error instanceof ApplicationLifecycleError
|
|
496
|
+
? error
|
|
497
|
+
: new ApplicationLifecycleError(
|
|
498
|
+
"start",
|
|
499
|
+
error
|
|
500
|
+
);
|
|
501
|
+
failure =
|
|
502
|
+
lifecycleError;
|
|
503
|
+
state =
|
|
504
|
+
"failed";
|
|
505
|
+
await cleanupAfterFailure();
|
|
506
|
+
throw lifecycleError;
|
|
507
|
+
} finally {
|
|
508
|
+
startPromise =
|
|
509
|
+
null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function shutdownApplication(
|
|
514
|
+
options: DeploymentShutdownOptions
|
|
515
|
+
): Promise<void> {
|
|
516
|
+
if (state === "stopped") {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (state === "stopping") {
|
|
520
|
+
return stopPromise ??
|
|
521
|
+
Promise.resolve();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (state === "starting") {
|
|
525
|
+
try {
|
|
526
|
+
await startPromise;
|
|
527
|
+
} catch {
|
|
528
|
+
// Startup cleanup already preserved the original failure.
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
state =
|
|
533
|
+
"stopping";
|
|
534
|
+
stopPromise =
|
|
535
|
+
stopApplication(options);
|
|
536
|
+
return stopPromise;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function stopApplication(
|
|
540
|
+
options: DeploymentShutdownOptions
|
|
541
|
+
): Promise<void> {
|
|
542
|
+
const errors:
|
|
543
|
+
unknown[] = [];
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
if (
|
|
547
|
+
deployment.state !== "idle" &&
|
|
548
|
+
deployment.state !== "stopped"
|
|
549
|
+
) {
|
|
550
|
+
try {
|
|
551
|
+
await deployment.shutdown(
|
|
552
|
+
options
|
|
553
|
+
);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
errors.push(error);
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
try {
|
|
559
|
+
await plugins.close();
|
|
560
|
+
} catch (error) {
|
|
561
|
+
errors.push(error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
await disposeApplication();
|
|
567
|
+
} catch (error) {
|
|
568
|
+
errors.push(error);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
state =
|
|
572
|
+
"stopped";
|
|
573
|
+
} finally {
|
|
574
|
+
stopPromise =
|
|
575
|
+
null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (errors.length > 0) {
|
|
579
|
+
throw new AggregateError(
|
|
580
|
+
errors,
|
|
581
|
+
"BCP Application: one or more shutdown operations failed."
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function registerApplicationResource():
|
|
587
|
+
void {
|
|
588
|
+
if (applicationResourceRegistered) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
deployment.addResource({
|
|
593
|
+
name:
|
|
594
|
+
"bcp:application",
|
|
595
|
+
async start() {
|
|
596
|
+
await runHook(
|
|
597
|
+
"start",
|
|
598
|
+
definition.start
|
|
599
|
+
);
|
|
600
|
+
},
|
|
601
|
+
ready() {
|
|
602
|
+
return true;
|
|
603
|
+
},
|
|
604
|
+
async stop() {
|
|
605
|
+
await runHook(
|
|
606
|
+
"stop",
|
|
607
|
+
definition.stop
|
|
608
|
+
);
|
|
609
|
+
},
|
|
610
|
+
diagnostics() {
|
|
611
|
+
return {
|
|
612
|
+
name,
|
|
613
|
+
...(version
|
|
614
|
+
? {
|
|
615
|
+
version,
|
|
616
|
+
}
|
|
617
|
+
: {}),
|
|
618
|
+
state,
|
|
619
|
+
services:
|
|
620
|
+
plugins.services
|
|
621
|
+
.keys()
|
|
622
|
+
.map(
|
|
623
|
+
formatServiceKey
|
|
624
|
+
),
|
|
625
|
+
pluginCount:
|
|
626
|
+
plugins.plugins().length,
|
|
627
|
+
};
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
applicationResourceRegistered =
|
|
632
|
+
true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function cleanupAfterFailure():
|
|
636
|
+
Promise<void> {
|
|
637
|
+
if (
|
|
638
|
+
deployment.state !== "idle" &&
|
|
639
|
+
deployment.state !== "stopped"
|
|
640
|
+
) {
|
|
641
|
+
try {
|
|
642
|
+
await deployment.shutdown({
|
|
643
|
+
reason:
|
|
644
|
+
"application-start-failed",
|
|
645
|
+
});
|
|
646
|
+
} catch {
|
|
647
|
+
// Preserve the original startup failure.
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
await plugins.close();
|
|
653
|
+
} catch {
|
|
654
|
+
// Preserve the original startup failure.
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
await disposeApplication();
|
|
659
|
+
} catch {
|
|
660
|
+
// Preserve the original startup failure.
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function disposeApplication():
|
|
665
|
+
Promise<void> {
|
|
666
|
+
if (disposed) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
disposed = true;
|
|
670
|
+
await runHook(
|
|
671
|
+
"dispose",
|
|
672
|
+
definition.dispose
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function runHook(
|
|
677
|
+
phase: string,
|
|
678
|
+
hook:
|
|
679
|
+
| ((
|
|
680
|
+
value: ApplicationContext<TConfig>
|
|
681
|
+
) => void | Promise<void>)
|
|
682
|
+
| undefined
|
|
683
|
+
): Promise<void> {
|
|
684
|
+
if (!hook) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
await hook(context);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
throw new ApplicationLifecycleError(
|
|
691
|
+
phase,
|
|
692
|
+
error
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function assertMutable(): void {
|
|
698
|
+
if (state !== "created") {
|
|
699
|
+
throw new Error(
|
|
700
|
+
"BCP Application: plugins, services and resources must be registered before start()."
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function parseConfig<TConfig>(
|
|
707
|
+
parser:
|
|
708
|
+
ApplicationConfigParser<TConfig> |
|
|
709
|
+
undefined,
|
|
710
|
+
value: unknown
|
|
711
|
+
): TConfig {
|
|
712
|
+
if (!parser) {
|
|
713
|
+
return value as TConfig;
|
|
714
|
+
}
|
|
715
|
+
if (typeof parser === "function") {
|
|
716
|
+
return parser(value);
|
|
717
|
+
}
|
|
718
|
+
if (
|
|
719
|
+
parser &&
|
|
720
|
+
typeof parser === "object" &&
|
|
721
|
+
typeof parser.parse === "function"
|
|
722
|
+
) {
|
|
723
|
+
return parser.parse(value);
|
|
724
|
+
}
|
|
725
|
+
throw new TypeError(
|
|
726
|
+
"BCP Application: config schema must be a function or an object with parse()."
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function validateDefinition<TConfig>(
|
|
731
|
+
definition: ApplicationDefinition<TConfig>
|
|
732
|
+
): void {
|
|
733
|
+
if (
|
|
734
|
+
!definition ||
|
|
735
|
+
typeof definition !== "object"
|
|
736
|
+
) {
|
|
737
|
+
throw new TypeError(
|
|
738
|
+
"BCP Application: definition must be an object."
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
normalizeName(
|
|
743
|
+
definition.name,
|
|
744
|
+
"application name"
|
|
745
|
+
);
|
|
746
|
+
normalizeOptionalText(
|
|
747
|
+
definition.version
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
if (
|
|
751
|
+
definition.plugins !== undefined &&
|
|
752
|
+
!Array.isArray(definition.plugins)
|
|
753
|
+
) {
|
|
754
|
+
throw new TypeError(
|
|
755
|
+
"BCP Application: plugins must be an array."
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (
|
|
759
|
+
definition.modules !== undefined &&
|
|
760
|
+
!Array.isArray(definition.modules)
|
|
761
|
+
) {
|
|
762
|
+
throw new TypeError(
|
|
763
|
+
"BCP Application: modules must be an array."
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (
|
|
767
|
+
definition.resources !== undefined &&
|
|
768
|
+
!Array.isArray(definition.resources)
|
|
769
|
+
) {
|
|
770
|
+
throw new TypeError(
|
|
771
|
+
"BCP Application: resources must be an array."
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
for (
|
|
776
|
+
const [phase, hook]
|
|
777
|
+
of [
|
|
778
|
+
["setup", definition.setup],
|
|
779
|
+
["start", definition.start],
|
|
780
|
+
["stop", definition.stop],
|
|
781
|
+
["dispose", definition.dispose],
|
|
782
|
+
] as const
|
|
783
|
+
) {
|
|
784
|
+
if (
|
|
785
|
+
hook !== undefined &&
|
|
786
|
+
typeof hook !== "function"
|
|
787
|
+
) {
|
|
788
|
+
throw new TypeError(
|
|
789
|
+
`BCP Application: ${phase} must be a function.`
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function normalizeName(
|
|
796
|
+
value: unknown,
|
|
797
|
+
label: string
|
|
798
|
+
): string {
|
|
799
|
+
if (typeof value !== "string") {
|
|
800
|
+
throw new TypeError(
|
|
801
|
+
`BCP Application: ${label} must be a string.`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
const normalized =
|
|
805
|
+
value.trim();
|
|
806
|
+
if (!normalized) {
|
|
807
|
+
throw new TypeError(
|
|
808
|
+
`BCP Application: ${label} cannot be empty.`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
return normalized;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function normalizeOptionalText(
|
|
815
|
+
value: unknown
|
|
816
|
+
): string | undefined {
|
|
817
|
+
if (value === undefined) {
|
|
818
|
+
return undefined;
|
|
819
|
+
}
|
|
820
|
+
if (typeof value !== "string") {
|
|
821
|
+
throw new TypeError(
|
|
822
|
+
"BCP Application: optional text values must be strings."
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
return value.trim() || undefined;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function formatServiceKey(
|
|
829
|
+
key: PluginServiceKey
|
|
830
|
+
): string {
|
|
831
|
+
return typeof key === "symbol"
|
|
832
|
+
? key.description
|
|
833
|
+
? `Symbol(${key.description})`
|
|
834
|
+
: key.toString()
|
|
835
|
+
: key;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function formatError(
|
|
839
|
+
value: unknown
|
|
840
|
+
): string {
|
|
841
|
+
if (value instanceof Error) {
|
|
842
|
+
return value.message;
|
|
843
|
+
}
|
|
844
|
+
return String(value);
|
|
845
|
+
}
|