@khanacademy/wonder-blocks-data 3.1.3 → 5.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/es/index.js +408 -349
  3. package/dist/index.js +599 -494
  4. package/docs.md +17 -35
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +7 -46
  7. package/src/__tests__/generated-snapshot.test.js +60 -126
  8. package/src/components/__tests__/data.test.js +373 -313
  9. package/src/components/__tests__/intercept-requests.test.js +58 -0
  10. package/src/components/data.js +139 -21
  11. package/src/components/data.md +38 -69
  12. package/src/components/intercept-context.js +6 -3
  13. package/src/components/intercept-requests.js +69 -0
  14. package/src/components/intercept-requests.md +54 -0
  15. package/src/components/track-data.md +9 -23
  16. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +17 -0
  17. package/src/hooks/__tests__/use-gql.test.js +1 -0
  18. package/src/hooks/__tests__/use-request-interception.test.js +255 -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 +36 -23
  22. package/src/hooks/use-request-interception.js +54 -0
  23. package/src/hooks/use-server-effect.js +45 -0
  24. package/src/hooks/use-shared-cache.js +106 -0
  25. package/src/index.js +18 -20
  26. package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.js.snap +19 -0
  27. package/src/util/__tests__/request-fulfillment.test.js +42 -85
  28. package/src/util/__tests__/request-tracking.test.js +72 -191
  29. package/src/util/__tests__/{result-from-cache-entry.test.js → result-from-cache-response.test.js} +9 -10
  30. package/src/util/__tests__/scoped-in-memory-cache.test.js +396 -0
  31. package/src/util/__tests__/ssr-cache.test.js +639 -0
  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/components/__tests__/intercept-data.test.js +0 -87
  39. package/src/components/intercept-data.js +0 -77
  40. package/src/components/intercept-data.md +0 -65
  41. package/src/hooks/__tests__/use-data.test.js +0 -826
  42. package/src/hooks/use-data.js +0 -143
  43. package/src/util/__tests__/memory-cache.test.js +0 -446
  44. package/src/util/__tests__/request-handler.test.js +0 -121
  45. package/src/util/__tests__/response-cache.test.js +0 -879
  46. package/src/util/memory-cache.js +0 -187
  47. package/src/util/request-handler.js +0 -42
  48. package/src/util/request-handler.md +0 -51
  49. package/src/util/response-cache.js +0 -213
