@zudojs/container 0.0.1 → 1.0.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 +205 -8
- package/dist/containerCore/containerCore.core.d.ts +105 -14
- package/dist/containerCore/containerCore.core.js +211 -44
- package/dist/containerCore/containerCore.scope.d.ts +63 -18
- package/dist/containerCore/containerCore.scope.js +108 -29
- package/dist/containerCore/containerCore.type.d.ts +22 -16
- package/dist/containerLifecycle/containerLifecycle.core.d.ts +59 -9
- package/dist/containerLifecycle/containerLifecycle.core.js +58 -15
- package/dist/containerOptions/containerOptions.type.d.ts +7 -17
- package/dist/containerOptions/containerOptions.type.js +0 -19
- package/dist/containerProvider/containerProvider.core.d.ts +11 -3
- package/dist/containerProvider/containerProvider.core.js +11 -5
- package/dist/containerRegistration/containerRegistration.core.d.ts +9 -3
- package/dist/containerRegistration/containerRegistration.core.js +0 -6
- package/dist/containerRegistry/containerRegistry.core.d.ts +23 -9
- package/dist/containerRegistry/containerRegistry.core.js +50 -47
- package/dist/containerRegistry/containerRegistry.type.d.ts +3 -1
- package/dist/containerRegistry/containerRegistry.type.js +2 -0
- package/dist/containerRegistry/index.d.ts +0 -1
- package/dist/containerRegistry/index.js +0 -1
- package/dist/containerResolution/containerResolution.core.d.ts +42 -10
- package/dist/containerResolution/containerResolution.core.js +207 -64
- package/dist/containerResolution/containerResolution.error.d.ts +68 -3
- package/dist/containerResolution/containerResolution.error.js +107 -3
- package/dist/containerResolution/containerResolution.type.d.ts +35 -3
- package/dist/containerResolution/index.d.ts +1 -0
- package/dist/containerResolution/index.js +1 -0
- package/dist/containerToken/containerToken.type.d.ts +26 -1
- package/dist/containerToken/containerToken.type.js +18 -0
- package/package.json +17 -8
- package/dist/.tsbuildinfo +0 -1
- package/dist/containerCore/containerCore.core.d.ts.map +0 -1
- package/dist/containerCore/containerCore.core.js.map +0 -1
- package/dist/containerCore/containerCore.scope.d.ts.map +0 -1
- package/dist/containerCore/containerCore.scope.js.map +0 -1
- package/dist/containerCore/containerCore.type.d.ts.map +0 -1
- package/dist/containerCore/containerCore.type.js.map +0 -1
- package/dist/containerCore/index.d.ts.map +0 -1
- package/dist/containerCore/index.js.map +0 -1
- package/dist/containerLifecycle/containerLifecycle.core.d.ts.map +0 -1
- package/dist/containerLifecycle/containerLifecycle.core.js.map +0 -1
- package/dist/containerLifecycle/index.d.ts.map +0 -1
- package/dist/containerLifecycle/index.js.map +0 -1
- package/dist/containerOptions/containerOptions.type.d.ts.map +0 -1
- package/dist/containerOptions/containerOptions.type.js.map +0 -1
- package/dist/containerOptions/index.d.ts.map +0 -1
- package/dist/containerOptions/index.js.map +0 -1
- package/dist/containerProvider/containerProvider.core.d.ts.map +0 -1
- package/dist/containerProvider/containerProvider.core.js.map +0 -1
- package/dist/containerProvider/index.d.ts.map +0 -1
- package/dist/containerProvider/index.js.map +0 -1
- package/dist/containerRegistration/containerRegistration.core.d.ts.map +0 -1
- package/dist/containerRegistration/containerRegistration.core.js.map +0 -1
- package/dist/containerRegistration/index.d.ts.map +0 -1
- package/dist/containerRegistration/index.js.map +0 -1
- package/dist/containerRegistry/containerRegistry.core.d.ts.map +0 -1
- package/dist/containerRegistry/containerRegistry.core.js.map +0 -1
- package/dist/containerRegistry/containerRegistry.error.d.ts +0 -12
- package/dist/containerRegistry/containerRegistry.error.d.ts.map +0 -1
- package/dist/containerRegistry/containerRegistry.error.js +0 -22
- package/dist/containerRegistry/containerRegistry.error.js.map +0 -1
- package/dist/containerRegistry/containerRegistry.type.d.ts.map +0 -1
- package/dist/containerRegistry/containerRegistry.type.js.map +0 -1
- package/dist/containerRegistry/index.d.ts.map +0 -1
- package/dist/containerRegistry/index.js.map +0 -1
- package/dist/containerResolution/containerResolution.core.d.ts.map +0 -1
- package/dist/containerResolution/containerResolution.core.js.map +0 -1
- package/dist/containerResolution/containerResolution.error.d.ts.map +0 -1
- package/dist/containerResolution/containerResolution.error.js.map +0 -1
- package/dist/containerResolution/containerResolution.type.d.ts.map +0 -1
- package/dist/containerResolution/containerResolution.type.js.map +0 -1
- package/dist/containerResolution/index.d.ts.map +0 -1
- package/dist/containerResolution/index.js.map +0 -1
- package/dist/containerScope/containerScope.type.d.ts.map +0 -1
- package/dist/containerScope/containerScope.type.js.map +0 -1
- package/dist/containerScope/index.d.ts.map +0 -1
- package/dist/containerScope/index.js.map +0 -1
- package/dist/containerToken/containerToken.type.d.ts.map +0 -1
- package/dist/containerToken/containerToken.type.js.map +0 -1
- package/dist/containerToken/index.d.ts.map +0 -1
- package/dist/containerToken/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core dependency injection container for Zudojs.
|
|
3
|
+
*
|
|
4
|
+
* Lifetime and disposal model:
|
|
5
|
+
* - SINGLETON instances are cached in the container's root singleton cache
|
|
6
|
+
* and tracked by the container lifecycle — including singletons created
|
|
7
|
+
* transitively as dependencies of another resolution; they are disposed
|
|
8
|
+
* when the container is disposed (or when their registration is
|
|
9
|
+
* replaced/removed).
|
|
10
|
+
* - SCOPED instances only exist inside a scope created with `createScope()`;
|
|
11
|
+
* resolving a SCOPED token at the root throws.
|
|
12
|
+
* - TRANSIENT instances are never cached and never tracked — callers own
|
|
13
|
+
* their disposal.
|
|
3
14
|
*/
|
|
4
15
|
import { existingProvider, factoryProvider, valueProvider, classProvider, } from "../containerProvider/containerProvider.core.js";
|
|
5
16
|
import { ContainerScope as Scope } from "../containerScope/containerScope.type.js";
|
|
@@ -7,36 +18,63 @@ import { ContainerRegistry } from "../containerRegistry/containerRegistry.core.j
|
|
|
7
18
|
import { ContainerResolver } from "../containerResolution/containerResolution.core.js";
|
|
8
19
|
import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecycle/containerLifecycle.core.js";
|
|
9
20
|
import { resolveContainerOptions } from "../containerOptions/containerOptions.type.js";
|
|
21
|
+
import { unwrapToken } from "../containerToken/containerToken.type.js";
|
|
22
|
+
import { RegistrationNotFoundError } from "@zudojs/errors";
|
|
10
23
|
import { ContainerScopeContext } from "./containerCore.scope.js";
|
|
11
24
|
export class Container {
|
|
12
25
|
name;
|
|
13
26
|
options;
|
|
14
|
-
registry;
|
|
15
|
-
resolver;
|
|
16
|
-
lifecycle;
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
#registry;
|
|
28
|
+
#resolver;
|
|
29
|
+
#lifecycle;
|
|
30
|
+
#liveScopes = new Set();
|
|
31
|
+
#started = false;
|
|
32
|
+
#disposed = false;
|
|
19
33
|
constructor(options = {}) {
|
|
20
34
|
this.options = resolveContainerOptions(options);
|
|
21
35
|
this.name = this.options.name;
|
|
22
|
-
this
|
|
23
|
-
this
|
|
24
|
-
this
|
|
36
|
+
this.#registry = new ContainerRegistry(this.options.registry);
|
|
37
|
+
this.#lifecycle = new ContainerLifecycle(this.options.lifecycle);
|
|
38
|
+
this.#resolver = new ContainerResolver(this.#registry, (token) => {
|
|
39
|
+
// A cached singleton was evicted because its registration changed;
|
|
40
|
+
// dispose the tracked instance. Eviction disposal is best-effort:
|
|
41
|
+
// failures cannot propagate through the synchronous registry mutation
|
|
42
|
+
// that triggered them.
|
|
43
|
+
void this.#lifecycle.disposeInstance(token).catch(() => {
|
|
44
|
+
/* see clearSingletons()/dispose() for error-surfacing disposal */
|
|
45
|
+
});
|
|
46
|
+
});
|
|
25
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Marks the container as started.
|
|
50
|
+
*
|
|
51
|
+
* Calling `start()` explicitly is optional: `resolve()` (and
|
|
52
|
+
* `createScope()`) auto-start the container on first use. Note that with
|
|
53
|
+
* `freezeRegistrations: true`, starting — including the implicit start
|
|
54
|
+
* performed by the first `resolve()` — freezes the registration set.
|
|
55
|
+
*/
|
|
26
56
|
start() {
|
|
27
57
|
this.ensureNotDisposed();
|
|
28
|
-
if (this
|
|
58
|
+
if (this.#started)
|
|
29
59
|
return this;
|
|
30
|
-
this
|
|
60
|
+
this.#started = true;
|
|
31
61
|
return this;
|
|
32
62
|
}
|
|
33
63
|
register(token, provider, options = {}) {
|
|
34
64
|
this.ensureMutable();
|
|
35
|
-
return this
|
|
65
|
+
return this.#registry.register(token, provider, options);
|
|
36
66
|
}
|
|
37
67
|
registerClass(token, ctor, options = {}) {
|
|
38
|
-
|
|
68
|
+
const { inject, ...rest } = options;
|
|
69
|
+
return this.register(token, classProvider(ctor, inject ?? []), rest);
|
|
39
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Registers a pre-built value.
|
|
73
|
+
*
|
|
74
|
+
* Note: the scope is forcibly set to SINGLETON (overriding any
|
|
75
|
+
* `options.scope`) — a value provider always returns the same instance, so
|
|
76
|
+
* any other lifetime would be misleading.
|
|
77
|
+
*/
|
|
40
78
|
registerValue(token, value, options = {}) {
|
|
41
79
|
return this.register(token, valueProvider(value), {
|
|
42
80
|
...options,
|
|
@@ -49,73 +87,168 @@ export class Container {
|
|
|
49
87
|
registerExisting(token, existing, options = {}) {
|
|
50
88
|
return this.register(token, existingProvider(existing), options);
|
|
51
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolves a dependency at the container root.
|
|
92
|
+
*
|
|
93
|
+
* Auto-starts the container on first use. SCOPED registrations cannot be
|
|
94
|
+
* resolved here — create a scope with {@link createScope} instead.
|
|
95
|
+
*/
|
|
52
96
|
resolve(token) {
|
|
53
97
|
this.ensureActive();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return result.value;
|
|
98
|
+
return this.#resolver.resolveDetailed(token, this.buildResolutionOptions())
|
|
99
|
+
.value;
|
|
57
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolves several tokens at once. The result is a tuple typed per token,
|
|
103
|
+
* so heterogeneous token lists keep their individual types.
|
|
104
|
+
*/
|
|
58
105
|
resolveMany(tokens) {
|
|
59
106
|
return tokens.map((t) => this.resolve(t));
|
|
60
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Like {@link resolve} but returns `undefined` when the token has no
|
|
110
|
+
* registration (and cannot be auto-registered). Other resolution failures
|
|
111
|
+
* — broken factories, missing dependencies, captive dependencies — still
|
|
112
|
+
* throw.
|
|
113
|
+
*/
|
|
61
114
|
resolveOptional(token) {
|
|
62
|
-
|
|
63
|
-
return
|
|
64
|
-
|
|
115
|
+
try {
|
|
116
|
+
return this.resolve(token);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof RegistrationNotFoundError)
|
|
120
|
+
return undefined;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
65
123
|
}
|
|
66
124
|
canResolve(token) {
|
|
67
125
|
this.ensureNotDisposed();
|
|
68
|
-
return this
|
|
126
|
+
return this.#resolver.canResolve(token, this.resolutionOptions.autoRegisterClasses);
|
|
69
127
|
}
|
|
70
128
|
has(token) {
|
|
71
129
|
this.ensureNotDisposed();
|
|
72
|
-
return this
|
|
130
|
+
return this.#registry.has(token);
|
|
73
131
|
}
|
|
74
132
|
getRegistration(token) {
|
|
75
133
|
this.ensureNotDisposed();
|
|
76
|
-
return this
|
|
134
|
+
return this.#registry.get(token);
|
|
135
|
+
}
|
|
136
|
+
replace(token, provider, options = {}) {
|
|
137
|
+
this.ensureMutable();
|
|
138
|
+
return this.#registry.replace(token, provider, options);
|
|
77
139
|
}
|
|
78
140
|
remove(token) {
|
|
79
141
|
this.ensureMutable();
|
|
80
|
-
return this
|
|
142
|
+
return this.#registry.remove(token);
|
|
143
|
+
}
|
|
144
|
+
/** Removes every registration (evicting and disposing cached singletons). */
|
|
145
|
+
clearRegistrations() {
|
|
146
|
+
this.ensureMutable();
|
|
147
|
+
this.#registry.clear();
|
|
81
148
|
}
|
|
82
149
|
createScope(options = {}) {
|
|
83
150
|
this.ensureActive();
|
|
84
151
|
if (!this.options.allowScopes)
|
|
85
|
-
throw new Error(
|
|
86
|
-
|
|
152
|
+
throw new Error(`Container scopes are disabled for "${this.name}".`);
|
|
153
|
+
const scope = new ContainerScopeContext(this, options);
|
|
154
|
+
this.#liveScopes.add(scope);
|
|
155
|
+
return scope;
|
|
87
156
|
}
|
|
88
157
|
getRegistrations() {
|
|
89
158
|
this.ensureNotDisposed();
|
|
90
|
-
return this
|
|
159
|
+
return this.#registry.getAll();
|
|
160
|
+
}
|
|
161
|
+
getTokens() {
|
|
162
|
+
this.ensureNotDisposed();
|
|
163
|
+
return this.#registry.getTokens();
|
|
91
164
|
}
|
|
92
165
|
get registrationCount() {
|
|
93
|
-
return this
|
|
166
|
+
return this.#registry.size;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Returns an immutable snapshot of the current registrations, suitable for
|
|
170
|
+
* later {@link restoreSnapshot}. Cached instances are NOT part of the
|
|
171
|
+
* snapshot — only the registrations.
|
|
172
|
+
*/
|
|
173
|
+
snapshot() {
|
|
174
|
+
this.ensureNotDisposed();
|
|
175
|
+
return this.#registry.snapshot();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Wholesale-replaces the registration set with a previous snapshot.
|
|
179
|
+
* Entries are validated, cached singletons for the old set are evicted and
|
|
180
|
+
* disposed, and the operation is refused when registrations are frozen.
|
|
181
|
+
*/
|
|
182
|
+
restoreSnapshot(registrations) {
|
|
183
|
+
this.ensureMutable();
|
|
184
|
+
this.#registry.restore(registrations);
|
|
94
185
|
}
|
|
95
186
|
isStarted() {
|
|
96
|
-
return this
|
|
187
|
+
return this.#started;
|
|
97
188
|
}
|
|
98
189
|
isDisposed() {
|
|
99
|
-
return this
|
|
190
|
+
return this.#disposed;
|
|
100
191
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Evicts all cached singleton instances and disposes the tracked ones.
|
|
194
|
+
* Fails with an AggregateError listing every disposal failure.
|
|
195
|
+
*/
|
|
196
|
+
async clearSingletons() {
|
|
197
|
+
this.ensureNotDisposed();
|
|
198
|
+
const tokens = this.#resolver.getCachedSingletonTokens();
|
|
199
|
+
this.#resolver.clearSingletonCache();
|
|
200
|
+
const failures = [];
|
|
201
|
+
for (const token of tokens) {
|
|
202
|
+
try {
|
|
203
|
+
await this.#lifecycle.disposeInstance(token);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
failures.push(error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (failures.length > 0)
|
|
210
|
+
throw new AggregateError(failures, `Failed to dispose ${failures.length} singleton instance(s) of container "${this.name}".`);
|
|
104
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Disposes the container: all live scopes first, then (with
|
|
214
|
+
* `autoDispose: true`, the default) every tracked singleton in reverse
|
|
215
|
+
* creation order.
|
|
216
|
+
*
|
|
217
|
+
* With `autoDispose: false` tracked instances are NOT disposed — their
|
|
218
|
+
* references are simply released and disposal becomes the caller's
|
|
219
|
+
* responsibility.
|
|
220
|
+
*
|
|
221
|
+
* Disposal is terminal: the container is marked disposed even when some
|
|
222
|
+
* instances fail to dispose; every failure is reported in the thrown
|
|
223
|
+
* AggregateError. Idempotent.
|
|
224
|
+
*/
|
|
105
225
|
async dispose() {
|
|
106
|
-
if (this
|
|
226
|
+
if (this.#disposed)
|
|
107
227
|
return;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
228
|
+
const failures = [];
|
|
229
|
+
for (const scope of [...this.#liveScopes]) {
|
|
230
|
+
try {
|
|
231
|
+
await scope.dispose();
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
failures.push(error);
|
|
235
|
+
}
|
|
113
236
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
237
|
+
this.#liveScopes.clear();
|
|
238
|
+
if (this.options.autoDispose) {
|
|
239
|
+
try {
|
|
240
|
+
await this.#lifecycle.dispose();
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
failures.push(error);
|
|
244
|
+
}
|
|
118
245
|
}
|
|
246
|
+
this.#lifecycle.shutdown();
|
|
247
|
+
this.#resolver.clearSingletonCache();
|
|
248
|
+
this.#disposed = true;
|
|
249
|
+
this.#started = false;
|
|
250
|
+
if (failures.length > 0)
|
|
251
|
+
throw new AggregateError(failures, `Container "${this.name}" was disposed, but ${failures.length} cleanup step(s) failed.`);
|
|
119
252
|
}
|
|
120
253
|
get resolutionOptions() {
|
|
121
254
|
return {
|
|
@@ -124,20 +257,54 @@ export class Container {
|
|
|
124
257
|
maxResolutionDepth: this.options.resolution.maxResolutionDepth ?? 100,
|
|
125
258
|
};
|
|
126
259
|
}
|
|
260
|
+
/** @internal Creates a fresh scoped-instance cache for a scope. */
|
|
261
|
+
createScopeCache(parent) {
|
|
262
|
+
return this.#resolver.createScope(parent);
|
|
263
|
+
}
|
|
264
|
+
/** @internal Resolves on behalf of a scope, tracking singleton results. */
|
|
265
|
+
resolveInScope(token, cache, onInstanceCreated) {
|
|
266
|
+
this.ensureActive();
|
|
267
|
+
return this.#resolver.resolveDetailed(token, this.buildResolutionOptions(cache, onInstanceCreated));
|
|
268
|
+
}
|
|
269
|
+
/** @internal Unregisters a top-level scope from the live-scope set. */
|
|
270
|
+
releaseScope(scope) {
|
|
271
|
+
this.#liveScopes.delete(scope);
|
|
272
|
+
}
|
|
273
|
+
buildResolutionOptions(cache, onInstanceCreated) {
|
|
274
|
+
return {
|
|
275
|
+
...this.resolutionOptions,
|
|
276
|
+
allowRegistration: !this.options.freezeRegistrations,
|
|
277
|
+
cache,
|
|
278
|
+
onInstanceCreated: (result) => {
|
|
279
|
+
this.trackCreated(result);
|
|
280
|
+
onInstanceCreated?.(result);
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Tracks every freshly created SINGLETON instance — top-level results and
|
|
286
|
+
* transitively created dependencies alike — in the container lifecycle.
|
|
287
|
+
* SCOPED instances are owned by scopes and TRANSIENT ones by callers.
|
|
288
|
+
*/
|
|
289
|
+
trackCreated(result) {
|
|
290
|
+
if (result.scope !== Scope.SINGLETON)
|
|
291
|
+
return;
|
|
292
|
+
this.#lifecycle.track(unwrapToken(result.token), result.value, ContainerLifecycleOwner.CONTAINER);
|
|
293
|
+
}
|
|
127
294
|
ensureActive() {
|
|
128
295
|
this.ensureNotDisposed();
|
|
129
|
-
if (!this
|
|
296
|
+
if (!this.#started)
|
|
130
297
|
this.start();
|
|
131
298
|
}
|
|
132
299
|
ensureNotDisposed() {
|
|
133
|
-
if (this
|
|
300
|
+
if (this.#disposed)
|
|
134
301
|
throw new Error(`Container "${this.name}" has already been disposed.`);
|
|
135
302
|
}
|
|
136
303
|
ensureMutable() {
|
|
137
304
|
this.ensureNotDisposed();
|
|
138
305
|
if (!this.options.freezeRegistrations)
|
|
139
306
|
return;
|
|
140
|
-
if (this
|
|
307
|
+
if (this.#started)
|
|
141
308
|
throw new Error(`Registrations for container "${this.name}" are frozen.`);
|
|
142
309
|
}
|
|
143
310
|
}
|
|
@@ -1,31 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ContainerScopeContext —
|
|
3
|
-
* its own
|
|
4
|
-
*/
|
|
5
|
-
import type { RegistrationToken } from "../containerRegistration/containerRegistration.core.js";
|
|
6
|
-
import type { ContainerScopeOptions, ContainerLike } from "./containerCore.type.js";
|
|
7
|
-
/**
|
|
8
|
-
* Represents a child dependency scope.
|
|
2
|
+
* ContainerScopeContext — a dependency scope with its own scoped-instance
|
|
3
|
+
* cache and its own lifecycle for SCOPED instances.
|
|
9
4
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Ownership model:
|
|
6
|
+
* - SCOPED instances created through this scope — including those created
|
|
7
|
+
* transitively as dependencies — are owned by the scope and disposed when
|
|
8
|
+
* the scope is disposed.
|
|
9
|
+
* - SINGLETON instances resolved through a scope stay owned by the parent
|
|
10
|
+
* container — a scope's disposal never touches them.
|
|
11
|
+
* - TRANSIENT instances are not tracked anywhere; callers own their disposal.
|
|
12
|
+
*
|
|
13
|
+
* Nesting: `scope.createScope()` creates a true child scope. A child sees
|
|
14
|
+
* SCOPED instances already created by its ancestors (lookups chain upward),
|
|
15
|
+
* while instances it creates itself are private to the child. Disposing a
|
|
16
|
+
* scope disposes its children first, and a child refuses to resolve once any
|
|
17
|
+
* ancestor (scope or container) is disposed.
|
|
12
18
|
*/
|
|
19
|
+
import type { RegistrationToken, ResolvedTokens } from "../containerRegistration/containerRegistration.core.js";
|
|
20
|
+
import type { ContainerScopeOptions, ContainerLike } from "./containerCore.type.js";
|
|
13
21
|
export declare class ContainerScopeContext {
|
|
14
|
-
private readonly parent;
|
|
15
22
|
private disposed;
|
|
16
23
|
private readonly cache;
|
|
17
24
|
private readonly lifecycle;
|
|
25
|
+
private readonly container;
|
|
26
|
+
private readonly parentScope;
|
|
27
|
+
private readonly children;
|
|
18
28
|
readonly name: string;
|
|
19
29
|
readonly metadata: Readonly<Record<string, unknown>>;
|
|
20
|
-
|
|
30
|
+
/**
|
|
31
|
+
* @param container The container that owns the scope tree.
|
|
32
|
+
* @param options Scope name / metadata.
|
|
33
|
+
* @param parentScope When given, the new scope is a nested child of it.
|
|
34
|
+
* Application code should use `container.createScope()` /
|
|
35
|
+
* `scope.createScope()` rather than constructing scopes directly.
|
|
36
|
+
*/
|
|
37
|
+
constructor(container: ContainerLike, options?: ContainerScopeOptions, parentScope?: ContainerScopeContext);
|
|
21
38
|
/**
|
|
22
39
|
* Resolves a dependency within this scope.
|
|
40
|
+
*
|
|
41
|
+
* SCOPED instances are cached per scope (visible to child scopes) and
|
|
42
|
+
* tracked for disposal with the scope that created them. SINGLETON
|
|
43
|
+
* instances come from (and are tracked by) the container. TRANSIENT
|
|
44
|
+
* instances are created fresh and never tracked.
|
|
23
45
|
*/
|
|
24
46
|
resolve<T>(token: RegistrationToken<T>): T;
|
|
25
47
|
/**
|
|
26
|
-
* Resolves multiple dependencies within this scope.
|
|
48
|
+
* Resolves multiple dependencies within this scope, typed per token.
|
|
27
49
|
*/
|
|
28
|
-
resolveMany<
|
|
50
|
+
resolveMany<const Tokens extends readonly RegistrationToken[]>(tokens: Tokens): ResolvedTokens<Tokens>;
|
|
29
51
|
/**
|
|
30
52
|
* Checks whether a dependency can be resolved.
|
|
31
53
|
*/
|
|
@@ -35,11 +57,20 @@ export declare class ContainerScopeContext {
|
|
|
35
57
|
*/
|
|
36
58
|
has<T>(token: RegistrationToken<T>): boolean;
|
|
37
59
|
/**
|
|
38
|
-
* Creates
|
|
60
|
+
* Creates a nested child scope.
|
|
61
|
+
*
|
|
62
|
+
* The child inherits this scope's cached SCOPED instances for lookups,
|
|
63
|
+
* caches the SCOPED instances it creates itself, and is disposed
|
|
64
|
+
* automatically when this scope is disposed.
|
|
39
65
|
*/
|
|
40
66
|
createScope(options?: ContainerScopeOptions): ContainerScopeContext;
|
|
41
67
|
/**
|
|
42
|
-
* Disposes
|
|
68
|
+
* Disposes child scopes (most recent first), then all SCOPED instances
|
|
69
|
+
* belonging to this scope in reverse creation order, and detaches the scope
|
|
70
|
+
* from its parent. Container-owned singletons are not touched. Idempotent.
|
|
71
|
+
*
|
|
72
|
+
* Every failure is collected; the scope is marked disposed regardless and
|
|
73
|
+
* an AggregateError listing the failures is thrown afterwards.
|
|
43
74
|
*/
|
|
44
75
|
dispose(): Promise<void>;
|
|
45
76
|
/**
|
|
@@ -47,11 +78,25 @@ export declare class ContainerScopeContext {
|
|
|
47
78
|
*/
|
|
48
79
|
isDisposed(): boolean;
|
|
49
80
|
/**
|
|
50
|
-
* Returns the parent container
|
|
81
|
+
* Returns the direct parent: the container for a top-level scope, or the
|
|
82
|
+
* parent scope for a nested one.
|
|
83
|
+
*/
|
|
84
|
+
getParent(): ContainerLike | ContainerScopeContext;
|
|
85
|
+
/**
|
|
86
|
+
* Returns the container that owns the whole scope tree.
|
|
87
|
+
*/
|
|
88
|
+
getContainer(): ContainerLike;
|
|
89
|
+
/** @internal Detaches a disposed child scope. */
|
|
90
|
+
releaseChild(scope: ContainerScopeContext): void;
|
|
91
|
+
/**
|
|
92
|
+
* Tracks SCOPED instances created during a resolution through this scope
|
|
93
|
+
* (top-level result or transitively created dependency). Instances served
|
|
94
|
+
* from an ancestor's cache are cache hits and never reach here.
|
|
51
95
|
*/
|
|
52
|
-
|
|
96
|
+
private trackCreated;
|
|
53
97
|
/**
|
|
54
|
-
* Throws when the scope
|
|
98
|
+
* Throws when the scope — or any ancestor scope, or the container that
|
|
99
|
+
* owns it — is no longer active.
|
|
55
100
|
*/
|
|
56
101
|
private ensureActive;
|
|
57
102
|
}
|
|
@@ -1,42 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ContainerScopeContext —
|
|
3
|
-
* its own
|
|
4
|
-
*/
|
|
5
|
-
import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecycle/containerLifecycle.core.js";
|
|
6
|
-
/**
|
|
7
|
-
* Represents a child dependency scope.
|
|
2
|
+
* ContainerScopeContext — a dependency scope with its own scoped-instance
|
|
3
|
+
* cache and its own lifecycle for SCOPED instances.
|
|
8
4
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Ownership model:
|
|
6
|
+
* - SCOPED instances created through this scope — including those created
|
|
7
|
+
* transitively as dependencies — are owned by the scope and disposed when
|
|
8
|
+
* the scope is disposed.
|
|
9
|
+
* - SINGLETON instances resolved through a scope stay owned by the parent
|
|
10
|
+
* container — a scope's disposal never touches them.
|
|
11
|
+
* - TRANSIENT instances are not tracked anywhere; callers own their disposal.
|
|
12
|
+
*
|
|
13
|
+
* Nesting: `scope.createScope()` creates a true child scope. A child sees
|
|
14
|
+
* SCOPED instances already created by its ancestors (lookups chain upward),
|
|
15
|
+
* while instances it creates itself are private to the child. Disposing a
|
|
16
|
+
* scope disposes its children first, and a child refuses to resolve once any
|
|
17
|
+
* ancestor (scope or container) is disposed.
|
|
11
18
|
*/
|
|
19
|
+
import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecycle/containerLifecycle.core.js";
|
|
20
|
+
import { ContainerScope } from "../containerScope/containerScope.type.js";
|
|
12
21
|
export class ContainerScopeContext {
|
|
13
|
-
parent;
|
|
14
22
|
disposed = false;
|
|
15
23
|
cache;
|
|
16
24
|
lifecycle;
|
|
25
|
+
container;
|
|
26
|
+
parentScope;
|
|
27
|
+
children = new Set();
|
|
17
28
|
name;
|
|
18
29
|
metadata;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
/**
|
|
31
|
+
* @param container The container that owns the scope tree.
|
|
32
|
+
* @param options Scope name / metadata.
|
|
33
|
+
* @param parentScope When given, the new scope is a nested child of it.
|
|
34
|
+
* Application code should use `container.createScope()` /
|
|
35
|
+
* `scope.createScope()` rather than constructing scopes directly.
|
|
36
|
+
*/
|
|
37
|
+
constructor(container, options = {}, parentScope) {
|
|
38
|
+
this.container = container;
|
|
39
|
+
this.parentScope = parentScope;
|
|
40
|
+
this.name = options.name ?? `${(parentScope ?? container).name}:scope`;
|
|
22
41
|
this.metadata = Object.freeze({ ...(options.metadata ?? {}) });
|
|
23
|
-
this.cache =
|
|
42
|
+
this.cache = container.createScopeCache(parentScope?.cache);
|
|
24
43
|
this.lifecycle = new ContainerLifecycle();
|
|
25
44
|
}
|
|
26
45
|
/**
|
|
27
46
|
* Resolves a dependency within this scope.
|
|
47
|
+
*
|
|
48
|
+
* SCOPED instances are cached per scope (visible to child scopes) and
|
|
49
|
+
* tracked for disposal with the scope that created them. SINGLETON
|
|
50
|
+
* instances come from (and are tracked by) the container. TRANSIENT
|
|
51
|
+
* instances are created fresh and never tracked.
|
|
28
52
|
*/
|
|
29
53
|
resolve(token) {
|
|
30
54
|
this.ensureActive();
|
|
31
|
-
const result = this.
|
|
32
|
-
...this.parent.resolutionOptions,
|
|
33
|
-
cache: this.cache,
|
|
34
|
-
});
|
|
35
|
-
this.lifecycle.track(result.token, result.value, ContainerLifecycleOwner.SCOPE);
|
|
55
|
+
const result = this.container.resolveInScope(token, this.cache, (created) => this.trackCreated(created));
|
|
36
56
|
return result.value;
|
|
37
57
|
}
|
|
38
58
|
/**
|
|
39
|
-
* Resolves multiple dependencies within this scope.
|
|
59
|
+
* Resolves multiple dependencies within this scope, typed per token.
|
|
40
60
|
*/
|
|
41
61
|
resolveMany(tokens) {
|
|
42
62
|
return tokens.map((token) => this.resolve(token));
|
|
@@ -46,35 +66,66 @@ export class ContainerScopeContext {
|
|
|
46
66
|
*/
|
|
47
67
|
canResolve(token) {
|
|
48
68
|
this.ensureActive();
|
|
49
|
-
return this.
|
|
69
|
+
return this.container.canResolve(token);
|
|
50
70
|
}
|
|
51
71
|
/**
|
|
52
72
|
* Checks whether a registration exists.
|
|
53
73
|
*/
|
|
54
74
|
has(token) {
|
|
55
75
|
this.ensureActive();
|
|
56
|
-
return this.
|
|
76
|
+
return this.container.has(token);
|
|
57
77
|
}
|
|
58
78
|
/**
|
|
59
|
-
* Creates
|
|
79
|
+
* Creates a nested child scope.
|
|
80
|
+
*
|
|
81
|
+
* The child inherits this scope's cached SCOPED instances for lookups,
|
|
82
|
+
* caches the SCOPED instances it creates itself, and is disposed
|
|
83
|
+
* automatically when this scope is disposed.
|
|
60
84
|
*/
|
|
61
85
|
createScope(options = {}) {
|
|
62
86
|
this.ensureActive();
|
|
63
|
-
|
|
87
|
+
const child = new ContainerScopeContext(this.container, options, this);
|
|
88
|
+
this.children.add(child);
|
|
89
|
+
return child;
|
|
64
90
|
}
|
|
65
91
|
/**
|
|
66
|
-
* Disposes
|
|
92
|
+
* Disposes child scopes (most recent first), then all SCOPED instances
|
|
93
|
+
* belonging to this scope in reverse creation order, and detaches the scope
|
|
94
|
+
* from its parent. Container-owned singletons are not touched. Idempotent.
|
|
95
|
+
*
|
|
96
|
+
* Every failure is collected; the scope is marked disposed regardless and
|
|
97
|
+
* an AggregateError listing the failures is thrown afterwards.
|
|
67
98
|
*/
|
|
68
99
|
async dispose() {
|
|
69
100
|
if (this.disposed)
|
|
70
101
|
return;
|
|
102
|
+
this.disposed = true;
|
|
103
|
+
const failures = [];
|
|
104
|
+
for (const child of [...this.children].reverse()) {
|
|
105
|
+
try {
|
|
106
|
+
await child.dispose();
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
failures.push(error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.children.clear();
|
|
71
113
|
try {
|
|
72
|
-
await this.lifecycle.
|
|
114
|
+
await this.lifecycle.dispose();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
failures.push(error);
|
|
73
118
|
}
|
|
74
119
|
finally {
|
|
120
|
+
this.lifecycle.shutdown();
|
|
75
121
|
this.cache.clear();
|
|
76
|
-
this.
|
|
122
|
+
if (this.parentScope)
|
|
123
|
+
this.parentScope.releaseChild(this);
|
|
124
|
+
else
|
|
125
|
+
this.container.releaseScope(this);
|
|
77
126
|
}
|
|
127
|
+
if (failures.length > 0)
|
|
128
|
+
throw new AggregateError(failures, `Container scope "${this.name}" was disposed, but ${failures.length} cleanup step(s) failed.`);
|
|
78
129
|
}
|
|
79
130
|
/**
|
|
80
131
|
* Returns whether the scope has been disposed.
|
|
@@ -83,18 +134,46 @@ export class ContainerScopeContext {
|
|
|
83
134
|
return this.disposed;
|
|
84
135
|
}
|
|
85
136
|
/**
|
|
86
|
-
* Returns the parent container
|
|
137
|
+
* Returns the direct parent: the container for a top-level scope, or the
|
|
138
|
+
* parent scope for a nested one.
|
|
87
139
|
*/
|
|
88
140
|
getParent() {
|
|
89
|
-
return this.
|
|
141
|
+
return this.parentScope ?? this.container;
|
|
90
142
|
}
|
|
91
143
|
/**
|
|
92
|
-
*
|
|
144
|
+
* Returns the container that owns the whole scope tree.
|
|
145
|
+
*/
|
|
146
|
+
getContainer() {
|
|
147
|
+
return this.container;
|
|
148
|
+
}
|
|
149
|
+
/** @internal Detaches a disposed child scope. */
|
|
150
|
+
releaseChild(scope) {
|
|
151
|
+
this.children.delete(scope);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Tracks SCOPED instances created during a resolution through this scope
|
|
155
|
+
* (top-level result or transitively created dependency). Instances served
|
|
156
|
+
* from an ancestor's cache are cache hits and never reach here.
|
|
157
|
+
*/
|
|
158
|
+
trackCreated(result) {
|
|
159
|
+
if (result.scope !== ContainerScope.SCOPED)
|
|
160
|
+
return;
|
|
161
|
+
this.lifecycle.track(result.token, result.value, ContainerLifecycleOwner.SCOPE);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Throws when the scope — or any ancestor scope, or the container that
|
|
165
|
+
* owns it — is no longer active.
|
|
93
166
|
*/
|
|
94
167
|
ensureActive() {
|
|
95
168
|
if (this.disposed) {
|
|
96
169
|
throw new Error(`Container scope "${this.name}" has already been disposed.`);
|
|
97
170
|
}
|
|
171
|
+
if (this.parentScope?.isDisposed()) {
|
|
172
|
+
throw new Error(`Parent scope "${this.parentScope.name}" of scope "${this.name}" has been disposed.`);
|
|
173
|
+
}
|
|
174
|
+
if (this.container.isDisposed()) {
|
|
175
|
+
throw new Error(`Container "${this.container.name}" owning scope "${this.name}" has been disposed.`);
|
|
176
|
+
}
|
|
98
177
|
}
|
|
99
178
|
}
|
|
100
179
|
//# sourceMappingURL=containerCore.scope.js.map
|