@khanacademy/wonder-blocks-data 2.3.3 → 3.1.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 (50) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/es/index.js +365 -429
  3. package/dist/index.js +455 -461
  4. package/docs.md +19 -13
  5. package/package.json +6 -6
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +40 -160
  7. package/src/__tests__/generated-snapshot.test.js +15 -195
  8. package/src/components/__tests__/data.test.js +159 -965
  9. package/src/components/__tests__/gql-router.test.js +64 -0
  10. package/src/components/__tests__/intercept-data.test.js +9 -66
  11. package/src/components/__tests__/track-data.test.js +6 -5
  12. package/src/components/data.js +9 -119
  13. package/src/components/data.md +38 -60
  14. package/src/components/gql-router.js +66 -0
  15. package/src/components/intercept-context.js +2 -3
  16. package/src/components/intercept-data.js +2 -34
  17. package/src/components/intercept-data.md +7 -105
  18. package/src/hooks/__tests__/use-data.test.js +826 -0
  19. package/src/hooks/__tests__/use-gql.test.js +233 -0
  20. package/src/hooks/use-data.js +143 -0
  21. package/src/hooks/use-gql.js +75 -0
  22. package/src/index.js +7 -9
  23. package/src/util/__tests__/get-gql-data-from-response.test.js +187 -0
  24. package/src/util/__tests__/memory-cache.test.js +134 -35
  25. package/src/util/__tests__/request-fulfillment.test.js +21 -36
  26. package/src/util/__tests__/request-handler.test.js +30 -30
  27. package/src/util/__tests__/request-tracking.test.js +29 -30
  28. package/src/util/__tests__/response-cache.test.js +521 -561
  29. package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
  30. package/src/util/get-gql-data-from-response.js +69 -0
  31. package/src/util/gql-error.js +36 -0
  32. package/src/util/gql-router-context.js +6 -0
  33. package/src/util/gql-types.js +60 -0
  34. package/src/util/memory-cache.js +20 -15
  35. package/src/util/request-fulfillment.js +4 -0
  36. package/src/util/request-handler.js +4 -28
  37. package/src/util/request-handler.md +0 -32
  38. package/src/util/request-tracking.js +2 -3
  39. package/src/util/response-cache.js +50 -110
  40. package/src/util/result-from-cache-entry.js +38 -0
  41. package/src/util/types.js +14 -35
  42. package/LICENSE +0 -21
  43. package/src/components/__tests__/intercept-cache.test.js +0 -124
  44. package/src/components/__tests__/internal-data.test.js +0 -1030
  45. package/src/components/intercept-cache.js +0 -79
  46. package/src/components/intercept-cache.md +0 -103
  47. package/src/components/internal-data.js +0 -219
  48. package/src/util/__tests__/no-cache.test.js +0 -112
  49. package/src/util/no-cache.js +0 -66
  50. package/src/util/no-cache.md +0 -66
