@khanacademy/wonder-blocks-data 3.1.1 → 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 +41 -0
- package/dist/es/index.js +375 -335
- package/dist/index.js +527 -461
- package/docs.md +17 -35
- package/package.json +3 -3
- 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/gql-router.js +1 -1
- 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-gql.test.js +1 -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-gql.js +39 -31
- 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/gql-types.js +5 -10
- 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
|
@@ -5,8 +5,6 @@ import {render} from "@testing-library/react";
|
|
|
5
5
|
import InterceptContext from "../intercept-context.js";
|
|
6
6
|
import InterceptData from "../intercept-data.js";
|
|
7
7
|
|
|
8
|
-
import type {IRequestHandler} from "../../util/types.js";
|
|
9
|
-
|
|
10
8
|
describe("InterceptData", () => {
|
|
11
9
|
afterEach(() => {
|
|
12
10
|
jest.resetAllMocks();
|
|
@@ -14,15 +12,10 @@ describe("InterceptData", () => {
|
|
|
14
12
|
|
|
15
13
|
it("should update context with fulfillRequest method", () => {
|
|
16
14
|
// Arrange
|
|
17
|
-
const fakeHandler
|
|
18
|
-
fulfillRequest: () => Promise.resolve("data"),
|
|
19
|
-
getKey: (o) => o,
|
|
20
|
-
type: "MY_HANDLER",
|
|
21
|
-
hydrate: true,
|
|
22
|
-
};
|
|
15
|
+
const fakeHandler = () => Promise.resolve("data");
|
|
23
16
|
const props = {
|
|
24
17
|
handler: fakeHandler,
|
|
25
|
-
|
|
18
|
+
requestId: "ID",
|
|
26
19
|
};
|
|
27
20
|
const captureContextFn = jest.fn();
|
|
28
21
|
|
|
@@ -38,36 +31,21 @@ describe("InterceptData", () => {
|
|
|
38
31
|
// Assert
|
|
39
32
|
expect(captureContextFn).toHaveBeenCalledWith(
|
|
40
33
|
expect.objectContaining({
|
|
41
|
-
|
|
42
|
-
fulfillRequest: props.fulfillRequest,
|
|
43
|
-
},
|
|
34
|
+
ID: props.handler,
|
|
44
35
|
}),
|
|
45
36
|
);
|
|
46
37
|
});
|
|
47
38
|
|
|
48
39
|
it("should override parent InterceptData", () => {
|
|
49
40
|
// Arrange
|
|
50
|
-
const fakeHandler: IRequestHandler<string, string> = {
|
|
51
|
-
fulfillRequest: () => Promise.resolve("data"),
|
|
52
|
-
getKey: (o) => o,
|
|
53
|
-
type: "MY_HANDLER",
|
|
54
|
-
cache: null,
|
|
55
|
-
hydrate: true,
|
|
56
|
-
};
|
|
57
41
|
const fulfillRequest1Fn = jest.fn();
|
|
58
42
|
const fulfillRequest2Fn = jest.fn();
|
|
59
43
|
const captureContextFn = jest.fn();
|
|
60
44
|
|
|
61
45
|
// Act
|
|
62
46
|
render(
|
|
63
|
-
<InterceptData
|
|
64
|
-
handler={
|
|
65
|
-
fulfillRequest={fulfillRequest1Fn}
|
|
66
|
-
>
|
|
67
|
-
<InterceptData
|
|
68
|
-
handler={fakeHandler}
|
|
69
|
-
fulfillRequest={fulfillRequest2Fn}
|
|
70
|
-
>
|
|
47
|
+
<InterceptData handler={fulfillRequest1Fn} requestId="ID">
|
|
48
|
+
<InterceptData handler={fulfillRequest2Fn} requestId="ID">
|
|
71
49
|
<InterceptContext.Consumer>
|
|
72
50
|
{captureContextFn}
|
|
73
51
|
</InterceptContext.Consumer>
|
|
@@ -78,9 +56,7 @@ describe("InterceptData", () => {
|
|
|
78
56
|
// Assert
|
|
79
57
|
expect(captureContextFn).toHaveBeenCalledWith(
|
|
80
58
|
expect.objectContaining({
|
|
81
|
-
|
|
82
|
-
fulfillRequest: fulfillRequest2Fn,
|
|
83
|
-
},
|
|
59
|
+
ID: fulfillRequest2Fn,
|
|
84
60
|
}),
|
|
85
61
|
);
|
|
86
62
|
});
|
package/src/components/data.js
CHANGED
|
@@ -1,36 +1,63 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
5
|
+
import {RequestFulfillment} from "../util/request-fulfillment.js";
|
|
6
|
+
import InterceptContext from "./intercept-context.js";
|
|
7
|
+
import {useServerEffect} from "../hooks/use-server-effect.js";
|
|
8
|
+
import {resultFromCachedResponse} from "../util/result-from-cache-response.js";
|
|
5
9
|
|
|
6
|
-
import type {Result,
|
|
10
|
+
import type {Result, ValidCacheData} from "../util/types.js";
|
|
7
11
|
|
|
8
12
|
type Props<
|
|
9
|
-
/**
|
|
10
|
-
* The type of options that the handler requires to define a request.
|
|
11
|
-
*/
|
|
12
|
-
TOptions,
|
|
13
13
|
/**
|
|
14
14
|
* The type of data resolved by the handler's fulfillRequest method.
|
|
15
15
|
*/
|
|
16
|
-
TData,
|
|
16
|
+
TData: ValidCacheData,
|
|
17
17
|
> = {|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* A unique identifier for the request.
|
|
20
|
+
*
|
|
21
|
+
* This should not be shared by other uses of this component.
|
|
22
|
+
*/
|
|
23
|
+
requestId: string,
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This defines how the request is fulfilled.
|
|
21
27
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
28
|
+
* If this is changed without changing the ID, there are cases where the
|
|
29
|
+
* old handler result may be given. This is not a supported mode of
|
|
30
|
+
* operation.
|
|
24
31
|
*/
|
|
25
|
-
handler:
|
|
32
|
+
handler: () => Promise<?TData>,
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
|
-
*
|
|
35
|
+
* When true, the result will be hydrated when client-side. Otherwise,
|
|
36
|
+
* the request will be fulfilled for us in SSR but will be ignored during
|
|
37
|
+
* hydration. Only set this to false if you know some other mechanism
|
|
38
|
+
* will be performing hydration (such as if requests are fulfilled by
|
|
39
|
+
* Apollo Client but you consolidated all SSR requests using WB Data).
|
|
29
40
|
*
|
|
30
|
-
*
|
|
31
|
-
|
|
41
|
+
* Defaults to true.
|
|
42
|
+
*/
|
|
43
|
+
hydrate?: boolean,
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* When true, the children will be rendered with the existing result
|
|
47
|
+
* until the pending load is completed. Otherwise, the children will be
|
|
48
|
+
* given a loading state until the request is fulfilled.
|
|
49
|
+
*
|
|
50
|
+
* Defaults to false.
|
|
51
|
+
*/
|
|
52
|
+
showOldDataWhileLoading?: boolean,
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* When true, the handler will always be invoked after hydration.
|
|
56
|
+
* This defaults to false.
|
|
57
|
+
* NOTE: The request is invoked after hydration if the hydrated result
|
|
58
|
+
* is an error.
|
|
32
59
|
*/
|
|
33
|
-
|
|
60
|
+
alwaysRequestOnHydration?: boolean,
|
|
34
61
|
|
|
35
62
|
/**
|
|
36
63
|
* A function that will render the content of this component using the
|
|
@@ -45,10 +72,115 @@ type Props<
|
|
|
45
72
|
* requirements can be placed in a React application in a manner that will
|
|
46
73
|
* support server-side rendering and efficient caching.
|
|
47
74
|
*/
|
|
48
|
-
const Data = <
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
const Data = <TData: ValidCacheData>({
|
|
76
|
+
requestId,
|
|
77
|
+
handler,
|
|
78
|
+
children,
|
|
79
|
+
hydrate,
|
|
80
|
+
showOldDataWhileLoading,
|
|
81
|
+
alwaysRequestOnHydration,
|
|
82
|
+
}: Props<TData>): React.Node => {
|
|
83
|
+
// Lookup to see if there's an interceptor for the handler.
|
|
84
|
+
// If we have one, we need to replace the handler with one that
|
|
85
|
+
// uses the interceptor.
|
|
86
|
+
const interceptorMap = React.useContext(InterceptContext);
|
|
87
|
+
|
|
88
|
+
// If we have an interceptor, we need to replace the handler with one
|
|
89
|
+
// that uses the interceptor. This helper function generates a new
|
|
90
|
+
// handler.
|
|
91
|
+
const maybeInterceptedHandler = React.useMemo(() => {
|
|
92
|
+
const interceptor = interceptorMap[requestId];
|
|
93
|
+
if (interceptor == null) {
|
|
94
|
+
return handler;
|
|
95
|
+
}
|
|
96
|
+
return () => interceptor() ?? handler();
|
|
97
|
+
}, [handler, interceptorMap, requestId]);
|
|
98
|
+
|
|
99
|
+
const hydrateResult = useServerEffect(
|
|
100
|
+
requestId,
|
|
101
|
+
maybeInterceptedHandler,
|
|
102
|
+
hydrate,
|
|
103
|
+
);
|
|
104
|
+
const [currentResult, setResult] = React.useState(hydrateResult);
|
|
105
|
+
|
|
106
|
+
// Here we make sure the request still occurs client-side as needed.
|
|
107
|
+
// This is for legacy usage that expects this. Eventually we will want
|
|
108
|
+
// to deprecate.
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
// This is here until I can do a better documentation example for
|
|
111
|
+
// the TrackData docs.
|
|
112
|
+
// istanbul ignore next
|
|
113
|
+
if (Server.isServerSide()) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// We don't bother with this if we have hydration data and we're not
|
|
118
|
+
// forcing a request on hydration.
|
|
119
|
+
// We don't care if these things change after the first render,
|
|
120
|
+
// so we don't want them in the inputs array.
|
|
121
|
+
if (!alwaysRequestOnHydration && hydrateResult?.data != null) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// If we're not hydrating a result and we're not going to render
|
|
126
|
+
// with old data until we're loaded, we want to make sure we set our
|
|
127
|
+
// result to null so that we're in the loading state.
|
|
128
|
+
if (!showOldDataWhileLoading) {
|
|
129
|
+
// Mark ourselves as loading.
|
|
130
|
+
setResult(null);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// We aren't server-side, so let's make the request.
|
|
134
|
+
// We don't need to use our built-in request fulfillment here if we
|
|
135
|
+
// don't want, but it does mean we'll share inflight requests for the
|
|
136
|
+
// same ID and the result will be in the same format as the
|
|
137
|
+
// hydrated value.
|
|
138
|
+
let cancel = false;
|
|
139
|
+
RequestFulfillment.Default.fulfill(requestId, {
|
|
140
|
+
handler: maybeInterceptedHandler,
|
|
141
|
+
})
|
|
142
|
+
.then((result) => {
|
|
143
|
+
if (cancel) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
setResult(result);
|
|
147
|
+
return;
|
|
148
|
+
})
|
|
149
|
+
.catch((e) => {
|
|
150
|
+
if (cancel) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* We should never get here as errors in fulfillment are part
|
|
155
|
+
* of the `then`, but if we do.
|
|
156
|
+
*/
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.error(
|
|
159
|
+
`Unexpected error occurred during data fulfillment: ${e}`,
|
|
160
|
+
);
|
|
161
|
+
setResult({
|
|
162
|
+
error: typeof e === "string" ? e : e.message,
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return () => {
|
|
168
|
+
cancel = true;
|
|
169
|
+
};
|
|
170
|
+
// If the handler changes, we don't care. The ID is what indicates
|
|
171
|
+
// the request that should be made and folks shouldn't be changing the
|
|
172
|
+
// handler without changing the ID as well.
|
|
173
|
+
// In addition, we don't want to include hydrateResult nor
|
|
174
|
+
// alwaysRequestOnHydration as them changinng after the first pass
|
|
175
|
+
// is irrelevant.
|
|
176
|
+
// Finally, we don't want to include showOldDataWhileLoading as that
|
|
177
|
+
// changing on its own is also not relevant. It only matters if the
|
|
178
|
+
// request itself changes. All of which is to say that we only
|
|
179
|
+
// run this effect for the ID changing.
|
|
180
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
181
|
+
}, [requestId]);
|
|
182
|
+
|
|
183
|
+
return children(resultFromCachedResponse(currentResult));
|
|
53
184
|
};
|
|
185
|
+
|
|
54
186
|
export default Data;
|
package/src/components/data.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
The `Data` component is the frontend piece of our data architecture
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
The `Data` component is the frontend piece of our data architecture.
|
|
2
|
+
It describes a data requirement in terms of a handler and an identifier.
|
|
3
|
+
It also has props to govern hydrate behavior as well as loading and client-side
|
|
4
|
+
request behavior.
|
|
5
5
|
|
|
6
6
|
The handler is responsible for fulfilling the request when asked to do so.
|
|
7
7
|
|
|
@@ -40,49 +40,30 @@ data or an error, we re-render.
|
|
|
40
40
|
```jsx
|
|
41
41
|
import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
|
|
42
42
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
43
|
-
import {Data
|
|
43
|
+
import {Data} from "@khanacademy/wonder-blocks-data";
|
|
44
44
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
45
45
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
46
46
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
fulfillRequest(options) {
|
|
54
|
-
return new Promise((resolve, reject) =>
|
|
55
|
-
setTimeout(() => resolve("I'm DATA from a request"), 3000),
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
class MyInvalidHandler extends RequestHandler {
|
|
61
|
-
constructor() {
|
|
62
|
-
super("CACHE_MISS_HANDLER_ERROR");
|
|
63
|
-
}
|
|
48
|
+
const myValidHandler = () => new Promise((resolve, reject) =>
|
|
49
|
+
setTimeout(() => resolve("I'm DATA from a request"), 3000),
|
|
50
|
+
);
|
|
64
51
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const valid = new MyValidHandler();
|
|
73
|
-
const invalid = new MyInvalidHandler();
|
|
52
|
+
const myInvalidHandler = () => new Promise((resolve, reject) =>
|
|
53
|
+
setTimeout(() => reject("I'm an ERROR from a request"), 3000),
|
|
54
|
+
);
|
|
74
55
|
|
|
75
56
|
<View>
|
|
76
57
|
<View>
|
|
77
58
|
<Body>This request will succeed and give us data!</Body>
|
|
78
|
-
<Data handler={
|
|
79
|
-
{(
|
|
80
|
-
if (loading) {
|
|
59
|
+
<Data handler={myValidHandler} requestId="VALID">
|
|
60
|
+
{(result) => {
|
|
61
|
+
if (result.status === "loading") {
|
|
81
62
|
return "Loading...";
|
|
82
63
|
}
|
|
83
64
|
|
|
84
65
|
return (
|
|
85
|
-
<BodyMonospace>{data}</BodyMonospace>
|
|
66
|
+
<BodyMonospace>{result.data}</BodyMonospace>
|
|
86
67
|
);
|
|
87
68
|
}}
|
|
88
69
|
</Data>
|
|
@@ -90,14 +71,14 @@ const invalid = new MyInvalidHandler();
|
|
|
90
71
|
<Strut size={Spacing.small_12} />
|
|
91
72
|
<View>
|
|
92
73
|
<Body>This request will go boom and give us an error!</Body>
|
|
93
|
-
<Data handler={
|
|
94
|
-
{(
|
|
95
|
-
if (loading) {
|
|
74
|
+
<Data handler={myInvalidHandler} requestId="INVALID">
|
|
75
|
+
{(result) => {
|
|
76
|
+
if (result.status === "loading") {
|
|
96
77
|
return "Loading...";
|
|
97
78
|
}
|
|
98
79
|
|
|
99
80
|
return (
|
|
100
|
-
<BodyMonospace style={{color: Color.red}}>ERROR: {error}</BodyMonospace>
|
|
81
|
+
<BodyMonospace style={{color: Color.red}}>ERROR: {result.error}</BodyMonospace>
|
|
101
82
|
);
|
|
102
83
|
}}
|
|
103
84
|
</Data>
|
|
@@ -114,49 +95,37 @@ populated using the `initializeCache` method before rendering.
|
|
|
114
95
|
```jsx
|
|
115
96
|
import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
|
|
116
97
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
117
|
-
import {Data,
|
|
98
|
+
import {Data, initializeCache} from "@khanacademy/wonder-blocks-data";
|
|
118
99
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
119
100
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
120
101
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
121
102
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* fulfillRequest should not get called as we already have data cached.
|
|
129
|
-
*/
|
|
130
|
-
fulfillRequest(options) {
|
|
131
|
-
throw new Error(
|
|
132
|
-
"If you're seeing this error, the examples are broken and data isn't in the cache that should be.",
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
103
|
+
const myHandler = () => {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"If you're seeing this error, the examples are broken and data isn't in the cache that should be.",
|
|
106
|
+
);
|
|
107
|
+
};
|
|
136
108
|
|
|
137
|
-
const handler = new MyHandler();
|
|
138
109
|
initializeCache({
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
error: "I'm an ERROR from hydration cache"
|
|
145
|
-
}
|
|
110
|
+
DATA: {
|
|
111
|
+
data: "I'm DATA from the hydration cache"
|
|
112
|
+
},
|
|
113
|
+
ERROR: {
|
|
114
|
+
error: "I'm an ERROR from hydration cache"
|
|
146
115
|
}
|
|
147
116
|
});
|
|
148
117
|
|
|
149
118
|
<View>
|
|
150
119
|
<View>
|
|
151
120
|
<Body>This cache has data!</Body>
|
|
152
|
-
<Data handler={
|
|
153
|
-
{(
|
|
154
|
-
if (
|
|
121
|
+
<Data handler={myHandler} requestId="DATA">
|
|
122
|
+
{(result) => {
|
|
123
|
+
if (result.status !== "success") {
|
|
155
124
|
return "If you see this, the example is broken!";
|
|
156
125
|
}
|
|
157
126
|
|
|
158
127
|
return (
|
|
159
|
-
<BodyMonospace>{data}</BodyMonospace>
|
|
128
|
+
<BodyMonospace>{result.data}</BodyMonospace>
|
|
160
129
|
);
|
|
161
130
|
}}
|
|
162
131
|
</Data>
|
|
@@ -164,14 +133,14 @@ initializeCache({
|
|
|
164
133
|
<Strut size={Spacing.small_12} />
|
|
165
134
|
<View>
|
|
166
135
|
<Body>This cache has error!</Body>
|
|
167
|
-
<Data handler={
|
|
168
|
-
{(
|
|
169
|
-
if (
|
|
136
|
+
<Data handler={myHandler} requestId="ERROR">
|
|
137
|
+
{(result) => {
|
|
138
|
+
if (result.status !== "error") {
|
|
170
139
|
return "If you see this, the example is broken!";
|
|
171
140
|
}
|
|
172
141
|
|
|
173
142
|
return (
|
|
174
|
-
<BodyMonospace style={{color: Color.red}}>ERROR: {error}</BodyMonospace>
|
|
143
|
+
<BodyMonospace style={{color: Color.red}}>ERROR: {result.error}</BodyMonospace>
|
|
175
144
|
);
|
|
176
145
|
}}
|
|
177
146
|
</Data>
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import type {ValidCacheData} from "../util/types.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
type InterceptContextData = {
|
|
6
|
+
[id: string]: <TData: ValidCacheData>() => ?Promise<?TData>,
|
|
7
|
+
...
|
|
8
|
+
};
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
|
-
* InterceptContext defines a map from
|
|
11
|
+
* InterceptContext defines a map from request ID to interception methods.
|
|
8
12
|
*
|
|
9
13
|
* INTERNAL USE ONLY
|
|
10
14
|
*/
|
|
@@ -3,75 +3,64 @@ import * as React from "react";
|
|
|
3
3
|
|
|
4
4
|
import InterceptContext from "./intercept-context.js";
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
ValidData,
|
|
8
|
-
IRequestHandler,
|
|
9
|
-
InterceptFulfillRequestFn,
|
|
10
|
-
} from "../util/types.js";
|
|
6
|
+
import type {ValidCacheData} from "../util/types.js";
|
|
11
7
|
|
|
12
|
-
type Props<
|
|
8
|
+
type Props<TData: ValidCacheData> = {|
|
|
13
9
|
/**
|
|
14
|
-
*
|
|
10
|
+
* The ID of the request to intercept.
|
|
15
11
|
*/
|
|
16
|
-
|
|
12
|
+
requestId: string,
|
|
17
13
|
|
|
18
14
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* this component (unless another `InterceptData` component overrides this
|
|
23
|
-
* one).
|
|
15
|
+
* Called to intercept and fulfill the request.
|
|
16
|
+
* If this returns null, the request will be fulfilled by the
|
|
17
|
+
* handler of the original request being intercepted.
|
|
24
18
|
*/
|
|
25
|
-
|
|
19
|
+
handler: () => ?Promise<?TData>,
|
|
26
20
|
|
|
27
21
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
22
|
+
* The children to render within this component. Any requests by `Data`
|
|
23
|
+
* components that use same ID as this component will be intercepted.
|
|
24
|
+
* (unless another `InterceptData` component overrides this one).
|
|
31
25
|
*/
|
|
32
|
-
|
|
26
|
+
children: React.Node,
|
|
33
27
|
|};
|
|
34
28
|
|
|
35
29
|
/**
|
|
36
|
-
* This component provides a mechanism to intercept
|
|
37
|
-
*
|
|
38
|
-
* useful for testing.
|
|
30
|
+
* This component provides a mechanism to intercept data requests.
|
|
31
|
+
* This is for use in testing.
|
|
39
32
|
*
|
|
40
33
|
* This component is not recommended for use in production code as it
|
|
41
34
|
* can prevent predictable functioning of the Wonder Blocks Data framework.
|
|
42
35
|
* One possible side-effect is that inflight requests from the interceptor could
|
|
43
|
-
* be picked up by `Data` component requests
|
|
44
|
-
*
|
|
36
|
+
* be picked up by `Data` component requests from outside the children of this
|
|
37
|
+
* component.
|
|
45
38
|
*
|
|
46
39
|
* These components do not chain. If a different `InterceptData` instance is
|
|
47
|
-
* rendered within this one that intercepts the same
|
|
40
|
+
* rendered within this one that intercepts the same id, then that
|
|
48
41
|
* new instance will replace this interceptor for its children. All methods
|
|
49
42
|
* will be replaced.
|
|
50
43
|
*/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</InterceptContext.Consumer>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
44
|
+
const InterceptData = <TData: ValidCacheData>({
|
|
45
|
+
requestId,
|
|
46
|
+
handler,
|
|
47
|
+
children,
|
|
48
|
+
}: Props<TData>): React.Node => {
|
|
49
|
+
const interceptMap = React.useContext(InterceptContext);
|
|
50
|
+
|
|
51
|
+
const updatedInterceptMap = React.useMemo(
|
|
52
|
+
() => ({
|
|
53
|
+
...interceptMap,
|
|
54
|
+
[requestId]: handler,
|
|
55
|
+
}),
|
|
56
|
+
[interceptMap, requestId, handler],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<InterceptContext.Provider value={updatedInterceptMap}>
|
|
61
|
+
{children}
|
|
62
|
+
</InterceptContext.Provider>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default InterceptData;
|