@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +270 -234
  3. package/dist/binding-select.d.mts +17 -6
  4. package/dist/binding-select.mjs +17 -6
  5. package/dist/binding.d.mts +167 -34
  6. package/dist/binding.mjs +111 -14
  7. package/dist/constraints.d.mts +18 -3
  8. package/dist/constraints.mjs +18 -3
  9. package/dist/container.d.mts +85 -35
  10. package/dist/container.mjs +140 -6
  11. package/dist/decorators/inject.d.mts +40 -9
  12. package/dist/decorators/inject.mjs +50 -11
  13. package/dist/decorators/injectable.d.mts +2 -1
  14. package/dist/decorators/injectable.mjs +14 -2
  15. package/dist/decorators/lifecycle-decorators.d.mts +16 -4
  16. package/dist/decorators/lifecycle-decorators.mjs +16 -4
  17. package/dist/dependency-graph.d.mts +36 -13
  18. package/dist/dependency-graph.mjs +42 -8
  19. package/dist/errors.d.mts +132 -21
  20. package/dist/errors.mjs +126 -18
  21. package/dist/graph-adapters/cytoscape.d.mts +10 -0
  22. package/dist/graph-adapters/cytoscape.mjs +40 -0
  23. package/dist/graph-adapters/dot.d.mts +9 -0
  24. package/dist/graph-adapters/dot.mjs +97 -0
  25. package/dist/graph-adapters/reactflow.d.mts +10 -0
  26. package/dist/graph-adapters/reactflow.mjs +80 -0
  27. package/dist/graph-adapters/types.d.mts +91 -0
  28. package/dist/graph-adapters/types.mjs +1 -0
  29. package/dist/index.d.mts +2 -3
  30. package/dist/index.mjs +2 -2
  31. package/dist/inspector.d.mts +42 -40
  32. package/dist/inspector.mjs +18 -169
  33. package/dist/lifecycle.d.mts +28 -6
  34. package/dist/lifecycle.mjs +29 -10
  35. package/dist/metadata/metadata-keys.d.mts +17 -6
  36. package/dist/metadata/metadata-keys.mjs +17 -6
  37. package/dist/metadata/metadata-types.d.mts +42 -18
  38. package/dist/metadata/param-registry.mjs +6 -0
  39. package/dist/metadata/symbol-metadata-reader.d.mts +20 -3
  40. package/dist/metadata/symbol-metadata-reader.mjs +23 -4
  41. package/dist/module.d.mts +46 -2
  42. package/dist/module.mjs +19 -0
  43. package/dist/registry.d.mts +39 -8
  44. package/dist/registry.mjs +39 -8
  45. package/dist/resolver.d.mts +107 -12
  46. package/dist/resolver.mjs +134 -37
  47. package/dist/scope-validation.d.mts +3 -2
  48. package/dist/scope-validation.mjs +3 -2
  49. package/dist/scope.d.mts +38 -6
  50. package/dist/scope.mjs +42 -13
  51. package/dist/token.d.mts +9 -2
  52. package/dist/token.mjs +7 -1
  53. 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
- /** Converts a registry key + binding into a {@link MaterializationFrame} for the captive-dependency stack. */
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
- * Walks the binding graph, manages circular-dependency detection, delegates caching to
17
- * {@link ScopeManager}, and calls lifecycle hooks. Used exclusively by the container.
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
- /** Entry point for synchronous single-binding resolution (no path prefix). */
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
- /** Entry point for async single-binding resolution (no path prefix). */
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
- /** Returns `undefined` instead of throwing when no binding is found; still throws on circular deps or async factories. */
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
- /** Resolves all bindings for `key` synchronously; throws on async factories. */
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 basePath = [label];
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(basePath, [], hint));
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, basePath);
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
- 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);
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
- /** Resolves all bindings for `key`, awaiting any async factories in the set. */
83
- async resolveAllAsyncRoot(key, hint) {
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 basePath = [label];
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(basePath, [], hint));
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, basePath);
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
- 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);
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
- * Dispatches on `binding.kind`; throws {@link AsyncResolutionError} for `async-dynamic` factories.
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 (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");
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 (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");
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: a singleton that directly or transitively depends on a
9
- * scoped or transient binding. Constant bindings are exempt (no stateful instance to capture).
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: a singleton that directly or transitively depends on a
8
- * scoped or transient binding. Constant bindings are exempt (no stateful instance to capture).
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 and runs deactivation hooks on disposal.
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
- /** Cached singleton instances: `bindingId → { binding, instance }`. */
16
+ /**
17
+ * Cached singleton instances: `bindingId → { binding, instance }`.
18
+ */
9
19
  private readonly singletonCache;
10
- /** Cached scoped instances for this container level: `bindingId → { binding, instance }`. */
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
- /** In-flight async singleton creation promises (deduplicate concurrent resolveAsync calls). */
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
- /** Creates a root scope manager that owns both the singleton cache and scoped cache. */
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
- /** Returns the cached instance for singleton/scoped bindings, or calls `createInstance` on first access. */
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 and runs deactivation hooks on disposal.
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
- /** Cached singleton instances: `bindingId → { binding, instance }`. */
16
+ /**
17
+ * Cached singleton instances: `bindingId → { binding, instance }`.
18
+ */
13
19
  singletonCache;
14
- /** Cached scoped instances for this container level: `bindingId → { binding, instance }`. */
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
- /** In-flight async singleton creation promises (deduplicate concurrent resolveAsync calls). */
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
- /** Creates a root scope manager that owns both the singleton cache and scoped cache. */
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
- /** Returns the cached instance for singleton/scoped bindings, or calls `createInstance` on first access. */
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
- for (const [bindingId, entry] of [...store.entries()]) {
157
- store.delete(bindingId);
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` (for debugging and errors).
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` (for debugging and errors).
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.13",
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.13"
160
+ "@codefast/typescript-config": "0.3.14-canary.1"
145
161
  },
146
162
  "engines": {
147
163
  "node": ">=22.0.0"