@navios/di 0.9.1 → 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 (178) hide show
  1. package/CHANGELOG.md +17 -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/browser/utils/get-injectors.mjs +9 -12
  87. package/lib/browser/utils/get-injectors.mjs.map +1 -1
  88. package/lib/{container-D-0Ho3qL.d.cts → container-D3j3KuD9.d.mts} +5 -289
  89. package/lib/container-D3j3KuD9.d.mts.map +1 -0
  90. package/lib/{container-8-z89TyQ.mjs → container-qgHMgGNG.mjs} +12 -239
  91. package/lib/container-qgHMgGNG.mjs.map +1 -0
  92. package/lib/{container-CNiqesCL.d.mts → container-r1KP4F-n.d.cts} +5 -289
  93. package/lib/container-r1KP4F-n.d.cts.map +1 -0
  94. package/lib/{container-CaY2fDuk.cjs → container-ycYJgTq7.cjs} +50 -331
  95. package/lib/container-ycYJgTq7.cjs.map +1 -0
  96. package/lib/factory.decorator-D4mem6YQ.cjs +21 -0
  97. package/lib/factory.decorator-D4mem6YQ.cjs.map +1 -0
  98. package/lib/factory.decorator-_IPWcwQn.mjs +16 -0
  99. package/lib/factory.decorator-_IPWcwQn.mjs.map +1 -0
  100. package/lib/index.cjs +14 -24
  101. package/lib/index.cjs.map +1 -1
  102. package/lib/index.d.cts +3 -52
  103. package/lib/index.d.cts.map +1 -1
  104. package/lib/index.d.mts +3 -52
  105. package/lib/index.d.mts.map +1 -1
  106. package/lib/index.mjs +3 -13
  107. package/lib/index.mjs.map +1 -1
  108. package/lib/injectable.decorator-BNfWpjr_.d.cts +56 -0
  109. package/lib/injectable.decorator-BNfWpjr_.d.cts.map +1 -0
  110. package/lib/injectable.decorator-Bc05hRQU.d.mts +56 -0
  111. package/lib/injectable.decorator-Bc05hRQU.d.mts.map +1 -0
  112. package/lib/injectable.decorator-CyPrBzBN.mjs +227 -0
  113. package/lib/injectable.decorator-CyPrBzBN.mjs.map +1 -0
  114. package/lib/injectable.decorator-DbpiDrg-.cjs +281 -0
  115. package/lib/injectable.decorator-DbpiDrg-.cjs.map +1 -0
  116. package/lib/legacy-compat/index.cjs +114 -0
  117. package/lib/legacy-compat/index.cjs.map +1 -0
  118. package/lib/legacy-compat/index.d.cts +63 -0
  119. package/lib/legacy-compat/index.d.cts.map +1 -0
  120. package/lib/legacy-compat/index.d.mts +63 -0
  121. package/lib/legacy-compat/index.d.mts.map +1 -0
  122. package/lib/legacy-compat/index.mjs +111 -0
  123. package/lib/legacy-compat/index.mjs.map +1 -0
  124. package/lib/registry-DKbKWFvJ.d.cts +290 -0
  125. package/lib/registry-DKbKWFvJ.d.cts.map +1 -0
  126. package/lib/registry-n8JhJoxm.d.mts +290 -0
  127. package/lib/registry-n8JhJoxm.d.mts.map +1 -0
  128. package/lib/testing/index.cjs +23 -22
  129. package/lib/testing/index.cjs.map +1 -1
  130. package/lib/testing/index.d.cts +2 -1
  131. package/lib/testing/index.d.cts.map +1 -1
  132. package/lib/testing/index.d.mts +2 -1
  133. package/lib/testing/index.d.mts.map +1 -1
  134. package/lib/testing/index.mjs +2 -1
  135. package/lib/testing/index.mjs.map +1 -1
  136. package/package.json +11 -1
  137. package/project.json +8 -0
  138. package/src/__tests__/gc/basic-container.spec.mts +358 -0
  139. package/src/__tests__/gc/circular-dependencies.spec.mts +501 -0
  140. package/src/__tests__/gc/gc-test-utils.mts +136 -0
  141. package/src/__tests__/gc/memory-pressure.spec.mts +542 -0
  142. package/src/__tests__/gc/scoped-container.spec.mts +444 -0
  143. package/src/__tests__/gc/transient-services.spec.mts +326 -0
  144. package/src/__tests__/get-injectors.spec.mts +166 -0
  145. package/src/__tests__/scope-upgrade.spec.mts +0 -18
  146. package/src/internal/core/instance-resolver.mts +0 -1
  147. package/src/legacy-compat/context-compat.mts +95 -0
  148. package/src/legacy-compat/factory.decorator.mts +37 -0
  149. package/src/legacy-compat/index.mts +16 -0
  150. package/src/legacy-compat/injectable.decorator.mts +41 -0
  151. package/src/utils/get-injectors.mts +36 -32
  152. package/tsdown.config.mts +1 -1
  153. package/vitest.config.mts +3 -7
  154. package/coverage/docs/examples/basic-usage.mts.html +0 -376
  155. package/coverage/docs/examples/factory-pattern.mts.html +0 -1039
  156. package/coverage/docs/examples/index.html +0 -176
  157. package/coverage/docs/examples/injection-tokens.mts.html +0 -760
  158. package/coverage/docs/examples/request-scope-example.mts.html +0 -847
  159. package/coverage/docs/examples/service-lifecycle.mts.html +0 -1162
  160. package/coverage/lib/_tsup-dts-rollup.d.mts.html +0 -3445
  161. package/coverage/lib/index.d.mts.html +0 -313
  162. package/coverage/src/errors/errors.enum.mts.html +0 -118
  163. package/coverage/src/errors/factory-not-found.mts.html +0 -118
  164. package/coverage/src/errors/factory-token-not-resolved.mts.html +0 -118
  165. package/coverage/src/errors/instance-destroying.mts.html +0 -118
  166. package/coverage/src/errors/instance-expired.mts.html +0 -118
  167. package/coverage/src/errors/instance-not-found.mts.html +0 -118
  168. package/coverage/src/errors/unknown-error.mts.html +0 -118
  169. package/coverage/src/instance-resolver.mts.html +0 -1762
  170. package/coverage/src/registry.mts.html +0 -247
  171. package/coverage/src/service-invalidator.mts.html +0 -1372
  172. package/coverage/src/service-locator-manager.mts.html +0 -340
  173. package/coverage/src/token-processor.mts.html +0 -607
  174. package/coverage/src/utils/defer.mts.html +0 -118
  175. package/lib/container-8-z89TyQ.mjs.map +0 -1
  176. package/lib/container-CNiqesCL.d.mts.map +0 -1
  177. package/lib/container-CaY2fDuk.cjs.map +0 -1
  178. package/lib/container-D-0Ho3qL.d.cts.map +0 -1
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Garbage Collection Tests: Memory Pressure
3
+ *
4
+ * Tests that verify the DI container properly handles memory under pressure
5
+ * and doesn't leak memory over time. These tests use process.memoryUsage()
6
+ * to measure actual heap usage.
7
+ *
8
+ * Run with: NODE_OPTIONS=--expose-gc yarn nx test @navios/di
9
+ */
10
+
11
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
12
+
13
+ import type { OnServiceDestroy } from '../../interfaces/on-service-destroy.interface.mjs'
14
+
15
+ import { Container } from '../../container/container.mjs'
16
+ import { Injectable } from '../../decorators/injectable.decorator.mjs'
17
+ import { InjectableScope } from '../../enums/injectable-scope.enum.mjs'
18
+ import { InjectionToken } from '../../index.mjs'
19
+ import { Registry } from '../../token/registry.mjs'
20
+ import { inject } from '../../utils/index.mjs'
21
+ import {
22
+ forceGC,
23
+ getHeapUsed,
24
+ getHeapUsedMB,
25
+ isGCAvailable,
26
+ measureMemoryDelta,
27
+ } from './gc-test-utils.mjs'
28
+
29
+ describe.skipIf(!isGCAvailable)('GC: Memory Pressure', () => {
30
+ let registry: Registry
31
+ let container: Container
32
+
33
+ beforeEach(() => {
34
+ registry = new Registry()
35
+ container = new Container(registry)
36
+ })
37
+
38
+ afterEach(async () => {
39
+ await container.dispose()
40
+ })
41
+
42
+ describe('Container disposal memory reclamation', () => {
43
+ it.todo(
44
+ 'should reclaim memory when container with many singletons is disposed',
45
+ async () => {
46
+ const SERVICE_COUNT = 50
47
+ const ALLOCATION_SIZE = 1024 * 100 // 100KB per service
48
+
49
+ // Create many singleton services
50
+ const services: Array<{ new (): { data: Uint8Array } }> = []
51
+ for (let i = 0; i < SERVICE_COUNT; i++) {
52
+ @Injectable({ registry })
53
+ class LargeService {
54
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
55
+ }
56
+ services.push(LargeService)
57
+ }
58
+
59
+ forceGC()
60
+ const beforeAllocation = getHeapUsed()
61
+
62
+ // Resolve all services
63
+ for (const Service of services) {
64
+ await container.get(Service)
65
+ }
66
+
67
+ forceGC()
68
+ const afterAllocation = getHeapUsed()
69
+ const allocated = afterAllocation - beforeAllocation
70
+
71
+ // Should have allocated approximately SERVICE_COUNT * ALLOCATION_SIZE
72
+ const expectedAllocation = SERVICE_COUNT * ALLOCATION_SIZE
73
+ expect(allocated).toBeGreaterThan(expectedAllocation * 0.8)
74
+
75
+ // Dispose container
76
+ await container.dispose()
77
+
78
+ // Create new container for afterEach cleanup
79
+ registry = new Registry()
80
+ container = new Container(registry)
81
+
82
+ forceGC()
83
+ const afterDisposal = getHeapUsed()
84
+ const reclaimed = afterAllocation - afterDisposal
85
+
86
+ // Should reclaim at least 80% of allocated memory
87
+ expect(reclaimed).toBeGreaterThan(allocated * 0.8)
88
+ },
89
+ )
90
+
91
+ it.todo('should measure memory baseline and peak correctly', async () => {
92
+ const ALLOCATION_SIZE = 1024 * 1024 // 1MB
93
+
94
+ @Injectable({ registry })
95
+ class LargeService {
96
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
97
+ }
98
+
99
+ const { after, delta } = await measureMemoryDelta(async () => {
100
+ await container.get(LargeService)
101
+ })
102
+
103
+ // Delta should be roughly the allocation size (with some overhead)
104
+ expect(delta).toBeGreaterThan(ALLOCATION_SIZE * 0.8)
105
+ expect(delta).toBeLessThan(ALLOCATION_SIZE * 1.5)
106
+
107
+ // Cleanup and measure reclamation
108
+ await container.dispose()
109
+ registry = new Registry()
110
+ container = new Container(registry)
111
+
112
+ forceGC()
113
+ const final = getHeapUsed()
114
+
115
+ // Memory should return close to baseline
116
+ const memoryReturn = after - final
117
+ expect(memoryReturn).toBeGreaterThan(ALLOCATION_SIZE * 0.8)
118
+ })
119
+ })
120
+
121
+ describe('Long-running container stress tests', () => {
122
+ it('should not accumulate memory with repeated service invalidation', async () => {
123
+ const ALLOCATION_SIZE = 1024 * 50 // 50KB
124
+ const ITERATIONS = 30
125
+
126
+ @Injectable({ registry })
127
+ class InvalidatableService implements OnServiceDestroy {
128
+ public readonly id = Math.random()
129
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
130
+
131
+ onServiceDestroy(): void {
132
+ // Cleanup
133
+ }
134
+ }
135
+
136
+ forceGC()
137
+ const baselineMemory = getHeapUsed()
138
+
139
+ // Repeatedly create and invalidate
140
+ for (let i = 0; i < ITERATIONS; i++) {
141
+ const instance = await container.get(InvalidatableService)
142
+ await container.invalidate(instance)
143
+ }
144
+
145
+ // Get one final instance
146
+ await container.get(InvalidatableService)
147
+
148
+ forceGC()
149
+ const finalMemory = getHeapUsed()
150
+ const memoryGrowth = finalMemory - baselineMemory
151
+
152
+ // Memory growth should be roughly one service instance
153
+ // (only the latest should remain)
154
+ expect(memoryGrowth).toBeLessThan(ALLOCATION_SIZE * 3)
155
+ })
156
+
157
+ it('should handle many request lifecycles without leaking', async () => {
158
+ const ALLOCATION_SIZE = 1024 * 20 // 20KB
159
+ const REQUEST_COUNT = 50
160
+ const SERVICES_PER_REQUEST = 5
161
+
162
+ // Create request-scoped services
163
+ const services: Array<{ new (): { data: Uint8Array } }> = []
164
+ for (let i = 0; i < SERVICES_PER_REQUEST; i++) {
165
+ const token = InjectionToken.create<RequestService>(
166
+ `RequestService${i}`,
167
+ )
168
+ @Injectable({ registry, scope: InjectableScope.Request, token })
169
+ class RequestService {
170
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
171
+ }
172
+ services.push(RequestService)
173
+ }
174
+
175
+ forceGC()
176
+ const baselineMemory = getHeapUsed()
177
+
178
+ // Simulate many request lifecycles
179
+ for (let reqId = 0; reqId < REQUEST_COUNT; reqId++) {
180
+ const scoped = container.beginRequest(`request-${reqId}`)
181
+
182
+ // Resolve all services in this request
183
+ for (const Service of services) {
184
+ await scoped.get(Service)
185
+ }
186
+
187
+ await scoped.endRequest()
188
+
189
+ // Periodically force GC to help cleanup
190
+ if (reqId % 10 === 0) {
191
+ forceGC()
192
+ }
193
+ }
194
+
195
+ forceGC()
196
+ const finalMemory = getHeapUsed()
197
+ const memoryGrowth = finalMemory - baselineMemory
198
+
199
+ // Memory growth should be minimal after all requests end
200
+ const maxExpectedGrowth = ALLOCATION_SIZE * SERVICES_PER_REQUEST * 2
201
+ expect(memoryGrowth).toBeLessThan(maxExpectedGrowth)
202
+ })
203
+ })
204
+
205
+ describe('High memory allocation scenarios', () => {
206
+ it.todo('should handle allocation spike and recovery', async () => {
207
+ const SPIKE_SIZE = 1024 * 1024 * 10 // 10MB spike
208
+
209
+ @Injectable({ registry })
210
+ class SpikeService {
211
+ public readonly data = new Uint8Array(SPIKE_SIZE)
212
+ }
213
+
214
+ forceGC()
215
+ const beforeSpike = getHeapUsedMB()
216
+
217
+ // Create spike
218
+ await container.get(SpikeService)
219
+
220
+ forceGC()
221
+ const atSpike = getHeapUsedMB()
222
+ const spikeDelta = atSpike - beforeSpike
223
+
224
+ // Verify spike occurred
225
+ expect(spikeDelta).toBeGreaterThan(8) // At least 8MB
226
+
227
+ // Release spike
228
+ await container.dispose()
229
+ registry = new Registry()
230
+ container = new Container(registry)
231
+
232
+ forceGC()
233
+ const afterRecovery = getHeapUsedMB()
234
+ const recovered = atSpike - afterRecovery
235
+
236
+ // Should recover most of the spike
237
+ expect(recovered).toBeGreaterThan(spikeDelta * 0.8)
238
+ })
239
+
240
+ it.todo(
241
+ 'should handle multiple containers without cross-contamination',
242
+ async () => {
243
+ const ALLOCATION_SIZE = 1024 * 1024 // 1MB per container's services
244
+ const CONTAINER_COUNT = 5
245
+
246
+ forceGC()
247
+ const baselineMemory = getHeapUsed()
248
+
249
+ const containers: Array<{ container: Container; registry: Registry }> =
250
+ []
251
+
252
+ // Create multiple containers
253
+ for (let i = 0; i < CONTAINER_COUNT; i++) {
254
+ const localRegistry = new Registry()
255
+ const localContainer = new Container(localRegistry)
256
+
257
+ @Injectable({ registry: localRegistry })
258
+ class ContainerService {
259
+ public readonly containerId = i
260
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
261
+ }
262
+
263
+ await localContainer.get(ContainerService)
264
+ containers.push({
265
+ container: localContainer,
266
+ registry: localRegistry,
267
+ })
268
+ }
269
+
270
+ forceGC()
271
+ const peakMemory = getHeapUsed()
272
+ const totalAllocated = peakMemory - baselineMemory
273
+
274
+ // Should have allocated approximately CONTAINER_COUNT * ALLOCATION_SIZE
275
+ expect(totalAllocated).toBeGreaterThan(
276
+ ALLOCATION_SIZE * CONTAINER_COUNT * 0.8,
277
+ )
278
+
279
+ // Dispose containers one by one and verify memory reclamation
280
+ for (let i = 0; i < CONTAINER_COUNT; i++) {
281
+ await containers[i].container.dispose()
282
+ forceGC()
283
+
284
+ const currentMemory = getHeapUsed()
285
+ const remainingContainers = CONTAINER_COUNT - (i + 1)
286
+ const expectedMemory =
287
+ baselineMemory + ALLOCATION_SIZE * remainingContainers
288
+
289
+ // Memory should decrease as containers are disposed
290
+ // Allow 50% tolerance for GC timing
291
+ expect(currentMemory).toBeLessThan(expectedMemory * 1.5)
292
+ }
293
+ },
294
+ )
295
+ })
296
+
297
+ describe('Memory fragmentation prevention', () => {
298
+ it.todo(
299
+ 'should handle alternating allocations without fragmentation issues',
300
+ async () => {
301
+ const SMALL_SIZE = 1024 * 10 // 10KB
302
+ const LARGE_SIZE = 1024 * 500 // 500KB
303
+ const ITERATIONS = 20
304
+
305
+ let smallServices: Array<{ new (): object }> = []
306
+ let largeServices: Array<{ new (): object }> = []
307
+
308
+ forceGC()
309
+ const baselineMemory = getHeapUsed()
310
+
311
+ // Alternate between small and large allocations
312
+ for (let i = 0; i < ITERATIONS; i++) {
313
+ @Injectable({ registry })
314
+ class SmallService {
315
+ public readonly data = new Uint8Array(SMALL_SIZE)
316
+ }
317
+ smallServices.push(SmallService)
318
+ await container.get(SmallService)
319
+
320
+ @Injectable({ registry })
321
+ class LargeService {
322
+ public readonly data = new Uint8Array(LARGE_SIZE)
323
+ }
324
+ largeServices.push(LargeService)
325
+ await container.get(LargeService)
326
+ }
327
+
328
+ forceGC()
329
+ const peakMemory = getHeapUsed()
330
+ const allocated = peakMemory - baselineMemory
331
+
332
+ // Dispose and verify reclamation
333
+ await container.dispose()
334
+ registry = new Registry()
335
+ container = new Container(registry)
336
+
337
+ // Clear references
338
+ smallServices = []
339
+ largeServices = []
340
+
341
+ forceGC()
342
+ const finalMemory = getHeapUsed()
343
+ const reclaimed = peakMemory - finalMemory
344
+
345
+ // Should reclaim at least 80% despite fragmentation potential
346
+ expect(reclaimed).toBeGreaterThan(allocated * 0.8)
347
+ },
348
+ )
349
+ })
350
+
351
+ describe('Dependency chain memory', () => {
352
+ it.todo('should properly reclaim deep dependency chains', async () => {
353
+ const ALLOCATION_SIZE = 1024 * 50 // 50KB per service
354
+
355
+ // Build a static chain of 10 services
356
+ @Injectable({ registry })
357
+ class Level10 {
358
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
359
+ }
360
+
361
+ @Injectable({ registry })
362
+ class Level9 {
363
+ public readonly next = inject(Level10)
364
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
365
+ }
366
+
367
+ @Injectable({ registry })
368
+ class Level8 {
369
+ public readonly next = inject(Level9)
370
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
371
+ }
372
+
373
+ @Injectable({ registry })
374
+ class Level7 {
375
+ public readonly next = inject(Level8)
376
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
377
+ }
378
+
379
+ @Injectable({ registry })
380
+ class Level6 {
381
+ public readonly next = inject(Level7)
382
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
383
+ }
384
+
385
+ @Injectable({ registry })
386
+ class Level5 {
387
+ public readonly next = inject(Level6)
388
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
389
+ }
390
+
391
+ @Injectable({ registry })
392
+ class Level4 {
393
+ public readonly next = inject(Level5)
394
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
395
+ }
396
+
397
+ @Injectable({ registry })
398
+ class Level3 {
399
+ public readonly next = inject(Level4)
400
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
401
+ }
402
+
403
+ @Injectable({ registry })
404
+ class Level2 {
405
+ public readonly next = inject(Level3)
406
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
407
+ }
408
+
409
+ @Injectable({ registry })
410
+ class Level1 {
411
+ public readonly next = inject(Level2)
412
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
413
+ }
414
+
415
+ const DEPTH = 10
416
+
417
+ forceGC()
418
+ const baselineMemory = getHeapUsed()
419
+
420
+ // Resolve the top of the chain (causes entire chain to resolve)
421
+ await container.get(Level1)
422
+
423
+ forceGC()
424
+ const peakMemory = getHeapUsed()
425
+ const allocated = peakMemory - baselineMemory
426
+
427
+ // Should have allocated approximately DEPTH * ALLOCATION_SIZE
428
+ expect(allocated).toBeGreaterThan(DEPTH * ALLOCATION_SIZE * 0.8)
429
+
430
+ // Dispose
431
+ await container.dispose()
432
+ registry = new Registry()
433
+ container = new Container(registry)
434
+
435
+ forceGC()
436
+ const finalMemory = getHeapUsed()
437
+ const reclaimed = peakMemory - finalMemory
438
+
439
+ // Should reclaim entire chain
440
+ expect(reclaimed).toBeGreaterThan(allocated * 0.8)
441
+ })
442
+
443
+ it.todo(
444
+ 'should handle diamond dependency pattern without memory duplication',
445
+ async () => {
446
+ const ALLOCATION_SIZE = 1024 * 100 // 100KB
447
+
448
+ // Diamond pattern: A depends on B and C, both B and C depend on D
449
+ @Injectable({ registry })
450
+ class ServiceD {
451
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
452
+ }
453
+
454
+ @Injectable({ registry })
455
+ class ServiceB {
456
+ public readonly d = inject(ServiceD)
457
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
458
+ }
459
+
460
+ @Injectable({ registry })
461
+ class ServiceC {
462
+ public readonly d = inject(ServiceD)
463
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
464
+ }
465
+
466
+ @Injectable({ registry })
467
+ class ServiceA {
468
+ public readonly b = inject(ServiceB)
469
+ public readonly c = inject(ServiceC)
470
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
471
+ }
472
+
473
+ forceGC()
474
+ const baselineMemory = getHeapUsed()
475
+
476
+ const a = await container.get(ServiceA)
477
+
478
+ // Verify diamond - B and C should share same D instance
479
+ expect(a.b.d).toBe(a.c.d)
480
+
481
+ forceGC()
482
+ const peakMemory = getHeapUsed()
483
+ const allocated = peakMemory - baselineMemory
484
+
485
+ // Should be 4 services worth (not 5), since D is shared
486
+ const expectedMax = 4 * ALLOCATION_SIZE * 1.3 // 30% overhead tolerance
487
+ expect(allocated).toBeLessThan(expectedMax)
488
+
489
+ // Dispose and verify reclamation
490
+ await container.dispose()
491
+ registry = new Registry()
492
+ container = new Container(registry)
493
+
494
+ forceGC()
495
+ const finalMemory = getHeapUsed()
496
+ const reclaimed = peakMemory - finalMemory
497
+
498
+ expect(reclaimed).toBeGreaterThan(allocated * 0.8)
499
+ },
500
+ )
501
+ })
502
+
503
+ describe('Concurrent resolution memory', () => {
504
+ it.todo(
505
+ 'should not duplicate memory with concurrent resolutions of same service',
506
+ async () => {
507
+ const ALLOCATION_SIZE = 1024 * 500 // 500KB
508
+ const CONCURRENT_REQUESTS = 10
509
+
510
+ @Injectable({ registry })
511
+ class ExpensiveService {
512
+ public readonly data = new Uint8Array(ALLOCATION_SIZE)
513
+ public readonly createdAt = Date.now()
514
+ }
515
+
516
+ forceGC()
517
+ const baselineMemory = getHeapUsed()
518
+
519
+ // Request same singleton concurrently
520
+ const instances = await Promise.all(
521
+ Array.from({ length: CONCURRENT_REQUESTS }, () =>
522
+ container.get(ExpensiveService),
523
+ ),
524
+ )
525
+
526
+ // All should be same instance
527
+ const first = instances[0]
528
+ for (const instance of instances) {
529
+ expect(instance).toBe(first)
530
+ }
531
+
532
+ forceGC()
533
+ const peakMemory = getHeapUsed()
534
+ const allocated = peakMemory - baselineMemory
535
+
536
+ // Should only have allocated once, not CONCURRENT_REQUESTS times
537
+ const maxExpected = ALLOCATION_SIZE * 1.5 // Allow 50% overhead
538
+ expect(allocated).toBeLessThan(maxExpected)
539
+ },
540
+ )
541
+ })
542
+ })