@navios/di 0.7.1 → 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 +110 -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 -1508
- package/lib/browser/index.mjs +29 -2650
- 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-Pb_Y4Z4x.mjs → container-8-z89TyQ.mjs} +1269 -1305
- package/lib/container-8-z89TyQ.mjs.map +1 -0
- package/lib/{container-BuAutHGg.d.mts → container-CNiqesCL.d.mts} +600 -569
- package/lib/container-CNiqesCL.d.mts.map +1 -0
- package/lib/{container-DnzgpfBe.cjs → container-CaY2fDuk.cjs} +1287 -1329
- package/lib/container-CaY2fDuk.cjs.map +1 -0
- package/lib/{container-oGTgX2iX.d.cts → container-D-0Ho3qL.d.cts} +601 -565
- 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 +461 -292
- 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-processor.mts → token-resolver.mts} +17 -88
- 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-BuAutHGg.d.mts.map +0 -1
- package/lib/container-DnzgpfBe.cjs.map +0 -1
- package/lib/container-Pb_Y4Z4x.mjs.map +0 -1
- package/lib/container-oGTgX2iX.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 -214
- package/src/internal/core/invalidator.mts +0 -437
- package/src/internal/core/service-locator.mts +0 -202
- package/src/internal/holder/base-holder-manager.mts +0 -238
- package/src/internal/holder/holder-manager.mts +0 -85
- package/src/internal/holder/request-storage.mts +0 -134
- package/src/internal/holder/singleton-storage.mts +0 -105
- package/src/testing/README.md +0 -80
- package/src/testing/__tests__/test-container.spec.mts +0 -173
|
@@ -1,966 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { z } from 'zod/v4'
|
|
3
|
-
|
|
4
|
-
import type { OnServiceDestroy } from '../index.mjs'
|
|
5
|
-
|
|
6
|
-
import { Container } from '../container/container.mjs'
|
|
7
|
-
import { Injectable } from '../decorators/injectable.decorator.mjs'
|
|
8
|
-
import { InjectableScope } from '../enums/index.mjs'
|
|
9
|
-
import { getInjectableToken } from '../index.mjs'
|
|
10
|
-
import { asyncInject, inject } from '../injectors.mjs'
|
|
11
|
-
import { ServiceLocator } from '../internal/core/service-locator.mjs'
|
|
12
|
-
import { InjectionToken } from '../token/injection-token.mjs'
|
|
13
|
-
import { globalRegistry } from '../token/registry.mjs'
|
|
14
|
-
|
|
15
|
-
describe('ServiceLocator', () => {
|
|
16
|
-
describe('getInstanceIdentifier', () => {
|
|
17
|
-
it('should be possible to simple token', () => {
|
|
18
|
-
const serviceLocator = new ServiceLocator()
|
|
19
|
-
const token = InjectionToken.create('test')
|
|
20
|
-
const identifier = serviceLocator.getInstanceIdentifier(token)
|
|
21
|
-
expect(identifier).toBe(`test(${token.id})`)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('should be possible to bound token', () => {
|
|
25
|
-
const serviceLocator = new ServiceLocator()
|
|
26
|
-
const token = InjectionToken.create('test')
|
|
27
|
-
const identifier = serviceLocator.getInstanceIdentifier(
|
|
28
|
-
InjectionToken.bound(token, {
|
|
29
|
-
test: 'test',
|
|
30
|
-
}),
|
|
31
|
-
)
|
|
32
|
-
expect(identifier).toBe(`test(${token.id}):test=test`)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('should be possible to bound token with function', () => {
|
|
36
|
-
const serviceLocator = new ServiceLocator()
|
|
37
|
-
const token = InjectionToken.create('test')
|
|
38
|
-
const identifier = serviceLocator.getInstanceIdentifier(
|
|
39
|
-
InjectionToken.bound(token, { test: () => 'test' }),
|
|
40
|
-
)
|
|
41
|
-
expect(identifier).toBe(`test(${token.id}):test=fn_test(0)`)
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
describe('clearAll', () => {
|
|
46
|
-
let container: Container
|
|
47
|
-
let mockLogger: Console
|
|
48
|
-
|
|
49
|
-
beforeEach(() => {
|
|
50
|
-
mockLogger = {
|
|
51
|
-
log: vi.fn(),
|
|
52
|
-
warn: vi.fn(),
|
|
53
|
-
error: vi.fn(),
|
|
54
|
-
debug: vi.fn(),
|
|
55
|
-
trace: vi.fn(),
|
|
56
|
-
} as any
|
|
57
|
-
|
|
58
|
-
container = new Container(globalRegistry, mockLogger)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('should clear all services gracefully', async () => {
|
|
62
|
-
const serviceLocator = container.getServiceLocator()
|
|
63
|
-
|
|
64
|
-
// Create Injectable services
|
|
65
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
66
|
-
class ServiceA {
|
|
67
|
-
name = 'ServiceA'
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
71
|
-
class ServiceB {
|
|
72
|
-
name = 'ServiceB'
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
76
|
-
class ServiceC {
|
|
77
|
-
name = 'ServiceC'
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Create instances using container
|
|
81
|
-
await container.get(ServiceA)
|
|
82
|
-
await container.get(ServiceB)
|
|
83
|
-
await container.get(ServiceC)
|
|
84
|
-
|
|
85
|
-
// Verify services exist (container also registers itself as +1)
|
|
86
|
-
expect(serviceLocator.getManager().size()).toBe(4)
|
|
87
|
-
|
|
88
|
-
// Clear all services
|
|
89
|
-
await serviceLocator.clearAll()
|
|
90
|
-
|
|
91
|
-
// Verify all services are cleared
|
|
92
|
-
expect(serviceLocator.getManager().size()).toBe(0)
|
|
93
|
-
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
94
|
-
'[Invalidator] Graceful clearing completed',
|
|
95
|
-
)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('should handle empty service locator', async () => {
|
|
99
|
-
const serviceLocator = new ServiceLocator(globalRegistry, mockLogger)
|
|
100
|
-
await serviceLocator.clearAll()
|
|
101
|
-
|
|
102
|
-
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
103
|
-
'[Invalidator] No services to clear',
|
|
104
|
-
)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('should clear service from a request context', async () => {
|
|
108
|
-
const serviceLocator = container.getServiceLocator()
|
|
109
|
-
|
|
110
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
111
|
-
class ServiceA {
|
|
112
|
-
name = 'ServiceA'
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
116
|
-
class ServiceB {
|
|
117
|
-
serviceA = inject(ServiceA)
|
|
118
|
-
name = 'ServiceB'
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const requestId = 'test-request'
|
|
122
|
-
const scoped = container.beginRequest(requestId)
|
|
123
|
-
const serviceB = await scoped.get(ServiceB)
|
|
124
|
-
expect(serviceB).toBeDefined()
|
|
125
|
-
|
|
126
|
-
await scoped.invalidate(await container.get(ServiceA))
|
|
127
|
-
// Container itself remains
|
|
128
|
-
expect(serviceLocator.getManager().size()).toBe(1)
|
|
129
|
-
await serviceLocator.clearAll()
|
|
130
|
-
await scoped.endRequest()
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('should clear request contexts via ScopedContainer', async () => {
|
|
134
|
-
// Create a request context
|
|
135
|
-
const requestId = 'test-request'
|
|
136
|
-
const scoped = container.beginRequest(requestId)
|
|
137
|
-
|
|
138
|
-
// Create Injectable service with request scope
|
|
139
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
140
|
-
class TestService {
|
|
141
|
-
name = 'TestService'
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
await scoped.get(TestService)
|
|
145
|
-
|
|
146
|
-
// Verify request context exists
|
|
147
|
-
expect(container.hasActiveRequest(requestId)).toBe(true)
|
|
148
|
-
|
|
149
|
-
// End request to clean up
|
|
150
|
-
await scoped.endRequest()
|
|
151
|
-
|
|
152
|
-
// Verify request context is cleared
|
|
153
|
-
expect(container.hasActiveRequest(requestId)).toBe(false)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('should track active request contexts', async () => {
|
|
157
|
-
// Create a request context
|
|
158
|
-
const requestId = 'test-request'
|
|
159
|
-
const scoped = container.beginRequest(requestId)
|
|
160
|
-
|
|
161
|
-
// Verify request context exists
|
|
162
|
-
expect(container.hasActiveRequest(requestId)).toBe(true)
|
|
163
|
-
|
|
164
|
-
// End request
|
|
165
|
-
await scoped.endRequest()
|
|
166
|
-
|
|
167
|
-
// Verify request context is gone
|
|
168
|
-
expect(container.hasActiveRequest(requestId)).toBe(false)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('should handle services with dependencies correctly', async () => {
|
|
172
|
-
const serviceLocator = container.getServiceLocator()
|
|
173
|
-
|
|
174
|
-
// Create Injectable services
|
|
175
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
176
|
-
class ServiceA {
|
|
177
|
-
name = 'ServiceA'
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
181
|
-
class ServiceB {
|
|
182
|
-
serviceA = inject(ServiceA)
|
|
183
|
-
name = 'ServiceB'
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Create instances using container
|
|
187
|
-
await container.get(ServiceB)
|
|
188
|
-
|
|
189
|
-
// Clear all services
|
|
190
|
-
await serviceLocator.clearAll()
|
|
191
|
-
|
|
192
|
-
// Verify all services are cleared
|
|
193
|
-
expect(serviceLocator.getManager().size()).toBe(0)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('should respect maxRounds option', async () => {
|
|
197
|
-
const serviceLocator = container.getServiceLocator()
|
|
198
|
-
|
|
199
|
-
// Create Injectable service
|
|
200
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
201
|
-
class TestService {
|
|
202
|
-
name = 'TestService'
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
await container.get(TestService)
|
|
206
|
-
|
|
207
|
-
// Clear with a very low maxRounds to test the limit
|
|
208
|
-
await serviceLocator.clearAll({ maxRounds: 1 })
|
|
209
|
-
|
|
210
|
-
// Should still clear the service
|
|
211
|
-
expect(serviceLocator.getManager().size()).toBe(0)
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('should clear services with dependencies in correct order', async () => {
|
|
215
|
-
const serviceLocator = container.getServiceLocator()
|
|
216
|
-
|
|
217
|
-
// Create services with dependencies
|
|
218
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
219
|
-
class DatabaseService {
|
|
220
|
-
name = 'DatabaseService'
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
224
|
-
class UserService {
|
|
225
|
-
public database = inject(DatabaseService)
|
|
226
|
-
name = 'UserService'
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
230
|
-
class AuthService {
|
|
231
|
-
public userService = inject(UserService)
|
|
232
|
-
name = 'AuthService'
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Create instances (this will establish dependencies)
|
|
236
|
-
await container.get(AuthService)
|
|
237
|
-
await container.get(UserService)
|
|
238
|
-
await container.get(DatabaseService)
|
|
239
|
-
|
|
240
|
-
// Verify services exist (container also registers itself as +1)
|
|
241
|
-
expect(serviceLocator.getManager().size()).toBe(4)
|
|
242
|
-
|
|
243
|
-
// Clear all services - should clear in dependency order
|
|
244
|
-
await serviceLocator.clearAll()
|
|
245
|
-
|
|
246
|
-
// Verify all services are cleared
|
|
247
|
-
expect(serviceLocator.getManager().size()).toBe(0)
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
it('should handle services with destroy listeners', async () => {
|
|
251
|
-
const serviceLocator = container.getServiceLocator()
|
|
252
|
-
|
|
253
|
-
let destroyCalled = false
|
|
254
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
255
|
-
class TestService implements OnServiceDestroy {
|
|
256
|
-
name = 'TestService'
|
|
257
|
-
|
|
258
|
-
constructor() {
|
|
259
|
-
// Simulate a service that needs cleanup
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async onServiceDestroy() {
|
|
263
|
-
destroyCalled = true
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
await container.get(TestService)
|
|
268
|
-
|
|
269
|
-
// Clear all services
|
|
270
|
-
await serviceLocator.clearAll()
|
|
271
|
-
|
|
272
|
-
// Verify all services are cleared
|
|
273
|
-
expect(serviceLocator.getManager().size()).toBe(0)
|
|
274
|
-
expect(destroyCalled).toBe(true)
|
|
275
|
-
})
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
describe('Mixed Scope Services', () => {
|
|
279
|
-
let container: Container
|
|
280
|
-
let mockLogger: Console
|
|
281
|
-
|
|
282
|
-
beforeEach(() => {
|
|
283
|
-
mockLogger = {
|
|
284
|
-
log: vi.fn(),
|
|
285
|
-
warn: vi.fn(),
|
|
286
|
-
error: vi.fn(),
|
|
287
|
-
debug: vi.fn(),
|
|
288
|
-
trace: vi.fn(),
|
|
289
|
-
} as any
|
|
290
|
-
|
|
291
|
-
container = new Container(globalRegistry, mockLogger)
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
describe('Services with dependencies across different scopes', () => {
|
|
295
|
-
it('should handle Singleton service depending on Transient service', async () => {
|
|
296
|
-
// Create Transient service
|
|
297
|
-
@Injectable({ scope: InjectableScope.Transient })
|
|
298
|
-
class TransientService {
|
|
299
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
300
|
-
name = 'TransientService'
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Create Singleton service that depends on Transient service
|
|
304
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
305
|
-
class SingletonService {
|
|
306
|
-
transientService = asyncInject(TransientService)
|
|
307
|
-
name = 'SingletonService'
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Get instances
|
|
311
|
-
const singleton1 = await container.get(SingletonService)
|
|
312
|
-
const singleton2 = await container.get(SingletonService)
|
|
313
|
-
|
|
314
|
-
expect(singleton1).toBe(singleton2) // Same singleton instance
|
|
315
|
-
|
|
316
|
-
// Get the actual transient service instances (asyncInject returns Promises)
|
|
317
|
-
const transient1 = await singleton1.transientService
|
|
318
|
-
const transient2 = await singleton2.transientService
|
|
319
|
-
|
|
320
|
-
// Note: Since Singleton is created once, both references point to the same Transient instance
|
|
321
|
-
// This is expected behavior - the Transient service is created once during Singleton instantiation
|
|
322
|
-
expect(transient1).toBe(transient2) // Same transient instance (created during singleton instantiation)
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
it('should handle Request service depending on Singleton service', async () => {
|
|
326
|
-
// Create Singleton service
|
|
327
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
328
|
-
class SingletonService {
|
|
329
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
330
|
-
name = 'SingletonService'
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Create Request service that depends on Singleton service
|
|
334
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
335
|
-
class RequestService {
|
|
336
|
-
singletonService = inject(SingletonService)
|
|
337
|
-
name = 'RequestService'
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Begin request context
|
|
341
|
-
const scoped1 = container.beginRequest('test-request-1')
|
|
342
|
-
|
|
343
|
-
// Get instances within the same request
|
|
344
|
-
const request1 = await scoped1.get(RequestService)
|
|
345
|
-
const request2 = await scoped1.get(RequestService)
|
|
346
|
-
|
|
347
|
-
expect(request1).toBe(request2) // Same request-scoped instance
|
|
348
|
-
expect(request1.singletonService).toBe(request2.singletonService) // Same singleton instance
|
|
349
|
-
|
|
350
|
-
// End request and start new one
|
|
351
|
-
await scoped1.endRequest()
|
|
352
|
-
const scoped2 = container.beginRequest('test-request-2')
|
|
353
|
-
|
|
354
|
-
// Get instance in new request
|
|
355
|
-
const request3 = await scoped2.get(RequestService)
|
|
356
|
-
|
|
357
|
-
expect(request1).not.toBe(request3) // Different request-scoped instances
|
|
358
|
-
expect(request1.singletonService).toBe(request3.singletonService) // Same singleton instance
|
|
359
|
-
|
|
360
|
-
await scoped2.endRequest()
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
it('should handle Transient service depending on Request service', async () => {
|
|
364
|
-
// Create Request service
|
|
365
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
366
|
-
class RequestService {
|
|
367
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
368
|
-
name = 'RequestService'
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Create Transient service that depends on Request service
|
|
372
|
-
@Injectable({ scope: InjectableScope.Transient })
|
|
373
|
-
class TransientService {
|
|
374
|
-
requestService = inject(RequestService)
|
|
375
|
-
name = 'TransientService'
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Begin request context
|
|
379
|
-
const scoped = container.beginRequest('test-request')
|
|
380
|
-
|
|
381
|
-
// Get multiple transient instances
|
|
382
|
-
const transient1 = await scoped.get(TransientService)
|
|
383
|
-
const transient2 = await scoped.get(TransientService)
|
|
384
|
-
|
|
385
|
-
expect(transient1).not.toBe(transient2) // Different transient instances
|
|
386
|
-
|
|
387
|
-
// Get the actual request service instances
|
|
388
|
-
const requestService1 = transient1.requestService
|
|
389
|
-
const requestService2 = transient2.requestService
|
|
390
|
-
expect(requestService1).toBe(requestService2) // Same request-scoped instance
|
|
391
|
-
|
|
392
|
-
await scoped.endRequest()
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
it('should handle complex dependency chain across all scopes', async () => {
|
|
396
|
-
// Create services with different scopes
|
|
397
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
398
|
-
class DatabaseService {
|
|
399
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
400
|
-
name = 'DatabaseService'
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
404
|
-
class UserSessionService {
|
|
405
|
-
database = inject(DatabaseService)
|
|
406
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
407
|
-
name = 'UserSessionService'
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
@Injectable({ scope: InjectableScope.Transient })
|
|
411
|
-
class UserActionService {
|
|
412
|
-
session = inject(UserSessionService)
|
|
413
|
-
database = inject(DatabaseService)
|
|
414
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
415
|
-
name = 'UserActionService'
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
419
|
-
class UserManagerService {
|
|
420
|
-
database = inject(DatabaseService)
|
|
421
|
-
name = 'UserManagerService'
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Begin request context
|
|
425
|
-
const scoped = container.beginRequest('complex-request')
|
|
426
|
-
|
|
427
|
-
// Get instances
|
|
428
|
-
const action1 = await scoped.get(UserActionService)
|
|
429
|
-
const action2 = await scoped.get(UserActionService)
|
|
430
|
-
const manager = await container.get(UserManagerService)
|
|
431
|
-
|
|
432
|
-
// Verify instances are created
|
|
433
|
-
expect(action1).toBeDefined()
|
|
434
|
-
expect(action2).toBeDefined()
|
|
435
|
-
expect(manager).toBeDefined()
|
|
436
|
-
|
|
437
|
-
// Verify scope behavior - check if dependencies are properly injected
|
|
438
|
-
expect(action1).not.toBe(action2) // Different transient instances
|
|
439
|
-
|
|
440
|
-
// Get the actual dependency instances
|
|
441
|
-
const action1Database = action1.database
|
|
442
|
-
const action2Database = action2.database
|
|
443
|
-
const action1Session = action1.session
|
|
444
|
-
const action2Session = action2.session
|
|
445
|
-
|
|
446
|
-
expect(action1Database).toBe(action2Database) // Same singleton instance
|
|
447
|
-
expect(action1Database).toBe(manager.database) // Same singleton instance
|
|
448
|
-
|
|
449
|
-
// Check if session dependency is properly injected
|
|
450
|
-
expect(action1Session).toBe(action2Session) // Same request-scoped instance
|
|
451
|
-
expect(action1Session.database).toBe(action1Database) // Same singleton instance
|
|
452
|
-
|
|
453
|
-
// End request and start new one
|
|
454
|
-
await scoped.endRequest()
|
|
455
|
-
const scoped2 = container.beginRequest('complex-request-2')
|
|
456
|
-
|
|
457
|
-
// Get instances in new request
|
|
458
|
-
const action3 = await scoped2.get(UserActionService)
|
|
459
|
-
const manager2 = await container.get(UserManagerService)
|
|
460
|
-
|
|
461
|
-
// Verify scope behavior across requests
|
|
462
|
-
expect(action1).not.toBe(action3) // Different transient instances
|
|
463
|
-
|
|
464
|
-
// Get the actual dependency instances for the new request
|
|
465
|
-
const action3Database = action3.database
|
|
466
|
-
const action3Session = action3.session
|
|
467
|
-
|
|
468
|
-
expect(action1Database).toBe(action3Database) // Same singleton instance
|
|
469
|
-
expect(manager).toBe(manager2) // Same singleton instance
|
|
470
|
-
|
|
471
|
-
// Check if session dependency is properly injected in new request
|
|
472
|
-
expect(action1Session).not.toBe(action3Session) // Different request-scoped instances
|
|
473
|
-
|
|
474
|
-
await scoped2.endRequest()
|
|
475
|
-
})
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
describe('Instance sharing and isolation', () => {
|
|
479
|
-
it('should isolate Request-scoped instances between different requests', async () => {
|
|
480
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
481
|
-
class RequestService {
|
|
482
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
483
|
-
name = 'RequestService'
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// First request
|
|
487
|
-
const scoped1 = container.beginRequest('request-1')
|
|
488
|
-
const service1 = await scoped1.get(RequestService)
|
|
489
|
-
await scoped1.endRequest()
|
|
490
|
-
|
|
491
|
-
// Second request
|
|
492
|
-
const scoped2 = container.beginRequest('request-2')
|
|
493
|
-
const service2 = await scoped2.get(RequestService)
|
|
494
|
-
await scoped2.endRequest()
|
|
495
|
-
|
|
496
|
-
expect(service1).not.toBe(service2) // Different instances
|
|
497
|
-
expect(service1.id).not.toBe(service2.id) // Different IDs
|
|
498
|
-
})
|
|
499
|
-
|
|
500
|
-
it('should share Singleton instances across requests', async () => {
|
|
501
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
502
|
-
class SingletonService {
|
|
503
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
504
|
-
name = 'SingletonService'
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// First request
|
|
508
|
-
const scoped1 = container.beginRequest('request-1')
|
|
509
|
-
const service1 = await scoped1.get(SingletonService)
|
|
510
|
-
await scoped1.endRequest()
|
|
511
|
-
|
|
512
|
-
// Second request
|
|
513
|
-
const scoped2 = container.beginRequest('request-2')
|
|
514
|
-
const service2 = await scoped2.get(SingletonService)
|
|
515
|
-
await scoped2.endRequest()
|
|
516
|
-
|
|
517
|
-
expect(service1).toBe(service2) // Same instance
|
|
518
|
-
expect(service1.id).toBe(service2.id) // Same ID
|
|
519
|
-
})
|
|
520
|
-
|
|
521
|
-
it('should create new Transient instances every time', async () => {
|
|
522
|
-
@Injectable({ scope: InjectableScope.Transient })
|
|
523
|
-
class TransientService {
|
|
524
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
525
|
-
name = 'TransientService'
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const service1 = await container.get(TransientService)
|
|
529
|
-
const service2 = await container.get(TransientService)
|
|
530
|
-
const service3 = await container.get(TransientService)
|
|
531
|
-
|
|
532
|
-
expect(service1).not.toBe(service2) // Different instances
|
|
533
|
-
expect(service1).not.toBe(service3) // Different instances
|
|
534
|
-
expect(service2).not.toBe(service3) // Different instances
|
|
535
|
-
expect(service1.id).not.toBe(service2.id) // Different IDs
|
|
536
|
-
expect(service1.id).not.toBe(service3.id) // Different IDs
|
|
537
|
-
expect(service2.id).not.toBe(service3.id) // Different IDs
|
|
538
|
-
})
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
describe('Request context management with mixed scopes', () => {
|
|
542
|
-
it('should properly clean up Request-scoped instances when ending request', async () => {
|
|
543
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
544
|
-
class RequestService {
|
|
545
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
546
|
-
name = 'RequestService'
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
550
|
-
class SingletonService {
|
|
551
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
552
|
-
name = 'SingletonService'
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
const scoped = container.beginRequest('cleanup-test')
|
|
556
|
-
|
|
557
|
-
// Create instances
|
|
558
|
-
const _requestService = await scoped.get(RequestService)
|
|
559
|
-
const singletonService = await scoped.get(SingletonService)
|
|
560
|
-
|
|
561
|
-
// Verify request context exists
|
|
562
|
-
expect(container.hasActiveRequest('cleanup-test')).toBe(true)
|
|
563
|
-
|
|
564
|
-
// End request
|
|
565
|
-
await scoped.endRequest()
|
|
566
|
-
|
|
567
|
-
// Verify request context is cleared
|
|
568
|
-
expect(container.hasActiveRequest('cleanup-test')).toBe(false)
|
|
569
|
-
|
|
570
|
-
// Singleton should still be available
|
|
571
|
-
const singletonService2 = await container.get(SingletonService)
|
|
572
|
-
expect(singletonService).toBe(singletonService2) // Same singleton instance
|
|
573
|
-
|
|
574
|
-
// Request service should not be available (no current request context)
|
|
575
|
-
await expect(container.get(RequestService)).rejects.toThrow(
|
|
576
|
-
/Cannot resolve request-scoped service/,
|
|
577
|
-
)
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
it('should handle parallel request contexts with mixed scopes', async () => {
|
|
581
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
582
|
-
class RequestService {
|
|
583
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
584
|
-
name = 'RequestService'
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
588
|
-
class SingletonService {
|
|
589
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
590
|
-
name = 'SingletonService'
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// First request
|
|
594
|
-
const scoped1 = container.beginRequest('outer-request')
|
|
595
|
-
const requestService1 = await scoped1.get(RequestService)
|
|
596
|
-
const singletonService1 = await scoped1.get(SingletonService)
|
|
597
|
-
|
|
598
|
-
// Second request (parallel)
|
|
599
|
-
const scoped2 = container.beginRequest('inner-request')
|
|
600
|
-
const requestService2 = await scoped2.get(RequestService)
|
|
601
|
-
const singletonService2 = await scoped2.get(SingletonService)
|
|
602
|
-
|
|
603
|
-
// Verify instances
|
|
604
|
-
expect(requestService1).not.toBe(requestService2) // Different request instances
|
|
605
|
-
expect(singletonService1).toBe(singletonService2) // Same singleton instance
|
|
606
|
-
|
|
607
|
-
// End both requests
|
|
608
|
-
await scoped2.endRequest()
|
|
609
|
-
await scoped1.endRequest()
|
|
610
|
-
|
|
611
|
-
// Verify no active contexts
|
|
612
|
-
expect(container.hasActiveRequest('outer-request')).toBe(false)
|
|
613
|
-
expect(container.hasActiveRequest('inner-request')).toBe(false)
|
|
614
|
-
})
|
|
615
|
-
|
|
616
|
-
it('should handle concurrent requests with mixed scopes', async () => {
|
|
617
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
618
|
-
class RequestService {
|
|
619
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
620
|
-
name = 'RequestService'
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
624
|
-
class SingletonService {
|
|
625
|
-
id = Math.random().toString(36).substr(2, 9)
|
|
626
|
-
name = 'SingletonService'
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Start multiple requests sequentially
|
|
630
|
-
const requestIds = ['req-1', 'req-2', 'req-3']
|
|
631
|
-
const results = []
|
|
632
|
-
|
|
633
|
-
for (const requestId of requestIds) {
|
|
634
|
-
const scoped = container.beginRequest(requestId)
|
|
635
|
-
const requestService = await scoped.get(RequestService)
|
|
636
|
-
const singletonService = await scoped.get(SingletonService)
|
|
637
|
-
await scoped.endRequest()
|
|
638
|
-
results.push({ requestService, singletonService, requestId })
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// Verify all requests completed successfully
|
|
642
|
-
results.forEach(({ requestService, singletonService }) => {
|
|
643
|
-
expect(requestService).toBeDefined()
|
|
644
|
-
expect(singletonService).toBeDefined()
|
|
645
|
-
})
|
|
646
|
-
|
|
647
|
-
// Verify request services are different
|
|
648
|
-
expect(results[0].requestService).not.toBe(results[1].requestService)
|
|
649
|
-
expect(results[0].requestService).not.toBe(results[2].requestService)
|
|
650
|
-
expect(results[1].requestService).not.toBe(results[2].requestService)
|
|
651
|
-
|
|
652
|
-
// Verify singleton services are the same
|
|
653
|
-
expect(results[0].singletonService).toBe(results[1].singletonService)
|
|
654
|
-
expect(results[0].singletonService).toBe(results[2].singletonService)
|
|
655
|
-
expect(results[1].singletonService).toBe(results[2].singletonService)
|
|
656
|
-
})
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
describe('Error handling with mixed scopes', () => {
|
|
660
|
-
it('should handle Request-scoped service without request context', async () => {
|
|
661
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
662
|
-
class RequestService {
|
|
663
|
-
name = 'RequestService'
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Try to get Request-scoped service without request context
|
|
667
|
-
await expect(container.get(RequestService)).rejects.toThrow(
|
|
668
|
-
/Cannot resolve request-scoped service/,
|
|
669
|
-
)
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
it('should handle service instantiation errors in mixed scope scenario', async () => {
|
|
673
|
-
@Injectable({ scope: InjectableScope.Singleton })
|
|
674
|
-
class SingletonService {
|
|
675
|
-
constructor() {
|
|
676
|
-
throw new Error('Singleton creation failed')
|
|
677
|
-
}
|
|
678
|
-
name = 'SingletonService'
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
@Injectable({ scope: InjectableScope.Request })
|
|
682
|
-
class RequestService {
|
|
683
|
-
singleton = inject(SingletonService)
|
|
684
|
-
name = 'RequestService'
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
const scoped = container.beginRequest('error-test')
|
|
688
|
-
|
|
689
|
-
// Try to get Request service that depends on failing Singleton
|
|
690
|
-
await expect(scoped.get(RequestService)).rejects.toThrow(
|
|
691
|
-
'Singleton creation failed',
|
|
692
|
-
)
|
|
693
|
-
|
|
694
|
-
await scoped.endRequest()
|
|
695
|
-
})
|
|
696
|
-
})
|
|
697
|
-
})
|
|
698
|
-
|
|
699
|
-
describe('Injectable with Schema', () => {
|
|
700
|
-
let container: Container
|
|
701
|
-
|
|
702
|
-
beforeEach(() => {
|
|
703
|
-
container = new Container(globalRegistry)
|
|
704
|
-
})
|
|
705
|
-
|
|
706
|
-
it('should work with simple schema definition', async () => {
|
|
707
|
-
const configSchema = z.object({
|
|
708
|
-
host: z.string(),
|
|
709
|
-
port: z.number(),
|
|
710
|
-
})
|
|
711
|
-
|
|
712
|
-
@Injectable({ schema: configSchema })
|
|
713
|
-
class DatabaseConfig {
|
|
714
|
-
constructor(public readonly config: z.output<typeof configSchema>) {}
|
|
715
|
-
|
|
716
|
-
getConnectionString() {
|
|
717
|
-
return `${this.config.host}:${this.config.port}`
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const token = getInjectableToken(DatabaseConfig)
|
|
722
|
-
const instance = await container.get(
|
|
723
|
-
InjectionToken.bound(token, {
|
|
724
|
-
host: 'localhost',
|
|
725
|
-
port: 5432,
|
|
726
|
-
}),
|
|
727
|
-
)
|
|
728
|
-
|
|
729
|
-
expect(instance).toBeInstanceOf(DatabaseConfig)
|
|
730
|
-
// @ts-expect-error - instance is of type DatabaseConfig
|
|
731
|
-
expect(instance.config).toEqual({ host: 'localhost', port: 5432 })
|
|
732
|
-
// @ts-expect-error - instance is of type DatabaseConfig
|
|
733
|
-
expect(instance.getConnectionString()).toBe('localhost:5432')
|
|
734
|
-
})
|
|
735
|
-
|
|
736
|
-
it('should work with schema and singleton scope', async () => {
|
|
737
|
-
const apiSchema = z.object({
|
|
738
|
-
apiKey: z.string(),
|
|
739
|
-
baseUrl: z.string(),
|
|
740
|
-
})
|
|
741
|
-
|
|
742
|
-
@Injectable({ schema: apiSchema, scope: InjectableScope.Singleton })
|
|
743
|
-
class ApiClient {
|
|
744
|
-
constructor(public readonly config: z.output<typeof apiSchema>) {}
|
|
745
|
-
|
|
746
|
-
getApiKey() {
|
|
747
|
-
return this.config.apiKey
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const instance1 = await container.get(ApiClient, {
|
|
752
|
-
apiKey: 'secret-key',
|
|
753
|
-
baseUrl: 'https://api.example.com',
|
|
754
|
-
})
|
|
755
|
-
const instance2 = await container.get(ApiClient, {
|
|
756
|
-
apiKey: 'secret-key',
|
|
757
|
-
baseUrl: 'https://api.example.com',
|
|
758
|
-
})
|
|
759
|
-
|
|
760
|
-
expect(instance1).toBe(instance2) // Same singleton instance
|
|
761
|
-
expect(instance1.getApiKey()).toBe('secret-key')
|
|
762
|
-
})
|
|
763
|
-
|
|
764
|
-
it('should work with schema and transient scope', async () => {
|
|
765
|
-
const loggerSchema = z.object({
|
|
766
|
-
level: z.enum(['debug', 'info', 'warn', 'error']),
|
|
767
|
-
prefix: z.string(),
|
|
768
|
-
})
|
|
769
|
-
|
|
770
|
-
@Injectable({ schema: loggerSchema, scope: InjectableScope.Transient })
|
|
771
|
-
class Logger {
|
|
772
|
-
constructor(public readonly config: z.output<typeof loggerSchema>) {}
|
|
773
|
-
|
|
774
|
-
log(message: string) {
|
|
775
|
-
return `[${this.config.prefix}] ${message}`
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
const instance1 = await container.get(Logger, {
|
|
780
|
-
level: 'info' as const,
|
|
781
|
-
prefix: 'APP',
|
|
782
|
-
})
|
|
783
|
-
const instance2 = await container.get(Logger, {
|
|
784
|
-
level: 'info' as const,
|
|
785
|
-
prefix: 'APP',
|
|
786
|
-
})
|
|
787
|
-
|
|
788
|
-
expect(instance1).not.toBe(instance2) // Different transient instances
|
|
789
|
-
expect(instance1.log('test')).toBe('[APP] test')
|
|
790
|
-
expect(instance2.log('test')).toBe('[APP] test')
|
|
791
|
-
})
|
|
792
|
-
|
|
793
|
-
it('should work with schema and dependency injection', async () => {
|
|
794
|
-
const dbConfigSchema = z.object({
|
|
795
|
-
connectionString: z.string(),
|
|
796
|
-
})
|
|
797
|
-
|
|
798
|
-
@Injectable({ schema: dbConfigSchema })
|
|
799
|
-
class DatabaseConfig {
|
|
800
|
-
constructor(public readonly config: z.output<typeof dbConfigSchema>) {}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
@Injectable()
|
|
804
|
-
class DatabaseService {
|
|
805
|
-
private dbConfig = inject(DatabaseConfig, {
|
|
806
|
-
connectionString: 'postgres://localhost:5432/mydb',
|
|
807
|
-
})
|
|
808
|
-
|
|
809
|
-
getConnectionString() {
|
|
810
|
-
return this.dbConfig.config.connectionString
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const instance = await container.get(DatabaseService)
|
|
815
|
-
|
|
816
|
-
expect(instance).toBeInstanceOf(DatabaseService)
|
|
817
|
-
expect(instance.getConnectionString()).toBe(
|
|
818
|
-
'postgres://localhost:5432/mydb',
|
|
819
|
-
)
|
|
820
|
-
})
|
|
821
|
-
|
|
822
|
-
it('should work with schema and async dependency injection', async () => {
|
|
823
|
-
const cacheConfigSchema = z.object({
|
|
824
|
-
ttl: z.number(),
|
|
825
|
-
maxSize: z.number(),
|
|
826
|
-
})
|
|
827
|
-
|
|
828
|
-
@Injectable({ schema: cacheConfigSchema })
|
|
829
|
-
class CacheConfig {
|
|
830
|
-
constructor(
|
|
831
|
-
public readonly config: z.output<typeof cacheConfigSchema>,
|
|
832
|
-
) {}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
@Injectable()
|
|
836
|
-
class CacheService {
|
|
837
|
-
private cacheConfig = asyncInject(CacheConfig, {
|
|
838
|
-
ttl: 3600,
|
|
839
|
-
maxSize: 1000,
|
|
840
|
-
})
|
|
841
|
-
|
|
842
|
-
async getConfig() {
|
|
843
|
-
const config = await this.cacheConfig
|
|
844
|
-
return config.config
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
const instance = await container.get(CacheService)
|
|
849
|
-
|
|
850
|
-
expect(instance).toBeInstanceOf(CacheService)
|
|
851
|
-
const config = await instance.getConfig()
|
|
852
|
-
expect(config).toEqual({ ttl: 3600, maxSize: 1000 })
|
|
853
|
-
})
|
|
854
|
-
|
|
855
|
-
it('should validate schema when using bound tokens', async () => {
|
|
856
|
-
const strictSchema = z.object({
|
|
857
|
-
required: z.string(),
|
|
858
|
-
optional: z.number().optional(),
|
|
859
|
-
})
|
|
860
|
-
|
|
861
|
-
@Injectable({ schema: strictSchema })
|
|
862
|
-
class StrictService {
|
|
863
|
-
constructor(public readonly config: z.output<typeof strictSchema>) {}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Valid configuration
|
|
867
|
-
const instance1 = await container.get(StrictService, {
|
|
868
|
-
required: 'value',
|
|
869
|
-
optional: 42,
|
|
870
|
-
})
|
|
871
|
-
|
|
872
|
-
expect(instance1).toBeInstanceOf(StrictService)
|
|
873
|
-
expect(instance1.config).toEqual({ required: 'value', optional: 42 })
|
|
874
|
-
|
|
875
|
-
// Valid with optional field missing
|
|
876
|
-
const instance2 = await container.get(StrictService, {
|
|
877
|
-
required: 'another value',
|
|
878
|
-
})
|
|
879
|
-
|
|
880
|
-
expect(instance2).toBeInstanceOf(StrictService)
|
|
881
|
-
expect(instance2.config).toEqual({ required: 'another value' })
|
|
882
|
-
})
|
|
883
|
-
|
|
884
|
-
it('should work with complex nested schemas', async () => {
|
|
885
|
-
const nestedSchema = z.object({
|
|
886
|
-
database: z.object({
|
|
887
|
-
host: z.string(),
|
|
888
|
-
port: z.number(),
|
|
889
|
-
credentials: z.object({
|
|
890
|
-
username: z.string(),
|
|
891
|
-
password: z.string(),
|
|
892
|
-
}),
|
|
893
|
-
}),
|
|
894
|
-
cache: z.object({
|
|
895
|
-
enabled: z.boolean(),
|
|
896
|
-
ttl: z.number(),
|
|
897
|
-
}),
|
|
898
|
-
})
|
|
899
|
-
|
|
900
|
-
@Injectable({ schema: nestedSchema })
|
|
901
|
-
class AppConfig {
|
|
902
|
-
constructor(public readonly config: z.output<typeof nestedSchema>) {}
|
|
903
|
-
|
|
904
|
-
getDatabaseHost() {
|
|
905
|
-
return this.config.database.host
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
isCacheEnabled() {
|
|
909
|
-
return this.config.cache.enabled
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
const instance = await container.get(AppConfig, {
|
|
914
|
-
database: {
|
|
915
|
-
host: 'db.example.com',
|
|
916
|
-
port: 5432,
|
|
917
|
-
credentials: {
|
|
918
|
-
username: 'admin',
|
|
919
|
-
password: 'secret',
|
|
920
|
-
},
|
|
921
|
-
},
|
|
922
|
-
cache: {
|
|
923
|
-
enabled: true,
|
|
924
|
-
ttl: 300,
|
|
925
|
-
},
|
|
926
|
-
})
|
|
927
|
-
|
|
928
|
-
expect(instance).toBeInstanceOf(AppConfig)
|
|
929
|
-
expect(instance.getDatabaseHost()).toBe('db.example.com')
|
|
930
|
-
expect(instance.isCacheEnabled()).toBe(true)
|
|
931
|
-
})
|
|
932
|
-
|
|
933
|
-
it('should work with schema in request-scoped services', async () => {
|
|
934
|
-
const userContextSchema = z.object({
|
|
935
|
-
userId: z.string(),
|
|
936
|
-
sessionId: z.string(),
|
|
937
|
-
})
|
|
938
|
-
|
|
939
|
-
@Injectable({
|
|
940
|
-
schema: userContextSchema,
|
|
941
|
-
scope: InjectableScope.Request,
|
|
942
|
-
})
|
|
943
|
-
class UserContext {
|
|
944
|
-
constructor(
|
|
945
|
-
public readonly context: z.output<typeof userContextSchema>,
|
|
946
|
-
) {}
|
|
947
|
-
|
|
948
|
-
getUserId() {
|
|
949
|
-
return this.context.userId
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
const scoped = container.beginRequest('test-request')
|
|
954
|
-
|
|
955
|
-
const instance = await scoped.get(UserContext, {
|
|
956
|
-
userId: 'user-123',
|
|
957
|
-
sessionId: 'session-456',
|
|
958
|
-
})
|
|
959
|
-
|
|
960
|
-
expect(instance).toBeInstanceOf(UserContext)
|
|
961
|
-
expect(instance.getUserId()).toBe('user-123')
|
|
962
|
-
|
|
963
|
-
await scoped.endRequest()
|
|
964
|
-
})
|
|
965
|
-
})
|
|
966
|
-
})
|