@navios/di 0.8.0 → 0.9.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 +87 -0
- package/README.md +117 -17
- package/lib/browser/container/abstract-container.d.mts +112 -0
- package/lib/browser/container/abstract-container.d.mts.map +1 -0
- package/lib/browser/container/abstract-container.mjs +100 -0
- package/lib/browser/container/abstract-container.mjs.map +1 -0
- package/lib/browser/container/container.d.mts +100 -0
- package/lib/browser/container/container.d.mts.map +1 -0
- package/lib/browser/container/container.mjs +424 -0
- package/lib/browser/container/container.mjs.map +1 -0
- package/lib/browser/container/scoped-container.d.mts +93 -0
- package/lib/browser/container/scoped-container.d.mts.map +1 -0
- package/lib/browser/container/scoped-container.mjs +119 -0
- package/lib/browser/container/scoped-container.mjs.map +1 -0
- package/lib/browser/decorators/factory.decorator.d.mts +26 -0
- package/lib/browser/decorators/factory.decorator.d.mts.map +1 -0
- package/lib/browser/decorators/factory.decorator.mjs +20 -0
- package/lib/browser/decorators/factory.decorator.mjs.map +1 -0
- package/lib/browser/decorators/injectable.decorator.d.mts +38 -0
- package/lib/browser/decorators/injectable.decorator.d.mts.map +1 -0
- package/lib/browser/decorators/injectable.decorator.mjs +21 -0
- package/lib/browser/decorators/injectable.decorator.mjs.map +1 -0
- package/lib/browser/enums/injectable-scope.enum.d.mts +18 -0
- package/lib/browser/enums/injectable-scope.enum.d.mts.map +1 -0
- package/lib/browser/enums/injectable-scope.enum.mjs +20 -0
- package/lib/browser/enums/injectable-scope.enum.mjs.map +1 -0
- package/lib/browser/enums/injectable-type.enum.d.mts +8 -0
- package/lib/browser/enums/injectable-type.enum.d.mts.map +1 -0
- package/lib/browser/enums/injectable-type.enum.mjs +10 -0
- package/lib/browser/enums/injectable-type.enum.mjs.map +1 -0
- package/lib/browser/errors/di-error.d.mts +43 -0
- package/lib/browser/errors/di-error.d.mts.map +1 -0
- package/lib/browser/errors/di-error.mjs +98 -0
- package/lib/browser/errors/di-error.mjs.map +1 -0
- package/lib/browser/event-emitter.d.mts +16 -0
- package/lib/browser/event-emitter.d.mts.map +1 -0
- package/lib/browser/event-emitter.mjs +320 -0
- package/lib/browser/event-emitter.mjs.map +1 -0
- package/lib/browser/index.d.mts +37 -1558
- package/lib/browser/index.mjs +29 -2749
- package/lib/browser/interfaces/container.interface.d.mts +59 -0
- package/lib/browser/interfaces/container.interface.d.mts.map +1 -0
- package/lib/browser/interfaces/factory.interface.d.mts +14 -0
- package/lib/browser/interfaces/factory.interface.d.mts.map +1 -0
- package/lib/browser/interfaces/on-service-destroy.interface.d.mts +7 -0
- package/lib/browser/interfaces/on-service-destroy.interface.d.mts.map +1 -0
- package/lib/browser/interfaces/on-service-init.interface.d.mts +7 -0
- package/lib/browser/interfaces/on-service-init.interface.d.mts.map +1 -0
- package/lib/browser/internal/context/async-local-storage.browser.mjs +20 -0
- package/lib/browser/internal/context/async-local-storage.browser.mjs.map +1 -0
- package/lib/browser/internal/context/async-local-storage.d.mts +9 -0
- package/lib/browser/internal/context/async-local-storage.d.mts.map +1 -0
- package/lib/browser/internal/context/async-local-storage.types.d.mts +11 -0
- package/lib/browser/internal/context/async-local-storage.types.d.mts.map +1 -0
- package/lib/browser/internal/context/factory-context.d.mts +23 -0
- package/lib/browser/internal/context/factory-context.d.mts.map +1 -0
- package/lib/browser/internal/context/resolution-context.d.mts +43 -0
- package/lib/browser/internal/context/resolution-context.d.mts.map +1 -0
- package/lib/browser/internal/context/resolution-context.mjs +56 -0
- package/lib/browser/internal/context/resolution-context.mjs.map +1 -0
- package/lib/browser/internal/context/service-initialization-context.d.mts +48 -0
- package/lib/browser/internal/context/service-initialization-context.d.mts.map +1 -0
- package/lib/browser/internal/context/sync-local-storage.mjs +53 -0
- package/lib/browser/internal/context/sync-local-storage.mjs.map +1 -0
- package/lib/browser/internal/core/instance-resolver.d.mts +119 -0
- package/lib/browser/internal/core/instance-resolver.d.mts.map +1 -0
- package/lib/browser/internal/core/instance-resolver.mjs +306 -0
- package/lib/browser/internal/core/instance-resolver.mjs.map +1 -0
- package/lib/browser/internal/core/name-resolver.d.mts +52 -0
- package/lib/browser/internal/core/name-resolver.d.mts.map +1 -0
- package/lib/browser/internal/core/name-resolver.mjs +118 -0
- package/lib/browser/internal/core/name-resolver.mjs.map +1 -0
- package/lib/browser/internal/core/scope-tracker.d.mts +65 -0
- package/lib/browser/internal/core/scope-tracker.d.mts.map +1 -0
- package/lib/browser/internal/core/scope-tracker.mjs +120 -0
- package/lib/browser/internal/core/scope-tracker.mjs.map +1 -0
- package/lib/browser/internal/core/service-initializer.d.mts +44 -0
- package/lib/browser/internal/core/service-initializer.d.mts.map +1 -0
- package/lib/browser/internal/core/service-initializer.mjs +109 -0
- package/lib/browser/internal/core/service-initializer.mjs.map +1 -0
- package/lib/browser/internal/core/service-invalidator.d.mts +81 -0
- package/lib/browser/internal/core/service-invalidator.d.mts.map +1 -0
- package/lib/browser/internal/core/service-invalidator.mjs +142 -0
- package/lib/browser/internal/core/service-invalidator.mjs.map +1 -0
- package/lib/browser/internal/core/token-resolver.d.mts +54 -0
- package/lib/browser/internal/core/token-resolver.d.mts.map +1 -0
- package/lib/browser/internal/core/token-resolver.mjs +77 -0
- package/lib/browser/internal/core/token-resolver.mjs.map +1 -0
- package/lib/browser/internal/holder/holder-storage.interface.d.mts +99 -0
- package/lib/browser/internal/holder/holder-storage.interface.d.mts.map +1 -0
- package/lib/browser/internal/holder/instance-holder.d.mts +101 -0
- package/lib/browser/internal/holder/instance-holder.d.mts.map +1 -0
- package/lib/browser/internal/holder/instance-holder.mjs +19 -0
- package/lib/browser/internal/holder/instance-holder.mjs.map +1 -0
- package/lib/browser/internal/holder/unified-storage.d.mts +53 -0
- package/lib/browser/internal/holder/unified-storage.d.mts.map +1 -0
- package/lib/browser/internal/holder/unified-storage.mjs +144 -0
- package/lib/browser/internal/holder/unified-storage.mjs.map +1 -0
- package/lib/browser/internal/lifecycle/circular-detector.d.mts +39 -0
- package/lib/browser/internal/lifecycle/circular-detector.d.mts.map +1 -0
- package/lib/browser/internal/lifecycle/circular-detector.mjs +55 -0
- package/lib/browser/internal/lifecycle/circular-detector.mjs.map +1 -0
- package/lib/browser/internal/lifecycle/lifecycle-event-bus.d.mts +18 -0
- package/lib/browser/internal/lifecycle/lifecycle-event-bus.d.mts.map +1 -0
- package/lib/browser/internal/lifecycle/lifecycle-event-bus.mjs +43 -0
- package/lib/browser/internal/lifecycle/lifecycle-event-bus.mjs.map +1 -0
- package/lib/browser/internal/stub-factory-class.d.mts +14 -0
- package/lib/browser/internal/stub-factory-class.d.mts.map +1 -0
- package/lib/browser/internal/stub-factory-class.mjs +18 -0
- package/lib/browser/internal/stub-factory-class.mjs.map +1 -0
- package/lib/browser/symbols/injectable-token.d.mts +5 -0
- package/lib/browser/symbols/injectable-token.d.mts.map +1 -0
- package/lib/browser/symbols/injectable-token.mjs +6 -0
- package/lib/browser/symbols/injectable-token.mjs.map +1 -0
- package/lib/browser/token/injection-token.d.mts +55 -0
- package/lib/browser/token/injection-token.d.mts.map +1 -0
- package/lib/browser/token/injection-token.mjs +100 -0
- package/lib/browser/token/injection-token.mjs.map +1 -0
- package/lib/browser/token/registry.d.mts +37 -0
- package/lib/browser/token/registry.d.mts.map +1 -0
- package/lib/browser/token/registry.mjs +86 -0
- package/lib/browser/token/registry.mjs.map +1 -0
- package/lib/browser/utils/default-injectors.d.mts +12 -0
- package/lib/browser/utils/default-injectors.d.mts.map +1 -0
- package/lib/browser/utils/default-injectors.mjs +13 -0
- package/lib/browser/utils/default-injectors.mjs.map +1 -0
- package/lib/browser/utils/get-injectable-token.d.mts +9 -0
- package/lib/browser/utils/get-injectable-token.d.mts.map +1 -0
- package/lib/browser/utils/get-injectable-token.mjs +13 -0
- package/lib/browser/utils/get-injectable-token.mjs.map +1 -0
- package/lib/browser/utils/get-injectors.d.mts +55 -0
- package/lib/browser/utils/get-injectors.d.mts.map +1 -0
- package/lib/browser/utils/get-injectors.mjs +121 -0
- package/lib/browser/utils/get-injectors.mjs.map +1 -0
- package/lib/browser/utils/types.d.mts +23 -0
- package/lib/browser/utils/types.d.mts.map +1 -0
- package/lib/{container-DAKOvAgr.mjs → container-8-z89TyQ.mjs} +1325 -1462
- package/lib/container-8-z89TyQ.mjs.map +1 -0
- package/lib/{container-Bp1W-pWJ.d.mts → container-CNiqesCL.d.mts} +598 -617
- package/lib/container-CNiqesCL.d.mts.map +1 -0
- package/lib/{container-DENMeJ87.cjs → container-CaY2fDuk.cjs} +1369 -1512
- package/lib/container-CaY2fDuk.cjs.map +1 -0
- package/lib/{container-YPwvmlK2.d.cts → container-D-0Ho3qL.d.cts} +598 -612
- package/lib/container-D-0Ho3qL.d.cts.map +1 -0
- package/lib/index.cjs +13 -15
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +58 -223
- package/lib/index.d.cts.map +1 -1
- package/lib/index.d.mts +62 -222
- package/lib/index.d.mts.map +1 -1
- package/lib/index.mjs +5 -6
- package/lib/index.mjs.map +1 -1
- package/lib/testing/index.cjs +569 -311
- package/lib/testing/index.cjs.map +1 -1
- package/lib/testing/index.d.cts +370 -41
- package/lib/testing/index.d.cts.map +1 -1
- package/lib/testing/index.d.mts +370 -41
- package/lib/testing/index.d.mts.map +1 -1
- package/lib/testing/index.mjs +568 -305
- package/lib/testing/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/circular-detector.spec.mts +193 -0
- package/src/__tests__/concurrent.spec.mts +368 -0
- package/src/__tests__/container.spec.mts +32 -30
- package/src/__tests__/di-error.spec.mts +351 -0
- package/src/__tests__/e2e.browser.spec.mts +0 -4
- package/src/__tests__/e2e.spec.mts +10 -19
- package/src/__tests__/event-emitter.spec.mts +232 -109
- package/src/__tests__/get-injectors.spec.mts +250 -39
- package/src/__tests__/injection-token.spec.mts +293 -349
- package/src/__tests__/library-findings.spec.mts +8 -8
- package/src/__tests__/registry.spec.mts +358 -210
- package/src/__tests__/resolution-context.spec.mts +255 -0
- package/src/__tests__/scope-tracker.spec.mts +598 -0
- package/src/__tests__/scope-upgrade.spec.mts +808 -0
- package/src/__tests__/scoped-container.spec.mts +595 -0
- package/src/__tests__/test-container.spec.mts +293 -0
- package/src/__tests__/token-resolver.spec.mts +207 -0
- package/src/__tests__/unified-storage.spec.mts +535 -0
- package/src/__tests__/unit-test-container.spec.mts +405 -0
- package/src/__type-tests__/container.spec-d.mts +180 -0
- package/src/__type-tests__/factory.spec-d.mts +15 -3
- package/src/__type-tests__/inject.spec-d.mts +115 -20
- package/src/__type-tests__/injectable.spec-d.mts +69 -52
- package/src/__type-tests__/injection-token.spec-d.mts +176 -0
- package/src/__type-tests__/scoped-container.spec-d.mts +212 -0
- package/src/container/abstract-container.mts +327 -0
- package/src/container/container.mts +142 -170
- package/src/container/scoped-container.mts +126 -208
- package/src/decorators/factory.decorator.mts +16 -11
- package/src/decorators/injectable.decorator.mts +20 -16
- package/src/enums/index.mts +2 -2
- package/src/enums/injectable-scope.enum.mts +1 -0
- package/src/enums/injectable-type.enum.mts +1 -0
- package/src/errors/di-error.mts +96 -0
- package/src/event-emitter.mts +3 -27
- package/src/index.mts +6 -153
- package/src/interfaces/container.interface.mts +13 -0
- package/src/interfaces/factory.interface.mts +1 -1
- package/src/interfaces/index.mts +1 -1
- package/src/internal/context/async-local-storage.mts +3 -2
- package/src/internal/context/async-local-storage.types.mts +1 -0
- package/src/internal/context/factory-context.mts +1 -0
- package/src/internal/context/index.mts +3 -1
- package/src/internal/context/resolution-context.mts +1 -0
- package/src/internal/context/service-initialization-context.mts +43 -0
- package/src/internal/core/index.mts +5 -4
- package/src/internal/core/instance-resolver.mts +460 -302
- package/src/internal/core/name-resolver.mts +196 -0
- package/src/internal/core/scope-tracker.mts +242 -0
- package/src/internal/core/{instantiator.mts → service-initializer.mts} +51 -29
- package/src/internal/core/service-invalidator.mts +290 -0
- package/src/internal/core/token-resolver.mts +122 -0
- package/src/internal/holder/holder-storage.interface.mts +11 -5
- package/src/internal/holder/index.mts +2 -5
- package/src/internal/holder/instance-holder.mts +1 -3
- package/src/internal/holder/unified-storage.mts +245 -0
- package/src/internal/index.mts +2 -1
- package/src/internal/lifecycle/circular-detector.mts +1 -0
- package/src/internal/lifecycle/index.mts +1 -1
- package/src/internal/lifecycle/lifecycle-event-bus.mts +1 -0
- package/src/internal/stub-factory-class.mts +16 -0
- package/src/symbols/injectable-token.mts +3 -1
- package/src/testing/index.mts +2 -0
- package/src/testing/test-container.mts +546 -85
- package/src/testing/types.mts +117 -0
- package/src/testing/unit-test-container.mts +509 -0
- package/src/token/injection-token.mts +41 -4
- package/src/token/registry.mts +75 -9
- package/src/utils/default-injectors.mts +16 -0
- package/src/utils/get-injectable-token.mts +2 -3
- package/src/utils/get-injectors.mts +26 -15
- package/src/utils/index.mts +3 -1
- package/src/utils/types.mts +1 -0
- package/tsdown.config.mts +11 -1
- package/lib/browser/index.d.mts.map +0 -1
- package/lib/browser/index.mjs.map +0 -1
- package/lib/container-Bp1W-pWJ.d.mts.map +0 -1
- package/lib/container-DAKOvAgr.mjs.map +0 -1
- package/lib/container-DENMeJ87.cjs.map +0 -1
- package/lib/container-YPwvmlK2.d.cts.map +0 -1
- package/src/__tests__/async-local-storage.browser.spec.mts +0 -166
- package/src/__tests__/async-local-storage.spec.mts +0 -333
- package/src/__tests__/errors.spec.mts +0 -87
- package/src/__tests__/factory.spec.mts +0 -137
- package/src/__tests__/injectable.spec.mts +0 -246
- package/src/__tests__/request-scope.spec.mts +0 -416
- package/src/__tests__/service-instantiator.spec.mts +0 -410
- package/src/__tests__/service-locator-event-bus.spec.mts +0 -242
- package/src/__tests__/service-locator-manager.spec.mts +0 -300
- package/src/__tests__/service-locator.spec.mts +0 -966
- package/src/__tests__/unified-api.spec.mts +0 -130
- package/src/browser.mts +0 -11
- package/src/injectors.mts +0 -18
- package/src/internal/context/request-context.mts +0 -225
- package/src/internal/core/invalidator.mts +0 -437
- package/src/internal/core/service-locator.mts +0 -202
- package/src/internal/core/token-processor.mts +0 -252
- package/src/internal/holder/base-holder-manager.mts +0 -334
- package/src/internal/holder/holder-manager.mts +0 -85
- package/src/internal/holder/request-storage.mts +0 -127
- package/src/internal/holder/singleton-storage.mts +0 -92
- package/src/testing/README.md +0 -80
- package/src/testing/__tests__/test-container.spec.mts +0 -173
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for scope upgrade behavior in the Container.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that ScopeTracker correctly upgrades Singleton services
|
|
5
|
+
* to Request scope when they depend on Request-scoped services.
|
|
6
|
+
*
|
|
7
|
+
* Key scenarios:
|
|
8
|
+
* 1. Simple Singleton -> Request upgrade
|
|
9
|
+
* 2. Complex chains: Singleton -> Request -> Singleton -> Request
|
|
10
|
+
* 3. Transient breaking the upgrade chain
|
|
11
|
+
* 4. Storage movement and registry updates
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
15
|
+
|
|
16
|
+
import { Container } from '../container/container.mjs'
|
|
17
|
+
import { Injectable } from '../decorators/injectable.decorator.mjs'
|
|
18
|
+
import { InjectableScope } from '../enums/index.mjs'
|
|
19
|
+
import { Registry } from '../token/registry.mjs'
|
|
20
|
+
import { getInjectableToken } from '../utils/get-injectable-token.mjs'
|
|
21
|
+
import { getInjectors } from '../utils/get-injectors.mjs'
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// TEST UTILITIES
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
function createTestSetup() {
|
|
28
|
+
const registry = new Registry()
|
|
29
|
+
const injectors = getInjectors()
|
|
30
|
+
const container = new Container(registry, null, injectors)
|
|
31
|
+
|
|
32
|
+
return { registry, injectors, container }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// SECTION 1: SIMPLE SINGLETON -> REQUEST UPGRADE
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
describe('Scope Upgrade: Simple Singleton -> Request', () => {
|
|
40
|
+
let registry: Registry
|
|
41
|
+
let container: Container
|
|
42
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
const setup = createTestSetup()
|
|
46
|
+
registry = setup.registry
|
|
47
|
+
container = setup.container
|
|
48
|
+
injectors = setup.injectors
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await container.dispose()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('Basic upgrade behavior', () => {
|
|
56
|
+
it('should upgrade Singleton to Request when it depends on Request-scoped service', async () => {
|
|
57
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
58
|
+
class RequestService {
|
|
59
|
+
id = Math.random()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
63
|
+
class SingletonWithRequestDep {
|
|
64
|
+
private requestService = injectors.inject(RequestService)
|
|
65
|
+
|
|
66
|
+
getRequestService() {
|
|
67
|
+
return this.requestService
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const token = getInjectableToken(SingletonWithRequestDep)
|
|
72
|
+
|
|
73
|
+
// Initially registered as Singleton
|
|
74
|
+
expect(registry.get(token).scope).toBe(InjectableScope.Singleton)
|
|
75
|
+
|
|
76
|
+
// Resolve within request context
|
|
77
|
+
const scoped = container.beginRequest('request-1')
|
|
78
|
+
await scoped.get(SingletonWithRequestDep)
|
|
79
|
+
|
|
80
|
+
// After resolution, the scope should be upgraded to Request
|
|
81
|
+
expect(registry.get(token).scope).toBe(InjectableScope.Request)
|
|
82
|
+
|
|
83
|
+
await scoped.endRequest()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should create different instances for different requests after upgrade', async () => {
|
|
87
|
+
let singletonInstanceCount = 0
|
|
88
|
+
|
|
89
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
90
|
+
class RequestService {
|
|
91
|
+
id = Math.random()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
95
|
+
class SingletonWithRequestDep {
|
|
96
|
+
instanceId = ++singletonInstanceCount
|
|
97
|
+
private requestService = injectors.inject(RequestService)
|
|
98
|
+
|
|
99
|
+
getRequestService() {
|
|
100
|
+
return this.requestService
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// First request
|
|
105
|
+
const scoped1 = container.beginRequest('request-1')
|
|
106
|
+
const instance1 = await scoped1.get(SingletonWithRequestDep)
|
|
107
|
+
|
|
108
|
+
// Second request
|
|
109
|
+
const scoped2 = container.beginRequest('request-2')
|
|
110
|
+
const instance2 = await scoped2.get(SingletonWithRequestDep)
|
|
111
|
+
|
|
112
|
+
// After scope upgrade, each request should get its own instance
|
|
113
|
+
expect(instance1.instanceId).not.toBe(instance2.instanceId)
|
|
114
|
+
|
|
115
|
+
await scoped1.endRequest()
|
|
116
|
+
await scoped2.endRequest()
|
|
117
|
+
})
|
|
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
|
+
it('should return same instance within the same request after upgrade', async () => {
|
|
129
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
130
|
+
class RequestService {
|
|
131
|
+
id = Math.random()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
135
|
+
class SingletonWithRequestDep {
|
|
136
|
+
id = Math.random()
|
|
137
|
+
private requestService = injectors.inject(RequestService)
|
|
138
|
+
|
|
139
|
+
getRequestService() {
|
|
140
|
+
return this.requestService
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const scoped = container.beginRequest('request-1')
|
|
145
|
+
|
|
146
|
+
// First resolution triggers the scope upgrade
|
|
147
|
+
const instance1 = await scoped.get(SingletonWithRequestDep)
|
|
148
|
+
|
|
149
|
+
// The scope upgrade happens during the first resolution.
|
|
150
|
+
// After the scope is upgraded in the registry, subsequent resolutions
|
|
151
|
+
// will correctly use request storage and return the same instance.
|
|
152
|
+
const instance2 = await scoped.get(SingletonWithRequestDep)
|
|
153
|
+
|
|
154
|
+
// The instance should be the same after upgrade
|
|
155
|
+
expect(instance1.id).toBe(instance2.id)
|
|
156
|
+
expect(instance1).toBe(instance2)
|
|
157
|
+
|
|
158
|
+
await scoped.endRequest()
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('Storage verification', () => {
|
|
163
|
+
it('should move holder from singleton to request storage', async () => {
|
|
164
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
165
|
+
class RequestService {
|
|
166
|
+
id = Math.random()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
170
|
+
class SingletonWithRequestDep {
|
|
171
|
+
private requestService = injectors.inject(RequestService)
|
|
172
|
+
|
|
173
|
+
getRequestService() {
|
|
174
|
+
return this.requestService
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const token = getInjectableToken(SingletonWithRequestDep)
|
|
179
|
+
const singletonStorage = container.getStorage()
|
|
180
|
+
|
|
181
|
+
const scoped = container.beginRequest('request-1')
|
|
182
|
+
const requestStorage = scoped.getStorage()
|
|
183
|
+
|
|
184
|
+
// Before resolution
|
|
185
|
+
const instanceNameBefore = container
|
|
186
|
+
.getNameResolver()
|
|
187
|
+
.generateInstanceName(
|
|
188
|
+
token,
|
|
189
|
+
undefined,
|
|
190
|
+
undefined,
|
|
191
|
+
InjectableScope.Singleton,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Resolve
|
|
195
|
+
await scoped.get(SingletonWithRequestDep)
|
|
196
|
+
|
|
197
|
+
// After resolution, the holder should NOT be in singleton storage with old name
|
|
198
|
+
const singletonResult = singletonStorage.get(instanceNameBefore)
|
|
199
|
+
expect(singletonResult).toBeNull()
|
|
200
|
+
|
|
201
|
+
// It should be in request storage with new name
|
|
202
|
+
const instanceNameAfter = container
|
|
203
|
+
.getNameResolver()
|
|
204
|
+
.generateInstanceName(
|
|
205
|
+
token,
|
|
206
|
+
undefined,
|
|
207
|
+
'request-1',
|
|
208
|
+
InjectableScope.Request,
|
|
209
|
+
)
|
|
210
|
+
const requestResult = requestStorage.get(instanceNameAfter)
|
|
211
|
+
expect(requestResult).not.toBeNull()
|
|
212
|
+
|
|
213
|
+
await scoped.endRequest()
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// SECTION 2: COMPLEX DEPENDENCY CHAINS
|
|
220
|
+
// ============================================================================
|
|
221
|
+
|
|
222
|
+
describe('Scope Upgrade: Complex Chains', () => {
|
|
223
|
+
let registry: Registry
|
|
224
|
+
let container: Container
|
|
225
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
226
|
+
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
const setup = createTestSetup()
|
|
229
|
+
registry = setup.registry
|
|
230
|
+
container = setup.container
|
|
231
|
+
injectors = setup.injectors
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
afterEach(async () => {
|
|
235
|
+
await container.dispose()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe('Singleton -> Request -> Singleton -> Request chain', () => {
|
|
239
|
+
it('should upgrade all Singletons in the chain that depend on Request-scoped', async () => {
|
|
240
|
+
// Level 4: Request-scoped (bottom of chain)
|
|
241
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
242
|
+
class RequestLevel4 {
|
|
243
|
+
id = Math.random()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Level 3: Singleton that depends on Request -> should upgrade
|
|
247
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
248
|
+
class SingletonLevel3 {
|
|
249
|
+
id = Math.random()
|
|
250
|
+
private dep = injectors.inject(RequestLevel4)
|
|
251
|
+
getDep() {
|
|
252
|
+
return this.dep
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Level 2: Request-scoped
|
|
257
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
258
|
+
class RequestLevel2 {
|
|
259
|
+
id = Math.random()
|
|
260
|
+
private dep = injectors.inject(SingletonLevel3)
|
|
261
|
+
getDep() {
|
|
262
|
+
return this.dep
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Level 1: Singleton that depends on Request -> should upgrade
|
|
267
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
268
|
+
class SingletonLevel1 {
|
|
269
|
+
id = Math.random()
|
|
270
|
+
private dep = injectors.inject(RequestLevel2)
|
|
271
|
+
getDep() {
|
|
272
|
+
return this.dep
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const token1 = getInjectableToken(SingletonLevel1)
|
|
277
|
+
const token3 = getInjectableToken(SingletonLevel3)
|
|
278
|
+
|
|
279
|
+
// Initially both Singletons are Singleton scoped
|
|
280
|
+
expect(registry.get(token1).scope).toBe(InjectableScope.Singleton)
|
|
281
|
+
expect(registry.get(token3).scope).toBe(InjectableScope.Singleton)
|
|
282
|
+
|
|
283
|
+
// Resolve within request context
|
|
284
|
+
const scoped = container.beginRequest('request-1')
|
|
285
|
+
await scoped.get(SingletonLevel1)
|
|
286
|
+
|
|
287
|
+
// Both Singletons should be upgraded to Request scope
|
|
288
|
+
expect(registry.get(token1).scope).toBe(InjectableScope.Request)
|
|
289
|
+
expect(registry.get(token3).scope).toBe(InjectableScope.Request)
|
|
290
|
+
|
|
291
|
+
await scoped.endRequest()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should create isolated instances for different requests in complex chain', async () => {
|
|
295
|
+
let level1Count = 0
|
|
296
|
+
let level3Count = 0
|
|
297
|
+
|
|
298
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
299
|
+
class RequestLevel4 {
|
|
300
|
+
id = Math.random()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
304
|
+
class SingletonLevel3 {
|
|
305
|
+
instanceId = ++level3Count
|
|
306
|
+
private dep = injectors.inject(RequestLevel4)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
310
|
+
class RequestLevel2 {
|
|
311
|
+
id = Math.random()
|
|
312
|
+
private dep = injectors.inject(SingletonLevel3)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
316
|
+
class SingletonLevel1 {
|
|
317
|
+
instanceId = ++level1Count
|
|
318
|
+
private dep = injectors.inject(RequestLevel2)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// First request
|
|
322
|
+
const scoped1 = container.beginRequest('request-1')
|
|
323
|
+
const instance1 = await scoped1.get(SingletonLevel1)
|
|
324
|
+
|
|
325
|
+
// Second request
|
|
326
|
+
const scoped2 = container.beginRequest('request-2')
|
|
327
|
+
const instance2 = await scoped2.get(SingletonLevel1)
|
|
328
|
+
|
|
329
|
+
// Each request should have its own instances after upgrade
|
|
330
|
+
expect(instance1.instanceId).not.toBe(instance2.instanceId)
|
|
331
|
+
|
|
332
|
+
await scoped1.endRequest()
|
|
333
|
+
await scoped2.endRequest()
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
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
|
+
it('should upgrade all dependent Singletons', async () => {
|
|
348
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
349
|
+
class SharedRequestService {
|
|
350
|
+
id = Math.random()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
354
|
+
class SingletonA {
|
|
355
|
+
id = Math.random()
|
|
356
|
+
private shared = injectors.inject(SharedRequestService)
|
|
357
|
+
getShared() {
|
|
358
|
+
return this.shared
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
363
|
+
class SingletonB {
|
|
364
|
+
id = Math.random()
|
|
365
|
+
private shared = injectors.inject(SharedRequestService)
|
|
366
|
+
getShared() {
|
|
367
|
+
return this.shared
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const tokenA = getInjectableToken(SingletonA)
|
|
372
|
+
const tokenB = getInjectableToken(SingletonB)
|
|
373
|
+
|
|
374
|
+
expect(registry.get(tokenA).scope).toBe(InjectableScope.Singleton)
|
|
375
|
+
expect(registry.get(tokenB).scope).toBe(InjectableScope.Singleton)
|
|
376
|
+
|
|
377
|
+
const scoped = container.beginRequest('request-1')
|
|
378
|
+
await scoped.get(SingletonA)
|
|
379
|
+
await scoped.get(SingletonB)
|
|
380
|
+
|
|
381
|
+
// Both should be upgraded
|
|
382
|
+
expect(registry.get(tokenA).scope).toBe(InjectableScope.Request)
|
|
383
|
+
expect(registry.get(tokenB).scope).toBe(InjectableScope.Request)
|
|
384
|
+
|
|
385
|
+
await scoped.endRequest()
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('should share the same Request-scoped instance within a request', async () => {
|
|
389
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
390
|
+
class SharedRequestService {
|
|
391
|
+
id = Math.random()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
395
|
+
class SingletonA {
|
|
396
|
+
private shared = injectors.inject(SharedRequestService)
|
|
397
|
+
getShared() {
|
|
398
|
+
return this.shared
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
403
|
+
class SingletonB {
|
|
404
|
+
private shared = injectors.inject(SharedRequestService)
|
|
405
|
+
getShared() {
|
|
406
|
+
return this.shared
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const scoped = container.beginRequest('request-1')
|
|
411
|
+
const a = await scoped.get(SingletonA)
|
|
412
|
+
const b = await scoped.get(SingletonB)
|
|
413
|
+
|
|
414
|
+
// Both should share the same Request-scoped instance
|
|
415
|
+
const sharedFromA = a.getShared()
|
|
416
|
+
const sharedFromB = b.getShared()
|
|
417
|
+
expect(sharedFromA.id).toBe(sharedFromB.id)
|
|
418
|
+
|
|
419
|
+
await scoped.endRequest()
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// SECTION 3: TRANSIENT BREAKING THE CHAIN
|
|
426
|
+
// ============================================================================
|
|
427
|
+
|
|
428
|
+
describe('Scope Upgrade: Transient Breaking Chain', () => {
|
|
429
|
+
let registry: Registry
|
|
430
|
+
let container: Container
|
|
431
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
432
|
+
|
|
433
|
+
beforeEach(() => {
|
|
434
|
+
const setup = createTestSetup()
|
|
435
|
+
registry = setup.registry
|
|
436
|
+
container = setup.container
|
|
437
|
+
injectors = setup.injectors
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
afterEach(async () => {
|
|
441
|
+
await container.dispose()
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
describe('Singleton -> Transient -> Request chain', () => {
|
|
445
|
+
it('should NOT upgrade Singleton when Transient is in between', async () => {
|
|
446
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
447
|
+
class RequestService {
|
|
448
|
+
id = Math.random()
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
@Injectable({ scope: InjectableScope.Transient, registry })
|
|
452
|
+
class TransientMiddle {
|
|
453
|
+
id = Math.random()
|
|
454
|
+
private requestService = injectors.inject(RequestService)
|
|
455
|
+
getRequestService() {
|
|
456
|
+
return this.requestService
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
461
|
+
class SingletonTop {
|
|
462
|
+
id = Math.random()
|
|
463
|
+
private transient = injectors.inject(TransientMiddle)
|
|
464
|
+
getTransient() {
|
|
465
|
+
return this.transient
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const singletonToken = getInjectableToken(SingletonTop)
|
|
470
|
+
|
|
471
|
+
// Initially Singleton
|
|
472
|
+
expect(registry.get(singletonToken).scope).toBe(InjectableScope.Singleton)
|
|
473
|
+
|
|
474
|
+
const scoped = container.beginRequest('request-1')
|
|
475
|
+
await scoped.get(SingletonTop)
|
|
476
|
+
|
|
477
|
+
// Singleton should NOT be upgraded because Transient breaks the chain
|
|
478
|
+
// Transient services are created fresh each time and don't trigger scope upgrade
|
|
479
|
+
expect(registry.get(singletonToken).scope).toBe(InjectableScope.Singleton)
|
|
480
|
+
|
|
481
|
+
await scoped.endRequest()
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('should keep Singleton shared across requests when Transient breaks chain', async () => {
|
|
485
|
+
let singletonInstanceCount = 0
|
|
486
|
+
|
|
487
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
488
|
+
class RequestService {
|
|
489
|
+
id = Math.random()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@Injectable({ scope: InjectableScope.Transient, registry })
|
|
493
|
+
class TransientMiddle {
|
|
494
|
+
private requestService = injectors.inject(RequestService)
|
|
495
|
+
getRequestService() {
|
|
496
|
+
return this.requestService
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
501
|
+
class SingletonTop {
|
|
502
|
+
instanceId = ++singletonInstanceCount
|
|
503
|
+
private transient = injectors.inject(TransientMiddle)
|
|
504
|
+
getTransient() {
|
|
505
|
+
return this.transient
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// First request
|
|
510
|
+
const scoped1 = container.beginRequest('request-1')
|
|
511
|
+
const instance1 = await scoped1.get(SingletonTop)
|
|
512
|
+
|
|
513
|
+
// Second request
|
|
514
|
+
const scoped2 = container.beginRequest('request-2')
|
|
515
|
+
const instance2 = await scoped2.get(SingletonTop)
|
|
516
|
+
|
|
517
|
+
// Same Singleton instance should be returned (no upgrade happened)
|
|
518
|
+
expect(instance1.instanceId).toBe(instance2.instanceId)
|
|
519
|
+
expect(instance1).toBe(instance2)
|
|
520
|
+
|
|
521
|
+
await scoped1.endRequest()
|
|
522
|
+
await scoped2.endRequest()
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it('should create new Transient instances for each resolution', async () => {
|
|
526
|
+
let transientCount = 0
|
|
527
|
+
|
|
528
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
529
|
+
class RequestService {
|
|
530
|
+
id = Math.random()
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
@Injectable({ scope: InjectableScope.Transient, registry })
|
|
534
|
+
class TransientMiddle {
|
|
535
|
+
instanceId = ++transientCount
|
|
536
|
+
private requestService = injectors.inject(RequestService)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
540
|
+
class SingletonTop {
|
|
541
|
+
private transient = injectors.inject(TransientMiddle)
|
|
542
|
+
getTransient() {
|
|
543
|
+
return this.transient
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const scoped = container.beginRequest('request-1')
|
|
548
|
+
|
|
549
|
+
const singleton = await scoped.get(SingletonTop)
|
|
550
|
+
const transient1 = singleton.getTransient()
|
|
551
|
+
|
|
552
|
+
// Get singleton again (same instance)
|
|
553
|
+
const singletonAgain = await scoped.get(SingletonTop)
|
|
554
|
+
singletonAgain.getTransient()
|
|
555
|
+
|
|
556
|
+
// Since injectors.inject() caches the reference, they should be the same
|
|
557
|
+
// But if we resolve TransientMiddle directly, it should be different
|
|
558
|
+
const transientDirect = await scoped.get(TransientMiddle)
|
|
559
|
+
expect(transientDirect.instanceId).not.toBe(transient1.instanceId)
|
|
560
|
+
|
|
561
|
+
await scoped.endRequest()
|
|
562
|
+
})
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
describe('Mixed chains with Transient', () => {
|
|
566
|
+
it('should handle Singleton -> Request -> Transient -> Request correctly', async () => {
|
|
567
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
568
|
+
class RequestBottom {
|
|
569
|
+
id = Math.random()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
@Injectable({ scope: InjectableScope.Transient, registry })
|
|
573
|
+
class TransientMiddle {
|
|
574
|
+
private bottom = injectors.inject(RequestBottom)
|
|
575
|
+
getBottom() {
|
|
576
|
+
return this.bottom
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
581
|
+
class RequestTop {
|
|
582
|
+
private transient = injectors.inject(TransientMiddle)
|
|
583
|
+
getTransient() {
|
|
584
|
+
return this.transient
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
589
|
+
class SingletonRoot {
|
|
590
|
+
private requestTop = injectors.inject(RequestTop)
|
|
591
|
+
getRequestTop() {
|
|
592
|
+
return this.requestTop
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const singletonToken = getInjectableToken(SingletonRoot)
|
|
597
|
+
|
|
598
|
+
// Singleton depends directly on Request (not Transient), so it should be upgraded
|
|
599
|
+
expect(registry.get(singletonToken).scope).toBe(InjectableScope.Singleton)
|
|
600
|
+
|
|
601
|
+
const scoped = container.beginRequest('request-1')
|
|
602
|
+
await scoped.get(SingletonRoot)
|
|
603
|
+
|
|
604
|
+
// Should be upgraded because it directly depends on Request-scoped
|
|
605
|
+
expect(registry.get(singletonToken).scope).toBe(InjectableScope.Request)
|
|
606
|
+
|
|
607
|
+
await scoped.endRequest()
|
|
608
|
+
})
|
|
609
|
+
})
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// SECTION 4: EDGE CASES AND ERROR SCENARIOS
|
|
614
|
+
// ============================================================================
|
|
615
|
+
|
|
616
|
+
describe('Scope Upgrade: Edge Cases', () => {
|
|
617
|
+
let registry: Registry
|
|
618
|
+
let container: Container
|
|
619
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
620
|
+
|
|
621
|
+
beforeEach(() => {
|
|
622
|
+
const setup = createTestSetup()
|
|
623
|
+
registry = setup.registry
|
|
624
|
+
container = setup.container
|
|
625
|
+
injectors = setup.injectors
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
afterEach(async () => {
|
|
629
|
+
await container.dispose()
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
describe('Singleton without Request dependencies', () => {
|
|
633
|
+
it('should NOT upgrade Singleton that only depends on Singletons', async () => {
|
|
634
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
635
|
+
class SingletonDep {
|
|
636
|
+
id = Math.random()
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
640
|
+
class SingletonMain {
|
|
641
|
+
private dep = injectors.inject(SingletonDep)
|
|
642
|
+
getDep() {
|
|
643
|
+
return this.dep
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const token = getInjectableToken(SingletonMain)
|
|
648
|
+
|
|
649
|
+
const scoped = container.beginRequest('request-1')
|
|
650
|
+
await scoped.get(SingletonMain)
|
|
651
|
+
|
|
652
|
+
// Should remain Singleton
|
|
653
|
+
expect(registry.get(token).scope).toBe(InjectableScope.Singleton)
|
|
654
|
+
|
|
655
|
+
await scoped.endRequest()
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
it('should NOT upgrade Singleton that only depends on Transients', async () => {
|
|
659
|
+
@Injectable({ scope: InjectableScope.Transient, registry })
|
|
660
|
+
class TransientDep {
|
|
661
|
+
id = Math.random()
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
665
|
+
class SingletonMain {
|
|
666
|
+
private dep = injectors.inject(TransientDep)
|
|
667
|
+
getDep() {
|
|
668
|
+
return this.dep
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const token = getInjectableToken(SingletonMain)
|
|
673
|
+
|
|
674
|
+
const scoped = container.beginRequest('request-1')
|
|
675
|
+
await scoped.get(SingletonMain)
|
|
676
|
+
|
|
677
|
+
// Should remain Singleton
|
|
678
|
+
expect(registry.get(token).scope).toBe(InjectableScope.Singleton)
|
|
679
|
+
|
|
680
|
+
await scoped.endRequest()
|
|
681
|
+
})
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
describe('Resolving from main container vs scoped container', () => {
|
|
685
|
+
it('should throw when resolving Request-scoped from main container', async () => {
|
|
686
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
687
|
+
class RequestService {
|
|
688
|
+
id = Math.random()
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Should throw - Request services need a request context
|
|
692
|
+
await expect(container.get(RequestService)).rejects.toThrow()
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
it('should resolve Singleton from main container without upgrade', async () => {
|
|
696
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
697
|
+
class PureSingleton {
|
|
698
|
+
id = Math.random()
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const token = getInjectableToken(PureSingleton)
|
|
702
|
+
|
|
703
|
+
const instance = await container.get(PureSingleton)
|
|
704
|
+
expect(instance).toBeDefined()
|
|
705
|
+
expect(registry.get(token).scope).toBe(InjectableScope.Singleton)
|
|
706
|
+
})
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
describe('Concurrent request handling with upgrades', () => {
|
|
710
|
+
it('should handle concurrent requests with scope upgrades correctly', async () => {
|
|
711
|
+
let instanceCount = 0
|
|
712
|
+
|
|
713
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
714
|
+
class RequestService {
|
|
715
|
+
id = Math.random()
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
719
|
+
class SingletonWithRequestDep {
|
|
720
|
+
instanceId = ++instanceCount
|
|
721
|
+
private requestService = injectors.inject(RequestService)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Start multiple concurrent requests
|
|
725
|
+
const scoped1 = container.beginRequest('request-1')
|
|
726
|
+
const scoped2 = container.beginRequest('request-2')
|
|
727
|
+
const scoped3 = container.beginRequest('request-3')
|
|
728
|
+
|
|
729
|
+
const [instance1, instance2, instance3] = await Promise.all([
|
|
730
|
+
scoped1.get(SingletonWithRequestDep),
|
|
731
|
+
scoped2.get(SingletonWithRequestDep),
|
|
732
|
+
scoped3.get(SingletonWithRequestDep),
|
|
733
|
+
])
|
|
734
|
+
|
|
735
|
+
// After upgrade, each request should have its own instance
|
|
736
|
+
const ids = [
|
|
737
|
+
instance1.instanceId,
|
|
738
|
+
instance2.instanceId,
|
|
739
|
+
instance3.instanceId,
|
|
740
|
+
]
|
|
741
|
+
const uniqueIds = new Set(ids)
|
|
742
|
+
expect(uniqueIds.size).toBe(3)
|
|
743
|
+
|
|
744
|
+
await Promise.all([
|
|
745
|
+
scoped1.endRequest(),
|
|
746
|
+
scoped2.endRequest(),
|
|
747
|
+
scoped3.endRequest(),
|
|
748
|
+
])
|
|
749
|
+
})
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
describe('Invalidation after scope upgrade', () => {
|
|
753
|
+
it('should properly invalidate upgraded services', async () => {
|
|
754
|
+
let instanceCount = 0
|
|
755
|
+
|
|
756
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
757
|
+
class RequestService {
|
|
758
|
+
id = Math.random()
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
762
|
+
class SingletonWithRequestDep {
|
|
763
|
+
instanceId = ++instanceCount
|
|
764
|
+
private requestService = injectors.inject(RequestService)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const scoped = container.beginRequest('request-1')
|
|
768
|
+
const instance1 = await scoped.get(SingletonWithRequestDep)
|
|
769
|
+
|
|
770
|
+
await scoped.invalidate(instance1)
|
|
771
|
+
|
|
772
|
+
const instance2 = await scoped.get(SingletonWithRequestDep)
|
|
773
|
+
|
|
774
|
+
// Should be a new instance after invalidation
|
|
775
|
+
expect(instance1.instanceId).not.toBe(instance2.instanceId)
|
|
776
|
+
|
|
777
|
+
await scoped.endRequest()
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
it('should destroy upgraded services when request ends', async () => {
|
|
781
|
+
let destroyCount = 0
|
|
782
|
+
|
|
783
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
784
|
+
class RequestService {
|
|
785
|
+
id = Math.random()
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
789
|
+
class SingletonWithRequestDep {
|
|
790
|
+
private requestService = injectors.inject(RequestService)
|
|
791
|
+
|
|
792
|
+
onServiceDestroy() {
|
|
793
|
+
destroyCount++
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const scoped = container.beginRequest('request-1')
|
|
798
|
+
await scoped.get(SingletonWithRequestDep)
|
|
799
|
+
|
|
800
|
+
expect(destroyCount).toBe(0)
|
|
801
|
+
|
|
802
|
+
await scoped.endRequest()
|
|
803
|
+
|
|
804
|
+
// Service should be destroyed when request ends (since it's now Request-scoped)
|
|
805
|
+
expect(destroyCount).toBe(1)
|
|
806
|
+
})
|
|
807
|
+
})
|
|
808
|
+
})
|