@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,42 +1,63 @@
1
1
  /**
2
- * ContainerScopeContext — child dependency scope with
3
- * its own scoped-instance cache.
4
- */
5
- import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecycle/containerLifecycle.core.js";
6
- /**
7
- * Represents a child dependency scope.
2
+ * ContainerScopeContext — a dependency scope with its own scoped-instance
3
+ * cache and its own lifecycle for SCOPED instances.
4
+ *
5
+ * Ownership model:
6
+ * - SCOPED instances created through this scope — including those created
7
+ * transitively as dependencies — are owned by the scope and disposed when
8
+ * the scope is disposed.
9
+ * - SINGLETON instances resolved through a scope stay owned by the parent
10
+ * container — a scope's disposal never touches them.
11
+ * - TRANSIENT instances are not tracked anywhere; callers own their disposal.
8
12
  *
9
- * A scope has its own scoped-instance cache while sharing
10
- * registrations and singleton instances with its parent.
13
+ * Nesting: `scope.createScope()` creates a true child scope. A child sees
14
+ * SCOPED instances already created by its ancestors (lookups chain upward),
15
+ * while instances it creates itself are private to the child. Disposing a
16
+ * scope disposes its children first, and a child refuses to resolve once any
17
+ * ancestor (scope or container) is disposed.
11
18
  */
