@khanacademy/wonder-blocks-data 6.0.1 → 8.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 (56) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/es/index.js +329 -777
  3. package/dist/index.js +1196 -834
  4. package/package.json +3 -3
  5. package/src/__docs__/_overview_ssr_.stories.mdx +13 -13
  6. package/src/__docs__/exports.abort-inflight-requests.stories.mdx +20 -0
  7. package/src/__docs__/exports.data.stories.mdx +3 -3
  8. package/src/__docs__/{exports.fulfill-all-data-requests.stories.mdx → exports.fetch-tracked-requests.stories.mdx} +5 -5
  9. package/src/__docs__/exports.get-gql-request-id.stories.mdx +24 -0
  10. package/src/__docs__/{exports.has-unfulfilled-requests.stories.mdx → exports.has-tracked-requests-to-be-fetched.stories.mdx} +4 -4
  11. package/src/__docs__/exports.intialize-hydration-cache.stories.mdx +29 -0
  12. package/src/__docs__/exports.purge-caches.stories.mdx +23 -0
  13. package/src/__docs__/{exports.remove-all-from-cache.stories.mdx → exports.purge-hydration-cache.stories.mdx} +4 -4
  14. package/src/__docs__/{exports.clear-shared-cache.stories.mdx → exports.purge-shared-cache.stories.mdx} +4 -4
  15. package/src/__docs__/exports.track-data.stories.mdx +4 -4
  16. package/src/__docs__/exports.use-cached-effect.stories.mdx +7 -4
  17. package/src/__docs__/exports.use-gql.stories.mdx +1 -33
  18. package/src/__docs__/exports.use-server-effect.stories.mdx +14 -2
  19. package/src/__docs__/exports.use-shared-cache.stories.mdx +2 -2
  20. package/src/__docs__/types.fetch-policy.stories.mdx +44 -0
  21. package/src/__docs__/types.response-cache.stories.mdx +1 -1
  22. package/src/__tests__/generated-snapshot.test.js +5 -5
  23. package/src/components/__tests__/data.test.js +2 -6
  24. package/src/hooks/__tests__/use-cached-effect.test.js +341 -100
  25. package/src/hooks/__tests__/use-hydratable-effect.test.js +17 -34
  26. package/src/hooks/__tests__/use-server-effect.test.js +51 -0
  27. package/src/hooks/__tests__/use-shared-cache.test.js +6 -6
  28. package/src/hooks/use-cached-effect.js +169 -93
  29. package/src/hooks/use-hydratable-effect.js +12 -12
  30. package/src/hooks/use-server-effect.js +30 -5
  31. package/src/hooks/use-shared-cache.js +2 -2
  32. package/src/index.js +14 -78
  33. package/src/util/__tests__/get-gql-request-id.test.js +74 -0
  34. package/src/util/__tests__/graphql-document-node-parser.test.js +542 -0
  35. package/src/util/__tests__/hydration-cache-api.test.js +35 -0
  36. package/src/util/__tests__/purge-caches.test.js +29 -0
  37. package/src/util/__tests__/request-api.test.js +188 -0
  38. package/src/util/__tests__/request-fulfillment.test.js +42 -0
  39. package/src/util/__tests__/ssr-cache.test.js +10 -60
  40. package/src/util/__tests__/to-gql-operation.test.js +42 -0
  41. package/src/util/data-error.js +6 -0
  42. package/src/util/get-gql-request-id.js +50 -0
  43. package/src/util/graphql-document-node-parser.js +133 -0
  44. package/src/util/graphql-types.js +30 -0
  45. package/src/util/hydration-cache-api.js +28 -0
  46. package/src/util/purge-caches.js +15 -0
  47. package/src/util/request-api.js +66 -0
  48. package/src/util/request-fulfillment.js +32 -12
  49. package/src/util/request-tracking.js +1 -1
  50. package/src/util/ssr-cache.js +1 -21
  51. package/src/util/to-gql-operation.js +44 -0
  52. package/src/util/types.js +31 -0
  53. package/src/__docs__/exports.intialize-cache.stories.mdx +0 -29
  54. package/src/__docs__/exports.remove-from-cache.stories.mdx +0 -25
  55. package/src/__docs__/exports.request-fulfillment.stories.mdx +0 -36
  56. package/src/util/abort-error.js +0 -15
