@navios/di 0.4.2 → 0.5.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 +211 -1
- package/coverage/clover.xml +1912 -1277
- package/coverage/coverage-final.json +37 -28
- package/coverage/docs/examples/basic-usage.mts.html +1 -1
- package/coverage/docs/examples/factory-pattern.mts.html +1 -1
- package/coverage/docs/examples/index.html +1 -1
- package/coverage/docs/examples/injection-tokens.mts.html +1 -1
- package/coverage/docs/examples/request-scope-example.mts.html +1 -1
- package/coverage/docs/examples/service-lifecycle.mts.html +1 -1
- package/coverage/index.html +71 -41
- package/coverage/lib/_tsup-dts-rollup.d.mts.html +682 -43
- package/coverage/lib/index.d.mts.html +7 -4
- package/coverage/lib/index.html +5 -5
- package/coverage/lib/testing/index.d.mts.html +91 -0
- package/coverage/lib/testing/index.html +116 -0
- package/coverage/src/base-instance-holder-manager.mts.html +589 -0
- package/coverage/src/container.mts.html +257 -74
- package/coverage/src/decorators/factory.decorator.mts.html +1 -1
- package/coverage/src/decorators/index.html +1 -1
- package/coverage/src/decorators/index.mts.html +1 -1
- package/coverage/src/decorators/injectable.decorator.mts.html +20 -20
- package/coverage/src/enums/index.html +1 -1
- package/coverage/src/enums/index.mts.html +1 -1
- package/coverage/src/enums/injectable-scope.enum.mts.html +1 -1
- package/coverage/src/enums/injectable-type.enum.mts.html +1 -1
- package/coverage/src/errors/di-error.mts.html +292 -0
- package/coverage/src/errors/errors.enum.mts.html +30 -21
- package/coverage/src/errors/factory-not-found.mts.html +31 -22
- package/coverage/src/errors/factory-token-not-resolved.mts.html +29 -26
- package/coverage/src/errors/index.html +56 -41
- package/coverage/src/errors/index.mts.html +15 -9
- package/coverage/src/errors/instance-destroying.mts.html +31 -22
- package/coverage/src/errors/instance-expired.mts.html +31 -22
- package/coverage/src/errors/instance-not-found.mts.html +31 -22
- package/coverage/src/errors/unknown-error.mts.html +31 -43
- package/coverage/src/event-emitter.mts.html +14 -14
- package/coverage/src/factory-context.mts.html +1 -1
- package/coverage/src/index.html +121 -46
- package/coverage/src/index.mts.html +7 -4
- package/coverage/src/injection-token.mts.html +28 -28
- package/coverage/src/injector.mts.html +1 -1
- package/coverage/src/instance-resolver.mts.html +1762 -0
- package/coverage/src/interfaces/factory.interface.mts.html +1 -1
- package/coverage/src/interfaces/index.html +1 -1
- package/coverage/src/interfaces/index.mts.html +1 -1
- package/coverage/src/interfaces/on-service-destroy.interface.mts.html +1 -1
- package/coverage/src/interfaces/on-service-init.interface.mts.html +1 -1
- package/coverage/src/registry.mts.html +28 -28
- package/coverage/src/request-context-holder.mts.html +183 -102
- package/coverage/src/request-context-manager.mts.html +532 -0
- package/coverage/src/service-instantiator.mts.html +49 -49
- package/coverage/src/service-invalidator.mts.html +1372 -0
- package/coverage/src/service-locator-event-bus.mts.html +48 -48
- package/coverage/src/service-locator-instance-holder.mts.html +2 -14
- package/coverage/src/service-locator-manager.mts.html +71 -335
- package/coverage/src/service-locator.mts.html +240 -2328
- package/coverage/src/symbols/index.html +1 -1
- package/coverage/src/symbols/index.mts.html +1 -1
- package/coverage/src/symbols/injectable-token.mts.html +1 -1
- package/coverage/src/testing/index.html +131 -0
- package/coverage/src/testing/index.mts.html +88 -0
- package/coverage/src/testing/test-container.mts.html +445 -0
- package/coverage/src/token-processor.mts.html +607 -0
- package/coverage/src/utils/defer.mts.html +28 -214
- package/coverage/src/utils/get-injectable-token.mts.html +7 -7
- package/coverage/src/utils/get-injectors.mts.html +99 -99
- package/coverage/src/utils/index.html +15 -15
- package/coverage/src/utils/index.mts.html +4 -7
- package/coverage/src/utils/types.mts.html +1 -1
- package/docs/injectable.md +51 -11
- package/docs/scopes.md +63 -29
- package/lib/_tsup-dts-rollup.d.mts +376 -213
- package/lib/_tsup-dts-rollup.d.ts +376 -213
- package/lib/{chunk-3NLYPYBY.mjs → chunk-44F3LXW5.mjs} +1021 -605
- package/lib/chunk-44F3LXW5.mjs.map +1 -0
- package/lib/index.d.mts +6 -4
- package/lib/index.d.ts +6 -4
- package/lib/index.js +1192 -776
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +2 -2
- package/lib/testing/index.js +1258 -840
- package/lib/testing/index.js.map +1 -1
- package/lib/testing/index.mjs +1 -1
- package/package.json +1 -1
- package/src/__tests__/container.spec.mts +47 -13
- package/src/__tests__/errors.spec.mts +53 -27
- package/src/__tests__/injectable.spec.mts +73 -0
- package/src/__tests__/request-scope.spec.mts +0 -2
- package/src/__tests__/service-locator-manager.spec.mts +12 -82
- package/src/__tests__/service-locator.spec.mts +1009 -1
- package/src/__type-tests__/inject.spec-d.mts +30 -7
- package/src/__type-tests__/injectable.spec-d.mts +76 -37
- package/src/base-instance-holder-manager.mts +2 -9
- package/src/container.mts +61 -9
- package/src/decorators/injectable.decorator.mts +29 -5
- package/src/errors/di-error.mts +69 -0
- package/src/errors/index.mts +9 -7
- package/src/injection-token.mts +1 -0
- package/src/injector.mts +2 -0
- package/src/instance-resolver.mts +559 -0
- package/src/request-context-holder.mts +0 -2
- package/src/request-context-manager.mts +149 -0
- package/src/service-invalidator.mts +429 -0
- package/src/service-locator-instance-holder.mts +0 -4
- package/src/service-locator-manager.mts +10 -40
- package/src/service-locator.mts +86 -782
- package/src/token-processor.mts +174 -0
- package/src/utils/get-injectors.mts +161 -24
- package/src/utils/index.mts +0 -1
- package/src/utils/types.mts +12 -8
- package/lib/chunk-3NLYPYBY.mjs.map +0 -1
- package/src/__tests__/defer.spec.mts +0 -166
- package/src/errors/errors.enum.mts +0 -8
- package/src/errors/factory-not-found.mts +0 -8
- package/src/errors/factory-token-not-resolved.mts +0 -10
- package/src/errors/instance-destroying.mts +0 -8
- package/src/errors/instance-expired.mts +0 -8
- package/src/errors/instance-not-found.mts +0 -8
- package/src/errors/unknown-error.mts +0 -15
- package/src/utils/defer.mts +0 -73
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { z } from 'zod/v4'
|
|
2
3
|
|
|
4
|
+
import type { OnServiceDestroy } from '../index.mjs'
|
|
5
|
+
|
|
6
|
+
import { Injectable } from '../decorators/injectable.decorator.mjs'
|
|
7
|
+
import { InjectableScope } from '../enums/index.mjs'
|
|
8
|
+
import { getInjectableToken } from '../index.mjs'
|
|
3
9
|
import { InjectionToken } from '../injection-token.mjs'
|
|
10
|
+
import { asyncInject, inject } from '../injector.mjs'
|
|
11
|
+
import { globalRegistry } from '../registry.mjs'
|
|
4
12
|
import { ServiceLocator } from '../service-locator.mjs'
|
|
5
13
|
|
|
6
14
|
describe('ServiceLocator', () => {
|
|
@@ -32,4 +40,1004 @@ describe('ServiceLocator', () => {
|
|
|
32
40
|
expect(identifier).toBe(`test(${token.id}):test=fn_test(0)`)
|
|
33
41
|
})
|
|
34
42
|
})
|
|
43
|
+
|
|
44
|
+
describe('clearAll', () => {
|
|
45
|
+
let serviceLocator: ServiceLocator
|
|
46
|
+
let mockLogger: Console
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
mockLogger = {
|
|
50
|
+
log: vi.fn(),
|
|
51
|
+
warn: vi.fn(),
|
|
52
|
+
error: vi.fn(),
|
|
53
|
+
debug: vi.fn(),
|
|
54
|
+
trace: vi.fn(),
|
|
55
|
+
} as any
|
|
56
|
+
|
|
57
|
+
serviceLocator = new ServiceLocator(globalRegistry, mockLogger)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should clear all services gracefully', async () => {
|
|
61
|
+
// Create Injectable services
|
|
62
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
63
|
+
class ServiceA {
|
|
64
|
+
name = 'ServiceA'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
68
|
+
class ServiceB {
|
|
69
|
+
name = 'ServiceB'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
73
|
+
class ServiceC {
|
|
74
|
+
name = 'ServiceC'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create instances
|
|
78
|
+
await serviceLocator.getInstance(ServiceA)
|
|
79
|
+
await serviceLocator.getInstance(ServiceB)
|
|
80
|
+
await serviceLocator.getInstance(ServiceC)
|
|
81
|
+
|
|
82
|
+
// Verify services exist
|
|
83
|
+
expect(serviceLocator.getManager().size()).toBe(3)
|
|
84
|
+
|
|
85
|
+
// Clear all services
|
|
86
|
+
await serviceLocator.clearAll()
|
|
87
|
+
|
|
88
|
+
// Verify all services are cleared
|
|
89
|
+
expect(serviceLocator.getManager().size()).toBe(0)
|
|
90
|
+
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
91
|
+
'[ServiceInvalidator] Graceful clearing completed',
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should handle empty service locator', async () => {
|
|
96
|
+
await serviceLocator.clearAll()
|
|
97
|
+
|
|
98
|
+
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
99
|
+
'[ServiceInvalidator] No singleton services to clear',
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should clear service from a request context', async () => {
|
|
104
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
105
|
+
class ServiceA {
|
|
106
|
+
name = 'ServiceA'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
110
|
+
class ServiceB {
|
|
111
|
+
serviceA = inject(ServiceA)
|
|
112
|
+
name = 'ServiceB'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const requestId = 'test-request'
|
|
116
|
+
serviceLocator.beginRequest(requestId)
|
|
117
|
+
const [error, serviceB] = await serviceLocator.getInstance(ServiceB)
|
|
118
|
+
expect(error).toBeUndefined()
|
|
119
|
+
expect(serviceB).toBeDefined()
|
|
120
|
+
|
|
121
|
+
await serviceLocator.invalidate(getInjectableToken(ServiceA).toString())
|
|
122
|
+
expect(serviceLocator.getManager().size()).toBe(0)
|
|
123
|
+
await serviceLocator.clearAll()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should clear request contexts when requested', async () => {
|
|
127
|
+
// Create a request context
|
|
128
|
+
const requestId = 'test-request'
|
|
129
|
+
serviceLocator.beginRequest(requestId)
|
|
130
|
+
|
|
131
|
+
// Create Injectable service with request scope
|
|
132
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
133
|
+
class TestService {
|
|
134
|
+
name = 'TestService'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await serviceLocator.getInstance(TestService)
|
|
138
|
+
|
|
139
|
+
// Verify request context exists before clearing
|
|
140
|
+
expect(serviceLocator.getCurrentRequestContext()).not.toBeNull()
|
|
141
|
+
|
|
142
|
+
// Clear all services including request contexts
|
|
143
|
+
await serviceLocator.clearAll({ clearRequestContexts: true })
|
|
144
|
+
|
|
145
|
+
// Verify request context is cleared
|
|
146
|
+
expect(serviceLocator.getCurrentRequestContext()).toBeNull()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should skip clearing request contexts when disabled', async () => {
|
|
150
|
+
// Create a request context
|
|
151
|
+
const requestId = 'test-request'
|
|
152
|
+
serviceLocator.beginRequest(requestId)
|
|
153
|
+
|
|
154
|
+
// Clear all services but skip request contexts
|
|
155
|
+
await serviceLocator.clearAll({ clearRequestContexts: false })
|
|
156
|
+
|
|
157
|
+
// Verify request context is still there
|
|
158
|
+
expect(serviceLocator.getCurrentRequestContext()).not.toBeNull()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should handle services with dependencies correctly', async () => {
|
|
162
|
+
// Create Injectable services
|
|
163
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
164
|
+
class ServiceA {
|
|
165
|
+
name = 'ServiceA'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
169
|
+
class ServiceB {
|
|
170
|
+
serviceA = inject(ServiceA)
|
|
171
|
+
name = 'ServiceB'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Create instances
|
|
175
|
+
await serviceLocator.getInstance(ServiceB)
|
|
176
|
+
|
|
177
|
+
// Clear all services
|
|
178
|
+
await serviceLocator.clearAll()
|
|
179
|
+
|
|
180
|
+
// Verify all services are cleared
|
|
181
|
+
expect(serviceLocator.getManager().size()).toBe(0)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should respect maxRounds option', async () => {
|
|
185
|
+
// Create Injectable service
|
|
186
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
187
|
+
class TestService {
|
|
188
|
+
name = 'TestService'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await serviceLocator.getInstance(TestService)
|
|
192
|
+
|
|
193
|
+
// Clear with a very low maxRounds to test the limit
|
|
194
|
+
await serviceLocator.clearAll({ maxRounds: 1 })
|
|
195
|
+
|
|
196
|
+
// Should still clear the service
|
|
197
|
+
expect(serviceLocator.getManager().size()).toBe(0)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should clear services with dependencies in correct order', async () => {
|
|
201
|
+
// Create services with dependencies
|
|
202
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
203
|
+
class DatabaseService {
|
|
204
|
+
name = 'DatabaseService'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
208
|
+
class UserService {
|
|
209
|
+
public database = inject(DatabaseService)
|
|
210
|
+
name = 'UserService'
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
214
|
+
class AuthService {
|
|
215
|
+
public userService = inject(UserService)
|
|
216
|
+
name = 'AuthService'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Create instances (this will establish dependencies)
|
|
220
|
+
await serviceLocator.getInstance(AuthService)
|
|
221
|
+
await serviceLocator.getInstance(UserService)
|
|
222
|
+
await serviceLocator.getInstance(DatabaseService)
|
|
223
|
+
|
|
224
|
+
// Verify services exist
|
|
225
|
+
expect(serviceLocator.getManager().size()).toBe(3)
|
|
226
|
+
|
|
227
|
+
// Clear all services - should clear in dependency order
|
|
228
|
+
await serviceLocator.clearAll()
|
|
229
|
+
|
|
230
|
+
// Verify all services are cleared
|
|
231
|
+
expect(serviceLocator.getManager().size()).toBe(0)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should handle services with destroy listeners', async () => {
|
|
235
|
+
let destroyCalled = false
|
|
236
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
237
|
+
class TestService implements OnServiceDestroy {
|
|
238
|
+
name = 'TestService'
|
|
239
|
+
|
|
240
|
+
constructor() {
|
|
241
|
+
// Simulate a service that needs cleanup
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async onServiceDestroy() {
|
|
245
|
+
destroyCalled = true
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await serviceLocator.getInstance(TestService)
|
|
250
|
+
|
|
251
|
+
// Clear all services
|
|
252
|
+
await serviceLocator.clearAll()
|
|
253
|
+
|
|
254
|
+
// Verify all services are cleared
|
|
255
|
+
expect(serviceLocator.getManager().size()).toBe(0)
|
|
256
|
+
expect(destroyCalled).toBe(true)
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe('Mixed Scope Services', () => {
|
|
261
|
+
let serviceLocator: ServiceLocator
|
|
262
|
+
let mockLogger: Console
|
|
263
|
+
|
|
264
|
+
beforeEach(() => {
|
|
265
|
+
mockLogger = {
|
|
266
|
+
log: vi.fn(),
|
|
267
|
+
warn: vi.fn(),
|
|
268
|
+
error: vi.fn(),
|
|
269
|
+
debug: vi.fn(),
|
|
270
|
+
trace: vi.fn(),
|
|
271
|
+
} as any
|
|
272
|
+
|
|
273
|
+
serviceLocator = new ServiceLocator(globalRegistry, mockLogger)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
describe('Services with dependencies across different scopes', () => {
|
|
277
|
+
it('should handle Singleton service depending on Transient service', async () => {
|
|
278
|
+
// Create Transient service
|
|
279
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
280
|
+
class TransientService {
|
|
281
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
282
|
+
name = 'TransientService'
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Create Singleton service that depends on Transient service
|
|
286
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
287
|
+
class SingletonService {
|
|
288
|
+
transientService = asyncInject(TransientService)
|
|
289
|
+
name = 'SingletonService'
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Get instances
|
|
293
|
+
const [error1, singleton1] =
|
|
294
|
+
await serviceLocator.getInstance(SingletonService)
|
|
295
|
+
const [error2, singleton2] =
|
|
296
|
+
await serviceLocator.getInstance(SingletonService)
|
|
297
|
+
|
|
298
|
+
expect(error1).toBeUndefined()
|
|
299
|
+
expect(error2).toBeUndefined()
|
|
300
|
+
expect(singleton1).toBe(singleton2) // Same singleton instance
|
|
301
|
+
|
|
302
|
+
// Get the actual transient service instances (asyncInject returns Promises)
|
|
303
|
+
const transient1 = await singleton1.transientService
|
|
304
|
+
const transient2 = await singleton2.transientService
|
|
305
|
+
|
|
306
|
+
// Note: Since Singleton is created once, both references point to the same Transient instance
|
|
307
|
+
// This is expected behavior - the Transient service is created once during Singleton instantiation
|
|
308
|
+
expect(transient1).toBe(transient2) // Same transient instance (created during singleton instantiation)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should handle Request service depending on Singleton service', async () => {
|
|
312
|
+
// Create Singleton service
|
|
313
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
314
|
+
class SingletonService {
|
|
315
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
316
|
+
name = 'SingletonService'
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Create Request service that depends on Singleton service
|
|
320
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
321
|
+
class RequestService {
|
|
322
|
+
singletonService = inject(SingletonService)
|
|
323
|
+
name = 'RequestService'
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Begin request context
|
|
327
|
+
const requestId = 'test-request-1'
|
|
328
|
+
serviceLocator.beginRequest(requestId)
|
|
329
|
+
|
|
330
|
+
// Get instances within the same request
|
|
331
|
+
const [error1, request1] =
|
|
332
|
+
await serviceLocator.getInstance(RequestService)
|
|
333
|
+
const [error2, request2] =
|
|
334
|
+
await serviceLocator.getInstance(RequestService)
|
|
335
|
+
|
|
336
|
+
expect(error1).toBeUndefined()
|
|
337
|
+
expect(error2).toBeUndefined()
|
|
338
|
+
expect(request1).toBe(request2) // Same request-scoped instance
|
|
339
|
+
expect(request1.singletonService).toBe(request2.singletonService) // Same singleton instance
|
|
340
|
+
|
|
341
|
+
// End request and start new one
|
|
342
|
+
await serviceLocator.endRequest(requestId)
|
|
343
|
+
const newRequestId = 'test-request-2'
|
|
344
|
+
serviceLocator.beginRequest(newRequestId)
|
|
345
|
+
|
|
346
|
+
// Get instance in new request
|
|
347
|
+
const [error3, request3] =
|
|
348
|
+
await serviceLocator.getInstance(RequestService)
|
|
349
|
+
|
|
350
|
+
expect(error3).toBeUndefined()
|
|
351
|
+
expect(request1).not.toBe(request3) // Different request-scoped instances
|
|
352
|
+
expect(request1.singletonService).toBe(request3.singletonService) // Same singleton instance
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('should handle Transient service depending on Request service', async () => {
|
|
356
|
+
// Create Request service
|
|
357
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
358
|
+
class RequestService {
|
|
359
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
360
|
+
name = 'RequestService'
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Create Transient service that depends on Request service
|
|
364
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
365
|
+
class TransientService {
|
|
366
|
+
requestService = inject(RequestService)
|
|
367
|
+
name = 'TransientService'
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Begin request context
|
|
371
|
+
const requestId = 'test-request'
|
|
372
|
+
serviceLocator.beginRequest(requestId)
|
|
373
|
+
|
|
374
|
+
// Get multiple transient instances
|
|
375
|
+
const [error1, transient1] =
|
|
376
|
+
await serviceLocator.getInstance(TransientService)
|
|
377
|
+
const [error2, transient2] =
|
|
378
|
+
await serviceLocator.getInstance(TransientService)
|
|
379
|
+
|
|
380
|
+
expect(error1).toBeUndefined()
|
|
381
|
+
expect(error2).toBeUndefined()
|
|
382
|
+
expect(transient1).not.toBe(transient2) // Different transient instances
|
|
383
|
+
|
|
384
|
+
// Get the actual request service instances (asyncInject returns Promises)
|
|
385
|
+
const requestService1 = transient1.requestService
|
|
386
|
+
const requestService2 = transient2.requestService
|
|
387
|
+
expect(requestService1).toBe(requestService2) // Same request-scoped instance
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('should handle complex dependency chain across all scopes', async () => {
|
|
391
|
+
// Create services with different scopes
|
|
392
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
393
|
+
class DatabaseService {
|
|
394
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
395
|
+
name = 'DatabaseService'
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
399
|
+
class UserSessionService {
|
|
400
|
+
database = inject(DatabaseService)
|
|
401
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
402
|
+
name = 'UserSessionService'
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
406
|
+
class UserActionService {
|
|
407
|
+
session = inject(UserSessionService)
|
|
408
|
+
database = inject(DatabaseService)
|
|
409
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
410
|
+
name = 'UserActionService'
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
414
|
+
class UserManagerService {
|
|
415
|
+
database = inject(DatabaseService)
|
|
416
|
+
name = 'UserManagerService'
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Begin request context
|
|
420
|
+
const requestId = 'complex-request'
|
|
421
|
+
serviceLocator.beginRequest(requestId)
|
|
422
|
+
|
|
423
|
+
// Get instances
|
|
424
|
+
const [error1, action1] =
|
|
425
|
+
await serviceLocator.getInstance(UserActionService)
|
|
426
|
+
const [error2, action2] =
|
|
427
|
+
await serviceLocator.getInstance(UserActionService)
|
|
428
|
+
const [error3, manager] =
|
|
429
|
+
await serviceLocator.getInstance(UserManagerService)
|
|
430
|
+
|
|
431
|
+
expect(error1).toBeUndefined()
|
|
432
|
+
expect(error2).toBeUndefined()
|
|
433
|
+
expect(error3).toBeUndefined()
|
|
434
|
+
|
|
435
|
+
// Verify instances are created
|
|
436
|
+
expect(action1).toBeDefined()
|
|
437
|
+
expect(action2).toBeDefined()
|
|
438
|
+
expect(manager).toBeDefined()
|
|
439
|
+
|
|
440
|
+
// Verify scope behavior - check if dependencies are properly injected
|
|
441
|
+
expect(action1).not.toBe(action2) // Different transient instances
|
|
442
|
+
|
|
443
|
+
// Get the actual dependency instances (asyncInject returns Promises)
|
|
444
|
+
const action1Database = action1.database
|
|
445
|
+
const action2Database = action2.database
|
|
446
|
+
const action1Session = action1.session
|
|
447
|
+
const action2Session = action2.session
|
|
448
|
+
|
|
449
|
+
expect(action1Database).toBe(action2Database) // Same singleton instance
|
|
450
|
+
expect(action1Database).toBe(manager.database) // Same singleton instance
|
|
451
|
+
|
|
452
|
+
// Check if session dependency is properly injected
|
|
453
|
+
expect(action1Session).toBe(action2Session) // Same request-scoped instance
|
|
454
|
+
expect(action1Session.database).toBe(action1Database) // Same singleton instance
|
|
455
|
+
|
|
456
|
+
// End request and start new one
|
|
457
|
+
await serviceLocator.endRequest(requestId)
|
|
458
|
+
const newRequestId = 'complex-request-2'
|
|
459
|
+
serviceLocator.beginRequest(newRequestId)
|
|
460
|
+
|
|
461
|
+
// Get instances in new request
|
|
462
|
+
const [error4, action3] =
|
|
463
|
+
await serviceLocator.getInstance(UserActionService)
|
|
464
|
+
const [error5, manager2] =
|
|
465
|
+
await serviceLocator.getInstance(UserManagerService)
|
|
466
|
+
|
|
467
|
+
expect(error4).toBeUndefined()
|
|
468
|
+
expect(error5).toBeUndefined()
|
|
469
|
+
|
|
470
|
+
// Verify scope behavior across requests
|
|
471
|
+
expect(action1).not.toBe(action3) // Different transient instances
|
|
472
|
+
|
|
473
|
+
// Get the actual dependency instances for the new request
|
|
474
|
+
const action3Database = action3.database
|
|
475
|
+
const action3Session = action3.session
|
|
476
|
+
|
|
477
|
+
expect(action1Database).toBe(action3Database) // Same singleton instance
|
|
478
|
+
expect(manager).toBe(manager2) // Same singleton instance
|
|
479
|
+
|
|
480
|
+
// Check if session dependency is properly injected in new request
|
|
481
|
+
expect(action1Session).not.toBe(action3Session) // Different request-scoped instances
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
describe('Instance sharing and isolation', () => {
|
|
486
|
+
it('should isolate Request-scoped instances between different requests', async () => {
|
|
487
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
488
|
+
class RequestService {
|
|
489
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
490
|
+
name = 'RequestService'
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// First request
|
|
494
|
+
const requestId1 = 'request-1'
|
|
495
|
+
serviceLocator.beginRequest(requestId1)
|
|
496
|
+
const [error1, service1] =
|
|
497
|
+
await serviceLocator.getInstance(RequestService)
|
|
498
|
+
await serviceLocator.endRequest(requestId1)
|
|
499
|
+
|
|
500
|
+
// Second request
|
|
501
|
+
const requestId2 = 'request-2'
|
|
502
|
+
serviceLocator.beginRequest(requestId2)
|
|
503
|
+
const [error2, service2] =
|
|
504
|
+
await serviceLocator.getInstance(RequestService)
|
|
505
|
+
await serviceLocator.endRequest(requestId2)
|
|
506
|
+
|
|
507
|
+
expect(error1).toBeUndefined()
|
|
508
|
+
expect(error2).toBeUndefined()
|
|
509
|
+
expect(service1).not.toBe(service2) // Different instances
|
|
510
|
+
expect(service1.id).not.toBe(service2.id) // Different IDs
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('should share Singleton instances across requests', async () => {
|
|
514
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
515
|
+
class SingletonService {
|
|
516
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
517
|
+
name = 'SingletonService'
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// First request
|
|
521
|
+
const requestId1 = 'request-1'
|
|
522
|
+
serviceLocator.beginRequest(requestId1)
|
|
523
|
+
const [error1, service1] =
|
|
524
|
+
await serviceLocator.getInstance(SingletonService)
|
|
525
|
+
await serviceLocator.endRequest(requestId1)
|
|
526
|
+
|
|
527
|
+
// Second request
|
|
528
|
+
const requestId2 = 'request-2'
|
|
529
|
+
serviceLocator.beginRequest(requestId2)
|
|
530
|
+
const [error2, service2] =
|
|
531
|
+
await serviceLocator.getInstance(SingletonService)
|
|
532
|
+
await serviceLocator.endRequest(requestId2)
|
|
533
|
+
|
|
534
|
+
expect(error1).toBeUndefined()
|
|
535
|
+
expect(error2).toBeUndefined()
|
|
536
|
+
expect(service1).toBe(service2) // Same instance
|
|
537
|
+
expect(service1.id).toBe(service2.id) // Same ID
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
it('should create new Transient instances every time', async () => {
|
|
541
|
+
@Injectable({ scope: InjectableScope.Transient })
|
|
542
|
+
class TransientService {
|
|
543
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
544
|
+
name = 'TransientService'
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const requestId = 'request'
|
|
548
|
+
serviceLocator.beginRequest(requestId)
|
|
549
|
+
|
|
550
|
+
const [error1, service1] =
|
|
551
|
+
await serviceLocator.getInstance(TransientService)
|
|
552
|
+
const [error2, service2] =
|
|
553
|
+
await serviceLocator.getInstance(TransientService)
|
|
554
|
+
const [error3, service3] =
|
|
555
|
+
await serviceLocator.getInstance(TransientService)
|
|
556
|
+
|
|
557
|
+
expect(error1).toBeUndefined()
|
|
558
|
+
expect(error2).toBeUndefined()
|
|
559
|
+
expect(error3).toBeUndefined()
|
|
560
|
+
expect(service1).not.toBe(service2) // Different instances
|
|
561
|
+
expect(service1).not.toBe(service3) // Different instances
|
|
562
|
+
expect(service2).not.toBe(service3) // Different instances
|
|
563
|
+
expect(service1.id).not.toBe(service2.id) // Different IDs
|
|
564
|
+
expect(service1.id).not.toBe(service3.id) // Different IDs
|
|
565
|
+
expect(service2.id).not.toBe(service3.id) // Different IDs
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
describe('Request context management with mixed scopes', () => {
|
|
570
|
+
it('should properly clean up Request-scoped instances when ending request', async () => {
|
|
571
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
572
|
+
class RequestService {
|
|
573
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
574
|
+
name = 'RequestService'
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
578
|
+
class SingletonService {
|
|
579
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
580
|
+
name = 'SingletonService'
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const requestId = 'cleanup-test'
|
|
584
|
+
serviceLocator.beginRequest(requestId)
|
|
585
|
+
|
|
586
|
+
// Create instances
|
|
587
|
+
const [error1, _requestService] =
|
|
588
|
+
await serviceLocator.getInstance(RequestService)
|
|
589
|
+
const [error2, singletonService] =
|
|
590
|
+
await serviceLocator.getInstance(SingletonService)
|
|
591
|
+
|
|
592
|
+
expect(error1).toBeUndefined()
|
|
593
|
+
expect(error2).toBeUndefined()
|
|
594
|
+
|
|
595
|
+
// Verify request context exists
|
|
596
|
+
expect(serviceLocator.getCurrentRequestContext()).not.toBeNull()
|
|
597
|
+
expect(serviceLocator.getCurrentRequestContext()?.requestId).toBe(
|
|
598
|
+
requestId,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
// End request
|
|
602
|
+
await serviceLocator.endRequest(requestId)
|
|
603
|
+
|
|
604
|
+
// Verify request context is cleared
|
|
605
|
+
expect(serviceLocator.getCurrentRequestContext()).toBeNull()
|
|
606
|
+
|
|
607
|
+
// Singleton should still be available
|
|
608
|
+
const [error3, singletonService2] =
|
|
609
|
+
await serviceLocator.getInstance(SingletonService)
|
|
610
|
+
expect(error3).toBeUndefined()
|
|
611
|
+
expect(singletonService).toBe(singletonService2) // Same singleton instance
|
|
612
|
+
|
|
613
|
+
// Request service should not be available (no current request context)
|
|
614
|
+
const [error4] = await serviceLocator.getInstance(RequestService)
|
|
615
|
+
expect(error4).toBeDefined() // Should error because no request context
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('should handle nested request contexts with mixed scopes', async () => {
|
|
619
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
620
|
+
class RequestService {
|
|
621
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
622
|
+
name = 'RequestService'
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
626
|
+
class SingletonService {
|
|
627
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
628
|
+
name = 'SingletonService'
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// First request
|
|
632
|
+
const requestId1 = 'outer-request'
|
|
633
|
+
serviceLocator.beginRequest(requestId1)
|
|
634
|
+
const [error1, requestService1] =
|
|
635
|
+
await serviceLocator.getInstance(RequestService)
|
|
636
|
+
const [error2, singletonService1] =
|
|
637
|
+
await serviceLocator.getInstance(SingletonService)
|
|
638
|
+
|
|
639
|
+
// Second request (nested)
|
|
640
|
+
const requestId2 = 'inner-request'
|
|
641
|
+
serviceLocator.beginRequest(requestId2)
|
|
642
|
+
const [error3, requestService2] =
|
|
643
|
+
await serviceLocator.getInstance(RequestService)
|
|
644
|
+
const [error4, singletonService2] =
|
|
645
|
+
await serviceLocator.getInstance(SingletonService)
|
|
646
|
+
|
|
647
|
+
expect(error1).toBeUndefined()
|
|
648
|
+
expect(error2).toBeUndefined()
|
|
649
|
+
expect(error3).toBeUndefined()
|
|
650
|
+
expect(error4).toBeUndefined()
|
|
651
|
+
|
|
652
|
+
// Verify instances
|
|
653
|
+
expect(requestService1).not.toBe(requestService2) // Different request instances
|
|
654
|
+
expect(singletonService1).toBe(singletonService2) // Same singleton instance
|
|
655
|
+
|
|
656
|
+
// End inner request
|
|
657
|
+
await serviceLocator.endRequest(requestId2)
|
|
658
|
+
|
|
659
|
+
// Verify current context is back to outer request
|
|
660
|
+
expect(serviceLocator.getCurrentRequestContext()?.requestId).toBe(
|
|
661
|
+
requestId1,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
// End outer request
|
|
665
|
+
await serviceLocator.endRequest(requestId1)
|
|
666
|
+
|
|
667
|
+
// Verify no current context
|
|
668
|
+
expect(serviceLocator.getCurrentRequestContext()).toBeNull()
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('should handle concurrent requests with mixed scopes', async () => {
|
|
672
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
673
|
+
class RequestService {
|
|
674
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
675
|
+
name = 'RequestService'
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
679
|
+
class SingletonService {
|
|
680
|
+
id = Math.random().toString(36).substr(2, 9)
|
|
681
|
+
name = 'SingletonService'
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Start multiple requests sequentially to avoid race conditions
|
|
685
|
+
const requestIds = ['req-1', 'req-2', 'req-3']
|
|
686
|
+
const results = []
|
|
687
|
+
|
|
688
|
+
for (const requestId of requestIds) {
|
|
689
|
+
serviceLocator.beginRequest(requestId)
|
|
690
|
+
const [_error1, requestService] =
|
|
691
|
+
await serviceLocator.getInstance(RequestService)
|
|
692
|
+
const [_error2, singletonService] =
|
|
693
|
+
await serviceLocator.getInstance(SingletonService)
|
|
694
|
+
await serviceLocator.endRequest(requestId)
|
|
695
|
+
results.push({ requestService, singletonService, requestId })
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Verify all requests completed successfully
|
|
699
|
+
results.forEach(({ requestService, singletonService }) => {
|
|
700
|
+
expect(requestService).toBeDefined()
|
|
701
|
+
expect(singletonService).toBeDefined()
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
// Verify request services are different
|
|
705
|
+
expect(results[0].requestService).not.toBe(results[1].requestService)
|
|
706
|
+
expect(results[0].requestService).not.toBe(results[2].requestService)
|
|
707
|
+
expect(results[1].requestService).not.toBe(results[2].requestService)
|
|
708
|
+
|
|
709
|
+
// Verify singleton services are the same
|
|
710
|
+
expect(results[0].singletonService).toBe(results[1].singletonService)
|
|
711
|
+
expect(results[0].singletonService).toBe(results[2].singletonService)
|
|
712
|
+
expect(results[1].singletonService).toBe(results[2].singletonService)
|
|
713
|
+
})
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
describe('Error handling with mixed scopes', () => {
|
|
717
|
+
it('should handle Request-scoped service without request context', async () => {
|
|
718
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
719
|
+
class RequestService {
|
|
720
|
+
name = 'RequestService'
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Try to get Request-scoped service without request context
|
|
724
|
+
const [error] = await serviceLocator.getInstance(RequestService)
|
|
725
|
+
|
|
726
|
+
expect(error).toBeDefined()
|
|
727
|
+
expect(error?.code).toBe('UnknownError')
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
it('should handle service instantiation errors in mixed scope scenario', async () => {
|
|
731
|
+
@Injectable({ scope: InjectableScope.Singleton })
|
|
732
|
+
class SingletonService {
|
|
733
|
+
constructor() {
|
|
734
|
+
throw new Error('Singleton creation failed')
|
|
735
|
+
}
|
|
736
|
+
name = 'SingletonService'
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
@Injectable({ scope: InjectableScope.Request })
|
|
740
|
+
class RequestService {
|
|
741
|
+
singleton = inject(SingletonService)
|
|
742
|
+
name = 'RequestService'
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const requestId = 'error-test'
|
|
746
|
+
serviceLocator.beginRequest(requestId)
|
|
747
|
+
|
|
748
|
+
// Try to get Request service that depends on failing Singleton
|
|
749
|
+
// The system should throw an error when the service cannot be instantiated
|
|
750
|
+
await expect(
|
|
751
|
+
serviceLocator.getInstance(RequestService),
|
|
752
|
+
).rejects.toThrow('Singleton creation failed')
|
|
753
|
+
})
|
|
754
|
+
})
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
describe('Injectable with Schema', () => {
|
|
758
|
+
let serviceLocator: ServiceLocator
|
|
759
|
+
|
|
760
|
+
beforeEach(() => {
|
|
761
|
+
serviceLocator = new ServiceLocator(globalRegistry)
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
it('should work with simple schema definition', async () => {
|
|
765
|
+
const configSchema = z.object({
|
|
766
|
+
host: z.string(),
|
|
767
|
+
port: z.number(),
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
@Injectable({ schema: configSchema })
|
|
771
|
+
class DatabaseConfig {
|
|
772
|
+
constructor(public readonly config: z.output<typeof configSchema>) {}
|
|
773
|
+
|
|
774
|
+
getConnectionString() {
|
|
775
|
+
return `${this.config.host}:${this.config.port}`
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const token = getInjectableToken(DatabaseConfig)
|
|
780
|
+
const [error, instance] = await serviceLocator.getInstance(
|
|
781
|
+
InjectionToken.bound(token, {
|
|
782
|
+
host: 'localhost',
|
|
783
|
+
port: 5432,
|
|
784
|
+
}),
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
expect(error).toBeUndefined()
|
|
788
|
+
expect(instance).toBeInstanceOf(DatabaseConfig)
|
|
789
|
+
expect(instance.config).toEqual({ host: 'localhost', port: 5432 })
|
|
790
|
+
expect(instance.getConnectionString()).toBe('localhost:5432')
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
it('should work with schema and singleton scope', async () => {
|
|
794
|
+
const apiSchema = z.object({
|
|
795
|
+
apiKey: z.string(),
|
|
796
|
+
baseUrl: z.string(),
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
@Injectable({ schema: apiSchema, scope: InjectableScope.Singleton })
|
|
800
|
+
class ApiClient {
|
|
801
|
+
constructor(public readonly config: z.output<typeof apiSchema>) {}
|
|
802
|
+
|
|
803
|
+
getApiKey() {
|
|
804
|
+
return this.config.apiKey
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const [error1, instance1] = await serviceLocator.getInstance(ApiClient, {
|
|
809
|
+
apiKey: 'secret-key',
|
|
810
|
+
baseUrl: 'https://api.example.com',
|
|
811
|
+
})
|
|
812
|
+
const [error2, instance2] = await serviceLocator.getInstance(ApiClient, {
|
|
813
|
+
apiKey: 'secret-key',
|
|
814
|
+
baseUrl: 'https://api.example.com',
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
expect(error1).toBeUndefined()
|
|
818
|
+
expect(error2).toBeUndefined()
|
|
819
|
+
expect(instance1).toBe(instance2) // Same singleton instance
|
|
820
|
+
expect(instance1.getApiKey()).toBe('secret-key')
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
it('should work with schema and transient scope', async () => {
|
|
824
|
+
const loggerSchema = z.object({
|
|
825
|
+
level: z.enum(['debug', 'info', 'warn', 'error']),
|
|
826
|
+
prefix: z.string(),
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
@Injectable({ schema: loggerSchema, scope: InjectableScope.Transient })
|
|
830
|
+
class Logger {
|
|
831
|
+
constructor(public readonly config: z.output<typeof loggerSchema>) {}
|
|
832
|
+
|
|
833
|
+
log(message: string) {
|
|
834
|
+
return `[${this.config.prefix}] ${message}`
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const [error1, instance1] = await serviceLocator.getInstance(Logger, {
|
|
839
|
+
level: 'info' as const,
|
|
840
|
+
prefix: 'APP',
|
|
841
|
+
})
|
|
842
|
+
const [error2, instance2] = await serviceLocator.getInstance(Logger, {
|
|
843
|
+
level: 'info' as const,
|
|
844
|
+
prefix: 'APP',
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
expect(error1).toBeUndefined()
|
|
848
|
+
expect(error2).toBeUndefined()
|
|
849
|
+
expect(instance1).not.toBe(instance2) // Different transient instances
|
|
850
|
+
expect(instance1.log('test')).toBe('[APP] test')
|
|
851
|
+
expect(instance2.log('test')).toBe('[APP] test')
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
it('should work with schema and dependency injection', async () => {
|
|
855
|
+
const dbConfigSchema = z.object({
|
|
856
|
+
connectionString: z.string(),
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
@Injectable({ schema: dbConfigSchema })
|
|
860
|
+
class DatabaseConfig {
|
|
861
|
+
constructor(public readonly config: z.output<typeof dbConfigSchema>) {}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
@Injectable()
|
|
865
|
+
class DatabaseService {
|
|
866
|
+
private dbConfig = inject(DatabaseConfig, {
|
|
867
|
+
connectionString: 'postgres://localhost:5432/mydb',
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
getConnectionString() {
|
|
871
|
+
return this.dbConfig.config.connectionString
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const [error, instance] =
|
|
876
|
+
await serviceLocator.getInstance(DatabaseService)
|
|
877
|
+
|
|
878
|
+
expect(error).toBeUndefined()
|
|
879
|
+
if (!instance) throw new Error('Instance is undefined')
|
|
880
|
+
expect(instance).toBeInstanceOf(DatabaseService)
|
|
881
|
+
expect(instance.getConnectionString()).toBe(
|
|
882
|
+
'postgres://localhost:5432/mydb',
|
|
883
|
+
)
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
it('should work with schema and async dependency injection', async () => {
|
|
887
|
+
const cacheConfigSchema = z.object({
|
|
888
|
+
ttl: z.number(),
|
|
889
|
+
maxSize: z.number(),
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
@Injectable({ schema: cacheConfigSchema })
|
|
893
|
+
class CacheConfig {
|
|
894
|
+
constructor(
|
|
895
|
+
public readonly config: z.output<typeof cacheConfigSchema>,
|
|
896
|
+
) {}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
@Injectable()
|
|
900
|
+
class CacheService {
|
|
901
|
+
private cacheConfig = asyncInject(CacheConfig, {
|
|
902
|
+
ttl: 3600,
|
|
903
|
+
maxSize: 1000,
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
async getConfig() {
|
|
907
|
+
const config = await this.cacheConfig
|
|
908
|
+
return config.config
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const [error, instance] = await serviceLocator.getInstance(CacheService)
|
|
913
|
+
|
|
914
|
+
expect(error).toBeUndefined()
|
|
915
|
+
if (!instance) throw new Error('Instance is undefined')
|
|
916
|
+
expect(instance).toBeInstanceOf(CacheService)
|
|
917
|
+
const config = await instance.getConfig()
|
|
918
|
+
expect(config).toEqual({ ttl: 3600, maxSize: 1000 })
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
it('should validate schema when using bound tokens', async () => {
|
|
922
|
+
const strictSchema = z.object({
|
|
923
|
+
required: z.string(),
|
|
924
|
+
optional: z.number().optional(),
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
@Injectable({ schema: strictSchema })
|
|
928
|
+
class StrictService {
|
|
929
|
+
constructor(public readonly config: z.output<typeof strictSchema>) {}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Valid configuration
|
|
933
|
+
const [error1, instance1] = await serviceLocator.getInstance(
|
|
934
|
+
StrictService,
|
|
935
|
+
{
|
|
936
|
+
required: 'value',
|
|
937
|
+
optional: 42,
|
|
938
|
+
},
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
expect(error1).toBeUndefined()
|
|
942
|
+
expect(instance1).toBeInstanceOf(StrictService)
|
|
943
|
+
expect(instance1.config).toEqual({ required: 'value', optional: 42 })
|
|
944
|
+
|
|
945
|
+
// Valid with optional field missing
|
|
946
|
+
const [error2, instance2] = await serviceLocator.getInstance(
|
|
947
|
+
StrictService,
|
|
948
|
+
{
|
|
949
|
+
required: 'another value',
|
|
950
|
+
},
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
expect(error2).toBeUndefined()
|
|
954
|
+
expect(instance2).toBeInstanceOf(StrictService)
|
|
955
|
+
expect(instance2.config).toEqual({ required: 'another value' })
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
it('should work with complex nested schemas', async () => {
|
|
959
|
+
const nestedSchema = z.object({
|
|
960
|
+
database: z.object({
|
|
961
|
+
host: z.string(),
|
|
962
|
+
port: z.number(),
|
|
963
|
+
credentials: z.object({
|
|
964
|
+
username: z.string(),
|
|
965
|
+
password: z.string(),
|
|
966
|
+
}),
|
|
967
|
+
}),
|
|
968
|
+
cache: z.object({
|
|
969
|
+
enabled: z.boolean(),
|
|
970
|
+
ttl: z.number(),
|
|
971
|
+
}),
|
|
972
|
+
})
|
|
973
|
+
|
|
974
|
+
@Injectable({ schema: nestedSchema })
|
|
975
|
+
class AppConfig {
|
|
976
|
+
constructor(public readonly config: z.output<typeof nestedSchema>) {}
|
|
977
|
+
|
|
978
|
+
getDatabaseHost() {
|
|
979
|
+
return this.config.database.host
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
isCacheEnabled() {
|
|
983
|
+
return this.config.cache.enabled
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const [error, instance] = await serviceLocator.getInstance(AppConfig, {
|
|
988
|
+
database: {
|
|
989
|
+
host: 'db.example.com',
|
|
990
|
+
port: 5432,
|
|
991
|
+
credentials: {
|
|
992
|
+
username: 'admin',
|
|
993
|
+
password: 'secret',
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
cache: {
|
|
997
|
+
enabled: true,
|
|
998
|
+
ttl: 300,
|
|
999
|
+
},
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
expect(error).toBeUndefined()
|
|
1003
|
+
expect(instance).toBeInstanceOf(AppConfig)
|
|
1004
|
+
expect(instance.getDatabaseHost()).toBe('db.example.com')
|
|
1005
|
+
expect(instance.isCacheEnabled()).toBe(true)
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
it('should work with schema in request-scoped services', async () => {
|
|
1009
|
+
const userContextSchema = z.object({
|
|
1010
|
+
userId: z.string(),
|
|
1011
|
+
sessionId: z.string(),
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
@Injectable({
|
|
1015
|
+
schema: userContextSchema,
|
|
1016
|
+
scope: InjectableScope.Request,
|
|
1017
|
+
})
|
|
1018
|
+
class UserContext {
|
|
1019
|
+
constructor(
|
|
1020
|
+
public readonly context: z.output<typeof userContextSchema>,
|
|
1021
|
+
) {}
|
|
1022
|
+
|
|
1023
|
+
getUserId() {
|
|
1024
|
+
return this.context.userId
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const requestId = 'test-request'
|
|
1029
|
+
serviceLocator.beginRequest(requestId)
|
|
1030
|
+
|
|
1031
|
+
const [error, instance] = await serviceLocator.getInstance(UserContext, {
|
|
1032
|
+
userId: 'user-123',
|
|
1033
|
+
sessionId: 'session-456',
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
expect(error).toBeUndefined()
|
|
1037
|
+
expect(instance).toBeInstanceOf(UserContext)
|
|
1038
|
+
expect(instance.getUserId()).toBe('user-123')
|
|
1039
|
+
|
|
1040
|
+
await serviceLocator.endRequest(requestId)
|
|
1041
|
+
})
|
|
1042
|
+
})
|
|
35
1043
|
})
|