@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +146 -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 +3012 -0
  15. package/lib/browser/index.mjs.map +1 -0
  16. package/lib/index-S_qX2VLI.d.mts +1211 -0
  17. package/lib/index-S_qX2VLI.d.mts.map +1 -0
  18. package/lib/index-fKPuT65j.d.cts +1206 -0
  19. package/lib/index-fKPuT65j.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-BMGmmxH7.cjs +2895 -0
  33. package/lib/testing-BMGmmxH7.cjs.map +1 -0
  34. package/lib/testing-DCXz8AJD.mjs +2655 -0
  35. package/lib/testing-DCXz8AJD.mjs.map +1 -0
  36. package/package.json +23 -1
  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__/errors.spec.mts +6 -6
  44. package/src/__tests__/factory.spec.mts +1 -1
  45. package/src/__tests__/get-injectors.spec.mts +1 -1
  46. package/src/__tests__/injectable.spec.mts +1 -1
  47. package/src/__tests__/injection-token.spec.mts +1 -1
  48. package/src/__tests__/library-findings.spec.mts +563 -0
  49. package/src/__tests__/registry.spec.mts +2 -2
  50. package/src/__tests__/request-scope.spec.mts +266 -274
  51. package/src/__tests__/service-instantiator.spec.mts +18 -17
  52. package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
  53. package/src/__tests__/service-locator-manager.spec.mts +15 -15
  54. package/src/__tests__/service-locator.spec.mts +167 -244
  55. package/src/__tests__/unified-api.spec.mts +27 -27
  56. package/src/__type-tests__/factory.spec-d.mts +2 -2
  57. package/src/__type-tests__/inject.spec-d.mts +2 -2
  58. package/src/__type-tests__/injectable.spec-d.mts +1 -1
  59. package/src/browser.mts +16 -0
  60. package/src/container/container.mts +319 -0
  61. package/src/container/index.mts +2 -0
  62. package/src/container/scoped-container.mts +350 -0
  63. package/src/decorators/factory.decorator.mts +4 -4
  64. package/src/decorators/injectable.decorator.mts +5 -5
  65. package/src/errors/di-error.mts +13 -7
  66. package/src/errors/index.mts +0 -8
  67. package/src/index.mts +156 -15
  68. package/src/interfaces/container.interface.mts +82 -0
  69. package/src/interfaces/factory.interface.mts +2 -2
  70. package/src/interfaces/index.mts +1 -0
  71. package/src/internal/context/async-local-storage.mts +120 -0
  72. package/src/internal/context/factory-context.mts +18 -0
  73. package/src/internal/context/index.mts +3 -0
  74. package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
  75. package/src/internal/context/resolution-context.mts +63 -0
  76. package/src/internal/context/sync-local-storage.mts +51 -0
  77. package/src/internal/core/index.mts +5 -0
  78. package/src/internal/core/instance-resolver.mts +641 -0
  79. package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
  80. package/src/internal/core/invalidator.mts +437 -0
  81. package/src/internal/core/service-locator.mts +202 -0
  82. package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
  83. package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
  84. package/src/internal/holder/holder-manager.mts +85 -0
  85. package/src/internal/holder/holder-storage.interface.mts +116 -0
  86. package/src/internal/holder/index.mts +6 -0
  87. package/src/internal/holder/instance-holder.mts +109 -0
  88. package/src/internal/holder/request-storage.mts +134 -0
  89. package/src/internal/holder/singleton-storage.mts +105 -0
  90. package/src/internal/index.mts +4 -0
  91. package/src/internal/lifecycle/circular-detector.mts +77 -0
  92. package/src/internal/lifecycle/index.mts +2 -0
  93. package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +11 -4
  94. package/src/testing/__tests__/test-container.spec.mts +2 -2
  95. package/src/testing/test-container.mts +4 -4
  96. package/src/token/index.mts +2 -0
  97. package/src/{injection-token.mts → token/injection-token.mts} +1 -1
  98. package/src/{registry.mts → token/registry.mts} +1 -1
  99. package/src/utils/get-injectable-token.mts +1 -1
  100. package/src/utils/get-injectors.mts +32 -15
  101. package/src/utils/types.mts +1 -1
  102. package/tsdown.config.mts +67 -0
  103. package/lib/_tsup-dts-rollup.d.mts +0 -1283
  104. package/lib/_tsup-dts-rollup.d.ts +0 -1283
  105. package/lib/chunk-2M576LCC.mjs +0 -2043
  106. package/lib/chunk-2M576LCC.mjs.map +0 -1
  107. package/lib/index.d.ts +0 -78
  108. package/lib/index.js +0 -2127
  109. package/lib/index.js.map +0 -1
  110. package/lib/testing/index.d.ts +0 -2
  111. package/lib/testing/index.js +0 -2060
  112. package/lib/testing/index.js.map +0 -1
  113. package/lib/testing/index.mjs.map +0 -1
  114. package/src/container.mts +0 -227
  115. package/src/factory-context.mts +0 -8
  116. package/src/instance-resolver.mts +0 -559
  117. package/src/request-context-manager.mts +0 -149
  118. package/src/service-invalidator.mts +0 -429
  119. package/src/service-locator-instance-holder.mts +0 -70
  120. package/src/service-locator-manager.mts +0 -85
  121. package/src/service-locator.mts +0 -246
  122. package/tsup.config.mts +0 -12
  123. /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 { createRequestContextHolder } from '../request-context-holder.mjs'
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 container.get(RequestService)
31
- const instance1b = await container.get(RequestService)
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 container.get(RequestService)
36
- const instance2b = await container.get(RequestService)
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 container.endRequest('request-1')
48
- await container.endRequest('request-2')
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 requestId = 'test-request'
64
- container.beginRequest(requestId)
62
+ // Start request - get a ScopedContainer
63
+ const scoped = container.beginRequest('test-request')
65
64
 
