@codefast/di 0.3.13 → 0.3.14-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +270 -234
- package/dist/binding-select.d.mts +17 -6
- package/dist/binding-select.mjs +17 -6
- package/dist/binding.d.mts +167 -34
- package/dist/binding.mjs +111 -14
- package/dist/constraints.d.mts +18 -3
- package/dist/constraints.mjs +18 -3
- package/dist/container.d.mts +85 -35
- package/dist/container.mjs +140 -6
- package/dist/decorators/inject.d.mts +40 -9
- package/dist/decorators/inject.mjs +50 -11
- package/dist/decorators/injectable.d.mts +2 -1
- package/dist/decorators/injectable.mjs +14 -2
- package/dist/decorators/lifecycle-decorators.d.mts +16 -4
- package/dist/decorators/lifecycle-decorators.mjs +16 -4
- package/dist/dependency-graph.d.mts +36 -13
- package/dist/dependency-graph.mjs +42 -8
- package/dist/errors.d.mts +132 -21
- package/dist/errors.mjs +126 -18
- package/dist/graph-adapters/cytoscape.d.mts +10 -0
- package/dist/graph-adapters/cytoscape.mjs +40 -0
- package/dist/graph-adapters/dot.d.mts +9 -0
- package/dist/graph-adapters/dot.mjs +97 -0
- package/dist/graph-adapters/reactflow.d.mts +10 -0
- package/dist/graph-adapters/reactflow.mjs +80 -0
- package/dist/graph-adapters/types.d.mts +91 -0
- package/dist/graph-adapters/types.mjs +1 -0
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +2 -2
- package/dist/inspector.d.mts +42 -40
- package/dist/inspector.mjs +18 -169
- package/dist/lifecycle.d.mts +28 -6
- package/dist/lifecycle.mjs +29 -10
- package/dist/metadata/metadata-keys.d.mts +17 -6
- package/dist/metadata/metadata-keys.mjs +17 -6
- package/dist/metadata/metadata-types.d.mts +42 -18
- package/dist/metadata/param-registry.mjs +6 -0
- package/dist/metadata/symbol-metadata-reader.d.mts +20 -3
- package/dist/metadata/symbol-metadata-reader.mjs +23 -4
- package/dist/module.d.mts +46 -2
- package/dist/module.mjs +19 -0
- package/dist/registry.d.mts +39 -8
- package/dist/registry.mjs +39 -8
- package/dist/resolver.d.mts +107 -12
- package/dist/resolver.mjs +134 -37
- package/dist/scope-validation.d.mts +3 -2
- package/dist/scope-validation.mjs +3 -2
- package/dist/scope.d.mts +38 -6
- package/dist/scope.mjs +42 -13
- package/dist/token.d.mts +9 -2
- package/dist/token.mjs +7 -1
- package/package.json +18 -2
package/dist/resolver.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { AsyncResolutionError, CircularDependencyError, InternalError, MissingMetadataError, NoMatchingBindingError, ScopeViolationError, TokenNotBoundError } from "./errors.mjs";
|
|
2
2
|
import { filterMatchingBindings, registryKeyLabel, selectBindingForRegistry } from "./binding-select.mjs";
|
|
3
|
-
import { runActivation, runActivationAsync, runPostConstruct, runPostConstructAsync } from "./lifecycle.mjs";
|
|
3
|
+
import { isPromiseLike, runActivation, runActivationAsync, runPostConstruct, runPostConstructAsync } from "./lifecycle.mjs";
|
|
4
4
|
//#region src/resolver.ts
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Converts a registry key + binding into a {@link MaterializationFrame} for the captive-dependency stack.
|
|
7
|
+
*/
|
|
6
8
|
function bindingToMaterializationFrame(registryKey, binding) {
|
|
7
9
|
return {
|
|
8
10
|
registryKey,
|
|
@@ -13,22 +15,66 @@ function bindingToMaterializationFrame(registryKey, binding) {
|
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* Stateless graph walker: selects a binding, checks for cycles and scope violations,
|
|
19
|
+
* delegates instance caching to `ScopeManager`, and runs lifecycle hooks.
|
|
20
|
+
*
|
|
21
|
+
* Resolution algorithm (per token):
|
|
22
|
+
* 1. Lookup all bindings for the registry key.
|
|
23
|
+
* 2. Apply name/tag hint and `when()` constraint filtering.
|
|
24
|
+
* 3. Circular-dependency check via a mutable `visiting` set (per call tree).
|
|
25
|
+
* 4. Captive-dependency check via the `materializationStack`.
|
|
26
|
+
* 5. Scope-cache hit → return cached instance.
|
|
27
|
+
* 6. Scope-cache miss → `materialize` → `@postConstruct` → `onActivation` → cache.
|
|
28
|
+
*
|
|
29
|
+
* Used exclusively by `DefaultContainer`; not part of the public API.
|
|
18
30
|
*/
|
|
19
31
|
var DependencyResolver = class {
|
|
20
32
|
constructor(deps) {
|
|
21
33
|
this.deps = deps;
|
|
22
34
|
}
|
|
23
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Entry point for synchronous single-binding resolution.
|
|
37
|
+
*
|
|
38
|
+
* @throws {@link TokenNotBoundError} — no binding registered for `key`, or a nested dependency is unbound.
|
|
39
|
+
* @throws {@link NoMatchingBindingError} — a name/tag `hint` was given for `key` but no binding matched it.
|
|
40
|
+
* @throws {@link InternalError} — multiple bindings matched for `key` after applying the hint (ambiguous).
|
|
41
|
+
* @throws {@link CircularDependencyError} — `key` or a nested token appears twice on the resolution stack.
|
|
42
|
+
* @throws {@link AsyncResolutionError} — an `async-dynamic` binding or async lifecycle/activation on the sync path.
|
|
43
|
+
* @throws {@link ScopeViolationError} — captive dependency (singleton → scoped/transient).
|
|
44
|
+
* @throws {@link MissingMetadataError} — `class` binding lacks injectable metadata when the reader requires it.
|
|
45
|
+
*/
|
|
24
46
|
resolveRoot(key, hint) {
|
|
25
47
|
return this.resolve(key, hint, [], /* @__PURE__ */ new Set(), []);
|
|
26
48
|
}
|
|
27
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Entry point for async single-binding resolution. Awaits `async-dynamic` factories
|
|
51
|
+
* and async lifecycle hooks that would cause {@link AsyncResolutionError} on the sync path.
|
|
52
|
+
*
|
|
53
|
+
* @throws {@link TokenNotBoundError} — no binding registered for `key`, or a nested dependency is unbound.
|
|
54
|
+
* @throws {@link NoMatchingBindingError} — a name/tag `hint` was given for `key` but no binding matched it.
|
|
55
|
+
* @throws {@link InternalError} — multiple bindings matched for `key` after applying the hint (ambiguous).
|
|
56
|
+
* @throws {@link CircularDependencyError} — `key` or a nested token appears twice on the resolution stack.
|
|
57
|
+
* @throws {@link ScopeViolationError} — captive dependency (singleton → scoped/transient).
|
|
58
|
+
* @throws {@link MissingMetadataError} — `class` binding lacks injectable metadata when the reader requires it.
|
|
59
|
+
*/
|
|
28
60
|
resolveAsyncRoot(key, hint) {
|
|
29
61
|
return this.resolveAsync(key, hint, [], /* @__PURE__ */ new Set(), []);
|
|
30
62
|
}
|
|
31
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Optional resolution for the **requested key only**: returns `undefined` when that key has no
|
|
65
|
+
* bindings or every candidate is filtered out **without** a name/tag hint — without throwing
|
|
66
|
+
* {@link TokenNotBoundError} for those cases. Instantiating the selected binding still runs the
|
|
67
|
+
* normal sync resolution path for nested dependencies; an unregistered transitive dependency
|
|
68
|
+
* throws {@link TokenNotBoundError} as usual.
|
|
69
|
+
*
|
|
70
|
+
* Behavioral rules:
|
|
71
|
+
* 1. If the registry key is completely unbound → returns `undefined`.
|
|
72
|
+
* 2. If no candidate survives constraint filtering (without a hint) → returns `undefined`.
|
|
73
|
+
* 3. If a name/tag hint was provided but no binding matches it → throws {@link NoMatchingBindingError}.
|
|
74
|
+
* 4. Still throws on: circular dependencies, async operations on the sync path,
|
|
75
|
+
* scope violations, ambiguous multi-binding matches, and {@link TokenNotBoundError} when a
|
|
76
|
+
* required nested dependency is unbound.
|
|
77
|
+
*/
|
|
32
78
|
resolveOptionalRoot(key, hint) {
|
|
33
79
|
const registryKey = key;
|
|
34
80
|
const label = registryKeyLabel(key);
|
|
@@ -53,54 +99,88 @@ var DependencyResolver = class {
|
|
|
53
99
|
visiting.delete(registryKey);
|
|
54
100
|
}
|
|
55
101
|
}
|
|
56
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Resolves every matching binding for `key` synchronously into an array.
|
|
104
|
+
* Returns an empty array when no bindings exist (does not throw).
|
|
105
|
+
*
|
|
106
|
+
* @throws {@link AsyncResolutionError} — any candidate is `async-dynamic`.
|
|
107
|
+
* @throws {@link NoMatchingBindingError} — hint was specified but no binding matched it.
|
|
108
|
+
* @throws {@link TokenNotBoundError} — nested dependency unbound (same as {@link resolveRoot}).
|
|
109
|
+
* @throws {@link CircularDependencyError} — cycle while materializing a candidate.
|
|
110
|
+
* @throws {@link ScopeViolationError} — captive dependency during materialization.
|
|
111
|
+
* @throws {@link MissingMetadataError} — class binding lacks injectable metadata when required.
|
|
112
|
+
*/
|
|
57
113
|
resolveAllRoot(key, hint) {
|
|
114
|
+
return this.resolveAll(key, hint, [], /* @__PURE__ */ new Set(), []);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Async counterpart of {@link resolveAllRoot}: resolves every matching binding for `key`,
|
|
118
|
+
* awaiting `async-dynamic` factories. Returns an empty array when no bindings exist.
|
|
119
|
+
*
|
|
120
|
+
* @throws {@link NoMatchingBindingError} — hint was specified but no binding matched it.
|
|
121
|
+
* @throws {@link TokenNotBoundError} — nested dependency unbound (same as {@link resolveAsyncRoot}).
|
|
122
|
+
* @throws {@link CircularDependencyError} — cycle while materializing a candidate.
|
|
123
|
+
* @throws {@link ScopeViolationError} — captive dependency during materialization.
|
|
124
|
+
* @throws {@link MissingMetadataError} — class binding lacks injectable metadata when required.
|
|
125
|
+
*/
|
|
126
|
+
async resolveAllAsyncRoot(key, hint) {
|
|
127
|
+
return this.resolveAllAsync(key, hint, [], /* @__PURE__ */ new Set(), []);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Context-aware sync `resolveAll`: extends the current resolution path and preserves
|
|
131
|
+
* visiting/materialization stacks so nested multi-resolution participates in cycle and
|
|
132
|
+
* captive-dependency checks exactly like single-value {@link resolve}.
|
|
133
|
+
*/
|
|
134
|
+
resolveAll(key, hint, pathLabels, visiting, materializationStack) {
|
|
58
135
|
const registryKey = key;
|
|
59
136
|
const label = registryKeyLabel(key);
|
|
60
|
-
const
|
|
137
|
+
const nextPath = [...pathLabels, label];
|
|
138
|
+
if (visiting.has(registryKey)) throw new CircularDependencyError(nextPath);
|
|
61
139
|
const bindings = this.deps.lookup(registryKey);
|
|
62
140
|
if (bindings === void 0 || bindings.length === 0) return [];
|
|
63
|
-
const candidates = filterMatchingBindings(bindings, hint, this.buildConstraintContext(
|
|
141
|
+
const candidates = filterMatchingBindings(bindings, hint, this.buildConstraintContext(nextPath, materializationStack, hint));
|
|
64
142
|
if (candidates.length === 0) {
|
|
65
|
-
if (hint !== void 0 && (hint.name !== void 0 || hint.tag !== void 0)) throw new NoMatchingBindingError(label, hint,
|
|
143
|
+
if (hint !== void 0 && (hint.name !== void 0 || hint.tag !== void 0)) throw new NoMatchingBindingError(label, hint, nextPath);
|
|
66
144
|
return [];
|
|
67
145
|
}
|
|
146
|
+
visiting.add(registryKey);
|
|
68
147
|
const results = [];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
results.push(this.instantiateBinding(binding, registryKey, hint, basePath, visiting, []));
|
|
76
|
-
} finally {
|
|
77
|
-
visiting.delete(registryKey);
|
|
148
|
+
try {
|
|
149
|
+
for (const binding of candidates) {
|
|
150
|
+
this.assertDependencyScopeAllowed(binding, nextPath, materializationStack);
|
|
151
|
+
if (binding.kind === "async-dynamic") throw new AsyncResolutionError(label, nextPath, "encountered async-dynamic factory during synchronous resolveAll");
|
|
152
|
+
results.push(this.instantiateBinding(binding, registryKey, hint, nextPath, visiting, materializationStack));
|
|
78
153
|
}
|
|
154
|
+
} finally {
|
|
155
|
+
visiting.delete(registryKey);
|
|
79
156
|
}
|
|
80
157
|
return results;
|
|
81
158
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Async counterpart of {@link resolveAll}: keeps the current path and materialization stack
|
|
161
|
+
* so `when()` constraints and scope validation behave consistently for nested multi-resolution.
|
|
162
|
+
*/
|
|
163
|
+
async resolveAllAsync(key, hint, pathLabels, visiting, materializationStack) {
|
|
84
164
|
const registryKey = key;
|
|
85
165
|
const label = registryKeyLabel(key);
|
|
86
|
-
const
|
|
166
|
+
const nextPath = [...pathLabels, label];
|
|
167
|
+
if (visiting.has(registryKey)) throw new CircularDependencyError(nextPath);
|
|
87
168
|
const bindings = this.deps.lookup(registryKey);
|
|
88
169
|
if (bindings === void 0 || bindings.length === 0) return [];
|
|
89
|
-
const candidates = filterMatchingBindings(bindings, hint, this.buildConstraintContext(
|
|
170
|
+
const candidates = filterMatchingBindings(bindings, hint, this.buildConstraintContext(nextPath, materializationStack, hint));
|
|
90
171
|
if (candidates.length === 0) {
|
|
91
|
-
if (hint !== void 0 && (hint.name !== void 0 || hint.tag !== void 0)) throw new NoMatchingBindingError(label, hint,
|
|
172
|
+
if (hint !== void 0 && (hint.name !== void 0 || hint.tag !== void 0)) throw new NoMatchingBindingError(label, hint, nextPath);
|
|
92
173
|
return [];
|
|
93
174
|
}
|
|
175
|
+
visiting.add(registryKey);
|
|
94
176
|
const results = [];
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
results.push(await this.instantiateBindingAsync(binding, registryKey, hint, basePath, visiting, []));
|
|
101
|
-
} finally {
|
|
102
|
-
visiting.delete(registryKey);
|
|
177
|
+
try {
|
|
178
|
+
for (const binding of candidates) {
|
|
179
|
+
this.assertDependencyScopeAllowed(binding, nextPath, materializationStack);
|
|
180
|
+
results.push(await this.instantiateBindingAsync(binding, registryKey, hint, nextPath, visiting, materializationStack));
|
|
103
181
|
}
|
|
182
|
+
} finally {
|
|
183
|
+
visiting.delete(registryKey);
|
|
104
184
|
}
|
|
105
185
|
return results;
|
|
106
186
|
}
|
|
@@ -160,10 +240,15 @@ var DependencyResolver = class {
|
|
|
160
240
|
throw caughtError;
|
|
161
241
|
}
|
|
162
242
|
},
|
|
243
|
+
resolveAll: (token, hint) => this.resolveAll(token, hint, [...pathLabels], visiting, materializationStack),
|
|
244
|
+
resolveAllAsync: (token, hint) => this.resolveAllAsync(token, hint, [...pathLabels], visiting, materializationStack),
|
|
163
245
|
graph: this.buildConstraintContext(pathLabels, materializationStack, currentResolveHint)
|
|
164
246
|
};
|
|
165
247
|
}
|
|
166
248
|
/**
|
|
249
|
+
* Core synchronous resolution: lookup → filter → cycle check → scope check → instantiate.
|
|
250
|
+
* Called recursively when a binding's dependencies need resolution.
|
|
251
|
+
*
|
|
167
252
|
* @param key - Token or constructor being resolved.
|
|
168
253
|
* @param hint - Optional name/tag filter for multi-binding selection.
|
|
169
254
|
* @param pathLabels - Mutable label path accumulated during graph walk; extended in place.
|
|
@@ -188,6 +273,9 @@ var DependencyResolver = class {
|
|
|
188
273
|
}
|
|
189
274
|
}
|
|
190
275
|
/**
|
|
276
|
+
* Core async resolution: same pipeline as {@link resolve} but awaits `async-dynamic`
|
|
277
|
+
* factories and async lifecycle hooks instead of throwing {@link AsyncResolutionError}.
|
|
278
|
+
*
|
|
191
279
|
* @param key - Token or constructor being resolved.
|
|
192
280
|
* @param hint - Optional name/tag filter for multi-binding selection.
|
|
193
281
|
* @param pathLabels - Mutable label path accumulated during graph walk; extended in place.
|
|
@@ -266,8 +354,12 @@ var DependencyResolver = class {
|
|
|
266
354
|
}
|
|
267
355
|
}
|
|
268
356
|
/**
|
|
269
|
-
* Synchronously produces the raw instance for a binding without touching the scope cache
|
|
270
|
-
*
|
|
357
|
+
* Synchronously produces the raw instance for a binding **without** touching the scope cache
|
|
358
|
+
* or running lifecycle hooks. The caller ({@link instantiateBinding}) wraps this with
|
|
359
|
+
* cache logic and post-construction hooks.
|
|
360
|
+
*
|
|
361
|
+
* Dispatches on `binding.kind`; throws {@link AsyncResolutionError} if the binding is
|
|
362
|
+
* `async-dynamic` or if a `dynamic` / `resolved` factory returns a Promise.
|
|
271
363
|
*/
|
|
272
364
|
materialize(binding, hint, ctx, pathLabels, visiting, materializationStack) {
|
|
273
365
|
switch (binding.kind) {
|
|
@@ -275,7 +367,7 @@ var DependencyResolver = class {
|
|
|
275
367
|
case "class": return this.instantiateClassBinding(binding, pathLabels, visiting, materializationStack);
|
|
276
368
|
case "dynamic": {
|
|
277
369
|
const factoryResult = binding.factory(ctx);
|
|
278
|
-
if (
|
|
370
|
+
if (isPromiseLike(factoryResult)) throw new AsyncResolutionError(pathLabels[pathLabels.length - 1] ?? "(unknown)", pathLabels, "dynamic factory returned a Promise during synchronous resolution");
|
|
279
371
|
return factoryResult;
|
|
280
372
|
}
|
|
281
373
|
case "async-dynamic": throw new AsyncResolutionError(pathLabels[pathLabels.length - 1] ?? "(unknown)", pathLabels, "async-dynamic factory cannot be materialized synchronously");
|
|
@@ -283,7 +375,7 @@ var DependencyResolver = class {
|
|
|
283
375
|
const deps = [];
|
|
284
376
|
for (const depToken of binding.dependencyTokens) deps.push(this.resolve(depToken, void 0, pathLabels, visiting, materializationStack));
|
|
285
377
|
const resolvedValue = binding.factory(...deps);
|
|
286
|
-
if (
|
|
378
|
+
if (isPromiseLike(resolvedValue)) throw new AsyncResolutionError(pathLabels[pathLabels.length - 1] ?? "(unknown)", pathLabels, "resolved factory returned a Promise during synchronous resolution");
|
|
287
379
|
return resolvedValue;
|
|
288
380
|
}
|
|
289
381
|
case "alias": return this.resolve(binding.targetToken, hint, pathLabels, visiting, materializationStack);
|
|
@@ -323,6 +415,7 @@ var DependencyResolver = class {
|
|
|
323
415
|
if (meta === void 0 || meta.params.length === 0) return new ImplementationClass();
|
|
324
416
|
return new ImplementationClass(...meta.params.map((param) => {
|
|
325
417
|
const paramHint = param.name !== void 0 ? { name: param.name } : param.tag !== void 0 ? { tag: param.tag } : void 0;
|
|
418
|
+
if (param.all === true) return this.resolveAll(param.token, paramHint, pathLabels, visiting, materializationStack);
|
|
326
419
|
if (param.optional) try {
|
|
327
420
|
return this.resolve(param.token, paramHint, pathLabels, visiting, materializationStack);
|
|
328
421
|
} catch (caughtError) {
|
|
@@ -346,6 +439,10 @@ var DependencyResolver = class {
|
|
|
346
439
|
const deps = [];
|
|
347
440
|
for (const param of meta.params) {
|
|
348
441
|
const paramHint = param.name !== void 0 ? { name: param.name } : param.tag !== void 0 ? { tag: param.tag } : void 0;
|
|
442
|
+
if (param.all === true) {
|
|
443
|
+
deps.push(await this.resolveAllAsync(param.token, paramHint, pathLabels, visiting, materializationStack));
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
349
446
|
if (param.optional) try {
|
|
350
447
|
deps.push(await this.resolveAsync(param.token, paramHint, pathLabels, visiting, materializationStack));
|
|
351
448
|
} catch (caughtError) {
|
|
@@ -5,8 +5,9 @@ import { MetadataReader } from "./metadata/metadata-types.mjs";
|
|
|
5
5
|
//#region src/scope-validation.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Walks every binding in the registry and throws {@link ScopeViolationError} on the first
|
|
8
|
-
* captive-dependency violation found
|
|
9
|
-
*
|
|
8
|
+
* captive-dependency violation found **along a direct static edge**: for each binding, only
|
|
9
|
+
* targets returned by {@link listResolvedDependencies} are considered (not a full recursive
|
|
10
|
+
* graph walk). Constant bindings are exempt.
|
|
10
11
|
*
|
|
11
12
|
* Called by {@link Container.validate} and automatically after each `load()` in non-production
|
|
12
13
|
* environments.
|
|
@@ -4,8 +4,9 @@ import { listResolvedDependencies } from "./dependency-graph.mjs";
|
|
|
4
4
|
//#region src/scope-validation.ts
|
|
5
5
|
/**
|
|
6
6
|
* Walks every binding in the registry and throws {@link ScopeViolationError} on the first
|
|
7
|
-
* captive-dependency violation found
|
|
8
|
-
*
|
|
7
|
+
* captive-dependency violation found **along a direct static edge**: for each binding, only
|
|
8
|
+
* targets returned by {@link listResolvedDependencies} are considered (not a full recursive
|
|
9
|
+
* graph walk). Constant bindings are exempt.
|
|
9
10
|
*
|
|
10
11
|
* Called by {@link Container.validate} and automatically after each `load()` in non-production
|
|
11
12
|
* environments.
|
package/dist/scope.d.mts
CHANGED
|
@@ -2,19 +2,49 @@ import { Binding, BindingIdentifier } from "./binding.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/scope.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* Caches singleton and scoped instances
|
|
5
|
+
* Caches singleton and scoped instances, deduplicates concurrent async creation, and runs
|
|
6
|
+
* deactivation hooks (`onDeactivation`, `@preDestroy`) on disposal.
|
|
7
|
+
*
|
|
8
|
+
* A root scope manager owns both singleton and scoped caches. A child scope manager (created
|
|
9
|
+
* via {@link createChildScope}) shares the parent's singleton cache but receives a fresh scoped
|
|
10
|
+
* cache — singletons are shared across the hierarchy, scoped instances are isolated per child.
|
|
11
|
+
*
|
|
12
|
+
* Invariant: `ownsSingletonDisposal` is `true` only for the root. When a child disposes, only
|
|
13
|
+
* its scoped bindings are deactivated; singletons remain alive until the root disposes.
|
|
6
14
|
*/
|
|
7
15
|
declare class ScopeManager {
|
|
8
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Cached singleton instances: `bindingId → { binding, instance }`.
|
|
18
|
+
*/
|
|
9
19
|
private readonly singletonCache;
|
|
10
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Cached scoped instances for this container level: `bindingId → { binding, instance }`.
|
|
22
|
+
*/
|
|
11
23
|
private readonly scopedCache;
|
|
24
|
+
/**
|
|
25
|
+
* True only for the root scope manager. Controls whether {@link dispose} / {@link disposeAsync}
|
|
26
|
+
* also drain the shared singleton cache; child scopes leave singleton disposal to the root.
|
|
27
|
+
*/
|
|
12
28
|
private readonly ownsSingletonDisposal;
|
|
13
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* In-flight async singleton creation promises.
|
|
31
|
+
* Guards against double-instantiation when multiple `resolveAsync` calls for the same
|
|
32
|
+
* singleton binding overlap before the first one settles.
|
|
33
|
+
*/
|
|
14
34
|
private readonly singletonPendingPromises;
|
|
35
|
+
/**
|
|
36
|
+
* In-flight async scoped creation promises (same deduplication role as
|
|
37
|
+
* {@link singletonPendingPromises} but for scoped bindings).
|
|
38
|
+
*/
|
|
15
39
|
private readonly scopedPendingPromises;
|
|
40
|
+
/**
|
|
41
|
+
* Internal constructor for root/child scope managers.
|
|
42
|
+
* Prefer {@link createRoot} and {@link createChildScope}.
|
|
43
|
+
*/
|
|
16
44
|
private constructor();
|
|
17
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Creates a root scope manager that owns both the singleton cache and scoped cache.
|
|
47
|
+
*/
|
|
18
48
|
static createRoot(): ScopeManager;
|
|
19
49
|
/**
|
|
20
50
|
* Shares the parent singleton cache; receives a fresh scoped cache (for child containers).
|
|
@@ -24,7 +54,9 @@ declare class ScopeManager {
|
|
|
24
54
|
* Whether a singleton/scoped binding currently has a cached instance (always false for transient).
|
|
25
55
|
*/
|
|
26
56
|
isBindingCached(binding: Binding<unknown>): boolean;
|
|
27
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Returns the cached instance for singleton/scoped bindings, or calls `createInstance` on first access.
|
|
59
|
+
*/
|
|
28
60
|
getOrCreate(binding: Binding<unknown>, createInstance: () => unknown): unknown;
|
|
29
61
|
/**
|
|
30
62
|
* Async variant of {@link getOrCreate}. Deduplicates concurrent creation calls for the same
|
package/dist/scope.mjs
CHANGED
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
import { InternalError } from "./errors.mjs";
|
|
2
|
-
import { runPreDestroy, runPreDestroyAsync } from "./lifecycle.mjs";
|
|
2
|
+
import { isPromiseLike, runPreDestroy, runPreDestroyAsync } from "./lifecycle.mjs";
|
|
3
3
|
//#region src/scope.ts
|
|
4
|
-
/** Returns `true` when `value` is a thenable (duck-typed Promise check). */
|
|
5
|
-
function isPromiseLike(value) {
|
|
6
|
-
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
7
|
-
}
|
|
8
4
|
/**
|
|
9
|
-
* Caches singleton and scoped instances
|
|
5
|
+
* Caches singleton and scoped instances, deduplicates concurrent async creation, and runs
|
|
6
|
+
* deactivation hooks (`onDeactivation`, `@preDestroy`) on disposal.
|
|
7
|
+
*
|
|
8
|
+
* A root scope manager owns both singleton and scoped caches. A child scope manager (created
|
|
9
|
+
* via {@link createChildScope}) shares the parent's singleton cache but receives a fresh scoped
|
|
10
|
+
* cache — singletons are shared across the hierarchy, scoped instances are isolated per child.
|
|
11
|
+
*
|
|
12
|
+
* Invariant: `ownsSingletonDisposal` is `true` only for the root. When a child disposes, only
|
|
13
|
+
* its scoped bindings are deactivated; singletons remain alive until the root disposes.
|
|
10
14
|
*/
|
|
11
15
|
var ScopeManager = class ScopeManager {
|
|
12
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Cached singleton instances: `bindingId → { binding, instance }`.
|
|
18
|
+
*/
|
|
13
19
|
singletonCache;
|
|
14
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Cached scoped instances for this container level: `bindingId → { binding, instance }`.
|
|
22
|
+
*/
|
|
15
23
|
scopedCache;
|
|
24
|
+
/**
|
|
25
|
+
* True only for the root scope manager. Controls whether {@link dispose} / {@link disposeAsync}
|
|
26
|
+
* also drain the shared singleton cache; child scopes leave singleton disposal to the root.
|
|
27
|
+
*/
|
|
16
28
|
ownsSingletonDisposal;
|
|
17
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* In-flight async singleton creation promises.
|
|
31
|
+
* Guards against double-instantiation when multiple `resolveAsync` calls for the same
|
|
32
|
+
* singleton binding overlap before the first one settles.
|
|
33
|
+
*/
|
|
18
34
|
singletonPendingPromises;
|
|
35
|
+
/**
|
|
36
|
+
* In-flight async scoped creation promises (same deduplication role as
|
|
37
|
+
* {@link singletonPendingPromises} but for scoped bindings).
|
|
38
|
+
*/
|
|
19
39
|
scopedPendingPromises;
|
|
40
|
+
/**
|
|
41
|
+
* Internal constructor for root/child scope managers.
|
|
42
|
+
* Prefer {@link createRoot} and {@link createChildScope}.
|
|
43
|
+
*/
|
|
20
44
|
constructor(singletonCache, scopedCache, ownsSingletonDisposal, singletonPendingPromises, scopedPendingPromises) {
|
|
21
45
|
this.singletonCache = singletonCache;
|
|
22
46
|
this.scopedCache = scopedCache;
|
|
@@ -24,7 +48,9 @@ var ScopeManager = class ScopeManager {
|
|
|
24
48
|
this.singletonPendingPromises = singletonPendingPromises;
|
|
25
49
|
this.scopedPendingPromises = scopedPendingPromises;
|
|
26
50
|
}
|
|
27
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Creates a root scope manager that owns both the singleton cache and scoped cache.
|
|
53
|
+
*/
|
|
28
54
|
static createRoot() {
|
|
29
55
|
return new ScopeManager(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), true, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
|
|
30
56
|
}
|
|
@@ -41,7 +67,9 @@ var ScopeManager = class ScopeManager {
|
|
|
41
67
|
if (binding.scope === "transient") return false;
|
|
42
68
|
return (binding.scope === "singleton" ? this.singletonCache : this.scopedCache).has(binding.id);
|
|
43
69
|
}
|
|
44
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Returns the cached instance for singleton/scoped bindings, or calls `createInstance` on first access.
|
|
72
|
+
*/
|
|
45
73
|
getOrCreate(binding, createInstance) {
|
|
46
74
|
if (binding.scope === "transient") return createInstance();
|
|
47
75
|
const cache = binding.scope === "singleton" ? this.singletonCache : this.scopedCache;
|
|
@@ -153,8 +181,9 @@ var ScopeManager = class ScopeManager {
|
|
|
153
181
|
* synchronously. Throws {@link InternalError} if any handler returns a Promise.
|
|
154
182
|
*/
|
|
155
183
|
disposeMap(store) {
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
const entries = [...store.values()];
|
|
185
|
+
store.clear();
|
|
186
|
+
for (const entry of entries) {
|
|
158
187
|
const handler = entry.binding.onDeactivation;
|
|
159
188
|
if (handler !== void 0) {
|
|
160
189
|
if (isPromiseLike(handler(entry.instance))) throw new InternalError("onDeactivation returned a Promise; use disposeAsync() instead of dispose().");
|
package/dist/token.d.mts
CHANGED
|
@@ -9,11 +9,18 @@ type Token<Value> = {
|
|
|
9
9
|
readonly name: string;
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
|
-
* Extracts the value type carried by a {@link Token}.
|
|
12
|
+
* Extracts the value type carried by a {@link Token} or the instance type of a {@link Constructor}.
|
|
13
|
+
* Falls through to `never` for types that are neither a token nor a constructor.
|
|
13
14
|
*/
|
|
14
15
|
type TokenValue<Type> = Type extends Token<infer Value> ? Value : Type extends (abstract new (...args: never[]) => infer Value) ? Value : never;
|
|
15
16
|
/**
|
|
16
|
-
* Creates a type-safe injection token identified by `name
|
|
17
|
+
* Creates a frozen, type-safe injection token identified by `name`.
|
|
18
|
+
*
|
|
19
|
+
* The returned object is `Object.freeze`-d; `name` is used only for debugging and error
|
|
20
|
+
* messages — binding lookup relies on **reference equality** of the token object.
|
|
21
|
+
* Store the return value in a module-level `const` and import it wherever needed.
|
|
22
|
+
*
|
|
23
|
+
* @param name - Human-readable label (appears in error messages, graph output, and debug snapshots).
|
|
17
24
|
*/
|
|
18
25
|
declare function token<Value>(name: string): Token<Value>;
|
|
19
26
|
//#endregion
|
package/dist/token.mjs
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
//#region src/token.ts
|
|
2
2
|
/**
|
|
3
|
-
* Creates a type-safe injection token identified by `name
|
|
3
|
+
* Creates a frozen, type-safe injection token identified by `name`.
|
|
4
|
+
*
|
|
5
|
+
* The returned object is `Object.freeze`-d; `name` is used only for debugging and error
|
|
6
|
+
* messages — binding lookup relies on **reference equality** of the token object.
|
|
7
|
+
* Store the return value in a module-level `const` and import it wherever needed.
|
|
8
|
+
*
|
|
9
|
+
* @param name - Human-readable label (appears in error messages, graph output, and debug snapshots).
|
|
4
10
|
*/
|
|
5
11
|
function token(name) {
|
|
6
12
|
return Object.freeze({ name });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codefast/di",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14-canary.1",
|
|
4
4
|
"description": "Lightweight dependency injection primitives for Codefast",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codefast",
|
|
@@ -81,6 +81,22 @@
|
|
|
81
81
|
"types": "./dist/errors.d.mts",
|
|
82
82
|
"import": "./dist/errors.mjs"
|
|
83
83
|
},
|
|
84
|
+
"./graph-adapters/cytoscape": {
|
|
85
|
+
"types": "./dist/graph-adapters/cytoscape.d.mts",
|
|
86
|
+
"import": "./dist/graph-adapters/cytoscape.mjs"
|
|
87
|
+
},
|
|
88
|
+
"./graph-adapters/dot": {
|
|
89
|
+
"types": "./dist/graph-adapters/dot.d.mts",
|
|
90
|
+
"import": "./dist/graph-adapters/dot.mjs"
|
|
91
|
+
},
|
|
92
|
+
"./graph-adapters/reactflow": {
|
|
93
|
+
"types": "./dist/graph-adapters/reactflow.d.mts",
|
|
94
|
+
"import": "./dist/graph-adapters/reactflow.mjs"
|
|
95
|
+
},
|
|
96
|
+
"./graph-adapters/types": {
|
|
97
|
+
"types": "./dist/graph-adapters/types.d.mts",
|
|
98
|
+
"import": "./dist/graph-adapters/types.mjs"
|
|
99
|
+
},
|
|
84
100
|
"./inspector": {
|
|
85
101
|
"types": "./dist/inspector.d.mts",
|
|
86
102
|
"import": "./dist/inspector.mjs"
|
|
@@ -141,7 +157,7 @@
|
|
|
141
157
|
"typescript": "^6.0.2",
|
|
142
158
|
"unplugin-swc": "^1.5.9",
|
|
143
159
|
"vitest": "^4.1.4",
|
|
144
|
-
"@codefast/typescript-config": "0.3.
|
|
160
|
+
"@codefast/typescript-config": "0.3.14-canary.1"
|
|
145
161
|
},
|
|
146
162
|
"engines": {
|
|
147
163
|
"node": ">=22.0.0"
|