@khanacademy/wonder-blocks-data 3.1.1 → 4.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 (46) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/es/index.js +375 -335
  3. package/dist/index.js +527 -461
  4. package/docs.md +17 -35
  5. package/package.json +3 -3
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +7 -46
  7. package/src/__tests__/generated-snapshot.test.js +56 -122
  8. package/src/components/__tests__/data.test.js +372 -297
  9. package/src/components/__tests__/intercept-data.test.js +6 -30
  10. package/src/components/data.js +153 -21
  11. package/src/components/data.md +38 -69
  12. package/src/components/gql-router.js +1 -1
  13. package/src/components/intercept-context.js +6 -2
  14. package/src/components/intercept-data.js +40 -51
  15. package/src/components/intercept-data.md +13 -27
  16. package/src/components/track-data.md +9 -23
  17. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +17 -0
  18. package/src/hooks/__tests__/use-gql.test.js +1 -0
  19. package/src/hooks/__tests__/use-server-effect.test.js +217 -0
  20. package/src/hooks/__tests__/use-shared-cache.test.js +307 -0
  21. package/src/hooks/use-gql.js +39 -31
  22. package/src/hooks/use-server-effect.js +45 -0
  23. package/src/hooks/use-shared-cache.js +106 -0
  24. package/src/index.js +15 -19
  25. package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.js.snap +19 -0
  26. package/src/util/__tests__/request-fulfillment.test.js +42 -85
  27. package/src/util/__tests__/request-tracking.test.js +72 -191
  28. package/src/util/__tests__/{result-from-cache-entry.test.js → result-from-cache-response.test.js} +9 -10
  29. package/src/util/__tests__/scoped-in-memory-cache.test.js +396 -0
  30. package/src/util/__tests__/ssr-cache.test.js +639 -0
  31. package/src/util/gql-types.js +5 -10
  32. package/src/util/request-fulfillment.js +36 -44
  33. package/src/util/request-tracking.js +62 -75
  34. package/src/util/{result-from-cache-entry.js → result-from-cache-response.js} +10 -13
  35. package/src/util/scoped-in-memory-cache.js +149 -0
  36. package/src/util/ssr-cache.js +206 -0
  37. package/src/util/types.js +43 -108
  38. package/src/hooks/__tests__/use-data.test.js +0 -826
  39. package/src/hooks/use-data.js +0 -143
  40. package/src/util/__tests__/memory-cache.test.js +0 -446
  41. package/src/util/__tests__/request-handler.test.js +0 -121
  42. package/src/util/__tests__/response-cache.test.js +0 -879
  43. package/src/util/memory-cache.js +0 -187
  44. package/src/util/request-handler.js +0 -42
  45. package/src/util/request-handler.md +0 -51
  46. package/src/util/response-cache.js +0 -213
