@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
|
@@ -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
|
+
};
|