@zudojs/container 0.1.0 → 1.1.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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -8
  3. package/dist/containerCore/containerCore.core.d.ts +112 -14
  4. package/dist/containerCore/containerCore.core.js +228 -46
  5. package/dist/containerCore/containerCore.scope.d.ts +66 -18
  6. package/dist/containerCore/containerCore.scope.js +120 -31
  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 +60 -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 +51 -10
  23. package/dist/containerResolution/containerResolution.core.js +255 -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/dist/index.d.ts +1 -0
  32. package/dist/index.js +3 -0
  33. package/package.json +25 -12
  34. package/dist/.tsbuildinfo +0 -1
  35. package/dist/containerCore/containerCore.core.d.ts.map +0 -1
  36. package/dist/containerCore/containerCore.core.js.map +0 -1
  37. package/dist/containerCore/containerCore.scope.d.ts.map +0 -1
  38. package/dist/containerCore/containerCore.scope.js.map +0 -1
  39. package/dist/containerCore/containerCore.type.d.ts.map +0 -1
  40. package/dist/containerCore/containerCore.type.js.map +0 -1
  41. package/dist/containerCore/index.d.ts.map +0 -1
  42. package/dist/containerCore/index.js.map +0 -1
  43. package/dist/containerLifecycle/containerLifecycle.core.d.ts.map +0 -1
  44. package/dist/containerLifecycle/containerLifecycle.core.js.map +0 -1
  45. package/dist/containerLifecycle/index.d.ts.map +0 -1
  46. package/dist/containerLifecycle/index.js.map +0 -1
  47. package/dist/containerOptions/containerOptions.type.d.ts.map +0 -1
  48. package/dist/containerOptions/containerOptions.type.js.map +0 -1
  49. package/dist/containerOptions/index.d.ts.map +0 -1
  50. package/dist/containerOptions/index.js.map +0 -1
  51. package/dist/containerProvider/containerProvider.core.d.ts.map +0 -1
  52. package/dist/containerProvider/containerProvider.core.js.map +0 -1
  53. package/dist/containerProvider/index.d.ts.map +0 -1
  54. package/dist/containerProvider/index.js.map +0 -1
  55. package/dist/containerRegistration/containerRegistration.core.d.ts.map +0 -1
  56. package/dist/containerRegistration/containerRegistration.core.js.map +0 -1
  57. package/dist/containerRegistration/index.d.ts.map +0 -1
  58. package/dist/containerRegistration/index.js.map +0 -1
  59. package/dist/containerRegistry/containerRegistry.core.d.ts.map +0 -1
  60. package/dist/containerRegistry/containerRegistry.core.js.map +0 -1
  61. package/dist/containerRegistry/containerRegistry.error.d.ts +0 -12
  62. package/dist/containerRegistry/containerRegistry.error.d.ts.map +0 -1
  63. package/dist/containerRegistry/containerRegistry.error.js +0 -22
  64. package/dist/containerRegistry/containerRegistry.error.js.map +0 -1
  65. package/dist/containerRegistry/containerRegistry.type.d.ts.map +0 -1
  66. package/dist/containerRegistry/containerRegistry.type.js.map +0 -1
  67. package/dist/containerRegistry/index.d.ts.map +0 -1
  68. package/dist/containerRegistry/index.js.map +0 -1
  69. package/dist/containerResolution/containerResolution.core.d.ts.map +0 -1
  70. package/dist/containerResolution/containerResolution.core.js.map +0 -1
  71. package/dist/containerResolution/containerResolution.error.d.ts.map +0 -1
  72. package/dist/containerResolution/containerResolution.error.js.map +0 -1
  73. package/dist/containerResolution/containerResolution.type.d.ts.map +0 -1
  74. package/dist/containerResolution/containerResolution.type.js.map +0 -1
  75. package/dist/containerResolution/index.d.ts.map +0 -1
  76. package/dist/containerResolution/index.js.map +0 -1
  77. package/dist/containerScope/containerScope.type.d.ts.map +0 -1
  78. package/dist/containerScope/containerScope.type.js.map +0 -1
  79. package/dist/containerScope/index.d.ts.map +0 -1
  80. package/dist/containerScope/index.js.map +0 -1
  81. package/dist/containerToken/containerToken.type.d.ts.map +0 -1
  82. package/dist/containerToken/containerToken.type.js.map +0 -1
  83. package/dist/containerToken/index.d.ts.map +0 -1
  84. package/dist/containerToken/index.js.map +0 -1
  85. package/dist/index.d.ts.map +0 -1
  86. package/dist/index.js.map +0 -1
