@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,573 @@
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 createServiceToken(description) {
34
+ const normalized = normalizeName(
35
+ description,
36
+ "service token description"
37
+ );
38
+ return Object.freeze({
39
+ id: Symbol(normalized),
40
+ description: normalized
41
+ });
42
+ }
43
+ function provideValue(token, value, options = {}) {
44
+ assertToken(token);
45
+ return {
46
+ token,
47
+ lifetime: "singleton",
48
+ factory() {
49
+ return value;
50
+ },
51
+ ...options.dispose ? {
52
+ dispose: options.dispose
53
+ } : {}
54
+ };
55
+ }
56
+ function provideFactory(token, dependencies, factory, options = {}) {
57
+ assertToken(token);
58
+ assertDependencies(dependencies);
59
+ const lifetime = normalizeLifetime(
60
+ options.lifetime
61
+ );
62
+ return {
63
+ token,
64
+ lifetime,
65
+ dependencies: [
66
+ ...dependencies
67
+ ],
68
+ factory(context, values) {
69
+ return factory(
70
+ context,
71
+ values
72
+ );
73
+ },
74
+ ...options.dispose ? {
75
+ dispose: options.dispose
76
+ } : {}
77
+ };
78
+ }
79
+ function provideClass(token, dependencies, Constructor, options = {}) {
80
+ if (typeof Constructor !== "function") {
81
+ throw new TypeError(
82
+ "BCP Container: service constructor must be a function."
83
+ );
84
+ }
85
+ return provideFactory(
86
+ token,
87
+ dependencies,
88
+ (_context, values) => new Constructor(
89
+ ...values
90
+ ),
91
+ options
92
+ );
93
+ }
94
+ function createServiceContainer(options = {}) {
95
+ const root = new ScopeImpl(
96
+ normalizeOptionalName(
97
+ options.name
98
+ ) ?? "root",
99
+ void 0
100
+ );
101
+ root.root = root;
102
+ const container = {
103
+ get name() {
104
+ return root.name;
105
+ },
106
+ get state() {
107
+ return root.state;
108
+ },
109
+ get parent() {
110
+ return void 0;
111
+ },
112
+ has(token) {
113
+ return root.has(token);
114
+ },
115
+ resolve(token) {
116
+ return root.resolve(token);
117
+ },
118
+ optional(token) {
119
+ return root.optional(token);
120
+ },
121
+ createScope(scopeOptions = {}) {
122
+ return root.createScope(
123
+ scopeOptions
124
+ );
125
+ },
126
+ graph() {
127
+ return root.graph();
128
+ },
129
+ dispose() {
130
+ return root.dispose();
131
+ },
132
+ register(provider, registerOptions = {}) {
133
+ root.registerProvider(
134
+ provider,
135
+ registerOptions.replace === true
136
+ );
137
+ return container;
138
+ },
139
+ registerMany(providers) {
140
+ for (const provider of providers) {
141
+ root.registerProvider(
142
+ provider,
143
+ false
144
+ );
145
+ }
146
+ return container;
147
+ },
148
+ providers() {
149
+ return Array.from(
150
+ root.providersMap.values()
151
+ );
152
+ }
153
+ };
154
+ for (const provider of options.providers ?? []) {
155
+ root.registerProvider(
156
+ provider,
157
+ false
158
+ );
159
+ }
160
+ return container;
161
+ }
162
+ var ScopeImpl = class _ScopeImpl {
163
+ constructor(name, parent) {
164
+ this.name = name;
165
+ this.parent = parent;
166
+ if (parent) {
167
+ this.root = parent.root;
168
+ parent.children.add(this);
169
+ }
170
+ }
171
+ name;
172
+ parent;
173
+ root;
174
+ providersMap = /* @__PURE__ */ new Map();
175
+ overrideProviders = /* @__PURE__ */ new Map();
176
+ singletonCache = /* @__PURE__ */ new Map();
177
+ scopedCache = /* @__PURE__ */ new Map();
178
+ disposals = [];
179
+ children = /* @__PURE__ */ new Set();
180
+ state = "active";
181
+ disposePromise;
182
+ has(token) {
183
+ assertToken(token);
184
+ return this.findProvider(token) !== void 0;
185
+ }
186
+ resolve(token) {
187
+ return this.resolveWithPath(
188
+ token,
189
+ []
190
+ );
191
+ }
192
+ async optional(token) {
193
+ if (!this.has(token)) {
194
+ return void 0;
195
+ }
196
+ return this.resolve(token);
197
+ }
198
+ createScope(options = {}) {
199
+ this.assertActive();
200
+ const scope = new _ScopeImpl(
201
+ normalizeOptionalName(
202
+ options.name
203
+ ) ?? `${this.name}:scope-${this.children.size + 1}`,
204
+ this
205
+ );
206
+ for (const provider of options.overrides ?? []) {
207
+ scope.registerOverride(
208
+ provider
209
+ );
210
+ }
211
+ return scope;
212
+ }
213
+ graph() {
214
+ const effective = /* @__PURE__ */ new Map();
215
+ let current = this;
216
+ while (current) {
217
+ for (const [id, provider] of current.overrideProviders) {
218
+ if (!effective.has(id)) {
219
+ effective.set(
220
+ id,
221
+ {
222
+ provider,
223
+ owner: current,
224
+ overridden: true
225
+ }
226
+ );
227
+ }
228
+ }
229
+ current = current.parent;
230
+ }
231
+ for (const [id, provider] of this.root.providersMap) {
232
+ if (!effective.has(id)) {
233
+ effective.set(
234
+ id,
235
+ {
236
+ provider,
237
+ owner: this.root,
238
+ overridden: false
239
+ }
240
+ );
241
+ }
242
+ }
243
+ return Array.from(
244
+ effective.values(),
245
+ (record) => ({
246
+ token: record.provider.token,
247
+ description: record.provider.token.description,
248
+ lifetime: normalizeLifetime(
249
+ record.provider.lifetime
250
+ ),
251
+ dependencies: (record.provider.dependencies ?? []).map(
252
+ (dependency) => dependency.description
253
+ ),
254
+ overridden: record.overridden
255
+ })
256
+ ).sort(
257
+ (left, right) => left.description.localeCompare(
258
+ right.description
259
+ )
260
+ );
261
+ }
262
+ dispose() {
263
+ if (this.disposePromise) {
264
+ return this.disposePromise;
265
+ }
266
+ if (this.state === "disposed") {
267
+ return Promise.resolve();
268
+ }
269
+ this.disposePromise = this.disposeInternal();
270
+ return this.disposePromise;
271
+ }
272
+ registerProvider(provider, replace) {
273
+ this.assertActive();
274
+ if (this.parent) {
275
+ throw new Error(
276
+ "BCP Container: providers can only be registered on the root container. Use scope overrides for child scopes."
277
+ );
278
+ }
279
+ validateProvider(provider);
280
+ const id = provider.token.id;
281
+ if (this.providersMap.has(id) && !replace) {
282
+ throw new Error(
283
+ `BCP Container: service "${provider.token.description}" is already registered.`
284
+ );
285
+ }
286
+ if (replace && this.hasResolved(id)) {
287
+ throw new Error(
288
+ `BCP Container: service "${provider.token.description}" cannot be replaced after it has been resolved.`
289
+ );
290
+ }
291
+ this.providersMap.set(
292
+ id,
293
+ provider
294
+ );
295
+ }
296
+ registerOverride(provider) {
297
+ validateProvider(provider);
298
+ const id = provider.token.id;
299
+ if (this.overrideProviders.has(id)) {
300
+ throw new Error(
301
+ `BCP Container: scope override for "${provider.token.description}" is already registered.`
302
+ );
303
+ }
304
+ this.overrideProviders.set(
305
+ id,
306
+ provider
307
+ );
308
+ }
309
+ async resolveWithPath(token, path) {
310
+ this.assertActive();
311
+ assertToken(token);
312
+ if (path.some(
313
+ (item) => item.id === token.id
314
+ )) {
315
+ throw new ServiceResolutionError(
316
+ token,
317
+ `Circular dependency detected: ${[
318
+ ...path,
319
+ token
320
+ ].map(
321
+ (item) => item.description
322
+ ).join(" -> ")}.`
323
+ );
324
+ }
325
+ const record = this.findProvider(token);
326
+ if (!record) {
327
+ throw new ServiceNotFoundError(
328
+ token
329
+ );
330
+ }
331
+ const lifetime = normalizeLifetime(
332
+ record.provider.lifetime
333
+ );
334
+ const cache = lifetime === "singleton" ? record.owner.singletonCache : lifetime === "scoped" ? this.scopedCache : void 0;
335
+ const existing = cache?.get(token.id);
336
+ if (existing) {
337
+ return existing;
338
+ }
339
+ const resolution = this.instantiate(
340
+ record.provider,
341
+ [
342
+ ...path,
343
+ token
344
+ ],
345
+ lifetime,
346
+ record.owner
347
+ );
348
+ cache?.set(
349
+ token.id,
350
+ resolution
351
+ );
352
+ try {
353
+ return await resolution;
354
+ } catch (error) {
355
+ cache?.delete(token.id);
356
+ if (error instanceof ServiceNotFoundError || error instanceof ServiceResolutionError) {
357
+ throw error;
358
+ }
359
+ throw new ServiceResolutionError(
360
+ token,
361
+ error instanceof Error ? error.message : String(error),
362
+ error
363
+ );
364
+ }
365
+ }
366
+ async instantiate(provider, path, lifetime, providerOwner) {
367
+ const dependencies = await Promise.all(
368
+ (provider.dependencies ?? []).map(
369
+ (dependency) => this.resolveWithPath(
370
+ dependency,
371
+ path
372
+ )
373
+ )
374
+ );
375
+ const context = {
376
+ scope: this,
377
+ resolve: (dependency) => this.resolveWithPath(
378
+ dependency,
379
+ path
380
+ ),
381
+ optional: async (dependency) => {
382
+ if (!this.has(dependency)) {
383
+ return void 0;
384
+ }
385
+ return this.resolveWithPath(
386
+ dependency,
387
+ path
388
+ );
389
+ }
390
+ };
391
+ const value = await provider.factory(
392
+ context,
393
+ dependencies
394
+ );
395
+ if (provider.dispose) {
396
+ const disposalOwner = lifetime === "singleton" ? providerOwner : this;
397
+ disposalOwner.disposals.push({
398
+ provider,
399
+ value
400
+ });
401
+ }
402
+ return value;
403
+ }
404
+ findProvider(token) {
405
+ let current = this;
406
+ while (current) {
407
+ const override = current.overrideProviders.get(
408
+ token.id
409
+ );
410
+ if (override) {
411
+ return {
412
+ provider: override,
413
+ owner: current,
414
+ overridden: true
415
+ };
416
+ }
417
+ current = current.parent;
418
+ }
419
+ const provider = this.root.providersMap.get(
420
+ token.id
421
+ );
422
+ if (!provider) {
423
+ return void 0;
424
+ }
425
+ return {
426
+ provider,
427
+ owner: this.root,
428
+ overridden: false
429
+ };
430
+ }
431
+ async disposeInternal() {
432
+ this.state = "disposing";
433
+ const errors = [];
434
+ for (const child of Array.from(
435
+ this.children
436
+ ).reverse()) {
437
+ try {
438
+ await child.dispose();
439
+ } catch (error) {
440
+ errors.push(error);
441
+ }
442
+ }
443
+ for (const record of [...this.disposals].reverse()) {
444
+ if (!record.provider.dispose) {
445
+ continue;
446
+ }
447
+ try {
448
+ await record.provider.dispose(
449
+ record.value
450
+ );
451
+ } catch (error) {
452
+ errors.push(error);
453
+ }
454
+ }
455
+ this.disposals.length = 0;
456
+ this.singletonCache.clear();
457
+ this.scopedCache.clear();
458
+ this.children.clear();
459
+ this.parent?.children.delete(this);
460
+ this.state = "disposed";
461
+ if (errors.length > 0) {
462
+ throw new ServiceDisposalError(
463
+ errors
464
+ );
465
+ }
466
+ }
467
+ hasResolved(id) {
468
+ if (this.singletonCache.has(id) || this.scopedCache.has(id)) {
469
+ return true;
470
+ }
471
+ for (const child of this.children) {
472
+ if (child.hasResolved(id)) {
473
+ return true;
474
+ }
475
+ }
476
+ return false;
477
+ }
478
+ assertActive() {
479
+ if (this.state !== "active") {
480
+ throw new Error(
481
+ `BCP Container: scope "${this.name}" is ${this.state}.`
482
+ );
483
+ }
484
+ }
485
+ };
486
+ function validateProvider(provider) {
487
+ if (!provider || typeof provider !== "object") {
488
+ throw new TypeError(
489
+ "BCP Container: provider must be an object."
490
+ );
491
+ }
492
+ assertToken(provider.token);
493
+ if (typeof provider.factory !== "function") {
494
+ throw new TypeError(
495
+ `BCP Container: provider "${provider.token.description}" must define factory().`
496
+ );
497
+ }
498
+ normalizeLifetime(
499
+ provider.lifetime
500
+ );
501
+ assertDependencies(
502
+ provider.dependencies ?? []
503
+ );
504
+ if (provider.dispose !== void 0 && typeof provider.dispose !== "function") {
505
+ throw new TypeError(
506
+ `BCP Container: provider "${provider.token.description}" dispose must be a function.`
507
+ );
508
+ }
509
+ }
510
+ function assertToken(token) {
511
+ if (!token || typeof token !== "object" || typeof token.id !== "symbol" || typeof token.description !== "string" || token.description.trim() === "") {
512
+ throw new TypeError(
513
+ "BCP Container: invalid service token. Use createServiceToken()."
514
+ );
515
+ }
516
+ }
517
+ function assertDependencies(dependencies) {
518
+ if (!Array.isArray(dependencies)) {
519
+ throw new TypeError(
520
+ "BCP Container: provider dependencies must be an array."
521
+ );
522
+ }
523
+ for (const dependency of dependencies) {
524
+ assertToken(dependency);
525
+ }
526
+ }
527
+ function normalizeLifetime(lifetime) {
528
+ const normalized = lifetime ?? "singleton";
529
+ if (normalized !== "singleton" && normalized !== "scoped" && normalized !== "transient") {
530
+ throw new TypeError(
531
+ "BCP Container: service lifetime must be singleton, scoped or transient."
532
+ );
533
+ }
534
+ return normalized;
535
+ }
536
+ function normalizeName(value, label) {
537
+ if (typeof value !== "string") {
538
+ throw new TypeError(
539
+ `BCP Container: ${label} must be a string.`
540
+ );
541
+ }
542
+ const normalized = value.trim();
543
+ if (!normalized) {
544
+ throw new TypeError(
545
+ `BCP Container: ${label} cannot be empty.`
546
+ );
547
+ }
548
+ if (normalized.length > 256 || /[\r\n]/.test(normalized)) {
549
+ throw new TypeError(
550
+ `BCP Container: ${label} must be at most 256 characters without line breaks.`
551
+ );
552
+ }
553
+ return normalized;
554
+ }
555
+ function normalizeOptionalName(value) {
556
+ if (value === void 0) {
557
+ return void 0;
558
+ }
559
+ return normalizeName(
560
+ value,
561
+ "scope name"
562
+ );
563
+ }
564
+ export {
565
+ ServiceDisposalError,
566
+ ServiceNotFoundError,
567
+ ServiceResolutionError,
568
+ createServiceContainer,
569
+ createServiceToken,
570
+ provideClass,
571
+ provideFactory,
572
+ provideValue
573
+ };
@@ -0,0 +1,24 @@
1
+ export {
2
+ createServiceContainer,
3
+ createServiceToken,
4
+ provideClass,
5
+ provideFactory,
6
+ provideValue,
7
+ ServiceDisposalError,
8
+ ServiceNotFoundError,
9
+ ServiceResolutionError,
10
+
11
+ type ServiceContainer,
12
+ type ServiceContainerOptions,
13
+ type ServiceDependencyValues,
14
+ type ServiceFactoryContext,
15
+ type ServiceGraphNode,
16
+ type ServiceLifetime,
17
+ type ServiceProvider,
18
+ type ServiceProviderOptions,
19
+ type ServiceScope,
20
+ type ServiceScopeOptions,
21
+ type ServiceScopeState,
22
+ type ServiceToken,
23
+ type ServiceTokenValue,
24
+ } from "../../server/src/container.js";