@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.
Files changed (62) hide show
  1. package/README.md +299 -38
  2. package/docs/README.md +121 -48
  3. package/docs/api-reference.md +763 -0
  4. package/docs/container.md +274 -0
  5. package/docs/examples/basic-usage.mts +97 -0
  6. package/docs/examples/factory-pattern.mts +318 -0
  7. package/docs/examples/injection-tokens.mts +225 -0
  8. package/docs/examples/request-scope-example.mts +254 -0
  9. package/docs/examples/service-lifecycle.mts +359 -0
  10. package/docs/factory.md +584 -0
  11. package/docs/getting-started.md +308 -0
  12. package/docs/injectable.md +496 -0
  13. package/docs/injection-tokens.md +400 -0
  14. package/docs/lifecycle.md +539 -0
  15. package/docs/scopes.md +749 -0
  16. package/lib/_tsup-dts-rollup.d.mts +490 -145
  17. package/lib/_tsup-dts-rollup.d.ts +490 -145
  18. package/lib/index.d.mts +26 -12
  19. package/lib/index.d.ts +26 -12
  20. package/lib/index.js +993 -462
  21. package/lib/index.js.map +1 -1
  22. package/lib/index.mjs +983 -453
  23. package/lib/index.mjs.map +1 -1
  24. package/package.json +2 -2
  25. package/project.json +10 -2
  26. package/src/__tests__/container.spec.mts +1301 -0
  27. package/src/__tests__/factory.spec.mts +137 -0
  28. package/src/__tests__/injectable.spec.mts +32 -88
  29. package/src/__tests__/injection-token.spec.mts +333 -17
  30. package/src/__tests__/request-scope.spec.mts +263 -0
  31. package/src/__type-tests__/factory.spec-d.mts +65 -0
  32. package/src/__type-tests__/inject.spec-d.mts +27 -28
  33. package/src/__type-tests__/injectable.spec-d.mts +42 -206
  34. package/src/container.mts +167 -0
  35. package/src/decorators/factory.decorator.mts +79 -0
  36. package/src/decorators/index.mts +1 -0
  37. package/src/decorators/injectable.decorator.mts +6 -56
  38. package/src/enums/injectable-scope.enum.mts +5 -1
  39. package/src/event-emitter.mts +18 -20
  40. package/src/factory-context.mts +2 -10
  41. package/src/index.mts +3 -2
  42. package/src/injection-token.mts +19 -4
  43. package/src/injector.mts +8 -20
  44. package/src/interfaces/factory.interface.mts +3 -3
  45. package/src/interfaces/index.mts +2 -0
  46. package/src/interfaces/on-service-destroy.interface.mts +3 -0
  47. package/src/interfaces/on-service-init.interface.mts +3 -0
  48. package/src/registry.mts +7 -16
  49. package/src/request-context-holder.mts +145 -0
  50. package/src/service-instantiator.mts +158 -0
  51. package/src/service-locator-event-bus.mts +0 -28
  52. package/src/service-locator-instance-holder.mts +27 -16
  53. package/src/service-locator-manager.mts +84 -0
  54. package/src/service-locator.mts +548 -393
  55. package/src/utils/defer.mts +73 -0
  56. package/src/utils/get-injectors.mts +91 -78
  57. package/src/utils/index.mts +2 -0
  58. package/src/utils/types.mts +52 -0
  59. package/docs/concepts/injectable.md +0 -182
  60. package/docs/concepts/injection-token.md +0 -145
  61. package/src/proxy-service-locator.mts +0 -83
  62. package/src/resolve-service.mts +0 -41
@@ -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 { Factory, FactoryWithArgs } from '../interfaces/index.mjs'
4
+ import type { Factorable, FactorableWithArgs } from '../interfaces/index.mjs'
5
5
 
6
- import { Injectable } from '../decorators/index.mjs'
7
- import { InjectableType } from '../enums/index.mjs'
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 inject(Test)
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 inject(token, {
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
- @Injectable({
49
+ @Factory({
47
50
  token,
48
- type: InjectableType.Factory,
49
51
  })
50
- class Test implements Factory<string> {
52
+ class Test implements Factorable<string> {
51
53
  async create() {
52
54
  return 'foo'
53
55
  }
54
56
  }
55
57
 
56
- const value = await inject(Test)
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
- @Injectable({
68
+ @Factory({
67
69
  token,
68
- type: InjectableType.Factory,
69
70
  })
70
- class Test implements FactoryWithArgs<string, typeof schema> {
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 inject(token, {
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 inject(boundToken)
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 inject(factoryInjectionToken)
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
+ })