@data-client/core 0.14.18 → 0.14.19

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 (83) hide show
  1. package/dist/index.js +553 -277
  2. package/dist/index.umd.min.js +1 -1
  3. package/legacy/actions.js +1 -1
  4. package/legacy/controller/Controller.js +100 -11
  5. package/legacy/controller/actions/createFetch.js +3 -2
  6. package/legacy/controller/ensurePojo.js +3 -2
  7. package/legacy/index.js +3 -4
  8. package/legacy/manager/DevtoolsManager.js +14 -7
  9. package/legacy/manager/NetworkManager.js +8 -5
  10. package/legacy/manager/PollingSubscription.js +6 -3
  11. package/legacy/manager/SubscriptionManager.js +4 -3
  12. package/legacy/manager/applyManager.js +3 -7
  13. package/legacy/manager/initManager.js +15 -0
  14. package/legacy/state/GCPolicy.js +153 -0
  15. package/legacy/state/reducer/createReducer.js +7 -3
  16. package/legacy/state/reducer/expireReducer.js +5 -4
  17. package/legacy/state/reducer/fetchReducer.js +3 -2
  18. package/legacy/state/reducer/invalidateReducer.js +6 -5
  19. package/legacy/state/reducer/setResponseReducer.js +10 -7
  20. package/legacy/types.js +1 -1
  21. package/lib/actions.d.ts +2 -2
  22. package/lib/actions.d.ts.map +1 -1
  23. package/lib/actions.js +1 -1
  24. package/lib/controller/Controller.d.ts +108 -5
  25. package/lib/controller/Controller.d.ts.map +1 -1
  26. package/lib/controller/Controller.js +96 -8
  27. package/lib/index.d.ts +2 -0
  28. package/lib/index.d.ts.map +1 -1
  29. package/lib/index.js +3 -4
  30. package/lib/manager/DevtoolsManager.js +7 -3
  31. package/lib/manager/NetworkManager.js +3 -2
  32. package/lib/manager/SubscriptionManager.d.ts.map +1 -1
  33. package/lib/manager/SubscriptionManager.js +1 -1
  34. package/lib/manager/applyManager.d.ts.map +1 -1
  35. package/lib/manager/applyManager.js +3 -7
  36. package/lib/manager/initManager.d.ts +4 -0
  37. package/lib/manager/initManager.d.ts.map +1 -0
  38. package/lib/manager/initManager.js +15 -0
  39. package/lib/state/GCPolicy.d.ts +55 -0
  40. package/lib/state/GCPolicy.d.ts.map +1 -0
  41. package/lib/state/GCPolicy.js +153 -0
  42. package/lib/state/reducer/createReducer.js +5 -2
  43. package/lib/state/reducer/expireReducer.d.ts +1 -0
  44. package/lib/state/reducer/expireReducer.d.ts.map +1 -1
  45. package/lib/state/reducer/invalidateReducer.d.ts +1 -0
  46. package/lib/state/reducer/invalidateReducer.d.ts.map +1 -1
  47. package/lib/state/reducer/setReducer.d.ts +3 -2
  48. package/lib/state/reducer/setReducer.d.ts.map +1 -1
  49. package/lib/state/reducer/setResponseReducer.d.ts +4 -2
  50. package/lib/state/reducer/setResponseReducer.d.ts.map +1 -1
  51. package/lib/state/reducer/setResponseReducer.js +4 -2
  52. package/lib/types.d.ts +1 -0
  53. package/lib/types.d.ts.map +1 -1
  54. package/lib/types.js +1 -1
  55. package/package.json +8 -3
  56. package/src/actions.ts +2 -1
  57. package/src/controller/Controller.ts +205 -8
  58. package/src/controller/__tests__/Controller.ts +8 -4
  59. package/src/controller/__tests__/__snapshots__/get.ts.snap +7 -0
  60. package/src/controller/__tests__/__snapshots__/getResponse.ts.snap +15 -0
  61. package/src/controller/__tests__/get.ts +45 -17
  62. package/src/controller/__tests__/getResponse.ts +46 -0
  63. package/src/index.ts +2 -6
  64. package/src/manager/SubscriptionManager.ts +0 -1
  65. package/src/manager/applyManager.ts +3 -4
  66. package/src/manager/initManager.ts +21 -0
  67. package/src/state/GCPolicy.ts +197 -0
  68. package/src/state/__tests__/GCPolicy.test.ts +258 -0
  69. package/src/state/__tests__/__snapshots__/reducer.ts.snap +2 -0
  70. package/src/state/__tests__/reducer.ts +4 -4
  71. package/src/state/reducer/createReducer.ts +1 -1
  72. package/src/state/reducer/setResponseReducer.ts +3 -1
  73. package/src/types.ts +1 -0
  74. package/ts3.4/actions.d.ts +2 -5
  75. package/ts3.4/controller/Controller.d.ts +141 -5
  76. package/ts3.4/index.d.ts +2 -0
  77. package/ts3.4/manager/initManager.d.ts +4 -0
  78. package/ts3.4/state/GCPolicy.d.ts +55 -0
  79. package/ts3.4/state/reducer/expireReducer.d.ts +1 -0
  80. package/ts3.4/state/reducer/invalidateReducer.d.ts +1 -0
  81. package/ts3.4/state/reducer/setReducer.d.ts +3 -2
  82. package/ts3.4/state/reducer/setResponseReducer.d.ts +4 -2
  83. package/ts3.4/types.d.ts +1 -0