@@ -47,9 +47,7 @@ describe("MemoryCache", () => {
47
47
  const fakeHandler: IRequestHandler<string, string> = {
48
48
  getKey: () => "MY_KEY",
49
49
  type: "MY_HANDLER",
50
- shouldRefreshCache: () => false,
51
50
  fulfillRequest: jest.fn(),
52
- cache: null,
53
51
  hydrate: true,
54
52
  };
55
53
 
@@ -71,9 +69,7 @@ describe("MemoryCache", () => {
71
69
  const fakeHandler: IRequestHandler<string, string> = {
72
70
  getKey: () => "MY_KEY",
73
71
  type: "MY_HANDLER",
74
- shouldRefreshCache: () => false,
75
72
  fulfillRequest: jest.fn(),
76
- cache: null,
77
73
  hydrate: true,
78
74
  };
79
75
 
@@ -95,9 +91,7 @@ describe("MemoryCache", () => {
95
91
  const fakeHandler: IRequestHandler<string, string> = {
96
92
  getKey: () => "MY_KEY",
97
93
  type: "MY_HANDLER",
98
- shouldRefreshCache: () => false,
99
94
  fulfillRequest: jest.fn(),
100
- cache: null,
101
95
  hydrate: true,
102
96
  };
103
97
 
@@ -119,9 +113,7 @@ describe("MemoryCache", () => {
119
113
  const fakeHandler: IRequestHandler<string, string> = {
120
114
  getKey: () => "MY_KEY",
121
115
  type: "MY_HANDLER",
122
- shouldRefreshCache: () => false,
123
116
  fulfillRequest: jest.fn(),
124
- cache: null,
125
117
  hydrate: true,
126
118
  };
127
119
 
@@ -142,9 +134,7 @@ describe("MemoryCache", () => {
142
134
  const fakeHandler: IRequestHandler<string, string> = {
143
135
  getKey: () => "MY_KEY",
144
136
  type: "MY_HANDLER",
145
- shouldRefreshCache: () => false,
146
137
  fulfillRequest: jest.fn(),
147
- cache: null,
148
138
  hydrate: true,
149
139
  };
150
140
 
@@ -165,9 +155,7 @@ describe("MemoryCache", () => {
165
155
  const fakeHandler: IRequestHandler<string, string> = {
166
156
  getKey: () => "MY_KEY",
167
157
  type: "MY_HANDLER",
168
- shouldRefreshCache: () => false,
169
158
  fulfillRequest: jest.fn(),
170
- cache: null,
171
159
  hydrate: true,
172
160
  };
173
161
 
@@ -186,9 +174,7 @@ describe("MemoryCache", () => {
186
174
  const fakeHandler: IRequestHandler<string, string> = {
187
175
  getKey: () => "MY_KEY",
188
176
  type: "MY_HANDLER",
189
- shouldRefreshCache: () => false,
190
177
  fulfillRequest: jest.fn(),
191
- cache: null,
192
178
  hydrate: true,
193
179
  };
194
180
 
@@ -207,9 +193,7 @@ describe("MemoryCache", () => {
207
193
  const fakeHandler: IRequestHandler<string, string> = {
208
194
  getKey: () => "MY_KEY",
209
195
  type: "MY_HANDLER",
210
- shouldRefreshCache: () => false,
211
196
  fulfillRequest: jest.fn(),
212
- cache: null,
213
197
  hydrate: true,
214
198
  };
215
199
 
@@ -230,9 +214,7 @@ describe("MemoryCache", () => {
230
214
  const fakeHandler: IRequestHandler<string, string> = {
231
215
  getKey: () => "MY_KEY",
232
216
  type: "MY_HANDLER",
233
- shouldRefreshCache: () => false,
234
217
  fulfillRequest: jest.fn(),
235
- cache: null,
236
218
  hydrate: true,
237
219
  };
238
220
 
@@ -253,9 +235,7 @@ describe("MemoryCache", () => {
253
235
  const fakeHandler: IRequestHandler<string, string> = {
254
236
  getKey: () => "MY_KEY",
255
237
  type: "MY_HANDLER",
256
- shouldRefreshCache: () => false,
257
238
  fulfillRequest: jest.fn(),
258
- cache: null,
259
239
  hydrate: true,
260
240
  };
261
241
 
@@ -269,6 +249,23 @@ describe("MemoryCache", () => {
269
249
  });
270
250
 
271
251
  describe("#removeAll", () => {
252
+ it("should return 0 if the handler subcache is absent", () => {
253
+ // Arrange
254
+ const cache = new MemoryCache();
255
+ const fakeHandler: IRequestHandler<string, string> = {
256
+ getKey: () => "MY_KEY",
257
+ type: "MY_HANDLER",
258
+ fulfillRequest: jest.fn(),
259
+ hydrate: true,
260
+ };
261
+
262
+ // Act
263
+ const result = cache.removeAll(fakeHandler);
264
+
265
+ // Assert
266
+ expect(result).toBe(0);
267
+ });
268
+
272
269
  it("should remove matching entries from handler subcache", () => {
273
270
  const cache = new MemoryCache({
274
271
  MY_HANDLER: {
@@ -283,32 +280,84 @@ describe("MemoryCache", () => {
283
280
  const fakeHandler: IRequestHandler<string, string> = {
284
281
  getKey: () => "MY_KEY",
285
282
  type: "MY_HANDLER",
286
- shouldRefreshCache: () => false,
287
283
  fulfillRequest: jest.fn(),
288
- cache: null,
284
+ hydrate: true,
285
+ };
286
+
287
+ // Act
288
+ cache.removeAll(fakeHandler, (k, d) => d.data === "2");
289
+ const result = cache.cloneData();
290
+
291
+ // Assert
292
+ expect(result).toStrictEqual({
293
+ MY_HANDLER: {
294
+ MY_KEY2: {data: "1"},
295
+ },
296
+ OTHER_HANDLER: {
297
+ MY_KEY: {data: "1"},
298
+ },
299
+ });
300
+ });
301
+
302
+ it("should return the number of items that matched the predicate and were removed", () => {
303
+ const cache = new MemoryCache({
304
+ MY_HANDLER: {
305
+ MY_KEY: {data: "a"},
306
+ MY_KEY2: {data: "b"},
307
+ MY_KEY3: {data: "a"},
308
+ },
309
+ OTHER_HANDLER: {
310
+ MY_KEY: {data: "b"},
311
+ },
312
+ });
313
+ const fakeHandler: IRequestHandler<string, string> = {
314
+ getKey: () => "MY_KEY",
315
+ type: "MY_HANDLER",
316
+ fulfillRequest: jest.fn(),
289
317
  hydrate: true,
290
318
  };
291
319
 
292
320
  // Act
293
321
  const result = cache.removeAll(
294
322
  fakeHandler,
295
- (k, d) => d.data === "2",
323
+ (k, d) => d.data === "a",
296
324
  );
297
- const after = cache.cloneData();
298
325
 
299
326
  // Assert
300
327
  expect(result).toBe(2);
301
- expect(after).toStrictEqual({
328
+ });
329
+
330
+ it("should remove the entire handler subcache if no predicate", () => {
331
+ const cache = new MemoryCache({
302
332
  MY_HANDLER: {
303
- MY_KEY2: {data: "1"},
333
+ MY_KEY: {data: "data!"},
334
+ MY_KEY2: {data: "data!"},
335
+ MY_KEY3: {data: "data!"},
304
336
  },
305
337
  OTHER_HANDLER: {
306
- MY_KEY: {data: "1"},
338
+ MY_KEY: {data: "data!"},
339
+ },
340
+ });
341
+ const fakeHandler: IRequestHandler<string, string> = {
342
+ getKey: () => "MY_KEY",
343
+ type: "MY_HANDLER",
344
+ fulfillRequest: jest.fn(),
345
+ hydrate: true,
346
+ };
347
+
348
+ // Act
349
+ cache.removeAll(fakeHandler);
350
+ const result = cache.cloneData();
351
+
352
+ // Assert
353
+ expect(result).toStrictEqual({
354
+ OTHER_HANDLER: {
355
+ MY_KEY: {data: "data!"},
307
356
  },
308
357
  });
309
358
  });
310
359
 
311
- it("should remove all entries from handler subcache if no predicate", () => {
360
+ it("should return the number of items that were in the handler subcache if there was no predicate", () => {
312
361
  const cache = new MemoryCache({
313
362
  MY_HANDLER: {
314
363
  MY_KEY: {data: "data!"},
@@ -322,26 +371,76 @@ describe("MemoryCache", () => {
322
371
  const fakeHandler: IRequestHandler<string, string> = {
323
372
  getKey: () => "MY_KEY",
324
373
  type: "MY_HANDLER",
325
- shouldRefreshCache: () => false,
326
374
  fulfillRequest: jest.fn(),
327
- cache: null,
328
375
  hydrate: true,
329
376
  };
330
377
 
331
378
  // Act
332
379
  const result = cache.removeAll(fakeHandler);
333
- const after = cache.cloneData();
334
380
 
335
381
  // Assert
336
382
  expect(result).toBe(3);
337
- expect(after).toStrictEqual({
338
- MY_HANDLER: {},
339
- OTHER_HANDLER: {
383
+ });
384
+ });
385
+
386
+ describe("#cloneData", () => {
387
+ it("should return a copy of the cache data", () => {});
388
+
389
+ it("should throw if there is an error during cloning", () => {
390
+ // Arrange
391
+ const cache = new MemoryCache({
392
+ MY_HANDLER: {
340
393
  MY_KEY: {data: "data!"},
341
394
  },
342
395
  });
396
+ jest.spyOn(JSON, "stringify").mockImplementation(() => {
397
+ throw new Error("BANG!");
398
+ });
399
+
400
+ // Act
401
+ const act = () => cache.cloneData();
402
+
403
+ // Assert
404
+ expect(act).toThrowErrorMatchingInlineSnapshot(
405
+ `"An error occurred while trying to clone the cache: Error: BANG!"`,
406
+ );
343
407
  });
344
408
  });
345
409
 
346
- describe("#cloneData", () => {});
410
+ describe("@inUse", () => {
411
+ it("should return true if the cache contains data", () => {
412
+ // Arrange
413
+ const cache = new MemoryCache({
414
+ MY_HANDLER: {
415
+ MY_KEY: {data: "data!"},
416
+ },
417
+ });
418
+
419
+ // Act
420
+ const result = cache.inUse;
421
+
422
+ // Assert
423
+ expect(result).toBeTruthy();
424
+ });
425
+
426
+ it("should return false if the cache is empty", () => {
427
+ // Arrange
428
+ const cache = new MemoryCache({
429
+ MY_HANDLER: {
430
+ MY_KEY: {data: "data!"},
431
+ },
432
+ });
433
+ cache.removeAll(
434
+ ({
435
+ type: "MY_HANDLER",
436
+ }: any),
437
+ );
438
+
439
+ // Act
440
+ const result = cache.inUse;
441
+
442
+ // Assert
443
+ expect(result).toBeFalsy();
444
+ });
445
+ });
347
446
  });
@@ -17,32 +17,30 @@ describe("RequestFulfillment", () => {
17
17
  });
18
18
 
19
19
  describe("#fulfill", () => {
20
- it("should cache errors caused directly by handlers", async () => {
20
+ it("should attempt to cache errors caused directly by handlers", async () => {
21
21
  // Arrange
22
22
  const responseCache = new ResponseCache();
23
23
  const requestFulfillment = new RequestFulfillment(responseCache);
24
+ const error = new Error("OH NO!");
24
25
  const fakeBadHandler: IRequestHandler<any, any> = {
25
26
  fulfillRequest: () => {
26
- throw new Error("OH NO!");
27
+ throw error;
27
28
  },
28
29
  getKey: jest.fn().mockReturnValue("MY_KEY"),
29
- shouldRefreshCache: () => false,
30
30
  type: "MY_TYPE",
31
- cache: null,
32
31
  hydrate: true,
33
32
  };
33
+ const cacheErrorSpy = jest.spyOn(responseCache, "cacheError");
34
34
 
35
35
  // Act
36
36
  await requestFulfillment.fulfill(fakeBadHandler, "OPTIONS");
37
37
 
38
38
  // Assert
39
- expect(responseCache.cloneHydratableData()).toStrictEqual({
40
- MY_TYPE: {
41
- MY_KEY: {
42
- error: "OH NO!",
43
- },
44
- },
45
- });
39
+ expect(cacheErrorSpy).toHaveBeenCalledWith(
40
+ fakeBadHandler,
41
+ "OPTIONS",
42
+ error,
43
+ );
46
44
  });
47
45
 
48
46
  it("should cache errors occurring in promises", async () => {
@@ -53,23 +51,20 @@ describe("RequestFulfillment", () => {
53
51
  fulfillRequest: () =>
54
52
  new Promise((resolve, reject) => reject("OH NO!")),
55
53
  getKey: (o) => o,
56
- shouldRefreshCache: () => false,
57
54
  type: "BAD_REQUEST",
58
- cache: null,
59
55
  hydrate: true,
60
56
  };
57
+ const cacheErrorSpy = jest.spyOn(responseCache, "cacheError");
61
58
 
62
59
  // Act
63
60
  await requestFulfillment.fulfill(fakeBadRequestHandler, "OPTIONS");
64
61
 
65
62
  // Assert
66
- expect(responseCache.cloneHydratableData()).toStrictEqual({
67
- BAD_REQUEST: {
68
- OPTIONS: {
69
- error: "OH NO!",
70
- },
71
- },
72
- });
63
+ expect(cacheErrorSpy).toHaveBeenCalledWith(
64
+ fakeBadRequestHandler,
65
+ "OPTIONS",
66
+ "OH NO!",
67
+ );
73
68
  });
74
69
 
75
70
  it("should cache data from requests", async () => {
@@ -79,24 +74,20 @@ describe("RequestFulfillment", () => {
79
74
  const fakeRequestHandler: IRequestHandler<string, any> = {
80
75
  fulfillRequest: () => Promise.resolve("DATA!"),
81
76
  getKey: (o) => o,
82
- shouldRefreshCache: () => false,
83
77
  type: "VALID_REQUEST",
84
- cache: null,
85
78
  hydrate: true,
86
79
  };
80
+ const cacheDataSpy = jest.spyOn(responseCache, "cacheData");
87
81
 
88
82
  // Act
89
83
  await requestFulfillment.fulfill(fakeRequestHandler, "OPTIONS");
90
- const result = responseCache.cloneHydratableData();
91
84
 
92
85
  // Assert
93
- expect(result).toStrictEqual({
94
- VALID_REQUEST: {
95
- OPTIONS: {
96
- data: "DATA!",
97
- },
98
- },
99
- });
86
+ expect(cacheDataSpy).toHaveBeenCalledWith(
87
+ fakeRequestHandler,
88
+ "OPTIONS",
89
+ "DATA!",
90
+ );
100
91
  });
101
92
 
102
93
  it("should return a promise of the result", async () => {
@@ -106,9 +97,7 @@ describe("RequestFulfillment", () => {
106
97
  const fakeRequestHandler: IRequestHandler<string, any> = {
107
98
  fulfillRequest: () => Promise.resolve("DATA!"),
108
99
  getKey: (o) => o,
109
- shouldRefreshCache: () => false,
110
100
  type: "VALID_REQUEST",
111
- cache: null,
112
101
  hydrate: true,
113
102
  };
114
103
 
@@ -131,9 +120,7 @@ describe("RequestFulfillment", () => {
131
120
  const fakeRequestHandler: IRequestHandler<string, any> = {
132
121
  fulfillRequest: () => Promise.resolve("DATA!"),
133
122
  getKey: (o) => o,
134
- shouldRefreshCache: () => false,
135
123
  type: "VALID_REQUEST",
136
- cache: null,
137
124
  hydrate: true,
138
125
  };
139
126
 
@@ -158,9 +145,7 @@ describe("RequestFulfillment", () => {
158
145
  const fakeRequestHandler: IRequestHandler<string, any> = {
159
146
  fulfillRequest: () => Promise.resolve("DATA!"),
160
147
  getKey: (o) => o,
161
- shouldRefreshCache: () => false,
162
148
  type: "VALID_REQUEST",
163
- cache: null,
164
149
  hydrate: false,
165
150
  };
166
151
 
@@ -14,7 +14,7 @@ describe("../request-handler.js", () => {
14
14
  jest.restoreAllMocks();
15
15
  });
16
16
 
17
- describe("#get type", () => {
17
+ describe("@type", () => {
18
18
  it("should return value passed in construction", () => {
19
19
  // Arrange
20
20
  const handler = new RequestHandler("MY_TYPE");
@@ -27,80 +27,80 @@ describe("../request-handler.js", () => {
27
27
  });
28
28
  });
29
29
 
30
- describe("#getKey", () => {
31
- it("should return a key for given options", () => {
30
+ describe("@hydrate", () => {
31
+ it("should return true when constructed with false", () => {
32
32
  // Arrange
33
- const handler = new RequestHandler("MY_TYPE");
33
+ const handler = new RequestHandler("MY_TYPE", false);
34
34
 
35
35
  // Act
36
- const result = handler.getKey({some: "options"});
36
+ const result = handler.hydrate;
37
37
 
38
38
  // Assert
39
- expect(result).toMatchInlineSnapshot(
40
- `"{\\"some\\":\\"options\\"}"`,
41
- );
39
+ expect(result).toBeFalse();
42
40
  });
43
41
 
44
- it("should return a key for undefined options", () => {
42
+ it("should return true when constructed with true", () => {
45
43
  // Arrange
46
- const handler = new RequestHandler("MY_TYPE");
44
+ const handler = new RequestHandler("MY_TYPE", true);
47
45
 
48
46
  // Act
49
- const result = handler.getKey(undefined);
47
+ const result = handler.hydrate;
50
48
 
51
49
  // Assert
52
- expect(result).toMatchInlineSnapshot(`"undefined"`);
50
+ expect(result).toBeTrue();
53
51
  });
54
52
 
55
- it("should throw if JSON.stringify fails", () => {
53
+ it("should return true by default", () => {
56
54
  // Arrange
57
55
  const handler = new RequestHandler("MY_TYPE");
58
- jest.spyOn(JSON, "stringify").mockImplementation(() => {
59
- throw new Error("OH NOES!");
60
- });
61
56
 
62
57
  // Act
63
- const underTest = () => handler.getKey({});
58
+ const result = handler.hydrate;
64
59
 
65
60
  // Assert
66
- expect(underTest).toThrowErrorMatchingInlineSnapshot(
67
- `"Failed to auto-generate key: Error: OH NOES!"`,
68
- );
61
+ expect(result).toBeTrue();
69
62
  });
70
63
  });
71
64
 
72
- describe("#shouldRefreshCache", () => {
73
- it("should return true if no current cached entry", () => {
65
+ describe("#getKey", () => {
66
+ it("should return a key for given options", () => {
74
67
  // Arrange
75
68
  const handler = new RequestHandler("MY_TYPE");
76
69
 
77
70
  // Act
78
- const result = handler.shouldRefreshCache({}, null);
71
+ const result = handler.getKey({some: "options"});
79
72
 
80
73
  // Assert
81
- expect(result).toBeTruthy();
74
+ expect(result).toMatchInlineSnapshot(
75
+ `"{\\"some\\":\\"options\\"}"`,
76
+ );
82
77
  });
83
78
 
84
- it("should return true if cached entry has error", () => {
79
+ it("should return a key for undefined options", () => {
85
80
  // Arrange
86
81
  const handler = new RequestHandler("MY_TYPE");
87
82
 
88
83
  // Act
89
- const result = handler.shouldRefreshCache({}, {error: "oops!"});
84
+ const result = handler.getKey(undefined);
90
85
 
91
86
  // Assert
92
- expect(result).toBeTruthy();
87
+ expect(result).toMatchInlineSnapshot(`"undefined"`);
93
88
  });
94
89
 
95
- it("should return false if cached entry is data", () => {
90
+ it("should throw if JSON.stringify fails", () => {
96
91
  // Arrange
97
92
  const handler = new RequestHandler("MY_TYPE");
93
+ jest.spyOn(JSON, "stringify").mockImplementation(() => {
94
+ throw new Error("OH NOES!");
95
+ });
98
96
 
99
97
  // Act
100
- const result = handler.shouldRefreshCache({}, {data: "yay! data"});
98
+ const underTest = () => handler.getKey({});
101
99
 
102
100
  // Assert
103
- expect(result).toBeFalsy();
101
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
102
+ `"Failed to auto-generate key: Error: OH NOES!"`,
103
+ );
104
104
  });
105
105
  });
106
106