ditox 2.4.1 → 3.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.
@@ -1,13 +1,11 @@
1
1
  import {
2
2
  CONTAINER,
3
3
  createContainer,
4
- FACTORIES_MAP,
5
- FAKE_FACTORY,
6
- PARENT_CONTAINER,
4
+ PARENT_CONTAINERS,
7
5
  ResolverError,
8
6
  } from './container';
9
- import {injectable} from './utils';
10
7
  import {optional, token} from './tokens';
8
+ import {injectable} from './utils';
11
9
 
12
10
  const NUMBER = token<number>('number');
13
11
  const STRING = token<string>('string');
@@ -44,10 +42,10 @@ describe('Container', () => {
44
42
 
45
43
  const custom = createContainer();
46
44
  container.bindValue(CONTAINER, custom);
47
- container.bindValue(PARENT_CONTAINER, custom);
45
+ container.bindValue(PARENT_CONTAINERS, [custom]);
48
46
 
49
47
  expect(container.get(CONTAINER)).toBe(container);
50
- expect(container.get(PARENT_CONTAINER)).toBe(parent);
48
+ expect(container.get(PARENT_CONTAINERS)).toEqual([parent]);
51
49
  });
52
50
  });
53
51
 
@@ -120,24 +118,40 @@ describe('Container', () => {
120
118
  scope: 'scoped',
121
119
  });
122
120
 
123
- expect(container.get(NUMBER)).toBe(21);
124
- expect(container.get(NUMBER)).toBe(21);
125
- expect(parent.get(NUMBER)).toBe(12);
126
- expect(container.get(NUMBER)).toBe(21);
121
+ expect(container.get(NUMBER)).toBe(11);
122
+ expect(container.get(NUMBER)).toBe(11);
123
+ expect(parent.get(NUMBER)).toBe(11);
124
+ expect(container.get(NUMBER)).toBe(11);
125
+
126
+ expect(factory).toBeCalledTimes(1);
127
+ });
128
+
129
+ it('should inherit a factory with "scoped" scope', async () => {
130
+ const parentParent = createContainer();
131
+ let counter = 0;
132
+ const factory = jest.fn(() => ++counter);
133
+
134
+ parentParent.bindFactory(NUMBER, factory);
135
+
136
+ expect(parentParent.get(NUMBER)).toBe(1);
137
+
138
+ const parent = createContainer(parentParent);
139
+
140
+ parent.bindFactory(NUMBER, injectable(factory), {
141
+ scope: 'scoped',
142
+ });
143
+
144
+ expect(parent.get(NUMBER)).toBe(2);
145
+
146
+ const container = createContainer(parent);
147
+
148
+ expect(container.get(NUMBER)).toBe(2);
149
+ expect(parent.get(NUMBER)).toBe(2);
127
150
 
128
151
  expect(factory).toBeCalledTimes(2);
129
152
 
130
- // Check for using the internal FAKE_FACTORY
131
- const internalFactories = container.resolve(FACTORIES_MAP);
132
- const fakeContext = internalFactories.get(NUMBER.symbol);
133
- expect(fakeContext?.factory).toBe(FAKE_FACTORY);
134
- expect(() => fakeContext?.factory?.(container)).toThrowError(
135
- 'FAKE_FACTORY',
136
- );
137
- const internalOnRemoved =
138
- fakeContext?.options?.scope === 'scoped' &&
139
- fakeContext?.options?.onRemoved;
140
- expect(internalOnRemoved).toBeUndefined();
153
+ container.removeAll();
154
+ expect(parent.get(NUMBER)).toBe(2);
141
155
  });
142
156
 
