@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.
- package/README.md +135 -111
- package/docs/README.md +53 -78
- package/docs/api-freeze-snapshot.json +24 -2
- package/docs/api-manifest.json +29 -13
- package/docs/api-reference.md +195 -197
- package/docs/application-platform.md +141 -171
- package/docs/docs-web-manifest.json +9 -5
- package/docs/migration-0.3.md +189 -68
- package/docs/module-system-v2.md +386 -0
- package/docs/platform-contract.md +53 -37
- package/docs/platform-manifest.json +36 -4
- package/docs/releases/0.3.1.md +116 -0
- package/docs/releases/0.3.2.md +125 -0
- package/docs/service-container.md +333 -0
- package/package.json +13 -1
- package/packages/bundler/src/client-boundary.ts +2 -0
- package/packages/client/src/application.mjs +1004 -34
- package/packages/client/src/container.mjs +573 -0
- package/packages/client/src/container.ts +24 -0
- package/packages/client/src/modules.mjs +392 -0
- package/packages/client/src/modules.ts +17 -0
- package/packages/server/src/application.ts +171 -11
- package/packages/server/src/container.ts +970 -0
- package/packages/server/src/modules.ts +682 -0
|
@@ -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
|
+
}
|