@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.
- package/CHANGELOG.md +24 -0
- package/dist/components/data.d.ts +2 -2
- package/dist/es/index.js +24 -18
- package/dist/hooks/use-gql.d.ts +5 -1
- package/dist/hooks/use-hydratable-effect.d.ts +6 -6
- package/dist/index.js +24 -18
- package/dist/util/status.d.ts +4 -3
- package/dist/util/types.d.ts +8 -6
- package/package.json +3 -3
- package/src/components/__tests__/data.test.tsx +6 -13
- package/src/components/data.ts +2 -4
- package/src/hooks/__tests__/use-cached-effect.test.tsx +79 -40
- package/src/hooks/__tests__/use-gql-router-context.test.tsx +1 -2
- package/src/hooks/__tests__/use-hydratable-effect.test.ts +1 -2
- package/src/hooks/__tests__/use-request-interception.test.tsx +2 -5
- package/src/hooks/__tests__/use-server-effect.test.ts +3 -6
- package/src/hooks/__tests__/use-shared-cache.test.ts +17 -13
- package/src/hooks/use-cached-effect.ts +24 -20
- package/src/hooks/use-gql.ts +12 -9
- package/src/hooks/use-hydratable-effect.ts +6 -7
- package/src/hooks/use-request-interception.ts +13 -11
- package/src/hooks/use-shared-cache.ts +4 -2
- package/src/util/__tests__/request-api.test.ts +2 -1
- package/src/util/__tests__/request-tracking.test.tsx +5 -9
- package/src/util/__tests__/result-from-cache-response.test.ts +2 -2
- package/src/util/__tests__/serializable-in-memory-cache.test.ts +1 -2
- package/src/util/__tests__/ssr-cache.test.ts +2 -4
- package/src/util/__tests__/to-gql-operation.test.ts +2 -4
- package/src/util/graphql-document-node-parser.ts +6 -6
- package/src/util/merge-gql-context.ts +2 -1
- package/src/util/request-tracking.ts +6 -2
- package/src/util/ssr-cache.ts +11 -8
- package/src/util/status.ts +6 -0
- package/src/util/types.ts +9 -7
- package/tsconfig-build.tsbuildinfo +1 -1
- package/dist/components/data.js.flow +0 -63
- package/dist/components/gql-router.js.flow +0 -33
- package/dist/components/intercept-context.js.flow +0 -18
- package/dist/components/intercept-requests.js.flow +0 -51
- package/dist/components/track-data.js.flow +0 -16
- package/dist/hooks/use-cached-effect.js.flow +0 -83
- package/dist/hooks/use-gql-router-context.js.flow +0 -14
- package/dist/hooks/use-gql.js.flow +0 -28
- package/dist/hooks/use-hydratable-effect.js.flow +0 -122
- package/dist/hooks/use-request-interception.js.flow +0 -24
- package/dist/hooks/use-server-effect.js.flow +0 -49
- package/dist/hooks/use-shared-cache.js.flow +0 -42
- package/dist/index.js.flow +0 -47
- package/dist/util/data-error.js.flow +0 -62
- package/dist/util/get-gql-data-from-response.js.flow +0 -12
- package/dist/util/get-gql-request-id.js.flow +0 -16
- package/dist/util/gql-error.js.flow +0 -41
- package/dist/util/gql-router-context.js.flow +0 -10
- package/dist/util/gql-types.js.flow +0 -50
- package/dist/util/graphql-document-node-parser.js.flow +0 -29
- package/dist/util/graphql-types.js.flow +0 -30
- package/dist/util/hydration-cache-api.js.flow +0 -29
- package/dist/util/merge-gql-context.js.flow +0 -18
- package/dist/util/purge-caches.js.flow +0 -14
- package/dist/util/request-api.js.flow +0 -33
- package/dist/util/request-fulfillment.js.flow +0 -48
- package/dist/util/request-tracking.js.flow +0 -81
- package/dist/util/result-from-cache-response.js.flow +0 -14
- package/dist/util/scoped-in-memory-cache.js.flow +0 -56
- package/dist/util/serializable-in-memory-cache.js.flow +0 -25
- package/dist/util/ssr-cache.js.flow +0 -86
- package/dist/util/status.js.flow +0 -17
- package/dist/util/to-gql-operation.js.flow +0 -41
- package/dist/util/types.js.flow +0 -142
- 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 = (
|
|
26
|
-
|
|
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(
|
|
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
|
-
|
|
248
|
-
await act((): Promise<unknown> => response);
|
|
269
|
+
await act(() => response);
|
|
249
270
|
act(refetch);
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
123
|
-
const
|
|
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
|
-
|
|
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
|
|
142
|
-
const
|
|
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
|
-
|
|
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
|
|
161
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
:
|
|
283
|
+
: shouldFetch
|
|
284
|
+
? Status.loading<TData>()
|
|
285
|
+
: Status.noData<TData>();
|
|
281
286
|
|
|
282
|
-
// Loading
|
|
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
|
};
|
package/src/hooks/use-gql.ts
CHANGED
|
@@ -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
|
-
):
|
|
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
|
};
|