@navios/di 0.7.1 → 0.9.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 (263) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +117 -17
  3. package/lib/browser/container/abstract-container.d.mts +112 -0
  4. package/lib/browser/container/abstract-container.d.mts.map +1 -0
  5. package/lib/browser/container/abstract-container.mjs +100 -0
  6. package/lib/browser/container/abstract-container.mjs.map +1 -0
  7. package/lib/browser/container/container.d.mts +100 -0
  8. package/lib/browser/container/container.d.mts.map +1 -0
  9. package/lib/browser/container/container.mjs +424 -0
  10. package/lib/browser/container/container.mjs.map +1 -0
  11. package/lib/browser/container/scoped-container.d.mts +93 -0
  12. package/lib/browser/container/scoped-container.d.mts.map +1 -0
  13. package/lib/browser/container/scoped-container.mjs +119 -0
  14. package/lib/browser/container/scoped-container.mjs.map +1 -0
  15. package/lib/browser/decorators/factory.decorator.d.mts +26 -0
  16. package/lib/browser/decorators/factory.decorator.d.mts.map +1 -0
  17. package/lib/browser/decorators/factory.decorator.mjs +20 -0
  18. package/lib/browser/decorators/factory.decorator.mjs.map +1 -0
  19. package/lib/browser/decorators/injectable.decorator.d.mts +38 -0
  20. package/lib/browser/decorators/injectable.decorator.d.mts.map +1 -0
  21. package/lib/browser/decorators/injectable.decorator.mjs +21 -0
  22. package/lib/browser/decorators/injectable.decorator.mjs.map +1 -0
  23. package/lib/browser/enums/injectable-scope.enum.d.mts +18 -0
  24. package/lib/browser/enums/injectable-scope.enum.d.mts.map +1 -0
  25. package/lib/browser/enums/injectable-scope.enum.mjs +20 -0
  26. package/lib/browser/enums/injectable-scope.enum.mjs.map +1 -0
  27. package/lib/browser/enums/injectable-type.enum.d.mts +8 -0
  28. package/lib/browser/enums/injectable-type.enum.d.mts.map +1 -0
  29. package/lib/browser/enums/injectable-type.enum.mjs +10 -0
  30. package/lib/browser/enums/injectable-type.enum.mjs.map +1 -0
  31. package/lib/browser/errors/di-error.d.mts +43 -0
  32. package/lib/browser/errors/di-error.d.mts.map +1 -0
  33. package/lib/browser/errors/di-error.mjs +98 -0
  34. package/lib/browser/errors/di-error.mjs.map +1 -0
  35. package/lib/browser/event-emitter.d.mts +16 -0
  36. package/lib/browser/event-emitter.d.mts.map +1 -0
  37. package/lib/browser/event-emitter.mjs +320 -0
  38. package/lib/browser/event-emitter.mjs.map +1 -0
  39. package/lib/browser/index.d.mts +37 -1508
  40. package/lib/browser/index.mjs +29 -2650
  41. package/lib/browser/interfaces/container.interface.d.mts +59 -0
  42. package/lib/browser/interfaces/container.interface.d.mts.map +1 -0
  43. package/lib/browser/interfaces/factory.interface.d.mts +14 -0
  44. package/lib/browser/interfaces/factory.interface.d.mts.map +1 -0
  45. package/lib/browser/interfaces/on-service-destroy.interface.d.mts +7 -0
  46. package/lib/browser/interfaces/on-service-destroy.interface.d.mts.map +1 -0
  47. package/lib/browser/interfaces/on-service-init.interface.d.mts +7 -0
  48. package/lib/browser/interfaces/on-service-init.interface.d.mts.map +1 -0
  49. package/lib/browser/internal/context/async-local-storage.browser.mjs +20 -0
  50. package/lib/browser/internal/context/async-local-storage.browser.mjs.map +1 -0
  51. package/lib/browser/internal/context/async-local-storage.d.mts +9 -0
  52. package/lib/browser/internal/context/async-local-storage.d.mts.map +1 -0
  53. package/lib/browser/internal/context/async-local-storage.types.d.mts +11 -0
  54. package/lib/browser/internal/context/async-local-storage.types.d.mts.map +1 -0
  55. package/lib/browser/internal/context/factory-context.d.mts +23 -0
  56. package/lib/browser/internal/context/factory-context.d.mts.map +1 -0
  57. package/lib/browser/internal/context/resolution-context.d.mts +43 -0
  58. package/lib/browser/internal/context/resolution-context.d.mts.map +1 -0
  59. package/lib/browser/internal/context/resolution-context.mjs +56 -0
  60. package/lib/browser/internal/context/resolution-context.mjs.map +1 -0
  61. package/lib/browser/internal/context/service-initialization-context.d.mts +48 -0
  62. package/lib/browser/internal/context/service-initialization-context.d.mts.map +1 -0
  63. package/lib/browser/internal/context/sync-local-storage.mjs +53 -0
  64. package/lib/browser/internal/context/sync-local-storage.mjs.map +1 -0
  65. package/lib/browser/internal/core/instance-resolver.d.mts +119 -0
  66. package/lib/browser/internal/core/instance-resolver.d.mts.map +1 -0
  67. package/lib/browser/internal/core/instance-resolver.mjs +306 -0
  68. package/lib/browser/internal/core/instance-resolver.mjs.map +1 -0
  69. package/lib/browser/internal/core/name-resolver.d.mts +52 -0
  70. package/lib/browser/internal/core/name-resolver.d.mts.map +1 -0
  71. package/lib/browser/internal/core/name-resolver.mjs +118 -0
  72. package/lib/browser/internal/core/name-resolver.mjs.map +1 -0
  73. package/lib/browser/internal/core/scope-tracker.d.mts +65 -0
  74. package/lib/browser/internal/core/scope-tracker.d.mts.map +1 -0
  75. package/lib/browser/internal/core/scope-tracker.mjs +120 -0
  76. package/lib/browser/internal/core/scope-tracker.mjs.map +1 -0
  77. package/lib/browser/internal/core/service-initializer.d.mts +44 -0
  78. package/lib/browser/internal/core/service-initializer.d.mts.map +1 -0
  79. package/lib/browser/internal/core/service-initializer.mjs +109 -0
  80. package/lib/browser/internal/core/service-initializer.mjs.map +1 -0
  81. package/lib/browser/internal/core/service-invalidator.d.mts +81 -0
  82. package/lib/browser/internal/core/service-invalidator.d.mts.map +1 -0
  83. package/lib/browser/internal/core/service-invalidator.mjs +142 -0
  84. package/lib/browser/internal/core/service-invalidator.mjs.map +1 -0
  85. package/lib/browser/internal/core/token-resolver.d.mts +54 -0
  86. package/lib/browser/internal/core/token-resolver.d.mts.map +1 -0
  87. package/lib/browser/internal/core/token-resolver.mjs +77 -0
  88. package/lib/browser/internal/core/token-resolver.mjs.map +1 -0
  89. package/lib/browser/internal/holder/holder-storage.interface.d.mts +99 -0
  90. package/lib/browser/internal/holder/holder-storage.interface.d.mts.map +1 -0
  91. package/lib/browser/internal/holder/instance-holder.d.mts +101 -0
  92. package/lib/browser/internal/holder/instance-holder.d.mts.map +1 -0
  93. package/lib/browser/internal/holder/instance-holder.mjs +19 -0
  94. package/lib/browser/internal/holder/instance-holder.mjs.map +1 -0
  95. package/lib/browser/internal/holder/unified-storage.d.mts +53 -0
  96. package/lib/browser/internal/holder/unified-storage.d.mts.map +1 -0
  97. package/lib/browser/internal/holder/unified-storage.mjs +144 -0
  98. package/lib/browser/internal/holder/unified-storage.mjs.map +1 -0
  99. package/lib/browser/internal/lifecycle/circular-detector.d.mts +39 -0
  100. package/lib/browser/internal/lifecycle/circular-detector.d.mts.map +1 -0
  101. package/lib/browser/internal/lifecycle/circular-detector.mjs +55 -0
  102. package/lib/browser/internal/lifecycle/circular-detector.mjs.map +1 -0
  103. package/lib/browser/internal/lifecycle/lifecycle-event-bus.d.mts +18 -0
  104. package/lib/browser/internal/lifecycle/lifecycle-event-bus.d.mts.map +1 -0
  105. package/lib/browser/internal/lifecycle/lifecycle-event-bus.mjs +43 -0
  106. package/lib/browser/internal/lifecycle/lifecycle-event-bus.mjs.map +1 -0
  107. package/lib/browser/internal/stub-factory-class.d.mts +14 -0
  108. package/lib/browser/internal/stub-factory-class.d.mts.map +1 -0
  109. package/lib/browser/internal/stub-factory-class.mjs +18 -0
  110. package/lib/browser/internal/stub-factory-class.mjs.map +1 -0
  111. package/lib/browser/symbols/injectable-token.d.mts +5 -0
  112. package/lib/browser/symbols/injectable-token.d.mts.map +1 -0
  113. package/lib/browser/symbols/injectable-token.mjs +6 -0
  114. package/lib/browser/symbols/injectable-token.mjs.map +1 -0
  115. package/lib/browser/token/injection-token.d.mts +55 -0
  116. package/lib/browser/token/injection-token.d.mts.map +1 -0
  117. package/lib/browser/token/injection-token.mjs +100 -0
  118. package/lib/browser/token/injection-token.mjs.map +1 -0
  119. package/lib/browser/token/registry.d.mts +37 -0
  120. package/lib/browser/token/registry.d.mts.map +1 -0
  121. package/lib/browser/token/registry.mjs +86 -0
  122. package/lib/browser/token/registry.mjs.map +1 -0
  123. package/lib/browser/utils/default-injectors.d.mts +12 -0
  124. package/lib/browser/utils/default-injectors.d.mts.map +1 -0
  125. package/lib/browser/utils/default-injectors.mjs +13 -0
  126. package/lib/browser/utils/default-injectors.mjs.map +1 -0
  127. package/lib/browser/utils/get-injectable-token.d.mts +9 -0
  128. package/lib/browser/utils/get-injectable-token.d.mts.map +1 -0
  129. package/lib/browser/utils/get-injectable-token.mjs +13 -0
  130. package/lib/browser/utils/get-injectable-token.mjs.map +1 -0
  131. package/lib/browser/utils/get-injectors.d.mts +55 -0
  132. package/lib/browser/utils/get-injectors.d.mts.map +1 -0
  133. package/lib/browser/utils/get-injectors.mjs +121 -0
  134. package/lib/browser/utils/get-injectors.mjs.map +1 -0
  135. package/lib/browser/utils/types.d.mts +23 -0
  136. package/lib/browser/utils/types.d.mts.map +1 -0
  137. package/lib/{container-Pb_Y4Z4x.mjs → container-8-z89TyQ.mjs} +1269 -1305
  138. package/lib/container-8-z89TyQ.mjs.map +1 -0
  139. package/lib/{container-BuAutHGg.d.mts → container-CNiqesCL.d.mts} +600 -569
  140. package/lib/container-CNiqesCL.d.mts.map +1 -0
  141. package/lib/{container-DnzgpfBe.cjs → container-CaY2fDuk.cjs} +1287 -1329
  142. package/lib/container-CaY2fDuk.cjs.map +1 -0
  143. package/lib/{container-oGTgX2iX.d.cts → container-D-0Ho3qL.d.cts} +601 -565
  144. package/lib/container-D-0Ho3qL.d.cts.map +1 -0
  145. package/lib/index.cjs +13 -15
  146. package/lib/index.cjs.map +1 -1
  147. package/lib/index.d.cts +58 -223
  148. package/lib/index.d.cts.map +1 -1
  149. package/lib/index.d.mts +62 -222
  150. package/lib/index.d.mts.map +1 -1
  151. package/lib/index.mjs +5 -6
  152. package/lib/index.mjs.map +1 -1
  153. package/lib/testing/index.cjs +569 -311
  154. package/lib/testing/index.cjs.map +1 -1
  155. package/lib/testing/index.d.cts +370 -41
  156. package/lib/testing/index.d.cts.map +1 -1
  157. package/lib/testing/index.d.mts +370 -41
  158. package/lib/testing/index.d.mts.map +1 -1
  159. package/lib/testing/index.mjs +568 -305
  160. package/lib/testing/index.mjs.map +1 -1
  161. package/package.json +2 -1
  162. package/src/__tests__/circular-detector.spec.mts +193 -0
  163. package/src/__tests__/concurrent.spec.mts +368 -0
  164. package/src/__tests__/container.spec.mts +32 -30
  165. package/src/__tests__/di-error.spec.mts +351 -0
  166. package/src/__tests__/e2e.browser.spec.mts +0 -4
  167. package/src/__tests__/e2e.spec.mts +10 -19
  168. package/src/__tests__/event-emitter.spec.mts +232 -109
  169. package/src/__tests__/get-injectors.spec.mts +250 -39
  170. package/src/__tests__/injection-token.spec.mts +293 -349
  171. package/src/__tests__/library-findings.spec.mts +8 -8
  172. package/src/__tests__/registry.spec.mts +358 -210
  173. package/src/__tests__/resolution-context.spec.mts +255 -0
  174. package/src/__tests__/scope-tracker.spec.mts +598 -0
  175. package/src/__tests__/scope-upgrade.spec.mts +808 -0
  176. package/src/__tests__/scoped-container.spec.mts +595 -0
  177. package/src/__tests__/test-container.spec.mts +293 -0
  178. package/src/__tests__/token-resolver.spec.mts +207 -0
  179. package/src/__tests__/unified-storage.spec.mts +535 -0
  180. package/src/__tests__/unit-test-container.spec.mts +405 -0
  181. package/src/__type-tests__/container.spec-d.mts +180 -0
  182. package/src/__type-tests__/factory.spec-d.mts +15 -3
  183. package/src/__type-tests__/inject.spec-d.mts +115 -20
  184. package/src/__type-tests__/injectable.spec-d.mts +69 -52
  185. package/src/__type-tests__/injection-token.spec-d.mts +176 -0
  186. package/src/__type-tests__/scoped-container.spec-d.mts +212 -0
  187. package/src/container/abstract-container.mts +327 -0
  188. package/src/container/container.mts +142 -170
  189. package/src/container/scoped-container.mts +126 -208
  190. package/src/decorators/factory.decorator.mts +16 -11
  191. package/src/decorators/injectable.decorator.mts +20 -16
  192. package/src/enums/index.mts +2 -2
  193. package/src/enums/injectable-scope.enum.mts +1 -0
  194. package/src/enums/injectable-type.enum.mts +1 -0
  195. package/src/errors/di-error.mts +96 -0
  196. package/src/event-emitter.mts +3 -27
  197. package/src/index.mts +6 -153
  198. package/src/interfaces/container.interface.mts +13 -0
  199. package/src/interfaces/factory.interface.mts +1 -1
  200. package/src/interfaces/index.mts +1 -1
  201. package/src/internal/context/async-local-storage.mts +3 -2
  202. package/src/internal/context/async-local-storage.types.mts +1 -0
  203. package/src/internal/context/factory-context.mts +1 -0
  204. package/src/internal/context/index.mts +3 -1
  205. package/src/internal/context/resolution-context.mts +1 -0
  206. package/src/internal/context/service-initialization-context.mts +43 -0
  207. package/src/internal/core/index.mts +5 -4
  208. package/src/internal/core/instance-resolver.mts +461 -292
  209. package/src/internal/core/name-resolver.mts +196 -0
  210. package/src/internal/core/scope-tracker.mts +242 -0
  211. package/src/internal/core/{instantiator.mts → service-initializer.mts} +51 -29
  212. package/src/internal/core/service-invalidator.mts +290 -0
  213. package/src/internal/core/{token-processor.mts → token-resolver.mts} +17 -88
  214. package/src/internal/holder/holder-storage.interface.mts +11 -5
  215. package/src/internal/holder/index.mts +2 -5
  216. package/src/internal/holder/instance-holder.mts +1 -3
  217. package/src/internal/holder/unified-storage.mts +245 -0
  218. package/src/internal/index.mts +2 -1
  219. package/src/internal/lifecycle/circular-detector.mts +1 -0
  220. package/src/internal/lifecycle/index.mts +1 -1
  221. package/src/internal/lifecycle/lifecycle-event-bus.mts +1 -0
  222. package/src/internal/stub-factory-class.mts +16 -0
  223. package/src/symbols/injectable-token.mts +3 -1
  224. package/src/testing/index.mts +2 -0
  225. package/src/testing/test-container.mts +546 -85
  226. package/src/testing/types.mts +117 -0
  227. package/src/testing/unit-test-container.mts +509 -0
  228. package/src/token/injection-token.mts +41 -4
  229. package/src/token/registry.mts +75 -9
  230. package/src/utils/default-injectors.mts +16 -0
  231. package/src/utils/get-injectable-token.mts +2 -3
  232. package/src/utils/get-injectors.mts +26 -15
  233. package/src/utils/index.mts +3 -1
  234. package/src/utils/types.mts +1 -0
  235. package/tsdown.config.mts +11 -1
  236. package/lib/browser/index.d.mts.map +0 -1
  237. package/lib/browser/index.mjs.map +0 -1
  238. package/lib/container-BuAutHGg.d.mts.map +0 -1
  239. package/lib/container-DnzgpfBe.cjs.map +0 -1
  240. package/lib/container-Pb_Y4Z4x.mjs.map +0 -1
  241. package/lib/container-oGTgX2iX.d.cts.map +0 -1
  242. package/src/__tests__/async-local-storage.browser.spec.mts +0 -166
  243. package/src/__tests__/async-local-storage.spec.mts +0 -333
  244. package/src/__tests__/errors.spec.mts +0 -87
  245. package/src/__tests__/factory.spec.mts +0 -137
  246. package/src/__tests__/injectable.spec.mts +0 -246
  247. package/src/__tests__/request-scope.spec.mts +0 -416
  248. package/src/__tests__/service-instantiator.spec.mts +0 -410
  249. package/src/__tests__/service-locator-event-bus.spec.mts +0 -242
  250. package/src/__tests__/service-locator-manager.spec.mts +0 -300
  251. package/src/__tests__/service-locator.spec.mts +0 -966
  252. package/src/__tests__/unified-api.spec.mts +0 -130
  253. package/src/browser.mts +0 -11
  254. package/src/injectors.mts +0 -18
  255. package/src/internal/context/request-context.mts +0 -214
  256. package/src/internal/core/invalidator.mts +0 -437
  257. package/src/internal/core/service-locator.mts +0 -202
  258. package/src/internal/holder/base-holder-manager.mts +0 -238
  259. package/src/internal/holder/holder-manager.mts +0 -85
  260. package/src/internal/holder/request-storage.mts +0 -134
  261. package/src/internal/holder/singleton-storage.mts +0 -105
  262. package/src/testing/README.md +0 -80
  263. package/src/testing/__tests__/test-container.spec.mts +0 -173
