@navios/di 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +490 -145
- package/lib/_tsup-dts-rollup.d.ts +490 -145
- package/lib/index.d.mts +26 -12
- package/lib/index.d.ts +26 -12
- package/lib/index.js +993 -462
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +983 -453
- 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 +263 -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 +145 -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
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
2
|
import { z } from 'zod/v4'
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { Factorable, FactorableWithArgs } from '../interfaces/index.mjs'
|
|
5
5
|
|
|
6
|
-
import { Injectable } from '../decorators/index.mjs'
|
|
7
|
-
import {
|
|
6
|
+
import { Factory, Injectable } from '../decorators/index.mjs'
|
|
7
|
+
import { Container } from '../index.mjs'
|
|
8
8
|
import { InjectionToken } from '../injection-token.mjs'
|
|
9
|
-
import { inject } from '../injector.mjs'
|
|
10
9
|
|
|
11
10
|
describe('InjectToken', () => {
|
|
11
|
+
let container: Container
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
container = new Container()
|
|
14
|
+
})
|
|
12
15
|
it('should work with class', async () => {
|
|
13
16
|
const token = InjectionToken.create('Test')
|
|
14
17
|
@Injectable({
|
|
@@ -16,7 +19,7 @@ describe('InjectToken', () => {
|
|
|
16
19
|
})
|
|
17
20
|
class Test {}
|
|
18
21
|
|
|
19
|
-
const value = await
|
|
22
|
+
const value = await container.get(Test)
|
|
20
23
|
expect(value).toBeInstanceOf(Test)
|
|
21
24
|
})
|
|
22
25
|
|
|
@@ -34,7 +37,7 @@ describe('InjectToken', () => {
|
|
|
34
37
|
return 'foo'
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
|
-
const value = await
|
|
40
|
+
const value = await container.get(token, {
|
|
38
41
|
test: 'test',
|
|
39
42
|
})
|
|
40
43
|
|
|
@@ -43,17 +46,16 @@ describe('InjectToken', () => {
|
|
|
43
46
|
|
|
44
47
|
it('should work with factory', async () => {
|
|
45
48
|
const token = InjectionToken.create<string>('Test')
|
|
46
|
-
@
|
|
49
|
+
@Factory({
|
|
47
50
|
token,
|
|
48
|
-
type: InjectableType.Factory,
|
|
49
51
|
})
|
|
50
|
-
class Test implements
|
|
52
|
+
class Test implements Factorable<string> {
|
|
51
53
|
async create() {
|
|
52
54
|
return 'foo'
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
const value = await
|
|
58
|
+
const value = await container.get(Test)
|
|
57
59
|
expect(value).toBe('foo')
|
|
58
60
|
})
|
|
59
61
|
|
|
@@ -63,16 +65,16 @@ describe('InjectToken', () => {
|
|
|
63
65
|
})
|
|
64
66
|
const token = InjectionToken.create<string, typeof schema>('Test', schema)
|
|
65
67
|
|
|
66
|
-
@
|
|
68
|
+
@Factory({
|
|
67
69
|
token,
|
|
68
|
-
type: InjectableType.Factory,
|
|
69
70
|
})
|
|
70
|
-
|
|
71
|
+
// oxlint-disable-next-line no-unused-vars
|
|
72
|
+
class Test implements FactorableWithArgs<string, typeof schema> {
|
|
71
73
|
async create(ctx: any, args: { test: string }) {
|
|
72
74
|
return args.test
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
|
-
const value = await
|
|
77
|
+
const value = await container.get(token, {
|
|
76
78
|
test: 'test',
|
|
77
79
|
})
|
|
78
80
|
|
|
@@ -96,7 +98,7 @@ describe('InjectToken', () => {
|
|
|
96
98
|
return 'foo'
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
|
-
const value = await
|
|
101
|
+
const value = await container.get(boundToken)
|
|
100
102
|
|
|
101
103
|
expect(value).toBeInstanceOf(Test)
|
|
102
104
|
})
|
|
@@ -120,8 +122,322 @@ describe('InjectToken', () => {
|
|
|
120
122
|
return 'foo'
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
|
-
const value = await
|
|
125
|
+
const value = await container.get(factoryInjectionToken)
|
|
124
126
|
|
|
125
127
|
expect(value).toBeInstanceOf(Test)
|
|
126
128
|
})
|
|
129
|
+
|
|
130
|
+
describe('Factory Token Resolution', () => {
|
|
131
|
+
it('should resolve factory token only once and cache the result', async () => {
|
|
132
|
+
let resolveCount = 0
|
|
133
|
+
const token = InjectionToken.create<Test>('Test')
|
|
134
|
+
const factoryInjectionToken = InjectionToken.factory(token, () => {
|
|
135
|
+
resolveCount++
|
|
136
|
+
return Promise.resolve({
|
|
137
|
+
value: 'cached-value',
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
@Injectable({
|
|
142
|
+
token,
|
|
143
|
+
})
|
|
144
|
+
class Test {
|
|
145
|
+
value = 'test-value'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// First resolution
|
|
149
|
+
const value1 = await container.get(factoryInjectionToken)
|
|
150
|
+
expect(value1).toBeInstanceOf(Test)
|
|
151
|
+
expect(resolveCount).toBe(1)
|
|
152
|
+
|
|
153
|
+
// Second resolution should use cached value
|
|
154
|
+
const value2 = await container.get(factoryInjectionToken)
|
|
155
|
+
expect(value2).toBeInstanceOf(Test)
|
|
156
|
+
expect(resolveCount).toBe(1) // Should not increment
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should handle async factory functions', async () => {
|
|
160
|
+
const token = InjectionToken.create<AsyncTest>('AsyncTest')
|
|
161
|
+
const factoryInjectionToken = InjectionToken.factory(token, async () => {
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
163
|
+
return {
|
|
164
|
+
value: 'async-value',
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
@Injectable({
|
|
169
|
+
token,
|
|
170
|
+
})
|
|
171
|
+
class AsyncTest {
|
|
172
|
+
value = 'async-test'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const value = await container.get(factoryInjectionToken)
|
|
176
|
+
expect(value).toBeInstanceOf(AsyncTest)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should handle factory functions that throw errors', async () => {
|
|
180
|
+
const token = InjectionToken.create<ErrorTest>('ErrorTest')
|
|
181
|
+
const factoryInjectionToken = InjectionToken.factory(token, () => {
|
|
182
|
+
throw new Error('Factory error')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
@Injectable({
|
|
186
|
+
token,
|
|
187
|
+
})
|
|
188
|
+
class ErrorTest {
|
|
189
|
+
value = 'error-test'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await expect(container.get(factoryInjectionToken)).rejects.toThrow(
|
|
193
|
+
'Factory error',
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should handle factory functions that return rejected promises', async () => {
|
|
198
|
+
const token = InjectionToken.create<RejectedTest>('RejectedTest')
|
|
199
|
+
const factoryInjectionToken = InjectionToken.factory(token, () => {
|
|
200
|
+
return Promise.reject(new Error('Promise rejection'))
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
@Injectable({
|
|
204
|
+
token,
|
|
205
|
+
})
|
|
206
|
+
class RejectedTest {
|
|
207
|
+
value = 'rejected-test'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await expect(container.get(factoryInjectionToken)).rejects.toThrow(
|
|
211
|
+
'Promise rejection',
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should support inject in factory', async () => {
|
|
216
|
+
@Injectable()
|
|
217
|
+
class InjectTest2 {
|
|
218
|
+
value = 'inject-test'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const token = InjectionToken.create('InjectTest')
|
|
222
|
+
const factoryInjectionToken = InjectionToken.factory(
|
|
223
|
+
token,
|
|
224
|
+
async (ctx) => {
|
|
225
|
+
const injectTest = await ctx.inject(InjectTest2)
|
|
226
|
+
return { value: injectTest.value }
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
@Injectable({
|
|
231
|
+
token,
|
|
232
|
+
})
|
|
233
|
+
class InjectTest {
|
|
234
|
+
value = 'inject-test'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const value = await container.get(factoryInjectionToken)
|
|
238
|
+
expect(value).toBeInstanceOf(InjectTest)
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('Factory Token with Different Schema Types', () => {
|
|
243
|
+
it('should work with optional schema', async () => {
|
|
244
|
+
const schema = z
|
|
245
|
+
.object({
|
|
246
|
+
optional: z.string().optional(),
|
|
247
|
+
})
|
|
248
|
+
.optional()
|
|
249
|
+
const token = InjectionToken.create('OptionalTest', schema)
|
|
250
|
+
const factoryInjectionToken = InjectionToken.factory(token, () =>
|
|
251
|
+
Promise.resolve(undefined),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
@Injectable({
|
|
255
|
+
token,
|
|
256
|
+
})
|
|
257
|
+
class OptionalTest {
|
|
258
|
+
value = 'optional-test'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const value = await container.get(factoryInjectionToken)
|
|
262
|
+
expect(value).toBeInstanceOf(OptionalTest)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should work with record schema', async () => {
|
|
266
|
+
const schema = z.record(z.string(), z.number())
|
|
267
|
+
const token = InjectionToken.create('RecordTest', schema)
|
|
268
|
+
const factoryInjectionToken = InjectionToken.factory(token, () =>
|
|
269
|
+
Promise.resolve({ count: 42, score: 100 }),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
@Injectable({
|
|
273
|
+
token,
|
|
274
|
+
})
|
|
275
|
+
class RecordTest {
|
|
276
|
+
value = 'record-test'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const value = await container.get(factoryInjectionToken)
|
|
280
|
+
expect(value).toBeInstanceOf(RecordTest)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should work with complex nested schema', async () => {
|
|
284
|
+
const schema = z.object({
|
|
285
|
+
user: z.object({
|
|
286
|
+
id: z.number(),
|
|
287
|
+
name: z.string(),
|
|
288
|
+
preferences: z.object({
|
|
289
|
+
theme: z.enum(['light', 'dark']),
|
|
290
|
+
notifications: z.boolean(),
|
|
291
|
+
}),
|
|
292
|
+
}),
|
|
293
|
+
metadata: z.array(z.string()),
|
|
294
|
+
})
|
|
295
|
+
const token = InjectionToken.create('ComplexTest', schema)
|
|
296
|
+
const factoryInjectionToken = InjectionToken.factory(token, () =>
|
|
297
|
+
Promise.resolve({
|
|
298
|
+
user: {
|
|
299
|
+
id: 1,
|
|
300
|
+
name: 'John Doe',
|
|
301
|
+
preferences: {
|
|
302
|
+
theme: 'dark' as const,
|
|
303
|
+
notifications: true,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
metadata: ['tag1', 'tag2'],
|
|
307
|
+
}),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
@Injectable({
|
|
311
|
+
token,
|
|
312
|
+
})
|
|
313
|
+
class ComplexTest {
|
|
314
|
+
value = 'complex-test'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const value = await container.get(factoryInjectionToken)
|
|
318
|
+
expect(value).toBeInstanceOf(ComplexTest)
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe('Factory Token with Multiple Instances', () => {
|
|
323
|
+
it('should create separate instances for different factory tokens', async () => {
|
|
324
|
+
const token1 = InjectionToken.create<Test1>('Test1')
|
|
325
|
+
const token2 = InjectionToken.create<Test2>('Test2')
|
|
326
|
+
|
|
327
|
+
const factoryToken1 = InjectionToken.factory(token1, () =>
|
|
328
|
+
Promise.resolve({
|
|
329
|
+
value: 'value1',
|
|
330
|
+
}),
|
|
331
|
+
)
|
|
332
|
+
const factoryToken2 = InjectionToken.factory(token2, () =>
|
|
333
|
+
Promise.resolve({
|
|
334
|
+
value: 'value2',
|
|
335
|
+
}),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
@Injectable({
|
|
339
|
+
token: token1,
|
|
340
|
+
})
|
|
341
|
+
class Test1 {
|
|
342
|
+
value = 'test1'
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@Injectable({
|
|
346
|
+
token: token2,
|
|
347
|
+
})
|
|
348
|
+
class Test2 {
|
|
349
|
+
value = 'test2'
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const value1 = await container.get(factoryToken1)
|
|
353
|
+
const value2 = await container.get(factoryToken2)
|
|
354
|
+
|
|
355
|
+
expect(value1).toBeInstanceOf(Test1)
|
|
356
|
+
expect(value2).toBeInstanceOf(Test2)
|
|
357
|
+
expect(value1).not.toBe(value2)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('should handle factory tokens with same underlying token but different factories', async () => {
|
|
361
|
+
const token = InjectionToken.create<SharedTest>('SharedTest')
|
|
362
|
+
|
|
363
|
+
const factoryToken1 = InjectionToken.factory(token, () =>
|
|
364
|
+
Promise.resolve({
|
|
365
|
+
value: 'factory1',
|
|
366
|
+
}),
|
|
367
|
+
)
|
|
368
|
+
const factoryToken2 = InjectionToken.factory(token, () =>
|
|
369
|
+
Promise.resolve({
|
|
370
|
+
value: 'factory2',
|
|
371
|
+
}),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
@Injectable({
|
|
375
|
+
token,
|
|
376
|
+
})
|
|
377
|
+
class SharedTest {
|
|
378
|
+
value = 'shared-test'
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const value1 = await container.get(factoryToken1)
|
|
382
|
+
const value2 = await container.get(factoryToken2)
|
|
383
|
+
|
|
384
|
+
expect(value1).toBeInstanceOf(SharedTest)
|
|
385
|
+
expect(value2).toBeInstanceOf(SharedTest)
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe('Factory Token Properties', () => {
|
|
390
|
+
it('should have correct properties', () => {
|
|
391
|
+
const token = InjectionToken.create('PropertyTest')
|
|
392
|
+
const factoryInjectionToken = InjectionToken.factory(token, () =>
|
|
393
|
+
Promise.resolve({
|
|
394
|
+
value: 'test',
|
|
395
|
+
}),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
expect(factoryInjectionToken.id).toBe(token.id)
|
|
399
|
+
expect(factoryInjectionToken.name).toBe(token.name)
|
|
400
|
+
expect(factoryInjectionToken.schema).toBe(token.schema)
|
|
401
|
+
expect(factoryInjectionToken.resolved).toBe(false)
|
|
402
|
+
expect(factoryInjectionToken.value).toBeUndefined()
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('should update resolved property after resolution', async () => {
|
|
406
|
+
const token = InjectionToken.create('ResolvedTest')
|
|
407
|
+
const factoryInjectionToken = InjectionToken.factory(token, () =>
|
|
408
|
+
Promise.resolve({
|
|
409
|
+
value: 'resolved',
|
|
410
|
+
}),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
expect(factoryInjectionToken.resolved).toBe(false)
|
|
414
|
+
expect(factoryInjectionToken.value).toBeUndefined()
|
|
415
|
+
|
|
416
|
+
// @ts-expect-error we are not using the context
|
|
417
|
+
await factoryInjectionToken.resolve()
|
|
418
|
+
|
|
419
|
+
expect(factoryInjectionToken.resolved).toBe(true)
|
|
420
|
+
expect(factoryInjectionToken.value).toMatchObject({ value: 'resolved' })
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('should return cached value on subsequent resolve calls', async () => {
|
|
424
|
+
let resolveCount = 0
|
|
425
|
+
const token = InjectionToken.create('CacheTest')
|
|
426
|
+
const factoryInjectionToken = InjectionToken.factory(token, () => {
|
|
427
|
+
resolveCount++
|
|
428
|
+
return Promise.resolve({
|
|
429
|
+
value: 'cached',
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// @ts-expect-error we are not using the context
|
|
434
|
+
const result1 = await factoryInjectionToken.resolve()
|
|
435
|
+
// @ts-expect-error we are not using the context
|
|
436
|
+
const result2 = await factoryInjectionToken.resolve()
|
|
437
|
+
|
|
438
|
+
expect(result1).toMatchObject({ value: 'cached' })
|
|
439
|
+
expect(result2).toMatchObject({ value: 'cached' })
|
|
440
|
+
expect(resolveCount).toBe(1)
|
|
441
|
+
})
|
|
442
|
+
})
|
|
127
443
|
})
|
|
@@ -0,0 +1,263 @@
|
|
|
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 { Registry } from '../registry.mjs'
|
|
8
|
+
import { createRequestContextHolder } from '../request-context-holder.mjs'
|
|
9
|
+
|
|
10
|
+
describe('Request Scope', () => {
|
|
11
|
+
let container: Container
|
|
12
|
+
let registry: Registry
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
registry = new Registry()
|
|
16
|
+
container = new Container(registry)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('Request-scoped services', () => {
|
|
20
|
+
it('should create different instances for different requests', async () => {
|
|
21
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
22
|
+
class RequestService {
|
|
23
|
+
public readonly requestId = Math.random().toString(36)
|
|
24
|
+
public readonly createdAt = new Date()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Start first request
|
|
28
|
+
container.beginRequest('request-1')
|
|
29
|
+
const instance1a = await container.get(RequestService)
|
|
30
|
+
const instance1b = await container.get(RequestService)
|
|
31
|
+
|
|
32
|
+
// Start second request
|
|
33
|
+
container.beginRequest('request-2')
|
|
34
|
+
const instance2a = await container.get(RequestService)
|
|
35
|
+
const instance2b = await container.get(RequestService)
|
|
36
|
+
|
|
37
|
+
// Within same request, instances should be the same
|
|
38
|
+
expect(instance1a).toBe(instance1b)
|
|
39
|
+
expect(instance2a).toBe(instance2b)
|
|
40
|
+
|
|
41
|
+
// Between different requests, instances should be different
|
|
42
|
+
expect(instance1a).not.toBe(instance2a)
|
|
43
|
+
expect(instance1a.requestId).not.toBe(instance2a.requestId)
|
|
44
|
+
|
|
45
|
+
// Clean up
|
|
46
|
+
await container.endRequest('request-1')
|
|
47
|
+
await container.endRequest('request-2')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should handle request context lifecycle correctly', async () => {
|
|
51
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
52
|
+
class RequestService {
|
|
53
|
+
public readonly requestId = Math.random().toString(36)
|
|
54
|
+
public destroyed = false
|
|
55
|
+
|
|
56
|
+
async onServiceDestroy() {
|
|
57
|
+
this.destroyed = true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Start request
|
|
62
|
+
const requestId = 'test-request'
|
|
63
|
+
container.beginRequest(requestId)
|
|
64
|
+
|
|
65
|
+
const instance = await container.get(RequestService)
|
|
66
|
+
expect(instance.destroyed).toBe(false)
|
|
67
|
+
|
|
68
|
+
// End request should trigger cleanup
|
|
69
|
+
await container.endRequest(requestId)
|
|
70
|
+
expect(instance.destroyed).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should support request metadata', async () => {
|
|
74
|
+
const requestId = 'test-request'
|
|
75
|
+
const metadata = { userId: 'user123', sessionId: 'session456' }
|
|
76
|
+
|
|
77
|
+
container.beginRequest(requestId, metadata)
|
|
78
|
+
|
|
79
|
+
// Note: In a real implementation, you might want to inject metadata
|
|
80
|
+
// For now, we'll just verify the context exists
|
|
81
|
+
const context = container.getCurrentRequestContext()
|
|
82
|
+
expect(context).not.toBeNull()
|
|
83
|
+
expect(context?.requestId).toBe(requestId)
|
|
84
|
+
expect(context?.getMetadata('userId')).toBe('user123')
|
|
85
|
+
expect(context?.getMetadata('sessionId')).toBe('session456')
|
|
86
|
+
|
|
87
|
+
await container.endRequest(requestId)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should handle pre-prepared instances', async () => {
|
|
91
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
92
|
+
class RequestService {
|
|
93
|
+
public readonly requestId = Math.random().toString(36)
|
|
94
|
+
public readonly prePrepared = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const requestId = 'test-request'
|
|
98
|
+
container.beginRequest(requestId)
|
|
99
|
+
|
|
100
|
+
// Getting the instance should be fast (pre-prepared)
|
|
101
|
+
const instance = await container.get(RequestService)
|
|
102
|
+
expect(instance.prePrepared).toBe(true)
|
|
103
|
+
|
|
104
|
+
await container.endRequest(requestId)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should handle mixed scopes correctly', async () => {
|
|
108
|
+
@Injectable({ registry, scope: InjectableScope.Singleton })
|
|
109
|
+
class SingletonService {
|
|
110
|
+
public readonly id = Math.random().toString(36)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
114
|
+
class RequestService {
|
|
115
|
+
public readonly id = Math.random().toString(36)
|
|
116
|
+
singleton: SingletonService = inject(SingletonService)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Injectable({ registry, scope: InjectableScope.Transient })
|
|
120
|
+
class TransientService {
|
|
121
|
+
requestService = inject(RequestService)
|
|
122
|
+
public readonly id = Math.random().toString(36)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Start request
|
|
126
|
+
container.beginRequest('test-request')
|
|
127
|
+
|
|
128
|
+
const requestService1 = await container.get(RequestService)
|
|
129
|
+
const requestService2 = await container.get(RequestService)
|
|
130
|
+
const singleton1 = await container.get(SingletonService)
|
|
131
|
+
const singleton2 = await container.get(SingletonService)
|
|
132
|
+
const transient1 = await container.get(TransientService)
|
|
133
|
+
const transient2 = await container.get(TransientService)
|
|
134
|
+
|
|
135
|
+
// Request-scoped: same instance within request
|
|
136
|
+
expect(requestService1).toBe(requestService2)
|
|
137
|
+
expect(requestService1.singleton).toBe(singleton1)
|
|
138
|
+
|
|
139
|
+
// Singleton: same instance always
|
|
140
|
+
expect(singleton1).toBe(singleton2)
|
|
141
|
+
|
|
142
|
+
// Transient: different instances always
|
|
143
|
+
expect(transient1).not.toBe(transient2)
|
|
144
|
+
expect(transient1.requestService).toBe(transient2.requestService)
|
|
145
|
+
|
|
146
|
+
await container.endRequest('test-request')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should handle nested request contexts', async () => {
|
|
150
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
151
|
+
class RequestService {
|
|
152
|
+
public readonly id = Math.random().toString(36)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Start first request
|
|
156
|
+
container.beginRequest('request-1')
|
|
157
|
+
const instance1 = await container.get(RequestService)
|
|
158
|
+
|
|
159
|
+
// Start second request (nested)
|
|
160
|
+
container.beginRequest('request-2')
|
|
161
|
+
const instance2 = await container.get(RequestService)
|
|
162
|
+
|
|
163
|
+
// Should be different instances
|
|
164
|
+
expect(instance1).not.toBe(instance2)
|
|
165
|
+
|
|
166
|
+
// End second request
|
|
167
|
+
await container.endRequest('request-2')
|
|
168
|
+
|
|
169
|
+
// Get instance from first request again
|
|
170
|
+
const instance1Again = await container.get(RequestService)
|
|
171
|
+
expect(instance1).toBe(instance1Again)
|
|
172
|
+
|
|
173
|
+
// End first request
|
|
174
|
+
await container.endRequest('request-1')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should handle request context switching', async () => {
|
|
178
|
+
@Injectable({ registry, scope: InjectableScope.Request })
|
|
179
|
+
class RequestService {
|
|
180
|
+
public readonly id = Math.random().toString(36)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Start multiple requests
|
|
184
|
+
container.beginRequest('request-1')
|
|
185
|
+
container.beginRequest('request-2')
|
|
186
|
+
container.beginRequest('request-3')
|
|
187
|
+
|
|
188
|
+
// Switch to request-1
|
|
189
|
+
container.setCurrentRequestContext('request-1')
|
|
190
|
+
const instance1 = await container.get(RequestService)
|
|
191
|
+
|
|
192
|
+
// Switch to request-2
|
|
193
|
+
container.setCurrentRequestContext('request-2')
|
|
194
|
+
const instance2 = await container.get(RequestService)
|
|
195
|
+
|
|
196
|
+
// Switch back to request-1
|
|
197
|
+
container.setCurrentRequestContext('request-1')
|
|
198
|
+
const instance1Again = await container.get(RequestService)
|
|
199
|
+
|
|
200
|
+
// Should get same instance for request-1
|
|
201
|
+
expect(instance1).toBe(instance1Again)
|
|
202
|
+
// Should get different instance for request-2
|
|
203
|
+
expect(instance1).not.toBe(instance2)
|
|
204
|
+
|
|
205
|
+
// Clean up all requests
|
|
206
|
+
await container.endRequest('request-1')
|
|
207
|
+
await container.endRequest('request-2')
|
|
208
|
+
await container.endRequest('request-3')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('RequestContextHolder', () => {
|
|
213
|
+
it('should manage instances correctly', () => {
|
|
214
|
+
const holder = createRequestContextHolder('test-request', 100, {
|
|
215
|
+
userId: 'user123',
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
expect(holder.requestId).toBe('test-request')
|
|
219
|
+
expect(holder.priority).toBe(100)
|
|
220
|
+
expect(holder.getMetadata('userId')).toBe('user123')
|
|
221
|
+
|
|
222
|
+
// Add instance
|
|
223
|
+
const mockInstance = { id: 'test-instance' }
|
|
224
|
+
const mockHolder = {
|
|
225
|
+
status: 'Created' as any,
|
|
226
|
+
name: 'test-instance',
|
|
227
|
+
instance: mockInstance,
|
|
228
|
+
creationPromise: null,
|
|
229
|
+
destroyPromise: null,
|
|
230
|
+
type: 'Class' as any,
|
|
231
|
+
scope: 'Request' as any,
|
|
232
|
+
deps: new Set<string>(),
|
|
233
|
+
destroyListeners: [],
|
|
234
|
+
createdAt: Date.now(),
|
|
235
|
+
ttl: Infinity,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
holder.addInstance('test-instance', mockInstance, mockHolder)
|
|
239
|
+
|
|
240
|
+
expect(holder.hasInstance('test-instance')).toBe(true)
|
|
241
|
+
expect(holder.getInstance('test-instance')).toBe(mockInstance)
|
|
242
|
+
expect(holder.getHolder('test-instance')).toBe(mockHolder)
|
|
243
|
+
|
|
244
|
+
// Clear instances
|
|
245
|
+
holder.clear()
|
|
246
|
+
expect(holder.hasInstance('test-instance')).toBe(false)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should handle metadata correctly', () => {
|
|
250
|
+
const holder = createRequestContextHolder('test-request')
|
|
251
|
+
|
|
252
|
+
holder.setMetadata('key1', 'value1')
|
|
253
|
+
holder.setMetadata('key2', 'value2')
|
|
254
|
+
|
|
255
|
+
expect(holder.getMetadata('key1')).toBe('value1')
|
|
256
|
+
expect(holder.getMetadata('key2')).toBe('value2')
|
|
257
|
+
expect(holder.getMetadata('nonexistent')).toBeUndefined()
|
|
258
|
+
|
|
259
|
+
holder.clear()
|
|
260
|
+
expect(holder.getMetadata('key1')).toBeUndefined()
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
})
|