@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
@@ -4,36 +4,51 @@ import { MetadataReader } from "./metadata/metadata-types.mjs";
4
4
  import { collectStaticDependencyEdges } from "./dependency-graph.mjs";
5
5
 
6
6
  //#region src/inspector.d.ts
7
- /** Whether a singleton/scoped binding's instance is currently held in the scope cache. */
7
+ /**
8
+ * Whether a singleton/scoped binding's instance is currently held in the scope cache.
9
+ */
8
10
  type BindingActivationStatus = "cached" | "not-cached" | "transient";
9
- /** Per-binding row inside a {@link ContainerSnapshot}. */
11
+ /**
12
+ * Per-binding row inside a {@link ContainerSnapshot}.
13
+ */
10
14
  type ContainerBindingSnapshot = {
11
- readonly registryKeyLabel: string;
12
- readonly bindingId: BindingIdentifier;
13
- readonly kind: Binding<unknown>["kind"];
14
- readonly scope: BindingScope;
15
- readonly activationStatus: BindingActivationStatus; /** True when {@link BindingBuilder.when} was used (runtime predicate; static graph may still show edges). */
15
+ /** Human-readable token/constructor label that owns this binding row. */readonly registryKeyLabel: string; /** Stable binding identifier. */
16
+ readonly bindingId: BindingIdentifier; /** Binding strategy kind. */
17
+ readonly kind: Binding<unknown>["kind"]; /** Declared binding scope. */
18
+ readonly scope: BindingScope; /** Cache/materialization status at snapshot time. */
19
+ readonly activationStatus: BindingActivationStatus;
20
+ /**
21
+ * True when {@link BindingBuilder.when} was used (runtime predicate; static graph may still show edges).
22
+ */
16
23
  readonly hasConditionalConstraint: boolean;
17
24
  readonly moduleId?: string;
18
25
  };
19
- /** Full debug snapshot returned by {@link Container.inspect}. */
26
+ /**
27
+ * Full debug snapshot returned by {@link Container.inspect}.
28
+ */
20
29
  type ContainerSnapshot = {
21
- readonly bindings: readonly ContainerBindingSnapshot[];
30
+ /** Flat list of every visible binding row in the container hierarchy. */readonly bindings: readonly ContainerBindingSnapshot[];
22
31
  };
23
- /** Structured dependency graph returned by {@link Container.generateDependencyGraph} with `format: "json"`. */
32
+ /**
33
+ * Canonical structured dependency graph returned by {@link Container.generateDependencyGraph}.
34
+ */
24
35
  type ContainerGraphJson = {
25
- nodes: ContainerBindingSnapshot[];
36
+ /** Graph nodes (same shape as snapshot rows). */nodes: ContainerBindingSnapshot[]; /** Directed dependency edges between node binding ids. */
26
37
  edges: ReturnType<typeof collectStaticDependencyEdges>[number][];
27
38
  };
28
- /** Read-only view of the container internals exposed to {@link ContainerInspector}. */
39
+ /**
40
+ * Read-only view of the container internals exposed to {@link ContainerInspector}.
41
+ */
29
42
  type ContainerInspectorContext = {
30
- collectAllRegistryKeys(): readonly RegistryKey[];
31
- lookupBindings(key: RegistryKey): readonly Binding<unknown>[] | undefined;
32
- isBindingCached(binding: Binding<unknown>): boolean;
43
+ /** Enumerates every registry key visible to the inspector. */collectAllRegistryKeys(): readonly RegistryKey[]; /** Returns bindings for a given key, including hierarchy lookup behavior. */
44
+ lookupBindings(key: RegistryKey): readonly Binding<unknown>[] | undefined; /** Reports whether a binding currently has a cached scoped/singleton instance. */
45
+ isBindingCached(binding: Binding<unknown>): boolean; /** Metadata reader used for static constructor/lifecycle analysis. */
33
46
  metadataReader: MetadataReader | undefined;
34
47
  };
