@zudojs/container 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/dist/containerCore/containerCore.core.d.ts +17 -5
- package/dist/containerCore/containerCore.core.js +19 -7
- package/dist/containerCore/containerCore.scope.js +4 -3
- package/dist/containerProvider/containerProvider.core.d.ts +21 -2
- package/dist/containerProvider/containerProvider.core.js +13 -2
- package/dist/containerResolution/containerResolution.autoRegister.d.ts +27 -0
- package/dist/containerResolution/containerResolution.autoRegister.js +40 -0
- package/dist/containerResolution/containerResolution.core.d.ts +18 -3
- package/dist/containerResolution/containerResolution.core.js +44 -10
- package/dist/containerToken/containerToken.type.d.ts +8 -0
- package/dist/containerToken/containerToken.type.js +9 -3
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -47,6 +47,21 @@ container.registerFactory(API, (db) => new Api(db), [DB]); // deps via inject li
|
|
|
47
47
|
container.registerExisting("db-alias", DB); // alias to another token
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
A factory's parameters are typed from its `inject` list, in order: with
|
|
51
|
+
`DB = createToken<Db>("Db")`, `(db) => new Api(db)` receives `db: Db`, and a
|
|
52
|
+
factory whose parameters do not match the tokens is a compile error. Class
|
|
53
|
+
tokens type the same way; plain string and symbol tokens give `unknown`.
|
|
54
|
+
`factoryProvider(factory, inject)` and `provideFactory(token, factory,
|
|
55
|
+
inject)` infer the same way.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
container.registerFactory(
|
|
59
|
+
REPORT,
|
|
60
|
+
(db, clock) => new Report(db, clock), // db: Db, clock: Clock
|
|
61
|
+
[DB, Clock],
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
50
65
|
## Tokens
|
|
51
66
|
|
|
52
67
|
Tokens can be strings, symbols, classes, or `InjectionToken`s from
|
|
@@ -58,6 +73,9 @@ Tokens can be strings, symbols, classes, or `InjectionToken`s from
|
|
|
58
73
|
- `createGlobalToken(key)` uses `Symbol.for(key)`: any other call with the
|
|
59
74
|
same key anywhere in the process yields the **same** token. That is by
|
|
60
75
|
design for cross-package sharing; namespace your keys (e.g. `"myapp:db"`).
|
|
76
|
+
- Error messages and default registration names show a symbol token by its
|
|
77
|
+
description: `createToken<Db>("Database")` appears as `Database`, not
|
|
78
|
+
`Symbol(Database)` (`describeToken(token)` returns the same string).
|
|
61
79
|
|
|
62
80
|
## Lifetimes (`scope` option)
|
|
63
81
|
|
|
@@ -185,7 +203,13 @@ const container = createContainer({
|
|
|
185
203
|
registers it on the fly as `TRANSIENT`. When registrations are frozen the
|
|
186
204
|
class is instantiated _ephemerally_ without being registered. Set to
|
|
187
205
|
`false` to require explicit registration (`canResolve`/`resolveOptional`
|
|
188
|
-
respect this).
|
|
206
|
+
respect this). Only a class whose constructor declares no required
|
|
207
|
+
parameters (`Class.length === 0`) is auto-registered: auto-registration
|
|
208
|
+
has no inject list, so `resolve(NeedsDep)` for
|
|
209
|
+
`constructor(dep: Dep)` throws `RegistrationNotFoundError` naming the
|
|
210
|
+
class and telling you to register it with an `inject` list, instead of
|
|
211
|
+
building it with `dep = undefined`. Parameters with defaults do not
|
|
212
|
+
count.
|
|
189
213
|
- `detectCircularDependencies` — circular chains throw
|
|
190
214
|
`CircularDependencyError` with the full chain. When disabled,
|
|
191
215
|
`maxResolutionDepth` still stops runaway recursion.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - TRANSIENT instances are never cached and never tracked — callers own
|
|
13
13
|
* their disposal.
|
|
14
14
|
*/
|
|
15
|
-
import type { ContainerProvider, ProviderToken } from "../containerProvider/containerProvider.core.js";
|
|
15
|
+
import type { ContainerProvider, InjectedFactory, ProviderToken } from "../containerProvider/containerProvider.core.js";
|
|
16
16
|
import type { ContainerRegistration, CreateRegistrationOptions, RegistrationToken, ResolvedTokens } from "../containerRegistration/containerRegistration.core.js";
|
|
17
17
|
import type { ResolutionCache, ResolutionResult } from "../containerResolution/containerResolution.type.js";
|
|
18
18
|
import type { ContainerOptions, ResolvedContainerOptions } from "../containerOptions/containerOptions.type.js";
|
|
@@ -51,7 +51,14 @@ export declare class Container implements ContainerLike {
|
|
|
51
51
|
* any other lifetime would be misleading.
|
|
52
52
|
*/
|
|
53
53
|
registerValue<T>(token: RegistrationToken<T>, value: T, options?: CreateRegistrationOptions): ContainerRegistration<T>;
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Registers a factory. Its parameters are typed from the `inject` tokens,
|
|
56
|
+
* in order: `registerFactory(API, (db) => new Api(db), [DB])` types `db`
|
|
57
|
+
* as `Db` when `DB` is a `createToken<Db>()` or class token, and rejects a
|
|
58
|
+
* factory whose parameters do not match. String/symbol tokens give
|
|
59
|
+
* `unknown`.
|
|
60
|
+
*/
|
|
61
|
+
registerFactory<T, const Deps extends readonly ProviderToken[] = readonly []>(token: RegistrationToken<T>, factory: InjectedFactory<T, Deps>, inject?: Deps, options?: CreateRegistrationOptions): ContainerRegistration<T>;
|
|
55
62
|
registerExisting<T>(token: RegistrationToken<T>, existing: ProviderToken<T>, options?: CreateRegistrationOptions): ContainerRegistration<T>;
|
|
56
63
|
/**
|
|
57
64
|
* Resolves a dependency at the container root.
|
|
@@ -77,7 +84,10 @@ export declare class Container implements ContainerLike {
|
|
|
77
84
|
getRegistration<T>(token: RegistrationToken<T>): ContainerRegistration<T> | undefined;
|
|
78
85
|
replace<T>(token: RegistrationToken<T>, provider: ContainerProvider<T>, options?: CreateRegistrationOptions): ContainerRegistration<T>;
|
|
79
86
|
remove<T>(token: RegistrationToken<T>): boolean;
|
|
80
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Removes every registration, evicting and disposing cached instances:
|
|
89
|
+
* container-owned singletons and every live scope's SCOPED copies alike.
|
|
90
|
+
*/
|
|
81
91
|
clearRegistrations(): void;
|
|
82
92
|
createScope(options?: ContainerScopeOptions): ContainerScopeContext;
|
|
83
93
|
getRegistrations(): readonly ContainerRegistration[];
|
|
@@ -91,8 +101,10 @@ export declare class Container implements ContainerLike {
|
|
|
91
101
|
snapshot(): readonly ContainerRegistration[];
|
|
92
102
|
/**
|
|
93
103
|
* Wholesale-replaces the registration set with a previous snapshot.
|
|
94
|
-
* Entries are validated, cached
|
|
95
|
-
*
|
|
104
|
+
* Entries are validated, cached instances built from the old set are
|
|
105
|
+
* evicted and disposed — container-owned singletons and every live
|
|
106
|
+
* scope's SCOPED copies alike — and the operation is refused when
|
|
107
|
+
* registrations are frozen.
|
|
96
108
|
*/
|
|
97
109
|
restoreSnapshot(registrations: readonly ContainerRegistration[]): void;
|
|
98
110
|
isStarted(): boolean;
|
|
@@ -19,7 +19,7 @@ import { ContainerResolver } from "../containerResolution/containerResolution.co
|
|
|
19
19
|
import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecycle/containerLifecycle.core.js";
|
|
20
20
|
import { resolveContainerOptions } from "../containerOptions/containerOptions.type.js";
|
|
21
21
|
import { unwrapToken } from "../containerToken/containerToken.type.js";
|
|
22
|
-
import { RegistrationNotFoundError } from "@zudojs/errors";
|
|
22
|
+
import { ContainerError, ContainerLifecycleError, RegistrationNotFoundError, } from "@zudojs/errors";
|
|
23
23
|
import { ContainerScopeContext } from "./containerCore.scope.js";
|
|
24
24
|
export class Container {
|
|
25
25
|
name;
|
|
@@ -87,6 +87,13 @@ export class Container {
|
|
|
87
87
|
scope: Scope.SINGLETON,
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Registers a factory. Its parameters are typed from the `inject` tokens,
|
|
92
|
+
* in order: `registerFactory(API, (db) => new Api(db), [DB])` types `db`
|
|
93
|
+
* as `Db` when `DB` is a `createToken<Db>()` or class token, and rejects a
|
|
94
|
+
* factory whose parameters do not match. String/symbol tokens give
|
|
95
|
+
* `unknown`.
|
|
96
|
+
*/
|
|
90
97
|
registerFactory(token, factory, inject = [], options = {}) {
|
|
91
98
|
return this.register(token, factoryProvider(factory, inject), options);
|
|
92
99
|
}
|
|
@@ -147,7 +154,10 @@ export class Container {
|
|
|
147
154
|
this.ensureMutable();
|
|
148
155
|
return this.#registry.remove(token);
|
|
149
156
|
}
|
|
150
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* Removes every registration, evicting and disposing cached instances:
|
|
159
|
+
* container-owned singletons and every live scope's SCOPED copies alike.
|
|
160
|
+
*/
|
|
151
161
|
clearRegistrations() {
|
|
152
162
|
this.ensureMutable();
|
|
153
163
|
this.#registry.clear();
|
|
@@ -155,7 +165,7 @@ export class Container {
|
|
|
155
165
|
createScope(options = {}) {
|
|
156
166
|
this.ensureActive();
|
|
157
167
|
if (!this.options.allowScopes)
|
|
158
|
-
throw new
|
|
168
|
+
throw new ContainerError(`Container scopes are disabled for "${this.name}".`);
|
|
159
169
|
const scope = new ContainerScopeContext(this, options);
|
|
160
170
|
this.#liveScopes.add(scope);
|
|
161
171
|
return scope;
|
|
@@ -182,8 +192,10 @@ export class Container {
|
|
|
182
192
|
}
|
|
183
193
|
/**
|
|
184
194
|
* Wholesale-replaces the registration set with a previous snapshot.
|
|
185
|
-
* Entries are validated, cached
|
|
186
|
-
*
|
|
195
|
+
* Entries are validated, cached instances built from the old set are
|
|
196
|
+
* evicted and disposed — container-owned singletons and every live
|
|
197
|
+
* scope's SCOPED copies alike — and the operation is refused when
|
|
198
|
+
* registrations are frozen.
|
|
187
199
|
*/
|
|
188
200
|
restoreSnapshot(registrations) {
|
|
189
201
|
this.ensureMutable();
|
|
@@ -318,14 +330,14 @@ export class Container {
|
|
|
318
330
|
}
|
|
319
331
|
ensureNotDisposed() {
|
|
320
332
|
if (this.#disposed)
|
|
321
|
-
throw new
|
|
333
|
+
throw new ContainerLifecycleError("dispose", `Container "${this.name}" has already been disposed.`);
|
|
322
334
|
}
|
|
323
335
|
ensureMutable() {
|
|
324
336
|
this.ensureNotDisposed();
|
|
325
337
|
if (!this.options.freezeRegistrations)
|
|
326
338
|
return;
|
|
327
339
|
if (this.#started)
|
|
328
|
-
throw new
|
|
340
|
+
throw new ContainerError(`Registrations for container "${this.name}" are frozen.`);
|
|
329
341
|
}
|
|
330
342
|
}
|
|
331
343
|
export function createContainer(options = {}) {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecycle/containerLifecycle.core.js";
|
|
20
20
|
import { ContainerScope } from "../containerScope/containerScope.type.js";
|
|
21
|
+
import { ContainerLifecycleError } from "@zudojs/errors";
|
|
21
22
|
export class ContainerScopeContext {
|
|
22
23
|
disposed = false;
|
|
23
24
|
disposing;
|
|
@@ -189,13 +190,13 @@ export class ContainerScopeContext {
|
|
|
189
190
|
*/
|
|
190
191
|
ensureActive() {
|
|
191
192
|
if (this.disposed) {
|
|
192
|
-
throw new
|
|
193
|
+
throw new ContainerLifecycleError("dispose", `Container scope "${this.name}" has already been disposed.`);
|
|
193
194
|
}
|
|
194
195
|
if (this.parentScope?.isDisposed()) {
|
|
195
|
-
throw new
|
|
196
|
+
throw new ContainerLifecycleError("dispose", `Parent scope "${this.parentScope.name}" of scope "${this.name}" has been disposed.`);
|
|
196
197
|
}
|
|
197
198
|
if (this.container.isDisposed()) {
|
|
198
|
-
throw new
|
|
199
|
+
throw new ContainerLifecycleError("dispose", `Container "${this.container.name}" owning scope "${this.name}" has been disposed.`);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
}
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Constructor, InjectionToken, Token } from "../containerToken/containerToken.type.js";
|
|
6
6
|
export type ProviderToken<T = unknown> = Token<T> | InjectionToken<T>;
|
|
7
|
+
/**
|
|
8
|
+
* The parameter list a factory receives for an `inject` list: each token is
|
|
9
|
+
* mapped to the type it resolves to, so `[DB, Clock]` gives `[Db, Clock]`.
|
|
10
|
+
* Untyped string/symbol tokens map to `unknown`, and a non-tuple
|
|
11
|
+
* `readonly ProviderToken[]` gives `unknown[]`, as before.
|
|
12
|
+
*/
|
|
13
|
+
export type InjectedDependencies<Deps extends readonly ProviderToken[]> = {
|
|
14
|
+
-readonly [K in keyof Deps]: Deps[K] extends ProviderToken<infer U> ? U : never;
|
|
15
|
+
};
|
|
16
|
+
/** A factory whose parameters are typed from its `inject` list. */
|
|
17
|
+
export type InjectedFactory<T, Deps extends readonly ProviderToken[]> = (...dependencies: InjectedDependencies<Deps>) => T;
|
|
7
18
|
export interface ClassProvider<T> {
|
|
8
19
|
readonly useClass: Constructor<T>;
|
|
9
20
|
/**
|
|
@@ -55,11 +66,19 @@ export declare function hasInjectedDependencies<T = unknown>(provider: Provider<
|
|
|
55
66
|
readonly inject: readonly ProviderToken[];
|
|
56
67
|
};
|
|
57
68
|
export declare function classProvider<T>(useClass: Constructor<T>, inject?: readonly ProviderToken[]): ClassProvider<T>;
|
|
58
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Builds a factory provider. The factory's parameters are inferred from the
|
|
71
|
+
* `inject` tokens, in order.
|
|
72
|
+
*/
|
|
73
|
+
export declare function factoryProvider<T, const Deps extends readonly ProviderToken[] = readonly []>(useFactory: InjectedFactory<T, Deps>, inject?: Deps): FactoryProvider<T>;
|
|
59
74
|
export declare function valueProvider<T>(useValue: T): ValueProvider<T>;
|
|
60
75
|
export declare function existingProvider<T>(useExisting: ProviderToken<T>): ExistingProvider<T>;
|
|
61
76
|
export declare function provideClass<T>(provide: ProviderToken<T>, useClass: Constructor<T>, inject?: readonly ProviderToken[]): ClassRegistration<T>;
|
|
62
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Builds a factory registration. The factory's parameters are inferred from
|
|
79
|
+
* the `inject` tokens, in order.
|
|
80
|
+
*/
|
|
81
|
+
export declare function provideFactory<T, const Deps extends readonly ProviderToken[] = readonly []>(provide: ProviderToken<T>, useFactory: InjectedFactory<T, Deps>, inject?: Deps): FactoryRegistration<T>;
|
|
63
82
|
export declare function provideValue<T>(provide: ProviderToken<T>, useValue: T): ValueRegistration<T>;
|
|
64
83
|
export declare function provideExisting<T>(provide: ProviderToken<T>, useExisting: ProviderToken<T>): ExistingRegistration<T>;
|
|
65
84
|
export declare function getProviderToken<T>(provider: ContainerProvider<T>): ProviderToken<T> | undefined;
|
|
@@ -25,8 +25,15 @@ export function hasInjectedDependencies(provider) {
|
|
|
25
25
|
export function classProvider(useClass, inject = []) {
|
|
26
26
|
return Object.freeze({ useClass, inject: Object.freeze([...inject]) });
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Builds a factory provider. The factory's parameters are inferred from the
|
|
30
|
+
* `inject` tokens, in order.
|
|
31
|
+
*/
|
|
28
32
|
export function factoryProvider(useFactory, inject = []) {
|
|
29
|
-
return Object.freeze({
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
useFactory: useFactory,
|
|
35
|
+
inject: Object.freeze([...inject]),
|
|
36
|
+
});
|
|
30
37
|
}
|
|
31
38
|
export function valueProvider(useValue) {
|
|
32
39
|
return Object.freeze({ useValue });
|
|
@@ -41,10 +48,14 @@ export function provideClass(provide, useClass, inject = []) {
|
|
|
41
48
|
inject: Object.freeze([...inject]),
|
|
42
49
|
});
|
|
43
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Builds a factory registration. The factory's parameters are inferred from
|
|
53
|
+
* the `inject` tokens, in order.
|
|
54
|
+
*/
|
|
44
55
|
export function provideFactory(provide, useFactory, inject = []) {
|
|
45
56
|
return Object.freeze({
|
|
46
57
|
provide,
|
|
47
|
-
useFactory,
|
|
58
|
+
useFactory: useFactory,
|
|
48
59
|
inject: Object.freeze([...inject]),
|
|
49
60
|
});
|
|
50
61
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/container/containerResolution/containerResolution.autoRegister
|
|
3
|
+
*
|
|
4
|
+
* Guards for `autoRegisterClasses`: resolving an unregistered class builds
|
|
5
|
+
* it with no arguments, which is only correct for a constructor that
|
|
6
|
+
* declares none.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Whether an unregistered token may be auto-registered: a class whose
|
|
10
|
+
* constructor declares no required parameters (`Class.length === 0`).
|
|
11
|
+
*
|
|
12
|
+
* Parameters with a default value do not count, so
|
|
13
|
+
* `constructor(value = 7)` still qualifies.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isAutoRegistrable(token: unknown): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Throws when a class cannot be auto-registered because its constructor
|
|
18
|
+
* declares parameters. Auto-registration has no inject list, so building
|
|
19
|
+
* it would pass `undefined` for every dependency.
|
|
20
|
+
*
|
|
21
|
+
* @throws {RegistrationNotFoundError} naming the class and how to
|
|
22
|
+
* register it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function assertAutoRegistrable(token: {
|
|
25
|
+
readonly length: number;
|
|
26
|
+
}): void;
|
|
27
|
+
//# sourceMappingURL=containerResolution.autoRegister.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/container/containerResolution/containerResolution.autoRegister
|
|
3
|
+
*
|
|
4
|
+
* Guards for `autoRegisterClasses`: resolving an unregistered class builds
|
|
5
|
+
* it with no arguments, which is only correct for a constructor that
|
|
6
|
+
* declares none.
|
|
7
|
+
*/
|
|
8
|
+
import { RegistrationNotFoundError } from "@zudojs/errors";
|
|
9
|
+
import { describeToken } from "../containerToken/containerToken.type.js";
|
|
10
|
+
/**
|
|
11
|
+
* Whether an unregistered token may be auto-registered: a class whose
|
|
12
|
+
* constructor declares no required parameters (`Class.length === 0`).
|
|
13
|
+
*
|
|
14
|
+
* Parameters with a default value do not count, so
|
|
15
|
+
* `constructor(value = 7)` still qualifies.
|
|
16
|
+
*/
|
|
17
|
+
export function isAutoRegistrable(token) {
|
|
18
|
+
return typeof token === "function" && token.length === 0;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Throws when a class cannot be auto-registered because its constructor
|
|
22
|
+
* declares parameters. Auto-registration has no inject list, so building
|
|
23
|
+
* it would pass `undefined` for every dependency.
|
|
24
|
+
*
|
|
25
|
+
* @throws {RegistrationNotFoundError} naming the class and how to
|
|
26
|
+
* register it.
|
|
27
|
+
*/
|
|
28
|
+
export function assertAutoRegistrable(token) {
|
|
29
|
+
if (isAutoRegistrable(token))
|
|
30
|
+
return;
|
|
31
|
+
const name = describeToken(token);
|
|
32
|
+
const count = token.length;
|
|
33
|
+
throw new RegistrationNotFoundError(name, `Cannot auto-register class "${name}": its constructor declares ` +
|
|
34
|
+
`${count} parameter${count === 1 ? "" : "s"} and no inject list is ` +
|
|
35
|
+
`registered, so it would be built with undefined dependencies. ` +
|
|
36
|
+
`Register it with container.registerClass(${name}, ${name}, ` +
|
|
37
|
+
`{ inject: [/* one token per parameter */] }) or ` +
|
|
38
|
+
`container.registerFactory(${name}, (...deps) => new ${name}(...deps), [/* tokens */]).`);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=containerResolution.autoRegister.js.map
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* The resolver subscribes to registry change events: REPLACE/REMOVE evict the
|
|
23
23
|
* affected token's cached singleton, CLEAR/RESTORE evict all cached
|
|
24
24
|
* singletons. Each eviction is reported through the `onSingletonEvicted`
|
|
25
|
-
* callback so the owning container can dispose the instance
|
|
25
|
+
* callback so the owning container can dispose the instance, and every
|
|
26
|
+
* invalidated token — for CLEAR/RESTORE that is every cached token, SCOPED
|
|
27
|
+
* ones included — through `onTokenInvalidated` so live scopes drop and
|
|
28
|
+
* dispose their own copies.
|
|
26
29
|
*/
|
|
27
30
|
import type { RegistrationToken } from "../containerRegistration/containerRegistration.core.js";
|
|
28
31
|
import type { ContainerRegistry } from "../containerRegistry/containerRegistry.core.js";
|
|
@@ -34,12 +37,24 @@ export declare class ContainerResolver {
|
|
|
34
37
|
private readonly onSingletonEvicted;
|
|
35
38
|
private readonly onTokenInvalidated;
|
|
36
39
|
private readonly dependents;
|
|
40
|
+
/**
|
|
41
|
+
* Tokens for which a SCOPED instance has ever been cached in a scope.
|
|
42
|
+
*
|
|
43
|
+
* The singleton cache only knows about SINGLETON tokens and the
|
|
44
|
+
* dependent index only records tokens consumed by another cached
|
|
45
|
+
* instance, so neither can name a SCOPED token that a scope resolved
|
|
46
|
+
* directly. A wholesale CLEAR/RESTORE must still tell live scopes to
|
|
47
|
+
* drop those instances.
|
|
48
|
+
*/
|
|
49
|
+
private readonly scopedTokens;
|
|
37
50
|
/**
|
|
38
51
|
* @param onSingletonEvicted Called for each evicted cached singleton so
|
|
39
52
|
* the owner can dispose it.
|
|
40
53
|
* @param onTokenInvalidated Called for every token invalidated by a
|
|
41
|
-
* `replace()`/`remove()`
|
|
42
|
-
*
|
|
54
|
+
* registry change — for `replace()`/`remove()` the token itself and
|
|
55
|
+
* each cached consumer, for `clear()`/`restore()` every token that
|
|
56
|
+
* was cached at all — so owners of scope caches can drop and dispose
|
|
57
|
+
* their SCOPED copies.
|
|
43
58
|
*/
|
|
44
59
|
constructor(registry: ContainerRegistry, onSingletonEvicted?: (token: Token<unknown>) => void, onTokenInvalidated?: (token: Token<unknown>) => void);
|
|
45
60
|
resolve<T>(token: RegistrationToken<T>, options?: ResolutionOptions): T;
|
|
@@ -22,16 +22,20 @@
|
|
|
22
22
|
* The resolver subscribes to registry change events: REPLACE/REMOVE evict the
|
|
23
23
|
* affected token's cached singleton, CLEAR/RESTORE evict all cached
|
|
24
24
|
* singletons. Each eviction is reported through the `onSingletonEvicted`
|
|
25
|
-
* callback so the owning container can dispose the instance
|
|
25
|
+
* callback so the owning container can dispose the instance, and every
|
|
26
|
+
* invalidated token — for CLEAR/RESTORE that is every cached token, SCOPED
|
|
27
|
+
* ones included — through `onTokenInvalidated` so live scopes drop and
|
|
28
|
+
* dispose their own copies.
|
|
26
29
|
*/
|
|
27
30
|
import { isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, normalizeProvider, } from "../containerProvider/containerProvider.core.js";
|
|
28
31
|
import { ContainerScope as Scope } from "../containerScope/containerScope.type.js";
|
|
29
32
|
import { defineRegistration, getRegistrationToken, } from "../containerRegistration/containerRegistration.core.js";
|
|
30
33
|
import { RegistryOperation } from "../containerRegistry/containerRegistry.type.js";
|
|
31
34
|
import { unwrapToken } from "../containerToken/containerToken.type.js";
|
|
32
|
-
import { CircularDependencyError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
|
|
35
|
+
import { CircularDependencyError, ContainerError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
|
|
33
36
|
import { AsyncProviderError, CaptiveDependencyError, DependencyResolutionError, MaxResolutionDepthError, ScopedResolutionError, } from "./containerResolution.error.js";
|
|
34
37
|
import { describeToken } from "../containerToken/containerToken.type.js";
|
|
38
|
+
import { assertAutoRegistrable, isAutoRegistrable, } from "./containerResolution.autoRegister.js";
|
|
35
39
|
import { DependentIndex } from "./containerResolution.dependents.js";
|
|
36
40
|
/**
|
|
37
41
|
* Scope cache that falls back to its parent scope's cache for lookups while
|
|
@@ -69,12 +73,24 @@ export class ContainerResolver {
|
|
|
69
73
|
onSingletonEvicted;
|
|
70
74
|
onTokenInvalidated;
|
|
71
75
|
dependents = new DependentIndex();
|
|
76
|
+
/**
|
|
77
|
+
* Tokens for which a SCOPED instance has ever been cached in a scope.
|
|
78
|
+
*
|
|
79
|
+
* The singleton cache only knows about SINGLETON tokens and the
|
|
80
|
+
* dependent index only records tokens consumed by another cached
|
|
81
|
+
* instance, so neither can name a SCOPED token that a scope resolved
|
|
82
|
+
* directly. A wholesale CLEAR/RESTORE must still tell live scopes to
|
|
83
|
+
* drop those instances.
|
|
84
|
+
*/
|
|
85
|
+
scopedTokens = new Set();
|
|
72
86
|
/**
|
|
73
87
|
* @param onSingletonEvicted Called for each evicted cached singleton so
|
|
74
88
|
* the owner can dispose it.
|
|
75
89
|
* @param onTokenInvalidated Called for every token invalidated by a
|
|
76
|
-
* `replace()`/`remove()`
|
|
77
|
-
*
|
|
90
|
+
* registry change — for `replace()`/`remove()` the token itself and
|
|
91
|
+
* each cached consumer, for `clear()`/`restore()` every token that
|
|
92
|
+
* was cached at all — so owners of scope caches can drop and dispose
|
|
93
|
+
* their SCOPED copies.
|
|
78
94
|
*/
|
|
79
95
|
constructor(registry, onSingletonEvicted, onTokenInvalidated) {
|
|
80
96
|
this.registry = registry;
|
|
@@ -109,6 +125,7 @@ export class ContainerResolver {
|
|
|
109
125
|
let registration = this.registry.get(token);
|
|
110
126
|
if (!registration) {
|
|
111
127
|
if (state.autoRegisterClasses && typeof token === "function") {
|
|
128
|
+
assertAutoRegistrable(token);
|
|
112
129
|
if (state.allowRegistration) {
|
|
113
130
|
registration = this.registry.register(token, { useClass: token }, { scope: Scope.TRANSIENT });
|
|
114
131
|
}
|
|
@@ -165,8 +182,11 @@ export class ContainerResolver {
|
|
|
165
182
|
}
|
|
166
183
|
if (registration.scope === Scope.SINGLETON)
|
|
167
184
|
this.singletonCache.set(token, value);
|
|
168
|
-
else if (registration.scope === Scope.SCOPED)
|
|
185
|
+
else if (registration.scope === Scope.SCOPED) {
|
|
169
186
|
state.scopeCache?.set(token, value);
|
|
187
|
+
if (state.scopeCache)
|
|
188
|
+
this.scopedTokens.add(token);
|
|
189
|
+
}
|
|
170
190
|
const result = {
|
|
171
191
|
value,
|
|
172
192
|
token,
|
|
@@ -196,9 +216,9 @@ export class ContainerResolver {
|
|
|
196
216
|
if (isExistingProvider(provider)) {
|
|
197
217
|
const target = unwrapToken(provider.useExisting);
|
|
198
218
|
if (!this.registry.has(target) &&
|
|
199
|
-
!(state.autoRegisterClasses &&
|
|
200
|
-
throw new
|
|
201
|
-
`"${describeToken(token)}" is not registered
|
|
219
|
+
!(state.autoRegisterClasses && isAutoRegistrable(target))) {
|
|
220
|
+
throw new ContainerError(`useExisting target "${describeToken(target)}" for token ` +
|
|
221
|
+
`"${describeToken(token)}" is not registered.`, { token: describeToken(token) });
|
|
202
222
|
}
|
|
203
223
|
const resolved = this.resolveInternal(target, state, singletonAncestor);
|
|
204
224
|
// Only a TRANSIENT target has no owner of its own; a cached alias
|
|
@@ -224,7 +244,9 @@ export class ContainerResolver {
|
|
|
224
244
|
const ctor = provider.useClass;
|
|
225
245
|
return { value: new ctor(...args), owned: true };
|
|
226
246
|
}
|
|
227
|
-
throw new
|
|
247
|
+
throw new ContainerError("Unsupported container provider.", {
|
|
248
|
+
token: describeToken(token),
|
|
249
|
+
});
|
|
228
250
|
}
|
|
229
251
|
catch (error) {
|
|
230
252
|
// Resolution errors created deeper in the chain already carry the full
|
|
@@ -263,7 +285,7 @@ export class ContainerResolver {
|
|
|
263
285
|
}
|
|
264
286
|
canResolve(token, autoRegisterClasses = true) {
|
|
265
287
|
const t = unwrapToken(token);
|
|
266
|
-
return (this.registry.has(t) || (autoRegisterClasses &&
|
|
288
|
+
return (this.registry.has(t) || (autoRegisterClasses && isAutoRegistrable(t)));
|
|
267
289
|
}
|
|
268
290
|
handleRegistryChange(event) {
|
|
269
291
|
switch (event.operation) {
|
|
@@ -282,9 +304,21 @@ export class ContainerResolver {
|
|
|
282
304
|
}
|
|
283
305
|
case RegistryOperation.CLEAR:
|
|
284
306
|
case RegistryOperation.RESTORE: {
|
|
307
|
+
// Every cached token is discarded by a wholesale change, so every
|
|
308
|
+
// one of them must be reported as invalidated — not just the
|
|
309
|
+
// singletons. Only REPLACE/REMOVE used to notify, so a live scope
|
|
310
|
+
// went on serving (and never disposed) the SCOPED instance built
|
|
311
|
+
// from a registration that clear()/restore() had thrown away.
|
|
312
|
+
const invalidated = new Set([
|
|
313
|
+
...this.singletonCache.keys(),
|
|
314
|
+
...this.scopedTokens,
|
|
315
|
+
]);
|
|
285
316
|
for (const t of [...this.singletonCache.keys()])
|
|
286
317
|
this.evictSingleton(t);
|
|
287
318
|
this.dependents.clear();
|
|
319
|
+
this.scopedTokens.clear();
|
|
320
|
+
for (const t of invalidated)
|
|
321
|
+
this.onTokenInvalidated?.(t);
|
|
288
322
|
break;
|
|
289
323
|
}
|
|
290
324
|
default:
|
|
@@ -44,5 +44,13 @@ export declare function isInjectionToken<T = unknown>(value: unknown): value is
|
|
|
44
44
|
export declare function isConstructorToken<T = unknown>(token: Token<T>): token is Constructor<T>;
|
|
45
45
|
export declare function isSymbolToken<T = unknown>(token: Token<T>): token is symbol;
|
|
46
46
|
export declare function isStringToken<T = unknown>(token: Token<T>): token is string;
|
|
47
|
+
/**
|
|
48
|
+
* Returns a human-readable name for a token, used in error messages and as
|
|
49
|
+
* the default registration name.
|
|
50
|
+
*
|
|
51
|
+
* A symbol token (including every `createToken`/`createGlobalToken` token)
|
|
52
|
+
* is shown by its description, so errors read `MissingService` rather than
|
|
53
|
+
* `Symbol(MissingService)`. A symbol without a description is `Symbol()`.
|
|
54
|
+
*/
|
|
47
55
|
export declare function describeToken<T>(token: Token<T> | InjectionToken<T>): string;
|
|
48
56
|
//# sourceMappingURL=containerToken.type.d.ts.map
|
|
@@ -48,14 +48,20 @@ export function isSymbolToken(token) {
|
|
|
48
48
|
export function isStringToken(token) {
|
|
49
49
|
return typeof token === "string";
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns a human-readable name for a token, used in error messages and as
|
|
53
|
+
* the default registration name.
|
|
54
|
+
*
|
|
55
|
+
* A symbol token (including every `createToken`/`createGlobalToken` token)
|
|
56
|
+
* is shown by its description, so errors read `MissingService` rather than
|
|
57
|
+
* `Symbol(MissingService)`. A symbol without a description is `Symbol()`.
|
|
58
|
+
*/
|
|
51
59
|
export function describeToken(token) {
|
|
52
60
|
const resolved = unwrapToken(token);
|
|
53
61
|
if (typeof resolved === "string")
|
|
54
62
|
return resolved;
|
|
55
63
|
if (typeof resolved === "symbol")
|
|
56
|
-
return resolved.description
|
|
57
|
-
? `Symbol(${resolved.description})`
|
|
58
|
-
: "Symbol()";
|
|
64
|
+
return resolved.description ? resolved.description : "Symbol()";
|
|
59
65
|
if (typeof resolved === "function")
|
|
60
66
|
return resolved.name || "AnonymousConstructor";
|
|
61
67
|
return "UnknownToken";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/container",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Token-based dependency injection container for managing application dependencies and service lifetimes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"!dist/.tsbuildinfo"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@zudojs/errors": "1.
|
|
21
|
+
"@zudojs/errors": "1.3.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"typescript": "7.0.2",
|
|
25
|
-
"vitest": "^
|
|
25
|
+
"vitest": "^5.0.1"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"author": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dependency-injection",
|
|
42
42
|
"container"
|
|
43
43
|
],
|
|
44
|
-
"homepage": "https://
|
|
44
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-container",
|
|
45
45
|
"bugs": {
|
|
46
46
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
47
47
|
},
|