@@ -1,143 +0,0 @@
1
- // @flow
2
- import {Server} from "@khanacademy/wonder-blocks-core";
3
- import {useState, useEffect, useContext, useRef} from "react";
4
- import {RequestFulfillment} from "../util/request-fulfillment.js";
5
- import InterceptContext from "../components/intercept-context.js";
6
- import {TrackerContext} from "../util/request-tracking.js";
7
- import {resultFromCacheEntry} from "../util/result-from-cache-entry.js";
8
- import {ResponseCache} from "../util/response-cache.js";
9
-
10
- import type {
11
- Result,
12
- IRequestHandler,
13
- ValidData,
14
- CacheEntry,
15
- } from "../util/types.js";
16
-
17
- export const useData = <TOptions, TData: ValidData>(
18
- handler: IRequestHandler<TOptions, TData>,
19
- options: TOptions,
20
- ): Result<TData> => {
21
- // If we're server-side or hydrating, we'll have a cached entry to use.
22
- // So we get that and use it to initialize our state.
23
- // This works in both hydration and SSR because the very first call to
24
- // this will have cached data in those cases as it will be present on the
25
- // initial render - and subsequent renders on the client it will be null.
26
- const cachedResult = ResponseCache.Default.getEntry<TOptions, TData>(
27
- handler,
28
- options,
29
- );
30
- const [result, setResult] = useState<?CacheEntry<TData>>(cachedResult);
31
-
32
- // Lookup to see if there's an interceptor for the handler.
33
- // If we have one, we need to replace the handler with one that
34
- // uses the interceptor.
35
- const interceptorMap = useContext(InterceptContext);
36
- const interceptor = interceptorMap[handler.type];
37
-
38
- // If we have an interceptor, we need to replace the handler with one that
39
- // uses the interceptor. This helper function generates a new handler.
40
- // We need this before we track the request as we want the interceptor
41
- // to also work for tracked requests to simplify testing the server-side
42
- // request fulfillment.
43
- const getMaybeInterceptedHandler = () => {
44
- if (interceptor == null) {
45
- return handler;
46
- }
47
-
48
- const fulfillRequestFn = (options) =>
49
- interceptor.fulfillRequest(options) ??
50
- handler.fulfillRequest(options);
51
- return {
52
- fulfillRequest: fulfillRequestFn,
53
- getKey: (options) => handler.getKey(options),
54
- type: handler.type,
55
- hydrate: handler.hydrate,
56
- };
57
- };
58
-
59
- // We only track data requests when we are server-side and we don't
60
- // already have a result, as given by the cachedData (which is also the
61
- // initial value for the result state).
62
- const maybeTrack = useContext(TrackerContext);
63
- if (result == null && Server.isServerSide()) {
64
- maybeTrack?.(getMaybeInterceptedHandler(), options);
65
- }
66
-
67
- // We need to update our request when the handler changes or the key
68
- // to the options change, so we keep track of those.
69
- // However, even if we are hydrating from cache, we still need to make the
70
- // request at least once, so we do not initialize these references.
71
- const handlerRef = useRef();
72
- const keyRef = useRef();
73
- const interceptorRef = useRef();
74
-
75
- // This effect will ensure that we fulfill the request as desired.
76
- useEffect(() => {
77
- // If we are server-side, then just skip the effect. We track requests
78
- // during SSR and fulfill them outside of the React render cycle.
79
- // NOTE: This shouldn't happen since effects would not run on the server
80
- // but let's be defensive - I think it makes the code clearer.
81
- /* istanbul ignore next */
82
- if (Server.isServerSide()) {
83
- return;
84
- }
85
-
86
- // Update our refs to the current handler and key.
87
- handlerRef.current = handler;
88
- keyRef.current = handler.getKey(options);
89
- interceptorRef.current = interceptor;
90
-
91
- // If we're not hydrating a result, we want to make sure we set our
92
- // result to null so that we're in the loading state.
93
- if (cachedResult == null) {
94
- // Mark ourselves as loading.
95
- setResult(null);
96
- }
97
-
98
- // We aren't server-side, so let's make the request.
99
- // The request handler is in control of whether that request actually
100
- // happens or not.
101
- let cancel = false;
102
- RequestFulfillment.Default.fulfill(
103
- getMaybeInterceptedHandler(),
104
- options,
105
- )
106
- .then((updateEntry) => {
107
- if (cancel) {
108
- return;
109
- }
110
- setResult(updateEntry);
111
- return;
112
- })
113
- .catch((e) => {
114
- if (cancel) {
115
- return;
116
- }
117
- /**
118
- * We should never get here as errors in fulfillment are part
119
- * of the `then`, but if we do.
120
- */
121
- // eslint-disable-next-line no-console
122
- console.error(
123
- `Unexpected error occurred during data fulfillment: ${e}`,
124
- );
125
- setResult({
126
- data: null,
127
- error: typeof e === "string" ? e : e.message,
128
- });
129
- return;
130
- });
131
-
132
- return () => {
133
- cancel = true;
134
- };
135
- // - handler.getKey is a proxy for options
136
- // - We don't want to trigger on cachedResult changing, we're
137
- // just using that as a flag for render state if the other things
138
- // trigger this effect.
139
- // eslint-disable-next-line react-hooks/exhaustive-deps
140
- }, [handler, handler.getKey(options), interceptor]);
141
-
142
- return resultFromCacheEntry(result);
143
- };
@@ -1,446 +0,0 @@
1
- // @flow
2
- import MemoryCache from "../memory-cache.js";
3
-
4
- import type {IRequestHandler} from "../types.js";
5
-
6
- describe("MemoryCache", () => {
7
- afterEach(() => {
8
- /**
9
- * This is needed or the JSON.stringify mocks need to be
10
- * mockImplementationOnce. This is because if the snapshots need
11
- * to update, they write the inline snapshot and that appears to invoke
12
- * prettier which in turn, calls JSON.stringify. And if that mock
13
- * throws, then boom. No snapshot update and a big old confusing test
14
- * failure.
15
- */
16
- jest.restoreAllMocks();
17
- });
18
-
19
- describe("#constructor", () => {
20
- it("should throw if the cloning fails", () => {
21
- // Arrange
22
- jest.spyOn(JSON, "stringify").mockImplementation(() => {
23
- throw new Error("BANG!");
24
- });
25
-
26
- // Act
27
- const underTest = () =>
28
- new MemoryCache({
29
- BAD: {
30
- BAD: {data: "FOOD"},
31
- },
32
- });
33
-
34
- // Assert
35
- expect(underTest).toThrowErrorMatchingInlineSnapshot(
36
- `"An error occurred trying to initialize from a response cache snapshot: Error: BANG!"`,
37
- );
38
- });
39
-
40
- it("should deep clone the passed source data", () => {
41
- // Arrange
42
- const sourceData = {
43
- MY_HANDLER: {
44
- MY_KEY: {data: "THE_DATA"},
45
- },
46
- };
47
- const fakeHandler: IRequestHandler<string, string> = {
48
- getKey: () => "MY_KEY",
49
- type: "MY_HANDLER",
50
- fulfillRequest: jest.fn(),
51
- hydrate: true,
52
- };
53
-
54
- // Act
55
- const cache = new MemoryCache(sourceData);
56
- // Try to mutate the cache.
57
- sourceData["MY_HANDLER"]["MY_KEY"] = {data: "SOME_NEW_DATA"};
58
- const result = cache.retrieve(fakeHandler, "options");
59
-
60
- // Assert
61
- expect(result).toStrictEqual({data: "THE_DATA"});
62
- });
63
- });
64
-
65
- describe("#store", () => {
66
- it("should store the entry in the cache", () => {
67
- // Arrange
68
- const cache = new MemoryCache();
69
- const fakeHandler: IRequestHandler<string, string> = {
70
- getKey: () => "MY_KEY",
71
- type: "MY_HANDLER",
72
- fulfillRequest: jest.fn(),
73
- hydrate: true,
74
- };
75
-
76
- // Act
77
- cache.store<string, string>(fakeHandler, "options", {data: "data"});
78
- const result = cache.retrieve(fakeHandler, "options");
79
-
80
- // Assert
81
- expect(result).toStrictEqual({data: "data"});
82
- });
83
-
84
- it("should replace the entry in the handler subcache", () => {
85
- // Arrange
86
- const cache = new MemoryCache({
87
- MY_HANDLER: {
88
- MY_KEY: {error: "Oh no!"},
89
- },
90
- });
91
- const fakeHandler: IRequestHandler<string, string> = {
92
- getKey: () => "MY_KEY",
93
- type: "MY_HANDLER",
94
- fulfillRequest: jest.fn(),
95
- hydrate: true,
96
- };
97
-
98
- // Act
99
- cache.store<string, string>(fakeHandler, "options", {
100
- data: "other_data",
101
- });
102
- const result = cache.retrieve(fakeHandler, "options");
103
-
104
- // Assert
105
- expect(result).toStrictEqual({data: "other_data"});
106
- });
107
- });
108
-
109
- describe("#retrieve", () => {
110
- it("should return null if the handler subcache is absent", () => {
111
- // Arrange
112
- const cache = new MemoryCache();
113
- const fakeHandler: IRequestHandler<string, string> = {
114
- getKey: () => "MY_KEY",
115
- type: "MY_HANDLER",
116
- fulfillRequest: jest.fn(),
117
- hydrate: true,
118
- };
119
-
120
- // Act
121
- const result = cache.retrieve(fakeHandler, "options");
122
-
123
- // Assert
124
- expect(result).toBeNull();
125
- });
126
-
127
- it("should return null if the request key is absent from the subcache", () => {
128
- // Arrange
129
- const cache = new MemoryCache({
130
- MY_HANDLER: {
131
- SOME_OTHER_KEY: {data: "data we don't want"},
132
- },
133
- });
134
- const fakeHandler: IRequestHandler<string, string> = {
135
- getKey: () => "MY_KEY",
136
- type: "MY_HANDLER",
137
- fulfillRequest: jest.fn(),
138
- hydrate: true,
139
- };
140
-
141
- // Act
142
- const result = cache.retrieve(fakeHandler, "options");
143
-
144
- // Assert
145
- expect(result).toBeNull();
146
- });
147
-
148
- it("should return the entry if it exists", () => {
149
- // Arrange
150
- const cache = new MemoryCache({
151
- MY_HANDLER: {
152
- MY_KEY: {data: "data!"},
153
- },
154
- });
155
- const fakeHandler: IRequestHandler<string, string> = {
156
- getKey: () => "MY_KEY",
157
- type: "MY_HANDLER",
158
- fulfillRequest: jest.fn(),
159
- hydrate: true,
160
- };
161
-
162
- // Act
163
- const result = cache.retrieve(fakeHandler, "options");
164
-
165
- // Assert
166
- expect(result).toStrictEqual({data: "data!"});
167
- });
168
- });
169
-
170
- describe("#remove", () => {
171
- it("should return false if the handler subcache does not exist", () => {
172
- // Arrange
173
- const cache = new MemoryCache();
174
- const fakeHandler: IRequestHandler<string, string> = {
175
- getKey: () => "MY_KEY",
176
- type: "MY_HANDLER",
177
- fulfillRequest: jest.fn(),
178
- hydrate: true,
179
- };
180
-
181
- // Act
182
- const result = cache.remove(fakeHandler, "options");
183
-
184
- // Assert
185
- expect(result).toBeFalsy();
186
- });
187
-
188
- it("should return false if the item does not exist in the subcache", () => {
189
- // Arrange
190
- const cache = new MemoryCache({
191
- MY_HANDLER: {},
192
- });
193
- const fakeHandler: IRequestHandler<string, string> = {
194
- getKey: () => "MY_KEY",
195
- type: "MY_HANDLER",
196
- fulfillRequest: jest.fn(),
197
- hydrate: true,
198
- };
199
-
200
- // Act
201
- const result = cache.remove(fakeHandler, "options");
202
-
203
- // Assert
204
- expect(result).toBeFalsy();
205
- });
206
-
207
- it("should return true if the entry was removed", () => {
208
- // Arrange
209
- const cache = new MemoryCache({
210
- MY_HANDLER: {
211
- MY_KEY: {data: "data!"},
212
- },
213
- });
214
- const fakeHandler: IRequestHandler<string, string> = {
215
- getKey: () => "MY_KEY",
216
- type: "MY_HANDLER",
217
- fulfillRequest: jest.fn(),
218
- hydrate: true,
219
- };
220
-
221
- // Act
222
- const result = cache.remove(fakeHandler, "options");
223
-
224
- // Assert
225
- expect(result).toBeTruthy();
226
- });
227
-
228
- it("should remove the entry", () => {
229
- // Arrange
230
- const cache = new MemoryCache({
231
- MY_HANDLER: {
232
- MY_KEY: {data: "data!"},
233
- },
234
- });
235
- const fakeHandler: IRequestHandler<string, string> = {
236
- getKey: () => "MY_KEY",
237
- type: "MY_HANDLER",
238
- fulfillRequest: jest.fn(),
239
- hydrate: true,
240
- };
241
-
242
- // Act
243
- cache.remove(fakeHandler, "options");
244
- const result = cache.retrieve(fakeHandler, "options");
245
-
246
- // Assert
247
- expect(result).toBeNull();
248
- });
249
- });
250
-
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
-
269
- it("should remove matching entries from handler subcache", () => {
270
- const cache = new MemoryCache({
271
- MY_HANDLER: {
272
- MY_KEY: {data: "2"},
273
- MY_KEY2: {data: "1"},
274
- MY_KEY3: {data: "2"},
275
- },
276
- OTHER_HANDLER: {
277
- MY_KEY: {data: "1"},
278
- },
279
- });
280
- const fakeHandler: IRequestHandler<string, string> = {
281
- getKey: () => "MY_KEY",
282
- type: "MY_HANDLER",
283
- fulfillRequest: jest.fn(),
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(),
317
- hydrate: true,
318
- };
319
-
320
- // Act
321
- const result = cache.removeAll(
322
- fakeHandler,
323
- (k, d) => d.data === "a",
324
- );
325
-
326
- // Assert
327
- expect(result).toBe(2);
328
- });
329
-
330
- it("should remove the entire handler subcache if no predicate", () => {
331
- const cache = new MemoryCache({
332
- MY_HANDLER: {
333
- MY_KEY: {data: "data!"},
334
- MY_KEY2: {data: "data!"},
335
- MY_KEY3: {data: "data!"},
336
- },
337
- OTHER_HANDLER: {
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!"},
356
- },
357
- });
358
- });
359
-
360
- it("should return the number of items that were in the handler subcache if there was no predicate", () => {
361
- const cache = new MemoryCache({
362
- MY_HANDLER: {
363
- MY_KEY: {data: "data!"},
364
- MY_KEY2: {data: "data!"},
365
- MY_KEY3: {data: "data!"},
366
- },
367
- OTHER_HANDLER: {
368
- MY_KEY: {data: "data!"},
369
- },
370
- });
371
- const fakeHandler: IRequestHandler<string, string> = {
372
- getKey: () => "MY_KEY",
373
- type: "MY_HANDLER",
374
- fulfillRequest: jest.fn(),
375
- hydrate: true,
376
- };
377
-
378
- // Act
379
- const result = cache.removeAll(fakeHandler);
380
-
381
- // Assert
382
- expect(result).toBe(3);
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: {
393
- MY_KEY: {data: "data!"},
394
- },
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
- );
407
- });
408
- });
409
-
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
- });
446
- });