@declaro/core 2.0.0-beta.9 → 2.0.0-y.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 (90) hide show
  1. package/dist/app/app-context.d.ts +8 -0
  2. package/dist/app/app-lifecycle.d.ts +4 -0
  3. package/dist/app/app.d.ts +22 -0
  4. package/dist/app/index.d.ts +3 -20
  5. package/dist/auth/permission-validator.d.ts +34 -0
  6. package/dist/auth/permission-validator.test.d.ts +1 -0
  7. package/dist/context/context.d.ts +88 -13
  8. package/dist/context/legacy-context.test.d.ts +1 -0
  9. package/dist/errors/errors.d.ts +36 -0
  10. package/dist/events/event-manager.d.ts +11 -6
  11. package/dist/http/headers.d.ts +4 -0
  12. package/dist/http/headers.spec.d.ts +1 -0
  13. package/dist/http/request-context.d.ts +12 -0
  14. package/dist/http/request-context.spec.d.ts +1 -0
  15. package/dist/http/request.d.ts +8 -0
  16. package/dist/http/request.spec.d.ts +1 -0
  17. package/dist/http/url.d.ts +8 -0
  18. package/dist/http/url.spec.d.ts +1 -0
  19. package/dist/index.d.ts +9 -3
  20. package/dist/pkg.cjs +30 -2
  21. package/dist/pkg.mjs +56461 -207
  22. package/dist/schema/application.d.ts +83 -0
  23. package/dist/schema/application.test.d.ts +1 -0
  24. package/dist/schema/define-model.d.ts +7 -4
  25. package/dist/schema/index.d.ts +7 -0
  26. package/dist/schema/labels.d.ts +13 -0
  27. package/dist/schema/labels.test.d.ts +1 -0
  28. package/dist/schema/module.d.ts +7 -0
  29. package/dist/schema/module.test.d.ts +1 -0
  30. package/dist/schema/properties.d.ts +19 -0
  31. package/dist/schema/response.d.ts +31 -0
  32. package/dist/schema/response.test.d.ts +1 -0
  33. package/dist/schema/transform-model.d.ts +1 -1
  34. package/dist/schema/types.d.ts +81 -15
  35. package/dist/schema/types.test.d.ts +1 -0
  36. package/dist/typescript/constant-manipulation/snake-case.d.ts +22 -0
  37. package/dist/typescript/index.d.ts +1 -0
  38. package/dist/typescript/objects.d.ts +6 -0
  39. package/package.json +8 -3
  40. package/src/app/app-context.ts +14 -0
  41. package/src/app/app-lifecycle.ts +14 -0
  42. package/src/app/app.ts +45 -0
  43. package/src/app/index.ts +3 -34
  44. package/src/auth/permission-validator.test.ts +209 -0
  45. package/src/auth/permission-validator.ts +135 -0
  46. package/src/context/context.test.ts +585 -94
  47. package/src/context/context.ts +348 -32
  48. package/src/context/legacy-context.test.ts +141 -0
  49. package/src/errors/errors.ts +73 -0
  50. package/src/events/event-manager.spec.ts +54 -8
  51. package/src/events/event-manager.ts +40 -24
  52. package/src/http/headers.spec.ts +48 -0
  53. package/src/http/headers.ts +16 -0
  54. package/src/http/request-context.spec.ts +39 -0
  55. package/src/http/request-context.ts +43 -0
  56. package/src/http/request.spec.ts +52 -0
  57. package/src/http/request.ts +22 -0
  58. package/src/http/url.spec.ts +87 -0
  59. package/src/http/url.ts +48 -0
  60. package/src/index.ts +9 -3
  61. package/src/schema/application.test.ts +286 -0
  62. package/src/schema/application.ts +150 -0
  63. package/src/schema/define-model.test.ts +48 -2
  64. package/src/schema/define-model.ts +40 -9
  65. package/src/schema/index.ts +7 -0
  66. package/src/schema/labels.test.ts +60 -0
  67. package/src/schema/labels.ts +30 -0
  68. package/src/schema/module.test.ts +39 -0
  69. package/src/schema/module.ts +6 -0
  70. package/src/schema/properties.ts +40 -0
  71. package/src/schema/response.test.ts +101 -0
  72. package/src/schema/response.ts +93 -0
  73. package/src/schema/transform-model.ts +1 -1
  74. package/src/schema/types.test.ts +28 -0
  75. package/src/schema/types.ts +135 -15
  76. package/src/typescript/constant-manipulation/snake-case.md +496 -0
  77. package/src/typescript/constant-manipulation/snake-case.ts +76 -0
  78. package/src/typescript/index.ts +1 -0
  79. package/src/typescript/objects.ts +8 -5
  80. package/tsconfig.json +4 -1
  81. package/dist/context/index.d.ts +0 -3
  82. package/dist/interfaces/IDatastoreProvider.d.ts +0 -16
  83. package/dist/interfaces/IStore.d.ts +0 -4
  84. package/dist/interfaces/index.d.ts +0 -2
  85. package/dist/server/index.d.ts +0 -2
  86. package/src/context/index.ts +0 -3
  87. package/src/interfaces/IDatastoreProvider.ts +0 -23
  88. package/src/interfaces/IStore.ts +0 -4
  89. package/src/interfaces/index.ts +0 -2
  90. package/src/server/index.ts +0 -3
