@zudojs/container 0.1.0 → 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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -8
  3. package/dist/containerCore/containerCore.core.d.ts +105 -14
  4. package/dist/containerCore/containerCore.core.js +211 -44
  5. package/dist/containerCore/containerCore.scope.d.ts +63 -18
  6. package/dist/containerCore/containerCore.scope.js +108 -29
  7. package/dist/containerCore/containerCore.type.d.ts +22 -16
  8. package/dist/containerLifecycle/containerLifecycle.core.d.ts +59 -9
  9. package/dist/containerLifecycle/containerLifecycle.core.js +58 -15
  10. package/dist/containerOptions/containerOptions.type.d.ts +7 -17
  11. package/dist/containerOptions/containerOptions.type.js +0 -19
  12. package/dist/containerProvider/containerProvider.core.d.ts +11 -3
  13. package/dist/containerProvider/containerProvider.core.js +11 -5
  14. package/dist/containerRegistration/containerRegistration.core.d.ts +9 -3
  15. package/dist/containerRegistration/containerRegistration.core.js +0 -6
  16. package/dist/containerRegistry/containerRegistry.core.d.ts +23 -9
  17. package/dist/containerRegistry/containerRegistry.core.js +50 -47
  18. package/dist/containerRegistry/containerRegistry.type.d.ts +3 -1
  19. package/dist/containerRegistry/containerRegistry.type.js +2 -0
  20. package/dist/containerRegistry/index.d.ts +0 -1
  21. package/dist/containerRegistry/index.js +0 -1
  22. package/dist/containerResolution/containerResolution.core.d.ts +42 -10
  23. package/dist/containerResolution/containerResolution.core.js +207 -64
  24. package/dist/containerResolution/containerResolution.error.d.ts +68 -3
  25. package/dist/containerResolution/containerResolution.error.js +107 -3
  26. package/dist/containerResolution/containerResolution.type.d.ts +35 -3
  27. package/dist/containerResolution/index.d.ts +1 -0
  28. package/dist/containerResolution/index.js +1 -0
  29. package/dist/containerToken/containerToken.type.d.ts +26 -1
  30. package/dist/containerToken/containerToken.type.js +18 -0
  31. package/package.json +21 -12
  32. package/dist/.tsbuildinfo +0 -1
  33. package/dist/containerCore/containerCore.core.d.ts.map +0 -1
  34. package/dist/containerCore/containerCore.core.js.map +0 -1
  35. package/dist/containerCore/containerCore.scope.d.ts.map +0 -1
  36. package/dist/containerCore/containerCore.scope.js.map +0 -1
  37. package/dist/containerCore/containerCore.type.d.ts.map +0 -1
  38. package/dist/containerCore/containerCore.type.js.map +0 -1
  39. package/dist/containerCore/index.d.ts.map +0 -1
  40. package/dist/containerCore/index.js.map +0 -1
  41. package/dist/containerLifecycle/containerLifecycle.core.d.ts.map +0 -1
  42. package/dist/containerLifecycle/containerLifecycle.core.js.map +0 -1
  43. package/dist/containerLifecycle/index.d.ts.map +0 -1
  44. package/dist/containerLifecycle/index.js.map +0 -1
  45. package/dist/containerOptions/containerOptions.type.d.ts.map +0 -1
  46. package/dist/containerOptions/containerOptions.type.js.map +0 -1
  47. package/dist/containerOptions/index.d.ts.map +0 -1
  48. package/dist/containerOptions/index.js.map +0 -1
  49. package/dist/containerProvider/containerProvider.core.d.ts.map +0 -1
  50. package/dist/containerProvider/containerProvider.core.js.map +0 -1
  51. package/dist/containerProvider/index.d.ts.map +0 -1
  52. package/dist/containerProvider/index.js.map +0 -1
  53. package/dist/containerRegistration/containerRegistration.core.d.ts.map +0 -1
  54. package/dist/containerRegistration/containerRegistration.core.js.map +0 -1
  55. package/dist/containerRegistration/index.d.ts.map +0 -1
  56. package/dist/containerRegistration/index.js.map +0 -1
  57. package/dist/containerRegistry/containerRegistry.core.d.ts.map +0 -1
  58. package/dist/containerRegistry/containerRegistry.core.js.map +0 -1
  59. package/dist/containerRegistry/containerRegistry.error.d.ts +0 -12
  60. package/dist/containerRegistry/containerRegistry.error.d.ts.map +0 -1
  61. package/dist/containerRegistry/containerRegistry.error.js +0 -22
  62. package/dist/containerRegistry/containerRegistry.error.js.map +0 -1
  63. package/dist/containerRegistry/containerRegistry.type.d.ts.map +0 -1
  64. package/dist/containerRegistry/containerRegistry.type.js.map +0 -1
  65. package/dist/containerRegistry/index.d.ts.map +0 -1
  66. package/dist/containerRegistry/index.js.map +0 -1
  67. package/dist/containerResolution/containerResolution.core.d.ts.map +0 -1
  68. package/dist/containerResolution/containerResolution.core.js.map +0 -1
  69. package/dist/containerResolution/containerResolution.error.d.ts.map +0 -1
  70. package/dist/containerResolution/containerResolution.error.js.map +0 -1
  71. package/dist/containerResolution/containerResolution.type.d.ts.map +0 -1
  72. package/dist/containerResolution/containerResolution.type.js.map +0 -1
  73. package/dist/containerResolution/index.d.ts.map +0 -1
  74. package/dist/containerResolution/index.js.map +0 -1
  75. package/dist/containerScope/containerScope.type.d.ts.map +0 -1
  76. package/dist/containerScope/containerScope.type.js.map +0 -1
  77. package/dist/containerScope/index.d.ts.map +0 -1
  78. package/dist/containerScope/index.js.map +0 -1
  79. package/dist/containerToken/containerToken.type.d.ts.map +0 -1
  80. package/dist/containerToken/containerToken.type.js.map +0 -1
  81. package/dist/containerToken/index.d.ts.map +0 -1
  82. package/dist/containerToken/index.js.map +0 -1
  83. package/dist/index.d.ts.map +0 -1
  84. 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