66
- const instance = await container.get(RequestService)
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 container.endRequest(requestId)
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
- // Note: In a real implementation, you might want to inject metadata
81
- // For now, we'll just verify the context exists
82
- const context = container.getCurrentRequestContext()
83
- expect(context).not.toBeNull()
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 container.endRequest(requestId)
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
- const requestId = 'test-request'
99
- container.beginRequest(requestId)
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
- // Getting the instance should be fast (pre-prepared)
102
- const instance = await container.get(RequestService)
103
- expect(instance.prePrepared).toBe(true)
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
- await container.endRequest(requestId)
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
- it('should handle mixed scopes correctly', async () => {
109
- @Injectable({ registry, scope: InjectableScope.Singleton })
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().toString(36)
144
+ public readonly id = Math.random()
112
145
  }
113
146
 
114
- @Injectable({ registry, scope: InjectableScope.Request })
115
- class RequestService {
116
- public readonly id = Math.random().toString(36)
117
- singleton: SingletonService = inject(SingletonService)
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
- requestService = inject(RequestService)
123
- public readonly id = Math.random().toString(36)
164
+ public readonly id = Math.random()
124
165
  }
125
166
 
126
- // Start request
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
- // Singleton: same instance always
141
- expect(singleton1).toBe(singleton2)
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
- // Transient: different instances always
144
- expect(transient1).not.toBe(transient2)
145
- expect(transient1.requestService).toBe(transient2.requestService)
173
+ expect(instance1).not.toBe(instance2)
146
174
 
147
- await container.endRequest('test-request')
175
+ await scoped.endRequest()
148
176
  })
149
177
 
150
- it('should handle nested request contexts', async () => {
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
- public readonly id = Math.random().toString(36)
186
+ singleton = inject(SingletonService)
187
+ public readonly id = Math.random()
154
188
  }
155
189
 
156
- // Start first request
157
- container.beginRequest('request-1')
158
- const instance1 = await container.get(RequestService)
190
+ const scoped = container.beginRequest('test-request')
159
191
 
160
- // Start second request (nested)
161
- container.beginRequest('request-2')
162
- const instance2 = await container.get(RequestService)
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
- // End second request
168
- await container.endRequest('request-2')
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
- // Get instance from first request again
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 id = Math.random().toString(36)
182
- }
210
+ public readonly instanceId: string
183
211
 
184
- // Start multiple requests
185
- container.beginRequest('request-1')
186
- container.beginRequest('request-2')
187
- container.beginRequest('request-3')
212
+ constructor() {
213
+ initializationCount++
214
+ this.instanceId = Math.random().toString(36)
215
+ }
188
216
 
189
- // Switch to request-1
190
- container.setCurrentRequestContext('request-1')
191
- const instance1 = await container.get(RequestService)
217
+ async onServiceInit() {
218
+ // Simulate async initialization that takes time
219
+ await new Promise((resolve) => setTimeout(resolve, 50))
220
+ }
221
+ }
192
222
 