@@ -1,144 +1,635 @@
1
- import { Context } from '.'
2
- import { sleep } from '../timing'
3
- import { ContextConsumer } from './context-consumer'
4
- import { describe, it, vi } from 'vitest'
1
+ import { describe, expect, it } from 'vitest'
2
+ import { Context } from './context'
5
3
 
6
4
  describe('Context', () => {
7
- it('Should allow simple value dependency injection', async ({ expect }) => {
8
- const context = new Context()
5
+ it('Should allow value dependency injection', async () => {
6
+ type Scope = {
7
+ foo: string
8
+ bar: number
9
+ }
10
+ const context = new Context<Scope>()
11
+
12
+ context.registerValue('foo', 'Hello')
13
+ context.registerValue('bar', 42)
14
+
15
+ const foo = context.resolve('foo')
16
+ const bar = context.resolve('bar')
17
+
18
+ expect(foo).toBe('Hello')
19
+ expect(bar).toBe(42)
20
+ })
21
+
22
+ it('Should allow factory dependency injection', async () => {
23
+ type Scope = {
24
+ foo: string
25
+ bar: number
26
+ name: string
27
+ }
28
+ const context = new Context<Scope>()
29
+
30
+ context.registerValue('bar', 42)
31
+ context.registerValue('name', 'Person')
32
+
33
+ context.registerFactory('foo', (name: string, bar: number) => `Hello ${name}, the answer is ${bar}`, [
34
+ 'name',
35
+ 'bar',
36
+ ])
37
+
38
+ const foo = context.resolve('foo')
39
+
40
+ expect(foo).toBe('Hello Person, the answer is 42')
41
+ })
42
+
43
+ it('Should allow async factory dependency injection', async () => {
44
+ type Scope = {
45
+ foo: Promise<string>
46
+ bar: number
47
+ name: Promise<string>
48
+ baz: string
49
+ }
50
+ const context = new Context<Scope>()
51
+
52
+ context.registerValue('bar', 42)
53
+ context.registerAsyncFactory('name', async () => 'Person')
54
+
55
+ context.registerAsyncFactory(
56
+ 'foo',
57
+ async (name: string, bar: number) => `Hello ${name}, the answer is ${bar}`,
58
+ ['name', 'bar'],
59
+ )
60
+
61
+ const foo = await context.resolve('foo')
62
+
63
+ expect(foo).toBe('Hello Person, the answer is 42')
64
+ })
65
+
66
+ it('should allow class dependency injection', async () => {
67
+ class Thing1 {
68
+ constructor(public name: string) {}
69
+
70
+ testThing1() {
71
+ return `Hello, ${this.name}, this is Thing 1`
72
+ }
73
+ }
74
+
75
+ class Thing2 {
76
+ constructor(public thing1: Thing1, public bar: number) {}
77
+
78
+ testThing2() {
79
+ return this.thing1.testThing1() + ' and Thing 2. The answer is ' + this.bar.toString()
80
+ }
81
+ }
82
+
83
+ type Scope = {
84
+ foo: string
85
+ bar: number
86
+ name: string
87
+ thing1: Thing1
88
+ thing2: Thing2
89
+ }
90
+ const context = new Context<Scope>()
91
+
92
+ context.registerValue('bar', 42)
93
+ context.registerValue('name', 'Person')
94
+
95
+ context.registerClass('thing1', Thing1, ['name'])
96
+ context.registerClass('thing2', Thing2, ['thing1', 'bar'])
97
+
98
+ const thing1 = context.resolve('thing1')
99
+ const thing2 = context.resolve('thing2')
100
+
101
+ expect(thing1.testThing1()).toBe('Hello, Person, this is Thing 1')
102
+ expect(thing2.testThing2()).toBe('Hello, Person, this is Thing 1 and Thing 2. The answer is 42')
103
+ })
104
+
105
+ it('should allow class dependency injection with async dependencies', async () => {
106
+ class Greeting {
107
+ constructor(public name: string) {}
108
+
109
+ greet() {
110
+ return `Hello, ${this.name}.`
111
+ }
112
+ }
113
+
114
+ class AgeCalculator {
115
+ constructor(public name: string, public age: number) {}
116
+
117
+ printAge() {
118
+ return `${this.name} is ${this.age} years old.`
119
+ }
120
+ }
9
121
 
10
- context.provide('test', 'Hello World')
122
+ type Scope = {
123
+ name: Promise<string>
124
+ age: number
125
+ greeting: Promise<Greeting>
126
+ ageCalculator: Promise<AgeCalculator>
127
+ }
11
128
 
12
- const message = context.inject('test')
129
+ const context = new Context<Scope>()
13
130
 
14
- expect(message).toBe('Hello World')
131
+ context.registerValue('age', 42)
132
+ context.registerAsyncFactory('name', async () => 'Person')
133
+ context.registerAsyncClass('greeting', Greeting, ['name'])
134
+ context.registerAsyncClass('ageCalculator', AgeCalculator, ['name', 'age'])
15
135
 
16
- const TEST_KEY = Symbol()
17
- context.provide(TEST_KEY, 42)
136
+ const greeting = await context.resolve('greeting')
137
+ const ageCalculator = await context.resolve('ageCalculator')
18
138
 
19
- const number = context.inject(TEST_KEY)
20
- expect(number).toBe(42)
139
+ expect(greeting.greet()).toBe('Hello, Person.')
140
+ expect(ageCalculator.printAge()).toBe('Person is 42 years old.')
21
141
  })
22
142
 
23
- it('Should allow subscription to arbitrary events', async ({ expect }) => {
24
- const customEventCall = vi.fn()
143
+ it('should support singletons', async () => {
144
+ let factoryInstances = 0
145
+ let asyncFactoryInstances = 0
146
+ let classInstances = 0
25
147
 
26
- const context = new Context()
27
- context.provide('test', 'Hello World')
28
- expect(customEventCall.mock.calls.length).toBe(0)
148
+ class Singleton {
149
+ constructor(public value: string) {
150
+ classInstances = classInstances + 1
151
+ }
29
152
 
30
- context.on('customEvent', async (ctx, arg1, arg2, arg3) => {
31
- customEventCall()
32
- const message = await ctx.inject('test')
33
- expect(message).toBe('Hello World')
34
- expect(arg1).toBe(1)
35
- expect(arg2).toBe(2)
36
- expect(arg3).toBe(3)
153
+ test() {
154
+ return 'From a class: ' + this.value
155
+ }
156
+ }
157
+
158
+ type Scope = {
159
+ singletonValue: number
160
+ singletonFactory: string
161
+ singletonAsyncFactory: Promise<string>
162
+ singletonClass: Singleton
163
+ singletonAsyncClass: Promise<Singleton>
164
+ }
165
+
166
+ const context = new Context<Scope>()
167
+
168
+ context.registerValue('singletonValue', 42) // Note: Values are inherently singletons
169
+ context.registerFactory(
170
+ 'singletonFactory',
171
+ (value: number) => {
172
+ factoryInstances = factoryInstances + 1
173
+ return `The answer is ${value}`
174
+ },
175
+ ['singletonValue'],
176
+ {
177
+ singleton: true,
178
+ },
179
+ )
180
+ context.registerAsyncFactory(
181
+ 'singletonAsyncFactory',
182
+ async (answer: string) => {
183
+ asyncFactoryInstances = asyncFactoryInstances + 1
184
+ return `Since you've waited a long time, ${answer}`
185
+ },
186
+ ['singletonFactory'],
187
+ {
188
+ singleton: true,
189
+ },
190
+ )
191
+ context.registerClass('singletonClass', Singleton, ['singletonFactory'], {
192
+ singleton: true,
37
193
  })
194
+ context.registerAsyncClass('singletonAsyncClass', Singleton, ['singletonAsyncFactory'], {
195
+ singleton: true,
196
+ })
197
+
198
+ const singletonValue = context.resolve('singletonValue')
199
+ const singletonFactory = context.resolve('singletonFactory')
200
+ const singletonAsyncFactory = await context.resolve('singletonAsyncFactory')
201
+ const singletonClass = context.resolve('singletonClass')
202
+ const singletonAsyncClass = await context.resolve('singletonAsyncClass')
38
203
 
39
- await context.emit('customEvent', 1, 2, 3)
204
+ expect(singletonValue).toBe(42)
205
+ expect(singletonFactory).toBe('The answer is 42')
206
+ expect(singletonAsyncFactory).toBe("Since you've waited a long time, The answer is 42")
207
+ expect(singletonClass.test()).toBe('From a class: The answer is 42')
208
+ expect(singletonAsyncClass.test()).toBe("From a class: Since you've waited a long time, The answer is 42")
40
209
 
41
- expect(customEventCall.mock.calls.length).toBe(1)
210
+ expect(factoryInstances).toBe(1)
211
+ expect(classInstances).toBe(2)
42
212
  })
43
213
 
44
- it('Should extend other contexts', async ({ expect }) => {
45
- const context1 = new Context()
46
- const context2 = new Context()
47
- const context3 = new Context()
214
+ it('should support eager dependencies', async () => {
215
+ type Scope = {
216
+ foo: string
217
+ bar: number
218
+ name: string
219
+ }
220
+ const context = new Context<Scope>()
221
+
222
+ context.registerValue('bar', 42)
223
+ context.registerValue('name', 'Person')
224
+
225
+ let factoryInstances = 0
226
+
227
+ context.registerFactory(
228
+ 'foo',
229
+ (name: string, bar: number) => {
230
+ factoryInstances = factoryInstances + 1
231
+ return `Hello ${name}, the answer is ${bar}`
232
+ },
233
+ ['name', 'bar'],
234
+ {
235
+ eager: true,
236
+ },
237
+ )
48
238
 
49
- const SYMBOL_KEY = Symbol()
239
+ expect(factoryInstances).toBe(0)
50
240
 
51
- context1.provide('a', 1)
52
- context1.provide('c', 1)
53
- context2.provide('b', 2)
54
- context2.provide('c', 2)
55
- context2.provide(SYMBOL_KEY, 42)
56
- context3.provide('c', 3)
241
+ await context.emit('declaro:init')
57
242
 
58
- context1.extend(context2, context3)
243
+ expect(factoryInstances).toBe(1)
59
244
 
60
- const a = context1.inject('a')
61
- const b = context1.inject('b')
62
- const c = context1.inject('c')
63
- const symbolValue = context1.inject(SYMBOL_KEY)
245
+ const foo = context.resolve('foo')
64
246
 
65
- expect(a).toBe(1)
66
- expect(b).toBe(2)
67
- expect(c).toBe(3)
68
- expect(symbolValue).toBe(42)
247
+ expect(factoryInstances).toBe(1)
248
+
249
+ expect(foo).toBe('Hello Person, the answer is 42')
69
250
  })
70
251
 
71
- it('Should nest contexts', async ({ expect }) => {
72
- const context1 = new Context()
73
- const context2 = new Context()
252
+ it('should support scope resolution', async () => {
253
+ type Scope = {
254
+ foo: string
255
+ bar: number
256
+ name: string
257
+ }
258
+ const context = new Context<Scope>()
259
+
260
+ context.registerValue('bar', 42)
261
+ context.registerValue('name', 'Person')
262
+
263
+ let factoryInstances = 0
264
+
265
+ context.registerFactory(
266
+ 'foo',
267
+ (name: string, bar: number) => {
268
+ factoryInstances = factoryInstances + 1
269
+ return `Hello ${name}, the answer is ${bar}`
270
+ },
271
+ ['name', 'bar'],
272
+ )
74
273
 
75
- context1.provide('a', 1)
76
- context2.provide('a', 2)
274
+ expect(factoryInstances).toBe(0)
77
275
 
78
- context1.provide('nested', context2)
276
+ const foo = context.scope.foo
79
277
 
80
- const a1 = context1.inject('a')
81
- const a2 = context1.inject<Context>('nested')?.inject('a')
278
+ expect(factoryInstances).toBe(1)
279
+ expect(foo).toBe('Hello Person, the answer is 42')
82
280
 
83
- expect(a1).toBe(1)
84
- expect(a2).toBe(2)
281
+ const foo2 = context.scope.foo
282
+
283
+ expect(factoryInstances).toBe(2)
284
+ expect(foo2).toBe('Hello Person, the answer is 42')
285
+
286
+ expect(context.scope.bar).toBe(42)
287
+ expect(context.scope.name).toBe('Person')
85
288
  })
86
289
 
87
- it('Should allow async middleware', async ({ expect }) => {
88
- const context = new Context()
290
+ it('should support merging scopes', async () => {
291
+ type ScopeA = {
292
+ bar: number
293
+ name: string
294
+ foo: string
295
+ }
296
+ const context = new Context<ScopeA>()
297
+
298
+ let factoryInstances = 0
89
299
 
90
- await context.use(async (context) => {
91
- context.provide('TEST1', 'Hello World')
92
- await sleep(1)
93
- context.provide('TEST2', 'Hello Test')
300
+ context.registerValue('bar', 42)
301
+ context.registerValue('name', 'Person')
302
+ context.registerFactory('foo', () => {
303
+ factoryInstances = factoryInstances + 1
304
+ return 'This should never run'
94
305
  })
95
306
 
96
- const test1 = context.inject('TEST1')
97
- const test2 = context.inject('TEST2')
98
- expect(test1).toBe('Hello World')
99
- expect(test2).toBe('Hello Test')
307
+ type ScopeB = ScopeA & {
308
+ foo: string
309
+ baz: number
310
+ }
311
+
312
+ const context2 = new Context<ScopeB>()
313
+ context2.extend(context)
314
+
315
+ context2.registerFactory(
316
+ 'foo',
317
+ (name: string, bar: number) => {
318
+ factoryInstances = factoryInstances + 1
319
+ return `Hello ${name}, the answer is ${bar}`
320
+ },
321
+ ['name', 'bar'],
322
+ )
323
+ context2.registerValue('baz', 100)
324
+
325
+ // Ensure that merging doesn't actually run any factories
326
+ expect(factoryInstances).toBe(0)
327
+
328
+ const foo = context2.scope.foo
329
+
330
+ expect(factoryInstances).toBe(1)
331
+ expect(foo).toBe('Hello Person, the answer is 42')
332
+
333
+ const foo2 = context2.scope.foo
334
+
335
+ expect(factoryInstances).toBe(2)
336
+ expect(foo2).toBe('Hello Person, the answer is 42')
337
+
338
+ expect(context2.scope.bar).toBe(42)
339
+ expect(context2.scope.name).toBe('Person')
100
340
  })
101
341
 
102
- it('Should allow singletons', ({ expect }) => {
103
- const context = new Context()
342
+ it(`Should allow factories without args`, () => {
343
+ type Scope = {
344
+ foo: string
345
+ }
346
+ const context = new Context<Scope>()
347
+
348
+ context.registerFactory('foo', () => 'Hello')
104
349
 
105
- const KEY = Symbol()
106
- let instance = context.singleton(KEY, 1)
107
- instance = context.singleton(KEY, 2)
108
- instance = context.singleton(KEY, 3)
350
+ const foo = context.resolve('foo')
109
351
 
110
- expect(instance).toBe(1)
352
+ expect(foo).toBe('Hello')
111
353
  })
112
354
 
113
- it('Should allow hydration', ({ expect }) => {
114
- const context = new Context()
355
+ it('should get nested dependencies', async () => {
356
+ type Scope = {
357
+ foo: string
358
+ }
115
359
 
116
- class Test extends ContextConsumer {
117
- constructor(public context: Context) {
118
- super(context)
119
- }
360
+ type ScopeA = Scope & {
361
+ bar: string
362
+ }
120
363
 
121
- test() {
122
- return 'Yes'
123
- }
364
+ type ScopeB = ScopeA & {
365
+ baz: string
124
366
  }
125
367
 
126
- expect(context.hydrate(Test).test()).toBe('Yes')
368
+ type ScopeC = ScopeB & {
369
+ qux: string
370
+ }
371
+
372
+ const context = new Context<Scope>()
373
+ context.registerFactory('foo', () => 'A partridge in a pear tree')
374
+
375
+ const contextA = new Context<ScopeA>()
376
+ contextA.extend(context)
377
+ contextA.registerFactory('bar', (foo: string) => `${foo}, two turtle doves`, ['foo'])
378
+
379
+ const contextB = new Context<ScopeB>()
380
+ contextB.extend(contextA)
381
+ contextB.registerFactory('baz', (bar: string) => `${bar}, three French hens`, ['bar'])
382
+
383
+ const contextC = new Context<ScopeC>()
384
+ contextC.extend(contextB)
385
+ contextC.registerFactory('qux', (baz: string) => `${baz}, four calling birds`, ['baz'])
386
+
387
+ const qux = contextC.resolve('qux')
388
+
389
+ const allDependencies = contextC.getAllDependencies('qux')
390
+
391
+ expect(allDependencies[0].key).toBe('baz')
392
+ expect(allDependencies[1].key).toBe('bar')
393
+ expect(allDependencies[2].key).toBe('foo')
394
+
395
+ expect(allDependencies[0].inject).toEqual(['bar'])
396
+ expect(allDependencies[1].inject).toEqual(['foo'])
397
+ expect(allDependencies[2].inject).toEqual([])
398
+
399
+ expect(qux).toBe('A partridge in a pear tree, two turtle doves, three French hens, four calling birds')
127
400
  })
128
401
 
129
- it('Should allow hydration with args', ({ expect }) => {
130
- const context = new Context()
402
+ it('should get nested dependent values', async () => {
403
+ type Scope = {
404
+ foo: string
405
+ }
131
406
 
132
- class Test extends ContextConsumer {
133
- constructor(public context: Context, foo: string, bar: number) {
134
- super(context)
135
- }
407
+ type ScopeA = Scope & {
408
+ bar: string
409
+ }
136
410
 
137
- test() {
138
- return 'Yes'
139
- }
411
+ type ScopeB = ScopeA & {
412
+ baz: string
413
+ }
414
+
415
+ type ScopeC = ScopeB & {
416
+ qux: string
417
+ }
418
+
419
+ const context = new Context<Scope>()
420
+ context.registerFactory('foo', () => 'A partridge in a pear tree')
421
+
422
+ const contextA = new Context<ScopeA>()
423
+ contextA.extend(context)
424
+ contextA.registerFactory('bar', (foo: string) => `${foo}, two turtle doves`, ['foo'])
425
+
426
+ const contextB = new Context<ScopeB>()
427
+ contextB.extend(contextA)
428
+ contextB.registerFactory('baz', (bar: string) => `${bar}, three French hens`, ['bar'])
429
+
430
+ const contextC = new Context<ScopeC>()
431
+ contextC.extend(contextB)
432
+ contextC.registerFactory('qux', (baz: string) => `${baz}, four calling birds`, ['baz'])
433
+
434
+ const dependents = contextC.getAllDependents('foo')
435
+
436
+ expect(dependents[0].key).toBe('bar')
437
+ expect(dependents[1].key).toBe('baz')
438
+ expect(dependents[2].key).toBe('qux')
439
+
440
+ expect(dependents[0].inject).toEqual(['foo'])
441
+ expect(dependents[1].inject).toEqual(['bar'])
442
+ expect(dependents[2].inject).toEqual(['baz'])
443
+ })
444
+
445
+ it('should protect against scope leakage', async () => {
446
+ type BaseScope = {
447
+ foo: string
448
+ bar: string
449
+ base: string
450
+ validSingleton: string
451
+ validCachedSingleton: string
452
+ }
453
+
454
+ type ScopeA = BaseScope & {
455
+ baz: string
456
+ }
457
+
458
+ type ScopeB = BaseScope & {
459
+ qux: string
140
460
  }
141
461
 
142
- expect(context.hydrate(Test, 'hello', 42).test()).toBe('Yes')
462
+ const baseContext = new Context<BaseScope>()
463
+
464
+ baseContext.registerFactory('foo', () => 'Hello', [], {
465
+ singleton: true,
466
+ })
467
+ baseContext.registerFactory('bar', () => 'World', [])
468
+ baseContext.registerFactory('base', (foo: string, bar: string) => `Base: ${foo} ${bar}`, ['foo', 'bar'], {
469
+ singleton: true,
470
+ })
471
+ let validSingletonInstances = 0
472
+ baseContext.registerFactory(
473
+ 'validSingleton',
474
+ () => {
475
+ validSingletonInstances = validSingletonInstances + 1
476
+ return 'Singleton'
477
+ },
478
+ [],
479
+ {
480
+ singleton: true,
481
+ },
482
+ )
483
+ let validCachedSingletonInstances = 0
484
+ baseContext.registerFactory(
485
+ 'validCachedSingleton',
486
+ () => {
487
+ validCachedSingletonInstances = validCachedSingletonInstances + 1
488
+ return 'Cached Singleton'
489
+ },
490
+ [],
491
+ {
492
+ singleton: true,
493
+ },
494
+ )
495
+
496
+ // Resolve this before forking context to ensure that the cached value gets passed on.
497
+ const base = baseContext.resolve('base')
498
+ const validCachedSingleton = baseContext.resolve('validCachedSingleton')
499
+
500
+ const contextA = new Context<ScopeA>()
501
+ contextA.extend(baseContext)
502
+ // Override a value that the singleton is going to depend on, but is not a singleton
503
+ contextA.registerFactory('bar', () => 'Robby', [])
504
+ // Add another property that dependes on the overridden value
505
+ contextA.registerFactory('baz', (foo: string, bar: string) => `${foo} ${bar}`, ['foo', 'bar'], {
506
+ singleton: true,
507
+ })
508
+
509
+ const contextB = new Context<ScopeB>()
510
+ contextB.extend(baseContext)
511
+ // Override a value that the singleton is going to depend on, and is also a singleton
512
+ contextB.registerFactory('foo', () => 'Goodbye', [], {
513
+ singleton: true,
514
+ })
515
+ // Add another property that dependes on the overridden value
516
+ contextB.registerFactory('qux', (foo: string, bar: string) => `${foo} ${bar}`, ['foo', 'bar'], {
517
+ singleton: true,
518
+ })
519
+
520
+ const baseA = contextA.resolve('base')
521
+ const baseB = contextB.resolve('base')
522
+
523
+ const baz = contextA.resolve('baz')
524
+ const qux = contextB.resolve('qux')
525
+ const foo = baseContext.resolve('foo')
526
+ const bar = baseContext.resolve('bar')
527
+
528
+ const validSingleton = baseContext.resolve('validSingleton')
529
+ const validSingletonA = contextA.resolve('validSingleton')
530
+ const validSingletonB = contextB.resolve('validSingleton')
531
+
532
+ const validCachedSingletonA = contextA.resolve('validCachedSingleton')
533
+ const validCachedSingletonB = contextB.resolve('validCachedSingleton')
534
+
535
+ // resolve them all again
536
+ baseContext.resolve('validSingleton')
537
+ contextA.resolve('validSingleton')
538
+ contextB.resolve('validSingleton')
539
+ baseContext.resolve('validCachedSingleton')
540
+ contextA.resolve('validCachedSingleton')
541
+ contextB.resolve('validCachedSingleton')
542
+
543
+ expect(baz).toBe('Hello Robby')
544
+ expect(qux).toBe('Goodbye World')
545
+ expect(foo).toBe('Hello')
546
+ expect(bar).toBe('World')
547
+
548
+ expect(baseA).toBe('Base: Hello Robby')
549
+ expect(baseB).toBe('Base: Goodbye World')
550
+ expect(base).toBe('Base: Hello World')
551
+
552
+ expect(validSingleton).toBe('Singleton')
553
+ expect(validSingletonA).toBe('Singleton')
554
+ expect(validSingletonB).toBe('Singleton')
555
+
556
+ expect(validCachedSingleton).toBe('Cached Singleton')
557
+ expect(validCachedSingletonA).toBe('Cached Singleton')
558
+ expect(validCachedSingletonB).toBe('Cached Singleton')
559
+
560
+ expect(validSingletonInstances).toBe(3)
561
+ expect(validCachedSingletonInstances).toBe(1)
562
+ })
563
+
564
+ it('should invalid nested dependency caches when overriding a value', async () => {
565
+ type Scope = {
566
+ foo: string
567
+ }
568
+
569
+ type ScopeA = Scope & {
570
+ bar: string
571
+ }
572
+
573
+ type ScopeB = ScopeA & {
574
+ baz: string
575
+ }
576
+
577
+ type ScopeC = ScopeB & {
578
+ qux: string
579
+ }
580
+
581
+ const context = new Context<Scope>()
582
+ context.registerFactory('foo', () => 'A partridge in a pear tree', [], {
583
+ singleton: true,
584
+ })
585
+ context.resolve('foo')
586
+
587
+ const contextA = new Context<ScopeA>()
588
+ contextA.extend(context)
589
+ contextA.registerFactory('bar', (foo: string) => `two turtle doves, ${foo}`, ['foo'], {
590
+ singleton: true,
591
+ })
592
+ contextA.resolve('bar')
593
+
594
+ const contextB = new Context<ScopeB>()
595
+ contextB.extend(contextA)
596
+ contextB.registerFactory('baz', (bar: string) => `three French hens, ${bar}`, ['bar'], {
597
+ singleton: true,
598
+ })
599
+ contextB.resolve('baz')
600
+
601
+ const contextC = new Context<ScopeC>()
602
+ contextC.extend(contextB)
603
+ contextC.registerFactory('qux', (baz: string) => `four calling birds, ${baz}`, ['baz'], {
604
+ singleton: true,
605
+ })
606
+ contextC.resolve('qux')
607
+ contextC.registerFactory('foo', () => 'and a partridge in a pear tree', [], {
608
+ singleton: true,
609
+ })
610
+
611
+ const fooCache = contextC.introspect('foo').cachedValue
612
+ const barCache = contextC.introspect('bar').cachedValue
613
+ const bazCache = contextC.introspect('baz').cachedValue
614
+ const quxCache = contextC.introspect('qux').cachedValue
615
+
616
+ expect(fooCache).toBeUndefined()
617
+ expect(barCache).toBeUndefined()
618
+ expect(bazCache).toBeUndefined()
619
+ expect(quxCache).toBeUndefined()
620
+
621
+ contextC.resolve('qux')
622
+
623
+ const fooCache2 = contextC.introspect('foo').cachedValue
624
+ const barCache2 = contextC.introspect('bar').cachedValue
625
+ const bazCache2 = contextC.introspect('baz').cachedValue
626
+ const quxCache2 = contextC.introspect('qux').cachedValue
627
+
628
+ expect(fooCache2).toBe('and a partridge in a pear tree')
629
+ expect(barCache2).toBe('two turtle doves, and a partridge in a pear tree')
630
+ expect(bazCache2).toBe('three French hens, two turtle doves, and a partridge in a pear tree')
631
+ expect(quxCache2).toBe(
632
+ 'four calling birds, three French hens, two turtle doves, and a partridge in a pear tree',
633
+ )
143
634
  })
144
635
  })