@@ -35,17 +35,23 @@ import {
35
35
  } from './actions/index.js';
36
36
  import ensurePojo from './ensurePojo.js';
37
37
  import type { EndpointUpdateFunction } from './types.js';
38
+ import { ReduxMiddlewareAPI } from '../manager/applyManager.js';
39
+ import type { GCInterface } from '../state/GCPolicy.js';
40
+ import { ImmortalGCPolicy } from '../state/GCPolicy.js';
38
41
  import { initialState } from '../state/reducer/createReducer.js';
39
42
  import selectMeta from '../state/selectMeta.js';
40
- import type { ActionTypes, State } from '../types.js';
43
+ import type { ActionTypes, Dispatch, State } from '../types.js';
41
44
 
42
45
  export type GenericDispatch = (value: any) => Promise<void>;
43
46
  export type DataClientDispatch = (value: ActionTypes) => Promise<void>;
44
47
 
45
- interface ConstructorProps<D extends GenericDispatch = DataClientDispatch> {
48
+ export interface ControllerConstructorProps<
49
+ D extends GenericDispatch = DataClientDispatch,
50
+ > {
46
51
  dispatch?: D;
47
52
  getState?: () => State<unknown>;
48
53
  memo?: Pick<MemoCache, 'denormalize' | 'query' | 'buildQueryKey'>;
54
+ gcPolicy?: GCInterface;
49
55
  }
50
56
 
51
57
  const unsetDispatch = (action: unknown): Promise<void> => {
@@ -65,6 +71,7 @@ const unsetState = (): State<unknown> => {
65
71
  * @see https://dataclient.io/docs/api/Controller
66
72
  */
67
73
  export default class Controller<
74
+ // NOTE: We template on entire dispatch, so we can be contravariant on ActionTypes
68
75
  D extends GenericDispatch = DataClientDispatch,
69
76
  > {
70
77
  /**
@@ -72,7 +79,7 @@ export default class Controller<
72
79
  *
73
80
  * @see https://dataclient.io/docs/api/Controller#dispatch
74
81
  */
75
- declare readonly dispatch: D;
82
+ declare protected _dispatch: D;
76
83
  /**
77
84
  * Gets the latest state snapshot that is fully committed.
78
85
  *
@@ -80,7 +87,7 @@ export default class Controller<
80
87
  * This should *not* be used to render; instead useSuspense() or useCache()
81
88
  * @see https://dataclient.io/docs/api/Controller#getState
82
89
  */
83
- declare readonly getState: () => State<unknown>;
90
+ declare getState: () => State<unknown>;
84
91
  /**
85
92
  * Singleton to maintain referential equality between calls
86
93
  */
@@ -89,14 +96,43 @@ export default class Controller<
89
96
  'denormalize' | 'query' | 'buildQueryKey'
90
97
  >;
91
98
 
99
+ /**
100
+ * Handles garbage collection
101
+ */
102
+ declare readonly gcPolicy: GCInterface;
103
+
92
104
  constructor({
93
105
  dispatch = unsetDispatch as any,
94
106
  getState = unsetState,
95
107
  memo = new MemoCache(),
96
- }: ConstructorProps<D> = {}) {
97
- this.dispatch = dispatch;
108
+ gcPolicy = new ImmortalGCPolicy(),
109
+ }: ControllerConstructorProps<D> = {}) {
110
+ this._dispatch = dispatch;
98
111
  this.getState = getState;
99
112
  this.memo = memo;
113
+ this.gcPolicy = gcPolicy;
114
+ }
115
+
116
+ // TODO: drop when drop support for destructuring (0.14 and below)
117
+ set dispatch(dispatch: D) {
118
+ /* istanbul ignore next */
119
+ this._dispatch = dispatch;
120
+ }
121
+
122
+ // TODO: drop when drop support for destructuring (0.14 and below)
123
+ get dispatch(): D {
124
+ return this._dispatch;
125
+ }
126
+
127
+ bindMiddleware({
128
+ dispatch,
129
+ getState,
130
+ }: {
131
+ dispatch: D;
132
+ getState: ReduxMiddlewareAPI['getState'];
133
+ }) {
134
+ this._dispatch = dispatch;
135
+ this.getState = getState;
100
136
  }
101
137
 
102
138
  /*************** Action Dispatchers ***************/
@@ -336,7 +372,7 @@ export default class Controller<
336
372
  * Gets a snapshot (https://dataclient.io/docs/api/Snapshot)
337
373
  * @see https://dataclient.io/docs/api/Controller#snapshot
338
374
  */
339
- snapshot = (state: State<unknown>, fetchedAt?: number): SnapshotInterface => {
375
+ snapshot = (state: State<unknown>, fetchedAt?: number): Snapshot<unknown> => {
340
376
  return new Snapshot(this, state, fetchedAt);
341
377
  };
342
378
 
@@ -389,6 +425,7 @@ export default class Controller<
389
425
  data: DenormalizeNullable<E['schema']>;
390
426
  expiryStatus: ExpiryStatus;
391
427
  expiresAt: number;
428
+ countRef: () => () => void;
392
429
  };
393
430
 
394
431
  getResponse<
@@ -403,6 +440,7 @@ export default class Controller<
403
440
  data: DenormalizeNullable<E['schema']>;
404
441
  expiryStatus: ExpiryStatus;
405
442
  expiresAt: number;
443
+ countRef: () => () => void;
406
444
  };
407
445
 
408
446
  getResponse(
@@ -412,6 +450,51 @@ export default class Controller<
412
450
  data: unknown;
413
451
  expiryStatus: ExpiryStatus;
414
452
  expiresAt: number;
453
+ countRef: () => () => void;
454
+ } {
455
+ // TODO: breaking: only return data
456
+ return this.getResponseMeta(endpoint, ...rest);
457
+ }
458
+
459
+ /**
460
+ * Gets the (globally referentially stable) response for a given endpoint/args pair from state given.
461
+ * @see https://dataclient.io/docs/api/Controller#getResponseMeta
462
+ */
463
+ getResponseMeta<E extends EndpointInterface>(
464
+ endpoint: E,
465
+ ...rest:
466
+ | readonly [null, State<unknown>]
467
+ | readonly [...Parameters<E>, State<unknown>]
468
+ ): {
469
+ data: DenormalizeNullable<E['schema']>;
470
+ expiryStatus: ExpiryStatus;
471
+ expiresAt: number;
472
+ countRef: () => () => void;
473
+ };
474
+
475
+ getResponseMeta<
476
+ E extends Pick<EndpointInterface, 'key' | 'schema' | 'invalidIfStale'>,
477
+ >(
478
+ endpoint: E,
479
+ ...rest: readonly [
480
+ ...(readonly [...Parameters<E['key']>] | readonly [null]),
481
+ State<unknown>,
482
+ ]
483
+ ): {
484
+ data: DenormalizeNullable<E['schema']>;
485
+ expiryStatus: ExpiryStatus;
486
+ expiresAt: number;
487
+ countRef: () => () => void;
488
+ };
489
+
490
+ getResponseMeta(
491
+ endpoint: EndpointInterface,
492
+ ...rest: readonly [...unknown[], State<unknown>]
493
+ ): {
494
+ data: unknown;
495
+ expiryStatus: ExpiryStatus;
496
+ expiresAt: number;
497
+ countRef: () => () => void;
415
498
  } {
416
499
  const state = rest[rest.length - 1] as State<unknown>;
417
500
  // this is typescript generics breaking
@@ -446,12 +529,14 @@ export default class Controller<
446
529
  data: input as any,
447
530
  expiryStatus: ExpiryStatus.Valid,
448
531
  expiresAt: Infinity,
532
+ countRef: () => () => undefined,
449
533
  };
450
534
  }
451
535
 
452
536
  let isInvalid = false;
453
537
  if (shouldQuery) {
454
538
  isInvalid = !validateQueryKey(input);
539
+ // endpoint without entities
455
540
  } else if (!schema || !schemaHasEntity(schema)) {
456
541
  return {
457
542
  data: cacheEndpoints,
@@ -460,6 +545,7 @@ export default class Controller<
460
545
  : cacheEndpoints && !endpoint.invalidIfStale ? ExpiryStatus.Valid
461
546
  : ExpiryStatus.InvalidIfStale,
462
547
  expiresAt: expiresAt || 0,
548
+ countRef: this.gcPolicy.createCountRef({ key }),
463
549
  };
464
550
  }
465
551
 
@@ -477,6 +563,7 @@ export default class Controller<
477
563
 
478
564
  return this.getSchemaResponse(
479
565
  data,
566
+ key,
480
567
  paths,
481
568
  state.entityMeta,
482
569
  expiresAt,
@@ -505,8 +592,55 @@ export default class Controller<
505
592
  return this.memo.query(schema, args, state.entities as any, state.indexes);
506
593
  }
507
594
 
595
+ /**
596
+ * Queries the store for a Querable schema; providing related metadata
597
+ * @see https://dataclient.io/docs/api/Controller#getQueryMeta
598
+ */
599
+ getQueryMeta<S extends Queryable>(
600
+ schema: S,
601
+ ...rest: readonly [
602
+ ...SchemaArgs<S>,
603
+ Pick<State<unknown>, 'entities' | 'entityMeta'>,
604
+ ]
605
+ ): {
606
+ data: DenormalizeNullable<S> | undefined;
607
+ countRef: () => () => void;
608
+ } {
609
+ const state = rest[rest.length - 1] as State<any>;
610
+ // this is typescript generics breaking
611
+ const args: any = rest
612
+ .slice(0, rest.length - 1)
613
+ .map(ensurePojo) as SchemaArgs<S>;
614
+
615
+ // TODO: breaking: Switch back to this.memo.query(schema, args, state.entities as any, state.indexes) to do
616
+ // this logic
617
+ const input = this.memo.buildQueryKey(
618
+ schema,
619
+ args,
620
+ state.entities as any,
621
+ state.indexes,
622
+ JSON.stringify(args),
623
+ );
624
+
625
+ if (!input) {
626
+ return { data: undefined, countRef: () => () => undefined };
627
+ }
628
+
629
+ const { data, paths } = this.memo.denormalize(
630
+ schema,
631
+ input,
632
+ state.entities,
633
+ args,
634
+ );
635
+ return {
636
+ data: typeof data === 'symbol' ? undefined : (data as any),
637
+ countRef: this.gcPolicy.createCountRef({ paths }),
638
+ };
639
+ }
640
+
508
641
  private getSchemaResponse<T>(
509
642
  data: T,
643
+ key: string,
510
644
  paths: EntityPath[],
511
645
  entityMeta: State<unknown>['entityMeta'],
512
646
  expiresAt: number,
@@ -516,6 +650,7 @@ export default class Controller<
516
650
  data: T;
517
651
  expiryStatus: ExpiryStatus;
518
652
  expiresAt: number;
653
+ countRef: () => () => void;
519
654
  } {
520
655
  const invalidDenormalize = typeof data === 'symbol';
521
656
 
@@ -533,7 +668,12 @@ export default class Controller<
533
668
  : invalidDenormalize || invalidIfStale ? ExpiryStatus.InvalidIfStale
534
669
  : ExpiryStatus.Valid;
535
670
 
536
- return { data, expiryStatus, expiresAt };
671
+ return {
672
+ data,
673
+ expiryStatus,
674
+ expiresAt,
675
+ countRef: this.gcPolicy.createCountRef({ key, paths }),
676
+ };
537
677
  }
538
678
  }
539
679
 
@@ -639,6 +779,49 @@ class Snapshot<T = unknown> implements SnapshotInterface {
639
779
  return this.controller.getResponse(endpoint, ...args, this.state);
640
780
  }
641
781
 
782
+ /** @see https://dataclient.io/docs/api/Snapshot#getResponseMeta */
783
+ getResponseMeta<E extends EndpointInterface>(
784
+ endpoint: E,
785
+ ...args: readonly [null]
786
+ ): {
787
+ data: DenormalizeNullable<E['schema']>;
788
+ expiryStatus: ExpiryStatus;
789
+ expiresAt: number;
790
+ };
791
+
792
+ getResponseMeta<E extends EndpointInterface>(
793
+ endpoint: E,
794
+ ...args: readonly [...Parameters<E>]
795
+ ): {
796
+ data: DenormalizeNullable<E['schema']>;
797
+ expiryStatus: ExpiryStatus;
798
+ expiresAt: number;
799
+ };
800
+
801
+ getResponseMeta<
802
+ E extends Pick<EndpointInterface, 'key' | 'schema' | 'invalidIfStale'>,
803
+ >(
804
+ endpoint: E,
805
+ ...args: readonly [...Parameters<E['key']>] | readonly [null]
806
+ ): {
807
+ data: DenormalizeNullable<E['schema']>;
808
+ expiryStatus: ExpiryStatus;
809
+ expiresAt: number;
810
+ };
811
+
812
+ getResponseMeta<
813
+ E extends Pick<EndpointInterface, 'key' | 'schema' | 'invalidIfStale'>,
814
+ >(
815
+ endpoint: E,
816
+ ...args: readonly [...Parameters<E['key']>] | readonly [null]
817
+ ): {
818
+ data: DenormalizeNullable<E['schema']>;
819
+ expiryStatus: ExpiryStatus;
820
+ expiresAt: number;
821
+ } {
822
+ return this.controller.getResponseMeta(endpoint, ...args, this.state);
823
+ }
824
+
642
825
  /** @see https://dataclient.io/docs/api/Snapshot#getError */
643
826
  getError<E extends EndpointInterface>(
644
827
  endpoint: E,
@@ -667,4 +850,18 @@ class Snapshot<T = unknown> implements SnapshotInterface {
667
850
  ): DenormalizeNullable<S> | undefined {
668
851
  return this.controller.get(schema, ...args, this.state);
669
852
  }
853
+
854
+ /**
855
+ * Queries the store for a Querable schema; providing related metadata
856
+ * @see https://dataclient.io/docs/api/Snapshot#getQueryMeta
857
+ */
858
+ getQueryMeta<S extends Queryable>(
859
+ schema: S,
860
+ ...args: SchemaArgs<S>
861
+ ): {
862
+ data: DenormalizeNullable<S> | undefined;
863
+ countRef: () => () => void;
864
+ } {
865
+ return this.controller.getQueryMeta(schema, ...args, this.state);
866
+ }
670
867
  }
@@ -50,19 +50,21 @@ describe('Controller', () => {
50
50
  meta: {
51
51
  [fetchKey]: {
52
52
  date: Date.now(),
53
+ fetchedAt: Date.now(),
53
54
  expiresAt: Date.now() + 10000,
54
55
  },
55
56
  },
56
57
  };
57
58
  const getState = () => state;
59
+ const dispatch = jest.fn(() => Promise.resolve());
58
60
  const controller = new Controller({
59
- dispatch: jest.fn(() => Promise.resolve()),
61
+ dispatch,
60
62
  getState,
61
63
  });
62
64
  const article = await controller.fetchIfStale(CoolerArticleResource.get, {
63
65
  id: payload.id,
64
66
  });
65
- expect(controller.dispatch.mock.calls.length).toBe(0);
67
+ expect(dispatch.mock.calls.length).toBe(0);
66
68
  expect(article.title).toBe(payload.title);
67
69
  });
