@navios/di 0.9.2 → 1.0.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 (174) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +28 -0
  3. package/coverage/block-navigation.js +1 -1
  4. package/coverage/clover.xml +1463 -3174
  5. package/coverage/coverage-final.json +56 -54
  6. package/coverage/index.html +179 -104
  7. package/coverage/sorter.js +21 -7
  8. package/coverage/src/{request-context-manager.mts.html → __tests__/gc/gc-test-utils.mts.html} +192 -231
  9. package/coverage/{lib → src/__tests__/gc}/index.html +25 -40
  10. package/coverage/src/container/abstract-container.mts.html +1066 -0
  11. package/coverage/src/container/container.mts.html +958 -0
  12. package/coverage/src/container/index.html +161 -0
  13. package/coverage/src/{testing → container}/index.mts.html +12 -9
  14. package/coverage/src/container/scoped-container.mts.html +907 -0
  15. package/coverage/src/decorators/factory.decorator.mts.html +68 -53
  16. package/coverage/src/decorators/index.html +34 -34
  17. package/coverage/src/decorators/index.mts.html +10 -10
  18. package/coverage/src/decorators/injectable.decorator.mts.html +144 -60
  19. package/coverage/src/enums/index.html +21 -21
  20. package/coverage/src/enums/index.mts.html +12 -12
  21. package/coverage/src/enums/injectable-scope.enum.mts.html +11 -8
  22. package/coverage/src/enums/injectable-type.enum.mts.html +10 -7
  23. package/coverage/src/errors/di-error.mts.html +380 -71
  24. package/coverage/src/errors/index.html +22 -127
  25. package/coverage/src/errors/index.mts.html +9 -33
  26. package/coverage/src/event-emitter.mts.html +35 -107
  27. package/coverage/src/index.html +14 -254
  28. package/coverage/src/index.mts.html +23 -50
  29. package/coverage/src/interfaces/container.interface.mts.html +370 -0
  30. package/coverage/src/interfaces/factory.interface.mts.html +12 -12
  31. package/coverage/src/interfaces/index.html +45 -30
  32. package/coverage/src/interfaces/index.mts.html +16 -13
  33. package/coverage/src/interfaces/on-service-destroy.interface.mts.html +1 -1
  34. package/coverage/src/interfaces/on-service-init.interface.mts.html +1 -1
  35. package/coverage/src/internal/context/async-local-storage.browser.mts.html +142 -0
  36. package/coverage/src/internal/context/async-local-storage.mts.html +292 -0
  37. package/coverage/src/{factory-context.mts.html → internal/context/async-local-storage.types.mts.html} +17 -17
  38. package/coverage/src/internal/context/factory-context.mts.html +142 -0
  39. package/coverage/src/internal/context/index.html +221 -0
  40. package/coverage/src/internal/context/index.mts.html +100 -0
  41. package/coverage/src/{service-locator-instance-holder.mts.html → internal/context/resolution-context.mts.html} +114 -78
  42. package/coverage/src/internal/context/service-initialization-context.mts.html +214 -0
  43. package/coverage/src/internal/context/sync-local-storage.mts.html +244 -0
  44. package/coverage/src/internal/core/index.html +206 -0
  45. package/coverage/src/internal/core/index.mts.html +103 -0
  46. package/coverage/src/internal/core/instance-resolver.mts.html +2533 -0
  47. package/coverage/src/{request-context-holder.mts.html → internal/core/name-resolver.mts.html} +245 -260
  48. package/coverage/src/{container.mts.html → internal/core/scope-tracker.mts.html} +352 -310
  49. package/coverage/src/{service-instantiator.mts.html → internal/core/service-initializer.mts.html} +254 -176
  50. package/coverage/src/internal/core/service-invalidator.mts.html +955 -0
  51. package/coverage/src/internal/core/token-resolver.mts.html +451 -0
  52. package/coverage/src/internal/holder/holder-storage.interface.mts.html +451 -0
  53. package/coverage/src/internal/holder/index.html +161 -0
  54. package/coverage/src/internal/holder/index.mts.html +94 -0
  55. package/coverage/src/internal/holder/instance-holder.mts.html +406 -0
  56. package/coverage/src/{service-locator.mts.html → internal/holder/unified-storage.mts.html} +376 -379
  57. package/coverage/{lib/testing → src/internal}/index.html +26 -11
  58. package/coverage/src/internal/index.mts.html +100 -0
  59. package/coverage/src/internal/lifecycle/circular-detector.mts.html +364 -0
  60. package/coverage/src/internal/lifecycle/index.html +146 -0
  61. package/coverage/src/internal/lifecycle/index.mts.html +91 -0
  62. package/coverage/src/{service-locator-event-bus.mts.html → internal/lifecycle/lifecycle-event-bus.mts.html} +104 -80
  63. package/coverage/src/internal/stub-factory-class.mts.html +133 -0
  64. package/coverage/src/symbols/index.html +14 -14
  65. package/coverage/src/symbols/index.mts.html +9 -9
  66. package/coverage/src/symbols/injectable-token.mts.html +9 -3
  67. package/coverage/src/testing/index.html +32 -32
  68. package/coverage/src/testing/test-container.mts.html +1576 -139
  69. package/coverage/src/testing/unit-test-container.mts.html +1888 -0
  70. package/coverage/src/token/index.html +146 -0
  71. package/coverage/{lib/testing/index.d.mts.html → src/token/index.mts.html} +11 -11
  72. package/coverage/src/{injection-token.mts.html → token/injection-token.mts.html} +225 -111
  73. package/coverage/src/{base-instance-holder-manager.mts.html → token/registry.mts.html} +188 -269
  74. package/coverage/src/{injector.mts.html → utils/default-injectors.mts.html} +31 -31
  75. package/coverage/src/utils/get-injectable-token.mts.html +19 -22
  76. package/coverage/src/utils/get-injectors.mts.html +684 -177
  77. package/coverage/src/utils/index.html +36 -36
  78. package/coverage/src/utils/index.mts.html +18 -12
  79. package/coverage/src/utils/types.mts.html +26 -11
  80. package/docs/examples/basic-usage.mts +1 -1
  81. package/docs/examples/factory-pattern.mts +3 -3
  82. package/docs/examples/request-scope-example.mts +1 -1
  83. package/docs/examples/service-lifecycle.mts +1 -1
  84. package/lib/browser/internal/core/instance-resolver.d.mts.map +1 -1
  85. package/lib/browser/internal/core/instance-resolver.mjs.map +1 -1
  86. package/lib/{container-D-0Ho3qL.d.cts → container-D3j3KuD9.d.mts} +5 -289
  87. package/lib/container-D3j3KuD9.d.mts.map +1 -0
  88. package/lib/{container-Bi0huFQX.mjs → container-qgHMgGNG.mjs} +3 -227
  89. package/lib/container-qgHMgGNG.mjs.map +1 -0
  90. package/lib/{container-CNiqesCL.d.mts → container-r1KP4F-n.d.cts} +5 -289
  91. package/lib/container-r1KP4F-n.d.cts.map +1 -0
  92. package/lib/{container-pmGNCZL_.cjs → container-ycYJgTq7.cjs} +41 -319
  93. package/lib/container-ycYJgTq7.cjs.map +1 -0
  94. package/lib/factory.decorator-D4mem6YQ.cjs +21 -0
  95. package/lib/factory.decorator-D4mem6YQ.cjs.map +1 -0
  96. package/lib/factory.decorator-_IPWcwQn.mjs +16 -0
  97. package/lib/factory.decorator-_IPWcwQn.mjs.map +1 -0
  98. package/lib/index.cjs +14 -24
  99. package/lib/index.cjs.map +1 -1
  100. package/lib/index.d.cts +3 -52
  101. package/lib/index.d.cts.map +1 -1
  102. package/lib/index.d.mts +3 -52
  103. package/lib/index.d.mts.map +1 -1
  104. package/lib/index.mjs +3 -13
  105. package/lib/index.mjs.map +1 -1
  106. package/lib/injectable.decorator-BNfWpjr_.d.cts +56 -0
  107. package/lib/injectable.decorator-BNfWpjr_.d.cts.map +1 -0
  108. package/lib/injectable.decorator-Bc05hRQU.d.mts +56 -0
  109. package/lib/injectable.decorator-Bc05hRQU.d.mts.map +1 -0
  110. package/lib/injectable.decorator-CyPrBzBN.mjs +227 -0
  111. package/lib/injectable.decorator-CyPrBzBN.mjs.map +1 -0
  112. package/lib/injectable.decorator-DbpiDrg-.cjs +281 -0
  113. package/lib/injectable.decorator-DbpiDrg-.cjs.map +1 -0
  114. package/lib/legacy-compat/index.cjs +114 -0
  115. package/lib/legacy-compat/index.cjs.map +1 -0
  116. package/lib/legacy-compat/index.d.cts +63 -0
  117. package/lib/legacy-compat/index.d.cts.map +1 -0
  118. package/lib/legacy-compat/index.d.mts +63 -0
  119. package/lib/legacy-compat/index.d.mts.map +1 -0
  120. package/lib/legacy-compat/index.mjs +111 -0
  121. package/lib/legacy-compat/index.mjs.map +1 -0
  122. package/lib/registry-DKbKWFvJ.d.cts +290 -0
  123. package/lib/registry-DKbKWFvJ.d.cts.map +1 -0
  124. package/lib/registry-n8JhJoxm.d.mts +290 -0
  125. package/lib/registry-n8JhJoxm.d.mts.map +1 -0
  126. package/lib/testing/index.cjs +23 -22
  127. package/lib/testing/index.cjs.map +1 -1
  128. package/lib/testing/index.d.cts +2 -1
  129. package/lib/testing/index.d.cts.map +1 -1
  130. package/lib/testing/index.d.mts +2 -1
  131. package/lib/testing/index.d.mts.map +1 -1
  132. package/lib/testing/index.mjs +2 -1
  133. package/lib/testing/index.mjs.map +1 -1
  134. package/package.json +11 -1
  135. package/project.json +8 -0
  136. package/src/__tests__/gc/basic-container.spec.mts +358 -0
  137. package/src/__tests__/gc/circular-dependencies.spec.mts +501 -0
  138. package/src/__tests__/gc/gc-test-utils.mts +136 -0
  139. package/src/__tests__/gc/memory-pressure.spec.mts +542 -0
  140. package/src/__tests__/gc/scoped-container.spec.mts +444 -0
  141. package/src/__tests__/gc/transient-services.spec.mts +326 -0
  142. package/src/__tests__/scope-upgrade.spec.mts +0 -18
  143. package/src/internal/core/instance-resolver.mts +0 -1
  144. package/src/legacy-compat/context-compat.mts +95 -0
  145. package/src/legacy-compat/factory.decorator.mts +37 -0
  146. package/src/legacy-compat/index.mts +16 -0
  147. package/src/legacy-compat/injectable.decorator.mts +41 -0
  148. package/tsdown.config.mts +1 -1
  149. package/vitest.config.mts +3 -7
  150. package/coverage/docs/examples/basic-usage.mts.html +0 -376
  151. package/coverage/docs/examples/factory-pattern.mts.html +0 -1039
  152. package/coverage/docs/examples/index.html +0 -176
  153. package/coverage/docs/examples/injection-tokens.mts.html +0 -760
  154. package/coverage/docs/examples/request-scope-example.mts.html +0 -847
  155. package/coverage/docs/examples/service-lifecycle.mts.html +0 -1162
  156. package/coverage/lib/_tsup-dts-rollup.d.mts.html +0 -3445
  157. package/coverage/lib/index.d.mts.html +0 -313
  158. package/coverage/src/errors/errors.enum.mts.html +0 -118
  159. package/coverage/src/errors/factory-not-found.mts.html +0 -118
  160. package/coverage/src/errors/factory-token-not-resolved.mts.html +0 -118
  161. package/coverage/src/errors/instance-destroying.mts.html +0 -118
  162. package/coverage/src/errors/instance-expired.mts.html +0 -118
  163. package/coverage/src/errors/instance-not-found.mts.html +0 -118
  164. package/coverage/src/errors/unknown-error.mts.html +0 -118
  165. package/coverage/src/instance-resolver.mts.html +0 -1762
  166. package/coverage/src/registry.mts.html +0 -247
  167. package/coverage/src/service-invalidator.mts.html +0 -1372
  168. package/coverage/src/service-locator-manager.mts.html +0 -340
  169. package/coverage/src/token-processor.mts.html +0 -607
  170. package/coverage/src/utils/defer.mts.html +0 -118
  171. package/lib/container-Bi0huFQX.mjs.map +0 -1
  172. package/lib/container-CNiqesCL.d.mts.map +0 -1
  173. package/lib/container-D-0Ho3qL.d.cts.map +0 -1
  174. package/lib/container-pmGNCZL_.cjs.map +0 -1
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Garbage Collection Tests: Transient Services
3
+ *
4
+ * Tests that transient services are properly garbage collected
5
+ * when no longer referenced, since they are not cached by the container.
6
+ *
7
+ * Run with: NODE_OPTIONS=--expose-gc yarn nx test @navios/di
8
+ */
9
+
10
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
11
+
12
+ import type { OnServiceDestroy } from '../../interfaces/on-service-destroy.interface.mjs'
13
+
14
+ import { Container } from '../../container/container.mjs'
15
+ import { Injectable } from '../../decorators/injectable.decorator.mjs'
16
+ import { InjectableScope } from '../../enums/injectable-scope.enum.mjs'
17
+ import { Registry } from '../../token/registry.mjs'
18
+ import { inject } from '../../utils/index.mjs'
19
+ import {
20
+ createGCTracker,
21
+ forceGC,
22
+ getHeapUsed,
23
+ isGCAvailable,
24
+ waitForGC,
25
+ } from './gc-test-utils.mjs'
26
+
27
+ describe.skipIf(!isGCAvailable)('GC: Transient Services', () => {
28
+ let registry: Registry
29
+ let container: Container
30
+
31
+ beforeEach(() => {
32
+ registry = new Registry()
33
+ container = new Container(registry)
34
+ })
35
+
36
+ afterEach(async () => {
37
+ await container.dispose()
38
+ })
39
+
40
+ describe('Transient services are collected when unreferenced', () => {
41
+ it('should garbage collect transient service when reference is released', async () => {
42
+ @Injectable({ registry, scope: InjectableScope.Transient })
43
+ class TransientService {
44
+ public readonly id = Math.random()
45
+ public readonly data = Array.from({ length: 1000 }, () => 'transient')
46
+ }
47
+
48
+ let instance: TransientService | null =
49
+ await container.get(TransientService)
50
+ const tracker = createGCTracker(instance)
51
+
52
+ expect(tracker().collected).toBe(false)
53
+
54
+ // Release the reference
55
+ instance = null
56
+
57
+ const collected = await waitForGC(tracker().ref)
58
+ expect(collected).toBe(true)
59
+ })
60
+
61
+ it('should collect multiple transient instances independently', async () => {
62
+ @Injectable({ registry, scope: InjectableScope.Transient })
63
+ class TransientService {
64
+ public readonly id = Math.random()
65
+ public readonly data = Array.from(
66
+ { length: 500 },
67
+ () => 'multi-transient',
68
+ )
69
+ }
70
+
71
+ let instance1: TransientService | null =
72
+ await container.get(TransientService)
73
+ let instance2: TransientService | null =
74
+ await container.get(TransientService)
75
+ let instance3: TransientService | null =
76
+ await container.get(TransientService)
77
+
78
+ expect(instance1).not.toBe(instance2)
79
+ expect(instance2).not.toBe(instance3)
80
+
81
+ const tracker1 = createGCTracker(instance1)
82
+ const tracker2 = createGCTracker(instance2)
83
+ const tracker3 = createGCTracker(instance3)
84
+
85
+ // Release only the first instance
86
+ instance1 = null
87
+ forceGC()
88
+
89
+ expect(await waitForGC(tracker1().ref, 500)).toBe(true)
90
+ expect(tracker2().collected).toBe(false)
91
+ expect(tracker3().collected).toBe(false)
92
+
93
+ // Release the second instance
94
+ instance2 = null
95
+ forceGC()
96
+
97
+ expect(await waitForGC(tracker2().ref, 500)).toBe(true)
98
+ expect(tracker3().collected).toBe(false)
99
+
100
+ // Release the third instance
101
+ instance3 = null
102
+ forceGC()
103
+
104
+ expect(await waitForGC(tracker3().ref, 500)).toBe(true)
105
+ })
106
+
107
+ it('should collect transient services with lifecycle hooks', async () => {
108
+ @Injectable({ registry, scope: InjectableScope.Transient })
109
+ class TransientWithDestroy implements OnServiceDestroy {
110
+ public readonly id = Math.random()
111
+
112
+ onServiceDestroy(): void {
113
+ // This won't be called when going out of scope
114
+ }
115
+ }
116
+
117
+ let instance: TransientWithDestroy | null =
118
+ await container.get(TransientWithDestroy)
119
+ const tracker = createGCTracker(instance)
120
+
121
+ instance = null
122
+
123
+ const collected = await waitForGC(tracker().ref)
124
+ expect(collected).toBe(true)
125
+
126
+ // Note: onServiceDestroy is NOT called for transient services when
127
+ // they go out of scope - only when the container is disposed.
128
+ // This is expected behavior since the container doesn't track them.
129
+ })
130
+ })
131
+
132
+ describe('Transient services with dependencies', () => {
133
+ it('should collect transient service but keep singleton dependency', async () => {
134
+ @Injectable({ registry })
135
+ class SingletonDep {
136
+ public readonly id = Math.random()
137
+ }
138
+
139
+ @Injectable({ registry, scope: InjectableScope.Transient })
140
+ class TransientWithSingletonDep {
141
+ public readonly dep = inject(SingletonDep)
142
+ public readonly id = Math.random()
143
+ }
144
+
145
+ const singletonRef = await container.get(SingletonDep)
146
+ let transient: TransientWithSingletonDep | null = await container.get(
147
+ TransientWithSingletonDep,
148
+ )
149
+
150
+ expect(transient.dep).toBe(singletonRef)
151
+
152
+ const transientTracker = createGCTracker(transient)
153
+ const singletonTracker = createGCTracker(singletonRef)
154
+
155
+ transient = null
156
+ forceGC()
157
+
158
+ // Transient should be collected
159
+ expect(await waitForGC(transientTracker().ref)).toBe(true)
160
+
161
+ // Singleton should NOT be collected (still in container)
162
+ forceGC()
163
+ expect(singletonTracker().collected).toBe(false)
164
+
165
+ // Singleton is still accessible
166
+ const stillThere = await container.get(SingletonDep)
167
+ expect(stillThere).toBe(singletonRef)
168
+ })
169
+
170
+ it('should collect transient service with transient dependency', async () => {
171
+ @Injectable({ registry, scope: InjectableScope.Transient })
172
+ class TransientDep {
173
+ public readonly id = Math.random()
174
+ public readonly data = Array.from({ length: 500 }, () => 'dep')
175
+ }
176
+
177
+ @Injectable({ registry, scope: InjectableScope.Transient })
178
+ class TransientParent {
179
+ public readonly dep = inject(TransientDep)
180
+ public readonly data = Array.from({ length: 500 }, () => 'parent')
181
+ }
182
+
183
+ let parent: TransientParent | null = await container.get(TransientParent)
184
+ let depRef: TransientDep | null = parent.dep
185
+
186
+ const parentTracker = createGCTracker(parent)
187
+ const depTracker = createGCTracker(depRef)
188
+
189
+ // Release both references
190
+ parent = null
191
+ depRef = null
192
+ forceGC()
193
+
194
+ // Both should be collected since no external references
195
+ expect(await waitForGC(parentTracker().ref)).toBe(true)
196
+ expect(await waitForGC(depTracker().ref)).toBe(true)
197
+ })
198
+ })
199
+
200
+ describe('Memory is reclaimed for transient services', () => {
201
+ // This test is flaky and no matter how many instances we create, the last instance is never collected.
202
+ it.skip('should reclaim memory when transient instances are released', async () => {
203
+ const INSTANCE_COUNT = 10
204
+
205
+ @Injectable({ registry, scope: InjectableScope.Transient })
206
+ class LargeTransientService {
207
+ public readonly data = new Uint8Array(1024 * 100) // 100KB
208
+ }
209
+
210
+ // Track all instances with WeakRefs
211
+ const trackers: ReturnType<typeof createGCTracker>[] = []
212
+ let instances: LargeTransientService[] = []
213
+
214
+ for (let i = 0; i < INSTANCE_COUNT; i++) {
215
+ const instance = await container.get(LargeTransientService)
216
+ instances.push(instance)
217
+ trackers.push(createGCTracker(instance))
218
+ }
219
+
220
+ // Verify none collected yet
221
+ for (const tracker of trackers) {
222
+ expect(tracker().collected).toBe(false)
223
+ }
224
+
225
+ // Release all references
226
+ instances = []
227
+ forceGC()
228
+
229
+ let collected = 0
230
+ // All should be collected
231
+ for (const tracker of trackers) {
232
+ console.log('waiting for GC', collected++, tracker().collected)
233
+ expect(await waitForGC(tracker().ref)).toBe(true)
234
+ }
235
+ })
236
+
237
+ it.todo(
238
+ 'should not accumulate memory with repeated transient creations',
239
+ async () => {
240
+ const ALLOCATION_SIZE = 1024 * 50 // 50KB per instance
241
+ const ITERATIONS = 50
242
+
243
+ @Injectable({ registry, scope: InjectableScope.Transient })
244
+ class TransientService {
245
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
246
+ }
247
+
248
+ forceGC()
249
+ const baselineMemory = getHeapUsed()
250
+
251
+ // Repeatedly create and discard transient instances
252
+ for (let i = 0; i < ITERATIONS; i++) {
253
+ const instance = await container.get(TransientService)
254
+ // instance goes out of scope immediately
255
+ void instance
256
+ }
257
+
258
+ forceGC()
259
+ const finalMemory = getHeapUsed()
260
+ const memoryGrowth = finalMemory - baselineMemory
261
+
262
+ // Memory growth should be minimal (less than 2 instances worth)
263
+ // since transients are not cached
264
+ expect(memoryGrowth).toBeLessThan(ALLOCATION_SIZE * 2)
265
+ },
266
+ )
267
+ })
268
+
269
+ describe('Transient services in mixed scope scenarios', () => {
270
+ it('should properly collect transients while singletons persist', async () => {
271
+ @Injectable({ registry })
272
+ class SingletonA {
273
+ public readonly data = Array.from({ length: 500 }, () => 'singleton-a')
274
+ }
275
+
276
+ @Injectable({ registry })
277
+ class SingletonB {
278
+ public readonly data = Array.from({ length: 500 }, () => 'singleton-b')
279
+ }
280
+
281
+ @Injectable({ registry, scope: InjectableScope.Transient })
282
+ class TransientA {
283
+ public readonly singletonA = inject(SingletonA)
284
+ public readonly data = Array.from({ length: 500 }, () => 'transient-a')
285
+ }
286
+
287
+ @Injectable({ registry, scope: InjectableScope.Transient })
288
+ class TransientB {
289
+ public readonly singletonB = inject(SingletonB)
290
+ public readonly data = Array.from({ length: 500 }, () => 'transient-b')
291
+ }
292
+
293
+ // Get singletons first
294
+ const singletonA = await container.get(SingletonA)
295
+ const singletonB = await container.get(SingletonB)
296
+
297
+ // Create transients
298
+ let transientA: TransientA | null = await container.get(TransientA)
299
+ let transientB: TransientB | null = await container.get(TransientB)
300
+
301
+ const trackers = {
302
+ singletonA: createGCTracker(singletonA),
303
+ singletonB: createGCTracker(singletonB),
304
+ transientA: createGCTracker(transientA),
305
+ transientB: createGCTracker(transientB),
306
+ }
307
+
308
+ // Release transients
309
+ transientA = null
310
+ transientB = null
311
+ forceGC()
312
+
313
+ // Transients should be collected
314
+ expect(await waitForGC(trackers.transientA().ref)).toBe(true)
315
+ expect(await waitForGC(trackers.transientB().ref)).toBe(true)
316
+
317
+ // Singletons should remain
318
+ expect(trackers.singletonA().collected).toBe(false)
319
+ expect(trackers.singletonB().collected).toBe(false)
320
+
321
+ // Singletons still accessible
322
+ expect(await container.get(SingletonA)).toBe(singletonA)
323
+ expect(await container.get(SingletonB)).toBe(singletonB)
324
+ })
325
+ })
326
+ })
@@ -116,15 +116,6 @@ describe('Scope Upgrade: Simple Singleton -> Request', () => {
116
116
  await scoped2.endRequest()
117
117
  })
