@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,1301 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { z } from 'zod/v4'
|
|
3
|
+
|
|
4
|
+
import type { FactoryContext } from '../factory-context.mjs'
|
|
5
|
+
import type {
|
|
6
|
+
Factorable,
|
|
7
|
+
FactorableWithArgs,
|
|
8
|
+
} from '../interfaces/factory.interface.mjs'
|
|
9
|
+
|
|
10
|
+
import { Container } from '../container.mjs'
|
|
11
|
+
import { Factory } from '../decorators/factory.decorator.mjs'
|
|
12
|
+
import { Injectable } from '../decorators/injectable.decorator.mjs'
|
|
13
|
+
import { InjectableScope } from '../enums/injectable-scope.enum.mjs'
|
|
14
|
+
import { getInjectors } from '../index.mjs'
|
|
15
|
+
import { InjectionToken } from '../injection-token.mjs'
|
|
16
|
+
import { asyncInject, inject } from '../injector.mjs'
|
|
17
|
+
import { Registry } from '../registry.mjs'
|
|
18
|
+
import { ServiceLocator } from '../service-locator.mjs'
|
|
19
|
+
import { createDeferred } from '../utils/defer.mjs'
|
|
20
|
+
|
|
21
|
+
describe('Container', () => {
|
|
22
|
+
let container: Container
|
|
23
|
+
let registry: Registry
|
|
24
|
+
let mockLogger: Console
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
registry = new Registry()
|
|
28
|
+
mockLogger = {
|
|
29
|
+
log: vi.fn(),
|
|
30
|
+
error: vi.fn(),
|
|
31
|
+
warn: vi.fn(),
|
|
32
|
+
info: vi.fn(),
|
|
33
|
+
debug: vi.fn(),
|
|
34
|
+
} as any
|
|
35
|
+
container = new Container(registry, mockLogger)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('Basic functionality', () => {
|
|
39
|
+
it('should create container with default registry', () => {
|
|
40
|
+
const defaultContainer = new Container()
|
|
41
|
+
expect(defaultContainer).toBeInstanceOf(Container)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should create container with custom registry and logger', () => {
|
|
45
|
+
expect(container).toBeInstanceOf(Container)
|
|
46
|
+
expect(container.getServiceLocator()).toBeInstanceOf(ServiceLocator)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should register itself in the container', async () => {
|
|
50
|
+
const selfInstance = await container.get(Container)
|
|
51
|
+
expect(selfInstance).toBe(container)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should return the same ServiceLocator instance', () => {
|
|
55
|
+
const serviceLocator1 = container.getServiceLocator()
|
|
56
|
+
const serviceLocator2 = container.getServiceLocator()
|
|
57
|
+
expect(serviceLocator1).toBe(serviceLocator2)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('Injectable decorator scenarios', () => {
|
|
62
|
+
describe('Singleton scope', () => {
|
|
63
|
+
it('should return the same instance for singleton services', async () => {
|
|
64
|
+
@Injectable({ registry })
|
|
65
|
+
class TestService {
|
|
66
|
+
public id = Math.random()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const instance1 = await container.get(TestService)
|
|
70
|
+
const instance2 = await container.get(TestService)
|
|
71
|
+
|
|
72
|
+
expect(instance1).toBeInstanceOf(TestService)
|
|
73
|
+
expect(instance2).toBeInstanceOf(TestService)
|
|
74
|
+
expect(instance1).toBe(instance2)
|
|
75
|
+
expect(instance1.id).toBe(instance2.id)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should work with default singleton scope', async () => {
|
|
79
|
+
@Injectable({ registry })
|
|
80
|
+
class TestService {
|
|
81
|
+
public id = Math.random()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const instance1 = await container.get(TestService)
|
|
85
|
+
const instance2 = await container.get(TestService)
|
|
86
|
+
|
|
87
|
+
expect(instance1).toBe(instance2)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('Transient scope', () => {
|
|
92
|
+
it('should return different instances for transient services', async () => {
|
|
93
|
+
@Injectable({ registry, scope: InjectableScope.Transient })
|
|
94
|
+
class TestService {
|
|
95
|
+
public id = Math.random()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const instance1 = await container.get(TestService)
|
|
99
|
+
const instance2 = await container.get(TestService)
|
|
100
|
+
|
|
101
|
+
expect(instance1).toBeInstanceOf(TestService)
|
|
102
|
+
expect(instance2).toBeInstanceOf(TestService)
|
|
103
|
+
expect(instance1).not.toBe(instance2)
|
|
104
|
+
expect(instance1.id).not.toBe(instance2.id)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('Custom injection tokens', () => {
|
|
109
|
+
it('should work with string tokens', async () => {
|
|
110
|
+
const token = InjectionToken.create<TestService>('TestService')
|
|
111
|
+
|
|
112
|
+
@Injectable({ token, registry })
|
|
113
|
+
class TestService {
|
|
114
|
+
public value = 'test'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const instance = await container.get(token)
|
|
118
|
+
expect(instance).toBeInstanceOf(TestService)
|
|
119
|
+
expect(instance.value).toBe('test')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should work with symbol tokens', async () => {
|
|
123
|
+
const token = InjectionToken.create<TestService>(Symbol('TestService'))
|
|
124
|
+
|
|
125
|
+
@Injectable({ token, registry })
|
|
126
|
+
class TestService {
|
|
127
|
+
public value = 'test'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const instance = await container.get(token)
|
|
131
|
+
expect(instance).toBeInstanceOf(TestService)
|
|
132
|
+
expect(instance.value).toBe('test')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should work with custom registry', async () => {
|
|
136
|
+
const customRegistry = new Registry()
|
|
137
|
+
const customContainer = new Container(customRegistry, mockLogger)
|
|
138
|
+
const token = InjectionToken.create<CustomService>('CustomService')
|
|
139
|
+
|
|
140
|
+
@Injectable({ token, registry: customRegistry })
|
|
141
|
+
class CustomService {
|
|
142
|
+
public value = 'custom'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const instance = await customContainer.get(token)
|
|
146
|
+
expect(instance).toBeInstanceOf(CustomService)
|
|
147
|
+
expect(instance.value).toBe('custom')
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('Constructor arguments with schemas', () => {
|
|
152
|
+
it('should work with required schema arguments', async () => {
|
|
153
|
+
const schema = z.object({
|
|
154
|
+
name: z.string(),
|
|
155
|
+
age: z.number(),
|
|
156
|
+
})
|
|
157
|
+
const token = InjectionToken.create<UserService, typeof schema>(
|
|
158
|
+
'UserService',
|
|
159
|
+
schema,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@Injectable({ token, registry })
|
|
163
|
+
class UserService {
|
|
164
|
+
constructor(public readonly config: z.output<typeof schema>) {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const config = { name: 'John', age: 30 }
|
|
168
|
+
const instance = await container.get(token, config)
|
|
169
|
+
|
|
170
|
+
expect(instance).toBeInstanceOf(UserService)
|
|
171
|
+
expect(instance.config).toEqual(config)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should work with optional schema arguments', async () => {
|
|
175
|
+
const schema = z
|
|
176
|
+
.object({
|
|
177
|
+
name: z.string(),
|
|
178
|
+
age: z.number().optional(),
|
|
179
|
+
})
|
|
180
|
+
.optional()
|
|
181
|
+
const token = InjectionToken.create<OptionalService, typeof schema>(
|
|
182
|
+
'OptionalService',
|
|
183
|
+
schema,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@Injectable({ token, registry })
|
|
187
|
+
class OptionalService {
|
|
188
|
+
constructor(public readonly config?: z.output<typeof schema>) {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Test with arguments
|
|
192
|
+
const config = { name: 'John', age: 30 }
|
|
193
|
+
const instance1 = await container.get(token, config)
|
|
194
|
+
expect(instance1).toBeInstanceOf(OptionalService)
|
|
195
|
+
expect(instance1.config).toEqual(config)
|
|
196
|
+
|
|
197
|
+
// Test without arguments
|
|
198
|
+
const instance2 = await container.get(token)
|
|
199
|
+
expect(instance2).toBeInstanceOf(OptionalService)
|
|
200
|
+
expect(instance2.config).toBeUndefined()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should work with complex nested schemas', async () => {
|
|
204
|
+
const schema = z.object({
|
|
205
|
+
user: z.object({
|
|
206
|
+
name: z.string(),
|
|
207
|
+
preferences: z.object({
|
|
208
|
+
theme: z.enum(['light', 'dark']),
|
|
209
|
+
notifications: z.boolean(),
|
|
210
|
+
}),
|
|
211
|
+
}),
|
|
212
|
+
settings: z.array(z.string()),
|
|
213
|
+
})
|
|
214
|
+
const token = InjectionToken.create<ComplexService, typeof schema>(
|
|
215
|
+
'ComplexService',
|
|
216
|
+
schema,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
@Injectable({ token, registry })
|
|
220
|
+
class ComplexService {
|
|
221
|
+
constructor(public readonly config: z.output<typeof schema>) {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const config = {
|
|
225
|
+
user: {
|
|
226
|
+
name: 'Alice',
|
|
227
|
+
preferences: {
|
|
228
|
+
theme: 'dark' as const,
|
|
229
|
+
notifications: true,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
settings: ['setting1', 'setting2'],
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const instance = await container.get(token, config)
|
|
236
|
+
expect(instance).toBeInstanceOf(ComplexService)
|
|
237
|
+
expect(instance.config).toEqual(config)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('Factory decorator scenarios', () => {
|
|
243
|
+
describe('Basic factory', () => {
|
|
244
|
+
it('should work with simple factory', async () => {
|
|
245
|
+
@Factory({ registry })
|
|
246
|
+
class TestFactory implements Factorable<TestService> {
|
|
247
|
+
async create() {
|
|
248
|
+
return new TestService()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@Injectable({ registry })
|
|
253
|
+
class TestService {
|
|
254
|
+
public value = 'created by factory'
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const instance = await container.get(TestFactory)
|
|
258
|
+
expect(instance).toBeInstanceOf(TestService)
|
|
259
|
+
expect(instance.value).toBe('created by factory')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should work with factory returning different instances', async () => {
|
|
263
|
+
@Factory({ scope: InjectableScope.Transient, registry })
|
|
264
|
+
class TestFactory implements Factorable<TestService> {
|
|
265
|
+
async create() {
|
|
266
|
+
return new TestService()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@Injectable({ registry })
|
|
271
|
+
class TestService {
|
|
272
|
+
public id = Math.random()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const instance1 = await container.get(TestFactory)
|
|
276
|
+
const instance2 = await container.get(TestFactory)
|
|
277
|
+
|
|
278
|
+
expect(instance1).toBeInstanceOf(TestService)
|
|
279
|
+
expect(instance2).toBeInstanceOf(TestService)
|
|
280
|
+
expect(instance1.id).not.toBe(instance2.id)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should work with factory using context', async () => {
|
|
284
|
+
@Factory({ registry })
|
|
285
|
+
class ContextFactory implements Factorable<TestService> {
|
|
286
|
+
async create(ctx: FactoryContext) {
|
|
287
|
+
const container = await ctx.inject(Container)
|
|
288
|
+
return new TestService(container)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@Injectable({ registry })
|
|
293
|
+
class TestService {
|
|
294
|
+
constructor(public readonly container?: Container) {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const instance = await container.get(ContextFactory)
|
|
298
|
+
expect(instance).toBeInstanceOf(TestService)
|
|
299
|
+
expect(instance.container).toBe(container)
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('Factory with arguments', () => {
|
|
304
|
+
it('should work with factory requiring arguments', async () => {
|
|
305
|
+
const schema = z.object({
|
|
306
|
+
name: z.string(),
|
|
307
|
+
value: z.number(),
|
|
308
|
+
})
|
|
309
|
+
const token = InjectionToken.create<TestService, typeof schema>(
|
|
310
|
+
'ArgFactory',
|
|
311
|
+
schema,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@Factory({ token, registry })
|
|
315
|
+
// oxlint-disable-next-line no-unused-vars
|
|
316
|
+
class ArgFactory
|
|
317
|
+
implements FactorableWithArgs<TestService, typeof schema>
|
|
318
|
+
{
|
|
319
|
+
async create(ctx: any, args: z.output<typeof schema>) {
|
|
320
|
+
return new TestService(args.name, args.value)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@Injectable({ registry })
|
|
325
|
+
class TestService {
|
|
326
|
+
constructor(
|
|
327
|
+
public readonly name: string,
|
|
328
|
+
public readonly value: number,
|
|
329
|
+
) {}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const args = { name: 'Test', value: 42 }
|
|
333
|
+
const instance = await container.get(token, args)
|
|
334
|
+
|
|
335
|
+
expect(instance).toBeInstanceOf(TestService)
|
|
336
|
+
expect(instance.name).toBe('Test')
|
|
337
|
+
expect(instance.value).toBe(42)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should work with factory and optional arguments', async () => {
|
|
341
|
+
const schema = z
|
|
342
|
+
.object({
|
|
343
|
+
name: z.string(),
|
|
344
|
+
optional: z.string().optional(),
|
|
345
|
+
})
|
|
346
|
+
.optional()
|
|
347
|
+
const token = InjectionToken.create<TestService, typeof schema>(
|
|
348
|
+
'OptionalArgFactory',
|
|
349
|
+
schema,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
@Factory({ token, registry })
|
|
353
|
+
// oxlint-disable-next-line no-unused-vars
|
|
354
|
+
class OptionalArgFactory
|
|
355
|
+
implements FactorableWithArgs<TestService, typeof schema>
|
|
356
|
+
{
|
|
357
|
+
async create(ctx: any, args: z.output<typeof schema>) {
|
|
358
|
+
return new TestService(args?.name || 'default', args?.optional)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@Injectable({ registry })
|
|
363
|
+
class TestService {
|
|
364
|
+
constructor(
|
|
365
|
+
public readonly name: string,
|
|
366
|
+
public readonly optional?: string,
|
|
367
|
+
) {}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Test with arguments
|
|
371
|
+
const args = { name: 'Test', optional: 'value' }
|
|
372
|
+
const instance1 = await container.get(token, args)
|
|
373
|
+
expect(instance1.name).toBe('Test')
|
|
374
|
+
expect(instance1.optional).toBe('value')
|
|
375
|
+
|
|
376
|
+
// Test without arguments
|
|
377
|
+
const instance2 = await container.get(token)
|
|
378
|
+
expect(instance2.name).toBe('default')
|
|
379
|
+
expect(instance2.optional).toBeUndefined()
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
describe('Factory with custom tokens', () => {
|
|
384
|
+
it('should work with factory using custom token', async () => {
|
|
385
|
+
const token = InjectionToken.create<TestService>(
|
|
386
|
+
Symbol('CustomFactory'),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
@Factory({ token, registry })
|
|
390
|
+
// oxlint-disable-next-line no-unused-vars
|
|
391
|
+
class CustomFactory implements Factorable<TestService> {
|
|
392
|
+
async create() {
|
|
393
|
+
return new TestService('custom')
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@Injectable({ registry })
|
|
398
|
+
class TestService {
|
|
399
|
+
constructor(public readonly type: string) {}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const instance = await container.get(token)
|
|
403
|
+
expect(instance).toBeInstanceOf(TestService)
|
|
404
|
+
expect(instance.type).toBe('custom')
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
describe('Injection token types', () => {
|
|
410
|
+
describe('Bound injection tokens', () => {
|
|
411
|
+
it('should work with bound tokens', async () => {
|
|
412
|
+
const schema = z.object({
|
|
413
|
+
config: z.string(),
|
|
414
|
+
})
|
|
415
|
+
const token = InjectionToken.create<ConfigService, typeof schema>(
|
|
416
|
+
'ConfigService',
|
|
417
|
+
schema,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
@Injectable({ token, registry })
|
|
421
|
+
class ConfigService {
|
|
422
|
+
constructor(public readonly config: z.output<typeof schema>) {}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const boundToken = InjectionToken.bound(token, {
|
|
426
|
+
config: 'bound-value',
|
|
427
|
+
})
|
|
428
|
+
const instance = await container.get(boundToken)
|
|
429
|
+
|
|
430
|
+
expect(instance).toBeInstanceOf(ConfigService)
|
|
431
|
+
expect(instance.config).toEqual({ config: 'bound-value' })
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('should work with bound tokens and factories', async () => {
|
|
435
|
+
const schema = z.object({
|
|
436
|
+
factory: z.string(),
|
|
437
|
+
})
|
|
438
|
+
const token = InjectionToken.create<TestService, typeof schema>(
|
|
439
|
+
'FactoryService',
|
|
440
|
+
schema,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
@Factory({ token, registry })
|
|
444
|
+
// oxlint-disable-next-line no-unused-vars
|
|
445
|
+
class FactoryService
|
|
446
|
+
implements FactorableWithArgs<TestService, typeof schema>
|
|
447
|
+
{
|
|
448
|
+
async create(ctx: any, args: z.output<typeof schema>) {
|
|
449
|
+
return new TestService(args.factory)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
@Injectable({ registry })
|
|
454
|
+
class TestService {
|
|
455
|
+
constructor(public readonly factory: string) {}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const boundToken = InjectionToken.bound(token, {
|
|
459
|
+
factory: 'bound-factory',
|
|
460
|
+
})
|
|
461
|
+
const instance = await container.get(boundToken)
|
|
462
|
+
|
|
463
|
+
expect(instance).toBeInstanceOf(TestService)
|
|
464
|
+
expect(instance.factory).toBe('bound-factory')
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
describe('Factory injection tokens', () => {
|
|
469
|
+
it('should work with factory injection tokens', async () => {
|
|
470
|
+
const schema = z.object({
|
|
471
|
+
data: z.string(),
|
|
472
|
+
})
|
|
473
|
+
const token = InjectionToken.create<DataService, typeof schema>(
|
|
474
|
+
'DataService',
|
|
475
|
+
schema,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
@Injectable({ token, registry })
|
|
479
|
+
class DataService {
|
|
480
|
+
constructor(public readonly data: z.output<typeof schema>) {}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const factoryToken = InjectionToken.factory(token, async () => ({
|
|
484
|
+
data: 'factory-generated',
|
|
485
|
+
}))
|
|
486
|
+
|
|
487
|
+
const instance = await container.get(factoryToken)
|
|
488
|
+
expect(instance).toBeInstanceOf(DataService)
|
|
489
|
+
expect(instance.data).toEqual({ data: 'factory-generated' })
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('should resolve factory tokens only once', async () => {
|
|
493
|
+
const schema = z.object({
|
|
494
|
+
counter: z.number(),
|
|
495
|
+
})
|
|
496
|
+
const token = InjectionToken.create<CounterService, typeof schema>(
|
|
497
|
+
'CounterService',
|
|
498
|
+
schema,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
@Injectable({ token, registry })
|
|
502
|
+
class CounterService {
|
|
503
|
+
constructor(public readonly counter: z.output<typeof schema>) {}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let callCount = 0
|
|
507
|
+
const factoryToken = InjectionToken.factory(token, async () => {
|
|
508
|
+
callCount++
|
|
509
|
+
return { counter: callCount }
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
const instance1 = await container.get(factoryToken)
|
|
513
|
+
const instance2 = await container.get(factoryToken)
|
|
514
|
+
|
|
515
|
+
expect(instance1).toBeInstanceOf(CounterService)
|
|
516
|
+
expect(instance2).toBeInstanceOf(CounterService)
|
|
517
|
+
expect(callCount).toBe(1) // Factory should only be called once
|
|
518
|
+
expect(instance1.counter).toEqual({ counter: 1 })
|
|
519
|
+
expect(instance2.counter).toEqual({ counter: 1 })
|
|
520
|
+
})
|
|
521
|
+
})
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
describe('Complex dependency injection scenarios', () => {
|
|
525
|
+
it('should handle circular dependencies gracefully', async () => {
|
|
526
|
+
@Injectable({ registry })
|
|
527
|
+
class ServiceA {
|
|
528
|
+
serviceB = asyncInject(ServiceB)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
@Injectable({ registry })
|
|
532
|
+
class ServiceB {
|
|
533
|
+
serviceA = asyncInject(ServiceA)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// This should not throw but handle the circular dependency
|
|
537
|
+
const serviceA = await container.get(ServiceA)
|
|
538
|
+
expect(serviceA).toBeInstanceOf(ServiceA)
|
|
539
|
+
expect(serviceA.serviceB).toBeInstanceOf(Promise)
|
|
540
|
+
const serviceB = await serviceA.serviceB
|
|
541
|
+
expect(serviceB).toBeInstanceOf(ServiceB)
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it('should handle deep dependency chains', async () => {
|
|
545
|
+
@Injectable({ registry })
|
|
546
|
+
class Level1 {
|
|
547
|
+
public value = 'level1'
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
@Injectable({ registry })
|
|
551
|
+
class Level2 {
|
|
552
|
+
level1 = inject(Level1)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
@Injectable({ registry })
|
|
556
|
+
class Level3 {
|
|
557
|
+
level2 = inject(Level2)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
@Injectable({ registry })
|
|
561
|
+
class Level4 {
|
|
562
|
+
level3 = inject(Level3)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const level4 = await container.get(Level4)
|
|
566
|
+
expect(level4).toBeInstanceOf(Level4)
|
|
567
|
+
|
|
568
|
+
const level3 = await level4.level3
|
|
569
|
+
expect(level3).toBeInstanceOf(Level3)
|
|
570
|
+
|
|
571
|
+
const level2 = await level3.level2
|
|
572
|
+
expect(level2).toBeInstanceOf(Level2)
|
|
573
|
+
|
|
574
|
+
const level1 = await level2.level1
|
|
575
|
+
expect(level1).toBeInstanceOf(Level1)
|
|
576
|
+
expect(level1.value).toBe('level1')
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it('should handle async factory dependencies', async () => {
|
|
580
|
+
@Injectable({ registry })
|
|
581
|
+
class DatabaseService {
|
|
582
|
+
public async connect() {
|
|
583
|
+
return 'connected'
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
@Factory({ registry })
|
|
588
|
+
class DatabaseFactory implements Factorable<DatabaseService> {
|
|
589
|
+
async create() {
|
|
590
|
+
const db = new DatabaseService()
|
|
591
|
+
await db.connect()
|
|
592
|
+
return db
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
@Injectable({ registry })
|
|
597
|
+
class AppService {
|
|
598
|
+
database = asyncInject(DatabaseFactory)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const app = await container.get(AppService)
|
|
602
|
+
const database = await app.database
|
|
603
|
+
expect(database).toBeInstanceOf(DatabaseService)
|
|
604
|
+
})
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
describe('Error handling and edge cases', () => {
|
|
608
|
+
it('should throw error for unregistered service', async () => {
|
|
609
|
+
class UnregisteredService {}
|
|
610
|
+
|
|
611
|
+
await expect(container.get(UnregisteredService)).rejects.toThrow()
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('should throw error for invalid arguments', async () => {
|
|
615
|
+
const schema = z.object({
|
|
616
|
+
required: z.string(),
|
|
617
|
+
})
|
|
618
|
+
const token = InjectionToken.create<RequiredService, typeof schema>(
|
|
619
|
+
'RequiredService',
|
|
620
|
+
schema,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
@Injectable({ token, registry })
|
|
624
|
+
class RequiredService {
|
|
625
|
+
constructor(public readonly config: z.output<typeof schema>) {}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
await expect(container.get(token)).rejects.toThrow()
|
|
629
|
+
// @ts-expect-error This is a test
|
|
630
|
+
await expect(container.get(token, { invalid: 'arg' })).rejects.toThrow()
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it.skip('should handle factory errors', async () => {
|
|
634
|
+
@Factory({ registry })
|
|
635
|
+
class ErrorFactory implements Factorable<TestService> {
|
|
636
|
+
async create() {
|
|
637
|
+
throw new Error('Factory error')
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
@Injectable({ registry })
|
|
642
|
+
class TestService {}
|
|
643
|
+
|
|
644
|
+
await expect(container.get(ErrorFactory)).rejects.toThrow('Factory error')
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
it.skip('should handle async factory errors', async () => {
|
|
648
|
+
@Factory({ registry })
|
|
649
|
+
class AsyncErrorFactory implements Factorable<TestService> {
|
|
650
|
+
async create() {
|
|
651
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
652
|
+
throw new Error('Async factory error')
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
@Injectable({ registry })
|
|
657
|
+
class TestService {}
|
|
658
|
+
|
|
659
|
+
await expect(container.get(AsyncErrorFactory)).rejects.toThrow(
|
|
660
|
+
'Async factory error',
|
|
661
|
+
)
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('should handle invalid schema validation', async () => {
|
|
665
|
+
const schema = z.object({
|
|
666
|
+
email: z.string().email(),
|
|
667
|
+
})
|
|
668
|
+
const token = InjectionToken.create<EmailService, typeof schema>(
|
|
669
|
+
'EmailService',
|
|
670
|
+
schema,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
@Injectable({ token, registry })
|
|
674
|
+
class EmailService {
|
|
675
|
+
constructor(public readonly config: z.output<typeof schema>) {}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
await expect(
|
|
679
|
+
container.get(token, { email: 'invalid-email' }),
|
|
680
|
+
).rejects.toThrow()
|
|
681
|
+
})
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
describe('Service invalidation', () => {
|
|
685
|
+
it('should invalidate singleton services', async () => {
|
|
686
|
+
@Injectable({ registry })
|
|
687
|
+
class TestService {
|
|
688
|
+
public id = Math.random()
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const instance1 = await container.get(TestService)
|
|
692
|
+
await container.invalidate(instance1)
|
|
693
|
+
|
|
694
|
+
const instance2 = await container.get(TestService)
|
|
695
|
+
expect(instance1).not.toBe(instance2)
|
|
696
|
+
expect(instance1.id).not.toBe(instance2.id)
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it('should invalidate services with dependencies', async () => {
|
|
700
|
+
@Injectable({ registry, scope: InjectableScope.Transient })
|
|
701
|
+
class DependencyService {
|
|
702
|
+
public id = Math.random()
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
@Injectable({ registry })
|
|
706
|
+
class MainService {
|
|
707
|
+
dependency = asyncInject(DependencyService)
|
|
708
|
+
public id = Math.random()
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const main1 = await container.get(MainService)
|
|
712
|
+
const dep1 = await main1.dependency
|
|
713
|
+
|
|
714
|
+
await container.invalidate(main1)
|
|
715
|
+
|
|
716
|
+
const main2 = await container.get(MainService)
|
|
717
|
+
const dep2 = await main2.dependency
|
|
718
|
+
|
|
719
|
+
expect(main1).not.toBe(main2)
|
|
720
|
+
expect(dep1).not.toBe(dep2)
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
it('should handle invalidation of non-existent service', async () => {
|
|
724
|
+
const fakeService = { id: 'fake' }
|
|
725
|
+
|
|
726
|
+
// Should not throw
|
|
727
|
+
await expect(container.invalidate(fakeService)).resolves.toBeUndefined()
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
it('should invalidate factory services', async () => {
|
|
731
|
+
@Factory({ scope: InjectableScope.Singleton, registry })
|
|
732
|
+
class TestFactory implements Factorable<TestService> {
|
|
733
|
+
async create() {
|
|
734
|
+
return new TestService()
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
@Injectable({ registry })
|
|
739
|
+
class TestService {
|
|
740
|
+
public id = Math.random()
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const instance1 = await container.get(TestFactory)
|
|
744
|
+
await container.invalidate(instance1)
|
|
745
|
+
|
|
746
|
+
const instance2 = await container.get(TestFactory)
|
|
747
|
+
expect(instance1).not.toBe(instance2)
|
|
748
|
+
expect(instance1.id).not.toBe(instance2.id)
|
|
749
|
+
})
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
describe('Ready method and async operations', () => {
|
|
753
|
+
it('should wait for all pending operations', async () => {
|
|
754
|
+
const deferred = createDeferred<string>()
|
|
755
|
+
|
|
756
|
+
@Factory({ registry })
|
|
757
|
+
class AsyncFactory implements Factorable<TestService> {
|
|
758
|
+
async create() {
|
|
759
|
+
const result = await deferred.promise
|
|
760
|
+
return new TestService(result)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
@Injectable({ registry })
|
|
765
|
+
class TestService {
|
|
766
|
+
constructor(public readonly value: string) {}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const promise = container.get(AsyncFactory)
|
|
770
|
+
|
|
771
|
+
// Should not be ready yet
|
|
772
|
+
await expect(container.ready()).resolves.toBeUndefined()
|
|
773
|
+
|
|
774
|
+
// Resolve the deferred
|
|
775
|
+
deferred.resolve('async-result')
|
|
776
|
+
|
|
777
|
+
const instance = await promise
|
|
778
|
+
expect(instance.value).toBe('async-result')
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('should handle multiple concurrent operations', async () => {
|
|
782
|
+
const deferred1 = createDeferred<string>()
|
|
783
|
+
const deferred2 = createDeferred<string>()
|
|
784
|
+
|
|
785
|
+
@Factory({ registry })
|
|
786
|
+
class AsyncFactory1 implements Factorable<TestService> {
|
|
787
|
+
async create() {
|
|
788
|
+
const result = await deferred1.promise
|
|
789
|
+
return new TestService(result)
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
@Factory({ registry })
|
|
794
|
+
class AsyncFactory2 implements Factorable<TestService> {
|
|
795
|
+
async create() {
|
|
796
|
+
const result = await deferred2.promise
|
|
797
|
+
return new TestService(result)
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
@Injectable({ registry })
|
|
802
|
+
class TestService {
|
|
803
|
+
constructor(public readonly value: string) {}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const promise1 = container.get(AsyncFactory1)
|
|
807
|
+
const promise2 = container.get(AsyncFactory2)
|
|
808
|
+
|
|
809
|
+
// Resolve both
|
|
810
|
+
deferred1.resolve('result1')
|
|
811
|
+
deferred2.resolve('result2')
|
|
812
|
+
|
|
813
|
+
const [instance1, instance2] = await Promise.all([promise1, promise2])
|
|
814
|
+
|
|
815
|
+
expect(instance1.value).toBe('result1')
|
|
816
|
+
expect(instance2.value).toBe('result2')
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
it.skip('should handle factory errors in ready state', async () => {
|
|
820
|
+
const deferred = createDeferred<string>()
|
|
821
|
+
|
|
822
|
+
@Factory({ registry })
|
|
823
|
+
class ErrorFactory implements Factorable<TestService> {
|
|
824
|
+
async create() {
|
|
825
|
+
const result = await deferred.promise
|
|
826
|
+
if (result === 'error') {
|
|
827
|
+
throw new Error('Factory error')
|
|
828
|
+
}
|
|
829
|
+
return new TestService(result)
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
@Injectable({ registry })
|
|
834
|
+
class TestService {
|
|
835
|
+
constructor(public readonly value: string) {}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const promise = container.get(ErrorFactory)
|
|
839
|
+
|
|
840
|
+
// Reject the deferred
|
|
841
|
+
deferred.reject(new Error('Deferred error'))
|
|
842
|
+
|
|
843
|
+
await expect(promise).rejects.toThrow('Deferred error')
|
|
844
|
+
})
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
describe('Type safety and overloads', () => {
|
|
848
|
+
it('should work with class type overload', async () => {
|
|
849
|
+
@Injectable({ registry })
|
|
850
|
+
class TestService {
|
|
851
|
+
public value = 'test'
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const instance = await container.get(TestService)
|
|
855
|
+
expect(instance).toBeInstanceOf(TestService)
|
|
856
|
+
expect(instance.value).toBe('test')
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it('should work with token with required schema overload', async () => {
|
|
860
|
+
const schema = z.object({
|
|
861
|
+
name: z.string(),
|
|
862
|
+
})
|
|
863
|
+
const token = InjectionToken.create<RequiredService, typeof schema>(
|
|
864
|
+
'RequiredService',
|
|
865
|
+
schema,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
@Injectable({ token, registry })
|
|
869
|
+
class RequiredService {
|
|
870
|
+
constructor(public readonly config: z.output<typeof schema>) {}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const instance = await container.get(token, { name: 'test' })
|
|
874
|
+
expect(instance).toBeInstanceOf(RequiredService)
|
|
875
|
+
expect(instance.config.name).toBe('test')
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
it('should work with token with optional schema overload', async () => {
|
|
879
|
+
const schema = z
|
|
880
|
+
.object({
|
|
881
|
+
name: z.string(),
|
|
882
|
+
})
|
|
883
|
+
.optional()
|
|
884
|
+
const token = InjectionToken.create<OptionalService, typeof schema>(
|
|
885
|
+
'OptionalService',
|
|
886
|
+
schema,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
@Injectable({ token, registry })
|
|
890
|
+
class OptionalService {
|
|
891
|
+
constructor(public readonly config?: z.output<typeof schema>) {}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const instance = await container.get(token)
|
|
895
|
+
expect(instance).toBeInstanceOf(OptionalService)
|
|
896
|
+
expect(instance.config).toBeUndefined()
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
it('should work with token with no schema overload', async () => {
|
|
900
|
+
const token = InjectionToken.create<NoSchemaService>('NoSchemaService')
|
|
901
|
+
|
|
902
|
+
@Injectable({ token, registry })
|
|
903
|
+
class NoSchemaService {
|
|
904
|
+
public value = 'no-schema'
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const instance = await container.get(token)
|
|
908
|
+
expect(instance).toBeInstanceOf(NoSchemaService)
|
|
909
|
+
expect(instance.value).toBe('no-schema')
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
it('should work with bound injection token overload', async () => {
|
|
913
|
+
const schema = z.object({
|
|
914
|
+
value: z.string(),
|
|
915
|
+
})
|
|
916
|
+
const token = InjectionToken.create<BoundService, typeof schema>(
|
|
917
|
+
'BoundService',
|
|
918
|
+
schema,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
@Injectable({ token, registry })
|
|
922
|
+
class BoundService {
|
|
923
|
+
constructor(public readonly config: z.output<typeof schema>) {}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const boundToken = InjectionToken.bound(token, { value: 'bound' })
|
|
927
|
+
const instance = await container.get(boundToken)
|
|
928
|
+
|
|
929
|
+
expect(instance).toBeInstanceOf(BoundService)
|
|
930
|
+
expect(instance.config.value).toBe('bound')
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
it('should work with factory injection token overload', async () => {
|
|
934
|
+
const schema = z.object({
|
|
935
|
+
data: z.string(),
|
|
936
|
+
})
|
|
937
|
+
const token = InjectionToken.create<FactoryService, typeof schema>(
|
|
938
|
+
'FactoryService',
|
|
939
|
+
schema,
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
@Injectable({ token, registry })
|
|
943
|
+
class FactoryService {
|
|
944
|
+
constructor(public readonly config: z.output<typeof schema>) {}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const factoryToken = InjectionToken.factory(token, async () => ({
|
|
948
|
+
data: 'factory-data',
|
|
949
|
+
}))
|
|
950
|
+
|
|
951
|
+
const instance = await container.get(factoryToken)
|
|
952
|
+
expect(instance).toBeInstanceOf(FactoryService)
|
|
953
|
+
expect(instance.config.data).toBe('factory-data')
|
|
954
|
+
})
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
describe('Performance and memory', () => {
|
|
958
|
+
it('should handle large number of services efficiently', async () => {
|
|
959
|
+
const services: any[] = []
|
|
960
|
+
|
|
961
|
+
// Create 100 services
|
|
962
|
+
for (let i = 0; i < 100; i++) {
|
|
963
|
+
@Injectable({ registry })
|
|
964
|
+
class TestService {
|
|
965
|
+
public id = i
|
|
966
|
+
}
|
|
967
|
+
services.push(TestService)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Get all services
|
|
971
|
+
const instances = await Promise.all(
|
|
972
|
+
services.map((Service) => container.get(Service)),
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
expect(instances).toHaveLength(100)
|
|
976
|
+
instances.forEach((instance, index) => {
|
|
977
|
+
expect(instance.id).toBe(index)
|
|
978
|
+
})
|
|
979
|
+
})
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
describe('inject scenarios', () => {
|
|
983
|
+
describe('Singleton scope with inject', () => {
|
|
984
|
+
it('should return same instances for singleton services with inject', async () => {
|
|
985
|
+
@Injectable({ registry })
|
|
986
|
+
class SingletonService {
|
|
987
|
+
public id = Math.random()
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
@Injectable({ registry })
|
|
991
|
+
class ServiceWithSyncInject {
|
|
992
|
+
singletonService = inject(SingletonService)
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const instance1 = await container.get(ServiceWithSyncInject)
|
|
996
|
+
const instance2 = await container.get(ServiceWithSyncInject)
|
|
997
|
+
|
|
998
|
+
expect(instance1).toBeInstanceOf(ServiceWithSyncInject)
|
|
999
|
+
expect(instance2).toBeInstanceOf(ServiceWithSyncInject)
|
|
1000
|
+
expect(instance1).toBe(instance2) // ServiceWithSyncInject is singleton
|
|
1001
|
+
|
|
1002
|
+
// The singleton service should be the same instance
|
|
1003
|
+
expect(instance1.singletonService).toBeInstanceOf(SingletonService)
|
|
1004
|
+
expect(instance2.singletonService).toBeInstanceOf(SingletonService)
|
|
1005
|
+
expect(instance1.singletonService).toBe(instance2.singletonService)
|
|
1006
|
+
expect(instance1.singletonService.id).toBe(
|
|
1007
|
+
instance2.singletonService.id,
|
|
1008
|
+
)
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
it('should handle nested singleton services with inject', async () => {
|
|
1012
|
+
@Injectable({ registry })
|
|
1013
|
+
class Level1Singleton {
|
|
1014
|
+
public id = Math.random()
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
@Injectable({ registry })
|
|
1018
|
+
class Level2Singleton {
|
|
1019
|
+
level1 = inject(Level1Singleton)
|
|
1020
|
+
public id = Math.random()
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
@Injectable({ registry })
|
|
1024
|
+
class RootService {
|
|
1025
|
+
level2 = inject(Level2Singleton)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const root1 = await container.get(RootService)
|
|
1029
|
+
const root2 = await container.get(RootService)
|
|
1030
|
+
|
|
1031
|
+
expect(root1).toBe(root2) // RootService is singleton
|
|
1032
|
+
|
|
1033
|
+
// Level2 should be the same instances
|
|
1034
|
+
expect(root1.level2).toBe(root2.level2)
|
|
1035
|
+
expect(root1.level2.id).toBe(root2.level2.id)
|
|
1036
|
+
|
|
1037
|
+
// Level1 should also be the same instances
|
|
1038
|
+
expect(root1.level2.level1).toBe(root2.level2.level1)
|
|
1039
|
+
expect(root1.level2.level1.id).toBe(root2.level2.level1.id)
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
it('should handle mixed singleton services with inject', async () => {
|
|
1043
|
+
@Injectable({ registry })
|
|
1044
|
+
class SingletonService1 {
|
|
1045
|
+
public id = Math.random()
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
@Injectable({ registry })
|
|
1049
|
+
class SingletonService2 {
|
|
1050
|
+
singleton1 = inject(SingletonService1)
|
|
1051
|
+
public id = Math.random()
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
@Injectable({ registry })
|
|
1055
|
+
class MixedService {
|
|
1056
|
+
singleton2 = inject(SingletonService2)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const mixed1 = await container.get(MixedService)
|
|
1060
|
+
const mixed2 = await container.get(MixedService)
|
|
1061
|
+
|
|
1062
|
+
expect(mixed1).toBe(mixed2) // MixedService is singleton
|
|
1063
|
+
|
|
1064
|
+
// SingletonService2 should be the same instances
|
|
1065
|
+
expect(mixed1.singleton2).toBe(mixed2.singleton2)
|
|
1066
|
+
expect(mixed1.singleton2.id).toBe(mixed2.singleton2.id)
|
|
1067
|
+
|
|
1068
|
+
// SingletonService1 should also be the same instance
|
|
1069
|
+
expect(mixed1.singleton2.singleton1).toBe(mixed2.singleton2.singleton1)
|
|
1070
|
+
expect(mixed1.singleton2.singleton1.id).toBe(
|
|
1071
|
+
mixed2.singleton2.singleton1.id,
|
|
1072
|
+
)
|
|
1073
|
+
})
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
describe('inject with invalidation', () => {
|
|
1077
|
+
it('should invalidate singleton services accessed via inject', async () => {
|
|
1078
|
+
@Injectable({ registry })
|
|
1079
|
+
class SingletonService {
|
|
1080
|
+
public id = Math.random()
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
@Injectable({ registry })
|
|
1084
|
+
class ServiceWithSyncInject {
|
|
1085
|
+
singletonService = inject(SingletonService)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const instance1 = await container.get(ServiceWithSyncInject)
|
|
1089
|
+
const singleton1 = instance1.singletonService
|
|
1090
|
+
|
|
1091
|
+
// Invalidate the singleton service
|
|
1092
|
+
await container.invalidate(singleton1)
|
|
1093
|
+
|
|
1094
|
+
const instance2 = await container.get(ServiceWithSyncInject)
|
|
1095
|
+
const singleton2 = instance2.singletonService
|
|
1096
|
+
|
|
1097
|
+
// Should get a new singleton instance
|
|
1098
|
+
expect(singleton1).not.toBe(singleton2)
|
|
1099
|
+
expect(singleton1.id).not.toBe(singleton2.id)
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
it('should invalidate services with nested inject dependencies', async () => {
|
|
1103
|
+
@Injectable({ registry })
|
|
1104
|
+
class Level1Service {
|
|
1105
|
+
public id = Math.random()
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
@Injectable({ registry })
|
|
1109
|
+
class Level2Service {
|
|
1110
|
+
level1 = inject(Level1Service)
|
|
1111
|
+
public id = Math.random()
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
@Injectable({ registry })
|
|
1115
|
+
class RootService {
|
|
1116
|
+
level2 = inject(Level2Service)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const root1 = await container.get(RootService)
|
|
1120
|
+
const level2_1 = root1.level2
|
|
1121
|
+
const level1_1 = level2_1.level1
|
|
1122
|
+
|
|
1123
|
+
// Invalidate the root service
|
|
1124
|
+
await container.invalidate(level1_1)
|
|
1125
|
+
|
|
1126
|
+
const root2 = await container.get(RootService)
|
|
1127
|
+
const level2_2 = root2.level2
|
|
1128
|
+
const level1_2 = level2_2.level1
|
|
1129
|
+
|
|
1130
|
+
// All should be new instances
|
|
1131
|
+
expect(root1).not.toBe(root2)
|
|
1132
|
+
expect(level2_1).not.toBe(level2_2)
|
|
1133
|
+
expect(level1_1).not.toBe(level1_2)
|
|
1134
|
+
expect(level2_1.id).not.toBe(level2_2.id)
|
|
1135
|
+
expect(level1_1.id).not.toBe(level1_2.id)
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
it('should handle invalidation of services with mixed inject and asyncInject', async () => {
|
|
1139
|
+
@Injectable({ registry })
|
|
1140
|
+
class AsyncService {
|
|
1141
|
+
public id = Math.random()
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
@Injectable({ registry })
|
|
1145
|
+
class SyncService {
|
|
1146
|
+
public id = Math.random()
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
@Injectable({ registry })
|
|
1150
|
+
class MixedService {
|
|
1151
|
+
asyncService = asyncInject(AsyncService)
|
|
1152
|
+
syncService = inject(SyncService)
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const mixed1 = await container.get(MixedService)
|
|
1156
|
+
const async1 = await mixed1.asyncService
|
|
1157
|
+
const sync1 = mixed1.syncService
|
|
1158
|
+
|
|
1159
|
+
// Invalidate the mixed service
|
|
1160
|
+
await container.invalidate(mixed1)
|
|
1161
|
+
|
|
1162
|
+
const mixed2 = await container.get(MixedService)
|
|
1163
|
+
const async2 = await mixed2.asyncService
|
|
1164
|
+
const sync2 = mixed2.syncService
|
|
1165
|
+
|
|
1166
|
+
// All should be new instances
|
|
1167
|
+
expect(mixed1).not.toBe(mixed2)
|
|
1168
|
+
expect(async1).toBe(async2)
|
|
1169
|
+
expect(sync1).toBe(sync2)
|
|
1170
|
+
})
|
|
1171
|
+
|
|
1172
|
+
it('should handle invalidation of factory services accessed via inject', async () => {
|
|
1173
|
+
@Injectable({ registry })
|
|
1174
|
+
class TestService {
|
|
1175
|
+
public id = Math.random()
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
@Factory({ registry })
|
|
1179
|
+
class TestFactory implements Factorable<TestService> {
|
|
1180
|
+
async create() {
|
|
1181
|
+
return new TestService()
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
@Injectable({ registry })
|
|
1186
|
+
class ServiceWithSyncInject {
|
|
1187
|
+
factoryService = inject(TestFactory)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const instance1 = await container.get(ServiceWithSyncInject)
|
|
1191
|
+
const factory1 = instance1.factoryService
|
|
1192
|
+
|
|
1193
|
+
// Invalidate the factory service
|
|
1194
|
+
await container.invalidate(factory1)
|
|
1195
|
+
|
|
1196
|
+
const instance2 = await container.get(ServiceWithSyncInject)
|
|
1197
|
+
const factory2 = instance2.factoryService
|
|
1198
|
+
|
|
1199
|
+
// Should get a new factory instance
|
|
1200
|
+
expect(factory1).not.toBe(factory2)
|
|
1201
|
+
expect(factory1.id).not.toBe(factory2.id)
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
it('should handle invalidation of services with complex dependency chains using inject', async () => {
|
|
1205
|
+
@Injectable({ registry })
|
|
1206
|
+
class DatabaseService {
|
|
1207
|
+
public id = Math.random()
|
|
1208
|
+
public async connect() {
|
|
1209
|
+
return 'connected'
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
@Injectable({ registry })
|
|
1214
|
+
class CacheService {
|
|
1215
|
+
public id = Math.random()
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
@Injectable({ registry })
|
|
1219
|
+
class UserService {
|
|
1220
|
+
database = inject(DatabaseService)
|
|
1221
|
+
cache = inject(CacheService)
|
|
1222
|
+
public id = Math.random()
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
@Injectable({ registry })
|
|
1226
|
+
class AuthService {
|
|
1227
|
+
userService = inject(UserService)
|
|
1228
|
+
public id = Math.random()
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
@Injectable({ registry })
|
|
1232
|
+
class AppService {
|
|
1233
|
+
authService = inject(AuthService)
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const app1 = await container.get(AppService)
|
|
1237
|
+
const auth1 = app1.authService
|
|
1238
|
+
const user1 = auth1.userService
|
|
1239
|
+
const db1 = user1.database
|
|
1240
|
+
const cache1 = user1.cache
|
|
1241
|
+
|
|
1242
|
+
// Invalidate the user service
|
|
1243
|
+
await container.invalidate(user1)
|
|
1244
|
+
|
|
1245
|
+
const app2 = await container.get(AppService)
|
|
1246
|
+
const auth2 = app2.authService
|
|
1247
|
+
const user2 = auth2.userService
|
|
1248
|
+
const db2 = user2.database
|
|
1249
|
+
const cache2 = user2.cache
|
|
1250
|
+
|
|
1251
|
+
// User service should be new instance but its dependencies should be the same
|
|
1252
|
+
expect(user1).not.toBe(user2)
|
|
1253
|
+
expect(db1).toBe(db2)
|
|
1254
|
+
expect(cache1).toBe(cache2)
|
|
1255
|
+
expect(user1.id).not.toBe(user2.id)
|
|
1256
|
+
expect(db1.id).toBe(db2.id)
|
|
1257
|
+
expect(cache1.id).toBe(cache2.id)
|
|
1258
|
+
|
|
1259
|
+
// Auth service should also be new since it depends on user service
|
|
1260
|
+
expect(auth1).not.toBe(auth2)
|
|
1261
|
+
expect(auth1.id).not.toBe(auth2.id)
|
|
1262
|
+
|
|
1263
|
+
// App service should also be new since it depends on auth service
|
|
1264
|
+
expect(app1).not.toBe(app2)
|
|
1265
|
+
})
|
|
1266
|
+
})
|
|
1267
|
+
|
|
1268
|
+
describe('inject error handling', () => {
|
|
1269
|
+
it('should handle unregistered services with inject', async () => {
|
|
1270
|
+
class UnregisteredService {}
|
|
1271
|
+
|
|
1272
|
+
@Injectable({ registry })
|
|
1273
|
+
class ServiceWithUnregistered {
|
|
1274
|
+
unregistered = inject(UnregisteredService)
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// This should throw during instantiation, not during get()
|
|
1278
|
+
await expect(container.get(ServiceWithUnregistered)).rejects.toThrow()
|
|
1279
|
+
})
|
|
1280
|
+
})
|
|
1281
|
+
})
|
|
1282
|
+
describe('custom injectors', () => {
|
|
1283
|
+
it('should work with custom injectors', async () => {
|
|
1284
|
+
const injectors = getInjectors()
|
|
1285
|
+
const container = new Container(registry, mockLogger, injectors)
|
|
1286
|
+
expect(container).toBeInstanceOf(Container)
|
|
1287
|
+
const { inject } = injectors
|
|
1288
|
+
@Injectable({ registry })
|
|
1289
|
+
class TestService {
|
|
1290
|
+
test = 'a'
|
|
1291
|
+
}
|
|
1292
|
+
@Injectable({ registry })
|
|
1293
|
+
class TestService2 {
|
|
1294
|
+
test = inject(TestService)
|
|
1295
|
+
}
|
|
1296
|
+
const instance = await container.get(TestService2)
|
|
1297
|
+
expect(instance).toBeInstanceOf(TestService2)
|
|
1298
|
+
expect(instance.test.test).toBe('a')
|
|
1299
|
+
})
|
|
1300
|
+
})
|
|
1301
|
+
})
|