@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.
- package/CHANGELOG.md +9 -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/{container-D-0Ho3qL.d.cts → container-D3j3KuD9.d.mts} +5 -289
- package/lib/container-D3j3KuD9.d.mts.map +1 -0
- package/lib/{container-Bi0huFQX.mjs → container-qgHMgGNG.mjs} +3 -227
- 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-pmGNCZL_.cjs → container-ycYJgTq7.cjs} +41 -319
- 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__/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/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-Bi0huFQX.mjs.map +0 -1
- package/lib/container-CNiqesCL.d.mts.map +0 -1
- package/lib/container-D-0Ho3qL.d.cts.map +0 -1
- package/lib/container-pmGNCZL_.cjs.map +0 -1
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Garbage Collection Tests: Scoped Container
|
|
3
|
+
*
|
|
4
|
+
* Tests that request-scoped services are properly garbage collected
|
|
5
|
+
* when the scoped container is ended/disposed.
|
|
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 { ScopedContainer } from '../../container/scoped-container.mjs'
|
|
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 { Registry } from '../../token/registry.mjs'
|
|
19
|
+
import { inject } from '../../utils/index.mjs'
|
|
20
|
+
import {
|
|
21
|
+
createGCTracker,
|
|
22
|
+
forceGC,
|
|
23
|
+
getHeapUsed,
|
|
24
|
+
isGCAvailable,
|
|
25
|
+
waitForGC,
|
|
26
|
+
} from './gc-test-utils.mjs'
|
|
27
|
+
|
|
28
|
+
describe.skipIf(!isGCAvailable)('GC: Scoped Container', () => {
|
|
29
|
+
let registry: Registry
|
|
30
|
+
let container: Container
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
registry = new Registry()
|
|
34
|
+
container = new Container(registry)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
await container.dispose()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('Request-scoped services released on endRequest', () => {
|
|
42
|
+
it('should garbage collect request-scoped service after endRequest', async () => {
|
|
43
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
44
|
+
class RequestService {
|
|
45
|
+
public readonly id = Math.random()
|
|
46
|
+
public readonly data = Array.from(
|
|
47
|
+
{ length: 1000 },
|
|
48
|
+
() => 'request-scoped',
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const scoped = container.beginRequest('test-request-1')
|
|
53
|
+
let instance: RequestService | null = await scoped.get(RequestService)
|
|
54
|
+
const tracker = createGCTracker(instance)
|
|
55
|
+
|
|
56
|
+
expect(tracker().collected).toBe(false)
|
|
57
|
+
|
|
58
|
+
await scoped.endRequest()
|
|
59
|
+
instance = null
|
|
60
|
+
|
|
61
|
+
const collected = await waitForGC(tracker().ref)
|
|
62
|
+
expect(collected).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should collect multiple request-scoped services on endRequest', async () => {
|
|
66
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
67
|
+
class ServiceA {
|
|
68
|
+
public readonly data = Array.from({ length: 500 }, () => 'a')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
72
|
+
class ServiceB {
|
|
73
|
+
public readonly data = Array.from({ length: 500 }, () => 'b')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
77
|
+
class ServiceC {
|
|
78
|
+
public readonly data = Array.from({ length: 500 }, () => 'c')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const scoped = container.beginRequest('test-request-2')
|
|
82
|
+
|
|
83
|
+
let instanceA: ServiceA | null = await scoped.get(ServiceA)
|
|
84
|
+
let instanceB: ServiceB | null = await scoped.get(ServiceB)
|
|
85
|
+
let instanceC: ServiceC | null = await scoped.get(ServiceC)
|
|
86
|
+
|
|
87
|
+
const trackerA = createGCTracker(instanceA)
|
|
88
|
+
const trackerB = createGCTracker(instanceB)
|
|
89
|
+
const trackerC = createGCTracker(instanceC)
|
|
90
|
+
|
|
91
|
+
await scoped.endRequest()
|
|
92
|
+
|
|
93
|
+
// Release local references
|
|
94
|
+
instanceA = null
|
|
95
|
+
instanceB = null
|
|
96
|
+
instanceC = null
|
|
97
|
+
|
|
98
|
+
expect(await waitForGC(trackerA().ref)).toBe(true)
|
|
99
|
+
expect(await waitForGC(trackerB().ref)).toBe(true)
|
|
100
|
+
expect(await waitForGC(trackerC().ref)).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should call onServiceDestroy and then collect', async () => {
|
|
104
|
+
const destroyedIds: number[] = []
|
|
105
|
+
|
|
106
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
107
|
+
class RequestServiceWithDestroy implements OnServiceDestroy {
|
|
108
|
+
public readonly id = Math.random()
|
|
109
|
+
|
|
110
|
+
onServiceDestroy(): void {
|
|
111
|
+
destroyedIds.push(this.id)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const scoped = container.beginRequest('test-request-3')
|
|
116
|
+
let instance: RequestServiceWithDestroy | null = await scoped.get(
|
|
117
|
+
RequestServiceWithDestroy,
|
|
118
|
+
)
|
|
119
|
+
const instanceId = instance.id
|
|
120
|
+
const tracker = createGCTracker(instance)
|
|
121
|
+
|
|
122
|
+
expect(destroyedIds).toHaveLength(0)
|
|
123
|
+
|
|
124
|
+
await scoped.endRequest()
|
|
125
|
+
instance = null
|
|
126
|
+
|
|
127
|
+
expect(destroyedIds).toContain(instanceId)
|
|
128
|
+
expect(await waitForGC(tracker().ref)).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should handle async onServiceDestroy before collection', async () => {
|
|
132
|
+
let destroyCompleted = false
|
|
133
|
+
|
|
134
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
135
|
+
class AsyncDestroyService implements OnServiceDestroy {
|
|
136
|
+
async onServiceDestroy(): Promise<void> {
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
138
|
+
destroyCompleted = true
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const scoped = container.beginRequest('test-request-4')
|
|
143
|
+
let instance: AsyncDestroyService | null =
|
|
144
|
+
await scoped.get(AsyncDestroyService)
|
|
145
|
+
const tracker = createGCTracker(instance)
|
|
146
|
+
|
|
147
|
+
await scoped.endRequest()
|
|
148
|
+
instance = null
|
|
149
|
+
|
|
150
|
+
expect(destroyCompleted).toBe(true)
|
|
151
|
+
expect(await waitForGC(tracker().ref)).toBe(true)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('Scoped container itself is collected', () => {
|
|
156
|
+
it('should garbage collect scoped container after endRequest', async () => {
|
|
157
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
158
|
+
class RequestService {}
|
|
159
|
+
|
|
160
|
+
let scoped: ScopedContainer | null =
|
|
161
|
+
container.beginRequest('test-gc-scoped')
|
|
162
|
+
await scoped.get(RequestService)
|
|
163
|
+
|
|
164
|
+
const scopedTracker = createGCTracker(scoped)
|
|
165
|
+
|
|
166
|
+
await scoped.endRequest()
|
|
167
|
+
scoped = null
|
|
168
|
+
|
|
169
|
+
const collected = await waitForGC(scopedTracker().ref)
|
|
170
|
+
expect(collected).toBe(true)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('Mixed scope scenarios with request scope', () => {
|
|
175
|
+
it('should collect request-scoped but keep singletons', async () => {
|
|
176
|
+
@Injectable({ registry })
|
|
177
|
+
class SingletonService {
|
|
178
|
+
public readonly id = Math.random()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
182
|
+
class RequestService {
|
|
183
|
+
public readonly singleton = inject(SingletonService)
|
|
184
|
+
public readonly id = Math.random()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Get singleton first via root container
|
|
188
|
+
const singleton = await container.get(SingletonService)
|
|
189
|
+
const singletonTracker = createGCTracker(singleton)
|
|
190
|
+
|
|
191
|
+
// Create request scope and get request-scoped service
|
|
192
|
+
const scoped = container.beginRequest('test-mixed-1')
|
|
193
|
+
let requestInstance: RequestService | null =
|
|
194
|
+
await scoped.get(RequestService)
|
|
195
|
+
const requestTracker = createGCTracker(requestInstance)
|
|
196
|
+
|
|
197
|
+
// Request service should reference same singleton
|
|
198
|
+
expect(requestInstance.singleton).toBe(singleton)
|
|
199
|
+
|
|
200
|
+
await scoped.endRequest()
|
|
201
|
+
requestInstance = null
|
|
202
|
+
|
|
203
|
+
// Request-scoped should be collected
|
|
204
|
+
expect(await waitForGC(requestTracker().ref)).toBe(true)
|
|
205
|
+
|
|
206
|
+
// Singleton should remain
|
|
207
|
+
expect(singletonTracker().collected).toBe(false)
|
|
208
|
+
expect(await container.get(SingletonService)).toBe(singleton)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should isolate request-scoped services between requests', async () => {
|
|
212
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
213
|
+
class RequestService {
|
|
214
|
+
public readonly id = Math.random()
|
|
215
|
+
public readonly data = Array.from({ length: 500 }, () => 'isolated')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const scoped1 = container.beginRequest('request-a')
|
|
219
|
+
const scoped2 = container.beginRequest('request-b')
|
|
220
|
+
|
|
221
|
+
let instance1: RequestService | null = await scoped1.get(RequestService)
|
|
222
|
+
let instance2: RequestService | null = await scoped2.get(RequestService)
|
|
223
|
+
|
|
224
|
+
expect(instance1).not.toBe(instance2)
|
|
225
|
+
|
|
226
|
+
const tracker1 = createGCTracker(instance1)
|
|
227
|
+
const tracker2 = createGCTracker(instance2)
|
|
228
|
+
|
|
229
|
+
// End first request
|
|
230
|
+
await scoped1.endRequest()
|
|
231
|
+
instance1 = null
|
|
232
|
+
|
|
233
|
+
expect(await waitForGC(tracker1().ref)).toBe(true)
|
|
234
|
+
expect(tracker2().collected).toBe(false)
|
|
235
|
+
|
|
236
|
+
// Second request's instance should still work
|
|
237
|
+
let stillThere: RequestService | null = await scoped2.get(RequestService)
|
|
238
|
+
expect(stillThere).toBe(instance2)
|
|
239
|
+
|
|
240
|
+
// End second request
|
|
241
|
+
await scoped2.endRequest()
|
|
242
|
+
instance2 = null
|
|
243
|
+
stillThere = null
|
|
244
|
+
|
|
245
|
+
expect(await waitForGC(tracker2().ref)).toBe(true)
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
describe('Request-scoped dependency chains', () => {
|
|
250
|
+
it('should collect entire request-scoped dependency chain', async () => {
|
|
251
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
252
|
+
class Level3 {
|
|
253
|
+
public readonly data = Array.from({ length: 200 }, () => 'l3')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
257
|
+
class Level2 {
|
|
258
|
+
public readonly level3 = inject(Level3)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
262
|
+
class Level1 {
|
|
263
|
+
public readonly level2 = inject(Level2)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
267
|
+
class RootService {
|
|
268
|
+
public readonly level1 = inject(Level1)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const scoped = container.beginRequest('chain-request')
|
|
272
|
+
let root: RootService | null = await scoped.get(RootService)
|
|
273
|
+
|
|
274
|
+
let l1: Level1 | null = root.level1
|
|
275
|
+
let l2: Level2 | null = l1.level2
|
|
276
|
+
let l3: Level3 | null = l2.level3
|
|
277
|
+
|
|
278
|
+
const rootTracker = createGCTracker(root)
|
|
279
|
+
const l1Tracker = createGCTracker(l1)
|
|
280
|
+
const l2Tracker = createGCTracker(l2)
|
|
281
|
+
const l3Tracker = createGCTracker(l3)
|
|
282
|
+
|
|
283
|
+
await scoped.endRequest()
|
|
284
|
+
|
|
285
|
+
// Release local references
|
|
286
|
+
root = null
|
|
287
|
+
l1 = null
|
|
288
|
+
l2 = null
|
|
289
|
+
l3 = null
|
|
290
|
+
|
|
291
|
+
expect(await waitForGC(rootTracker().ref)).toBe(true)
|
|
292
|
+
expect(await waitForGC(l1Tracker().ref)).toBe(true)
|
|
293
|
+
expect(await waitForGC(l2Tracker().ref)).toBe(true)
|
|
294
|
+
expect(await waitForGC(l3Tracker().ref)).toBe(true)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('should handle mixed singleton/request dependency chain', async () => {
|
|
298
|
+
@Injectable({ registry })
|
|
299
|
+
class SingletonBase {
|
|
300
|
+
public readonly id = 'singleton-base'
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
304
|
+
class RequestMiddle {
|
|
305
|
+
public readonly base = inject(SingletonBase)
|
|
306
|
+
public readonly id = Math.random()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
310
|
+
class RequestTop {
|
|
311
|
+
public readonly middle = inject(RequestMiddle)
|
|
312
|
+
public readonly id = Math.random()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const singletonBase = await container.get(SingletonBase)
|
|
316
|
+
const baseTracker = createGCTracker(singletonBase)
|
|
317
|
+
|
|
318
|
+
const scoped = container.beginRequest('mixed-chain')
|
|
319
|
+
let top: RequestTop | null = await scoped.get(RequestTop)
|
|
320
|
+
let middle: RequestMiddle | null = top.middle
|
|
321
|
+
|
|
322
|
+
const middleTracker = createGCTracker(middle)
|
|
323
|
+
const topTracker = createGCTracker(top)
|
|
324
|
+
|
|
325
|
+
expect(middle.base).toBe(singletonBase)
|
|
326
|
+
|
|
327
|
+
await scoped.endRequest()
|
|
328
|
+
|
|
329
|
+
// Release local references
|
|
330
|
+
top = null
|
|
331
|
+
middle = null
|
|
332
|
+
|
|
333
|
+
// Request-scoped should be collected
|
|
334
|
+
expect(await waitForGC(topTracker().ref)).toBe(true)
|
|
335
|
+
expect(await waitForGC(middleTracker().ref)).toBe(true)
|
|
336
|
+
|
|
337
|
+
// Singleton should remain
|
|
338
|
+
expect(baseTracker().collected).toBe(false)
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
describe('Memory reclamation for request scopes', () => {
|
|
343
|
+
it.todo('should reclaim memory when request ends', async () => {
|
|
344
|
+
const ALLOCATION_SIZE = 1024 * 100 // 100KB
|
|
345
|
+
const SERVICE_COUNT = 10
|
|
346
|
+
|
|
347
|
+
// Create multiple request-scoped services
|
|
348
|
+
const services: Array<{ new (): { data: Uint8Array } }> = []
|
|
349
|
+
for (let i = 0; i < SERVICE_COUNT; i++) {
|
|
350
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
351
|
+
class LargeRequestService {
|
|
352
|
+
public readonly data = new Uint8Array(ALLOCATION_SIZE)
|
|
353
|
+
}
|
|
354
|
+
services.push(LargeRequestService)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
forceGC()
|
|
358
|
+
const baselineMemory = getHeapUsed()
|
|
359
|
+
|
|
360
|
+
const scoped = container.beginRequest('memory-test')
|
|
361
|
+
|
|
362
|
+
// Resolve all services
|
|
363
|
+
for (const Service of services) {
|
|
364
|
+
await scoped.get(Service)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
forceGC()
|
|
368
|
+
const afterAllocationMemory = getHeapUsed()
|
|
369
|
+
const allocated = afterAllocationMemory - baselineMemory
|
|
370
|
+
|
|
371
|
+
expect(allocated).toBeGreaterThan(ALLOCATION_SIZE * SERVICE_COUNT * 0.8)
|
|
372
|
+
|
|
373
|
+
await scoped.endRequest()
|
|
374
|
+
|
|
375
|
+
forceGC()
|
|
376
|
+
const afterReleaseMemory = getHeapUsed()
|
|
377
|
+
const reclaimed = afterAllocationMemory - afterReleaseMemory
|
|
378
|
+
|
|
379
|
+
// Should reclaim most memory (at least 70%)
|
|
380
|
+
expect(reclaimed).toBeGreaterThan(allocated * 0.7)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it.todo('should not leak memory across multiple requests', async () => {
|
|
384
|
+
const ALLOCATION_SIZE = 1024 * 50 // 50KB
|
|
385
|
+
const REQUEST_COUNT = 20
|
|
386
|
+
|
|
387
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
388
|
+
class RequestService {
|
|
389
|
+
public readonly data = new Uint8Array(ALLOCATION_SIZE)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
forceGC()
|
|
393
|
+
const baselineMemory = getHeapUsed()
|
|
394
|
+
|
|
395
|
+
// Process many requests
|
|
396
|
+
for (let i = 0; i < REQUEST_COUNT; i++) {
|
|
397
|
+
const scoped = container.beginRequest(`request-${i}`)
|
|
398
|
+
await scoped.get(RequestService)
|
|
399
|
+
await scoped.endRequest()
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
forceGC()
|
|
403
|
+
const finalMemory = getHeapUsed()
|
|
404
|
+
const memoryGrowth = finalMemory - baselineMemory
|
|
405
|
+
|
|
406
|
+
// Memory growth should be minimal (less than 2 allocations worth)
|
|
407
|
+
expect(memoryGrowth).toBeLessThan(ALLOCATION_SIZE * 2)
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
describe('Concurrent request handling', () => {
|
|
412
|
+
// This test is flaky and no matter how many requests we make, the last instance is never collected.
|
|
413
|
+
it.skip('should properly collect services from concurrent requests', async () => {
|
|
414
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
415
|
+
class RequestService {
|
|
416
|
+
public readonly id = Math.random()
|
|
417
|
+
public readonly data = Array.from({ length: 500 }, () => 'concurrent')
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const requestCount = 10
|
|
421
|
+
const scopedContainers: ScopedContainer[] = []
|
|
422
|
+
const trackers: ReturnType<typeof createGCTracker>[] = []
|
|
423
|
+
|
|
424
|
+
// Start multiple concurrent requests
|
|
425
|
+
for (let i = 0; i < requestCount; i++) {
|
|
426
|
+
const scoped = container.beginRequest(`concurrent-${i}`)
|
|
427
|
+
scopedContainers.push(scoped)
|
|
428
|
+
const instance = await scoped.get(RequestService)
|
|
429
|
+
trackers.push(createGCTracker(instance))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// End all requests
|
|
433
|
+
await Promise.all(scopedContainers.map((s) => s.endRequest()))
|
|
434
|
+
|
|
435
|
+
forceGC()
|
|
436
|
+
// All should be collected
|
|
437
|
+
let collected = 0
|
|
438
|
+
for (const tracker of trackers) {
|
|
439
|
+
console.log('waiting for GC', collected++, tracker().collected)
|
|
440
|
+
expect(await waitForGC(tracker().ref)).toBe(true)
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
})
|