@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.
- package/CHANGELOG.md +17 -0
- package/README.md +28 -0
- package/coverage/block-navigation.js +1 -1
- package/coverage/clover.xml +1463 -3174
- package/coverage/coverage-final.json +56 -54
- package/coverage/index.html +179 -104
- package/coverage/sorter.js +21 -7
- package/coverage/src/{request-context-manager.mts.html → __tests__/gc/gc-test-utils.mts.html} +192 -231
- package/coverage/{lib → src/__tests__/gc}/index.html +25 -40
- package/coverage/src/container/abstract-container.mts.html +1066 -0
- package/coverage/src/container/container.mts.html +958 -0
- package/coverage/src/container/index.html +161 -0
- package/coverage/src/{testing → container}/index.mts.html +12 -9
- package/coverage/src/container/scoped-container.mts.html +907 -0
- package/coverage/src/decorators/factory.decorator.mts.html +68 -53
- package/coverage/src/decorators/index.html +34 -34
- package/coverage/src/decorators/index.mts.html +10 -10
- package/coverage/src/decorators/injectable.decorator.mts.html +144 -60
- package/coverage/src/enums/index.html +21 -21
- package/coverage/src/enums/index.mts.html +12 -12
- package/coverage/src/enums/injectable-scope.enum.mts.html +11 -8
- package/coverage/src/enums/injectable-type.enum.mts.html +10 -7
- package/coverage/src/errors/di-error.mts.html +380 -71
- package/coverage/src/errors/index.html +22 -127
- package/coverage/src/errors/index.mts.html +9 -33
- package/coverage/src/event-emitter.mts.html +35 -107
- package/coverage/src/index.html +14 -254
- package/coverage/src/index.mts.html +23 -50
- package/coverage/src/interfaces/container.interface.mts.html +370 -0
- package/coverage/src/interfaces/factory.interface.mts.html +12 -12
- package/coverage/src/interfaces/index.html +45 -30
- package/coverage/src/interfaces/index.mts.html +16 -13
- package/coverage/src/interfaces/on-service-destroy.interface.mts.html +1 -1
- package/coverage/src/interfaces/on-service-init.interface.mts.html +1 -1
- package/coverage/src/internal/context/async-local-storage.browser.mts.html +142 -0
- package/coverage/src/internal/context/async-local-storage.mts.html +292 -0
- package/coverage/src/{factory-context.mts.html → internal/context/async-local-storage.types.mts.html} +17 -17
- package/coverage/src/internal/context/factory-context.mts.html +142 -0
- package/coverage/src/internal/context/index.html +221 -0
- package/coverage/src/internal/context/index.mts.html +100 -0
- package/coverage/src/{service-locator-instance-holder.mts.html → internal/context/resolution-context.mts.html} +114 -78
- package/coverage/src/internal/context/service-initialization-context.mts.html +214 -0
- package/coverage/src/internal/context/sync-local-storage.mts.html +244 -0
- package/coverage/src/internal/core/index.html +206 -0
- package/coverage/src/internal/core/index.mts.html +103 -0
- package/coverage/src/internal/core/instance-resolver.mts.html +2533 -0
- package/coverage/src/{request-context-holder.mts.html → internal/core/name-resolver.mts.html} +245 -260
- package/coverage/src/{container.mts.html → internal/core/scope-tracker.mts.html} +352 -310
- package/coverage/src/{service-instantiator.mts.html → internal/core/service-initializer.mts.html} +254 -176
- package/coverage/src/internal/core/service-invalidator.mts.html +955 -0
- package/coverage/src/internal/core/token-resolver.mts.html +451 -0
- package/coverage/src/internal/holder/holder-storage.interface.mts.html +451 -0
- package/coverage/src/internal/holder/index.html +161 -0
- package/coverage/src/internal/holder/index.mts.html +94 -0
- package/coverage/src/internal/holder/instance-holder.mts.html +406 -0
- package/coverage/src/{service-locator.mts.html → internal/holder/unified-storage.mts.html} +376 -379
- package/coverage/{lib/testing → src/internal}/index.html +26 -11
- package/coverage/src/internal/index.mts.html +100 -0
- package/coverage/src/internal/lifecycle/circular-detector.mts.html +364 -0
- package/coverage/src/internal/lifecycle/index.html +146 -0
- package/coverage/src/internal/lifecycle/index.mts.html +91 -0
- package/coverage/src/{service-locator-event-bus.mts.html → internal/lifecycle/lifecycle-event-bus.mts.html} +104 -80
- package/coverage/src/internal/stub-factory-class.mts.html +133 -0
- package/coverage/src/symbols/index.html +14 -14
- package/coverage/src/symbols/index.mts.html +9 -9
- package/coverage/src/symbols/injectable-token.mts.html +9 -3
- package/coverage/src/testing/index.html +32 -32
- package/coverage/src/testing/test-container.mts.html +1576 -139
- package/coverage/src/testing/unit-test-container.mts.html +1888 -0
- package/coverage/src/token/index.html +146 -0
- package/coverage/{lib/testing/index.d.mts.html → src/token/index.mts.html} +11 -11
- package/coverage/src/{injection-token.mts.html → token/injection-token.mts.html} +225 -111
- package/coverage/src/{base-instance-holder-manager.mts.html → token/registry.mts.html} +188 -269
- package/coverage/src/{injector.mts.html → utils/default-injectors.mts.html} +31 -31
- package/coverage/src/utils/get-injectable-token.mts.html +19 -22
- package/coverage/src/utils/get-injectors.mts.html +684 -177
- package/coverage/src/utils/index.html +36 -36
- package/coverage/src/utils/index.mts.html +18 -12
- package/coverage/src/utils/types.mts.html +26 -11
- package/docs/examples/basic-usage.mts +1 -1
- package/docs/examples/factory-pattern.mts +3 -3
- package/docs/examples/request-scope-example.mts +1 -1
- package/docs/examples/service-lifecycle.mts +1 -1
- package/lib/browser/internal/core/instance-resolver.d.mts.map +1 -1
- package/lib/browser/internal/core/instance-resolver.mjs.map +1 -1
- package/lib/browser/utils/get-injectors.mjs +9 -12
- package/lib/browser/utils/get-injectors.mjs.map +1 -1
- package/lib/{container-D-0Ho3qL.d.cts → container-D3j3KuD9.d.mts} +5 -289
- package/lib/container-D3j3KuD9.d.mts.map +1 -0
- package/lib/{container-8-z89TyQ.mjs → container-qgHMgGNG.mjs} +12 -239
- package/lib/container-qgHMgGNG.mjs.map +1 -0
- package/lib/{container-CNiqesCL.d.mts → container-r1KP4F-n.d.cts} +5 -289
- package/lib/container-r1KP4F-n.d.cts.map +1 -0
- package/lib/{container-CaY2fDuk.cjs → container-ycYJgTq7.cjs} +50 -331
- package/lib/container-ycYJgTq7.cjs.map +1 -0
- package/lib/factory.decorator-D4mem6YQ.cjs +21 -0
- package/lib/factory.decorator-D4mem6YQ.cjs.map +1 -0
- package/lib/factory.decorator-_IPWcwQn.mjs +16 -0
- package/lib/factory.decorator-_IPWcwQn.mjs.map +1 -0
- package/lib/index.cjs +14 -24
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +3 -52
- package/lib/index.d.cts.map +1 -1
- package/lib/index.d.mts +3 -52
- package/lib/index.d.mts.map +1 -1
- package/lib/index.mjs +3 -13
- package/lib/index.mjs.map +1 -1
- package/lib/injectable.decorator-BNfWpjr_.d.cts +56 -0
- package/lib/injectable.decorator-BNfWpjr_.d.cts.map +1 -0
- package/lib/injectable.decorator-Bc05hRQU.d.mts +56 -0
- package/lib/injectable.decorator-Bc05hRQU.d.mts.map +1 -0
- package/lib/injectable.decorator-CyPrBzBN.mjs +227 -0
- package/lib/injectable.decorator-CyPrBzBN.mjs.map +1 -0
- package/lib/injectable.decorator-DbpiDrg-.cjs +281 -0
- package/lib/injectable.decorator-DbpiDrg-.cjs.map +1 -0
- package/lib/legacy-compat/index.cjs +114 -0
- package/lib/legacy-compat/index.cjs.map +1 -0
- package/lib/legacy-compat/index.d.cts +63 -0
- package/lib/legacy-compat/index.d.cts.map +1 -0
- package/lib/legacy-compat/index.d.mts +63 -0
- package/lib/legacy-compat/index.d.mts.map +1 -0
- package/lib/legacy-compat/index.mjs +111 -0
- package/lib/legacy-compat/index.mjs.map +1 -0
- package/lib/registry-DKbKWFvJ.d.cts +290 -0
- package/lib/registry-DKbKWFvJ.d.cts.map +1 -0
- package/lib/registry-n8JhJoxm.d.mts +290 -0
- package/lib/registry-n8JhJoxm.d.mts.map +1 -0
- package/lib/testing/index.cjs +23 -22
- package/lib/testing/index.cjs.map +1 -1
- package/lib/testing/index.d.cts +2 -1
- package/lib/testing/index.d.cts.map +1 -1
- package/lib/testing/index.d.mts +2 -1
- package/lib/testing/index.d.mts.map +1 -1
- package/lib/testing/index.mjs +2 -1
- package/lib/testing/index.mjs.map +1 -1
- package/package.json +11 -1
- package/project.json +8 -0
- package/src/__tests__/gc/basic-container.spec.mts +358 -0
- package/src/__tests__/gc/circular-dependencies.spec.mts +501 -0
- package/src/__tests__/gc/gc-test-utils.mts +136 -0
- package/src/__tests__/gc/memory-pressure.spec.mts +542 -0
- package/src/__tests__/gc/scoped-container.spec.mts +444 -0
- package/src/__tests__/gc/transient-services.spec.mts +326 -0
- package/src/__tests__/get-injectors.spec.mts +166 -0
- package/src/__tests__/scope-upgrade.spec.mts +0 -18
- package/src/internal/core/instance-resolver.mts +0 -1
- package/src/legacy-compat/context-compat.mts +95 -0
- package/src/legacy-compat/factory.decorator.mts +37 -0
- package/src/legacy-compat/index.mts +16 -0
- package/src/legacy-compat/injectable.decorator.mts +41 -0
- package/src/utils/get-injectors.mts +36 -32
- package/tsdown.config.mts +1 -1
- package/vitest.config.mts +3 -7
- package/coverage/docs/examples/basic-usage.mts.html +0 -376
- package/coverage/docs/examples/factory-pattern.mts.html +0 -1039
- package/coverage/docs/examples/index.html +0 -176
- package/coverage/docs/examples/injection-tokens.mts.html +0 -760
- package/coverage/docs/examples/request-scope-example.mts.html +0 -847
- package/coverage/docs/examples/service-lifecycle.mts.html +0 -1162
- package/coverage/lib/_tsup-dts-rollup.d.mts.html +0 -3445
- package/coverage/lib/index.d.mts.html +0 -313
- package/coverage/src/errors/errors.enum.mts.html +0 -118
- package/coverage/src/errors/factory-not-found.mts.html +0 -118
- package/coverage/src/errors/factory-token-not-resolved.mts.html +0 -118
- package/coverage/src/errors/instance-destroying.mts.html +0 -118
- package/coverage/src/errors/instance-expired.mts.html +0 -118
- package/coverage/src/errors/instance-not-found.mts.html +0 -118
- package/coverage/src/errors/unknown-error.mts.html +0 -118
- package/coverage/src/instance-resolver.mts.html +0 -1762
- package/coverage/src/registry.mts.html +0 -247
- package/coverage/src/service-invalidator.mts.html +0 -1372
- package/coverage/src/service-locator-manager.mts.html +0 -340
- package/coverage/src/token-processor.mts.html +0 -607
- package/coverage/src/utils/defer.mts.html +0 -118
- package/lib/container-8-z89TyQ.mjs.map +0 -1
- package/lib/container-CNiqesCL.d.mts.map +0 -1
- package/lib/container-CaY2fDuk.cjs.map +0 -1
- 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
|
+
})
|