@zudojs/container 1.1.1 → 1.2.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 CHANGED
@@ -47,6 +47,21 @@ container.registerFactory(API, (db) => new Api(db), [DB]); // deps via inject li
47
47
  container.registerExisting("db-alias", DB); // alias to another token
48
48
  ```
49
49
 
50
+ A factory's parameters are typed from its `inject` list, in order: with
51
+ `DB = createToken<Db>("Db")`, `(db) => new Api(db)` receives `db: Db`, and a
52
+ factory whose parameters do not match the tokens is a compile error. Class
53
+ tokens type the same way; plain string and symbol tokens give `unknown`.
54
+ `factoryProvider(factory, inject)` and `provideFactory(token, factory,
55
+ inject)` infer the same way.
56
+
57
+ ```typescript
58
+ container.registerFactory(
59
+ REPORT,
60
+ (db, clock) => new Report(db, clock), // db: Db, clock: Clock
61
+ [DB, Clock],
62
+ );
63
+ ```
64
+
50
65
  ## Tokens
51
66
 
52
67
  Tokens can be strings, symbols, classes, or `InjectionToken`s from
@@ -58,6 +73,9 @@ Tokens can be strings, symbols, classes, or `InjectionToken`s from
58
73
  - `createGlobalToken(key)` uses `Symbol.for(key)`: any other call with the
59
74
  same key anywhere in the process yields the **same** token. That is by
60
75
  design for cross-package sharing; namespace your keys (e.g. `"myapp:db"`).
76
+ - Error messages and default registration names show a symbol token by its
77
+ description: `createToken<Db>("Database")` appears as `Database`, not
78
+ `Symbol(Database)` (`describeToken(token)` returns the same string).
61
79
 
62
80
  ## Lifetimes (`scope` option)
63
81
 
@@ -185,7 +203,13 @@ const container = createContainer({
185
203
  registers it on the fly as `TRANSIENT`. When registrations are frozen the
186
204
  class is instantiated _ephemerally_ without being registered. Set to
187
205
  `false` to require explicit registration (`canResolve`/`resolveOptional`
188
- respect this).
206
+ respect this). Only a class whose constructor declares no required
207
+ parameters (`Class.length === 0`) is auto-registered: auto-registration
208
+ has no inject list, so `resolve(NeedsDep)` for
209
+ `constructor(dep: Dep)` throws `RegistrationNotFoundError` naming the
210
+ class and telling you to register it with an `inject` list, instead of
211
+ building it with `dep = undefined`. Parameters with defaults do not
212
+ count.
189
213
  - `detectCircularDependencies` — circular chains throw
190
214
  `CircularDependencyError` with the full chain. When disabled,
191
215
  `maxResolutionDepth` still stops runaway recursion.
@@ -12,7 +12,7 @@
12
12
  * - TRANSIENT instances are never cached and never tracked — callers own
13
13
  * their disposal.
14
14
  */
15
- import type { ContainerProvider, ProviderToken } from "../containerProvider/containerProvider.core.js";
15
+ import type { ContainerProvider, InjectedFactory, ProviderToken } from "../containerProvider/containerProvider.core.js";
16
16
  import type { ContainerRegistration, CreateRegistrationOptions, RegistrationToken, ResolvedTokens } from "../containerRegistration/containerRegistration.core.js";
17
17
  import type { ResolutionCache, ResolutionResult } from "../containerResolution/containerResolution.type.js";
18
18
  import type { ContainerOptions, ResolvedContainerOptions } from "../containerOptions/containerOptions.type.js";
@@ -51,7 +51,14 @@ export declare class Container implements ContainerLike {
51
51
  * any other lifetime would be misleading.
52
52
  */
53
53
  registerValue<T>(token: RegistrationToken<T>, value: T, options?: CreateRegistrationOptions): ContainerRegistration<T>;
54
- registerFactory<T>(token: RegistrationToken<T>, factory: (...deps: unknown[]) => T, inject?: readonly ProviderToken[], options?: CreateRegistrationOptions): ContainerRegistration<T>;
54
+ /**
55
+ * Registers a factory. Its parameters are typed from the `inject` tokens,
56
+ * in order: `registerFactory(API, (db) => new Api(db), [DB])` types `db`
57
+ * as `Db` when `DB` is a `createToken<Db>()` or class token, and rejects a
58
+ * factory whose parameters do not match. String/symbol tokens give
59
+ * `unknown`.
60
+ */
61
+ registerFactory<T, const Deps extends readonly ProviderToken[] = readonly []>(token: RegistrationToken<T>, factory: InjectedFactory<T, Deps>, inject?: Deps, options?: CreateRegistrationOptions): ContainerRegistration<T>;
55
62
  registerExisting<T>(token: RegistrationToken<T>, existing: ProviderToken<T>, options?: CreateRegistrationOptions): ContainerRegistration<T>;
56
63
  /**
57
64
  * Resolves a dependency at the container root.
@@ -77,7 +84,10 @@ export declare class Container implements ContainerLike {
77
84
  getRegistration<T>(token: RegistrationToken<T>): ContainerRegistration<T> | undefined;
78
85
  replace<T>(token: RegistrationToken<T>, provider: ContainerProvider<T>, options?: CreateRegistrationOptions): ContainerRegistration<T>;
79
86
  remove<T>(token: RegistrationToken<T>): boolean;
80
- /** Removes every registration (evicting and disposing cached singletons). */
87
+ /**
88
+ * Removes every registration, evicting and disposing cached instances:
89
+ * container-owned singletons and every live scope's SCOPED copies alike.
90
+ */
81
91
  clearRegistrations(): void;
82
92
  createScope(options?: ContainerScopeOptions): ContainerScopeContext;
83
93
  getRegistrations(): readonly ContainerRegistration[];
@@ -91,8 +101,10 @@ export declare class Container implements ContainerLike {
91
101
  snapshot(): readonly ContainerRegistration[];
92
102
  /**
93
103
  * 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.
104
+ * Entries are validated, cached instances built from the old set are
105
+ * evicted and disposed — container-owned singletons and every live
106
+ * scope's SCOPED copies alike — and the operation is refused when
107
+ * registrations are frozen.
96
108
  */
97
109
  restoreSnapshot(registrations: readonly ContainerRegistration[]): void;
98
110
  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;
@@ -87,6 +87,13 @@ export class Container {
87
87
  scope: Scope.SINGLETON,
88
88
  });
89
89
  }
90
+ /**
91
+ * Registers a factory. Its parameters are typed from the `inject` tokens,
92
+ * in order: `registerFactory(API, (db) => new Api(db), [DB])` types `db`
93
+ * as `Db` when `DB` is a `createToken<Db>()` or class token, and rejects a
94
+ * factory whose parameters do not match. String/symbol tokens give
95
+ * `unknown`.
96
+ */
90
97
  registerFactory(token, factory, inject = [], options = {}) {
91
98
  return this.register(token, factoryProvider(factory, inject), options);
92
99
  }
@@ -147,7 +154,10 @@ export class Container {
147
154
  this.ensureMutable();
148
155
  return this.#registry.remove(token);
149
156
  }
150
- /** Removes every registration (evicting and disposing cached singletons). */
157
+ /**
158
+ * Removes every registration, evicting and disposing cached instances:
159
+ * container-owned singletons and every live scope's SCOPED copies alike.
160
+ */
151
161
  clearRegistrations() {
152
162
  this.ensureMutable();
153
163
  this.#registry.clear();
@@ -155,7 +165,7 @@ export class Container {
155
165
  createScope(options = {}) {
156
166
  this.ensureActive();
157
167
  if (!this.options.allowScopes)
158
- throw new Error(`Container scopes are disabled for "${this.name}".`);
168
+ throw new ContainerError(`Container scopes are disabled for "${this.name}".`);
159
169
  const scope = new ContainerScopeContext(this, options);
160
170
  this.#liveScopes.add(scope);
161
171
  return scope;
@@ -182,8 +192,10 @@ export class Container {
182
192
  }
183
193
  /**
184
194
  * 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.
195
+ * Entries are validated, cached instances built from the old set are
196
+ * evicted and disposed — container-owned singletons and every live
197
+ * scope's SCOPED copies alike — and the operation is refused when
198
+ * registrations are frozen.
187
199
  */
188
200
  restoreSnapshot(registrations) {
189
201
  this.ensureMutable();
@@ -318,14 +330,14 @@ export class Container {
318
330
  }
319
331
  ensureNotDisposed() {
320
332
  if (this.#disposed)
321
- throw new Error(`Container "${this.name}" has already been disposed.`);
333
+ throw new ContainerLifecycleError("dispose", `Container "${this.name}" has already been disposed.`);
322
334
  }
323
335
  ensureMutable() {
324
336
  this.ensureNotDisposed();
325
337
  if (!this.options.freezeRegistrations)
326
338
  return;
327
339
  if (this.#started)
328
- throw new Error(`Registrations for container "${this.name}" are frozen.`);
340
+ throw new ContainerError(`Registrations for container "${this.name}" are frozen.`);
329
341
  }
330
342
  }
331
343
  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
  }
@@ -4,6 +4,17 @@
4
4
  */
5
5
  import type { Constructor, InjectionToken, Token } from "../containerToken/containerToken.type.js";
6
6
  export type ProviderToken<T = unknown> = Token<T> | InjectionToken<T>;
7
+ /**
8
+ * The parameter list a factory receives for an `inject` list: each token is
9
+ * mapped to the type it resolves to, so `[DB, Clock]` gives `[Db, Clock]`.
10
+ * Untyped string/symbol tokens map to `unknown`, and a non-tuple
11
+ * `readonly ProviderToken[]` gives `unknown[]`, as before.
12
+ */
13
+ export type InjectedDependencies<Deps extends readonly ProviderToken[]> = {
14
+ -readonly [K in keyof Deps]: Deps[K] extends ProviderToken<infer U> ? U : never;
15
+ };
16
+ /** A factory whose parameters are typed from its `inject` list. */
17
+ export type InjectedFactory<T, Deps extends readonly ProviderToken[]> = (...dependencies: InjectedDependencies<Deps>) => T;
7
18
  export interface ClassProvider<T> {
8
19
  readonly useClass: Constructor<T>;
9
20
  /**
@@ -55,11 +66,19 @@ export declare function hasInjectedDependencies<T = unknown>(provider: Provider<
55
66
  readonly inject: readonly ProviderToken[];
56
67
  };
57
68
  export declare function classProvider<T>(useClass: Constructor<T>, inject?: readonly ProviderToken[]): ClassProvider<T>;
58
- export declare function factoryProvider<T>(useFactory: (...dependencies: unknown[]) => T, inject?: readonly ProviderToken[]): FactoryProvider<T>;
69
+ /**
70
+ * Builds a factory provider. The factory's parameters are inferred from the
71
+ * `inject` tokens, in order.
72
+ */
73
+ export declare function factoryProvider<T, const Deps extends readonly ProviderToken[] = readonly []>(useFactory: InjectedFactory<T, Deps>, inject?: Deps): FactoryProvider<T>;
59
74
  export declare function valueProvider<T>(useValue: T): ValueProvider<T>;
60
75
  export declare function existingProvider<T>(useExisting: ProviderToken<T>): ExistingProvider<T>;
61
76
  export declare function provideClass<T>(provide: ProviderToken<T>, useClass: Constructor<T>, inject?: readonly ProviderToken[]): ClassRegistration<T>;
62
- export declare function provideFactory<T>(provide: ProviderToken<T>, useFactory: (...dependencies: unknown[]) => T, inject?: readonly ProviderToken[]): FactoryRegistration<T>;
77
+ /**
78
+ * Builds a factory registration. The factory's parameters are inferred from
79
+ * the `inject` tokens, in order.
80
+ */
81
+ export declare function provideFactory<T, const Deps extends readonly ProviderToken[] = readonly []>(provide: ProviderToken<T>, useFactory: InjectedFactory<T, Deps>, inject?: Deps): FactoryRegistration<T>;
63
82
  export declare function provideValue<T>(provide: ProviderToken<T>, useValue: T): ValueRegistration<T>;
64
83
  export declare function provideExisting<T>(provide: ProviderToken<T>, useExisting: ProviderToken<T>): ExistingRegistration<T>;
65
84
  export declare function getProviderToken<T>(provider: ContainerProvider<T>): ProviderToken<T> | undefined;
@@ -25,8 +25,15 @@ export function hasInjectedDependencies(provider) {
25
25
  export function classProvider(useClass, inject = []) {
26
26
  return Object.freeze({ useClass, inject: Object.freeze([...inject]) });
27
27
  }
28
+ /**
29
+ * Builds a factory provider. The factory's parameters are inferred from the
30
+ * `inject` tokens, in order.
31
+ */
28
32
  export function factoryProvider(useFactory, inject = []) {
29
- return Object.freeze({ useFactory, inject: Object.freeze([...inject]) });
33
+ return Object.freeze({
34
+ useFactory: useFactory,
35
+ inject: Object.freeze([...inject]),
36
+ });
30
37
  }
31
38
  export function valueProvider(useValue) {
32
39
  return Object.freeze({ useValue });
@@ -41,10 +48,14 @@ export function provideClass(provide, useClass, inject = []) {
41
48
  inject: Object.freeze([...inject]),
42
49
  });
43
50
  }
51
+ /**
52
+ * Builds a factory registration. The factory's parameters are inferred from
53
+ * the `inject` tokens, in order.
54
+ */
44
55
  export function provideFactory(provide, useFactory, inject = []) {
45
56
  return Object.freeze({
46
57
  provide,
47
- useFactory,
58
+ useFactory: useFactory,
48
59
  inject: Object.freeze([...inject]),
49
60
  });
50
61
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @zudojs/container/containerResolution/containerResolution.autoRegister
3
+ *
4
+ * Guards for `autoRegisterClasses`: resolving an unregistered class builds
5
+ * it with no arguments, which is only correct for a constructor that
6
+ * declares none.
7
+ */
8
+ /**
9
+ * Whether an unregistered token may be auto-registered: a class whose
10
+ * constructor declares no required parameters (`Class.length === 0`).
11
+ *
12
+ * Parameters with a default value do not count, so
13
+ * `constructor(value = 7)` still qualifies.
14
+ */
15
+ export declare function isAutoRegistrable(token: unknown): boolean;
16
+ /**
17
+ * Throws when a class cannot be auto-registered because its constructor
18
+ * declares parameters. Auto-registration has no inject list, so building
19
+ * it would pass `undefined` for every dependency.
20
+ *
21
+ * @throws {RegistrationNotFoundError} naming the class and how to
22
+ * register it.
23
+ */
24
+ export declare function assertAutoRegistrable(token: {
25
+ readonly length: number;
26
+ }): void;
27
+ //# sourceMappingURL=containerResolution.autoRegister.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @zudojs/container/containerResolution/containerResolution.autoRegister
3
+ *
4
+ * Guards for `autoRegisterClasses`: resolving an unregistered class builds
5
+ * it with no arguments, which is only correct for a constructor that
6
+ * declares none.
7
+ */
8
+ import { RegistrationNotFoundError } from "@zudojs/errors";
9
+ import { describeToken } from "../containerToken/containerToken.type.js";
10
+ /**
11
+ * Whether an unregistered token may be auto-registered: a class whose
12
+ * constructor declares no required parameters (`Class.length === 0`).
13
+ *
14
+ * Parameters with a default value do not count, so
15
+ * `constructor(value = 7)` still qualifies.
16
+ */
17
+ export function isAutoRegistrable(token) {
18
+ return typeof token === "function" && token.length === 0;
19
+ }
20
+ /**
21
+ * Throws when a class cannot be auto-registered because its constructor
22
+ * declares parameters. Auto-registration has no inject list, so building
23
+ * it would pass `undefined` for every dependency.
24
+ *
25
+ * @throws {RegistrationNotFoundError} naming the class and how to
26
+ * register it.
27
+ */
28
+ export function assertAutoRegistrable(token) {
29
+ if (isAutoRegistrable(token))
30
+ return;
31
+ const name = describeToken(token);
32
+ const count = token.length;
33
+ throw new RegistrationNotFoundError(name, `Cannot auto-register class "${name}": its constructor declares ` +
34
+ `${count} parameter${count === 1 ? "" : "s"} and no inject list is ` +
35
+ `registered, so it would be built with undefined dependencies. ` +
36
+ `Register it with container.registerClass(${name}, ${name}, ` +
37
+ `{ inject: [/* one token per parameter */] }) or ` +
38
+ `container.registerFactory(${name}, (...deps) => new ${name}(...deps), [/* tokens */]).`);
39
+ }
40
+ //# sourceMappingURL=containerResolution.autoRegister.js.map
@@ -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,16 +22,20 @@
22
22
  * The resolver subscribes to registry change events: REPLACE/REMOVE evict the
23
23
  * affected token's cached singleton, CLEAR/RESTORE evict all cached
24
24
  * singletons. Each eviction is reported through the `onSingletonEvicted`
25
- * callback so the owning container can dispose the instance.
25
+ * callback so the owning container can dispose the instance, and every
26
+ * invalidated token — for CLEAR/RESTORE that is every cached token, SCOPED
27
+ * ones included — through `onTokenInvalidated` so live scopes drop and
28
+ * dispose their own copies.
26
29
  */
27
30
  import { isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, normalizeProvider, } from "../containerProvider/containerProvider.core.js";
28
31
  import { ContainerScope as Scope } from "../containerScope/containerScope.type.js";
29
32
  import { defineRegistration, getRegistrationToken, } from "../containerRegistration/containerRegistration.core.js";
30
33
  import { RegistryOperation } from "../containerRegistry/containerRegistry.type.js";
31
34
  import { unwrapToken } from "../containerToken/containerToken.type.js";
32
- import { CircularDependencyError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
35
+ import { CircularDependencyError, ContainerError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
33
36
  import { AsyncProviderError, CaptiveDependencyError, DependencyResolutionError, MaxResolutionDepthError, ScopedResolutionError, } from "./containerResolution.error.js";
34
37
  import { describeToken } from "../containerToken/containerToken.type.js";
38
+ import { assertAutoRegistrable, isAutoRegistrable, } from "./containerResolution.autoRegister.js";
35
39
  import { DependentIndex } from "./containerResolution.dependents.js";
36
40
  /**
37
41
  * Scope cache that falls back to its parent scope's cache for lookups while
@@ -69,12 +73,24 @@ export class ContainerResolver {
69
73
  onSingletonEvicted;
70
74
  onTokenInvalidated;
71
75
  dependents = new DependentIndex();
76
+ /**
77
+ * Tokens for which a SCOPED instance has ever been cached in a scope.
78
+ *
79
+ * The singleton cache only knows about SINGLETON tokens and the
80
+ * dependent index only records tokens consumed by another cached
81
+ * instance, so neither can name a SCOPED token that a scope resolved
82
+ * directly. A wholesale CLEAR/RESTORE must still tell live scopes to
83
+ * drop those instances.
84
+ */
85
+ scopedTokens = new Set();
72
86
  /**
73
87
  * @param onSingletonEvicted Called for each evicted cached singleton so
74
88
  * the owner can dispose it.
75
89
  * @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.
90
+ * registry change — for `replace()`/`remove()` the token itself and
91
+ * each cached consumer, for `clear()`/`restore()` every token that
92
+ * was cached at all — so owners of scope caches can drop and dispose
93
+ * their SCOPED copies.
78
94
  */
79
95
  constructor(registry, onSingletonEvicted, onTokenInvalidated) {
80
96
  this.registry = registry;
@@ -109,6 +125,7 @@ export class ContainerResolver {
109
125
  let registration = this.registry.get(token);
110
126
  if (!registration) {
111
127
  if (state.autoRegisterClasses && typeof token === "function") {
128
+ assertAutoRegistrable(token);
112
129
  if (state.allowRegistration) {
113
130
  registration = this.registry.register(token, { useClass: token }, { scope: Scope.TRANSIENT });
114
131
  }
@@ -165,8 +182,11 @@ export class ContainerResolver {
165
182
  }
166
183
  if (registration.scope === Scope.SINGLETON)
167
184
  this.singletonCache.set(token, value);
168
- else if (registration.scope === Scope.SCOPED)
185
+ else if (registration.scope === Scope.SCOPED) {
169
186
  state.scopeCache?.set(token, value);
187
+ if (state.scopeCache)
188
+ this.scopedTokens.add(token);
189
+ }
170
190
  const result = {
171
191
  value,
172
192
  token,
@@ -196,9 +216,9 @@ export class ContainerResolver {
196
216
  if (isExistingProvider(provider)) {
197
217
  const target = unwrapToken(provider.useExisting);
198
218
  if (!this.registry.has(target) &&
199
- !(state.autoRegisterClasses && typeof target === "function")) {
200
- throw new Error(`useExisting target "${describeToken(target)}" for token ` +
201
- `"${describeToken(token)}" is not registered.`);
219
+ !(state.autoRegisterClasses && isAutoRegistrable(target))) {
220
+ throw new ContainerError(`useExisting target "${describeToken(target)}" for token ` +
221
+ `"${describeToken(token)}" is not registered.`, { token: describeToken(token) });
202
222
  }
203
223
  const resolved = this.resolveInternal(target, state, singletonAncestor);
204
224
  // Only a TRANSIENT target has no owner of its own; a cached alias
@@ -224,7 +244,9 @@ export class ContainerResolver {
224
244
  const ctor = provider.useClass;
225
245
  return { value: new ctor(...args), owned: true };
226
246
  }
227
- throw new Error("Unsupported container provider.");
247
+ throw new ContainerError("Unsupported container provider.", {
248
+ token: describeToken(token),
249
+ });
228
250
  }
229
251
  catch (error) {
230
252
  // Resolution errors created deeper in the chain already carry the full
@@ -263,7 +285,7 @@ export class ContainerResolver {
263
285
  }
264
286
  canResolve(token, autoRegisterClasses = true) {
265
287
  const t = unwrapToken(token);
266
- return (this.registry.has(t) || (autoRegisterClasses && typeof t === "function"));
288
+ return (this.registry.has(t) || (autoRegisterClasses && isAutoRegistrable(t)));
267
289
  }
268
290
  handleRegistryChange(event) {
269
291
  switch (event.operation) {
@@ -282,9 +304,21 @@ export class ContainerResolver {
282
304
  }
283
305
  case RegistryOperation.CLEAR:
284
306
  case RegistryOperation.RESTORE: {
307
+ // Every cached token is discarded by a wholesale change, so every
308
+ // one of them must be reported as invalidated — not just the
309
+ // singletons. Only REPLACE/REMOVE used to notify, so a live scope
310
+ // went on serving (and never disposed) the SCOPED instance built
311
+ // from a registration that clear()/restore() had thrown away.
312
+ const invalidated = new Set([
313
+ ...this.singletonCache.keys(),
314
+ ...this.scopedTokens,
315
+ ]);
285
316
  for (const t of [...this.singletonCache.keys()])
286
317
  this.evictSingleton(t);
287
318
  this.dependents.clear();
319
+ this.scopedTokens.clear();
320
+ for (const t of invalidated)
321
+ this.onTokenInvalidated?.(t);
288
322
  break;
289
323
  }
290
324
  default:
@@ -44,5 +44,13 @@ export declare function isInjectionToken<T = unknown>(value: unknown): value is
44
44
  export declare function isConstructorToken<T = unknown>(token: Token<T>): token is Constructor<T>;
45
45
  export declare function isSymbolToken<T = unknown>(token: Token<T>): token is symbol;
46
46
  export declare function isStringToken<T = unknown>(token: Token<T>): token is string;
47
+ /**
48
+ * Returns a human-readable name for a token, used in error messages and as
49
+ * the default registration name.
50
+ *
51
+ * A symbol token (including every `createToken`/`createGlobalToken` token)
52
+ * is shown by its description, so errors read `MissingService` rather than
53
+ * `Symbol(MissingService)`. A symbol without a description is `Symbol()`.
54
+ */
47
55
  export declare function describeToken<T>(token: Token<T> | InjectionToken<T>): string;
48
56
  //# sourceMappingURL=containerToken.type.d.ts.map
@@ -48,14 +48,20 @@ export function isSymbolToken(token) {
48
48
  export function isStringToken(token) {
49
49
  return typeof token === "string";
50
50
  }
51
+ /**
52
+ * Returns a human-readable name for a token, used in error messages and as
53
+ * the default registration name.
54
+ *
55
+ * A symbol token (including every `createToken`/`createGlobalToken` token)
56
+ * is shown by its description, so errors read `MissingService` rather than
57
+ * `Symbol(MissingService)`. A symbol without a description is `Symbol()`.
58
+ */
51
59
  export function describeToken(token) {
52
60
  const resolved = unwrapToken(token);
53
61
  if (typeof resolved === "string")
54
62
  return resolved;
55
63
  if (typeof resolved === "symbol")
56
- return resolved.description
57
- ? `Symbol(${resolved.description})`
58
- : "Symbol()";
64
+ return resolved.description ? resolved.description : "Symbol()";
59
65
  if (typeof resolved === "function")
60
66
  return resolved.name || "AnonymousConstructor";
61
67
  return "UnknownToken";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/container",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
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,11 +18,11 @@
18
18
  "!dist/.tsbuildinfo"
19
19
  ],
20
20
  "dependencies": {
21
- "@zudojs/errors": "1.1.0"
21
+ "@zudojs/errors": "1.3.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "7.0.2",
25
- "vitest": "^4.1.11"
25
+ "vitest": "^5.0.1"
26
26
  },
27
27
  "license": "MIT",
28
28
  "author": {
@@ -41,7 +41,7 @@
41
41
  "dependency-injection",
42
42
  "container"
43
43
  ],
44
- "homepage": "https://github.com/oyinlola-tech/zudo#readme",
44
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-container",
45
45
  "bugs": {
46
46
  "url": "https://github.com/oyinlola-tech/zudo/issues"
47
47
  },