@khanacademy/wonder-blocks-data 11.0.16 → 13.0.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 (70) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/components/data.d.ts +2 -2
  3. package/dist/es/index.js +24 -18
  4. package/dist/hooks/use-gql.d.ts +5 -1
  5. package/dist/hooks/use-hydratable-effect.d.ts +6 -6
  6. package/dist/index.js +24 -18
  7. package/dist/util/status.d.ts +4 -3
  8. package/dist/util/types.d.ts +8 -6
  9. package/package.json +3 -3
  10. package/src/components/__tests__/data.test.tsx +6 -13
  11. package/src/components/data.ts +2 -4
  12. package/src/hooks/__tests__/use-cached-effect.test.tsx +79 -40
  13. package/src/hooks/__tests__/use-gql-router-context.test.tsx +1 -2
  14. package/src/hooks/__tests__/use-hydratable-effect.test.ts +1 -2
  15. package/src/hooks/__tests__/use-request-interception.test.tsx +2 -5
  16. package/src/hooks/__tests__/use-server-effect.test.ts +3 -6
  17. package/src/hooks/__tests__/use-shared-cache.test.ts +17 -13
  18. package/src/hooks/use-cached-effect.ts +24 -20
  19. package/src/hooks/use-gql.ts +12 -9
  20. package/src/hooks/use-hydratable-effect.ts +6 -7
  21. package/src/hooks/use-request-interception.ts +13 -11
  22. package/src/hooks/use-shared-cache.ts +4 -2
  23. package/src/util/__tests__/request-api.test.ts +2 -1
  24. package/src/util/__tests__/request-tracking.test.tsx +5 -9
  25. package/src/util/__tests__/result-from-cache-response.test.ts +2 -2
  26. package/src/util/__tests__/serializable-in-memory-cache.test.ts +1 -2
  27. package/src/util/__tests__/ssr-cache.test.ts +2 -4
  28. package/src/util/__tests__/to-gql-operation.test.ts +2 -4
  29. package/src/util/graphql-document-node-parser.ts +6 -6
  30. package/src/util/merge-gql-context.ts +2 -1
  31. package/src/util/request-tracking.ts +6 -2
  32. package/src/util/ssr-cache.ts +11 -8
  33. package/src/util/status.ts +6 -0
  34. package/src/util/types.ts +9 -7
  35. package/tsconfig-build.tsbuildinfo +1 -1
  36. package/dist/components/data.js.flow +0 -63
  37. package/dist/components/gql-router.js.flow +0 -33
  38. package/dist/components/intercept-context.js.flow +0 -18
  39. package/dist/components/intercept-requests.js.flow +0 -51
  40. package/dist/components/track-data.js.flow +0 -16
  41. package/dist/hooks/use-cached-effect.js.flow +0 -83
  42. package/dist/hooks/use-gql-router-context.js.flow +0 -14
  43. package/dist/hooks/use-gql.js.flow +0 -28
  44. package/dist/hooks/use-hydratable-effect.js.flow +0 -122
  45. package/dist/hooks/use-request-interception.js.flow +0 -24
  46. package/dist/hooks/use-server-effect.js.flow +0 -49
  47. package/dist/hooks/use-shared-cache.js.flow +0 -42
  48. package/dist/index.js.flow +0 -47
  49. package/dist/util/data-error.js.flow +0 -62
  50. package/dist/util/get-gql-data-from-response.js.flow +0 -12
  51. package/dist/util/get-gql-request-id.js.flow +0 -16
  52. package/dist/util/gql-error.js.flow +0 -41
  53. package/dist/util/gql-router-context.js.flow +0 -10
  54. package/dist/util/gql-types.js.flow +0 -50
  55. package/dist/util/graphql-document-node-parser.js.flow +0 -29
  56. package/dist/util/graphql-types.js.flow +0 -30
  57. package/dist/util/hydration-cache-api.js.flow +0 -29
  58. package/dist/util/merge-gql-context.js.flow +0 -18
  59. package/dist/util/purge-caches.js.flow +0 -14
  60. package/dist/util/request-api.js.flow +0 -33
  61. package/dist/util/request-fulfillment.js.flow +0 -48
  62. package/dist/util/request-tracking.js.flow +0 -81
  63. package/dist/util/result-from-cache-response.js.flow +0 -14
  64. package/dist/util/scoped-in-memory-cache.js.flow +0 -56
  65. package/dist/util/serializable-in-memory-cache.js.flow +0 -25
  66. package/dist/util/ssr-cache.js.flow +0 -86
  67. package/dist/util/status.js.flow +0 -17
  68. package/dist/util/to-gql-operation.js.flow +0 -41
  69. package/dist/util/types.js.flow +0 -142
  70. package/src/util/graphql-types.js.flow +0 -30