@@ -0,0 +1,117 @@
1
+ import type { InjectionToken } from '../token/injection-token.mjs'
2
+ import type { Registry } from '../token/registry.mjs'
3
+
4
+ /**
5
+ * Tracks method calls on a mocked service.
6
+ */
7
+ export interface MethodCallRecord {
8
+ method: string
9
+ args: unknown[]
10
+ timestamp: number
11
+ result?: unknown
12
+ error?: Error
13
+ }
14
+
15
+ /**
16
+ * Tracks lifecycle events for a service.
17
+ */
18
+ export interface LifecycleRecord {
19
+ event: 'created' | 'initialized' | 'destroyed'
20
+ timestamp: number
21
+ instanceName: string
22
+ }
23
+
24
+ /**
25
+ * Statistics about a mocked service.
26
+ */
27
+ export interface MockServiceStats {
28
+ instanceCount: number
29
+ methodCalls: MethodCallRecord[]
30
+ lifecycleEvents: LifecycleRecord[]
31
+ }
32
+
33
+ /**
34
+ * A node in the dependency graph.
35
+ */
36
+ export interface DependencyNode {
37
+ token: string
38
+ instanceName: string
39
+ scope: string
40
+ dependencies: string[]
41
+ dependents: string[]
42
+ }
43
+
44
+ /**
45
+ * Serializable dependency graph for snapshot testing.
46
+ */
47
+ export interface DependencyGraph {
48
+ nodes: Record<string, DependencyNode>
49
+ rootTokens: string[]
50
+ }
51
+
52
+ /**
53
+ * Binding configuration for TestContainer.
54
+ */
55
+ export interface BindingBuilder<T> {
56
+ /**
57
+ * Bind to a concrete value.
58
+ */
59
+ toValue(value: T): void
60
+
61
+ /**
62
+ * Bind to a class implementation.
63
+ */
64
+ toClass<C extends new (...args: any[]) => T>(cls: C): void
65
+
66
+ /**
67
+ * Bind to a factory function.
68
+ */
69
+ toFactory(factory: () => T | Promise<T>): void
70
+ }
71
+
72
+ /**
73
+ * Provider configuration for UnitTestContainer.
74
+ */
75
+ export interface ProviderConfig<T = any> {
76
+ token: InjectionToken<T, any> | (new (...args: any[]) => T)
77
+ useValue?: T
78
+ useClass?: new (...args: any[]) => T
79
+ useFactory?: () => T | Promise<T>
80
+ }
81
+
82
+ /**
83
+ * Options for TestContainer.
84
+ */
85
+ export interface TestContainerOptions {
86
+ /**
87
+ * Parent registry. Defaults to globalRegistry.
88
+ * Pass `null` to create a completely isolated container.
89
+ */
90
+ parentRegistry?: Registry | null
91
+
92
+ /**
93
+ * Logger for debugging.
94
+ */
95
+ logger?: Console | null
96
+ }
97
+
98
+ /**
99
+ * Options for UnitTestContainer.
100
+ */
101
+ export interface UnitTestContainerOptions {
102
+ /**
103
+ * List of providers to register. Only these services can be resolved.
104
+ */
105
+ providers: ProviderConfig[]
106
+
107
+ /**
108
+ * If true, unregistered dependencies will be auto-mocked instead of throwing.
109
+ * Default: false (throws on unregistered dependencies)
110
+ */
111
+ allowUnregistered?: boolean
112
+
113
+ /**
114
+ * Logger for debugging.
115
+ */
116
+ logger?: Console | null
117
+ }
@@ -0,0 +1,509 @@
1
+ import type { LifecycleRecord, MethodCallRecord, MockServiceStats, ProviderConfig, UnitTestContainerOptions } from './types.mjs'
2
+
3
+ import { Container } from '../container/container.mjs'
4
+ import { InjectableScope, InjectableType } from '../enums/index.mjs'
5
+ import { DIError } from '../errors/index.mjs'
6
+ import { InjectionToken } from '../token/injection-token.mjs'
7
+ import { Registry } from '../token/registry.mjs'
8
+ import { getInjectableToken } from '../utils/get-injectable-token.mjs'
9
+ import { defaultInjectors } from '../utils/index.mjs'
10
+
11
+ type AnyToken = InjectionToken<any, any> | (new (...args: any[]) => any)
12
+
13
+ /**
14
+ * Creates a tracking proxy that records method calls.
15
+ */
16
+ function createTrackingProxy<T extends object>(
17
+ target: T,
18
+ tokenId: string,
19
+ methodCalls: Map<string, MethodCallRecord[]>,
20
+ ): T {
21
+ return new Proxy(target, {
22
+ get(obj, prop) {
23
+ const value = Reflect.get(obj, prop)
24
+
25
+ if (typeof value === 'function' && typeof prop === 'string') {
26
+ return function (this: unknown, ...args: unknown[]) {
27
+ const calls = methodCalls.get(tokenId) || []
28
+ const record: MethodCallRecord = {
29
+ method: prop,
30
+ args,
31
+ timestamp: Date.now(),
32
+ }
33
+
34
+ try {
35
+ const result = value.apply(this === undefined ? obj : this, args)
36
+
37
+ if (result instanceof Promise) {
38
+ return result
39
+ .then((res) => {
40
+ record.result = res
41
+ calls.push(record)
42
+ methodCalls.set(tokenId, calls)
43
+ return res
44
+ })
45
+ .catch((err) => {
46
+ record.error = err
47
+ calls.push(record)
48
+ methodCalls.set(tokenId, calls)
49
+ throw err
50
+ })
51
+ }
52
+
53
+ record.result = result
54
+ calls.push(record)
55
+ methodCalls.set(tokenId, calls)
56
+ return result
57
+ } catch (err) {
58
+ record.error = err as Error
59
+ calls.push(record)
60
+ methodCalls.set(tokenId, calls)
61
+ throw err
62
+ }
63
+ }
64
+ }
65
+
66
+ return value
67
+ },
68
+ })
69
+ }
70
+
71
+ /**
72
+ * Creates an auto-mock proxy that throws on method access.
73
+ */
74
+ function createAutoMockProxy(tokenId: string): object {
75
+ return new Proxy(
76
+ {},
77
+ {
78
+ get(_, prop) {
79
+ if (prop === 'then' || prop === 'catch' || prop === 'finally') {
80
+ return undefined
81
+ }
82
+ if (typeof prop === 'symbol') {
83
+ return undefined
84
+ }
85
+ throw new Error(
86
+ `[UnitTestContainer] Attempted to access '${prop}' on auto-mocked service '${tokenId}'. ` +
87
+ `This service was not provided in the providers list. ` +
88
+ `Add it to providers or use allowUnregistered: false to catch this earlier.`,
89
+ )
90
+ },
91
+ },
92
+ )
93
+ }
94
+
95
+ /**
96
+ * UnitTestContainer for isolated unit testing.
97
+ *
98
+ * Only services explicitly listed in `providers` can be resolved.
99
+ * All method calls are automatically tracked via proxies.
100
+ * Unregistered dependencies throw by default, or can be auto-mocked.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const container = new UnitTestContainer({
105
+ * providers: [
106
+ * { token: UserService, useClass: MockUserService },
107
+ * { token: ConfigToken, useValue: { apiUrl: 'test' } },
108
+ * ],
109
+ * })
110
+ *
111
+ * const service = await container.get(UserService)
112
+ *
113
+ * // All method calls are automatically tracked
114
+ * await service.findUser('123')
115
+ *
116
+ * container.expectCalled(UserService, 'findUser')
117
+ * container.expectCalledWith(UserService, 'findUser', ['123'])
118
+ * ```
119
+ */
120
+ export class UnitTestContainer extends Container {
121
+ private readonly testRegistry: Registry
122
+ private readonly methodCalls = new Map<string, MethodCallRecord[]>()
123
+ private readonly lifecycleEvents = new Map<string, LifecycleRecord[]>()
124
+ private readonly instanceCounts = new Map<string, number>()
125
+ private readonly registeredTokenIds = new Set<string>()
126
+ private readonly autoMockedTokenIds = new Set<string>()
127
+ private allowUnregistered: boolean
128
+
129
+ constructor(options: UnitTestContainerOptions) {
130
+ const testRegistry = new Registry()
131
+ super(testRegistry, options.logger ?? null, defaultInjectors)
132
+ this.testRegistry = testRegistry
133
+ this.allowUnregistered = options.allowUnregistered ?? false
134
+
135
+ // Register all providers
136
+ for (const provider of options.providers) {
137
+ this.registerProvider(provider)
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Enables auto-mocking for unregistered dependencies.
143
+ * Call this to switch from strict mode to auto-mock mode.
144
+ */
145
+ enableAutoMocking(): this {
146
+ this.allowUnregistered = true
147
+ return this
148
+ }
149
+
150
+ /**
151
+ * Disables auto-mocking (strict mode).
152
+ * Unregistered dependencies will throw.
153
+ */
154
+ disableAutoMocking(): this {
155
+ this.allowUnregistered = false
156
+ return this
157
+ }
158
+
159
+ /**
160
+ * Override get to wrap instances in tracking proxies.
161
+ */
162
+ override async get(token: any, args?: unknown): Promise<any> {
163
+ const realToken = this.resolveToken(token)
164
+
165
+ // Check if this is a registered provider
166
+ if (!this.registeredTokenIds.has(realToken.id)) {
167
+ if (!this.allowUnregistered) {
168
+ throw DIError.factoryNotFound(
169
+ `${realToken.toString()} is not in the providers list. ` +
170
+ `Add it to providers or enable allowUnregistered.`,
171
+ )
172
+ }
173
+
174
+ // Auto-mock unregistered dependency
175
+ if (!this.autoMockedTokenIds.has(realToken.id)) {
176
+ this.autoMockedTokenIds.add(realToken.id)
177
+ }
178
+
179
+ return createAutoMockProxy(realToken.id)
180
+ }
181
+
182
+ const instance = await super.get(token, args)
183
+
184
+ // Wrap in tracking proxy if it's an object
185
+ if (instance && typeof instance === 'object') {
186
+ return createTrackingProxy(instance, realToken.id, this.methodCalls)
187
+ }
188
+
189
+ return instance
190
+ }
191
+
192
+ /**
193
+ * Clears all state and disposes the container.
194
+ */
195
+ async clear(): Promise<void> {
196
+ await this.dispose()
197
+ this.methodCalls.clear()
198
+ this.lifecycleEvents.clear()
199
+ this.instanceCounts.clear()
200
+ this.autoMockedTokenIds.clear()
201
+ }
202
+
203
+ // ============================================================================
204
+ // ASSERTION HELPERS
205
+ // ============================================================================
206
+
207
+ /**
208
+ * Asserts that a service has been resolved at least once.
209
+ */
210
+ expectResolved(token: AnyToken): void {
211
+ const realToken = this.resolveToken(token)
212
+ const storage = this.getStorage()
213
+ const names = storage.getAllNames()
214
+ const found = names.some((name) => name.includes(realToken.id))
215
+
216
+ if (!found) {
217
+ throw new Error(`Expected ${realToken.toString()} to be resolved, but it was not`)
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Asserts that a service has NOT been resolved.
223
+ */
224
+ expectNotResolved(token: AnyToken): void {
225
+ const realToken = this.resolveToken(token)
226
+ const storage = this.getStorage()
227
+ const names = storage.getAllNames()
228
+ const found = names.some((name) => name.includes(realToken.id))
229
+
230
+ if (found) {
231
+ throw new Error(`Expected ${realToken.toString()} to NOT be resolved, but it was`)
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Asserts that a service was auto-mocked (not in providers list).
237
+ */
238
+ expectAutoMocked(token: AnyToken): void {
239
+ const realToken = this.resolveToken(token)
240
+
241
+ if (!this.autoMockedTokenIds.has(realToken.id)) {
242
+ throw new Error(
243
+ `Expected ${realToken.toString()} to be auto-mocked, but it was not. ` +
244
+ `Either it's in the providers list or hasn't been resolved.`,
245
+ )
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Asserts that a service was NOT auto-mocked (is in providers list).
251
+ */
252
+ expectNotAutoMocked(token: AnyToken): void {
253
+ const realToken = this.resolveToken(token)
254
+
255
+ if (this.autoMockedTokenIds.has(realToken.id)) {
256
+ throw new Error(
257
+ `Expected ${realToken.toString()} to NOT be auto-mocked, but it was.`,
258
+ )
259
+ }
260
+ }
261
+
262
+ // ============================================================================
263
+ // LIFECYCLE ASSERTIONS
264
+ // ============================================================================
265
+
266
+ /**
267
+ * Records a lifecycle event for tracking.
268
+ */
269
+ recordLifecycleEvent(token: AnyToken, event: 'created' | 'initialized' | 'destroyed', instanceName: string): void {
270
+ const realToken = this.resolveToken(token)
271
+ const events = this.lifecycleEvents.get(realToken.id) || []
272
+ events.push({
273
+ event,
274
+ timestamp: Date.now(),
275
+ instanceName,
276
+ })
277
+ this.lifecycleEvents.set(realToken.id, events)
278
+
279
+ if (event === 'created') {
280
+ const count = this.instanceCounts.get(realToken.id) || 0
281
+ this.instanceCounts.set(realToken.id, count + 1)
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Asserts that a service's onServiceInit was called.
287
+ */
288
+ expectInitialized(token: AnyToken): void {
289
+ const realToken = this.resolveToken(token)
290
+ const events = this.lifecycleEvents.get(realToken.id) || []
291
+ const initialized = events.some((e) => e.event === 'initialized')
292
+
293
+ if (!initialized) {
294
+ throw new Error(`Expected ${realToken.toString()} to be initialized, but onServiceInit was not called`)
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Asserts that a service's onServiceDestroy was called.
300
+ */
301
+ expectDestroyed(token: AnyToken): void {
302
+ const realToken = this.resolveToken(token)
303
+ const events = this.lifecycleEvents.get(realToken.id) || []
304
+ const destroyed = events.some((e) => e.event === 'destroyed')
305
+
306
+ if (!destroyed) {
307
+ throw new Error(`Expected ${realToken.toString()} to be destroyed, but onServiceDestroy was not called`)
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Asserts that a service has NOT been destroyed.
313
+ */
314
+ expectNotDestroyed(token: AnyToken): void {
315
+ const realToken = this.resolveToken(token)
316
+ const events = this.lifecycleEvents.get(realToken.id) || []
317
+ const destroyed = events.some((e) => e.event === 'destroyed')
318
+
319
+ if (destroyed) {
320
+ throw new Error(`Expected ${realToken.toString()} to NOT be destroyed, but onServiceDestroy was called`)
321
+ }
322
+ }
323
+
324
+ // ============================================================================
325
+ // CALL TRACKING (AUTO-TRACKED VIA PROXY)
326
+ // ============================================================================
327
+
328
+ /**
329
+ * Asserts that a method was called on a service.
330
+ */
331
+ expectCalled(token: AnyToken, method: string): void {
332
+ const realToken = this.resolveToken(token)
333
+ const calls = this.methodCalls.get(realToken.id) || []
334
+ const found = calls.some((c) => c.method === method)
335
+
336
+ if (!found) {
337
+ throw new Error(`Expected ${realToken.toString()}.${method}() to be called, but it was not`)
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Asserts that a method was NOT called on a service.
343
+ */
344
+ expectNotCalled(token: AnyToken, method: string): void {
345
+ const realToken = this.resolveToken(token)
346
+ const calls = this.methodCalls.get(realToken.id) || []
347
+ const found = calls.some((c) => c.method === method)
348
+
349
+ if (found) {
350
+ throw new Error(`Expected ${realToken.toString()}.${method}() to NOT be called, but it was`)
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Asserts that a method was called with specific arguments.
356
+ */
357
+ expectCalledWith(token: AnyToken, method: string, expectedArgs: unknown[]): void {
358
+ const realToken = this.resolveToken(token)
359
+ const calls = this.methodCalls.get(realToken.id) || []
360
+ const found = calls.some(
361
+ (c) => c.method === method && this.argsMatch(c.args, expectedArgs),
362
+ )
363
+
364
+ if (!found) {
365
+ const methodCalls = calls.filter((c) => c.method === method)
366
+ const actualArgs = methodCalls.map((c) => JSON.stringify(c.args)).join(', ')
367
+ throw new Error(
368
+ `Expected ${realToken.toString()}.${method}() to be called with ${JSON.stringify(expectedArgs)}. ` +
369
+ `Actual calls: [${actualArgs}]`,
370
+ )
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Asserts that a method was called a specific number of times.
376
+ */
377
+ expectCallCount(token: AnyToken, method: string, count: number): void {
378
+ const realToken = this.resolveToken(token)
379
+ const calls = this.methodCalls.get(realToken.id) || []
380
+ const actualCount = calls.filter((c) => c.method === method).length
381
+
382
+ if (actualCount !== count) {
383
+ throw new Error(
384
+ `Expected ${realToken.toString()}.${method}() to be called ${count} times, but was called ${actualCount} times`,
385
+ )
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Gets all recorded method calls for a service.
391
+ */
392
+ getMethodCalls(token: AnyToken): MethodCallRecord[] {
393
+ const realToken = this.resolveToken(token)
394
+ return this.methodCalls.get(realToken.id) || []
395
+ }
396
+
397
+ /**
398
+ * Gets statistics about a service.
399
+ */
400
+ getServiceStats(token: AnyToken): MockServiceStats {
401
+ const realToken = this.resolveToken(token)
402
+ return {
403
+ instanceCount: this.instanceCounts.get(realToken.id) || 0,
404
+ methodCalls: this.methodCalls.get(realToken.id) || [],
405
+ lifecycleEvents: this.lifecycleEvents.get(realToken.id) || [],
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Clears all recorded method calls.
411
+ */
412
+ clearMethodCalls(): void {
413
+ this.methodCalls.clear()
414
+ }
415
+
416
+ /**
417
+ * Gets list of all registered provider token IDs.
418
+ */
419
+ getRegisteredTokenIds(): ReadonlySet<string> {
420
+ return this.registeredTokenIds
421
+ }
422
+
423
+ /**
424
+ * Gets list of all auto-mocked token IDs.
425
+ */
426
+ getAutoMockedTokenIds(): ReadonlySet<string> {
427
+ return this.autoMockedTokenIds
428
+ }
429
+
430
+ // ============================================================================
431
+ // INTERNAL HELPERS
432
+ // ============================================================================
433
+
434
+ private resolveToken(token: AnyToken): InjectionToken<any, any> {
435
+ if (typeof token === 'function') {
436
+ try {
437
+ return getInjectableToken(token)
438
+ } catch {
439
+ // Class doesn't have @Injectable, create a token for it
440
+ return InjectionToken.create(token)
441
+ }
442
+ }
443
+ return token
444
+ }
445
+
446
+ private registerProvider<T>(provider: ProviderConfig<T>): void {
447
+ const realToken = this.resolveToken(provider.token as AnyToken)
448
+ this.registeredTokenIds.add(realToken.id)
449
+
450
+ if (provider.useValue !== undefined) {
451
+ this.registerValueBinding(realToken, provider.useValue)
452
+ } else if (provider.useClass) {
453
+ this.registerClassBinding(realToken, provider.useClass)
454
+ } else if (provider.useFactory) {
455
+ this.registerFactoryBinding(realToken, provider.useFactory)
456
+ } else {
457
+ // Just the token - register as itself
458
+ if (typeof provider.token === 'function') {
459
+ this.testRegistry.set(realToken, InjectableScope.Singleton, provider.token, InjectableType.Class)
460
+ }
461
+ }
462
+ }
463
+
464
+ private registerValueBinding<T>(token: InjectionToken<T, any>, value: T): void {
465
+ const ValueHolder = class {
466
+ static instance = value
467
+ }
468
+
469
+ this.testRegistry.set(token, InjectableScope.Singleton, ValueHolder, InjectableType.Class)
470
+
471
+ const nameResolver = this.getNameResolver()
472
+ const instanceName = nameResolver.generateInstanceName(
473
+ token,
474
+ undefined,
475
+ undefined,
476
+ InjectableScope.Singleton,
477
+ )
478
+ this.getStorage().storeInstance(instanceName, value)
479
+ this.recordLifecycleEvent(token, 'created', instanceName)
480
+ }
481
+
482
+ private registerClassBinding<T>(token: InjectionToken<T, any>, cls: new (...args: any[]) => T): void {
483
+ this.testRegistry.set(token, InjectableScope.Singleton, cls, InjectableType.Class)
484
+ }
485
+
486
+ private registerFactoryBinding<T>(token: InjectionToken<T, any>, factory: () => T | Promise<T>): void {
487
+ const FactoryWrapper = class {
488
+ static factory = factory
489
+ async create(): Promise<T> {
490
+ return await factory()
491
+ }
492
+ }
493
+
494
+ this.testRegistry.set(token, InjectableScope.Singleton, FactoryWrapper, InjectableType.Factory)
495
+ }
496
+
497
+ private argsMatch(actual: unknown[], expected: unknown[]): boolean {
498
+ if (actual.length !== expected.length) {
499
+ return false
500
+ }
501
+ return actual.every((arg, index) => {
502
+ const exp = expected[index]
503
+ if (typeof exp === 'object' && exp !== null) {
504
+ return JSON.stringify(arg) === JSON.stringify(exp)
505
+ }
506
+ return arg === exp
507
+ })
508
+ }
509
+ }
@@ -23,6 +23,39 @@ export type InjectionTokenSchemaType =
23
23
  | BaseInjectionTokenSchemaType
24
24
  | OptionalInjectionTokenSchemaType
25
25
 
26
+ /**
27
+ * Simple hash function for deterministic ID generation
28
+ */
29
+ function simpleHash(str: string): string {
30
+ let hash = 0
31
+ for (let i = 0; i < str.length; i++) {
32
+ const char = str.charCodeAt(i)
33
+ hash = (hash << 5) - hash + char
34
+ hash = hash & hash // Convert to 32-bit integer
35
+ }
36
+ return Math.abs(hash).toString(36)
37
+ }
38
+
39
+ /**
40
+ * Generate deterministic ID from token name
41
+ */
42
+ function generateTokenId(name: string | symbol | ClassType, customId?: string): string {
43
+ if (customId) {
44
+ return customId
45
+ }
46
+
47
+ let base: string
48
+ if (typeof name === 'function') {
49
+ base = `${name.name}_${name.toString()}`
50
+ } else if (typeof name === 'symbol') {
51
+ base = `symbol_${name.toString()}`
52
+ } else {
53
+ base = `token_${name}`
54
+ }
55
+
56
+ return `${base.split('_')[0]}_${simpleHash(base)}`
57
+ }
58
+
26
59
  export class InjectionToken<
27
60
  // oxlint-disable-next-line no-unused-vars
28
61
  T,
@@ -38,13 +71,16 @@ export class InjectionToken<
38
71
  ? true
39
72
  : false,
40
73
  > {
41
- public id = globalThis.crypto.randomUUID()
74
+ public readonly id: string
42
75
  private formattedName: string | null = null
43
76
 
44
77
  constructor(
45
78
  public readonly name: string | symbol | ClassType,
46
79
  public readonly schema: ZodObject | undefined,
47
- ) {}
80
+ customId?: string,
81
+ ) {
82
+ this.id = generateTokenId(name, customId)
83
+ }
48
84
 
49
85
  static create<T extends ClassType>(
50
86
  name: T,
@@ -60,9 +96,9 @@ export class InjectionToken<
60
96
  name: string | any,
61
97
  schema: Schema,
62
98
  ): InjectionToken<T, Schema>
63
- static create(name: string | symbol, schema?: unknown) {
99
+ static create(name: string | symbol, schema?: unknown, customId?: string) {
64
100
  // @ts-expect-error
65
- return new InjectionToken(name, schema)
101
+ return new InjectionToken(name, schema, customId)
66
102
  }
67
103
 
68
104
  static bound<T, S extends InjectionTokenSchemaType>(
@@ -161,3 +197,4 @@ export type InjectionTokenType =
161
197
  | InjectionToken<any, any>
162
198
  | BoundInjectionToken<any, any>
163
199
  | FactoryInjectionToken<any, any>
200
+