@navios/di 0.5.0 → 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 +26 -4
  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 +19 -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} +12 -5
  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-44F3LXW5.mjs +0 -2043
  106. package/lib/chunk-44F3LXW5.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,133 +1,171 @@
1
1
  # Request Contexts
2
2
 
3
- Request contexts in Navios DI provide a powerful way to manage request-scoped services with automatic cleanup and priority-based resolution. This is particularly useful in web applications where you need to maintain request-specific data and ensure proper cleanup after each request.
3
+ Request contexts in Navios DI provide a powerful way to manage request-scoped services with automatic cleanup. This is particularly useful in web applications where you need to maintain request-specific data and ensure proper cleanup after each request.
4
4
 
5
5
  ## Overview
6
6
 
7
- A request context is a scoped container that can hold pre-prepared instances and metadata for a specific request. When a request context is active, services can access request-specific data through dependency injection.
7
+ A `ScopedContainer` provides isolated request context management while sharing singleton and transient services with the parent `Container`. Each request gets its own `ScopedContainer` instance, ensuring complete isolation between concurrent requests.
8
8
 
9
9
  ### Key Features
10
10
 
11
- - **Request-scoped instances**: Services that exist only for the duration of a request
11
+ - **Request isolation**: Each request has its own isolated container - no race conditions
12
+ - **Concurrent resolution locking**: Multiple concurrent requests for the same service within a request context are properly synchronized - only one instance is created
12
13
  - **Automatic cleanup**: All request-scoped instances are automatically cleaned up when the request ends
13
- - **Priority-based resolution**: Multiple contexts can exist with different priorities
14
+ - **Seamless delegation**: Singleton and transient services are resolved through the parent container
14
15
  - **Metadata support**: Attach arbitrary metadata to request contexts
15
- - **Thread-safe**: Safe to use in concurrent environments
16
+ - **Thread-safe**: Safe to use in concurrent environments with no shared mutable state
16
17
 
17
18
  ## Basic Usage
18
19
 
19
20
  ### Creating and Managing Request Contexts
20
21
 
21
22
  ```typescript
22
- import { Container, Injectable, InjectionToken } from '@navios/di'
23
+ import { Container, Injectable, InjectableScope } from '@navios/di'
23
24
 
24
25
  const container = new Container()
25
26
 
26
- // Begin a new request context
27
- const context = container.beginRequest('req-123', { userId: 456 }, 100)
27
+ // Begin a new request context - returns a ScopedContainer
28
+ const scoped = container.beginRequest('req-123', { userId: 456 })
28
29
 
29
- // Set it as the current context
30
- container.setCurrentRequestContext('req-123')
30
+ // Use the scoped container for this request
31
+ const service = await scoped.get(RequestService)
31
32
 
32
- // End the request context when done
33
- await container.endRequest('req-123')
33
+ // End the request when done
34
+ await scoped.endRequest()
34
35
  ```
35
36
 
36
- ### Using Request-Scoped Data
37
+ ### Using Request-Scoped Services
37
38
 
38
39
  ```typescript
39
- const REQUEST_ID_TOKEN = InjectionToken.create<string>('REQUEST_ID')
40
- const USER_ID_TOKEN = InjectionToken.create<number>('USER_ID')
40
+ import { Injectable, InjectableScope, inject } from '@navios/di'
41
41
 
42
- @Injectable()
42
+ @Injectable({ scope: InjectableScope.Request })
43
43
  class RequestLogger {
44
- private readonly requestId = asyncInject(REQUEST_ID_TOKEN)
45
- private readonly userId = asyncInject(USER_ID_TOKEN)
44
+ private requestId: string
46
45
 
47
- async log(message: string) {
48
- const reqId = await this.requestId
49
- const uid = await this.userId
50
- console.log(`[${reqId}] User ${uid}: ${message}`)
46
+ setRequestId(id: string) {
47
+ this.requestId = id
48
+ }
49
+
50
+ log(message: string) {
51
+ console.log(`[${this.requestId}] ${message}`)
52
+ }
53
+ }
54
+
55
+ @Injectable({ scope: InjectableScope.Singleton })
56
+ class DatabaseService {
57
+ query(sql: string) {
58
+ return `Result: ${sql}`
51
59
  }
52
60
  }
53
61
 
54
- // Setup request context
55
- const context = container.beginRequest('req-123')
56
- context.addInstance(REQUEST_ID_TOKEN, 'req-123')
57
- context.addInstance(USER_ID_TOKEN, 456)
62
+ @Injectable({ scope: InjectableScope.Request })
63
+ class RequestHandler {
64
+ private logger = inject(RequestLogger)
65
+ private db = inject(DatabaseService) // Singleton, shared across requests
58
66
 
59
- container.setCurrentRequestContext('req-123')
67
+ async handle() {
68
+ this.logger.log('Processing request')
69
+ return this.db.query('SELECT * FROM users')
70
+ }
71
+ }
60
72
 
61
- // Use the service
62
- const logger = await container.get(RequestLogger)
63
- await logger.log('Processing request') // "[req-123] User 456: Processing request"
73
+ // Usage
74
+ const scoped = container.beginRequest('req-123')
75
+ const handler = await scoped.get(RequestHandler)
76
+ await handler.handle()
77
+ await scoped.endRequest()
64
78
  ```
