@navios/di 0.2.0 → 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.
Files changed (62) hide show
  1. package/README.md +301 -39
  2. package/docs/README.md +122 -49
  3. package/docs/api-reference.md +763 -0
  4. package/docs/container.md +274 -0
  5. package/docs/examples/basic-usage.mts +97 -0
  6. package/docs/examples/factory-pattern.mts +318 -0
  7. package/docs/examples/injection-tokens.mts +225 -0
  8. package/docs/examples/request-scope-example.mts +254 -0
  9. package/docs/examples/service-lifecycle.mts +359 -0
  10. package/docs/factory.md +584 -0
  11. package/docs/getting-started.md +308 -0
  12. package/docs/injectable.md +496 -0
  13. package/docs/injection-tokens.md +400 -0
  14. package/docs/lifecycle.md +539 -0
  15. package/docs/scopes.md +749 -0
  16. package/lib/_tsup-dts-rollup.d.mts +495 -150
  17. package/lib/_tsup-dts-rollup.d.ts +495 -150
  18. package/lib/index.d.mts +26 -12
  19. package/lib/index.d.ts +26 -12
  20. package/lib/index.js +993 -462
  21. package/lib/index.js.map +1 -1
  22. package/lib/index.mjs +983 -453
  23. package/lib/index.mjs.map +1 -1
  24. package/package.json +2 -2
  25. package/project.json +10 -2
  26. package/src/__tests__/container.spec.mts +1301 -0
  27. package/src/__tests__/factory.spec.mts +137 -0
  28. package/src/__tests__/injectable.spec.mts +32 -88
  29. package/src/__tests__/injection-token.spec.mts +333 -17
  30. package/src/__tests__/request-scope.spec.mts +263 -0
  31. package/src/__type-tests__/factory.spec-d.mts +65 -0
  32. package/src/__type-tests__/inject.spec-d.mts +27 -28
  33. package/src/__type-tests__/injectable.spec-d.mts +42 -206
  34. package/src/container.mts +167 -0
  35. package/src/decorators/factory.decorator.mts +79 -0
  36. package/src/decorators/index.mts +1 -0
  37. package/src/decorators/injectable.decorator.mts +6 -56
  38. package/src/enums/injectable-scope.enum.mts +5 -1
  39. package/src/event-emitter.mts +18 -20
  40. package/src/factory-context.mts +2 -10
  41. package/src/index.mts +3 -2
  42. package/src/injection-token.mts +24 -9
  43. package/src/injector.mts +8 -20
  44. package/src/interfaces/factory.interface.mts +3 -3
  45. package/src/interfaces/index.mts +2 -0
  46. package/src/interfaces/on-service-destroy.interface.mts +3 -0
  47. package/src/interfaces/on-service-init.interface.mts +3 -0
  48. package/src/registry.mts +7 -16
  49. package/src/request-context-holder.mts +145 -0
  50. package/src/service-instantiator.mts +158 -0
  51. package/src/service-locator-event-bus.mts +0 -28
  52. package/src/service-locator-instance-holder.mts +27 -16
  53. package/src/service-locator-manager.mts +84 -0
  54. package/src/service-locator.mts +550 -395
  55. package/src/utils/defer.mts +73 -0
  56. package/src/utils/get-injectors.mts +93 -80
  57. package/src/utils/index.mts +2 -0
  58. package/src/utils/types.mts +52 -0
  59. package/docs/concepts/injectable.md +0 -182
  60. package/docs/concepts/injection-token.md +0 -145
  61. package/src/proxy-service-locator.mts +0 -83
  62. 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)