@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.
- 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 +494 -145
- package/lib/_tsup-dts-rollup.d.ts +494 -145
- package/lib/index.d.mts +26 -12
- package/lib/index.d.ts +26 -12
- package/lib/index.js +1021 -470
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +1011 -461
- 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 +427 -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 +174 -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,427 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { Container } from '../container.mjs'
|
|
4
|
+
import { Injectable } from '../decorators/injectable.decorator.mjs'
|
|
5
|
+
import { InjectableScope } from '../enums/index.mjs'
|
|
6
|
+
import { inject } from '../index.mjs'
|
|
7
|
+
import { InjectionToken } from '../injection-token.mjs'
|
|
8
|
+
import { Registry } from '../registry.mjs'
|
|
9
|
+
import { createRequestContextHolder } from '../request-context-holder.mjs'
|
|
10
|
+
|
|
11
|
+
describe('Request Scope', () => {
|
|
12
|
+
let container: Container
|
|
13
|
+
let registry: Registry
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
registry = new Registry()
|
|
17
|
+
container = new Container(registry)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('Request-scoped services', () => {
|
|
21
|
+
it('should create different instances for different requests', async () => {
|
|
22
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
23
|
+
class RequestService {
|
|
24
|
+
public readonly requestId = Math.random().toString(36)
|
|
25
|
+
public readonly createdAt = new Date()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Start first request
|
|
29
|
+
container.beginRequest('request-1')
|
|
30
|
+
const instance1a = await container.get(RequestService)
|
|
31
|
+
const instance1b = await container.get(RequestService)
|
|
32
|
+
|
|
33
|
+
// Start second request
|
|
34
|
+
container.beginRequest('request-2')
|
|
35
|
+
const instance2a = await container.get(RequestService)
|
|
36
|
+
const instance2b = await container.get(RequestService)
|
|
37
|
+
|
|
38
|
+
// Within same request, instances should be the same
|
|
39
|
+
expect(instance1a).toBe(instance1b)
|
|
40
|
+
expect(instance2a).toBe(instance2b)
|
|
41
|
+
|
|
42
|
+
// Between different requests, instances should be different
|
|
43
|
+
expect(instance1a).not.toBe(instance2a)
|
|
44
|
+
expect(instance1a.requestId).not.toBe(instance2a.requestId)
|
|
45
|
+
|
|
46
|
+
// Clean up
|
|
47
|
+
await container.endRequest('request-1')
|
|
48
|
+
await container.endRequest('request-2')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should handle request context lifecycle correctly', async () => {
|
|
52
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
53
|
+
class RequestService {
|
|
54
|
+
public readonly requestId = Math.random().toString(36)
|
|
55
|
+
public destroyed = false
|
|
56
|
+
|
|
57
|
+
async onServiceDestroy() {
|
|
58
|
+
this.destroyed = true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Start request
|
|
63
|
+
const requestId = 'test-request'
|
|
64
|
+
container.beginRequest(requestId)
|
|
65
|
+
|
|
66
|
+
const instance = await container.get(RequestService)
|
|
67
|
+
expect(instance.destroyed).toBe(false)
|
|
68
|
+
|
|
69
|
+
// End request should trigger cleanup
|
|
70
|
+
await container.endRequest(requestId)
|
|
71
|
+
expect(instance.destroyed).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should support request metadata', async () => {
|
|
75
|
+
const requestId = 'test-request'
|
|
76
|
+
const metadata = { userId: 'user123', sessionId: 'session456' }
|
|
77
|
+
|
|
78
|
+
container.beginRequest(requestId, metadata)
|
|
79
|
+
|
|
80
|
+
// Note: In a real implementation, you might want to inject metadata
|
|
81
|
+
// For now, we'll just verify the context exists
|
|
82
|
+
const context = container.getCurrentRequestContext()
|
|
83
|
+
expect(context).not.toBeNull()
|
|
84
|
+
expect(context?.requestId).toBe(requestId)
|
|
85
|
+
expect(context?.getMetadata('userId')).toBe('user123')
|
|
86
|
+
expect(context?.getMetadata('sessionId')).toBe('session456')
|
|
87
|
+
|
|
88
|
+
await container.endRequest(requestId)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should handle pre-prepared instances', async () => {
|
|
92
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
93
|
+
class RequestService {
|
|
94
|
+
public readonly requestId = Math.random().toString(36)
|
|
95
|
+
public readonly prePrepared = true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const requestId = 'test-request'
|
|
99
|
+
container.beginRequest(requestId)
|
|
100
|
+
|
|
101
|
+
// Getting the instance should be fast (pre-prepared)
|
|
102
|
+
const instance = await container.get(RequestService)
|
|
103
|
+
expect(instance.prePrepared).toBe(true)
|
|
104
|
+
|
|
105
|
+
await container.endRequest(requestId)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should handle mixed scopes correctly', async () => {
|
|
109
|
+
@Injectable({ registry, scope: InjectableScope.Singleton })
|
|
110
|
+
class SingletonService {
|
|
111
|
+
public readonly id = Math.random().toString(36)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
115
|
+
class RequestService {
|
|
116
|
+
public readonly id = Math.random().toString(36)
|
|
117
|
+
singleton: SingletonService = inject(SingletonService)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Injectable({ registry, scope: InjectableScope.Transient })
|
|
121
|
+
class TransientService {
|
|
122
|
+
requestService = inject(RequestService)
|
|
123
|
+
public readonly id = Math.random().toString(36)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Start request
|
|
127
|
+
container.beginRequest('test-request')
|
|
128
|
+
|
|
129
|
+
const requestService1 = await container.get(RequestService)
|
|
130
|
+
const requestService2 = await container.get(RequestService)
|
|
131
|
+
const singleton1 = await container.get(SingletonService)
|
|
132
|
+
const singleton2 = await container.get(SingletonService)
|
|
133
|
+
const transient1 = await container.get(TransientService)
|
|
134
|
+
const transient2 = await container.get(TransientService)
|
|
135
|
+
|
|
136
|
+
// Request-scoped: same instance within request
|
|
137
|
+
expect(requestService1).toBe(requestService2)
|
|
138
|
+
expect(requestService1.singleton).toBe(singleton1)
|
|
139
|
+
|
|
140
|
+
// Singleton: same instance always
|
|
141
|
+
expect(singleton1).toBe(singleton2)
|
|
142
|
+
|
|
143
|
+
// Transient: different instances always
|
|
144
|
+
expect(transient1).not.toBe(transient2)
|
|
145
|
+
expect(transient1.requestService).toBe(transient2.requestService)
|
|
146
|
+
|
|
147
|
+
await container.endRequest('test-request')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should handle nested request contexts', async () => {
|
|
151
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
152
|
+
class RequestService {
|
|
153
|
+
public readonly id = Math.random().toString(36)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Start first request
|
|
157
|
+
container.beginRequest('request-1')
|
|
158
|
+
const instance1 = await container.get(RequestService)
|
|
159
|
+
|
|
160
|
+
// Start second request (nested)
|
|
161
|
+
container.beginRequest('request-2')
|
|
162
|
+
const instance2 = await container.get(RequestService)
|
|
163
|
+
|
|
164
|
+
// Should be different instances
|
|
165
|
+
expect(instance1).not.toBe(instance2)
|
|
166
|
+
|
|
167
|
+
// End second request
|
|
168
|
+
await container.endRequest('request-2')
|
|
169
|
+
|
|
170
|
+
// Get instance from first request again
|
|
171
|
+
const instance1Again = await container.get(RequestService)
|
|
172
|
+
expect(instance1).toBe(instance1Again)
|
|
173
|
+
|
|
174
|
+
// End first request
|
|
175
|
+
await container.endRequest('request-1')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should handle request context switching', async () => {
|
|
179
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
180
|
+
class RequestService {
|
|
181
|
+
public readonly id = Math.random().toString(36)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Start multiple requests
|
|
185
|
+
container.beginRequest('request-1')
|
|
186
|
+
container.beginRequest('request-2')
|
|
187
|
+
container.beginRequest('request-3')
|
|
188
|
+
|
|
189
|
+
// Switch to request-1
|
|
190
|
+
container.setCurrentRequestContext('request-1')
|
|
191
|
+
const instance1 = await container.get(RequestService)
|
|
192
|
+
|
|
193
|
+
// Switch to request-2
|
|
194
|
+
container.setCurrentRequestContext('request-2')
|
|
195
|
+
const instance2 = await container.get(RequestService)
|
|
196
|
+
|
|
197
|
+
// Switch back to request-1
|
|
198
|
+
container.setCurrentRequestContext('request-1')
|
|
199
|
+
const instance1Again = await container.get(RequestService)
|
|
200
|
+
|
|
201
|
+
// Should get same instance for request-1
|
|
202
|
+
expect(instance1).toBe(instance1Again)
|
|
203
|
+
// Should get different instance for request-2
|
|
204
|
+
expect(instance1).not.toBe(instance2)
|
|
205
|
+
|
|
206
|
+
// Clean up all requests
|
|
207
|
+
await container.endRequest('request-1')
|
|
208
|
+
await container.endRequest('request-2')
|
|
209
|
+
await container.endRequest('request-3')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('RequestContextHolder', () => {
|
|
214
|
+
it('should manage instances correctly', () => {
|
|
215
|
+
const holder = createRequestContextHolder('test-request', 100, {
|
|
216
|
+
userId: 'user123',
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
expect(holder.requestId).toBe('test-request')
|
|
220
|
+
expect(holder.priority).toBe(100)
|
|
221
|
+
expect(holder.getMetadata('userId')).toBe('user123')
|
|
222
|
+
|
|
223
|
+
// Add instance
|
|
224
|
+
const mockInstance = { id: 'test-instance' }
|
|
225
|
+
const mockHolder = {
|
|
226
|
+
status: 'Created' as any,
|
|
227
|
+
name: 'test-instance',
|
|
228
|
+
instance: mockInstance,
|
|
229
|
+
creationPromise: null,
|
|
230
|
+
destroyPromise: null,
|
|
231
|
+
type: 'Class' as any,
|
|
232
|
+
scope: 'Request' as any,
|
|
233
|
+
deps: new Set<string>(),
|
|
234
|
+
destroyListeners: [],
|
|
235
|
+
createdAt: Date.now(),
|
|
236
|
+
ttl: Infinity,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
holder.addInstance('test-instance', mockInstance, mockHolder)
|
|
240
|
+
|
|
241
|
+
expect(holder.hasInstance('test-instance')).toBe(true)
|
|
242
|
+
expect(holder.getInstance('test-instance')).toBe(mockInstance)
|
|
243
|
+
expect(holder.getHolder('test-instance')).toBe(mockHolder)
|
|
244
|
+
|
|
245
|
+
// Clear instances
|
|
246
|
+
holder.clear()
|
|
247
|
+
expect(holder.hasInstance('test-instance')).toBe(false)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should handle metadata correctly', () => {
|
|
251
|
+
const holder = createRequestContextHolder('test-request')
|
|
252
|
+
|
|
253
|
+
holder.setMetadata('key1', 'value1')
|
|
254
|
+
holder.setMetadata('key2', 'value2')
|
|
255
|
+
|
|
256
|
+
expect(holder.getMetadata('key1')).toBe('value1')
|
|
257
|
+
expect(holder.getMetadata('key2')).toBe('value2')
|
|
258
|
+
expect(holder.getMetadata('nonexistent')).toBeUndefined()
|
|
259
|
+
|
|
260
|
+
holder.clear()
|
|
261
|
+
expect(holder.getMetadata('key1')).toBeUndefined()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('should store instances by InjectionToken', () => {
|
|
265
|
+
const holder = createRequestContextHolder('test-request')
|
|
266
|
+
const token = InjectionToken.create<string>('TestService')
|
|
267
|
+
const instance = { id: 'test-instance', data: 'test-data' }
|
|
268
|
+
|
|
269
|
+
// Store instance by InjectionToken
|
|
270
|
+
holder.addInstance(token, instance)
|
|
271
|
+
|
|
272
|
+
// Verify instance is stored and retrievable
|
|
273
|
+
expect(holder.hasInstance(token.toString())).toBe(true)
|
|
274
|
+
expect(holder.getInstance(token.toString())).toBe(instance)
|
|
275
|
+
|
|
276
|
+
// Verify holder is created with correct properties
|
|
277
|
+
const holderInfo = holder.getHolder(token.toString())
|
|
278
|
+
expect(holderInfo).toBeDefined()
|
|
279
|
+
expect(holderInfo?.instance).toBe(instance)
|
|
280
|
+
expect(holderInfo?.name).toBe(token.toString())
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should store multiple instances by different InjectionTokens', () => {
|
|
284
|
+
const holder = createRequestContextHolder('test-request')
|
|
285
|
+
|
|
286
|
+
const token1 = InjectionToken.create<string>('Service1')
|
|
287
|
+
const token2 = InjectionToken.create<number>('Service2')
|
|
288
|
+
const token3 = InjectionToken.create<boolean>('Service3')
|
|
289
|
+
|
|
290
|
+
const instance1 = { id: 'instance1', type: 'string' }
|
|
291
|
+
const instance2 = { id: 'instance2', type: 'number' }
|
|
292
|
+
const instance3 = { id: 'instance3', type: 'boolean' }
|
|
293
|
+
|
|
294
|
+
// Store multiple instances
|
|
295
|
+
holder.addInstance(token1, instance1)
|
|
296
|
+
holder.addInstance(token2, instance2)
|
|
297
|
+
holder.addInstance(token3, instance3)
|
|
298
|
+
|
|
299
|
+
// Verify all instances are stored correctly
|
|
300
|
+
expect(holder.hasInstance(token1.toString())).toBe(true)
|
|
301
|
+
expect(holder.hasInstance(token2.toString())).toBe(true)
|
|
302
|
+
expect(holder.hasInstance(token3.toString())).toBe(true)
|
|
303
|
+
|
|
304
|
+
expect(holder.getInstance(token1.toString())).toBe(instance1)
|
|
305
|
+
expect(holder.getInstance(token2.toString())).toBe(instance2)
|
|
306
|
+
expect(holder.getInstance(token3.toString())).toBe(instance3)
|
|
307
|
+
|
|
308
|
+
// Verify each has its own holder
|
|
309
|
+
const holder1 = holder.getHolder(token1.toString())
|
|
310
|
+
const holder2 = holder.getHolder(token2.toString())
|
|
311
|
+
const holder3 = holder.getHolder(token3.toString())
|
|
312
|
+
|
|
313
|
+
expect(holder1?.instance).toBe(instance1)
|
|
314
|
+
expect(holder2?.instance).toBe(instance2)
|
|
315
|
+
expect(holder3?.instance).toBe(instance3)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('should override instances stored by InjectionToken', () => {
|
|
319
|
+
const holder = createRequestContextHolder('test-request')
|
|
320
|
+
const token = InjectionToken.create<string>('TestService')
|
|
321
|
+
|
|
322
|
+
const originalInstance = { id: 'original', data: 'original-data' }
|
|
323
|
+
const newInstance = { id: 'new', data: 'new-data' }
|
|
324
|
+
|
|
325
|
+
// Store original instance
|
|
326
|
+
holder.addInstance(token, originalInstance)
|
|
327
|
+
expect(holder.getInstance(token.toString())).toBe(originalInstance)
|
|
328
|
+
|
|
329
|
+
// Override with new instance
|
|
330
|
+
holder.addInstance(token, newInstance)
|
|
331
|
+
expect(holder.getInstance(token.toString())).toBe(newInstance)
|
|
332
|
+
expect(holder.getInstance(token.toString())).not.toBe(originalInstance)
|
|
333
|
+
|
|
334
|
+
// Verify holder is updated
|
|
335
|
+
const holderInfo = holder.getHolder(token.toString())
|
|
336
|
+
expect(holderInfo?.instance).toBe(newInstance)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('should handle InjectionToken with different name types', () => {
|
|
340
|
+
const holder = createRequestContextHolder('test-request')
|
|
341
|
+
|
|
342
|
+
// Test with string name
|
|
343
|
+
const stringToken = InjectionToken.create<string>('StringService')
|
|
344
|
+
const stringInstance = { type: 'string' }
|
|
345
|
+
|
|
346
|
+
// Test with symbol name
|
|
347
|
+
const symbolToken = InjectionToken.create<number>(Symbol('SymbolService'))
|
|
348
|
+
const symbolInstance = { type: 'symbol' }
|
|
349
|
+
|
|
350
|
+
// Test with class name
|
|
351
|
+
class TestClass {}
|
|
352
|
+
const classToken = InjectionToken.create(TestClass)
|
|
353
|
+
const classInstance = { type: 'class' }
|
|
354
|
+
|
|
355
|
+
holder.addInstance(stringToken, stringInstance)
|
|
356
|
+
holder.addInstance(symbolToken, symbolInstance)
|
|
357
|
+
holder.addInstance(classToken, classInstance)
|
|
358
|
+
|
|
359
|
+
expect(holder.getInstance(stringToken.toString())).toBe(stringInstance)
|
|
360
|
+
expect(holder.getInstance(symbolToken.toString())).toBe(symbolInstance)
|
|
361
|
+
expect(holder.getInstance(classToken.toString())).toBe(classInstance)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('should clear instances stored by InjectionToken', () => {
|
|
365
|
+
const holder = createRequestContextHolder('test-request')
|
|
366
|
+
const token1 = InjectionToken.create<string>('Service1')
|
|
367
|
+
const token2 = InjectionToken.create<number>('Service2')
|
|
368
|
+
|
|
369
|
+
const instance1 = { id: 'instance1' }
|
|
370
|
+
const instance2 = { id: 'instance2' }
|
|
371
|
+
|
|
372
|
+
holder.addInstance(token1, instance1)
|
|
373
|
+
holder.addInstance(token2, instance2)
|
|
374
|
+
|
|
375
|
+
expect(holder.hasInstance(token1.toString())).toBe(true)
|
|
376
|
+
expect(holder.hasInstance(token2.toString())).toBe(true)
|
|
377
|
+
|
|
378
|
+
// Clear all instances
|
|
379
|
+
holder.clear()
|
|
380
|
+
|
|
381
|
+
expect(holder.hasInstance(token1.toString())).toBe(false)
|
|
382
|
+
expect(holder.hasInstance(token2.toString())).toBe(false)
|
|
383
|
+
expect(holder.getInstance(token1.toString())).toBeUndefined()
|
|
384
|
+
expect(holder.getInstance(token2.toString())).toBeUndefined()
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('should handle mixed storage by InjectionToken and string name', () => {
|
|
388
|
+
const holder = createRequestContextHolder('test-request')
|
|
389
|
+
|
|
390
|
+
const token = InjectionToken.create<string>('TokenService')
|
|
391
|
+
const tokenInstance = { id: 'token-instance' }
|
|
392
|
+
|
|
393
|
+
const stringName = 'string-service'
|
|
394
|
+
const stringInstance = { id: 'string-instance' }
|
|
395
|
+
|
|
396
|
+
// Store by InjectionToken
|
|
397
|
+
holder.addInstance(token, tokenInstance)
|
|
398
|
+
|
|
399
|
+
// Store by string name (requires holder)
|
|
400
|
+
const mockHolder = {
|
|
401
|
+
status: 'Created' as any,
|
|
402
|
+
name: stringName,
|
|
403
|
+
instance: stringInstance,
|
|
404
|
+
creationPromise: null,
|
|
405
|
+
destroyPromise: null,
|
|
406
|
+
type: 'Class' as any,
|
|
407
|
+
scope: 'Singleton' as any,
|
|
408
|
+
deps: new Set<string>(),
|
|
409
|
+
destroyListeners: [],
|
|
410
|
+
createdAt: Date.now(),
|
|
411
|
+
ttl: Infinity,
|
|
412
|
+
}
|
|
413
|
+
holder.addInstance(stringName, stringInstance, mockHolder)
|
|
414
|
+
|
|
415
|
+
// Verify both are stored correctly
|
|
416
|
+
expect(holder.hasInstance(token.toString())).toBe(true)
|
|
417
|
+
expect(holder.hasInstance(stringName)).toBe(true)
|
|
418
|
+
|
|
419
|
+
expect(holder.getInstance(token.toString())).toBe(tokenInstance)
|
|
420
|
+
expect(holder.getInstance(stringName)).toBe(stringInstance)
|
|
421
|
+
|
|
422
|
+
// Verify holders
|
|
423
|
+
expect(holder.getHolder(token.toString())?.instance).toBe(tokenInstance)
|
|
424
|
+
expect(holder.getHolder(stringName)?.instance).toBe(stringInstance)
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from 'zod/v4'
|
|
2
|
+
|
|
3
|
+
import type { Factorable, FactorableWithArgs } from '../interfaces/index.mjs'
|
|
4
|
+
|
|
5
|
+
import { Factory } from '../decorators/index.mjs'
|
|
6
|
+
import { InjectableScope } from '../enums/index.mjs'
|
|
7
|
+
import { InjectionToken } from '../injection-token.mjs'
|
|
8
|
+
import { Registry } from '../registry.mjs'
|
|
9
|
+
|
|
10
|
+
// Test factory without arguments
|
|
11
|
+
@Factory()
|
|
12
|
+
class TestFactory1 implements Factorable<string> {
|
|
13
|
+
create() {
|
|
14
|
+
return 'test'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Test factory with scope
|
|
19
|
+
@Factory({ scope: InjectableScope.Transient })
|
|
20
|
+
class TestFactory2 implements Factorable<number> {
|
|
21
|
+
create() {
|
|
22
|
+
return 42
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Test factory with token
|
|
27
|
+
const token = InjectionToken.create('TestToken')
|
|
28
|
+
@Factory({ token })
|
|
29
|
+
class TestFactory3 implements Factorable<boolean> {
|
|
30
|
+
create() {
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Test factory with token and schema
|
|
36
|
+
const schema = z.object({ name: z.string() })
|
|
37
|
+
const tokenWithSchema = InjectionToken.create('TestTokenWithSchema', schema)
|
|
38
|
+
@Factory({ token: tokenWithSchema })
|
|
39
|
+
class TestFactory4
|
|
40
|
+
implements FactorableWithArgs<{ name: string }, typeof schema>
|
|
41
|
+
{
|
|
42
|
+
create(ctx: any, args: z.output<typeof schema>) {
|
|
43
|
+
return args
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Test factory with custom registry
|
|
48
|
+
const registry = new Registry()
|
|
49
|
+
@Factory({ registry })
|
|
50
|
+
class TestFactory5 implements Factorable<object> {
|
|
51
|
+
create() {
|
|
52
|
+
return {}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Type tests
|
|
57
|
+
const test1: string = new TestFactory1().create()
|
|
58
|
+
const test2: number = new TestFactory2().create()
|
|
59
|
+
const test3: boolean = new TestFactory3().create()
|
|
60
|
+
const test4: { name: string } = new TestFactory4().create(undefined, {
|
|
61
|
+
name: 'test',
|
|
62
|
+
})
|
|
63
|
+
const test5: object = new TestFactory5().create()
|
|
64
|
+
|
|
65
|
+
export { test1, test2, test3, test4, test5 }
|
|
@@ -2,9 +2,8 @@ import { assertType, describe, test } from 'vitest'
|
|
|
2
2
|
import { z } from 'zod/v4'
|
|
3
3
|
|
|
4
4
|
import { Injectable } from '../decorators/index.mjs'
|
|
5
|
-
import { InjectableType } from '../enums/index.mjs'
|
|
6
5
|
import { InjectionToken } from '../injection-token.mjs'
|
|
7
|
-
import {
|
|
6
|
+
import { asyncInject } from '../injector.mjs'
|
|
8
7
|
|
|
9
8
|
interface FooService {
|
|
10
9
|
makeFoo(): string
|
|
@@ -18,8 +17,8 @@ const simpleOptionalObjectSchema = z
|
|
|
18
17
|
foo: z.string(),
|
|
19
18
|
})
|
|
20
19
|
.optional()
|
|
21
|
-
const simpleRecordSchema = z.record(z.string(), z.string())
|
|
22
|
-
const simpleOptionalRecordSchema = z.record(z.string(), z.string()).optional()
|
|
20
|
+
// const simpleRecordSchema = z.record(z.string(), z.string())
|
|
21
|
+
// const simpleOptionalRecordSchema = z.record(z.string(), z.string()).optional()
|
|
23
22
|
|
|
24
23
|
const typelessObjectToken = InjectionToken.create(
|
|
25
24
|
Symbol.for('Typeless object token'),
|
|
@@ -29,14 +28,14 @@ const typelessOptionalObjectToken = InjectionToken.create(
|
|
|
29
28
|
Symbol.for('Typeless optional object token'),
|
|
30
29
|
simpleOptionalObjectSchema,
|
|
31
30
|
)
|
|
32
|
-
const typelessRecordToken = InjectionToken.create(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
const typelessOptionalRecordToken = InjectionToken.create(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
31
|
+
// const typelessRecordToken = InjectionToken.create(
|
|
32
|
+
// Symbol.for('Typeless record token'),
|
|
33
|
+
// simpleRecordSchema,
|
|
34
|
+
// )
|
|
35
|
+
// const typelessOptionalRecordToken = InjectionToken.create(
|
|
36
|
+
// Symbol.for('Typeless optional record token'),
|
|
37
|
+
// simpleOptionalRecordSchema,
|
|
38
|
+
// )
|
|
40
39
|
|
|
41
40
|
const typedObjectToken = InjectionToken.create<
|
|
42
41
|
FooService,
|
|
@@ -46,14 +45,14 @@ const typedOptionalObjectToken = InjectionToken.create<
|
|
|
46
45
|
FooService,
|
|
47
46
|
typeof simpleOptionalObjectSchema
|
|
48
47
|
>(Symbol.for('Typed optional object token'), simpleOptionalObjectSchema)
|
|
49
|
-
const typedRecordToken = InjectionToken.create<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
>(Symbol.for('Typed record token'), simpleRecordSchema)
|
|
53
|
-
const typedOptionalRecordToken = InjectionToken.create<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
>(Symbol.for('Typed optional record token'), simpleOptionalRecordSchema)
|
|
48
|
+
// const typedRecordToken = InjectionToken.create<
|
|
49
|
+
// FooService,
|
|
50
|
+
// typeof simpleRecordSchema
|
|
51
|
+
// >(Symbol.for('Typed record token'), simpleRecordSchema)
|
|
52
|
+
// const typedOptionalRecordToken = InjectionToken.create<
|
|
53
|
+
// FooService,
|
|
54
|
+
// typeof simpleOptionalRecordSchema
|
|
55
|
+
// >(Symbol.for('Typed optional record token'), simpleOptionalRecordSchema)
|
|
57
56
|
|
|
58
57
|
const typedToken = InjectionToken.create<FooService>(Symbol.for('Typed token'))
|
|
59
58
|
|
|
@@ -66,34 +65,34 @@ describe('inject', () => {
|
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
assertType<Foo>(await
|
|
68
|
+
assertType<Foo>(await asyncInject(Foo))
|
|
70
69
|
})
|
|
71
70
|
test('#2 Token with required Schema', async () => {
|
|
72
|
-
const result = await
|
|
71
|
+
const result = await asyncInject(typelessObjectToken, { foo: 'bar' })
|
|
73
72
|
assertType<unknown>(result)
|
|
74
73
|
|
|
75
|
-
const result2 = await
|
|
74
|
+
const result2 = await asyncInject(typedObjectToken, { foo: 'bar' })
|
|
76
75
|
assertType<FooService>(result2)
|
|
77
76
|
|
|
78
77
|
// @ts-expect-error We show error when we pass the wrong type
|
|
79
|
-
await
|
|
78
|
+
await asyncInject(typedObjectToken, undefined)
|
|
80
79
|
})
|
|
81
80
|
|
|
82
81
|
test('#3 Token with optional Schema', async () => {
|
|
83
|
-
const result = await
|
|
82
|
+
const result = await asyncInject(typelessOptionalObjectToken)
|
|
84
83
|
assertType<unknown>(result)
|
|
85
84
|
|
|
86
|
-
const result2 = await
|
|
85
|
+
const result2 = await asyncInject(typedOptionalObjectToken)
|
|
87
86
|
assertType<FooService>(result2)
|
|
88
87
|
|
|
89
|
-
const result3 = await
|
|
88
|
+
const result3 = await asyncInject(typedObjectToken)
|
|
90
89
|
// Special case when we pass the token without args
|
|
91
90
|
// We can only return an error string
|
|
92
91
|
assertType<'Error: Your token requires args: foo'>(result3)
|
|
93
92
|
})
|
|
94
93
|
|
|
95
94
|
test('#4 Token with no Schema', async () => {
|
|
96
|
-
const result = await
|
|
95
|
+
const result = await asyncInject(typedToken)
|
|
97
96
|
assertType<FooService>(result)
|
|
98
97
|
})
|
|
99
98
|
})
|