65
79
 
66
- ## Advanced Features
80
+ ## ScopedContainer API
67
81
 
68
- ### Priority-Based Resolution
82
+ The `ScopedContainer` implements the same `IContainer` interface as `Container`:
69
83
 
70
- When multiple request contexts exist, the one with the highest priority is used:
84
+ ```typescript
85
+ interface IContainer {
86
+ get<T>(token, args?): Promise<T>
87
+ invalidate(service: unknown): Promise<void>
88
+ isRegistered(token): boolean
89
+ dispose(): Promise<void>
90
+ ready(): Promise<void>
91
+ tryGetSync<T>(token, args?): T | null
92
+ }
93
+ ```
94
+
95
+ ### Additional ScopedContainer Methods
71
96
 
72
97
  ```typescript
73
- // High priority context (e.g., admin request)
74
- const adminContext = container.beginRequest('admin-req', {}, 200)
98
+ class ScopedContainer implements IContainer {
99
+ // Get the parent Container
100
+ getParent(): Container
101
+
102
+ // Get the request ID
103
+ getRequestId(): string
104
+
105
+ // Get metadata value
106
+ getMetadata(key: string): any | undefined
75
107
 
76
- // Normal priority context
77
- const userContext = container.beginRequest('user-req', {}, 100)
108
+ // Add a pre-prepared instance to the request context
109
+ addInstance(token: InjectionToken<any>, instance: any): void
78
110
 
79
- // Admin context will be used due to higher priority
80
- container.setCurrentRequestContext('admin-req')
111
+ // End the request and cleanup all request-scoped services
112
+ endRequest(): Promise<void>
113
+ }
81
114
  ```
82
115
 
83
- ### Request Metadata
116
+ ## Advanced Features
84
117
 
85
- Request contexts can carry metadata that services can access:
118
+ ### Pre-prepared Instances
119
+
120
+ You can add pre-prepared instances to a request context:
86
121
 
87
122
  ```typescript
88
- @Injectable()
89
- class AuditService {
90
- private readonly context = asyncInject(Container)
123
+ const REQUEST_TOKEN = InjectionToken.create<{ userId: string }>('RequestData')
91
124
 
92
- async logAction(action: string) {
93
- const container = await this.context
94
- const requestContext = container.getCurrentRequestContext()
125
+ const scoped = container.beginRequest('req-123')
95
126
 
96
- if (requestContext) {
97
- const userId = requestContext.getMetadata('userId')
98
- const traceId = requestContext.getMetadata('traceId')
127
+ // Add a pre-prepared instance
128
+ scoped.addInstance(REQUEST_TOKEN, { userId: 'user-456' })
99
129
 
100
- console.log(`User ${userId} performed ${action} (trace: ${traceId})`)
101
- }
102
- }
103
- }
130
+ // This will return the pre-prepared instance
131
+ const requestData = await scoped.get(REQUEST_TOKEN)
132
+ ```
133
+
134
+ ### Request Metadata
135
+
136
+ Request contexts can carry metadata:
104
137
 