@@ -22,8 +22,9 @@ jest.mock("../use-request-interception");
22
22
  jest.mock("../use-shared-cache");
23
23
 
24
24
  const allPolicies = Array.from(values(FetchPolicy));
25
- const allPoliciesBut = (policy: typeof FetchPolicy[keyof typeof FetchPolicy]) =>
26
- allPolicies.filter((p: any) => p !== policy);
25
+ const allPoliciesBut = (
26
+ ...policies: Array<typeof FetchPolicy[keyof typeof FetchPolicy]>
27
+ ) => allPolicies.filter((p: any) => !policies.includes(p));
27
28
 
28
29
  describe("#useCachedEffect", () => {
29
30
  beforeEach(() => {
@@ -116,7 +117,7 @@ describe("#useCachedEffect", () => {
116
117
  },
117
118
  );
118
119
 
119
- describe.each(allPolicies)(
120
+ describe.each(allPoliciesBut(FetchPolicy.CacheOnly))(
120
121
  "with FetchPolicy.%s without cached result",
121
122
  (fetchPolicy: any) => {
122
123
  it("should return a loading result", () => {
@@ -138,6 +139,27 @@ describe("#useCachedEffect", () => {
138
139
  },
139
140
  );
140
141
 
142
+ describe("with FetchPolicy.CacheOnly without cached result", () => {
143
+ it("should return a no-data result", () => {
144
+ // Arrange
145
+ const fakeHandler = jest.fn();
146
+
147
+ // Act
148
+ const {
149
+ result: {
150
+ current: [result],
151
+ },
152
+ } = serverRenderHook(() =>
153
+ useCachedEffect("ID", fakeHandler, {
154
+ fetchPolicy: FetchPolicy.CacheOnly,
155
+ }),
156
+ );
157
+
158
+ // Assert
159
+ expect(result).toStrictEqual(Status.noData());
160
+ });
161
+ });
162
+
141
163
  describe.each(allPoliciesBut(FetchPolicy.NetworkOnly))(
142
164
  "with FetchPolicy.%s with cached result",
143
165
  (fetchPolicy: any) => {
@@ -232,7 +254,7 @@ describe("#useCachedEffect", () => {
232
254
  "should provide function that causes refetch with FetchPolicy.%s",
233
255
  async (fetchPolicy: any) => {
234
256
  // Arrange
235
- const response = Promise.resolve("DATA1");
257
+ const response: any = Promise.resolve("DATA1");
236
258
  const fakeHandler = jest.fn().mockReturnValue(response);
237
259
 
238
260
  // Act
@@ -244,11 +266,9 @@ describe("#useCachedEffect", () => {
244
266
  useCachedEffect("ID", fakeHandler, {fetchPolicy}),
245
267
  );
246
268
  fakeHandler.mockClear();
247
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
248
- await act((): Promise<unknown> => response);
269
+ await act(() => response);
249
270
  act(refetch);
250
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
251
- await act((): Promise<unknown> => response);
271
+ await act(() => response);
252
272
 
253
273
  // Assert
254
274
  expect(fakeHandler).toHaveBeenCalledTimes(1);
@@ -426,7 +446,7 @@ describe("#useCachedEffect", () => {
426
446
 
427
447
  it("should ignore inflight request if requestId changes", async () => {
428
448
  // Arrange
429
- const response1 = Promise.resolve("DATA1");
449
+ const response1: any = Promise.resolve("DATA1");
430
450
  const response2 = Promise.resolve("DATA2");
431
451
  const fakeHandler = jest
432
452
  .fn()
@@ -449,7 +469,7 @@ describe("#useCachedEffect", () => {
449
469
 
450
470
  it("should return result of fulfilled request for current requestId", async () => {
451
471
  // Arrange
452
- const response1 = Promise.resolve("DATA1");
472
+ const response1: any = Promise.resolve("DATA1");
453
473
  const response2 = Promise.resolve("DATA2");
454
474
  const fakeHandler = jest
455
475
  .fn()
@@ -485,7 +505,7 @@ describe("#useCachedEffect", () => {
485
505
 
486
506
  it("should ignore result of inflight request if skip changes", async () => {
487
507
  // Arrange
488
- const response1 = Promise.resolve("DATA1");
508
+ const response1: any = Promise.resolve("DATA1");
489
509
  const fakeHandler = jest.fn().mockReturnValueOnce(response1);
490
510
 
491
511
  // Act
@@ -496,8 +516,7 @@ describe("#useCachedEffect", () => {
496
516
  },
497
517
  );
498
518
  rerender({skip: true});
499
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
500
- await act((): Promise<unknown> => response1);
519
+ await act(() => response1);
501
520
 
502
521
  // Assert
503
522
  expect(result.all).not.toContainEqual(Status.success("DATA1"));
@@ -505,7 +524,7 @@ describe("#useCachedEffect", () => {
505
524
 
506
525
  it("should not ignore result of inflight request if handler changes", async () => {
507
526
  // Arrange
508
- const response1 = Promise.resolve("DATA1");
527
+ const response1: any = Promise.resolve("DATA1");
509
528
  const response2 = Promise.resolve("DATA2");
510
529
  const fakeHandler1 = jest.fn().mockReturnValueOnce(response1);
511
530
  const fakeHandler2 = jest.fn().mockReturnValueOnce(response2);
@@ -526,7 +545,7 @@ describe("#useCachedEffect", () => {
526
545
 
527
546
  it("should not ignore inflight request if options (other than skip) change", async () => {
528
547
  // Arrange
529
- const response1 = Promise.resolve("DATA1");
548
+ const response1: any = Promise.resolve("DATA1");
530
549
  const fakeHandler = jest.fn().mockReturnValueOnce(response1);
531
550
 
532
551
  // Act
@@ -537,13 +556,11 @@ describe("#useCachedEffect", () => {
537
556
  },
538
557
  );
539
558
  rerender({
540
- // @ts-expect-error [FEI-5019] - TS2322 - Type '{ scope: string; }' is not assignable to type 'undefined'.
541
559
  options: {
542
560
  scope: "BLAH!",
543
561
  },
544
- });
545
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
546
- await act((): Promise<unknown> => response1);
562
+ } as any);
563
+ await act(() => response1);
547
564
 
548
565
  // Assert
549
566
  expect(result.current[0]).toStrictEqual(Status.success("DATA1"));
@@ -551,7 +568,7 @@ describe("#useCachedEffect", () => {
551
568
 
552
569
  it("should return previous result when requestId changes and retainResultOnChange is true", async () => {
553
570
  // Arrange
554
- const response1 = Promise.resolve("DATA1");
571
+ const response1: any = Promise.resolve("DATA1");
555
572
  const response2 = Promise.resolve("DATA2");
556
573
  const fakeHandler = jest
557
574
  .fn()
@@ -572,8 +589,7 @@ describe("#useCachedEffect", () => {
572
589
  initialProps: {requestId: "ID"},
573
590
  },
574
591
  );
575
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
576
- await act((): Promise<unknown> => response1);
592
+ await act(() => response1);
577
593
  rerender({requestId: "ID2"});
578
594
  const [result] = hookResult.current;
579
595
  await waitForNextUpdate();
@@ -584,7 +600,7 @@ describe("#useCachedEffect", () => {
584
600
 
585
601
  it("should return loading status when requestId changes and retainResultOnChange is false", async () => {
586
602
  // Arrange
587
- const response1 = Promise.resolve("DATA1");
603
+ const response1: any = Promise.resolve("DATA1");
588
604
  const response2 = new Promise(() => {
589
605
  /*pending*/
590
606
  });
@@ -603,19 +619,47 @@ describe("#useCachedEffect", () => {
603
619
  initialProps: {requestId: "ID"},
604
620
  },
605
621
  );
606
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
607
- await act((): Promise<unknown> => response1);
622
+ await act(() => response1);
608
623
  rerender({requestId: "ID2"});
609
624
 
610
625
  // Assert
611
626
  expect(result.current[0]).toStrictEqual(Status.loading());
612
627
  });
613
628
 
629
+ it("should return no-data status when requestId changes, retainResultOnChange is false, and skip is true", async () => {
630
+ // Arrange
631
+ const response1: any = Promise.resolve("DATA1");
632
+ const response2 = new Promise(() => {
633
+ /*pending*/
634
+ });
635
+ const fakeHandler = jest
636
+ .fn()
637
+ .mockReturnValueOnce(response1)
638
+ .mockReturnValueOnce(response2);
639
+
640
+ // Act
641
+ const {rerender, result} = clientRenderHook(
642
+ ({requestId}: any) =>
643
+ useCachedEffect(requestId, fakeHandler, {
644
+ retainResultOnChange: false,
645
+ skip: true,
646
+ }),
647
+ {
648
+ initialProps: {requestId: "ID"},
649
+ },
650
+ );
651
+ await act(() => response1);
652
+ rerender({requestId: "ID2"});
653
+
654
+ // Assert
655
+ expect(result.current[0]).toStrictEqual(Status.noData());
656
+ });
657
+
614
658
  it.each(allPoliciesBut(FetchPolicy.CacheOnly))(
615
659
  "should trigger render when request is fulfilled and onResultChanged is undefined for FetchPolicy.%s",
616
660
  async (fetchPolicy: any) => {
617
661
  // Arrange
618
- const response = Promise.resolve("DATA");
662
+ const response: any = Promise.resolve("DATA");
619
663
  const fakeHandler = jest.fn().mockReturnValue(response);
620
664
  let renderCount = 0;
621
665
  const Component = React.memo(() => {
@@ -626,8 +670,7 @@ describe("#useCachedEffect", () => {
626
670
 
627
671
  // Act
628
672
  render(<Component />);
629
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
630
- await reactAct((): Promise<unknown> => response);
673
+ await reactAct(() => response);
631
674
 
632
675
  // Assert
633
676
  expect(renderCount).toBe(2);
@@ -638,7 +681,7 @@ describe("#useCachedEffect", () => {
638
681
  "should trigger render once per inflight request being fulfilled and onResultChanged is undefined for FetchPolicy.%s",
639
682
  async (fetchPolicy: any) => {
640
683
  // Arrange
641
- const response = Promise.resolve("DATA");
684
+ const response: any = Promise.resolve("DATA");
642
685
  const fakeHandler = jest.fn().mockReturnValue(response);
643
686
  let renderCount = 0;
644
687
  const Component = React.memo(() => {
@@ -657,8 +700,7 @@ describe("#useCachedEffect", () => {
657
700
 
658
701
  // Act
659
702
  render(<Component />);
660
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
661
- await reactAct((): Promise<unknown> => response);
703
+ await reactAct(() => response);
662
704
 
663
705
  // Assert
664
706
  expect(renderCount).toBe(2);
@@ -669,7 +711,7 @@ describe("#useCachedEffect", () => {
669
711
  "should not trigger render when request is fulfilled and onResultChanged is defined for FetchPolicy.%s",
670
712
  async (fetchPolicy: any) => {
671
713
  // Arrange
672
- const response = Promise.resolve("DATA");
714
+ const response: any = Promise.resolve("DATA");
673
715
  const fakeHandler = jest.fn().mockReturnValue(response);
674
716
  let renderCount = 0;
675
717
  const Component = React.memo(() => {
@@ -683,8 +725,7 @@ describe("#useCachedEffect", () => {
683
725
 
684
726
  // Act
685
727
  render(<Component />);
686
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
687
- await reactAct((): Promise<unknown> => response);
728
+ await reactAct(() => response);
688
729
 
689
730
  // Assert
690
731
  expect(renderCount).toBe(1);
@@ -695,7 +736,7 @@ describe("#useCachedEffect", () => {
695
736
  "should call onResultChanged when request is fulfilled and onResultChanged is defined for FetchPolicy.%s",
696
737
  async (fetchPolicy: any) => {
697
738
  // Arrange
698
- const response = Promise.resolve("DATA");
739
+ const response: any = Promise.resolve("DATA");
699
740
  const fakeHandler = jest.fn().mockReturnValue(response);
700
741
  const onResultChanged = jest.fn();
701
742
 
@@ -706,8 +747,7 @@ describe("#useCachedEffect", () => {
706
747
  fetchPolicy,
707
748
  }),
708
749
  );
709
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
710
- await act((): Promise<unknown> => response);
750
+ await act(() => response);
711
751
 
712
752
  // Assert
713
753
  expect(onResultChanged).toHaveBeenCalledWith(
@@ -720,7 +760,7 @@ describe("#useCachedEffect", () => {
720
760
  "should call onResultChanged once per inflight request being fulfilled and onResultChanged is defined for FetchPolicy.%s",
721
761
  async (fetchPolicy: any) => {
722
762
  // Arrange
723
- const response = Promise.resolve("DATA");
763
+ const response: any = Promise.resolve("DATA");
724
764
  const fakeHandler = jest.fn().mockReturnValue(response);
725
765
  const onResultChanged = jest.fn();
726
766
 
@@ -739,8 +779,7 @@ describe("#useCachedEffect", () => {
739
779
  act(refetch);
740
780
  act(refetch);
741
781
  act(refetch);
742
- // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
743
- await act((): Promise<unknown> => response);
782
+ await act(() => response);
744
783
 
745
784
  // Assert
746
785
  expect(onResultChanged).toHaveBeenCalledTimes(1);
@@ -123,8 +123,7 @@ describe("#useGqlRouterContext", () => {
123
123
  },
124
124
  );
125
125
  const result1 = wrapper.result.current;
126
- // @ts-expect-error [FEI-5019] - TS2741 - Property 'fiz' is missing in type '{}' but required in type '{ fiz: string; }'.
127
- wrapper.rerender({overrides: {}});
126
+ wrapper.rerender({overrides: {} as any});
128
127
  const result2 = wrapper.result.current;
129
128
 
130
129
  // Assert
@@ -562,10 +562,9 @@ describe("#useHydratableEffect", () => {
562
562
  },
563
563
  );
564
564
  rerender({
565
- // @ts-expect-error [FEI-5019] - TS2322 - Type '{ scope: string; }' is not assignable to type 'undefined'.
566
565
  options: {
567
566
  scope: "BLAH!",
568
- },
567
+ } as any,
569
568
  });
570
569
 
571
570
  await act((): Promise<any> => response1);
@@ -68,7 +68,7 @@ describe("#useRequestInterception", () => {
68
68
  const handler = jest.fn();
69
69
  const interceptor1 = jest.fn();
70
70
  const interceptor2 = jest.fn();
71
- const Wrapper = ({children, interceptor}: any) => (
71
+ const Wrapper = ({children, interceptor}: any): React.ReactElement => (
72
72
  <InterceptRequests interceptor={interceptor}>
73
73
  {children}
74
74
  </InterceptRequests>
@@ -80,8 +80,7 @@ describe("#useRequestInterception", () => {
80
80
  {wrapper: Wrapper, initialProps: {interceptor: interceptor1}},
81
81
  );
82
82
  const result1 = wrapper.result.current;
83
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '{ wrapper: ({ children, interceptor, }: any) => JSX.Element; interceptor: jest.Mock<any, any, any>; }' is not assignable to parameter of type '{ interceptor: jest.Mock<any, any, any>; }'.
84
- wrapper.rerender({wrapper: Wrapper, interceptor: interceptor2});
83
+ wrapper.rerender({interceptor: interceptor2});
85
84
  const result2 = wrapper.result.current;
86
85
 
87
86
  // Assert
@@ -126,7 +125,6 @@ describe("#useRequestInterception", () => {
126
125
  interceptedHandler();
127
126
 
128
127
  // Assert
129
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'toHaveBeenCalledBefore' does not exist on type 'JestMatchers<Mock<null, [], any>>'.
130
128
  expect(interceptorNearest).toHaveBeenCalledBefore(
131
129
  interceptorFurthest,
132
130
  );
@@ -154,7 +152,6 @@ describe("#useRequestInterception", () => {
154
152
  interceptedHandler();
155
153
 
156
154
  // Assert
157
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'toHaveBeenCalledBefore' does not exist on type 'JestMatchers<Mock<null, [], any>>'.
158
155
  expect(interceptorFurthest).toHaveBeenCalledBefore(handler);
159
156
  });
160
157
 
@@ -139,8 +139,7 @@ describe("#useServerEffect", () => {
139
139
  const interceptedHandler = jest.fn();
140
140
  jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
141
141
  data: "DATA",
142
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
143
- error: null,
142
+ error: undefined,
144
143
  });
145
144
  jest.spyOn(
146
145
  UseRequestInterception,
@@ -165,8 +164,7 @@ describe("#useServerEffect", () => {
165
164
  const fakeHandler = jest.fn();
166
165
  jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
167
166
  data: "DATA",
168
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
169
- error: null,
167
+ error: undefined,
170
168
  });
171
169
 
172
170
  // Act
@@ -221,8 +219,7 @@ describe("#useServerEffect", () => {
221
219
  const fakeHandler = jest.fn();
222
220
  jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
223
221
  data: "DATA",
224
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
225
- error: null,
222
+ error: undefined,
226
223
  });
227
224
 
228
225
  // Act
@@ -1,3 +1,5 @@
1
+ // eslint-disable-next-line import/no-unassigned-import
2
+ import "jest-extended";
1
3
  import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
2
4
 
3
5
  import {useSharedCache, SharedCache} from "../use-shared-cache";
@@ -48,7 +50,6 @@ describe("#useSharedCache", () => {
48
50
  } = clientRenderHook(() => useSharedCache("id", "scope"));
49
51
 
50
52
  // Assert
51
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'toBeArrayOfSize' does not exist on type 'JestMatchers<[ValidCacheData | null | undefined, CacheValueFn<ValidCacheData>]>'.
52
53
  expect(result).toBeArrayOfSize(2);
53
54
  });
54
55
 
@@ -119,12 +120,13 @@ describe("#useSharedCache", () => {
119
120
  id: "id",
120
121
  scope: "scope",
121
122
  });
122
- const result1 = wrapper.result.all[wrapper.result.all.length - 2];
123
- const result2 = wrapper.result.current;
123
+ const value1 = wrapper.result.all[wrapper.result.all.length - 2];
124
+ const value2 = wrapper.result.current;
125
+ const result1 = Array.isArray(value1) ? value1[1] : "BAD1";
126
+ const result2 = Array.isArray(value2) ? value2[1] : "BAD2";
124
127
 
125
128
  // Assert
126
- // @ts-expect-error [FEI-5019] - TS7053 - Element implicitly has an 'any' type because expression of type '1' can't be used to index type 'Error | [ValidCacheData | null | undefined, CacheValueFn<ValidCacheData>]'.
127
- expect(result1[1]).toBe(result2[1]);
129
+ expect(result1).toBe(result2);
128
130
  });
129
131
 
130
132
  it("should be a new function if the id changes", () => {
@@ -138,12 +140,13 @@ describe("#useSharedCache", () => {
138
140
 
139
141
  // Act
140
142
  wrapper.rerender({id: "new-id"});
141
- const result1 = wrapper.result.all[wrapper.result.all.length - 2];
142
- const result2 = wrapper.result.current;
143
+ const value1 = wrapper.result.all[wrapper.result.all.length - 2];
144
+ const value2 = wrapper.result.current;
145
+ const result1 = Array.isArray(value1) ? value1[1] : "BAD1";
146
+ const result2 = Array.isArray(value2) ? value2[1] : "BAD2";
143
147
 
144
148
  // Assert
145
- // @ts-expect-error [FEI-5019] - TS7053 - Element implicitly has an 'any' type because expression of type '1' can't be used to index type 'Error | [ValidCacheData | null | undefined, CacheValueFn<ValidCacheData>]'.
146
- expect(result1[1]).not.toBe(result2[1]);
149
+ expect(result1).not.toBe(result2);
147
150
  });
148
151
 
149
152
  it("should be a new function if the scope changes", () => {
@@ -157,12 +160,13 @@ describe("#useSharedCache", () => {
157
160
 
158
161
  // Act
159
162
  wrapper.rerender({scope: "new-scope"});
160
- const result1 = wrapper.result.all[wrapper.result.all.length - 2];
161
- const result2 = wrapper.result.current;
163
+ const value1 = wrapper.result.all[wrapper.result.all.length - 2];
164
+ const value2 = wrapper.result.current;
165
+ const result1 = Array.isArray(value1) ? value1[1] : "BAD1";
166
+ const result2 = Array.isArray(value2) ? value2[1] : "BAD2";
162
167
 
163
168
  // Assert
164
- // @ts-expect-error [FEI-5019] - TS7053 - Element implicitly has an 'any' type because expression of type '1' can't be used to index type 'Error | [ValidCacheData | null | undefined, CacheValueFn<ValidCacheData>]'.
165
- expect(result1[1]).not.toBe(result2[1]);
169
+ expect(result1).not.toBe(result2);
166
170
  });
167
171
 
168
172
  it("should set the value in the cache", () => {
@@ -63,6 +63,12 @@ type CachedEffectOptions<TData extends ValidCacheData> = {
63
63
  scope?: string;
64
64
  };
65
65
 
66
+ type InflightRequest<TData extends ValidCacheData> = {
67
+ requestId: string;
68
+ request: Promise<Result<TData>>;
69
+ cancel(): void;
70
+ };
71
+
66
72
  const DefaultScope = "useCachedEffect";
67
73
 
68
74
  /**
@@ -114,19 +120,16 @@ export const useCachedEffect = <TData extends ValidCacheData>(
114
120
  const forceUpdate = useForceUpdate();
115
121
  // For the NetworkOnly fetch policy, we ignore the cached value.
116
122
  // So we need somewhere else to store the network value.
117
- const networkResultRef = React.useRef();
123
+ const networkResultRef = React.useRef<Result<TData> | null>();
118
124
 
119
125
  // Set up the function that will do the fetching.
120
- const currentRequestRef = React.useRef();
126
+ const currentRequestRef = React.useRef<InflightRequest<TData> | null>();
121
127
  const fetchRequest = React.useMemo(() => {
122
128
  // We aren't using useCallback here because we need to make sure that
123
129
  // if we are rememo-izing, we cancel any inflight request for the old
124
130
  // callback.
125
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'cancel' does not exist on type 'never'.
126
131
  currentRequestRef.current?.cancel();
127
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
128
132
  currentRequestRef.current = null;
129
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
130
133
  networkResultRef.current = null;
131
134
 
132
135
  const fetchFn = () => {
@@ -153,7 +156,6 @@ export const useCachedEffect = <TData extends ValidCacheData>(
153
156
  },
154
157
  );
155
158
 
156
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'request' does not exist on type 'never'.
157
159
  if (request === currentRequestRef.current?.request) {
158
160
  // The request inflight is the same, so do nothing.
159
161
  // NOTE: Perhaps if invoked via a refetch, we will want to
@@ -162,11 +164,9 @@ export const useCachedEffect = <TData extends ValidCacheData>(
162
164
  }
163
165
 
164
166
  // Clear the last network result.
165
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
166
167
  networkResultRef.current = null;
167
168
 
168
169
  // Cancel the previous request.
169
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'cancel' does not exist on type 'never'.
170
170
  currentRequestRef.current?.cancel();
171
171
 
172
172
  // TODO(somewhatabstract, FEI-4276):
@@ -178,7 +178,6 @@ export const useCachedEffect = <TData extends ValidCacheData>(
178
178
  // Catching shouldn't serve a purpose.
179
179
  // eslint-disable-next-line promise/catch-or-return
180
180
  request.then((result) => {
181
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
182
181
  currentRequestRef.current = null;
183
182
  if (cancel) {
184
183
  // We don't modify our result if the request was cancelled
@@ -189,7 +188,6 @@ export const useCachedEffect = <TData extends ValidCacheData>(
189
188
 
190
189
  // Now we need to update the cache and notify or force a rerender.
191
190
  setMostRecentResult(result);
192
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'Result<TData>' is not assignable to type 'undefined'.
193
191
  networkResultRef.current = result;
194
192
 
195
193
  if (onResultChanged != null) {
@@ -204,7 +202,6 @@ export const useCachedEffect = <TData extends ValidCacheData>(
204
202
  return; // Shut up eslint always-return rule.
205
203
  });
206
204
 
207
- // @ts-expect-error [FEI-5019] - TS2322 - Type '{ requestId: string; request: Promise<Result<TData>>; cancel(): void; }' is not assignable to type 'undefined'.
208
205
  currentRequestRef.current = {
209
206
  requestId,
210
207
  request,
@@ -265,29 +262,36 @@ export const useCachedEffect = <TData extends ValidCacheData>(
265
262
  }
266
263
  fetchRequest();
267
264
  return () => {
268
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'cancel' does not exist on type 'never'.
269
265
  currentRequestRef.current?.cancel();
270
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
271
266
  currentRequestRef.current = null;
272
267
  };
273
268
  }, [shouldFetch, fetchRequest]);
274
269
 
275
270
  // We track the last result we returned in order to support the
276
- // "retainResultOnChange" option.
277
- const lastResultAgnosticOfIdRef = React.useRef(Status.loading());
271
+ // "retainResultOnChange" option. To begin, the last result is no-data.
272
+ const lastResultAgnosticOfIdRef = React.useRef<Result<TData>>(
273
+ Status.noData<TData>(),
274
+ );
275
+ // The default return value is:
276
+ // - The last result we returned if we're retaining results on change.
277
+ // - The no-data state if shouldFetch is false, and therefore there is no
278
+ // in-flight request.
279
+ // - Otherwise, the loading state (we can assume there's an inflight
280
+ // request if skip is not true).
278
281
  const loadingResult = retainResultOnChange
279
282
  ? lastResultAgnosticOfIdRef.current
280
- : Status.loading();
283
+ : shouldFetch
284
+ ? Status.loading<TData>()
285
+ : Status.noData<TData>();
281
286
 
282
- // Loading is a transient state, so we only use it here; it's not something
283
- // we cache.
284
- const result =
287
+ // Loading and no-data are transient states, so we only use them here;
288
+ // they're not something we cache.
289
+ const result: Result<TData> =
285
290
  (fetchPolicy === FetchPolicy.NetworkOnly
286
291
  ? networkResultRef.current
287
292
  : mostRecentResult) ?? loadingResult;
288
293
  lastResultAgnosticOfIdRef.current = result;
289
294
 
290
295
  // We return the result and a function for triggering a refetch.
291
- // @ts-expect-error [FEI-5019] - TS2322 - Type '{ status: "loading"; } | { status: "error"; error: Error; } | { status: "aborted"; } | { status: "success"; data: ValidCacheData; }' is not assignable to type 'Result<TData>'.
292
296
  return [result, fetchRequest];
293
297
  };
@@ -10,6 +10,13 @@ import type {
10
10
  GqlFetchOptions,
11
11
  } from "../util/gql-types";
12
12
 
13
+ interface GqlFetchFn<TContext extends GqlContext> {
14
+ <TData, TVariables extends Record<any, any>>(
15
+ operation: GqlOperation<TData, TVariables>,
16
+ options?: GqlFetchOptions<TVariables, TContext>,
17
+ ): Promise<TData>;
18
+ }
19
+
13
20
  /**
14
21
  * Hook to obtain a gqlFetch function for performing GraphQL requests.
15
22
  *
@@ -22,10 +29,7 @@ import type {
22
29
  */
23
30
  export const useGql = <TContext extends GqlContext>(
24
31
  context: Partial<TContext> = {} as Partial<TContext>,
25
- ): (<TData, TVariables extends Record<any, any>>(
26
- operation: GqlOperation<TData, TVariables>,
27
- options?: GqlFetchOptions<TVariables, TContext>,
28
- ) => Promise<TData>) => {
32
+ ): GqlFetchFn<TContext> => {
29
33
  // This hook only works if the `GqlRouter` has been used to setup context.
30
34
  const gqlRouterContext = useGqlRouterContext(context);
31
35
 
@@ -34,22 +38,21 @@ export const useGql = <TContext extends GqlContext>(
34
38
  // we give the same function instance back to our callers instead of
35
39
  // making a new one. That then means they can safely use the return value
36
40
  // in hooks deps without fear of it triggering extra renders.
37
- const gqlFetch = useCallback(
41
+ const gqlFetch: GqlFetchFn<TContext> = useCallback(
38
42
  <TData, TVariables extends Record<any, any>>(
39
43
  operation: GqlOperation<TData, TVariables>,
40
44
  options: GqlFetchOptions<TVariables, TContext> = Object.freeze({}),
41
- ) => {
45
+ ): Promise<TData> => {
42
46
  const {fetch, defaultContext} = gqlRouterContext;
43
47
  const {variables, context = {}} = options;
44
48
  const finalContext = mergeGqlContext(defaultContext, context);
45
49
 
46
50
  // Invoke the fetch and extract the data.
47
- return fetch(operation, variables, finalContext).then(
48
- getGqlDataFromResponse,
51
+ return fetch(operation, variables, finalContext).then((response) =>
52
+ getGqlDataFromResponse<TData>(response),
49
53
  );
50
54
  },
51
55
  [gqlRouterContext],
52
56
  );
53
- // @ts-expect-error [FEI-5019] - TS2322 - Type '<TData, TVariables extends Record<any, any>>(operation: GqlOperation<TData, TVariables>, options?: GqlFetchOptions<TVariables, TContext>) => Promise<unknown>' is not assignable to type '<TData, TVariables extends Record<any, any>>(operation: GqlOperation<TData, TVariables>, options?: GqlFetchOptions<TVariables, TContext> | undefined) => Promise<...>'.
54
57
  return gqlFetch;
55
58
  };