193
- // Switch to request-2
194
- container.setCurrentRequestContext('request-2')
195
- const instance2 = await container.get(RequestService)
223
+ const scoped = container.beginRequest('test-request')
196
224
 
197
- // Switch back to request-1
198
- container.setCurrentRequestContext('request-1')
199
- const instance1Again = await container.get(RequestService)
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
- // Should get same instance for request-1
202
- expect(instance1).toBe(instance1Again)
203
- // Should get different instance for request-2
204
- expect(instance1).not.toBe(instance2)
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
- // Clean up all requests
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
- holder.addInstance('test-instance', mockInstance, mockHolder)
240
+ it('should return correct instance when waiting for in-progress creation', async () => {
241
+ let creationOrder: string[] = []
239
242
 
240
- expect(holder.has('test-instance')).toBe(true)
241
- expect(holder.get('test-instance')).toBe(mockHolder)
243
+ @Injectable({ registry, scope: InjectableScope.Request })
244
+ class SlowService {
245
+ public readonly id: string
242
246
 
243
- // Clear instances
244
- holder.clear()
245
- expect(holder.has('test-instance')).toBe(false)
246
- })
247
+ constructor() {
248
+ this.id = Math.random().toString(36)
249
+ }
247
250
 
248
- it('should handle metadata correctly', () => {
249
- const holder = createRequestContextHolder('test-request')
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
- holder.setMetadata('key1', 'value1')
252
- holder.setMetadata('key2', 'value2')
259
+ const scoped = container.beginRequest('test-request')
253
260
 
254
- expect(holder.getMetadata('key1')).toBe('value1')
255
- expect(holder.getMetadata('key2')).toBe('value2')
256
- expect(holder.getMetadata('nonexistent')).toBeUndefined()
261
+ // Start first resolution (will start creating)
262
+ const promise1 = scoped.get(SlowService)
263
+ creationOrder.push('promise1-started')
257
264
 
258
- holder.clear()
259
- expect(holder.getMetadata('key1')).toBeUndefined()
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
- it('should store instances by InjectionToken', () => {
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
- // Store instance by InjectionToken
268
- holder.addInstance(token, instance)
272
+ // Both should be the same instance
273
+ expect(instance1).toBe(instance2)
274
+ expect(instance1.id).toBe(instance2.id)
269
275
 
270
- // Verify instance is stored and retrievable
271
- expect(holder.has(token.toString())).toBe(true)
272
- expect(holder.get(token.toString())?.instance).toBe(instance)
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
- // Verify holder is created with correct properties
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 store multiple instances by different InjectionTokens', () => {
282
- const holder = createRequestContextHolder('test-request')
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
- const token1 = InjectionToken.create<string>('Service1')
285
- const token2 = InjectionToken.create<number>('Service2')
286
- const token3 = InjectionToken.create<boolean>('Service3')
288
+ constructor() {
289
+ this.requestId = Math.random().toString(36)
290
+ }
291
+ }
287
292
 
288
- const instance1 = { id: 'instance1', type: 'string' }
289
- const instance2 = { id: 'instance2', type: 'number' }
290
- const instance3 = { id: 'instance3', type: 'boolean' }
293
+ // Start two requests concurrently
294
+ const scoped1 = container.beginRequest('request-1')
295
+ const scoped2 = container.beginRequest('request-2')
291
296
 
292
- // Store multiple instances
293
- holder.addInstance(token1, instance1)
294
- holder.addInstance(token2, instance2)
295
- holder.addInstance(token3, instance3)
297
+ // Simulate concurrent async operations
298
+ const [instance1, instance2] = await Promise.all([
299
+ scoped1.get(RequestService),
300
+ scoped2.get(RequestService),
301
+ ])
296
302
 
297
- // Verify all instances are stored correctly
298
- expect(holder.has(token1.toString())).toBe(true)
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
- expect(holder.get(token1.toString())?.instance).toBe(instance1)
303
- expect(holder.get(token2.toString())?.instance).toBe(instance2)
304
- expect(holder.get(token3.toString())?.instance).toBe(instance3)
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
- // Verify each has its own holder
307
- const holder1 = holder.get(token1.toString())
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
- expect(holder1?.instance).toBe(instance1)
312
- expect(holder2?.instance).toBe(instance2)
313
- expect(holder3?.instance).toBe(instance3)
313
+ await scoped1.endRequest()
314
+ await scoped2.endRequest()
314
315
  })
315
316
 
316
- it('should override instances stored by InjectionToken', () => {
317
- const holder = createRequestContextHolder('test-request')
318
- const token = InjectionToken.create<string>('TestService')
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
- const originalInstance = { id: 'original', data: 'original-data' }
321
- const newInstance = { id: 'new', data: 'new-data' }
323
+ constructor() {
324
+ this.requestId = Math.random().toString(36)
325
+ }
322
326
 
323
- // Store original instance
324
- holder.addInstance(token, originalInstance)
325
- expect(holder.get(token.toString())?.instance).toBe(originalInstance)
327
+ async asyncOperation(delay: number): Promise<void> {
328
+ await new Promise((resolve) => setTimeout(resolve, delay))
329
+ this.value++
330
+ }
331
+ }
326
332
 
327
- // Override with new instance
328
- holder.addInstance(token, newInstance)
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
- // Verify holder is updated
333
- const holderInfo = holder.get(token.toString())
334
- expect(holderInfo?.instance).toBe(newInstance)
335
- })
336
+ const instance1 = await scoped1.get(RequestService)
337
+ const instance2 = await scoped2.get(RequestService)
336
338
 
337
- it('should handle InjectionToken with different name types', () => {
338
- const holder = createRequestContextHolder('test-request')
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
- // Test with string name
341
- const stringToken = InjectionToken.create<string>('StringService')
342
- const stringInstance = { type: 'string' }
347
+ // Each instance should have been modified independently
348
+ expect(instance1.value).toBe(2)
349
+ expect(instance2.value).toBe(2)
343
350
 
344
- // Test with symbol name
345
- const symbolToken = InjectionToken.create<number>(Symbol('SymbolService'))
346
- const symbolInstance = { type: 'symbol' }
351
+ await scoped1.endRequest()
352
+ await scoped2.endRequest()
353
+ })
354
+ })
347
355
 
