@navios/di 0.5.1 → 0.6.1
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 +145 -0
- package/README.md +196 -219
- package/docs/README.md +69 -11
- package/docs/api-reference.md +281 -117
- package/docs/container.md +220 -56
- package/docs/examples/request-scope-example.mts +2 -2
- package/docs/factory.md +3 -8
- package/docs/getting-started.md +37 -8
- package/docs/migration.md +318 -37
- package/docs/request-contexts.md +263 -175
- package/docs/scopes.md +79 -42
- package/lib/browser/index.d.mts +1577 -0
- package/lib/browser/index.d.mts.map +1 -0
- package/lib/browser/index.mjs +3013 -0
- package/lib/browser/index.mjs.map +1 -0
- package/lib/index-7jfWsiG4.d.mts +1211 -0
- package/lib/index-7jfWsiG4.d.mts.map +1 -0
- package/lib/index-DW3K5sOX.d.cts +1206 -0
- package/lib/index-DW3K5sOX.d.cts.map +1 -0
- package/lib/index.cjs +389 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +376 -0
- package/lib/index.d.cts.map +1 -0
- package/lib/index.d.mts +371 -78
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +325 -63
- package/lib/index.mjs.map +1 -1
- package/lib/testing/index.cjs +9 -0
- package/lib/testing/index.d.cts +2 -0
- package/lib/testing/index.d.mts +2 -2
- package/lib/testing/index.mjs +2 -72
- package/lib/testing-BG_fa9TJ.mjs +2656 -0
- package/lib/testing-BG_fa9TJ.mjs.map +1 -0
- package/lib/testing-DIaIRiJz.cjs +2896 -0
- package/lib/testing-DIaIRiJz.cjs.map +1 -0
- package/package.json +29 -7
- package/project.json +2 -2
- package/src/__tests__/async-local-storage.browser.spec.mts +240 -0
- package/src/__tests__/async-local-storage.spec.mts +333 -0
- package/src/__tests__/container.spec.mts +30 -25
- package/src/__tests__/e2e.browser.spec.mts +790 -0
- package/src/__tests__/e2e.spec.mts +1222 -0
- package/src/__tests__/factory.spec.mts +1 -1
- package/src/__tests__/get-injectors.spec.mts +1 -1
- package/src/__tests__/injectable.spec.mts +1 -1
- package/src/__tests__/injection-token.spec.mts +1 -1
- package/src/__tests__/library-findings.spec.mts +563 -0
- package/src/__tests__/registry.spec.mts +2 -2
- package/src/__tests__/request-scope.spec.mts +266 -274
- package/src/__tests__/service-instantiator.spec.mts +18 -17
- package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
- package/src/__tests__/service-locator-manager.spec.mts +15 -15
- package/src/__tests__/service-locator.spec.mts +167 -244
- package/src/__tests__/unified-api.spec.mts +27 -27
- package/src/__type-tests__/factory.spec-d.mts +2 -2
- package/src/__type-tests__/inject.spec-d.mts +2 -2
- package/src/__type-tests__/injectable.spec-d.mts +1 -1
- package/src/browser.mts +16 -0
- package/src/container/container.mts +319 -0
- package/src/container/index.mts +2 -0
- package/src/container/scoped-container.mts +350 -0
- package/src/decorators/factory.decorator.mts +4 -4
- package/src/decorators/injectable.decorator.mts +5 -5
- package/src/errors/di-error.mts +12 -5
- package/src/errors/index.mts +0 -8
- package/src/index.mts +156 -15
- package/src/interfaces/container.interface.mts +82 -0
- package/src/interfaces/factory.interface.mts +2 -2
- package/src/interfaces/index.mts +1 -0
- package/src/internal/context/async-local-storage.mts +120 -0
- package/src/internal/context/factory-context.mts +18 -0
- package/src/internal/context/index.mts +3 -0
- package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
- package/src/internal/context/resolution-context.mts +63 -0
- package/src/internal/context/sync-local-storage.mts +51 -0
- package/src/internal/core/index.mts +5 -0
- package/src/internal/core/instance-resolver.mts +641 -0
- package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
- package/src/internal/core/invalidator.mts +437 -0
- package/src/internal/core/service-locator.mts +202 -0
- package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
- package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
- package/src/internal/holder/holder-manager.mts +85 -0
- package/src/internal/holder/holder-storage.interface.mts +116 -0
- package/src/internal/holder/index.mts +6 -0
- package/src/internal/holder/instance-holder.mts +109 -0
- package/src/internal/holder/request-storage.mts +134 -0
- package/src/internal/holder/singleton-storage.mts +105 -0
- package/src/internal/index.mts +4 -0
- package/src/internal/lifecycle/circular-detector.mts +77 -0
- package/src/internal/lifecycle/index.mts +2 -0
- package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +11 -4
- package/src/testing/__tests__/test-container.spec.mts +2 -2
- package/src/testing/test-container.mts +4 -4
- package/src/token/index.mts +2 -0
- package/src/{injection-token.mts → token/injection-token.mts} +1 -1
- package/src/{registry.mts → token/registry.mts} +1 -1
- package/src/utils/get-injectable-token.mts +1 -1
- package/src/utils/get-injectors.mts +32 -15
- package/src/utils/types.mts +1 -1
- package/tsdown.config.mts +67 -0
- package/lib/_tsup-dts-rollup.d.mts +0 -1283
- package/lib/_tsup-dts-rollup.d.ts +0 -1283
- package/lib/chunk-2M576LCC.mjs +0 -2043
- package/lib/chunk-2M576LCC.mjs.map +0 -1
- package/lib/index.d.ts +0 -78
- package/lib/index.js +0 -2127
- package/lib/index.js.map +0 -1
- package/lib/testing/index.d.ts +0 -2
- package/lib/testing/index.js +0 -2060
- package/lib/testing/index.js.map +0 -1
- package/lib/testing/index.mjs.map +0 -1
- package/src/container.mts +0 -227
- package/src/factory-context.mts +0 -8
- package/src/instance-resolver.mts +0 -559
- package/src/request-context-manager.mts +0 -149
- package/src/service-invalidator.mts +0 -429
- package/src/service-locator-instance-holder.mts +0 -70
- package/src/service-locator-manager.mts +0 -85
- package/src/service-locator.mts +0 -246
- package/tsup.config.mts +0 -12
- /package/src/{injector.mts → injectors.mts} +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import { Container } from '../container.mjs'
|
|
3
|
+
import { Container } from '../container/container.mjs'
|
|
4
4
|
import { Injectable } from '../decorators/injectable.decorator.mjs'
|
|
5
5
|
import { InjectableScope } from '../enums/index.mjs'
|
|
6
6
|
import { inject } from '../index.mjs'
|
|
7
|
-
import { InjectionToken } from '../injection-token.mjs'
|
|
8
|
-
import { Registry } from '../registry.mjs'
|
|
9
|
-
import {
|
|
7
|
+
import { InjectionToken } from '../token/injection-token.mjs'
|
|
8
|
+
import { Registry } from '../token/registry.mjs'
|
|
9
|
+
import { ScopedContainer } from '../container/scoped-container.mjs'
|
|
10
10
|
|
|
11
11
|
describe('Request Scope', () => {
|
|
12
12
|
let container: Container
|
|
@@ -17,7 +17,7 @@ describe('Request Scope', () => {
|
|
|
17
17
|
container = new Container(registry)
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
describe('Request-scoped services', () => {
|
|
20
|
+
describe('Request-scoped services with ScopedContainer', () => {
|
|
21
21
|
it('should create different instances for different requests', async () => {
|
|
22
22
|
@Injectable({ registry, scope: InjectableScope.Request })
|
|
23
23
|
class RequestService {
|
|
@@ -25,15 +25,15 @@ describe('Request Scope', () => {
|
|
|
25
25
|
public readonly createdAt = new Date()
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// Start first request
|
|
29
|
-
container.beginRequest('request-1')
|
|
30
|
-
const instance1a = await
|
|
31
|
-
const instance1b = await
|
|
28
|
+
// Start first request - get a ScopedContainer
|
|
29
|
+
const scoped1 = container.beginRequest('request-1')
|
|
30
|
+
const instance1a = await scoped1.get(RequestService)
|
|
31
|
+
const instance1b = await scoped1.get(RequestService)
|
|
32
32
|
|
|
33
|
-
// Start second request
|
|
34
|
-
container.beginRequest('request-2')
|
|
35
|
-
const instance2a = await
|
|
36
|
-
const instance2b = await
|
|
33
|
+
// Start second request - get another ScopedContainer
|
|
34
|
+
const scoped2 = container.beginRequest('request-2')
|
|
35
|
+
const instance2a = await scoped2.get(RequestService)
|
|
36
|
+
const instance2b = await scoped2.get(RequestService)
|
|
37
37
|
|
|
38
38
|
// Within same request, instances should be the same
|
|
39
39
|
expect(instance1a).toBe(instance1b)
|
|
@@ -44,8 +44,8 @@ describe('Request Scope', () => {
|
|
|
44
44
|
expect(instance1a.requestId).not.toBe(instance2a.requestId)
|
|
45
45
|
|
|
46
46
|
// Clean up
|
|
47
|
-
await
|
|
48
|
-
await
|
|
47
|
+
await scoped1.endRequest()
|
|
48
|
+
await scoped2.endRequest()
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
it('should handle request context lifecycle correctly', async () => {
|
|
@@ -59,15 +59,14 @@ describe('Request Scope', () => {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// Start request
|
|
63
|
-
const
|
|
64
|
-
container.beginRequest(requestId)
|
|
62
|
+
// Start request - get a ScopedContainer
|
|
63
|
+
const scoped = container.beginRequest('test-request')
|
|
65
64
|
|
|
66
|
-
const instance = await
|
|
65
|
+
const instance = await scoped.get(RequestService)
|
|
67
66
|
expect(instance.destroyed).toBe(false)
|
|
68
67
|
|
|
69
68
|
// End request should trigger cleanup
|
|
70
|
-
await
|
|
69
|
+
await scoped.endRequest()
|
|
71
70
|
expect(instance.destroyed).toBe(true)
|
|
72
71
|
})
|
|
73
72
|
|
|
@@ -75,350 +74,343 @@ describe('Request Scope', () => {
|
|
|
75
74
|
const requestId = 'test-request'
|
|
76
75
|
const metadata = { userId: 'user123', sessionId: 'session456' }
|
|
77
76
|
|
|
78
|
-
container.beginRequest(requestId, metadata)
|
|
77
|
+
const scoped = container.beginRequest(requestId, metadata)
|
|
79
78
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
expect(
|
|
84
|
-
expect(context?.requestId).toBe(requestId)
|
|
85
|
-
expect(context?.getMetadata('userId')).toBe('user123')
|
|
86
|
-
expect(context?.getMetadata('sessionId')).toBe('session456')
|
|
79
|
+
// Verify metadata is accessible from the scoped container
|
|
80
|
+
expect(scoped.getRequestId()).toBe(requestId)
|
|
81
|
+
expect(scoped.getMetadata('userId')).toBe('user123')
|
|
82
|
+
expect(scoped.getMetadata('sessionId')).toBe('session456')
|
|
87
83
|
|
|
88
|
-
await
|
|
84
|
+
await scoped.endRequest()
|
|
89
85
|
})
|
|
90
86
|
|
|
91
87
|
it('should handle pre-prepared instances', async () => {
|
|
88
|
+
const token = InjectionToken.create<{ value: string }>('PrePrepared')
|
|
89
|
+
|
|
90
|
+
@Injectable({ registry, scope: InjectableScope.Request, token })
|
|
91
|
+
class RequestService {
|
|
92
|
+
constructor(public readonly value: string = 'default') {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const scoped = container.beginRequest('test-request')
|
|
96
|
+
|
|
97
|
+
// Add a pre-prepared instance
|
|
98
|
+
const prePreparedInstance = { value: 'pre-prepared' }
|
|
99
|
+
scoped.addInstance(token, prePreparedInstance)
|
|
100
|
+
|
|
101
|
+
// Getting the service should return the pre-prepared instance
|
|
102
|
+
const instance = await scoped.get(token)
|
|
103
|
+
expect(instance).toBe(prePreparedInstance)
|
|
104
|
+
|
|
105
|
+
await scoped.endRequest()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should throw error when resolving request-scoped service from Container directly', async () => {
|
|
92
109
|
@Injectable({ registry, scope: InjectableScope.Request })
|
|
93
110
|
class RequestService {
|
|
94
111
|
public readonly requestId = Math.random().toString(36)
|
|
95
|
-
public readonly prePrepared = true
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
|
|
99
|
-
container.
|
|
114
|
+
// Trying to resolve request-scoped service from Container should throw
|
|
115
|
+
await expect(container.get(RequestService)).rejects.toThrow(
|
|
116
|
+
/Cannot resolve request-scoped service/,
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should throw error when creating duplicate request ID', () => {
|
|
121
|
+
container.beginRequest('request-1')
|
|
100
122
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
// Creating another request with the same ID should throw
|
|
124
|
+
expect(() => container.beginRequest('request-1')).toThrow(
|
|
125
|
+
/Request context "request-1" already exists/,
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should allow reusing request ID after ending the request', async () => {
|
|
130
|
+
const scoped1 = container.beginRequest('request-1')
|
|
131
|
+
await scoped1.endRequest()
|
|
104
132
|
|
|
105
|
-
|
|
133
|
+
// Should be able to create a new request with the same ID
|
|
134
|
+
const scoped2 = container.beginRequest('request-1')
|
|
135
|
+
expect(scoped2).toBeInstanceOf(ScopedContainer)
|
|
136
|
+
await scoped2.endRequest()
|
|
106
137
|
})
|
|
138
|
+
})
|
|
107
139
|
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
describe('ScopedContainer delegation', () => {
|
|
141
|
+
it('should delegate singleton resolution to parent Container', async () => {
|
|
142
|
+
@Injectable({ registry })
|
|
110
143
|
class SingletonService {
|
|
111
|
-
public readonly id = Math.random()
|
|
144
|
+
public readonly id = Math.random()
|
|
112
145
|
}
|
|
113
146
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
147
|
+
const scoped = container.beginRequest('test-request')
|
|
148
|
+
|
|
149
|
+
// Get singleton from scoped container
|
|
150
|
+
const instance1 = await scoped.get(SingletonService)
|
|
151
|
+
|
|
152
|
+
// Get singleton from main container
|
|
153
|
+
const instance2 = await container.get(SingletonService)
|
|
154
|
+
|
|
155
|
+
// Should be the same instance
|
|
156
|
+
expect(instance1).toBe(instance2)
|
|
157
|
+
|
|
158
|
+
await scoped.endRequest()
|
|
159
|
+
})
|
|
119
160
|
|
|
161
|
+
it('should delegate transient resolution to parent Container', async () => {
|
|
120
162
|
@Injectable({ registry, scope: InjectableScope.Transient })
|
|
121
163
|
class TransientService {
|
|
122
|
-
|
|
123
|
-
public readonly id = Math.random().toString(36)
|
|
164
|
+
public readonly id = Math.random()
|
|
124
165
|
}
|
|
125
166
|
|
|
126
|
-
|
|
127
|
-
container.beginRequest('test-request')
|
|
128
|
-
|
|
129
|
-
const requestService1 = await container.get(RequestService)
|
|
130
|
-
const requestService2 = await container.get(RequestService)
|
|
131
|
-
const singleton1 = await container.get(SingletonService)
|
|
132
|
-
const singleton2 = await container.get(SingletonService)
|
|
133
|
-
const transient1 = await container.get(TransientService)
|
|
134
|
-
const transient2 = await container.get(TransientService)
|
|
135
|
-
|
|
136
|
-
// Request-scoped: same instance within request
|
|
137
|
-
expect(requestService1).toBe(requestService2)
|
|
138
|
-
expect(requestService1.singleton).toBe(singleton1)
|
|
167
|
+
const scoped = container.beginRequest('test-request')
|
|
139
168
|
|
|
140
|
-
//
|
|
141
|
-
|
|
169
|
+
// Each get should create a new instance
|
|
170
|
+
const instance1 = await scoped.get(TransientService)
|
|
171
|
+
const instance2 = await scoped.get(TransientService)
|
|
142
172
|
|
|
143
|
-
|
|
144
|
-
expect(transient1).not.toBe(transient2)
|
|
145
|
-
expect(transient1.requestService).toBe(transient2.requestService)
|
|
173
|
+
expect(instance1).not.toBe(instance2)
|
|
146
174
|
|
|
147
|
-
await
|
|
175
|
+
await scoped.endRequest()
|
|
148
176
|
})
|
|
149
177
|
|
|
150
|
-
it('should
|
|
178
|
+
it('should allow request-scoped services to depend on singletons', async () => {
|
|
179
|
+
@Injectable({ registry })
|
|
180
|
+
class SingletonService {
|
|
181
|
+
public readonly id = Math.random()
|
|
182
|
+
}
|
|
183
|
+
|
|
151
184
|
@Injectable({ registry, scope: InjectableScope.Request })
|
|
152
185
|
class RequestService {
|
|
153
|
-
|
|
186
|
+
singleton = inject(SingletonService)
|
|
187
|
+
public readonly id = Math.random()
|
|
154
188
|
}
|
|
155
189
|
|
|
156
|
-
|
|
157
|
-
container.beginRequest('request-1')
|
|
158
|
-
const instance1 = await container.get(RequestService)
|
|
190
|
+
const scoped = container.beginRequest('test-request')
|
|
159
191
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
// Should be different instances
|
|
165
|
-
expect(instance1).not.toBe(instance2)
|
|
192
|
+
const requestInstance = await scoped.get(RequestService)
|
|
193
|
+
const singletonFromRequest = requestInstance.singleton
|
|
194
|
+
const singletonDirect = await container.get(SingletonService)
|
|
166
195
|
|
|
167
|
-
//
|
|
168
|
-
|
|
196
|
+
// The singleton injected into the request service should be the same
|
|
197
|
+
// as the one from the main container
|
|
198
|
+
expect(singletonFromRequest).toBe(singletonDirect)
|
|
169
199
|
|
|
170
|
-
|
|
171
|
-
const instance1Again = await container.get(RequestService)
|
|
172
|
-
expect(instance1).toBe(instance1Again)
|
|
173
|
-
|
|
174
|
-
// End first request
|
|
175
|
-
await container.endRequest('request-1')
|
|
200
|
+
await scoped.endRequest()
|
|
176
201
|
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('Request isolation (race condition prevention)', () => {
|
|
205
|
+
it('should prevent duplicate initialization during concurrent resolution within same request', async () => {
|
|
206
|
+
let initializationCount = 0
|
|
177
207
|
|
|
178
|
-
it('should handle request context switching', async () => {
|
|
179
208
|
@Injectable({ registry, scope: InjectableScope.Request })
|
|
180
209
|
class RequestService {
|
|
181
|
-
public readonly
|
|
182
|
-
}
|
|
210
|
+
public readonly instanceId: string
|
|
183
211
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
constructor() {
|
|
213
|
+
initializationCount++
|
|
214
|
+
this.instanceId = Math.random().toString(36)
|
|
215
|
+
}
|
|
188
216
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
217
|
+
async onServiceInit() {
|
|
218
|
+
// Simulate async initialization that takes time
|
|
219
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
192
222
|
|
|
193
|
-
|
|
194
|
-
container.setCurrentRequestContext('request-2')
|
|
195
|
-
const instance2 = await container.get(RequestService)
|
|
223
|
+
const scoped = container.beginRequest('test-request')
|
|
196
224
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
225
|
+
// Fire multiple concurrent resolution requests for the same service
|
|
226
|
+
const [instance1, instance2, instance3] = await Promise.all([
|
|
227
|
+
scoped.get(RequestService),
|
|
228
|
+
scoped.get(RequestService),
|
|
229
|
+
scoped.get(RequestService),
|
|
230
|
+
])
|
|
200
231
|
|
|
201
|
-
//
|
|
202
|
-
expect(instance1).toBe(
|
|
203
|
-
|
|
204
|
-
expect(
|
|
232
|
+
// All instances should be the same (no duplicate initialization)
|
|
233
|
+
expect(instance1).toBe(instance2)
|
|
234
|
+
expect(instance2).toBe(instance3)
|
|
235
|
+
expect(initializationCount).toBe(1) // Only initialized once
|
|
205
236
|
|
|
206
|
-
|
|
207
|
-
await container.endRequest('request-1')
|
|
208
|
-
await container.endRequest('request-2')
|
|
209
|
-
await container.endRequest('request-3')
|
|
237
|
+
await scoped.endRequest()
|
|
210
238
|
})
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
describe('RequestContextHolder', () => {
|
|
214
|
-
it('should manage instances correctly', () => {
|
|
215
|
-
const holder = createRequestContextHolder('test-request', 100, {
|
|
216
|
-
userId: 'user123',
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
expect(holder.requestId).toBe('test-request')
|
|
220
|
-
expect(holder.priority).toBe(100)
|
|
221
|
-
expect(holder.getMetadata('userId')).toBe('user123')
|
|
222
|
-
|
|
223
|
-
// Add instance
|
|
224
|
-
const mockInstance = { id: 'test-instance' }
|
|
225
|
-
const mockHolder = {
|
|
226
|
-
status: 'Created' as any,
|
|
227
|
-
name: 'test-instance',
|
|
228
|
-
instance: mockInstance,
|
|
229
|
-
creationPromise: null,
|
|
230
|
-
destroyPromise: null,
|
|
231
|
-
type: 'Class' as any,
|
|
232
|
-
scope: 'Request' as any,
|
|
233
|
-
deps: new Set<string>(),
|
|
234
|
-
destroyListeners: [],
|
|
235
|
-
createdAt: Date.now(),
|
|
236
|
-
}
|
|
237
239
|
|
|
238
|
-
|
|
240
|
+
it('should return correct instance when waiting for in-progress creation', async () => {
|
|
241
|
+
let creationOrder: string[] = []
|
|
239
242
|
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
244
|
+
class SlowService {
|
|
245
|
+
public readonly id: string
|
|
242
246
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
})
|
|
247
|
+
constructor() {
|
|
248
|
+
this.id = Math.random().toString(36)
|
|
249
|
+
}
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
async onServiceInit() {
|
|
252
|
+
creationOrder.push('init-start')
|
|
253
|
+
// Simulate slow async initialization
|
|
254
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
255
|
+
creationOrder.push('init-end')
|
|
256
|
+
}
|
|
257
|
+
}
|
|
250
258
|
|
|
251
|
-
|
|
252
|
-
holder.setMetadata('key2', 'value2')
|
|
259
|
+
const scoped = container.beginRequest('test-request')
|
|
253
260
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
261
|
+
// Start first resolution (will start creating)
|
|
262
|
+
const promise1 = scoped.get(SlowService)
|
|
263
|
+
creationOrder.push('promise1-started')
|
|
257
264
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
265
|
+
// Start second resolution while first is still in progress
|
|
266
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
267
|
+
creationOrder.push('promise2-starting')
|
|
268
|
+
const promise2 = scoped.get(SlowService)
|
|
261
269
|
|
|
262
|
-
|
|
263
|
-
const holder = createRequestContextHolder('test-request')
|
|
264
|
-
const token = InjectionToken.create<string>('TestService')
|
|
265
|
-
const instance = { id: 'test-instance', data: 'test-data' }
|
|
270
|
+
const [instance1, instance2] = await Promise.all([promise1, promise2])
|
|
266
271
|
|
|
267
|
-
//
|
|
268
|
-
|
|
272
|
+
// Both should be the same instance
|
|
273
|
+
expect(instance1).toBe(instance2)
|
|
274
|
+
expect(instance1.id).toBe(instance2.id)
|
|
269
275
|
|
|
270
|
-
// Verify
|
|
271
|
-
expect(
|
|
272
|
-
expect(
|
|
276
|
+
// Verify the initialization only happened once
|
|
277
|
+
expect(creationOrder.filter((x) => x === 'init-start').length).toBe(1)
|
|
278
|
+
expect(creationOrder.filter((x) => x === 'init-end').length).toBe(1)
|
|
273
279
|
|
|
274
|
-
|
|
275
|
-
const holderInfo = holder.get(token.toString())
|
|
276
|
-
expect(holderInfo).toBeDefined()
|
|
277
|
-
expect(holderInfo?.instance).toBe(instance)
|
|
278
|
-
expect(holderInfo?.name).toBe(token.toString())
|
|
280
|
+
await scoped.endRequest()
|
|
279
281
|
})
|
|
280
282
|
|
|
281
|
-
it('should
|
|
282
|
-
|
|
283
|
+
it('should isolate request contexts during concurrent async operations', async () => {
|
|
284
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
285
|
+
class RequestService {
|
|
286
|
+
public readonly requestId: string
|
|
283
287
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
288
|
+
constructor() {
|
|
289
|
+
this.requestId = Math.random().toString(36)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
287
292
|
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
const
|
|
293
|
+
// Start two requests concurrently
|
|
294
|
+
const scoped1 = container.beginRequest('request-1')
|
|
295
|
+
const scoped2 = container.beginRequest('request-2')
|
|
291
296
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
297
|
+
// Simulate concurrent async operations
|
|
298
|
+
const [instance1, instance2] = await Promise.all([
|
|
299
|
+
scoped1.get(RequestService),
|
|
300
|
+
scoped2.get(RequestService),
|
|
301
|
+
])
|
|
296
302
|
|
|
297
|
-
//
|
|
298
|
-
expect(
|
|
299
|
-
expect(holder.has(token2.toString())).toBe(true)
|
|
300
|
-
expect(holder.has(token3.toString())).toBe(true)
|
|
303
|
+
// Each request should have its own instance
|
|
304
|
+
expect(instance1.requestId).not.toBe(instance2.requestId)
|
|
301
305
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
306
|
+
// Verify they're still accessible after concurrent resolution
|
|
307
|
+
const instance1Again = await scoped1.get(RequestService)
|
|
308
|
+
const instance2Again = await scoped2.get(RequestService)
|
|
305
309
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const holder2 = holder.get(token2.toString())
|
|
309
|
-
const holder3 = holder.get(token3.toString())
|
|
310
|
+
expect(instance1).toBe(instance1Again)
|
|
311
|
+
expect(instance2).toBe(instance2Again)
|
|
310
312
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
expect(holder3?.instance).toBe(instance3)
|
|
313
|
+
await scoped1.endRequest()
|
|
314
|
+
await scoped2.endRequest()
|
|
314
315
|
})
|
|
315
316
|
|
|
316
|
-
it('should
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
it('should maintain correct context during interleaved async operations', async () => {
|
|
318
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
319
|
+
class RequestService {
|
|
320
|
+
public readonly requestId: string
|
|
321
|
+
public value = 0
|
|
319
322
|
|
|
320
|
-
|
|
321
|
-
|
|
323
|
+
constructor() {
|
|
324
|
+
this.requestId = Math.random().toString(36)
|
|
325
|
+
}
|
|
322
326
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
327
|
+
async asyncOperation(delay: number): Promise<void> {
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
329
|
+
this.value++
|
|
330
|
+
}
|
|
331
|
+
}
|
|
326
332
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
expect(holder.get(token.toString())?.instance).toBe(newInstance)
|
|
330
|
-
expect(holder.get(token.toString())?.instance).not.toBe(originalInstance)
|
|
333
|
+
const scoped1 = container.beginRequest('request-1')
|
|
334
|
+
const scoped2 = container.beginRequest('request-2')
|
|
331
335
|
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
expect(holderInfo?.instance).toBe(newInstance)
|
|
335
|
-
})
|
|
336
|
+
const instance1 = await scoped1.get(RequestService)
|
|
337
|
+
const instance2 = await scoped2.get(RequestService)
|
|
336
338
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
+
// Start async operations with different delays
|
|
340
|
+
await Promise.all([
|
|
341
|
+
instance1.asyncOperation(50),
|
|
342
|
+
instance2.asyncOperation(25),
|
|
343
|
+
instance1.asyncOperation(10),
|
|
344
|
+
instance2.asyncOperation(75),
|
|
345
|
+
])
|
|
339
346
|
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
// Each instance should have been modified independently
|
|
348
|
+
expect(instance1.value).toBe(2)
|
|
349
|
+
expect(instance2.value).toBe(2)
|
|
343
350
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
351
|
+
await scoped1.endRequest()
|
|
352
|
+
await scoped2.endRequest()
|
|
353
|
+
})
|
|
354
|
+
})
|
|
347
355
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
const classInstance = { type: 'class' }
|
|
356
|
+
describe('ScopedContainer API', () => {
|
|
357
|
+
it('should implement IContainer interface', async () => {
|
|
358
|
+
const scoped = container.beginRequest('test-request')
|
|
352
359
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
360
|
+
// Check that all IContainer methods exist
|
|
361
|
+
expect(typeof scoped.get).toBe('function')
|
|
362
|
+
expect(typeof scoped.invalidate).toBe('function')
|
|
363
|
+
expect(typeof scoped.isRegistered).toBe('function')
|
|
364
|
+
expect(typeof scoped.dispose).toBe('function')
|
|
365
|
+
expect(typeof scoped.ready).toBe('function')
|
|
366
|
+
expect(typeof scoped.tryGetSync).toBe('function')
|
|
356
367
|
|
|
357
|
-
|
|
358
|
-
expect(holder.get(symbolToken.toString())?.instance).toBe(symbolInstance)
|
|
359
|
-
expect(holder.get(classToken.toString())?.instance).toBe(classInstance)
|
|
368
|
+
await scoped.endRequest()
|
|
360
369
|
})
|
|
361
370
|
|
|
362
|
-
it('should
|
|
363
|
-
|
|
364
|
-
const token1 = InjectionToken.create<string>('Service1')
|
|
365
|
-
const token2 = InjectionToken.create<number>('Service2')
|
|
371
|
+
it('should track active request IDs in Container', async () => {
|
|
372
|
+
expect(container.hasActiveRequest('request-1')).toBe(false)
|
|
366
373
|
|
|
367
|
-
const
|
|
368
|
-
|
|
374
|
+
const scoped1 = container.beginRequest('request-1')
|
|
375
|
+
expect(container.hasActiveRequest('request-1')).toBe(true)
|
|
369
376
|
|
|
370
|
-
|
|
371
|
-
|
|
377
|
+
const scoped2 = container.beginRequest('request-2')
|
|
378
|
+
expect(container.hasActiveRequest('request-2')).toBe(true)
|
|
372
379
|
|
|
373
|
-
expect(
|
|
374
|
-
expect(holder.has(token2.toString())).toBe(true)
|
|
380
|
+
expect(container.getActiveRequestIds().size).toBe(2)
|
|
375
381
|
|
|
376
|
-
|
|
377
|
-
|
|
382
|
+
await scoped1.endRequest()
|
|
383
|
+
expect(container.hasActiveRequest('request-1')).toBe(false)
|
|
384
|
+
expect(container.hasActiveRequest('request-2')).toBe(true)
|
|
378
385
|
|
|
379
|
-
|
|
380
|
-
expect(
|
|
381
|
-
expect(holder.get(token1.toString())?.instance).toBeUndefined()
|
|
382
|
-
expect(holder.get(token2.toString())?.instance).toBeUndefined()
|
|
386
|
+
await scoped2.endRequest()
|
|
387
|
+
expect(container.getActiveRequestIds().size).toBe(0)
|
|
383
388
|
})
|
|
384
389
|
|
|
385
|
-
it('should
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
status: 'Created' as any,
|
|
400
|
-
name: stringName,
|
|
401
|
-
instance: stringInstance,
|
|
402
|
-
creationPromise: null,
|
|
403
|
-
destroyPromise: null,
|
|
404
|
-
type: 'Class' as any,
|
|
405
|
-
scope: 'Singleton' as any,
|
|
406
|
-
deps: new Set<string>(),
|
|
407
|
-
destroyListeners: [],
|
|
408
|
-
createdAt: Date.now(),
|
|
390
|
+
it('should return parent Container from ScopedContainer', async () => {
|
|
391
|
+
const scoped = container.beginRequest('test-request')
|
|
392
|
+
expect(scoped.getParent()).toBe(container)
|
|
393
|
+
await scoped.endRequest()
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('dispose() should be an alias for endRequest()', async () => {
|
|
397
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
398
|
+
class RequestService {
|
|
399
|
+
public destroyed = false
|
|
400
|
+
|
|
401
|
+
async onServiceDestroy() {
|
|
402
|
+
this.destroyed = true
|
|
403
|
+
}
|
|
409
404
|
}
|
|
410
|
-
holder.addInstance(stringName, stringInstance, mockHolder)
|
|
411
405
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
expect(holder.has(stringName)).toBe(true)
|
|
406
|
+
const scoped = container.beginRequest('test-request')
|
|
407
|
+
const instance = await scoped.get(RequestService)
|
|
415
408
|
|
|
416
|
-
|
|
417
|
-
|
|
409
|
+
// Use dispose() instead of endRequest()
|
|
410
|
+
await scoped.dispose()
|
|
418
411
|
|
|
419
|
-
|
|
420
|
-
expect(
|
|
421
|
-
expect(holder.get(stringName)?.instance).toBe(stringInstance)
|
|
412
|
+
expect(instance.destroyed).toBe(true)
|
|
413
|
+
expect(container.hasActiveRequest('test-request')).toBe(false)
|
|
422
414
|
})
|
|
423
415
|
})
|
|
424
416
|
})
|