@khanacademy/wonder-blocks-data 5.0.1 → 7.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 (84) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/es/index.js +767 -371
  3. package/dist/index.js +1194 -564
  4. package/legacy-docs.md +3 -0
  5. package/package.json +2 -2
  6. package/src/__docs__/_overview_.stories.mdx +18 -0
  7. package/src/__docs__/_overview_graphql.stories.mdx +35 -0
  8. package/src/__docs__/_overview_ssr_.stories.mdx +185 -0
  9. package/src/__docs__/_overview_testing_.stories.mdx +123 -0
  10. package/src/__docs__/exports.clear-shared-cache.stories.mdx +20 -0
  11. package/src/__docs__/exports.data-error.stories.mdx +23 -0
  12. package/src/__docs__/exports.data-errors.stories.mdx +23 -0
  13. package/src/{components/data.md → __docs__/exports.data.stories.mdx} +15 -18
  14. package/src/__docs__/exports.fulfill-all-data-requests.stories.mdx +24 -0
  15. package/src/__docs__/exports.gql-error.stories.mdx +23 -0
  16. package/src/__docs__/exports.gql-errors.stories.mdx +20 -0
  17. package/src/__docs__/exports.gql-router.stories.mdx +29 -0
  18. package/src/__docs__/exports.has-unfulfilled-requests.stories.mdx +20 -0
  19. package/src/{components/intercept-requests.md → __docs__/exports.intercept-requests.stories.mdx} +16 -1
  20. package/src/__docs__/exports.intialize-cache.stories.mdx +29 -0
  21. package/src/__docs__/exports.remove-all-from-cache.stories.mdx +24 -0
  22. package/src/__docs__/exports.remove-from-cache.stories.mdx +25 -0
  23. package/src/__docs__/exports.request-fulfillment.stories.mdx +36 -0
  24. package/src/__docs__/exports.scoped-in-memory-cache.stories.mdx +92 -0
  25. package/src/__docs__/exports.serializable-in-memory-cache.stories.mdx +112 -0
  26. package/src/__docs__/exports.status.stories.mdx +31 -0
  27. package/src/{components/track-data.md → __docs__/exports.track-data.stories.mdx} +15 -0
  28. package/src/__docs__/exports.use-cached-effect.stories.mdx +41 -0
  29. package/src/__docs__/exports.use-gql.stories.mdx +73 -0
  30. package/src/__docs__/exports.use-hydratable-effect.stories.mdx +43 -0
  31. package/src/__docs__/exports.use-server-effect.stories.mdx +50 -0
  32. package/src/__docs__/exports.use-shared-cache.stories.mdx +30 -0
  33. package/src/__docs__/exports.when-client-side.stories.mdx +33 -0
  34. package/src/__docs__/types.cached-response.stories.mdx +29 -0
  35. package/src/__docs__/types.error-options.stories.mdx +21 -0
  36. package/src/__docs__/types.gql-context.stories.mdx +20 -0
  37. package/src/__docs__/types.gql-fetch-fn.stories.mdx +24 -0
  38. package/src/__docs__/types.gql-fetch-options.stories.mdx +24 -0
  39. package/src/__docs__/types.gql-operation-type.stories.mdx +24 -0
  40. package/src/__docs__/types.gql-operation.stories.mdx +67 -0
  41. package/src/__docs__/types.response-cache.stories.mdx +33 -0
  42. package/src/__docs__/types.result.stories.mdx +39 -0
  43. package/src/__docs__/types.scoped-cache.stories.mdx +27 -0
  44. package/src/__docs__/types.valid-cache-data.stories.mdx +23 -0
  45. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -80
  46. package/src/__tests__/generated-snapshot.test.js +0 -24
  47. package/src/components/__tests__/data.test.js +149 -128
  48. package/src/components/data.js +22 -112
  49. package/src/components/intercept-requests.js +1 -1
  50. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +8 -8
  51. package/src/hooks/__tests__/use-cached-effect.test.js +507 -0
  52. package/src/hooks/__tests__/use-gql-router-context.test.js +133 -0
  53. package/src/hooks/__tests__/use-gql.test.js +1 -30
  54. package/src/hooks/__tests__/use-hydratable-effect.test.js +705 -0
  55. package/src/hooks/__tests__/use-server-effect.test.js +90 -11
  56. package/src/hooks/use-cached-effect.js +225 -0
  57. package/src/hooks/use-gql-router-context.js +50 -0
  58. package/src/hooks/use-gql.js +22 -52
  59. package/src/hooks/use-hydratable-effect.js +206 -0
  60. package/src/hooks/use-request-interception.js +20 -23
  61. package/src/hooks/use-server-effect.js +42 -10
  62. package/src/hooks/use-shared-cache.js +13 -11
  63. package/src/index.js +53 -3
  64. package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.js.snap +19 -0
  65. package/src/util/__tests__/merge-gql-context.test.js +74 -0
  66. package/src/util/__tests__/request-fulfillment.test.js +23 -42
  67. package/src/util/__tests__/request-tracking.test.js +26 -7
  68. package/src/util/__tests__/result-from-cache-response.test.js +19 -5
  69. package/src/util/__tests__/scoped-in-memory-cache.test.js +6 -85
  70. package/src/util/__tests__/serializable-in-memory-cache.test.js +398 -0
  71. package/src/util/__tests__/ssr-cache.test.js +52 -52
  72. package/src/util/data-error.js +58 -0
  73. package/src/util/get-gql-data-from-response.js +3 -2
  74. package/src/util/gql-error.js +19 -11
  75. package/src/util/merge-gql-context.js +34 -0
  76. package/src/util/request-fulfillment.js +49 -46
  77. package/src/util/request-tracking.js +69 -15
  78. package/src/util/result-from-cache-response.js +12 -16
  79. package/src/util/scoped-in-memory-cache.js +24 -47
  80. package/src/util/serializable-in-memory-cache.js +49 -0
  81. package/src/util/ssr-cache.js +9 -8
  82. package/src/util/status.js +30 -0
  83. package/src/util/types.js +18 -1
  84. package/docs.md +0 -122
