@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.
Files changed (122) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +196 -219
  3. package/docs/README.md +69 -11
  4. package/docs/api-reference.md +281 -117
  5. package/docs/container.md +220 -56
  6. package/docs/examples/request-scope-example.mts +2 -2
  7. package/docs/factory.md +3 -8
  8. package/docs/getting-started.md +37 -8
  9. package/docs/migration.md +318 -37
  10. package/docs/request-contexts.md +263 -175
  11. package/docs/scopes.md +79 -42
  12. package/lib/browser/index.d.mts +1577 -0
  13. package/lib/browser/index.d.mts.map +1 -0
  14. package/lib/browser/index.mjs +3013 -0
  15. package/lib/browser/index.mjs.map +1 -0
  16. package/lib/index-7jfWsiG4.d.mts +1211 -0
  17. package/lib/index-7jfWsiG4.d.mts.map +1 -0
  18. package/lib/index-DW3K5sOX.d.cts +1206 -0
  19. package/lib/index-DW3K5sOX.d.cts.map +1 -0
  20. package/lib/index.cjs +389 -0
  21. package/lib/index.cjs.map +1 -0
  22. package/lib/index.d.cts +376 -0
  23. package/lib/index.d.cts.map +1 -0
  24. package/lib/index.d.mts +371 -78
  25. package/lib/index.d.mts.map +1 -0
  26. package/lib/index.mjs +325 -63
  27. package/lib/index.mjs.map +1 -1
  28. package/lib/testing/index.cjs +9 -0
  29. package/lib/testing/index.d.cts +2 -0
  30. package/lib/testing/index.d.mts +2 -2
  31. package/lib/testing/index.mjs +2 -72
  32. package/lib/testing-BG_fa9TJ.mjs +2656 -0
  33. package/lib/testing-BG_fa9TJ.mjs.map +1 -0
  34. package/lib/testing-DIaIRiJz.cjs +2896 -0
  35. package/lib/testing-DIaIRiJz.cjs.map +1 -0
  36. package/package.json +29 -7
  37. package/project.json +2 -2
  38. package/src/__tests__/async-local-storage.browser.spec.mts +240 -0
  39. package/src/__tests__/async-local-storage.spec.mts +333 -0
  40. package/src/__tests__/container.spec.mts +30 -25
  41. package/src/__tests__/e2e.browser.spec.mts +790 -0
  42. package/src/__tests__/e2e.spec.mts +1222 -0
  43. package/src/__tests__/factory.spec.mts +1 -1
  44. package/src/__tests__/get-injectors.spec.mts +1 -1
  45. package/src/__tests__/injectable.spec.mts +1 -1
  46. package/src/__tests__/injection-token.spec.mts +1 -1
  47. package/src/__tests__/library-findings.spec.mts +563 -0
  48. package/src/__tests__/registry.spec.mts +2 -2
  49. package/src/__tests__/request-scope.spec.mts +266 -274
  50. package/src/__tests__/service-instantiator.spec.mts +18 -17
  51. package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
  52. package/src/__tests__/service-locator-manager.spec.mts +15 -15
  53. package/src/__tests__/service-locator.spec.mts +167 -244
  54. package/src/__tests__/unified-api.spec.mts +27 -27
  55. package/src/__type-tests__/factory.spec-d.mts +2 -2
  56. package/src/__type-tests__/inject.spec-d.mts +2 -2
  57. package/src/__type-tests__/injectable.spec-d.mts +1 -1
  58. package/src/browser.mts +16 -0
  59. package/src/container/container.mts +319 -0
  60. package/src/container/index.mts +2 -0
  61. package/src/container/scoped-container.mts +350 -0
  62. package/src/decorators/factory.decorator.mts +4 -4
  63. package/src/decorators/injectable.decorator.mts +5 -5
  64. package/src/errors/di-error.mts +12 -5
  65. package/src/errors/index.mts +0 -8
  66. package/src/index.mts +156 -15
  67. package/src/interfaces/container.interface.mts +82 -0
  68. package/src/interfaces/factory.interface.mts +2 -2
  69. package/src/interfaces/index.mts +1 -0
  70. package/src/internal/context/async-local-storage.mts +120 -0
  71. package/src/internal/context/factory-context.mts +18 -0
  72. package/src/internal/context/index.mts +3 -0
  73. package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
  74. package/src/internal/context/resolution-context.mts +63 -0
  75. package/src/internal/context/sync-local-storage.mts +51 -0
  76. package/src/internal/core/index.mts +5 -0
  77. package/src/internal/core/instance-resolver.mts +641 -0
  78. package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
  79. package/src/internal/core/invalidator.mts +437 -0
  80. package/src/internal/core/service-locator.mts +202 -0
  81. package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
  82. package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
  83. package/src/internal/holder/holder-manager.mts +85 -0
  84. package/src/internal/holder/holder-storage.interface.mts +116 -0
  85. package/src/internal/holder/index.mts +6 -0
  86. package/src/internal/holder/instance-holder.mts +109 -0
  87. package/src/internal/holder/request-storage.mts +134 -0
  88. package/src/internal/holder/singleton-storage.mts +105 -0
  89. package/src/internal/index.mts +4 -0
  90. package/src/internal/lifecycle/circular-detector.mts +77 -0
  91. package/src/internal/lifecycle/index.mts +2 -0
  92. package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +11 -4
  93. package/src/testing/__tests__/test-container.spec.mts +2 -2
  94. package/src/testing/test-container.mts +4 -4
  95. package/src/token/index.mts +2 -0
  96. package/src/{injection-token.mts → token/injection-token.mts} +1 -1
  97. package/src/{registry.mts → token/registry.mts} +1 -1
  98. package/src/utils/get-injectable-token.mts +1 -1
  99. package/src/utils/get-injectors.mts +32 -15
  100. package/src/utils/types.mts +1 -1
  101. package/tsdown.config.mts +67 -0
  102. package/lib/_tsup-dts-rollup.d.mts +0 -1283
  103. package/lib/_tsup-dts-rollup.d.ts +0 -1283
  104. package/lib/chunk-2M576LCC.mjs +0 -2043
  105. package/lib/chunk-2M576LCC.mjs.map +0 -1
  106. package/lib/index.d.ts +0 -78
  107. package/lib/index.js +0 -2127
  108. package/lib/index.js.map +0 -1
  109. package/lib/testing/index.d.ts +0 -2
  110. package/lib/testing/index.js +0 -2060
  111. package/lib/testing/index.js.map +0 -1
  112. package/lib/testing/index.mjs.map +0 -1
  113. package/src/container.mts +0 -227
  114. package/src/factory-context.mts +0 -8
  115. package/src/instance-resolver.mts +0 -559
  116. package/src/request-context-manager.mts +0 -149
  117. package/src/service-invalidator.mts +0 -429
  118. package/src/service-locator-instance-holder.mts +0 -70
  119. package/src/service-locator-manager.mts +0 -85
  120. package/src/service-locator.mts +0 -246
  121. package/tsup.config.mts +0 -12
  122. /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 { InjectionToken } from '../injection-token.mjs'
