@codefast/di 0.3.13-canary.4

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 (50) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -0
  3. package/README.md +572 -0
  4. package/dist/binding-select.d.mts +22 -0
  5. package/dist/binding-select.mjs +50 -0
  6. package/dist/binding.d.mts +219 -0
  7. package/dist/binding.mjs +240 -0
  8. package/dist/constraints.d.mts +18 -0
  9. package/dist/constraints.mjs +24 -0
  10. package/dist/container.d.mts +82 -0
  11. package/dist/container.mjs +406 -0
  12. package/dist/decorators/inject.d.mts +24 -0
  13. package/dist/decorators/inject.mjs +69 -0
  14. package/dist/decorators/injectable.d.mts +40 -0
  15. package/dist/decorators/injectable.mjs +62 -0
  16. package/dist/decorators/lifecycle-decorators.d.mts +13 -0
  17. package/dist/decorators/lifecycle-decorators.mjs +34 -0
  18. package/dist/dependency-graph.d.mts +35 -0
  19. package/dist/dependency-graph.mjs +126 -0
  20. package/dist/environment.d.mts +14 -0
  21. package/dist/environment.mjs +20 -0
  22. package/dist/errors.d.mts +100 -0
  23. package/dist/errors.mjs +152 -0
  24. package/dist/index.d.mts +10 -0
  25. package/dist/index.mjs +8 -0
  26. package/dist/inspector.d.mts +76 -0
  27. package/dist/inspector.mjs +247 -0
  28. package/dist/lifecycle.d.mts +34 -0
  29. package/dist/lifecycle.mjs +83 -0
  30. package/dist/metadata/metadata-keys.d.mts +17 -0
  31. package/dist/metadata/metadata-keys.mjs +19 -0
  32. package/dist/metadata/metadata-types.d.mts +55 -0
  33. package/dist/metadata/metadata-types.mjs +1 -0
  34. package/dist/metadata/param-registry.d.mts +16 -0
  35. package/dist/metadata/param-registry.mjs +25 -0
  36. package/dist/metadata/symbol-metadata-reader.d.mts +15 -0
  37. package/dist/metadata/symbol-metadata-reader.mjs +32 -0
  38. package/dist/module.d.mts +60 -0
  39. package/dist/module.mjs +57 -0
  40. package/dist/registry.d.mts +38 -0
  41. package/dist/registry.mjs +65 -0
  42. package/dist/resolver.d.mts +102 -0
  43. package/dist/resolver.mjs +361 -0
  44. package/dist/scope-validation.d.mts +20 -0
  45. package/dist/scope-validation.mjs +34 -0
  46. package/dist/scope.d.mts +80 -0
  47. package/dist/scope.mjs +185 -0
  48. package/dist/token.d.mts +20 -0
  49. package/dist/token.mjs +9 -0
  50. package/package.json +157 -0