68
70
  it('should fetch if result stale', () => {
@@ -87,20 +89,22 @@ describe('Controller', () => {
87
89
  meta: {
88
90
  [fetchKey]: {
89
91
  date: 0,
92
+ fetchedAt: 0,
90
93
  expiresAt: 0,
91
94
  },
92
95
  },
93
96
  };
94
97
  const getState = () => state;
98
+ const dispatch = jest.fn(() => Promise.resolve());
95
99
  const controller = new Controller({
96
- dispatch: jest.fn(() => Promise.resolve()),
100
+ dispatch,
97
101
  getState,
98
102
  });
99
103
  controller.fetchIfStale(CoolerArticleResource.get, {
100
104
  id: payload.id,
101
105
  });
102
106
 
103
- expect(controller.dispatch.mock.calls.length).toBe(1);
107
+ expect(dispatch.mock.calls.length).toBe(1);
104
108
  });
105
109
  });
106
110
  });
@@ -118,3 +118,10 @@ Tacos {
118
118
  "type": "foo",
119
119
  }
120
120
  `;
121
+
122
+ exports[`Snapshot.getQueryMeta() query Entity based on pk 1`] = `
123
+ Tacos {
124
+ "id": "1",
125
+ "type": "foo",
126
+ }
127
+ `;
@@ -33,3 +33,18 @@ exports[`Controller.getResponse() infers schema with extra members but not set 1
33
33
  },
