@khanacademy/wonder-blocks-data 4.0.0 → 6.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 (91) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/es/index.js +793 -375
  3. package/dist/index.js +1203 -523
  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/__docs__/exports.intercept-requests.stories.mdx +69 -0
  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 +38 -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 +7 -31
  47. package/src/components/__tests__/data.test.js +160 -154
  48. package/src/components/__tests__/intercept-requests.test.js +58 -0
  49. package/src/components/data.js +22 -126
  50. package/src/components/intercept-context.js +4 -5
  51. package/src/components/intercept-requests.js +69 -0
  52. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +8 -8
  53. package/src/hooks/__tests__/use-cached-effect.test.js +507 -0
  54. package/src/hooks/__tests__/use-gql-router-context.test.js +133 -0
  55. package/src/hooks/__tests__/use-gql.test.js +1 -30
  56. package/src/hooks/__tests__/use-hydratable-effect.test.js +708 -0
  57. package/src/hooks/__tests__/use-request-interception.test.js +255 -0
  58. package/src/hooks/__tests__/use-server-effect.test.js +39 -11
  59. package/src/hooks/use-cached-effect.js +225 -0
  60. package/src/hooks/use-gql-router-context.js +50 -0
  61. package/src/hooks/use-gql.js +22 -52
  62. package/src/hooks/use-hydratable-effect.js +206 -0
  63. package/src/hooks/use-request-interception.js +51 -0
  64. package/src/hooks/use-server-effect.js +14 -7
  65. package/src/hooks/use-shared-cache.js +13 -11
  66. package/src/index.js +54 -2
  67. package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.js.snap +19 -0
  68. package/src/util/__tests__/merge-gql-context.test.js +74 -0
  69. package/src/util/__tests__/request-fulfillment.test.js +23 -42
  70. package/src/util/__tests__/request-tracking.test.js +26 -7
  71. package/src/util/__tests__/result-from-cache-response.test.js +19 -5
  72. package/src/util/__tests__/scoped-in-memory-cache.test.js +6 -85
  73. package/src/util/__tests__/serializable-in-memory-cache.test.js +398 -0
  74. package/src/util/__tests__/ssr-cache.test.js +52 -52
  75. package/src/util/abort-error.js +15 -0
  76. package/src/util/data-error.js +58 -0
  77. package/src/util/get-gql-data-from-response.js +3 -2
  78. package/src/util/gql-error.js +19 -11
  79. package/src/util/merge-gql-context.js +34 -0
  80. package/src/util/request-fulfillment.js +49 -46
  81. package/src/util/request-tracking.js +69 -15
  82. package/src/util/result-from-cache-response.js +12 -16
  83. package/src/util/scoped-in-memory-cache.js +24 -47
  84. package/src/util/serializable-in-memory-cache.js +49 -0
  85. package/src/util/ssr-cache.js +9 -8
  86. package/src/util/status.js +30 -0
  87. package/src/util/types.js +18 -1
  88. package/docs.md +0 -122
  89. package/src/components/__tests__/intercept-data.test.js +0 -63
  90. package/src/components/intercept-data.js +0 -66
  91. package/src/components/intercept-data.md +0 -51
@@ -0,0 +1,398 @@
1
+ // @flow
2
+ import * as WSCore from "@khanacademy/wonder-stuff-core";
3
+ import {SerializableInMemoryCache} from "../serializable-in-memory-cache.js";
4
+
5
+ describe("SerializableInMemoryCache", () => {
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 SerializableInMemoryCache(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 SerializableInMemoryCache({
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 SerializableInMemoryCache();
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 SerializableInMemoryCache();
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 SerializableInMemoryCache();
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 SerializableInMemoryCache();
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 SerializableInMemoryCache({
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 SerializableInMemoryCache();
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 SerializableInMemoryCache({
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 SerializableInMemoryCache({
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 SerializableInMemoryCache({
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 SerializableInMemoryCache({
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 SerializableInMemoryCache({
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 SerializableInMemoryCache({
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 SerializableInMemoryCache({
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 SerializableInMemoryCache({
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 SerializableInMemoryCache({
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 SerializableInMemoryCache(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 SerializableInMemoryCache({
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
360
+ caused by
361
+ Error: BANG!"
362
+ `);
363
+ });
364
+ });
365
+
366
+ describe("@inUse", () => {
367
+ it("should return true if the cache contains data", () => {
368
+ // Arrange
369
+ const cache = new SerializableInMemoryCache({
370
+ scope1: {key: "2"},
371
+ scope2: {key: "1"},
372
+ scope3: {key: "2"},
373
+ });
374
+
375
+ // Act
376
+ const result = cache.inUse;
377
+
378
+ // Assert
379
+ expect(result).toBeTruthy();
380
+ });
381
+
382
+ it("should return false if the cache is empty", () => {
383
+ // Arrange
384
+ const cache = new SerializableInMemoryCache({
385
+ scope1: {key: "2"},
386
+ scope2: {key: "1"},
387
+ scope3: {key: "2"},
388
+ });
389
+ cache.purgeAll();
390
+
391
+ // Act
392
+ const result = cache.inUse;
393
+
394
+ // Assert
395
+ expect(result).toBeFalsy();
396
+ });
397
+ });
398
+ });