@@ -1,6 +1,6 @@
1
1
  // @flow
2
- import {SsrCache} from "../ssr-cache.js";
3
2
  import {RequestFulfillment} from "../request-fulfillment.js";
3
+ import {DataError} from "../data-error.js";
4
4
 
5
5
  describe("RequestFulfillment", () => {
6
6
  it("should provide static default instance", () => {
@@ -15,62 +15,44 @@ describe("RequestFulfillment", () => {
15
15
  });
16
16
 
17
17
  describe("#fulfill", () => {
18
- it("should attempt to cache errors caused directly by handlers", async () => {
18
+ it("should resolve to an error result", async () => {
19
19
  // Arrange
20
- const responseCache = new SsrCache();
21
- const requestFulfillment = new RequestFulfillment(responseCache);
22
- const error = new Error("OH NO!");
23
- const fakeBadHandler = () => {
24
- throw error;
25
- };
26
- const cacheErrorSpy = jest.spyOn(responseCache, "cacheError");
20
+ const requestFulfillment = new RequestFulfillment();
21
+ const fakeBadRequestHandler = () => Promise.reject("OH NO!");
27
22
 
28
23
  // Act
29
- await requestFulfillment.fulfill("ID", {
30
- handler: fakeBadHandler,
24
+ const result = await requestFulfillment.fulfill("ID", {
25
+ handler: fakeBadRequestHandler,
31
26
  });
32
27
 
33
28
  // Assert
34
- expect(cacheErrorSpy).toHaveBeenCalledWith("ID", error, true);
29
+ expect(result).toStrictEqual({
30
+ status: "error",
31
+ error: expect.any(DataError),
32
+ });
35
33
  });
36
34
 
37
- it("should cache errors occurring in promises", async () => {
35
+ it("should resolve to an aborted result", async () => {
38
36
  // Arrange
39
- const responseCache = new SsrCache();
40
- const requestFulfillment = new RequestFulfillment(responseCache);
41
- const fakeBadRequestHandler = () =>
42
- new Promise((resolve, reject) => reject("OH NO!"));
43
- const cacheErrorSpy = jest.spyOn(responseCache, "cacheError");
37
+ const requestFulfillment = new RequestFulfillment();
38
+ const abortError = new Error("abort abort abort, awoooga");
39
+ abortError.name = "AbortError";
40
+ const fakeBadRequestHandler = () => Promise.reject(abortError);
44
41
 
45
42
  // Act
46
- await requestFulfillment.fulfill("ID", {
43
+ const result = await requestFulfillment.fulfill("ID", {
47
44
  handler: fakeBadRequestHandler,
48
45
  });
49
46
 
50
47
  // Assert
51
- expect(cacheErrorSpy).toHaveBeenCalledWith("ID", "OH NO!", true);
52
- });
53
-
54
- it("should cache data from requests", async () => {
55
- // Arrange
56
- const responseCache = new SsrCache();
57
- const requestFulfillment = new RequestFulfillment(responseCache);
58
- const fakeRequestHandler = () => Promise.resolve("DATA!");
59
- const cacheDataSpy = jest.spyOn(responseCache, "cacheData");
60
-
61
- // Act
62
- await requestFulfillment.fulfill("ID", {
63
- handler: fakeRequestHandler,
48
+ expect(result).toStrictEqual({
49
+ status: "aborted",
64
50
  });
65
-
66
- // Assert
67
- expect(cacheDataSpy).toHaveBeenCalledWith("ID", "DATA!", true);
68
51
  });
69
52
 
70
- it("should return a promise of the result", async () => {
53
+ it("should resolve to a data result", async () => {
71
54
  // Arrange
72
- const responseCache = new SsrCache();
73
- const requestFulfillment = new RequestFulfillment(responseCache);
55
+ const requestFulfillment = new RequestFulfillment();
74
56
  const fakeRequestHandler = () => Promise.resolve("DATA!");
75
57
 
76
58
  // Act
@@ -80,14 +62,14 @@ describe("RequestFulfillment", () => {
80
62
 
81
63
  // Assert
82
64
  expect(result).toStrictEqual({
65
+ status: "success",
83
66
  data: "DATA!",
84
67
  });
85
68
  });
86
69
 
87
70
  it("should reuse inflight requests", () => {
88
71
  // Arrange
89
- const responseCache = new SsrCache();
90
- const requestFulfillment = new RequestFulfillment(responseCache);
72
+ const requestFulfillment = new RequestFulfillment();
91
73
  const fakeRequestHandler = () => Promise.resolve("DATA!");
92
74
 
93
75
  // Act
@@ -104,8 +86,7 @@ describe("RequestFulfillment", () => {
104
86
 
105
87
  it("should remove inflight requests upon completion", async () => {
106
88
  // Arrange
107
- const responseCache = new SsrCache();
108
- const requestFulfillment = new RequestFulfillment(responseCache);
89
+ const requestFulfillment = new RequestFulfillment();
109
90
  const fakeRequestHandler = () => Promise.resolve("DATA!");
110
91
 
111
92
  // Act
@@ -78,7 +78,7 @@ describe("../request-tracking.js", () => {
78
78
  it("should track each matching request once", async () => {
79
79
  // Arrange
80
80
  const requestTracker = createRequestTracker();
81
- const fakeHandler = jest.fn().mockResolvedValue(null);
81
+ const fakeHandler = jest.fn().mockResolvedValue("DATA");
82
82
 
83
83
  // Act
84
84
  requestTracker.trackDataRequest("ID", fakeHandler, true);
@@ -177,7 +177,7 @@ describe("../request-tracking.js", () => {
177
177
  // Assert
178
178
  expect(result).toStrictEqual({
179
179
  ID: {
180
- error: "OH NO!",
180
+ error: "Request failed",
181
181
  },
182
182
  });
183
183
  });
@@ -230,7 +230,7 @@ describe("../request-tracking.js", () => {
230
230
  // Assert
231
231
  expect(result).toStrictEqual({
232
232
  BAD_REQUEST: {
233
- error: "OH NO!",
233
+ error: "Request failed",
234
234
  },
235
235
  BAD_HANDLER: {
236
236
  error: "OH NO!",
@@ -244,14 +244,33 @@ describe("../request-tracking.js", () => {
244
244
  });
245
245
  });
246
246
 
247
- it("should cope gracefully with null fulfillments", async () => {
247
+ it("should ignore loading results", async () => {
248
+ // Arrange
249
+ const requestTracker = createRequestTracker();
250
+ jest.spyOn(
251
+ requestTracker._requestFulfillment,
252
+ "fulfill",
253
+ ).mockResolvedValue({status: "loading"});
254
+ const fakeValidHandler = () =>
255
+ Promise.reject(new Error("Not called for this test case"));
256
+ requestTracker.trackDataRequest("ID", fakeValidHandler, true);
257
+
258
+ // Act
259
+ const result = await requestTracker.fulfillTrackedRequests();
260
+
261
+ // Assert
262
+ expect(result).toStrictEqual({});
263
+ });
264
+
265
+ it("should ignore aborted results", async () => {
248
266
  // Arrange
249
267
  const requestTracker = createRequestTracker();
250
268
  jest.spyOn(
251
269
  requestTracker._requestFulfillment,
252
270
  "fulfill",
253
- ).mockReturnValue(null);
254
- const fakeValidHandler = () => Promise.resolve("DATA");
271
+ ).mockResolvedValue({status: "aborted"});
272
+ const fakeValidHandler = () =>
273
+ Promise.reject(new Error("Not called for this test case"));
255
274
  requestTracker.trackDataRequest("ID", fakeValidHandler, false);
256
275
 
257
276
  // Act
@@ -285,7 +304,7 @@ describe("../request-tracking.js", () => {
285
304
  it("should clear the tracked data requests", async () => {
286
305
  // Arrange
287
306
  const requestTracker = createRequestTracker();
288
- const fakeHandler = jest.fn().mockResolvedValue(null);
307
+ const fakeHandler = jest.fn().mockResolvedValue("DATA");
289
308
  requestTracker.trackDataRequest("ID", fakeHandler, true);
290
309
 
291
310
  // Act
@@ -2,7 +2,7 @@
2
2
  import {resultFromCachedResponse} from "../result-from-cache-response.js";
3
3
 
4
4
  describe("#resultFromCachedResponse", () => {
5
- it("should return loading status if cache entry is null", () => {
5
+ it("should return null cache entry is null", () => {
6
6
  // Arrange
7
7
  const cacheEntry = null;
8
8
 
@@ -10,9 +10,7 @@ describe("#resultFromCachedResponse", () => {
10
10
  const result = resultFromCachedResponse(cacheEntry);
11
11
 
12
12
  // Assert
13
- expect(result).toStrictEqual({
14
- status: "loading",
15
- });
13
+ expect(result).toBeNull();
16
14
  });
17
15
 
18
16
  it("should return success status if cache entry has data", () => {
@@ -61,7 +59,23 @@ describe("#resultFromCachedResponse", () => {
61
59
  // Assert
62
60
  expect(result).toStrictEqual({
63
61
  status: "error",
64
- error: "ERROR",
62
+ error: expect.any(Error),
65
63
  });
66
64
  });
65
+
66
+ it("should hydrate the error into an error object", () => {
67
+ // Arrange
68
+ const cacheEntry: any = {
69
+ data: null,
70
+ error: "ERROR",
71
+ };
72
+
73
+ // Act
74
+ // $FlowIgnore[incompatible-use]
75
+ // $FlowIgnore[prop-missing]
76
+ const {error} = resultFromCachedResponse(cacheEntry);
77
+
78
+ // Assert
79
+ expect(error).toMatchInlineSnapshot(`[HydratedDataError: ERROR]`);
80
+ });
67
81
  });
@@ -1,48 +1,7 @@
1
1
  // @flow
2
- import * as WSCore from "@khanacademy/wonder-stuff-core";
3
2
  import {ScopedInMemoryCache} from "../scoped-in-memory-cache.js";
4
3
 
5
4
  describe("ScopedInMemoryCache", () => {
6
- describe("#constructor", () => {
7
- it("should clone the passed source data", () => {
8
- // Arrange
9
- const sourceData = {
10
- scope: {
11
- key: "value",
12
- },
13
- };
14
-
15
- // Act
16
- const cache = new ScopedInMemoryCache(sourceData);
17
- // Try to mutate the cache.
18
- sourceData["scope"] = {key: "SOME_NEW_DATA"};
19
- const result = cache.get("scope", "key");
20
-
21
- // Assert
22
- expect(result).toStrictEqual("value");
23
- });
24
-
25
- it("should throw if the cloning fails", () => {
26
- // Arrange
27
- jest.spyOn(WSCore, "clone").mockImplementationOnce(() => {
28
- throw new Error("BANG!");
29
- });
30
-
31
- // Act
32
- const underTest = () =>
33
- new ScopedInMemoryCache({
34
- scope: {
35
- BAD: "FOOD",
36
- },
37
- });
38
-
39
- // Assert
40
- expect(underTest).toThrowErrorMatchingInlineSnapshot(
41
- `"An error occurred trying to initialize from a response cache snapshot: Error: BANG!"`,
42
- );
43
- });
44
- });
45
-
46
5
  describe("#set", () => {
47
6
  it.each`
48
7
  id
@@ -175,7 +134,7 @@ describe("ScopedInMemoryCache", () => {
175
134
 
176
135
  // Act
177
136
  cache.purge("scope1", "key2");
178
- const result = cache.clone();
137
+ const result = cache._cache;
179
138
 
180
139
  // Assert
181
140
  expect(result).toStrictEqual({
@@ -202,7 +161,7 @@ describe("ScopedInMemoryCache", () => {
202
161
 
203
162
  // Act
204
163
  cache.purge("scope1", "key2");
205
- const result = cache.clone();
164
+ const result = cache._cache;
206
165
 
207
166
  // Assert
208
167
  expect(result).toStrictEqual({
@@ -232,7 +191,7 @@ describe("ScopedInMemoryCache", () => {
232
191
 
233
192
  // Act
234
193
  cache.purgeScope("scope1", (id, value) => value === "a");
235
- const result = cache.clone();
194
+ const result = cache._cache;
236
195
 
237
196
  // Assert
238
197
  expect(result).toStrictEqual({
@@ -262,7 +221,7 @@ describe("ScopedInMemoryCache", () => {
262
221
 
263
222
  // Act
264
223
  cache.purgeScope("scope1");
265
- const result = cache.clone();
224
+ const result = cache._cache;
266
225
 
267
226
  // Assert
268
227
  expect(result).toStrictEqual({
@@ -299,7 +258,7 @@ describe("ScopedInMemoryCache", () => {
299
258
 
300
259
  // Act
301
260
  cache.purgeAll((scope, id, value) => value === "2");
302
- const result = cache.clone();
261
+ const result = cache._cache;
303
262
 
304
263
  // Assert
305
264
  expect(result).toStrictEqual({
@@ -316,51 +275,13 @@ describe("ScopedInMemoryCache", () => {
316
275
 
317
276
  // Act
318
277
  cache.purgeAll();
319
- const result = cache.clone();
278
+ const result = cache._cache;
320
279
 
321
280
  // Assert
322
281
  expect(result).toStrictEqual({});
323
282
  });
324
283
  });
325
284
 
326
- describe("#clone", () => {
327
- it("should return a copy of the cache data", () => {
328
- // Arrange
329
- const data = {
330
- scope1: {key: "2"},
331
- scope2: {key: "1"},
332
- scope3: {key: "2"},
333
- };
334
- const cache = new ScopedInMemoryCache(data);
335
-
336
- // Act
337
- const result = cache.clone();
338
-
339
- // Assert
340
- expect(result).not.toBe(data);
341
- });
342
-
343
- it("should throw if there is an error during cloning", () => {
344
- // Arrange
345
- const cache = new ScopedInMemoryCache({
346
- scope1: {key: "2"},
347
- scope2: {key: "1"},
348
- scope3: {key: "2"},
349
- });
350
- jest.spyOn(WSCore, "clone").mockImplementationOnce(() => {
351
- throw new Error("BANG!");
352
- });
353
-
354
- // Act
355
- const act = () => cache.clone();
356
-
357
- // Assert
358
- expect(act).toThrowErrorMatchingInlineSnapshot(
359
- `"An error occurred while trying to clone the cache: Error: BANG!"`,
360
- );
361
- });
362
- });
363
-
364
285
  describe("@inUse", () => {
365
286
  it("should return true if the cache contains data", () => {
366
287
  // Arrange