@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
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# Injectable Decorator
|
|
2
|
+
|
|
3
|
+
The `@Injectable` decorator is the primary way to mark classes as injectable services in Navios DI. It registers the class with the dependency injection container and makes it available for injection into other services.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
### Simple Service Registration
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Injectable } from '@navios/di'
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
class UserService {
|
|
14
|
+
getUsers() {
|
|
15
|
+
return ['Alice', 'Bob', 'Charlie']
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Service with Dependencies
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { inject, Injectable } from '@navios/di'
|
|
24
|
+
|
|
25
|
+
@Injectable()
|
|
26
|
+
class DatabaseService {
|
|
27
|
+
async connect() {
|
|
28
|
+
return 'Connected to database'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Injectable()
|
|
33
|
+
class UserService {
|
|
34
|
+
private readonly db = inject(DatabaseService)
|
|
35
|
+
|
|
36
|
+
async getUsers() {
|
|
37
|
+
const connection = await this.db.connect()
|
|
38
|
+
return `Users from ${connection}`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration Options
|
|
44
|
+
|
|
45
|
+
The `@Injectable` decorator accepts an options object with the following properties:
|
|
46
|
+
|
|
47
|
+
### Scope Configuration
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { Injectable, InjectableScope } from '@navios/di'
|
|
51
|
+
|
|
52
|
+
// Singleton (default) - one instance shared across the application
|
|
53
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
54
|
+
class SingletonService {}
|
|
55
|
+
|
|
56
|
+
// Transient - new instance created for each injection
|
|
57
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
58
|
+
class TransientService {}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Custom Injection Token
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { Injectable, InjectionToken } from '@navios/di'
|
|
65
|
+
|
|
66
|
+
const USER_SERVICE_TOKEN = InjectionToken.create<UserService>('UserService')
|
|
67
|
+
|
|
68
|
+
@Injectable({ token: USER_SERVICE_TOKEN })
|
|
69
|
+
class UserService {
|
|
70
|
+
getUsers() {
|
|
71
|
+
return ['Alice', 'Bob']
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Custom Registry
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { Injectable, Registry } from '@navios/di'
|
|
80
|
+
|
|
81
|
+
const customRegistry = new Registry()
|
|
82
|
+
|
|
83
|
+
@Injectable({ registry: customRegistry })
|
|
84
|
+
class CustomService {}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Injection Methods
|
|
88
|
+
|
|
89
|
+
### Synchronous Injection with `inject`
|
|
90
|
+
|
|
91
|
+
Use `inject` when you need immediate access to a dependency:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { inject, Injectable } from '@navios/di'
|
|
95
|
+
|
|
96
|
+
@Injectable()
|
|
97
|
+
class EmailService {
|
|
98
|
+
sendEmail(to: string, subject: string) {
|
|
99
|
+
return `Email sent to ${to}: ${subject}`
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Injectable()
|
|
104
|
+
class NotificationService {
|
|
105
|
+
private readonly emailService = inject(EmailService)
|
|
106
|
+
|
|
107
|
+
notify(user: string, message: string) {
|
|
108
|
+
// Direct access - no await needed
|
|
109
|
+
return this.emailService.sendEmail(user, `Notification: ${message}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Important:** `inject` only works with singleton services. For transient services, use `asyncInject` instead.
|
|
115
|
+
|
|
116
|
+
### Asynchronous Injection with `inject`
|
|
117
|
+
|
|
118
|
+
Use `inject` for services that might not be immediately available:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { inject, Injectable } from '@navios/di'
|
|
122
|
+
|
|
123
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
124
|
+
class TransientService {
|
|
125
|
+
constructor() {
|
|
126
|
+
console.log('Creating new transient instance')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getValue() {
|
|
130
|
+
return Math.random()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Injectable()
|
|
135
|
+
class ConsumerService {
|
|
136
|
+
private readonly transientService = asyncInject(TransientService)
|
|
137
|
+
|
|
138
|
+
async getRandomValue() {
|
|
139
|
+
const service = await this.transientService
|
|
140
|
+
return service.getValue()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Advanced Patterns
|
|
146
|
+
|
|
147
|
+
### Service with Configuration Schema
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { Injectable, InjectionToken } from '@navios/di'
|
|
151
|
+
|
|
152
|
+
import { z } from 'zod'
|
|
153
|
+
|
|
154
|
+
const configSchema = z.object({
|
|
155
|
+
apiUrl: z.string(),
|
|
156
|
+
timeout: z.number(),
|
|
157
|
+
retries: z.number().optional(),
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const CONFIG_TOKEN = InjectionToken.create<ConfigService, typeof configSchema>(
|
|
161
|
+
'APP_CONFIG',
|
|
162
|
+
configSchema,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@Injectable({ token: CONFIG_TOKEN })
|
|
166
|
+
class ConfigService {
|
|
167
|
+
constructor(private config: z.infer<typeof configSchema>) {}
|
|
168
|
+
|
|
169
|
+
getApiUrl() {
|
|
170
|
+
return this.config.apiUrl
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getTimeout() {
|
|
174
|
+
return this.config.timeout
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
getRetries() {
|
|
178
|
+
return this.config.retries ?? 3
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Service with Multiple Dependencies
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { inject, Injectable } from '@navios/di'
|
|
187
|
+
|
|
188
|
+
@Injectable()
|
|
189
|
+
class LoggerService {
|
|
190
|
+
log(message: string) {
|
|
191
|
+
console.log(`[LOG] ${message}`)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Injectable()
|
|
196
|
+
class DatabaseService {
|
|
197
|
+
async query(sql: string) {
|
|
198
|
+
return `Query result: ${sql}`
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@Injectable()
|
|
203
|
+
class CacheService {
|
|
204
|
+
set(key: string, value: any) {
|
|
205
|
+
console.log(`Cache set: ${key}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
get(key: string) {
|
|
209
|
+
return `Cached value for ${key}`
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@Injectable()
|
|
214
|
+
class UserService {
|
|
215
|
+
private readonly logger = inject(LoggerService)
|
|
216
|
+
private readonly db = inject(DatabaseService)
|
|
217
|
+
private readonly cache = inject(CacheService)
|
|
218
|
+
|
|
219
|
+
async getUser(id: string) {
|
|
220
|
+
this.logger.log(`Getting user ${id}`)
|
|
221
|
+
|
|
222
|
+
// Check cache first
|
|
223
|
+
const cached = this.cache.get(`user:${id}`)
|
|
224
|
+
if (cached) {
|
|
225
|
+
return cached
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Query database
|
|
229
|
+
const user = await this.db.query(`SELECT * FROM users WHERE id = ${id}`)
|
|
230
|
+
|
|
231
|
+
// Cache the result
|
|
232
|
+
this.cache.set(`user:${id}`, user)
|
|
233
|
+
|
|
234
|
+
return user
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Service with Lifecycle Hooks
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { Injectable, OnServiceDestroy, OnServiceInit } from '@navios/di'
|
|
243
|
+
|
|
244
|
+
@Injectable()
|
|
245
|
+
class DatabaseService implements OnServiceInit, OnServiceDestroy {
|
|
246
|
+
private connection: any = null
|
|
247
|
+
|
|
248
|
+
async onServiceInit() {
|
|
249
|
+
console.log('Initializing database connection...')
|
|
250
|
+
this.connection = await this.connect()
|
|
251
|
+
console.log('Database connected successfully')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async onServiceDestroy() {
|
|
255
|
+
console.log('Closing database connection...')
|
|
256
|
+
if (this.connection) {
|
|
257
|
+
await this.connection.close()
|
|
258
|
+
console.log('Database connection closed')
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async connect() {
|
|
263
|
+
// Simulate database connection
|
|
264
|
+
return new Promise((resolve) => {
|
|
265
|
+
setTimeout(() => resolve({ connected: true }), 100)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async query(sql: string) {
|
|
270
|
+
if (!this.connection) {
|
|
271
|
+
throw new Error('Database not connected')
|
|
272
|
+
}
|
|
273
|
+
return `Query result: ${sql}`
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Scopes and Injection Compatibility
|
|
279
|
+
|
|
280
|
+
### Singleton Services
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
284
|
+
class SingletonService {
|
|
285
|
+
private readonly id = Math.random()
|
|
286
|
+
|
|
287
|
+
getId() {
|
|
288
|
+
return this.id
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@Injectable()
|
|
293
|
+
class ConsumerService {
|
|
294
|
+
// Both injection methods work with singletons
|
|
295
|
+
private readonly syncService = inject(SingletonService)
|
|
296
|
+
private readonly asyncService = asyncInject(SingletonService)
|
|
297
|
+
|
|
298
|
+
async demonstrate() {
|
|
299
|
+
// Both return the same instance
|
|
300
|
+
console.log(this.syncService.getId()) // Same ID
|
|
301
|
+
const asyncInstance = await this.asyncService
|
|
302
|
+
console.log(asyncInstance.getId()) // Same ID
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Transient Services
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
311
|
+
class TransientService {
|
|
312
|
+
private readonly id = Math.random()
|
|
313
|
+
|
|
314
|
+
getId() {
|
|
315
|
+
return this.id
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@Injectable()
|
|
320
|
+
class ConsumerService {
|
|
321
|
+
// Only asyncInject() works with transient services
|
|
322
|
+
private readonly transientService = asyncInject(TransientService)
|
|
323
|
+
|
|
324
|
+
async demonstrate() {
|
|
325
|
+
const instance1 = await this.transientService
|
|
326
|
+
const instance2 = await this.transientService
|
|
327
|
+
|
|
328
|
+
console.log(instance1.getId()) // Different ID
|
|
329
|
+
console.log(instance2.getId()) // Different ID
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Error Handling
|
|
335
|
+
|
|
336
|
+
### Common Errors
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { inject, Injectable } from '@navios/di'
|
|
340
|
+
|
|
341
|
+
@Injectable()
|
|
342
|
+
class ProblematicService {
|
|
343
|
+
private readonly nonExistentService = inject(NonExistentService)
|
|
344
|
+
// Error: NonExistentService is not registered
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
348
|
+
class TransientService {}
|
|
349
|
+
|
|
350
|
+
@Injectable()
|
|
351
|
+
class WrongInjectionService {
|
|
352
|
+
private readonly transientService = inject(TransientService)
|
|
353
|
+
// Error: Cannot use inject with transient services
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Proper Error Handling
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
import { inject, Injectable } from '@navios/di'
|
|
361
|
+
|
|
362
|
+
@Injectable()
|
|
363
|
+
class SafeService {
|
|
364
|
+
private readonly optionalService = asyncInject(OptionalService).catch(
|
|
365
|
+
() => null,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
async doSomething() {
|
|
369
|
+
try {
|
|
370
|
+
const service = await this.optionalService
|
|
371
|
+
if (service) {
|
|
372
|
+
return service.doSomething()
|
|
373
|
+
}
|
|
374
|
+
return 'Service not available'
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error('Error accessing service:', error)
|
|
377
|
+
return 'Error occurred'
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Best Practices
|
|
384
|
+
|
|
385
|
+
### 1. Use Appropriate Injection Method
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// ✅ Good: Use inject for singleton dependencies
|
|
389
|
+
@Injectable()
|
|
390
|
+
class UserService {
|
|
391
|
+
private readonly logger = inject(LoggerService)
|
|
392
|
+
|
|
393
|
+
getUser(id: string) {
|
|
394
|
+
this.logger.log(`Getting user ${id}`)
|
|
395
|
+
// ...
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ✅ Good: Use asyncInject for transient dependencies
|
|
400
|
+
@Injectable()
|
|
401
|
+
class RequestService {
|
|
402
|
+
private readonly transientService = asyncInject(TransientService)
|
|
403
|
+
|
|
404
|
+
async handleRequest() {
|
|
405
|
+
const service = await this.transientService
|
|
406
|
+
return service.process()
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### 2. Prefer Singleton for Stateless Services
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
// ✅ Good: Stateless service as singleton
|
|
415
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
416
|
+
class EmailService {
|
|
417
|
+
sendEmail(to: string, subject: string) {
|
|
418
|
+
// No state, safe to share
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ❌ Avoid: Stateful service as singleton
|
|
423
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
424
|
+
class UserSessionService {
|
|
425
|
+
private currentUser: User | null = null // State!
|
|
426
|
+
|
|
427
|
+
setCurrentUser(user: User) {
|
|
428
|
+
this.currentUser = user // Shared state can cause issues
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### 3. Use Injection Tokens for Interfaces
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// ✅ Good: Use injection tokens for interfaces
|
|
437
|
+
interface PaymentProcessor {
|
|
438
|
+
processPayment(amount: number): Promise<string>
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const PAYMENT_PROCESSOR_TOKEN =
|
|
442
|
+
InjectionToken.create<PaymentProcessor>('PaymentProcessor')
|
|
443
|
+
|
|
444
|
+
@Injectable({ token: PAYMENT_PROCESSOR_TOKEN })
|
|
445
|
+
class StripePaymentProcessor implements PaymentProcessor {
|
|
446
|
+
async processPayment(amount: number) {
|
|
447
|
+
return `Processed $${amount} via Stripe`
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### 4. Implement Lifecycle Hooks for Resource Management
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
// ✅ Good: Proper resource management
|
|
456
|
+
@Injectable()
|
|
457
|
+
class DatabaseService implements OnServiceInit, OnServiceDestroy {
|
|
458
|
+
private connection: any = null
|
|
459
|
+
|
|
460
|
+
async onServiceInit() {
|
|
461
|
+
this.connection = await this.connect()
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async onServiceDestroy() {
|
|
465
|
+
if (this.connection) {
|
|
466
|
+
await this.connection.close()
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## API Reference
|
|
473
|
+
|
|
474
|
+
### Injectable Options
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
interface InjectableOptions {
|
|
478
|
+
scope?: InjectableScope
|
|
479
|
+
token?: InjectionToken<any, any>
|
|
480
|
+
registry?: Registry
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### InjectableScope Enum
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
enum InjectableScope {
|
|
488
|
+
Singleton = 'Singleton', // One instance shared across the application
|
|
489
|
+
Transient = 'Transient', // New instance created for each injection
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Injection Methods
|
|
494
|
+
|
|
495
|
+
- `inject<T>(token: T): T` - Synchronous injection (singleton only)
|
|
496
|
+
- `asyncInject<T>(token: T): Promise<T>` - Asynchronous injection (all scopes)
|