@zudojs/container 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,16 @@ 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.
84
+
85
+ A `useExisting` alias never becomes a second owner of its target's instance:
86
+ a `SINGLETON` or `SCOPED` target is disposed exactly once by whoever created
87
+ it, and a `SCOPED` alias of a `SINGLETON` leaves the singleton alive when the
88
+ scope is disposed. Only a `TRANSIENT` target captured by a cached alias is
89
+ tracked (and disposed) through the alias.
75
90
 
76
91
  Tracking covers **every** `SINGLETON`/`SCOPED` instance the container
77
92
  creates — including ones created transitively as dependencies of another
@@ -141,7 +156,18 @@ which case the later registration wins. Use `container.replace()` to
141
156
  intentionally swap a registration.
142
157
 
143
158
  Replacing or removing a registration **evicts and disposes** its cached
144
- singleton, so the next `resolve()` uses the new provider.
159
+ singleton — and evicts any cached `useExisting` alias of it — so the next
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.
166
+
167
+ `DuplicateRegistrationError`, `CircularDependencyError`,
168
+ `RegistrationNotFoundError` and `ProviderResolutionError` are defined in
169
+ `@zudojs/errors` and re-exported from this package, so `instanceof` checks
170
+ need only one import.
145
171
 
146
172
  ## Resolution options
147
173
 
@@ -196,11 +222,16 @@ await container.dispose();
196
222
  have a `dispose()` method or implement `Symbol.dispose` /
197
223
  `Symbol.asyncDispose`.
198
224
  - `TRANSIENT` instances are never tracked; dispose them yourself.
225
+ - `registerValue()` values are never disposed by the container.
199
226
  - With `autoDispose: false`, `dispose()` releases tracked references
200
227
  **without disposing them** — disposal becomes your responsibility.
201
228
  - Disposal is terminal: the container is marked disposed even if some
202
229
  instances fail to dispose, and every failure is reported in the thrown
203
230
  `AggregateError`. `dispose()` is idempotent; any use after disposal throws.
231
+ The container counts as disposed from the moment `dispose()` is called —
232
+ a `resolve()` racing the disposal throws rather than creating an instance
233
+ nobody will clean up — and concurrent `dispose()` calls all settle when
234
+ the one disposal finishes.
204
235
  - `clearSingletons()` evicts and disposes cached singletons without
205
236
  disposing the container.
206
237
 