34
34
  }
35
35
  `;
36
+
37
+ exports[`Snapshot.getResponseMeta() denormalizes schema with extra members but not set 1`] = `
38
+ {
39
+ "data": [
40
+ Tacos {
41
+ "id": "1",
42
+ "type": "foo",
43
+ },
44
+ Tacos {
45
+ "id": "2",
46
+ "type": "bar",
47
+ },
48
+ ],
49
+ }
50
+ `;
@@ -3,24 +3,24 @@ import { Entity, schema } from '@data-client/endpoint';
3
3
  import { initialState } from '../../state/reducer/createReducer';
4
4
  import Controller from '../Controller';
5
5
 
6
- describe('Controller.get()', () => {
7
- class Tacos extends Entity {
8
- type = '';
9
- id = '';
10
- }
11
- const TacoList = new schema.Collection([Tacos]);
12
- const entities = {
13
- Tacos: {
14
- 1: { id: '1', type: 'foo' },
15
- 2: { id: '2', type: 'bar' },
16
- },
17
- [TacoList.key]: {
18
- [TacoList.pk(undefined, undefined, '', [{ type: 'foo' }])]: ['1'],
19
- [TacoList.pk(undefined, undefined, '', [{ type: 'bar' }])]: ['2'],
20
- [TacoList.pk(undefined, undefined, '', [])]: ['1', '2'],
21
- },
22
- };
6
+ class Tacos extends Entity {
7
+ type = '';
8
+ id = '';
9
+ }
10
+ const TacoList = new schema.Collection([Tacos]);
11
+ const entities = {
12
+ Tacos: {
13
+ 1: { id: '1', type: 'foo' },
14
+ 2: { id: '2', type: 'bar' },
15
+ },
16
+ [TacoList.key]: {
17
+ [TacoList.pk(undefined, undefined, '', [{ type: 'foo' }])]: ['1'],
18
+ [TacoList.pk(undefined, undefined, '', [{ type: 'bar' }])]: ['2'],
19
+ [TacoList.pk(undefined, undefined, '', [])]: ['1', '2'],
20
+ },
21
+ };
23
22
 
23
+ describe('Controller.get()', () => {
24
24
  it('query Entity based on pk', () => {
25
25
  const controller = new Controller();
26
26
  const state = {
@@ -273,3 +273,31 @@ describe('Controller.get()', () => {
273
273
  () => controller.get(queryPerson, { id: '1', doesnotexist: 5 }, state);
274
274
  });
275
275
  });
276
+
277
+ describe('Snapshot.getQueryMeta()', () => {
278
+ it('query Entity based on pk', () => {
279
+ const controller = new Controller();
280
+ const state = {
281
+ ...initialState,
282
+ entities,
283
+ };
284
+ const snapshot = controller.snapshot(state);
285
+ const taco = snapshot.getQueryMeta(Tacos, { id: '1' }).data;
286
+ expect(taco).toBeDefined();
287
+ expect(taco).toBeInstanceOf(Tacos);
288
+ expect(taco).toMatchSnapshot();
289
+ const taco2 = snapshot.getQueryMeta(Tacos, { id: '2' }).data;
290
+ expect(taco2).toBeDefined();
291
+ expect(taco2).toBeInstanceOf(Tacos);
292
+ expect(taco2).not.toEqual(taco);
293
+ // should maintain referential equality
294
+ expect(taco).toBe(snapshot.getQueryMeta(Tacos, { id: '1' }).data);
295
+
296
+ // @ts-expect-error
297
+ () => snapshot.getQueryMeta(Tacos, { id: { bob: 5 } });
298
+ // @ts-expect-error
299
+ expect(snapshot.getQueryMeta(Tacos, 5).data).toBeUndefined();
300
+ // @ts-expect-error
301
+ () => snapshot.getQueryMeta(Tacos, { doesnotexist: 5 });
302
+ });
303
+ });
@@ -174,3 +174,49 @@ describe('Controller.getResponse()', () => {
174
174
  expect(second.expiresAt).toBe(expiresAt);
175
175
  });
176
176
  });
177
+
178
+ describe('Snapshot.getResponseMeta()', () => {
179
+ it('denormalizes schema with extra members but not set', () => {
180
+ const controller = new Contoller();
181
+ class Tacos extends Entity {
182
+ type = '';
183
+ id = '';
184
+ }
185
+ const ep = new Endpoint(() => Promise.resolve(), {
186
+ key() {
187
+ return 'mytest';
188
+ },
189
+ schema: {
190
+ data: [Tacos],
191
+ extra: '',
192
+ page: {
193
+ first: null,
194
+ second: undefined,
195
+ third: 0,
196
+ complex: { complex: true, next: false },
197
+ },
198
+ },
199
+ });
200
+ const entities = {
201
+ Tacos: {
202
+ 1: { id: '1', type: 'foo' },
203
+ 2: { id: '2', type: 'bar' },
204
+ },
205
+ };
206
+
207
+ const state = {
208
+ ...initialState,
209
+ entities,
210
+ endpoints: {
211
+ [ep.key()]: {
212
+ data: ['1', '2'],
213
+ },
214
+ },
215
+ };
216
+ const { data, expiryStatus } = controller
217
+ .snapshot(state)
218
+ .getResponseMeta(ep);
219
+ expect(expiryStatus).toBe(ExpiryStatus.Valid);
220
+ expect(data).toMatchSnapshot();
221
+ });
222
+ });
package/src/index.ts CHANGED
@@ -1,9 +1,3 @@
1
- Object.hasOwn =
2
- Object.hasOwn ||
3
- /* istanbul ignore next */ function hasOwn(it, key) {
4
- return Object.prototype.hasOwnProperty.call(it, key);
5
- };
6
-
7
1
  export * as __INTERNAL__ from './internal.js';
8
2
  export type {
9
3
  NetworkError,
@@ -29,11 +23,13 @@ export {
29
23
  default as NetworkManager,
30
24
  ResetError,
31
25
  } from './manager/NetworkManager.js';
26
+ export * from './state/GCPolicy.js';
32
27
  export {
33
28
  default as createReducer,
34
29
  initialState,
35
30
  } from './state/reducer/createReducer.js';
36
31
  export { default as applyManager } from './manager/applyManager.js';
32
+ export { default as initManager } from './manager/initManager.js';
37
33
 
38
34
  export { default as Controller } from './controller/Controller.js';
39
35
  export type {
@@ -2,7 +2,6 @@ import { SUBSCRIBE, UNSUBSCRIBE } from '../actionTypes.js';
2
2
  import Controller from '../controller/Controller.js';
3
3
  import type {
4
4
  Manager,
5
- MiddlewareAPI,
6
5
  Middleware,
7
6
  UnsubscribeAction,
8
7
  SubscribeAction,
@@ -18,14 +18,13 @@ export default function applyManager(
18
18
  }
19
19
  return managers.map((manager, i) => {
20
20
  if (!manager.middleware) manager.middleware = manager.getMiddleware?.();
21
- return ({ dispatch, getState }) => {
21
+ return (api: ReduxMiddlewareAPI) => {
22
22
  if (i === 0) {
23
- (controller as any).dispatch = dispatch;
24
- (controller as any).getState = getState;
23
+ controller.bindMiddleware(api);
25
24
  }
26
25
  // controller is a superset of the middleware API
27
26
  return (manager as Manager & { middleware: ReduxMiddleware }).middleware(
28
- controller as Controller<any>,
27
+ controller as any,
29
28
  );
30
29
  };
31
30
  });
@@ -0,0 +1,21 @@
1
+ import type Controller from '../controller/Controller.js';
2
+ import { Manager, State } from '../types.js';
3
+
4
+ export default function initManager(
5
+ managers: Manager[],
6
+ controller: Controller,
7
+ initialState: State<unknown>,
8
+ ) {
9
+ return () => {
10
+ managers.forEach(manager => {
11
+ manager.init?.(initialState);
12
+ });
13
+ controller.gcPolicy.init(controller);
14
+ return () => {
15
+ managers.forEach(manager => {
16
+ manager.cleanup();
17
+ });
18
+ controller.gcPolicy.cleanup();
19
+ };
20
+ };
21
+ }