118
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
119
  it('should return same instance within the same request after upgrade', async () => {
129
120
  @Injectable({ scope: InjectableScope.Request, registry })
130
121
  class RequestService {
@@ -335,15 +326,6 @@ describe('Scope Upgrade: Complex Chains', () => {
335
326
  })
336
327
 
337
328
  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
329
  it('should upgrade all dependent Singletons', async () => {
348
330
  @Injectable({ scope: InjectableScope.Request, registry })
349
331
  class SharedRequestService {
@@ -14,7 +14,6 @@ import type { LifecycleEventBus } from '../lifecycle/lifecycle-event-bus.mjs'
14
14
  import { InjectableScope } from '../../enums/index.mjs'
15
15
  import { DIError, DIErrorCode } from '../../errors/index.mjs'
16
16
  import {
17
- BoundInjectionToken,
18
17
  FactoryInjectionToken,
19
18
  InjectionToken,
20
19
  } from '../../token/injection-token.mjs'
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Compatibility layer for converting legacy decorator signatures to Stage 3 format.
3
+ *
4
+ * This module provides utilities to create mock Stage 3 decorator contexts
5
+ * from legacy decorator arguments, and manages metadata storage using WeakMap.
6
+ */
7
+
8
+ import type { ClassType } from '../token/injection-token.mjs'
9
+
10
+ // WeakMap to store metadata for legacy decorators
11
+ // Keyed by class constructor for class decorators
12
+ // For method decorators, we use the class constructor (extracted from the prototype)
13
+ const classMetadataMap = new WeakMap<ClassType, Record<string | symbol, any>>()
14
+
15
+ /**
16
+ * Gets the constructor from a prototype (for method decorators).
17
+ */
18
+ function getConstructor(prototype: any): ClassType | null {
19
+ if (!prototype || typeof prototype !== 'object') {
20
+ return null
21
+ }
22
+ // In legacy decorators, target is the prototype
23
+ // The constructor is typically available via prototype.constructor
24
+ const constructor = prototype.constructor
25
+ if (constructor && typeof constructor === 'function') {
26
+ return constructor as ClassType
27
+ }
28
+ return null
29
+ }
30
+
31
+ /**
32
+ * Creates a mock ClassDecoratorContext for legacy class decorators.
33
+ * @internal
34
+ */
35
+ export function createClassContext(target: ClassType): ClassDecoratorContext {
36
+ // Get or create metadata storage for this class
37
+ if (!classMetadataMap.has(target)) {
38
+ classMetadataMap.set(target, {})
39
+ }
40
+ const metadata = classMetadataMap.get(target)!
41
+
42
+ return {
43
+ kind: 'class',
44
+ name: target.name,
45
+ metadata,
46
+ addInitializer() {
47
+ // Legacy decorators don't support initializers
48
+ },
49
+ } as ClassDecoratorContext
50
+ }
51
+
52
+ /**
53
+ * Creates a mock ClassMethodDecoratorContext for legacy method decorators.
54
+ *
55
+ * Note: Method decorators need to share metadata with the class context
56
+ * because endpoint metadata is stored at the class level.
57
+ * @internal
58
+ */
59
+ export function createMethodContext(
60
+ target: any,
61
+ propertyKey: string | symbol,
62
+ descriptor: PropertyDescriptor,
63
+ ): ClassMethodDecoratorContext {
64
+ // For method decorators, target is the prototype
65
+ // We need to get the class constructor to access class-level metadata
66
+ const constructor = getConstructor(target)
67
+ if (!constructor) {
68
+ throw new Error(
69
+ '[Navios] Could not determine class constructor from method decorator target.',
70
+ )
71
+ }
72
+
73
+ // Get or create metadata storage for the class
74
+ // Method decorators share metadata with the class
75
+ if (!classMetadataMap.has(constructor)) {
76
+ classMetadataMap.set(constructor, {})
77
+ }
78
+ const metadata = classMetadataMap.get(constructor)!
79
+
80
+ return {
81
+ kind: 'method',
82
+ name: propertyKey,
83
+ metadata,
84
+ static: false, // We can't determine this from legacy decorators
85
+ private: false, // We can't determine this from legacy decorators
86
+ access: {
87
+ has: () => true,
88
+ get: () => descriptor.value,
89
+ set: () => {},
90
+ },
91
+ addInitializer() {
92
+ // Legacy decorators don't support initializers
93
+ },
94
+ } as ClassMethodDecoratorContext
95
+ }
@@ -0,0 +1,37 @@
1
+ import type { ClassType } from '../token/injection-token.mjs'
2
+
3
+ import { Factory as OriginalFactory, type FactoryOptions } from '../decorators/index.mjs'
4
+
5
+ import { createClassContext } from './context-compat.mjs'
6
+
7
+ export type { FactoryOptions }
8
+
9
+ /**
10
+ * Legacy-compatible Factory decorator.
11
+ *
12
+ * Works with TypeScript experimental decorators (legacy API).
13
+ *
14
+ * @param options - Factory configuration options
15
+ * @returns A class decorator compatible with legacy decorator API
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * @Factory()
20
+ * export class DatabaseConnectionFactory {
21
+ * create() {
22
+ * return { host: 'localhost', port: 5432 }
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ export function Factory(options: FactoryOptions = {}) {
28
+ return function (target: ClassType) {
29
+ const context = createClassContext(target)
30
+ // Use the no-args overload when options is empty, otherwise pass options
31
+ const originalDecorator =
32
+ Object.keys(options).length === 0
33
+ ? OriginalFactory()
34
+ : OriginalFactory(options)
35
+ return originalDecorator(target, context)
36
+ }
37
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Legacy-compatible decorators for projects using TypeScript experimental decorators.
3
+ *
4
+ * Use this when you cannot use Stage 3 decorators (e.g., existing projects with
5
+ * experimentalDecorators enabled, certain bundler configurations, or Bun).
6
+ */
7
+
8
+ // Re-export decorator options types
9
+ export type { InjectableOptions, FactoryOptions } from '../decorators/index.mjs'
10
+
11
+ // Export legacy-compatible decorators
12
+ export { Injectable } from './injectable.decorator.mjs'
13
+ export { Factory } from './factory.decorator.mjs'
14
+
15
+ // Export context compatibility utilities for building custom legacy decorators
16
+ export { createClassContext, createMethodContext } from './context-compat.mjs'
@@ -0,0 +1,41 @@
1
+ import type { ClassType } from '../token/injection-token.mjs'
2
+
3
+ import {
4
+ Injectable as OriginalInjectable,
5
+ type InjectableOptions,
6
+ } from '../decorators/index.mjs'
7
+
8
+ import { createClassContext } from './context-compat.mjs'
9
+
10
+ export type { InjectableOptions }
11
+
12
+ /**
13
+ * Legacy-compatible Injectable decorator.
14
+ *
15
+ * Works with TypeScript experimental decorators (legacy API).
16
+ *
17
+ * @param options - Injectable configuration options
18
+ * @returns A class decorator compatible with legacy decorator API
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * @Injectable()
23
+ * export class UserService {
24
+ * getUser(id: string) {
25
+ * return { id, name: 'John' }
26
+ * }
27
+ * }
28
+ * ```
29
+ */
30
+ export function Injectable(options: InjectableOptions = {}) {
31
+ return function (target: ClassType) {
32
+ const context = createClassContext(target)
33
+ // Use the no-args overload when options is empty, otherwise pass options
34
+ const originalDecorator =
35
+ Object.keys(options).length === 0
36
+ ? OriginalInjectable()
37
+ : // @ts-expect-error - InjectableOptions is a union type, we let runtime handle it
38
+ OriginalInjectable(options)
39
+ return originalDecorator(target, context)
40
+ }
41
+ }
package/tsdown.config.mts CHANGED
@@ -5,7 +5,7 @@ import swc from 'unplugin-swc'
5
5
  export default defineConfig([
6
6
  // Node.js build (default)
7
7
  {
8
- entry: ['src/index.mts', 'src/testing/index.mts'],
8
+ entry: ['src/index.mts', 'src/testing/index.mts', 'src/legacy-compat/index.mts'],
9
9
  outDir: 'lib',
10
10
  format: ['esm', 'cjs'],
11
11
  clean: true,
package/vitest.config.mts CHANGED
@@ -7,6 +7,9 @@ export default defineProject({
7
7
  test: {
8
8
  include: ['src/**/__tests__/**/*.spec.mts'],
9
9
  exclude: ['src/**/__tests__/**/*.browser.spec.mts'],
10
+ typecheck: {
11
+ enabled: true,
12
+ },
10
13
  },
11
14
  },
12
15
  {
@@ -19,13 +22,6 @@ export default defineProject({
19
22
  },
20
23
  },
21
24
  },
22
- {
23
- test: {
24
- typecheck: {
25
- enabled: true,
26
- },
27
- },
28
- },
29
25
  ],
30
26
  },
31
27
  })