@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.
- package/CHANGELOG.md +146 -0
- package/README.md +196 -219
- package/docs/README.md +69 -11
- package/docs/api-reference.md +281 -117
- package/docs/container.md +220 -56
- package/docs/examples/request-scope-example.mts +2 -2
- package/docs/factory.md +3 -8
- package/docs/getting-started.md +37 -8
- package/docs/migration.md +318 -37
- package/docs/request-contexts.md +263 -175
- package/docs/scopes.md +79 -42
- package/lib/browser/index.d.mts +1577 -0
- package/lib/browser/index.d.mts.map +1 -0
- package/lib/browser/index.mjs +3012 -0
- package/lib/browser/index.mjs.map +1 -0
- package/lib/index-S_qX2VLI.d.mts +1211 -0
- package/lib/index-S_qX2VLI.d.mts.map +1 -0
- package/lib/index-fKPuT65j.d.cts +1206 -0
- package/lib/index-fKPuT65j.d.cts.map +1 -0
- package/lib/index.cjs +389 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +376 -0
- package/lib/index.d.cts.map +1 -0
- package/lib/index.d.mts +371 -78
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +325 -63
- package/lib/index.mjs.map +1 -1
- package/lib/testing/index.cjs +9 -0
- package/lib/testing/index.d.cts +2 -0
- package/lib/testing/index.d.mts +2 -2
- package/lib/testing/index.mjs +2 -72
- package/lib/testing-BMGmmxH7.cjs +2895 -0
- package/lib/testing-BMGmmxH7.cjs.map +1 -0
- package/lib/testing-DCXz8AJD.mjs +2655 -0
- package/lib/testing-DCXz8AJD.mjs.map +1 -0
- package/package.json +26 -4
- package/project.json +2 -2
- package/src/__tests__/async-local-storage.browser.spec.mts +240 -0
- package/src/__tests__/async-local-storage.spec.mts +333 -0
- package/src/__tests__/container.spec.mts +30 -25
- package/src/__tests__/e2e.browser.spec.mts +790 -0
- package/src/__tests__/e2e.spec.mts +1222 -0
- package/src/__tests__/errors.spec.mts +6 -6
- package/src/__tests__/factory.spec.mts +1 -1
- package/src/__tests__/get-injectors.spec.mts +1 -1
- package/src/__tests__/injectable.spec.mts +1 -1
- package/src/__tests__/injection-token.spec.mts +1 -1
- package/src/__tests__/library-findings.spec.mts +563 -0
- package/src/__tests__/registry.spec.mts +2 -2
- package/src/__tests__/request-scope.spec.mts +266 -274
- package/src/__tests__/service-instantiator.spec.mts +19 -17
- package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
- package/src/__tests__/service-locator-manager.spec.mts +15 -15
- package/src/__tests__/service-locator.spec.mts +167 -244
- package/src/__tests__/unified-api.spec.mts +27 -27
- package/src/__type-tests__/factory.spec-d.mts +2 -2
- package/src/__type-tests__/inject.spec-d.mts +2 -2
- package/src/__type-tests__/injectable.spec-d.mts +1 -1
- package/src/browser.mts +16 -0
- package/src/container/container.mts +319 -0
- package/src/container/index.mts +2 -0
- package/src/container/scoped-container.mts +350 -0
- package/src/decorators/factory.decorator.mts +4 -4
- package/src/decorators/injectable.decorator.mts +5 -5
- package/src/errors/di-error.mts +13 -7
- package/src/errors/index.mts +0 -8
- package/src/index.mts +156 -15
- package/src/interfaces/container.interface.mts +82 -0
- package/src/interfaces/factory.interface.mts +2 -2
- package/src/interfaces/index.mts +1 -0
- package/src/internal/context/async-local-storage.mts +120 -0
- package/src/internal/context/factory-context.mts +18 -0
- package/src/internal/context/index.mts +3 -0
- package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
- package/src/internal/context/resolution-context.mts +63 -0
- package/src/internal/context/sync-local-storage.mts +51 -0
- package/src/internal/core/index.mts +5 -0
- package/src/internal/core/instance-resolver.mts +641 -0
- package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
- package/src/internal/core/invalidator.mts +437 -0
- package/src/internal/core/service-locator.mts +202 -0
- package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
- package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
- package/src/internal/holder/holder-manager.mts +85 -0
- package/src/internal/holder/holder-storage.interface.mts +116 -0
- package/src/internal/holder/index.mts +6 -0
- package/src/internal/holder/instance-holder.mts +109 -0
- package/src/internal/holder/request-storage.mts +134 -0
- package/src/internal/holder/singleton-storage.mts +105 -0
- package/src/internal/index.mts +4 -0
- package/src/internal/lifecycle/circular-detector.mts +77 -0
- package/src/internal/lifecycle/index.mts +2 -0
- package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +12 -5
- package/src/testing/__tests__/test-container.spec.mts +2 -2
- package/src/testing/test-container.mts +4 -4
- package/src/token/index.mts +2 -0
- package/src/{injection-token.mts → token/injection-token.mts} +1 -1
- package/src/{registry.mts → token/registry.mts} +1 -1
- package/src/utils/get-injectable-token.mts +1 -1
- package/src/utils/get-injectors.mts +32 -15
- package/src/utils/types.mts +1 -1
- package/tsdown.config.mts +67 -0
- package/lib/_tsup-dts-rollup.d.mts +0 -1283
- package/lib/_tsup-dts-rollup.d.ts +0 -1283
- package/lib/chunk-44F3LXW5.mjs +0 -2043
- package/lib/chunk-44F3LXW5.mjs.map +0 -1
- package/lib/index.d.ts +0 -78
- package/lib/index.js +0 -2127
- package/lib/index.js.map +0 -1
- package/lib/testing/index.d.ts +0 -2
- package/lib/testing/index.js +0 -2060
- package/lib/testing/index.js.map +0 -1
- package/lib/testing/index.mjs.map +0 -1
- package/src/container.mts +0 -227
- package/src/factory-context.mts +0 -8
- package/src/instance-resolver.mts +0 -559
- package/src/request-context-manager.mts +0 -149
- package/src/service-invalidator.mts +0 -429
- package/src/service-locator-instance-holder.mts +0 -70
- package/src/service-locator-manager.mts +0 -85
- package/src/service-locator.mts +0 -246
- package/tsup.config.mts +0 -12
- /package/src/{injector.mts → injectors.mts} +0 -0
package/docs/request-contexts.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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,
|
|
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
|
|
27
|
+
// Begin a new request context - returns a ScopedContainer
|
|
28
|
+
const scoped = container.beginRequest('req-123', { userId: 456 })
|
|
28
29
|
|
|
29
|
-
//
|
|
30
|
-
|
|
30
|
+
// Use the scoped container for this request
|
|
31
|
+
const service = await scoped.get(RequestService)
|
|
31
32
|
|
|
32
|
-
// End the request
|
|
33
|
-
await
|
|
33
|
+
// End the request when done
|
|
34
|
+
await scoped.endRequest()
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
### Using Request-Scoped
|
|
37
|
+
### Using Request-Scoped Services
|
|
37
38
|
|
|
38
39
|
```typescript
|
|
39
|
-
|
|
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
|
|
45
|
-
private readonly userId = asyncInject(USER_ID_TOKEN)
|
|
44
|
+
private requestId: string
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
67
|
+
async handle() {
|
|
68
|
+
this.logger.log('Processing request')
|
|
69
|
+
return this.db.query('SELECT * FROM users')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
60
72
|
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
await
|
|
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
|
-
##
|
|
80
|
+
## ScopedContainer API
|
|
67
81
|
|
|
68
|
-
|
|
82
|
+
The `ScopedContainer` implements the same `IContainer` interface as `Container`:
|
|
69
83
|
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
77
|
-
|
|
108
|
+
// Add a pre-prepared instance to the request context
|
|
109
|
+
addInstance(token: InjectionToken<any>, instance: any): void
|
|
78
110
|
|
|
79
|
-
//
|
|
80
|
-
|
|
111
|
+
// End the request and cleanup all request-scoped services
|
|
112
|
+
endRequest(): Promise<void>
|
|
113
|
+
}
|
|
81
114
|
```
|
|
82
115
|
|
|
83
|
-
|
|
116
|
+
## Advanced Features
|
|
84
117
|
|
|
85
|
-
|
|
118
|
+
### Pre-prepared Instances
|
|
119
|
+
|
|
120
|
+
You can add pre-prepared instances to a request context:
|
|
86
121
|
|
|
87
122
|
```typescript
|
|
88
|
-
|
|
89
|
-
class AuditService {
|
|
90
|
-
private readonly context = asyncInject(Container)
|
|
123
|
+
const REQUEST_TOKEN = InjectionToken.create<{ userId: string }>('RequestData')
|
|
91
124
|
|
|
92
|
-
|
|
93
|
-
const container = await this.context
|
|
94
|
-
const requestContext = container.getCurrentRequestContext()
|
|
125
|
+
const scoped = container.beginRequest('req-123')
|
|
95
126
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const traceId = requestContext.getMetadata('traceId')
|
|
127
|
+
// Add a pre-prepared instance
|
|
128
|
+
scoped.addInstance(REQUEST_TOKEN, { userId: 'user-456' })
|
|
99
129
|
|
|
100
|
-
|
|
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
|
-
|
|
106
|
-
const
|
|
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
|
-
###
|
|
150
|
+
### Parallel Requests
|
|
114
151
|
|
|
115
|
-
|
|
152
|
+
Multiple requests can run concurrently without interference:
|
|
116
153
|
|
|
117
154
|
```typescript
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
155
|
+
// Start multiple requests in parallel
|
|
156
|
+
const scoped1 = container.beginRequest('req-1')
|
|
157
|
+
const scoped2 = container.beginRequest('req-2')
|
|
121
158
|
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
//
|
|
128
|
-
|
|
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,
|
|
139
|
-
|
|
176
|
+
import { Container, Injectable, InjectableScope, inject } from '@navios/di'
|
|
140
177
|
import express from 'express'
|
|
141
178
|
|
|
142
|
-
|
|
143
|
-
const RESPONSE_TOKEN = InjectionToken.create<express.Response>('RESPONSE')
|
|
144
|
-
|
|
145
|
-
@Injectable()
|
|
179
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
146
180
|
class RequestHandler {
|
|
147
|
-
private
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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(
|
|
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
|
|
169
|
-
const
|
|
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
|
-
//
|
|
176
|
-
|
|
177
|
-
context.addInstance(RESPONSE_TOKEN, res)
|
|
205
|
+
// Store scoped container on request for later use
|
|
206
|
+
;(req as any).scoped = scoped
|
|
178
207
|
|
|
179
|
-
//
|
|
180
|
-
|
|
208
|
+
// Cleanup on response finish
|
|
209
|
+
res.on('finish', async () => {
|
|
210
|
+
await scoped.endRequest()
|
|
211
|
+
})
|
|
181
212
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
221
|
-
await
|
|
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
|
|
234
|
-
const context = container.beginRequest(requestId)
|
|
269
|
+
const scoped = container.beginRequest(requestId)
|
|
235
270
|
|
|
236
271
|
try {
|
|
237
|
-
|
|
238
|
-
await
|
|
272
|
+
const service = await scoped.get(RequestService)
|
|
273
|
+
await service.process()
|
|
239
274
|
} finally {
|
|
240
275
|
// Always clean up, even on errors
|
|
241
|
-
await
|
|
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.
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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):
|
|
332
|
-
- `
|
|
333
|
-
- `
|
|
334
|
-
|
|
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
|
-
|
|
429
|
+
- `SingletonHolderStorage` - stores holders in the global ServiceLocatorManager
|
|
430
|
+
- `RequestHolderStorage` - stores holders in the ScopedContainer's RequestContextHolder
|
|
337
431
|
|
|
338
|
-
|
|
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
|
-
-
|
|
350
|
-
-
|
|
351
|
-
-
|
|
352
|
-
-
|
|
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
|
-
**
|
|
446
|
+
**"Cannot resolve request-scoped service from Container"**: Use `container.beginRequest()` to create a `ScopedContainer` first.
|
|
359
447
|
|
|
360
|
-
**
|
|
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
|
|
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.
|