- started = false;
18
- disposed = false;
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.registry = new ContainerRegistry(this.options.registry);
23
- this.resolver = new ContainerResolver(this.registry);
24
- this.lifecycle = new ContainerLifecycle(this.options.lifecycle);
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.started)
58
+ if (this.#started)
29
59
  return this;
30
- this.started = true;
60
+ this.#started = true;
31
61
  return this;
32
62
  }
33
63
  register(token, provider, options = {}) {
34
64
  this.ensureMutable();
35
- return this.registry.register(token, provider, options);
65
+ return this.#registry.register(token, provider, options);
36
66
  }
37
67
  registerClass(token, ctor, options = {}) {
38
- return this.register(token, classProvider(ctor), options);
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
- const result = this.resolver.resolveDetailed(token, this.resolutionOptions);
55
- this.lifecycle.track(result.token, result.value, ContainerLifecycleOwner.CONTAINER);
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
- if (!this.canResolve(token))
63
- return undefined;
64
- return this.resolve(token);
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.resolver.canResolve(token);
126
+ return this.#resolver.canResolve(token, this.resolutionOptions.autoRegisterClasses);
69
127
  }
70
128
  has(token) {
71
129
  this.ensureNotDisposed();
72
- return this.registry.has(token);
130
+ return this.#registry.has(token);
73
131
  }
74
132
  getRegistration(token) {
75
133
  this.ensureNotDisposed();
76
- return this.registry.get(token);
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.registry.remove(token);
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("Container scopes are disabled.");
86
- return new ContainerScopeContext(this, options);
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.registry.getAll();
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.registry.size;
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.started;
187
+ return this.#started;
97
188
  }
98
189
  isDisposed() {
99
- return this.disposed;
190
+ return this.#disposed;
100
191
  }
101
- clearSingletons() {
102
- this.ensureActive();
103
- this.resolver.clearSingletons();
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.disposed)
226
+ if (this.#disposed)
107
227
  return;
108
- try {
109
- if (this.options.autoDispose)
110
- await this.lifecycle.disposeContainer();
111
- else
112
- this.lifecycle.reset();
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
- finally {
115
- this.resolver.clearSingletons();
116
- this.disposed = true;
117
- this.started = false;
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.started)
296
+ if (!this.#started)
130
297
  this.start();
131
298
  }
132
299
  ensureNotDisposed() {
133
- if (this.disposed)
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.started)
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 — child dependency scope with
3
- * its own scoped-instance cache.
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
- * A scope has its own scoped-instance cache while sharing
11
- * registrations and singleton instances with its parent.
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
- constructor(parent: ContainerLike, options?: ContainerScopeOptions);
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<T>(tokens: readonly RegistrationToken<T>[]): T[];
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 another nested scope.
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 all instances belonging to this scope.
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
- getParent(): ContainerLike;
96
+ private trackCreated;
53
97
  /**
54
- * Throws when the scope is no longer active.
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 — child dependency scope with
3
- * its own scoped-instance cache.
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
- * A scope has its own scoped-instance cache while sharing
10
- * registrations and singleton instances with its parent.
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
- constructor(parent, options = {}) {
20
- this.parent = parent;
21
- this.name = options.name ?? `${parent.name}:scope`;
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 = parent.resolver.createScope();
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.parent.resolver.resolveDetailed(token, {
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.parent.resolver.canResolve(token);
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.parent.has(token);
76
+ return this.container.has(token);
57
77
  }
58
78
  /**
59
- * Creates another nested scope.
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
- return this.parent.createScope(options);
87
+ const child = new ContainerScopeContext(this.container, options, this);
88
+ this.children.add(child);
89
+ return child;
64
90
  }
65
91
  /**
66
- * Disposes all instances belonging to this scope.
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.disposeScope();
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.disposed = true;
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.parent;
141
+ return this.parentScope ?? this.container;
90
142
  }
91
143
  /**
92
- * Throws when the scope is no longer active.
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