@khanacademy/wonder-blocks-data 7.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.
- package/CHANGELOG.md +20 -0
- package/dist/es/index.js +284 -100
- package/dist/index.js +1180 -800
- package/package.json +1 -1
- package/src/__docs__/_overview_ssr_.stories.mdx +13 -13
- package/src/__docs__/exports.abort-inflight-requests.stories.mdx +20 -0
- package/src/__docs__/exports.data.stories.mdx +3 -3
- package/src/__docs__/{exports.fulfill-all-data-requests.stories.mdx → exports.fetch-tracked-requests.stories.mdx} +5 -5
- package/src/__docs__/exports.get-gql-request-id.stories.mdx +24 -0
- package/src/__docs__/{exports.has-unfulfilled-requests.stories.mdx → exports.has-tracked-requests-to-be-fetched.stories.mdx} +4 -4
- package/src/__docs__/exports.intialize-hydration-cache.stories.mdx +29 -0
- package/src/__docs__/exports.purge-caches.stories.mdx +23 -0
- package/src/__docs__/{exports.remove-all-from-cache.stories.mdx → exports.purge-hydration-cache.stories.mdx} +4 -4
- package/src/__docs__/{exports.clear-shared-cache.stories.mdx → exports.purge-shared-cache.stories.mdx} +4 -4
- package/src/__docs__/exports.track-data.stories.mdx +4 -4
- package/src/__docs__/exports.use-cached-effect.stories.mdx +7 -4
- package/src/__docs__/exports.use-gql.stories.mdx +1 -33
- package/src/__docs__/exports.use-server-effect.stories.mdx +1 -1
- package/src/__docs__/exports.use-shared-cache.stories.mdx +2 -2
- package/src/__docs__/types.fetch-policy.stories.mdx +44 -0
- package/src/__docs__/types.response-cache.stories.mdx +1 -1
- package/src/__tests__/generated-snapshot.test.js +5 -5
- package/src/components/__tests__/data.test.js +2 -6
- package/src/hooks/__tests__/use-cached-effect.test.js +341 -100
- package/src/hooks/__tests__/use-hydratable-effect.test.js +15 -9
- package/src/hooks/__tests__/use-shared-cache.test.js +6 -6
- package/src/hooks/use-cached-effect.js +169 -93
- package/src/hooks/use-hydratable-effect.js +8 -1
- package/src/hooks/use-shared-cache.js +2 -2
- package/src/index.js +14 -78
- package/src/util/__tests__/get-gql-request-id.test.js +74 -0
- package/src/util/__tests__/graphql-document-node-parser.test.js +542 -0
- package/src/util/__tests__/hydration-cache-api.test.js +35 -0
- package/src/util/__tests__/purge-caches.test.js +29 -0
- package/src/util/__tests__/request-api.test.js +188 -0
- package/src/util/__tests__/request-fulfillment.test.js +42 -0
- package/src/util/__tests__/ssr-cache.test.js +10 -60
- package/src/util/__tests__/to-gql-operation.test.js +42 -0
- package/src/util/data-error.js +6 -0
- package/src/util/get-gql-request-id.js +50 -0
- package/src/util/graphql-document-node-parser.js +133 -0
- package/src/util/graphql-types.js +30 -0
- package/src/util/hydration-cache-api.js +28 -0
- package/src/util/purge-caches.js +15 -0
- package/src/util/request-api.js +66 -0
- package/src/util/request-fulfillment.js +32 -12
- package/src/util/request-tracking.js +1 -1
- package/src/util/ssr-cache.js +1 -21
- package/src/util/to-gql-operation.js +44 -0
- package/src/util/types.js +31 -0
- package/src/__docs__/exports.intialize-cache.stories.mdx +0 -29
- package/src/__docs__/exports.remove-from-cache.stories.mdx +0 -25
- package/src/__docs__/exports.request-fulfillment.stories.mdx +0 -36
|
@@ -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("#
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|
package/src/util/data-error.js
CHANGED
|
@@ -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
|
+
};
|