@zudojs/container 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,7 +77,10 @@ export declare class Container implements ContainerLike {
77
77
  getRegistration<T>(token: RegistrationToken<T>): ContainerRegistration<T> | undefined;
78
78
  replace<T>(token: RegistrationToken<T>, provider: ContainerProvider<T>, options?: CreateRegistrationOptions): ContainerRegistration<T>;
79
79
  remove<T>(token: RegistrationToken<T>): boolean;
80
- /** Removes every registration (evicting and disposing cached singletons). */
80
+ /**
81
+ * Removes every registration, evicting and disposing cached instances:
82
+ * container-owned singletons and every live scope's SCOPED copies alike.
83
+ */
81
84
  clearRegistrations(): void;
82
85
  createScope(options?: ContainerScopeOptions): ContainerScopeContext;
83
86
  getRegistrations(): readonly ContainerRegistration[];
@@ -91,8 +94,10 @@ export declare class Container implements ContainerLike {
91
94
  snapshot(): readonly ContainerRegistration[];
92
95
  /**
93
96
  * Wholesale-replaces the registration set with a previous snapshot.
94
- * Entries are validated, cached singletons for the old set are evicted and
95
- * disposed, and the operation is refused when registrations are frozen.
97
+ * Entries are validated, cached instances built from the old set are
98
+ * evicted and disposed — container-owned singletons and every live
99
+ * scope's SCOPED copies alike — and the operation is refused when
100
+ * registrations are frozen.
96
101
  */
97
102
  restoreSnapshot(registrations: readonly ContainerRegistration[]): void;
98
103
  isStarted(): boolean;
@@ -19,7 +19,7 @@ import { ContainerResolver } from "../containerResolution/containerResolution.co
19
19
  import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecycle/containerLifecycle.core.js";
20
20
  import { resolveContainerOptions } from "../containerOptions/containerOptions.type.js";
21
21
  import { unwrapToken } from "../containerToken/containerToken.type.js";
22
- import { RegistrationNotFoundError } from "@zudojs/errors";
22
+ import { ContainerError, ContainerLifecycleError, RegistrationNotFoundError, } from "@zudojs/errors";
23
23
  import { ContainerScopeContext } from "./containerCore.scope.js";
24
24
  export class Container {
25
25
  name;
@@ -147,7 +147,10 @@ export class Container {
147
147
  this.ensureMutable();
148
148
  return this.#registry.remove(token);
149
149
  }
150
- /** Removes every registration (evicting and disposing cached singletons). */
150
+ /**
151
+ * Removes every registration, evicting and disposing cached instances:
152
+ * container-owned singletons and every live scope's SCOPED copies alike.
153
+ */
151
154
  clearRegistrations() {
152
155
  this.ensureMutable();
153
156
  this.#registry.clear();
@@ -155,7 +158,7 @@ export class Container {
155
158
  createScope(options = {}) {
156
159
  this.ensureActive();
157
160
  if (!this.options.allowScopes)
158
- throw new Error(`Container scopes are disabled for "${this.name}".`);
161
+ throw new ContainerError(`Container scopes are disabled for "${this.name}".`);
159
162
  const scope = new ContainerScopeContext(this, options);
160
163
  this.#liveScopes.add(scope);
161
164
  return scope;
@@ -182,8 +185,10 @@ export class Container {
182
185
  }
183
186
  /**
184
187
  * Wholesale-replaces the registration set with a previous snapshot.
185
- * Entries are validated, cached singletons for the old set are evicted and
186
- * disposed, and the operation is refused when registrations are frozen.
188
+ * Entries are validated, cached instances built from the old set are
189
+ * evicted and disposed — container-owned singletons and every live
190
+ * scope's SCOPED copies alike — and the operation is refused when
191
+ * registrations are frozen.
187
192
  */
188
193
  restoreSnapshot(registrations) {
189
194
  this.ensureMutable();
@@ -318,14 +323,14 @@ export class Container {
318
323
  }
319
324
  ensureNotDisposed() {
320
325
  if (this.#disposed)
321
- throw new Error(`Container "${this.name}" has already been disposed.`);
326
+ throw new ContainerLifecycleError("dispose", `Container "${this.name}" has already been disposed.`);
322
327
  }
323
328
  ensureMutable() {
324
329
  this.ensureNotDisposed();
325
330
  if (!this.options.freezeRegistrations)
326
331
  return;
327
332
  if (this.#started)
328
- throw new Error(`Registrations for container "${this.name}" are frozen.`);
333
+ throw new ContainerError(`Registrations for container "${this.name}" are frozen.`);
329
334
  }
330
335
  }
331
336
  export function createContainer(options = {}) {
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecycle/containerLifecycle.core.js";
20
20
  import { ContainerScope } from "../containerScope/containerScope.type.js";
21
+ import { ContainerLifecycleError } from "@zudojs/errors";
21
22
  export class ContainerScopeContext {
22
23
  disposed = false;
23
24
  disposing;
@@ -189,13 +190,13 @@ export class ContainerScopeContext {
189
190
  */
190
191
  ensureActive() {
191
192
  if (this.disposed) {
192
- throw new Error(`Container scope "${this.name}" has already been disposed.`);
193
+ throw new ContainerLifecycleError("dispose", `Container scope "${this.name}" has already been disposed.`);
193
194
  }
194
195
  if (this.parentScope?.isDisposed()) {
195
- throw new Error(`Parent scope "${this.parentScope.name}" of scope "${this.name}" has been disposed.`);
196
+ throw new ContainerLifecycleError("dispose", `Parent scope "${this.parentScope.name}" of scope "${this.name}" has been disposed.`);
196
197
  }
197
198
  if (this.container.isDisposed()) {
198
- throw new Error(`Container "${this.container.name}" owning scope "${this.name}" has been disposed.`);
199
+ throw new ContainerLifecycleError("dispose", `Container "${this.container.name}" owning scope "${this.name}" has been disposed.`);
199
200
  }
200
201
  }
201
202
  }
@@ -22,7 +22,10 @@
22
22
  * The resolver subscribes to registry change events: REPLACE/REMOVE evict the
23
23
  * affected token's cached singleton, CLEAR/RESTORE evict all cached
24
24
  * singletons. Each eviction is reported through the `onSingletonEvicted`
25
- * callback so the owning container can dispose the instance.
25
+ * callback so the owning container can dispose the instance, and every
26
+ * invalidated token — for CLEAR/RESTORE that is every cached token, SCOPED
27
+ * ones included — through `onTokenInvalidated` so live scopes drop and
28
+ * dispose their own copies.
26
29
  */
27
30
  import type { RegistrationToken } from "../containerRegistration/containerRegistration.core.js";
28
31
  import type { ContainerRegistry } from "../containerRegistry/containerRegistry.core.js";
@@ -34,12 +37,24 @@ export declare class ContainerResolver {
34
37
  private readonly onSingletonEvicted;
35
38
  private readonly onTokenInvalidated;
36
39
  private readonly dependents;
40
+ /**
41
+ * Tokens for which a SCOPED instance has ever been cached in a scope.
42
+ *
43
+ * The singleton cache only knows about SINGLETON tokens and the
44
+ * dependent index only records tokens consumed by another cached
45
+ * instance, so neither can name a SCOPED token that a scope resolved
46
+ * directly. A wholesale CLEAR/RESTORE must still tell live scopes to
47
+ * drop those instances.
48
+ */
49
+ private readonly scopedTokens;
37
50
  /**
38
51
  * @param onSingletonEvicted Called for each evicted cached singleton so
39
52
  * the owner can dispose it.
40
53
  * @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.
54
+ * registry change — for `replace()`/`remove()` the token itself and
55
+ * each cached consumer, for `clear()`/`restore()` every token that
56
+ * was cached at all — so owners of scope caches can drop and dispose
57
+ * their SCOPED copies.
43
58
  */
44
59
  constructor(registry: ContainerRegistry, onSingletonEvicted?: (token: Token<unknown>) => void, onTokenInvalidated?: (token: Token<unknown>) => void);
45
60
  resolve<T>(token: RegistrationToken<T>, options?: ResolutionOptions): T;
@@ -22,14 +22,17 @@
22
22
  * The resolver subscribes to registry change events: REPLACE/REMOVE evict the
23
23
  * affected token's cached singleton, CLEAR/RESTORE evict all cached
24
24
  * singletons. Each eviction is reported through the `onSingletonEvicted`
25
- * callback so the owning container can dispose the instance.
25
+ * callback so the owning container can dispose the instance, and every
26
+ * invalidated token — for CLEAR/RESTORE that is every cached token, SCOPED
27
+ * ones included — through `onTokenInvalidated` so live scopes drop and
28
+ * dispose their own copies.
26
29
  */
27
30
  import { isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, normalizeProvider, } from "../containerProvider/containerProvider.core.js";
28
31
  import { ContainerScope as Scope } from "../containerScope/containerScope.type.js";
29
32
  import { defineRegistration, getRegistrationToken, } from "../containerRegistration/containerRegistration.core.js";
30
33
  import { RegistryOperation } from "../containerRegistry/containerRegistry.type.js";
31
34
  import { unwrapToken } from "../containerToken/containerToken.type.js";
32
- import { CircularDependencyError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
35
+ import { CircularDependencyError, ContainerError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
33
36
  import { AsyncProviderError, CaptiveDependencyError, DependencyResolutionError, MaxResolutionDepthError, ScopedResolutionError, } from "./containerResolution.error.js";
34
37
  import { describeToken } from "../containerToken/containerToken.type.js";
35
38
  import { DependentIndex } from "./containerResolution.dependents.js";
@@ -69,12 +72,24 @@ export class ContainerResolver {
69
72
  onSingletonEvicted;
70
73
  onTokenInvalidated;
71
74
  dependents = new DependentIndex();
75
+ /**
76
+ * Tokens for which a SCOPED instance has ever been cached in a scope.
77
+ *
78
+ * The singleton cache only knows about SINGLETON tokens and the
79
+ * dependent index only records tokens consumed by another cached
80
+ * instance, so neither can name a SCOPED token that a scope resolved
81
+ * directly. A wholesale CLEAR/RESTORE must still tell live scopes to
82
+ * drop those instances.
83
+ */
84
+ scopedTokens = new Set();
72
85
  /**
73
86
  * @param onSingletonEvicted Called for each evicted cached singleton so
74
87
  * the owner can dispose it.
75
88
  * @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.
89
+ * registry change — for `replace()`/`remove()` the token itself and
90
+ * each cached consumer, for `clear()`/`restore()` every token that
91
+ * was cached at all — so owners of scope caches can drop and dispose
92
+ * their SCOPED copies.
78
93
  */
79
94
  constructor(registry, onSingletonEvicted, onTokenInvalidated) {
80
95
  this.registry = registry;
@@ -165,8 +180,11 @@ export class ContainerResolver {
165
180
  }
166
181
  if (registration.scope === Scope.SINGLETON)
167
182
  this.singletonCache.set(token, value);
168
- else if (registration.scope === Scope.SCOPED)
183
+ else if (registration.scope === Scope.SCOPED) {
169
184
  state.scopeCache?.set(token, value);
185
+ if (state.scopeCache)
186
+ this.scopedTokens.add(token);
187
+ }
170
188
  const result = {
171
189
  value,
172
190
  token,
@@ -197,8 +215,8 @@ export class ContainerResolver {
197
215
  const target = unwrapToken(provider.useExisting);
198
216
  if (!this.registry.has(target) &&
199
217
  !(state.autoRegisterClasses && typeof target === "function")) {
200
- throw new Error(`useExisting target "${describeToken(target)}" for token ` +
201
- `"${describeToken(token)}" is not registered.`);
218
+ throw new ContainerError(`useExisting target "${describeToken(target)}" for token ` +
219
+ `"${describeToken(token)}" is not registered.`, { token: describeToken(token) });
202
220
  }
203
221
  const resolved = this.resolveInternal(target, state, singletonAncestor);
204
222
  // Only a TRANSIENT target has no owner of its own; a cached alias
@@ -224,7 +242,9 @@ export class ContainerResolver {
224
242
  const ctor = provider.useClass;
225
243
  return { value: new ctor(...args), owned: true };
226
244
  }
227
- throw new Error("Unsupported container provider.");
245
+ throw new ContainerError("Unsupported container provider.", {
246
+ token: describeToken(token),
247
+ });
228
248
  }
229
249
  catch (error) {
230
250
  // Resolution errors created deeper in the chain already carry the full
@@ -282,9 +302,21 @@ export class ContainerResolver {
282
302
  }
283
303
  case RegistryOperation.CLEAR:
284
304
  case RegistryOperation.RESTORE: {
305
+ // Every cached token is discarded by a wholesale change, so every
306
+ // one of them must be reported as invalidated — not just the
307
+ // singletons. Only REPLACE/REMOVE used to notify, so a live scope
308
+ // went on serving (and never disposed) the SCOPED instance built
309
+ // from a registration that clear()/restore() had thrown away.
310
+ const invalidated = new Set([
311
+ ...this.singletonCache.keys(),
312
+ ...this.scopedTokens,
313
+ ]);
285
314
  for (const t of [...this.singletonCache.keys()])
286
315
  this.evictSingleton(t);
287
316
  this.dependents.clear();
317
+ this.scopedTokens.clear();
318
+ for (const t of invalidated)
319
+ this.onTokenInvalidated?.(t);
288
320
  break;
289
321
  }
290
322
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/container",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Token-based dependency injection container for managing application dependencies and service lifetimes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "!dist/.tsbuildinfo"
19
19
  ],
20
20
  "dependencies": {
21
- "@zudojs/errors": "1.1.0"
21
+ "@zudojs/errors": "1.2.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "7.0.2",