@@ -114,8 +114,15 @@ export declare class Container implements ContainerLike {
114
114
  * Disposal is terminal: the container is marked disposed even when some
115
115
  * instances fail to dispose; every failure is reported in the thrown
116
116
  * AggregateError. Idempotent.
117
+ *
118
+ * The container is marked disposed *before* any cleanup runs, so a
119
+ * `resolve()` racing the disposal throws instead of creating a singleton
120
+ * that would be tracked after the disposal snapshot and then silently
121
+ * dropped. Concurrent `dispose()` calls share the in-flight disposal and
122
+ * settle only when it has finished.
117
123
  */
118
124
  dispose(): Promise<void>;
125
+ private runDispose;
119
126
  get resolutionOptions(): {
120
127
  autoRegisterClasses: boolean;
121
128
  detectCircularDependencies: boolean;
@@ -30,6 +30,7 @@ export class Container {
30
30
  #liveScopes = new Set();
31
31
  #started = false;
32
32
  #disposed = false;
33
+ #disposing;
33
34
  constructor(options = {}) {
34
35
  this.options = resolveContainerOptions(options);
35
36
  this.name = this.options.name;
@@ -43,6 +44,11 @@ export class Container {
43
44
  void this.#lifecycle.disposeInstance(token).catch(() => {
44
45
  /* see clearSingletons()/dispose() for error-surfacing disposal */
45
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);
46
52
  });
47
53
  }
48
54
  /**
@@ -221,10 +227,26 @@ export class Container {
221
227
  * Disposal is terminal: the container is marked disposed even when some
222
228
  * instances fail to dispose; every failure is reported in the thrown
223
229
  * AggregateError. Idempotent.
230
+ *
231
+ * The container is marked disposed *before* any cleanup runs, so a
232
+ * `resolve()` racing the disposal throws instead of creating a singleton
233
+ * that would be tracked after the disposal snapshot and then silently
234
+ * dropped. Concurrent `dispose()` calls share the in-flight disposal and
235
+ * settle only when it has finished.
224
236
  */
225
- async dispose() {
237
+ dispose() {
238
+ if (this.#disposing)
239
+ return this.#disposing;
226
240
  if (this.#disposed)
227
- return;
241
+ return Promise.resolve();
242
+ this.#disposed = true;
243
+ this.#started = false;
244
+ this.#disposing = this.runDispose().finally(() => {
245
+ this.#disposing = undefined;
246
+ });
247
+ return this.#disposing;
248
+ }
249
+ async runDispose() {
228
250
  const failures = [];
229
251
  for (const scope of [...this.#liveScopes]) {
230
252
  try {
@@ -245,8 +267,6 @@ export class Container {
245
267
  }
246
268
  this.#lifecycle.shutdown();
247
269
  this.#resolver.clearSingletonCache();
248
- this.#disposed = true;
249
- this.#started = false;
250
270
  if (failures.length > 0)
251
271
  throw new AggregateError(failures, `Container "${this.name}" was disposed, but ${failures.length} cleanup step(s) failed.`);
252
272
  }
@@ -17,9 +17,11 @@
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;
24
+ private disposing;
23
25
  private readonly cache;
24
26
  private readonly lifecycle;
25
27
  private readonly container;
@@ -70,9 +72,11 @@ export declare class ContainerScopeContext {
70
72
  * from its parent. Container-owned singletons are not touched. Idempotent.
71
73
  *
72
74
  * Every failure is collected; the scope is marked disposed regardless and
73
- * an AggregateError listing the failures is thrown afterwards.
75
+ * an AggregateError listing the failures is thrown afterwards. Concurrent
76
+ * callers share the in-flight disposal rather than returning early.
74
77
  */
75
78
  dispose(): Promise<void>;
79
+ private runDispose;
76
80
  /**
77
81
  * Returns whether the scope has been disposed.
78
82
  */
@@ -86,6 +90,12 @@ export declare class ContainerScopeContext {
86
90
  * Returns the container that owns the whole scope tree.
87
91
  */
88
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;
89
99
  /** @internal Detaches a disposed child scope. */
90
100
  releaseChild(scope: ContainerScopeContext): void;
91
101
  /**
@@ -20,6 +20,7 @@ import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecy
20
20
  import { ContainerScope } from "../containerScope/containerScope.type.js";
21
21
  export class ContainerScopeContext {
22
22
  disposed = false;
23
+ disposing;
23
24
  cache;
24
25
  lifecycle;
25
26
  container;
@@ -94,12 +95,21 @@ export class ContainerScopeContext {
94
95
  * from its parent. Container-owned singletons are not touched. Idempotent.
95
96
  *
96
97
  * Every failure is collected; the scope is marked disposed regardless and
97
- * an AggregateError listing the failures is thrown afterwards.
98
+ * an AggregateError listing the failures is thrown afterwards. Concurrent
99
+ * callers share the in-flight disposal rather than returning early.
98
100
  */
99
- async dispose() {
101
+ dispose() {
102
+ if (this.disposing)
103
+ return this.disposing;
100
104
  if (this.disposed)
101
- return;
105
+ return Promise.resolve();
102
106
  this.disposed = true;
107
+ this.disposing = this.runDispose().finally(() => {
108
+ this.disposing = undefined;
109
+ });
110
+ return this.disposing;
111
+ }
112
+ async runDispose() {
103
113
  const failures = [];
104
114
  for (const child of [...this.children].reverse()) {
105
115
  try {
@@ -146,6 +156,19 @@ export class ContainerScopeContext {
146
156
  getContainer() {
147
157
  return this.container;
148
158
  }
159
+ /**
160
+ * @internal Drops a cached SCOPED instance whose registration (or a
161
+ * dependency of it) was replaced or removed, disposing it best-effort,
162
+ * in this scope and every child scope.
163
+ */
164
+ evictCached(token) {
165
+ if (this.disposed)
166
+ return;
167
+ this.cache.delete?.(token);
168
+ void this.lifecycle.disposeInstance(token).catch(() => undefined);
169
+ for (const child of this.children)
170
+ child.evictCached(token);
171
+ }
149
172
  /** @internal Detaches a disposed child scope. */
150
173
  releaseChild(scope) {
151
174
  this.children.delete(scope);
@@ -120,6 +120,10 @@ export class ContainerLifecycle {
120
120
  async dispose(owner) {
121
121
  if (this.disposed)
122
122
  return;
123
+ // Mark before the first await: an instance tracked while disposal is in
124
+ // flight would be dropped by `shutdown()` without ever being disposed.
125
+ if (owner === undefined)
126
+ this.disposed = true;
123
127
  const tracked = [...this.instances.values()].reverse();
124
128
  const selected = owner
125
129
  ? tracked.filter((entry) => entry.owner === owner)
@@ -141,8 +145,6 @@ export class ContainerLifecycle {
141
145
  break;
142
146
  }
143
147
  }
144
- if (owner === undefined)
145
- this.disposed = true;
146
148
  if (errors.length > 0)
147
149
  throw new ContainerDisposalError(errors, failedTokens);
148
150
  }
@@ -32,7 +32,16 @@ export declare class ContainerResolver {
32
32
  private readonly registry;
33
33
  private readonly singletonCache;
34
34
  private readonly onSingletonEvicted;
35
- constructor(registry: ContainerRegistry, onSingletonEvicted?: (token: Token<unknown>) => void);
35
+ private readonly onTokenInvalidated;
36
+ private readonly dependents;
37
+ /**
38
+ * @param onSingletonEvicted Called for each evicted cached singleton so
39
+ * the owner can dispose it.
40
+ * @param onTokenInvalidated Called for every token invalidated by a
41
+ * `replace()`/`remove()` (the token itself and each cached consumer),
42
+ * so owners of scope caches can drop and dispose their SCOPED copies.
43
+ */
44
+ constructor(registry: ContainerRegistry, onSingletonEvicted?: (token: Token<unknown>) => void, onTokenInvalidated?: (token: Token<unknown>) => void);
36
45
  resolve<T>(token: RegistrationToken<T>, options?: ResolutionOptions): T;
37
46
  resolveDetailed<T>(token: RegistrationToken<T>, options?: ResolutionOptions): ResolutionResult<T>;
38
47
  private resolveInternal;
@@ -54,5 +63,14 @@ export declare class ContainerResolver {
54
63
  canResolve<T>(token: RegistrationToken<T>, autoRegisterClasses?: boolean): boolean;
55
64
  private handleRegistryChange;
56
65
  private evictSingleton;
66
+ /**
67
+ * Evicts every cached singleton whose `useExisting` chain ends at `target`.
68
+ *
69
+ * A cached alias holds the target's instance under its own token, so
70
+ * evicting the target alone left the alias serving the old — by now
71
+ * disposed — instance after `replace()`/`remove()`.
72
+ */
73
+ private evictAliasesOf;
74
+ private aliasChainReaches;
57
75
  }
58
76
  //# sourceMappingURL=containerResolution.core.d.ts.map
@@ -32,6 +32,7 @@ import { unwrapToken } from "../containerToken/containerToken.type.js";
32
32
  import { CircularDependencyError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
33
33
  import { AsyncProviderError, CaptiveDependencyError, DependencyResolutionError, MaxResolutionDepthError, ScopedResolutionError, } from "./containerResolution.error.js";
34
34
  import { describeToken } from "../containerToken/containerToken.type.js";
35
+ import { DependentIndex } from "./containerResolution.dependents.js";
35
36
  /**
36
37
  * Scope cache that falls back to its parent scope's cache for lookups while
37
38
  * writing only to its own map. Nested scopes therefore see SCOPED instances
@@ -55,6 +56,9 @@ class ChainedResolutionCache {
55
56
  set(token, value) {
56
57
  this.#own.set(token, value);
57
58
  }
59
+ delete(token) {
60
+ return this.#own.delete(token);
61
+ }
58
62
  clear() {
59
63
  this.#own.clear();
60
64
  }
@@ -63,9 +67,19 @@ export class ContainerResolver {
63
67
  registry;
64
68
  singletonCache = new Map();
65
69
  onSingletonEvicted;
66
- constructor(registry, onSingletonEvicted) {
70
+ onTokenInvalidated;
71
+ dependents = new DependentIndex();
72
+ /**
73
+ * @param onSingletonEvicted Called for each evicted cached singleton so
74
+ * the owner can dispose it.
75
+ * @param onTokenInvalidated Called for every token invalidated by a
76
+ * `replace()`/`remove()` (the token itself and each cached consumer),
77
+ * so owners of scope caches can drop and dispose their SCOPED copies.
78
+ */
79
+ constructor(registry, onSingletonEvicted, onTokenInvalidated) {
67
80
  this.registry = registry;
68
81
  this.onSingletonEvicted = onSingletonEvicted;
82
+ this.onTokenInvalidated = onTokenInvalidated;
69
83
  registry.subscribe((event) => this.handleRegistryChange(event));
70
84
  }
71
85
  resolve(token, options = {}) {
@@ -108,6 +122,7 @@ export class ContainerResolver {
108
122
  }
109
123
  }
110
124
  const currentPath = [...state.path, token];
125
+ this.dependents.record(token, state.path, (t) => this.registry.get(t)?.scope);
111
126
  if (registration.scope === Scope.SINGLETON) {
112
127
  if (this.singletonCache.has(token)) {
113
128
  return {
@@ -140,8 +155,9 @@ export class ContainerResolver {
140
155
  state.path.push(token);
141
156
  state.pathSet.add(token);
142
157
  let value;
158
+ let owned;
143
159
  try {
144
- value = this.createInstance(registration, state, nextAncestor);
160
+ ({ value, owned } = this.createInstance(registration, state, nextAncestor));
145
161
  }
146
162
  finally {
147
163
  state.path.pop();
@@ -159,15 +175,24 @@ export class ContainerResolver {
159
175
  fromCache: false,
160
176
  path: currentPath,
161
177
  };
162
- state.onInstanceCreated?.(result);
178
+ // A `useExisting` alias of a cached (SINGLETON/SCOPED) target does not
179
+ // own the instance it hands out — the target's own creation already
180
+ // reported it. Reporting it again registered a second owner for the same
181
+ // object: the container disposed it twice, and a SCOPED alias let a
182
+ // scope dispose a container-owned singleton.
183
+ if (owned)
184
+ state.onInstanceCreated?.(result);
163
185
  return result;
164
186
  }
165
187
  createInstance(registration, state, singletonAncestor) {
166
188
  const provider = normalizeProvider(registration.provider);
167
189
  const token = getRegistrationToken(registration);
168
190
  try {
191
+ // A pre-built value belongs to whoever built it (often shared
192
+ // between containers); the container did not create it, so it
193
+ // must not dispose it.
169
194
  if (isValueProvider(provider))
170
- return provider.useValue;
195
+ return { value: provider.useValue, owned: false };
171
196
  if (isExistingProvider(provider)) {
172
197
  const target = unwrapToken(provider.useExisting);
173
198
  if (!this.registry.has(target) &&
@@ -175,8 +200,13 @@ export class ContainerResolver {
175
200
  throw new Error(`useExisting target "${describeToken(target)}" for token ` +
176
201
  `"${describeToken(token)}" is not registered.`);
177
202
  }
178
- return this.resolveInternal(target, state, singletonAncestor)
179
- .value;
203
+ const resolved = this.resolveInternal(target, state, singletonAncestor);
204
+ // Only a TRANSIENT target has no owner of its own; a cached alias
205
+ // of it is the one place the instance can be tracked.
206
+ return {
207
+ value: resolved.value,
208
+ owned: resolved.scope === Scope.TRANSIENT,
209
+ };
180
210
  }
181
211
  if (isFactoryProvider(provider)) {
182
212
  const deps = provider.inject ?? [];
@@ -185,14 +215,14 @@ export class ContainerResolver {
185
215
  const produced = provider.useFactory(...args);
186
216
  if (registration.scope !== Scope.TRANSIENT && isPromiseLike(produced))
187
217
  throw new AsyncProviderError(describeToken(token), registration.scope);
188
- return produced;
218
+ return { value: produced, owned: true };
189
219
  }
190
220
  if (isClassProvider(provider)) {
191
221
  const deps = provider.inject ?? [];
192
222
  const args = deps.map((d) => this.resolveInternal(unwrapToken(d), state, singletonAncestor)
193
223
  .value);
194
224
  const ctor = provider.useClass;
195
- return new ctor(...args);
225
+ return { value: new ctor(...args), owned: true };
196
226
  }
197
227
  throw new Error("Unsupported container provider.");
198
228
  }
@@ -239,13 +269,22 @@ export class ContainerResolver {
239
269
  switch (event.operation) {
240
270
  case RegistryOperation.REPLACE:
241
271
  case RegistryOperation.REMOVE: {
242
- this.evictSingleton(unwrapToken(event.token));
272
+ const token = unwrapToken(event.token);
273
+ // Consumers first, so each is disposed before what it consumed.
274
+ for (const dependent of this.dependents.take(token)) {
275
+ this.evictSingleton(dependent);
276
+ this.onTokenInvalidated?.(dependent);
277
+ }
278
+ this.evictSingleton(token);
279
+ this.evictAliasesOf(token);
280
+ this.onTokenInvalidated?.(token);
243
281
  break;
244
282
  }
245
283
  case RegistryOperation.CLEAR:
246
284
  case RegistryOperation.RESTORE: {
247
285
  for (const t of [...this.singletonCache.keys()])
248
286
  this.evictSingleton(t);
287
+ this.dependents.clear();
249
288
  break;
250
289
  }
251
290
  default:
@@ -258,6 +297,40 @@ export class ContainerResolver {
258
297
  this.singletonCache.delete(token);
259
298
  this.onSingletonEvicted?.(token);
260
299
  }
300
+ /**
301
+ * Evicts every cached singleton whose `useExisting` chain ends at `target`.
302
+ *
303
+ * A cached alias holds the target's instance under its own token, so
304
+ * evicting the target alone left the alias serving the old — by now
305
+ * disposed — instance after `replace()`/`remove()`.
306
+ */
307
+ evictAliasesOf(target) {
308
+ for (const cached of [...this.singletonCache.keys()]) {
309
+ if (cached === target)
310
+ continue;
311
+ if (this.aliasChainReaches(cached, target))
312
+ this.evictSingleton(cached);
313
+ }
314
+ }
315
+ aliasChainReaches(token, target) {
316
+ const visited = new Set();
317
+ let current = token;
318
+ for (;;) {
319
+ const registration = this.registry.get(current);
320
+ if (!registration)
321
+ return false;
322
+ const provider = normalizeProvider(registration.provider);
323
+ if (!isExistingProvider(provider))
324
+ return false;
325
+ const next = unwrapToken(provider.useExisting);
326
+ if (next === target)
327
+ return true;
328
+ if (visited.has(next))
329
+ return false;
330
+ visited.add(next);
331
+ current = next;
332
+ }
333
+ }
261
334
  }
262
335
  function isPromiseLike(value) {
263
336
  return (typeof value === "object" &&
@@ -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/dist/index.d.ts CHANGED
@@ -12,4 +12,5 @@ export * from "./containerRegistry/index.js";
12
12
  export * from "./containerResolution/index.js";
13
13
  export * from "./containerScope/index.js";
14
14
  export * from "./containerToken/index.js";
15
+ export { CircularDependencyError, DuplicateRegistrationError, RegistrationNotFoundError, ProviderResolutionError, } from "@zudojs/errors";
15
16
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -12,4 +12,7 @@ export * from "./containerRegistry/index.js";
12
12
  export * from "./containerResolution/index.js";
13
13
  export * from "./containerScope/index.js";
14
14
  export * from "./containerToken/index.js";
15
+ // Error classes the container throws but that live in @zudojs/errors,
16
+ // re-exported so `instanceof` checks need only this package.
17
+ export { CircularDependencyError, DuplicateRegistrationError, RegistrationNotFoundError, ProviderResolutionError, } from "@zudojs/errors";
15
18
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/container",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Token-based dependency injection container for managing application dependencies and service lifetimes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,13 +18,17 @@
18
18
  "!dist/.tsbuildinfo"
19
19
  ],
20
20
  "dependencies": {
21
- "@zudojs/errors": "1.0.0"
21
+ "@zudojs/errors": "1.1.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "7.0.2",
25
25
  "vitest": "^4.1.11"
26
26
  },
27
27
  "license": "MIT",
28
+ "author": {
29
+ "name": "Oluwayemi Oyinlola",
30
+ "url": "https://github.com/oyinlola-tech"
31
+ },
28
32
  "publishConfig": {
29
33
  "access": "public"
30
34
  },