@chidchanun/bcp 0.3.0 → 0.3.2

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,970 @@
1
+ declare const SERVICE_TOKEN_VALUE:
2
+ unique symbol;
3
+
4
+ export type ServiceLifetime =
5
+ | "singleton"
6
+ | "scoped"
7
+ | "transient";
8
+
9
+ export type ServiceScopeState =
10
+ | "active"
11
+ | "disposing"
12
+ | "disposed";
13
+
14
+ export interface ServiceToken<T = unknown> {
15
+ readonly id: symbol;
16
+ readonly description: string;
17
+ readonly [SERVICE_TOKEN_VALUE]?: T;
18
+ }
19
+
20
+ export type ServiceTokenValue<TToken> =
21
+ TToken extends ServiceToken<infer TValue>
22
+ ? TValue
23
+ : never;
24
+
25
+ export type ServiceDependencyValues<
26
+ TDependencies extends readonly ServiceToken<any>[]
27
+ > = {
28
+ [TIndex in keyof TDependencies]:
29
+ ServiceTokenValue<TDependencies[TIndex]>;
30
+ };
31
+
32
+ export interface ServiceFactoryContext {
33
+ readonly scope: ServiceScope;
34
+ resolve<T>(token: ServiceToken<T>): Promise<T>;
35
+ optional<T>(
36
+ token: ServiceToken<T>
37
+ ): Promise<T | undefined>;
38
+ }
39
+
40
+ export interface ServiceProvider<T = unknown> {
41
+ readonly token: ServiceToken<T>;
42
+ readonly lifetime?: ServiceLifetime;
43
+ readonly dependencies?:
44
+ readonly ServiceToken<any>[];
45
+ factory(
46
+ context: ServiceFactoryContext,
47
+ dependencies: readonly unknown[]
48
+ ): T | Promise<T>;
49
+ dispose?(
50
+ value: T
51
+ ): void | Promise<void>;
52
+ }
53
+
54
+ export interface ServiceProviderOptions<T> {
55
+ lifetime?: ServiceLifetime;
56
+ dispose?: (
57
+ value: T
58
+ ) => void | Promise<void>;
59
+ }
60
+
61
+ export interface ServiceScopeOptions {
62
+ name?: string;
63
+ overrides?: readonly ServiceProvider<any>[];
64
+ }
65
+
66
+ export interface ServiceContainerOptions {
67
+ name?: string;
68
+ providers?: readonly ServiceProvider<any>[];
69
+ }
70
+
71
+ export interface ServiceGraphNode {
72
+ token: ServiceToken<any>;
73
+ description: string;
74
+ lifetime: ServiceLifetime;
75
+ dependencies: string[];
76
+ overridden: boolean;
77
+ }
78
+
79
+ export interface ServiceScope {
80
+ readonly name: string;
81
+ readonly state: ServiceScopeState;
82
+ readonly parent: ServiceScope | undefined;
83
+ has(token: ServiceToken<any>): boolean;
84
+ resolve<T>(token: ServiceToken<T>): Promise<T>;
85
+ optional<T>(
86
+ token: ServiceToken<T>
87
+ ): Promise<T | undefined>;
88
+ createScope(
89
+ options?: ServiceScopeOptions
90
+ ): ServiceScope;
91
+ graph(): ServiceGraphNode[];
92
+ dispose(): Promise<void>;
93
+ }
94
+
95
+ export interface ServiceContainer
96
+ extends ServiceScope {
97
+ register<T>(
98
+ provider: ServiceProvider<T>,
99
+ options?: {
100
+ replace?: boolean;
101
+ }
102
+ ): ServiceContainer;
103
+ registerMany(
104
+ providers:
105
+ readonly ServiceProvider<any>[]
106
+ ): ServiceContainer;
107
+ providers(): ServiceProvider<any>[];
108
+ }
109
+
110
+ interface ProviderRecord {
111
+ provider: ServiceProvider<any>;
112
+ owner: ScopeImpl;
113
+ overridden: boolean;
114
+ }
115
+
116
+ interface DisposalRecord {
117
+ provider: ServiceProvider<any>;
118
+ value: unknown;
119
+ }
120
+
121
+ export class ServiceNotFoundError
122
+ extends Error {
123
+ readonly token: ServiceToken<any>;
124
+
125
+ constructor(token: ServiceToken<any>) {
126
+ super(
127
+ `BCP Container: service "${token.description}" is not registered.`
128
+ );
129
+ this.name = "ServiceNotFoundError";
130
+ this.token = token;
131
+ }
132
+ }
133
+
134
+ export class ServiceResolutionError
135
+ extends Error {
136
+ readonly token: ServiceToken<any>;
137
+ readonly cause: unknown;
138
+
139
+ constructor(
140
+ token: ServiceToken<any>,
141
+ message: string,
142
+ cause?: unknown
143
+ ) {
144
+ super(
145
+ `BCP Container: could not resolve "${token.description}". ${message}`
146
+ );
147
+ this.name = "ServiceResolutionError";
148
+ this.token = token;
149
+ this.cause = cause;
150
+ }
151
+ }
152
+
153
+ export class ServiceDisposalError
154
+ extends AggregateError {
155
+ constructor(errors: readonly unknown[]) {
156
+ super(
157
+ errors,
158
+ "BCP Container: one or more services failed to dispose."
159
+ );
160
+ this.name = "ServiceDisposalError";
161
+ }
162
+ }
163
+
164
+ export function createServiceToken<T>(
165
+ description: string
166
+ ): ServiceToken<T> {
167
+ const normalized =
168
+ normalizeName(
169
+ description,
170
+ "service token description"
171
+ );
172
+
173
+ return Object.freeze({
174
+ id: Symbol(normalized),
175
+ description: normalized,
176
+ });
177
+ }
178
+
179
+ export function provideValue<T>(
180
+ token: ServiceToken<T>,
181
+ value: T,
182
+ options: Pick<
183
+ ServiceProviderOptions<T>,
184
+ "dispose"
185
+ > = {}
186
+ ): ServiceProvider<T> {
187
+ assertToken(token);
188
+
189
+ return {
190
+ token,
191
+ lifetime: "singleton",
192
+ factory() {
193
+ return value;
194
+ },
195
+ ...(options.dispose
196
+ ? {
197
+ dispose: options.dispose,
198
+ }
199
+ : {}),
200
+ };
201
+ }
202
+
203
+ export function provideFactory<
204
+ T,
205
+ TDependencies extends
206
+ readonly ServiceToken<any>[]
207
+ >(
208
+ token: ServiceToken<T>,
209
+ dependencies: TDependencies,
210
+ factory: (
211
+ context: ServiceFactoryContext,
212
+ dependencies:
213
+ ServiceDependencyValues<TDependencies>
214
+ ) => T | Promise<T>,
215
+ options: ServiceProviderOptions<T> = {}
216
+ ): ServiceProvider<T> {
217
+ assertToken(token);
218
+ assertDependencies(dependencies);
219
+ const lifetime =
220
+ normalizeLifetime(
221
+ options.lifetime
222
+ );
223
+
224
+ return {
225
+ token,
226
+ lifetime,
227
+ dependencies: [
228
+ ...dependencies,
229
+ ],
230
+ factory(
231
+ context,
232
+ values
233
+ ) {
234
+ return factory(
235
+ context,
236
+ values as
237
+ ServiceDependencyValues<TDependencies>
238
+ );
239
+ },
240
+ ...(options.dispose
241
+ ? {
242
+ dispose: options.dispose,
243
+ }
244
+ : {}),
245
+ };
246
+ }
247
+
248
+ export function provideClass<
249
+ T,
250
+ TDependencies extends
251
+ readonly ServiceToken<any>[]
252
+ >(
253
+ token: ServiceToken<T>,
254
+ dependencies: TDependencies,
255
+ Constructor: new (
256
+ ...dependencies:
257
+ ServiceDependencyValues<TDependencies>
258
+ ) => T,
259
+ options: ServiceProviderOptions<T> = {}
260
+ ): ServiceProvider<T> {
261
+ if (typeof Constructor !== "function") {
262
+ throw new TypeError(
263
+ "BCP Container: service constructor must be a function."
264
+ );
265
+ }
266
+
267
+ return provideFactory(
268
+ token,
269
+ dependencies,
270
+ (
271
+ _context,
272
+ values
273
+ ) =>
274
+ new Constructor(
275
+ ...values
276
+ ),
277
+ options
278
+ );
279
+ }
280
+
281
+ export function createServiceContainer(
282
+ options: ServiceContainerOptions = {}
283
+ ): ServiceContainer {
284
+ const root =
285
+ new ScopeImpl(
286
+ normalizeOptionalName(
287
+ options.name
288
+ ) ?? "root",
289
+ undefined
290
+ );
291
+ root.root = root;
292
+
293
+ const container:
294
+ ServiceContainer = {
295
+ get name() {
296
+ return root.name;
297
+ },
298
+ get state() {
299
+ return root.state;
300
+ },
301
+ get parent() {
302
+ return undefined;
303
+ },
304
+ has(token) {
305
+ return root.has(token);
306
+ },
307
+ resolve<T>(token: ServiceToken<T>) {
308
+ return root.resolve(token);
309
+ },
310
+ optional<T>(token: ServiceToken<T>) {
311
+ return root.optional(token);
312
+ },
313
+ createScope(scopeOptions = {}) {
314
+ return root.createScope(
315
+ scopeOptions
316
+ );
317
+ },
318
+ graph() {
319
+ return root.graph();
320
+ },
321
+ dispose() {
322
+ return root.dispose();
323
+ },
324
+ register<T>(
325
+ provider: ServiceProvider<T>,
326
+ registerOptions: {
327
+ replace?: boolean;
328
+ } = {}
329
+ ) {
330
+ root.registerProvider(
331
+ provider,
332
+ registerOptions.replace === true
333
+ );
334
+ return container;
335
+ },
336
+ registerMany(providers) {
337
+ for (const provider of providers) {
338
+ root.registerProvider(
339
+ provider,
340
+ false
341
+ );
342
+ }
343
+ return container;
344
+ },
345
+ providers() {
346
+ return Array.from(
347
+ root.providersMap.values()
348
+ );
349
+ },
350
+ };
351
+
352
+ for (
353
+ const provider
354
+ of options.providers ?? []
355
+ ) {
356
+ root.registerProvider(
357
+ provider,
358
+ false
359
+ );
360
+ }
361
+
362
+ return container;
363
+ }
364
+
365
+ class ScopeImpl
366
+ implements ServiceScope {
367
+ root!: ScopeImpl;
368
+ readonly providersMap =
369
+ new Map<
370
+ symbol,
371
+ ServiceProvider<any>
372
+ >();
373
+ readonly overrideProviders =
374
+ new Map<
375
+ symbol,
376
+ ServiceProvider<any>
377
+ >();
378
+ readonly singletonCache =
379
+ new Map<
380
+ symbol,
381
+ Promise<unknown>
382
+ >();
383
+ readonly scopedCache =
384
+ new Map<
385
+ symbol,
386
+ Promise<unknown>
387
+ >();
388
+ readonly disposals:
389
+ DisposalRecord[] = [];
390
+ readonly children =
391
+ new Set<ScopeImpl>();
392
+ state:
393
+ ServiceScopeState = "active";
394
+ private disposePromise:
395
+ Promise<void> | undefined;
396
+
397
+ constructor(
398
+ readonly name: string,
399
+ readonly parent: ScopeImpl | undefined
400
+ ) {
401
+ if (parent) {
402
+ this.root = parent.root;
403
+ parent.children.add(this);
404
+ }
405
+ }
406
+
407
+ has(token: ServiceToken<any>): boolean {
408
+ assertToken(token);
409
+ return this.findProvider(token) !==
410
+ undefined;
411
+ }
412
+
413
+ resolve<T>(
414
+ token: ServiceToken<T>
415
+ ): Promise<T> {
416
+ return this.resolveWithPath(
417
+ token,
418
+ []
419
+ );
420
+ }
421
+
422
+ async optional<T>(
423
+ token: ServiceToken<T>
424
+ ): Promise<T | undefined> {
425
+ if (!this.has(token)) {
426
+ return undefined;
427
+ }
428
+ return this.resolve(token);
429
+ }
430
+
431
+ createScope(
432
+ options: ServiceScopeOptions = {}
433
+ ): ServiceScope {
434
+ this.assertActive();
435
+
436
+ const scope =
437
+ new ScopeImpl(
438
+ normalizeOptionalName(
439
+ options.name
440
+ ) ??
441
+ `${this.name}:scope-${this.children.size + 1}`,
442
+ this
443
+ );
444
+
445
+ for (
446
+ const provider
447
+ of options.overrides ?? []
448
+ ) {
449
+ scope.registerOverride(
450
+ provider
451
+ );
452
+ }
453
+
454
+ return scope;
455
+ }
456
+
457
+ graph(): ServiceGraphNode[] {
458
+ const effective =
459
+ new Map<
460
+ symbol,
461
+ ProviderRecord
462
+ >();
463
+
464
+ let current:
465
+ ScopeImpl | undefined = this;
466
+ while (current) {
467
+ for (
468
+ const [id, provider]
469
+ of current.overrideProviders
470
+ ) {
471
+ if (!effective.has(id)) {
472
+ effective.set(
473
+ id,
474
+ {
475
+ provider,
476
+ owner: current,
477
+ overridden: true,
478
+ }
479
+ );
480
+ }
481
+ }
482
+ current = current.parent;
483
+ }
484
+
485
+ for (
486
+ const [id, provider]
487
+ of this.root.providersMap
488
+ ) {
489
+ if (!effective.has(id)) {
490
+ effective.set(
491
+ id,
492
+ {
493
+ provider,
494
+ owner: this.root,
495
+ overridden: false,
496
+ }
497
+ );
498
+ }
499
+ }
500
+
501
+ return Array.from(
502
+ effective.values(),
503
+ record => ({
504
+ token:
505
+ record.provider.token,
506
+ description:
507
+ record.provider.token.description,
508
+ lifetime:
509
+ normalizeLifetime(
510
+ record.provider.lifetime
511
+ ),
512
+ dependencies:
513
+ (
514
+ record.provider.dependencies ?? []
515
+ ).map(
516
+ dependency =>
517
+ dependency.description
518
+ ),
519
+ overridden:
520
+ record.overridden,
521
+ })
522
+ ).sort(
523
+ (left, right) =>
524
+ left.description.localeCompare(
525
+ right.description
526
+ )
527
+ );
528
+ }
529
+
530
+ dispose(): Promise<void> {
531
+ if (this.disposePromise) {
532
+ return this.disposePromise;
533
+ }
534
+ if (this.state === "disposed") {
535
+ return Promise.resolve();
536
+ }
537
+
538
+ this.disposePromise =
539
+ this.disposeInternal();
540
+ return this.disposePromise;
541
+ }
542
+
543
+ registerProvider<T>(
544
+ provider: ServiceProvider<T>,
545
+ replace: boolean
546
+ ): void {
547
+ this.assertActive();
548
+ if (this.parent) {
549
+ throw new Error(
550
+ "BCP Container: providers can only be registered on the root container. Use scope overrides for child scopes."
551
+ );
552
+ }
553
+ validateProvider(provider);
554
+
555
+ const id =
556
+ provider.token.id;
557
+ if (
558
+ this.providersMap.has(id) &&
559
+ !replace
560
+ ) {
561
+ throw new Error(
562
+ `BCP Container: service "${provider.token.description}" is already registered.`
563
+ );
564
+ }
565
+ if (
566
+ replace &&
567
+ this.hasResolved(id)
568
+ ) {
569
+ throw new Error(
570
+ `BCP Container: service "${provider.token.description}" cannot be replaced after it has been resolved.`
571
+ );
572
+ }
573
+
574
+ this.providersMap.set(
575
+ id,
576
+ provider
577
+ );
578
+ }
579
+
580
+ private registerOverride(
581
+ provider: ServiceProvider<any>
582
+ ): void {
583
+ validateProvider(provider);
584
+ const id =
585
+ provider.token.id;
586
+ if (
587
+ this.overrideProviders.has(id)
588
+ ) {
589
+ throw new Error(
590
+ `BCP Container: scope override for "${provider.token.description}" is already registered.`
591
+ );
592
+ }
593
+ this.overrideProviders.set(
594
+ id,
595
+ provider
596
+ );
597
+ }
598
+
599
+ private async resolveWithPath<T>(
600
+ token: ServiceToken<T>,
601
+ path: readonly ServiceToken<any>[]
602
+ ): Promise<T> {
603
+ this.assertActive();
604
+ assertToken(token);
605
+
606
+ if (
607
+ path.some(
608
+ item =>
609
+ item.id === token.id
610
+ )
611
+ ) {
612
+ throw new ServiceResolutionError(
613
+ token,
614
+ `Circular dependency detected: ${[
615
+ ...path,
616
+ token,
617
+ ].map(
618
+ item =>
619
+ item.description
620
+ ).join(" -> ")}.`
621
+ );
622
+ }
623
+
624
+ const record =
625
+ this.findProvider(token);
626
+ if (!record) {
627
+ throw new ServiceNotFoundError(
628
+ token
629
+ );
630
+ }
631
+
632
+ const lifetime =
633
+ normalizeLifetime(
634
+ record.provider.lifetime
635
+ );
636
+ const cache =
637
+ lifetime === "singleton"
638
+ ? record.owner.singletonCache
639
+ : lifetime === "scoped"
640
+ ? this.scopedCache
641
+ : undefined;
642
+ const existing =
643
+ cache?.get(token.id);
644
+ if (existing) {
645
+ return existing as Promise<T>;
646
+ }
647
+
648
+ const resolution =
649
+ this.instantiate(
650
+ record.provider as
651
+ ServiceProvider<T>,
652
+ [
653
+ ...path,
654
+ token,
655
+ ],
656
+ lifetime,
657
+ record.owner
658
+ );
659
+
660
+ cache?.set(
661
+ token.id,
662
+ resolution
663
+ );
664
+
665
+ try {
666
+ return await resolution;
667
+ } catch (error) {
668
+ cache?.delete(token.id);
669
+ if (
670
+ error instanceof ServiceNotFoundError ||
671
+ error instanceof ServiceResolutionError
672
+ ) {
673
+ throw error;
674
+ }
675
+ throw new ServiceResolutionError(
676
+ token,
677
+ error instanceof Error
678
+ ? error.message
679
+ : String(error),
680
+ error
681
+ );
682
+ }
683
+ }
684
+
685
+ private async instantiate<T>(
686
+ provider: ServiceProvider<T>,
687
+ path: readonly ServiceToken<any>[],
688
+ lifetime: ServiceLifetime,
689
+ providerOwner: ScopeImpl
690
+ ): Promise<T> {
691
+ const dependencies =
692
+ await Promise.all(
693
+ (
694
+ provider.dependencies ?? []
695
+ ).map(
696
+ dependency =>
697
+ this.resolveWithPath(
698
+ dependency,
699
+ path
700
+ )
701
+ )
702
+ );
703
+ const context:
704
+ ServiceFactoryContext = {
705
+ scope: this,
706
+ resolve: <TValue>(
707
+ dependency: ServiceToken<TValue>
708
+ ) =>
709
+ this.resolveWithPath(
710
+ dependency,
711
+ path
712
+ ),
713
+ optional: async <TValue>(
714
+ dependency: ServiceToken<TValue>
715
+ ) => {
716
+ if (!this.has(dependency)) {
717
+ return undefined;
718
+ }
719
+ return this.resolveWithPath(
720
+ dependency,
721
+ path
722
+ );
723
+ },
724
+ };
725
+ const value =
726
+ await provider.factory(
727
+ context,
728
+ dependencies
729
+ );
730
+
731
+ if (provider.dispose) {
732
+ const disposalOwner =
733
+ lifetime === "singleton"
734
+ ? providerOwner
735
+ : this;
736
+ disposalOwner.disposals.push({
737
+ provider,
738
+ value,
739
+ });
740
+ }
741
+
742
+ return value;
743
+ }
744
+
745
+ private findProvider(
746
+ token: ServiceToken<any>
747
+ ): ProviderRecord | undefined {
748
+ let current:
749
+ ScopeImpl | undefined = this;
750
+
751
+ while (current) {
752
+ const override =
753
+ current.overrideProviders.get(
754
+ token.id
755
+ );
756
+ if (override) {
757
+ return {
758
+ provider: override,
759
+ owner: current,
760
+ overridden: true,
761
+ };
762
+ }
763
+ current = current.parent;
764
+ }
765
+
766
+ const provider =
767
+ this.root.providersMap.get(
768
+ token.id
769
+ );
770
+ if (!provider) {
771
+ return undefined;
772
+ }
773
+
774
+ return {
775
+ provider,
776
+ owner: this.root,
777
+ overridden: false,
778
+ };
779
+ }
780
+
781
+ private async disposeInternal(): Promise<void> {
782
+ this.state = "disposing";
783
+ const errors:
784
+ unknown[] = [];
785
+
786
+ for (
787
+ const child
788
+ of Array.from(
789
+ this.children
790
+ ).reverse()
791
+ ) {
792
+ try {
793
+ await child.dispose();
794
+ } catch (error) {
795
+ errors.push(error);
796
+ }
797
+ }
798
+
799
+ for (
800
+ const record
801
+ of [...this.disposals].reverse()
802
+ ) {
803
+ if (!record.provider.dispose) {
804
+ continue;
805
+ }
806
+ try {
807
+ await record.provider.dispose(
808
+ record.value
809
+ );
810
+ } catch (error) {
811
+ errors.push(error);
812
+ }
813
+ }
814
+
815
+ this.disposals.length = 0;
816
+ this.singletonCache.clear();
817
+ this.scopedCache.clear();
818
+ this.children.clear();
819
+ this.parent?.children.delete(this);
820
+ this.state = "disposed";
821
+
822
+ if (errors.length > 0) {
823
+ throw new ServiceDisposalError(
824
+ errors
825
+ );
826
+ }
827
+ }
828
+
829
+ private hasResolved(id: symbol): boolean {
830
+ if (
831
+ this.singletonCache.has(id) ||
832
+ this.scopedCache.has(id)
833
+ ) {
834
+ return true;
835
+ }
836
+ for (const child of this.children) {
837
+ if (child.hasResolved(id)) {
838
+ return true;
839
+ }
840
+ }
841
+ return false;
842
+ }
843
+
844
+ private assertActive(): void {
845
+ if (this.state !== "active") {
846
+ throw new Error(
847
+ `BCP Container: scope "${this.name}" is ${this.state}.`
848
+ );
849
+ }
850
+ }
851
+ }
852
+
853
+ function validateProvider<T>(
854
+ provider: ServiceProvider<T>
855
+ ): void {
856
+ if (
857
+ !provider ||
858
+ typeof provider !== "object"
859
+ ) {
860
+ throw new TypeError(
861
+ "BCP Container: provider must be an object."
862
+ );
863
+ }
864
+ assertToken(provider.token);
865
+ if (typeof provider.factory !== "function") {
866
+ throw new TypeError(
867
+ `BCP Container: provider "${provider.token.description}" must define factory().`
868
+ );
869
+ }
870
+ normalizeLifetime(
871
+ provider.lifetime
872
+ );
873
+ assertDependencies(
874
+ provider.dependencies ?? []
875
+ );
876
+ if (
877
+ provider.dispose !== undefined &&
878
+ typeof provider.dispose !== "function"
879
+ ) {
880
+ throw new TypeError(
881
+ `BCP Container: provider "${provider.token.description}" dispose must be a function.`
882
+ );
883
+ }
884
+ }
885
+
886
+ function assertToken(
887
+ token: ServiceToken<any>
888
+ ): void {
889
+ if (
890
+ !token ||
891
+ typeof token !== "object" ||
892
+ typeof token.id !== "symbol" ||
893
+ typeof token.description !== "string" ||
894
+ token.description.trim() === ""
895
+ ) {
896
+ throw new TypeError(
897
+ "BCP Container: invalid service token. Use createServiceToken()."
898
+ );
899
+ }
900
+ }
901
+
902
+ function assertDependencies(
903
+ dependencies:
904
+ readonly ServiceToken<any>[]
905
+ ): void {
906
+ if (!Array.isArray(dependencies)) {
907
+ throw new TypeError(
908
+ "BCP Container: provider dependencies must be an array."
909
+ );
910
+ }
911
+ for (const dependency of dependencies) {
912
+ assertToken(dependency);
913
+ }
914
+ }
915
+
916
+ function normalizeLifetime(
917
+ lifetime: ServiceLifetime | undefined
918
+ ): ServiceLifetime {
919
+ const normalized =
920
+ lifetime ?? "singleton";
921
+ if (
922
+ normalized !== "singleton" &&
923
+ normalized !== "scoped" &&
924
+ normalized !== "transient"
925
+ ) {
926
+ throw new TypeError(
927
+ "BCP Container: service lifetime must be singleton, scoped or transient."
928
+ );
929
+ }
930
+ return normalized;
931
+ }
932
+
933
+ function normalizeName(
934
+ value: unknown,
935
+ label: string
936
+ ): string {
937
+ if (typeof value !== "string") {
938
+ throw new TypeError(
939
+ `BCP Container: ${label} must be a string.`
940
+ );
941
+ }
942
+ const normalized =
943
+ value.trim();
944
+ if (!normalized) {
945
+ throw new TypeError(
946
+ `BCP Container: ${label} cannot be empty.`
947
+ );
948
+ }
949
+ if (
950
+ normalized.length > 256 ||
951
+ /[\r\n]/.test(normalized)
952
+ ) {
953
+ throw new TypeError(
954
+ `BCP Container: ${label} must be at most 256 characters without line breaks.`
955
+ );
956
+ }
957
+ return normalized;
958
+ }
959
+
960
+ function normalizeOptionalName(
961
+ value: string | undefined
962
+ ): string | undefined {
963
+ if (value === undefined) {
964
+ return undefined;
965
+ }
966
+ return normalizeName(
967
+ value,
968
+ "scope name"
969
+ );
970
+ }