@khanacademy/wonder-blocks-data 2.3.1 → 3.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 +7 -0
- package/dist/es/index.js +212 -446
- package/dist/index.js +553 -729
- package/docs.md +19 -13
- package/package.json +6 -7
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +40 -160
- package/src/__tests__/generated-snapshot.test.js +15 -195
- package/src/components/__tests__/data.test.js +159 -965
- package/src/components/__tests__/intercept-data.test.js +9 -66
- package/src/components/__tests__/track-data.test.js +6 -5
- package/src/components/data.js +9 -119
- package/src/components/data.md +38 -60
- package/src/components/intercept-context.js +2 -3
- package/src/components/intercept-data.js +2 -34
- package/src/components/intercept-data.md +7 -105
- package/src/hooks/__tests__/use-data.test.js +790 -0
- package/src/hooks/use-data.js +138 -0
- package/src/index.js +1 -3
- package/src/util/__tests__/memory-cache.test.js +134 -35
- package/src/util/__tests__/request-fulfillment.test.js +21 -36
- package/src/util/__tests__/request-handler.test.js +30 -30
- package/src/util/__tests__/request-tracking.test.js +29 -30
- package/src/util/__tests__/response-cache.test.js +521 -561
- package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
- package/src/util/memory-cache.js +20 -15
- package/src/util/request-fulfillment.js +4 -0
- package/src/util/request-handler.js +4 -28
- package/src/util/request-handler.md +0 -32
- package/src/util/request-tracking.js +2 -3
- package/src/util/response-cache.js +50 -110
- package/src/util/result-from-cache-entry.js +38 -0
- package/src/util/types.js +14 -35
- package/LICENSE +0 -21
- package/src/components/__tests__/intercept-cache.test.js +0 -124
- package/src/components/__tests__/internal-data.test.js +0 -1030
- package/src/components/intercept-cache.js +0 -79
- package/src/components/intercept-cache.md +0 -103
- package/src/components/internal-data.js +0 -219
- package/src/util/__tests__/no-cache.test.js +0 -112
- package/src/util/no-cache.js +0 -66
- package/src/util/no-cache.md +0 -66
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import {
|
|
3
|
+
import {render} from "@testing-library/react";
|
|
4
4
|
|
|
5
5
|
import InterceptContext from "../intercept-context.js";
|
|
6
6
|
import InterceptData from "../intercept-data.js";
|
|
7
|
-
import InterceptCache from "../intercept-cache.js";
|
|
8
7
|
|
|
9
8
|
import type {IRequestHandler} from "../../util/types.js";
|
|
10
9
|
|
|
@@ -13,31 +12,22 @@ describe("InterceptData", () => {
|
|
|
13
12
|
jest.resetAllMocks();
|
|
14
13
|
});
|
|
15
14
|
|
|
16
|
-
it
|
|
17
|
-
["with only fulfillRequest method", {fulfillRequest: jest.fn()}],
|
|
18
|
-
[
|
|
19
|
-
"with only shouldRefreshCache method",
|
|
20
|
-
{shouldRefreshCache: jest.fn()},
|
|
21
|
-
],
|
|
22
|
-
[
|
|
23
|
-
"with both fulfillRequest and shouldRefreshCache methods",
|
|
24
|
-
{fulfillRequest: jest.fn(), shouldRefreshCache: jest.fn()},
|
|
25
|
-
],
|
|
26
|
-
])("should update context %s", (_, props) => {
|
|
15
|
+
it("should update context with fulfillRequest method", () => {
|
|
27
16
|
// Arrange
|
|
28
17
|
const fakeHandler: IRequestHandler<string, string> = {
|
|
29
18
|
fulfillRequest: () => Promise.resolve("data"),
|
|
30
19
|
getKey: (o) => o,
|
|
31
|
-
shouldRefreshCache: () => false,
|
|
32
20
|
type: "MY_HANDLER",
|
|
33
|
-
cache: null,
|
|
34
21
|
hydrate: true,
|
|
35
22
|
};
|
|
36
|
-
props
|
|
23
|
+
const props = {
|
|
24
|
+
handler: fakeHandler,
|
|
25
|
+
fulfillRequest: jest.fn(),
|
|
26
|
+
};
|
|
37
27
|
const captureContextFn = jest.fn();
|
|
38
28
|
|
|
39
29
|
// Act
|
|
40
|
-
|
|
30
|
+
render(
|
|
41
31
|
<InterceptData {...props}>
|
|
42
32
|
<InterceptContext.Consumer>
|
|
43
33
|
{captureContextFn}
|
|
@@ -49,8 +39,7 @@ describe("InterceptData", () => {
|
|
|
49
39
|
expect(captureContextFn).toHaveBeenCalledWith(
|
|
50
40
|
expect.objectContaining({
|
|
51
41
|
MY_HANDLER: {
|
|
52
|
-
fulfillRequest: props.fulfillRequest
|
|
53
|
-
shouldRefreshCache: props.shouldRefreshCache || null,
|
|
42
|
+
fulfillRequest: props.fulfillRequest,
|
|
54
43
|
},
|
|
55
44
|
}),
|
|
56
45
|
);
|
|
@@ -61,28 +50,23 @@ describe("InterceptData", () => {
|
|
|
61
50
|
const fakeHandler: IRequestHandler<string, string> = {
|
|
62
51
|
fulfillRequest: () => Promise.resolve("data"),
|
|
63
52
|
getKey: (o) => o,
|
|
64
|
-
shouldRefreshCache: () => false,
|
|
65
53
|
type: "MY_HANDLER",
|
|
66
54
|
cache: null,
|
|
67
55
|
hydrate: true,
|
|
68
56
|
};
|
|
69
57
|
const fulfillRequest1Fn = jest.fn();
|
|
70
|
-
const shouldRefreshCache1Fn = jest.fn();
|
|
71
58
|
const fulfillRequest2Fn = jest.fn();
|
|
72
|
-
const shouldRefreshCache2Fn = jest.fn();
|
|
73
59
|
const captureContextFn = jest.fn();
|
|
74
60
|
|
|
75
61
|
// Act
|
|
76
|
-
|
|
62
|
+
render(
|
|
77
63
|
<InterceptData
|
|
78
64
|
handler={fakeHandler}
|
|
79
65
|
fulfillRequest={fulfillRequest1Fn}
|
|
80
|
-
shouldRefreshCache={shouldRefreshCache1Fn}
|
|
81
66
|
>
|
|
82
67
|
<InterceptData
|
|
83
68
|
handler={fakeHandler}
|
|
84
69
|
fulfillRequest={fulfillRequest2Fn}
|
|
85
|
-
shouldRefreshCache={shouldRefreshCache2Fn}
|
|
86
70
|
>
|
|
87
71
|
<InterceptContext.Consumer>
|
|
88
72
|
{captureContextFn}
|
|
@@ -96,47 +80,6 @@ describe("InterceptData", () => {
|
|
|
96
80
|
expect.objectContaining({
|
|
97
81
|
MY_HANDLER: {
|
|
98
82
|
fulfillRequest: fulfillRequest2Fn,
|
|
99
|
-
shouldRefreshCache: shouldRefreshCache2Fn,
|
|
100
|
-
},
|
|
101
|
-
}),
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("should not change InterceptCache methods on existing interceptor", () => {
|
|
106
|
-
// Arrange
|
|
107
|
-
const fakeHandler: IRequestHandler<string, string> = {
|
|
108
|
-
fulfillRequest: () => Promise.resolve("data"),
|
|
109
|
-
getKey: (o) => o,
|
|
110
|
-
shouldRefreshCache: () => false,
|
|
111
|
-
type: "MY_HANDLER",
|
|
112
|
-
cache: null,
|
|
113
|
-
hydrate: true,
|
|
114
|
-
};
|
|
115
|
-
const fulfillRequestFn = jest.fn();
|
|
116
|
-
const getEntryFn = jest.fn();
|
|
117
|
-
const captureContextFn = jest.fn();
|
|
118
|
-
|
|
119
|
-
// Act
|
|
120
|
-
mount(
|
|
121
|
-
<InterceptCache handler={fakeHandler} getEntry={getEntryFn}>
|
|
122
|
-
<InterceptData
|
|
123
|
-
handler={fakeHandler}
|
|
124
|
-
fulfillRequest={fulfillRequestFn}
|
|
125
|
-
>
|
|
126
|
-
<InterceptContext.Consumer>
|
|
127
|
-
{captureContextFn}
|
|
128
|
-
</InterceptContext.Consumer>
|
|
129
|
-
</InterceptData>
|
|
130
|
-
</InterceptCache>,
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
// Assert
|
|
134
|
-
expect(captureContextFn).toHaveBeenCalledWith(
|
|
135
|
-
expect.objectContaining({
|
|
136
|
-
MY_HANDLER: {
|
|
137
|
-
fulfillRequest: fulfillRequestFn,
|
|
138
|
-
shouldRefreshCache: null,
|
|
139
|
-
getEntry: getEntryFn,
|
|
140
83
|
},
|
|
141
84
|
}),
|
|
142
85
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
4
|
-
import {
|
|
4
|
+
import {render, screen} from "@testing-library/react";
|
|
5
5
|
|
|
6
6
|
import TrackData from "../track-data.js";
|
|
7
7
|
import {RequestTracker, TrackerContext} from "../../util/request-tracking.js";
|
|
@@ -16,7 +16,7 @@ describe("TrackData", () => {
|
|
|
16
16
|
jest.spyOn(Server, "isServerSide").mockReturnValue(false);
|
|
17
17
|
|
|
18
18
|
// Act
|
|
19
|
-
const underTest = () =>
|
|
19
|
+
const underTest = () => render(<TrackData>SOME CHILDREN</TrackData>);
|
|
20
20
|
|
|
21
21
|
// Assert
|
|
22
22
|
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
@@ -29,10 +29,11 @@ describe("TrackData", () => {
|
|
|
29
29
|
jest.spyOn(Server, "isServerSide").mockReturnValue(true);
|
|
30
30
|
|
|
31
31
|
// Act
|
|
32
|
-
|
|
32
|
+
render(<TrackData>SOME CHILDREN</TrackData>);
|
|
33
|
+
const result = screen.getByText("SOME CHILDREN");
|
|
33
34
|
|
|
34
35
|
// Assert
|
|
35
|
-
expect(result).
|
|
36
|
+
expect(result).toBeInTheDocument();
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
it("should provide tracker function for tracking context", async () => {
|
|
@@ -41,7 +42,7 @@ describe("TrackData", () => {
|
|
|
41
42
|
|
|
42
43
|
// Act
|
|
43
44
|
const result = await new Promise((resolve, reject) => {
|
|
44
|
-
|
|
45
|
+
render(
|
|
45
46
|
<TrackData>
|
|
46
47
|
<TrackerContext.Consumer>
|
|
47
48
|
{(fn) => resolve(fn)}
|
package/src/components/data.js
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import InternalData from "./internal-data.js";
|
|
4
|
+
import {useData} from "../hooks/use-data.js";
|
|
6
5
|
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
CacheEntry,
|
|
11
|
-
Interceptor,
|
|
12
|
-
Result,
|
|
13
|
-
IRequestHandler,
|
|
14
|
-
ValidData,
|
|
15
|
-
} from "../util/types.js";
|
|
6
|
+
import type {Result, IRequestHandler, ValidData} from "../util/types.js";
|
|
16
7
|
|
|
17
8
|
type Props<
|
|
18
9
|
/**
|
|
@@ -54,111 +45,10 @@ type Props<
|
|
|
54
45
|
* requirements can be placed in a React application in a manner that will
|
|
55
46
|
* support server-side rendering and efficient caching.
|
|
56
47
|
*/
|
|
57
|
-
|
|
58
|
-
Props<TOptions, TData>,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!interceptor) {
|
|
65
|
-
return handler;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const {fulfillRequest, shouldRefreshCache} = interceptor;
|
|
69
|
-
const fulfillRequestFn = fulfillRequest
|
|
70
|
-
? (options: TOptions): Promise<TData> => {
|
|
71
|
-
const interceptedResult = fulfillRequest(options);
|
|
72
|
-
return interceptedResult != null
|
|
73
|
-
? interceptedResult
|
|
74
|
-
: handler.fulfillRequest(options);
|
|
75
|
-
}
|
|
76
|
-
: (options) => handler.fulfillRequest(options);
|
|
77
|
-
const shouldRefreshCacheFn = shouldRefreshCache
|
|
78
|
-
? (
|
|
79
|
-
options: TOptions,
|
|
80
|
-
cacheEntry: ?$ReadOnly<CacheEntry<TData>>,
|
|
81
|
-
): boolean => {
|
|
82
|
-
const interceptedResult = shouldRefreshCache(
|
|
83
|
-
options,
|
|
84
|
-
cacheEntry,
|
|
85
|
-
);
|
|
86
|
-
return interceptedResult != null
|
|
87
|
-
? interceptedResult
|
|
88
|
-
: handler.shouldRefreshCache(options, cacheEntry);
|
|
89
|
-
}
|
|
90
|
-
: (options, cacheEntry) =>
|
|
91
|
-
handler.shouldRefreshCache(options, cacheEntry);
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
fulfillRequest: fulfillRequestFn,
|
|
95
|
-
shouldRefreshCache: shouldRefreshCacheFn,
|
|
96
|
-
getKey: (options) => handler.getKey(options),
|
|
97
|
-
type: handler.type,
|
|
98
|
-
cache: handler.cache,
|
|
99
|
-
hydrate: handler.hydrate,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
_getCacheLookupFnFromInterceptor(
|
|
104
|
-
interceptor: ?Interceptor,
|
|
105
|
-
): $PropertyType<ResponseCache, "getEntry"> {
|
|
106
|
-
const getEntry = interceptor && interceptor.getEntry;
|
|
107
|
-
if (!getEntry) {
|
|
108
|
-
return ResponseCache.Default.getEntry;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return <TOptions, TData: ValidData>(
|
|
112
|
-
handler: IRequestHandler<TOptions, TData>,
|
|
113
|
-
options: TOptions,
|
|
114
|
-
): ?$ReadOnly<CacheEntry<TData>> => {
|
|
115
|
-
// 1. Lookup the current cache value.
|
|
116
|
-
const cacheEntry = ResponseCache.Default.getEntry<TOptions, TData>(
|
|
117
|
-
handler,
|
|
118
|
-
options,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// 2. See if our interceptor wants to override it.
|
|
122
|
-
const interceptedData = getEntry(options, cacheEntry);
|
|
123
|
-
|
|
124
|
-
// 3. Return the appropriate response.
|
|
125
|
-
return interceptedData != null ? interceptedData : cacheEntry;
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
render(): React.Node {
|
|
130
|
-
return (
|
|
131
|
-
<InterceptContext.Consumer>
|
|
132
|
-
{(value) => {
|
|
133
|
-
const handlerType = this.props.handler.type;
|
|
134
|
-
const interceptor = value[handlerType];
|
|
135
|
-
const handler = this._getHandlerFromInterceptor(
|
|
136
|
-
interceptor,
|
|
137
|
-
);
|
|
138
|
-
const getEntry = this._getCacheLookupFnFromInterceptor(
|
|
139
|
-
interceptor,
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Need to share our types with InternalData so Flow
|
|
144
|
-
* doesn't need to infer them and find mismatches.
|
|
145
|
-
* However, just deriving a new component creates issues
|
|
146
|
-
* where InternalData starts rerendering too often.
|
|
147
|
-
* Couldn't track down why, so suppressing the error
|
|
148
|
-
* instead.
|
|
149
|
-
*/
|
|
150
|
-
return (
|
|
151
|
-
<InternalData
|
|
152
|
-
// $FlowIgnore[incompatible-type-arg]
|
|
153
|
-
handler={handler}
|
|
154
|
-
options={this.props.options}
|
|
155
|
-
getEntry={getEntry}
|
|
156
|
-
>
|
|
157
|
-
{(result) => this.props.children(result)}
|
|
158
|
-
</InternalData>
|
|
159
|
-
);
|
|
160
|
-
}}
|
|
161
|
-
</InterceptContext.Consumer>
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
48
|
+
const Data = <TOptions, TData: ValidData>(
|
|
49
|
+
props: Props<TOptions, TData>,
|
|
50
|
+
): React.Node => {
|
|
51
|
+
const data = useData(props.handler, props.options);
|
|
52
|
+
return props.children(data);
|
|
53
|
+
};
|
|
54
|
+
export default Data;
|
package/src/components/data.md
CHANGED
|
@@ -3,34 +3,36 @@ most folks will use. It describes a data requirement in terms of a handler, and
|
|
|
3
3
|
some options. Handlers must implement the
|
|
4
4
|
`IRequestHandler` interface.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
asked to do so.
|
|
6
|
+
The handler is responsible for fulfilling the request when asked to do so.
|
|
8
7
|
|
|
9
|
-
####
|
|
8
|
+
#### Server-side Rendering and Hydration
|
|
10
9
|
|
|
11
|
-
The Wonder Blocks Data framework
|
|
12
|
-
|
|
13
|
-
obtained during SSR to support hydration of the SSR result.
|
|
10
|
+
The Wonder Blocks Data framework uses an in-memory cache for supporting
|
|
11
|
+
server-side rendering (SSR) and hydration.
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
handler. Custom caches allow for different client-side caching
|
|
17
|
-
strategies (such as using local storage instead of memory). Any custom cache
|
|
18
|
-
provided is ignored during SSR.
|
|
13
|
+
##### Server-side behavior
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
entry. If the custom cache returns an entry, that is used. If the custom cache
|
|
22
|
-
returns `null`, the framework will look for a corresponding in-memory entry with
|
|
23
|
-
which the framework has been initialzied, and if there, store the entry in the
|
|
24
|
-
custom cache and then return it.
|
|
15
|
+
###### Cache miss
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
When the `Data` component does not get data or an error from the cache and it
|
|
18
|
+
is rendering server-side, it tells our request tracking that it wants data, and
|
|
19
|
+
it renders in its `loading` state. It will always render in this state if there
|
|
20
|
+
is no cached response.
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
###### Cache hit
|
|
30
23
|
|
|
31
|
-
|
|
24
|
+
When the `Data` component gets data or an error from the cache and it is
|
|
25
|
+
rendering server-side, it will render as loaded, with that data or error,
|
|
26
|
+
as it would client-side. In this situation, it does not track the request it
|
|
27
|
+
would have made, as it already has the data and doesn't need to.
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
|
|
30
|
+
##### Client-side behavior
|
|
31
|
+
|
|
32
|
+
###### Cache miss
|
|
33
|
+
|
|
34
|
+
When the hydration cache does not contain the data, the data will be requested.
|
|
35
|
+
While the request is pending, the data is rendered in the loading state.
|
|
34
36
|
In this example, we use a 3 second delayed promise to simulate the request.
|
|
35
37
|
We start out without any data and so the request is made. Upon receipt of that
|
|
36
38
|
data or an error, we re-render.
|
|
@@ -103,21 +105,16 @@ const invalid = new MyInvalidHandler();
|
|
|
103
105
|
</View>
|
|
104
106
|
```
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
If the cache already contains data or an error for our request, then the `Data`
|
|
109
|
-
component will render it immediately. The cache data is placed there either
|
|
110
|
-
by prior successful requests as in the above Cache Miss example, or via calling
|
|
111
|
-
`initializeCache` before any requests have been made. A cache hit may also
|
|
112
|
-
occur due to the use of the `InterceptCache` component.
|
|
108
|
+
###### Cache hit
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
If the hydration cache already contains data or an error for our request, then
|
|
111
|
+
the `Data` component will render it immediately. The hydration cache is
|
|
112
|
+
populated using the `initializeCache` method before rendering.
|
|
116
113
|
|
|
117
114
|
```jsx
|
|
118
115
|
import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
|
|
119
116
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
120
|
-
import {
|
|
117
|
+
import {Data, RequestHandler, initializeCache} from "@khanacademy/wonder-blocks-data";
|
|
121
118
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
122
119
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
123
120
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
@@ -135,24 +132,21 @@ class MyHandler extends RequestHandler {
|
|
|
135
132
|
"If you're seeing this error, the examples are broken and data isn't in the cache that should be.",
|
|
136
133
|
);
|
|
137
134
|
}
|
|
138
|
-
|
|
139
|
-
shouldRefreshCache(options, cachedEntry) {
|
|
140
|
-
/**
|
|
141
|
-
* For our purposes, the cache never needs a refresh.
|
|
142
|
-
*/
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
135
|
}
|
|
146
136
|
|
|
147
137
|
const handler = new MyHandler();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
initializeCache({
|
|
139
|
+
CACHE_HIT_HANDLER: {
|
|
140
|
+
DATA: {
|
|
141
|
+
data: "I'm DATA from the hydration cache"
|
|
142
|
+
},
|
|
143
|
+
ERROR: {
|
|
144
|
+
error: "I'm an ERROR from hydration cache"
|
|
145
|
+
}
|
|
151
146
|
}
|
|
152
|
-
|
|
153
|
-
};
|
|
147
|
+
});
|
|
154
148
|
|
|
155
|
-
<
|
|
149
|
+
<View>
|
|
156
150
|
<View>
|
|
157
151
|
<Body>This cache has data!</Body>
|
|
158
152
|
<Data handler={handler} options={"DATA"}>
|
|
@@ -182,21 +176,5 @@ const getEntryInterceptor = function(options) {
|
|
|
182
176
|
}}
|
|
183
177
|
</Data>
|
|
184
178
|
</View>
|
|
185
|
-
</
|
|
179
|
+
</View>
|
|
186
180
|
```
|
|
187
|
-
|
|
188
|
-
#### Server-side behavior
|
|
189
|
-
|
|
190
|
-
##### Cache miss
|
|
191
|
-
|
|
192
|
-
When the `Data` component does not get data or an error from the cache and it
|
|
193
|
-
is rendering server-side, it tells our request tracking that it wants data, and
|
|
194
|
-
it renders in its `loading` state. It will always render in this state if there
|
|
195
|
-
is no cached response.
|
|
196
|
-
|
|
197
|
-
##### Cache hit
|
|
198
|
-
|
|
199
|
-
When the `Data` component gets data or an error from the cache and it is
|
|
200
|
-
rendering server-side, it will render as loaded, with that data or error,
|
|
201
|
-
as it would client-side. In this situation, it does not track the request it
|
|
202
|
-
would have made, as it already has the data and doesn't need to.
|
|
@@ -8,8 +8,7 @@ import type {InterceptContextData} from "../util/types.js";
|
|
|
8
8
|
*
|
|
9
9
|
* INTERNAL USE ONLY
|
|
10
10
|
*/
|
|
11
|
-
const InterceptContext: React.Context<InterceptContextData> =
|
|
12
|
-
{}
|
|
13
|
-
);
|
|
11
|
+
const InterceptContext: React.Context<InterceptContextData> =
|
|
12
|
+
React.createContext<InterceptContextData>({});
|
|
14
13
|
|
|
15
14
|
export default InterceptContext;
|
|
@@ -7,10 +7,9 @@ import type {
|
|
|
7
7
|
ValidData,
|
|
8
8
|
IRequestHandler,
|
|
9
9
|
InterceptFulfillRequestFn,
|
|
10
|
-
InterceptShouldRefreshCacheFn,
|
|
11
10
|
} from "../util/types.js";
|
|
12
11
|
|
|
13
|
-
type
|
|
12
|
+
type Props<TOptions, TData> = {|
|
|
14
13
|
/**
|
|
15
14
|
* A handler of the type to be intercepted.
|
|
16
15
|
*/
|
|
@@ -24,9 +23,7 @@ type BaseProps<TOptions, TData> = {|
|
|
|
24
23
|
* one).
|
|
25
24
|
*/
|
|
26
25
|
children: React.Node,
|
|
27
|
-
|};
|
|
28
26
|
|
|
29
|
-
type FulfillRequestProps<TOptions, TData> = {|
|
|
30
27
|
/**
|
|
31
28
|
* Called to fulfill a request.
|
|
32
29
|
* If this returns null, the request will be fulfilled by the
|
|
@@ -35,38 +32,11 @@ type FulfillRequestProps<TOptions, TData> = {|
|
|
|
35
32
|
fulfillRequest: InterceptFulfillRequestFn<TOptions, TData>,
|
|
36
33
|
|};
|
|
37
34
|
|
|
38
|
-
type ShouldRefreshCacheProps<TOptions, TData> = {|
|
|
39
|
-
/**
|
|
40
|
-
* Called to determine if the cache should be refreshed.
|
|
41
|
-
* If this returns null, the handler being intercepted will be asked if
|
|
42
|
-
* the cache should be refreshed.
|
|
43
|
-
*/
|
|
44
|
-
shouldRefreshCache: InterceptShouldRefreshCacheFn<TOptions, TData>,
|
|
45
|
-
|};
|
|
46
|
-
|
|
47
|
-
type Props<TOptions, TData> =
|
|
48
|
-
| {|
|
|
49
|
-
...BaseProps<TOptions, TData>,
|
|
50
|
-
...FulfillRequestProps<TOptions, TData>,
|
|
51
|
-
...ShouldRefreshCacheProps<TOptions, TData>,
|
|
52
|
-
|}
|
|
53
|
-
| {|
|
|
54
|
-
...BaseProps<TOptions, TData>,
|
|
55
|
-
...FulfillRequestProps<TOptions, TData>,
|
|
56
|
-
|}
|
|
57
|
-
| {|
|
|
58
|
-
...BaseProps<TOptions, TData>,
|
|
59
|
-
...ShouldRefreshCacheProps<TOptions, TData>,
|
|
60
|
-
|};
|
|
61
|
-
|
|
62
35
|
/**
|
|
63
36
|
* This component provides a mechanism to intercept the data requests for the
|
|
64
37
|
* type of a given handler and provide alternative results. This is mostly
|
|
65
38
|
* useful for testing.
|
|
66
39
|
*
|
|
67
|
-
* Results from this interceptor will end up in the cache. If you
|
|
68
|
-
* wish to only override the cache, use `InterceptCache` instead.
|
|
69
|
-
*
|
|
70
40
|
* This component is not recommended for use in production code as it
|
|
71
41
|
* can prevent predictable functioning of the Wonder Blocks Data framework.
|
|
72
42
|
* One possible side-effect is that inflight requests from the interceptor could
|
|
@@ -89,9 +59,7 @@ export default class InterceptData<
|
|
|
89
59
|
const handlerType = this.props.handler.type;
|
|
90
60
|
const interceptor = {
|
|
91
61
|
...value[handlerType],
|
|
92
|
-
fulfillRequest: this.props.fulfillRequest
|
|
93
|
-
shouldRefreshCache:
|
|
94
|
-
this.props.shouldRefreshCache || null,
|
|
62
|
+
fulfillRequest: this.props.fulfillRequest,
|
|
95
63
|
};
|
|
96
64
|
const newValue = {
|
|
97
65
|
...value,
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
subsequent loaded state are working correctly for your uses of `Data
|
|
3
|
-
|
|
4
|
-
manipulate request fulfillment and cache refresh using the `InterceptData`
|
|
5
|
-
component.
|
|
1
|
+
When you want to generate tests that check the loading state and
|
|
2
|
+
subsequent loaded state are working correctly for your uses of `Data` you can
|
|
3
|
+
use the `InterceptData` component.
|
|
6
4
|
|
|
7
|
-
This component takes
|
|
8
|
-
type of data requests that are to be intercepted, and
|
|
9
|
-
method, a `shouldRefreshCache` method, or both.
|
|
5
|
+
This component takes four props; children to be rendered, the handler of the
|
|
6
|
+
type of data requests that are to be intercepted, and a `fulfillRequest`.
|
|
10
7
|
|
|
11
8
|
Note that this component is expected to be used only within test cases and
|
|
12
9
|
usually only as a single instance. In flight requests for a given handler
|
|
13
10
|
type can be shared and as such, using `InterceptData` alongside non-intercepted
|
|
14
|
-
`Data` components with the same handler type can have
|
|
11
|
+
`Data` components with the same handler type can have indeterminate outcomes.
|
|
15
12
|
|
|
16
13
|
The `fulfillRequest` intercept function has the form:
|
|
17
14
|
|
|
@@ -19,7 +16,7 @@ The `fulfillRequest` intercept function has the form:
|
|
|
19
16
|
(options: TOptions) => ?Promise<TData>;
|
|
20
17
|
```
|
|
21
18
|
|
|
22
|
-
If this method
|
|
19
|
+
If this method returns `null`, the default behavior occurs. This
|
|
23
20
|
means that a request will be made for data via the handler assigned to the
|
|
24
21
|
`Data` component being intercepted.
|
|
25
22
|
|
|
@@ -66,98 +63,3 @@ const fulfillRequestInterceptor = function(options) {
|
|
|
66
63
|
</View>
|
|
67
64
|
</InterceptData>
|
|
68
65
|
```
|
|
69
|
-
|
|
70
|
-
The `shouldRefreshCache` intercept function has the form:
|
|
71
|
-
|
|
72
|
-
```js static
|
|
73
|
-
(options: TOptions, cachedEntry: ?$ReadOnly<CacheEntry<TData>>) => ?boolean;
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
If this method is omitted or returns `null`, the default behavior occurs. This
|
|
77
|
-
means that if `cacheEntry` has a value, that will be used; otherwise, the
|
|
78
|
-
`shouldRefreshCache` method of the handler assigned to the `Data` component
|
|
79
|
-
being intercepted will be invoked.
|
|
80
|
-
|
|
81
|
-
```jsx
|
|
82
|
-
import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
|
|
83
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
84
|
-
import {withActionScheduler} from "@khanacademy/wonder-blocks-timing";
|
|
85
|
-
import {InterceptData, Data, RequestHandler} from "@khanacademy/wonder-blocks-data";
|
|
86
|
-
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
87
|
-
import Color from "@khanacademy/wonder-blocks-color";
|
|
88
|
-
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
89
|
-
|
|
90
|
-
class MyHandler extends RequestHandler {
|
|
91
|
-
constructor() {
|
|
92
|
-
super("INTERCEPT_DATA_HANDLER2");
|
|
93
|
-
let _counter = 0;
|
|
94
|
-
this.fulfillRequest = function(options) {
|
|
95
|
-
return Promise.resolve(`DATA ${_counter++}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
shouldRefreshData(options) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const handler = new MyHandler();
|
|
105
|
-
const shouldRefreshCacheInterceptor = function(options) {
|
|
106
|
-
if (options === "DATA") {
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
class SchedulableExample extends React.Component {
|
|
113
|
-
constructor(props) {
|
|
114
|
-
super(props);
|
|
115
|
-
this.state = {
|
|
116
|
-
stamp: Date.now(),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
componentDidMount() {
|
|
121
|
-
this.props.schedule.interval(() => this.setState({
|
|
122
|
-
stamp: Date.now(),
|
|
123
|
-
}), 1000);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
render() {
|
|
127
|
-
/**
|
|
128
|
-
* The key on the View is what causes the re-render.
|
|
129
|
-
*
|
|
130
|
-
* The re-render causes the `Data` component to render again.
|
|
131
|
-
* That causes it to check if it should refresh the cache.
|
|
132
|
-
* and that in turn causes a new data request that updates the value
|
|
133
|
-
* being rendered.
|
|
134
|
-
*
|
|
135
|
-
* Without the key to cause the re-render, no additional request would
|
|
136
|
-
* be made.
|
|
137
|
-
*/
|
|
138
|
-
return (
|
|
139
|
-
<InterceptData handler={handler} shouldRefreshCache={shouldRefreshCacheInterceptor}>
|
|
140
|
-
<View key={this.state.stamp}>
|
|
141
|
-
<Body>This re-renders once a second. On the render, the cache is refreshed and so we see an update.</Body>
|
|
142
|
-
<Data handler={handler} options={"DATA"}>
|
|
143
|
-
{({loading, data}) => {
|
|
144
|
-
if (loading) {
|
|
145
|
-
return "If you see this, the example is broken!";
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<BodyMonospace>{data}</BodyMonospace>
|
|
150
|
-
);
|
|
151
|
-
}}
|
|
152
|
-
</Data>
|
|
153
|
-
</View>
|
|
154
|
-
</InterceptData>
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const Example = withActionScheduler(SchedulableExample);
|
|
160
|
-
|
|
161
|
-
<Example />
|
|
162
|
-
|
|
163
|
-
```
|