@zudojs/container 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/dist/containerCore/containerCore.core.js +5 -0
- package/dist/containerCore/containerCore.scope.d.ts +7 -0
- package/dist/containerCore/containerCore.scope.js +13 -0
- package/dist/containerResolution/containerResolution.core.d.ts +10 -1
- package/dist/containerResolution/containerResolution.core.js +27 -2
- package/dist/containerResolution/containerResolution.dependents.d.ts +33 -0
- package/dist/containerResolution/containerResolution.dependents.js +66 -0
- package/dist/containerResolution/containerResolution.type.d.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Token-based dependency injection container for managing application dependencies and service lifetimes.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-container](https://zudojs.oyinlola.site/docs/packages-container) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-container.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -71,7 +77,10 @@ not capture a shorter-lived dependency.
|
|
|
71
77
|
|
|
72
78
|
`registerValue()` always registers as `SINGLETON`, overriding any
|
|
73
79
|
`options.scope` you pass — a value provider can only ever return the one
|
|
74
|
-
instance you gave it.
|
|
80
|
+
instance you gave it. The container did not create that value, so it
|
|
81
|
+
never disposes it: a pool or logger shared between containers, or owned
|
|
82
|
+
by the host, stays open when a container is disposed. Register a factory
|
|
83
|
+
instead when the container should own the instance.
|
|
75
84
|
|
|
76
85
|
A `useExisting` alias never becomes a second owner of its target's instance:
|
|
77
86
|
a `SINGLETON` or `SCOPED` target is disposed exactly once by whoever created
|
|
@@ -148,7 +157,12 @@ intentionally swap a registration.
|
|
|
148
157
|
|
|
149
158
|
Replacing or removing a registration **evicts and disposes** its cached
|
|
150
159
|
singleton — and evicts any cached `useExisting` alias of it — so the next
|
|
151
|
-
`resolve()` of either token uses the new provider.
|
|
160
|
+
`resolve()` of either token uses the new provider. Eviction cascades: every
|
|
161
|
+
cached singleton that was built on the replaced token (directly or through
|
|
162
|
+
a `TRANSIENT` in between) is evicted and disposed too, so consumers are
|
|
163
|
+
rebuilt against the new instance instead of keeping the old, disposed one.
|
|
164
|
+
Live scopes drop (and dispose) their cached `SCOPED` copies of the replaced
|
|
165
|
+
token and of its consumers.
|
|
152
166
|
|
|
153
167
|
`DuplicateRegistrationError`, `CircularDependencyError`,
|
|
154
168
|
`RegistrationNotFoundError` and `ProviderResolutionError` are defined in
|
|
@@ -208,6 +222,7 @@ await container.dispose();
|
|
|
208
222
|
have a `dispose()` method or implement `Symbol.dispose` /
|
|
209
223
|
`Symbol.asyncDispose`.
|
|
210
224
|
- `TRANSIENT` instances are never tracked; dispose them yourself.
|
|
225
|
+
- `registerValue()` values are never disposed by the container.
|
|
211
226
|
- With `autoDispose: false`, `dispose()` releases tracked references
|
|
212
227
|
**without disposing them** — disposal becomes your responsibility.
|
|
213
228
|
- Disposal is terminal: the container is marked disposed even if some
|
|
@@ -44,6 +44,11 @@ export class Container {
|
|
|
44
44
|
void this.#lifecycle.disposeInstance(token).catch(() => {
|
|
45
45
|
/* see clearSingletons()/dispose() for error-surfacing disposal */
|
|
46
46
|
});
|
|
47
|
+
}, (token) => {
|
|
48
|
+
// Live scopes would otherwise keep serving a replaced SCOPED
|
|
49
|
+
// instance, or one built on a replaced dependency.
|
|
50
|
+
for (const scope of this.#liveScopes)
|
|
51
|
+
scope.evictCached(token);
|
|
47
52
|
});
|
|
48
53
|
}
|
|
49
54
|
/**
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* ancestor (scope or container) is disposed.
|
|
18
18
|
*/
|
|
19
19
|
import type { RegistrationToken, ResolvedTokens } from "../containerRegistration/containerRegistration.core.js";
|
|
20
|
+
import type { Token } from "../containerToken/containerToken.type.js";
|
|
20
21
|
import type { ContainerScopeOptions, ContainerLike } from "./containerCore.type.js";
|
|
21
22
|
export declare class ContainerScopeContext {
|
|
22
23
|
private disposed;
|
|
@@ -89,6 +90,12 @@ export declare class ContainerScopeContext {
|
|
|
89
90
|
* Returns the container that owns the whole scope tree.
|
|
90
91
|
*/
|
|
91
92
|
getContainer(): ContainerLike;
|
|
93
|
+
/**
|
|
94
|
+
* @internal Drops a cached SCOPED instance whose registration (or a
|
|
95
|
+
* dependency of it) was replaced or removed, disposing it best-effort,
|
|
96
|
+
* in this scope and every child scope.
|
|
97
|
+
*/
|
|
98
|
+
evictCached(token: Token): void;
|
|
92
99
|
/** @internal Detaches a disposed child scope. */
|
|
93
100
|
releaseChild(scope: ContainerScopeContext): void;
|
|
94
101
|
/**
|
|
@@ -156,6 +156,19 @@ export class ContainerScopeContext {
|
|
|
156
156
|
getContainer() {
|
|
157
157
|
return this.container;
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* @internal Drops a cached SCOPED instance whose registration (or a
|
|
161
|
+
* dependency of it) was replaced or removed, disposing it best-effort,
|
|
162
|
+
* in this scope and every child scope.
|
|
163
|
+
*/
|
|
164
|
+
evictCached(token) {
|
|
165
|
+
if (this.disposed)
|
|
166
|
+
return;
|
|
167
|
+
this.cache.delete?.(token);
|
|
168
|
+
void this.lifecycle.disposeInstance(token).catch(() => undefined);
|
|
169
|
+
for (const child of this.children)
|
|
170
|
+
child.evictCached(token);
|
|
171
|
+
}
|
|
159
172
|
/** @internal Detaches a disposed child scope. */
|
|
160
173
|
releaseChild(scope) {
|
|
161
174
|
this.children.delete(scope);
|
|
@@ -32,7 +32,16 @@ export declare class ContainerResolver {
|
|
|
32
32
|
private readonly registry;
|
|
33
33
|
private readonly singletonCache;
|
|
34
34
|
private readonly onSingletonEvicted;
|
|
35
|
-
|
|
35
|
+
private readonly onTokenInvalidated;
|
|
36
|
+
private readonly dependents;
|
|
37
|
+
/**
|
|
38
|
+
* @param onSingletonEvicted Called for each evicted cached singleton so
|
|
39
|
+
* the owner can dispose it.
|
|
40
|
+
* @param onTokenInvalidated Called for every token invalidated by a
|
|
41
|
+
* `replace()`/`remove()` (the token itself and each cached consumer),
|
|
42
|
+
* so owners of scope caches can drop and dispose their SCOPED copies.
|
|
43
|
+
*/
|
|
44
|
+
constructor(registry: ContainerRegistry, onSingletonEvicted?: (token: Token<unknown>) => void, onTokenInvalidated?: (token: Token<unknown>) => void);
|
|
36
45
|
resolve<T>(token: RegistrationToken<T>, options?: ResolutionOptions): T;
|
|
37
46
|
resolveDetailed<T>(token: RegistrationToken<T>, options?: ResolutionOptions): ResolutionResult<T>;
|
|
38
47
|
private resolveInternal;
|
|
@@ -32,6 +32,7 @@ import { unwrapToken } from "../containerToken/containerToken.type.js";
|
|
|
32
32
|
import { CircularDependencyError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
|
|
33
33
|
import { AsyncProviderError, CaptiveDependencyError, DependencyResolutionError, MaxResolutionDepthError, ScopedResolutionError, } from "./containerResolution.error.js";
|
|
34
34
|
import { describeToken } from "../containerToken/containerToken.type.js";
|
|
35
|
+
import { DependentIndex } from "./containerResolution.dependents.js";
|
|
35
36
|
/**
|
|
36
37
|
* Scope cache that falls back to its parent scope's cache for lookups while
|
|
37
38
|
* writing only to its own map. Nested scopes therefore see SCOPED instances
|
|
@@ -55,6 +56,9 @@ class ChainedResolutionCache {
|
|
|
55
56
|
set(token, value) {
|
|
56
57
|
this.#own.set(token, value);
|
|
57
58
|
}
|
|
59
|
+
delete(token) {
|
|
60
|
+
return this.#own.delete(token);
|
|
61
|
+
}
|
|
58
62
|
clear() {
|
|
59
63
|
this.#own.clear();
|
|
60
64
|
}
|
|
@@ -63,9 +67,19 @@ export class ContainerResolver {
|
|
|
63
67
|
registry;
|
|
64
68
|
singletonCache = new Map();
|
|
65
69
|
onSingletonEvicted;
|
|
66
|
-
|
|
70
|
+
onTokenInvalidated;
|
|
71
|
+
dependents = new DependentIndex();
|
|
72
|
+
/**
|
|
73
|
+
* @param onSingletonEvicted Called for each evicted cached singleton so
|
|
74
|
+
* the owner can dispose it.
|
|
75
|
+
* @param onTokenInvalidated Called for every token invalidated by a
|
|
76
|
+
* `replace()`/`remove()` (the token itself and each cached consumer),
|
|
77
|
+
* so owners of scope caches can drop and dispose their SCOPED copies.
|
|
78
|
+
*/
|
|
79
|
+
constructor(registry, onSingletonEvicted, onTokenInvalidated) {
|
|
67
80
|
this.registry = registry;
|
|
68
81
|
this.onSingletonEvicted = onSingletonEvicted;
|
|
82
|
+
this.onTokenInvalidated = onTokenInvalidated;
|
|
69
83
|
registry.subscribe((event) => this.handleRegistryChange(event));
|
|
70
84
|
}
|
|
71
85
|
resolve(token, options = {}) {
|
|
@@ -108,6 +122,7 @@ export class ContainerResolver {
|
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
const currentPath = [...state.path, token];
|
|
125
|
+
this.dependents.record(token, state.path, (t) => this.registry.get(t)?.scope);
|
|
111
126
|
if (registration.scope === Scope.SINGLETON) {
|
|
112
127
|
if (this.singletonCache.has(token)) {
|
|
113
128
|
return {
|
|
@@ -173,8 +188,11 @@ export class ContainerResolver {
|
|
|
173
188
|
const provider = normalizeProvider(registration.provider);
|
|
174
189
|
const token = getRegistrationToken(registration);
|
|
175
190
|
try {
|
|
191
|
+
// A pre-built value belongs to whoever built it (often shared
|
|
192
|
+
// between containers); the container did not create it, so it
|
|
193
|
+
// must not dispose it.
|
|
176
194
|
if (isValueProvider(provider))
|
|
177
|
-
return { value: provider.useValue, owned:
|
|
195
|
+
return { value: provider.useValue, owned: false };
|
|
178
196
|
if (isExistingProvider(provider)) {
|
|
179
197
|
const target = unwrapToken(provider.useExisting);
|
|
180
198
|
if (!this.registry.has(target) &&
|
|
@@ -252,14 +270,21 @@ export class ContainerResolver {
|
|
|
252
270
|
case RegistryOperation.REPLACE:
|
|
253
271
|
case RegistryOperation.REMOVE: {
|
|
254
272
|
const token = unwrapToken(event.token);
|
|
273
|
+
// Consumers first, so each is disposed before what it consumed.
|
|
274
|
+
for (const dependent of this.dependents.take(token)) {
|
|
275
|
+
this.evictSingleton(dependent);
|
|
276
|
+
this.onTokenInvalidated?.(dependent);
|
|
277
|
+
}
|
|
255
278
|
this.evictSingleton(token);
|
|
256
279
|
this.evictAliasesOf(token);
|
|
280
|
+
this.onTokenInvalidated?.(token);
|
|
257
281
|
break;
|
|
258
282
|
}
|
|
259
283
|
case RegistryOperation.CLEAR:
|
|
260
284
|
case RegistryOperation.RESTORE: {
|
|
261
285
|
for (const t of [...this.singletonCache.keys()])
|
|
262
286
|
this.evictSingleton(t);
|
|
287
|
+
this.dependents.clear();
|
|
263
288
|
break;
|
|
264
289
|
}
|
|
265
290
|
default:
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse dependency index for cached container instances.
|
|
3
|
+
*
|
|
4
|
+
* `replace()`/`remove()` used to evict only the changed token, so a
|
|
5
|
+
* singleton that had already captured it kept serving the old — by then
|
|
6
|
+
* disposed — instance. Recording which cached instance consumed which
|
|
7
|
+
* token lets eviction cascade to every consumer.
|
|
8
|
+
*/
|
|
9
|
+
import { ContainerScope as Scope } from "../containerScope/containerScope.type.js";
|
|
10
|
+
import type { Token } from "../containerToken/containerToken.type.js";
|
|
11
|
+
/** Looks up the lifetime of a registered token. */
|
|
12
|
+
export type LifetimeLookup = (token: Token<unknown>) => Scope | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Maps a token to the cached (SINGLETON or SCOPED) instances whose
|
|
15
|
+
* construction resolved it.
|
|
16
|
+
*/
|
|
17
|
+
export declare class DependentIndex {
|
|
18
|
+
#private;
|
|
19
|
+
/**
|
|
20
|
+
* Records that the nearest cached ancestor on `path` depends on
|
|
21
|
+
* `token`. Transient ancestors are skipped: they are rebuilt on every
|
|
22
|
+
* resolution, so the cached instance above them is the real consumer.
|
|
23
|
+
*/
|
|
24
|
+
record(token: Token<unknown>, path: readonly Token<unknown>[], lifetimeOf: LifetimeLookup): void;
|
|
25
|
+
/**
|
|
26
|
+
* Returns every token that transitively depends on `token`, nearest
|
|
27
|
+
* consumers last, and forgets them.
|
|
28
|
+
*/
|
|
29
|
+
take(token: Token<unknown>): readonly Token<unknown>[];
|
|
30
|
+
/** Forgets every recorded dependency. */
|
|
31
|
+
clear(): void;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=containerResolution.dependents.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse dependency index for cached container instances.
|
|
3
|
+
*
|
|
4
|
+
* `replace()`/`remove()` used to evict only the changed token, so a
|
|
5
|
+
* singleton that had already captured it kept serving the old — by then
|
|
6
|
+
* disposed — instance. Recording which cached instance consumed which
|
|
7
|
+
* token lets eviction cascade to every consumer.
|
|
8
|
+
*/
|
|
9
|
+
import { ContainerScope as Scope } from "../containerScope/containerScope.type.js";
|
|
10
|
+
/**
|
|
11
|
+
* Maps a token to the cached (SINGLETON or SCOPED) instances whose
|
|
12
|
+
* construction resolved it.
|
|
13
|
+
*/
|
|
14
|
+
export class DependentIndex {
|
|
15
|
+
#dependents = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Records that the nearest cached ancestor on `path` depends on
|
|
18
|
+
* `token`. Transient ancestors are skipped: they are rebuilt on every
|
|
19
|
+
* resolution, so the cached instance above them is the real consumer.
|
|
20
|
+
*/
|
|
21
|
+
record(token, path, lifetimeOf) {
|
|
22
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
23
|
+
const owner = path[i];
|
|
24
|
+
const lifetime = lifetimeOf(owner);
|
|
25
|
+
if (lifetime === Scope.SINGLETON || lifetime === Scope.SCOPED) {
|
|
26
|
+
if (owner === token)
|
|
27
|
+
return;
|
|
28
|
+
let set = this.#dependents.get(token);
|
|
29
|
+
if (set === undefined) {
|
|
30
|
+
set = new Set();
|
|
31
|
+
this.#dependents.set(token, set);
|
|
32
|
+
}
|
|
33
|
+
set.add(owner);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns every token that transitively depends on `token`, nearest
|
|
40
|
+
* consumers last, and forgets them.
|
|
41
|
+
*/
|
|
42
|
+
take(token) {
|
|
43
|
+
const ordered = [];
|
|
44
|
+
const visited = new Set([token]);
|
|
45
|
+
const visit = (current) => {
|
|
46
|
+
const direct = this.#dependents.get(current);
|
|
47
|
+
this.#dependents.delete(current);
|
|
48
|
+
if (direct === undefined)
|
|
49
|
+
return;
|
|
50
|
+
for (const dependent of direct) {
|
|
51
|
+
if (visited.has(dependent))
|
|
52
|
+
continue;
|
|
53
|
+
visited.add(dependent);
|
|
54
|
+
visit(dependent);
|
|
55
|
+
ordered.push(dependent);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
visit(token);
|
|
59
|
+
return ordered;
|
|
60
|
+
}
|
|
61
|
+
/** Forgets every recorded dependency. */
|
|
62
|
+
clear() {
|
|
63
|
+
this.#dependents.clear();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=containerResolution.dependents.js.map
|
|
@@ -15,6 +15,8 @@ export interface ResolutionCache {
|
|
|
15
15
|
has(token: Token<unknown>): boolean;
|
|
16
16
|
get(token: Token<unknown>): unknown;
|
|
17
17
|
set(token: Token<unknown>, value: unknown): void;
|
|
18
|
+
/** Drops one entry from this cache (not its parents). Optional. */
|
|
19
|
+
delete?(token: Token<unknown>): boolean;
|
|
18
20
|
clear(): void;
|
|
19
21
|
}
|
|
20
22
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/container",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
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,7 +18,7 @@
|
|
|
18
18
|
"!dist/.tsbuildinfo"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@zudojs/errors": "1.0
|
|
21
|
+
"@zudojs/errors": "1.1.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"typescript": "7.0.2",
|