@@ -1,51 +1,117 @@
1
1
  /**
2
2
  * Dependency resolver for Zudojs.
3
+ *
4
+ * Lifetime semantics:
5
+ * - SINGLETON instances are always looked up in and stored to the resolver's
6
+ * root singleton cache, no matter where the resolution happens (root or
7
+ * scope). A scope can therefore never re-create or shadow a singleton.
8
+ * - SCOPED instances live in the scope cache supplied via
9
+ * `ResolutionOptions.cache`. Resolving a SCOPED registration without a
10
+ * scope cache throws a ScopedResolutionError; a SCOPED dependency of a
11
+ * SINGLETON throws a CaptiveDependencyError.
12
+ * - TRANSIENT instances are never cached.
13
+ *
14
+ * Every freshly created instance (any lifetime, never a cache hit) is
15
+ * reported through `ResolutionOptions.onInstanceCreated` in creation order,
16
+ * so owners can track transitively created dependencies for disposal — not
17
+ * just the top-level result of a `resolve()` call.
18
+ *
19
+ * Factories registered as SINGLETON or SCOPED must be synchronous: a Promise
20
+ * result throws an AsyncProviderError instead of being cached as the instance.
21
+ *
22
+ * The resolver subscribes to registry change events: REPLACE/REMOVE evict the
23
+ * affected token's cached singleton, CLEAR/RESTORE evict all cached
24
+ * singletons. Each eviction is reported through the `onSingletonEvicted`
25
+ * callback so the owning container can dispose the instance.
3
26
  */
4
27
  import { isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, normalizeProvider, } from "../containerProvider/containerProvider.core.js";
5
28
  import { ContainerScope as Scope } from "../containerScope/containerScope.type.js";
6
- import { getRegistrationToken } from "../containerRegistration/containerRegistration.core.js";
7
- import { RegistrationNotFoundError } from "@zudojs/errors";
8
- import { describeRegistryToken } from "../containerRegistry/containerRegistry.error.js";
29
+ import { defineRegistration, getRegistrationToken, } from "../containerRegistration/containerRegistration.core.js";
30
+ import { RegistryOperation } from "../containerRegistry/containerRegistry.type.js";
9
31
  import { unwrapToken } from "../containerToken/containerToken.type.js";
10
- import { CircularDependencyError, ProviderResolutionError, } from "@zudojs/errors";
32
+ import { CircularDependencyError, ProviderResolutionError, RegistrationNotFoundError, } from "@zudojs/errors";
33
+ import { AsyncProviderError, CaptiveDependencyError, DependencyResolutionError, MaxResolutionDepthError, ScopedResolutionError, } from "./containerResolution.error.js";
11
34
  import { describeToken } from "../containerToken/containerToken.type.js";