19
+ import { ContainerLifecycle, ContainerLifecycleOwner, } from "../containerLifecycle/containerLifecycle.core.js";
20
+ import { ContainerScope } from "../containerScope/containerScope.type.js";
12
21
  export class ContainerScopeContext {
13
- parent;
14
22
  disposed = false;
23
+ disposing;
15
24
  cache;
16
25
  lifecycle;
26
+ container;
27
+ parentScope;
28
+ children = new Set();
17
29
  name;
18
30
  metadata;
19
- constructor(parent, options = {}) {
20
- this.parent = parent;
21
- this.name = options.name ?? `${parent.name}:scope`;
31
+ /**
32
+ * @param container The container that owns the scope tree.
33
+ * @param options Scope name / metadata.
34
+ * @param parentScope When given, the new scope is a nested child of it.
35
+ * Application code should use `container.createScope()` /
36
+ * `scope.createScope()` rather than constructing scopes directly.
37
+ */
38
+ constructor(container, options = {}, parentScope) {
39
+ this.container = container;
40
+ this.parentScope = parentScope;
41
+ this.name = options.name ?? `${(parentScope ?? container).name}:scope`;
22
42
  this.metadata = Object.freeze({ ...(options.metadata ?? {}) });
23
- this.cache = parent.resolver.createScope();
43
+ this.cache = container.createScopeCache(parentScope?.cache);
24
44
  this.lifecycle = new ContainerLifecycle();
25
45
  }
26
46
  /**
27
47
  * Resolves a dependency within this scope.
48
+ *
49
+ * SCOPED instances are cached per scope (visible to child scopes) and
50
+ * tracked for disposal with the scope that created them. SINGLETON
51
+ * instances come from (and are tracked by) the container. TRANSIENT
52
+ * instances are created fresh and never tracked.
28
53
  */
29
54
  resolve(token) {
30
55
  this.ensureActive();
31
- const result = this.parent.resolver.resolveDetailed(token, {
32
- ...this.parent.resolutionOptions,
33
- cache: this.cache,
34
- });
35
- this.lifecycle.track(result.token, result.value, ContainerLifecycleOwner.SCOPE);
56
+ const result = this.container.resolveInScope(token, this.cache, (created) => this.trackCreated(created));
36
57
  return result.value;
37
58
  }
38
59
  /**
39
- * Resolves multiple dependencies within this scope.
60
+ * Resolves multiple dependencies within this scope, typed per token.
40
61
  */
41
62
  resolveMany(tokens) {
42
63
  return tokens.map((token) => this.resolve(token));
@@ -46,35 +67,75 @@ export class ContainerScopeContext {
46
67
  */
47
68
  canResolve(token) {
48
69
  this.ensureActive();
49
- return this.parent.resolver.canResolve(token);
70
+ return this.container.canResolve(token);
50
71
  }
51
72
  /**
52
73
  * Checks whether a registration exists.
53
74
  */
54
75
  has(token) {
55
76
  this.ensureActive();
56
- return this.parent.has(token);
77
+ return this.container.has(token);
57
78
  }
58
79
  /**
59
- * Creates another nested scope.
80
+ * Creates a nested child scope.
81
+ *
82
+ * The child inherits this scope's cached SCOPED instances for lookups,
83
+ * caches the SCOPED instances it creates itself, and is disposed
84
+ * automatically when this scope is disposed.
60
85
  */
61
86
  createScope(options = {}) {
62
87
  this.ensureActive();
63
- return this.parent.createScope(options);
88
+ const child = new ContainerScopeContext(this.container, options, this);
89
+ this.children.add(child);
90
+ return child;
64
91
  }
65
92
  /**
66
- * Disposes all instances belonging to this scope.
93
+ * Disposes child scopes (most recent first), then all SCOPED instances
94
+ * belonging to this scope in reverse creation order, and detaches the scope
95
+ * from its parent. Container-owned singletons are not touched. Idempotent.
96
+ *
97
+ * Every failure is collected; the scope is marked disposed regardless and
98
+ * an AggregateError listing the failures is thrown afterwards. Concurrent
99
+ * callers share the in-flight disposal rather than returning early.
67
100
  */
68
- async dispose() {
101
+ dispose() {
102
+ if (this.disposing)
103
+ return this.disposing;
69
104
  if (this.disposed)
70
- return;
105
+ return Promise.resolve();
106
+ this.disposed = true;
107
+ this.disposing = this.runDispose().finally(() => {
108
+ this.disposing = undefined;
109
+ });
110
+ return this.disposing;
111
+ }
112
+ async runDispose() {
113
+ const failures = [];
114
+ for (const child of [...this.children].reverse()) {
115
+ try {
116
+ await child.dispose();
117
+ }
118
+ catch (error) {
119
+ failures.push(error);
120
+ }
121
+ }
122
+ this.children.clear();
71
123
  try {
72
- await this.lifecycle.disposeScope();
124
+ await this.lifecycle.dispose();
125
+ }
126
+ catch (error) {
127
+ failures.push(error);
73
128
  }
74
129
  finally {
130
+ this.lifecycle.shutdown();
75
131
  this.cache.clear();
76
- this.disposed = true;
132
+ if (this.parentScope)
133
+ this.parentScope.releaseChild(this);
134
+ else
135
+ this.container.releaseScope(this);
77
136
  }
137
+ if (failures.length > 0)
138
+ throw new AggregateError(failures, `Container scope "${this.name}" was disposed, but ${failures.length} cleanup step(s) failed.`);
78
139
  }
79
140
  /**
80
141
  * Returns whether the scope has been disposed.
@@ -83,18 +144,46 @@ export class ContainerScopeContext {
83
144
  return this.disposed;
84
145
  }
85
146
  /**
86
- * Returns the parent container.
147
+ * Returns the direct parent: the container for a top-level scope, or the
148
+ * parent scope for a nested one.
87
149
  */
88
150
  getParent() {
89
- return this.parent;
151
+ return this.parentScope ?? this.container;
152
+ }
153
+ /**
154
+ * Returns the container that owns the whole scope tree.
155
+ */
156
+ getContainer() {
157
+ return this.container;
158
+ }
159
+ /** @internal Detaches a disposed child scope. */
160
+ releaseChild(scope) {
161
+ this.children.delete(scope);
162
+ }
163
+ /**
164
+ * Tracks SCOPED instances created during a resolution through this scope
165
+ * (top-level result or transitively created dependency). Instances served
166
+ * from an ancestor's cache are cache hits and never reach here.
167
+ */
168
+ trackCreated(result) {
169
+ if (result.scope !== ContainerScope.SCOPED)
170
+ return;
171
+ this.lifecycle.track(result.token, result.value, ContainerLifecycleOwner.SCOPE);
90
172
  }
91
173
  /**
92
- * Throws when the scope is no longer active.
174
+ * Throws when the scope — or any ancestor scope, or the container that
175
+ * owns it — is no longer active.
93
176
  */
94
177
  ensureActive() {
95
178
  if (this.disposed) {
96
179
  throw new Error(`Container scope "${this.name}" has already been disposed.`);
97
180
  }
181
+ if (this.parentScope?.isDisposed()) {
182
+ throw new Error(`Parent scope "${this.parentScope.name}" of scope "${this.name}" has been disposed.`);
183
+ }
184
+ if (this.container.isDisposed()) {
185
+ throw new Error(`Container "${this.container.name}" owning scope "${this.name}" has been disposed.`);
186
+ }
98
187
  }
99
188
  }
100
189
  //# sourceMappingURL=containerCore.scope.js.map
@@ -2,6 +2,8 @@
2
2
  * Types for the core container.
3
3
  */
4
4
  import type { RegistrationToken } from "../containerRegistration/containerRegistration.core.js";
5
+ import type { ResolutionCache, ResolutionResult } from "../containerResolution/containerResolution.type.js";
6
+ import type { ContainerScopeContext } from "./containerCore.scope.js";
5
7
  /**
6
8
  * Options used when creating a child container scope.
7
9
  */
@@ -16,25 +18,29 @@ export interface ContainerScopeOptions {
16
18
  readonly metadata?: Readonly<Record<string, unknown>>;
17
19
  }
18
20
  /**
19
- * Full Container interface including mixin methods.
20
- * Used by ContainerScopeContext to reference the parent container.
21
+ * The surface a ContainerScopeContext needs from its owning container.
22
+ * Implemented by Container; the `resolveInScope` / `createScopeCache` /
23
+ * `releaseScope` members are internal plumbing for scopes and should not be
24
+ * called by application code.
21
25
  */
22
26
  export interface ContainerLike {
23
27
  readonly name: string;
24
- readonly resolver: {
25
- createScope(): import("../containerResolution/containerResolution.type.js").ResolutionCache;
26
- resolveDetailed(token: unknown, options: unknown): {
27
- token: unknown;
28
- value: unknown;
29
- };
30
- canResolve(token: unknown): boolean;
31
- };
32
- readonly resolutionOptions: {
33
- autoRegisterClasses: boolean;
34
- detectCircularDependencies: boolean;
35
- maxResolutionDepth: number;
36
- };
37
28
  has<T>(token: RegistrationToken<T>): boolean;
38
- createScope(options?: ContainerScopeOptions): import("./containerCore.scope.js").ContainerScopeContext;
29
+ canResolve<T>(token: RegistrationToken<T>): boolean;
30
+ isDisposed(): boolean;
31
+ createScope(options?: ContainerScopeOptions): ContainerScopeContext;
32
+ /**
33
+ * @internal Creates a fresh scoped-instance cache, optionally chained to a
34
+ * parent scope's cache (nested scopes).
35
+ */
36
+ createScopeCache(parent?: ResolutionCache): ResolutionCache;
37
+ /**
38
+ * @internal Resolves a token using the given scope cache. `onInstanceCreated`
39
+ * is invoked for every instance created during the resolution (including
40
+ * transitively created dependencies) so the scope can track SCOPED ones.
41
+ */
42
+ resolveInScope<T>(token: RegistrationToken<T>, cache: ResolutionCache, onInstanceCreated?: (result: ResolutionResult<unknown>) => void): ResolutionResult<T>;
43
+ /** @internal Unregisters a top-level scope from the container's live-scope set. */
44
+ releaseScope(scope: ContainerScopeContext): void;
39
45
  }
40
46
  //# sourceMappingURL=containerCore.type.d.ts.map
@@ -1,16 +1,41 @@
1
1
  /**
2
2
  * Lifecycle management for resolved container instances.
3
- * Tracks instances that need cleanup and disposes them when a container or scope is destroyed.
3
+ *
4
+ * Tracks SINGLETON instances (container-owned) and SCOPED instances
5
+ * (scope-owned) that need cleanup and disposes them when their owner is
6
+ * destroyed, in reverse creation order.
7
+ *
8
+ * TRANSIENT instances are deliberately NOT tracked: the container creates a
9
+ * new transient per resolution, so tracking them per token would either leak
10
+ * (unbounded growth) or silently drop all but the last instance. Callers own
11
+ * the disposal of transient instances they resolve.
4
12
  */
5
13
  import type { Token } from "../containerToken/containerToken.type.js";
6
14
  import { ContainerLifecycleError } from "@zudojs/errors";
7
- export interface Disposable {
8
- dispose(): void;
15
+ /**
16
+ * An object disposable via a `dispose()` method (sync or async).
17
+ * Named `DisposableLike` to avoid shadowing the ES2023 `Disposable` built-in.
18
+ */
19
+ export interface DisposableLike {
20
+ dispose(): void | Promise<void>;
21
+ }
22
+ /** An object disposable via the ES2023 `Symbol.dispose` protocol. */
23
+ export interface SymbolDisposableLike {
24
+ [Symbol.dispose](): void;
9
25
  }
10
- export interface AsyncDisposable {
11
- [Symbol.asyncDispose]?: () => Promise<void>;
26
+ /**
27
+ * An object disposable via the ES2023 `Symbol.asyncDispose` protocol.
28
+ * Named `AsyncDisposableLike` to avoid shadowing the `AsyncDisposable`
29
+ * built-in.
30
+ */
31
+ export interface AsyncDisposableLike {
32
+ [Symbol.asyncDispose](): Promise<void> | void;
12
33
  }
13
- export type DisposableInstance = Disposable | AsyncDisposable;
34
+ /** @deprecated Use {@link DisposableLike} instead. */
35
+ export type Disposable = DisposableLike;
36
+ /** @deprecated Use {@link AsyncDisposableLike} instead. */
37
+ export type AsyncDisposable = AsyncDisposableLike;
38
+ export type DisposableInstance = DisposableLike | SymbolDisposableLike | AsyncDisposableLike;
14
39
  export declare enum ContainerLifecycleOwner {
15
40
  CONTAINER = "container",
16
41
  SCOPE = "scope"
@@ -30,26 +55,51 @@ export declare class ContainerDisposalError extends ContainerLifecycleError {
30
55
  readonly tokens: readonly Token<unknown>[];
31
56
  constructor(errors: readonly unknown[], tokens: readonly Token<unknown>[]);
32
57
  }
33
- export declare function isDisposable(value: unknown): value is Disposable;
34
- export declare function isAsyncDisposable(value: unknown): value is AsyncDisposable;
58
+ export declare function isDisposable(value: unknown): value is DisposableLike;
59
+ export declare function isSymbolDisposable(value: unknown): value is SymbolDisposableLike;
60
+ export declare function isAsyncDisposable(value: unknown): value is AsyncDisposableLike;
35
61
  export declare function isDisposableInstance(value: unknown): value is DisposableInstance;
36
62
  export declare class ContainerLifecycle {
37
63
  private readonly instances;
38
64
  private readonly options;
39
65
  private disposed;
40
66
  constructor(options?: ContainerLifecycleOptions);
67
+ /**
68
+ * Tracks a disposable instance for later cleanup.
69
+ *
70
+ * Re-tracking a token removes the previous entry and re-inserts it at the
71
+ * end of the map, so reverse-creation disposal order stays sound.
72
+ * Non-disposable instances are ignored.
73
+ */
41
74
  track<T>(token: Token<T>, instance: T, owner?: ContainerLifecycleOwner): void;
42
75
  has<T>(token: Token<T>): boolean;
43
76
  get<T>(token: Token<T>): TrackedInstance<T> | undefined;
44
77
  getAll(): readonly TrackedInstance[];
45
78
  get size(): number;
46
79
  untrack<T>(token: Token<T>): boolean;
80
+ /**
81
+ * Disposes a single tracked instance and untracks it. Untracking happens
82
+ * even when disposal fails — a failed disposal is terminal for the entry.
83
+ */
47
84
  disposeInstance<T>(token: Token<T>): Promise<void>;
85
+ /**
86
+ * Disposes tracked instances in reverse creation order.
87
+ *
88
+ * Entries are untracked even when their disposal fails (terminal). When
89
+ * `owner` is omitted the whole lifecycle is marked disposed — this happens
90
+ * even on failure — and every failure is reported in the thrown
91
+ * ContainerDisposalError.
92
+ */
48
93
  dispose(owner?: ContainerLifecycleOwner): Promise<void>;
49
94
  disposeScope(): Promise<void>;
50
95
  disposeContainer(): Promise<void>;
96
+ /**
97
+ * Releases all tracked references WITHOUT disposing them and marks the
98
+ * lifecycle disposed so further `track()` calls are refused. Used when a
99
+ * container is disposed with `autoDispose: false`.
100
+ */
101
+ shutdown(): void;
51
102
  isDisposed(): boolean;
52
- reset(): void;
53
103
  getTrackedTokens(): readonly Token<unknown>[];
54
104
  }
55
105
  export declare function createContainerLifecycle(options?: ContainerLifecycleOptions): ContainerLifecycle;
@@ -1,6 +1,14 @@
1
1
  /**
2
2
  * Lifecycle management for resolved container instances.
3
- * Tracks instances that need cleanup and disposes them when a container or scope is destroyed.
3
+ *
4
+ * Tracks SINGLETON instances (container-owned) and SCOPED instances
5
+ * (scope-owned) that need cleanup and disposes them when their owner is
6
+ * destroyed, in reverse creation order.
7
+ *
8
+ * TRANSIENT instances are deliberately NOT tracked: the container creates a
9
+ * new transient per resolution, so tracking them per token would either leak
10
+ * (unbounded growth) or silently drop all but the last instance. Callers own
11
+ * the disposal of transient instances they resolve.
4
12
  */
5
13
  import { describeToken } from "../containerToken/containerToken.type.js";
6
14
  import { ContainerLifecycleError } from "@zudojs/errors";
@@ -24,6 +32,12 @@ export function isDisposable(value) {
24
32
  "dispose" in value &&
25
33
  typeof value.dispose === "function");
26
34
  }
35
+ export function isSymbolDisposable(value) {
36
+ return (typeof value === "object" &&
37
+ value !== null &&
38
+ Symbol.dispose in value &&
39
+ typeof value[Symbol.dispose] === "function");
40
+ }
27
41
  export function isAsyncDisposable(value) {
28
42
  return (typeof value === "object" &&
29
43
  value !== null &&
@@ -31,7 +45,7 @@ export function isAsyncDisposable(value) {
31
45
  typeof value[Symbol.asyncDispose] === "function");
32
46
  }
33
47
  export function isDisposableInstance(value) {
34
- return isDisposable(value) || isAsyncDisposable(value);
48
+ return (isDisposable(value) || isSymbolDisposable(value) || isAsyncDisposable(value));
35
49
  }
36
50
  export class ContainerLifecycle {
37
51
  instances = new Map();
@@ -40,11 +54,19 @@ export class ContainerLifecycle {
40
54
  constructor(options = {}) {
41
55
  this.options = { failFast: options.failFast ?? false };
42
56
  }
57
+ /**
58
+ * Tracks a disposable instance for later cleanup.
59
+ *
60
+ * Re-tracking a token removes the previous entry and re-inserts it at the
61
+ * end of the map, so reverse-creation disposal order stays sound.
62
+ * Non-disposable instances are ignored.
63
+ */
43
64
  track(token, instance, owner = ContainerLifecycleOwner.CONTAINER) {
44
65
  if (this.disposed)
45
- throw new Error("Cannot track an instance after the container lifecycle has been disposed.");
66
+ throw new ContainerLifecycleError("track", "Cannot track an instance after the container lifecycle has been disposed.");
46
67
  if (!isDisposableInstance(instance))
47
68
  return;
69
+ this.instances.delete(token);
48
70
  this.instances.set(token, {
49
71
  token,
50
72
  instance,
@@ -68,14 +90,18 @@ export class ContainerLifecycle {
68
90
  untrack(token) {
69
91
  return this.instances.delete(token);
70
92
  }
93
+ /**
94
+ * Disposes a single tracked instance and untracks it. Untracking happens
95
+ * even when disposal fails — a failed disposal is terminal for the entry.
96
+ */
71
97
  async disposeInstance(token) {
72
- const tracked = this.get(token);
98
+ const tracked = this.instances.get(token);
73
99
  if (!tracked || tracked.disposed)
74
100
  return;
101
+ tracked.disposed = true;
102
+ this.instances.delete(token);
75
103
  try {
76
104
  await disposeValue(tracked.instance);
77
- tracked.disposed = true;
78
- this.instances.delete(token);
79
105
  }
80
106
  catch (error) {
81
107
  if (this.options.failFast)
@@ -83,9 +109,21 @@ export class ContainerLifecycle {
83
109
  throw new ContainerDisposalError([error], [token]);
84
110
  }
85
111
  }
112
+ /**
113
+ * Disposes tracked instances in reverse creation order.
114
+ *
115
+ * Entries are untracked even when their disposal fails (terminal). When
116
+ * `owner` is omitted the whole lifecycle is marked disposed — this happens
117
+ * even on failure — and every failure is reported in the thrown
118
+ * ContainerDisposalError.
119
+ */
86
120
  async dispose(owner) {
87
121
  if (this.disposed)
88
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;
89
127
  const tracked = [...this.instances.values()].reverse();
90
128
  const selected = owner
91
129
  ? tracked.filter((entry) => entry.owner === owner)
@@ -95,10 +133,10 @@ export class ContainerLifecycle {
95
133
  for (const entry of selected) {
96
134
  if (entry.disposed)
97
135
  continue;
136
+ entry.disposed = true;
137
+ this.instances.delete(entry.token);
98
138
  try {
99
139
  await disposeValue(entry.instance);
100
- entry.disposed = true;
101
- this.instances.delete(entry.token);
102
140
  }
103
141
  catch (error) {
104
142
  errors.push(error);
@@ -109,8 +147,6 @@ export class ContainerLifecycle {
109
147
  }
110
148
  if (errors.length > 0)
111
149
  throw new ContainerDisposalError(errors, failedTokens);
112
- if (owner === undefined)
113
- this.disposed = true;
114
150
  }
115
151
  async disposeScope() {
116
152
  await this.dispose(ContainerLifecycleOwner.SCOPE);
@@ -118,13 +154,18 @@ export class ContainerLifecycle {
118
154
  async disposeContainer() {
119
155
  await this.dispose(ContainerLifecycleOwner.CONTAINER);
120
156
  }
157
+ /**
158
+ * Releases all tracked references WITHOUT disposing them and marks the
159
+ * lifecycle disposed so further `track()` calls are refused. Used when a
160
+ * container is disposed with `autoDispose: false`.
161
+ */
162
+ shutdown() {
163
+ this.instances.clear();
164
+ this.disposed = true;
165
+ }
121
166
  isDisposed() {
122
167
  return this.disposed;
123
168
  }
124
- reset() {
125
- this.instances.clear();
126
- this.disposed = false;
127
- }
128
169
  getTrackedTokens() {
129
170
  return [...this.instances.keys()];
130
171
  }
@@ -134,8 +175,12 @@ async function disposeValue(value) {
134
175
  await value[Symbol.asyncDispose]();
135
176
  return;
136
177
  }
178
+ if (isSymbolDisposable(value)) {
179
+ value[Symbol.dispose]();
180
+ return;
181
+ }
137
182
  if (isDisposable(value)) {
138
- value.dispose();
183
+ await value.dispose();
139
184
  return;
140
185
  }
141
186
  }
@@ -4,26 +4,18 @@
4
4
  import type { ContainerLifecycleOptions } from "../containerLifecycle/containerLifecycle.core.js";
5
5
  import type { ContainerRegistryOptions } from "../containerRegistry/containerRegistry.type.js";
6
6
  import type { ResolutionOptions } from "../containerResolution/containerResolution.type.js";
7
- export declare enum ContainerLogLevel {
8
- NONE = "none",
9
- ERROR = "error",
10
- WARN = "warn",
11
- INFO = "info",
12
- DEBUG = "debug",
13
- TRACE = "trace"
14
- }
15
- export declare const DEFAULT_CONTAINER_LOG_LEVEL: ContainerLogLevel;
16
- export interface ContainerResolutionOptions extends ResolutionOptions {
17
- readonly autoRegisterClasses?: boolean;
18
- readonly detectCircularDependencies?: boolean;
19
- readonly maxResolutionDepth?: number;
20
- }
7
+ /**
8
+ * The subset of {@link ResolutionOptions} a container accepts at
9
+ * construction time. Per-call plumbing (`cache`, `path`,
10
+ * `allowRegistration`, `onInstanceCreated`) is managed by the container
11
+ * itself and deliberately not configurable here.
12
+ */
13
+ export type ContainerResolutionOptions = Pick<ResolutionOptions, "autoRegisterClasses" | "detectCircularDependencies" | "maxResolutionDepth">;
21
14
  export interface ContainerOptions {
22
15
  readonly name?: string;
23
16
  readonly registry?: ContainerRegistryOptions;
24
17
  readonly lifecycle?: ContainerLifecycleOptions;
25
18
  readonly resolution?: ContainerResolutionOptions;
26
- readonly logLevel?: ContainerLogLevel;
27
19
  readonly autoDispose?: boolean;
28
20
  readonly allowScopes?: boolean;
29
21
  readonly freezeRegistrations?: boolean;
@@ -34,7 +26,6 @@ export interface ResolvedContainerOptions {
34
26
  readonly registry: ContainerRegistryOptions;
35
27
  readonly lifecycle: ContainerLifecycleOptions;
36
28
  readonly resolution: ContainerResolutionOptions;
37
- readonly logLevel: ContainerLogLevel;
38
29
  readonly autoDispose: boolean;
39
30
  readonly allowScopes: boolean;
40
31
  readonly freezeRegistrations: boolean;
@@ -47,7 +38,6 @@ export declare const DEFAULT_FREEZE_REGISTRATIONS = false;
47
38
  export declare const DEFAULT_RESOLUTION_OPTIONS: Required<Pick<ContainerResolutionOptions, "autoRegisterClasses" | "detectCircularDependencies" | "maxResolutionDepth">>;
48
39
  export declare function resolveContainerOptions(options?: ContainerOptions): ResolvedContainerOptions;
49
40
  export declare function validateResolutionOptions(options: ContainerResolutionOptions): void;
50
- export declare function isContainerLogLevel(value: unknown): value is ContainerLogLevel;
51
41
  export declare function allowsContainerScopes(options: ResolvedContainerOptions): boolean;
52
42
  export declare function shouldAutoDisposeContainer(options: ResolvedContainerOptions): boolean;
53
43
  export declare function canModifyRegistrations(options: ResolvedContainerOptions): boolean;
@@ -1,16 +1,6 @@
1
1
  /**
2
2
  * Configuration options for the Zudojs dependency injection container.
3
3
  */
4
- export var ContainerLogLevel;
5
- (function (ContainerLogLevel) {
6
- ContainerLogLevel["NONE"] = "none";
7
- ContainerLogLevel["ERROR"] = "error";
8
- ContainerLogLevel["WARN"] = "warn";
9
- ContainerLogLevel["INFO"] = "info";
10
- ContainerLogLevel["DEBUG"] = "debug";
11
- ContainerLogLevel["TRACE"] = "trace";
12
- })(ContainerLogLevel || (ContainerLogLevel = {}));
13
- export const DEFAULT_CONTAINER_LOG_LEVEL = ContainerLogLevel.WARN;
14
4
  export const DEFAULT_CONTAINER_NAME = "zudojs-container";
15
5
  export const DEFAULT_AUTO_DISPOSE = true;
16
6
  export const DEFAULT_ALLOW_SCOPES = true;
@@ -39,7 +29,6 @@ export function resolveContainerOptions(options = {}) {
39
29
  registry: Object.freeze({ ...(options.registry ?? {}) }),
40
30
  lifecycle: Object.freeze({ ...(options.lifecycle ?? {}) }),
41
31
  resolution: Object.freeze({ ...resolution }),
42
- logLevel: options.logLevel ?? DEFAULT_CONTAINER_LOG_LEVEL,
43
32
  autoDispose: options.autoDispose ?? DEFAULT_AUTO_DISPOSE,
44
33
  allowScopes: options.allowScopes ?? DEFAULT_ALLOW_SCOPES,
45
34
  freezeRegistrations: options.freezeRegistrations ?? DEFAULT_FREEZE_REGISTRATIONS,
@@ -53,14 +42,6 @@ export function validateResolutionOptions(options) {
53
42
  throw new RangeError("Container maxResolutionDepth must be a positive integer.");
54
43
  }
55
44
  }
56
- export function isContainerLogLevel(value) {
57
- return (value === ContainerLogLevel.NONE ||
58
- value === ContainerLogLevel.ERROR ||
59
- value === ContainerLogLevel.WARN ||
60
- value === ContainerLogLevel.INFO ||
61
- value === ContainerLogLevel.DEBUG ||
62
- value === ContainerLogLevel.TRACE);
63
- }
64
45
  export function allowsContainerScopes(options) {
65
46
  return options.allowScopes;
66
47
  }
@@ -6,6 +6,11 @@ import type { Constructor, InjectionToken, Token } from "../containerToken/conta
6
6
  export type ProviderToken<T = unknown> = Token<T> | InjectionToken<T>;
7
7
  export interface ClassProvider<T> {
8
8
  readonly useClass: Constructor<T>;
9
+ /**
10
+ * Tokens resolved and passed to the constructor, in order.
11
+ * Omit (or leave empty) for zero-argument constructors.
12
+ */
13
+ readonly inject?: readonly ProviderToken[];
9
14
  }
10
15
  export interface FactoryProvider<T> {
11
16
  readonly useFactory: (...dependencies: unknown[]) => T;
@@ -25,6 +30,7 @@ export interface TokenProvider<T = unknown> {
25
30
  export interface ClassRegistration<T = unknown> {
26
31
  readonly provide: ProviderToken<T>;
27
32
  readonly useClass: Constructor<T>;
33
+ readonly inject?: readonly ProviderToken[];
28
34
  }
29
35
  export interface FactoryRegistration<T = unknown> {
30
36
  readonly provide: ProviderToken<T>;
@@ -45,12 +51,14 @@ export declare function isFactoryProvider<T = unknown>(provider: Provider<T>): p
45
51
  export declare function isValueProvider<T = unknown>(provider: Provider<T>): provider is ValueProvider<T>;
46
52
  export declare function isExistingProvider<T = unknown>(provider: Provider<T>): provider is ExistingProvider<T>;
47
53
  export declare function isTokenProvider<T = unknown>(provider: ContainerProvider<T>): provider is TokenProvider<T>;
48
- export declare function hasInjectedDependencies<T = unknown>(provider: Provider<T>): provider is FactoryProvider<T>;
49
- export declare function classProvider<T>(useClass: Constructor<T>): ClassProvider<T>;
54
+ export declare function hasInjectedDependencies<T = unknown>(provider: Provider<T>): provider is (FactoryProvider<T> | ClassProvider<T>) & {
55
+ readonly inject: readonly ProviderToken[];
56
+ };
57
+ export declare function classProvider<T>(useClass: Constructor<T>, inject?: readonly ProviderToken[]): ClassProvider<T>;
50
58
  export declare function factoryProvider<T>(useFactory: (...dependencies: unknown[]) => T, inject?: readonly ProviderToken[]): FactoryProvider<T>;
51
59
  export declare function valueProvider<T>(useValue: T): ValueProvider<T>;
52
60
  export declare function existingProvider<T>(useExisting: ProviderToken<T>): ExistingProvider<T>;
53
- export declare function provideClass<T>(provide: ProviderToken<T>, useClass: Constructor<T>): ClassRegistration<T>;
61
+ export declare function provideClass<T>(provide: ProviderToken<T>, useClass: Constructor<T>, inject?: readonly ProviderToken[]): ClassRegistration<T>;
54
62
  export declare function provideFactory<T>(provide: ProviderToken<T>, useFactory: (...dependencies: unknown[]) => T, inject?: readonly ProviderToken[]): FactoryRegistration<T>;
55
63
  export declare function provideValue<T>(provide: ProviderToken<T>, useValue: T): ValueRegistration<T>;
56
64
  export declare function provideExisting<T>(provide: ProviderToken<T>, useExisting: ProviderToken<T>): ExistingRegistration<T>;