@@ -0,0 +1,57 @@
1
+ //#region src/module.ts
2
+ /**
3
+ * Immutable description of a bundle of bindings. A {@link Module} holds no runtime state and the
4
+ * same instance may be loaded into any number of containers independently (spec §7.3).
5
+ *
6
+ * The owning container is responsible for tracking which modules have been loaded and which
7
+ * binding ids each module produced; the module itself never sees a container reference.
8
+ */
9
+ var Module = class Module {
10
+ name;
11
+ syncSetup;
12
+ constructor(name, syncSetup) {
13
+ this.name = name;
14
+ this.syncSetup = syncSetup;
15
+ }
16
+ /**
17
+ * Defines a synchronous module.
18
+ * @param name - Human-readable label used in error messages and debug output.
19
+ * @param setup - Callback that registers bindings via the {@link ModuleBuilder}.
20
+ */
21
+ static create(name, setup) {
22
+ return new Module(name, setup);
23
+ }
24
+ /**
25
+ * Defines an async module — use when setup requires awaiting (e.g. reading config, dynamic imports).
26
+ * Load with {@link Container.loadAsync} or {@link Container.fromModulesAsync}.
27
+ */
28
+ static createAsync(name, setup) {
29
+ return new AsyncModule(name, setup);
30
+ }
31
+ /**
32
+ * @internal Invoked by the container while loading this module.
33
+ */
34
+ runSyncSetup(builder) {
35
+ this.syncSetup(builder);
36
+ }
37
+ };
38
+ /**
39
+ * An async module whose setup callback may `await` before registering bindings.
40
+ * Prefer {@link Module.createAsync} over constructing this class directly.
41
+ */
42
+ var AsyncModule = class {
43
+ name;
44
+ asyncSetup;
45
+ constructor(name, asyncSetup) {
46
+ this.name = name;
47
+ this.asyncSetup = asyncSetup;
48
+ }
49
+ /**
50
+ * @internal Invoked by the container while loading this module.
51
+ */
52
+ async runAsyncSetup(builder) {
53
+ await this.asyncSetup(builder);
54
+ }
55
+ };
56
+ //#endregion
57
+ export { AsyncModule, Module };
@@ -0,0 +1,38 @@
1
+ import { Token } from "./token.mjs";
2
+ import { Binding, BindingIdentifier, Constructor } from "./binding.mjs";
3
+
4
+ //#region src/registry.d.ts
5
+ /**
6
+ * Key used to group {@link Binding} instances in the registry (reference equality for tokens).
7
+ */
8
+ type RegistryKey = Token<unknown> | Constructor<unknown>;
9
+ /**
10
+ * Dumb storage for bindings keyed by token or constructor. Selection and construction logic live elsewhere.
11
+ */
12
+ declare class BindingRegistry {
13
+ private readonly bindingsByKey;
14
+ /** Appends `binding` to the list for `key` (multi-binding: each call adds an entry). */
15
+ add<Value>(key: Token<Value> | Constructor<Value>, binding: Binding<Value>): void;
16
+ /** Returns all bindings registered for `key`, or `undefined` if none exist. */
17
+ get<Value>(key: Token<Value> | Constructor<Value>): readonly Binding<Value>[] | undefined;
18
+ /** Removes all bindings for `key` without running any deactivation hooks. */
19
+ remove(key: RegistryKey): void;
20
+ /**
21
+ * Returns owned registry rows (does not include parent containers).
22
+ */
23
+ listEntries(): readonly {
24
+ key: RegistryKey;
25
+ bindings: readonly Binding<unknown>[];
26
+ }[];
27
+ /** Removes the single binding with the given `id` across all keys. */
28
+ removeById(id: BindingIdentifier): void;
29
+ /** Swaps the binding with the given `id` in place, preserving its position in the list. */
30
+ replaceById(id: BindingIdentifier, next: Binding<unknown>): void;
31
+ /**
32
+ * Replaces all bindings for `key` with a single binding (module "last-wins" semantics).
33
+ * Invokes `onReplaced` for every removed binding so scopes can run deactivation.
34
+ */
35
+ replaceKeyLastWins<Value>(key: Token<Value> | Constructor<Value>, binding: Binding<Value>, onReplaced: (removed: Binding<unknown>) => void): void;
36
+ }
37
+ //#endregion
38
+ export { BindingRegistry, RegistryKey };
@@ -0,0 +1,65 @@
1
+ //#region src/registry.ts
2
+ /**
3
+ * Dumb storage for bindings keyed by token or constructor. Selection and construction logic live elsewhere.
4
+ */
5
+ var BindingRegistry = class {
6
+ bindingsByKey = /* @__PURE__ */ new Map();
7
+ /** Appends `binding` to the list for `key` (multi-binding: each call adds an entry). */
8
+ add(key, binding) {
9
+ const registryKey = key;
10
+ const nextBinding = binding;
11
+ const existing = this.bindingsByKey.get(registryKey);
12
+ const merged = existing === void 0 ? [nextBinding] : [...existing, nextBinding];
13
+ this.bindingsByKey.set(registryKey, merged);
14
+ }
15
+ /** Returns all bindings registered for `key`, or `undefined` if none exist. */
16
+ get(key) {
17
+ return this.bindingsByKey.get(key);
18
+ }
19
+ /** Removes all bindings for `key` without running any deactivation hooks. */
20
+ remove(key) {
21
+ this.bindingsByKey.delete(key);
22
+ }
23
+ /**
24
+ * Returns owned registry rows (does not include parent containers).
25
+ */
26
+ listEntries() {
27
+ return [...this.bindingsByKey.entries()].map(([key, bindings]) => ({
28
+ key,
29
+ bindings
30
+ }));
31
+ }
32
+ /** Removes the single binding with the given `id` across all keys. */
33
+ removeById(id) {
34
+ for (const [registryKey, list] of [...this.bindingsByKey.entries()]) {
35
+ const filtered = list.filter((binding) => binding.id !== id);
36
+ if (filtered.length === list.length) continue;
37
+ if (filtered.length === 0) this.bindingsByKey.delete(registryKey);
38
+ else this.bindingsByKey.set(registryKey, filtered);
39
+ }
40
+ }
41
+ /** Swaps the binding with the given `id` in place, preserving its position in the list. */
42
+ replaceById(id, next) {
43
+ for (const [registryKey, list] of this.bindingsByKey.entries()) {
44
+ const index = list.findIndex((binding) => binding.id === id);
45
+ if (index === -1) continue;
46
+ const updated = [...list];
47
+ updated[index] = next;
48
+ this.bindingsByKey.set(registryKey, updated);
49
+ return;
50
+ }
51
+ }
52
+ /**
53
+ * Replaces all bindings for `key` with a single binding (module "last-wins" semantics).
54
+ * Invokes `onReplaced` for every removed binding so scopes can run deactivation.
55
+ */
56
+ replaceKeyLastWins(key, binding, onReplaced) {
57
+ const registryKey = key;
58
+ const nextBinding = binding;
59
+ const existing = this.bindingsByKey.get(registryKey);
60
+ if (existing !== void 0) for (const removed of existing) onReplaced(removed);
61
+ this.bindingsByKey.set(registryKey, [nextBinding]);
62
+ }
63
+ };
64
+ //#endregion
65
+ export { BindingRegistry };
@@ -0,0 +1,102 @@
1
+ import { Token } from "./token.mjs";
2
+ import { RegistryKey } from "./registry.mjs";
3
+ import { Binding, Constructor, ResolveHint } from "./binding.mjs";
4
+ import { MetadataReader } from "./metadata/metadata-types.mjs";
5
+ import { ScopeManager } from "./scope.mjs";
6
+
7
+ //#region src/resolver.d.ts
8
+ /** Dependencies injected into {@link DependencyResolver} at construction time. */
9
+ type ResolverDependencies = {
10
+ /** Looks up all bindings registered for a given registry key (own + parent containers). */readonly lookup: (key: RegistryKey) => readonly Binding<unknown>[] | undefined; /** Manages singleton/scoped instance caches and deactivation. */
11
+ readonly scopeManager: ScopeManager; /** Reads `@injectable()` and lifecycle metadata from constructors. Omit to disable decorator support. */
12
+ readonly metadataReader?: MetadataReader;
13
+ };
14
+ /**
15
+ * Walks the binding graph, manages circular-dependency detection, delegates caching to
16
+ * {@link ScopeManager}, and calls lifecycle hooks. Used exclusively by the container.
17
+ */
18
+ declare class DependencyResolver {
19
+ private readonly deps;
20
+ constructor(deps: ResolverDependencies);
21
+ /** Entry point for synchronous single-binding resolution (no path prefix). */
22
+ resolveRoot<Value>(key: Token<Value> | Constructor<Value>, hint?: ResolveHint): Value;
23
+ /** Entry point for async single-binding resolution (no path prefix). */
24
+ resolveAsyncRoot<Value>(key: Token<Value> | Constructor<Value>, hint?: ResolveHint): Promise<Value>;
25
+ /** Returns `undefined` instead of throwing when no binding is found; still throws on circular deps or async factories. */
26
+ resolveOptionalRoot<Value>(key: Token<Value> | Constructor<Value>, hint?: ResolveHint): Value | undefined;
27
+ /** Resolves all bindings for `key` synchronously; throws on async factories. */
28
+ resolveAllRoot<Value>(key: Token<Value> | Constructor<Value>, hint?: ResolveHint): Value[];
29
+ /** Resolves all bindings for `key`, awaiting any async factories in the set. */
30
+ resolveAllAsyncRoot<Value>(key: Token<Value> | Constructor<Value>, hint?: ResolveHint): Promise<Value[]>;
31
+ /**
32
+ * Assembles the read-only {@link ConstraintContext} snapshot passed to `when()` predicates.
33
+ * Extracts the top-of-stack frame as `parent` and the rest as `ancestors`.
34
+ */
35
+ private buildConstraintContext;
36
+ /**
37
+ * Throws {@link ScopeViolationError} when the immediate consumer on the stack is a singleton
38
+ * and the dependency has `"scoped"` or `"transient"` lifetime (captive dependency).
39
+ * Constants are exempt — they have no scope cache.
40
+ */
41
+ private assertDependencyScopeAllowed;
42
+ /**
43
+ * Builds a {@link ResolutionContext} for use inside factories and lifecycle hooks.
44
+ * The `resolve` / `resolveAsync` / `resolveOptional` closures carry the current path and
45
+ * materialization stack forward so nested calls inherit captive-dependency checks.
46
+ */
47
+ private createContext;
48
+ /**
49
+ * @param key - Token or constructor being resolved.
50
+ * @param hint - Optional name/tag filter for multi-binding selection.
51
+ * @param pathLabels - Mutable label path accumulated during graph walk; extended in place.
52
+ * @param visiting - Registry keys currently on the call stack; used for circular-dependency detection.
53
+ * @param materializationStack - Bindings along the current construction chain; used to block singleton→scoped/transient.
54
+ */
55
+ private resolve;
56
+ /**
57
+ * @param key - Token or constructor being resolved.
58
+ * @param hint - Optional name/tag filter for multi-binding selection.
59
+ * @param pathLabels - Mutable label path accumulated during graph walk; extended in place.
60
+ * @param visiting - Registry keys currently on the call stack; used for circular-dependency detection.
61
+ * @param materializationStack - Same captive-dependency chain as {@link resolve}.
62
+ */
63
+ private resolveAsync;
64
+ /**
65
+ * Handles scope-cache lookup / storage and lifecycle hooks (`@postConstruct`, `onActivation`)
66
+ * around a synchronous call to {@link materialize}.
67
+ */
68
+ private instantiateBinding;
69
+ /**
70
+ * Async counterpart of {@link instantiateBinding}: delegates to {@link materializeAsync}
71
+ * and awaits `@postConstruct` and `onActivation` hooks.
72
+ */
73
+ private instantiateBindingAsync;
74
+ /**
75
+ * Duplicate captive-dependency guard applied at instantiation time (after scope-cache miss),
76
+ * checking the top of the materialization stack rather than the raw `ConstraintContext`.
77
+ */
78
+ private assertCaptiveDependencyFromMaterializationStack;
79
+ /**
80
+ * Synchronously produces the raw instance for a binding without touching the scope cache.
81
+ * Dispatches on `binding.kind`; throws {@link AsyncResolutionError} for `async-dynamic` factories.
82
+ */
83
+ private materialize;
84
+ /**
85
+ * Async counterpart of {@link materialize}; awaits `async-dynamic` factories and
86
+ * recursively resolves `resolved`-binding dependencies with {@link resolveAsync}.
87
+ */
88
+ private materializeAsync;
89
+ /**
90
+ * Reads `@injectable()` constructor metadata and synchronously resolves each parameter,
91
+ * then calls `new ImplementationClass(...deps)`. Throws {@link MissingMetadataError} when
92
+ * the class has constructor parameters but no metadata.
93
+ */
94
+ private instantiateClassBinding;
95
+ /**
96
+ * Async counterpart of {@link instantiateClassBinding}: resolves constructor dependencies
97
+ * with {@link resolveAsync} so `async-dynamic` parameters are awaited in order.
98
+ */
99
+ private instantiateClassBindingAsync;
100
+ }
101
+ //#endregion
102
+ export { DependencyResolver, ResolverDependencies };
@@ -0,0 +1,361 @@
1
+ import { AsyncResolutionError, CircularDependencyError, InternalError, MissingMetadataError, NoMatchingBindingError, ScopeViolationError, TokenNotBoundError } from "./errors.mjs";
2
+ import { filterMatchingBindings, registryKeyLabel, selectBindingForRegistry } from "./binding-select.mjs";
3
+ import { runActivation, runActivationAsync, runPostConstruct, runPostConstructAsync } from "./lifecycle.mjs";
4
+ //#region src/resolver.ts
5
+ /** Converts a registry key + binding into a {@link MaterializationFrame} for the captive-dependency stack. */
6
+ function bindingToMaterializationFrame(registryKey, binding) {
7
+ return {
8
+ registryKey,
9
+ bindingId: binding.id,
10
+ bindingKind: binding.kind,
11
+ tags: binding.tags,
12
+ scope: binding.scope
13
+ };
14
+ }
15
+ /**
16
+ * Walks the binding graph, manages circular-dependency detection, delegates caching to
17
+ * {@link ScopeManager}, and calls lifecycle hooks. Used exclusively by the container.
18
+ */
19
+ var DependencyResolver = class {
20
+ constructor(deps) {
21
+ this.deps = deps;
22
+ }
23
+ /** Entry point for synchronous single-binding resolution (no path prefix). */
24
+ resolveRoot(key, hint) {
25
+ return this.resolve(key, hint, [], /* @__PURE__ */ new Set(), []);
26
+ }
27
+ /** Entry point for async single-binding resolution (no path prefix). */
28
+ resolveAsyncRoot(key, hint) {
29
+ return this.resolveAsync(key, hint, [], /* @__PURE__ */ new Set(), []);
30
+ }
31
+ /** Returns `undefined` instead of throwing when no binding is found; still throws on circular deps or async factories. */
32
+ resolveOptionalRoot(key, hint) {
33
+ const registryKey = key;
34
+ const label = registryKeyLabel(key);
35
+ const pathLabels = [label];
36
+ const bindings = this.deps.lookup(registryKey);
37
+ if (bindings === void 0 || bindings.length === 0) return;
38
+ const candidates = filterMatchingBindings(bindings, hint, this.buildConstraintContext(pathLabels, [], hint));
39
+ if (candidates.length === 0) {
40
+ if (hint !== void 0 && (hint.name !== void 0 || hint.tag !== void 0)) throw new NoMatchingBindingError(label, hint, pathLabels);
41
+ return;
42
+ }
43
+ if (candidates.length !== 1) throw new InternalError(`Ambiguous binding for "${label}": ${String(candidates.length)} candidates matched (resolution path: ${pathLabels.join(" -> ")})`);
44
+ const binding = candidates[0];
45
+ if (binding === void 0) throw new InternalError(`Internal: expected single binding candidate for "${label}" (resolution path: ${pathLabels.join(" -> ")})`);
46
+ this.assertDependencyScopeAllowed(binding, pathLabels, []);
47
+ if (binding.kind === "async-dynamic") throw new AsyncResolutionError(label, pathLabels, "encountered async-dynamic factory during synchronous resolution");
48
+ const visiting = /* @__PURE__ */ new Set();
49
+ visiting.add(registryKey);
50
+ try {
51
+ return this.instantiateBinding(binding, registryKey, hint, pathLabels, visiting, []);
52
+ } finally {
53
+ visiting.delete(registryKey);
54
+ }
55
+ }
56
+ /** Resolves all bindings for `key` synchronously; throws on async factories. */
57
+ resolveAllRoot(key, hint) {
58
+ const registryKey = key;
59
+ const label = registryKeyLabel(key);
60
+ const basePath = [label];
61
+ const bindings = this.deps.lookup(registryKey);
62
+ if (bindings === void 0 || bindings.length === 0) return [];
63
+ const candidates = filterMatchingBindings(bindings, hint, this.buildConstraintContext(basePath, [], hint));
64
+ if (candidates.length === 0) {
65
+ if (hint !== void 0 && (hint.name !== void 0 || hint.tag !== void 0)) throw new NoMatchingBindingError(label, hint, basePath);
66
+ return [];
67
+ }
68
+ const results = [];
69
+ for (const binding of candidates) {
70
+ this.assertDependencyScopeAllowed(binding, basePath, []);
71
+ if (binding.kind === "async-dynamic") throw new AsyncResolutionError(label, basePath, "encountered async-dynamic factory during synchronous resolveAll");
72
+ const visiting = /* @__PURE__ */ new Set();
73
+ visiting.add(registryKey);
74
+ try {
75
+ results.push(this.instantiateBinding(binding, registryKey, hint, basePath, visiting, []));
76
+ } finally {
77
+ visiting.delete(registryKey);
78
+ }
79
+ }
80
+ return results;
81
+ }
82
+ /** Resolves all bindings for `key`, awaiting any async factories in the set. */
83
+ async resolveAllAsyncRoot(key, hint) {
84
+ const registryKey = key;
85
+ const label = registryKeyLabel(key);
86
+ const basePath = [label];
87
+ const bindings = this.deps.lookup(registryKey);
88
+ if (bindings === void 0 || bindings.length === 0) return [];
89
+ const candidates = filterMatchingBindings(bindings, hint, this.buildConstraintContext(basePath, [], hint));
90
+ if (candidates.length === 0) {
91
+ if (hint !== void 0 && (hint.name !== void 0 || hint.tag !== void 0)) throw new NoMatchingBindingError(label, hint, basePath);
92
+ return [];
93
+ }
94
+ const results = [];
95
+ for (const binding of candidates) {
96
+ this.assertDependencyScopeAllowed(binding, basePath, []);
97
+ const visiting = /* @__PURE__ */ new Set();
98
+ visiting.add(registryKey);
99
+ try {
100
+ results.push(await this.instantiateBindingAsync(binding, registryKey, hint, basePath, visiting, []));
101
+ } finally {
102
+ visiting.delete(registryKey);
103
+ }
104
+ }
105
+ return results;
106
+ }
107
+ /**
108
+ * Assembles the read-only {@link ConstraintContext} snapshot passed to `when()` predicates.
109
+ * Extracts the top-of-stack frame as `parent` and the rest as `ancestors`.
110
+ */
111
+ buildConstraintContext(resolutionPath, materializationStack, currentResolveHint) {
112
+ return {
113
+ resolutionPath,
114
+ materializationStack,
115
+ parent: materializationStack.length > 0 ? materializationStack[materializationStack.length - 1] : void 0,
116
+ ancestors: materializationStack.slice(0, -1),
117
+ currentResolveHint
118
+ };
119
+ }
120
+ /**
121
+ * Throws {@link ScopeViolationError} when the immediate consumer on the stack is a singleton
122
+ * and the dependency has `"scoped"` or `"transient"` lifetime (captive dependency).
123
+ * Constants are exempt — they have no scope cache.
124
+ */
125
+ assertDependencyScopeAllowed(dependencyBinding, resolutionPath, materializationStack) {
126
+ const consumer = materializationStack[materializationStack.length - 1];
127
+ if (consumer === void 0) return;
128
+ if (consumer.scope !== "singleton") return;
129
+ if (dependencyBinding.kind === "constant") return;
130
+ if (dependencyBinding.scope === "transient" || dependencyBinding.scope === "scoped") {
131
+ const dependencyLabel = resolutionPath[resolutionPath.length - 1];
132
+ const consumerLabel = resolutionPath.length >= 2 ? resolutionPath[resolutionPath.length - 2] : void 0;
133
+ throw new ScopeViolationError({
134
+ consumerBindingId: consumer.bindingId,
135
+ consumerKind: consumer.bindingKind,
136
+ consumerScope: consumer.scope,
137
+ consumerLabel,
138
+ dependencyBindingId: dependencyBinding.id,
139
+ dependencyKind: dependencyBinding.kind,
140
+ dependencyScope: dependencyBinding.scope,
141
+ dependencyLabel,
142
+ resolutionPath: [...resolutionPath]
143
+ });
144
+ }
145
+ }
146
+ /**
147
+ * Builds a {@link ResolutionContext} for use inside factories and lifecycle hooks.
148
+ * The `resolve` / `resolveAsync` / `resolveOptional` closures carry the current path and
149
+ * materialization stack forward so nested calls inherit captive-dependency checks.
150
+ */
151
+ createContext(pathLabels, visiting, materializationStack, currentResolveHint) {
152
+ return {
153
+ resolve: (token, hint) => this.resolve(token, hint, [...pathLabels], visiting, materializationStack),
154
+ resolveAsync: (token, hint) => this.resolveAsync(token, hint, [...pathLabels], visiting, materializationStack),
155
+ resolveOptional: (token, hint) => {
156
+ try {
157
+ return this.resolve(token, hint, [...pathLabels], visiting, materializationStack);
158
+ } catch (caughtError) {
159
+ if (caughtError instanceof TokenNotBoundError) return;
160
+ throw caughtError;
161
+ }
162
+ },
163
+ graph: this.buildConstraintContext(pathLabels, materializationStack, currentResolveHint)
164
+ };
165
+ }
166
+ /**
167
+ * @param key - Token or constructor being resolved.
168
+ * @param hint - Optional name/tag filter for multi-binding selection.
169
+ * @param pathLabels - Mutable label path accumulated during graph walk; extended in place.
170
+ * @param visiting - Registry keys currently on the call stack; used for circular-dependency detection.
171
+ * @param materializationStack - Bindings along the current construction chain; used to block singleton→scoped/transient.
172
+ */
173
+ resolve(key, hint, pathLabels, visiting, materializationStack) {
174
+ const registryKey = key;
175
+ const label = registryKeyLabel(key);
176
+ const nextPath = [...pathLabels, label];
177
+ if (visiting.has(registryKey)) throw new CircularDependencyError(nextPath);
178
+ const bindings = this.deps.lookup(registryKey);
179
+ if (bindings === void 0 || bindings.length === 0) throw new TokenNotBoundError(label, nextPath);
180
+ const binding = selectBindingForRegistry(bindings, hint, label, nextPath, this.buildConstraintContext(nextPath, materializationStack, hint));
181
+ this.assertDependencyScopeAllowed(binding, nextPath, materializationStack);
182
+ if (binding.kind === "async-dynamic") throw new AsyncResolutionError(label, nextPath, "encountered async-dynamic factory during synchronous resolution");
183
+ visiting.add(registryKey);
184
+ try {
185
+ return this.instantiateBinding(binding, registryKey, hint, nextPath, visiting, materializationStack);
186
+ } finally {
187
+ visiting.delete(registryKey);
188
+ }
189
+ }
190
+ /**
191
+ * @param key - Token or constructor being resolved.
192
+ * @param hint - Optional name/tag filter for multi-binding selection.
193
+ * @param pathLabels - Mutable label path accumulated during graph walk; extended in place.
194
+ * @param visiting - Registry keys currently on the call stack; used for circular-dependency detection.
195
+ * @param materializationStack - Same captive-dependency chain as {@link resolve}.
196
+ */
197
+ async resolveAsync(key, hint, pathLabels, visiting, materializationStack) {
198
+ const registryKey = key;
199
+ const label = registryKeyLabel(key);
200
+ const nextPath = [...pathLabels, label];
201
+ if (visiting.has(registryKey)) throw new CircularDependencyError(nextPath);
202
+ const bindings = this.deps.lookup(registryKey);
203
+ if (bindings === void 0 || bindings.length === 0) throw new TokenNotBoundError(label, nextPath);
204
+ const binding = selectBindingForRegistry(bindings, hint, label, nextPath, this.buildConstraintContext(nextPath, materializationStack, hint));
205
+ this.assertDependencyScopeAllowed(binding, nextPath, materializationStack);
206
+ visiting.add(registryKey);
207
+ try {
208
+ return await this.instantiateBindingAsync(binding, registryKey, hint, nextPath, visiting, materializationStack);
209
+ } finally {
210
+ visiting.delete(registryKey);
211
+ }
212
+ }
213
+ /**
214
+ * Handles scope-cache lookup / storage and lifecycle hooks (`@postConstruct`, `onActivation`)
215
+ * around a synchronous call to {@link materialize}.
216
+ */
217
+ instantiateBinding(binding, registryKey, hint, pathLabels, visiting, materializationStack) {
218
+ this.assertCaptiveDependencyFromMaterializationStack(binding, pathLabels, materializationStack);
219
+ return this.deps.scopeManager.getOrCreate(binding, () => {
220
+ const frame = bindingToMaterializationFrame(registryKey, binding);
221
+ const extendedStack = [...materializationStack, frame];
222
+ const ctx = this.createContext(pathLabels, visiting, extendedStack, hint);
223
+ const instance = this.materialize(binding, hint, ctx, pathLabels, visiting, extendedStack);
224
+ if (binding.kind === "class") runPostConstruct(binding.implementationClass, instance, pathLabels);
225
+ return runActivation(binding, instance, ctx, pathLabels);
226
+ });
227
+ }
228
+ /**
229
+ * Async counterpart of {@link instantiateBinding}: delegates to {@link materializeAsync}
230
+ * and awaits `@postConstruct` and `onActivation` hooks.
231
+ */
232
+ async instantiateBindingAsync(binding, registryKey, hint, pathLabels, visiting, materializationStack) {
233
+ this.assertCaptiveDependencyFromMaterializationStack(binding, pathLabels, materializationStack);
234
+ return this.deps.scopeManager.getOrCreateAsync(binding, async () => {
235
+ const frame = bindingToMaterializationFrame(registryKey, binding);
236
+ const extendedStack = [...materializationStack, frame];
237
+ const ctx = this.createContext(pathLabels, visiting, extendedStack, hint);
238
+ const instance = await this.materializeAsync(binding, hint, ctx, pathLabels, visiting, extendedStack);
239
+ if (binding.kind === "class") await runPostConstructAsync(binding.implementationClass, instance);
240
+ return await runActivationAsync(binding, instance, ctx, pathLabels);
241
+ });
242
+ }
243
+ /**
244
+ * Duplicate captive-dependency guard applied at instantiation time (after scope-cache miss),
245
+ * checking the top of the materialization stack rather than the raw `ConstraintContext`.
246
+ */
247
+ assertCaptiveDependencyFromMaterializationStack(dependencyBinding, resolutionPath, materializationStack) {
248
+ const parentFrame = materializationStack[materializationStack.length - 1];
249
+ if (parentFrame === void 0) return;
250
+ if (parentFrame.scope !== "singleton") return;
251
+ if (dependencyBinding.kind === "constant") return;
252
+ if (dependencyBinding.scope === "scoped" || dependencyBinding.scope === "transient") {
253
+ const dependencyLabel = resolutionPath[resolutionPath.length - 1];
254
+ const consumerLabel = resolutionPath.length >= 2 ? resolutionPath[resolutionPath.length - 2] : void 0;
255
+ throw new ScopeViolationError({
256
+ consumerBindingId: parentFrame.bindingId,
257
+ consumerKind: parentFrame.bindingKind,
258
+ consumerScope: parentFrame.scope,
259
+ consumerLabel,
260
+ dependencyBindingId: dependencyBinding.id,
261
+ dependencyKind: dependencyBinding.kind,
262
+ dependencyScope: dependencyBinding.scope,
263
+ dependencyLabel,
264
+ resolutionPath: [...resolutionPath]
265
+ });
266
+ }
267
+ }
268
+ /**
269
+ * Synchronously produces the raw instance for a binding without touching the scope cache.
270
+ * Dispatches on `binding.kind`; throws {@link AsyncResolutionError} for `async-dynamic` factories.
271
+ */
272
+ materialize(binding, hint, ctx, pathLabels, visiting, materializationStack) {
273
+ switch (binding.kind) {
274
+ case "constant": return binding.value;
275
+ case "class": return this.instantiateClassBinding(binding, pathLabels, visiting, materializationStack);
276
+ case "dynamic": {
277
+ const factoryResult = binding.factory(ctx);
278
+ if (typeof factoryResult === "object" && factoryResult !== null && "then" in factoryResult && typeof factoryResult.then === "function") throw new AsyncResolutionError(pathLabels[pathLabels.length - 1] ?? "(unknown)", pathLabels, "dynamic factory returned a Promise during synchronous resolution");
279
+ return factoryResult;
280
+ }
281
+ case "async-dynamic": throw new AsyncResolutionError(pathLabels[pathLabels.length - 1] ?? "(unknown)", pathLabels, "async-dynamic factory cannot be materialized synchronously");
282
+ case "resolved": {
283
+ const deps = [];
284
+ for (const depToken of binding.dependencyTokens) deps.push(this.resolve(depToken, void 0, pathLabels, visiting, materializationStack));
285
+ const resolvedValue = binding.factory(...deps);
286
+ if (typeof resolvedValue === "object" && resolvedValue !== null && "then" in resolvedValue && typeof resolvedValue.then === "function") throw new AsyncResolutionError(pathLabels[pathLabels.length - 1] ?? "(unknown)", pathLabels, "resolved factory returned a Promise during synchronous resolution");
287
+ return resolvedValue;
288
+ }
289
+ case "alias": return this.resolve(binding.targetToken, hint, pathLabels, visiting, materializationStack);
290
+ default: return binding;
291
+ }
292
+ }
293
+ /**
294
+ * Async counterpart of {@link materialize}; awaits `async-dynamic` factories and
295
+ * recursively resolves `resolved`-binding dependencies with {@link resolveAsync}.
296
+ */
297
+ async materializeAsync(binding, hint, ctx, pathLabels, visiting, materializationStack) {
298
+ switch (binding.kind) {
299
+ case "constant": return binding.value;
300
+ case "class": return this.instantiateClassBindingAsync(binding, pathLabels, visiting, materializationStack);
301
+ case "dynamic": return binding.factory(ctx);
302
+ case "async-dynamic": return await binding.factory(ctx);
303
+ case "resolved": {
304
+ const deps = [];
305
+ for (const depToken of binding.dependencyTokens) deps.push(await this.resolveAsync(depToken, void 0, pathLabels, visiting, materializationStack));
306
+ return binding.factory(...deps);
307
+ }
308
+ case "alias": return this.resolveAsync(binding.targetToken, hint, pathLabels, visiting, materializationStack);
309
+ default: return binding;
310
+ }
311
+ }
312
+ /**
313
+ * Reads `@injectable()` constructor metadata and synchronously resolves each parameter,
314
+ * then calls `new ImplementationClass(...deps)`. Throws {@link MissingMetadataError} when
315
+ * the class has constructor parameters but no metadata.
316
+ */
317
+ instantiateClassBinding(binding, pathLabels, visiting, materializationStack) {
318
+ const reader = this.deps.metadataReader;
319
+ const ImplementationClass = binding.implementationClass;
320
+ if (reader === void 0) return new ImplementationClass();
321
+ const meta = reader.getConstructorMetadata(binding.implementationClass);
322
+ if (ImplementationClass.length > 0 && meta === void 0) throw new MissingMetadataError(registryKeyLabel(binding.implementationClass), pathLabels);
323
+ if (meta === void 0 || meta.params.length === 0) return new ImplementationClass();
324
+ return new ImplementationClass(...meta.params.map((param) => {
325
+ const paramHint = param.name !== void 0 ? { name: param.name } : param.tag !== void 0 ? { tag: param.tag } : void 0;
326
+ if (param.optional) try {
327
+ return this.resolve(param.token, paramHint, pathLabels, visiting, materializationStack);
328
+ } catch (caughtError) {
329
+ if (caughtError instanceof TokenNotBoundError) return;
330
+ throw caughtError;
331
+ }
332
+ return this.resolve(param.token, paramHint, pathLabels, visiting, materializationStack);
333
+ }));
334
+ }
335
+ /**
336
+ * Async counterpart of {@link instantiateClassBinding}: resolves constructor dependencies
337
+ * with {@link resolveAsync} so `async-dynamic` parameters are awaited in order.
338
+ */
339
+ async instantiateClassBindingAsync(binding, pathLabels, visiting, materializationStack) {
340
+ const reader = this.deps.metadataReader;
341
+ const ImplementationClass = binding.implementationClass;
342
+ if (reader === void 0) return new ImplementationClass();
343
+ const meta = reader.getConstructorMetadata(binding.implementationClass);
344
+ if (ImplementationClass.length > 0 && meta === void 0) throw new MissingMetadataError(registryKeyLabel(binding.implementationClass), pathLabels);
345
+ if (meta === void 0 || meta.params.length === 0) return new ImplementationClass();
346
+ const deps = [];
347
+ for (const param of meta.params) {
348
+ const paramHint = param.name !== void 0 ? { name: param.name } : param.tag !== void 0 ? { tag: param.tag } : void 0;
349
+ if (param.optional) try {
350
+ deps.push(await this.resolveAsync(param.token, paramHint, pathLabels, visiting, materializationStack));
351
+ } catch (caughtError) {
352
+ if (caughtError instanceof TokenNotBoundError) deps.push(void 0);
353
+ else throw caughtError;
354
+ }
355
+ else deps.push(await this.resolveAsync(param.token, paramHint, pathLabels, visiting, materializationStack));
356
+ }
357
+ return new ImplementationClass(...deps);
358
+ }
359
+ };
360
+ //#endregion
361
+ export { DependencyResolver };
@@ -0,0 +1,20 @@
1
+ import { RegistryKey } from "./registry.mjs";
2
+ import { Binding } from "./binding.mjs";
3
+ import { MetadataReader } from "./metadata/metadata-types.mjs";
4
+
5
+ //#region src/scope-validation.d.ts
6
+ /**
7
+ * Walks every binding in the registry and throws {@link ScopeViolationError} on the first
8
+ * captive-dependency violation found: a singleton that directly or transitively depends on a
9
+ * scoped or transient binding. Constant bindings are exempt (no stateful instance to capture).
10
+ *
11
+ * Called by {@link Container.validate} and automatically after each `load()` in non-production
12
+ * environments.
13
+ */
14
+ declare function validateScopeRules(context: {
15
+ collectAllRegistryKeys(): readonly RegistryKey[];
16
+ lookupBindings(key: RegistryKey): readonly Binding<unknown>[] | undefined;
17
+ getMetadataReader(): MetadataReader | undefined;
18
+ }): void;
19
+ //#endregion
20
+ export { validateScopeRules };