@khanacademy/wonder-blocks-data 5.0.0 → 6.0.1
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 +31 -0
- package/dist/es/index.js +778 -372
- package/dist/index.js +1203 -551
- package/legacy-docs.md +3 -0
- package/package.json +2 -2
- package/src/__docs__/_overview_.stories.mdx +18 -0
- package/src/__docs__/_overview_graphql.stories.mdx +35 -0
- package/src/__docs__/_overview_ssr_.stories.mdx +185 -0
- package/src/__docs__/_overview_testing_.stories.mdx +123 -0
- package/src/__docs__/exports.clear-shared-cache.stories.mdx +20 -0
- package/src/__docs__/exports.data-error.stories.mdx +23 -0
- package/src/__docs__/exports.data-errors.stories.mdx +23 -0
- package/src/{components/data.md → __docs__/exports.data.stories.mdx} +15 -18
- package/src/__docs__/exports.fulfill-all-data-requests.stories.mdx +24 -0
- package/src/__docs__/exports.gql-error.stories.mdx +23 -0
- package/src/__docs__/exports.gql-errors.stories.mdx +20 -0
- package/src/__docs__/exports.gql-router.stories.mdx +29 -0
- package/src/__docs__/exports.has-unfulfilled-requests.stories.mdx +20 -0
- package/src/{components/intercept-requests.md → __docs__/exports.intercept-requests.stories.mdx} +16 -1
- package/src/__docs__/exports.intialize-cache.stories.mdx +29 -0
- package/src/__docs__/exports.remove-all-from-cache.stories.mdx +24 -0
- package/src/__docs__/exports.remove-from-cache.stories.mdx +25 -0
- package/src/__docs__/exports.request-fulfillment.stories.mdx +36 -0
- package/src/__docs__/exports.scoped-in-memory-cache.stories.mdx +92 -0
- package/src/__docs__/exports.serializable-in-memory-cache.stories.mdx +112 -0
- package/src/__docs__/exports.status.stories.mdx +31 -0
- package/src/{components/track-data.md → __docs__/exports.track-data.stories.mdx} +15 -0
- package/src/__docs__/exports.use-cached-effect.stories.mdx +41 -0
- package/src/__docs__/exports.use-gql.stories.mdx +73 -0
- package/src/__docs__/exports.use-hydratable-effect.stories.mdx +43 -0
- package/src/__docs__/exports.use-server-effect.stories.mdx +38 -0
- package/src/__docs__/exports.use-shared-cache.stories.mdx +30 -0
- package/src/__docs__/exports.when-client-side.stories.mdx +33 -0
- package/src/__docs__/types.cached-response.stories.mdx +29 -0
- package/src/__docs__/types.error-options.stories.mdx +21 -0
- package/src/__docs__/types.gql-context.stories.mdx +20 -0
- package/src/__docs__/types.gql-fetch-fn.stories.mdx +24 -0
- package/src/__docs__/types.gql-fetch-options.stories.mdx +24 -0
- package/src/__docs__/types.gql-operation-type.stories.mdx +24 -0
- package/src/__docs__/types.gql-operation.stories.mdx +67 -0
- package/src/__docs__/types.response-cache.stories.mdx +33 -0
- package/src/__docs__/types.result.stories.mdx +39 -0
- package/src/__docs__/types.scoped-cache.stories.mdx +27 -0
- package/src/__docs__/types.valid-cache-data.stories.mdx +23 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -80
- package/src/__tests__/generated-snapshot.test.js +0 -24
- package/src/components/__tests__/data.test.js +149 -128
- package/src/components/data.js +22 -112
- package/src/components/intercept-requests.js +1 -1
- package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +8 -8
- package/src/hooks/__tests__/use-cached-effect.test.js +507 -0
- package/src/hooks/__tests__/use-gql-router-context.test.js +133 -0
- package/src/hooks/__tests__/use-gql.test.js +1 -30
- package/src/hooks/__tests__/use-hydratable-effect.test.js +728 -0
- package/src/hooks/__tests__/use-server-effect.test.js +39 -11
- package/src/hooks/use-cached-effect.js +225 -0
- package/src/hooks/use-gql-router-context.js +50 -0
- package/src/hooks/use-gql.js +22 -52
- package/src/hooks/use-hydratable-effect.js +213 -0
- package/src/hooks/use-request-interception.js +20 -23
- package/src/hooks/use-server-effect.js +12 -5
- package/src/hooks/use-shared-cache.js +13 -11
- package/src/index.js +53 -2
- package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.js.snap +19 -0
- package/src/util/__tests__/merge-gql-context.test.js +74 -0
- package/src/util/__tests__/request-fulfillment.test.js +23 -42
- package/src/util/__tests__/request-tracking.test.js +26 -7
- package/src/util/__tests__/result-from-cache-response.test.js +19 -5
- package/src/util/__tests__/scoped-in-memory-cache.test.js +6 -85
- package/src/util/__tests__/serializable-in-memory-cache.test.js +398 -0
- package/src/util/__tests__/ssr-cache.test.js +52 -52
- package/src/util/abort-error.js +15 -0
- package/src/util/data-error.js +58 -0
- package/src/util/get-gql-data-from-response.js +3 -2
- package/src/util/gql-error.js +19 -11
- package/src/util/merge-gql-context.js +34 -0
- package/src/util/request-fulfillment.js +49 -46
- package/src/util/request-tracking.js +69 -15
- package/src/util/result-from-cache-response.js +12 -16
- package/src/util/scoped-in-memory-cache.js +24 -47
- package/src/util/serializable-in-memory-cache.js +49 -0
- package/src/util/ssr-cache.js +9 -8
- package/src/util/status.js +30 -0
- package/src/util/types.js +18 -1
- package/docs.md +0 -122
|
@@ -7,19 +7,29 @@ import {render, act} from "@testing-library/react";
|
|
|
7
7
|
import * as ReactDOMServer from "react-dom/server";
|
|
8
8
|
import {Server, View} from "@khanacademy/wonder-blocks-core";
|
|
9
9
|
|
|
10
|
+
import {clearSharedCache} from "../../hooks/use-shared-cache.js";
|
|
10
11
|
import TrackData from "../track-data.js";
|
|
11
12
|
import {RequestFulfillment} from "../../util/request-fulfillment.js";
|
|
12
13
|
import {SsrCache} from "../../util/ssr-cache.js";
|
|
13
14
|
import {RequestTracker} from "../../util/request-tracking.js";
|
|
14
15
|
import InterceptRequests from "../intercept-requests.js";
|
|
15
16
|
import Data from "../data.js";
|
|
17
|
+
import {
|
|
18
|
+
// TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
|
|
19
|
+
// have fixed:
|
|
20
|
+
// https://github.com/import-js/eslint-plugin-import/issues/2073
|
|
21
|
+
// eslint-disable-next-line import/named
|
|
22
|
+
WhenClientSide,
|
|
23
|
+
} from "../../hooks/use-hydratable-effect.js";
|
|
16
24
|
|
|
17
25
|
describe("Data", () => {
|
|
18
26
|
beforeEach(() => {
|
|
27
|
+
clearSharedCache();
|
|
28
|
+
|
|
19
29
|
const responseCache = new SsrCache();
|
|
20
30
|
jest.spyOn(SsrCache, "Default", "get").mockReturnValue(responseCache);
|
|
21
31
|
jest.spyOn(RequestFulfillment, "Default", "get").mockReturnValue(
|
|
22
|
-
new RequestFulfillment(
|
|
32
|
+
new RequestFulfillment(),
|
|
23
33
|
);
|
|
24
34
|
jest.spyOn(RequestTracker, "Default", "get").mockReturnValue(
|
|
25
35
|
new RequestTracker(responseCache),
|
|
@@ -35,7 +45,7 @@ describe("Data", () => {
|
|
|
35
45
|
jest.spyOn(Server, "isServerSide").mockReturnValue(false);
|
|
36
46
|
});
|
|
37
47
|
|
|
38
|
-
describe("without
|
|
48
|
+
describe("without hydrated data", () => {
|
|
39
49
|
beforeEach(() => {
|
|
40
50
|
/**
|
|
41
51
|
* Each of these test cases will not have cached data to be
|
|
@@ -107,7 +117,7 @@ describe("Data", () => {
|
|
|
107
117
|
expect(fakeHandler).toHaveBeenCalledTimes(1);
|
|
108
118
|
});
|
|
109
119
|
|
|
110
|
-
it("should render with an error if the request rejects to an error", async () => {
|
|
120
|
+
it("should render with an error if the handler request rejects to an error", async () => {
|
|
111
121
|
// Arrange
|
|
112
122
|
const fulfillSpy = jest.spyOn(
|
|
113
123
|
RequestFulfillment.Default,
|
|
@@ -122,20 +132,25 @@ describe("Data", () => {
|
|
|
122
132
|
{fakeChildrenFn}
|
|
123
133
|
</Data>,
|
|
124
134
|
);
|
|
135
|
+
|
|
125
136
|
/**
|
|
126
137
|
* We wait for the fulfillment to resolve.
|
|
127
138
|
*/
|
|
128
|
-
await act(() =>
|
|
139
|
+
await act(() =>
|
|
140
|
+
fulfillSpy.mock.results[0].value.catch(() => {
|
|
141
|
+
/* do nothing */
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
129
144
|
|
|
130
145
|
// Assert
|
|
131
|
-
expect(fakeChildrenFn).
|
|
132
|
-
expect(fakeChildrenFn).toHaveBeenLastCalledWith({
|
|
146
|
+
expect(fakeChildrenFn).toHaveBeenNthCalledWith(2, {
|
|
133
147
|
status: "error",
|
|
134
|
-
error:
|
|
148
|
+
error: expect.any(Error),
|
|
135
149
|
});
|
|
150
|
+
expect(fakeChildrenFn).toHaveBeenCalledTimes(2);
|
|
136
151
|
});
|
|
137
152
|
|
|
138
|
-
it("should render with data if the
|
|
153
|
+
it("should render with data if the handler resolves with data", async () => {
|
|
139
154
|
// Arrange
|
|
140
155
|
const fulfillSpy = jest.spyOn(
|
|
141
156
|
RequestFulfillment.Default,
|
|
@@ -164,52 +179,68 @@ describe("Data", () => {
|
|
|
164
179
|
});
|
|
165
180
|
});
|
|
166
181
|
|
|
167
|
-
it
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
async ({error}) => {
|
|
174
|
-
// Arrange
|
|
175
|
-
const fulfillSpy = jest
|
|
176
|
-
.spyOn(RequestFulfillment.Default, "fulfill")
|
|
177
|
-
.mockReturnValue(Promise.reject(error));
|
|
182
|
+
it("should render with aborted if the request rejects with an abort error", async () => {
|
|
183
|
+
// Arrange
|
|
184
|
+
const fulfillSpy = jest.spyOn(
|
|
185
|
+
RequestFulfillment.Default,
|
|
186
|
+
"fulfill",
|
|
187
|
+
);
|
|
178
188
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.mockImplementation(() => {
|
|
184
|
-
/* Just to shut it up */
|
|
185
|
-
});
|
|
189
|
+
const abortError = new Error("bang bang, abort!");
|
|
190
|
+
abortError.name = "AbortError";
|
|
191
|
+
const fakeHandler = () => Promise.reject(abortError);
|
|
192
|
+
const fakeChildrenFn = jest.fn(() => null);
|
|
186
193
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
fulfillSpy.mock.results[0].value.catch(() => {}),
|
|
198
|
-
);
|
|
194
|
+
// Act
|
|
195
|
+
render(
|
|
196
|
+
<Data handler={fakeHandler} requestId="ID">
|
|
197
|
+
{fakeChildrenFn}
|
|
198
|
+
</Data>,
|
|
199
|
+
);
|
|
200
|
+
/**
|
|
201
|
+
* We wait for the fulfillment to resolve.
|
|
202
|
+
*/
|
|
203
|
+
await act(() => fulfillSpy.mock.results[0].value);
|
|
199
204
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
// Assert
|
|
206
|
+
expect(fakeChildrenFn).toHaveBeenCalledTimes(2);
|
|
207
|
+
expect(fakeChildrenFn).toHaveBeenLastCalledWith({
|
|
208
|
+
status: "aborted",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should render with an error if the RequestFulfillment rejects with an error", async () => {
|
|
213
|
+
// Arrange
|
|
214
|
+
const fulfillSpy = jest
|
|
215
|
+
.spyOn(RequestFulfillment.Default, "fulfill")
|
|
216
|
+
.mockResolvedValue({
|
|
203
217
|
status: "error",
|
|
204
|
-
error: "CATASTROPHE!",
|
|
218
|
+
error: new Error("CATASTROPHE!"),
|
|
205
219
|
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
|
|
221
|
+
const fakeHandler = () => Promise.resolve("YAY!");
|
|
222
|
+
const fakeChildrenFn = jest.fn(() => null);
|
|
223
|
+
|
|
224
|
+
// Act
|
|
225
|
+
render(
|
|
226
|
+
<Data handler={fakeHandler} requestId="ID">
|
|
227
|
+
{fakeChildrenFn}
|
|
228
|
+
</Data>,
|
|
229
|
+
);
|
|
230
|
+
/**
|
|
231
|
+
* We wait for the fulfillment to reject.
|
|
232
|
+
*/
|
|
233
|
+
await act(() =>
|
|
234
|
+
fulfillSpy.mock.results[0].value.catch(() => {}),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Assert
|
|
238
|
+
expect(fakeChildrenFn).toHaveBeenCalledTimes(2);
|
|
239
|
+
expect(fakeChildrenFn).toHaveBeenLastCalledWith({
|
|
240
|
+
status: "error",
|
|
241
|
+
error: expect.any(Error),
|
|
242
|
+
});
|
|
243
|
+
});
|
|
213
244
|
|
|
214
245
|
it("should start loading if the id changes and request not cached", async () => {
|
|
215
246
|
// Arrange
|
|
@@ -239,12 +270,13 @@ describe("Data", () => {
|
|
|
239
270
|
);
|
|
240
271
|
|
|
241
272
|
// Assert
|
|
242
|
-
|
|
243
|
-
// Render 2: Caused by result state changing to null
|
|
244
|
-
expect(fakeChildrenFn).toHaveBeenCalledTimes(2);
|
|
273
|
+
expect(fakeChildrenFn).toHaveBeenCalledTimes(1);
|
|
245
274
|
expect(fakeChildrenFn).toHaveBeenLastCalledWith({
|
|
246
275
|
status: "loading",
|
|
247
276
|
});
|
|
277
|
+
|
|
278
|
+
// We have to do this or testing-library gets very upset.
|
|
279
|
+
await act(() => fulfillSpy.mock.results[0].value);
|
|
248
280
|
});
|
|
249
281
|
|
|
250
282
|
it("should ignore resolution of pending handler fulfillment when id changes", async () => {
|
|
@@ -319,7 +351,10 @@ describe("Data", () => {
|
|
|
319
351
|
|
|
320
352
|
it("should ignore catastrophic request fulfillment when id changes", async () => {
|
|
321
353
|
// Arrange
|
|
322
|
-
const catastrophe = Promise.
|
|
354
|
+
const catastrophe = Promise.resolve({
|
|
355
|
+
status: "error",
|
|
356
|
+
error: new Error("CATASTROPHE!"),
|
|
357
|
+
});
|
|
323
358
|
jest.spyOn(
|
|
324
359
|
RequestFulfillment.Default,
|
|
325
360
|
"fulfill",
|
|
@@ -347,12 +382,12 @@ describe("Data", () => {
|
|
|
347
382
|
// Assert
|
|
348
383
|
expect(fakeChildrenFn).not.toHaveBeenCalledWith({
|
|
349
384
|
status: "error",
|
|
350
|
-
error:
|
|
385
|
+
error: expect.any(Error),
|
|
351
386
|
});
|
|
352
387
|
});
|
|
353
388
|
|
|
354
389
|
describe("with data interceptor", () => {
|
|
355
|
-
it("should request data from interceptor", () => {
|
|
390
|
+
it("should request data from interceptor", async () => {
|
|
356
391
|
// Arrange
|
|
357
392
|
const fakeHandler = jest.fn().mockResolvedValue("data");
|
|
358
393
|
const fakeChildrenFn = jest.fn(() => null);
|
|
@@ -368,13 +403,14 @@ describe("Data", () => {
|
|
|
368
403
|
</Data>
|
|
369
404
|
</InterceptRequests>,
|
|
370
405
|
);
|
|
406
|
+
await act(() => interceptHandler.mock.results[0].value);
|
|
371
407
|
|
|
372
408
|
// Assert
|
|
373
409
|
expect(interceptHandler).toHaveBeenCalledTimes(1);
|
|
374
410
|
expect(fakeHandler).not.toHaveBeenCalled();
|
|
375
411
|
});
|
|
376
412
|
|
|
377
|
-
it("should invoke handler method if interceptor method returns null", () => {
|
|
413
|
+
it("should invoke handler method if interceptor method returns null", async () => {
|
|
378
414
|
// Arrange
|
|
379
415
|
const fakeHandler = jest.fn().mockResolvedValue("data");
|
|
380
416
|
const fakeChildrenFn = jest.fn(() => null);
|
|
@@ -388,50 +424,15 @@ describe("Data", () => {
|
|
|
388
424
|
</Data>
|
|
389
425
|
</InterceptRequests>,
|
|
390
426
|
);
|
|
427
|
+
await act(() => fakeHandler.mock.results[0].value);
|
|
391
428
|
|
|
392
429
|
// Assert
|
|
393
430
|
expect(interceptHandler).toHaveBeenCalledTimes(1);
|
|
394
431
|
expect(fakeHandler).toHaveBeenCalledTimes(1);
|
|
395
432
|
});
|
|
396
433
|
});
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
describe("with cache data", () => {
|
|
400
|
-
beforeEach(() => {
|
|
401
|
-
/**
|
|
402
|
-
* Each of these test cases will start out with some cached data
|
|
403
|
-
* retrieved.
|
|
404
|
-
*/
|
|
405
|
-
jest.spyOn(
|
|
406
|
-
SsrCache.Default,
|
|
407
|
-
"getEntry",
|
|
408
|
-
// Fake once because that's how the cache would work,
|
|
409
|
-
// deleting the hydrated value as soon as it was used.
|
|
410
|
-
).mockReturnValueOnce({
|
|
411
|
-
data: "YAY! DATA!",
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
434
|
|
|
415
|
-
it("should
|
|
416
|
-
// Arrange
|
|
417
|
-
const fakeHandler = () => Promise.resolve("data");
|
|
418
|
-
const fakeChildrenFn = jest.fn(() => null);
|
|
419
|
-
|
|
420
|
-
// Act
|
|
421
|
-
render(
|
|
422
|
-
<Data handler={fakeHandler} requestId="ID">
|
|
423
|
-
{fakeChildrenFn}
|
|
424
|
-
</Data>,
|
|
425
|
-
);
|
|
426
|
-
|
|
427
|
-
// Assert
|
|
428
|
-
expect(fakeChildrenFn).toHaveBeenCalledWith({
|
|
429
|
-
status: "success",
|
|
430
|
-
data: "YAY! DATA!",
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
it("should retain old data while reloading if showOldDataWhileLoading is true", async () => {
|
|
435
|
+
it("should retain old data while reloading if retainResultOnChange is true", async () => {
|
|
435
436
|
// Arrange
|
|
436
437
|
const response1 = Promise.resolve("data1");
|
|
437
438
|
const response2 = Promise.resolve("data2");
|
|
@@ -443,49 +444,76 @@ describe("Data", () => {
|
|
|
443
444
|
const wrapper = render(
|
|
444
445
|
<Data
|
|
445
446
|
handler={fakeHandler1}
|
|
446
|
-
requestId="
|
|
447
|
-
|
|
447
|
+
requestId="ID1"
|
|
448
|
+
retainResultOnChange={true}
|
|
448
449
|
>
|
|
449
450
|
{fakeChildrenFn}
|
|
450
451
|
</Data>,
|
|
451
452
|
);
|
|
453
|
+
fakeChildrenFn.mockClear();
|
|
454
|
+
await act(() => response1);
|
|
452
455
|
wrapper.rerender(
|
|
453
456
|
<Data
|
|
454
457
|
handler={fakeHandler2}
|
|
455
|
-
requestId="
|
|
456
|
-
|
|
458
|
+
requestId="ID2"
|
|
459
|
+
retainResultOnChange={true}
|
|
457
460
|
>
|
|
458
461
|
{fakeChildrenFn}
|
|
459
462
|
</Data>,
|
|
460
463
|
);
|
|
464
|
+
await act(() => response2);
|
|
461
465
|
|
|
462
466
|
// Assert
|
|
463
467
|
expect(fakeChildrenFn).not.toHaveBeenCalledWith({
|
|
464
468
|
status: "loading",
|
|
465
469
|
});
|
|
470
|
+
expect(fakeChildrenFn).toHaveBeenCalledWith({
|
|
471
|
+
status: "success",
|
|
472
|
+
data: "data1",
|
|
473
|
+
});
|
|
474
|
+
expect(fakeChildrenFn).toHaveBeenLastCalledWith({
|
|
475
|
+
status: "success",
|
|
476
|
+
data: "data2",
|
|
477
|
+
});
|
|
466
478
|
});
|
|
479
|
+
});
|
|
467
480
|
|
|
468
|
-
|
|
481
|
+
describe("with hydrated data", () => {
|
|
482
|
+
beforeEach(() => {
|
|
483
|
+
/**
|
|
484
|
+
* Each of these test cases will start out with some cached data
|
|
485
|
+
* retrieved.
|
|
486
|
+
*/
|
|
487
|
+
jest.spyOn(
|
|
488
|
+
SsrCache.Default,
|
|
489
|
+
"getEntry",
|
|
490
|
+
// Fake once because that's how the cache would work,
|
|
491
|
+
// deleting the hydrated value as soon as it was used.
|
|
492
|
+
).mockReturnValueOnce({
|
|
493
|
+
data: "YAY! DATA!",
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("should render first time with the cached data", () => {
|
|
469
498
|
// Arrange
|
|
470
|
-
const fakeHandler =
|
|
499
|
+
const fakeHandler = () => Promise.resolve("data");
|
|
471
500
|
const fakeChildrenFn = jest.fn(() => null);
|
|
472
501
|
|
|
473
502
|
// Act
|
|
474
503
|
render(
|
|
475
|
-
<Data
|
|
476
|
-
handler={fakeHandler}
|
|
477
|
-
requestId="ID"
|
|
478
|
-
alwaysRequestOnHydration={false}
|
|
479
|
-
>
|
|
504
|
+
<Data handler={fakeHandler} requestId="ID">
|
|
480
505
|
{fakeChildrenFn}
|
|
481
506
|
</Data>,
|
|
482
507
|
);
|
|
483
508
|
|
|
484
509
|
// Assert
|
|
485
|
-
expect(
|
|
510
|
+
expect(fakeChildrenFn).toHaveBeenCalledWith({
|
|
511
|
+
status: "success",
|
|
512
|
+
data: "YAY! DATA!",
|
|
513
|
+
});
|
|
486
514
|
});
|
|
487
515
|
|
|
488
|
-
it("should request data
|
|
516
|
+
it("should not request data when clientBehavior is WhenClientSide.ExecuteWhenNoSuccessResult and cache has a valid success result", () => {
|
|
489
517
|
// Arrange
|
|
490
518
|
const fakeHandler = jest.fn().mockResolvedValue("data");
|
|
491
519
|
const fakeChildrenFn = jest.fn(() => null);
|
|
@@ -495,43 +523,34 @@ describe("Data", () => {
|
|
|
495
523
|
<Data
|
|
496
524
|
handler={fakeHandler}
|
|
497
525
|
requestId="ID"
|
|
498
|
-
|
|
526
|
+
clientBehavior={
|
|
527
|
+
WhenClientSide.ExecuteWhenNoSuccessResult
|
|
528
|
+
}
|
|
499
529
|
>
|
|
500
530
|
{fakeChildrenFn}
|
|
501
531
|
</Data>,
|
|
502
532
|
);
|
|
503
533
|
|
|
504
534
|
// Assert
|
|
505
|
-
expect(fakeHandler).
|
|
506
|
-
});
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
describe("with cached abort", () => {
|
|
510
|
-
beforeEach(() => {
|
|
511
|
-
/**
|
|
512
|
-
* Each of these test cases will start out with a cached abort.
|
|
513
|
-
*/
|
|
514
|
-
jest.spyOn(
|
|
515
|
-
SsrCache.Default,
|
|
516
|
-
"getEntry",
|
|
517
|
-
// Fake once because that's how the cache would work,
|
|
518
|
-
// deleting the hydrated value as soon as it was used.
|
|
519
|
-
).mockReturnValueOnce({
|
|
520
|
-
data: null,
|
|
521
|
-
});
|
|
535
|
+
expect(fakeHandler).not.toHaveBeenCalled();
|
|
522
536
|
});
|
|
523
537
|
|
|
524
|
-
it("should request data if cached data value is
|
|
538
|
+
it("should request data if cached data value is valid but clientBehavior is WhenClientSide.AlwaysExecute is true", async () => {
|
|
525
539
|
// Arrange
|
|
526
540
|
const fakeHandler = jest.fn().mockResolvedValue("data");
|
|
527
541
|
const fakeChildrenFn = jest.fn(() => null);
|
|
528
542
|
|
|
529
543
|
// Act
|
|
530
544
|
render(
|
|
531
|
-
<Data
|
|
545
|
+
<Data
|
|
546
|
+
handler={fakeHandler}
|
|
547
|
+
requestId="ID"
|
|
548
|
+
clientBehavior={WhenClientSide.AlwaysExecute}
|
|
549
|
+
>
|
|
532
550
|
{fakeChildrenFn}
|
|
533
551
|
</Data>,
|
|
534
552
|
);
|
|
553
|
+
await act(() => fakeHandler.mock.results[0].value);
|
|
535
554
|
|
|
536
555
|
// Assert
|
|
537
556
|
expect(fakeHandler).toHaveBeenCalledTimes(1);
|
|
@@ -553,7 +572,7 @@ describe("Data", () => {
|
|
|
553
572
|
});
|
|
554
573
|
});
|
|
555
574
|
|
|
556
|
-
it("should always request data if there's a cached error", () => {
|
|
575
|
+
it("should always request data if there's a cached error", async () => {
|
|
557
576
|
// Arrange
|
|
558
577
|
const fakeHandler = jest.fn().mockResolvedValue("data");
|
|
559
578
|
const fakeChildrenFn = jest.fn(() => null);
|
|
@@ -564,6 +583,8 @@ describe("Data", () => {
|
|
|
564
583
|
{fakeChildrenFn}
|
|
565
584
|
</Data>,
|
|
566
585
|
);
|
|
586
|
+
// Have to await the promise in an act to keep TL/R happy.
|
|
587
|
+
await act(() => fakeHandler.mock.results[0].value);
|
|
567
588
|
|
|
568
589
|
// Assert
|
|
569
590
|
expect(fakeHandler).toHaveBeenCalledTimes(1);
|
|
@@ -765,7 +786,7 @@ describe("Data", () => {
|
|
|
765
786
|
// Assert
|
|
766
787
|
expect(fakeChildrenFn).toHaveBeenCalledWith({
|
|
767
788
|
status: "error",
|
|
768
|
-
error:
|
|
789
|
+
error: expect.any(Error),
|
|
769
790
|
});
|
|
770
791
|
});
|
|
771
792
|
|
package/src/components/data.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import {
|
|
5
|
+
useHydratableEffect,
|
|
6
|
+
// TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
|
|
7
|
+
// have fixed:
|
|
8
|
+
// https://github.com/import-js/eslint-plugin-import/issues/2073
|
|
9
|
+
// eslint-disable-next-line import/named
|
|
10
|
+
WhenClientSide,
|
|
11
|
+
} from "../hooks/use-hydratable-effect.js";
|
|
9
12
|
|
|
10
13
|
import type {Result, ValidCacheData} from "../util/types.js";
|
|
11
14
|
|
|
@@ -29,18 +32,16 @@ type Props<
|
|
|
29
32
|
* old handler result may be given. This is not a supported mode of
|
|
30
33
|
* operation.
|
|
31
34
|
*/
|
|
32
|
-
handler: () => Promise
|
|
35
|
+
handler: () => Promise<TData>,
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
*
|
|
36
|
-
* the request will be fulfilled for us in SSR but will be ignored during
|
|
37
|
-
* hydration. Only set this to false if you know some other mechanism
|
|
38
|
-
* will be performing hydration (such as if requests are fulfilled by
|
|
39
|
-
* Apollo Client but you consolidated all SSR requests using WB Data).
|
|
38
|
+
* How the hook should behave when rendering client-side for the first time.
|
|
40
39
|
*
|
|
41
|
-
*
|
|
40
|
+
* This controls how the hook hydrates and executes when client-side.
|
|
41
|
+
*
|
|
42
|
+
* Default is `OnClientRender.ExecuteWhenNoSuccessResult`.
|
|
42
43
|
*/
|
|
43
|
-
|
|
44
|
+
clientBehavior?: WhenClientSide,
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* When true, the children will be rendered with the existing result
|
|
@@ -49,15 +50,7 @@ type Props<
|
|
|
49
50
|
*
|
|
50
51
|
* Defaults to false.
|
|
51
52
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* When true, the handler will always be invoked after hydration.
|
|
56
|
-
* This defaults to false.
|
|
57
|
-
* NOTE: The request is invoked after hydration if the hydrated result
|
|
58
|
-
* is an error.
|
|
59
|
-
*/
|
|
60
|
-
alwaysRequestOnHydration?: boolean,
|
|
53
|
+
retainResultOnChange?: boolean,
|
|
61
54
|
|
|
62
55
|
/**
|
|
63
56
|
* A function that will render the content of this component using the
|
|
@@ -76,97 +69,14 @@ const Data = <TData: ValidCacheData>({
|
|
|
76
69
|
requestId,
|
|
77
70
|
handler,
|
|
78
71
|
children,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
alwaysRequestOnHydration,
|
|
72
|
+
retainResultOnChange = false,
|
|
73
|
+
clientBehavior = WhenClientSide.ExecuteWhenNoSuccessResult,
|
|
82
74
|
}: Props<TData>): React.Node => {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
hydrate,
|
|
89
|
-
);
|
|
90
|
-
const [currentResult, setResult] = React.useState(hydrateResult);
|
|
91
|
-
|
|
92
|
-
// Here we make sure the request still occurs client-side as needed.
|
|
93
|
-
// This is for legacy usage that expects this. Eventually we will want
|
|
94
|
-
// to deprecate.
|
|
95
|
-
React.useEffect(() => {
|
|
96
|
-
// This is here until I can do a better documentation example for
|
|
97
|
-
// the TrackData docs.
|
|
98
|
-
// istanbul ignore next
|
|
99
|
-
if (Server.isServerSide()) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// We don't bother with this if we have hydration data and we're not
|
|
104
|
-
// forcing a request on hydration.
|
|
105
|
-
// We don't care if these things change after the first render,
|
|
106
|
-
// so we don't want them in the inputs array.
|
|
107
|
-
if (!alwaysRequestOnHydration && hydrateResult?.data != null) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// If we're not hydrating a result and we're not going to render
|
|
112
|
-
// with old data until we're loaded, we want to make sure we set our
|
|
113
|
-
// result to null so that we're in the loading state.
|
|
114
|
-
if (!showOldDataWhileLoading) {
|
|
115
|
-
// Mark ourselves as loading.
|
|
116
|
-
setResult(null);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// We aren't server-side, so let's make the request.
|
|
120
|
-
// We don't need to use our built-in request fulfillment here if we
|
|
121
|
-
// don't want, but it does mean we'll share inflight requests for the
|
|
122
|
-
// same ID and the result will be in the same format as the
|
|
123
|
-
// hydrated value.
|
|
124
|
-
let cancel = false;
|
|
125
|
-
RequestFulfillment.Default.fulfill(requestId, {
|
|
126
|
-
handler: interceptedHandler,
|
|
127
|
-
})
|
|
128
|
-
.then((result) => {
|
|
129
|
-
if (cancel) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
setResult(result);
|
|
133
|
-
return;
|
|
134
|
-
})
|
|
135
|
-
.catch((e) => {
|
|
136
|
-
if (cancel) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* We should never get here as errors in fulfillment are part
|
|
141
|
-
* of the `then`, but if we do.
|
|
142
|
-
*/
|
|
143
|
-
// eslint-disable-next-line no-console
|
|
144
|
-
console.error(
|
|
145
|
-
`Unexpected error occurred during data fulfillment: ${e}`,
|
|
146
|
-
);
|
|
147
|
-
setResult({
|
|
148
|
-
error: typeof e === "string" ? e : e.message,
|
|
149
|
-
});
|
|
150
|
-
return;
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return () => {
|
|
154
|
-
cancel = true;
|
|
155
|
-
};
|
|
156
|
-
// If the handler changes, we don't care. The ID is what indicates
|
|
157
|
-
// the request that should be made and folks shouldn't be changing the
|
|
158
|
-
// handler without changing the ID as well.
|
|
159
|
-
// In addition, we don't want to include hydrateResult nor
|
|
160
|
-
// alwaysRequestOnHydration as them changinng after the first pass
|
|
161
|
-
// is irrelevant.
|
|
162
|
-
// Finally, we don't want to include showOldDataWhileLoading as that
|
|
163
|
-
// changing on its own is also not relevant. It only matters if the
|
|
164
|
-
// request itself changes. All of which is to say that we only
|
|
165
|
-
// run this effect for the ID changing.
|
|
166
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
167
|
-
}, [requestId]);
|
|
168
|
-
|
|
169
|
-
return children(resultFromCachedResponse(currentResult));
|
|
75
|
+
const result = useHydratableEffect(requestId, handler, {
|
|
76
|
+
retainResultOnChange,
|
|
77
|
+
clientBehavior,
|
|
78
|
+
});
|
|
79
|
+
return children(result);
|
|
170
80
|
};
|
|
171
81
|
|
|
172
82
|
export default Data;
|
|
@@ -20,7 +20,7 @@ type Props<TData: ValidCacheData> = {|
|
|
|
20
20
|
* so make sure to only intercept requests that you recognize from the
|
|
21
21
|
* identifier.
|
|
22
22
|
*/
|
|
23
|
-
interceptor: (requestId: string) => ?Promise
|
|
23
|
+
interceptor: (requestId: string) => ?Promise<TData>,
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* The children to render within this component. Any requests by `Data`
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
|
-
exports[`#useSharedCache should throw if the id is 1`] = `[
|
|
3
|
+
exports[`#useSharedCache should throw if the id is 1`] = `[InvalidInputDataError: id must be a non-empty string]`;
|
|
4
4
|
|
|
5
|
-
exports[`#useSharedCache should throw if the id is [Function anonymous] 1`] = `[
|
|
5
|
+
exports[`#useSharedCache should throw if the id is [Function anonymous] 1`] = `[InvalidInputDataError: id must be a non-empty string]`;
|
|
6
6
|
|
|
7
|
-
exports[`#useSharedCache should throw if the id is 5 1`] = `[
|
|
7
|
+
exports[`#useSharedCache should throw if the id is 5 1`] = `[InvalidInputDataError: id must be a non-empty string]`;
|
|
8
8
|
|
|
9
|
-
exports[`#useSharedCache should throw if the id is null 1`] = `[
|
|
9
|
+
exports[`#useSharedCache should throw if the id is null 1`] = `[InvalidInputDataError: id must be a non-empty string]`;
|
|
10
10
|
|
|
11
|
-
exports[`#useSharedCache should throw if the scope is 1`] = `[
|
|
11
|
+
exports[`#useSharedCache should throw if the scope is 1`] = `[InvalidInputDataError: scope must be a non-empty string]`;
|
|
12
12
|
|
|
13
|
-
exports[`#useSharedCache should throw if the scope is [Function anonymous] 1`] = `[
|
|
13
|
+
exports[`#useSharedCache should throw if the scope is [Function anonymous] 1`] = `[InvalidInputDataError: scope must be a non-empty string]`;
|
|
14
14
|
|
|
15
|
-
exports[`#useSharedCache should throw if the scope is 5 1`] = `[
|
|
15
|
+
exports[`#useSharedCache should throw if the scope is 5 1`] = `[InvalidInputDataError: scope must be a non-empty string]`;
|
|
16
16
|
|
|
17
|
-
exports[`#useSharedCache should throw if the scope is null 1`] = `[
|
|
17
|
+
exports[`#useSharedCache should throw if the scope is null 1`] = `[InvalidInputDataError: scope must be a non-empty string]`;
|