@chidchanun/bcp 0.2.19 → 0.3.1

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,2439 @@
1
+ // packages/server/src/container.ts
2
+ var ServiceNotFoundError = class extends Error {
3
+ token;
4
+ constructor(token) {
5
+ super(
6
+ `BCP Container: service "${token.description}" is not registered.`
7
+ );
8
+ this.name = "ServiceNotFoundError";
9
+ this.token = token;
10
+ }
11
+ };
12
+ var ServiceResolutionError = class extends Error {
13
+ token;
14
+ cause;
15
+ constructor(token, message, cause) {
16
+ super(
17
+ `BCP Container: could not resolve "${token.description}". ${message}`
18
+ );
19
+ this.name = "ServiceResolutionError";
20
+ this.token = token;
21
+ this.cause = cause;
22
+ }
23
+ };
24
+ var ServiceDisposalError = class extends AggregateError {
25
+ constructor(errors) {
26
+ super(
27
+ errors,
28
+ "BCP Container: one or more services failed to dispose."
29
+ );
30
+ this.name = "ServiceDisposalError";
31
+ }
32
+ };
33
+ function createServiceContainer(options = {}) {
34
+ const root = new ScopeImpl(
35
+ normalizeOptionalName(
36
+ options.name
37
+ ) ?? "root",
38
+ void 0
39
+ );
40
+ root.root = root;
41
+ const container = {
42
+ get name() {
43
+ return root.name;
44
+ },
45
+ get state() {
46
+ return root.state;
47
+ },
48
+ get parent() {
49
+ return void 0;
50
+ },
51
+ has(token) {
52
+ return root.has(token);
53
+ },
54
+ resolve(token) {
55
+ return root.resolve(token);
56
+ },
57
+ optional(token) {
58
+ return root.optional(token);
59
+ },
60
+ createScope(scopeOptions = {}) {
61
+ return root.createScope(
62
+ scopeOptions
63
+ );
64
+ },
65
+ graph() {
66
+ return root.graph();
67
+ },
68
+ dispose() {
69
+ return root.dispose();
70
+ },
71
+ register(provider, registerOptions = {}) {
72
+ root.registerProvider(
73
+ provider,
74
+ registerOptions.replace === true
75
+ );
76
+ return container;
77
+ },
78
+ registerMany(providers) {
79
+ for (const provider of providers) {
80
+ root.registerProvider(
81
+ provider,
82
+ false
83
+ );
84
+ }
85
+ return container;
86
+ },
87
+ providers() {
88
+ return Array.from(
89
+ root.providersMap.values()
90
+ );
91
+ }
92
+ };
93
+ for (const provider of options.providers ?? []) {
94
+ root.registerProvider(
95
+ provider,
96
+ false
97
+ );
98
+ }
99
+ return container;
100
+ }
101
+ var ScopeImpl = class _ScopeImpl {
102
+ constructor(name, parent) {
103
+ this.name = name;
104
+ this.parent = parent;
105
+ if (parent) {
106
+ this.root = parent.root;
107
+ parent.children.add(this);
108
+ }
109
+ }
110
+ name;
111
+ parent;
112
+ root;
113
+ providersMap = /* @__PURE__ */ new Map();
114
+ overrideProviders = /* @__PURE__ */ new Map();
115
+ singletonCache = /* @__PURE__ */ new Map();
116
+ scopedCache = /* @__PURE__ */ new Map();
117
+ disposals = [];
118
+ children = /* @__PURE__ */ new Set();
119
+ state = "active";
120
+ disposePromise;
121
+ has(token) {
122
+ assertToken(token);
123
+ return this.findProvider(token) !== void 0;
124
+ }
125
+ resolve(token) {
126
+ return this.resolveWithPath(
127
+ token,
128
+ []
129
+ );
130
+ }
131
+ async optional(token) {
132
+ if (!this.has(token)) {
133
+ return void 0;
134
+ }
135
+ return this.resolve(token);
136
+ }
137
+ createScope(options = {}) {
138
+ this.assertActive();
139
+ const scope = new _ScopeImpl(
140
+ normalizeOptionalName(
141
+ options.name
142
+ ) ?? `${this.name}:scope-${this.children.size + 1}`,
143
+ this
144
+ );
145
+ for (const provider of options.overrides ?? []) {
146
+ scope.registerOverride(
147
+ provider
148
+ );
149
+ }
150
+ return scope;
151
+ }
152
+ graph() {
153
+ const effective = /* @__PURE__ */ new Map();
154
+ let current = this;
155
+ while (current) {
156
+ for (const [id, provider] of current.overrideProviders) {
157
+ if (!effective.has(id)) {
158
+ effective.set(
159
+ id,
160
+ {
161
+ provider,
162
+ owner: current,
163
+ overridden: true
164
+ }
165
+ );
166
+ }
167
+ }
168
+ current = current.parent;
169
+ }
170
+ for (const [id, provider] of this.root.providersMap) {
171
+ if (!effective.has(id)) {
172
+ effective.set(
173
+ id,
174
+ {
175
+ provider,
176
+ owner: this.root,
177
+ overridden: false
178
+ }
179
+ );
180
+ }
181
+ }
182
+ return Array.from(
183
+ effective.values(),
184
+ (record) => ({
185
+ token: record.provider.token,
186
+ description: record.provider.token.description,
187
+ lifetime: normalizeLifetime(
188
+ record.provider.lifetime
189
+ ),
190
+ dependencies: (record.provider.dependencies ?? []).map(
191
+ (dependency) => dependency.description
192
+ ),
193
+ overridden: record.overridden
194
+ })
195
+ ).sort(
196
+ (left, right) => left.description.localeCompare(
197
+ right.description
198
+ )
199
+ );
200
+ }
201
+ dispose() {
202
+ if (this.disposePromise) {
203
+ return this.disposePromise;
204
+ }
205
+ if (this.state === "disposed") {
206
+ return Promise.resolve();
207
+ }
208
+ this.disposePromise = this.disposeInternal();
209
+ return this.disposePromise;
210
+ }
211
+ registerProvider(provider, replace) {
212
+ this.assertActive();
213
+ if (this.parent) {
214
+ throw new Error(
215
+ "BCP Container: providers can only be registered on the root container. Use scope overrides for child scopes."
216
+ );
217
+ }
218
+ validateProvider(provider);
219
+ const id = provider.token.id;
220
+ if (this.providersMap.has(id) && !replace) {
221
+ throw new Error(
222
+ `BCP Container: service "${provider.token.description}" is already registered.`
223
+ );
224
+ }
225
+ if (replace && this.hasResolved(id)) {
226
+ throw new Error(
227
+ `BCP Container: service "${provider.token.description}" cannot be replaced after it has been resolved.`
228
+ );
229
+ }
230
+ this.providersMap.set(
231
+ id,
232
+ provider
233
+ );
234
+ }
235
+ registerOverride(provider) {
236
+ validateProvider(provider);
237
+ const id = provider.token.id;
238
+ if (this.overrideProviders.has(id)) {
239
+ throw new Error(
240
+ `BCP Container: scope override for "${provider.token.description}" is already registered.`
241
+ );
242
+ }
243
+ this.overrideProviders.set(
244
+ id,
245
+ provider
246
+ );
247
+ }
248
+ async resolveWithPath(token, path) {
249
+ this.assertActive();
250
+ assertToken(token);
251
+ if (path.some(
252
+ (item) => item.id === token.id
253
+ )) {
254
+ throw new ServiceResolutionError(
255
+ token,
256
+ `Circular dependency detected: ${[
257
+ ...path,
258
+ token
259
+ ].map(
260
+ (item) => item.description
261
+ ).join(" -> ")}.`
262
+ );
263
+ }
264
+ const record = this.findProvider(token);
265
+ if (!record) {
266
+ throw new ServiceNotFoundError(
267
+ token
268
+ );
269
+ }
270
+ const lifetime = normalizeLifetime(
271
+ record.provider.lifetime
272
+ );
273
+ const cache = lifetime === "singleton" ? record.owner.singletonCache : lifetime === "scoped" ? this.scopedCache : void 0;
274
+ const existing = cache?.get(token.id);
275
+ if (existing) {
276
+ return existing;
277
+ }
278
+ const resolution = this.instantiate(
279
+ record.provider,
280
+ [
281
+ ...path,
282
+ token
283
+ ],
284
+ lifetime,
285
+ record.owner
286
+ );
287
+ cache?.set(
288
+ token.id,
289
+ resolution
290
+ );
291
+ try {
292
+ return await resolution;
293
+ } catch (error) {
294
+ cache?.delete(token.id);
295
+ if (error instanceof ServiceNotFoundError || error instanceof ServiceResolutionError) {
296
+ throw error;
297
+ }
298
+ throw new ServiceResolutionError(
299
+ token,
300
+ error instanceof Error ? error.message : String(error),
301
+ error
302
+ );
303
+ }
304
+ }
305
+ async instantiate(provider, path, lifetime, providerOwner) {
306
+ const dependencies = await Promise.all(
307
+ (provider.dependencies ?? []).map(
308
+ (dependency) => this.resolveWithPath(
309
+ dependency,
310
+ path
311
+ )
312
+ )
313
+ );
314
+ const context = {
315
+ scope: this,
316
+ resolve: (dependency) => this.resolveWithPath(
317
+ dependency,
318
+ path
319
+ ),
320
+ optional: async (dependency) => {
321
+ if (!this.has(dependency)) {
322
+ return void 0;
323
+ }
324
+ return this.resolveWithPath(
325
+ dependency,
326
+ path
327
+ );
328
+ }
329
+ };
330
+ const value = await provider.factory(
331
+ context,
332
+ dependencies
333
+ );
334
+ if (provider.dispose) {
335
+ const disposalOwner = lifetime === "singleton" ? providerOwner : this;
336
+ disposalOwner.disposals.push({
337
+ provider,
338
+ value
339
+ });
340
+ }
341
+ return value;
342
+ }
343
+ findProvider(token) {
344
+ let current = this;
345
+ while (current) {
346
+ const override = current.overrideProviders.get(
347
+ token.id
348
+ );
349
+ if (override) {
350
+ return {
351
+ provider: override,
352
+ owner: current,
353
+ overridden: true
354
+ };
355
+ }
356
+ current = current.parent;
357
+ }
358
+ const provider = this.root.providersMap.get(
359
+ token.id
360
+ );
361
+ if (!provider) {
362
+ return void 0;
363
+ }
364
+ return {
365
+ provider,
366
+ owner: this.root,
367
+ overridden: false
368
+ };
369
+ }
370
+ async disposeInternal() {
371
+ this.state = "disposing";
372
+ const errors = [];
373
+ for (const child of Array.from(
374
+ this.children
375
+ ).reverse()) {
376
+ try {
377
+ await child.dispose();
378
+ } catch (error) {
379
+ errors.push(error);
380
+ }
381
+ }
382
+ for (const record of [...this.disposals].reverse()) {
383
+ if (!record.provider.dispose) {
384
+ continue;
385
+ }
386
+ try {
387
+ await record.provider.dispose(
388
+ record.value
389
+ );
390
+ } catch (error) {
391
+ errors.push(error);
392
+ }
393
+ }
394
+ this.disposals.length = 0;
395
+ this.singletonCache.clear();
396
+ this.scopedCache.clear();
397
+ this.children.clear();
398
+ this.parent?.children.delete(this);
399
+ this.state = "disposed";
400
+ if (errors.length > 0) {
401
+ throw new ServiceDisposalError(
402
+ errors
403
+ );
404
+ }
405
+ }
406
+ hasResolved(id) {
407
+ if (this.singletonCache.has(id) || this.scopedCache.has(id)) {
408
+ return true;
409
+ }
410
+ for (const child of this.children) {
411
+ if (child.hasResolved(id)) {
412
+ return true;
413
+ }
414
+ }
415
+ return false;
416
+ }
417
+ assertActive() {
418
+ if (this.state !== "active") {
419
+ throw new Error(
420
+ `BCP Container: scope "${this.name}" is ${this.state}.`
421
+ );
422
+ }
423
+ }
424
+ };
425
+ function validateProvider(provider) {
426
+ if (!provider || typeof provider !== "object") {
427
+ throw new TypeError(
428
+ "BCP Container: provider must be an object."
429
+ );
430
+ }
431
+ assertToken(provider.token);
432
+ if (typeof provider.factory !== "function") {
433
+ throw new TypeError(
434
+ `BCP Container: provider "${provider.token.description}" must define factory().`
435
+ );
436
+ }
437
+ normalizeLifetime(
438
+ provider.lifetime
439
+ );
440
+ assertDependencies(
441
+ provider.dependencies ?? []
442
+ );
443
+ if (provider.dispose !== void 0 && typeof provider.dispose !== "function") {
444
+ throw new TypeError(
445
+ `BCP Container: provider "${provider.token.description}" dispose must be a function.`
446
+ );
447
+ }
448
+ }
449
+ function assertToken(token) {
450
+ if (!token || typeof token !== "object" || typeof token.id !== "symbol" || typeof token.description !== "string" || token.description.trim() === "") {
451
+ throw new TypeError(
452
+ "BCP Container: invalid service token. Use createServiceToken()."
453
+ );
454
+ }
455
+ }
456
+ function assertDependencies(dependencies) {
457
+ if (!Array.isArray(dependencies)) {
458
+ throw new TypeError(
459
+ "BCP Container: provider dependencies must be an array."
460
+ );
461
+ }
462
+ for (const dependency of dependencies) {
463
+ assertToken(dependency);
464
+ }
465
+ }
466
+ function normalizeLifetime(lifetime) {
467
+ const normalized = lifetime ?? "singleton";
468
+ if (normalized !== "singleton" && normalized !== "scoped" && normalized !== "transient") {
469
+ throw new TypeError(
470
+ "BCP Container: service lifetime must be singleton, scoped or transient."
471
+ );
472
+ }
473
+ return normalized;
474
+ }
475
+ function normalizeName(value, label) {
476
+ if (typeof value !== "string") {
477
+ throw new TypeError(
478
+ `BCP Container: ${label} must be a string.`
479
+ );
480
+ }
481
+ const normalized = value.trim();
482
+ if (!normalized) {
483
+ throw new TypeError(
484
+ `BCP Container: ${label} cannot be empty.`
485
+ );
486
+ }
487
+ if (normalized.length > 256 || /[\r\n]/.test(normalized)) {
488
+ throw new TypeError(
489
+ `BCP Container: ${label} must be at most 256 characters without line breaks.`
490
+ );
491
+ }
492
+ return normalized;
493
+ }
494
+ function normalizeOptionalName(value) {
495
+ if (value === void 0) {
496
+ return void 0;
497
+ }
498
+ return normalizeName(
499
+ value,
500
+ "scope name"
501
+ );
502
+ }
503
+
504
+ // packages/server/src/deployment.ts
505
+ import {
506
+ randomUUID
507
+ } from "node:crypto";
508
+
509
+ // packages/server/src/production-hardening.ts
510
+ var nextShutdownHookId = 1;
511
+ var shutdownHooks = /* @__PURE__ */ new Map();
512
+ function registerShutdownHook(hook, options = {}) {
513
+ if (typeof hook !== "function") {
514
+ throw new TypeError(
515
+ "BCP Framework: shutdown hook must be a function."
516
+ );
517
+ }
518
+ const id = nextShutdownHookId++;
519
+ const name = normalizeHookName(
520
+ options.name,
521
+ id
522
+ );
523
+ shutdownHooks.set(
524
+ id,
525
+ {
526
+ id,
527
+ name,
528
+ hook
529
+ }
530
+ );
531
+ return () => {
532
+ shutdownHooks.delete(
533
+ id
534
+ );
535
+ };
536
+ }
537
+ function normalizeHookName(value, id) {
538
+ const normalized = value?.trim();
539
+ if (!normalized) {
540
+ return `hook-${id}`;
541
+ }
542
+ if (normalized.length > 128 || /[\r\n]/.test(
543
+ normalized
544
+ )) {
545
+ throw new TypeError(
546
+ "BCP Framework: shutdown hook name must be at most 128 characters without line breaks."
547
+ );
548
+ }
549
+ return normalized;
550
+ }
551
+
552
+ // packages/server/src/deployment.ts
553
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 1e4;
554
+ var DEFAULT_READINESS_TIMEOUT_MS = 5e3;
555
+ var DEFAULT_SIGNALS = [
556
+ "SIGTERM",
557
+ "SIGINT"
558
+ ];
559
+ function createDeploymentRuntime(options) {
560
+ const environment = options.environment ?? process.env;
561
+ const now = options.now ?? Date.now;
562
+ const idFactory = options.idFactory ?? randomUUID;
563
+ const shutdownTimeoutMs = positiveInteger(
564
+ options.shutdownTimeoutMs ?? parseOptionalPositiveInteger(
565
+ environment.BCP_SHUTDOWN_TIMEOUT_MS
566
+ ) ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
567
+ "shutdownTimeoutMs"
568
+ );
569
+ const readinessTimeoutMs = positiveInteger(
570
+ options.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS,
571
+ "readinessTimeoutMs"
572
+ );
573
+ const startedAtMs = now();
574
+ assertTimestamp(
575
+ startedAtMs,
576
+ "runtime start timestamp"
577
+ );
578
+ const metadata = {
579
+ serviceName: normalizeName2(
580
+ options.serviceName,
581
+ "serviceName"
582
+ ),
583
+ ...normalizeOptionalText(
584
+ options.version
585
+ ) ? {
586
+ version: normalizeOptionalText(
587
+ options.version
588
+ )
589
+ } : {},
590
+ deploymentId: normalizeName2(
591
+ options.deploymentId ?? environment.BCP_DEPLOYMENT_ID ?? idFactory(),
592
+ "deploymentId"
593
+ ),
594
+ ...normalizeOptionalText(
595
+ options.instanceId ?? environment.BCP_INSTANCE_ID
596
+ ) ? {
597
+ instanceId: normalizeOptionalText(
598
+ options.instanceId ?? environment.BCP_INSTANCE_ID
599
+ )
600
+ } : {},
601
+ ...normalizeOptionalText(
602
+ options.release ?? environment.BCP_RELEASE
603
+ ) ? {
604
+ release: normalizeOptionalText(
605
+ options.release ?? environment.BCP_RELEASE
606
+ )
607
+ } : {},
608
+ ...normalizeOptionalText(
609
+ options.environmentName ?? environment.NODE_ENV
610
+ ) ? {
611
+ environment: normalizeOptionalText(
612
+ options.environmentName ?? environment.NODE_ENV
613
+ )
614
+ } : {},
615
+ startedAt: new Date(startedAtMs).toISOString(),
616
+ pid: process.pid,
617
+ nodeVersion: process.version,
618
+ platform: process.platform,
619
+ arch: process.arch
620
+ };
621
+ const abortController = new AbortController();
622
+ const resourceEntries = [];
623
+ const names = /* @__PURE__ */ new Set();
624
+ let state = "idle";
625
+ let startPromise;
626
+ let shutdownPromise;
627
+ const runtime = {
628
+ metadata,
629
+ get state() {
630
+ return state;
631
+ },
632
+ addResource(resource) {
633
+ if (state !== "idle") {
634
+ throw new Error(
635
+ "BCP Deployment: resources can only be registered before runtime start."
636
+ );
637
+ }
638
+ const name = normalizeName2(
639
+ resource.name,
640
+ "resource name"
641
+ );
642
+ if (names.has(name)) {
643
+ throw new Error(
644
+ `BCP Deployment: resource "${name}" is already registered.`
645
+ );
646
+ }
647
+ const normalized = {
648
+ ...resource,
649
+ name
650
+ };
651
+ const entry = {
652
+ resource: normalized,
653
+ status: {
654
+ name,
655
+ state: "registered"
656
+ }
657
+ };
658
+ names.add(name);
659
+ resourceEntries.push(entry);
660
+ return () => {
661
+ if (state !== "idle") {
662
+ return;
663
+ }
664
+ const index = resourceEntries.indexOf(
665
+ entry
666
+ );
667
+ if (index >= 0) {
668
+ resourceEntries.splice(
669
+ index,
670
+ 1
671
+ );
672
+ names.delete(name);
673
+ }
674
+ };
675
+ },
676
+ resources() {
677
+ return resourceEntries.map(
678
+ (entry) => ({
679
+ ...entry.status
680
+ })
681
+ );
682
+ },
683
+ async start() {
684
+ if (state === "ready") {
685
+ return;
686
+ }
687
+ if (startPromise) {
688
+ return startPromise;
689
+ }
690
+ if (state === "draining" || state === "stopped") {
691
+ throw new Error(
692
+ "BCP Deployment: stopped runtime cannot be started again."
693
+ );
694
+ }
695
+ if (state === "failed") {
696
+ throw new Error(
697
+ "BCP Deployment: failed runtime cannot be started again."
698
+ );
699
+ }
700
+ startPromise = startResources();
701
+ return startPromise;
702
+ },
703
+ async readiness() {
704
+ const checkedAtMs = now();
705
+ assertTimestamp(
706
+ checkedAtMs,
707
+ "readiness timestamp"
708
+ );
709
+ const results = [];
710
+ for (const entry of resourceEntries) {
711
+ if (entry.status.state !== "started") {
712
+ results.push({
713
+ name: entry.resource.name,
714
+ ok: false,
715
+ durationMs: 0,
716
+ detail: `Resource state is ${entry.status.state}.`
717
+ });
718
+ continue;
719
+ }
720
+ if (!entry.resource.ready) {
721
+ results.push({
722
+ name: entry.resource.name,
723
+ ok: true,
724
+ durationMs: 0
725
+ });
726
+ continue;
727
+ }
728
+ const checkStarted = performance.now();
729
+ try {
730
+ const value = await withTimeout(
731
+ Promise.resolve(
732
+ entry.resource.ready(
733
+ createContext()
734
+ )
735
+ ),
736
+ readinessTimeoutMs,
737
+ `readiness check for ${entry.resource.name}`
738
+ );
739
+ const normalized = typeof value === "boolean" ? {
740
+ ok: value
741
+ } : value;
742
+ results.push({
743
+ name: entry.resource.name,
744
+ ok: normalized.ok === true,
745
+ durationMs: elapsedMilliseconds(
746
+ checkStarted
747
+ ),
748
+ ...normalized.detail ? {
749
+ detail: normalized.detail
750
+ } : {}
751
+ });
752
+ } catch (error) {
753
+ results.push({
754
+ name: entry.resource.name,
755
+ ok: false,
756
+ durationMs: elapsedMilliseconds(
757
+ checkStarted
758
+ ),
759
+ detail: errorMessage(error)
760
+ });
761
+ }
762
+ }
763
+ return {
764
+ ok: state === "ready" && results.every(
765
+ (item) => item.ok
766
+ ),
767
+ state,
768
+ checkedAt: new Date(
769
+ checkedAtMs
770
+ ).toISOString(),
771
+ resources: results
772
+ };
773
+ },
774
+ async diagnostics() {
775
+ const resources = [];
776
+ for (const entry of resourceEntries) {
777
+ let details;
778
+ let error;
779
+ if (entry.resource.diagnostics) {
780
+ try {
781
+ details = await entry.resource.diagnostics(
782
+ createContext()
783
+ );
784
+ } catch (diagnosticError) {
785
+ error = errorMessage(
786
+ diagnosticError
787
+ );
788
+ }
789
+ }
790
+ resources.push({
791
+ name: entry.resource.name,
792
+ lifecycle: {
793
+ ...entry.status
794
+ },
795
+ ...details ? {
796
+ details
797
+ } : {},
798
+ ...error ? {
799
+ error
800
+ } : {}
801
+ });
802
+ }
803
+ return {
804
+ metadata: {
805
+ ...metadata
806
+ },
807
+ state,
808
+ uptimeSeconds: Math.max(
809
+ 0,
810
+ (now() - startedAtMs) / 1e3
811
+ ),
812
+ resources
813
+ };
814
+ },
815
+ async shutdown(_options = {}) {
816
+ if (shutdownPromise) {
817
+ return shutdownPromise;
818
+ }
819
+ if (state === "stopped") {
820
+ return;
821
+ }
822
+ shutdownPromise = stopResources();
823
+ return shutdownPromise;
824
+ },
825
+ installSignalHandlers(signalOptions = {}) {
826
+ const signals = normalizeSignals(
827
+ signalOptions.signals ?? DEFAULT_SIGNALS
828
+ );
829
+ const handlers = /* @__PURE__ */ new Map();
830
+ for (const signal of signals) {
831
+ const handler = () => {
832
+ void runtime.shutdown({
833
+ reason: signal
834
+ }).then(
835
+ () => {
836
+ if (signalOptions.setExitCode !== false) {
837
+ process.exitCode = 0;
838
+ }
839
+ },
840
+ () => {
841
+ process.exitCode = 1;
842
+ }
843
+ );
844
+ };
845
+ handlers.set(
846
+ signal,
847
+ handler
848
+ );
849
+ process.on(
850
+ signal,
851
+ handler
852
+ );
853
+ }
854
+ return () => {
855
+ for (const [
856
+ signal,
857
+ handler
858
+ ] of handlers) {
859
+ process.off(
860
+ signal,
861
+ handler
862
+ );
863
+ }
864
+ handlers.clear();
865
+ };
866
+ },
867
+ registerShutdownHook(name = `deployment:${metadata.serviceName}`) {
868
+ return registerShutdownHook(
869
+ () => runtime.shutdown({
870
+ reason: "framework-shutdown"
871
+ }),
872
+ {
873
+ name
874
+ }
875
+ );
876
+ }
877
+ };
878
+ for (const resource of options.resources ?? []) {
879
+ runtime.addResource(resource);
880
+ }
881
+ return runtime;
882
+ async function startResources() {
883
+ state = "starting";
884
+ try {
885
+ for (const entry of resourceEntries) {
886
+ entry.status = {
887
+ name: entry.resource.name,
888
+ state: "starting"
889
+ };
890
+ try {
891
+ await entry.resource.start?.(
892
+ createContext()
893
+ );
894
+ entry.status = {
895
+ name: entry.resource.name,
896
+ state: "started",
897
+ startedAt: new Date(
898
+ now()
899
+ ).toISOString()
900
+ };
901
+ } catch (error) {
902
+ entry.status = {
903
+ name: entry.resource.name,
904
+ state: "failed",
905
+ error: errorMessage(error)
906
+ };
907
+ throw new Error(
908
+ `BCP Deployment: resource "${entry.resource.name}" failed to start. ${errorMessage(error)}`
909
+ );
910
+ }
911
+ }
912
+ state = "ready";
913
+ } catch (error) {
914
+ state = "failed";
915
+ abortController.abort(error);
916
+ await stopStartedResources(
917
+ shutdownTimeoutMs
918
+ );
919
+ throw error;
920
+ }
921
+ }
922
+ async function stopResources() {
923
+ state = "draining";
924
+ if (!abortController.signal.aborted) {
925
+ abortController.abort(
926
+ new Error(
927
+ "BCP Deployment: runtime is shutting down."
928
+ )
929
+ );
930
+ }
931
+ const failures = await stopStartedResources(
932
+ shutdownTimeoutMs
933
+ );
934
+ state = failures.length > 0 ? "failed" : "stopped";
935
+ if (failures.length > 0) {
936
+ throw new AggregateError(
937
+ failures,
938
+ "BCP Deployment: one or more resources failed to stop."
939
+ );
940
+ }
941
+ }
942
+ async function stopStartedResources(timeoutMs) {
943
+ const failures = [];
944
+ const deadline = Date.now() + timeoutMs;
945
+ for (const entry of [...resourceEntries].reverse()) {
946
+ if (entry.status.state !== "started") {
947
+ continue;
948
+ }
949
+ entry.status = {
950
+ ...entry.status,
951
+ state: "stopping"
952
+ };
953
+ try {
954
+ const remaining = Math.max(
955
+ 1,
956
+ deadline - Date.now()
957
+ );
958
+ await withTimeout(
959
+ Promise.resolve(
960
+ entry.resource.stop?.(
961
+ createContext()
962
+ )
963
+ ),
964
+ remaining,
965
+ `shutdown of ${entry.resource.name}`
966
+ );
967
+ entry.status = {
968
+ ...entry.status,
969
+ state: "stopped",
970
+ stoppedAt: new Date(
971
+ now()
972
+ ).toISOString()
973
+ };
974
+ } catch (error) {
975
+ const message = errorMessage(error);
976
+ entry.status = {
977
+ ...entry.status,
978
+ state: "failed",
979
+ error: message
980
+ };
981
+ failures.push(
982
+ new Error(
983
+ `BCP Deployment: resource "${entry.resource.name}" failed to stop. ${message}`
984
+ )
985
+ );
986
+ }
987
+ }
988
+ return failures;
989
+ }
990
+ function createContext() {
991
+ return {
992
+ metadata,
993
+ signal: abortController.signal
994
+ };
995
+ }
996
+ }
997
+ function normalizeSignals(signals) {
998
+ return Array.from(
999
+ new Set(signals)
1000
+ );
1001
+ }
1002
+ function normalizeName2(value, field) {
1003
+ const normalized = String(value ?? "").trim();
1004
+ if (!normalized) {
1005
+ throw new TypeError(
1006
+ `BCP Deployment: ${field} must be a non-empty string.`
1007
+ );
1008
+ }
1009
+ if (normalized.length > 256 || /[\r\n]/.test(normalized)) {
1010
+ throw new TypeError(
1011
+ `BCP Deployment: ${field} must be at most 256 characters without line breaks.`
1012
+ );
1013
+ }
1014
+ return normalized;
1015
+ }
1016
+ function normalizeOptionalText(value) {
1017
+ if (value === void 0) {
1018
+ return void 0;
1019
+ }
1020
+ const normalized = value.trim();
1021
+ return normalized || void 0;
1022
+ }
1023
+ function positiveInteger(value, field) {
1024
+ if (!Number.isSafeInteger(value) || value <= 0) {
1025
+ throw new TypeError(
1026
+ `BCP Deployment: ${field} must be a positive safe integer.`
1027
+ );
1028
+ }
1029
+ return value;
1030
+ }
1031
+ function parseOptionalPositiveInteger(value) {
1032
+ if (value === void 0) {
1033
+ return void 0;
1034
+ }
1035
+ const parsed = Number(value.trim());
1036
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
1037
+ throw new TypeError(
1038
+ "BCP Deployment: BCP_SHUTDOWN_TIMEOUT_MS must be a positive safe integer."
1039
+ );
1040
+ }
1041
+ return parsed;
1042
+ }
1043
+ function assertTimestamp(value, field) {
1044
+ if (!Number.isFinite(value)) {
1045
+ throw new TypeError(
1046
+ `BCP Deployment: ${field} must be a finite number.`
1047
+ );
1048
+ }
1049
+ }
1050
+ function errorMessage(error) {
1051
+ return error instanceof Error ? error.message : String(error);
1052
+ }
1053
+ function elapsedMilliseconds(startedAt) {
1054
+ return Math.max(
1055
+ 0,
1056
+ performance.now() - startedAt
1057
+ );
1058
+ }
1059
+ function withTimeout(promise, timeoutMs, label) {
1060
+ return new Promise(
1061
+ (resolve, reject) => {
1062
+ const timer = setTimeout(
1063
+ () => reject(
1064
+ new Error(
1065
+ `BCP Deployment: ${label} timed out after ${timeoutMs}ms.`
1066
+ )
1067
+ ),
1068
+ timeoutMs
1069
+ );
1070
+ timer.unref?.();
1071
+ promise.then(
1072
+ (value) => {
1073
+ clearTimeout(timer);
1074
+ resolve(value);
1075
+ },
1076
+ (error) => {
1077
+ clearTimeout(timer);
1078
+ reject(error);
1079
+ }
1080
+ );
1081
+ }
1082
+ );
1083
+ }
1084
+
1085
+ // packages/server/src/plugins.ts
1086
+ var PluginDependencyError = class extends Error {
1087
+ constructor(message) {
1088
+ super(message);
1089
+ this.name = "PluginDependencyError";
1090
+ }
1091
+ };
1092
+ var PluginLifecycleError = class extends Error {
1093
+ plugin;
1094
+ phase;
1095
+ cause;
1096
+ constructor(plugin, phase, cause) {
1097
+ super(
1098
+ `BCP Plugins: ${phase} failed for plugin "${plugin}": ${formatError(cause)}`
1099
+ );
1100
+ this.name = "PluginLifecycleError";
1101
+ this.plugin = plugin;
1102
+ this.phase = phase;
1103
+ this.cause = cause;
1104
+ }
1105
+ };
1106
+ function defineModule(module) {
1107
+ if (!module || typeof module !== "object") {
1108
+ throw new TypeError(
1109
+ "BCP Plugins: module must be an object."
1110
+ );
1111
+ }
1112
+ normalizeName3(
1113
+ module.name,
1114
+ "module name"
1115
+ );
1116
+ if (!Array.isArray(module.plugins)) {
1117
+ throw new TypeError(
1118
+ "BCP Plugins: module plugins must be an array."
1119
+ );
1120
+ }
1121
+ for (const plugin of module.plugins) {
1122
+ validatePluginDefinition(plugin);
1123
+ }
1124
+ return module;
1125
+ }
1126
+ function createPluginServiceRegistry(initial) {
1127
+ const values = /* @__PURE__ */ new Map();
1128
+ if (initial) {
1129
+ for (const [key, value] of initial) {
1130
+ assertServiceKey(key);
1131
+ if (values.has(key)) {
1132
+ throw new Error(
1133
+ `BCP Plugins: duplicate initial service ${formatServiceKey(key)}.`
1134
+ );
1135
+ }
1136
+ values.set(key, value);
1137
+ }
1138
+ }
1139
+ const registry = {
1140
+ provide(key, value, options = {}) {
1141
+ assertServiceKey(key);
1142
+ if (values.has(key) && !options.replace) {
1143
+ throw new Error(
1144
+ `BCP Plugins: service ${formatServiceKey(key)} is already registered.`
1145
+ );
1146
+ }
1147
+ values.set(key, value);
1148
+ },
1149
+ get(key) {
1150
+ assertServiceKey(key);
1151
+ if (!values.has(key)) {
1152
+ throw new Error(
1153
+ `BCP Plugins: service ${formatServiceKey(key)} is not registered.`
1154
+ );
1155
+ }
1156
+ return values.get(key);
1157
+ },
1158
+ optional(key) {
1159
+ assertServiceKey(key);
1160
+ return values.get(key);
1161
+ },
1162
+ has(key) {
1163
+ assertServiceKey(key);
1164
+ return values.has(key);
1165
+ },
1166
+ delete(key) {
1167
+ assertServiceKey(key);
1168
+ return values.delete(key);
1169
+ },
1170
+ keys() {
1171
+ return [
1172
+ ...values.keys()
1173
+ ];
1174
+ }
1175
+ };
1176
+ return registry;
1177
+ }
1178
+ function createPluginHookBus() {
1179
+ const hooks = /* @__PURE__ */ new Map();
1180
+ const bus = {
1181
+ on(name, handler) {
1182
+ const normalized = normalizeName3(
1183
+ name,
1184
+ "hook name"
1185
+ );
1186
+ if (typeof handler !== "function") {
1187
+ throw new TypeError(
1188
+ "BCP Plugins: hook handler must be a function."
1189
+ );
1190
+ }
1191
+ let group = hooks.get(normalized);
1192
+ if (!group) {
1193
+ group = /* @__PURE__ */ new Set();
1194
+ hooks.set(
1195
+ normalized,
1196
+ group
1197
+ );
1198
+ }
1199
+ group.add(
1200
+ handler
1201
+ );
1202
+ return () => {
1203
+ group?.delete(
1204
+ handler
1205
+ );
1206
+ if (group?.size === 0) {
1207
+ hooks.delete(normalized);
1208
+ }
1209
+ };
1210
+ },
1211
+ async emit(name, payload) {
1212
+ const normalized = normalizeName3(
1213
+ name,
1214
+ "hook name"
1215
+ );
1216
+ const group = hooks.get(normalized);
1217
+ if (!group) {
1218
+ return;
1219
+ }
1220
+ for (const handler of [
1221
+ ...group
1222
+ ]) {
1223
+ await handler(payload);
1224
+ }
1225
+ },
1226
+ listenerCount(name) {
1227
+ return hooks.get(
1228
+ normalizeName3(
1229
+ name,
1230
+ "hook name"
1231
+ )
1232
+ )?.size ?? 0;
1233
+ },
1234
+ clear(name) {
1235
+ if (name === void 0) {
1236
+ hooks.clear();
1237
+ return;
1238
+ }
1239
+ hooks.delete(
1240
+ normalizeName3(
1241
+ name,
1242
+ "hook name"
1243
+ )
1244
+ );
1245
+ }
1246
+ };
1247
+ return bus;
1248
+ }
1249
+ function createPluginHost(options = {}) {
1250
+ const now = options.now ?? Date.now;
1251
+ const services = createPluginServiceRegistry(
1252
+ options.services
1253
+ );
1254
+ const hooks = createPluginHookBus();
1255
+ const entries = /* @__PURE__ */ new Map();
1256
+ const configs = {
1257
+ ...options.configs ?? {}
1258
+ };
1259
+ let setupCompleted = false;
1260
+ let started = false;
1261
+ let closed = false;
1262
+ let lifecycleActive = false;
1263
+ let fatalError = null;
1264
+ const host = {
1265
+ services,
1266
+ hooks,
1267
+ get started() {
1268
+ return started;
1269
+ },
1270
+ use(extension) {
1271
+ assertMutable();
1272
+ if (isPluginModule(extension)) {
1273
+ defineModule(extension);
1274
+ for (const plugin of extension.plugins) {
1275
+ register(plugin);
1276
+ }
1277
+ return host;
1278
+ }
1279
+ register(extension);
1280
+ return host;
1281
+ },
1282
+ resolveOrder() {
1283
+ return resolvePluginOrder(
1284
+ entries
1285
+ );
1286
+ },
1287
+ async setup() {
1288
+ assertOpen();
1289
+ assertHealthy();
1290
+ if (setupCompleted) {
1291
+ return;
1292
+ }
1293
+ assertNotActive();
1294
+ lifecycleActive = true;
1295
+ try {
1296
+ const order = resolvePluginOrder(
1297
+ entries
1298
+ );
1299
+ for (const name of order) {
1300
+ const entry = requireInternal(name);
1301
+ if (entry.record.state !== "registered") {
1302
+ continue;
1303
+ }
1304
+ entry.record.state = "setting-up";
1305
+ try {
1306
+ const context = createContext(entry);
1307
+ entry.context = context;
1308
+ await entry.definition.setup?.(
1309
+ context
1310
+ );
1311
+ entry.record.state = "ready";
1312
+ entry.record.setupAt = timestamp();
1313
+ } catch (error) {
1314
+ markFailed(
1315
+ entry,
1316
+ error
1317
+ );
1318
+ const lifecycleError = error instanceof PluginLifecycleError ? error : new PluginLifecycleError(
1319
+ name,
1320
+ "setup",
1321
+ error
1322
+ );
1323
+ fatalError = lifecycleError;
1324
+ await disposePrepared(
1325
+ order,
1326
+ name
1327
+ );
1328
+ throw lifecycleError;
1329
+ }
1330
+ }
1331
+ setupCompleted = true;
1332
+ } finally {
1333
+ lifecycleActive = false;
1334
+ }
1335
+ },
1336
+ async start() {
1337
+ assertOpen();
1338
+ assertHealthy();
1339
+ if (started) {
1340
+ return;
1341
+ }
1342
+ if (!setupCompleted) {
1343
+ await host.setup();
1344
+ }
1345
+ assertHealthy();
1346
+ assertNotActive();
1347
+ lifecycleActive = true;
1348
+ const order = resolvePluginOrder(
1349
+ entries
1350
+ );
1351
+ const startedNames = [];
1352
+ try {
1353
+ for (const name of order) {
1354
+ const entry = requireInternal(name);
1355
+ if (entry.record.state !== "ready" && entry.record.state !== "stopped") {
1356
+ continue;
1357
+ }
1358
+ entry.record.state = "starting";
1359
+ try {
1360
+ await entry.definition.start?.(
1361
+ requireContext(entry)
1362
+ );
1363
+ entry.record.state = "started";
1364
+ entry.record.startedAt = timestamp();
1365
+ entry.record.error = void 0;
1366
+ startedNames.push(name);
1367
+ } catch (error) {
1368
+ markFailed(
1369
+ entry,
1370
+ error
1371
+ );
1372
+ const lifecycleError = new PluginLifecycleError(
1373
+ name,
1374
+ "start",
1375
+ error
1376
+ );
1377
+ fatalError = lifecycleError;
1378
+ await stopNames(
1379
+ [
1380
+ ...startedNames
1381
+ ].reverse()
1382
+ );
1383
+ throw lifecycleError;
1384
+ }
1385
+ }
1386
+ started = true;
1387
+ } finally {
1388
+ lifecycleActive = false;
1389
+ }
1390
+ },
1391
+ async stop() {
1392
+ if (closed || !started) {
1393
+ return;
1394
+ }
1395
+ assertNotActive();
1396
+ lifecycleActive = true;
1397
+ try {
1398
+ const errors = await stopNames(
1399
+ resolvePluginOrder(
1400
+ entries
1401
+ ).reverse()
1402
+ );
1403
+ started = false;
1404
+ if (errors.length > 0) {
1405
+ throw new AggregateError(
1406
+ errors,
1407
+ "BCP Plugins: one or more plugin stop hooks failed."
1408
+ );
1409
+ }
1410
+ } finally {
1411
+ lifecycleActive = false;
1412
+ }
1413
+ },
1414
+ async close() {
1415
+ if (closed) {
1416
+ return;
1417
+ }
1418
+ const errors = [];
1419
+ if (started) {
1420
+ try {
1421
+ await host.stop();
1422
+ } catch (error) {
1423
+ if (error instanceof AggregateError) {
1424
+ errors.push(
1425
+ ...error.errors
1426
+ );
1427
+ } else {
1428
+ errors.push(error);
1429
+ }
1430
+ }
1431
+ }
1432
+ assertNotActive();
1433
+ lifecycleActive = true;
1434
+ try {
1435
+ const order = resolvePluginOrder(
1436
+ entries
1437
+ ).reverse();
1438
+ for (const name of order) {
1439
+ const entry = requireInternal(name);
1440
+ if (entry.disposed || !entry.context) {
1441
+ continue;
1442
+ }
1443
+ try {
1444
+ await entry.definition.dispose?.(
1445
+ entry.context
1446
+ );
1447
+ } catch (error) {
1448
+ errors.push(
1449
+ new PluginLifecycleError(
1450
+ name,
1451
+ "dispose",
1452
+ error
1453
+ )
1454
+ );
1455
+ } finally {
1456
+ entry.disposed = true;
1457
+ }
1458
+ }
1459
+ hooks.clear();
1460
+ closed = true;
1461
+ } finally {
1462
+ lifecycleActive = false;
1463
+ }
1464
+ if (errors.length > 0) {
1465
+ throw new AggregateError(
1466
+ errors,
1467
+ "BCP Plugins: one or more plugin shutdown hooks failed."
1468
+ );
1469
+ }
1470
+ },
1471
+ plugin(name) {
1472
+ const entry = entries.get(
1473
+ normalizeName3(
1474
+ name,
1475
+ "plugin name"
1476
+ )
1477
+ );
1478
+ return entry ? cloneRecord(
1479
+ entry.record
1480
+ ) : null;
1481
+ },
1482
+ plugins() {
1483
+ return [
1484
+ ...entries.values()
1485
+ ].map(
1486
+ (entry) => cloneRecord(
1487
+ entry.record
1488
+ )
1489
+ ).sort(
1490
+ (left, right) => left.registeredAt - right.registeredAt || left.name.localeCompare(
1491
+ right.name
1492
+ )
1493
+ );
1494
+ }
1495
+ };
1496
+ for (const module of options.modules ?? []) {
1497
+ host.use(module);
1498
+ }
1499
+ for (const plugin of options.plugins ?? []) {
1500
+ host.use(plugin);
1501
+ }
1502
+ return host;
1503
+ function register(definition) {
1504
+ validatePluginDefinition(
1505
+ definition
1506
+ );
1507
+ const name = normalizeName3(
1508
+ definition.name,
1509
+ "plugin name"
1510
+ );
1511
+ if (entries.has(name)) {
1512
+ throw new Error(
1513
+ `BCP Plugins: plugin "${name}" is already registered.`
1514
+ );
1515
+ }
1516
+ const requires = normalizeDependencyList(
1517
+ definition.requires,
1518
+ name,
1519
+ "requires"
1520
+ );
1521
+ const optional = normalizeDependencyList(
1522
+ definition.optional,
1523
+ name,
1524
+ "optional"
1525
+ );
1526
+ entries.set(
1527
+ name,
1528
+ {
1529
+ definition: {
1530
+ ...definition,
1531
+ name,
1532
+ requires,
1533
+ optional
1534
+ },
1535
+ record: {
1536
+ name,
1537
+ version: normalizeOptionalVersion(
1538
+ definition.version
1539
+ ),
1540
+ state: "registered",
1541
+ requires,
1542
+ optional,
1543
+ registeredAt: timestamp()
1544
+ },
1545
+ disposed: false
1546
+ }
1547
+ );
1548
+ }
1549
+ function createContext(entry) {
1550
+ const rawConfig = Object.prototype.hasOwnProperty.call(
1551
+ configs,
1552
+ entry.record.name
1553
+ ) ? configs[entry.record.name] : entry.definition.config;
1554
+ const config = parseConfig(
1555
+ entry.definition.schema,
1556
+ rawConfig,
1557
+ entry.record.name
1558
+ );
1559
+ return {
1560
+ name: entry.record.name,
1561
+ config,
1562
+ services,
1563
+ hooks,
1564
+ host
1565
+ };
1566
+ }
1567
+ async function stopNames(names) {
1568
+ const errors = [];
1569
+ for (const name of names) {
1570
+ const entry = requireInternal(name);
1571
+ if (entry.record.state !== "started") {
1572
+ continue;
1573
+ }
1574
+ entry.record.state = "stopping";
1575
+ try {
1576
+ await entry.definition.stop?.(
1577
+ requireContext(entry)
1578
+ );
1579
+ entry.record.state = "stopped";
1580
+ entry.record.stoppedAt = timestamp();
1581
+ } catch (error) {
1582
+ markFailed(
1583
+ entry,
1584
+ error
1585
+ );
1586
+ errors.push(
1587
+ new PluginLifecycleError(
1588
+ name,
1589
+ "stop",
1590
+ error
1591
+ )
1592
+ );
1593
+ }
1594
+ }
1595
+ return errors;
1596
+ }
1597
+ async function disposePrepared(order, failedName) {
1598
+ const index = order.indexOf(failedName);
1599
+ const names = order.slice(
1600
+ 0,
1601
+ Math.max(0, index) + 1
1602
+ ).reverse();
1603
+ for (const name of names) {
1604
+ const entry = requireInternal(name);
1605
+ if (entry.disposed || !entry.context) {
1606
+ continue;
1607
+ }
1608
+ try {
1609
+ await entry.definition.dispose?.(
1610
+ entry.context
1611
+ );
1612
+ } catch {
1613
+ } finally {
1614
+ entry.disposed = true;
1615
+ }
1616
+ }
1617
+ }
1618
+ function requireInternal(name) {
1619
+ const entry = entries.get(name);
1620
+ if (!entry) {
1621
+ throw new Error(
1622
+ `BCP Plugins: plugin "${name}" is not registered.`
1623
+ );
1624
+ }
1625
+ return entry;
1626
+ }
1627
+ function timestamp() {
1628
+ const value = now();
1629
+ if (!Number.isFinite(value)) {
1630
+ throw new TypeError(
1631
+ "BCP Plugins: now() must return a finite number."
1632
+ );
1633
+ }
1634
+ return value;
1635
+ }
1636
+ function assertOpen() {
1637
+ if (closed) {
1638
+ throw new Error(
1639
+ "BCP Plugins: plugin host is closed."
1640
+ );
1641
+ }
1642
+ }
1643
+ function assertHealthy() {
1644
+ if (fatalError) {
1645
+ throw fatalError;
1646
+ }
1647
+ }
1648
+ function assertMutable() {
1649
+ assertOpen();
1650
+ if (setupCompleted || lifecycleActive || fatalError) {
1651
+ throw new Error(
1652
+ "BCP Plugins: plugins cannot be registered after setup begins or after a lifecycle failure."
1653
+ );
1654
+ }
1655
+ }
1656
+ function assertNotActive() {
1657
+ if (lifecycleActive) {
1658
+ throw new Error(
1659
+ "BCP Plugins: another lifecycle transition is already running."
1660
+ );
1661
+ }
1662
+ }
1663
+ }
1664
+ function resolvePluginOrder(entries) {
1665
+ for (const entry of entries.values()) {
1666
+ for (const dependency of entry.record.requires) {
1667
+ if (!entries.has(dependency)) {
1668
+ throw new PluginDependencyError(
1669
+ `BCP Plugins: plugin "${entry.record.name}" requires missing plugin "${dependency}".`
1670
+ );
1671
+ }
1672
+ }
1673
+ }
1674
+ const visiting = /* @__PURE__ */ new Set();
1675
+ const visited = /* @__PURE__ */ new Set();
1676
+ const order = [];
1677
+ const stack = [];
1678
+ const visit = (name) => {
1679
+ if (visited.has(name)) {
1680
+ return;
1681
+ }
1682
+ if (visiting.has(name)) {
1683
+ const start = stack.indexOf(name);
1684
+ const cycle = [
1685
+ ...stack.slice(
1686
+ Math.max(0, start)
1687
+ ),
1688
+ name
1689
+ ];
1690
+ throw new PluginDependencyError(
1691
+ `BCP Plugins: dependency cycle detected: ${cycle.join(" -> ")}.`
1692
+ );
1693
+ }
1694
+ const entry = entries.get(name);
1695
+ if (!entry) {
1696
+ return;
1697
+ }
1698
+ visiting.add(name);
1699
+ stack.push(name);
1700
+ for (const dependency of [
1701
+ ...entry.record.requires,
1702
+ ...entry.record.optional.filter(
1703
+ (candidate) => entries.has(candidate)
1704
+ )
1705
+ ]) {
1706
+ visit(dependency);
1707
+ }
1708
+ stack.pop();
1709
+ visiting.delete(name);
1710
+ visited.add(name);
1711
+ order.push(name);
1712
+ };
1713
+ for (const name of entries.keys()) {
1714
+ visit(name);
1715
+ }
1716
+ return order;
1717
+ }
1718
+ function validatePluginDefinition(definition) {
1719
+ if (!definition || typeof definition !== "object") {
1720
+ throw new TypeError(
1721
+ "BCP Plugins: plugin definition must be an object."
1722
+ );
1723
+ }
1724
+ const name = normalizeName3(
1725
+ definition.name,
1726
+ "plugin name"
1727
+ );
1728
+ normalizeDependencyList(
1729
+ definition.requires,
1730
+ name,
1731
+ "requires"
1732
+ );
1733
+ normalizeDependencyList(
1734
+ definition.optional,
1735
+ name,
1736
+ "optional"
1737
+ );
1738
+ for (const hook of [
1739
+ "setup",
1740
+ "start",
1741
+ "stop",
1742
+ "dispose"
1743
+ ]) {
1744
+ const value = definition[hook];
1745
+ if (value !== void 0 && typeof value !== "function") {
1746
+ throw new TypeError(
1747
+ `BCP Plugins: plugin "${name}" ${hook} must be a function.`
1748
+ );
1749
+ }
1750
+ }
1751
+ if (definition.schema !== void 0 && typeof definition.schema !== "function" && (!definition.schema || typeof definition.schema.parse !== "function")) {
1752
+ throw new TypeError(
1753
+ `BCP Plugins: plugin "${name}" schema must be a parser function or object with parse().`
1754
+ );
1755
+ }
1756
+ }
1757
+ function normalizeDependencyList(value, plugin, field) {
1758
+ if (value === void 0) {
1759
+ return [];
1760
+ }
1761
+ if (!Array.isArray(value)) {
1762
+ throw new TypeError(
1763
+ `BCP Plugins: plugin "${plugin}" ${field} must be an array.`
1764
+ );
1765
+ }
1766
+ const normalized = value.map(
1767
+ (item) => normalizeName3(
1768
+ item,
1769
+ `${field} dependency`
1770
+ )
1771
+ );
1772
+ if (new Set(normalized).size !== normalized.length) {
1773
+ throw new Error(
1774
+ `BCP Plugins: plugin "${plugin}" ${field} contains duplicate dependencies.`
1775
+ );
1776
+ }
1777
+ if (normalized.includes(plugin)) {
1778
+ throw new PluginDependencyError(
1779
+ `BCP Plugins: plugin "${plugin}" cannot depend on itself.`
1780
+ );
1781
+ }
1782
+ return normalized;
1783
+ }
1784
+ function parseConfig(parser, raw, plugin) {
1785
+ if (!parser) {
1786
+ return raw;
1787
+ }
1788
+ try {
1789
+ return typeof parser === "function" ? parser(raw) : parser.parse(raw);
1790
+ } catch (error) {
1791
+ throw new PluginLifecycleError(
1792
+ plugin,
1793
+ "config",
1794
+ error
1795
+ );
1796
+ }
1797
+ }
1798
+ function requireContext(entry) {
1799
+ if (!entry.context) {
1800
+ throw new Error(
1801
+ `BCP Plugins: plugin "${entry.record.name}" has not been set up.`
1802
+ );
1803
+ }
1804
+ return entry.context;
1805
+ }
1806
+ function markFailed(entry, error) {
1807
+ entry.record.state = "failed";
1808
+ entry.record.error = formatError(error);
1809
+ }
1810
+ function cloneRecord(record) {
1811
+ return {
1812
+ ...record,
1813
+ requires: [
1814
+ ...record.requires
1815
+ ],
1816
+ optional: [
1817
+ ...record.optional
1818
+ ]
1819
+ };
1820
+ }
1821
+ function isPluginModule(value) {
1822
+ return Boolean(
1823
+ value && typeof value === "object" && Array.isArray(
1824
+ value.plugins
1825
+ )
1826
+ );
1827
+ }
1828
+ function normalizeName3(value, field) {
1829
+ const text = String(value ?? "").trim();
1830
+ if (!text) {
1831
+ throw new TypeError(
1832
+ `BCP Plugins: ${field} must be a non-empty string.`
1833
+ );
1834
+ }
1835
+ if (text.length > 200) {
1836
+ throw new TypeError(
1837
+ `BCP Plugins: ${field} must not exceed 200 characters.`
1838
+ );
1839
+ }
1840
+ return text;
1841
+ }
1842
+ function normalizeOptionalVersion(value) {
1843
+ if (value === void 0) {
1844
+ return void 0;
1845
+ }
1846
+ return normalizeName3(
1847
+ value,
1848
+ "plugin version"
1849
+ );
1850
+ }
1851
+ function assertServiceKey(key) {
1852
+ if (typeof key === "string") {
1853
+ normalizeName3(
1854
+ key,
1855
+ "service key"
1856
+ );
1857
+ return;
1858
+ }
1859
+ if (typeof key !== "symbol") {
1860
+ throw new TypeError(
1861
+ "BCP Plugins: service key must be a string or symbol."
1862
+ );
1863
+ }
1864
+ }
1865
+ function formatServiceKey(key) {
1866
+ return typeof key === "symbol" ? String(key) : `"${key}"`;
1867
+ }
1868
+ function formatError(error) {
1869
+ if (error instanceof Error) {
1870
+ return error.message || error.name;
1871
+ }
1872
+ if (typeof error === "string") {
1873
+ return error;
1874
+ }
1875
+ try {
1876
+ return JSON.stringify(error) ?? String(error);
1877
+ } catch {
1878
+ return String(error);
1879
+ }
1880
+ }
1881
+
1882
+ // packages/server/src/application.ts
1883
+ var DEFAULT_APPLICATION_SIGNALS = [
1884
+ "SIGTERM",
1885
+ "SIGINT"
1886
+ ];
1887
+ var ApplicationLifecycleError = class extends Error {
1888
+ phase;
1889
+ cause;
1890
+ constructor(phase, cause) {
1891
+ super(
1892
+ `BCP Application: ${phase} failed: ${formatError2(cause)}`
1893
+ );
1894
+ this.name = "ApplicationLifecycleError";
1895
+ this.phase = phase;
1896
+ this.cause = cause;
1897
+ }
1898
+ };
1899
+ function defineApp(definition) {
1900
+ validateDefinition(definition);
1901
+ return definition;
1902
+ }
1903
+ function createApp(definition) {
1904
+ defineApp(definition);
1905
+ const name = normalizeName4(
1906
+ definition.name,
1907
+ "application name"
1908
+ );
1909
+ const version = normalizeOptionalText2(
1910
+ definition.version
1911
+ );
1912
+ const config = parseConfig2(
1913
+ definition.schema,
1914
+ definition.config
1915
+ );
1916
+ const container = createServiceContainer({
1917
+ name: `${name}:container`,
1918
+ providers: definition.providers
1919
+ });
1920
+ const plugins = createPluginHost({
1921
+ plugins: definition.plugins,
1922
+ modules: definition.modules,
1923
+ configs: definition.pluginConfigs,
1924
+ services: definition.services
1925
+ });
1926
+ const deployment = createDeploymentRuntime({
1927
+ ...definition.deployment ?? {},
1928
+ serviceName: name,
1929
+ ...version ? {
1930
+ version
1931
+ } : {}
1932
+ });
1933
+ let state = "created";
1934
+ let startPromise = null;
1935
+ let stopPromise = null;
1936
+ let setupCompleted = false;
1937
+ let applicationResourceRegistered = false;
1938
+ let disposed = false;
1939
+ let failure = null;
1940
+ const context = {
1941
+ name,
1942
+ ...version ? {
1943
+ version
1944
+ } : {},
1945
+ config,
1946
+ container,
1947
+ services: plugins.services,
1948
+ hooks: plugins.hooks,
1949
+ plugins,
1950
+ deployment,
1951
+ get metadata() {
1952
+ return deployment.metadata;
1953
+ },
1954
+ get state() {
1955
+ return state;
1956
+ }
1957
+ };
1958
+ deployment.addResource({
1959
+ name: "bcp:container",
1960
+ ready() {
1961
+ return {
1962
+ ok: container.state === "active",
1963
+ detail: `Service container is ${container.state}.`
1964
+ };
1965
+ },
1966
+ async stop() {
1967
+ await container.dispose();
1968
+ },
1969
+ diagnostics() {
1970
+ return {
1971
+ state: container.state,
1972
+ providers: container.graph().map(
1973
+ (node) => ({
1974
+ token: node.description,
1975
+ lifetime: node.lifetime
1976
+ })
1977
+ )
1978
+ };
1979
+ }
1980
+ });
1981
+ deployment.addResource({
1982
+ name: "bcp:plugins",
1983
+ async start() {
1984
+ await plugins.start();
1985
+ },
1986
+ ready() {
1987
+ return {
1988
+ ok: plugins.started,
1989
+ detail: plugins.started ? "Plugin host started." : "Plugin host is not started."
1990
+ };
1991
+ },
1992
+ async stop() {
1993
+ await plugins.close();
1994
+ },
1995
+ diagnostics() {
1996
+ return {
1997
+ started: plugins.started,
1998
+ plugins: plugins.plugins()
1999
+ };
2000
+ }
2001
+ });
2002
+ for (const resource of definition.resources ?? []) {
2003
+ deployment.addResource(resource);
2004
+ }
2005
+ const app = {
2006
+ name,
2007
+ ...version ? {
2008
+ version
2009
+ } : {},
2010
+ config,
2011
+ context,
2012
+ container,
2013
+ services: plugins.services,
2014
+ hooks: plugins.hooks,
2015
+ plugins,
2016
+ deployment,
2017
+ get state() {
2018
+ return state;
2019
+ },
2020
+ use(extension) {
2021
+ assertMutable();
2022
+ plugins.use(extension);
2023
+ return app;
2024
+ },
2025
+ provide(key, value, options = {}) {
2026
+ assertMutable();
2027
+ plugins.services.provide(
2028
+ key,
2029
+ value,
2030
+ options
2031
+ );
2032
+ return app;
2033
+ },
2034
+ register(provider, options = {}) {
2035
+ assertMutable();
2036
+ container.register(
2037
+ provider,
2038
+ options
2039
+ );
2040
+ return app;
2041
+ },
2042
+ createScope(options = {}) {
2043
+ return container.createScope(
2044
+ options
2045
+ );
2046
+ },
2047
+ addResource(resource) {
2048
+ assertMutable();
2049
+ deployment.addResource(
2050
+ resource
2051
+ );
2052
+ return app;
2053
+ },
2054
+ async start() {
2055
+ if (state === "ready") {
2056
+ return;
2057
+ }
2058
+ if (state === "starting") {
2059
+ if (!startPromise) {
2060
+ throw new Error(
2061
+ "BCP Application: startup promise is unavailable while starting."
2062
+ );
2063
+ }
2064
+ return startPromise;
2065
+ }
2066
+ if (state === "stopping") {
2067
+ throw new Error(
2068
+ "BCP Application: application cannot start while stopping."
2069
+ );
2070
+ }
2071
+ if (state === "stopped") {
2072
+ throw new Error(
2073
+ "BCP Application: a stopped application cannot be started again."
2074
+ );
2075
+ }
2076
+ if (state === "failed") {
2077
+ throw failure ?? new Error(
2078
+ "BCP Application: failed application cannot be started again."
2079
+ );
2080
+ }
2081
+ state = "starting";
2082
+ startPromise = startApplication();
2083
+ return startPromise;
2084
+ },
2085
+ stop(options = {}) {
2086
+ return shutdownApplication(
2087
+ options
2088
+ );
2089
+ },
2090
+ shutdown(options = {}) {
2091
+ return shutdownApplication(
2092
+ options
2093
+ );
2094
+ },
2095
+ close(options = {}) {
2096
+ return shutdownApplication(
2097
+ options
2098
+ );
2099
+ },
2100
+ readiness() {
2101
+ return deployment.readiness();
2102
+ },
2103
+ diagnostics() {
2104
+ return deployment.diagnostics();
2105
+ },
2106
+ installSignalHandlers(signalOptions = {}) {
2107
+ const signals = Array.from(
2108
+ new Set(
2109
+ signalOptions.signals ?? DEFAULT_APPLICATION_SIGNALS
2110
+ )
2111
+ );
2112
+ const handlers = /* @__PURE__ */ new Map();
2113
+ for (const signal of signals) {
2114
+ const handler = () => {
2115
+ void app.shutdown({
2116
+ reason: signal
2117
+ }).then(
2118
+ () => {
2119
+ if (signalOptions.setExitCode !== false) {
2120
+ process.exitCode = 0;
2121
+ }
2122
+ },
2123
+ () => {
2124
+ process.exitCode = 1;
2125
+ }
2126
+ );
2127
+ };
2128
+ handlers.set(
2129
+ signal,
2130
+ handler
2131
+ );
2132
+ process.on(
2133
+ signal,
2134
+ handler
2135
+ );
2136
+ }
2137
+ return () => {
2138
+ for (const [
2139
+ signal,
2140
+ handler
2141
+ ] of handlers) {
2142
+ process.off(
2143
+ signal,
2144
+ handler
2145
+ );
2146
+ }
2147
+ handlers.clear();
2148
+ };
2149
+ },
2150
+ registerShutdownHook(hookName) {
2151
+ return registerShutdownHook(
2152
+ () => app.shutdown({
2153
+ reason: "framework-shutdown"
2154
+ }),
2155
+ {
2156
+ name: hookName ?? `application:${name}`
2157
+ }
2158
+ );
2159
+ }
2160
+ };
2161
+ return app;
2162
+ async function startApplication() {
2163
+ try {
2164
+ if (!setupCompleted) {
2165
+ await runHook(
2166
+ "setup",
2167
+ definition.setup
2168
+ );
2169
+ setupCompleted = true;
2170
+ }
2171
+ registerApplicationResource();
2172
+ await deployment.start();
2173
+ state = "ready";
2174
+ failure = null;
2175
+ } catch (error) {
2176
+ const lifecycleError = error instanceof ApplicationLifecycleError ? error : new ApplicationLifecycleError(
2177
+ "start",
2178
+ error
2179
+ );
2180
+ failure = lifecycleError;
2181
+ state = "failed";
2182
+ await cleanupAfterFailure();
2183
+ throw lifecycleError;
2184
+ } finally {
2185
+ startPromise = null;
2186
+ }
2187
+ }
2188
+ async function shutdownApplication(options) {
2189
+ if (state === "stopped") {
2190
+ return;
2191
+ }
2192
+ if (state === "stopping") {
2193
+ return stopPromise ?? Promise.resolve();
2194
+ }
2195
+ if (state === "starting") {
2196
+ try {
2197
+ await startPromise;
2198
+ } catch {
2199
+ }
2200
+ }
2201
+ state = "stopping";
2202
+ stopPromise = stopApplication(options);
2203
+ return stopPromise;
2204
+ }
2205
+ async function stopApplication(options) {
2206
+ const errors = [];
2207
+ try {
2208
+ if (deployment.state !== "idle" && deployment.state !== "stopped") {
2209
+ try {
2210
+ await deployment.shutdown(
2211
+ options
2212
+ );
2213
+ } catch (error) {
2214
+ errors.push(error);
2215
+ }
2216
+ } else {
2217
+ try {
2218
+ await plugins.close();
2219
+ } catch (error) {
2220
+ errors.push(error);
2221
+ }
2222
+ }
2223
+ if (container.state !== "disposed") {
2224
+ try {
2225
+ await container.dispose();
2226
+ } catch (error) {
2227
+ errors.push(error);
2228
+ }
2229
+ }
2230
+ try {
2231
+ await disposeApplication();
2232
+ } catch (error) {
2233
+ errors.push(error);
2234
+ }
2235
+ state = "stopped";
2236
+ } finally {
2237
+ stopPromise = null;
2238
+ }
2239
+ if (errors.length > 0) {
2240
+ throw new AggregateError(
2241
+ errors,
2242
+ "BCP Application: one or more shutdown operations failed."
2243
+ );
2244
+ }
2245
+ }
2246
+ function registerApplicationResource() {
2247
+ if (applicationResourceRegistered) {
2248
+ return;
2249
+ }
2250
+ deployment.addResource({
2251
+ name: "bcp:application",
2252
+ async start() {
2253
+ await runHook(
2254
+ "start",
2255
+ definition.start
2256
+ );
2257
+ },
2258
+ ready() {
2259
+ return true;
2260
+ },
2261
+ async stop() {
2262
+ await runHook(
2263
+ "stop",
2264
+ definition.stop
2265
+ );
2266
+ },
2267
+ diagnostics() {
2268
+ return {
2269
+ name,
2270
+ ...version ? {
2271
+ version
2272
+ } : {},
2273
+ state,
2274
+ services: plugins.services.keys().map(
2275
+ formatServiceKey2
2276
+ ),
2277
+ containerProviders: container.graph().map(
2278
+ (node) => node.description
2279
+ ),
2280
+ pluginCount: plugins.plugins().length
2281
+ };
2282
+ }
2283
+ });
2284
+ applicationResourceRegistered = true;
2285
+ }
2286
+ async function cleanupAfterFailure() {
2287
+ if (deployment.state !== "idle" && deployment.state !== "stopped") {
2288
+ try {
2289
+ await deployment.shutdown({
2290
+ reason: "application-start-failed"
2291
+ });
2292
+ } catch {
2293
+ }
2294
+ }
2295
+ try {
2296
+ await plugins.close();
2297
+ } catch {
2298
+ }
2299
+ if (container.state !== "disposed") {
2300
+ try {
2301
+ await container.dispose();
2302
+ } catch {
2303
+ }
2304
+ }
2305
+ try {
2306
+ await disposeApplication();
2307
+ } catch {
2308
+ }
2309
+ }
2310
+ async function disposeApplication() {
2311
+ if (disposed) {
2312
+ return;
2313
+ }
2314
+ disposed = true;
2315
+ await runHook(
2316
+ "dispose",
2317
+ definition.dispose
2318
+ );
2319
+ }
2320
+ async function runHook(phase, hook) {
2321
+ if (!hook) {
2322
+ return;
2323
+ }
2324
+ try {
2325
+ await hook(context);
2326
+ } catch (error) {
2327
+ throw new ApplicationLifecycleError(
2328
+ phase,
2329
+ error
2330
+ );
2331
+ }
2332
+ }
2333
+ function assertMutable() {
2334
+ if (state !== "created") {
2335
+ throw new Error(
2336
+ "BCP Application: plugins, services, providers and resources must be registered before start()."
2337
+ );
2338
+ }
2339
+ }
2340
+ }
2341
+ function parseConfig2(parser, value) {
2342
+ if (!parser) {
2343
+ return value;
2344
+ }
2345
+ if (typeof parser === "function") {
2346
+ return parser(value);
2347
+ }
2348
+ if (parser && typeof parser === "object" && typeof parser.parse === "function") {
2349
+ return parser.parse(value);
2350
+ }
2351
+ throw new TypeError(
2352
+ "BCP Application: config schema must be a function or an object with parse()."
2353
+ );
2354
+ }
2355
+ function validateDefinition(definition) {
2356
+ if (!definition || typeof definition !== "object") {
2357
+ throw new TypeError(
2358
+ "BCP Application: definition must be an object."
2359
+ );
2360
+ }
2361
+ normalizeName4(
2362
+ definition.name,
2363
+ "application name"
2364
+ );
2365
+ normalizeOptionalText2(
2366
+ definition.version
2367
+ );
2368
+ if (definition.providers !== void 0 && !Array.isArray(definition.providers)) {
2369
+ throw new TypeError(
2370
+ "BCP Application: providers must be an array."
2371
+ );
2372
+ }
2373
+ if (definition.plugins !== void 0 && !Array.isArray(definition.plugins)) {
2374
+ throw new TypeError(
2375
+ "BCP Application: plugins must be an array."
2376
+ );
2377
+ }
2378
+ if (definition.modules !== void 0 && !Array.isArray(definition.modules)) {
2379
+ throw new TypeError(
2380
+ "BCP Application: modules must be an array."
2381
+ );
2382
+ }
2383
+ if (definition.resources !== void 0 && !Array.isArray(definition.resources)) {
2384
+ throw new TypeError(
2385
+ "BCP Application: resources must be an array."
2386
+ );
2387
+ }
2388
+ for (const [phase, hook] of [
2389
+ ["setup", definition.setup],
2390
+ ["start", definition.start],
2391
+ ["stop", definition.stop],
2392
+ ["dispose", definition.dispose]
2393
+ ]) {
2394
+ if (hook !== void 0 && typeof hook !== "function") {
2395
+ throw new TypeError(
2396
+ `BCP Application: ${phase} must be a function.`
2397
+ );
2398
+ }
2399
+ }
2400
+ }
2401
+ function normalizeName4(value, label) {
2402
+ if (typeof value !== "string") {
2403
+ throw new TypeError(
2404
+ `BCP Application: ${label} must be a string.`
2405
+ );
2406
+ }
2407
+ const normalized = value.trim();
2408
+ if (!normalized) {
2409
+ throw new TypeError(
2410
+ `BCP Application: ${label} cannot be empty.`
2411
+ );
2412
+ }
2413
+ return normalized;
2414
+ }
2415
+ function normalizeOptionalText2(value) {
2416
+ if (value === void 0) {
2417
+ return void 0;
2418
+ }
2419
+ if (typeof value !== "string") {
2420
+ throw new TypeError(
2421
+ "BCP Application: optional text values must be strings."
2422
+ );
2423
+ }
2424
+ return value.trim() || void 0;
2425
+ }
2426
+ function formatServiceKey2(key) {
2427
+ return typeof key === "symbol" ? key.description ? `Symbol(${key.description})` : key.toString() : key;
2428
+ }
2429
+ function formatError2(value) {
2430
+ if (value instanceof Error) {
2431
+ return value.message;
2432
+ }
2433
+ return String(value);
2434
+ }
2435
+ export {
2436
+ ApplicationLifecycleError,
2437
+ createApp,
2438
+ defineApp
2439
+ };