10
- import { asyncInject, inject } from '../injector.mjs'
11
- import { globalRegistry } from '../registry.mjs'
12
- import { ServiceLocator } from '../service-locator.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'
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 serviceLocator: ServiceLocator
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
- serviceLocator = new ServiceLocator(globalRegistry, mockLogger)
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 serviceLocator.getInstance(ServiceA)
79
- await serviceLocator.getInstance(ServiceB)
80
- await serviceLocator.getInstance(ServiceC)
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(3)
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
- '[ServiceInvalidator] Graceful clearing completed',
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
- '[ServiceInvalidator] No singleton services to clear',
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
- serviceLocator.beginRequest(requestId)
117
- const [error, serviceB] = await serviceLocator.getInstance(ServiceB)
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 serviceLocator.invalidate(getInjectableToken(ServiceA).toString())
122
- expect(serviceLocator.getManager().size()).toBe(0)
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 when requested', async () => {
133
+ it('should clear request contexts via ScopedContainer', async () => {
127
134
  // Create a request context
128
135
  const requestId = 'test-request'
129
- serviceLocator.beginRequest(requestId)
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 serviceLocator.getInstance(TestService)
144
+ await scoped.get(TestService)
138
145
 
139
- // Verify request context exists before clearing
140
- expect(serviceLocator.getCurrentRequestContext()).not.toBeNull()
146
+ // Verify request context exists
147
+ expect(container.hasActiveRequest(requestId)).toBe(true)
141
148
 
142
- // Clear all services including request contexts
143
- await serviceLocator.clearAll({ clearRequestContexts: true })
149
+ // End request to clean up
150
+ await scoped.endRequest()
144
151
 
145
152
  // Verify request context is cleared
146
- expect(serviceLocator.getCurrentRequestContext()).toBeNull()
153
+ expect(container.hasActiveRequest(requestId)).toBe(false)
147
154
  })
