@nexus-ioc/core 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +731 -1
  2. package/dist/cjs/browser.js +2 -2
  3. package/dist/cjs/browser.js.map +1 -1
  4. package/dist/cjs/core/child-module.js +32 -0
  5. package/dist/cjs/core/child-module.js.map +1 -0
  6. package/dist/cjs/core/graph/analyze-lazy-module.js +54 -0
  7. package/dist/cjs/core/graph/analyze-lazy-module.js.map +1 -0
  8. package/dist/cjs/core/graph/analyze-module.js +11 -0
  9. package/dist/cjs/core/graph/analyze-module.js.map +1 -1
  10. package/dist/cjs/core/graph/module-graph.js +685 -70
  11. package/dist/cjs/core/graph/module-graph.js.map +1 -1
  12. package/dist/cjs/core/injection/injection-context.js +29 -0
  13. package/dist/cjs/core/injection/injection-context.js.map +1 -0
  14. package/dist/cjs/core/lazy-module-loader.js +41 -0
  15. package/dist/cjs/core/lazy-module-loader.js.map +1 -0
  16. package/dist/cjs/core/module-ref.js +84 -0
  17. package/dist/cjs/core/module-ref.js.map +1 -0
  18. package/dist/cjs/core/modules/container.js +305 -17
  19. package/dist/cjs/core/modules/container.js.map +1 -1
  20. package/dist/cjs/core/modules/module-container-factory.js +2 -2
  21. package/dist/cjs/core/modules/module-container-factory.js.map +1 -1
  22. package/dist/cjs/core/modules/module-container.js +54 -16
  23. package/dist/cjs/core/modules/module-container.js.map +1 -1
  24. package/dist/cjs/core/modules/module-token-factory.js.map +1 -1
  25. package/dist/cjs/core/modules/modules-container.js +43 -3
  26. package/dist/cjs/core/modules/modules-container.js.map +1 -1
  27. package/dist/cjs/core/nexus-applications.browser.js +57 -15
  28. package/dist/cjs/core/nexus-applications.browser.js.map +1 -1
  29. package/dist/cjs/core/nexus-applications.js +440 -82
  30. package/dist/cjs/core/nexus-applications.js.map +1 -1
  31. package/dist/cjs/core/nexus-applications.server.js +57 -15
  32. package/dist/cjs/core/nexus-applications.server.js.map +1 -1
  33. package/dist/cjs/core/resolver/providers-container.js.map +1 -1
  34. package/dist/cjs/core/resolver/resolution-context.js +62 -0
  35. package/dist/cjs/core/resolver/resolution-context.js.map +1 -0
  36. package/dist/cjs/core/resolver/resolver.js +523 -99
  37. package/dist/cjs/core/resolver/resolver.js.map +1 -1
  38. package/dist/cjs/decorators/inject.js +6 -3
  39. package/dist/cjs/decorators/inject.js.map +1 -1
  40. package/dist/cjs/decorators/injectable.js +0 -7
  41. package/dist/cjs/decorators/injectable.js.map +1 -1
  42. package/dist/cjs/decorators/{nsModule.js → module.js} +9 -7
  43. package/dist/cjs/decorators/module.js.map +1 -0
  44. package/dist/cjs/errors/bootstrap-error.js +47 -0
  45. package/dist/cjs/errors/bootstrap-error.js.map +1 -0
  46. package/dist/cjs/errors/child-errors.js +21 -0
  47. package/dist/cjs/errors/child-errors.js.map +1 -0
  48. package/dist/cjs/errors/index.js +24 -0
  49. package/dist/cjs/errors/index.js.map +1 -0
  50. package/dist/cjs/errors/injection-errors.js +25 -0
  51. package/dist/cjs/errors/injection-errors.js.map +1 -0
  52. package/dist/cjs/errors/lazy-module-errors.js +26 -0
  53. package/dist/cjs/errors/lazy-module-errors.js.map +1 -0
  54. package/dist/cjs/errors/provider-errors.js +95 -0
  55. package/dist/cjs/errors/provider-errors.js.map +1 -0
  56. package/dist/cjs/index.js +30 -6
  57. package/dist/cjs/index.js.map +1 -1
  58. package/dist/cjs/interfaces/index.js +1 -0
  59. package/dist/cjs/interfaces/index.js.map +1 -1
  60. package/dist/cjs/interfaces/modules/index.js +1 -0
  61. package/dist/cjs/interfaces/modules/index.js.map +1 -1
  62. package/dist/cjs/interfaces/modules/module-graph.interface.js +2 -0
  63. package/dist/cjs/interfaces/modules/module-graph.interface.js.map +1 -1
  64. package/dist/cjs/interfaces/modules/parent-link.interface.js +3 -0
  65. package/dist/cjs/interfaces/modules/parent-link.interface.js.map +1 -0
  66. package/dist/cjs/interfaces/on-module-destroy.interface.js +3 -0
  67. package/dist/cjs/interfaces/on-module-destroy.interface.js.map +1 -0
  68. package/dist/cjs/interfaces/scope.interface.js +2 -55
  69. package/dist/cjs/interfaces/scope.interface.js.map +1 -1
  70. package/dist/cjs/interfaces/validate-module-keys.js +2 -0
  71. package/dist/cjs/interfaces/validate-module-keys.js.map +1 -1
  72. package/dist/cjs/internal.js +9 -0
  73. package/dist/cjs/internal.js.map +1 -0
  74. package/dist/cjs/server.js +2 -2
  75. package/dist/cjs/server.js.map +1 -1
  76. package/dist/cjs/utils/forward-ref.js +14 -0
  77. package/dist/cjs/utils/forward-ref.js.map +1 -0
  78. package/dist/cjs/utils/helpers.js +14 -4
  79. package/dist/cjs/utils/helpers.js.map +1 -1
  80. package/dist/cjs/utils/lazy-module.js +33 -0
  81. package/dist/cjs/utils/lazy-module.js.map +1 -0
  82. package/dist/esm/browser.js +1 -1
  83. package/dist/esm/browser.js.map +1 -1
  84. package/dist/esm/core/child-module.js +29 -0
  85. package/dist/esm/core/child-module.js.map +1 -0
  86. package/dist/esm/core/graph/analyze-lazy-module.js +50 -0
  87. package/dist/esm/core/graph/analyze-lazy-module.js.map +1 -0
  88. package/dist/esm/core/graph/analyze-module.js +11 -0
  89. package/dist/esm/core/graph/analyze-module.js.map +1 -1
  90. package/dist/esm/core/graph/module-graph.js +686 -71
  91. package/dist/esm/core/graph/module-graph.js.map +1 -1
  92. package/dist/esm/core/injection/injection-context.js +25 -0
  93. package/dist/esm/core/injection/injection-context.js.map +1 -0
  94. package/dist/esm/core/lazy-module-loader.js +36 -0
  95. package/dist/esm/core/lazy-module-loader.js.map +1 -0
  96. package/dist/esm/core/module-ref.js +80 -0
  97. package/dist/esm/core/module-ref.js.map +1 -0
  98. package/dist/esm/core/modules/container.js +305 -17
  99. package/dist/esm/core/modules/container.js.map +1 -1
  100. package/dist/esm/core/modules/module-container-factory.js +2 -2
  101. package/dist/esm/core/modules/module-container-factory.js.map +1 -1
  102. package/dist/esm/core/modules/module-container.js +55 -17
  103. package/dist/esm/core/modules/module-container.js.map +1 -1
  104. package/dist/esm/core/modules/module-token-factory.js.map +1 -1
  105. package/dist/esm/core/modules/modules-container.js +43 -3
  106. package/dist/esm/core/modules/modules-container.js.map +1 -1
  107. package/dist/esm/core/nexus-applications.browser.js +55 -13
  108. package/dist/esm/core/nexus-applications.browser.js.map +1 -1
  109. package/dist/esm/core/nexus-applications.js +439 -81
  110. package/dist/esm/core/nexus-applications.js.map +1 -1
  111. package/dist/esm/core/nexus-applications.server.js +55 -13
  112. package/dist/esm/core/nexus-applications.server.js.map +1 -1
  113. package/dist/esm/core/resolver/providers-container.js.map +1 -1
  114. package/dist/esm/core/resolver/resolution-context.js +58 -0
  115. package/dist/esm/core/resolver/resolution-context.js.map +1 -0
  116. package/dist/esm/core/resolver/resolver.js +524 -100
  117. package/dist/esm/core/resolver/resolver.js.map +1 -1
  118. package/dist/esm/decorators/inject.js +6 -3
  119. package/dist/esm/decorators/inject.js.map +1 -1
  120. package/dist/esm/decorators/injectable.js +0 -7
  121. package/dist/esm/decorators/injectable.js.map +1 -1
  122. package/dist/esm/decorators/{nsModule.js → module.js} +8 -6
  123. package/dist/esm/decorators/module.js.map +1 -0
  124. package/dist/esm/errors/bootstrap-error.js +42 -0
  125. package/dist/esm/errors/bootstrap-error.js.map +1 -0
  126. package/dist/esm/errors/child-errors.js +17 -0
  127. package/dist/esm/errors/child-errors.js.map +1 -0
  128. package/dist/esm/errors/index.js +6 -0
  129. package/dist/esm/errors/index.js.map +1 -0
  130. package/dist/esm/errors/injection-errors.js +20 -0
  131. package/dist/esm/errors/injection-errors.js.map +1 -0
  132. package/dist/esm/errors/lazy-module-errors.js +21 -0
  133. package/dist/esm/errors/lazy-module-errors.js.map +1 -0
  134. package/dist/esm/errors/provider-errors.js +84 -0
  135. package/dist/esm/errors/provider-errors.js.map +1 -0
  136. package/dist/esm/index.js +9 -4
  137. package/dist/esm/index.js.map +1 -1
  138. package/dist/esm/interfaces/index.js +1 -0
  139. package/dist/esm/interfaces/index.js.map +1 -1
  140. package/dist/esm/interfaces/modules/index.js +1 -0
  141. package/dist/esm/interfaces/modules/index.js.map +1 -1
  142. package/dist/esm/interfaces/modules/module-graph.interface.js +2 -0
  143. package/dist/esm/interfaces/modules/module-graph.interface.js.map +1 -1
  144. package/dist/esm/interfaces/modules/parent-link.interface.js +2 -0
  145. package/dist/esm/interfaces/modules/parent-link.interface.js.map +1 -0
  146. package/dist/esm/interfaces/on-module-destroy.interface.js +2 -0
  147. package/dist/esm/interfaces/on-module-destroy.interface.js.map +1 -0
  148. package/dist/esm/interfaces/scope.interface.js +1 -55
  149. package/dist/esm/interfaces/scope.interface.js.map +1 -1
  150. package/dist/esm/interfaces/validate-module-keys.js +2 -0
  151. package/dist/esm/interfaces/validate-module-keys.js.map +1 -1
  152. package/dist/esm/internal.js +3 -0
  153. package/dist/esm/internal.js.map +1 -0
  154. package/dist/esm/server.js +1 -1
  155. package/dist/esm/server.js.map +1 -1
  156. package/dist/esm/utils/forward-ref.js +10 -0
  157. package/dist/esm/utils/forward-ref.js.map +1 -0
  158. package/dist/esm/utils/helpers.js +14 -4
  159. package/dist/esm/utils/helpers.js.map +1 -1
  160. package/dist/esm/utils/lazy-module.js +29 -0
  161. package/dist/esm/utils/lazy-module.js.map +1 -0
  162. package/dist/types/browser.d.ts +1 -1
  163. package/dist/types/core/child-module.d.ts +8 -0
  164. package/dist/types/core/graph/analyze-lazy-module.d.ts +28 -0
  165. package/dist/types/core/graph/analyze-module.d.ts +4 -2
  166. package/dist/types/core/graph/analyze-provider.d.ts +4 -4
  167. package/dist/types/core/graph/module-graph.d.ts +200 -7
  168. package/dist/types/core/graph/providers/analyze-class-provider.d.ts +1 -1
  169. package/dist/types/core/graph/providers/analyze-factory-provider.d.ts +2 -2
  170. package/dist/types/core/graph/providers/analyze-value-provider.d.ts +1 -1
  171. package/dist/types/core/injection/injection-context.d.ts +29 -0
  172. package/dist/types/core/lazy-module-loader.d.ts +17 -0
  173. package/dist/types/core/module-ref.d.ts +34 -0
  174. package/dist/types/core/modules/container.d.ts +134 -18
  175. package/dist/types/core/modules/module-container-factory.d.ts +3 -3
  176. package/dist/types/core/modules/module-container.d.ts +44 -5
  177. package/dist/types/core/modules/module-token-factory.d.ts +2 -2
  178. package/dist/types/core/modules/modules-container.d.ts +23 -4
  179. package/dist/types/core/nexus-applications.browser.d.ts +30 -8
  180. package/dist/types/core/nexus-applications.d.ts +219 -55
  181. package/dist/types/core/nexus-applications.server.d.ts +30 -8
  182. package/dist/types/core/resolver/providers-container.d.ts +2 -2
  183. package/dist/types/core/resolver/resolution-context.d.ts +22 -0
  184. package/dist/types/core/resolver/resolver.d.ts +122 -4
  185. package/dist/types/decorators/inject.d.ts +9 -5
  186. package/dist/types/decorators/injectable.d.ts +0 -7
  187. package/dist/types/decorators/{nsModule.d.ts → module.d.ts} +6 -5
  188. package/dist/types/errors/bootstrap-error.d.ts +6 -0
  189. package/dist/types/errors/child-errors.d.ts +10 -0
  190. package/dist/types/errors/index.d.ts +5 -0
  191. package/dist/types/errors/injection-errors.d.ts +11 -0
  192. package/dist/types/errors/lazy-module-errors.d.ts +12 -0
  193. package/dist/types/errors/provider-errors.d.ts +47 -0
  194. package/dist/types/index.d.ts +13 -4
  195. package/dist/types/interfaces/abstract.interface.d.ts +1 -3
  196. package/dist/types/interfaces/dynamic-module.interface.d.ts +7 -5
  197. package/dist/types/interfaces/index.d.ts +1 -0
  198. package/dist/types/interfaces/injection-token.interface.d.ts +1 -3
  199. package/dist/types/interfaces/module-metadata.interface.d.ts +12 -4
  200. package/dist/types/interfaces/module-types.interface.d.ts +21 -24
  201. package/dist/types/interfaces/modules/container.interface.d.ts +10 -4
  202. package/dist/types/interfaces/modules/index.d.ts +1 -0
  203. package/dist/types/interfaces/modules/module-container-factory.interface.d.ts +2 -2
  204. package/dist/types/interfaces/modules/module-graph.interface.d.ts +85 -12
  205. package/dist/types/interfaces/modules/module-token-factory.interface.d.ts +2 -2
  206. package/dist/types/interfaces/modules/modules-container.interface.d.ts +5 -4
  207. package/dist/types/interfaces/modules/parent-link.interface.d.ts +12 -0
  208. package/dist/types/interfaces/nexus-application.interface.d.ts +49 -6
  209. package/dist/types/interfaces/on-module-destroy.interface.d.ts +3 -0
  210. package/dist/types/interfaces/plugins/scanner-plugin.interface.d.ts +1 -1
  211. package/dist/types/interfaces/scope.interface.d.ts +1 -54
  212. package/dist/types/interfaces/validate-module-keys.d.ts +1 -0
  213. package/dist/types/internal.d.ts +3 -0
  214. package/dist/types/server.d.ts +1 -1
  215. package/dist/types/utils/forward-ref.d.ts +5 -0
  216. package/dist/types/utils/helpers.d.ts +8 -8
  217. package/dist/types/utils/lazy-module.d.ts +20 -0
  218. package/package.json +21 -4
  219. package/dist/cjs/decorators/nsModule.js.map +0 -1
  220. package/dist/esm/decorators/nsModule.js.map +0 -1
