@declaro/core 2.0.0-beta.99 → 2.1.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 (60) hide show
  1. package/dist/browser/index.js +21 -27
  2. package/dist/browser/index.js.map +37 -27
  3. package/dist/browser/scope/index.js +1 -2
  4. package/dist/browser/scope/index.js.map +1 -1
  5. package/dist/bun/index.js +19011 -0
  6. package/dist/bun/index.js.map +132 -0
  7. package/dist/bun/scope/index.js +4 -0
  8. package/dist/bun/scope/index.js.map +9 -0
  9. package/dist/node/index.cjs +2581 -874
  10. package/dist/node/index.cjs.map +36 -27
  11. package/dist/node/index.js +2572 -868
  12. package/dist/node/index.js.map +36 -27
  13. package/dist/node/scope/index.cjs +31 -10
  14. package/dist/node/scope/index.cjs.map +1 -1
  15. package/dist/node/scope/index.js +1 -27
  16. package/dist/node/scope/index.js.map +1 -1
  17. package/dist/ts/context/async-context.d.ts +54 -0
  18. package/dist/ts/context/async-context.d.ts.map +1 -0
  19. package/dist/ts/context/async-context.test.d.ts +2 -0
  20. package/dist/ts/context/async-context.test.d.ts.map +1 -0
  21. package/dist/ts/context/context.circular-deps.test.d.ts +2 -0
  22. package/dist/ts/context/context.circular-deps.test.d.ts.map +1 -0
  23. package/dist/ts/context/context.d.ts +297 -38
  24. package/dist/ts/context/context.d.ts.map +1 -1
  25. package/dist/ts/http/request-context.d.ts.map +1 -1
  26. package/dist/ts/index.d.ts +2 -0
  27. package/dist/ts/index.d.ts.map +1 -1
  28. package/dist/ts/schema/json-schema.d.ts +9 -1
  29. package/dist/ts/schema/json-schema.d.ts.map +1 -1
  30. package/dist/ts/schema/model.d.ts +6 -1
  31. package/dist/ts/schema/model.d.ts.map +1 -1
  32. package/dist/ts/schema/test/mock-model.d.ts +2 -2
  33. package/dist/ts/schema/test/mock-model.d.ts.map +1 -1
  34. package/dist/ts/shared/utils/schema-utils.d.ts +3 -0
  35. package/dist/ts/shared/utils/schema-utils.d.ts.map +1 -0
  36. package/dist/ts/shared/utils/schema-utils.test.d.ts +2 -0
  37. package/dist/ts/shared/utils/schema-utils.test.d.ts.map +1 -0
  38. package/dist/ts/shims/async-local-storage.d.ts +36 -0
  39. package/dist/ts/shims/async-local-storage.d.ts.map +1 -0
  40. package/dist/ts/shims/async-local-storage.test.d.ts +2 -0
  41. package/dist/ts/shims/async-local-storage.test.d.ts.map +1 -0
  42. package/package.json +17 -9
  43. package/src/context/async-context.test.ts +348 -0
  44. package/src/context/async-context.ts +129 -0
  45. package/src/context/context.circular-deps.test.ts +1047 -0
  46. package/src/context/context.test.ts +150 -0
  47. package/src/context/context.ts +493 -55
  48. package/src/http/request-context.ts +1 -3
  49. package/src/index.ts +2 -0
  50. package/src/schema/json-schema.ts +14 -1
  51. package/src/schema/model-schema.test.ts +155 -1
  52. package/src/schema/model.ts +34 -3
  53. package/src/schema/test/mock-model.ts +6 -2
  54. package/src/shared/utils/schema-utils.test.ts +33 -0
  55. package/src/shared/utils/schema-utils.ts +17 -0
  56. package/src/shims/async-local-storage.test.ts +258 -0
  57. package/src/shims/async-local-storage.ts +82 -0
  58. package/dist/ts/schema/entity-schema.test.d.ts +0 -1
  59. package/dist/ts/schema/entity-schema.test.d.ts.map +0 -1
  60. package/src/schema/entity-schema.test.ts +0 -0
