@zudojs/container 1.1.0 → 1.1.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 +17 -2
- package/dist/containerCore/containerCore.core.d.ts +8 -3
- package/dist/containerCore/containerCore.core.js +17 -7
- package/dist/containerCore/containerCore.scope.d.ts +7 -0
- package/dist/containerCore/containerCore.scope.js +17 -3
- package/dist/containerResolution/containerResolution.core.d.ts +26 -2
- package/dist/containerResolution/containerResolution.core.js +65 -8
- 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
|
|
@@ -77,7 +77,10 @@ export declare class Container implements ContainerLike {
|
|
|
77
77
|
getRegistration<T>(token: RegistrationToken<T>): ContainerRegistration<T> | undefined;
|
|
78
78
|
replace<T>(token: RegistrationToken<T>, provider: ContainerProvider<T>, options?: CreateRegistrationOptions): ContainerRegistration<T>;
|
|
79
79
|
remove<T>(token: RegistrationToken<T>): boolean;
|
|
80
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Removes every registration, evicting and disposing cached instances:
|
|
82
|
+
* container-owned singletons and every live scope's SCOPED copies alike.
|
|
83
|
+
*/
|
|
81
84
|
clearRegistrations(): void;
|
|
82
85
|
createScope(options?: ContainerScopeOptions): ContainerScopeContext;
|
|
83
86
|
getRegistrations(): readonly ContainerRegistration[];
|
|
@@ -91,8 +94,10 @@ export declare class Container implements ContainerLike {
|
|
|
91
94
|
snapshot(): readonly ContainerRegistration[];
|
|
92
95
|
/**
|
|
93
96
|
* Wholesale-replaces the registration set with a previous snapshot.
|
|
94
|
-
* Entries are validated, cached
|
|
95
|
-
*
|
|
97
|
+
* Entries are validated, cached instances built from the old set are
|
|
98
|
+
* evicted and disposed — container-owned singletons and every live
|
|
99
|
+
* scope's SCOPED copies alike — and the operation is refused when
|
|
100
|
+
* registrations are frozen.
|
|
96
101
|
*/
|
|
97
102
|
restoreSnapshot(registrations: readonly ContainerRegistration[]): void;
|
|
98
103
|
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;
|
|
@@ -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
|
/**
|
|
@@ -142,7 +147,10 @@ export class Container {
|
|
|
142
147
|
this.ensureMutable();
|
|
143
148
|
return this.#registry.remove(token);
|
|
144
149
|
}
|
|
145
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Removes every registration, evicting and disposing cached instances:
|
|
152
|
+
* container-owned singletons and every live scope's SCOPED copies alike.
|
|
153
|
+
*/
|
|
146
154
|
clearRegistrations() {
|
|
147
155
|
this.ensureMutable();
|
|
148
156
|
this.#registry.clear();
|
|
@@ -150,7 +158,7 @@ export class Container {
|
|
|
150
158
|
createScope(options = {}) {
|
|
151
159
|
this.ensureActive();
|
|
152
160
|
if (!this.options.allowScopes)
|
|
153
|
-
throw new
|
|
161
|
+
throw new ContainerError(`Container scopes are disabled for "${this.name}".`);
|
|
154
162
|
const scope = new ContainerScopeContext(this, options);
|
|
155
163
|
this.#liveScopes.add(scope);
|
|
156
164
|
return scope;
|
|
@@ -177,8 +185,10 @@ export class Container {
|
|
|
177
185
|
}
|
|
178
186
|
/**
|
|
179
187
|
* Wholesale-replaces the registration set with a previous snapshot.
|
|
180
|
-
* Entries are validated, cached
|
|
181
|
-
*
|
|
188
|
+
* Entries are validated, cached instances built from the old set are
|
|
189
|
+
* evicted and disposed — container-owned singletons and every live
|
|
190
|
+
* scope's SCOPED copies alike — and the operation is refused when
|
|
191
|
+
* registrations are frozen.
|
|
182
192
|
*/
|
|
183
193
|
restoreSnapshot(registrations) {
|
|
184
194
|
this.ensureMutable();
|
|
@@ -313,14 +323,14 @@ export class Container {
|
|
|
313
323
|
}
|
|
314
324
|
ensureNotDisposed() {
|
|
315
325
|
if (this.#disposed)
|
|
316
|
-
throw new
|
|
326
|
+
throw new ContainerLifecycleError("dispose", `Container "${this.name}" has already been disposed.`);
|
|
317
327
|
}
|
|
318
328
|
ensureMutable() {
|
|
319
329
|
this.ensureNotDisposed();
|
|
320
330
|
if (!this.options.freezeRegistrations)
|
|
321
331
|
return;
|
|
322
332
|
if (this.#started)
|
|
323
|
-
throw new
|
|
333
|
+
throw new ContainerError(`Registrations for container "${this.name}" are frozen.`);
|
|
324
334
|
}
|
|
325
335
|
}
|
|
326
336
|
export function createContainer(options = {}) {
|
|
@@ -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
|
/**
|
|
@@ -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;
|
|
@@ -156,6 +157,19 @@ export class ContainerScopeContext {
|
|
|
156
157
|
getContainer() {
|
|
157
158
|
return this.container;
|
|
158
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* @internal Drops a cached SCOPED instance whose registration (or a
|
|
162
|
+
* dependency of it) was replaced or removed, disposing it best-effort,
|
|
163
|
+
* in this scope and every child scope.
|
|
164
|
+
*/
|
|
165
|
+
evictCached(token) {
|
|
166
|
+
if (this.disposed)
|
|
167
|
+
return;
|
|
168
|
+
this.cache.delete?.(token);
|
|
169
|
+
void this.lifecycle.disposeInstance(token).catch(() => undefined);
|
|
170
|
+
for (const child of this.children)
|
|
171
|
+
child.evictCached(token);
|
|
172
|
+
}
|
|
159
173
|
/** @internal Detaches a disposed child scope. */
|
|
160
174
|
releaseChild(scope) {
|
|
161
175
|
this.children.delete(scope);
|
|
@@ -176,13 +190,13 @@ export class ContainerScopeContext {
|
|
|
176
190
|
*/
|
|
177
191
|
ensureActive() {
|
|
178
192
|
if (this.disposed) {
|
|
179
|
-
throw new
|
|
193
|
+
throw new ContainerLifecycleError("dispose", `Container scope "${this.name}" has already been disposed.`);
|
|
180
194
|
}
|
|
181
195
|
if (this.parentScope?.isDisposed()) {
|
|
182
|
-
throw new
|
|
196
|
+
throw new ContainerLifecycleError("dispose", `Parent scope "${this.parentScope.name}" of scope "${this.name}" has been disposed.`);
|
|
183
197
|
}
|
|
184
198
|
if (this.container.isDisposed()) {
|
|
185
|
-
throw new
|
|
199
|
+
throw new ContainerLifecycleError("dispose", `Container "${this.container.name}" owning scope "${this.name}" has been disposed.`);
|
|
186
200
|
}
|
|
187
201
|
}
|
|
188
202
|
}
|
|
@@ -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";
|
|
@@ -32,7 +35,28 @@ export declare class ContainerResolver {
|
|
|
32
35
|
private readonly registry;
|
|
33
36
|
private readonly singletonCache;
|
|
34
37
|
private readonly onSingletonEvicted;
|
|
35
|
-
|
|
38
|
+
private readonly onTokenInvalidated;
|
|
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;
|
|
50
|
+
/**
|
|
51
|
+
* @param onSingletonEvicted Called for each evicted cached singleton so
|
|
52
|
+
* the owner can dispose it.
|
|
53
|
+
* @param onTokenInvalidated Called for every token invalidated by a
|
|
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.
|
|
58
|
+
*/
|
|
59
|
+
constructor(registry: ContainerRegistry, onSingletonEvicted?: (token: Token<unknown>) => void, onTokenInvalidated?: (token: Token<unknown>) => void);
|
|
36
60
|
resolve<T>(token: RegistrationToken<T>, options?: ResolutionOptions): T;
|
|
37
61
|
resolveDetailed<T>(token: RegistrationToken<T>, options?: ResolutionOptions): ResolutionResult<T>;
|
|
38
62
|
private resolveInternal;
|
|
@@ -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 { DependentIndex } from "./containerResolution.dependents.js";
|
|
35
39
|
/**
|
|
36
40
|
* Scope cache that falls back to its parent scope's cache for lookups while
|
|
37
41
|
* writing only to its own map. Nested scopes therefore see SCOPED instances
|
|
@@ -55,6 +59,9 @@ class ChainedResolutionCache {
|
|
|
55
59
|
set(token, value) {
|
|
56
60
|
this.#own.set(token, value);
|
|
57
61
|
}
|
|
62
|
+
delete(token) {
|
|
63
|
+
return this.#own.delete(token);
|
|
64
|
+
}
|
|
58
65
|
clear() {
|
|
59
66
|
this.#own.clear();
|
|
60
67
|
}
|
|
@@ -63,9 +70,31 @@ export class ContainerResolver {
|
|
|
63
70
|
registry;
|
|
64
71
|
singletonCache = new Map();
|
|
65
72
|
onSingletonEvicted;
|
|
66
|
-
|
|
73
|
+
onTokenInvalidated;
|
|
74
|
+
dependents = new DependentIndex();
|
|
75
|
+
/**
|
|
76
|
+
* Tokens for which a SCOPED instance has ever been cached in a scope.
|
|
77
|
+
*
|
|
78
|
+
* The singleton cache only knows about SINGLETON tokens and the
|
|
79
|
+
* dependent index only records tokens consumed by another cached
|
|
80
|
+
* instance, so neither can name a SCOPED token that a scope resolved
|
|
81
|
+
* directly. A wholesale CLEAR/RESTORE must still tell live scopes to
|
|
82
|
+
* drop those instances.
|
|
83
|
+
*/
|
|
84
|
+
scopedTokens = new Set();
|
|
85
|
+
/**
|
|
86
|
+
* @param onSingletonEvicted Called for each evicted cached singleton so
|
|
87
|
+
* the owner can dispose it.
|
|
88
|
+
* @param onTokenInvalidated Called for every token invalidated by a
|
|
89
|
+
* registry change — for `replace()`/`remove()` the token itself and
|
|
90
|
+
* each cached consumer, for `clear()`/`restore()` every token that
|
|
91
|
+
* was cached at all — so owners of scope caches can drop and dispose
|
|
92
|
+
* their SCOPED copies.
|
|
93
|
+
*/
|
|
94
|
+
constructor(registry, onSingletonEvicted, onTokenInvalidated) {
|
|
67
95
|
this.registry = registry;
|
|
68
96
|
this.onSingletonEvicted = onSingletonEvicted;
|
|
97
|
+
this.onTokenInvalidated = onTokenInvalidated;
|
|
69
98
|
registry.subscribe((event) => this.handleRegistryChange(event));
|
|
70
99
|
}
|
|
71
100
|
resolve(token, options = {}) {
|
|
@@ -108,6 +137,7 @@ export class ContainerResolver {
|
|
|
108
137
|
}
|
|
109
138
|
}
|
|
110
139
|
const currentPath = [...state.path, token];
|
|
140
|
+
this.dependents.record(token, state.path, (t) => this.registry.get(t)?.scope);
|
|
111
141
|
if (registration.scope === Scope.SINGLETON) {
|
|
112
142
|
if (this.singletonCache.has(token)) {
|
|
113
143
|
return {
|
|
@@ -150,8 +180,11 @@ export class ContainerResolver {
|
|
|
150
180
|
}
|
|
151
181
|
if (registration.scope === Scope.SINGLETON)
|
|
152
182
|
this.singletonCache.set(token, value);
|
|
153
|
-
else if (registration.scope === Scope.SCOPED)
|
|
183
|
+
else if (registration.scope === Scope.SCOPED) {
|
|
154
184
|
state.scopeCache?.set(token, value);
|
|
185
|
+
if (state.scopeCache)
|
|
186
|
+
this.scopedTokens.add(token);
|
|
187
|
+
}
|
|
155
188
|
const result = {
|
|
156
189
|
value,
|
|
157
190
|
token,
|
|
@@ -173,14 +206,17 @@ export class ContainerResolver {
|
|
|
173
206
|
const provider = normalizeProvider(registration.provider);
|
|
174
207
|
const token = getRegistrationToken(registration);
|
|
175
208
|
try {
|
|
209
|
+
// A pre-built value belongs to whoever built it (often shared
|
|
210
|
+
// between containers); the container did not create it, so it
|
|
211
|
+
// must not dispose it.
|
|
176
212
|
if (isValueProvider(provider))
|
|
177
|
-
return { value: provider.useValue, owned:
|
|
213
|
+
return { value: provider.useValue, owned: false };
|
|
178
214
|
if (isExistingProvider(provider)) {
|
|
179
215
|
const target = unwrapToken(provider.useExisting);
|
|
180
216
|
if (!this.registry.has(target) &&
|
|
181
217
|
!(state.autoRegisterClasses && typeof target === "function")) {
|
|
182
|
-
throw new
|
|
183
|
-
`"${describeToken(token)}" is not registered
|
|
218
|
+
throw new ContainerError(`useExisting target "${describeToken(target)}" for token ` +
|
|
219
|
+
`"${describeToken(token)}" is not registered.`, { token: describeToken(token) });
|
|
184
220
|
}
|
|
185
221
|
const resolved = this.resolveInternal(target, state, singletonAncestor);
|
|
186
222
|
// Only a TRANSIENT target has no owner of its own; a cached alias
|
|
@@ -206,7 +242,9 @@ export class ContainerResolver {
|
|
|
206
242
|
const ctor = provider.useClass;
|
|
207
243
|
return { value: new ctor(...args), owned: true };
|
|
208
244
|
}
|
|
209
|
-
throw new
|
|
245
|
+
throw new ContainerError("Unsupported container provider.", {
|
|
246
|
+
token: describeToken(token),
|
|
247
|
+
});
|
|
210
248
|
}
|
|
211
249
|
catch (error) {
|
|
212
250
|
// Resolution errors created deeper in the chain already carry the full
|
|
@@ -252,14 +290,33 @@ export class ContainerResolver {
|
|
|
252
290
|
case RegistryOperation.REPLACE:
|
|
253
291
|
case RegistryOperation.REMOVE: {
|
|
254
292
|
const token = unwrapToken(event.token);
|
|
293
|
+
// Consumers first, so each is disposed before what it consumed.
|
|
294
|
+
for (const dependent of this.dependents.take(token)) {
|
|
295
|
+
this.evictSingleton(dependent);
|
|
296
|
+
this.onTokenInvalidated?.(dependent);
|
|
297
|
+
}
|
|
255
298
|
this.evictSingleton(token);
|
|
256
299
|
this.evictAliasesOf(token);
|
|
300
|
+
this.onTokenInvalidated?.(token);
|
|
257
301
|
break;
|
|
258
302
|
}
|
|
259
303
|
case RegistryOperation.CLEAR:
|
|
260
304
|
case RegistryOperation.RESTORE: {
|
|
305
|
+
// Every cached token is discarded by a wholesale change, so every
|
|
306
|
+
// one of them must be reported as invalidated — not just the
|
|
307
|
+
// singletons. Only REPLACE/REMOVE used to notify, so a live scope
|
|
308
|
+
// went on serving (and never disposed) the SCOPED instance built
|
|
309
|
+
// from a registration that clear()/restore() had thrown away.
|
|
310
|
+
const invalidated = new Set([
|
|
311
|
+
...this.singletonCache.keys(),
|
|
312
|
+
...this.scopedTokens,
|
|
313
|
+
]);
|
|
261
314
|
for (const t of [...this.singletonCache.keys()])
|
|
262
315
|
this.evictSingleton(t);
|
|
316
|
+
this.dependents.clear();
|
|
317
|
+
this.scopedTokens.clear();
|
|
318
|
+
for (const t of invalidated)
|
|
319
|
+
this.onTokenInvalidated?.(t);
|
|
263
320
|
break;
|
|
264
321
|
}
|
|
265
322
|
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.2",
|
|
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.2.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"typescript": "7.0.2",
|