105
- // Setup with metadata
106
- const context = container.beginRequest('req-123', {
138
+ ```typescript
139
+ const scoped = container.beginRequest('req-123', {
107
140
  userId: 456,
108
141
  traceId: 'abc-123',
109
142
  userAgent: 'Mozilla/5.0...',
110
143
  })
144
+
145
+ // Access metadata
146
+ const userId = scoped.getMetadata('userId') // 456
147
+ const traceId = scoped.getMetadata('traceId') // 'abc-123'
111
148
  ```
112
149
 
113
- ### Pre-prepared Instances
150
+ ### Parallel Requests
114
151
 
115
- You can add pre-prepared instances to a request context:
152
+ Multiple requests can run concurrently without interference:
116
153
 
117
154
  ```typescript
118
- @Injectable()
119
- class DatabaseConnection {
120
- constructor(private connectionString: string) {}
155
+ // Start multiple requests in parallel
156
+ const scoped1 = container.beginRequest('req-1')
157
+ const scoped2 = container.beginRequest('req-2')
121
158
 
122
- async query(sql: string) {
123
- return `Executing: ${sql} on ${this.connectionString}`
124
- }
125
- }
159
+ // Resolve services concurrently - each gets its own instance
160
+ const [service1, service2] = await Promise.all([
161
+ scoped1.get(RequestService),
162
+ scoped2.get(RequestService),
163
+ ])
164
+
165
+ // service1 !== service2 (different instances)
126
166
 
127
- // Create a request-specific database connection
128
- const dbConnection = new DatabaseConnection('user-specific-db')
129
- const context = container.beginRequest('req-123')
130
- context.addInstance('DatabaseConnection', dbConnection)
167
+ // Clean up both
168
+ await Promise.all([scoped1.endRequest(), scoped2.endRequest()])
131
169
  ```
132
170
 
133
171
  ## Web Framework Integration
@@ -135,92 +173,90 @@ context.addInstance('DatabaseConnection', dbConnection)
135
173
  ### Express.js Example
136
174
 
137
175
  ```typescript
138
- import { Container, Injectable, InjectionToken } from '@navios/di'
139
-
176
+ import { Container, Injectable, InjectableScope, inject } from '@navios/di'
140
177
  import express from 'express'
141
178
 
142
- const REQUEST_TOKEN = InjectionToken.create<express.Request>('REQUEST')
143
- const RESPONSE_TOKEN = InjectionToken.create<express.Response>('RESPONSE')
144
-
145
- @Injectable()
179
+ @Injectable({ scope: InjectableScope.Request })
146
180
  class RequestHandler {
147
- private readonly req = asyncInject(REQUEST_TOKEN)
148
- private readonly res = asyncInject(RESPONSE_TOKEN)
149
-
150
- async handleRequest() {
151
- const request = await this.req
152
- const response = await this.res
153
-
154
- response.json({
155
- message: 'Hello!',
156
- path: request.path,
157
- method: request.method,
158
- })
181
+ private requestId: string = ''
182
+
183
+ setContext(req: express.Request) {
184
+ this.requestId = req.headers['x-request-id'] as string
185
+ }
186
+
187
+ async handle() {
188
+ return { message: 'Hello!', requestId: this.requestId }
159
189
  }
160
190
  }
161
191
 
162
192
  const app = express()
163
193
  const container = new Container()
164
194
 
165
- app.use('*', async (req, res, next) => {
166
- const requestId = `req-${Date.now()}-${Math.random()}`
195
+ app.use(async (req, res, next) => {
196
+ const requestId = `req-${Date.now()}-${Math.random().toString(36).slice(2)}`
167
197
 
168
- // Create request context
169
- const context = container.beginRequest(requestId, {
198
+ // Create scoped container for this request
199
+ const scoped = container.beginRequest(requestId, {
170
200
  path: req.path,
171
201
  method: req.method,
172
202
  userAgent: req.get('User-Agent'),
173
203
  })
174
204
 
175
- // Add request-specific instances
176
- context.addInstance(REQUEST_TOKEN, req)
177
- context.addInstance(RESPONSE_TOKEN, res)
205
+ // Store scoped container on request for later use
206
+ ;(req as any).scoped = scoped
178
207
 
179
- // Set as current context
180
- container.setCurrentRequestContext(requestId)
208
+ // Cleanup on response finish
209
+ res.on('finish', async () => {
210
+ await scoped.endRequest()
211
+ })
181
212
 
182
- try {
183
- const handler = await container.get(RequestHandler)
184
- await handler.handleRequest()
185
- } finally {
186
- // Clean up request context
187
- await container.endRequest(requestId)
188
- }
213
+ next()
214
+ })
215
+
216
+ app.get('/', async (req, res) => {
217
+ const scoped = (req as any).scoped
218
+ const handler = await scoped.get(RequestHandler)
219
+ handler.setContext(req)
220
+ const result = await handler.handle()
221
+ res.json(result)
189
222
  })
190
223
  ```
191
224
 
192
225
  ### Fastify Example
193
226
 
194
227
  ```typescript
195
- import { Container, Injectable, InjectionToken } from '@navios/di'
196
-
228
+ import { Container, Injectable, InjectableScope } from '@navios/di'
197
229
  import fastify from 'fastify'
198
230
 
199
- const REQUEST_TOKEN = InjectionToken.create<any>('FASTIFY_REQUEST')
200
-
201
231
  const app = fastify()
202
232
  const container = new Container()
203
233
 
234
+ // Type augmentation for request
235
+ declare module 'fastify' {
236
+ interface FastifyRequest {
237
+ scoped: ScopedContainer
238
+ }
239
+ }
240
+
204
241
  app.addHook('preHandler', async (request, reply) => {
205
242
  const requestId = `req-${request.id}`
206
243
 
207
- const context = container.beginRequest(requestId, {
244
+ request.scoped = container.beginRequest(requestId, {
208
245
  ip: request.ip,
209
246
  userAgent: request.headers['user-agent'],
210
247
  })
211
-
212
- context.addInstance(REQUEST_TOKEN, request)
213
- container.setCurrentRequestContext(requestId)
214
-
215
- // Store requestId for cleanup
216
- request.requestId = requestId
217
248
  })
218
249
 
219
250
  app.addHook('onResponse', async (request, reply) => {
220
- if (request.requestId) {
221
- await container.endRequest(request.requestId)
251
+ if (request.scoped) {
252
+ await request.scoped.endRequest()
222
253
  }
223
254
  })
255
+
256
+ app.get('/', async (request, reply) => {
257
+ const service = await request.scoped.get(MyRequestService)
258
+ return service.getData()
259
+ })
224
260
  ```
225
261
 
226
262
  ## Best Practices
@@ -230,15 +266,14 @@ app.addHook('onResponse', async (request, reply) => {
230
266
  Always ensure request contexts are properly cleaned up:
231
267
 
232
268
  ```typescript
233
- const requestId = generateRequestId()
234
- const context = container.beginRequest(requestId)
269
+ const scoped = container.beginRequest(requestId)
235
270
 
236
271
  try {
237
- // Process request
238
- await processRequest()
272
+ const service = await scoped.get(RequestService)
273
+ await service.process()
239
274
  } finally {
240
275
  // Always clean up, even on errors
241
- await container.endRequest(requestId)
276
+ await scoped.endRequest()
242
277
  }
243
278
  ```
244
279
 
@@ -248,29 +283,15 @@ Use descriptive request IDs that help with debugging:
248
283
 
249
284
  ```typescript
250
285
  const requestId = `${req.method}-${req.path}-${Date.now()}-${Math.random().toString(36).slice(2)}`
286
+ const scoped = container.beginRequest(requestId)
251
287
  ```
252
288
 
253
- ### 3. Set Appropriate Priorities
254
-
255
- Use priorities to ensure correct resolution order:
256
-
257
- ```typescript
258
- // System/admin requests - highest priority
259
- const adminContext = container.beginRequest('admin-req', {}, 1000)
260
-
261
- // Authenticated user requests
262
- const userContext = container.beginRequest('user-req', {}, 500)
263
-
264
- // Anonymous requests - lowest priority
265
- const anonContext = container.beginRequest('anon-req', {}, 100)
266
- ```
267
-
268
- ### 4. Leverage Metadata
289
+ ### 3. Leverage Metadata
269
290
 
270
291
  Use metadata for cross-cutting concerns:
271
292
 
272
293
  ```typescript
273
- const context = container.beginRequest('req-123', {
294
+ const scoped = container.beginRequest('req-123', {
274
295
  traceId: generateTraceId(),
275
296
  correlationId: req.headers['x-correlation-id'],
276
297
  userId: req.user?.id,
@@ -279,12 +300,12 @@ const context = container.beginRequest('req-123', {
279
300
  })
280
301
  ```
281
302
 
282
- ### 5. Combine with Lifecycle Hooks
303
+ ### 4. Combine with Lifecycle Hooks
283
304
 
284
305
  Use lifecycle hooks for request-scoped resource management:
285
306
 
286
307
  ```typescript
287
- @Injectable()
308
+ @Injectable({ scope: InjectableScope.Request })
288
309
  class DatabaseTransaction implements OnServiceInit, OnServiceDestroy {
289
310
  private transaction: any = null
290
311
 
@@ -300,65 +321,132 @@ class DatabaseTransaction implements OnServiceInit, OnServiceDestroy {
300
321
 
301
322
  async commit() {
302
323
  await this.transaction.commit()
324
+ this.transaction = null
303
325
  }
304
326
  }
305
327
  ```
306
328
 
307
329
  ## Error Handling
308
330
 
309
- Request contexts handle errors gracefully:
331
+ ### Request-Scoped Services from Container
332
+
333
+ Attempting to resolve a request-scoped service directly from `Container` throws an error:
310
334
 
311
335
  ```typescript
312
- try {
313
- const context = container.beginRequest('req-123')
314
- container.setCurrentRequestContext('req-123')
315
-
316
- // If this throws, cleanup will still happen
317
- await processRequest()
318
- } catch (error) {
319
- console.error('Request failed:', error)
320
- throw error
321
- } finally {
322
- // Context cleanup happens automatically
323
- await container.endRequest('req-123')
324
- }
336
+ @Injectable({ scope: InjectableScope.Request })
337
+ class RequestService {}
338
+
339
+ // This throws an error!
340
+ await container.get(RequestService)
341
+ // Error: Cannot resolve request-scoped service "RequestService" from Container.
342
+ // Use beginRequest() to create a ScopedContainer for request-scoped services.
343
+
344
+ // Correct way:
345
+ const scoped = container.beginRequest('req-123')
346
+ await scoped.get(RequestService) // Works!
347
+ ```
348
+
349
+ ### Duplicate Request IDs
350
+
351
+ Each request ID must be unique while the request is active:
352
+
353
+ ```typescript
354
+ const scoped1 = container.beginRequest('req-123')
355
+
356
+ // This throws an error!
357
+ const scoped2 = container.beginRequest('req-123')
358
+ // Error: Request context "req-123" already exists. Use a unique request ID.
359
+
360
+ // After ending the first request, the ID can be reused
361
+ await scoped1.endRequest()
362
+ const scoped3 = container.beginRequest('req-123') // Works!
325
363
  ```
326
364
 
327
365
  ## API Reference
328
366
 
329
367
  ### Container Methods
330
368
 
331
- - `beginRequest(requestId: string, metadata?: Record<string, any>, priority?: number): RequestContextHolder`
332
- - `endRequest(requestId: string): Promise<void>`
333
- - `getCurrentRequestContext(): RequestContextHolder | null`
334
- - `setCurrentRequestContext(requestId: string): void`
369
+ - `beginRequest(requestId: string, metadata?: Record<string, any>, priority?: number): ScopedContainer` - Creates a new ScopedContainer for the request
370
+ - `hasActiveRequest(requestId: string): boolean` - Checks if a request ID is currently active
371
+ - `getActiveRequestIds(): ReadonlySet<string>` - Gets all active request IDs
372
+
373
+ ### ScopedContainer Methods
374
+
375
+ - `get<T>(token, args?): Promise<T>` - Gets a service instance (request-scoped resolved locally, others delegated)
376
+ - `invalidate(service: unknown): Promise<void>` - Invalidates a service and its dependents
377
+ - `isRegistered(token): boolean` - Checks if a token is registered
378
+ - `dispose(): Promise<void>` - Alias for `endRequest()`
379
+ - `ready(): Promise<void>` - Waits for pending operations
380
+ - `tryGetSync<T>(token, args?): T | null` - Synchronously gets an instance if available
381
+ - `getParent(): Container` - Gets the parent Container
382
+ - `getRequestId(): string` - Gets the request ID
383
+ - `getMetadata(key: string): any` - Gets metadata value
384
+ - `addInstance(token, instance): void` - Adds a pre-prepared instance
385
+ - `endRequest(): Promise<void>` - Ends the request and cleans up all request-scoped services
386
+
387
+ ## Concurrent Resolution Safety
388
+
389
+ Request-scoped services have the same locking mechanism as singletons - when multiple concurrent operations request the same service simultaneously, only one instance is created. This prevents duplicate initialization which could cause issues with resources like database connections or sessions.
390
+
391
+ ```typescript
392
+ @Injectable({ scope: InjectableScope.Request })
393
+ class ExpensiveResource {
394
+ constructor() {
395
+ // Only called once per request, even with concurrent resolution
396
+ console.log('Creating expensive resource')
397
+ }
398
+
399
+ async onServiceInit() {
400
+ // Simulate expensive async initialization
401
+ await connectToDatabase()
402
+ }
403
+ }
404
+
405
+ // In a request handler with concurrent operations:
406
+ const scoped = container.beginRequest('request-123')
407
+
408
+ // These run concurrently, but only ONE instance is created
409
+ const [resource1, resource2, resource3] = await Promise.all([
410
+ scoped.get(ExpensiveResource),
411
+ scoped.get(ExpensiveResource),
412
+ scoped.get(ExpensiveResource),
413
+ ])
414
+
415
+ // resource1 === resource2 === resource3 (same instance)
416
+ ```
417
+
418
+ The locking works by:
419
+ 1. When the first call starts creating a service, it stores a "creating" holder immediately (synchronously, before any async operations)
420
+ 2. Subsequent concurrent calls find this holder and wait for the creation to complete
421
+ 3. Once created, all waiting calls receive the same instance
422
+
423
+ This is handled transparently - no special handling is needed in your code.
424
+
425
+ ### Storage Strategy Pattern
426
+
427
+ Internally, both Singleton and Request-scoped services use the same unified resolution logic through the `IHolderStorage` interface. This ensures consistent behavior and eliminates code duplication:
335
428
 
336
- ### RequestContextHolder Interface
429
+ - `SingletonHolderStorage` - stores holders in the global ServiceLocatorManager
430
+ - `RequestHolderStorage` - stores holders in the ScopedContainer's RequestContextHolder
337
431
 
338
- - `requestId: string` - Unique identifier for this request
339
- - `priority: number` - Priority for resolution
340
- - `metadata: Map<string, any>` - Request-specific metadata
341
- - `createdAt: number` - Timestamp when context was created
342
- - `addInstance(token: InjectionToken<any>, instance: any): void`
343
- - `getMetadata(key: string): any | undefined`
344
- - `setMetadata(key: string, value: any): void`
345
- - `clear(): void` - Clear all instances and metadata
432
+ The storage strategy is automatically selected based on the service's scope.
346
433
 
347
434
  ## Performance Considerations
348
435
 
349
- - Request contexts are lightweight and designed for high-throughput scenarios
350
- - Cleanup is asynchronous and won't block request processing
351
- - Use appropriate priorities to avoid unnecessary context switching
352
- - Consider pooling request contexts for very high-frequency scenarios
436
+ - `ScopedContainer` instances are lightweight - create one per request
437
+ - Request-scoped services are stored in a simple Map per request
438
+ - Cleanup is asynchronous but fast - lifecycle hooks run in parallel
439
+ - No global state to synchronize - each request is completely isolated
440
+ - Concurrent resolution uses async/await locking - minimal overhead compared to creating duplicate instances
353
441
 
354
442
  ## Troubleshooting
355
443
 
356
444
  ### Common Issues
357
445
 
358
- **Context not found**: Make sure you've called `setCurrentRequestContext()` before accessing request-scoped services.
446
+ **"Cannot resolve request-scoped service from Container"**: Use `container.beginRequest()` to create a `ScopedContainer` first.
359
447
 
360
- **Wrong priority resolution**: Check that your priority values are set correctly (higher = more priority).
448
+ **"Request context already exists"**: Each request needs a unique ID. Generate unique IDs using timestamps or UUIDs.
361
449
 
362
- **Memory leaks**: Always call `endRequest()` to clean up contexts, preferably in a `finally` block.
450
+ **Memory leaks**: Always call `endRequest()` to clean up contexts, preferably in a `finally` block or response hook.
363
451
 
364
- **Service not found in context**: Ensure you've added the instance to the context with `addInstance()`.
452
+ **Service not available after request ends**: Request-scoped services are destroyed when `endRequest()` is called. Don't hold references to them after the request.