35
+ /**
36
+ * Scope cache that falls back to its parent scope's cache for lookups while
37
+ * writing only to its own map. Nested scopes therefore see SCOPED instances
38
+ * already created by their ancestors, while instances they create themselves
39
+ * stay private to (and are disposed with) the nested scope.
40
+ */
41
+ class ChainedResolutionCache {
42
+ #own = new Map();
43
+ #parent;
44
+ constructor(parent) {
45
+ this.#parent = parent;
46
+ }
47
+ has(token) {
48
+ return this.#own.has(token) || (this.#parent?.has(token) ?? false);
49
+ }
50
+ get(token) {
51
+ if (this.#own.has(token))
52
+ return this.#own.get(token);
53
+ return this.#parent?.get(token);
54
+ }
55
+ set(token, value) {
56
+ this.#own.set(token, value);
57
+ }
58
+ clear() {
59
+ this.#own.clear();
60
+ }
61
+ }
12
62
  export class ContainerResolver {
13
63
  registry;
14
- singletonCache;
15
- constructor(registry) {
64
+ singletonCache = new Map();
65
+ onSingletonEvicted;
66
+ constructor(registry, onSingletonEvicted) {
16
67
  this.registry = registry;
17
- this.singletonCache = new Map();
68
+ this.onSingletonEvicted = onSingletonEvicted;
69
+ registry.subscribe((event) => this.handleRegistryChange(event));
18
70
  }
19
71
  resolve(token, options = {}) {
20
72
  return this.resolveDetailed(token, options).value;
21
73
  }
22
74
  resolveDetailed(token, options = {}) {
23
75
  const normalized = unwrapToken(token);
24
- const cache = options.cache ?? this.singletonCache;
25
- const path = options.path ?? [];
26
- return this.resolveInternal(normalized, cache, path, options);
27
- }
28
- resolveInternal(token, cache, path, options) {
29
- if (path.includes(token))
30
- throw new CircularDependencyError([...path, token].map((t) => describeToken(t)));
31
- const registration = this.registry.get(token);
76
+ const path = [...(options.path ?? [])];
77
+ const state = {
78
+ scopeCache: options.cache,
79
+ autoRegisterClasses: options.autoRegisterClasses ?? true,
80
+ allowRegistration: options.allowRegistration ?? true,
81
+ detectCircularDependencies: options.detectCircularDependencies ?? true,
82
+ maxResolutionDepth: options.maxResolutionDepth ?? 100,
83
+ onInstanceCreated: options.onInstanceCreated,
84
+ path,
85
+ pathSet: new Set(path),
86
+ };
87
+ return this.resolveInternal(normalized, state, undefined);
88
+ }
89
+ resolveInternal(token, state, singletonAncestor) {
90
+ const depth = state.path.length + 1;
91
+ if (depth > state.maxResolutionDepth)
92
+ throw new MaxResolutionDepthError(describeToken(token), depth, state.maxResolutionDepth, [...state.path, token].map((t) => describeToken(t)));
93
+ if (state.detectCircularDependencies && state.pathSet.has(token))
94
+ throw new CircularDependencyError([...state.path, token].map((t) => describeToken(t)));
95
+ let registration = this.registry.get(token);
32
96
  if (!registration) {
33
- if (options.autoRegisterClasses !== false &&
34
- typeof token === "function") {
35
- this.registry.register(token, { useClass: token }, { scope: Scope.TRANSIENT });
36
- return this.resolveInternal(token, cache, path, {
37
- ...options,
38
- autoRegisterClasses: false,
39
- });
97
+ if (state.autoRegisterClasses && typeof token === "function") {
98
+ if (state.allowRegistration) {
99
+ registration = this.registry.register(token, { useClass: token }, { scope: Scope.TRANSIENT });
100
+ }
101
+ else {
102
+ // Registrations are frozen: instantiate ephemerally, do not register.
103
+ registration = defineRegistration(token, { useClass: token }, { scope: Scope.TRANSIENT });
104
+ }
105
+ }
106
+ else {
107
+ throw new RegistrationNotFoundError(describeToken(token));
40
108
  }
41
- throw new RegistrationNotFoundError(describeRegistryToken(token));
42
109
  }
43
- const currentPath = [...path, token];
44
- if (registration.scope !== Scope.TRANSIENT) {
45
- const cached = cache.get(token);
46
- if (cached !== undefined) {
110
+ const currentPath = [...state.path, token];
111
+ if (registration.scope === Scope.SINGLETON) {
112
+ if (this.singletonCache.has(token)) {
47
113
  return {
48
- value: cached,
114
+ value: this.singletonCache.get(token),
49
115
  token,
50
116
  registration,
51
117
  scope: registration.scope,
@@ -54,12 +120,39 @@ export class ContainerResolver {
54
120
  };
55
121
  }
56
122
  }
57
- const value = this.createInstance(registration, cache, currentPath, options);
123
+ else if (registration.scope === Scope.SCOPED) {
124
+ if (singletonAncestor !== undefined)
125
+ throw new CaptiveDependencyError(describeToken(singletonAncestor), describeToken(token), currentPath.map((t) => describeToken(t)));
126
+ if (!state.scopeCache)
127
+ throw new ScopedResolutionError(describeToken(token), currentPath.map((t) => describeToken(t)));
128
+ if (state.scopeCache.has(token)) {
129
+ return {
130
+ value: state.scopeCache.get(token),
131
+ token,
132
+ registration,
133
+ scope: registration.scope,
134
+ fromCache: true,
135
+ path: currentPath,
136
+ };
137
+ }
138
+ }
139
+ const nextAncestor = registration.scope === Scope.SINGLETON ? token : singletonAncestor;
140
+ state.path.push(token);
141
+ state.pathSet.add(token);
142
+ let value;
143
+ let owned;
144
+ try {
145
+ ({ value, owned } = this.createInstance(registration, state, nextAncestor));
146
+ }
147
+ finally {
148
+ state.path.pop();
149
+ state.pathSet.delete(token);
150
+ }
58
151
  if (registration.scope === Scope.SINGLETON)
59
152
  this.singletonCache.set(token, value);
60
153
  else if (registration.scope === Scope.SCOPED)
61
- cache.set(token, value);
62
- return {
154
+ state.scopeCache?.set(token, value);
155
+ const result = {
63
156
  value,
64
157
  token,
65
158
  registration,
@@ -67,59 +160,157 @@ export class ContainerResolver {
67
160
  fromCache: false,
68
161
  path: currentPath,
69
162
  };
163
+ // A `useExisting` alias of a cached (SINGLETON/SCOPED) target does not
164
+ // own the instance it hands out — the target's own creation already
165
+ // reported it. Reporting it again registered a second owner for the same
166
+ // object: the container disposed it twice, and a SCOPED alias let a
167
+ // scope dispose a container-owned singleton.
168
+ if (owned)
169
+ state.onInstanceCreated?.(result);
170
+ return result;
70
171
  }
71
- createInstance(registration, cache, path, options) {
172
+ createInstance(registration, state, singletonAncestor) {
72
173
  const provider = normalizeProvider(registration.provider);
174
+ const token = getRegistrationToken(registration);
73
175
  try {
74
176
  if (isValueProvider(provider))
75
- return provider.useValue;
76
- if (isExistingProvider(provider))
77
- return this.resolve(provider.useExisting, { ...options, cache, path });
78
- if (isFactoryProvider(provider))
79
- return this.createFromFactory(provider, cache, path, options);
80
- if (isClassProvider(provider))
81
- return this.createFromClass(provider.useClass);
177
+ return { value: provider.useValue, owned: true };
178
+ if (isExistingProvider(provider)) {
179
+ const target = unwrapToken(provider.useExisting);
180
+ if (!this.registry.has(target) &&
181
+ !(state.autoRegisterClasses && typeof target === "function")) {
182
+ throw new Error(`useExisting target "${describeToken(target)}" for token ` +
183
+ `"${describeToken(token)}" is not registered.`);
184
+ }
185
+ const resolved = this.resolveInternal(target, state, singletonAncestor);
186
+ // Only a TRANSIENT target has no owner of its own; a cached alias
187
+ // of it is the one place the instance can be tracked.
188
+ return {
189
+ value: resolved.value,
190
+ owned: resolved.scope === Scope.TRANSIENT,
191
+ };
192
+ }
193
+ if (isFactoryProvider(provider)) {
194
+ const deps = provider.inject ?? [];
195
+ const args = deps.map((d) => this.resolveInternal(unwrapToken(d), state, singletonAncestor)
196
+ .value);
197
+ const produced = provider.useFactory(...args);
198
+ if (registration.scope !== Scope.TRANSIENT && isPromiseLike(produced))
199
+ throw new AsyncProviderError(describeToken(token), registration.scope);
200
+ return { value: produced, owned: true };
201
+ }
202
+ if (isClassProvider(provider)) {
203
+ const deps = provider.inject ?? [];
204
+ const args = deps.map((d) => this.resolveInternal(unwrapToken(d), state, singletonAncestor)
205
+ .value);
206
+ const ctor = provider.useClass;
207
+ return { value: new ctor(...args), owned: true };
208
+ }
82
209
  throw new Error("Unsupported container provider.");
83
210
  }
84
211
  catch (error) {
212
+ // Resolution errors created deeper in the chain already carry the full
213
+ // chain in their message/details — propagate them unchanged.
85
214
  if (error instanceof CircularDependencyError ||
86
- error instanceof ProviderResolutionError)
215
+ error instanceof ProviderResolutionError ||
216
+ error instanceof ScopedResolutionError ||
217
+ error instanceof CaptiveDependencyError ||
218
+ error instanceof MaxResolutionDepthError ||
219
+ error instanceof AsyncProviderError)
87
220
  throw error;
88
- const message = error instanceof Error ? error.message : undefined;
89
- throw new ProviderResolutionError(describeRegistryToken(getRegistrationToken(registration)), message, error);
221
+ throw new DependencyResolutionError(describeToken(token), error, state.path.map((t) => describeToken(t)));
90
222
  }
91
223
  }
92
- createFromFactory(provider, cache, path, options) {
93
- const deps = provider.inject ?? [];
94
- return provider.useFactory(...deps.map((d) => this.resolve(d, { ...options, cache, path })));
95
- }
96
- createFromClass(constructor) {
97
- return new constructor();
224
+ /**
225
+ * Creates an empty scope cache for SCOPED instances. When `parent` is
226
+ * given, lookups fall back to it (nested scope semantics) while writes stay
227
+ * local to the new cache.
228
+ */
229
+ createScope(parent) {
230
+ return new ChainedResolutionCache(parent);
98
231
  }
99
- createScope() {
100
- return new Map();
232
+ /** Tokens currently held in the singleton cache. */
233
+ getCachedSingletonTokens() {
234
+ return [...this.singletonCache.keys()];
101
235
  }
102
- clearSingletons() {
236
+ /**
237
+ * Clears the singleton cache WITHOUT invoking eviction callbacks.
238
+ * Callers are responsible for disposing the previously cached instances.
239
+ */
240
+ clearSingletonCache() {
103
241
  this.singletonCache.clear();
104
242
  }
105
- hasSingleton(token) {
106
- return this.singletonCache.has(unwrapToken(token));
243
+ resolveMany(tokens, options = {}) {
244
+ return tokens.map((t) => this.resolve(t, options));
107
245
  }
108
- getSingleton(token) {
109
- return this.singletonCache.get(unwrapToken(token));
246
+ canResolve(token, autoRegisterClasses = true) {
247
+ const t = unwrapToken(token);
248
+ return (this.registry.has(t) || (autoRegisterClasses && typeof t === "function"));
110
249
  }
111
- removeSingleton(token) {
112
- return this.singletonCache.delete(unwrapToken(token));
250
+ handleRegistryChange(event) {
251
+ switch (event.operation) {
252
+ case RegistryOperation.REPLACE:
253
+ case RegistryOperation.REMOVE: {
254
+ const token = unwrapToken(event.token);
255
+ this.evictSingleton(token);
256
+ this.evictAliasesOf(token);
257
+ break;
258
+ }
259
+ case RegistryOperation.CLEAR:
260
+ case RegistryOperation.RESTORE: {
261
+ for (const t of [...this.singletonCache.keys()])
262
+ this.evictSingleton(t);
263
+ break;
264
+ }
265
+ default:
266
+ break;
267
+ }
113
268
  }
114
- clearScope(cache) {
115
- cache.clear();
269
+ evictSingleton(token) {
270
+ if (!this.singletonCache.has(token))
271
+ return;
272
+ this.singletonCache.delete(token);
273
+ this.onSingletonEvicted?.(token);
116
274
  }
117
- resolveMany(tokens, options = {}) {
118
- return tokens.map((t) => this.resolve(t, options));
275
+ /**
276
+ * Evicts every cached singleton whose `useExisting` chain ends at `target`.
277
+ *
278
+ * A cached alias holds the target's instance under its own token, so
279
+ * evicting the target alone left the alias serving the old — by now
280
+ * disposed — instance after `replace()`/`remove()`.
281
+ */
282
+ evictAliasesOf(target) {
283
+ for (const cached of [...this.singletonCache.keys()]) {
284
+ if (cached === target)
285
+ continue;
286
+ if (this.aliasChainReaches(cached, target))
287
+ this.evictSingleton(cached);
288
+ }
119
289
  }
120
- canResolve(token) {
121
- const t = unwrapToken(token);
122
- return this.registry.has(t) || typeof t === "function";
290
+ aliasChainReaches(token, target) {
291
+ const visited = new Set();
292
+ let current = token;
293
+ for (;;) {
294
+ const registration = this.registry.get(current);
295
+ if (!registration)
296
+ return false;
297
+ const provider = normalizeProvider(registration.provider);
298
+ if (!isExistingProvider(provider))
299
+ return false;
300
+ const next = unwrapToken(provider.useExisting);
301
+ if (next === target)
302
+ return true;
303
+ if (visited.has(next))
304
+ return false;
305
+ visited.add(next);
306
+ current = next;
307
+ }
123
308
  }
124
309
  }
310
+ function isPromiseLike(value) {
311
+ return (typeof value === "object" &&
312
+ value !== null &&
313
+ "then" in value &&
314
+ typeof value.then === "function");
315
+ }
125
316
  //# sourceMappingURL=containerResolution.core.js.map
@@ -1,8 +1,73 @@
1
1
  /**
2
2
  * @zudojs/container/containerResolution/containerResolution.error
3
3
  *
4
- * Error classes re-exported from @zudojs/errors.
5
- * Token description helpers live in containerToken.type.ts.
4
+ * Resolution error classes local to this package. They extend the published
5
+ * `ContainerError` base from @zudojs/errors; the published package does not
6
+ * yet ship dedicated classes for these failure modes.
6
7
  */
7
- export {};
8
+ import { ContainerError, ProviderResolutionError } from "@zudojs/errors";
9
+ /**
10
+ * Thrown when a SCOPED registration is resolved without a scope
11
+ * (i.e. directly from the root container).
12
+ */
13
+ export declare class ScopedResolutionError extends ContainerError {
14
+ readonly chain: readonly string[];
15
+ constructor(token: string, chain?: readonly string[]);
16
+ }
17
+ /**
18
+ * Thrown when a longer-lived consumer (a SINGLETON) would capture a
19
+ * shorter-lived SCOPED dependency, freezing it beyond its scope's lifetime.
20
+ */
21
+ export declare class CaptiveDependencyError extends ContainerError {
22
+ readonly consumer: string;
23
+ readonly dependency: string;
24
+ readonly chain: readonly string[];
25
+ constructor(consumer: string, dependency: string, chain?: readonly string[]);
26
+ }
27
+ /**
28
+ * Thrown when a resolution chain exceeds `maxResolutionDepth`.
29
+ */
30
+ export declare class MaxResolutionDepthError extends ContainerError {
31
+ readonly depth: number;
32
+ readonly maxDepth: number;
33
+ readonly chain: readonly string[];
34
+ constructor(token: string, depth: number, maxDepth: number, chain?: readonly string[]);
35
+ }
36
+ /**
37
+ * Thrown when a provider fails while resolving a token. Extends the published
38
+ * `ProviderResolutionError` with the resolution chain that led to the failing
39
+ * token (outermost request first) so callers can inspect it programmatically
40
+ * instead of parsing the message.
41
+ */
42
+ export declare class DependencyResolutionError extends ProviderResolutionError {
43
+ readonly chain: readonly string[];
44
+ constructor(token: string, cause: unknown, chain?: readonly string[]);
45
+ toJSON(): {
46
+ name: string;
47
+ message: string;
48
+ code: string;
49
+ category: import("@zudojs/errors").ErrorCategory;
50
+ severity: import("@zudojs/errors").ErrorSeverity;
51
+ statusCode: number;
52
+ expose: boolean;
53
+ isOperational: boolean;
54
+ metadata: Readonly<import("@zudojs/errors").ErrorMetadata>;
55
+ stack?: string;
56
+ cause?: import("@zudojs/errors").SerializedBaseError | unknown;
57
+ token?: string | undefined;
58
+ chain: readonly string[];
59
+ };
60
+ }
61
+ /**
62
+ * Thrown when a factory registered as SINGLETON or SCOPED returns a Promise.
63
+ *
64
+ * The container is synchronous: it would otherwise cache the Promise itself
65
+ * as the instance and the eventual value would never be disposal-tracked.
66
+ * Await the resource before registering it (e.g. `registerValue`), or use a
67
+ * TRANSIENT registration whose callers await the result themselves.
68
+ */
69
+ export declare class AsyncProviderError extends ContainerError {
70
+ readonly scope: string;
71
+ constructor(token: string, scope: string);
72
+ }
8
73
  //# sourceMappingURL=containerResolution.error.d.ts.map
@@ -1,8 +1,112 @@
1
1
  /**
2
2
  * @zudojs/container/containerResolution/containerResolution.error
3
3
  *
4
- * Error classes re-exported from @zudojs/errors.
5
- * Token description helpers live in containerToken.type.ts.
4
+ * Resolution error classes local to this package. They extend the published
5
+ * `ContainerError` base from @zudojs/errors; the published package does not
6
+ * yet ship dedicated classes for these failure modes.
6
7
  */
7
- export {};
8
+ import { ContainerError, ProviderResolutionError } from "@zudojs/errors";
9
+ function formatChain(chain) {
10
+ return chain.join(" -> ");
11
+ }
12
+ /**
13
+ * Thrown when a SCOPED registration is resolved without a scope
14
+ * (i.e. directly from the root container).
15
+ */
16
+ export class ScopedResolutionError extends ContainerError {
17
+ chain;
18
+ constructor(token, chain = []) {
19
+ const suffix = chain.length > 1 ? ` (chain: ${formatChain(chain)})` : "";
20
+ super(`Scoped token ${token} cannot be resolved outside a scope. ` +
21
+ `Create one with container.createScope() and resolve through it.${suffix}`, {
22
+ code: "CONTAINER_SCOPED_OUTSIDE_SCOPE",
23
+ token,
24
+ statusCode: 500,
25
+ expose: false,
26
+ });
27
+ this.chain = chain;
28
+ }
29
+ }
30
+ /**
31
+ * Thrown when a longer-lived consumer (a SINGLETON) would capture a
32
+ * shorter-lived SCOPED dependency, freezing it beyond its scope's lifetime.
33
+ */
34
+ export class CaptiveDependencyError extends ContainerError {
35
+ consumer;
36
+ dependency;
37
+ chain;
38
+ constructor(consumer, dependency, chain = []) {
39
+ const suffix = chain.length > 1 ? ` (chain: ${formatChain(chain)})` : "";
40
+ super(`Captive dependency: singleton "${consumer}" depends on scoped "${dependency}". ` +
41
+ `A longer-lived consumer cannot capture a shorter-lived dependency.${suffix}`, {
42
+ code: "CONTAINER_CAPTIVE_DEPENDENCY",
43
+ token: dependency,
44
+ statusCode: 500,
45
+ expose: false,
46
+ });
47
+ this.consumer = consumer;
48
+ this.dependency = dependency;
49
+ this.chain = chain;
50
+ }
51
+ }
52
+ /**
53
+ * Thrown when a resolution chain exceeds `maxResolutionDepth`.
54
+ */
55
+ export class MaxResolutionDepthError extends ContainerError {
56
+ depth;
57
+ maxDepth;
58
+ chain;
59
+ constructor(token, depth, maxDepth, chain = []) {
60
+ super(`Maximum resolution depth of ${maxDepth} exceeded while resolving ` +
61
+ `token ${token} at depth ${depth}.`, {
62
+ code: "CONTAINER_MAX_RESOLUTION_DEPTH",
63
+ token,
64
+ statusCode: 500,
65
+ expose: false,
66
+ });
67
+ this.depth = depth;
68
+ this.maxDepth = maxDepth;
69
+ this.chain = chain;
70
+ }
71
+ }
72
+ /**
73
+ * Thrown when a provider fails while resolving a token. Extends the published
74
+ * `ProviderResolutionError` with the resolution chain that led to the failing
75
+ * token (outermost request first) so callers can inspect it programmatically
76
+ * instead of parsing the message.
77
+ */
78
+ export class DependencyResolutionError extends ProviderResolutionError {
79
+ chain;
80
+ constructor(token, cause, chain = []) {
81
+ const detail = cause instanceof Error ? cause.message : String(cause);
82
+ const suffix = chain.length > 0 ? ` (chain: ${formatChain(chain)})` : "";
83
+ super(token, `Failed to resolve ${token}: ${detail}${suffix}`, cause);
84
+ this.chain = Object.freeze([...chain]);
85
+ }
86
+ toJSON() {
87
+ return { ...super.toJSON(), chain: this.chain };
88
+ }
89
+ }
90
+ /**
91
+ * Thrown when a factory registered as SINGLETON or SCOPED returns a Promise.
92
+ *
93
+ * The container is synchronous: it would otherwise cache the Promise itself
94
+ * as the instance and the eventual value would never be disposal-tracked.
95
+ * Await the resource before registering it (e.g. `registerValue`), or use a
96
+ * TRANSIENT registration whose callers await the result themselves.
97
+ */
98
+ export class AsyncProviderError extends ContainerError {
99
+ scope;
100
+ constructor(token, scope) {
101
+ super(`Factory for ${scope} token ${token} returned a Promise. Async factories ` +
102
+ `are not supported for cached (singleton/scoped) registrations: ` +
103
+ `await the value before registering it, or register it as TRANSIENT.`, {
104
+ code: "CONTAINER_ASYNC_PROVIDER_UNSUPPORTED",
105
+ token,
106
+ statusCode: 500,
107
+ expose: false,
108
+ });
109
+ this.scope = scope;
110
+ }
111
+ }
8
112
  //# sourceMappingURL=containerResolution.error.js.map
@@ -6,8 +6,17 @@ import type { ContainerScope } from "../containerScope/containerScope.type.js";
6
6
  import type { Token } from "../containerToken/containerToken.type.js";
7
7
  /**
8
8
  * A cache containing resolved dependency instances.
9
+ *
10
+ * A plain `Map` satisfies this interface; scope caches created by the
11
+ * resolver additionally fall back to their parent scope's cache for lookups
12
+ * (see {@link ContainerResolver.createScope}).
9
13
  */
10
- export type ResolutionCache = Map<Token<unknown>, unknown>;
14
+ export interface ResolutionCache {
15
+ has(token: Token<unknown>): boolean;
16
+ get(token: Token<unknown>): unknown;
17
+ set(token: Token<unknown>, value: unknown): void;
18
+ clear(): void;
19
+ }
11
20
  /**
12
21
  * Dependency resolution path.
13
22
  * Used for diagnostics and circular dependency detection.
@@ -17,12 +26,35 @@ export type ResolutionPath = readonly Token<unknown>[];
17
26
  * Options controlling dependency resolution.
18
27
  */
19
28
  export interface ResolutionOptions {
20
- /** Existing cache for the current resolution scope. */
29
+ /**
30
+ * Scope cache for SCOPED instances. When absent the resolution is a root
31
+ * resolution and SCOPED registrations throw. SINGLETON instances always
32
+ * live in the resolver's own singleton cache, never in this cache.
33
+ */
21
34
  readonly cache?: ResolutionCache;
22
35
  /** Current dependency resolution path. Normally managed internally. */
23
36
  readonly path?: ResolutionPath;
24
- /** Whether to allow resolving unregistered classes directly from their constructors. Defaults to true. */
37
+ /** Whether unregistered class tokens may be resolved. Defaults to true. */
25
38
  readonly autoRegisterClasses?: boolean;
39
+ /**
40
+ * Whether auto-resolved class tokens may be added to the registry.
41
+ * When false (e.g. registrations are frozen) unregistered classes are
42
+ * instantiated ephemerally without being registered. Defaults to true.
43
+ */
44
+ readonly allowRegistration?: boolean;
45
+ /** Whether circular dependencies are detected. Defaults to true. */
46
+ readonly detectCircularDependencies?: boolean;
47
+ /**
48
+ * Maximum resolution chain depth before resolution aborts with a
49
+ * MaxResolutionDepthError. Defaults to 100.
50
+ */
51
+ readonly maxResolutionDepth?: number;
52
+ /**
53
+ * Invoked for every instance the resolver creates during this resolution
54
+ * (never for cache hits), in creation order — dependencies are reported
55
+ * before their dependents. Owners use it to track instances for disposal.
56
+ */
57
+ readonly onInstanceCreated?: (result: ResolutionResult<unknown>) => void;
26
58
  }
27
59
  /**
28
60
  * Resolution result containing the resolved value and diagnostic information.
@@ -4,5 +4,6 @@
4
4
  * Dependency resolution and injection.
5
5
  */
6
6
  export * from "./containerResolution.type.js";
7
+ export * from "./containerResolution.error.js";
7
8
  export * from "./containerResolution.core.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -4,5 +4,6 @@
4
4
  * Dependency resolution and injection.
5
5
  */
6
6
  export * from "./containerResolution.type.js";
7
+ export * from "./containerResolution.error.js";
7
8
  export * from "./containerResolution.core.js";
8
9
  //# sourceMappingURL=index.js.map