@khanacademy/wonder-blocks-data 3.1.2 → 5.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 +408 -349
- package/dist/index.js +568 -467
- 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 +60 -126
- package/src/components/__tests__/data.test.js +373 -313
- package/src/components/__tests__/intercept-requests.test.js +58 -0
- package/src/components/data.js +139 -21
- package/src/components/data.md +38 -69
- package/src/components/gql-router.js +1 -1
- package/src/components/intercept-context.js +6 -3
- package/src/components/intercept-requests.js +69 -0
- package/src/components/intercept-requests.md +54 -0
- 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-request-interception.test.js +255 -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-request-interception.js +54 -0
- package/src/hooks/use-server-effect.js +45 -0
- package/src/hooks/use-shared-cache.js +106 -0
- package/src/index.js +17 -20
- 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/components/__tests__/intercept-data.test.js +0 -87
- package/src/components/intercept-data.js +0 -77
- package/src/components/intercept-data.md +0 -65
- 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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
import {ScopedInMemoryCache} from "./scoped-in-memory-cache.js";
|
|
4
|
+
|
|
5
|
+
import type {ValidCacheData, CachedResponse, ResponseCache} from "./types.js";
|
|
6
|
+
|
|
7
|
+
const DefaultScope = "default";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The default instance is stored here.
|
|
11
|
+
* It's created below in the Default() static property.
|
|
12
|
+
*/
|
|
13
|
+
let _default: SsrCache;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Implements the response cache.
|
|
17
|
+
*
|
|
18
|
+
* INTERNAL USE ONLY
|
|
19
|
+
*/
|
|
20
|
+
export class SsrCache {
|
|
21
|
+
static get Default(): SsrCache {
|
|
22
|
+
if (!_default) {
|
|
23
|
+
_default = new SsrCache();
|
|
24
|
+
}
|
|
25
|
+
return _default;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_hydrationCache: ScopedInMemoryCache;
|
|
29
|
+
_ssrOnlyCache: ?ScopedInMemoryCache;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
hydrationCache: ?ScopedInMemoryCache = null,
|
|
33
|
+
ssrOnlyCache: ?ScopedInMemoryCache = null,
|
|
34
|
+
) {
|
|
35
|
+
this._ssrOnlyCache = Server.isServerSide()
|
|
36
|
+
? ssrOnlyCache || new ScopedInMemoryCache()
|
|
37
|
+
: undefined;
|
|
38
|
+
this._hydrationCache = hydrationCache || new ScopedInMemoryCache();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_setCachedResponse<TData: ValidCacheData>(
|
|
42
|
+
id: string,
|
|
43
|
+
entry: CachedResponse<TData>,
|
|
44
|
+
hydrate: boolean,
|
|
45
|
+
): CachedResponse<TData> {
|
|
46
|
+
const frozenEntry = Object.freeze(entry);
|
|
47
|
+
if (Server.isServerSide()) {
|
|
48
|
+
// We are server-side.
|
|
49
|
+
// We need to store this value.
|
|
50
|
+
if (hydrate) {
|
|
51
|
+
this._hydrationCache.set(DefaultScope, id, frozenEntry);
|
|
52
|
+
} else {
|
|
53
|
+
// Usually, when server-side, this cache will always be present.
|
|
54
|
+
// We do fake server-side in our doc example though, when it
|
|
55
|
+
// won't be.
|
|
56
|
+
this._ssrOnlyCache?.set(DefaultScope, id, frozenEntry);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return frozenEntry;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initialize the cache from a given cache state.
|
|
64
|
+
*
|
|
65
|
+
* This can only be called if the cache is not already in use.
|
|
66
|
+
*/
|
|
67
|
+
initialize: (source: ResponseCache) => void = (source) => {
|
|
68
|
+
if (this._hydrationCache.inUse) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Cannot initialize data response cache more than once",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
this._hydrationCache = new ScopedInMemoryCache({
|
|
74
|
+
// $FlowIgnore[incompatible-call]
|
|
75
|
+
[DefaultScope]: source,
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Cache data for a specific response.
|
|
81
|
+
*
|
|
82
|
+
* This is a noop when client-side.
|
|
83
|
+
*/
|
|
84
|
+
cacheData: <TData: ValidCacheData>(
|
|
85
|
+
id: string,
|
|
86
|
+
data: TData,
|
|
87
|
+
hydrate: boolean,
|
|
88
|
+
) => CachedResponse<TData> = <TData: ValidCacheData>(
|
|
89
|
+
id: string,
|
|
90
|
+
data: TData,
|
|
91
|
+
hydrate: boolean,
|
|
92
|
+
): CachedResponse<TData> => this._setCachedResponse(id, {data}, hydrate);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Cache an error for a specific response.
|
|
96
|
+
*
|
|
97
|
+
* This is a noop when client-side.
|
|
98
|
+
*/
|
|
99
|
+
cacheError: <TData: ValidCacheData>(
|
|
100
|
+
id: string,
|
|
101
|
+
error: Error | string,
|
|
102
|
+
hydrate: boolean,
|
|
103
|
+
) => CachedResponse<TData> = <TData: ValidCacheData>(
|
|
104
|
+
id: string,
|
|
105
|
+
error: Error | string,
|
|
106
|
+
hydrate: boolean,
|
|
107
|
+
): CachedResponse<TData> => {
|
|
108
|
+
const errorMessage = typeof error === "string" ? error : error.message;
|
|
109
|
+
return this._setCachedResponse(id, {error: errorMessage}, hydrate);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Retrieve data from our cache.
|
|
114
|
+
*/
|
|
115
|
+
getEntry: <TData: ValidCacheData>(
|
|
116
|
+
id: string,
|
|
117
|
+
) => ?$ReadOnly<CachedResponse<TData>> = <TData: ValidCacheData>(
|
|
118
|
+
id: string,
|
|
119
|
+
): ?$ReadOnly<CachedResponse<TData>> => {
|
|
120
|
+
// Get the cached entry for this value.
|
|
121
|
+
|
|
122
|
+
// We first look in the ssr cache and then the hydration cache.
|
|
123
|
+
const internalEntry =
|
|
124
|
+
this._ssrOnlyCache?.get(DefaultScope, id) ??
|
|
125
|
+
this._hydrationCache.get(DefaultScope, id);
|
|
126
|
+
|
|
127
|
+
// If we are not server-side and we hydrated something, let's clear
|
|
128
|
+
// that from the hydration cache to save memory.
|
|
129
|
+
if (this._ssrOnlyCache == null && internalEntry != null) {
|
|
130
|
+
// We now delete this from our hydration cache as we don't need it.
|
|
131
|
+
// This does mean that if another handler of the same type but
|
|
132
|
+
// without some sort of linked cache won't get the value, but
|
|
133
|
+
// that's not an expected use-case. If two different places use the
|
|
134
|
+
// same handler and options (i.e. the same request), then the
|
|
135
|
+
// handler should cater to that to ensure they share the result.
|
|
136
|
+
this._hydrationCache.purge(DefaultScope, id);
|
|
137
|
+
}
|
|
138
|
+
// Getting the typing right between the in-memory cache and this
|
|
139
|
+
// is hard. Just telling flow it's OK.
|
|
140
|
+
// $FlowIgnore[incompatible-return]
|
|
141
|
+
return internalEntry;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Remove from cache, the entry matching the given handler and options.
|
|
146
|
+
*
|
|
147
|
+
* This will, if present therein, remove the value from the custom cache
|
|
148
|
+
* associated with the handler and the framework in-memory cache.
|
|
149
|
+
*
|
|
150
|
+
* Returns true if something was removed from any cache; otherwise, false.
|
|
151
|
+
*/
|
|
152
|
+
remove: (id: string) => boolean = (id: string): boolean => {
|
|
153
|
+
// NOTE(somewhatabstract): We could invoke removeAll with a predicate
|
|
154
|
+
// to match the key of the entry we're removing, but that's an
|
|
155
|
+
// inefficient way to remove a single item, so let's not do that.
|
|
156
|
+
|
|
157
|
+
// Delete the entry from the appropriate cache.
|
|
158
|
+
return (
|
|
159
|
+
this._hydrationCache.purge(DefaultScope, id) ||
|
|
160
|
+
(this._ssrOnlyCache?.purge(DefaultScope, id) ?? false)
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Remove from cache, any entries matching the given handler and predicate.
|
|
166
|
+
*
|
|
167
|
+
* This will, if present therein, remove matching values from the framework
|
|
168
|
+
* in-memory cache.
|
|
169
|
+
*
|
|
170
|
+
* It returns a count of all records removed.
|
|
171
|
+
*/
|
|
172
|
+
removeAll: (
|
|
173
|
+
predicate?: (
|
|
174
|
+
key: string,
|
|
175
|
+
cachedEntry: $ReadOnly<CachedResponse<ValidCacheData>>,
|
|
176
|
+
) => boolean,
|
|
177
|
+
) => void = (predicate) => {
|
|
178
|
+
const realPredicate = predicate
|
|
179
|
+
? // We know what we're putting into the cache so let's assume it
|
|
180
|
+
// conforms.
|
|
181
|
+
// $FlowIgnore[incompatible-call]
|
|
182
|
+
(_, key, cachedEntry) => predicate(key, cachedEntry)
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
// Apply the predicate to what we have in our caches.
|
|
186
|
+
this._hydrationCache.purgeAll(realPredicate);
|
|
187
|
+
this._ssrOnlyCache?.purgeAll(realPredicate);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Deep clone the hydration cache.
|
|
192
|
+
*
|
|
193
|
+
* By design, this only clones the data that is to be used for hydration.
|
|
194
|
+
*/
|
|
195
|
+
cloneHydratableData: () => ResponseCache = (): ResponseCache => {
|
|
196
|
+
// We return our hydration cache only.
|
|
197
|
+
const cache = this._hydrationCache.clone();
|
|
198
|
+
|
|
199
|
+
// If we're empty, we still want to return an object, so we default
|
|
200
|
+
// to an empty object.
|
|
201
|
+
// We only need the default scope out of our scoped in-memory cache.
|
|
202
|
+
// We know that it conforms to our expectations.
|
|
203
|
+
// $FlowIgnore[incompatible-return]
|
|
204
|
+
return cache[DefaultScope] ?? {};
|
|
205
|
+
};
|
|
206
|
+
}
|
package/src/util/types.js
CHANGED
|
@@ -1,135 +1,70 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Define what can be cached.
|
|
4
|
+
*
|
|
5
|
+
* We disallow functions and undefined as undefined represents a cache miss
|
|
6
|
+
* and functions are not allowed.
|
|
7
|
+
*/
|
|
8
|
+
export type ValidCacheData =
|
|
9
|
+
| string
|
|
10
|
+
| boolean
|
|
11
|
+
| number
|
|
12
|
+
| {...}
|
|
13
|
+
| Array<?ValidCacheData>;
|
|
5
14
|
|
|
6
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The normalized result of a request.
|
|
17
|
+
*/
|
|
18
|
+
export type Result<TData: ValidCacheData> =
|
|
7
19
|
| {|
|
|
8
20
|
status: "loading",
|
|
9
21
|
|}
|
|
10
22
|
| {|
|
|
11
23
|
status: "success",
|
|
12
|
-
data
|
|
24
|
+
data: TData,
|
|
13
25
|
|}
|
|
14
26
|
| {|
|
|
15
27
|
status: "error",
|
|
16
28
|
error: string,
|
|
29
|
+
|}
|
|
30
|
+
| {|
|
|
31
|
+
status: "aborted",
|
|
17
32
|
|};
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
/**
|
|
35
|
+
* A cache entry for a fulfilled request response.
|
|
36
|
+
*/
|
|
37
|
+
export type CachedResponse<TData: ValidCacheData> =
|
|
20
38
|
| {|
|
|
21
39
|
+error: string,
|
|
22
|
-
+data?:
|
|
40
|
+
+data?: void,
|
|
23
41
|
|}
|
|
24
42
|
| {|
|
|
25
43
|
+data: TData,
|
|
26
|
-
+error?:
|
|
44
|
+
+error?: void,
|
|
27
45
|
|};
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
export type InterceptFulfillRequestFn<TOptions, TData: ValidData> = (
|
|
35
|
-
options: TOptions,
|
|
36
|
-
) => ?Promise<TData>;
|
|
37
|
-
|
|
38
|
-
export type Interceptor = {|
|
|
39
|
-
fulfillRequest: InterceptFulfillRequestFn<any, any>,
|
|
40
|
-
|};
|
|
41
|
-
|
|
42
|
-
export type InterceptContextData = {
|
|
43
|
-
[key: string]: Interceptor,
|
|
44
|
-
...
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type Cache = {
|
|
48
|
-
[key: string]: HandlerSubcache,
|
|
47
|
+
/**
|
|
48
|
+
* A cache of fulfilled request responses.
|
|
49
|
+
*/
|
|
50
|
+
export type ResponseCache = {
|
|
51
|
+
[key: string]: CachedResponse<any>,
|
|
49
52
|
...
|
|
50
53
|
};
|
|
51
54
|
|
|
52
|
-
export interface ICache<TOptions, TData: ValidData> {
|
|
53
|
-
/**
|
|
54
|
-
* Stores a value in the cache for the given handler and options.
|
|
55
|
-
*/
|
|
56
|
-
store(
|
|
57
|
-
handler: IRequestHandler<TOptions, TData>,
|
|
58
|
-
options: TOptions,
|
|
59
|
-
entry: CacheEntry<TData>,
|
|
60
|
-
): void;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Retrieves a value from the cache for the given handler and options.
|
|
64
|
-
*/
|
|
65
|
-
retrieve(
|
|
66
|
-
handler: IRequestHandler<TOptions, TData>,
|
|
67
|
-
options: TOptions,
|
|
68
|
-
): ?$ReadOnly<CacheEntry<TData>>;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Remove the cached entry for the given handler and options.
|
|
72
|
-
*
|
|
73
|
-
* If the item exists in the cache, the cached entry is deleted and true
|
|
74
|
-
* is returned. Otherwise, this returns false.
|
|
75
|
-
*/
|
|
76
|
-
remove(
|
|
77
|
-
handler: IRequestHandler<TOptions, TData>,
|
|
78
|
-
options: TOptions,
|
|
79
|
-
): boolean;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Remove all cached entries for the given handler that, optionally, match
|
|
83
|
-
* a given predicate.
|
|
84
|
-
*
|
|
85
|
-
* Returns the number of entries that were cleared from the cache.
|
|
86
|
-
*/
|
|
87
|
-
removeAll(
|
|
88
|
-
handler: IRequestHandler<TOptions, TData>,
|
|
89
|
-
predicate?: (
|
|
90
|
-
key: string,
|
|
91
|
-
cachedEntry: $ReadOnly<CacheEntry<TData>>,
|
|
92
|
-
) => boolean,
|
|
93
|
-
): number;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
55
|
/**
|
|
97
|
-
* A
|
|
56
|
+
* A cache with scoped sections.
|
|
98
57
|
*/
|
|
99
|
-
export
|
|
58
|
+
export type ScopedCache = {
|
|
100
59
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* @param {TOptions} options Options tha the request may need.
|
|
104
|
-
* @return {Promise<TData>} A promise of the requested data.
|
|
60
|
+
* The cache is scoped to allow easier clearing of different types of usage.
|
|
105
61
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
* When true, server-side results are cached and hydrated in the client.
|
|
116
|
-
* When false, the server-side cache is not used and results are not
|
|
117
|
-
* hydrated.
|
|
118
|
-
* This should only be set to false if something is ensuring that the
|
|
119
|
-
* hydrated client result will match the server result.
|
|
120
|
-
*
|
|
121
|
-
* For example, if Apollo is used to handle GraphQL requests in SSR mode,
|
|
122
|
-
* it has its own cache that is used to hydrate the client. Setting this
|
|
123
|
-
* to false makes sure we don't store the data twice, which would
|
|
124
|
-
* unnecessarily bloat the data sent back to the client.
|
|
125
|
-
*/
|
|
126
|
-
get hydrate(): boolean;
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Get the key to use for a given request. This should be idempotent for a
|
|
130
|
-
* given options set if you want caching to work across requests.
|
|
131
|
-
*/
|
|
132
|
-
getKey(options: TOptions): string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export type ResponseCache = $ReadOnly<Cache>;
|
|
62
|
+
[scope: string]: {
|
|
63
|
+
/**
|
|
64
|
+
* Each value in the cache is then identified within a given scope.
|
|
65
|
+
*/
|
|
66
|
+
[id: string]: ValidCacheData,
|
|
67
|
+
...
|
|
68
|
+
},
|
|
69
|
+
...
|
|
70
|
+
};
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
import {render} from "@testing-library/react";
|
|
4
|
-
|
|
5
|
-
import InterceptContext from "../intercept-context.js";
|
|
6
|
-
import InterceptData from "../intercept-data.js";
|
|
7
|
-
|
|
8
|
-
import type {IRequestHandler} from "../../util/types.js";
|
|
9
|
-
|
|
10
|
-
describe("InterceptData", () => {
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
jest.resetAllMocks();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("should update context with fulfillRequest method", () => {
|
|
16
|
-
// Arrange
|
|
17
|
-
const fakeHandler: IRequestHandler<string, string> = {
|
|
18
|
-
fulfillRequest: () => Promise.resolve("data"),
|
|
19
|
-
getKey: (o) => o,
|
|
20
|
-
type: "MY_HANDLER",
|
|
21
|
-
hydrate: true,
|
|
22
|
-
};
|
|
23
|
-
const props = {
|
|
24
|
-
handler: fakeHandler,
|
|
25
|
-
fulfillRequest: jest.fn(),
|
|
26
|
-
};
|
|
27
|
-
const captureContextFn = jest.fn();
|
|
28
|
-
|
|
29
|
-
// Act
|
|
30
|
-
render(
|
|
31
|
-
<InterceptData {...props}>
|
|
32
|
-
<InterceptContext.Consumer>
|
|
33
|
-
{captureContextFn}
|
|
34
|
-
</InterceptContext.Consumer>
|
|
35
|
-
</InterceptData>,
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
// Assert
|
|
39
|
-
expect(captureContextFn).toHaveBeenCalledWith(
|
|
40
|
-
expect.objectContaining({
|
|
41
|
-
MY_HANDLER: {
|
|
42
|
-
fulfillRequest: props.fulfillRequest,
|
|
43
|
-
},
|
|
44
|
-
}),
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("should override parent InterceptData", () => {
|
|
49
|
-
// 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
|
-
const fulfillRequest1Fn = jest.fn();
|
|
58
|
-
const fulfillRequest2Fn = jest.fn();
|
|
59
|
-
const captureContextFn = jest.fn();
|
|
60
|
-
|
|
61
|
-
// Act
|
|
62
|
-
render(
|
|
63
|
-
<InterceptData
|
|
64
|
-
handler={fakeHandler}
|
|
65
|
-
fulfillRequest={fulfillRequest1Fn}
|
|
66
|
-
>
|
|
67
|
-
<InterceptData
|
|
68
|
-
handler={fakeHandler}
|
|
69
|
-
fulfillRequest={fulfillRequest2Fn}
|
|
70
|
-
>
|
|
71
|
-
<InterceptContext.Consumer>
|
|
72
|
-
{captureContextFn}
|
|
73
|
-
</InterceptContext.Consumer>
|
|
74
|
-
</InterceptData>
|
|
75
|
-
</InterceptData>,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// Assert
|
|
79
|
-
expect(captureContextFn).toHaveBeenCalledWith(
|
|
80
|
-
expect.objectContaining({
|
|
81
|
-
MY_HANDLER: {
|
|
82
|
-
fulfillRequest: fulfillRequest2Fn,
|
|
83
|
-
},
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
|
|
4
|
-
import InterceptContext from "./intercept-context.js";
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
ValidData,
|
|
8
|
-
IRequestHandler,
|
|
9
|
-
InterceptFulfillRequestFn,
|
|
10
|
-
} from "../util/types.js";
|
|
11
|
-
|
|
12
|
-
type Props<TOptions, TData> = {|
|
|
13
|
-
/**
|
|
14
|
-
* A handler of the type to be intercepted.
|
|
15
|
-
*/
|
|
16
|
-
handler: IRequestHandler<TOptions, TData>,
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The children to render within this component. Any requests by `Data`
|
|
20
|
-
* components that use a handler of the same type as the handler for this
|
|
21
|
-
* component that are rendered within these children will be intercepted by
|
|
22
|
-
* this component (unless another `InterceptData` component overrides this
|
|
23
|
-
* one).
|
|
24
|
-
*/
|
|
25
|
-
children: React.Node,
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Called to fulfill a request.
|
|
29
|
-
* If this returns null, the request will be fulfilled by the
|
|
30
|
-
* handler of the original request being intercepted.
|
|
31
|
-
*/
|
|
32
|
-
fulfillRequest: InterceptFulfillRequestFn<TOptions, TData>,
|
|
33
|
-
|};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* This component provides a mechanism to intercept the data requests for the
|
|
37
|
-
* type of a given handler and provide alternative results. This is mostly
|
|
38
|
-
* useful for testing.
|
|
39
|
-
*
|
|
40
|
-
* This component is not recommended for use in production code as it
|
|
41
|
-
* can prevent predictable functioning of the Wonder Blocks Data framework.
|
|
42
|
-
* One possible side-effect is that inflight requests from the interceptor could
|
|
43
|
-
* be picked up by `Data` component requests of the same handler type from
|
|
44
|
-
* outside the children of this component.
|
|
45
|
-
*
|
|
46
|
-
* These components do not chain. If a different `InterceptData` instance is
|
|
47
|
-
* rendered within this one that intercepts the same handler type, then that
|
|
48
|
-
* new instance will replace this interceptor for its children. All methods
|
|
49
|
-
* will be replaced.
|
|
50
|
-
*/
|
|
51
|
-
export default class InterceptData<
|
|
52
|
-
TOptions,
|
|
53
|
-
TData: ValidData,
|
|
54
|
-
> extends React.Component<Props<TOptions, TData>> {
|
|
55
|
-
render(): React.Node {
|
|
56
|
-
return (
|
|
57
|
-
<InterceptContext.Consumer>
|
|
58
|
-
{(value) => {
|
|
59
|
-
const handlerType = this.props.handler.type;
|
|
60
|
-
const interceptor = {
|
|
61
|
-
...value[handlerType],
|
|
62
|
-
fulfillRequest: this.props.fulfillRequest,
|
|
63
|
-
};
|
|
64
|
-
const newValue = {
|
|
65
|
-
...value,
|
|
66
|
-
[handlerType]: interceptor,
|
|
67
|
-
};
|
|
68
|
-
return (
|
|
69
|
-
<InterceptContext.Provider value={newValue}>
|
|
70
|
-
{this.props.children}
|
|
71
|
-
</InterceptContext.Provider>
|
|
72
|
-
);
|
|
73
|
-
}}
|
|
74
|
-
</InterceptContext.Consumer>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
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.
|
|
4
|
-
|
|
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`.
|
|
7
|
-
|
|
8
|
-
Note that this component is expected to be used only within test cases and
|
|
9
|
-
usually only as a single instance. In flight requests for a given handler
|
|
10
|
-
type can be shared and as such, using `InterceptData` alongside non-intercepted
|
|
11
|
-
`Data` components with the same handler type can have indeterminate outcomes.
|
|
12
|
-
|
|
13
|
-
The `fulfillRequest` intercept function has the form:
|
|
14
|
-
|
|
15
|
-
```js static
|
|
16
|
-
(options: TOptions) => ?Promise<TData>;
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
If this method returns `null`, the default behavior occurs. This
|
|
20
|
-
means that a request will be made for data via the handler assigned to the
|
|
21
|
-
`Data` component being intercepted.
|
|
22
|
-
|
|
23
|
-
```jsx
|
|
24
|
-
import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
|
|
25
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
26
|
-
import {InterceptData, Data, RequestHandler} from "@khanacademy/wonder-blocks-data";
|
|
27
|
-
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
28
|
-
import Color from "@khanacademy/wonder-blocks-color";
|
|
29
|
-
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
30
|
-
|
|
31
|
-
class MyHandler extends RequestHandler {
|
|
32
|
-
constructor() {
|
|
33
|
-
super("INTERCEPT_DATA_HANDLER1");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
fulfillRequest(options) {
|
|
37
|
-
return Promise.reject(new Error("You should not see this!"));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const handler = new MyHandler();
|
|
42
|
-
const fulfillRequestInterceptor = function(options) {
|
|
43
|
-
if (options === "DATA") {
|
|
44
|
-
return Promise.resolve("INTERCEPTED DATA!");
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
<InterceptData handler={handler} fulfillRequest={fulfillRequestInterceptor}>
|
|
50
|
-
<View>
|
|
51
|
-
<Body>This received intercepted data!</Body>
|
|
52
|
-
<Data handler={handler} options={"DATA"}>
|
|
53
|
-
{({loading, data}) => {
|
|
54
|
-
if (loading) {
|
|
55
|
-
return "If you see this, the example is broken!";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<BodyMonospace>{data}</BodyMonospace>
|
|
60
|
-
);
|
|
61
|
-
}}
|
|
62
|
-
</Data>
|
|
63
|
-
</View>
|
|
64
|
-
</InterceptData>
|
|
65
|
-
```
|