143
157
  it('should bind a factory with "singleton" scope by default', () => {
@@ -165,6 +179,9 @@ describe('Container', () => {
165
179
  const container1 = createContainer(parent);
166
180
  const container2 = createContainer(parent);
167
181
  const container3 = createContainer(parent);
182
+ const childContainer1 = createContainer(container1);
183
+ const childContainer2 = createContainer(container2);
184
+ const childContainer3 = createContainer(container3);
168
185
 
169
186
  const START = token<number>();
170
187
  parent.bindValue(START, 0);
@@ -178,13 +195,33 @@ describe('Container', () => {
178
195
  scope: 'scoped',
179
196
  });
180
197
 
181
- expect(container1.get(NUMBER)).toBe(11);
198
+ container1.bindFactory(NUMBER, injectable(factory, START), {
199
+ scope: 'scoped',
200
+ });
201
+
202
+ container2.bindFactory(NUMBER, injectable(factory, START), {
203
+ scope: 'scoped',
204
+ });
205
+
206
+ container3.bindFactory(NUMBER, injectable(factory, START), {
207
+ scope: 'scoped',
208
+ });
209
+
210
+ childContainer1.bindValue(START, 1);
211
+ childContainer2.bindValue(START, 2);
212
+ childContainer3.bindValue(START, 3);
213
+
182
214
  expect(container1.get(NUMBER)).toBe(11);
183
215
  expect(container2.get(NUMBER)).toBe(22);
184
- expect(parent.get(NUMBER)).toBe(3);
216
+ expect(container3.get(NUMBER)).toBe(33);
217
+ expect(parent.get(NUMBER)).toBe(4);
185
218
  expect(container1.get(NUMBER)).toBe(11);
186
219
  expect(container2.get(NUMBER)).toBe(22);
187
- expect(container3.get(NUMBER)).toBe(34);
220
+ expect(container3.get(NUMBER)).toBe(33);
221
+
222
+ expect(childContainer1.get(NUMBER)).toBe(11);
223
+ expect(childContainer2.get(NUMBER)).toBe(22);
224
+ expect(childContainer3.get(NUMBER)).toBe(33);
188
225
 
189
226
  expect(factory).toBeCalledTimes(4);
190
227
  });
@@ -228,12 +265,11 @@ describe('Container', () => {
228
265
  const parent = createContainer();
229
266
  const container = createContainer(parent);
230
267
 
231
- const factory = () => createContainer();
232
- container.bindFactory(CONTAINER, factory);
233
- container.bindFactory(PARENT_CONTAINER, factory);
268
+ container.bindFactory(CONTAINER, () => createContainer());
269
+ container.bindFactory(PARENT_CONTAINERS, () => [createContainer()]);
234
270
 
235
271
  expect(container.get(CONTAINER)).toBe(container);
236
- expect(container.get(PARENT_CONTAINER)).toBe(parent);
272
+ expect(container.get(PARENT_CONTAINERS)).toEqual([parent]);
237
273
  });
238
274
  });
239
275
 
@@ -284,25 +320,13 @@ describe('Container', () => {
284
320
  parent.bindFactory(NUMBER, factory, {scope: 'scoped', onRemoved});
285
321
 
286
322
  expect(parent.get(NUMBER)).toBe(1);
287
- expect(container.get(NUMBER)).toBe(2);
288
-
289
- // Check for using the internal FAKE_FACTORY
290
- const internalFactories = container.resolve(FACTORIES_MAP);
291
- const fakeContext = internalFactories.get(NUMBER.symbol);
292
- expect(fakeContext?.factory).toBe(FAKE_FACTORY);
293
- expect(() => fakeContext?.factory?.(container)).toThrowError(
294
- 'FAKE_FACTORY',
295
- );
296
- const internalOnRemoved =
297
- fakeContext?.options?.scope === 'scoped' &&
298
- fakeContext?.options?.onRemoved;
299
- expect(internalOnRemoved).toBe(onRemoved);
323
+ expect(container.get(NUMBER)).toBe(1);
300
324
 
301
325
  // Continue the main test
302
326
  parent.remove(NUMBER);
303
327
  container.remove(NUMBER);
304
328
  expect(onRemoved).toHaveBeenNthCalledWith(1, 1);
305
- expect(onRemoved).toHaveBeenNthCalledWith(2, 2);
329
+ expect(onRemoved).toHaveBeenCalledTimes(1);
306
330
  });
307
331
 
308
332
  it('should remove "transient" factory in case its value has never been resolved', () => {
@@ -333,10 +357,10 @@ describe('Container', () => {
333
357
  const container = createContainer(parent);
334
358
 
335
359
  container.remove(CONTAINER);
336
- container.remove(PARENT_CONTAINER);
360
+ container.remove(PARENT_CONTAINERS);
337
361
 
338
362
  expect(container.get(CONTAINER)).toBe(container);
339
- expect(container.get(PARENT_CONTAINER)).toBe(parent);
363
+ expect(container.get(PARENT_CONTAINERS)).toEqual([parent]);
340
364
  });
341
365
  });