@@ -1,160 +1,584 @@
1
- import { EdgeTypeEnum, Scope, } from "../../interfaces/index.js";
1
+ import { CircularInjectionError } from "../../errors/injection-errors.js";
2
+ import { AsyncFactoryError, ContainerClosedError, ProviderNotFoundError, ProviderNotInstantiatedError, ProviderUnloadedError, } from "../../errors/provider-errors.js";
3
+ import { EdgeTypeEnum, NodeTypeEnum, Scope, } from "../../interfaces/index.js";
2
4
  import { isClassProvider, isFactoryProvider, isValueProvider, } from "../../utils/helpers.js";
5
+ import { withInjectionContext, } from "../injection/injection-context.js";
3
6
  import { ProvidersContainer } from "./providers-container.js";
7
+ import { ResolutionContext } from "./resolution-context.js";
4
8
  function isCircularDependencyFn(fn) {
5
9
  // @ts-expect-error
6
10
  return !!(typeof fn === "function" && fn.dependencyName);
7
11
  }
12
+ function isThenable(value) {
13
+ return (value !== null &&
14
+ (typeof value === "object" || typeof value === "function") &&
15
+ typeof value.then === "function");
16
+ }
8
17
  export class Resolver {
9
- constructor(graph) {
18
+ /**
19
+ * @param link - The parent chain of a child container. Edges marked
20
+ * `external` resolve through it; their results are never cached here.
21
+ */
22
+ constructor(graph, link = null) {
10
23
  this.graph = graph;
24
+ this.link = link;
11
25
  this.providersContainer = new ProvidersContainer();
26
+ this.initializationOrder = [];
27
+ /** `lateBoundVisible` results, valid while the graph version matches. */
28
+ this.lateBoundMemo = new WeakMap();
29
+ /**
30
+ * Singletons created by inject() whose onModuleInit (or the wait on
31
+ * inits queued before them) has not settled yet. Other resolution trees
32
+ * that hit such a cached singleton wait on this promise, so they never
33
+ * see it half-initialised or keep it after a failed init evicted it.
34
+ */
35
+ this.inFlightInits = new Map();
36
+ /**
37
+ * Singletons that resolveProvider is creating (construction through
38
+ * onModuleInit), not yet in the cache. Concurrent callers await the same
39
+ * promise, so each singleton is created once. An entry is removed when
40
+ * its creation settles: after the commit on success, so a token is always
41
+ * in the cache or here; on failure, so the next resolve retries.
42
+ */
43
+ this.inFlightCreations = new Map();
44
+ /**
45
+ * Set once close() starts. From then on nothing new is created or
46
+ * cached: creations still in flight are destroyed when they finish
47
+ * (close() does not wait for them, see commitCreation), and inject()
48
+ * in a resolution tree that outlived close() throws instead of creating.
49
+ */
50
+ this.closed = false;
51
+ }
52
+ /**
53
+ * The link that satisfies an external edge. The graph marks an edge
54
+ * external only when a link can provide its target, so a missing link
55
+ * is an internal error, reported as not found rather than `undefined`.
56
+ */
57
+ linkFor(edge) {
58
+ if (!this.link) {
59
+ throw new ProviderNotFoundError(edge.target, edge.source);
60
+ }
61
+ return this.link;
62
+ }
63
+ /** Cached singleton lookup; never creates. Uses has() so falsy values count. */
64
+ getInstantiated(token) {
65
+ // A singleton inject() created is cached before its async init settles;
66
+ // like the async path, it counts as instantiated only after the init.
67
+ if (!this.providersContainer.has(token) || this.inFlightInits.has(token)) {
68
+ return { found: false };
69
+ }
70
+ return { found: true, instance: this.providersContainer.get(token) };
71
+ }
72
+ /**
73
+ * Marks the resolver closed without destroying anything; used when the
74
+ * container was closed before this resolver existed.
75
+ */
76
+ markClosed() {
77
+ this.closed = true;
78
+ // Every creation still in flight is now stale (see commitCreation).
79
+ this.inFlightCreations.clear();
80
+ }
81
+ async close() {
82
+ // Before the first await: a creation that finishes while the hooks
83
+ // below run must not be cached behind the destroy pass.
84
+ this.markClosed();
85
+ const errors = [];
86
+ for (const token of [...this.initializationOrder].reverse()) {
87
+ const instance = this.providersContainer.get(token);
88
+ if (instance?.onModuleDestroy) {
89
+ try {
90
+ await instance.onModuleDestroy();
91
+ }
92
+ catch (error) {
93
+ errors.push(error);
94
+ }
95
+ }
96
+ }
97
+ this.providersContainer.clear();
98
+ this.initializationOrder.length = 0;
99
+ this.inFlightInits.clear();
100
+ this.inFlightCreations.clear();
101
+ if (errors.length > 0) {
102
+ throw new AggregateError(errors, "One or more onModuleDestroy hooks failed");
103
+ }
104
+ }
105
+ /**
106
+ * Destroys exactly the given singletons: `onModuleDestroy` in reverse
107
+ * initialization order, then removed from the cache and from
108
+ * `initializationOrder`. A token that was never instantiated (never
109
+ * resolved, or `Transient`/non-singleton scope) is silently skipped.
110
+ * Like `close()`, every hook runs even if some throw, the cleanup always
111
+ * completes, and the failures are thrown afterwards as one
112
+ * `AggregateError`.
113
+ */
114
+ async destroy(tokens) {
115
+ const dead = new Set(tokens);
116
+ // Before the first await: a creation of a dead token still in flight
117
+ // is stale from now on and is destroyed, not cached, when it finishes.
118
+ for (const token of dead) {
119
+ this.inFlightInits.delete(token);
120
+ this.inFlightCreations.delete(token);
121
+ }
122
+ const orderedDead = this.initializationOrder.filter((token) => dead.has(token));
123
+ const errors = [];
124
+ for (const token of [...orderedDead].reverse()) {
125
+ const instance = this.providersContainer.get(token);
126
+ if (instance?.onModuleDestroy) {
127
+ try {
128
+ await instance.onModuleDestroy();
129
+ }
130
+ catch (error) {
131
+ errors.push(error);
132
+ }
133
+ }
134
+ this.providersContainer.delete(token);
135
+ }
136
+ const survivors = this.initializationOrder.filter((token) => !dead.has(token));
137
+ this.initializationOrder.length = 0;
138
+ this.initializationOrder.push(...survivors);
139
+ if (errors.length > 0) {
140
+ throw new AggregateError(errors, "One or more onModuleDestroy hooks failed");
141
+ }
12
142
  }
13
- async resolveProvider(token, resolveCache = new ProvidersContainer(), isCircularDependency = false) {
143
+ async resolveProvider(token, ctx = new ResolutionContext(), isCircularDependency = false) {
144
+ if (this.closed) {
145
+ throw new ContainerClosedError();
146
+ }
14
147
  const node = this.graph.getNode(token);
15
148
  if (!node) {
16
149
  return undefined;
17
150
  }
151
+ // Module and LAZY placeholder nodes share the token space with providers
152
+ // but carry no instance, so they resolve to undefined instead of failing.
153
+ if (node.type !== NodeTypeEnum.PROVIDER) {
154
+ return undefined;
155
+ }
18
156
  // Get the scope of the provider
19
157
  const scope = node.scope;
20
158
  // For Transient scope, always create a new instance (no caching at all)
21
159
  if (scope === Scope.Transient) {
22
- const [instance] = await this.createInstance(node, resolveCache, isCircularDependency);
23
- return instance;
160
+ return (await this.createInstance(node, ctx, isCircularDependency));
24
161
  }
25
162
  // For Singleton scope, check global cache first
26
163
  if (scope === Scope.Singleton && this.providersContainer.has(token)) {
27
- return this.providersContainer.get(token);
164
+ const cached = this.providersContainer.get(token);
165
+ const inFlight = this.inFlightInits.get(token);
166
+ if (inFlight) {
167
+ // Created by inject() in another tree; its init is still running.
168
+ // On failure it was evicted, so the error propagates.
169
+ await inFlight;
170
+ }
171
+ return cached;
172
+ }
173
+ const inFlight = this.inFlightCreations.get(token);
174
+ if (inFlight) {
175
+ // Defensive: this tree is creating the token itself, so awaiting its
176
+ // own creation would never settle. Static cycles are marked circular
177
+ // by the graph and never get here.
178
+ if (ctx.isConstructing(token)) {
179
+ throw new CircularInjectionError([...ctx.path, token]);
180
+ }
181
+ return (await inFlight);
28
182
  }
29
- // For Request and Singleton scopes, check resolve cache (within current resolution tree)
30
- if (resolveCache.has(token)) {
31
- return resolveCache.get(token);
183
+ const creation = this.createInstance(node, ctx, isCircularDependency).then((instance) => this.commitCreation(node, creation, instance));
184
+ this.inFlightCreations.set(token, creation);
185
+ const clear = () => {
186
+ // close()/destroy() may have dropped it, or a retry replaced it.
187
+ if (this.inFlightCreations.get(token) === creation) {
188
+ this.inFlightCreations.delete(token);
189
+ }
190
+ };
191
+ creation.then(clear, clear);
192
+ return (await creation);
193
+ }
194
+ /**
195
+ * Caches what `creation` produced, unless the creation went stale while
196
+ * it ran: close() or destroy() (an unload) dropped its in-flight entry.
197
+ * A stale instance is never cached. Its onModuleDestroy is called (best
198
+ * effort: a failure there is ignored, since the caller already gets the
199
+ * rejection below and nothing else holds the instance), and the caller
200
+ * gets ContainerClosedError after a close, ProviderUnloadedError after
201
+ * an unload.
202
+ */
203
+ async commitCreation(node, creation, instance) {
204
+ if (this.inFlightCreations.get(node.id) === creation) {
205
+ this.commitInstance(node, instance);
206
+ return instance;
32
207
  }
33
- const [instance, saveInCache] = await this.createInstance(node, resolveCache, isCircularDependency);
34
- // Add to resolve cache for current resolution context (for Request and Singleton)
35
- // This ensures same instance is used within a single dependency resolution tree
36
- if (scope === Scope.Singleton || scope === Scope.Request) {
37
- resolveCache.set(token, instance);
208
+ const stale = instance;
209
+ try {
210
+ await stale?.onModuleDestroy?.();
38
211
  }
39
- // Only save to global cache for Singleton scope
40
- if (saveInCache) {
41
- this.providersContainer.set(token, instance);
212
+ catch {
213
+ // Best effort; see above.
214
+ }
215
+ throw this.closed
216
+ ? new ContainerClosedError()
217
+ : new ProviderUnloadedError(node.id);
218
+ }
219
+ /**
220
+ * Caching rules shared by the async and sync paths: Singletons go into the
221
+ * global cache and the destroy order. Nothing else is cached.
222
+ */
223
+ commitInstance(node, instance) {
224
+ if (node.scope !== Scope.Singleton) {
225
+ return;
226
+ }
227
+ // First writer wins if concurrent creations race for the same token.
228
+ if (!this.providersContainer.has(node.id)) {
229
+ this.providersContainer.set(node.id, instance);
230
+ this.initializationOrder.push(node.id);
231
+ }
232
+ }
233
+ /** Undoes commitInstance for `instance`, if it is still the cached one. */
234
+ evictInstance(node, instance) {
235
+ const token = node.id;
236
+ if (this.providersContainer.has(token) &&
237
+ this.providersContainer.get(token) === instance) {
238
+ this.providersContainer.delete(token);
239
+ const index = this.initializationOrder.lastIndexOf(token);
240
+ if (index !== -1) {
241
+ this.initializationOrder.splice(index, 1);
242
+ }
243
+ }
244
+ }
245
+ async createInstance(node, ctx, isCircularDependency = false) {
246
+ ctx.enter(node.id);
247
+ try {
248
+ return await this.createInstanceInContext(node, ctx, isCircularDependency);
249
+ }
250
+ finally {
251
+ ctx.leave(node.id);
42
252
  }
43
- return instance;
44
253
  }
45
- async createInstance(node, resolveCache, isCircularDependency = false) {
254
+ async createInstanceInContext(node, ctx, isCircularDependency) {
46
255
  const provider = node.metatype;
47
- const dependencies = this.graph
48
- .getEdge(node.id)
49
- .filter((edge) => edge.type === EdgeTypeEnum.DEPENDENCY &&
50
- edge.metadata.inject === "constructor" &&
51
- edge.metadata.unreached === false)
52
- .map((edge) => {
256
+ const dependencyEdges = this.constructorEdges(node).filter((edge) => edge.metadata.unreached === false);
257
+ const resolvedDependencies = [];
258
+ for (const edge of dependencyEdges) {
259
+ // An external target lives in the parent chain. Branch before any
260
+ // local lookup: the child graph may have no node for it (which would
261
+ // read as undefined) or a hidden one the requester must not get.
262
+ if (edge.metadata.external) {
263
+ resolvedDependencies.push(await this.linkFor(edge).resolve(edge.target));
264
+ continue;
265
+ }
266
+ if (edge.metadata.lateBound && !this.lateBoundVisible(edge, node)) {
267
+ resolvedDependencies.push(undefined);
268
+ continue;
269
+ }
53
270
  if (edge.metadata.isCircular) {
54
- const circularResolver = () => new Proxy({}, {
55
- get: (_, prop) => {
56
- const instance = resolveCache.get(edge.target);
57
- return instance?.[prop];
58
- },
59
- });
60
- circularResolver.dependencyName = edge.target;
61
- return circularResolver;
62
- }
63
- return edge.target;
64
- });
65
- const depsChunks = dependencies.reduce((prev, depToken) => {
66
- if (isCircularDependencyFn(depToken)) {
67
- prev[0].push(depToken);
68
- prev[1].push(null);
69
- return prev;
70
- }
71
- prev[0].push(null);
72
- prev[1].push(depToken);
73
- return prev;
74
- }, [[], []]);
75
- // biome-ignore lint/suspicious/noExplicitAny: dependency resolution
76
- let resolvedDependencies = [];
77
- for (const depToken of dependencies) {
78
- if (isCircularDependencyFn(depToken)) {
79
- if (isCircularDependency) {
80
- resolvedDependencies.push(depToken);
271
+ // A circular target that another tree is creating gets the proxy
272
+ // too: awaiting it could deadlock, since that tree may be waiting
273
+ // on this one through the same cycle.
274
+ if (isCircularDependency ||
275
+ (this.inFlightCreations.has(edge.target) &&
276
+ !this.providersContainer.has(edge.target))) {
277
+ const circularResolver = () => new Proxy({}, {
278
+ get: (_, prop) => {
279
+ const instance = this.providersContainer.get(edge.target);
280
+ return instance?.[prop];
281
+ },
282
+ });
283
+ circularResolver.dependencyName = edge.target;
284
+ resolvedDependencies.push(circularResolver);
81
285
  continue;
82
286
  }
83
- const instance = await this.resolveProvider(
84
- // @ts-expect-error
85
- depToken.dependencyName, resolveCache, true);
86
- resolvedDependencies.push(instance);
287
+ resolvedDependencies.push(await this.resolveProvider(edge.target, ctx, true));
87
288
  continue;
88
289
  }
89
- const instance = await this.resolveProvider(depToken, resolveCache);
90
- resolvedDependencies.push(instance);
290
+ resolvedDependencies.push(await this.resolveProvider(edge.target, ctx));
91
291
  }
92
- resolvedDependencies = resolvedDependencies.map((dependency, index) => {
93
- if (depsChunks[0]?.[index]) {
94
- return depsChunks[0]?.[index];
95
- }
96
- return dependency;
97
- });
292
+ const deps = resolvedDependencies.map((dep) => isCircularDependencyFn(dep)
293
+ ? dep()
294
+ : dep);
98
295
  // biome-ignore lint/suspicious/noExplicitAny: instance creation
99
296
  let instance;
100
- let saveInCache = true;
101
- const deps = resolvedDependencies.map((depToken) => isCircularDependencyFn(depToken) ? depToken() : depToken);
297
+ const injectionContext = this.contextFor(node, ctx);
102
298
  if (isClassProvider(provider)) {
103
- instance = new provider.useClass(...deps);
104
- await this.injectPropertyDependencies(instance, node, resolveCache);
105
- // Determine caching strategy based on scope
106
- const scope = node.scope;
107
- saveInCache = scope === Scope.Singleton;
299
+ instance = withInjectionContext(injectionContext, () => new provider.useClass(...deps));
300
+ await this.injectPropertyDependencies(instance, node, ctx);
108
301
  }
109
302
  else if (isValueProvider(provider)) {
110
303
  instance = provider.useValue;
111
- // Value providers are always singleton
112
- saveInCache = true;
113
304
  }
114
305
  else if (isFactoryProvider(provider)) {
115
- instance = await provider.useFactory(...deps);
116
- // Check scope for factory providers
117
- const scope = node.scope;
118
- saveInCache = scope === Scope.Singleton;
306
+ instance = this.callFactory(node, provider, deps, injectionContext);
119
307
  }
120
308
  else {
121
- instance = new provider(...deps);
122
- await this.injectPropertyDependencies(instance, node, resolveCache);
123
- // Check scope for function providers
124
- const scope = node.scope;
125
- saveInCache = scope === Scope.Singleton;
309
+ instance = withInjectionContext(injectionContext, () => new provider(...deps));
310
+ await this.injectPropertyDependencies(instance, node, ctx);
126
311
  }
127
- // Call lifecycle hook with error handling
312
+ // onModuleInit of providers created via inject() must finish before
313
+ // this instance's own onModuleInit runs.
314
+ await ctx.drainPendingInits();
128
315
  if (instance?.onModuleInit) {
129
316
  try {
130
317
  await instance.onModuleInit();
131
318
  }
132
319
  catch (error) {
133
- // Re-throw with additional context about which provider failed
134
- const providerName = typeof provider === "function"
135
- ? provider.name
136
- : provider.provide?.toString() ||
137
- "Unknown";
138
- const errorMessage = `Failed to initialize provider "${providerName}": ${error instanceof Error ? error.message : String(error)}`;
139
- const wrappedError = new Error(errorMessage);
140
- // Preserve original error stack if available
141
- if (error instanceof Error && error.stack) {
142
- wrappedError.stack = `${wrappedError.stack}\nCaused by: ${error.stack}`;
143
- }
144
- throw wrappedError;
320
+ throw this.wrapInitError(provider, error);
321
+ }
322
+ }
323
+ return instance;
324
+ }
325
+ /**
326
+ * inject() entry point: resolves `token` synchronously for `requester`.
327
+ * The token must be visible from the requester's module, the same rule
328
+ * as `@Inject`.
329
+ */
330
+ resolveSync(token, ctx, requester, options) {
331
+ const node = this.graph.getNode(token);
332
+ const visible = node?.type === NodeTypeEnum.PROVIDER &&
333
+ this.graph.isProviderVisible(requester.moduleContainer, token);
334
+ if (!visible) {
335
+ // Not visible locally: the parent chain may provide it. `link.get`
336
+ // returns only an existing parent singleton (never creates one) and
337
+ // throws for a parent Transient, a not-yet-created singleton or a
338
+ // closed parent, even with `optional`. The dependency crosses
339
+ // containers, so no runtime edge is recorded and nothing is cached.
340
+ if (this.link?.canProvide(token)) {
341
+ return this.link.get(token);
342
+ }
343
+ if (options.optional) {
344
+ return undefined;
345
+ }
346
+ throw new ProviderNotFoundError(token, requester.id);
347
+ }
348
+ const instance = this.resolveProviderSync(node, ctx);
349
+ // Idempotent; covers cached and freshly created instances alike.
350
+ this.graph.addRuntimeDependency(requester.id, node.id);
351
+ return instance;
352
+ }
353
+ /**
354
+ * Synchronous counterpart of resolveProvider for a provider node whose
355
+ * reachability was already established (visibility check or graph edge).
356
+ */
357
+ resolveProviderSync(node, ctx) {
358
+ const token = node.id;
359
+ if (ctx.isConstructing(token)) {
360
+ throw new CircularInjectionError([...ctx.path, token]);
361
+ }
362
+ if (node.scope === Scope.Singleton && this.providersContainer.has(token)) {
363
+ const inFlight = this.inFlightInits.get(token);
364
+ if (inFlight) {
365
+ // The requester's own init and eviction chain wait on it.
366
+ ctx.addPendingInit(inFlight);
367
+ }
368
+ return this.providersContainer.get(token);
369
+ }
370
+ if (node.scope === Scope.Singleton && this.inFlightCreations.has(token)) {
371
+ // resolveProvider is creating it in another tree. This path is
372
+ // synchronous and cannot wait, and creating it here would make a
373
+ // second instance.
374
+ throw new ProviderNotInstantiatedError(token, "creation-in-flight");
375
+ }
376
+ return this.createInstanceSync(node, ctx);
377
+ }
378
+ /** Resolves a static @Inject edge of `requester` on the synchronous path. */
379
+ resolveEdgeSync(edge, requester, ctx) {
380
+ // External first, before any node lookup (see createInstanceInContext).
381
+ if (edge.metadata.external) {
382
+ return this.linkFor(edge).get(edge.target);
383
+ }
384
+ if (edge.metadata.isCircular) {
385
+ throw new CircularInjectionError([...ctx.path, edge.target]);
386
+ }
387
+ // Unreached edges resolve to undefined, as on the async path. Reached
388
+ // edges were checked by the graph, so there is no second visibility
389
+ // check, except for a late-bound one (see lateBoundVisible); a missing
390
+ // (@Optional) node resolves to undefined.
391
+ if (edge.metadata.unreached) {
392
+ return undefined;
393
+ }
394
+ if (edge.metadata.lateBound && !this.lateBoundVisible(edge, requester)) {
395
+ return undefined;
396
+ }
397
+ const target = this.graph.getNode(edge.target);
398
+ if (!target || target.type !== NodeTypeEnum.PROVIDER) {
399
+ return undefined;
400
+ }
401
+ return this.resolveProviderSync(target, ctx);
402
+ }
403
+ /**
404
+ * Synchronous mirror of createInstance + resolveProvider caching, used
405
+ * when inject() reaches a provider that does not exist yet.
406
+ */
407
+ createInstanceSync(node, ctx) {
408
+ // A resolution tree that outlived close() (it was awaiting something
409
+ // when close() ran) must not create and cache anything: the destroy
410
+ // pass is over. Refused before construction, so there is nothing to
411
+ // destroy. No unload counterpart is needed: this path always reads
412
+ // the live graph, so it never creates an instance of an unloaded node.
413
+ if (this.closed) {
414
+ throw new ContainerClosedError();
415
+ }
416
+ ctx.enter(node.id);
417
+ try {
418
+ const provider = node.metatype;
419
+ const deps = this.constructorEdges(node).map((edge) => this.resolveEdgeSync(edge, node, ctx));
420
+ const injectionContext = this.contextFor(node, ctx);
421
+ // biome-ignore lint/suspicious/noExplicitAny: instance creation
422
+ let instance;
423
+ if (isValueProvider(provider)) {
424
+ instance = provider.useValue;
425
+ }
426
+ else if (isFactoryProvider(provider)) {
427
+ instance = this.callFactory(node, provider, deps, injectionContext);
428
+ }
429
+ else {
430
+ const Class = isClassProvider(provider)
431
+ ? provider.useClass
432
+ : provider;
433
+ instance = withInjectionContext(injectionContext, () => new Class(...deps));
434
+ this.injectPropertyDependenciesSync(instance, node, ctx);
435
+ }
436
+ // onModuleInit is not part of creation: it runs with no injection
437
+ // context, so inject() there throws InjectionContextError.
438
+ const runInit = () => withInjectionContext(null, () => this.callInitSync(provider, instance));
439
+ // Conservatively wait on every init queued before this instance (it
440
+ // may hold some of those instances, created here or via cache hits),
441
+ // even without its own onModuleInit, so a failure there evicts this
442
+ // instance too. No deadlock: each entry only waits on earlier ones.
443
+ const earlier = [...ctx.pendingInits];
444
+ let pendingInit;
445
+ if (earlier.length > 0) {
446
+ const afterEarlier = Promise.all(earlier);
447
+ pendingInit = instance?.onModuleInit
448
+ ? afterEarlier.then(runInit)
449
+ : afterEarlier;
145
450
  }
451
+ else if (instance?.onModuleInit) {
452
+ pendingInit = runInit();
453
+ }
454
+ this.commitInstance(node, instance);
455
+ if (pendingInit) {
456
+ // A failed init (own or earlier) must not leave a broken cached
457
+ // instance. The eviction is chained here, not in a drain, so it
458
+ // also happens when the requester's constructor throws later.
459
+ const settled = pendingInit.catch((error) => {
460
+ this.evictInstance(node, instance);
461
+ throw error;
462
+ });
463
+ ctx.addPendingInit(settled);
464
+ this.trackInFlightInit(node.id, instance, settled);
465
+ }
466
+ return instance;
467
+ }
468
+ finally {
469
+ ctx.leave(node.id);
470
+ }
471
+ }
472
+ /** Runs a factory; factories must be synchronous. */
473
+ callFactory(node, provider, deps, injectionContext) {
474
+ const result = withInjectionContext(injectionContext, () => provider.useFactory(...deps));
475
+ if (isThenable(result)) {
476
+ // Never leave a rejected promise unhandled. A foreign thenable's
477
+ // then() is user code with unknown effects, so it is not called.
478
+ if (result instanceof Promise) {
479
+ result.catch(() => { });
480
+ }
481
+ throw new AsyncFactoryError(node.id);
482
+ }
483
+ return result;
484
+ }
485
+ /** Registers `init` as the in-flight init of a just-committed singleton. */
486
+ trackInFlightInit(token, instance, init) {
487
+ if (!this.providersContainer.has(token) ||
488
+ this.providersContainer.get(token) !== instance) {
489
+ return; // not the global singleton (lost a race)
490
+ }
491
+ this.inFlightInits.set(token, init);
492
+ const clear = () => {
493
+ if (this.inFlightInits.get(token) === init) {
494
+ this.inFlightInits.delete(token);
495
+ }
496
+ };
497
+ init.then(clear, clear);
498
+ }
499
+ /**
500
+ * Calls onModuleInit synchronously. A synchronous throw is wrapped and
501
+ * rethrown; an async result is returned with its rejection wrapped.
502
+ */
503
+ callInitSync(provider, instance) {
504
+ let result;
505
+ try {
506
+ result = instance.onModuleInit();
146
507
  }
147
- return [instance, saveInCache];
508
+ catch (error) {
509
+ throw this.wrapInitError(provider, error);
510
+ }
511
+ if (isThenable(result)) {
512
+ return Promise.resolve(result).catch((error) => {
513
+ throw this.wrapInitError(provider, error);
514
+ });
515
+ }
516
+ return undefined;
517
+ }
518
+ wrapInitError(provider, error) {
519
+ const providerName = typeof provider === "function"
520
+ ? provider.name
521
+ : (provider.provide?.toString() ??
522
+ "Unknown");
523
+ const errorMessage = `Failed to initialize provider "${providerName}": ${error instanceof Error ? error.message : String(error)}`;
524
+ const wrappedError = new Error(errorMessage);
525
+ // Assigned rather than passed as ErrorOptions: the build targets ES2021.
526
+ wrappedError.cause = error;
527
+ if (error instanceof Error && error.stack) {
528
+ wrappedError.stack = `${wrappedError.stack}\nCaused by: ${error.stack}`;
529
+ }
530
+ return wrappedError;
531
+ }
532
+ contextFor(node, ctx) {
533
+ return {
534
+ inject: (token, options) => this.resolveSync(token, ctx, node, options),
535
+ };
536
+ }
537
+ /**
538
+ * Whether a late-bound edge's target may be injected into `requester`:
539
+ * it exists now and is visible from the requester's module. The graph
540
+ * records the referrer (keeping the target's module alive) under the
541
+ * same rule, so injection and unload bookkeeping agree.
542
+ */
543
+ lateBoundVisible(edge, requester) {
544
+ const version = this.graph.version;
545
+ const memo = this.lateBoundMemo.get(edge);
546
+ if (memo?.version === version) {
547
+ return memo.visible;
548
+ }
549
+ const visible = this.graph.getNode(edge.target)?.type === NodeTypeEnum.PROVIDER &&
550
+ this.graph.isProviderVisible(requester.moduleContainer, edge.target);
551
+ this.lateBoundMemo.set(edge, { version, visible });
552
+ return visible;
148
553
  }
149
- async injectPropertyDependencies(instance, node, resolveCache) {
150
- const dependencies = this.graph
554
+ constructorEdges(node) {
555
+ return this.graph
556
+ .getEdge(node.id)
557
+ .filter((edge) => edge.type === EdgeTypeEnum.DEPENDENCY &&
558
+ edge.metadata.inject === "constructor");
559
+ }
560
+ propertyEdges(node) {
561
+ return this.graph
151
562
  .getEdge(node.id)
152
563
  .filter((edge) => edge.type === EdgeTypeEnum.DEPENDENCY &&
153
564
  edge.metadata.inject === "property" &&
154
565
  edge.metadata.unreached === false &&
155
566
  edge.metadata.isCircular === false);
156
- for (const edge of dependencies) {
157
- instance[edge.metadata.key] = await this.resolveProvider(edge.target, resolveCache);
567
+ }
568
+ injectPropertyDependenciesSync(instance, node, ctx) {
569
+ for (const edge of this.propertyEdges(node)) {
570
+ instance[edge.metadata.key] = this.resolveEdgeSync(edge, node, ctx);
571
+ }
572
+ }
573
+ async injectPropertyDependencies(instance, node, ctx) {
574
+ for (const edge of this.propertyEdges(node)) {
575
+ // External first, before resolveProvider's node lookup.
576
+ instance[edge.metadata.key] = edge.metadata.external
577
+ ? await this.linkFor(edge).resolve(edge.target)
578
+ : edge.metadata.lateBound &&
579
+ !this.lateBoundVisible(edge, node)
580
+ ? undefined
581
+ : await this.resolveProvider(edge.target, ctx);
158
582
  }
159
583
  }
160
584
  }