@@ -0,0 +1,396 @@
1
+ // @flow
2
+ import * as WSCore from "@khanacademy/wonder-stuff-core";
3
+ import {ScopedInMemoryCache} from "../scoped-in-memory-cache.js";
4
+
5
+ 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
+ describe("#set", () => {
47
+ it.each`
48
+ id
49
+ ${null}
50
+ ${""}
51
+ ${5}
52
+ ${() => "BOO"}
53
+ `("should throw if the id is $id", ({id}) => {
54
+ // Arrange
55
+ const cache = new ScopedInMemoryCache();
56
+
57
+ // Act
58
+ const underTest = () => cache.set("scope", id, "value");
59
+
60
+ // Assert
61
+ expect(underTest).toThrowErrorMatchingSnapshot();
62
+ });
63
+
64
+ it.each`
65
+ scope
66
+ ${null}
67
+ ${""}
68
+ ${5}
69
+ ${() => "BOO"}
70
+ `("should throw if the scope is $scope", ({scope}) => {
71
+ // Arrange
72
+ const cache = new ScopedInMemoryCache();
73
+
74
+ // Act
75
+ const underTest = () => cache.set(scope, "key", "value");
76
+
77
+ // Assert
78
+ expect(underTest).toThrowErrorMatchingSnapshot();
79
+ });
80
+
81
+ it("should throw if the value is a function", () => {
82
+ // Arrange
83
+ const cache = new ScopedInMemoryCache();
84
+
85
+ // Act
86
+ const underTest = () => cache.set("scope", "key", () => "value");
87
+
88
+ // Assert
89
+ expect(underTest).toThrowErrorMatchingSnapshot();
90
+ });
91
+
92
+ it("should store the entry in the cache", () => {
93
+ // Arrange
94
+ const cache = new ScopedInMemoryCache();
95
+
96
+ // Act
97
+ cache.set("scope", "key", "data");
98
+ const result = cache.get("scope", "key");
99
+
100
+ // Assert
101
+ expect(result).toStrictEqual("data");
102
+ });
103
+
104
+ it("should replace the entry in the cache", () => {
105
+ // Arrange
106
+ const cache = new ScopedInMemoryCache({
107
+ scope: {
108
+ key: "data",
109
+ },
110
+ });
111
+
112
+ // Act
113
+ cache.set("scope", "key", "other_data");
114
+ const result = cache.get("scope", "key");
115
+
116
+ // Assert
117
+ expect(result).toStrictEqual("other_data");
118
+ });
119
+ });
120
+
121
+ describe("#retrieve", () => {
122
+ it("should return null if the scope is not cached", () => {
123
+ // Arrange
124
+ const cache = new ScopedInMemoryCache();
125
+
126
+ // Act
127
+ const result = cache.get("scope", "key");
128
+
129
+ // Assert
130
+ expect(result).toBeNull();
131
+ });
132
+
133
+ it("should return null if the value is not in the cache scope", () => {
134
+ // Arrange
135
+ const cache = new ScopedInMemoryCache({
136
+ scope: {
137
+ key1: "data",
138
+ },
139
+ });
140
+
141
+ // Act
142
+ const result = cache.get("scope", "key2");
143
+
144
+ // Assert
145
+ expect(result).toBeNull();
146
+ });
147
+
148
+ it("should return the entry if it exists", () => {
149
+ // Arrange
150
+ const cache = new ScopedInMemoryCache({
151
+ scope: {key: "value"},
152
+ });
153
+
154
+ // Act
155
+ const result = cache.get("scope", "key");
156
+
157
+ // Assert
158
+ expect(result).toStrictEqual("value");
159
+ });
160
+ });
161
+
162
+ describe("#purge", () => {
163
+ it("should remove the entry", () => {
164
+ // Arrange
165
+ const cache = new ScopedInMemoryCache({
166
+ scope1: {
167
+ key1: "data1",
168
+ key2: "data2",
169
+ },
170
+ scope2: {
171
+ key1: "data1",
172
+ key2: "data2",
173
+ },
174
+ });
175
+
176
+ // Act
177
+ cache.purge("scope1", "key2");
178
+ const result = cache.clone();
179
+
180
+ // Assert
181
+ expect(result).toStrictEqual({
182
+ scope1: {
183
+ key1: "data1",
184
+ },
185
+ scope2: {
186
+ key1: "data1",
187
+ key2: "data2",
188
+ },
189
+ });
190
+ });
191
+
192
+ it("should remove the entire scope if the purged item is the last item in the scope", () => {
193
+ const cache = new ScopedInMemoryCache({
194
+ scope1: {
195
+ key2: "data2",
196
+ },
197
+ scope2: {
198
+ key1: "data1",
199
+ key2: "data2",
200
+ },
201
+ });
202
+
203
+ // Act
204
+ cache.purge("scope1", "key2");
205
+ const result = cache.clone();
206
+
207
+ // Assert
208
+ expect(result).toStrictEqual({
209
+ scope2: {
210
+ key1: "data1",
211
+ key2: "data2",
212
+ },
213
+ });
214
+ });
215
+ });
216
+
217
+ describe("#purgeScope", () => {
218
+ it("should remove matching entries only", () => {
219
+ // Arrange
220
+ const cache = new ScopedInMemoryCache({
221
+ scope1: {
222
+ key1: "a",
223
+ key2: "b",
224
+ key3: "a",
225
+ },
226
+ scope2: {
227
+ key1: "a",
228
+ key2: "b",
229
+ key3: "a",
230
+ },
231
+ });
232
+
233
+ // Act
234
+ cache.purgeScope("scope1", (id, value) => value === "a");
235
+ const result = cache.clone();
236
+
237
+ // Assert
238
+ expect(result).toStrictEqual({
239
+ scope1: {
240
+ key2: "b",
241
+ },
242
+ scope2: {
243
+ key1: "a",
244
+ key2: "b",
245
+ key3: "a",
246
+ },
247
+ });
248
+ });
249
+
250
+ it("should remove the entire scope when there is no predicate", () => {
251
+ // Arrange
252
+ const cache = new ScopedInMemoryCache({
253
+ scope1: {
254
+ key1: "data1",
255
+ key2: "data2",
256
+ },
257
+ scope2: {
258
+ key1: "data1",
259
+ key2: "data2",
260
+ },
261
+ });
262
+
263
+ // Act
264
+ cache.purgeScope("scope1");
265
+ const result = cache.clone();
266
+
267
+ // Assert
268
+ expect(result).toStrictEqual({
269
+ scope2: {
270
+ key1: "data1",
271
+ key2: "data2",
272
+ },
273
+ });
274
+ });
275
+
276
+ it("should not throw if the scope does not exist", () => {
277
+ // Arrange
278
+ const cache = new ScopedInMemoryCache({
279
+ scope1: {
280
+ key1: "data1",
281
+ },
282
+ });
283
+
284
+ // Act
285
+ const act = () => cache.purgeScope("scope2");
286
+
287
+ // Arrange
288
+ expect(act).not.toThrow();
289
+ });
290
+ });
291
+
292
+ describe("#purgeAll", () => {
293
+ it("should remove matching entries only", () => {
294
+ const cache = new ScopedInMemoryCache({
295
+ scope1: {key: "2"},
296
+ scope2: {key: "1"},
297
+ scope3: {key: "2"},
298
+ });
299
+
300
+ // Act
301
+ cache.purgeAll((scope, id, value) => value === "2");
302
+ const result = cache.clone();
303
+
304
+ // Assert
305
+ expect(result).toStrictEqual({
306
+ scope2: {key: "1"},
307
+ });
308
+ });
309
+
310
+ it("should remove the all items if there is no predicate", () => {
311
+ const cache = new ScopedInMemoryCache({
312
+ scope1: {key: "2"},
313
+ scope2: {key: "1"},
314
+ scope3: {key: "2"},
315
+ });
316
+
317
+ // Act
318
+ cache.purgeAll();
319
+ const result = cache.clone();
320
+
321
+ // Assert
322
+ expect(result).toStrictEqual({});
323
+ });
324
+ });
325
+
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
+ describe("@inUse", () => {
365
+ it("should return true if the cache contains data", () => {
366
+ // Arrange
367
+ const cache = new ScopedInMemoryCache({
368
+ scope1: {key: "2"},
369
+ scope2: {key: "1"},
370
+ scope3: {key: "2"},
371
+ });
372
+
373
+ // Act
374
+ const result = cache.inUse;
375
+
376
+ // Assert
377
+ expect(result).toBeTruthy();
378
+ });
379
+
380
+ it("should return false if the cache is empty", () => {
381
+ // Arrange
382
+ const cache = new ScopedInMemoryCache({
383
+ scope1: {key: "2"},
384
+ scope2: {key: "1"},
385
+ scope3: {key: "2"},
386
+ });
387
+ cache.purgeAll();
388
+
389
+ // Act
390
+ const result = cache.inUse;
391
+
392
+ // Assert
393
+ expect(result).toBeFalsy();
394
+ });
395
+ });
396
+ });