@navios/di 0.2.1 → 0.3.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/README.md +299 -38
- package/docs/README.md +121 -48
- package/docs/api-reference.md +763 -0
- package/docs/container.md +274 -0
- package/docs/examples/basic-usage.mts +97 -0
- package/docs/examples/factory-pattern.mts +318 -0
- package/docs/examples/injection-tokens.mts +225 -0
- package/docs/examples/request-scope-example.mts +254 -0
- package/docs/examples/service-lifecycle.mts +359 -0
- package/docs/factory.md +584 -0
- package/docs/getting-started.md +308 -0
- package/docs/injectable.md +496 -0
- package/docs/injection-tokens.md +400 -0
- package/docs/lifecycle.md +539 -0
- package/docs/scopes.md +749 -0
- package/lib/_tsup-dts-rollup.d.mts +490 -145
- package/lib/_tsup-dts-rollup.d.ts +490 -145
- package/lib/index.d.mts +26 -12
- package/lib/index.d.ts +26 -12
- package/lib/index.js +993 -462
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +983 -453
- package/lib/index.mjs.map +1 -1
- package/package.json +2 -2
- package/project.json +10 -2
- package/src/__tests__/container.spec.mts +1301 -0
- package/src/__tests__/factory.spec.mts +137 -0
- package/src/__tests__/injectable.spec.mts +32 -88
- package/src/__tests__/injection-token.spec.mts +333 -17
- package/src/__tests__/request-scope.spec.mts +263 -0
- package/src/__type-tests__/factory.spec-d.mts +65 -0
- package/src/__type-tests__/inject.spec-d.mts +27 -28
- package/src/__type-tests__/injectable.spec-d.mts +42 -206
- package/src/container.mts +167 -0
- package/src/decorators/factory.decorator.mts +79 -0
- package/src/decorators/index.mts +1 -0
- package/src/decorators/injectable.decorator.mts +6 -56
- package/src/enums/injectable-scope.enum.mts +5 -1
- package/src/event-emitter.mts +18 -20
- package/src/factory-context.mts +2 -10
- package/src/index.mts +3 -2
- package/src/injection-token.mts +19 -4
- package/src/injector.mts +8 -20
- package/src/interfaces/factory.interface.mts +3 -3
- package/src/interfaces/index.mts +2 -0
- package/src/interfaces/on-service-destroy.interface.mts +3 -0
- package/src/interfaces/on-service-init.interface.mts +3 -0
- package/src/registry.mts +7 -16
- package/src/request-context-holder.mts +145 -0
- package/src/service-instantiator.mts +158 -0
- package/src/service-locator-event-bus.mts +0 -28
- package/src/service-locator-instance-holder.mts +27 -16
- package/src/service-locator-manager.mts +84 -0
- package/src/service-locator.mts +548 -393
- package/src/utils/defer.mts +73 -0
- package/src/utils/get-injectors.mts +91 -78
- package/src/utils/index.mts +2 -0
- package/src/utils/types.mts +52 -0
- package/docs/concepts/injectable.md +0 -182
- package/docs/concepts/injection-token.md +0 -145
- package/src/proxy-service-locator.mts +0 -83
- package/src/resolve-service.mts +0 -41
package/docs/scopes.md
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
# Service Scopes
|
|
2
|
+
|
|
3
|
+
Service scopes determine the lifetime and sharing behavior of service instances in Navios DI. Understanding scopes is crucial for managing resources efficiently and avoiding common pitfalls.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Navios DI supports three service scopes:
|
|
8
|
+
|
|
9
|
+
- **Singleton**: One instance shared across the entire application
|
|
10
|
+
- **Transient**: New instance created for each injection
|
|
11
|
+
- **Request**: One instance shared within a request context, isolated between requests
|
|
12
|
+
|
|
13
|
+
## Singleton Scope
|
|
14
|
+
|
|
15
|
+
Singleton is the default scope. A single instance is created and shared across all injections.
|
|
16
|
+
|
|
17
|
+
### Basic Usage
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { Injectable, InjectableScope } from '@navios/di'
|
|
21
|
+
|
|
22
|
+
// Explicit singleton (default)
|
|
23
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
24
|
+
class DatabaseService {
|
|
25
|
+
private connection: any = null
|
|
26
|
+
|
|
27
|
+
async connect() {
|
|
28
|
+
if (!this.connection) {
|
|
29
|
+
this.connection = await this.createConnection()
|
|
30
|
+
}
|
|
31
|
+
return this.connection
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async createConnection() {
|
|
35
|
+
console.log('Creating database connection...')
|
|
36
|
+
return { connected: true, id: Math.random() }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Implicit singleton (same as above)
|
|
41
|
+
@Injectable()
|
|
42
|
+
class CacheService {
|
|
43
|
+
private cache = new Map()
|
|
44
|
+
|
|
45
|
+
set(key: string, value: any) {
|
|
46
|
+
this.cache.set(key, value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get(key: string) {
|
|
50
|
+
return this.cache.get(key)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Singleton Behavior
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { Container } from '@navios/di'
|
|
59
|
+
|
|
60
|
+
const container = new Container()
|
|
61
|
+
|
|
62
|
+
// All these calls return the same instance
|
|
63
|
+
const db1 = await container.get(DatabaseService)
|
|
64
|
+
const db2 = await container.get(DatabaseService)
|
|
65
|
+
const db3 = await container.get(DatabaseService)
|
|
66
|
+
|
|
67
|
+
console.log(db1 === db2) // true
|
|
68
|
+
console.log(db2 === db3) // true
|
|
69
|
+
console.log(db1 === db3) // true
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Singleton with Dependencies
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { inject, Injectable } from '@navios/di'
|
|
76
|
+
|
|
77
|
+
@Injectable()
|
|
78
|
+
class LoggerService {
|
|
79
|
+
private logs: string[] = []
|
|
80
|
+
|
|
81
|
+
log(message: string) {
|
|
82
|
+
this.logs.push(message)
|
|
83
|
+
console.log(`[LOG] ${message}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getLogs() {
|
|
87
|
+
return [...this.logs]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Injectable()
|
|
92
|
+
class UserService {
|
|
93
|
+
private readonly logger = inject(LoggerService)
|
|
94
|
+
|
|
95
|
+
createUser(name: string) {
|
|
96
|
+
this.logger.log(`Creating user: ${name}`)
|
|
97
|
+
return { id: Math.random().toString(36), name }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Injectable()
|
|
102
|
+
class OrderService {
|
|
103
|
+
private readonly logger = inject(LoggerService)
|
|
104
|
+
|
|
105
|
+
createOrder(userId: string) {
|
|
106
|
+
this.logger.log(`Creating order for user: ${userId}`)
|
|
107
|
+
return { id: Math.random().toString(36), userId }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Usage
|
|
112
|
+
const container = new Container()
|
|
113
|
+
const userService = await container.get(UserService)
|
|
114
|
+
const orderService = await container.get(OrderService)
|
|
115
|
+
|
|
116
|
+
const user = userService.createUser('Alice')
|
|
117
|
+
const order = orderService.createOrder(user.id)
|
|
118
|
+
|
|
119
|
+
// Both services share the same logger instance
|
|
120
|
+
const logger = await container.get(LoggerService)
|
|
121
|
+
console.log(logger.getLogs()) // Contains logs from both services
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Transient Scope
|
|
125
|
+
|
|
126
|
+
Transient scope creates a new instance for each injection request.
|
|
127
|
+
|
|
128
|
+
### Basic Usage
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { Injectable, InjectableScope } from '@navios/di'
|
|
132
|
+
|
|
133
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
134
|
+
class RequestService {
|
|
135
|
+
private readonly requestId = Math.random().toString(36)
|
|
136
|
+
private readonly createdAt = new Date()
|
|
137
|
+
|
|
138
|
+
getRequestId() {
|
|
139
|
+
return this.requestId
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getCreatedAt() {
|
|
143
|
+
return this.createdAt
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Transient Behavior
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { Container } from '@navios/di'
|
|
152
|
+
|
|
153
|
+
const container = new Container()
|
|
154
|
+
|
|
155
|
+
// Each call creates a new instance
|
|
156
|
+
const req1 = await container.get(RequestService)
|
|
157
|
+
const req2 = await container.get(RequestService)
|
|
158
|
+
const req3 = await container.get(RequestService)
|
|
159
|
+
|
|
160
|
+
console.log(req1 === req2) // false
|
|
161
|
+
console.log(req2 === req3) // false
|
|
162
|
+
console.log(req1 === req3) // false
|
|
163
|
+
|
|
164
|
+
console.log(req1.getRequestId()) // Different ID
|
|
165
|
+
console.log(req2.getRequestId()) // Different ID
|
|
166
|
+
console.log(req3.getRequestId()) // Different ID
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Transient with Dependencies
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { inject, Injectable, InjectableScope } from '@navios/di'
|
|
173
|
+
|
|
174
|
+
@Injectable()
|
|
175
|
+
class LoggerService {
|
|
176
|
+
log(message: string) {
|
|
177
|
+
console.log(`[LOG] ${message}`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
182
|
+
class RequestHandler {
|
|
183
|
+
private readonly logger = inject(LoggerService)
|
|
184
|
+
private readonly requestId = Math.random().toString(36)
|
|
185
|
+
|
|
186
|
+
async handleRequest() {
|
|
187
|
+
const logger = await this.logger
|
|
188
|
+
logger.log(`Handling request ${this.requestId}`)
|
|
189
|
+
return { requestId: this.requestId, status: 'processed' }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Usage
|
|
194
|
+
const container = new Container()
|
|
195
|
+
const handler1 = await container.get(RequestHandler)
|
|
196
|
+
const handler2 = await container.get(RequestHandler)
|
|
197
|
+
|
|
198
|
+
const result1 = await handler1.handleRequest()
|
|
199
|
+
const result2 = await handler2.handleRequest()
|
|
200
|
+
|
|
201
|
+
console.log(result1.requestId) // Different ID
|
|
202
|
+
console.log(result2.requestId) // Different ID
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Request Scope
|
|
206
|
+
|
|
207
|
+
Request scope creates one instance per request context and shares it within that request. This is ideal for web applications where you need request-specific data that should be isolated between different requests.
|
|
208
|
+
|
|
209
|
+
### Basic Usage
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { Injectable, InjectableScope } from '@navios/di'
|
|
213
|
+
|
|
214
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
215
|
+
class RequestContext {
|
|
216
|
+
private readonly requestId = Math.random().toString(36)
|
|
217
|
+
private readonly startTime = Date.now()
|
|
218
|
+
private readonly userId: string
|
|
219
|
+
|
|
220
|
+
constructor(userId: string) {
|
|
221
|
+
this.userId = userId
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getRequestId() {
|
|
225
|
+
return this.requestId
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getDuration() {
|
|
229
|
+
return Date.now() - this.startTime
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getUserId() {
|
|
233
|
+
return this.userId
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Request Context Management
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { Container } from '@navios/di'
|
|
242
|
+
|
|
243
|
+
const container = new Container()
|
|
244
|
+
|
|
245
|
+
// Begin a request context
|
|
246
|
+
const requestId = 'req-123'
|
|
247
|
+
container.beginRequest(requestId, { userId: 'user123' })
|
|
248
|
+
|
|
249
|
+
// All injections within this request will share the same Request-scoped instances
|
|
250
|
+
const context1 = await container.get(RequestContext)
|
|
251
|
+
const context2 = await container.get(RequestContext)
|
|
252
|
+
|
|
253
|
+
console.log(context1 === context2) // true - same instance within request
|
|
254
|
+
|
|
255
|
+
// End the request context (cleans up all request-scoped instances)
|
|
256
|
+
await container.endRequest(requestId)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Request Scope with Dependencies
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { inject, Injectable, InjectableScope } from '@navios/di'
|
|
263
|
+
|
|
264
|
+
@Injectable()
|
|
265
|
+
class LoggerService {
|
|
266
|
+
log(message: string) {
|
|
267
|
+
console.log(`[LOG] ${message}`)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
272
|
+
class UserSession {
|
|
273
|
+
private readonly logger = inject(LoggerService)
|
|
274
|
+
private readonly sessionId = Math.random().toString(36)
|
|
275
|
+
private readonly userId: string
|
|
276
|
+
|
|
277
|
+
constructor(userId: string) {
|
|
278
|
+
this.userId = userId
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async logActivity(activity: string) {
|
|
282
|
+
const logger = await this.logger
|
|
283
|
+
logger.log(`User ${this.userId}: ${activity}`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getSessionId() {
|
|
287
|
+
return this.sessionId
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
292
|
+
class OrderService {
|
|
293
|
+
private readonly userSession = inject(UserSession)
|
|
294
|
+
private orders: string[] = []
|
|
295
|
+
|
|
296
|
+
async createOrder(productName: string) {
|
|
297
|
+
const session = await this.userSession
|
|
298
|
+
const orderId = `order_${Math.random().toString(36)}`
|
|
299
|
+
|
|
300
|
+
this.orders.push(orderId)
|
|
301
|
+
await session.logActivity(`Created order ${orderId} for ${productName}`)
|
|
302
|
+
|
|
303
|
+
return { orderId, userId: session.getSessionId() }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Request Context Switching
|
|
309
|
+
|
|
310
|
+
You can manage multiple request contexts and switch between them:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
const container = new Container()
|
|
314
|
+
|
|
315
|
+
// Start multiple requests
|
|
316
|
+
container.beginRequest('req-1', { userId: 'user1' })
|
|
317
|
+
container.beginRequest('req-2', { userId: 'user2' })
|
|
318
|
+
|
|
319
|
+
// Switch to request 1
|
|
320
|
+
container.setCurrentRequestContext('req-1')
|
|
321
|
+
const context1 = await container.get(RequestContext)
|
|
322
|
+
|
|
323
|
+
// Switch to request 2
|
|
324
|
+
container.setCurrentRequestContext('req-2')
|
|
325
|
+
const context2 = await container.get(RequestContext)
|
|
326
|
+
|
|
327
|
+
// Different instances for different requests
|
|
328
|
+
console.log(context1 !== context2) // true
|
|
329
|
+
|
|
330
|
+
// Clean up
|
|
331
|
+
await container.endRequest('req-1')
|
|
332
|
+
await container.endRequest('req-2')
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Scope Compatibility
|
|
336
|
+
|
|
337
|
+
### Injection Method Compatibility
|
|
338
|
+
|
|
339
|
+
| Scope | inject | asyncInject |
|
|
340
|
+
| --------- | ---------------- | ------------ |
|
|
341
|
+
| Singleton | ✅ Supported | ✅ Supported |
|
|
342
|
+
| Transient | ❌ Not Supported | ✅ Supported |
|
|
343
|
+
| Request | ✅ Supported | ✅ Supported |
|
|
344
|
+
|
|
345
|
+
### Why inject Doesn't Work with Transient
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// ❌ This will cause an error
|
|
349
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
350
|
+
class TransientService {}
|
|
351
|
+
|
|
352
|
+
@Injectable()
|
|
353
|
+
class ConsumerService {
|
|
354
|
+
private readonly service = inject(TransientService)
|
|
355
|
+
// Error: Cannot use inject with transient services
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ✅ Use inject instead
|
|
359
|
+
@Injectable()
|
|
360
|
+
class ConsumerService {
|
|
361
|
+
private readonly service = asyncInject(TransientService)
|
|
362
|
+
|
|
363
|
+
async doSomething() {
|
|
364
|
+
const service = await this.service
|
|
365
|
+
// Use the service
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Real-World Examples
|
|
371
|
+
|
|
372
|
+
### Singleton: Database Connection Pool
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { Injectable, OnServiceDestroy, OnServiceInit } from '@navios/di'
|
|
376
|
+
|
|
377
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
378
|
+
class DatabasePool implements OnServiceInit, OnServiceDestroy {
|
|
379
|
+
private connections: any[] = []
|
|
380
|
+
private maxConnections = 10
|
|
381
|
+
|
|
382
|
+
async onServiceInit() {
|
|
383
|
+
console.log('Initializing database connection pool...')
|
|
384
|
+
// Initialize connection pool
|
|
385
|
+
for (let i = 0; i < this.maxConnections; i++) {
|
|
386
|
+
this.connections.push({ id: i, busy: false })
|
|
387
|
+
}
|
|
388
|
+
console.log(`Pool initialized with ${this.maxConnections} connections`)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async onServiceDestroy() {
|
|
392
|
+
console.log('Closing database connection pool...')
|
|
393
|
+
this.connections = []
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async getConnection() {
|
|
397
|
+
const available = this.connections.find((conn) => !conn.busy)
|
|
398
|
+
if (!available) {
|
|
399
|
+
throw new Error('No available connections')
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
available.busy = true
|
|
403
|
+
console.log(`Using connection ${available.id}`)
|
|
404
|
+
return available
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
releaseConnection(connection: any) {
|
|
408
|
+
connection.busy = false
|
|
409
|
+
console.log(`Released connection ${connection.id}`)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Transient: Request Context
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import { Injectable, InjectableScope } from '@navios/di'
|
|
418
|
+
|
|
419
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
420
|
+
class RequestContext {
|
|
421
|
+
private readonly requestId = Math.random().toString(36)
|
|
422
|
+
private readonly startTime = Date.now()
|
|
423
|
+
private readonly userAgent: string
|
|
424
|
+
private readonly ip: string
|
|
425
|
+
|
|
426
|
+
constructor(userAgent: string, ip: string) {
|
|
427
|
+
this.userAgent = userAgent
|
|
428
|
+
this.ip = ip
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
getRequestId() {
|
|
432
|
+
return this.requestId
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
getDuration() {
|
|
436
|
+
return Date.now() - this.startTime
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
getUserAgent() {
|
|
440
|
+
return this.userAgent
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
getIp() {
|
|
444
|
+
return this.ip
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@Injectable()
|
|
449
|
+
class RequestHandler {
|
|
450
|
+
private readonly context = asyncInject(RequestContext)
|
|
451
|
+
|
|
452
|
+
async handleRequest() {
|
|
453
|
+
const ctx = await this.context
|
|
454
|
+
console.log(`Handling request ${ctx.getRequestId()} from ${ctx.getIp()}`)
|
|
455
|
+
|
|
456
|
+
// Process request...
|
|
457
|
+
|
|
458
|
+
console.log(
|
|
459
|
+
`Request ${ctx.getRequestId()} completed in ${ctx.getDuration()}ms`,
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Mixed Scopes
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import { asyncInject, inject, Injectable, InjectableScope } from '@navios/di'
|
|
469
|
+
|
|
470
|
+
// Singleton services
|
|
471
|
+
@Injectable()
|
|
472
|
+
class ConfigService {
|
|
473
|
+
getConfig() {
|
|
474
|
+
return { apiUrl: 'https://api.example.com', timeout: 5000 }
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
@Injectable()
|
|
479
|
+
class LoggerService {
|
|
480
|
+
log(message: string) {
|
|
481
|
+
console.log(`[LOG] ${new Date().toISOString()} - ${message}`)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Transient service
|
|
486
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
487
|
+
class UserSession {
|
|
488
|
+
private readonly sessionId = Math.random().toString(36)
|
|
489
|
+
private readonly userId: string
|
|
490
|
+
|
|
491
|
+
constructor(userId: string) {
|
|
492
|
+
this.userId = userId
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
getSessionId() {
|
|
496
|
+
return this.sessionId
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
getUserId() {
|
|
500
|
+
return this.userId
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Service using both scopes
|
|
505
|
+
@Injectable()
|
|
506
|
+
class UserService {
|
|
507
|
+
private readonly config = inject(ConfigService) // Singleton
|
|
508
|
+
private readonly logger = inject(LoggerService) // Singleton
|
|
509
|
+
private readonly session = asyncInject(UserSession) // Transient
|
|
510
|
+
|
|
511
|
+
async authenticateUser(userId: string) {
|
|
512
|
+
this.logger.log(`Authenticating user ${userId}`)
|
|
513
|
+
|
|
514
|
+
const session = await this.session
|
|
515
|
+
this.logger.log(
|
|
516
|
+
`Created session ${session.getSessionId()} for user ${userId}`,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
userId,
|
|
521
|
+
sessionId: session.getSessionId(),
|
|
522
|
+
apiUrl: this.config.getConfig().apiUrl,
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
## Best Practices
|
|
529
|
+
|
|
530
|
+
### 1. Use Singleton for Stateless Services
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
// ✅ Good: Stateless service as singleton
|
|
534
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
535
|
+
class EmailService {
|
|
536
|
+
async sendEmail(to: string, subject: string, body: string) {
|
|
537
|
+
// No state, safe to share
|
|
538
|
+
return await this.sendViaProvider(to, subject, body)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ❌ Avoid: Stateful service as singleton
|
|
543
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
544
|
+
class UserSessionService {
|
|
545
|
+
private currentUser: User | null = null // State!
|
|
546
|
+
|
|
547
|
+
setCurrentUser(user: User) {
|
|
548
|
+
this.currentUser = user // Shared state can cause issues
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### 2. Use Transient for Stateful Services
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// ✅ Good: Stateful service as transient
|
|
557
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
558
|
+
class UserSession {
|
|
559
|
+
private readonly userId: string
|
|
560
|
+
private readonly sessionId: string
|
|
561
|
+
private readonly createdAt: Date
|
|
562
|
+
|
|
563
|
+
constructor(userId: string) {
|
|
564
|
+
this.userId = userId
|
|
565
|
+
this.sessionId = Math.random().toString(36)
|
|
566
|
+
this.createdAt = new Date()
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
getUserId() {
|
|
570
|
+
return this.userId
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### 3. Use Singleton for Expensive Resources
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
// ✅ Good: Expensive resource as singleton
|
|
579
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
580
|
+
class DatabaseConnection {
|
|
581
|
+
private connection: any = null
|
|
582
|
+
|
|
583
|
+
async getConnection() {
|
|
584
|
+
if (!this.connection) {
|
|
585
|
+
// Expensive operation - only do once
|
|
586
|
+
this.connection = await this.createConnection()
|
|
587
|
+
}
|
|
588
|
+
return this.connection
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### 4. Use Transient for Request-Specific Data
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// ✅ Good: Request-specific data as transient
|
|
597
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
598
|
+
class RequestContext {
|
|
599
|
+
private readonly requestId: string
|
|
600
|
+
private readonly startTime: number
|
|
601
|
+
private readonly headers: Record<string, string>
|
|
602
|
+
|
|
603
|
+
constructor(headers: Record<string, string>) {
|
|
604
|
+
this.requestId = Math.random().toString(36)
|
|
605
|
+
this.startTime = Date.now()
|
|
606
|
+
this.headers = headers
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### 5. Consider Performance Implications
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
// ✅ Good: Lightweight transient service
|
|
615
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
616
|
+
class RequestIdGenerator {
|
|
617
|
+
generate() {
|
|
618
|
+
return Math.random().toString(36)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ❌ Avoid: Heavy transient service
|
|
623
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
624
|
+
class HeavyService {
|
|
625
|
+
constructor() {
|
|
626
|
+
// Heavy initialization for each instance
|
|
627
|
+
this.initializeExpensiveResources()
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## Common Pitfalls
|
|
633
|
+
|
|
634
|
+
### 1. State Leakage in Singletons
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
// ❌ Problem: State leakage
|
|
638
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
639
|
+
class CacheService {
|
|
640
|
+
private cache = new Map()
|
|
641
|
+
|
|
642
|
+
set(key: string, value: any) {
|
|
643
|
+
this.cache.set(key, value)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
get(key: string) {
|
|
647
|
+
return this.cache.get(key)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Problem: Cache persists across requests
|
|
651
|
+
clear() {
|
|
652
|
+
this.cache.clear()
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ✅ Solution: Use transient for request-scoped cache
|
|
657
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
658
|
+
class RequestCache {
|
|
659
|
+
private cache = new Map()
|
|
660
|
+
|
|
661
|
+
set(key: string, value: any) {
|
|
662
|
+
this.cache.set(key, value)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
get(key: string) {
|
|
666
|
+
return this.cache.get(key)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### 2. Incorrect Injection Method
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
// ❌ Problem: Using inject with transient
|
|
675
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
676
|
+
class TransientService {}
|
|
677
|
+
|
|
678
|
+
@Injectable()
|
|
679
|
+
class ConsumerService {
|
|
680
|
+
private readonly service = inject(TransientService)
|
|
681
|
+
// Error: Cannot use inject with transient services
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ✅ Solution: Use asyncInject with transient
|
|
685
|
+
@Injectable()
|
|
686
|
+
class ConsumerService {
|
|
687
|
+
private readonly service = asyncInject(TransientService)
|
|
688
|
+
|
|
689
|
+
async doSomething() {
|
|
690
|
+
const service = await this.service
|
|
691
|
+
// Use the service
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### 3. Memory Leaks with Transient Services
|
|
697
|
+
|
|
698
|
+
```typescript
|
|
699
|
+
// ❌ Problem: Transient service holding references
|
|
700
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
701
|
+
class TransientService {
|
|
702
|
+
private listeners: Function[] = []
|
|
703
|
+
|
|
704
|
+
addListener(listener: Function) {
|
|
705
|
+
this.listeners.push(listener)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Problem: Listeners are never cleaned up
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ✅ Solution: Implement cleanup
|
|
712
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
713
|
+
class TransientService implements OnServiceDestroy {
|
|
714
|
+
private listeners: Function[] = []
|
|
715
|
+
|
|
716
|
+
addListener(listener: Function) {
|
|
717
|
+
this.listeners.push(listener)
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async onServiceDestroy() {
|
|
721
|
+
this.listeners = []
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
## API Reference
|
|
727
|
+
|
|
728
|
+
### InjectableScope Enum
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
enum InjectableScope {
|
|
732
|
+
Singleton = 'Singleton', // One instance shared across the application
|
|
733
|
+
Transient = 'Transient', // New instance created for each injection
|
|
734
|
+
Request = 'Request', // One instance shared within a request context
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### Scope Configuration
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
742
|
+
class SingletonService {}
|
|
743
|
+
|
|
744
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
745
|
+
class TransientService {}
|
|
746
|
+
|
|
747
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
748
|
+
class RequestService {}
|
|
749
|
+
```
|