348
- // Test with class name
349
- class TestClass {}
350
- const classToken = InjectionToken.create(TestClass)
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
- holder.addInstance(stringToken, stringInstance)
354
- holder.addInstance(symbolToken, symbolInstance)
355
- holder.addInstance(classToken, classInstance)
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
- expect(holder.get(stringToken.toString())?.instance).toBe(stringInstance)
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 clear instances stored by InjectionToken', () => {
363
- const holder = createRequestContextHolder('test-request')
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 instance1 = { id: 'instance1' }
368
- const instance2 = { id: 'instance2' }
374
+ const scoped1 = container.beginRequest('request-1')
375
+ expect(container.hasActiveRequest('request-1')).toBe(true)
369
376
 
370
- holder.addInstance(token1, instance1)
371
- holder.addInstance(token2, instance2)
377
+ const scoped2 = container.beginRequest('request-2')
378
+ expect(container.hasActiveRequest('request-2')).toBe(true)
372
379
 
373
- expect(holder.has(token1.toString())).toBe(true)
374
- expect(holder.has(token2.toString())).toBe(true)
380
+ expect(container.getActiveRequestIds().size).toBe(2)
375
381
 
376
- // Clear all instances
377
- holder.clear()
382
+ await scoped1.endRequest()
383
+ expect(container.hasActiveRequest('request-1')).toBe(false)
384
+ expect(container.hasActiveRequest('request-2')).toBe(true)
378
385
 
379
- expect(holder.has(token1.toString())).toBe(false)
380
- expect(holder.has(token2.toString())).toBe(false)
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 handle mixed storage by InjectionToken and string name', () => {
386
- const holder = createRequestContextHolder('test-request')
387
-
388
- const token = InjectionToken.create<string>('TokenService')
389
- const tokenInstance = { id: 'token-instance' }
390
-
391
- const stringName = 'string-service'
392
- const stringInstance = { id: 'string-instance' }
393
-
394
- // Store by InjectionToken
395
- holder.addInstance(token, tokenInstance)
396
-
397
- // Store by string name (requires holder)
398
- const mockHolder = {
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
- // Verify both are stored correctly
413
- expect(holder.has(token.toString())).toBe(true)
414
- expect(holder.has(stringName)).toBe(true)
406
+ const scoped = container.beginRequest('test-request')
407
+ const instance = await scoped.get(RequestService)
415
408
 
416
- expect(holder.get(token.toString())?.instance).toBe(tokenInstance)
417
- expect(holder.get(stringName)?.instance).toBe(stringInstance)
409
+ // Use dispose() instead of endRequest()
410
+ await scoped.dispose()
418
411
 
419
- // Verify holders
420
- expect(holder.get(token.toString())?.instance).toBe(tokenInstance)
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
  })