@navios/di 0.5.1 → 0.6.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 +146 -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 +3012 -0
- package/lib/browser/index.mjs.map +1 -0
- package/lib/index-S_qX2VLI.d.mts +1211 -0
- package/lib/index-S_qX2VLI.d.mts.map +1 -0
- package/lib/index-fKPuT65j.d.cts +1206 -0
- package/lib/index-fKPuT65j.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-BMGmmxH7.cjs +2895 -0
- package/lib/testing-BMGmmxH7.cjs.map +1 -0
- package/lib/testing-DCXz8AJD.mjs +2655 -0
- package/lib/testing-DCXz8AJD.mjs.map +1 -0
- package/package.json +23 -1
- 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__/errors.spec.mts +6 -6
- 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 +13 -7
- 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
|
@@ -3,13 +3,14 @@ import { z } from 'zod/v4'
|
|
|
3
3
|
|
|
4
4
|
import type { OnServiceDestroy } from '../index.mjs'
|
|
5
5
|
|
|
6
|
+
import { Container } from '../container/container.mjs'
|
|
6
7
|
import { Injectable } from '../decorators/injectable.decorator.mjs'
|
|
7
8
|
import { InjectableScope } from '../enums/index.mjs'
|
|
8
9
|
import { getInjectableToken } from '../index.mjs'
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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'
|
|
13
14
|
|
|
14
15
|
describe('ServiceLocator', () => {
|
|
15
16
|
describe('getInstanceIdentifier', () => {
|
|
@@ -42,7 +43,7 @@ describe('ServiceLocator', () => {
|
|
|
42
43
|
})
|
|
43
44
|
|
|
44
45
|
describe('clearAll', () => {
|
|
45
|
-
let
|
|
46
|
+
let container: Container
|
|
46
47
|
let mockLogger: Console
|
|
47
48
|
|
|
48
49
|
beforeEach(() => {
|
|
@@ -54,10 +55,12 @@ describe('ServiceLocator', () => {
|
|
|
54
55
|
trace: vi.fn(),
|
|
55
56
|
} as any
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
container = new Container(globalRegistry, mockLogger)
|
|
58
59
|
})
|
|
59
60
|
|
|
60
61
|
it('should clear all services gracefully', async () => {
|
|
62
|
+
const serviceLocator = container.getServiceLocator()
|
|
63
|
+
|
|
61
64
|
// Create Injectable services
|
|
62
65
|
@Injectable({ scope: InjectableScope.Singleton })
|
|
63
66
|
class ServiceA {
|
|
@@ -74,13 +77,13 @@ describe('ServiceLocator', () => {
|
|
|
74
77
|
name = 'ServiceC'
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
// Create instances
|
|
78
|
-
await
|
|
79
|
-
await
|
|
80
|
-
await
|
|
80
|
+
// Create instances using container
|
|
81
|
+
await container.get(ServiceA)
|
|
82
|
+
await container.get(ServiceB)
|
|
83
|
+
await container.get(ServiceC)
|
|
81
84
|
|
|
82
|
-
// Verify services exist
|
|
83
|
-
expect(serviceLocator.getManager().size()).toBe(
|
|
85
|
+
// Verify services exist (container also registers itself as +1)
|
|
86
|
+
expect(serviceLocator.getManager().size()).toBe(4)
|
|
84
87
|
|
|
85
88
|
// Clear all services
|
|
86
89
|
await serviceLocator.clearAll()
|
|
@@ -88,19 +91,22 @@ describe('ServiceLocator', () => {
|
|
|
88
91
|
// Verify all services are cleared
|
|
89
92
|
expect(serviceLocator.getManager().size()).toBe(0)
|
|
90
93
|
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
91
|
-
'[
|
|
94
|
+
'[Invalidator] Graceful clearing completed',
|
|
92
95
|
)
|
|
93
96
|
})
|
|
94
97
|
|
|
95
98
|
it('should handle empty service locator', async () => {
|
|
99
|
+
const serviceLocator = new ServiceLocator(globalRegistry, mockLogger)
|
|
96
100
|
await serviceLocator.clearAll()
|
|
97
101
|
|
|
98
102
|
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
99
|
-
'[
|
|
103
|
+
'[Invalidator] No services to clear',
|
|
100
104
|
)
|
|
101
105
|
})
|
|
102
106
|
|
|
103
107
|
it('should clear service from a request context', async () => {
|
|
108
|
+
const serviceLocator = container.getServiceLocator()
|
|
109
|
+
|
|
104
110
|
@Injectable({ scope: InjectableScope.Singleton })
|
|
105
111
|
class ServiceA {
|
|
106
112
|
name = 'ServiceA'
|
|
@@ -113,20 +119,21 @@ describe('ServiceLocator', () => {
|
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
const requestId = 'test-request'
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
expect(error).toBeUndefined()
|
|
122
|
+
const scoped = container.beginRequest(requestId)
|
|
123
|
+
const serviceB = await scoped.get(ServiceB)
|
|
119
124
|
expect(serviceB).toBeDefined()
|
|
120
125
|
|
|
121
|
-
await
|
|
122
|
-
|
|
126
|
+
await scoped.invalidate(await container.get(ServiceA))
|
|
127
|
+
// Container itself remains
|
|
128
|
+
expect(serviceLocator.getManager().size()).toBe(1)
|
|
123
129
|
await serviceLocator.clearAll()
|
|
130
|
+
await scoped.endRequest()
|
|
124
131
|
})
|
|
125
132
|
|
|
126
|
-
it('should clear request contexts
|
|
133
|
+
it('should clear request contexts via ScopedContainer', async () => {
|
|
127
134
|
// Create a request context
|
|
128
135
|
const requestId = 'test-request'
|
|
129
|
-
|
|
136
|
+
const scoped = container.beginRequest(requestId)
|
|
130
137
|
|
|
131
138
|
// Create Injectable service with request scope
|
|
132
139
|
@Injectable({ scope: InjectableScope.Request })
|
|
@@ -134,31 +141,36 @@ describe('ServiceLocator', () => {
|
|
|
134
141
|
name = 'TestService'
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
await
|
|
144
|
+
await scoped.get(TestService)
|
|
138
145
|
|
|
139
|
-
// Verify request context exists
|
|
140
|
-
expect(
|
|
146
|
+
// Verify request context exists
|
|
147
|
+
expect(container.hasActiveRequest(requestId)).toBe(true)
|
|
141
148
|
|
|
142
|
-
//
|
|
143
|
-
await
|
|
149
|
+
// End request to clean up
|
|
150
|
+
await scoped.endRequest()
|
|
144
151
|
|
|
145
152
|
// Verify request context is cleared
|
|
146
|
-
expect(
|
|
153
|
+
expect(container.hasActiveRequest(requestId)).toBe(false)
|
|
147
154
|
})
|
|
148
155
|
|
|
149
|
-
it('should
|
|
156
|
+
it('should track active request contexts', async () => {
|
|
150
157
|
// Create a request context
|
|
151
158
|
const requestId = 'test-request'
|
|
152
|
-
|
|
159
|
+
const scoped = container.beginRequest(requestId)
|
|
153
160
|
|
|
154
|
-
//
|
|
155
|
-
|
|
161
|
+
// Verify request context exists
|
|
162
|
+
expect(container.hasActiveRequest(requestId)).toBe(true)
|
|
156
163
|
|
|
157
|
-
//
|
|
158
|
-
|
|
164
|
+
// End request
|
|
165
|
+
await scoped.endRequest()
|
|
166
|
+
|
|
167
|
+
// Verify request context is gone
|
|
168
|
+
expect(container.hasActiveRequest(requestId)).toBe(false)
|
|
159
169
|
})
|
|
160
170
|
|
|
161
171
|
it('should handle services with dependencies correctly', async () => {
|
|
172
|
+
const serviceLocator = container.getServiceLocator()
|
|
173
|
+
|
|
162
174
|
// Create Injectable services
|
|
163
175
|
@Injectable({ scope: InjectableScope.Singleton })
|
|
164
176
|
class ServiceA {
|
|
@@ -171,8 +183,8 @@ describe('ServiceLocator', () => {
|
|
|
171
183
|
name = 'ServiceB'
|
|
172
184
|
}
|
|
173
185
|
|
|
174
|
-
// Create instances
|
|
175
|
-
await
|
|
186
|
+
// Create instances using container
|
|
187
|
+
await container.get(ServiceB)
|
|
176
188
|
|
|
177
189
|
// Clear all services
|
|
178
190
|
await serviceLocator.clearAll()
|
|
@@ -182,13 +194,15 @@ describe('ServiceLocator', () => {
|
|
|
182
194
|
})
|
|
183
195
|
|
|
184
196
|
it('should respect maxRounds option', async () => {
|
|
197
|
+
const serviceLocator = container.getServiceLocator()
|
|
198
|
+
|
|
185
199
|
// Create Injectable service
|
|
186
200
|
@Injectable({ scope: InjectableScope.Singleton })
|
|
187
201
|
class TestService {
|
|
188
202
|
name = 'TestService'
|
|
189
203
|
}
|
|
190
204
|
|
|
191
|
-
await
|
|
205
|
+
await container.get(TestService)
|
|
192
206
|
|
|
193
207
|
// Clear with a very low maxRounds to test the limit
|
|
194
208
|
await serviceLocator.clearAll({ maxRounds: 1 })
|
|
@@ -198,6 +212,8 @@ describe('ServiceLocator', () => {
|
|
|
198
212
|
})
|
|
199
213
|
|
|
200
214
|
it('should clear services with dependencies in correct order', async () => {
|
|
215
|
+
const serviceLocator = container.getServiceLocator()
|
|
216
|
+
|
|
201
217
|
// Create services with dependencies
|
|
202
218
|
@Injectable({ scope: InjectableScope.Singleton })
|
|
203
219
|
class DatabaseService {
|
|
@@ -217,12 +233,12 @@ describe('ServiceLocator', () => {
|
|
|
217
233
|
}
|
|
218
234
|
|
|
219
235
|
// Create instances (this will establish dependencies)
|
|
220
|
-
await
|
|
221
|
-
await
|
|
222
|
-
await
|
|
236
|
+
await container.get(AuthService)
|
|
237
|
+
await container.get(UserService)
|
|
238
|
+
await container.get(DatabaseService)
|
|
223
239
|
|
|
224
|
-
// Verify services exist
|
|
225
|
-
expect(serviceLocator.getManager().size()).toBe(
|
|
240
|
+
// Verify services exist (container also registers itself as +1)
|
|
241
|
+
expect(serviceLocator.getManager().size()).toBe(4)
|
|
226
242
|
|
|
227
243
|
// Clear all services - should clear in dependency order
|
|
228
244
|
await serviceLocator.clearAll()
|
|
@@ -232,6 +248,8 @@ describe('ServiceLocator', () => {
|
|
|
232
248
|
})
|
|
233
249
|
|
|
234
250
|
it('should handle services with destroy listeners', async () => {
|
|
251
|
+
const serviceLocator = container.getServiceLocator()
|
|
252
|
+
|
|
235
253
|
let destroyCalled = false
|
|
236
254
|
@Injectable({ scope: InjectableScope.Singleton })
|
|
237
255
|
class TestService implements OnServiceDestroy {
|
|
@@ -246,7 +264,7 @@ describe('ServiceLocator', () => {
|
|
|
246
264
|
}
|
|
247
265
|
}
|
|
248
266
|
|
|
249
|
-
await
|
|
267
|
+
await container.get(TestService)
|
|
250
268
|
|
|
251
269
|
// Clear all services
|
|
252
270
|
await serviceLocator.clearAll()
|
|
@@ -258,7 +276,7 @@ describe('ServiceLocator', () => {
|
|
|
258
276
|
})
|
|
259
277
|
|
|
260
278
|
describe('Mixed Scope Services', () => {
|
|
261
|
-
let
|
|
279
|
+
let container: Container
|
|
262
280
|
let mockLogger: Console
|
|
263
281
|
|
|
264
282
|
beforeEach(() => {
|
|
@@ -270,7 +288,7 @@ describe('ServiceLocator', () => {
|
|
|
270
288
|
trace: vi.fn(),
|
|
271
289
|
} as any
|
|
272
290
|
|
|
273
|
-
|
|
291
|
+
container = new Container(globalRegistry, mockLogger)
|
|
274
292
|
})
|
|
275
293
|
|
|
276
294
|
describe('Services with dependencies across different scopes', () => {
|
|
@@ -290,13 +308,9 @@ describe('ServiceLocator', () => {
|
|
|
290
308
|
}
|
|
291
309
|
|
|
292
310
|
// Get instances
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
const [error2, singleton2] =
|
|
296
|
-
await serviceLocator.getInstance(SingletonService)
|
|
311
|
+
const singleton1 = await container.get(SingletonService)
|
|
312
|
+
const singleton2 = await container.get(SingletonService)
|
|
297
313
|
|
|
298
|
-
expect(error1).toBeUndefined()
|
|
299
|
-
expect(error2).toBeUndefined()
|
|
300
314
|
expect(singleton1).toBe(singleton2) // Same singleton instance
|
|
301
315
|
|
|
302
316
|
// Get the actual transient service instances (asyncInject returns Promises)
|
|
@@ -324,32 +338,26 @@ describe('ServiceLocator', () => {
|
|
|
324
338
|
}
|
|
325
339
|
|
|
326
340
|
// Begin request context
|
|
327
|
-
const
|
|
328
|
-
serviceLocator.beginRequest(requestId)
|
|
341
|
+
const scoped1 = container.beginRequest('test-request-1')
|
|
329
342
|
|
|
330
343
|
// Get instances within the same request
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
const [error2, request2] =
|
|
334
|
-
await serviceLocator.getInstance(RequestService)
|
|
344
|
+
const request1 = await scoped1.get(RequestService)
|
|
345
|
+
const request2 = await scoped1.get(RequestService)
|
|
335
346
|
|
|
336
|
-
expect(error1).toBeUndefined()
|
|
337
|
-
expect(error2).toBeUndefined()
|
|
338
347
|
expect(request1).toBe(request2) // Same request-scoped instance
|
|
339
348
|
expect(request1.singletonService).toBe(request2.singletonService) // Same singleton instance
|
|
340
349
|
|
|
341
350
|
// End request and start new one
|
|
342
|
-
await
|
|
343
|
-
const
|
|
344
|
-
serviceLocator.beginRequest(newRequestId)
|
|
351
|
+
await scoped1.endRequest()
|
|
352
|
+
const scoped2 = container.beginRequest('test-request-2')
|
|
345
353
|
|
|
346
354
|
// Get instance in new request
|
|
347
|
-
const
|
|
348
|
-
await serviceLocator.getInstance(RequestService)
|
|
355
|
+
const request3 = await scoped2.get(RequestService)
|
|
349
356
|
|
|
350
|
-
expect(error3).toBeUndefined()
|
|
351
357
|
expect(request1).not.toBe(request3) // Different request-scoped instances
|
|
352
358
|
expect(request1.singletonService).toBe(request3.singletonService) // Same singleton instance
|
|
359
|
+
|
|
360
|
+
await scoped2.endRequest()
|
|
353
361
|
})
|
|
354
362
|
|
|
355
363
|
it('should handle Transient service depending on Request service', async () => {
|
|
@@ -368,23 +376,20 @@ describe('ServiceLocator', () => {
|
|
|
368
376
|
}
|
|
369
377
|
|
|
370
378
|
// Begin request context
|
|
371
|
-
const
|
|
372
|
-
serviceLocator.beginRequest(requestId)
|
|
379
|
+
const scoped = container.beginRequest('test-request')
|
|
373
380
|
|
|
374
381
|
// Get multiple transient instances
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
const [error2, transient2] =
|
|
378
|
-
await serviceLocator.getInstance(TransientService)
|
|
382
|
+
const transient1 = await scoped.get(TransientService)
|
|
383
|
+
const transient2 = await scoped.get(TransientService)
|
|
379
384
|
|
|
380
|
-
expect(error1).toBeUndefined()
|
|
381
|
-
expect(error2).toBeUndefined()
|
|
382
385
|
expect(transient1).not.toBe(transient2) // Different transient instances
|
|
383
386
|
|
|
384
|
-
// Get the actual request service instances
|
|
387
|
+
// Get the actual request service instances
|
|
385
388
|
const requestService1 = transient1.requestService
|
|
386
389
|
const requestService2 = transient2.requestService
|
|
387
390
|
expect(requestService1).toBe(requestService2) // Same request-scoped instance
|
|
391
|
+
|
|
392
|
+
await scoped.endRequest()
|
|
388
393
|
})
|
|
389
394
|
|
|
390
395
|
it('should handle complex dependency chain across all scopes', async () => {
|
|
@@ -417,20 +422,12 @@ describe('ServiceLocator', () => {
|
|
|
417
422
|
}
|
|
418
423
|
|
|
419
424
|
// Begin request context
|
|
420
|
-
const
|
|
421
|
-
serviceLocator.beginRequest(requestId)
|
|
425
|
+
const scoped = container.beginRequest('complex-request')
|
|
422
426
|
|
|
423
427
|
// Get instances
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
await serviceLocator.getInstance(UserActionService)
|
|
428
|
-
const [error3, manager] =
|
|
429
|
-
await serviceLocator.getInstance(UserManagerService)
|
|
430
|
-
|
|
431
|
-
expect(error1).toBeUndefined()
|
|
432
|
-
expect(error2).toBeUndefined()
|
|
433
|
-
expect(error3).toBeUndefined()
|
|
428
|
+
const action1 = await scoped.get(UserActionService)
|
|
429
|
+
const action2 = await scoped.get(UserActionService)
|
|
430
|
+
const manager = await container.get(UserManagerService)
|
|
434
431
|
|
|
435
432
|
// Verify instances are created
|
|
436
433
|
expect(action1).toBeDefined()
|
|
@@ -440,7 +437,7 @@ describe('ServiceLocator', () => {
|
|
|
440
437
|
// Verify scope behavior - check if dependencies are properly injected
|
|
441
438
|
expect(action1).not.toBe(action2) // Different transient instances
|
|
442
439
|
|
|
443
|
-
// Get the actual dependency instances
|
|
440
|
+
// Get the actual dependency instances
|
|
444
441
|
const action1Database = action1.database
|
|
445
442
|
const action2Database = action2.database
|
|
446
443
|
const action1Session = action1.session
|
|
@@ -454,18 +451,12 @@ describe('ServiceLocator', () => {
|
|
|
454
451
|
expect(action1Session.database).toBe(action1Database) // Same singleton instance
|
|
455
452
|
|
|
456
453
|
// End request and start new one
|
|
457
|
-
await
|
|
458
|
-
const
|
|
459
|
-
serviceLocator.beginRequest(newRequestId)
|
|
454
|
+
await scoped.endRequest()
|
|
455
|
+
const scoped2 = container.beginRequest('complex-request-2')
|
|
460
456
|
|
|
461
457
|
// Get instances in new request
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
const [error5, manager2] =
|
|
465
|
-
await serviceLocator.getInstance(UserManagerService)
|
|
466
|
-
|
|
467
|
-
expect(error4).toBeUndefined()
|
|
468
|
-
expect(error5).toBeUndefined()
|
|
458
|
+
const action3 = await scoped2.get(UserActionService)
|
|
459
|
+
const manager2 = await container.get(UserManagerService)
|
|
469
460
|
|
|
470
461
|
// Verify scope behavior across requests
|
|
471
462
|
expect(action1).not.toBe(action3) // Different transient instances
|
|
@@ -479,6 +470,8 @@ describe('ServiceLocator', () => {
|
|
|
479
470
|
|
|
480
471
|
// Check if session dependency is properly injected in new request
|
|
481
472
|
expect(action1Session).not.toBe(action3Session) // Different request-scoped instances
|
|
473
|
+
|
|
474
|
+
await scoped2.endRequest()
|
|
482
475
|
})
|
|
483
476
|
})
|
|
484
477
|
|
|
@@ -491,21 +484,15 @@ describe('ServiceLocator', () => {
|
|
|
491
484
|
}
|
|
492
485
|
|
|
493
486
|
// First request
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
await serviceLocator.getInstance(RequestService)
|
|
498
|
-
await serviceLocator.endRequest(requestId1)
|
|
487
|
+
const scoped1 = container.beginRequest('request-1')
|
|
488
|
+
const service1 = await scoped1.get(RequestService)
|
|
489
|
+
await scoped1.endRequest()
|
|
499
490
|
|
|
500
491
|
// Second request
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
await serviceLocator.endRequest(requestId2)
|
|
506
|
-
|
|
507
|
-
expect(error1).toBeUndefined()
|
|
508
|
-
expect(error2).toBeUndefined()
|
|
492
|
+
const scoped2 = container.beginRequest('request-2')
|
|
493
|
+
const service2 = await scoped2.get(RequestService)
|
|
494
|
+
await scoped2.endRequest()
|
|
495
|
+
|
|
509
496
|
expect(service1).not.toBe(service2) // Different instances
|
|
510
497
|
expect(service1.id).not.toBe(service2.id) // Different IDs
|
|
511
498
|
})
|
|
@@ -518,21 +505,15 @@ describe('ServiceLocator', () => {
|
|
|
518
505
|
}
|
|
519
506
|
|
|
520
507
|
// First request
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
await serviceLocator.getInstance(SingletonService)
|
|
525
|
-
await serviceLocator.endRequest(requestId1)
|
|
508
|
+
const scoped1 = container.beginRequest('request-1')
|
|
509
|
+
const service1 = await scoped1.get(SingletonService)
|
|
510
|
+
await scoped1.endRequest()
|
|
526
511
|
|
|
527
512
|
// Second request
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
await serviceLocator.endRequest(requestId2)
|
|
533
|
-
|
|
534
|
-
expect(error1).toBeUndefined()
|
|
535
|
-
expect(error2).toBeUndefined()
|
|
513
|
+
const scoped2 = container.beginRequest('request-2')
|
|
514
|
+
const service2 = await scoped2.get(SingletonService)
|
|
515
|
+
await scoped2.endRequest()
|
|
516
|
+
|
|
536
517
|
expect(service1).toBe(service2) // Same instance
|
|
537
518
|
expect(service1.id).toBe(service2.id) // Same ID
|
|
538
519
|
})
|
|
@@ -544,19 +525,10 @@ describe('ServiceLocator', () => {
|
|
|
544
525
|
name = 'TransientService'
|
|
545
526
|
}
|
|
546
527
|
|
|
547
|
-
const
|
|
548
|
-
|
|
528
|
+
const service1 = await container.get(TransientService)
|
|
529
|
+
const service2 = await container.get(TransientService)
|
|
530
|
+
const service3 = await container.get(TransientService)
|
|
549
531
|
|
|
550
|
-
const [error1, service1] =
|
|
551
|
-
await serviceLocator.getInstance(TransientService)
|
|
552
|
-
const [error2, service2] =
|
|
553
|
-
await serviceLocator.getInstance(TransientService)
|
|
554
|
-
const [error3, service3] =
|
|
555
|
-
await serviceLocator.getInstance(TransientService)
|
|
556
|
-
|
|
557
|
-
expect(error1).toBeUndefined()
|
|
558
|
-
expect(error2).toBeUndefined()
|
|
559
|
-
expect(error3).toBeUndefined()
|
|
560
532
|
expect(service1).not.toBe(service2) // Different instances
|
|
561
533
|
expect(service1).not.toBe(service3) // Different instances
|
|
562
534
|
expect(service2).not.toBe(service3) // Different instances
|
|
@@ -580,42 +552,32 @@ describe('ServiceLocator', () => {
|
|
|
580
552
|
name = 'SingletonService'
|
|
581
553
|
}
|
|
582
554
|
|
|
583
|
-
const
|
|
584
|
-
serviceLocator.beginRequest(requestId)
|
|
555
|
+
const scoped = container.beginRequest('cleanup-test')
|
|
585
556
|
|
|
586
557
|
// Create instances
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
const [error2, singletonService] =
|
|
590
|
-
await serviceLocator.getInstance(SingletonService)
|
|
591
|
-
|
|
592
|
-
expect(error1).toBeUndefined()
|
|
593
|
-
expect(error2).toBeUndefined()
|
|
558
|
+
const _requestService = await scoped.get(RequestService)
|
|
559
|
+
const singletonService = await scoped.get(SingletonService)
|
|
594
560
|
|
|
595
561
|
// Verify request context exists
|
|
596
|
-
expect(
|
|
597
|
-
expect(serviceLocator.getCurrentRequestContext()?.requestId).toBe(
|
|
598
|
-
requestId,
|
|
599
|
-
)
|
|
562
|
+
expect(container.hasActiveRequest('cleanup-test')).toBe(true)
|
|
600
563
|
|
|
601
564
|
// End request
|
|
602
|
-
await
|
|
565
|
+
await scoped.endRequest()
|
|
603
566
|
|
|
604
567
|
// Verify request context is cleared
|
|
605
|
-
expect(
|
|
568
|
+
expect(container.hasActiveRequest('cleanup-test')).toBe(false)
|
|
606
569
|
|
|
607
570
|
// Singleton should still be available
|
|
608
|
-
const
|
|
609
|
-
await serviceLocator.getInstance(SingletonService)
|
|
610
|
-
expect(error3).toBeUndefined()
|
|
571
|
+
const singletonService2 = await container.get(SingletonService)
|
|
611
572
|
expect(singletonService).toBe(singletonService2) // Same singleton instance
|
|
612
573
|
|
|
613
574
|
// Request service should not be available (no current request context)
|
|
614
|
-
|
|
615
|
-
|
|
575
|
+
await expect(container.get(RequestService)).rejects.toThrow(
|
|
576
|
+
/Cannot resolve request-scoped service/,
|
|
577
|
+
)
|
|
616
578
|
})
|
|
617
579
|
|
|
618
|
-
it('should handle
|
|
580
|
+
it('should handle parallel request contexts with mixed scopes', async () => {
|
|
619
581
|
@Injectable({ scope: InjectableScope.Request })
|
|
620
582
|
class RequestService {
|
|
621
583
|
id = Math.random().toString(36).substr(2, 9)
|
|
@@ -629,43 +591,26 @@ describe('ServiceLocator', () => {
|
|
|
629
591
|
}
|
|
630
592
|
|
|
631
593
|
// First request
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const requestId2 = 'inner-request'
|
|
641
|
-
serviceLocator.beginRequest(requestId2)
|
|
642
|
-
const [error3, requestService2] =
|
|
643
|
-
await serviceLocator.getInstance(RequestService)
|
|
644
|
-
const [error4, singletonService2] =
|
|
645
|
-
await serviceLocator.getInstance(SingletonService)
|
|
646
|
-
|
|
647
|
-
expect(error1).toBeUndefined()
|
|
648
|
-
expect(error2).toBeUndefined()
|
|
649
|
-
expect(error3).toBeUndefined()
|
|
650
|
-
expect(error4).toBeUndefined()
|
|
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)
|
|
651
602
|
|
|
652
603
|
// Verify instances
|
|
653
604
|
expect(requestService1).not.toBe(requestService2) // Different request instances
|
|
654
605
|
expect(singletonService1).toBe(singletonService2) // Same singleton instance
|
|
655
606
|
|
|
656
|
-
// End
|
|
657
|
-
await
|
|
658
|
-
|
|
659
|
-
// Verify current context is back to outer request
|
|
660
|
-
expect(serviceLocator.getCurrentRequestContext()?.requestId).toBe(
|
|
661
|
-
requestId1,
|
|
662
|
-
)
|
|
607
|
+
// End both requests
|
|
608
|
+
await scoped2.endRequest()
|
|
609
|
+
await scoped1.endRequest()
|
|
663
610
|
|
|
664
|
-
//
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
// Verify no current context
|
|
668
|
-
expect(serviceLocator.getCurrentRequestContext()).toBeNull()
|
|
611
|
+
// Verify no active contexts
|
|
612
|
+
expect(container.hasActiveRequest('outer-request')).toBe(false)
|
|
613
|
+
expect(container.hasActiveRequest('inner-request')).toBe(false)
|
|
669
614
|
})
|
|
670
615
|
|
|
671
616
|
it('should handle concurrent requests with mixed scopes', async () => {
|
|
@@ -681,17 +626,15 @@ describe('ServiceLocator', () => {
|
|
|
681
626
|
name = 'SingletonService'
|
|
682
627
|
}
|
|
683
628
|
|
|
684
|
-
// Start multiple requests sequentially
|
|
629
|
+
// Start multiple requests sequentially
|
|
685
630
|
const requestIds = ['req-1', 'req-2', 'req-3']
|
|
686
631
|
const results = []
|
|
687
632
|
|
|
688
633
|
for (const requestId of requestIds) {
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
await serviceLocator.getInstance(SingletonService)
|
|
694
|
-
await serviceLocator.endRequest(requestId)
|
|
634
|
+
const scoped = container.beginRequest(requestId)
|
|
635
|
+
const requestService = await scoped.get(RequestService)
|
|
636
|
+
const singletonService = await scoped.get(SingletonService)
|
|
637
|
+
await scoped.endRequest()
|
|
695
638
|
results.push({ requestService, singletonService, requestId })
|
|
696
639
|
}
|
|
697
640
|
|
|
@@ -721,10 +664,9 @@ describe('ServiceLocator', () => {
|
|
|
721
664
|
}
|
|
722
665
|
|
|
723
666
|
// Try to get Request-scoped service without request context
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
expect(error?.code).toBe('UnknownError')
|
|
667
|
+
await expect(container.get(RequestService)).rejects.toThrow(
|
|
668
|
+
/Cannot resolve request-scoped service/,
|
|
669
|
+
)
|
|
728
670
|
})
|
|
729
671
|
|
|
730
672
|
it('should handle service instantiation errors in mixed scope scenario', async () => {
|
|
@@ -742,23 +684,23 @@ describe('ServiceLocator', () => {
|
|
|
742
684
|
name = 'RequestService'
|
|
743
685
|
}
|
|
744
686
|
|
|
745
|
-
const
|
|
746
|
-
serviceLocator.beginRequest(requestId)
|
|
687
|
+
const scoped = container.beginRequest('error-test')
|
|
747
688
|
|
|
748
689
|
// Try to get Request service that depends on failing Singleton
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
690
|
+
await expect(scoped.get(RequestService)).rejects.toThrow(
|
|
691
|
+
'Singleton creation failed',
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
await scoped.endRequest()
|
|
753
695
|
})
|
|
754
696
|
})
|
|
755
697
|
})
|
|
756
698
|
|
|
757
699
|
describe('Injectable with Schema', () => {
|
|
758
|
-
let
|
|
700
|
+
let container: Container
|
|
759
701
|
|
|
760
702
|
beforeEach(() => {
|
|
761
|
-
|
|
703
|
+
container = new Container(globalRegistry)
|
|
762
704
|
})
|
|
763
705
|
|
|
764
706
|
it('should work with simple schema definition', async () => {
|
|
@@ -777,16 +719,17 @@ describe('ServiceLocator', () => {
|
|
|
777
719
|
}
|
|
778
720
|
|
|
779
721
|
const token = getInjectableToken(DatabaseConfig)
|
|
780
|
-
const
|
|
722
|
+
const instance = await container.get(
|
|
781
723
|
InjectionToken.bound(token, {
|
|
782
724
|
host: 'localhost',
|
|
783
725
|
port: 5432,
|
|
784
726
|
}),
|
|
785
727
|
)
|
|
786
728
|
|
|
787
|
-
expect(error).toBeUndefined()
|
|
788
729
|
expect(instance).toBeInstanceOf(DatabaseConfig)
|
|
730
|
+
// @ts-expect-error - instance is of type DatabaseConfig
|
|
789
731
|
expect(instance.config).toEqual({ host: 'localhost', port: 5432 })
|
|
732
|
+
// @ts-expect-error - instance is of type DatabaseConfig
|
|
790
733
|
expect(instance.getConnectionString()).toBe('localhost:5432')
|
|
791
734
|
})
|
|
792
735
|
|
|
@@ -805,17 +748,15 @@ describe('ServiceLocator', () => {
|
|
|
805
748
|
}
|
|
806
749
|
}
|
|
807
750
|
|
|
808
|
-
const
|
|
751
|
+
const instance1 = await container.get(ApiClient, {
|
|
809
752
|
apiKey: 'secret-key',
|
|
810
753
|
baseUrl: 'https://api.example.com',
|
|
811
754
|
})
|
|
812
|
-
const
|
|
755
|
+
const instance2 = await container.get(ApiClient, {
|
|
813
756
|
apiKey: 'secret-key',
|
|
814
757
|
baseUrl: 'https://api.example.com',
|
|
815
758
|
})
|
|
816
759
|
|
|
817
|
-
expect(error1).toBeUndefined()
|
|
818
|
-
expect(error2).toBeUndefined()
|
|
819
760
|
expect(instance1).toBe(instance2) // Same singleton instance
|
|
820
761
|
expect(instance1.getApiKey()).toBe('secret-key')
|
|
821
762
|
})
|
|
@@ -835,17 +776,15 @@ describe('ServiceLocator', () => {
|
|
|
835
776
|
}
|
|
836
777
|
}
|
|
837
778
|
|
|
838
|
-
const
|
|
779
|
+
const instance1 = await container.get(Logger, {
|
|
839
780
|
level: 'info' as const,
|
|
840
781
|
prefix: 'APP',
|
|
841
782
|
})
|
|
842
|
-
const
|
|
783
|
+
const instance2 = await container.get(Logger, {
|
|
843
784
|
level: 'info' as const,
|
|
844
785
|
prefix: 'APP',
|
|
845
786
|
})
|
|
846
787
|
|
|
847
|
-
expect(error1).toBeUndefined()
|
|
848
|
-
expect(error2).toBeUndefined()
|
|
849
788
|
expect(instance1).not.toBe(instance2) // Different transient instances
|
|
850
789
|
expect(instance1.log('test')).toBe('[APP] test')
|
|
851
790
|
expect(instance2.log('test')).toBe('[APP] test')
|
|
@@ -872,11 +811,8 @@ describe('ServiceLocator', () => {
|
|
|
872
811
|
}
|
|
873
812
|
}
|
|
874
813
|
|
|
875
|
-
const
|
|
876
|
-
await serviceLocator.getInstance(DatabaseService)
|
|
814
|
+
const instance = await container.get(DatabaseService)
|
|
877
815
|
|
|
878
|
-
expect(error).toBeUndefined()
|
|
879
|
-
if (!instance) throw new Error('Instance is undefined')
|
|
880
816
|
expect(instance).toBeInstanceOf(DatabaseService)
|
|
881
817
|
expect(instance.getConnectionString()).toBe(
|
|
882
818
|
'postgres://localhost:5432/mydb',
|
|
@@ -909,10 +845,8 @@ describe('ServiceLocator', () => {
|
|
|
909
845
|
}
|
|
910
846
|
}
|
|
911
847
|
|
|
912
|
-
const
|
|
848
|
+
const instance = await container.get(CacheService)
|
|
913
849
|
|
|
914
|
-
expect(error).toBeUndefined()
|
|
915
|
-
if (!instance) throw new Error('Instance is undefined')
|
|
916
850
|
expect(instance).toBeInstanceOf(CacheService)
|
|
917
851
|
const config = await instance.getConfig()
|
|
918
852
|
expect(config).toEqual({ ttl: 3600, maxSize: 1000 })
|
|
@@ -930,27 +864,19 @@ describe('ServiceLocator', () => {
|
|
|
930
864
|
}
|
|
931
865
|
|
|
932
866
|
// Valid configuration
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
optional: 42,
|
|
938
|
-
},
|
|
939
|
-
)
|
|
867
|
+
const instance1 = await container.get(StrictService, {
|
|
868
|
+
required: 'value',
|
|
869
|
+
optional: 42,
|
|
870
|
+
})
|
|
940
871
|
|
|
941
|
-
expect(error1).toBeUndefined()
|
|
942
872
|
expect(instance1).toBeInstanceOf(StrictService)
|
|
943
873
|
expect(instance1.config).toEqual({ required: 'value', optional: 42 })
|
|
944
874
|
|
|
945
875
|
// Valid with optional field missing
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
required: 'another value',
|
|
950
|
-
},
|
|
951
|
-
)
|
|
876
|
+
const instance2 = await container.get(StrictService, {
|
|
877
|
+
required: 'another value',
|
|
878
|
+
})
|
|
952
879
|
|
|
953
|
-
expect(error2).toBeUndefined()
|
|
954
880
|
expect(instance2).toBeInstanceOf(StrictService)
|
|
955
881
|
expect(instance2.config).toEqual({ required: 'another value' })
|
|
956
882
|
})
|
|
@@ -984,7 +910,7 @@ describe('ServiceLocator', () => {
|
|
|
984
910
|
}
|
|
985
911
|
}
|
|
986
912
|
|
|
987
|
-
const
|
|
913
|
+
const instance = await container.get(AppConfig, {
|
|
988
914
|
database: {
|
|
989
915
|
host: 'db.example.com',
|
|
990
916
|
port: 5432,
|
|
@@ -999,7 +925,6 @@ describe('ServiceLocator', () => {
|
|
|
999
925
|
},
|
|
1000
926
|
})
|
|
1001
927
|
|
|
1002
|
-
expect(error).toBeUndefined()
|
|
1003
928
|
expect(instance).toBeInstanceOf(AppConfig)
|
|
1004
929
|
expect(instance.getDatabaseHost()).toBe('db.example.com')
|
|
1005
930
|
expect(instance.isCacheEnabled()).toBe(true)
|
|
@@ -1025,19 +950,17 @@ describe('ServiceLocator', () => {
|
|
|
1025
950
|
}
|
|
1026
951
|
}
|
|
1027
952
|
|
|
1028
|
-
const
|
|
1029
|
-
serviceLocator.beginRequest(requestId)
|
|
953
|
+
const scoped = container.beginRequest('test-request')
|
|
1030
954
|
|
|
1031
|
-
const
|
|
955
|
+
const instance = await scoped.get(UserContext, {
|
|
1032
956
|
userId: 'user-123',
|
|
1033
957
|
sessionId: 'session-456',
|
|
1034
958
|
})
|
|
1035
959
|
|
|
1036
|
-
expect(error).toBeUndefined()
|
|
1037
960
|
expect(instance).toBeInstanceOf(UserContext)
|
|
1038
961
|
expect(instance.getUserId()).toBe('user-123')
|
|
1039
962
|
|
|
1040
|
-
await
|
|
963
|
+
await scoped.endRequest()
|
|
1041
964
|
})
|
|
1042
965
|
})
|
|
1043
966
|
})
|