148
155
 
149
- it('should skip clearing request contexts when disabled', async () => {
156
+ it('should track active request contexts', async () => {
150
157
  // Create a request context
151
158
  const requestId = 'test-request'
152
- serviceLocator.beginRequest(requestId)
159
+ const scoped = container.beginRequest(requestId)
153
160
 
154
- // Clear all services but skip request contexts
155
- await serviceLocator.clearAll({ clearRequestContexts: false })
161
+ // Verify request context exists
162
+ expect(container.hasActiveRequest(requestId)).toBe(true)
156
163
 
157
- // Verify request context is still there
158
- expect(serviceLocator.getCurrentRequestContext()).not.toBeNull()
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 serviceLocator.getInstance(ServiceB)
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 serviceLocator.getInstance(TestService)
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 serviceLocator.getInstance(AuthService)
221
- await serviceLocator.getInstance(UserService)
222
- await serviceLocator.getInstance(DatabaseService)
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(3)
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 serviceLocator.getInstance(TestService)
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 serviceLocator: ServiceLocator
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
- serviceLocator = new ServiceLocator(globalRegistry, mockLogger)
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 [error1, singleton1] =
294
- await serviceLocator.getInstance(SingletonService)
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 requestId = 'test-request-1'
328
- serviceLocator.beginRequest(requestId)
341
+ const scoped1 = container.beginRequest('test-request-1')
329
342
 
330
343
  // Get instances within the same request
331
- const [error1, request1] =
332
- await serviceLocator.getInstance(RequestService)
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 serviceLocator.endRequest(requestId)
343
- const newRequestId = 'test-request-2'
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 [error3, request3] =
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 requestId = 'test-request'
372
- serviceLocator.beginRequest(requestId)
379
+ const scoped = container.beginRequest('test-request')
373
380
 
374
381
  // Get multiple transient instances
375
- const [error1, transient1] =
376
- await serviceLocator.getInstance(TransientService)
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 (asyncInject returns Promises)
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 requestId = 'complex-request'
421
- serviceLocator.beginRequest(requestId)
425
+ const scoped = container.beginRequest('complex-request')
422
426
 
423
427
  // Get instances
424
- const [error1, action1] =
425
- await serviceLocator.getInstance(UserActionService)
426
- const [error2, action2] =
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 (asyncInject returns Promises)
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 serviceLocator.endRequest(requestId)
458
- const newRequestId = 'complex-request-2'
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 [error4, action3] =
463
- await serviceLocator.getInstance(UserActionService)
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 requestId1 = 'request-1'
495
- serviceLocator.beginRequest(requestId1)
496
- const [error1, service1] =
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 requestId2 = 'request-2'
502
- serviceLocator.beginRequest(requestId2)
503
- const [error2, service2] =
504
- await serviceLocator.getInstance(RequestService)
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 requestId1 = 'request-1'
522
- serviceLocator.beginRequest(requestId1)
523
- const [error1, service1] =
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 requestId2 = 'request-2'
529
- serviceLocator.beginRequest(requestId2)
530
- const [error2, service2] =
531
- await serviceLocator.getInstance(SingletonService)
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 requestId = 'request'
548
- serviceLocator.beginRequest(requestId)
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 requestId = 'cleanup-test'
584
- serviceLocator.beginRequest(requestId)
555
+ const scoped = container.beginRequest('cleanup-test')
585
556
 
586
557
  // Create instances
587
- const [error1, _requestService] =
588
- await serviceLocator.getInstance(RequestService)
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(serviceLocator.getCurrentRequestContext()).not.toBeNull()
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 serviceLocator.endRequest(requestId)
565
+ await scoped.endRequest()
603
566
 
604
567
  // Verify request context is cleared
605
- expect(serviceLocator.getCurrentRequestContext()).toBeNull()
568
+ expect(container.hasActiveRequest('cleanup-test')).toBe(false)
606
569
 
607
570
  // Singleton should still be available
608
- const [error3, singletonService2] =
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
- const [error4] = await serviceLocator.getInstance(RequestService)
615
- expect(error4).toBeDefined() // Should error because no request context
575
+ await expect(container.get(RequestService)).rejects.toThrow(
576
+ /Cannot resolve request-scoped service/,
577
+ )
616
578
  })
617
579
 
618
- it('should handle nested request contexts with mixed scopes', async () => {
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 requestId1 = 'outer-request'
633
- serviceLocator.beginRequest(requestId1)
634
- const [error1, requestService1] =
635
- await serviceLocator.getInstance(RequestService)
636
- const [error2, singletonService1] =
637
- await serviceLocator.getInstance(SingletonService)
638
-
639
- // Second request (nested)
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 inner request
657
- await serviceLocator.endRequest(requestId2)
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
- // End outer request
665
- await serviceLocator.endRequest(requestId1)
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 to avoid race conditions
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
- serviceLocator.beginRequest(requestId)
690
- const [_error1, requestService] =
691
- await serviceLocator.getInstance(RequestService)
692
- const [_error2, singletonService] =
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
- const [error] = await serviceLocator.getInstance(RequestService)
725
-
726
- expect(error).toBeDefined()
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 requestId = 'error-test'
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
- // The system should throw an error when the service cannot be instantiated
750
- await expect(
751
- serviceLocator.getInstance(RequestService),
752
- ).rejects.toThrow('Singleton creation failed')
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 serviceLocator: ServiceLocator
700
+ let container: Container
759
701
 
760
702
  beforeEach(() => {
761
- serviceLocator = new ServiceLocator(globalRegistry)
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 [error, instance] = await serviceLocator.getInstance(
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 [error1, instance1] = await serviceLocator.getInstance(ApiClient, {
751
+ const instance1 = await container.get(ApiClient, {
809
752
  apiKey: 'secret-key',
810
753
  baseUrl: 'https://api.example.com',
811
754
  })
812
- const [error2, instance2] = await serviceLocator.getInstance(ApiClient, {
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 [error1, instance1] = await serviceLocator.getInstance(Logger, {
779
+ const instance1 = await container.get(Logger, {
839
780
  level: 'info' as const,
840
781
  prefix: 'APP',
841
782
  })
842
- const [error2, instance2] = await serviceLocator.getInstance(Logger, {
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 [error, instance] =
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 [error, instance] = await serviceLocator.getInstance(CacheService)
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 [error1, instance1] = await serviceLocator.getInstance(
934
- StrictService,
935
- {
936
- required: 'value',
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 [error2, instance2] = await serviceLocator.getInstance(
947
- StrictService,
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 [error, instance] = await serviceLocator.getInstance(AppConfig, {
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 requestId = 'test-request'
1029
- serviceLocator.beginRequest(requestId)
953
+ const scoped = container.beginRequest('test-request')
1030
954
 
1031
- const [error, instance] = await serviceLocator.getInstance(UserContext, {
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 serviceLocator.endRequest(requestId)
963
+ await scoped.endRequest()
1041
964
  })
1042
965
  })
1043
966
  })