@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,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
|
})
|