@khanacademy/wonder-blocks-data 3.2.0 → 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.
- package/CHANGELOG.md +23 -0
- package/dist/es/index.js +356 -332
- package/dist/index.js +507 -456
- package/docs.md +17 -35
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +7 -46
- package/src/__tests__/generated-snapshot.test.js +56 -122
- package/src/components/__tests__/data.test.js +372 -297
- package/src/components/__tests__/intercept-data.test.js +6 -30
- package/src/components/data.js +153 -21
- package/src/components/data.md +38 -69
- package/src/components/intercept-context.js +6 -2
- package/src/components/intercept-data.js +40 -51
- package/src/components/intercept-data.md +13 -27
- package/src/components/track-data.md +9 -23
- package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +17 -0
- package/src/hooks/__tests__/use-server-effect.test.js +217 -0
- package/src/hooks/__tests__/use-shared-cache.test.js +307 -0
- package/src/hooks/use-server-effect.js +45 -0
- package/src/hooks/use-shared-cache.js +106 -0
- package/src/index.js +15 -19
- package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.js.snap +19 -0
- package/src/util/__tests__/request-fulfillment.test.js +42 -85
- package/src/util/__tests__/request-tracking.test.js +72 -191
- package/src/util/__tests__/{result-from-cache-entry.test.js → result-from-cache-response.test.js} +9 -10
- package/src/util/__tests__/scoped-in-memory-cache.test.js +396 -0
- package/src/util/__tests__/ssr-cache.test.js +639 -0
- package/src/util/request-fulfillment.js +36 -44
- package/src/util/request-tracking.js +62 -75
- package/src/util/{result-from-cache-entry.js → result-from-cache-response.js} +10 -13
- package/src/util/scoped-in-memory-cache.js +149 -0
- package/src/util/ssr-cache.js +206 -0
- package/src/util/types.js +43 -108
- package/src/hooks/__tests__/use-data.test.js +0 -826
- package/src/hooks/use-data.js +0 -143
- package/src/util/__tests__/memory-cache.test.js +0 -446
- package/src/util/__tests__/request-handler.test.js +0 -121
- package/src/util/__tests__/response-cache.test.js +0 -879
- package/src/util/memory-cache.js +0 -187
- package/src/util/request-handler.js +0 -42
- package/src/util/request-handler.md +0 -51
- package/src/util/response-cache.js +0 -213
|
@@ -65,19 +65,11 @@ import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
|
65
65
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
66
66
|
import Button from "@khanacademy/wonder-blocks-button";
|
|
67
67
|
import {Server, View} from "@khanacademy/wonder-blocks-core";
|
|
68
|
-
import {Data, TrackData,
|
|
68
|
+
import {Data, TrackData, fulfillAllDataRequests} from "@khanacademy/wonder-blocks-data";
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
fulfillRequest(options) {
|
|
76
|
-
return new Promise((resolve, reject) =>
|
|
77
|
-
setTimeout(() => resolve("DATA!"), 3000),
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
70
|
+
const myPretendHandler = () => new Promise((resolve, reject) =>
|
|
71
|
+
setTimeout(() => resolve("DATA!"), 3000),
|
|
72
|
+
);
|
|
81
73
|
|
|
82
74
|
class Example extends React.Component {
|
|
83
75
|
constructor() {
|
|
@@ -87,7 +79,6 @@ class Example extends React.Component {
|
|
|
87
79
|
* for the scope of this component.
|
|
88
80
|
*/
|
|
89
81
|
this.state = {};
|
|
90
|
-
this._handler = new MyPretendHandler();
|
|
91
82
|
}
|
|
92
83
|
|
|
93
84
|
static getDerivedStateFromError(error) {
|
|
@@ -133,15 +124,16 @@ class Example extends React.Component {
|
|
|
133
124
|
const data = this.state.data
|
|
134
125
|
? JSON.stringify(this.state.data, undefined, " ")
|
|
135
126
|
: "Data requested...";
|
|
127
|
+
|
|
136
128
|
return (
|
|
137
129
|
<React.Fragment>
|
|
138
130
|
<Strut size={Spacing.small_12} />
|
|
139
131
|
<TrackData>
|
|
140
|
-
<Data handler={
|
|
141
|
-
{(
|
|
132
|
+
<Data handler={myPretendHandler} requestId="TRACK_DATA_EXAMPLE">
|
|
133
|
+
{(result) => (
|
|
142
134
|
<View>
|
|
143
|
-
<BodyMonospace>{`Loading: ${loading}`}</BodyMonospace>
|
|
144
|
-
<BodyMonospace>{`Data: ${JSON.stringify(data)}`}</BodyMonospace>
|
|
135
|
+
<BodyMonospace>{`Loading: ${result.status === "loading"}`}</BodyMonospace>
|
|
136
|
+
<BodyMonospace>{`Data: ${JSON.stringify(result.data)}`}</BodyMonospace>
|
|
145
137
|
</View>
|
|
146
138
|
)}
|
|
147
139
|
</Data>
|
|
@@ -162,12 +154,6 @@ class Example extends React.Component {
|
|
|
162
154
|
rendered tree.
|
|
163
155
|
</Body>
|
|
164
156
|
<Strut size={Spacing.small_12} />
|
|
165
|
-
<Body>
|
|
166
|
-
If you click to remount after the data appears, we'll
|
|
167
|
-
rerender with the now cached data, and above should
|
|
168
|
-
update accordingly.
|
|
169
|
-
</Body>
|
|
170
|
-
<Strut size={Spacing.small_12} />
|
|
171
157
|
<BodyMonospace>{data}</BodyMonospace>
|
|
172
158
|
</View>
|
|
173
159
|
</React.Fragment>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`#useSharedCache should throw if the id is 1`] = `[InvalidInputError: id must be a non-empty string]`;
|
|
4
|
+
|
|
5
|
+
exports[`#useSharedCache should throw if the id is [Function anonymous] 1`] = `[InvalidInputError: id must be a non-empty string]`;
|
|
6
|
+
|
|
7
|
+
exports[`#useSharedCache should throw if the id is 5 1`] = `[InvalidInputError: id must be a non-empty string]`;
|
|
8
|
+
|
|
9
|
+
exports[`#useSharedCache should throw if the id is null 1`] = `[InvalidInputError: id must be a non-empty string]`;
|
|
10
|
+
|
|
11
|
+
exports[`#useSharedCache should throw if the scope is 1`] = `[InvalidInputError: scope must be a non-empty string]`;
|
|
12
|
+
|
|
13
|
+
exports[`#useSharedCache should throw if the scope is [Function anonymous] 1`] = `[InvalidInputError: scope must be a non-empty string]`;
|
|
14
|
+
|
|
15
|
+
exports[`#useSharedCache should throw if the scope is 5 1`] = `[InvalidInputError: scope must be a non-empty string]`;
|
|
16
|
+
|
|
17
|
+
exports[`#useSharedCache should throw if the scope is null 1`] = `[InvalidInputError: scope must be a non-empty string]`;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
|
|
3
|
+
import {renderHook as serverRenderHook} from "@testing-library/react-hooks/server";
|
|
4
|
+
|
|
5
|
+
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
6
|
+
|
|
7
|
+
import TrackData from "../../components/track-data.js";
|
|
8
|
+
import {RequestFulfillment} from "../../util/request-fulfillment.js";
|
|
9
|
+
import {SsrCache} from "../../util/ssr-cache.js";
|
|
10
|
+
import {RequestTracker} from "../../util/request-tracking.js";
|
|
11
|
+
|
|
12
|
+
import {useServerEffect} from "../use-server-effect.js";
|
|
13
|
+
|
|
14
|
+
describe("#useServerEffect", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
const responseCache = new SsrCache();
|
|
17
|
+
jest.spyOn(SsrCache, "Default", "get").mockReturnValue(responseCache);
|
|
18
|
+
jest.spyOn(RequestFulfillment, "Default", "get").mockReturnValue(
|
|
19
|
+
new RequestFulfillment(responseCache),
|
|
20
|
+
);
|
|
21
|
+
jest.spyOn(RequestTracker, "Default", "get").mockReturnValue(
|
|
22
|
+
new RequestTracker(responseCache),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
jest.resetAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("when server-side", () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest.spyOn(Server, "isServerSide").mockReturnValue(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should return null if no cached result", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const fakeHandler = jest.fn();
|
|
38
|
+
|
|
39
|
+
// Act
|
|
40
|
+
const {
|
|
41
|
+
result: {current: result},
|
|
42
|
+
} = serverRenderHook(() => useServerEffect("ID", fakeHandler));
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
expect(result).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should not directly request fulfillment", () => {
|
|
49
|
+
// Arrange
|
|
50
|
+
const fakeHandler = jest.fn();
|
|
51
|
+
const fulfillRequestSpy = jest.spyOn(
|
|
52
|
+
RequestFulfillment.Default,
|
|
53
|
+
"fulfill",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Act
|
|
57
|
+
serverRenderHook(() => useServerEffect("ID", fakeHandler));
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
expect(fulfillRequestSpy).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should track the request", () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const fakeHandler = jest.fn();
|
|
66
|
+
const trackDataRequestSpy = jest.spyOn(
|
|
67
|
+
RequestTracker.Default,
|
|
68
|
+
"trackDataRequest",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Act
|
|
72
|
+
serverRenderHook(() => useServerEffect("ID", fakeHandler), {
|
|
73
|
+
wrapper: TrackData,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(trackDataRequestSpy).toHaveBeenCalledWith(
|
|
78
|
+
"ID",
|
|
79
|
+
fakeHandler,
|
|
80
|
+
true,
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should return data cached result", () => {
|
|
85
|
+
// Arrange
|
|
86
|
+
const fakeHandler = jest.fn();
|
|
87
|
+
jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
|
|
88
|
+
data: "DATA",
|
|
89
|
+
error: null,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Act
|
|
93
|
+
const {
|
|
94
|
+
result: {current: result},
|
|
95
|
+
} = serverRenderHook(() => useServerEffect("ID", fakeHandler));
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
expect(result).toEqual({data: "DATA", error: null});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should return error cached result", () => {
|
|
102
|
+
// Arrange
|
|
103
|
+
const fakeHandler = jest.fn();
|
|
104
|
+
jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
|
|
105
|
+
data: null,
|
|
106
|
+
error: "ERROR",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Act
|
|
110
|
+
const {
|
|
111
|
+
result: {current: result},
|
|
112
|
+
} = serverRenderHook(() => useServerEffect("ID", fakeHandler));
|
|
113
|
+
|
|
114
|
+
// Assert
|
|
115
|
+
expect(result).toEqual({
|
|
116
|
+
data: null,
|
|
117
|
+
error: "ERROR",
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("when client-side", () => {
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
jest.spyOn(Server, "isServerSide").mockReturnValue(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should return null if no cached result", () => {
|
|
128
|
+
// Arrange
|
|
129
|
+
const fakeHandler = jest.fn();
|
|
130
|
+
|
|
131
|
+
// Act
|
|
132
|
+
const {
|
|
133
|
+
result: {current: result},
|
|
134
|
+
} = clientRenderHook(() => useServerEffect("ID", fakeHandler));
|
|
135
|
+
|
|
136
|
+
// Assert
|
|
137
|
+
expect(result).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should return data cached result", () => {
|
|
141
|
+
// Arrange
|
|
142
|
+
const fakeHandler = jest.fn();
|
|
143
|
+
jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
|
|
144
|
+
data: "DATA",
|
|
145
|
+
error: null,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Act
|
|
149
|
+
const {
|
|
150
|
+
result: {current: result},
|
|
151
|
+
} = clientRenderHook(() => useServerEffect("ID", fakeHandler));
|
|
152
|
+
|
|
153
|
+
// Assert
|
|
154
|
+
expect(result).toEqual({data: "DATA", error: null});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should return error cached result", () => {
|
|
158
|
+
// Arrange
|
|
159
|
+
const fakeHandler = jest.fn();
|
|
160
|
+
jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
|
|
161
|
+
data: null,
|
|
162
|
+
error: "ERROR",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Act
|
|
166
|
+
const {
|
|
167
|
+
result: {current: result},
|
|
168
|
+
} = clientRenderHook(() => useServerEffect("ID", fakeHandler));
|
|
169
|
+
|
|
170
|
+
// Assert
|
|
171
|
+
expect(result).toEqual({
|
|
172
|
+
data: null,
|
|
173
|
+
error: "ERROR",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should not track the request", () => {
|
|
178
|
+
// Arrange
|
|
179
|
+
const fakeHandler = jest.fn().mockReturnValue(
|
|
180
|
+
new Promise(() => {
|
|
181
|
+
/*prevent act() warning*/
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
const trackDataRequestSpy = jest.spyOn(
|
|
185
|
+
RequestTracker.Default,
|
|
186
|
+
"trackDataRequest",
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Act
|
|
190
|
+
clientRenderHook(() => useServerEffect("ID", fakeHandler), {
|
|
191
|
+
wrapper: TrackData,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Assert
|
|
195
|
+
expect(trackDataRequestSpy).not.toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should not request fulfillment", () => {
|
|
199
|
+
// Arrange
|
|
200
|
+
const fakeHandler = jest.fn().mockReturnValue(
|
|
201
|
+
new Promise(() => {
|
|
202
|
+
/*prevent act() warning*/
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
const fulfillRequestSpy = jest.spyOn(
|
|
206
|
+
RequestFulfillment.Default,
|
|
207
|
+
"fulfill",
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Act
|
|
211
|
+
clientRenderHook(() => useServerEffect("ID", fakeHandler));
|
|
212
|
+
|
|
213
|
+
// Assert
|
|
214
|
+
expect(fulfillRequestSpy).not.toHaveBeenCalled();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
|
|
3
|
+
|
|
4
|
+
import {useSharedCache, clearSharedCache} from "../use-shared-cache.js";
|
|
5
|
+
|
|
6
|
+
describe("#useSharedCache", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
clearSharedCache();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it.each`
|
|
12
|
+
id
|
|
13
|
+
${null}
|
|
14
|
+
${""}
|
|
15
|
+
${5}
|
|
16
|
+
${() => "BOO"}
|
|
17
|
+
`("should throw if the id is $id", ({id}) => {
|
|
18
|
+
// Arrange
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
const {result} = clientRenderHook(() => useSharedCache(id, "scope"));
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(result.error).toMatchSnapshot();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each`
|
|
28
|
+
scope
|
|
29
|
+
${null}
|
|
30
|
+
${""}
|
|
31
|
+
${5}
|
|
32
|
+
${() => "BOO"}
|
|
33
|
+
`("should throw if the scope is $scope", ({scope}) => {
|
|
34
|
+
// Arrange
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
const {result} = clientRenderHook(() => useSharedCache("id", scope));
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(result.error).toMatchSnapshot();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return a tuple of two items", () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
const {
|
|
48
|
+
result: {current: result},
|
|
49
|
+
} = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
50
|
+
|
|
51
|
+
// Assert
|
|
52
|
+
expect(result).toBeArrayOfSize(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("tuple[0] - currentValue", () => {
|
|
56
|
+
it("should be null if nothing is cached", () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
|
|
59
|
+
// Act
|
|
60
|
+
const {
|
|
61
|
+
result: {current: result},
|
|
62
|
+
} = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(result[0]).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should match initialValue when provided as a non-function", () => {
|
|
69
|
+
// Arrange
|
|
70
|
+
|
|
71
|
+
// Act
|
|
72
|
+
const {
|
|
73
|
+
result: {current: result},
|
|
74
|
+
} = clientRenderHook(() =>
|
|
75
|
+
useSharedCache("id", "scope", "INITIAL VALUE"),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect(result[0]).toBe("INITIAL VALUE");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should match the return of initialValue when provided as non-function", () => {
|
|
83
|
+
// Arrange
|
|
84
|
+
|
|
85
|
+
// Act
|
|
86
|
+
const {
|
|
87
|
+
result: {current: result},
|
|
88
|
+
} = clientRenderHook(() =>
|
|
89
|
+
useSharedCache("id", "scope", () => "INITIAL VALUE"),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Assert
|
|
93
|
+
expect(result[0]).toBe("INITIAL VALUE");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("tuple[1] - setValue", () => {
|
|
98
|
+
it("should be a function", () => {
|
|
99
|
+
// Arrange
|
|
100
|
+
|
|
101
|
+
// Act
|
|
102
|
+
const {
|
|
103
|
+
result: {current: result},
|
|
104
|
+
} = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
105
|
+
|
|
106
|
+
// Assert
|
|
107
|
+
expect(result[1]).toBeFunction();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should be the same function if the id and scope remain the same", () => {
|
|
111
|
+
// Arrange
|
|
112
|
+
const wrapper = clientRenderHook(
|
|
113
|
+
({id, scope}) => useSharedCache(id, scope),
|
|
114
|
+
{initialProps: {id: "id", scope: "scope"}},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Act
|
|
118
|
+
wrapper.rerender({
|
|
119
|
+
id: "id",
|
|
120
|
+
scope: "scope",
|
|
121
|
+
});
|
|
122
|
+
const result1 = wrapper.result.all[wrapper.result.all.length - 2];
|
|
123
|
+
const result2 = wrapper.result.current;
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
126
|
+
// $FlowIgnore[prop-missing]
|
|
127
|
+
expect(result1[1]).toBe(result2[1]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should be a new function if the id changes", () => {
|
|
131
|
+
// Arrange
|
|
132
|
+
const wrapper = clientRenderHook(
|
|
133
|
+
({id}) => useSharedCache(id, "scope"),
|
|
134
|
+
{
|
|
135
|
+
initialProps: {id: "id"},
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Act
|
|
140
|
+
wrapper.rerender({id: "new-id"});
|
|
141
|
+
const result1 = wrapper.result.all[wrapper.result.all.length - 2];
|
|
142
|
+
const result2 = wrapper.result.current;
|
|
143
|
+
|
|
144
|
+
// Assert
|
|
145
|
+
// $FlowIgnore[prop-missing]
|
|
146
|
+
expect(result1[1]).not.toBe(result2[1]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should be a new function if the scope changes", () => {
|
|
150
|
+
// Arrange
|
|
151
|
+
const wrapper = clientRenderHook(
|
|
152
|
+
({scope}) => useSharedCache("id", scope),
|
|
153
|
+
{
|
|
154
|
+
initialProps: {scope: "scope"},
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Act
|
|
159
|
+
wrapper.rerender({scope: "new-scope"});
|
|
160
|
+
const result1 = wrapper.result.all[wrapper.result.all.length - 2];
|
|
161
|
+
const result2 = wrapper.result.current;
|
|
162
|
+
|
|
163
|
+
// Assert
|
|
164
|
+
// $FlowIgnore[prop-missing]
|
|
165
|
+
expect(result1[1]).not.toBe(result2[1]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should set the value in the cache", () => {
|
|
169
|
+
// Arrange
|
|
170
|
+
const wrapper = clientRenderHook(() =>
|
|
171
|
+
useSharedCache("id", "scope"),
|
|
172
|
+
);
|
|
173
|
+
const setValue = wrapper.result.current[1];
|
|
174
|
+
|
|
175
|
+
// Act
|
|
176
|
+
setValue("CACHED_VALUE");
|
|
177
|
+
// Rerender so the hook retrieves this new value.
|
|
178
|
+
wrapper.rerender();
|
|
179
|
+
const result = wrapper.result.current[0];
|
|
180
|
+
|
|
181
|
+
// Assert
|
|
182
|
+
expect(result).toBe("CACHED_VALUE");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it.each`
|
|
186
|
+
value
|
|
187
|
+
${undefined}
|
|
188
|
+
${null}
|
|
189
|
+
`("should purge the value from the cache if $value", ({value}) => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const wrapper = clientRenderHook(() =>
|
|
192
|
+
useSharedCache("id", "scope"),
|
|
193
|
+
);
|
|
194
|
+
const setValue = wrapper.result.current[1];
|
|
195
|
+
setValue("CACHED_VALUE");
|
|
196
|
+
|
|
197
|
+
// Act
|
|
198
|
+
// Rerender so the result has the cached value.
|
|
199
|
+
wrapper.rerender();
|
|
200
|
+
setValue(value);
|
|
201
|
+
// Rerender so the hook retrieves this new value.
|
|
202
|
+
wrapper.rerender();
|
|
203
|
+
const result = wrapper.result.current[0];
|
|
204
|
+
|
|
205
|
+
// Assert
|
|
206
|
+
expect(result).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should share cache across all uses", () => {
|
|
211
|
+
// Arrange
|
|
212
|
+
const hook1 = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
213
|
+
const hook2 = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
214
|
+
hook1.result.current[1]("VALUE_1");
|
|
215
|
+
|
|
216
|
+
// Act
|
|
217
|
+
hook2.rerender();
|
|
218
|
+
const result = hook2.result.current[0];
|
|
219
|
+
|
|
220
|
+
// Assert
|
|
221
|
+
expect(result).toBe("VALUE_1");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it.each`
|
|
225
|
+
id
|
|
226
|
+
${"id1"}
|
|
227
|
+
${"id2"}
|
|
228
|
+
`("should not share cache if scope is different", ({id}) => {
|
|
229
|
+
// Arrange
|
|
230
|
+
const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
|
|
231
|
+
const hook2 = clientRenderHook(() => useSharedCache(id, "scope2"));
|
|
232
|
+
hook1.result.current[1]("VALUE_1");
|
|
233
|
+
|
|
234
|
+
// Act
|
|
235
|
+
hook2.rerender();
|
|
236
|
+
const result = hook2.result.current[0];
|
|
237
|
+
|
|
238
|
+
// Assert
|
|
239
|
+
expect(result).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it.each`
|
|
243
|
+
scope
|
|
244
|
+
${"scope1"}
|
|
245
|
+
${"scope2"}
|
|
246
|
+
`("should not share cache if id is different", ({scope}) => {
|
|
247
|
+
// Arrange
|
|
248
|
+
const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
|
|
249
|
+
const hook2 = clientRenderHook(() => useSharedCache("id2", scope));
|
|
250
|
+
hook1.result.current[1]("VALUE_1");
|
|
251
|
+
|
|
252
|
+
// Act
|
|
253
|
+
hook2.rerender();
|
|
254
|
+
const result = hook2.result.current[0];
|
|
255
|
+
|
|
256
|
+
// Assert
|
|
257
|
+
expect(result).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("#clearSharedCache", () => {
|
|
262
|
+
beforeEach(() => {
|
|
263
|
+
clearSharedCache();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should clear the entire cache if no scope given", () => {
|
|
267
|
+
// Arrange
|
|
268
|
+
const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
|
|
269
|
+
const hook2 = clientRenderHook(() => useSharedCache("id2", "scope2"));
|
|
270
|
+
hook1.result.current[1]("VALUE_1");
|
|
271
|
+
hook2.result.current[1]("VALUE_2");
|
|
272
|
+
// Make sure both hook results include the updated value.
|
|
273
|
+
hook1.rerender();
|
|
274
|
+
hook2.rerender();
|
|
275
|
+
|
|
276
|
+
// Act
|
|
277
|
+
clearSharedCache();
|
|
278
|
+
// Make sure we refresh the hook results.
|
|
279
|
+
hook1.rerender();
|
|
280
|
+
hook2.rerender();
|
|
281
|
+
|
|
282
|
+
// Assert
|
|
283
|
+
expect(hook1.result.current[0]).toBeNull();
|
|
284
|
+
expect(hook2.result.current[0]).toBeNull();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should clear the given scope only", () => {
|
|
288
|
+
// Arrange
|
|
289
|
+
const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
|
|
290
|
+
const hook2 = clientRenderHook(() => useSharedCache("id2", "scope2"));
|
|
291
|
+
hook1.result.current[1]("VALUE_1");
|
|
292
|
+
hook2.result.current[1]("VALUE_2");
|
|
293
|
+
// Make sure both hook results include the updated value.
|
|
294
|
+
hook1.rerender();
|
|
295
|
+
hook2.rerender();
|
|
296
|
+
|
|
297
|
+
// Act
|
|
298
|
+
clearSharedCache("scope2");
|
|
299
|
+
// Make sure we refresh the hook results.
|
|
300
|
+
hook1.rerender();
|
|
301
|
+
hook2.rerender();
|
|
302
|
+
|
|
303
|
+
// Assert
|
|
304
|
+
expect(hook1.result.current[0]).toBe("VALUE_1");
|
|
305
|
+
expect(hook2.result.current[0]).toBeNull();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
import {useContext} from "react";
|
|
4
|
+
import {TrackerContext} from "../util/request-tracking.js";
|
|
5
|
+
import {SsrCache} from "../util/ssr-cache.js";
|
|
6
|
+
|
|
7
|
+
import type {CachedResponse, ValidCacheData} from "../util/types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook to perform an asynchronous action during server-side rendering.
|
|
11
|
+
*
|
|
12
|
+
* This hook registers an asynchronous action to be performed during
|
|
13
|
+
* server-side rendering. The action is performed only once, and the result
|
|
14
|
+
* is cached against the given identifier so that subsequent calls return that
|
|
15
|
+
* cached result allowing components to render more of the component.
|
|
16
|
+
*
|
|
17
|
+
* This hook requires the Wonder Blocks Data functionality for resolving
|
|
18
|
+
* pending requests, as well as support for the hydration cache to be
|
|
19
|
+
* embedded into a page so that the result can by hydrated (if that is a
|
|
20
|
+
* requirement).
|
|
21
|
+
*
|
|
22
|
+
* The asynchronous action is never invoked on the client-side.
|
|
23
|
+
*/
|
|
24
|
+
export const useServerEffect = <TData: ValidCacheData>(
|
|
25
|
+
id: string,
|
|
26
|
+
handler: () => Promise<?TData>,
|
|
27
|
+
hydrate: boolean = true,
|
|
28
|
+
): ?CachedResponse<TData> => {
|
|
29
|
+
// If we're server-side or hydrating, we'll have a cached entry to use.
|
|
30
|
+
// So we get that and use it to initialize our state.
|
|
31
|
+
// This works in both hydration and SSR because the very first call to
|
|
32
|
+
// this will have cached data in those cases as it will be present on the
|
|
33
|
+
// initial render - and subsequent renders on the client it will be null.
|
|
34
|
+
const cachedResult = SsrCache.Default.getEntry<TData>(id);
|
|
35
|
+
|
|
36
|
+
// We only track data requests when we are server-side and we don't
|
|
37
|
+
// already have a result, as given by the cachedData (which is also the
|
|
38
|
+
// initial value for the result state).
|
|
39
|
+
const maybeTrack = useContext(TrackerContext);
|
|
40
|
+
if (cachedResult == null && Server.isServerSide()) {
|
|
41
|
+
maybeTrack?.(id, handler, hydrate);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return cachedResult;
|
|
45
|
+
};
|