@@ -0,0 +1,188 @@
1
+ // @flow
2
+ import {Server} from "@khanacademy/wonder-blocks-core";
3
+ import {RequestFulfillment} from "../request-fulfillment.js";
4
+ import {RequestTracker} from "../request-tracking.js";
5
+
6
+ import {
7
+ abortInflightRequests,
8
+ fetchTrackedRequests,
9
+ hasTrackedRequestsToBeFetched,
10
+ } from "../request-api.js";
11
+
12
+ describe("#fetchTrackedRequests", () => {
13
+ describe("when server-side", () => {
14
+ beforeEach(() => {
15
+ jest.spyOn(Server, "isServerSide").mockReturnValue(true);
16
+ });
17
+
18
+ it("should call RequestTracker.Default.fulfillTrackedRequests", () => {
19
+ // Arrange
20
+ const fulfillTrackedRequestsSpy = jest.spyOn(
21
+ RequestTracker.Default,
22
+ "fulfillTrackedRequests",
23
+ );
24
+
25
+ // Act
26
+ fetchTrackedRequests();
27
+
28
+ // Assert
29
+ expect(fulfillTrackedRequestsSpy).toHaveBeenCalled();
30
+ });
31
+
32
+ it("should return the response cache", async () => {
33
+ // Arrange
34
+ const responseCache = {};
35
+ jest.spyOn(
36
+ RequestTracker.Default,
37
+ "fulfillTrackedRequests",
38
+ ).mockResolvedValue(responseCache);
39
+
40
+ // Act
41
+ const result = await fetchTrackedRequests();
42
+
43
+ // Assert
44
+ expect(result).toBe(responseCache);
45
+ });
46
+ });
47
+
48
+ describe("when client-side", () => {
49
+ const NODE_ENV = process.env.NODE_ENV;
50
+ beforeEach(() => {
51
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
52
+ });
53
+
54
+ afterEach(() => {
55
+ if (NODE_ENV === undefined) {
56
+ delete process.env.NODE_ENV;
57
+ } else {
58
+ process.env.NODE_ENV = NODE_ENV;
59
+ }
60
+ });
61
+
62
+ describe("in production", () => {
63
+ it("should reject with error", async () => {
64
+ // Arrange
65
+ process.env.NODE_ENV = "production";
66
+
67
+ // Act
68
+ const result = fetchTrackedRequests();
69
+
70
+ // Assert
71
+ await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(
72
+ `"No CSR tracking"`,
73
+ );
74
+ });
75
+ });
76
+
77
+ describe("not in production", () => {
78
+ it("should reject with error", async () => {
79
+ // Arrange
80
+ process.env.NODE_ENV = "test";
81
+
82
+ // Act
83
+ const result = fetchTrackedRequests();
84
+
85
+ // Assert
86
+ await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(
87
+ `"Data requests are not tracked for fulfillment when when client-side"`,
88
+ );
89
+ });
90
+ });
91
+ });
92
+ });
93
+
94
+ describe("#hasTrackedRequestsToBeFetched", () => {
95
+ describe("when server-side", () => {
96
+ beforeEach(() => {
97
+ jest.spyOn(Server, "isServerSide").mockReturnValue(true);
98
+ });
99
+
100
+ it("should call RequestTracker.Default.hasUnfulfilledRequests", () => {
101
+ // Arrange
102
+ const hasUnfulfilledRequestsSpy = jest.spyOn(
103
+ RequestTracker.Default,
104
+ "hasUnfulfilledRequests",
105
+ "get",
106
+ );
107
+
108
+ // Act
109
+ hasTrackedRequestsToBeFetched();
110
+
111
+ // Assert
112
+ expect(hasUnfulfilledRequestsSpy).toHaveBeenCalled();
113
+ });
114
+
115
+ it("should return the boolean value from RequestTracker.Default.hasUnfulfilledRequests", () => {
116
+ // Arrange
117
+ jest.spyOn(
118
+ RequestTracker.Default,
119
+ "hasUnfulfilledRequests",
120
+ "get",
121
+ ).mockReturnValue(true);
122
+
123
+ // Act
124
+ const result = hasTrackedRequestsToBeFetched();
125
+
126
+ // Assert
127
+ expect(result).toBeTrue();
128
+ });
129
+ });
130
+
131
+ describe("when client-side", () => {
132
+ const NODE_ENV = process.env.NODE_ENV;
133
+ beforeEach(() => {
134
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
135
+ });
136
+
137
+ afterEach(() => {
138
+ if (NODE_ENV === undefined) {
139
+ delete process.env.NODE_ENV;
140
+ } else {
141
+ process.env.NODE_ENV = NODE_ENV;
142
+ }
143
+ });
144
+
145
+ describe("in production", () => {
146
+ it("should reject with error", () => {
147
+ // Arrange
148
+ process.env.NODE_ENV = "production";
149
+
150
+ // Act
151
+ const underTest = () => hasTrackedRequestsToBeFetched();
152
+
153
+ // Assert
154
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
155
+ `"No CSR tracking"`,
156
+ );
157
+ });
158
+ });
159
+
160
+ describe("not in production", () => {
161
+ it("should reject with error", () => {
162
+ // Arrange
163
+ process.env.NODE_ENV = "test";
164
+
165
+ // Act
166
+ const underTest = () => hasTrackedRequestsToBeFetched();
167
+
168
+ // Assert
169
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
170
+ `"Data requests are not tracked for fulfillment when when client-side"`,
171
+ );
172
+ });
173
+ });
174
+ });
175
+ });
176
+
177
+ describe("#abortInflightRequests", () => {
178
+ it("should call RequestFulfillment.Default.abortAll", () => {
179
+ // Arrange
180
+ const abortAllSpy = jest.spyOn(RequestFulfillment.Default, "abortAll");
181
+
182
+ // Act
183
+ abortInflightRequests();
184
+
185
+ // Assert
186
+ expect(abortAllSpy).toHaveBeenCalled();
187
+ });
188
+ });
@@ -102,4 +102,46 @@ describe("RequestFulfillment", () => {
102
102
  expect(result).not.toBe(promise);
103
103
  });
