@navios/di 0.2.1 → 0.3.1

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 +299 -38
  2. package/docs/README.md +121 -48
  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 +494 -145
  17. package/lib/_tsup-dts-rollup.d.ts +494 -145
  18. package/lib/index.d.mts +26 -12
  19. package/lib/index.d.ts +26 -12
  20. package/lib/index.js +1021 -470
  21. package/lib/index.js.map +1 -1
  22. package/lib/index.mjs +1011 -461
  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 +427 -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 +19 -4
  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 +174 -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 +548 -393
  55. package/src/utils/defer.mts +73 -0
  56. package/src/utils/get-injectors.mts +91 -78
  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,225 @@
1
+ /**
2
+ * Injection Tokens Example
3
+ *
4
+ * This example demonstrates:
5
+ * - Creating injection tokens with schemas
6
+ * - Using bound injection tokens
7
+ * - Using factory injection tokens
8
+ * - Token-based dependency resolution
9
+ */
10
+
11
+ import { Container, Injectable, InjectionToken } from '@navios/di'
12
+
13
+ import { z } from 'zod'
14
+
15
+ const container = new Container()
16
+
17
+ // 1. Define schemas for configuration
18
+ const databaseConfigSchema = z.object({
19
+ host: z.string(),
20
+ port: z.number(),
21
+ database: z.string(),
22
+ username: z.string(),
23
+ password: z.string(),
24
+ })
25
+
26
+ const emailConfigSchema = z.object({
27
+ provider: z.enum(['smtp', 'sendgrid', 'ses']),
28
+ apiKey: z.string(),
29
+ fromEmail: z.string().email(),
30
+ })
31
+
32
+ // 2. Create injection tokens
33
+ const DB_CONFIG_TOKEN = InjectionToken.create<
34
+ DatabaseConfigService,
35
+ typeof databaseConfigSchema
36
+ >('DB_CONFIG', databaseConfigSchema)
37
+
38
+ const EMAIL_CONFIG_TOKEN = InjectionToken.create<
39
+ EmailConfigService,
40
+ typeof emailConfigSchema
41
+ >('EMAIL_CONFIG', emailConfigSchema)
42
+
43
+ // 3. Create services that use injection tokens
44
+ @Injectable({ token: DB_CONFIG_TOKEN })
45
+ class DatabaseConfigService {
46
+ constructor(private config: z.infer<typeof databaseConfigSchema>) {}
47
+
48
+ getConnectionString() {
49
+ return `postgresql://${this.config.username}:${this.config.password}@${this.config.host}:${this.config.port}/${this.config.database}`
50
+ }
51
+
52
+ getHost() {
53
+ return this.config.host
54
+ }
55
+
56
+ getPort() {
57
+ return this.config.port
58
+ }
59
+ }
60
+
61
+ @Injectable({ token: EMAIL_CONFIG_TOKEN })
62
+ class EmailConfigService {
63
+ constructor(private config: z.infer<typeof emailConfigSchema>) {}
64
+
65
+ getProvider() {
66
+ return this.config.provider
67
+ }
68
+
69
+ getApiKey() {
70
+ return this.config.apiKey
71
+ }
72
+
73
+ getFromEmail() {
74
+ return this.config.fromEmail
75
+ }
76
+ }
77
+
78
+ // 4. Create bound tokens for different environments
79
+ const PRODUCTION_DB_CONFIG = InjectionToken.bound(DB_CONFIG_TOKEN, {
80
+ host: 'prod-db.example.com',
81
+ port: 5432,
82
+ database: 'production',
83
+ username: 'prod_user',
84
+ password: 'prod_password',
85
+ })
86
+
87
+ const DEVELOPMENT_DB_CONFIG = InjectionToken.bound(DB_CONFIG_TOKEN, {
88
+ host: 'localhost',
89
+ port: 5432,
90
+ database: 'development',
91
+ username: 'dev_user',
92
+ password: 'dev_password',
93
+ })
94
+
95
+ const PRODUCTION_EMAIL_CONFIG = InjectionToken.bound(EMAIL_CONFIG_TOKEN, {
96
+ provider: 'sendgrid',
97
+ apiKey: 'sg.prod_key',
98
+ fromEmail: 'noreply@example.com',
99
+ })
100
+
101
+ const DEVELOPMENT_EMAIL_CONFIG = InjectionToken.bound(EMAIL_CONFIG_TOKEN, {
102
+ provider: 'smtp',
103
+ apiKey: 'smtp_dev_key',
104
+ fromEmail: 'dev@example.com',
105
+ })
106
+
107
+ // 5. Create factory tokens for dynamic configuration
108
+ const DYNAMIC_DB_CONFIG = InjectionToken.factory(DB_CONFIG_TOKEN, async () => {
109
+ const env = process.env.NODE_ENV || 'development'
110
+
111
+ if (env === 'production') {
112
+ return {
113
+ host: 'prod-db.example.com',
114
+ port: 5432,
115
+ database: 'production',
116
+ username: 'prod_user',
117
+ password: 'prod_password',
118
+ }
119
+ } else {
120
+ return {
121
+ host: 'localhost',
122
+ port: 5432,
123
+ database: 'development',
124
+ username: 'dev_user',
125
+ password: 'dev_password',
126
+ }
127
+ }
128
+ })
129
+
130
+ const DYNAMIC_EMAIL_CONFIG = InjectionToken.factory(
131
+ EMAIL_CONFIG_TOKEN,
132
+ async () => {
133
+ const env = process.env.NODE_ENV || 'development'
134
+
135
+ if (env === 'production') {
136
+ return {
137
+ provider: 'sendgrid' as const,
138
+ apiKey: 'sg.prod_key',
139
+ fromEmail: 'noreply@example.com',
140
+ }
141
+ } else {
142
+ return {
143
+ provider: 'smtp' as const,
144
+ apiKey: 'smtp_dev_key',
145
+ fromEmail: 'dev@example.com',
146
+ }
147
+ }
148
+ },
149
+ )
150
+
151
+ // 6. Usage examples
152
+ async function demonstrateBoundTokens() {
153
+ console.log('=== Bound Tokens Example ===\n')
154
+
155
+ // Use production configuration
156
+ const prodDbConfig = await container.get(PRODUCTION_DB_CONFIG)
157
+ console.log('Production DB:', prodDbConfig.getConnectionString())
158
+
159
+ const prodEmailConfig = await container.get(PRODUCTION_EMAIL_CONFIG)
160
+ console.log('Production Email:', prodEmailConfig.getProvider())
161
+
162
+ // Use development configuration
163
+ const devDbConfig = await container.get(DEVELOPMENT_DB_CONFIG)
164
+ console.log('Development DB:', devDbConfig.getConnectionString())
165
+
166
+ const devEmailConfig = await container.get(DEVELOPMENT_EMAIL_CONFIG)
167
+ console.log('Development Email:', devEmailConfig.getProvider())
168
+ }
169
+
170
+ async function demonstrateFactoryTokens() {
171
+ console.log('\n=== Factory Tokens Example ===\n')
172
+
173
+ // Use dynamic configuration based on environment
174
+ const dbConfig = await container.get(DYNAMIC_DB_CONFIG)
175
+ console.log('Dynamic DB:', dbConfig.getConnectionString())
176
+
177
+ const emailConfig = await container.get(DYNAMIC_EMAIL_CONFIG)
178
+ console.log('Dynamic Email:', emailConfig.getProvider())
179
+ }
180
+
181
+ async function demonstrateDirectTokens() {
182
+ console.log('\n=== Direct Tokens Example ===\n')
183
+
184
+ // Use tokens directly with configuration
185
+ const dbConfig = await container.get(DB_CONFIG_TOKEN, {
186
+ host: 'custom-db.example.com',
187
+ port: 5432,
188
+ database: 'custom',
189
+ username: 'custom_user',
190
+ password: 'custom_password',
191
+ })
192
+ console.log('Custom DB:', dbConfig.getConnectionString())
193
+
194
+ const emailConfig = await container.get(EMAIL_CONFIG_TOKEN, {
195
+ provider: 'ses',
196
+ apiKey: 'aws_ses_key',
197
+ fromEmail: 'custom@example.com',
198
+ })
199
+ console.log('Custom Email:', emailConfig.getProvider())
200
+ }
201
+
202
+ // Main function
203
+ async function main() {
204
+ await demonstrateBoundTokens()
205
+ await demonstrateFactoryTokens()
206
+ await demonstrateDirectTokens()
207
+ }
208
+
209
+ // Run the example
210
+ if (require.main === module) {
211
+ main().catch(console.error)
212
+ }
213
+
214
+ export {
215
+ DB_CONFIG_TOKEN,
216
+ EMAIL_CONFIG_TOKEN,
217
+ PRODUCTION_DB_CONFIG,
218
+ DEVELOPMENT_DB_CONFIG,
219
+ PRODUCTION_EMAIL_CONFIG,
220
+ DEVELOPMENT_EMAIL_CONFIG,
221
+ DYNAMIC_DB_CONFIG,
222
+ DYNAMIC_EMAIL_CONFIG,
223
+ DatabaseConfigService,
224
+ EmailConfigService,
225
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Example demonstrating the Request scope functionality in Navios DI.
3
+ *
4
+ * This example shows how to use request-scoped services for web applications
5
+ * where you need services that are shared within a request but isolated between requests.
6
+ */
7
+
8
+ import { Container } from '../../src/container.mjs'
9
+ import { Injectable } from '../../src/decorators/injectable.decorator.mjs'
10
+ import { InjectableScope } from '../../src/enums/index.mjs'
11
+ import { inject } from '../../src/injector.mjs'
12
+
13
+ // ============================================================================
14
+ // EXAMPLE SERVICES
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Singleton service - shared across the entire application
19
+ */
20
+ @Injectable({ scope: InjectableScope.Singleton })
21
+ class DatabaseService {
22
+ private connectionCount = 0
23
+
24
+ async getConnection() {
25
+ this.connectionCount++
26
+ console.log(`[DatabaseService] Created connection #${this.connectionCount}`)
27
+ return {
28
+ id: this.connectionCount,
29
+ connected: true,
30
+ createdAt: new Date(),
31
+ }
32
+ }
33
+
34
+ getConnectionCount() {
35
+ return this.connectionCount
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Request-scoped service - shared within a request, isolated between requests
41
+ */
42
+ @Injectable({ scope: InjectableScope.Request })
43
+ class UserContext {
44
+ public readonly requestId: string
45
+ public readonly userId: string
46
+ public readonly sessionId: string
47
+ public readonly startTime: number
48
+ private readonly database = inject(DatabaseService)
49
+
50
+ constructor({ userId, sessionId }: { userId: string; sessionId: string }) {
51
+ this.requestId = Math.random().toString(36).substring(2, 15)
52
+ this.userId = userId
53
+ this.sessionId = sessionId
54
+ this.startTime = Date.now()
55
+
56
+ console.log(
57
+ `[UserContext] Created for user ${userId} in request ${this.requestId}`,
58
+ )
59
+ }
60
+
61
+ async getDatabaseConnection() {
62
+ const db = await this.database
63
+ return await db.getConnection()
64
+ }
65
+
66
+ getRequestDuration() {
67
+ return Date.now() - this.startTime
68
+ }
69
+
70
+ async onServiceDestroy() {
71
+ console.log(
72
+ `[UserContext] Destroying context for user ${this.userId} (request ${this.requestId})`,
73
+ )
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Request-scoped service that depends on UserContext
79
+ */
80
+ @Injectable({ scope: InjectableScope.Request })
81
+ class OrderService {
82
+ private readonly userContext = inject(UserContext)
83
+ private orders: string[] = []
84
+
85
+ async createOrder(productName: string) {
86
+ const userCtx = await this.userContext
87
+ const orderId = `order_${Math.random().toString(36).substring(2, 15)}`
88
+
89
+ this.orders.push(orderId)
90
+ console.log(
91
+ `[OrderService] Created order ${orderId} for user ${userCtx.userId}`,
92
+ )
93
+
94
+ return {
95
+ orderId,
96
+ userId: userCtx.userId,
97
+ productName,
98
+ requestId: userCtx.requestId,
99
+ }
100
+ }
101
+
102
+ getOrders() {
103
+ return [...this.orders]
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Transient service - new instance for each injection
109
+ */
110
+ @Injectable({ scope: InjectableScope.Transient })
111
+ class LoggerService {
112
+ private readonly logId = Math.random().toString(36).substring(2, 15)
113
+
114
+ log(message: string) {
115
+ console.log(`[LoggerService:${this.logId}] ${message}`)
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Service that uses all scopes
121
+ */
122
+ @Injectable({ scope: InjectableScope.Singleton })
123
+ class RequestHandler {
124
+ private readonly logger = inject(LoggerService)
125
+ private readonly userContext = inject(UserContext)
126
+ private readonly orderService = inject(OrderService)
127
+
128
+ async handleRequest(userId: string, sessionId: string) {
129
+ const logger = await this.logger
130
+ logger.log(`Handling request for user ${userId}`)
131
+
132
+ const userCtx = await this.userContext
133
+ logger.log(`User context request ID: ${userCtx.requestId}`)
134
+
135
+ const orderSvc = await this.orderService
136
+ const order = await orderSvc.createOrder('Sample Product')
137
+
138
+ logger.log(`Created order: ${order.orderId}`)
139
+ logger.log(`Request duration: ${userCtx.getRequestDuration()}ms`)
140
+
141
+ return {
142
+ userId: userCtx.userId,
143
+ requestId: userCtx.requestId,
144
+ orderId: order.orderId,
145
+ duration: userCtx.getRequestDuration(),
146
+ }
147
+ }
148
+ }
149
+
150
+ // ============================================================================
151
+ // EXAMPLE USAGE
152
+ // ============================================================================
153
+
154
+ async function demonstrateRequestScope() {
155
+ console.log('šŸš€ Request Scope Example\n')
156
+
157
+ const container = new Container()
158
+
159
+ // Simulate multiple requests
160
+ const requests = [
161
+ { userId: 'user1', sessionId: 'session1' },
162
+ { userId: 'user2', sessionId: 'session2' },
163
+ { userId: 'user1', sessionId: 'session3' }, // Same user, different session
164
+ ]
165
+
166
+ for (let i = 0; i < requests.length; i++) {
167
+ const { userId, sessionId } = requests[i]
168
+
169
+ console.log(
170
+ `\nšŸ“ Processing Request ${i + 1}: User ${userId}, Session ${sessionId}`,
171
+ )
172
+ console.log('─'.repeat(50))
173
+
174
+ // Begin request context
175
+ const requestId = `req_${i + 1}`
176
+ container.beginRequest(requestId, { userId, sessionId })
177
+
178
+ // Handle the request
179
+ const handler = await container.get(RequestHandler)
180
+ const result = await handler.handleRequest(userId, sessionId)
181
+
182
+ console.log(`āœ… Request completed:`, result)
183
+
184
+ // End request context (cleans up request-scoped instances)
185
+ await container.endRequest(requestId)
186
+
187
+ console.log('─'.repeat(50))
188
+ }
189
+
190
+ // Show that singleton services persist across requests
191
+ const dbService = await container.get(DatabaseService)
192
+ console.log(
193
+ `\nšŸ“Š Total database connections created: ${dbService.getConnectionCount()}`,
194
+ )
195
+
196
+ console.log('\n✨ Example completed!')
197
+ }
198
+
199
+ // ============================================================================
200
+ // PERFORMANCE COMPARISON
201
+ // ============================================================================
202
+
203
+ async function demonstratePerformanceBenefits() {
204
+ console.log('\n⚔ Performance Comparison\n')
205
+
206
+ const container = new Container()
207
+
208
+ // Without pre-preparation
209
+ console.log('🐌 Without pre-preparation:')
210
+ const start1 = Date.now()
211
+
212
+ container.beginRequest('perf-test-1')
213
+ const handler1 = await container.get(RequestHandler)
214
+ await handler1.handleRequest('user1', 'session1')
215
+ await container.endRequest('perf-test-1')
216
+
217
+ const time1 = Date.now() - start1
218
+ console.log(` Time: ${time1}ms`)
219
+
220
+ // With pre-preparation
221
+ console.log('šŸš€ With pre-preparation:')
222
+ const start2 = Date.now()
223
+
224
+ container.beginRequest('perf-test-2')
225
+ const handler2 = await container.get(RequestHandler)
226
+ await handler2.handleRequest('user1', 'session1')
227
+ await container.endRequest('perf-test-2')
228
+
229
+ const time2 = Date.now() - start2
230
+ console.log(` Time: ${time2}ms`)
231
+ console.log(
232
+ ` Improvement: ${(((time1 - time2) / time1) * 100).toFixed(1)}% faster`,
233
+ )
234
+ }
235
+
236
+ // ============================================================================
237
+ // MAIN EXECUTION
238
+ // ============================================================================
239
+
240
+ if (import.meta.url === `file://${process.argv[1]}`) {
241
+ demonstrateRequestScope()
242
+ .then(() => demonstratePerformanceBenefits())
243
+ .catch(console.error)
244
+ }
245
+
246
+ export {
247
+ DatabaseService,
248
+ UserContext,
249
+ OrderService,
250
+ LoggerService,
251
+ RequestHandler,
252
+ demonstrateRequestScope,
253
+ demonstratePerformanceBenefits,
254
+ }