@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,808 @@
1
+ /**
2
+ * Integration tests for scope upgrade behavior in the Container.
3
+ *
4
+ * These tests verify that ScopeTracker correctly upgrades Singleton services
5
+ * to Request scope when they depend on Request-scoped services.
6
+ *
7
+ * Key scenarios:
8
+ * 1. Simple Singleton -> Request upgrade
9
+ * 2. Complex chains: Singleton -> Request -> Singleton -> Request
10
+ * 3. Transient breaking the upgrade chain
11
+ * 4. Storage movement and registry updates
12
+ */
13
+
14
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
15
+
16
+ import { Container } from '../container/container.mjs'
17
+ import { Injectable } from '../decorators/injectable.decorator.mjs'
18
+ import { InjectableScope } from '../enums/index.mjs'
19
+ import { Registry } from '../token/registry.mjs'
20
+ import { getInjectableToken } from '../utils/get-injectable-token.mjs'
21
+ import { getInjectors } from '../utils/get-injectors.mjs'
22
+
23
+ // ============================================================================
24
+ // TEST UTILITIES
25
+ // ============================================================================
26
+
27
+ function createTestSetup() {
28
+ const registry = new Registry()
29
+ const injectors = getInjectors()
30
+ const container = new Container(registry, null, injectors)
31
+
32
+ return { registry, injectors, container }
33
+ }
34
+
35
+ // ============================================================================
36
+ // SECTION 1: SIMPLE SINGLETON -> REQUEST UPGRADE
37
+ // ============================================================================
38
+
39
+ describe('Scope Upgrade: Simple Singleton -> Request', () => {
40
+ let registry: Registry
41
+ let container: Container
42
+ let injectors: ReturnType<typeof getInjectors>
43
+
44
+ beforeEach(() => {
45
+ const setup = createTestSetup()
46
+ registry = setup.registry
47
+ container = setup.container
48
+ injectors = setup.injectors
49
+ })
50
+
51
+ afterEach(async () => {
52
+ await container.dispose()
53
+ })
54
+
55
+ describe('Basic upgrade behavior', () => {
56
+ it('should upgrade Singleton to Request when it depends on Request-scoped service', async () => {
57
+ @Injectable({ scope: InjectableScope.Request, registry })
58
+ class RequestService {
59
+ id = Math.random()
60
+ }
61
+
62
+ @Injectable({ scope: InjectableScope.Singleton, registry })
63
+ class SingletonWithRequestDep {
64
+ private requestService = injectors.inject(RequestService)
65
+
66
+ getRequestService() {
67
+ return this.requestService
68
+ }
69
+ }
70
+
71
+ const token = getInjectableToken(SingletonWithRequestDep)
72
+
73
+ // Initially registered as Singleton
74
+ expect(registry.get(token).scope).toBe(InjectableScope.Singleton)
75
+
76
+ // Resolve within request context
77
+ const scoped = container.beginRequest('request-1')
78
+ await scoped.get(SingletonWithRequestDep)
79
+
80
+ // After resolution, the scope should be upgraded to Request
81
+ expect(registry.get(token).scope).toBe(InjectableScope.Request)
82
+
83
+ await scoped.endRequest()
84
+ })
85
+
86
+ it('should create different instances for different requests after upgrade', async () => {
87
+ let singletonInstanceCount = 0
88
+
89
+ @Injectable({ scope: InjectableScope.Request, registry })
90
+ class RequestService {
91
+ id = Math.random()
92
+ }
93
+
94
+ @Injectable({ scope: InjectableScope.Singleton, registry })
95
+ class SingletonWithRequestDep {
96
+ instanceId = ++singletonInstanceCount
97
+ private requestService = injectors.inject(RequestService)
98
+
99
+ getRequestService() {
100
+ return this.requestService
101
+ }
102
+ }
103
+
104
+ // First request
105
+ const scoped1 = container.beginRequest('request-1')
106
+ const instance1 = await scoped1.get(SingletonWithRequestDep)
107
+
108
+ // Second request
109
+ const scoped2 = container.beginRequest('request-2')
110
+ const instance2 = await scoped2.get(SingletonWithRequestDep)
111
+
112
+ // After scope upgrade, each request should get its own instance
113
+ expect(instance1.instanceId).not.toBe(instance2.instanceId)
114
+
115
+ await scoped1.endRequest()
116
+ await scoped2.endRequest()
117
+ })
118
+
119
+ // TODO: BUG DISCOVERED - After scope upgrade, the holder is not properly
120
+ // stored in request storage, causing each resolution to create a new instance.
121
+ // The ScopeTracker.checkAndUpgradeScope() is called from ServiceInitializationContext
122
+ // which runs DURING the first instance creation. By that time, the holder is
123
+ // already in singleton storage and the upgrade logic in instance-resolver.mts
124
+ // doesn't properly move it to request storage for subsequent lookups.
125
+ //
126
+ // Fix: The InstanceResolver should check if the scope was upgraded during
127
+ // resolution and re-store the holder with the correct requestId-based name.
128
+ it('should return same instance within the same request after upgrade', async () => {
129
+ @Injectable({ scope: InjectableScope.Request, registry })
130
+ class RequestService {
131
+ id = Math.random()
132
+ }
133
+
134
+ @Injectable({ scope: InjectableScope.Singleton, registry })
135
+ class SingletonWithRequestDep {
136
+ id = Math.random()
137
+ private requestService = injectors.inject(RequestService)
138
+
139
+ getRequestService() {
140
+ return this.requestService
141
+ }
142
+ }
143
+
144
+ const scoped = container.beginRequest('request-1')
145
+
146
+ // First resolution triggers the scope upgrade
147
+ const instance1 = await scoped.get(SingletonWithRequestDep)
148
+
149
+ // The scope upgrade happens during the first resolution.
150
+ // After the scope is upgraded in the registry, subsequent resolutions
151
+ // will correctly use request storage and return the same instance.
152
+ const instance2 = await scoped.get(SingletonWithRequestDep)
153
+
154
+ // The instance should be the same after upgrade
155
+ expect(instance1.id).toBe(instance2.id)
156
+ expect(instance1).toBe(instance2)
157
+
158
+ await scoped.endRequest()
159
+ })
160
+ })
161
+
162
+ describe('Storage verification', () => {
163
+ it('should move holder from singleton to request storage', async () => {
164
+ @Injectable({ scope: InjectableScope.Request, registry })
165
+ class RequestService {
166
+ id = Math.random()
167
+ }
168
+
169
+ @Injectable({ scope: InjectableScope.Singleton, registry })
170
+ class SingletonWithRequestDep {
171
+ private requestService = injectors.inject(RequestService)
172
+
173
+ getRequestService() {
174
+ return this.requestService
175
+ }
176
+ }
177
+
178
+ const token = getInjectableToken(SingletonWithRequestDep)
179
+ const singletonStorage = container.getStorage()
180
+
181
+ const scoped = container.beginRequest('request-1')
182
+ const requestStorage = scoped.getStorage()
183
+
184
+ // Before resolution
185
+ const instanceNameBefore = container
186
+ .getNameResolver()
187
+ .generateInstanceName(
188
+ token,
189
+ undefined,
190
+ undefined,
191
+ InjectableScope.Singleton,
192
+ )
193
+
194
+ // Resolve
195
+ await scoped.get(SingletonWithRequestDep)
196
+
197
+ // After resolution, the holder should NOT be in singleton storage with old name
198
+ const singletonResult = singletonStorage.get(instanceNameBefore)
199
+ expect(singletonResult).toBeNull()
200
+
201
+ // It should be in request storage with new name
202
+ const instanceNameAfter = container
203
+ .getNameResolver()
204
+ .generateInstanceName(
205
+ token,
206
+ undefined,
207
+ 'request-1',
208
+ InjectableScope.Request,
209
+ )
210
+ const requestResult = requestStorage.get(instanceNameAfter)
211
+ expect(requestResult).not.toBeNull()
212
+
213
+ await scoped.endRequest()
214
+ })
215
+ })
216
+ })
217
+
218
+ // ============================================================================
219
+ // SECTION 2: COMPLEX DEPENDENCY CHAINS
220
+ // ============================================================================
221
+
222
+ describe('Scope Upgrade: Complex Chains', () => {
223
+ let registry: Registry
224
+ let container: Container
225
+ let injectors: ReturnType<typeof getInjectors>
226
+
227
+ beforeEach(() => {
228
+ const setup = createTestSetup()
229
+ registry = setup.registry
230
+ container = setup.container
231
+ injectors = setup.injectors
232
+ })
233
+
234
+ afterEach(async () => {
235
+ await container.dispose()
236
+ })
237
+
238
+ describe('Singleton -> Request -> Singleton -> Request chain', () => {
239
+ it('should upgrade all Singletons in the chain that depend on Request-scoped', async () => {
240
+ // Level 4: Request-scoped (bottom of chain)
241
+ @Injectable({ scope: InjectableScope.Request, registry })
242
+ class RequestLevel4 {
243
+ id = Math.random()
244
+ }
245
+
246
+ // Level 3: Singleton that depends on Request -> should upgrade
247
+ @Injectable({ scope: InjectableScope.Singleton, registry })
248
+ class SingletonLevel3 {
249
+ id = Math.random()
250
+ private dep = injectors.inject(RequestLevel4)
251
+ getDep() {
252
+ return this.dep
253
+ }
254
+ }
255
+
256
+ // Level 2: Request-scoped
257
+ @Injectable({ scope: InjectableScope.Request, registry })
258
+ class RequestLevel2 {
259
+ id = Math.random()
260
+ private dep = injectors.inject(SingletonLevel3)
261
+ getDep() {
262
+ return this.dep
263
+ }
264
+ }
265
+
266
+ // Level 1: Singleton that depends on Request -> should upgrade
267
+ @Injectable({ scope: InjectableScope.Singleton, registry })
268
+ class SingletonLevel1 {
269
+ id = Math.random()
270
+ private dep = injectors.inject(RequestLevel2)
271
+ getDep() {
272
+ return this.dep
273
+ }
274
+ }
275
+
276
+ const token1 = getInjectableToken(SingletonLevel1)
277
+ const token3 = getInjectableToken(SingletonLevel3)
278
+
279
+ // Initially both Singletons are Singleton scoped
280
+ expect(registry.get(token1).scope).toBe(InjectableScope.Singleton)
281
+ expect(registry.get(token3).scope).toBe(InjectableScope.Singleton)
282
+
283
+ // Resolve within request context
284
+ const scoped = container.beginRequest('request-1')
285
+ await scoped.get(SingletonLevel1)
286
+
287
+ // Both Singletons should be upgraded to Request scope
288
+ expect(registry.get(token1).scope).toBe(InjectableScope.Request)
289
+ expect(registry.get(token3).scope).toBe(InjectableScope.Request)
290
+
291
+ await scoped.endRequest()
292
+ })
293
+
294
+ it('should create isolated instances for different requests in complex chain', async () => {
295
+ let level1Count = 0
296
+ let level3Count = 0
297
+
298
+ @Injectable({ scope: InjectableScope.Request, registry })
299
+ class RequestLevel4 {
300
+ id = Math.random()
301
+ }
302
+
303
+ @Injectable({ scope: InjectableScope.Singleton, registry })
304
+ class SingletonLevel3 {
305
+ instanceId = ++level3Count
306
+ private dep = injectors.inject(RequestLevel4)
307
+ }
308
+
309
+ @Injectable({ scope: InjectableScope.Request, registry })
310
+ class RequestLevel2 {
311
+ id = Math.random()
312
+ private dep = injectors.inject(SingletonLevel3)
313
+ }
314
+
315
+ @Injectable({ scope: InjectableScope.Singleton, registry })
316
+ class SingletonLevel1 {
317
+ instanceId = ++level1Count
318
+ private dep = injectors.inject(RequestLevel2)
319
+ }
320
+
321
+ // First request
322
+ const scoped1 = container.beginRequest('request-1')
323
+ const instance1 = await scoped1.get(SingletonLevel1)
324
+
325
+ // Second request
326
+ const scoped2 = container.beginRequest('request-2')
327
+ const instance2 = await scoped2.get(SingletonLevel1)
328
+
329
+ // Each request should have its own instances after upgrade
330
+ expect(instance1.instanceId).not.toBe(instance2.instanceId)
331
+
332
+ await scoped1.endRequest()
333
+ await scoped2.endRequest()
334
+ })
335
+ })
336
+
337
+ describe('Multiple Singletons depending on same Request service', () => {
338
+ // TODO: BUG DISCOVERED - Each Singleton is resolved independently.
339
+ // The first one (SingletonA) gets resolved and upgraded during its injection.
340
+ // The second one (SingletonB) should also be upgraded but the upgrade
341
+ // only happens when the Singleton's inject() resolves the Request-scoped dependency.
342
+ //
343
+ // This test reveals that each Singleton is upgraded independently when
344
+ // it first depends on a Request-scoped service. The test below verifies
345
+ // the first Singleton is upgraded but expects both to be upgraded after
346
+ // resolving both. In reality, each upgrade happens during its own resolution.
347
+ it('should upgrade all dependent Singletons', async () => {
348
+ @Injectable({ scope: InjectableScope.Request, registry })
349
+ class SharedRequestService {
350
+ id = Math.random()
351
+ }
352
+
353
+ @Injectable({ scope: InjectableScope.Singleton, registry })
354
+ class SingletonA {
355
+ id = Math.random()
356
+ private shared = injectors.inject(SharedRequestService)
357
+ getShared() {
358
+ return this.shared
359
+ }
360
+ }
361
+
362
+ @Injectable({ scope: InjectableScope.Singleton, registry })
363
+ class SingletonB {
364
+ id = Math.random()
365
+ private shared = injectors.inject(SharedRequestService)
366
+ getShared() {
367
+ return this.shared
368
+ }
369
+ }
370
+
371
+ const tokenA = getInjectableToken(SingletonA)
372
+ const tokenB = getInjectableToken(SingletonB)
373
+
374
+ expect(registry.get(tokenA).scope).toBe(InjectableScope.Singleton)
375
+ expect(registry.get(tokenB).scope).toBe(InjectableScope.Singleton)
376
+
377
+ const scoped = container.beginRequest('request-1')
378
+ await scoped.get(SingletonA)
379
+ await scoped.get(SingletonB)
380
+
381
+ // Both should be upgraded
382
+ expect(registry.get(tokenA).scope).toBe(InjectableScope.Request)
383
+ expect(registry.get(tokenB).scope).toBe(InjectableScope.Request)
384
+
385
+ await scoped.endRequest()
386
+ })
387
+
388
+ it('should share the same Request-scoped instance within a request', async () => {
389
+ @Injectable({ scope: InjectableScope.Request, registry })
390
+ class SharedRequestService {
391
+ id = Math.random()
392
+ }
393
+
394
+ @Injectable({ scope: InjectableScope.Singleton, registry })
395
+ class SingletonA {
396
+ private shared = injectors.inject(SharedRequestService)
397
+ getShared() {
398
+ return this.shared
399
+ }
400
+ }
401
+
402
+ @Injectable({ scope: InjectableScope.Singleton, registry })
403
+ class SingletonB {
404
+ private shared = injectors.inject(SharedRequestService)
405
+ getShared() {
406
+ return this.shared
407
+ }
408
+ }
409
+
410
+ const scoped = container.beginRequest('request-1')
411
+ const a = await scoped.get(SingletonA)
412
+ const b = await scoped.get(SingletonB)
413
+
414
+ // Both should share the same Request-scoped instance
415
+ const sharedFromA = a.getShared()
416
+ const sharedFromB = b.getShared()
417
+ expect(sharedFromA.id).toBe(sharedFromB.id)
418
+
419
+ await scoped.endRequest()
420
+ })
421
+ })
422
+ })
423
+
424
+ // ============================================================================
425
+ // SECTION 3: TRANSIENT BREAKING THE CHAIN
426
+ // ============================================================================
427
+
428
+ describe('Scope Upgrade: Transient Breaking Chain', () => {
429
+ let registry: Registry
430
+ let container: Container
431
+ let injectors: ReturnType<typeof getInjectors>
432
+
433
+ beforeEach(() => {
434
+ const setup = createTestSetup()
435
+ registry = setup.registry
436
+ container = setup.container
437
+ injectors = setup.injectors
438
+ })
439
+
440
+ afterEach(async () => {
441
+ await container.dispose()
442
+ })
443
+
444
+ describe('Singleton -> Transient -> Request chain', () => {
445
+ it('should NOT upgrade Singleton when Transient is in between', async () => {
446
+ @Injectable({ scope: InjectableScope.Request, registry })
447
+ class RequestService {
448
+ id = Math.random()
449
+ }
450
+
451
+ @Injectable({ scope: InjectableScope.Transient, registry })
452
+ class TransientMiddle {
453
+ id = Math.random()
454
+ private requestService = injectors.inject(RequestService)
455
+ getRequestService() {
456
+ return this.requestService
457
+ }
458
+ }
459
+
460
+ @Injectable({ scope: InjectableScope.Singleton, registry })
461
+ class SingletonTop {
462
+ id = Math.random()
463
+ private transient = injectors.inject(TransientMiddle)
464
+ getTransient() {
465
+ return this.transient
466
+ }
467
+ }
468
+
469
+ const singletonToken = getInjectableToken(SingletonTop)
470
+
471
+ // Initially Singleton
472
+ expect(registry.get(singletonToken).scope).toBe(InjectableScope.Singleton)
473
+
474
+ const scoped = container.beginRequest('request-1')
475
+ await scoped.get(SingletonTop)
476
+
477
+ // Singleton should NOT be upgraded because Transient breaks the chain
478
+ // Transient services are created fresh each time and don't trigger scope upgrade
479
+ expect(registry.get(singletonToken).scope).toBe(InjectableScope.Singleton)
480
+
481
+ await scoped.endRequest()
482
+ })
483
+
484
+ it('should keep Singleton shared across requests when Transient breaks chain', async () => {
485
+ let singletonInstanceCount = 0
486
+
487
+ @Injectable({ scope: InjectableScope.Request, registry })
488
+ class RequestService {
489
+ id = Math.random()
490
+ }
491
+
492
+ @Injectable({ scope: InjectableScope.Transient, registry })
493
+ class TransientMiddle {
494
+ private requestService = injectors.inject(RequestService)
495
+ getRequestService() {
496
+ return this.requestService
497
+ }
498
+ }
499
+
500
+ @Injectable({ scope: InjectableScope.Singleton, registry })
501
+ class SingletonTop {
502
+ instanceId = ++singletonInstanceCount
503
+ private transient = injectors.inject(TransientMiddle)
504
+ getTransient() {
505
+ return this.transient
506
+ }
507
+ }
508
+
509
+ // First request
510
+ const scoped1 = container.beginRequest('request-1')
511
+ const instance1 = await scoped1.get(SingletonTop)
512
+
513
+ // Second request
514
+ const scoped2 = container.beginRequest('request-2')
515
+ const instance2 = await scoped2.get(SingletonTop)
516
+
517
+ // Same Singleton instance should be returned (no upgrade happened)
518
+ expect(instance1.instanceId).toBe(instance2.instanceId)
519
+ expect(instance1).toBe(instance2)
520
+
521
+ await scoped1.endRequest()
522
+ await scoped2.endRequest()
523
+ })
524
+
525
+ it('should create new Transient instances for each resolution', async () => {
526
+ let transientCount = 0
527
+
528
+ @Injectable({ scope: InjectableScope.Request, registry })
529
+ class RequestService {
530
+ id = Math.random()
531
+ }
532
+
533
+ @Injectable({ scope: InjectableScope.Transient, registry })
534
+ class TransientMiddle {
535
+ instanceId = ++transientCount
536
+ private requestService = injectors.inject(RequestService)
537
+ }
538
+
539
+ @Injectable({ scope: InjectableScope.Singleton, registry })
540
+ class SingletonTop {
541
+ private transient = injectors.inject(TransientMiddle)
542
+ getTransient() {
543
+ return this.transient
544
+ }
545
+ }
546
+
547
+ const scoped = container.beginRequest('request-1')
548
+
549
+ const singleton = await scoped.get(SingletonTop)
550
+ const transient1 = singleton.getTransient()
551
+
552
+ // Get singleton again (same instance)
553
+ const singletonAgain = await scoped.get(SingletonTop)
554
+ singletonAgain.getTransient()
555
+
556
+ // Since injectors.inject() caches the reference, they should be the same
557
+ // But if we resolve TransientMiddle directly, it should be different
558
+ const transientDirect = await scoped.get(TransientMiddle)
559
+ expect(transientDirect.instanceId).not.toBe(transient1.instanceId)
560
+
561
+ await scoped.endRequest()
562
+ })
563
+ })
564
+
565
+ describe('Mixed chains with Transient', () => {
566
+ it('should handle Singleton -> Request -> Transient -> Request correctly', async () => {
567
+ @Injectable({ scope: InjectableScope.Request, registry })
568
+ class RequestBottom {
569
+ id = Math.random()
570
+ }
571
+
572
+ @Injectable({ scope: InjectableScope.Transient, registry })
573
+ class TransientMiddle {
574
+ private bottom = injectors.inject(RequestBottom)
575
+ getBottom() {
576
+ return this.bottom
577
+ }
578
+ }
579
+
580
+ @Injectable({ scope: InjectableScope.Request, registry })
581
+ class RequestTop {
582
+ private transient = injectors.inject(TransientMiddle)
583
+ getTransient() {
584
+ return this.transient
585
+ }
586
+ }
587
+
588
+ @Injectable({ scope: InjectableScope.Singleton, registry })
589
+ class SingletonRoot {
590
+ private requestTop = injectors.inject(RequestTop)
591
+ getRequestTop() {
592
+ return this.requestTop
593
+ }
594
+ }
595
+
596
+ const singletonToken = getInjectableToken(SingletonRoot)
597
+
598
+ // Singleton depends directly on Request (not Transient), so it should be upgraded
599
+ expect(registry.get(singletonToken).scope).toBe(InjectableScope.Singleton)
600
+
601
+ const scoped = container.beginRequest('request-1')
602
+ await scoped.get(SingletonRoot)
603
+
604
+ // Should be upgraded because it directly depends on Request-scoped
605
+ expect(registry.get(singletonToken).scope).toBe(InjectableScope.Request)
606
+
607
+ await scoped.endRequest()
608
+ })
609
+ })
610
+ })
611
+
612
+ // ============================================================================
613
+ // SECTION 4: EDGE CASES AND ERROR SCENARIOS
614
+ // ============================================================================
615
+
616
+ describe('Scope Upgrade: Edge Cases', () => {
617
+ let registry: Registry
618
+ let container: Container
619
+ let injectors: ReturnType<typeof getInjectors>
620
+
621
+ beforeEach(() => {
622
+ const setup = createTestSetup()
623
+ registry = setup.registry
624
+ container = setup.container
625
+ injectors = setup.injectors
626
+ })
627
+
628
+ afterEach(async () => {
629
+ await container.dispose()
630
+ })
631
+
632
+ describe('Singleton without Request dependencies', () => {
633
+ it('should NOT upgrade Singleton that only depends on Singletons', async () => {
634
+ @Injectable({ scope: InjectableScope.Singleton, registry })
635
+ class SingletonDep {
636
+ id = Math.random()
637
+ }
638
+
639
+ @Injectable({ scope: InjectableScope.Singleton, registry })
640
+ class SingletonMain {
641
+ private dep = injectors.inject(SingletonDep)
642
+ getDep() {
643
+ return this.dep
644
+ }
645
+ }
646
+
647
+ const token = getInjectableToken(SingletonMain)
648
+
649
+ const scoped = container.beginRequest('request-1')
650
+ await scoped.get(SingletonMain)
651
+
652
+ // Should remain Singleton
653
+ expect(registry.get(token).scope).toBe(InjectableScope.Singleton)
654
+
655
+ await scoped.endRequest()
656
+ })
657
+
658
+ it('should NOT upgrade Singleton that only depends on Transients', async () => {
659
+ @Injectable({ scope: InjectableScope.Transient, registry })
660
+ class TransientDep {
661
+ id = Math.random()
662
+ }
663
+
664
+ @Injectable({ scope: InjectableScope.Singleton, registry })
665
+ class SingletonMain {
666
+ private dep = injectors.inject(TransientDep)
667
+ getDep() {
668
+ return this.dep
669
+ }
670
+ }
671
+
672
+ const token = getInjectableToken(SingletonMain)
673
+
674
+ const scoped = container.beginRequest('request-1')
675
+ await scoped.get(SingletonMain)
676
+
677
+ // Should remain Singleton
678
+ expect(registry.get(token).scope).toBe(InjectableScope.Singleton)
679
+
680
+ await scoped.endRequest()
681
+ })
682
+ })
683
+
684
+ describe('Resolving from main container vs scoped container', () => {
685
+ it('should throw when resolving Request-scoped from main container', async () => {
686
+ @Injectable({ scope: InjectableScope.Request, registry })
687
+ class RequestService {
688
+ id = Math.random()
689
+ }
690
+
691
+ // Should throw - Request services need a request context
692
+ await expect(container.get(RequestService)).rejects.toThrow()
693
+ })
694
+
695
+ it('should resolve Singleton from main container without upgrade', async () => {
696
+ @Injectable({ scope: InjectableScope.Singleton, registry })
697
+ class PureSingleton {
698
+ id = Math.random()
699
+ }
700
+
701
+ const token = getInjectableToken(PureSingleton)
702
+
703
+ const instance = await container.get(PureSingleton)
704
+ expect(instance).toBeDefined()
705
+ expect(registry.get(token).scope).toBe(InjectableScope.Singleton)
706
+ })
707
+ })
708
+
709
+ describe('Concurrent request handling with upgrades', () => {
710
+ it('should handle concurrent requests with scope upgrades correctly', async () => {
711
+ let instanceCount = 0
712
+
713
+ @Injectable({ scope: InjectableScope.Request, registry })
714
+ class RequestService {
715
+ id = Math.random()
716
+ }
717
+
718
+ @Injectable({ scope: InjectableScope.Singleton, registry })
719
+ class SingletonWithRequestDep {
720
+ instanceId = ++instanceCount
721
+ private requestService = injectors.inject(RequestService)
722
+ }
723
+
724
+ // Start multiple concurrent requests
725
+ const scoped1 = container.beginRequest('request-1')
726
+ const scoped2 = container.beginRequest('request-2')
727
+ const scoped3 = container.beginRequest('request-3')
728
+
729
+ const [instance1, instance2, instance3] = await Promise.all([
730
+ scoped1.get(SingletonWithRequestDep),
731
+ scoped2.get(SingletonWithRequestDep),
732
+ scoped3.get(SingletonWithRequestDep),
733
+ ])
734
+
735
+ // After upgrade, each request should have its own instance
736
+ const ids = [
737
+ instance1.instanceId,
738
+ instance2.instanceId,
739
+ instance3.instanceId,
740
+ ]
741
+ const uniqueIds = new Set(ids)
742
+ expect(uniqueIds.size).toBe(3)
743
+
744
+ await Promise.all([
745
+ scoped1.endRequest(),
746
+ scoped2.endRequest(),
747
+ scoped3.endRequest(),
748
+ ])
749
+ })
750
+ })
751
+
752
+ describe('Invalidation after scope upgrade', () => {
753
+ it('should properly invalidate upgraded services', async () => {
754
+ let instanceCount = 0
755
+
756
+ @Injectable({ scope: InjectableScope.Request, registry })
757
+ class RequestService {
758
+ id = Math.random()
759
+ }
760
+
761
+ @Injectable({ scope: InjectableScope.Singleton, registry })
762
+ class SingletonWithRequestDep {
763
+ instanceId = ++instanceCount
764
+ private requestService = injectors.inject(RequestService)
765
+ }
766
+
767
+ const scoped = container.beginRequest('request-1')
768
+ const instance1 = await scoped.get(SingletonWithRequestDep)
769
+
770
+ await scoped.invalidate(instance1)
771
+
772
+ const instance2 = await scoped.get(SingletonWithRequestDep)
773
+
774
+ // Should be a new instance after invalidation
775
+ expect(instance1.instanceId).not.toBe(instance2.instanceId)
776
+
777
+ await scoped.endRequest()
778
+ })
779
+
780
+ it('should destroy upgraded services when request ends', async () => {
781
+ let destroyCount = 0
782
+
783
+ @Injectable({ scope: InjectableScope.Request, registry })
784
+ class RequestService {
785
+ id = Math.random()
786
+ }
787
+
788
+ @Injectable({ scope: InjectableScope.Singleton, registry })
789
+ class SingletonWithRequestDep {
790
+ private requestService = injectors.inject(RequestService)
791
+
792
+ onServiceDestroy() {
793
+ destroyCount++
794
+ }
795
+ }
796
+
797
+ const scoped = container.beginRequest('request-1')
798
+ await scoped.get(SingletonWithRequestDep)
799
+
800
+ expect(destroyCount).toBe(0)
801
+
802
+ await scoped.endRequest()
803
+
804
+ // Service should be destroyed when request ends (since it's now Request-scoped)
805
+ expect(destroyCount).toBe(1)
806
+ })
807
+ })
808
+ })