104
104
  });
105
+
106
+ describe("#abort", () => {
107
+ it("should delete the given request from the inflight requests", () => {
108
+ // Arrange
109
+ const requestFulfillment = new RequestFulfillment();
110
+ const fakeRequestHandler = () => Promise.resolve("DATA!");
111
+ const promise = requestFulfillment.fulfill("ID", {
112
+ handler: fakeRequestHandler,
113
+ });
114
+
115
+ // Act
116
+ requestFulfillment.abort("ID");
117
+ const result = requestFulfillment.fulfill("ID", {
118
+ handler: fakeRequestHandler,
119
+ });
120
+
121
+ // Assert
122
+ expect(result).not.toBe(promise);
123
+ });
124
+ });
125
+
126
+ describe("#abortAll", () => {
127
+ it("should abort all inflight requests", () => {
128
+ // Arrange
129
+ const requestFulfillment = new RequestFulfillment();
130
+ const abortSpy = jest.spyOn(requestFulfillment, "abort");
131
+ const fakeRequestHandler = () => Promise.resolve("DATA!");
132
+ requestFulfillment.fulfill("ID1", {
133
+ handler: fakeRequestHandler,
134
+ });
135
+ requestFulfillment.fulfill("ID2", {
136
+ handler: fakeRequestHandler,
137
+ });
138
+
139
+ // Act
140
+ requestFulfillment.abortAll();
141
+
142
+ // Assert
143
+ expect(abortSpy).toHaveBeenCalledWith("ID1");
144
+ expect(abortSpy).toHaveBeenCalledWith("ID2");
145
+ });
146
+ });
105
147
  });
@@ -393,56 +393,6 @@ describe("../ssr-cache.js", () => {
393
393
  });
394
394
  });
395
395
 