35
- /** Options for {@link Container.generateDependencyGraph}. */
36
- type DotGraphOptions = {
48
+ /**
49
+ * Options for {@link Container.generateDependencyGraph}.
50
+ */
51
+ type GraphOptions = {
37
52
  /**
38
53
  * When true, omit registry keys whose label starts with `CODEFAST_DI_` (framework-style tokens)
39
54
  * and any edges that would only connect hidden nodes.
@@ -41,36 +56,23 @@ type DotGraphOptions = {
41
56
  readonly hideInternals?: boolean;
42
57
  };
43
58
  /**
44
- * Read-only introspection and Graphviz DOT export for a container graph.
45
- */
46
- /**
47
- * Reads the registry and scope-cache state to produce debug snapshots and dependency graphs.
48
- * Instantiated internally by the container; advanced consumers can construct it directly via
49
- * the `@codefast/di/inspector` subpath export.
59
+ * Reads the container's registry and scope-cache state to produce debug snapshots
60
+ * and canonical dependency-graph output (`nodes` + `edges`).
61
+ *
62
+ * Constructed internally by the container; advanced consumers can also construct it
63
+ * directly via the `@codefast/di/inspector` subpath export.
50
64
  */
51
65
  declare class ContainerInspector {
52
66
  private readonly ctx;
53
67
  constructor(ctx: ContainerInspectorContext);
54
- /** Collects all registered bindings into a flat, serialisable snapshot. */
55
- getSnapshot(): ContainerSnapshot;
56
68
  /**
57
- * Produces a Graphviz `digraph` string with HTML-label nodes and styled edges.
58
- * Cycles are represented as ordinary edges (Graphviz renders them correctly).
59
- * Pass `hideInternals: true` to suppress `CODEFAST_DI_`-prefixed tokens.
69
+ * Collects all registered bindings into a flat, serialisable snapshot.
60
70
  */
61
- generateDotGraph(options?: DotGraphOptions): string;
62
- generateDependencyGraph(options?: DotGraphOptions & {
63
- format?: "dot";
64
- }): string;
65
- generateDependencyGraph(options: DotGraphOptions & {
66
- format: "json";
67
- }): ContainerGraphJson;
68
- generateDependencyGraphJsonTyped(options?: DotGraphOptions): ContainerGraphJson;
71
+ getSnapshot(): ContainerSnapshot;
69
72
  /**
70
- * Produces a JSON graph representation with `nodes` and `edges`.
71
- * @internal Use `generateDependencyGraph({ format: "json" })` for typed output.
73
+ * Builds the canonical JSON graph (`nodes` + `edges`).
72
74
  */
73
- generateDependencyGraphJson(options?: DotGraphOptions): string;
75
+ generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
74
76
  }
75
77
  //#endregion
76
- export { BindingActivationStatus, ContainerBindingSnapshot, ContainerGraphJson, ContainerInspector, ContainerInspectorContext, ContainerSnapshot, DotGraphOptions };
78
+ export { BindingActivationStatus, ContainerBindingSnapshot, ContainerGraphJson, ContainerInspector, ContainerInspectorContext, ContainerSnapshot, GraphOptions };
@@ -1,65 +1,39 @@
1
1
  import { registryKeyLabel } from "./binding-select.mjs";
2
2
  import { collectStaticDependencyEdges } from "./dependency-graph.mjs";
3
3
  //#region src/inspector.ts
4
- /** Maps a binding's scope and cache state to a {@link BindingActivationStatus} label. */
4
+ /**
5
+ * Maps a binding's scope and cache state to a {@link BindingActivationStatus} label.
6
+ */
5
7
  function activationStatusFor(binding, isCached) {
6
8
  if (binding.scope === "transient") return "transient";
7
9
  return isCached(binding) ? "cached" : "not-cached";
8
10
  }
9
- /** Escapes a string for use as a DOT `label="..."` attribute value. */
10
- function dotEscapeLabel(text) {
11
- return text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n");
12
- }
13
- /** Escapes a string for safe embedding inside a DOT HTML-label (`<...>`) table cell. */
14
- function dotEscapeHtml(text) {
15
- return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
16
- }
17
- /** Returns the Graphviz node shape name for a given binding kind. */
18
- function nodeShapeForKind(kind) {
19
- switch (kind) {
20
- case "constant": return "ellipse";
21
- case "class": return "box";
22
- case "dynamic":
23
- case "async-dynamic":
24
- case "resolved": return "diamond";
25
- case "alias": return "octagon";
26
- default: return kind;
27
- }
28
- }
29
- /** Returns DOT fill-color and style attributes that visually distinguish binding scopes. */
30
- function scopeVisualAttributes(scope) {
31
- switch (scope) {
32
- case "singleton": return "style=\"filled\", fillcolor=\"#FFD700\", penwidth=2";
33
- case "scoped": return "style=\"filled\", fillcolor=\"#ADD8E6\"";
34
- case "transient": return "style=\"dashed\"";
35
- default: return scope;
36
- }
37
- }
38
- /** Strips non-alphanumeric characters from a module name to produce a valid DOT subgraph identifier. */
39
- function sanitizeClusterId(moduleName) {
40
- return moduleName.replace(/[^0-9a-zA-Z_]/g, "_");
41
- }
42
- /** Returns `true` when the label string belongs to a framework-internal registry key. */
11
+ /**
12
+ * Returns `true` when the label string belongs to a framework-internal registry key.
13
+ */
43
14
  function registryKeyLabelIsInternal(label) {
44
15
  return label.startsWith("CODEFAST_DI_");
45
16
  }
46
- /** Returns `true` when the registry key resolves to an internal framework label. */
17
+ /**
18
+ * Returns `true` when the registry key resolves to an internal framework label.
19
+ */
47
20
  function isInternalRegistryKey(key) {
48
21
  return registryKeyLabelIsInternal(registryKeyLabel(key));
49
22
  }
50
23
  /**
51
- * Read-only introspection and Graphviz DOT export for a container graph.
52
- */
53
- /**
54
- * Reads the registry and scope-cache state to produce debug snapshots and dependency graphs.
55
- * Instantiated internally by the container; advanced consumers can construct it directly via
56
- * the `@codefast/di/inspector` subpath export.
24
+ * Reads the container's registry and scope-cache state to produce debug snapshots
25
+ * and canonical dependency-graph output (`nodes` + `edges`).
26
+ *
27
+ * Constructed internally by the container; advanced consumers can also construct it
28
+ * directly via the `@codefast/di/inspector` subpath export.
57
29
  */
58
30
  var ContainerInspector = class {
59
31
  constructor(ctx) {
60
32
  this.ctx = ctx;
61
33
  }
62
- /** Collects all registered bindings into a flat, serialisable snapshot. */
34
+ /**
35
+ * Collects all registered bindings into a flat, serialisable snapshot.
36
+ */
63
37
  getSnapshot() {
64
38
  const bindings = [];
65
39
  const seen = /* @__PURE__ */ new Set();
@@ -87,102 +61,9 @@ var ContainerInspector = class {
87
61
  return { bindings };
88
62
  }
89
63
  /**
90
- * Produces a Graphviz `digraph` string with HTML-label nodes and styled edges.
91
- * Cycles are represented as ordinary edges (Graphviz renders them correctly).
92
- * Pass `hideInternals: true` to suppress `CODEFAST_DI_`-prefixed tokens.
64
+ * Builds the canonical JSON graph (`nodes` + `edges`).
93
65
  */
94
- generateDotGraph(options) {
95
- const hideInternals = options?.hideInternals === true;
96
- const fullSnapshot = this.getSnapshot();
97
- const visibleRows = hideInternals ? fullSnapshot.bindings.filter((row) => !registryKeyLabelIsInternal(row.registryKeyLabel)) : fullSnapshot.bindings;
98
- const allowedBindingIds = new Set(visibleRows.map((row) => row.bindingId));
99
- const lines = [
100
- "digraph codefast_di {",
101
- " rankdir=LR;",
102
- " graph [fontname=\"Arial\", fontsize=12, nodesep=0.8, ranksep=1.2];",
103
- " node [fontname=\"Arial\", fontsize=12, shape=box, style=\"filled,rounded\", fillcolor=\"#F5F5F5\"];",
104
- " edge [fontname=\"Arial\", fontsize=10];"
105
- ];
106
- const byModule = /* @__PURE__ */ new Map();
107
- for (const row of visibleRows) {
108
- const key = row.moduleId;
109
- const moduleGroup = byModule.get(key);
110
- if (moduleGroup === void 0) byModule.set(key, [row]);
111
- else moduleGroup.push(row);
112
- }
113
- const clusteredEntries = [...byModule.entries()].filter((entry) => entry[0] !== void 0);
114
- const unclustered = byModule.get(void 0) ?? [];
115
- const nodeAttributeLine = (row, indent) => {
116
- const shape = nodeShapeForKind(row.kind);
117
- const scopeAttrs = scopeVisualAttributes(row.scope);
118
- const kindText = dotEscapeHtml(row.kind);
119
- const nameText = dotEscapeHtml(row.registryKeyLabel);
120
- const scopeText = dotEscapeHtml(`scope=${row.scope}`);
121
- const whenRow = row.hasConditionalConstraint ? ` <TR><TD ALIGN="LEFT"><FONT POINT-SIZE="9" COLOR="#666666">when(...)</FONT></TD></TR>\n` : "";
122
- const htmlLabel = [
123
- "<",
124
- " <TABLE BORDER=\"0\" CELLPADDING=\"4\" CELLSPACING=\"0\">",
125
- ` <TR><TD ALIGN="LEFT"><FONT POINT-SIZE="9" COLOR="#666666">${kindText}</FONT></TD></TR>`,
126
- ` <TR><TD ALIGN="LEFT"><B>${nameText}</B></TD></TR>`,
127
- ` <TR><TD ALIGN="LEFT"><FONT POINT-SIZE="9">${scopeText}</FONT></TD></TR>`,
128
- whenRow.trimEnd(),
129
- " </TABLE>",
130
- " >"
131
- ].filter((line) => line.length > 0).join("\n");
132
- return `${indent}"${row.bindingId}" [shape=${shape}, ${scopeAttrs}, label=${htmlLabel}];`;
133
- };
134
- for (const [moduleName, rows] of clusteredEntries) {
135
- const clusterId = sanitizeClusterId(moduleName);
136
- lines.push(` subgraph cluster_${clusterId} {`);
137
- lines.push(` label="${dotEscapeLabel(moduleName)}";`);
138
- lines.push(` style=filled;`);
139
- lines.push(` fillcolor=lightgray;`);
140
- for (const row of rows) lines.push(nodeAttributeLine(row, " "));
141
- lines.push(` }`);
142
- }
143
- for (const row of unclustered) lines.push(nodeAttributeLine(row, " "));
144
- const emittedNodeIds = new Set(visibleRows.map((row) => row.bindingId));
145
- const edgeSeen = /* @__PURE__ */ new Set();
146
- for (const registryKey of this.ctx.collectAllRegistryKeys()) {
147
- if (hideInternals && isInternalRegistryKey(registryKey)) continue;
148
- const list = this.ctx.lookupBindings(registryKey);
149
- if (list === void 0) continue;
150
- const pathStart = [registryKeyLabel(registryKey)];
151
- for (const consumerBinding of list) {
152
- if (hideInternals && !allowedBindingIds.has(consumerBinding.id)) continue;
153
- const edges = collectStaticDependencyEdges(consumerBinding, (dependencyKey) => this.ctx.lookupBindings(dependencyKey), this.ctx.metadataReader, pathStart);
154
- for (const edge of edges) {
155
- if (hideInternals && (!allowedBindingIds.has(edge.fromBindingId) || !allowedBindingIds.has(edge.toBindingId))) continue;
156
- const edgeKey = `${edge.fromBindingId}->${edge.toBindingId}:${edge.edgeKind}:${edge.injectHintLabel ?? ""}`;
157
- if (edgeSeen.has(edgeKey)) continue;
158
- edgeSeen.add(edgeKey);
159
- if (!emittedNodeIds.has(edge.fromBindingId)) {
160
- emittedNodeIds.add(edge.fromBindingId);
161
- lines.push(` "${edge.fromBindingId}" [shape=box, style=dashed, label="(unlisted ${edge.fromBindingId})"];`);
162
- }
163
- if (!emittedNodeIds.has(edge.toBindingId)) {
164
- emittedNodeIds.add(edge.toBindingId);
165
- lines.push(` "${edge.toBindingId}" [shape=box, style=dashed, label="(unlisted ${edge.toBindingId})"];`);
166
- }
167
- const labelParts = [];
168
- if (edge.injectHintLabel !== void 0) labelParts.push(edge.injectHintLabel);
169
- labelParts.push(edge.edgeKind);
170
- if (edge.toBindingConditional) labelParts.push("conditional");
171
- const edgeLabel = dotEscapeLabel(labelParts.join(" | "));
172
- const pathLabel = dotEscapeLabel(edge.resolutionPath.join(" -> "));
173
- const edgeStyle = edge.isAliasEdge ? ", style=dashed" : "";
174
- lines.push(` "${edge.fromBindingId}" -> "${edge.toBindingId}" [label="${edgeLabel}", xlabel="${pathLabel}"${edgeStyle}];`);
175
- }
176
- }
177
- }
178
- lines.push("}");
179
- return lines.join("\n");
180
- }
181
66
  generateDependencyGraph(options) {
182
- if (options?.format === "json") return this.generateDependencyGraphJsonTyped(options);
183
- return this.generateDotGraph(options);
184
- }
185
- generateDependencyGraphJsonTyped(options) {
186
67
  const hideInternals = options?.hideInternals === true;
187
68
  const snapshot = this.getSnapshot();
188
69
  const visibleNodes = hideInternals ? snapshot.bindings.filter((row) => !registryKeyLabelIsInternal(row.registryKeyLabel)) : [...snapshot.bindings];
@@ -210,38 +91,6 @@ var ContainerInspector = class {
210
91
  edges
211
92
  };
212
93
  }
213
- /**
214
- * Produces a JSON graph representation with `nodes` and `edges`.
215
- * @internal Use `generateDependencyGraph({ format: "json" })` for typed output.
216
- */
217
- generateDependencyGraphJson(options) {
218
- const hideInternals = options?.hideInternals === true;
219
- const snapshot = this.getSnapshot();
220
- const visibleNodes = hideInternals ? snapshot.bindings.filter((row) => !registryKeyLabelIsInternal(row.registryKeyLabel)) : snapshot.bindings;
221
- const allowedBindingIds = new Set(visibleNodes.map((row) => row.bindingId));
222
- const edges = [];
223
- const edgeSeen = /* @__PURE__ */ new Set();
224
- for (const registryKey of this.ctx.collectAllRegistryKeys()) {
225
- if (hideInternals && isInternalRegistryKey(registryKey)) continue;
226
- const list = this.ctx.lookupBindings(registryKey);
227
- if (list === void 0) continue;
228
- const pathStart = [registryKeyLabel(registryKey)];
229
- for (const consumerBinding of list) {
230
- if (hideInternals && !allowedBindingIds.has(consumerBinding.id)) continue;
231
- for (const edge of collectStaticDependencyEdges(consumerBinding, (dependencyKey) => this.ctx.lookupBindings(dependencyKey), this.ctx.metadataReader, pathStart)) {
232
- if (hideInternals && (!allowedBindingIds.has(edge.fromBindingId) || !allowedBindingIds.has(edge.toBindingId))) continue;
233
- const edgeKey = `${edge.fromBindingId}->${edge.toBindingId}:${edge.edgeKind}:${edge.injectHintLabel ?? ""}`;
234
- if (edgeSeen.has(edgeKey)) continue;
235
- edgeSeen.add(edgeKey);
236
- edges.push(edge);
237
- }
238
- }
239
- }
240
- return JSON.stringify({
241
- nodes: visibleNodes,
242
- edges
243
- });
244
- }
245
94
  };
246
95
  //#endregion
247
96
  export { ContainerInspector };
@@ -3,15 +3,30 @@ import { LifecycleMetadata } from "./metadata/metadata-types.mjs";
3
3
 
4
4
  //#region src/lifecycle.d.ts
5
5
  /**
6
- * Runs `onActivation` synchronously; rejects async activations on sync resolution paths.
6
+ * Duck-typed Promise check: returns `true` when `value` has a `then` method.
7
+ * Used throughout the lifecycle layer to guard against async return values on sync resolution paths.
8
+ */
9
+ declare function isPromiseLike(value: unknown): value is Promise<unknown>;
10
+ /**
11
+ * Runs `onActivation` synchronously on a newly constructed instance.
12
+ * If the handler returns a Promise during synchronous resolution, throws
13
+ * {@link AsyncResolutionError} — callers must use `resolveAsync` for async activation handlers.
14
+ *
15
+ * Lifecycle ordering: `construct` → `@postConstruct` → **`onActivation`** → cache.
7
16
  */
8
17
  declare function runActivation(binding: Binding<unknown>, instance: unknown, ctx: ResolutionContext, pathLabels: readonly string[]): unknown;
9
18
  /**
10
- * Reads lifecycle metadata directly from a constructor's Symbol.metadata.
19
+ * Reads lifecycle metadata ({@link LifecycleMetadata}) directly from a constructor's
20
+ * `Symbol.metadata` object. Bypasses the {@link MetadataReader} abstraction —
21
+ * used by the scope manager during deactivation when the reader is not available.
11
22
  */
12
23
  declare function readLifecycleMetadataFromCtor(implementationClass: Constructor<unknown>): LifecycleMetadata | undefined;
13
24
  /**
14
- * Runs the `@postConstruct()` method synchronously if present. Throws if it returns a Promise.
25
+ * Runs the `@postConstruct()` method synchronously if present.
26
+ * Throws {@link AsyncResolutionError} if the method returns a Promise — async lifecycle
27
+ * methods require `resolveAsync`.
28
+ *
29
+ * Lifecycle ordering: `construct` → **`@postConstruct`** → `onActivation` → cache.
15
30
  */
16
31
  declare function runPostConstruct(implementationClass: Constructor<unknown>, instance: unknown, pathLabels?: string[]): void;
17
32
  /**
@@ -19,16 +34,23 @@ declare function runPostConstruct(implementationClass: Constructor<unknown>, ins
19
34
  */
20
35
  declare function runPostConstructAsync(implementationClass: Constructor<unknown>, instance: unknown): Promise<void>;
21
36
  /**
22
- * Runs the `@preDestroy()` method synchronously if present. Throws if it returns a Promise.
37
+ * Runs the `@preDestroy()` method synchronously if present.
38
+ * Throws if the method returns a Promise — use `disposeAsync()` / `unloadAsync()` for async teardown.
39
+ *
40
+ * Lifecycle ordering: `onDeactivation` → **`@preDestroy`**.
23
41
  */
24
42
  declare function runPreDestroy(implementationClass: Constructor<unknown>, instance: unknown): void;
25
43
  /**
26
44
  * Runs the `@preDestroy()` method, awaiting if it returns a Promise.
45
+ *
46
+ * Lifecycle ordering: `onDeactivation` → **`@preDestroy`** (async variant).
27
47
  */
28
48
  declare function runPreDestroyAsync(implementationClass: Constructor<unknown>, instance: unknown): Promise<void>;
29
49
  /**
30
- * Runs `onActivation`, awaiting promises returned by the handler.
50
+ * Runs `onActivation`, awaiting if the handler returns a Promise.
51
+ *
52
+ * Lifecycle ordering: `construct` → `@postConstruct` → **`onActivation`** (async variant) → cache.
31
53
  */
32
54
  declare function runActivationAsync(binding: Binding<unknown>, instance: unknown, ctx: ResolutionContext, _pathLabels: readonly string[]): Promise<unknown>;
33
55
  //#endregion
34
- export { readLifecycleMetadataFromCtor, runActivation, runActivationAsync, runPostConstruct, runPostConstructAsync, runPreDestroy, runPreDestroyAsync };
56
+ export { isPromiseLike, readLifecycleMetadataFromCtor, runActivation, runActivationAsync, runPostConstruct, runPostConstructAsync, runPreDestroy, runPreDestroyAsync };
@@ -1,11 +1,19 @@
1
1
  import { AsyncResolutionError } from "./errors.mjs";
2
2
  import { CODEFAST_DI_LIFECYCLE_METADATA, decoratorMetadataObjectSymbol } from "./metadata/metadata-keys.mjs";
3
3
  //#region src/lifecycle.ts
4
+ /**
5
+ * Duck-typed Promise check: returns `true` when `value` has a `then` method.
6
+ * Used throughout the lifecycle layer to guard against async return values on sync resolution paths.
7
+ */
4
8
  function isPromiseLike(value) {
5
9
  return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
6
10
  }
7
11
  /**
8
- * Runs `onActivation` synchronously; rejects async activations on sync resolution paths.
12
+ * Runs `onActivation` synchronously on a newly constructed instance.
13
+ * If the handler returns a Promise during synchronous resolution, throws
14
+ * {@link AsyncResolutionError} — callers must use `resolveAsync` for async activation handlers.
15
+ *
16
+ * Lifecycle ordering: `construct` → `@postConstruct` → **`onActivation`** → cache.
9
17
  */
10
18
  function runActivation(binding, instance, ctx, pathLabels) {
11
19
  const handler = binding.onActivation;
@@ -16,7 +24,9 @@ function runActivation(binding, instance, ctx, pathLabels) {
16
24
  return activationResult;
17
25
  }
18
26
  /**
19
- * Reads lifecycle metadata directly from a constructor's Symbol.metadata.
27
+ * Reads lifecycle metadata ({@link LifecycleMetadata}) directly from a constructor's
28
+ * `Symbol.metadata` object. Bypasses the {@link MetadataReader} abstraction —
29
+ * used by the scope manager during deactivation when the reader is not available.
20
30
  */
21
31
  function readLifecycleMetadataFromCtor(implementationClass) {
22
32
  const metadataObject = implementationClass[decoratorMetadataObjectSymbol()];
@@ -25,7 +35,11 @@ function readLifecycleMetadataFromCtor(implementationClass) {
25
35
  return typeof raw === "object" && raw !== null ? raw : void 0;
26
36
  }
27
37
  /**
28
- * Runs the `@postConstruct()` method synchronously if present. Throws if it returns a Promise.
38
+ * Runs the `@postConstruct()` method synchronously if present.
39
+ * Throws {@link AsyncResolutionError} if the method returns a Promise — async lifecycle
40
+ * methods require `resolveAsync`.
41
+ *
42
+ * Lifecycle ordering: `construct` → **`@postConstruct`** → `onActivation` → cache.
29
43
  */
30
44
  function runPostConstruct(implementationClass, instance, pathLabels) {
31
45
  const meta = readLifecycleMetadataFromCtor(implementationClass);
@@ -33,8 +47,7 @@ function runPostConstruct(implementationClass, instance, pathLabels) {
33
47
  const methodName = meta.postConstruct;
34
48
  const lifecycleMethod = instance[methodName];
35
49
  if (typeof lifecycleMethod !== "function") return;
36
- const postConstructResult = lifecycleMethod.call(instance);
37
- if (typeof postConstructResult === "object" && postConstructResult !== null && "then" in postConstructResult && typeof postConstructResult.then === "function") {
50
+ if (isPromiseLike(lifecycleMethod.call(instance))) {
38
51
  const labels = pathLabels ?? [];
39
52
  throw new AsyncResolutionError(labels[labels.length - 1] ?? "(unknown)", labels, `@postConstruct() "${methodName}" returned a Promise during synchronous resolution`);
40
53
  }
@@ -50,7 +63,10 @@ async function runPostConstructAsync(implementationClass, instance) {
50
63
  await lifecycleMethod.call(instance);
51
64
  }
52
65
  /**
53
- * Runs the `@preDestroy()` method synchronously if present. Throws if it returns a Promise.
66
+ * Runs the `@preDestroy()` method synchronously if present.
67
+ * Throws if the method returns a Promise — use `disposeAsync()` / `unloadAsync()` for async teardown.
68
+ *
69
+ * Lifecycle ordering: `onDeactivation` → **`@preDestroy`**.
54
70
  */
55
71
  function runPreDestroy(implementationClass, instance) {
56
72
  const meta = readLifecycleMetadataFromCtor(implementationClass);
@@ -58,11 +74,12 @@ function runPreDestroy(implementationClass, instance) {
58
74
  const methodName = meta.preDestroy;
59
75
  const lifecycleMethod = instance[methodName];
60
76
  if (typeof lifecycleMethod !== "function") return;
61
- const preDestroyResult = lifecycleMethod.call(instance);
62
- if (typeof preDestroyResult === "object" && preDestroyResult !== null && "then" in preDestroyResult && typeof preDestroyResult.then === "function") throw new Error(`@preDestroy() "${methodName}" returned a Promise during synchronous disposal; use disposeAsync() / unloadAsync().`);
77
+ if (isPromiseLike(lifecycleMethod.call(instance))) throw new Error(`@preDestroy() "${methodName}" returned a Promise during synchronous disposal; use disposeAsync() / unloadAsync().`);
63
78
  }
64
79
  /**
65
80
  * Runs the `@preDestroy()` method, awaiting if it returns a Promise.
81
+ *
82
+ * Lifecycle ordering: `onDeactivation` → **`@preDestroy`** (async variant).
66
83
  */
67
84
  async function runPreDestroyAsync(implementationClass, instance) {
68
85
  const meta = readLifecycleMetadataFromCtor(implementationClass);
@@ -72,7 +89,9 @@ async function runPreDestroyAsync(implementationClass, instance) {
72
89
  await lifecycleMethod.call(instance);
73
90
  }
74
91
  /**
75
- * Runs `onActivation`, awaiting promises returned by the handler.
92
+ * Runs `onActivation`, awaiting if the handler returns a Promise.
93
+ *
94
+ * Lifecycle ordering: `construct` → `@postConstruct` → **`onActivation`** (async variant) → cache.
76
95
  */
77
96
  async function runActivationAsync(binding, instance, ctx, _pathLabels) {
78
97
  const handler = binding.onActivation;
@@ -80,4 +99,4 @@ async function runActivationAsync(binding, instance, ctx, _pathLabels) {
80
99
  return await handler(ctx, instance);
81
100
  }
82
101
  //#endregion
83
- export { readLifecycleMetadataFromCtor, runActivation, runActivationAsync, runPostConstruct, runPostConstructAsync, runPreDestroy, runPreDestroyAsync };
102
+ export { isPromiseLike, readLifecycleMetadataFromCtor, runActivation, runActivationAsync, runPostConstruct, runPostConstructAsync, runPreDestroy, runPreDestroyAsync };
@@ -1,16 +1,27 @@
1
1
  //#region src/metadata/metadata-keys.d.ts
2
2
  /**
3
- * Well-known key for Codefast DI constructor metadata on `Symbol.metadata`.
3
+ * Well-known property key for constructor injection metadata on `Symbol.metadata`.
4
+ * Written by `@injectable()`, read by `SymbolMetadataReader.getConstructorMetadata()`.
5
+ * The `:v1` suffix is a schema version — bump only when the `ConstructorMetadata` shape changes.
4
6
  */
5
7
  declare const CODEFAST_DI_CONSTRUCTOR_METADATA = "codefast/di:constructor-metadata:v1";
6
- /** Well-known key for accessor field injection metadata written by `@inject` on `accessor` fields. */
8
+ /**
9
+ * Well-known property key for accessor-field injection metadata on `Symbol.metadata`.
10
+ * Written by `@inject()` when used as an accessor decorator; read during post-construction
11
+ * property injection by the resolver.
12
+ */
7
13
  declare const CODEFAST_DI_ACCESSOR_INJECTIONS = "codefast/di:accessor-injections:v1";
8
- /** Well-known key for lifecycle method names written by `@postConstruct()` / `@preDestroy()`. */
14
+ /**
15
+ * Well-known key for lifecycle method names written by `@postConstruct()` / `@preDestroy()`.
16
+ */
9
17
  declare const CODEFAST_DI_LIFECYCLE_METADATA = "codefast/di:lifecycle-metadata:v1";
10
18
  /**
11
- * Runtime symbol for the decorator metadata object (TC39 `Symbol.metadata`).
12
- * Node may expose this only via `Symbol.for("Symbol.metadata")` until the global
13
- * `Symbol.metadata` property is available.
19
+ * Returns the runtime symbol used to access TC39 decorator metadata on a class.
20
+ *
21
+ * Prefers the native `Symbol.metadata` when available (Stage 3 decorators); falls back to
22
+ * `Symbol.for("Symbol.metadata")` for Node/runtime versions that polyfill or partially
23
+ * implement the proposal. The fallback key is the de facto convention used by TypeScript
24
+ * and polyfill libraries.
14
25
  */
15
26
  declare function decoratorMetadataObjectSymbol(): symbol;
16
27
  //#endregion
@@ -1,16 +1,27 @@
1
1
  //#region src/metadata/metadata-keys.ts
2
2
  /**
3
- * Well-known key for Codefast DI constructor metadata on `Symbol.metadata`.
3
+ * Well-known property key for constructor injection metadata on `Symbol.metadata`.
4
+ * Written by `@injectable()`, read by `SymbolMetadataReader.getConstructorMetadata()`.
5
+ * The `:v1` suffix is a schema version — bump only when the `ConstructorMetadata` shape changes.
4
6
  */
5
7
  const CODEFAST_DI_CONSTRUCTOR_METADATA = "codefast/di:constructor-metadata:v1";
6
- /** Well-known key for accessor field injection metadata written by `@inject` on `accessor` fields. */
8
+ /**
9
+ * Well-known property key for accessor-field injection metadata on `Symbol.metadata`.
10
+ * Written by `@inject()` when used as an accessor decorator; read during post-construction
11
+ * property injection by the resolver.
12
+ */
7
13
  const CODEFAST_DI_ACCESSOR_INJECTIONS = "codefast/di:accessor-injections:v1";
8
- /** Well-known key for lifecycle method names written by `@postConstruct()` / `@preDestroy()`. */
14
+ /**
15
+ * Well-known key for lifecycle method names written by `@postConstruct()` / `@preDestroy()`.
16
+ */
9
17
  const CODEFAST_DI_LIFECYCLE_METADATA = "codefast/di:lifecycle-metadata:v1";
10
18
  /**
11
- * Runtime symbol for the decorator metadata object (TC39 `Symbol.metadata`).
12
- * Node may expose this only via `Symbol.for("Symbol.metadata")` until the global
13
- * `Symbol.metadata` property is available.
19
+ * Returns the runtime symbol used to access TC39 decorator metadata on a class.
20
+ *
21
+ * Prefers the native `Symbol.metadata` when available (Stage 3 decorators); falls back to
22
+ * `Symbol.for("Symbol.metadata")` for Node/runtime versions that polyfill or partially
23
+ * implement the proposal. The fallback key is the de facto convention used by TypeScript
24
+ * and polyfill libraries.
14
25
  */
15
26
  function decoratorMetadataObjectSymbol() {
16
27
  return typeof Symbol.metadata === "symbol" ? Symbol.metadata : Symbol.for("Symbol.metadata");