@chidchanun/bcp 0.2.19 → 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.
@@ -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
+ };