396
- describe("#remove", () => {
397
- it("should return false if nothing was removed", () => {
398
- // Arrange
399
- const hydrationCache = new SerializableInMemoryCache();
400
- const ssrOnlycache = new SerializableInMemoryCache();
401
- jest.spyOn(hydrationCache, "purge").mockReturnValue(false);
402
- jest.spyOn(ssrOnlycache, "purge").mockReturnValue(false);
403
- const cache = new SsrCache(hydrationCache, ssrOnlycache);
404
-
405
- // Act
406
- const result = cache.remove("A");
407
-
408
- // Assert
409
- expect(result).toBeFalsy();
410
- });
411
-
412
- it("should return true if something was removed from hydration cache", () => {
413
- // Arrange
414
- const hydrationCache = new SerializableInMemoryCache();
415
- jest.spyOn(hydrationCache, "purge").mockReturnValue(true);
416
- const cache = new SsrCache(hydrationCache);
417
-
418
- // Act
419
- const result = cache.remove("A");
420
-
421
- // Assert
422
- expect(result).toBeTruthy();
423
- });
424
-
425
- describe("when server-side", () => {
426
- beforeEach(() => {
427
- jest.spyOn(Server, "isServerSide").mockReturnValue(true);
428
- });
429
-
430
- it("should return true if something was removed from ssr-only cache", () => {
431
- // Arrange
432
- const hydrationCache = new SerializableInMemoryCache();
433
- const ssrOnlyCache = new SerializableInMemoryCache();
434
- jest.spyOn(ssrOnlyCache, "purge").mockReturnValue(true);
435
- const cache = new SsrCache(hydrationCache, ssrOnlyCache);
436
-
437
- // Act
438
- const result = cache.remove("A");
439
-
440
- // Assert
441
- expect(result).toBeTruthy();
442
- });
443
- });
444
- });
445
-
446
396
  describe("#cloneHydratableData", () => {
447
397
  it("should clone the hydration cache", () => {
448
398
  // Arrange
@@ -467,7 +417,7 @@ describe("../ssr-cache.js", () => {
467
417
  });
468
418
  });
469
419
 
470
- describe("#removeAll", () => {
420
+ describe("#purgeData", () => {
471
421
  describe("when client-side", () => {
472
422
  beforeEach(() => {
473
423
  jest.spyOn(Server, "isServerSide").mockReturnValue(false);
@@ -480,7 +430,7 @@ describe("../ssr-cache.js", () => {
480
430
  const cache = new SsrCache(hydrationCache);
481
431
 
482
432
  // Act
483
- cache.removeAll();
433
+ cache.purgeData();
484
434
 
485
435
  // Assert
486
436
  expect(purgeAllSpy).toHaveBeenCalledWith(undefined);
@@ -496,7 +446,7 @@ describe("../ssr-cache.js", () => {
496
446
  );
497
447
 
498
448
  // Act
499
- cache.removeAll(() => true);
449
+ cache.purgeData(() => true);
500
450
 
501
451
  // Assert
502
452
  expect(purgeAllSpy).toHaveBeenCalledWith(expect.any(Function));
@@ -518,7 +468,7 @@ describe("../ssr-cache.js", () => {
518
468
  const predicate = jest.fn().mockReturnValue(false);
519
469
 
520
470
  // Act
521
- cache.removeAll(predicate);
471
+ cache.purgeData(predicate);
522
472
 
523
473
  // Assert
524
474
  expect(predicate).toHaveBeenCalledWith("KEY1", {data: "DATA"});
@@ -543,7 +493,7 @@ describe("../ssr-cache.js", () => {
543
493
  );
544
494
 
545
495
  // Act
546
- cache.removeAll();
496
+ cache.purgeData();
547
497
 
548
498
  // Assert
549
499
  expect(hydrationPurgeAllSpy).toHaveBeenCalledWith(undefined);
@@ -559,7 +509,7 @@ describe("../ssr-cache.js", () => {
559
509
  );
560
510
 
561
511
  // Act
562
- cache.removeAll();
512
+ cache.purgeData();
563
513
 
564
514
  // Assert
565
515
  expect(ssrPurgeAllSpy).toHaveBeenCalledWith(undefined);
@@ -575,7 +525,7 @@ describe("../ssr-cache.js", () => {
575
525
  );
576
526
 
577
527
  // Act
578
- cache.removeAll(() => true);
528
+ cache.purgeData(() => true);
579
529
 
580
530
  // Assert
581
531
  expect(purgeAllSpy).toHaveBeenCalledWith(expect.any(Function));
@@ -591,7 +541,7 @@ describe("../ssr-cache.js", () => {
591
541
  );
592
542
 
593
543
  // Act
594
- cache.removeAll(() => true);
544
+ cache.purgeData(() => true);
595
545
 
596
546
  // Assert
597
547
  expect(purgeAllSpy).toHaveBeenCalledWith(expect.any(Function));
@@ -608,7 +558,7 @@ describe("../ssr-cache.js", () => {
608
558
  const predicate = jest.fn().mockReturnValue(false);
609
559
 
610
560
  // Act
611
- cache.removeAll(predicate);
561
+ cache.purgeData(predicate);
612
562
 
613
563
  // Assert
614
564
  expect(predicate).toHaveBeenCalledWith("KEY1", {data: "DATA"});
@@ -629,7 +579,7 @@ describe("../ssr-cache.js", () => {
629
579
  const predicate = jest.fn().mockReturnValue(false);
630
580
 
631
581
  // Act
632
- cache.removeAll(predicate);
582
+ cache.purgeData(predicate);
633
583
 
634
584
  // Assert
635
585
  expect(predicate).toHaveBeenCalledWith("KEY1", {data: "DATA"});
@@ -0,0 +1,42 @@
1
+ // @flow
2
+ import {toGqlOperation} from "../to-gql-operation.js";
3
+ import * as GDNP from "../graphql-document-node-parser.js";
4
+
5
+ jest.mock("../graphql-document-node-parser.js");
6
+
7
+ describe("#toGqlOperation", () => {
8
+ it("should parse the document node", () => {
9
+ // Arrange
10
+ const documentNode: any = {};
11
+ const parserSpy = jest
12
+ .spyOn(GDNP, "graphQLDocumentNodeParser")
13
+ .mockReturnValue({
14
+ name: "operationName",
15
+ type: "query",
16
+ });
17
+
18
+ // Act
19
+ toGqlOperation(documentNode);
20
+
21
+ // Assert
22
+ expect(parserSpy).toHaveBeenCalledWith(documentNode);
23
+ });
24
+
25
+ it("should return the Wonder Blocks Data representation of the given document node", () => {
26
+ // Arrange
27
+ const documentNode: any = {};
28
+ jest.spyOn(GDNP, "graphQLDocumentNodeParser").mockReturnValue({
29
+ name: "operationName",
30
+ type: "mutation",
31
+ });
32
+
33
+ // Act
34
+ const result = toGqlOperation(documentNode);
35
+
36
+ // Assert
37
+ expect(result).toStrictEqual({
38
+ id: "operationName",
39
+ type: "mutation",
40
+ });
41
+ });
42
+ });
@@ -26,6 +26,12 @@ export const DataErrors = Object.freeze({
26
26
  */
27
27
  Network: "Network",
28
28
 
29
+ /**
30
+ * There was a problem due to the state of the system not matching the
31
+ * requested operation or input.
32
+ */
33
+ NotAllowed: "NotAllowed",
34
+
29
35
  /**
30
36
  * Response could not be parsed.
31
37
  */
@@ -0,0 +1,50 @@
1
+ // @flow
2
+ import type {GqlOperation, GqlContext} from "./gql-types.js";
3
+
4
+ const toString = (valid: mixed): string => {
5
+ if (typeof valid === "string") {
6
+ return valid;
7
+ }
8
+ return JSON.stringify(valid) ?? "";
9
+ };
10
+
11
+ /**
12
+ * Get an identifier for a given request.
13
+ */
14
+ export const getGqlRequestId = <TData, TVariables: {...}>(
15
+ operation: GqlOperation<TData, TVariables>,
16
+ variables: ?TVariables,
17
+ context: GqlContext,
18
+ ): string => {
19
+ // We add all the bits for this into an array and then join them with
20
+ // a chosen separator.
21
+ const parts = [];
22
+
23
+ // First, we push the context values.
24
+ const sortableContext = new URLSearchParams(context);
25
+ // $FlowIgnore[prop-missing] Flow has incomplete support for URLSearchParams
26
+ sortableContext.sort();
27
+ parts.push(sortableContext.toString());
28
+
29
+ // Now we add the operation identifier.
30
+ parts.push(operation.id);
31
+
32
+ // Finally, if we have variables, we add those too.
33
+ if (variables != null) {
34
+ // We need to turn each variable into a string.
35
+ const stringifiedVariables = Object.keys(variables).reduce(
36
+ (acc, key) => {
37
+ acc[key] = toString(variables[key]);
38
+ return acc;
39
+ },
40
+ {},
41
+ );
42
+ // We use the same mechanism as context to sort and arrange the
43
+ // variables.
44
+ const sortableVariables = new URLSearchParams(stringifiedVariables);
45
+ // $FlowIgnore[prop-missing] Flow has incomplete support for URLSearchParams
46
+ sortableVariables.sort();
47
+ parts.push(sortableVariables.toString());
48
+ }
49
+ return parts.join("|");
50
+ };
@@ -0,0 +1,133 @@
1
+ // @flow
2
+ import type {
3
+ DocumentNode,
4
+ DefinitionNode,
5
+ VariableDefinitionNode,
6
+ OperationDefinitionNode,
7
+ } from "./graphql-types.js";
8
+ import {DataError, DataErrors} from "./data-error.js";
9
+
10
+ export const DocumentTypes = Object.freeze({
11
+ query: "query",
12
+ mutation: "mutation",
13
+ });
14
+
15
+ export type DocumentType = $Values<typeof DocumentTypes>;
16
+
17
+ export interface IDocumentDefinition {
18
+ type: DocumentType;
19
+ name: string;
20
+ variables: $ReadOnlyArray<VariableDefinitionNode>;
21
+ }
22
+
23
+ const cache = new Map<DocumentNode, IDocumentDefinition>();
24
+
25
+ /**
26
+ * Parse a GraphQL document node to determine some info about it.
27
+ *
28
+ * This is based on:
29
+ * https://github.com/apollographql/react-apollo/blob/3bc993b2ea91704bd6a2667f42d1940656c071ff/src/parser.ts
30
+ */
31
+ export function graphQLDocumentNodeParser(
32
+ document: DocumentNode,
33
+ ): IDocumentDefinition {
34
+ const cached = cache.get(document);
35
+ if (cached) {
36
+ return cached;
37
+ }
38
+
39
+ /**
40
+ * Saftey check for proper usage.
41
+ */
42
+ if (!document?.kind) {
43
+ if (process.env.NODE_ENV === "production") {
44
+ throw new DataError("Bad DocumentNode", DataErrors.InvalidInput);
45
+ } else {
46
+ throw new DataError(
47
+ `Argument of ${JSON.stringify(
48
+ document,
49
+ )} passed to parser was not a valid GraphQL ` +
50
+ `DocumentNode. You may need to use 'graphql-tag' or another method ` +
51
+ `to convert your operation into a document`,
52
+ DataErrors.InvalidInput,
53
+ );
54
+ }
55
+ }
56
+
57
+ const fragments = document.definitions.filter(
58
+ (x: DefinitionNode) => x.kind === "FragmentDefinition",
59
+ );
60
+
61
+ const queries = document.definitions.filter(
62
+ (x: DefinitionNode) =>
63
+ // $FlowIgnore[prop-missing]
64
+ x.kind === "OperationDefinition" && x.operation === "query",
65
+ );
66
+
67
+ const mutations = document.definitions.filter(
68
+ (x: DefinitionNode) =>
69
+ // $FlowIgnore[prop-missing]
70
+ x.kind === "OperationDefinition" && x.operation === "mutation",
71
+ );
72
+
73
+ const subscriptions = document.definitions.filter(
74
+ (x: DefinitionNode) =>
75
+ // $FlowIgnore[prop-missing]
76
+ x.kind === "OperationDefinition" && x.operation === "subscription",
77
+ );
78
+
79
+ if (fragments.length && !queries.length && !mutations.length) {
80
+ if (process.env.NODE_ENV === "production") {
81
+ throw new DataError("Fragment only", DataErrors.InvalidInput);
82
+ } else {
83
+ throw new DataError(
84
+ `Passing only a fragment to 'graphql' is not supported. ` +
85
+ `You must include a query or mutation as well`,
86
+ DataErrors.InvalidInput,
87
+ );
88
+ }
89
+ }
90
+
91
+ if (subscriptions.length) {
92
+ if (process.env.NODE_ENV === "production") {
93
+ throw new DataError("No subscriptions", DataErrors.InvalidInput);
94
+ } else {
95
+ throw new DataError(
96
+ `We do not support subscriptions. ` +
97
+ `${JSON.stringify(document)} had ${
98
+ subscriptions.length
99
+ } subscriptions`,
100
+ DataErrors.InvalidInput,
101
+ );
102
+ }
103
+ }
104
+
105
+ if (queries.length + mutations.length > 1) {
106
+ if (process.env.NODE_ENV === "production") {
107
+ throw new DataError("Too many ops", DataErrors.InvalidInput);
108
+ } else {
109
+ throw new DataError(
110
+ `We only support one query or mutation per component. ` +
111
+ `${JSON.stringify(document)} had ${
112
+ queries.length
113
+ } queries and ` +
114
+ `${mutations.length} mutations. `,
115
+ DataErrors.InvalidInput,
116
+ );
117
+ }
118
+ }
119
+
120
+ const type = queries.length ? DocumentTypes.query : DocumentTypes.mutation;
121
+ const definitions = queries.length ? queries : mutations;
122
+
123
+ const definition: OperationDefinitionNode = (definitions[0]: any);
124
+ const variables = definition.variableDefinitions || [];
125
+
126
+ // fallback to using data if no name
127
+ const name =
128
+ definition.name?.kind === "Name" ? definition.name.value : "data";
129
+
130
+ const payload: IDocumentDefinition = {name, type, variables};
131
+ cache.set(document, payload);
132
+ return payload;
133
+ }
@@ -0,0 +1,30 @@
1
+ // @flow
2
+ // NOTE(somewhatabstract):
3
+ // These types are bare minimum to support document parsing. They're derived
4
+ // from graphql@14.5.8, the last version that provided flow types.
5
+ // Doing this avoids us having to take a dependency on that library just for
6
+ // these types.
7
+ export interface DefinitionNode {
8
+ +kind: string;
9
+ }
10
+
11
+ export type VariableDefinitionNode = {
12
+ +kind: "VariableDefinition",
13
+ ...
14
+ };
15
+
16
+ export interface OperationDefinitionNode extends DefinitionNode {
17
+ +kind: "OperationDefinition";
18
+ +operation: string;
19
+ +variableDefinitions: $ReadOnlyArray<VariableDefinitionNode>;
20
+ +name?: {|
21
+ +kind: mixed,
22
+ +value: string,
23
+ |};
24
+ }
25
+
26
+ export type DocumentNode = {
27
+ +kind: "Document",
28
+ +definitions: $ReadOnlyArray<DefinitionNode>,
29
+ ...
30
+ };
@@ -0,0 +1,28 @@
1
+ // @flow
2
+ import {SsrCache} from "./ssr-cache.js";
3
+
4
+ import type {ValidCacheData, CachedResponse, ResponseCache} from "./types.js";
5
+
6
+ /**
7
+ * Initialize the hydration cache.
8
+ *
9
+ * @param {ResponseCache} source The cache content to use for initializing the
10
+ * cache.
11
+ * @throws {Error} If the cache is already initialized.
12
+ */
13
+ export const initializeHydrationCache = (source: ResponseCache): void =>
14
+ SsrCache.Default.initialize(source);
15
+
16
+ /**
17
+ * Purge cached hydration responses that match the given predicate.
18
+ *
19
+ * @param {(id: string) => boolean} [predicate] The predicate to match against
20
+ * the cached hydration responses. If no predicate is provided, all cached
21
+ * hydration responses will be purged.
22
+ */
23
+ export const purgeHydrationCache = (
24
+ predicate?: (
25
+ key: string,
26
+ cacheEntry: ?$ReadOnly<CachedResponse<ValidCacheData>>,
27
+ ) => boolean,
28
+ ): void => SsrCache.Default.purgeData(predicate);
@@ -0,0 +1,15 @@
1
+ // @flow
2
+ import {purgeSharedCache} from "../hooks/use-shared-cache.js";
3
+ import {purgeHydrationCache} from "./hydration-cache-api.js";
4
+
5
+ /**
6
+ * Purge all caches managed by Wonder Blocks Data.
7
+ *
8
+ * This is a convenience method that purges the shared cache and the hydration
9
+ * cache. It is useful for testing purposes to avoid having to reason about
10
+ * which caches may have been used during a given test run.
11
+ */
12
+ export const purgeCaches = () => {
13
+ purgeSharedCache();
14
+ purgeHydrationCache();
15
+ };