@@ -0,0 +1,1047 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { Context, isProxy } from './context'
3
+
4
+ describe('Context - Circular Dependencies', () => {
5
+ it('should handle simple circular dependencies using proxies', () => {
6
+ class ServiceA {
7
+ constructor(public serviceB: ServiceB) {}
8
+
9
+ getName() {
10
+ return 'ServiceA'
11
+ }
12
+
13
+ getOtherName() {
14
+ return this.serviceB.getName()
15
+ }
16
+ }
17
+
18
+ class ServiceB {
19
+ constructor(public serviceA: ServiceA) {}
20
+
21
+ getName() {
22
+ return 'ServiceB'
23
+ }
24
+
25
+ getOtherName() {
26
+ return this.serviceA.getName()
27
+ }
28
+ }
29
+
30
+ type Scope = {
31
+ serviceA: ServiceA
32
+ serviceB: ServiceB
33
+ }
34
+
35
+ const context = new Context<Scope>()
36
+
37
+ context.registerClass('serviceA', ServiceA, ['serviceB'])
38
+ context.registerClass('serviceB', ServiceB, ['serviceA'])
39
+
40
+ const serviceA = context.resolve('serviceA')
41
+ const serviceB = context.resolve('serviceB')
42
+
43
+ expect(serviceA).toBeInstanceOf(ServiceA)
44
+ expect(serviceB).toBeInstanceOf(ServiceB)
45
+ expect(serviceA.getName()).toBe('ServiceA')
46
+ expect(serviceB.getName()).toBe('ServiceB')
47
+
48
+ // Test that circular references work within each resolution
49
+ expect(serviceA.serviceB).toBeInstanceOf(ServiceB)
50
+ expect(serviceB.serviceA).toBeInstanceOf(ServiceA)
51
+ expect(serviceA.getOtherName()).toBe('ServiceB')
52
+ expect(serviceB.getOtherName()).toBe('ServiceA')
53
+
54
+ // For non-singletons, separate resolve calls should create different instances
55
+ expect(serviceA.serviceB).not.toBe(serviceB)
56
+ expect(serviceB.serviceA).not.toBe(serviceA)
57
+ })
58
+
59
+ it('should handle complex circular dependencies with multiple services', () => {
60
+ class ServiceA {
61
+ constructor(public serviceB: ServiceB, public serviceC: ServiceC) {}
62
+
63
+ getName() {
64
+ return 'ServiceA'
65
+ }
66
+ }
67
+
68
+ class ServiceB {
69
+ constructor(public serviceC: ServiceC) {}
70
+
71
+ getName() {
72
+ return 'ServiceB'
73
+ }
74
+ }
75
+
76
+ class ServiceC {
77
+ constructor(public serviceA: ServiceA) {}
78
+
79
+ getName() {
80
+ return 'ServiceC'
81
+ }
82
+ }
83
+
84
+ type Scope = {
85
+ serviceA: ServiceA
86
+ serviceB: ServiceB
87
+ serviceC: ServiceC
88
+ }
89
+
90
+ const context = new Context<Scope>()
91
+
92
+ context.registerClass('serviceA', ServiceA, ['serviceB', 'serviceC'])
93
+ context.registerClass('serviceB', ServiceB, ['serviceC'])
94
+ context.registerClass('serviceC', ServiceC, ['serviceA'])
95
+
96
+ const serviceA = context.resolve('serviceA')
97
+ const serviceB = context.resolve('serviceB')
98
+ const serviceC = context.resolve('serviceC')
99
+
100
+ expect(serviceA).toBeInstanceOf(ServiceA)
101
+ expect(serviceB).toBeInstanceOf(ServiceB)
102
+ expect(serviceC).toBeInstanceOf(ServiceC)
103
+
104
+ // Test that all circular references are properly resolved within each resolution
105
+ expect(serviceA.serviceB).toBeInstanceOf(ServiceB)
106
+ expect(serviceA.serviceC).toBeInstanceOf(ServiceC)
107
+ expect(serviceB.serviceC).toBeInstanceOf(ServiceC)
108
+ expect(serviceC.serviceA).toBeInstanceOf(ServiceA)
109
+
110
+ // For non-singletons, separate resolve calls create different instances
111
+ expect(serviceA.serviceB).not.toBe(serviceB)
112
+ expect(serviceA.serviceC).not.toBe(serviceC)
113
+ expect(serviceB.serviceC).not.toBe(serviceC)
114
+ expect(serviceC.serviceA).not.toBe(serviceA)
115
+
116
+ // Test that methods can be called on the circular dependencies
117
+ expect(serviceA.getName()).toBe('ServiceA')
118
+ expect(serviceB.getName()).toBe('ServiceB')
119
+ expect(serviceC.getName()).toBe('ServiceC')
120
+ })
121
+
122
+ it('should handle circular dependencies with singletons', () => {
123
+ class ServiceA {
124
+ constructor(public serviceB: ServiceB) {}
125
+
126
+ getName() {
127
+ return 'ServiceA'
128
+ }
129
+ }
130
+
131
+ class ServiceB {
132
+ constructor(public serviceA: ServiceA) {}
133
+
134
+ getName() {
135
+ return 'ServiceB'
136
+ }
137
+ }
138
+
139
+ type Scope = {
140
+ serviceA: ServiceA
141
+ serviceB: ServiceB
142
+ }
143
+
144
+ const context = new Context<Scope>()
145
+
146
+ context.registerClass('serviceA', ServiceA, ['serviceB'], { singleton: true })
147
+ context.registerClass('serviceB', ServiceB, ['serviceA'], { singleton: true })
148
+
149
+ const serviceA1 = context.resolve('serviceA')
150
+ const serviceB1 = context.resolve('serviceB')
151
+ const serviceA2 = context.resolve('serviceA')
152
+ const serviceB2 = context.resolve('serviceB')
153
+
154
+ // Test that singletons work with circular dependencies
155
+ expect(serviceA1).toEqual(serviceA2)
156
+ expect(serviceB1).toEqual(serviceB2)
157
+ expect(serviceA1.serviceB).toEqual(serviceB1)
158
+ expect(serviceB1.serviceA).toEqual(serviceA1)
159
+ })
160
+
161
+ it('should handle circular dependencies with factory functions', () => {
162
+ type ServiceA = { name: string; serviceB: ServiceB }
163
+ type ServiceB = { name: string; serviceA: ServiceA }
164
+
165
+ type Scope = {
166
+ serviceA: ServiceA
167
+ serviceB: ServiceB
168
+ }
169
+
170
+ const context = new Context<Scope>()
171
+
172
+ context.registerFactory(
173
+ 'serviceA',
174
+ (serviceB: ServiceB) => ({
175
+ name: 'ServiceA',
176
+ serviceB,
177
+ }),
178
+ ['serviceB'],
179
+ )
180
+
181
+ context.registerFactory(
182
+ 'serviceB',
183
+ (serviceA: ServiceA) => ({
184
+ name: 'ServiceB',
185
+ serviceA,
186
+ }),
187
+ ['serviceA'],
188
+ )
189
+
190
+ const serviceA = context.resolve('serviceA')
191
+ const serviceB = context.resolve('serviceB')
192
+
193
+ expect(serviceA.name).toBe('ServiceA')
194
+ expect(serviceB.name).toBe('ServiceB')
195
+ expect(serviceA.serviceB).not.toBe(serviceB)
196
+ expect(serviceB.serviceA).not.toBe(serviceA)
197
+
198
+ // But circular references should work within each resolution
199
+ expect(serviceA.serviceB.name).toBe('ServiceB')
200
+ expect(serviceB.serviceA.name).toBe('ServiceA')
201
+ })
202
+
203
+ it('should handle mixed circular dependencies with classes and factories', () => {
204
+ type OrderService = { name: string; userService: UserService; getOrdersByUserId: (userId: string) => string[] }
205
+
206
+ class UserService {
207
+ constructor(public orderService: OrderService) {}
208
+
209
+ getUserOrders(userId: string) {
210
+ return this.orderService.getOrdersByUserId(userId)
211
+ }
212
+
213
+ getName() {
214
+ return 'UserService'
215
+ }
216
+ }
217
+
218
+ type Scope = {
219
+ userService: UserService
220
+ orderService: OrderService
221
+ }
222
+
223
+ const context = new Context<Scope>()
224
+
225
+ context.registerClass('userService', UserService, ['orderService'])
226
+
227
+ context.registerFactory(
228
+ 'orderService',
229
+ (userService: UserService) => ({
230
+ name: 'OrderService',
231
+ userService,
232
+ getOrdersByUserId: (userId: string) => [`Order 1 for ${userId}`, `Order 2 for ${userId}`],
233
+ }),
234
+ ['userService'],
235
+ )
236
+
237
+ const userService = context.resolve('userService')
238
+ const orderService = context.resolve('orderService')
239
+
240
+ expect(userService).toBeInstanceOf(UserService)
241
+ expect(userService.getName()).toBe('UserService')
242
+ expect(orderService.name).toBe('OrderService')
243
+ expect(userService.orderService).not.toBe(orderService)
244
+ expect(orderService.userService).not.toBe(userService)
245
+
246
+ // But circular references should work within each resolution
247
+ expect(userService.orderService.name).toBe('OrderService')
248
+ expect(orderService.userService.getName()).toBe('UserService')
249
+ expect(userService.getUserOrders('user123')).toEqual(['Order 1 for user123', 'Order 2 for user123'])
250
+ })
251
+
252
+ it('should handle circular dependencies with eager initialization', async () => {
253
+ class ServiceA {
254
+ constructor(public serviceB: ServiceB) {}
255
+
256
+ getName() {
257
+ return 'ServiceA'
258
+ }
259
+ }
260
+
261
+ class ServiceB {
262
+ constructor(public serviceA: ServiceA) {}
263
+
264
+ getName() {
265
+ return 'ServiceB'
266
+ }
267
+ }
268
+
269
+ type Scope = {
270
+ serviceA: ServiceA
271
+ serviceB: ServiceB
272
+ }
273
+
274
+ const context = new Context<Scope>()
275
+
276
+ context.registerClass('serviceA', ServiceA, ['serviceB'], { eager: true })
277
+ context.registerClass('serviceB', ServiceB, ['serviceA'], { eager: true })
278
+
279
+ // Initialize eager dependencies
280
+ await context.initializeEagerDependencies()
281
+
282
+ const serviceA = context.resolve('serviceA')
283
+ const serviceB = context.resolve('serviceB')
284
+
285
+ expect(serviceA).toBeInstanceOf(ServiceA)
286
+ expect(serviceB).toBeInstanceOf(ServiceB)
287
+ expect(serviceA.serviceB).toEqual(serviceB)
288
+ expect(serviceB.serviceA).toEqual(serviceA)
289
+ })
290
+
291
+ it('should handle circular dependencies with registerFactory creating class instances', () => {
292
+ class ServiceA {
293
+ constructor(public serviceB: ServiceB) {}
294
+
295
+ getName() {
296
+ return 'ServiceA'
297
+ }
298
+
299
+ getOtherName() {
300
+ return this.serviceB.getName()
301
+ }
302
+ }
303
+
304
+ class ServiceB {
305
+ constructor(public serviceA: ServiceA) {}
306
+
307
+ getName() {
308
+ return 'ServiceB'
309
+ }
310
+
311
+ getOtherName() {
312
+ return this.serviceA.getName()
313
+ }
314
+ }
315
+
316
+ type Scope = {
317
+ serviceA: ServiceA
318
+ serviceB: ServiceB
319
+ }
320
+
321
+ const context = new Context<Scope>()
322
+
323
+ context.registerFactory('serviceA', (serviceB: ServiceB) => new ServiceA(serviceB), ['serviceB'])
324
+ context.registerFactory('serviceB', (serviceA: ServiceA) => new ServiceB(serviceA), ['serviceA'])
325
+
326
+ const serviceA = context.resolve('serviceA')
327
+ const serviceB = context.resolve('serviceB')
328
+
329
+ expect(serviceA).toBeInstanceOf(ServiceA)
330
+ expect(serviceB).toBeInstanceOf(ServiceB)
331
+ expect(serviceA.getName()).toBe('ServiceA')
332
+ expect(serviceB.getName()).toBe('ServiceB')
333
+ expect(serviceA.serviceB).not.toBe(serviceB)
334
+ expect(serviceB.serviceA).not.toBe(serviceA)
335
+ expect(serviceA.getOtherName()).toBe('ServiceB')
336
+ expect(serviceB.getOtherName()).toBe('ServiceA')
337
+ })
338
+
339
+ it('should handle circular dependencies with registerFactory creating POJOs', () => {
340
+ type ServiceA = {
341
+ name: string
342
+ serviceB: ServiceB
343
+ greet: () => string
344
+ }
345
+
346
+ type ServiceB = {
347
+ name: string
348
+ serviceA: ServiceA
349
+ greet: () => string
350
+ }
351
+
352
+ type Scope = {
353
+ serviceA: ServiceA
354
+ serviceB: ServiceB
355
+ }
356
+
357
+ const context = new Context<Scope>()
358
+
359
+ context.registerFactory(
360
+ 'serviceA',
361
+ (serviceB: ServiceB): ServiceA => ({
362
+ name: 'ServiceA',
363
+ serviceB,
364
+ greet: () => `Hello from ${serviceB.name}`,
365
+ }),
366
+ ['serviceB'],
367
+ )
368
+
369
+ context.registerFactory(
370
+ 'serviceB',
371
+ (serviceA: ServiceA): ServiceB => ({
372
+ name: 'ServiceB',
373
+ serviceA,
374
+ greet: () => `Hello from ${serviceA.name}`,
375
+ }),
376
+ ['serviceA'],
377
+ )
378
+
379
+ const serviceA = context.resolve('serviceA')
380
+ const serviceB = context.resolve('serviceB')
381
+
382
+ expect(serviceA.name).toBe('ServiceA')
383
+ expect(serviceB.name).toBe('ServiceB')
384
+ expect(serviceA.serviceB).not.toBe(serviceB)
385
+ expect(serviceB.serviceA).not.toBe(serviceA)
386
+ expect(serviceA.greet()).toBe('Hello from ServiceB')
387
+ expect(serviceB.greet()).toBe('Hello from ServiceA')
388
+ })
389
+
390
+ it('should handle circular dependencies with factory functions and singletons', () => {
391
+ type ServiceA = { name: string; serviceB: ServiceB; counter: number }
392
+ type ServiceB = { name: string; serviceA: ServiceA; counter: number }
393
+
394
+ type Scope = {
395
+ serviceA: ServiceA
396
+ serviceB: ServiceB
397
+ }
398
+
399
+ let serviceACreations = 0
400
+ let serviceBCreations = 0
401
+
402
+ const context = new Context<Scope>()
403
+
404
+ context.registerFactory(
405
+ 'serviceA',
406
+ (serviceB: ServiceB): ServiceA => {
407
+ serviceACreations++
408
+ return {
409
+ name: 'ServiceA',
410
+ serviceB,
411
+ counter: serviceACreations,
412
+ }
413
+ },
414
+ ['serviceB'],
415
+ { singleton: true },
416
+ )
417
+
418
+ context.registerFactory(
419
+ 'serviceB',
420
+ (serviceA: ServiceA): ServiceB => {
421
+ serviceBCreations++
422
+ return {
423
+ name: 'ServiceB',
424
+ serviceA,
425
+ counter: serviceBCreations,
426
+ }
427
+ },
428
+ ['serviceA'],
429
+ { singleton: true },
430
+ )
431
+
432
+ const serviceA1 = context.resolve('serviceA')
433
+ const serviceB1 = context.resolve('serviceB')
434
+ const serviceA2 = context.resolve('serviceA')
435
+ const serviceB2 = context.resolve('serviceB')
436
+
437
+ // Test that singletons work with circular dependencies
438
+ expect(serviceA1).toBe(serviceA2)
439
+ expect(serviceB1).toBe(serviceB2)
440
+ expect(serviceA1.serviceB).toEqual(serviceB1)
441
+ expect(serviceB1.serviceA).toEqual(serviceA1)
442
+
443
+ // Test that factories were only called once each
444
+ expect(serviceACreations).toBe(1)
445
+ expect(serviceBCreations).toBe(1)
446
+ expect(serviceA1.counter).toBe(1)
447
+ expect(serviceB1.counter).toBe(1)
448
+ })
449
+
450
+ it('should demonstrate that circular dependencies work with registerAsyncFactory in a realistic scenario', async () => {
451
+ // This test shows how to handle circular async dependencies by deferring resolution
452
+ // type UserService = {
453
+ // name: string
454
+ // getUser: (id: string) => Promise<{ id: string; orders: string[] }>
455
+ // }
456
+ class UserService {
457
+ constructor(public orderService: OrderService) {}
458
+
459
+ name = 'UserService'
460
+
461
+ async getUser(id: string): Promise<{ id: string; orders: string[] }> {
462
+ const orders = await this.orderService.getOrdersForUser(id)
463
+ return { id, orders }
464
+ }
465
+ }
466
+
467
+ // type OrderService = {
468
+ // name: string
469
+ // getOrdersForUser: (userId: string) => Promise<string[]>
470
+ // }
471
+ class OrderService {
472
+ constructor(public userService: UserService) {}
473
+
474
+ name = 'OrderService'
475
+
476
+ async getOrdersForUser(userId: string): Promise<string[]> {
477
+ // This could potentially use userService in the future
478
+ return [`Order1-${userId}`, `Order2-${userId}`]
479
+ }
480
+ }
481
+
482
+ type Scope = {
483
+ userService: Promise<UserService>
484
+ orderService: Promise<OrderService>
485
+ }
486
+
487
+ const context = new Context<Scope>()
488
+
489
+ context.registerAsyncFactory(
490
+ 'userService',
491
+ async (orderService: OrderService): Promise<UserService> => {
492
+ await new Promise((resolve) => setTimeout(resolve, 5))
493
+ return new UserService(orderService)
494
+ },
495
+ ['orderService'],
496
+ { singleton: true },
497
+ )
498
+
499
+ context.registerAsyncFactory(
500
+ 'orderService',
501
+ async (userService: UserService): Promise<OrderService> => {
502
+ await new Promise((resolve) => setTimeout(resolve, 5))
503
+ return new OrderService(userService)
504
+ },
505
+ ['userService'],
506
+ { singleton: true },
507
+ )
508
+
509
+ const userService = await context.resolve('userService')
510
+ const orderService = await context.resolve('orderService')
511
+
512
+ expect(userService.name).toBe('UserService')
513
+ expect(orderService.name).toBe('OrderService')
514
+
515
+ const user = await userService.getUser('user123')
516
+ expect(user.id).toBe('user123')
517
+ expect(user.orders).toEqual(['Order1-user123', 'Order2-user123'])
518
+
519
+ const orders = await orderService.getOrdersForUser('user456')
520
+ expect(orders).toEqual(['Order1-user456', 'Order2-user456'])
521
+ })
522
+
523
+ it('should handle circular dependencies between registerClass and registerAsyncClass', async () => {
524
+ class SyncService {
525
+ constructor(public asyncService: Promise<AsyncService>) {}
526
+
527
+ getName() {
528
+ return 'SyncService'
529
+ }
530
+
531
+ async getAsyncName() {
532
+ const service = await this.asyncService
533
+ return service.getName()
534
+ }
535
+ }
536
+
537
+ class AsyncService {
538
+ constructor(public syncService: SyncService) {}
539
+
540
+ getName() {
541
+ return 'AsyncService'
542
+ }
543
+
544
+ getSyncName() {
545
+ return this.syncService.getName()
546
+ }
547
+ }
548
+
549
+ type Scope = {
550
+ syncService: SyncService
551
+ asyncService: Promise<AsyncService>
552
+ }
553
+
554
+ const context = new Context<Scope>()
555
+
556
+ context.registerClass('syncService', SyncService, ['asyncService'])
557
+ context.registerAsyncClass('asyncService', AsyncService, ['syncService'])
558
+
559
+ const syncService = context.resolve('syncService')
560
+ const asyncService = await context.resolve('asyncService')
561
+
562
+ expect(syncService).toBeInstanceOf(SyncService)
563
+ expect(asyncService).toBeInstanceOf(AsyncService)
564
+ expect(syncService.getName()).toBe('SyncService')
565
+ expect(asyncService.getName()).toBe('AsyncService')
566
+
567
+ // Test circular references work
568
+ expect(await syncService.getAsyncName()).toBe('AsyncService')
569
+ expect(asyncService.getSyncName()).toBe('SyncService')
570
+
571
+ // The async service should match
572
+ expect(await syncService.asyncService).toBeInstanceOf(AsyncService)
573
+ expect(asyncService.syncService).toBeInstanceOf(SyncService)
574
+ })
575
+
576
+ it('should support structured dependency injection via async factories', async () => {
577
+ // Define argument interfaces for structured dependency injection
578
+ interface IServiceAArgs {
579
+ serviceB: ServiceB
580
+ }
581
+
582
+ interface IServiceBArgs {
583
+ serviceA: ServiceA
584
+ }
585
+
586
+ // Service classes that accept structured arguments
587
+ class ServiceA {
588
+ constructor(public readonly args: IServiceAArgs) {}
589
+
590
+ getName() {
591
+ return 'ServiceA'
592
+ }
593
+
594
+ getServiceBName() {
595
+ return this.args.serviceB.getName()
596
+ }
597
+ }
598
+
599
+ class ServiceB {
600
+ constructor(public readonly args: IServiceBArgs) {}
601
+
602
+ getName() {
603
+ return 'ServiceB'
604
+ }
605
+
606
+ getServiceAName() {
607
+ return this.args.serviceA.getName()
608
+ }
609
+ }
610
+
611
+ type Scope = {
612
+ serviceA: Promise<ServiceA>
613
+ serviceB: Promise<ServiceB>
614
+ }
615
+
616
+ const context = new Context<Scope>()
617
+
618
+ // This is the ACTUAL way users would try to register circular async factories
619
+ // This will fail because registerAsyncFactory expects Promise<ServiceB> not ServiceB
620
+ context.registerAsyncFactory(
621
+ 'serviceA',
622
+ async (serviceB: ServiceB): Promise<ServiceA> => {
623
+ await new Promise((resolve) => setTimeout(resolve, 2))
624
+ return new ServiceA({ serviceB })
625
+ },
626
+ ['serviceB'],
627
+ { singleton: true },
628
+ )
629
+
630
+ context.registerAsyncFactory(
631
+ 'serviceB',
632
+ async (serviceA: ServiceA): Promise<ServiceB> => {
633
+ await new Promise((resolve) => setTimeout(resolve, 2))
634
+ return new ServiceB({ serviceA })
635
+ },
636
+ ['serviceA'],
637
+ { singleton: true },
638
+ )
639
+
640
+ const serviceA = await context.resolve('serviceA')
641
+ const serviceB = await context.resolve('serviceB')
642
+
643
+ expect(serviceA).toBeInstanceOf(ServiceA)
644
+ expect(serviceB).toBeInstanceOf(ServiceB)
645
+ expect(serviceA.args.serviceB).toEqual(serviceB)
646
+ expect(serviceB.args.serviceA).toEqual(serviceA)
647
+ })
648
+
649
+ it('should handle multiple async circular dependency chains in parallel', async () => {
650
+ // Chain 1: Multiple async factories with circular dependencies
651
+ type ServiceA = { name: string; serviceB: Promise<ServiceB>; serviceC: Promise<ServiceC> }
652
+ type ServiceB = { name: string; serviceA: Promise<ServiceA>; serviceC: Promise<ServiceC> }
653
+ type ServiceC = { name: string; serviceA: Promise<ServiceA>; serviceB: Promise<ServiceB> }
654
+
655
+ // Chain 2: Another set of async factories with circular dependencies
656
+ type ServiceX = { name: string; serviceY: Promise<ServiceY>; serviceZ: Promise<ServiceZ> }
657
+ type ServiceY = { name: string; serviceX: Promise<ServiceX>; serviceZ: Promise<ServiceZ> }
658
+ type ServiceZ = { name: string; serviceX: Promise<ServiceX>; serviceY: Promise<ServiceY> }
659
+
660
+ type Scope1 = {
661
+ serviceA: Promise<ServiceA>
662
+ serviceB: Promise<ServiceB>
663
+ serviceC: Promise<ServiceC>
664
+ }
665
+
666
+ type Scope2 = {
667
+ serviceX: Promise<ServiceX>
668
+ serviceY: Promise<ServiceY>
669
+ serviceZ: Promise<ServiceZ>
670
+ }
671
+
672
+ const context1 = new Context<Scope1>()
673
+ const context2 = new Context<Scope2>()
674
+
675
+ // Register async factories with multiple circular dependencies for chain 1
676
+ context1.registerAsyncFactory(
677
+ 'serviceA',
678
+ async (): Promise<ServiceA> => {
679
+ await new Promise((resolve) => setTimeout(resolve, 1))
680
+ return {
681
+ name: 'ServiceA',
682
+ serviceB: context1.resolve('serviceB'),
683
+ serviceC: context1.resolve('serviceC'),
684
+ }
685
+ },
686
+ [],
687
+ { singleton: true },
688
+ )
689
+
690
+ context1.registerAsyncFactory(
691
+ 'serviceB',
692
+ async (): Promise<ServiceB> => {
693
+ await new Promise((resolve) => setTimeout(resolve, 1))
694
+ return {
695
+ name: 'ServiceB',
696
+ serviceA: context1.resolve('serviceA'),
697
+ serviceC: context1.resolve('serviceC'),
698
+ }
699
+ },
700
+ [],
701
+ { singleton: true },
702
+ )
703
+
704
+ context1.registerAsyncFactory(
705
+ 'serviceC',
706
+ async (): Promise<ServiceC> => {
707
+ await new Promise((resolve) => setTimeout(resolve, 1))
708
+ return {
709
+ name: 'ServiceC',
710
+ serviceA: context1.resolve('serviceA'),
711
+ serviceB: context1.resolve('serviceB'),
712
+ }
713
+ },
714
+ [],
715
+ { singleton: true },
716
+ )
717
+
718
+ // Register async factories with multiple circular dependencies for chain 2
719
+ context2.registerAsyncFactory(
720
+ 'serviceX',
721
+ async (): Promise<ServiceX> => {
722
+ await new Promise((resolve) => setTimeout(resolve, 1))
723
+ return {
724
+ name: 'ServiceX',
725
+ serviceY: context2.resolve('serviceY'),
726
+ serviceZ: context2.resolve('serviceZ'),
727
+ }
728
+ },
729
+ [],
730
+ { singleton: true },
731
+ )
732
+
733
+ context2.registerAsyncFactory(
734
+ 'serviceY',
735
+ async (): Promise<ServiceY> => {
736
+ await new Promise((resolve) => setTimeout(resolve, 1))
737
+ return {
738
+ name: 'ServiceY',
739
+ serviceX: context2.resolve('serviceX'),
740
+ serviceZ: context2.resolve('serviceZ'),
741
+ }
742
+ },
743
+ [],
744
+ { singleton: true },
745
+ )
746
+
747
+ context2.registerAsyncFactory(
748
+ 'serviceZ',
749
+ async (): Promise<ServiceZ> => {
750
+ await new Promise((resolve) => setTimeout(resolve, 1))
751
+ return {
752
+ name: 'ServiceZ',
753
+ serviceX: context2.resolve('serviceX'),
754
+ serviceY: context2.resolve('serviceY'),
755
+ }
756
+ },
757
+ [],
758
+ { singleton: true },
759
+ )
760
+
761
+ // Resolve both chains in parallel with multiple async dependencies
762
+ const [{ serviceA, serviceB, serviceC }, { serviceX, serviceY, serviceZ }] = await Promise.all([
763
+ Promise.all([
764
+ context1.resolve('serviceA'),
765
+ context1.resolve('serviceB'),
766
+ context1.resolve('serviceC'),
767
+ ]).then(([a, b, c]) => ({ serviceA: a, serviceB: b, serviceC: c })),
768
+ Promise.all([
769
+ context2.resolve('serviceX'),
770
+ context2.resolve('serviceY'),
771
+ context2.resolve('serviceZ'),
772
+ ]).then(([x, y, z]) => ({ serviceX: x, serviceY: y, serviceZ: z })),
773
+ ])
774
+
775
+ // Verify both chains resolved correctly
776
+ expect(serviceA.name).toBe('ServiceA')
777
+ expect(serviceB.name).toBe('ServiceB')
778
+ expect(serviceC.name).toBe('ServiceC')
779
+ expect(serviceX.name).toBe('ServiceX')
780
+ expect(serviceY.name).toBe('ServiceY')
781
+ expect(serviceZ.name).toBe('ServiceZ')
782
+
783
+ // Verify circular references work within each chain (singletons should be same instances)
784
+ expect(await serviceA.serviceB).toEqual(serviceB)
785
+ expect(await serviceA.serviceC).toEqual(serviceC)
786
+ expect(await serviceB.serviceA).toEqual(serviceA)
787
+ expect(await serviceB.serviceC).toEqual(serviceC)
788
+ expect(await serviceC.serviceA).toEqual(serviceA)
789
+ expect(await serviceC.serviceB).toEqual(serviceB)
790
+
791
+ expect(await serviceX.serviceY).toEqual(serviceY)
792
+ expect(await serviceX.serviceZ).toEqual(serviceZ)
793
+ expect(await serviceY.serviceX).toEqual(serviceX)
794
+ expect(await serviceY.serviceZ).toEqual(serviceZ)
795
+ expect(await serviceZ.serviceX).toEqual(serviceX)
796
+ expect(await serviceZ.serviceY).toEqual(serviceY)
797
+
798
+ // Verify chains are independent (no cross-contamination)
799
+ expect(serviceA.name).not.toEqual(serviceX.name)
800
+ expect(serviceB.name).not.toEqual(serviceY.name)
801
+ expect(serviceC.name).not.toEqual(serviceZ.name)
802
+ })
803
+
804
+ it('should not leak memory after circular dependency resolution', async () => {
805
+ class ServiceA {
806
+ constructor(public serviceB: ServiceB) {}
807
+ }
808
+
809
+ class ServiceB {
810
+ constructor(public serviceA: ServiceA) {}
811
+ }
812
+
813
+ type Scope = {
814
+ serviceA: ServiceA
815
+ serviceB: ServiceB
816
+ }
817
+
818
+ let weakRefA: WeakRef<ServiceA> | undefined
819
+ let weakRefB: WeakRef<ServiceB> | undefined
820
+ let weakRefContext: WeakRef<any> | undefined
821
+
822
+ // Scope the resolution to allow garbage collection
823
+ {
824
+ const context = new Context<Scope>()
825
+ context.registerClass('serviceA', ServiceA, ['serviceB'])
826
+ context.registerClass('serviceB', ServiceB, ['serviceA'])
827
+
828
+ const resolutionContext = new Map()
829
+ const serviceA = context.resolve('serviceA', { resolutionContext })
830
+ const serviceB = context.resolve('serviceB', { resolutionContext })
831
+
832
+ // Create weak references to track garbage collection
833
+ weakRefA = new WeakRef(serviceA)
834
+ weakRefB = new WeakRef(serviceB)
835
+ weakRefContext = new WeakRef(resolutionContext)
836
+
837
+ // Verify objects exist
838
+ expect(weakRefA.deref()).toBeDefined()
839
+ expect(weakRefB.deref()).toBeDefined()
840
+ expect(weakRefContext.deref()).toBeDefined()
841
+ }
842
+
843
+ // Force garbage collection (if available)
844
+ if (global.gc) {
845
+ global.gc()
846
+ // Wait a bit for GC to complete
847
+ await new Promise((resolve) => setTimeout(resolve, 5))
848
+ }
849
+
850
+ // Note: We can't reliably test GC in all environments, but we can at least
851
+ // verify the test structure works and doesn't throw errors
852
+ expect(weakRefA).toBeDefined()
853
+ expect(weakRefB).toBeDefined()
854
+ expect(weakRefContext).toBeDefined()
855
+ })
856
+
857
+ it('should handle circular dependencies with context extension and inheritance', () => {
858
+ class ServiceA {
859
+ constructor(public serviceB: ServiceB) {}
860
+ getName() {
861
+ return 'ServiceA'
862
+ }
863
+ }
864
+
865
+ class ServiceB {
866
+ constructor(public serviceA: ServiceA) {}
867
+ getName() {
868
+ return 'ServiceB'
869
+ }
870
+ }
871
+
872
+ class CustomServiceB extends ServiceB {
873
+ getName() {
874
+ return 'CustomServiceB'
875
+ }
876
+ }
877
+
878
+ type BaseScope = {
879
+ serviceA: ServiceA
880
+ serviceB: ServiceB
881
+ }
882
+
883
+ type CustomScope = BaseScope & {
884
+ serviceB: CustomServiceB
885
+ }
886
+
887
+ // Base context with circular dependencies
888
+ const baseContext = new Context<BaseScope>()
889
+ baseContext.registerClass('serviceA', ServiceA, ['serviceB'])
890
+ baseContext.registerClass('serviceB', ServiceB, ['serviceA'])
891
+
892
+ // Custom context extends base and overrides serviceB
893
+ const customContext = new Context<CustomScope>()
894
+ customContext.extend(baseContext)
895
+ customContext.registerClass('serviceB', CustomServiceB, ['serviceA'])
896
+
897
+ const serviceA = customContext.resolve('serviceA')
898
+ const serviceB = customContext.resolve('serviceB')
899
+
900
+ expect(serviceA).toBeInstanceOf(ServiceA)
901
+ expect(serviceB).toBeInstanceOf(CustomServiceB)
902
+ expect(serviceA.getName()).toBe('ServiceA')
903
+ expect(serviceB.getName()).toBe('CustomServiceB')
904
+
905
+ // Test that circular dependency resolved with the overridden class
906
+ expect(serviceA.serviceB).toBeInstanceOf(CustomServiceB)
907
+ expect(serviceB.serviceA).toBeInstanceOf(ServiceA)
908
+ expect(serviceA.serviceB.getName()).toBe('CustomServiceB')
909
+ })
910
+
911
+ it('should handle resolution context with pre-populated values', () => {
912
+ // Test case 1: Pre-populate a simple non-circular dependency
913
+ class ServiceC {
914
+ getName() {
915
+ return 'ServiceC'
916
+ }
917
+ getSpecialValue() {
918
+ return 'special'
919
+ }
920
+ }
921
+
922
+ class ServiceA {
923
+ constructor(public serviceC: ServiceC) {}
924
+ getName() {
925
+ return 'ServiceA'
926
+ }
927
+ }
928
+
929
+ type Scope = {
930
+ serviceA: ServiceA
931
+ serviceC: ServiceC
932
+ }
933
+
934
+ const context = new Context<Scope>()
935
+ context.registerClass('serviceA', ServiceA, ['serviceC'])
936
+ context.registerClass('serviceC', ServiceC, [])
937
+
938
+ // Create a custom ServiceC instance with modified behavior
939
+ const customServiceC = new ServiceC()
940
+ customServiceC.getSpecialValue = () => 'custom-special'
941
+
942
+ // Pre-populate resolution context using the internal API
943
+ // Note: This uses the internal __instanceCache which is the current API
944
+ const resolutionContext = new Map([['serviceC', customServiceC]])
945
+
946
+ // Resolve serviceA with the pre-populated context
947
+ const serviceA = context.resolve('serviceA', { resolutionContext })
948
+
949
+ expect(serviceA).toBeInstanceOf(ServiceA)
950
+ expect(serviceA.serviceC).toBe(customServiceC) // Should use pre-populated instance
951
+ expect(serviceA.serviceC.getName()).toBe('ServiceC')
952
+ expect(serviceA.serviceC.getSpecialValue()).toBe('custom-special') // Verify it's our custom instance
953
+
954
+ // When resolving serviceC directly, it should return the pre-populated instance
955
+ const serviceC = context.resolve('serviceC', { resolutionContext })
956
+ expect(serviceC).toEqual(customServiceC)
957
+ expect(serviceC.getSpecialValue()).toBe('custom-special')
958
+ expect(isProxy(serviceC)).toBeFalsy()
959
+
960
+ // Test case 2: Pre-populate in a simpler non-circular scenario to show the API works
961
+ class ServiceD {
962
+ constructor(public value: string) {}
963
+ getValue() {
964
+ return this.value
965
+ }
966
+ }
967
+
968
+ class ServiceB2 {
969
+ constructor(public serviceD: ServiceD) {}
970
+ getName() {
971
+ return 'ServiceB2'
972
+ }
973
+ }
974
+
975
+ type Scope2 = {
976
+ serviceB2: ServiceB2
977
+ serviceD: ServiceD
978
+ value: string
979
+ }
980
+
981
+ const context2 = new Context<Scope2>()
982
+ context2.registerClass('serviceB2', ServiceB2, ['serviceD'])
983
+ context2.registerFactory('serviceD', (value: string) => new ServiceD(value), ['value'])
984
+ context2.registerValue('value', 'default-value')
985
+
986
+ // Pre-populate serviceD with a custom instance
987
+ const customServiceD = new ServiceD('custom-value')
988
+ const resolutionContext2 = new Map([['serviceD', customServiceD]])
989
+
990
+ // Resolve serviceB2, which should use the pre-populated serviceD
991
+ const serviceB2 = context2.resolve('serviceB2', { resolutionContext: resolutionContext2 })
992
+
993
+ expect(serviceB2).toBeInstanceOf(ServiceB2)
994
+ expect(serviceB2.serviceD).toBe(customServiceD)
995
+ expect(serviceB2.serviceD.getValue()).toBe('custom-value')
996
+ expect(isProxy(serviceB2)).toBeFalsy()
997
+
998
+ // Test case 3: Verify that different resolution contexts create different instances
999
+ const anotherResolutionContext = new Map()
1000
+
1001
+ const anotherServiceA = context.resolve('serviceA', { resolutionContext: anotherResolutionContext })
1002
+ const anotherServiceC = context.resolve('serviceC', { resolutionContext: anotherResolutionContext })
1003
+
1004
+ // Different resolution contexts should create different instances
1005
+ expect(anotherServiceA).not.toBe(serviceA)
1006
+ expect(anotherServiceC).not.toBe(customServiceC)
1007
+ expect(anotherServiceA.serviceC).toEqual(anotherServiceC) // But within same context, should be same
1008
+ })
1009
+
1010
+ it('should not return proxies when resolving values', async () => {
1011
+ type Scope = {
1012
+ foo: String
1013
+ }
1014
+
1015
+ const context = new Context<Scope>()
1016
+
1017
+ context.registerValue('foo', 'bar')
1018
+
1019
+ const foo = context.resolve('foo')
1020
+ expect(foo).toBe('bar')
1021
+
1022
+ expect((foo as any).__isProxy).toBeFalsy()
1023
+ })
1024
+
1025
+ it('should not return proxies when resolving non-circular factories', async () => {
1026
+ // Service classes that accept structured arguments
1027
+ class ServiceA {
1028
+ constructor() {}
1029
+
1030
+ getName() {
1031
+ return 'ServiceA'
1032
+ }
1033
+ }
1034
+
1035
+ type Scope = {
1036
+ serviceA: ServiceA
1037
+ }
1038
+
1039
+ const context = new Context<Scope>()
1040
+
1041
+ context.registerFactory('serviceA', () => new ServiceA(), [], { singleton: true })
1042
+
1043
+ const serviceA = context.resolve('serviceA')
1044
+ expect(serviceA).toBeInstanceOf(ServiceA)
1045
+ expect((serviceA as any).__isProxy).toBeFalsy()
1046
+ })
1047
+ })