@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.
- package/dist/browser/index.js +21 -27
- package/dist/browser/index.js.map +37 -27
- package/dist/browser/scope/index.js +1 -2
- package/dist/browser/scope/index.js.map +1 -1
- package/dist/bun/index.js +19011 -0
- package/dist/bun/index.js.map +132 -0
- package/dist/bun/scope/index.js +4 -0
- package/dist/bun/scope/index.js.map +9 -0
- package/dist/node/index.cjs +2581 -874
- package/dist/node/index.cjs.map +36 -27
- package/dist/node/index.js +2572 -868
- package/dist/node/index.js.map +36 -27
- package/dist/node/scope/index.cjs +31 -10
- package/dist/node/scope/index.cjs.map +1 -1
- package/dist/node/scope/index.js +1 -27
- package/dist/node/scope/index.js.map +1 -1
- package/dist/ts/context/async-context.d.ts +54 -0
- package/dist/ts/context/async-context.d.ts.map +1 -0
- package/dist/ts/context/async-context.test.d.ts +2 -0
- package/dist/ts/context/async-context.test.d.ts.map +1 -0
- package/dist/ts/context/context.circular-deps.test.d.ts +2 -0
- package/dist/ts/context/context.circular-deps.test.d.ts.map +1 -0
- package/dist/ts/context/context.d.ts +297 -38
- package/dist/ts/context/context.d.ts.map +1 -1
- package/dist/ts/http/request-context.d.ts.map +1 -1
- package/dist/ts/index.d.ts +2 -0
- package/dist/ts/index.d.ts.map +1 -1
- package/dist/ts/schema/json-schema.d.ts +9 -1
- package/dist/ts/schema/json-schema.d.ts.map +1 -1
- package/dist/ts/schema/model.d.ts +6 -1
- package/dist/ts/schema/model.d.ts.map +1 -1
- package/dist/ts/schema/test/mock-model.d.ts +2 -2
- package/dist/ts/schema/test/mock-model.d.ts.map +1 -1
- package/dist/ts/shared/utils/schema-utils.d.ts +3 -0
- package/dist/ts/shared/utils/schema-utils.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-utils.test.d.ts +2 -0
- package/dist/ts/shared/utils/schema-utils.test.d.ts.map +1 -0
- package/dist/ts/shims/async-local-storage.d.ts +36 -0
- package/dist/ts/shims/async-local-storage.d.ts.map +1 -0
- package/dist/ts/shims/async-local-storage.test.d.ts +2 -0
- package/dist/ts/shims/async-local-storage.test.d.ts.map +1 -0
- package/package.json +17 -9
- package/src/context/async-context.test.ts +348 -0
- package/src/context/async-context.ts +129 -0
- package/src/context/context.circular-deps.test.ts +1047 -0
- package/src/context/context.test.ts +150 -0
- package/src/context/context.ts +493 -55
- package/src/http/request-context.ts +1 -3
- package/src/index.ts +2 -0
- package/src/schema/json-schema.ts +14 -1
- package/src/schema/model-schema.test.ts +155 -1
- package/src/schema/model.ts +34 -3
- package/src/schema/test/mock-model.ts +6 -2
- package/src/shared/utils/schema-utils.test.ts +33 -0
- package/src/shared/utils/schema-utils.ts +17 -0
- package/src/shims/async-local-storage.test.ts +258 -0
- package/src/shims/async-local-storage.ts +82 -0
- package/dist/ts/schema/entity-schema.test.d.ts +0 -1
- package/dist/ts/schema/entity-schema.test.d.ts.map +0 -1
- 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
|
+
})
|