342
366
 
@@ -389,7 +413,7 @@ describe('Container', () => {
389
413
  container.removeAll();
390
414
 
391
415
  expect(container.get(CONTAINER)).toBe(container);
392
- expect(container.get(PARENT_CONTAINER)).toBe(parent);
416
+ expect(container.get(PARENT_CONTAINERS)).toEqual([parent]);
393
417
  });
394
418
  });
395
419
 
@@ -477,13 +501,13 @@ describe('Container', () => {
477
501
  it('should return the parent container for PARENT_CONTAINER token', () => {
478
502
  const parent = createContainer();
479
503
  const container = createContainer(parent);
480
- const result = container.get(PARENT_CONTAINER);
481
- expect(result).toBe(parent);
504
+ const result = container.get(PARENT_CONTAINERS);
505
+ expect(result).toEqual([parent]);
482
506
  });
483
507
 
484
508
  it('should return "undefined" for PARENT_CONTAINER token in case there is no parent container', () => {
485
509
  const container = createContainer();
486
- const result = container.get(PARENT_CONTAINER);
510
+ const result = container.get(PARENT_CONTAINERS);
487
511
  expect(result).toBeUndefined();
488
512
  });
489
513
 
@@ -572,15 +596,15 @@ describe('Container', () => {
572
596
  it('should resolve PARENT_CONTAINER as the parent container', () => {
573
597
  const parent = createContainer();
574
598
  const container = createContainer(parent);
575
- const result = container.get(PARENT_CONTAINER);
576
- expect(result).toBe(parent);
599
+ const result = container.get(PARENT_CONTAINERS);
600
+ expect(result).toEqual([parent]);
577
601
  });
578
602
 
579
603
  it('should throw ResolverError for PARENT_CONTAINER token in case there is no parent container', () => {
580
604
  const container = createContainer();
581
- expect(() => container.resolve(PARENT_CONTAINER)).toThrowError(
605
+ expect(() => container.resolve(PARENT_CONTAINERS)).toThrowError(
582
606
  new ResolverError(
583
- `Token "${PARENT_CONTAINER.symbol.description}" is not provided`,
607
+ `Token "${PARENT_CONTAINERS.symbol.description}" is not provided`,
584
608
  ),
585
609
  );
586
610
  });
@@ -608,4 +632,60 @@ describe('Container', () => {
608
632
  expect(container.resolve(t2)).toBe(1);
609
633
  });
610
634
  });
635
+
636
+ describe('multiple parent containers', () => {
637
+ it('should expose all parent containers in order via PARENT_CONTAINERS', () => {
638
+ const p1 = createContainer();
639
+ const p2 = createContainer();
640
+ const child = createContainer([p1, p2]);
641
+
642
+ expect(child.get(PARENT_CONTAINERS)).toEqual([p1, p2]);
643
+ });
644
+
645
+ it('get(): should return a value from the first parent that provides it', () => {
646
+ const p1 = createContainer();
647
+ const p2 = createContainer();
648
+ p1.bindValue(NUMBER, 1);
649
+ p2.bindValue(NUMBER, 2);
650
+
651
+ const child = createContainer([p1, p2]);
652
+ expect(child.get(NUMBER)).toBe(1);
653
+ });
654
+
655
+ it('get(): should return a value from the next parent when previous parents do not provide it', () => {
656
+ const p1 = createContainer();
657
+ const p2 = createContainer();
658
+ p2.bindValue(NUMBER, 2);
659
+
660
+ const child = createContainer([p1, p2]);
661
+ expect(child.get(NUMBER)).toBe(2);
662
+ });
663
+
664
+ it('resolve(): should resolve a value from the first parent that provides it', () => {
665
+ const p1 = createContainer();
666
+ const p2 = createContainer();
667
+ p1.bindValue(NUMBER, 1);
668
+ p2.bindValue(NUMBER, 2);
669
+
670
+ const child = createContainer([p1, p2]);
671
+ expect(child.resolve(NUMBER)).toBe(1);
672
+ });
673
+
674
+ it('resolve(): should resolve a value from the next parent when previous parents do not provide it', () => {
675
+ const p1 = createContainer();
676
+ const p2 = createContainer();
677
+ p2.bindValue(NUMBER, 2);
678
+
679
+ const child = createContainer([p1, p2]);
680
+ expect(child.resolve(NUMBER)).toBe(2);
681
+ });
682
+
683
+ it('resolve(): should throw if none of the parents provide the value', () => {
684
+ const p1 = createContainer();
685
+ const p2 = createContainer();
686
+ const child = createContainer([p1, p2]);
687
+
688
+ expect(() => child.resolve(NUMBER)).toThrow(ResolverError);
689
+ });
690
+ });
611
691
  });
package/src/container.ts CHANGED
@@ -20,8 +20,8 @@ export type FactoryScope = 'scoped' | 'singleton' | 'transient';
20
20
  * Options for factory binding.
21
21
  *
22
22
  * `scope` types:
23
- * - `singleton` - **This is the default**. The value is created and cached by the container which registered the factory.
24
- * - `scoped` - The value is created and cached by the container which starts resolving.
23
+ * - `singleton` - **This is the default**. The value is created and cached by the most distant parent container which owns the factory function.
24
+ * - `scoped` - The value is created and cached by the nearest container which owns the factory function.
25
25
  * - `transient` - The value is created every time it is resolved.
26
26
  *
27
27
  * `scoped` and `singleton` scopes can have `onRemoved` callback. It is called when a token is removed from the container.
@@ -64,7 +64,7 @@ export type Container = {
64
64
  get<T>(token: Token<T>): T | undefined;
65
65
 
66
66
  /**
67
- * Returns a resolved value by the token, or throws `ResolverError` in case the token is not found.
67
+ * Returns a resolved value by the token or throws `ResolverError` in case the token is not found.
68
68
  */
69
69
  resolve<T>(token: Token<T>): T;
70
70
 
@@ -82,7 +82,7 @@ export type Container = {
82
82
  /** @internal */
83
83
  export const CONTAINER: Token<Container> = token('ditox.Container');
84
84
  /** @internal */
85
- export const PARENT_CONTAINER: Token<Container> = token(
85
+ export const PARENT_CONTAINERS: Token<ReadonlyArray<Container>> = token(
86
86
  'ditox.ParentContainer',
87
87
  );
88
88
  /** @internal */
@@ -91,11 +91,6 @@ export const RESOLVER: Token<Resolver> = token('ditox.Resolver');
91
91
  /** @internal */
92
92
  const NOT_FOUND = Symbol();
93
93
 
94
- /** @internal */
95
- export const FAKE_FACTORY = (): never => {
96
- throw new Error('FAKE_FACTORY');
97
- };
98
-
99
94
  /** @internal */
100
95
  const DEFAULT_SCOPE: FactoryScope = 'singleton';
101
96
 
@@ -135,7 +130,7 @@ function getOnRemoved<T>(options: FactoryOptions<T>) {
135
130
  function isInternalToken<T>(token: Token<T>): boolean {
136
131
  return (
137
132
  token.symbol === CONTAINER.symbol ||
138
- token.symbol === PARENT_CONTAINER.symbol ||
133
+ token.symbol === PARENT_CONTAINERS.symbol ||
139
134
  token.symbol === RESOLVER.symbol
140
135
  );
141
136
  }
@@ -145,9 +140,17 @@ function isInternalToken<T>(token: Token<T>): boolean {
145
140
  *
146
141
  * Container can have an optional parent to chain token resolution. The parent is used in case the current container does not have a registered token.
147
142
  *
148
- * @param parentContainer - Optional parent container.
143
+ * @param parentArg - Optional parent container or an array of containers.
149
144
  */
150
- export function createContainer(parentContainer?: Container): Container {
145
+ export function createContainer(
146
+ parentArg?: Container | ReadonlyArray<Container>,
147
+ ): Container {
148
+ const parents: ReadonlyArray<Container> | undefined = parentArg
149
+ ? Array.isArray(parentArg)
150
+ ? [...parentArg]
151
+ : [parentArg]
152
+ : undefined;
153
+
151
154
  const values: ValuesMap = new Map<symbol, any>();
152
155
  const factories: FactoriesMap = new Map<symbol, FactoryContext<any>>();
153
156
 
@@ -202,7 +205,7 @@ export function createContainer(parentContainer?: Container): Container {
202
205
  return (
203
206
  values.has(token.symbol) ||
204
207
  factories.has(token.symbol) ||
205
- (parentContainer?.hasToken(token) ?? false)
208
+ parentContainersHaveToken(parents, token)
206
209
  );
207
210
  },
208
211
 
@@ -247,14 +250,14 @@ export function createContainer(parentContainer?: Container): Container {
247
250
  }
248
251
 
249
252
  const factoryContext = factories.get(token.symbol);
250
- if (factoryContext && factoryContext.factory !== FAKE_FACTORY) {
253
+ if (factoryContext) {
251
254
  const scope = getScope(factoryContext.options);
252
255
 
253
256
  switch (scope) {
254
257
  case 'singleton': {
255
258
  if (hasValue) {
256
259
  return value;
257
- } else if (parentContainer?.hasToken(token)) {
260
+ } else if (parentContainersHaveToken(parents, token)) {
258
261
  break;
259
262
  } else {
260
263
  // Cache the value in the same container where the factory is registered.
@@ -265,16 +268,14 @@ export function createContainer(parentContainer?: Container): Container {
265
268
  }
266
269
 
267
270
  case 'scoped': {
268
- // Create a value within the origin container and cache it.
269
- const value = factoryContext.factory(origin);
270
- origin.bindValue(token, value);
271
-
272
- if (origin !== container) {
273
- // Bind a fake factory with actual options to make onRemoved() works.
274
- origin.bindFactory(token, FAKE_FACTORY, factoryContext.options);
271
+ if (hasValue) {
272
+ return value;
273
+ } else {
274
+ // Create a value within the factory's container and cache it.
275
+ const value = factoryContext.factory(container);
276
+ container.bindValue(token, value);
277
+ return value;
275
278
  }
276
-
277
- return value;
278
279
  }
279
280
 
280
281
  case 'transient': {
@@ -288,9 +289,8 @@ export function createContainer(parentContainer?: Container): Container {
288
289
  return value;
289
290
  }
290
291
 
291
- const parentResolver = parentContainer?.get(RESOLVER);
292
- if (parentResolver) {
293
- return parentResolver(token, origin);
292
+ if (parents) {
293
+ return parentContainersResolveToken(parents, token, origin);
294
294
  }
295
295
 
296
296
  return NOT_FOUND;
@@ -314,11 +314,44 @@ export function createContainer(parentContainer?: Container): Container {
314
314
  values.set(RESOLVER.symbol, resolver);
315
315
  values.set(FACTORIES_MAP.symbol, factories);
316
316
 
317
- if (parentContainer) {
318
- values.set(PARENT_CONTAINER.symbol, parentContainer);
317
+ if (parents) {
318
+ values.set(PARENT_CONTAINERS.symbol, parents);
319
319
  }
320
320
  }
321
321
 
322
322
  bindInternalTokens();
323
323
  return container;
324
324
  }
325
+
326
+ function parentContainersHaveToken<T>(
327
+ parents: ReadonlyArray<Container> | undefined,
328
+ token: Token<T>,
329
+ ): boolean {
330
+ if (!parents) return false;
331
+
332
+ for (let i = 0; i < parents.length; i++) {
333
+ if (parents[i].hasToken(token)) {
334
+ return true;
335
+ }
336
+ }
337
+
338
+ return false;
339
+ }
340
+
341
+ function parentContainersResolveToken<T>(
342
+ parents: ReadonlyArray<Container>,
343
+ token: Token<T>,
344
+ origin: Container,
345
+ ): T | typeof NOT_FOUND {
346
+ for (const parentContainer of parents) {
347
+ const parentResolver = parentContainer.get(RESOLVER);
348
+ if (parentResolver) {
349
+ const resolved = parentResolver(token, origin);
350
+ if (resolved !== NOT_FOUND) {
351
+ return resolved;
352
+ }
353
+ }
354
+ }
355
+
356
+ return NOT_FOUND;
357
+ }
@@ -7,8 +7,8 @@ import {
7
7
  Module,
8
8
  ModuleDeclaration,
9
9
  } from './modules';
10
- import {injectable} from './utils';
11
10
  import {token} from './tokens';
11
+ import {injectable} from './utils';
12
12
 
13
13
  describe('bindModule()', () => {
14
14
  type TestQueries = {getValue: () => number};
@@ -107,8 +107,8 @@ describe('bindModule()', () => {
107
107
  const container = createContainer(parent);
108
108
 
109
109
  bindModule(parent, MODULE, {scope: 'scoped'});
110
- expect(parent.get(MODULE_TOKEN)).not.toBe(container.get(MODULE_TOKEN));
111
- expect(parent.get(QUERIES_TOKEN)).not.toBe(container.get(QUERIES_TOKEN));
110
+ expect(parent.get(MODULE_TOKEN)).toBe(container.get(MODULE_TOKEN));
111
+ expect(parent.get(QUERIES_TOKEN)).toBe(container.get(QUERIES_TOKEN));
112
112
  });
113
113
 
114
114
  it('should remove "singleton" module when a container is cleaning', () => {
@@ -149,11 +149,12 @@ describe('bindModule()', () => {
149
149
  parent.resolve(MODULE_TOKEN);
150
150
  container.resolve(MODULE_TOKEN);
151
151
 
152
- parent.removeAll();
153
- expect(destroy).toBeCalledTimes(1);
154
-
155
152
  destroy.mockClear();
156
153
  container.removeAll();
154
+ expect(destroy).toBeCalledTimes(0);
155
+
156
+ destroy.mockClear();
157
+ parent.removeAll();
157
158
  expect(destroy).toBeCalledTimes(1);
158
159
  });
159
160
 
@@ -227,6 +228,68 @@ describe('bindModule()', () => {
227
228
  expect(container.resolve(ARG_TOKEN)).toBe('12');
228
229
  expect(container.resolve(RESULT_TOKEN)).toBe('123');
229
230
  });
231
+
232
+ it('should call beforeBinding() and afterBinding() in deep-first order', () => {
233
+ const route: string[] = [];
234
+
235
+ const declareTestModule = (
236
+ id: string,
237
+ imports?: ModuleDeclaration<Record<string, any>>[],
238
+ ) =>
239
+ declareModule({
240
+ imports,
241
+ beforeBinding: () => route.push(`${id}:before`),
242
+ factory: (container) => {
243
+ imports?.forEach((m) => container.resolve(m.token));
244
+ route.push(`${id}:factory`);
245
+ return {};
246
+ },
247
+ afterBinding: () => route.push(`${id}:after`),
248
+ });
249
+
250
+ /**
251
+ * m1
252
+ * / | \
253
+ * m2 m3 m4
254
+ * / \ \ |
255
+ * m5 m6 m7
256
+ */
257
+ const m7 = declareTestModule('m7');
258
+ const m6 = declareTestModule('m6');
259
+ const m5 = declareTestModule('m5');
260
+ const m4 = declareTestModule('m4', [m7]);
261
+ const m3 = declareTestModule('m3', [m7]);
262
+ const m2 = declareTestModule('m2', [m5, m6]);
263
+ const m1 = declareTestModule('m1', [m2, m3, m4]);
264
+
265
+ const container = createContainer();
266
+ bindModule(container, m1);
267
+ container.resolve(m1.token);
268
+
269
+ expect(route).toEqual([
270
+ 'm1:before',
271
+ 'm2:before',
272
+ 'm3:before',
273
+ 'm4:before',
274
+ 'm5:before',
275
+ 'm6:before',
276
+ 'm7:before',
277
+ 'm7:after',
278
+ 'm6:after',
279
+ 'm5:after',
280
+ 'm4:after',
281
+ 'm3:after',
282
+ 'm2:after',
283
+ 'm1:after',
284
+ 'm5:factory',
285
+ 'm6:factory',
286
+ 'm2:factory',
287
+ 'm7:factory',
288
+ 'm3:factory',
289
+ 'm4:factory',
290
+ 'm1:factory',
291
+ ]);
292
+ });
230
293
  });
231
294
 
232
295
  describe('bindModules()', () => {