@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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {resultFromCacheEntry} from "../result-from-cache-entry.js";
|
|
3
|
+
|
|
4
|
+
describe("#resultFromCacheEntry", () => {
|
|
5
|
+
it("should return loading status if cache entry is null", () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const cacheEntry = null;
|
|
8
|
+
|
|
9
|
+
// Act
|
|
10
|
+
const result = resultFromCacheEntry(cacheEntry);
|
|
11
|
+
|
|
12
|
+
// Assert
|
|
13
|
+
expect(result).toStrictEqual({
|
|
14
|
+
status: "loading",
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should return success status if cache entry has data", () => {
|
|
19
|
+
// Arrange
|
|
20
|
+
const cacheEntry = {
|
|
21
|
+
data: "DATA",
|
|
22
|
+
error: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Act
|
|
26
|
+
const result = resultFromCacheEntry(cacheEntry);
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(result).toStrictEqual({
|
|
30
|
+
status: "success",
|
|
31
|
+
data: "DATA",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should return error status if cache entry has no data and no error", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const cacheEntry: any = {
|
|
38
|
+
data: null,
|
|
39
|
+
error: null,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
const result = resultFromCacheEntry(cacheEntry);
|
|
44
|
+
|
|
45
|
+
// Assert
|
|
46
|
+
expect(result).toStrictEqual({
|
|
47
|
+
status: "error",
|
|
48
|
+
error: "Loaded result has invalid state where data and error are missing",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should return error status if cache entry has error", () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
const cacheEntry: any = {
|
|
55
|
+
data: null,
|
|
56
|
+
error: "ERROR",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Act
|
|
60
|
+
const result = resultFromCacheEntry(cacheEntry);
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(result).toStrictEqual({
|
|
64
|
+
status: "error",
|
|
65
|
+
error: "ERROR",
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/util/memory-cache.js
CHANGED
|
@@ -25,12 +25,11 @@ function deepClone<T: {...}>(source: T | $ReadOnly<T>): $ReadOnly<T> {
|
|
|
25
25
|
*
|
|
26
26
|
* Special case cache implementation for the memory cache.
|
|
27
27
|
*
|
|
28
|
-
* This is only used within our framework
|
|
29
|
-
* provide this as a custom cache as the framework will default to this in the
|
|
30
|
-
* absence of a custom cache. We use this for SSR too (see ./response-cache.js).
|
|
28
|
+
* This is only used within our framework for SSR (see ./response-cache.js).
|
|
31
29
|
*/
|
|
32
30
|
export default class MemoryCache<TOptions, TData: ValidData>
|
|
33
|
-
implements ICache<TOptions, TData>
|
|
31
|
+
implements ICache<TOptions, TData>
|
|
32
|
+
{
|
|
34
33
|
_cache: Cache;
|
|
35
34
|
|
|
36
35
|
constructor(source: ?$ReadOnly<Cache> = null) {
|
|
@@ -52,6 +51,11 @@ export default class MemoryCache<TOptions, TData: ValidData>
|
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Indicate if this cache is being used or now.
|
|
56
|
+
*
|
|
57
|
+
* When the cache has entries, returns `true`; otherwise, returns `false`.
|
|
58
|
+
*/
|
|
55
59
|
get inUse(): boolean {
|
|
56
60
|
return Object.keys(this._cache).length > 0;
|
|
57
61
|
}
|
|
@@ -67,9 +71,7 @@ export default class MemoryCache<TOptions, TData: ValidData>
|
|
|
67
71
|
): void => {
|
|
68
72
|
const requestType = handler.type;
|
|
69
73
|
|
|
70
|
-
const frozenEntry = Object.
|
|
71
|
-
? entry
|
|
72
|
-
: Object.freeze(entry);
|
|
74
|
+
const frozenEntry = Object.freeze(entry);
|
|
73
75
|
|
|
74
76
|
// Ensure we have a cache location for this handler type.
|
|
75
77
|
this._cache[requestType] = this._cache[requestType] || {};
|
|
@@ -156,16 +158,19 @@ export default class MemoryCache<TOptions, TData: ValidData>
|
|
|
156
158
|
return 0;
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
// Apply the predicate to what we have cached.
|
|
160
161
|
let removedCount = 0;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
predicate(key, (entry: any))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
if (typeof predicate === "function") {
|
|
163
|
+
// Apply the predicate to what we have cached.
|
|
164
|
+
for (const [key, entry] of Object.entries(handlerCache)) {
|
|
165
|
+
if (predicate(key, (entry: any))) {
|
|
166
|
+
removedCount++;
|
|
167
|
+
delete handlerCache[key];
|
|
168
|
+
}
|
|
168
169
|
}
|
|
170
|
+
} else {
|
|
171
|
+
// We're removing everything so delete the entire subcache.
|
|
172
|
+
removedCount = Object.keys(handlerCache).length;
|
|
173
|
+
delete this._cache[requestType];
|
|
169
174
|
}
|
|
170
175
|
return removedCount;
|
|
171
176
|
};
|
|
@@ -76,6 +76,8 @@ export class RequestFulfillment {
|
|
|
76
76
|
delete handlerRequests[key];
|
|
77
77
|
/**
|
|
78
78
|
* Let's cache the data!
|
|
79
|
+
*
|
|
80
|
+
* NOTE: This only caches when we're server side.
|
|
79
81
|
*/
|
|
80
82
|
return cacheData<TOptions, TData>(handler, options, data);
|
|
81
83
|
})
|
|
@@ -83,6 +85,8 @@ export class RequestFulfillment {
|
|
|
83
85
|
delete handlerRequests[key];
|
|
84
86
|
/**
|
|
85
87
|
* Let's cache the error!
|
|
88
|
+
*
|
|
89
|
+
* NOTE: This only caches when we're server side.
|
|
86
90
|
*/
|
|
87
91
|
return cacheError<TOptions, TData>(handler, options, error);
|
|
88
92
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import type {ValidData,
|
|
2
|
+
import type {ValidData, IRequestHandler} from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Base implementation for creating a request handler.
|
|
@@ -8,18 +8,13 @@ import type {ValidData, CacheEntry, IRequestHandler, ICache} from "./types.js";
|
|
|
8
8
|
* use with the Wonder Blocks Data framework.
|
|
9
9
|
*/
|
|
10
10
|
export default class RequestHandler<TOptions, TData: ValidData>
|
|
11
|
-
implements IRequestHandler<TOptions, TData>
|
|
11
|
+
implements IRequestHandler<TOptions, TData>
|
|
12
|
+
{
|
|
12
13
|
_type: string;
|
|
13
|
-
_cache: ?ICache<TOptions, TData>;
|
|
14
14
|
_hydrate: boolean;
|
|
15
15
|
|
|
16
|
-
constructor(
|
|
17
|
-
type: string,
|
|
18
|
-
cache?: ICache<TOptions, TData>,
|
|
19
|
-
hydrate?: boolean = true,
|
|
20
|
-
) {
|
|
16
|
+
constructor(type: string, hydrate?: boolean = true) {
|
|
21
17
|
this._type = type;
|
|
22
|
-
this._cache = cache || null;
|
|
23
18
|
this._hydrate = !!hydrate;
|
|
24
19
|
}
|
|
25
20
|
|
|
@@ -27,29 +22,10 @@ export default class RequestHandler<TOptions, TData: ValidData>
|
|
|
27
22
|
return this._type;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
get cache(): ?ICache<TOptions, TData> {
|
|
31
|
-
return this._cache;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
25
|
get hydrate(): boolean {
|
|
35
26
|
return this._hydrate;
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
shouldRefreshCache(
|
|
39
|
-
options: TOptions,
|
|
40
|
-
cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
|
|
41
|
-
): boolean {
|
|
42
|
-
/**
|
|
43
|
-
* By default, the cache needs a refresh if the current entry is an
|
|
44
|
-
* error.
|
|
45
|
-
*
|
|
46
|
-
* This means that an error will cause a re-request on render.
|
|
47
|
-
* Useful if the server rendered an error, as it means the client
|
|
48
|
-
* will update after rehydration.
|
|
49
|
-
*/
|
|
50
|
-
return cachedEntry == null || cachedEntry.error != null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
29
|
getKey(options: TOptions): string {
|
|
54
30
|
try {
|
|
55
31
|
return options === undefined
|
|
@@ -14,12 +14,6 @@ interface IRequestHandler<TOptions, TData> {
|
|
|
14
14
|
*/
|
|
15
15
|
get type(): string;
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* A custom cache to use with data that this handler requests.
|
|
19
|
-
* This only affects client-side caching of data.
|
|
20
|
-
*/
|
|
21
|
-
get cache(): ?ICache<TOptions, TData>;
|
|
22
|
-
|
|
23
17
|
/**
|
|
24
18
|
* When true, server-side results are cached and hydrated in the client.
|
|
25
19
|
* When false, the server-side cache is not used and results are not
|
|
@@ -29,17 +23,6 @@ interface IRequestHandler<TOptions, TData> {
|
|
|
29
23
|
*/
|
|
30
24
|
get hydrate(): boolean;
|
|
31
25
|
|
|
32
|
-
/**
|
|
33
|
-
* Determine if the cached data should be refreshed.
|
|
34
|
-
*
|
|
35
|
-
* If this returns true, the framework will use the currently cached value
|
|
36
|
-
* but also request a new value.
|
|
37
|
-
*/
|
|
38
|
-
shouldRefreshCache(
|
|
39
|
-
options: TOptions,
|
|
40
|
-
cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
|
|
41
|
-
): boolean;
|
|
42
|
-
|
|
43
26
|
/**
|
|
44
27
|
* Get the key to use for a given request. This should be idempotent for a
|
|
45
28
|
* given options set if you want caching to work across requests.
|
|
@@ -52,13 +35,6 @@ The constructor requires a `type` to identify your handler. This should be uniqu
|
|
|
52
35
|
among the handlers that are used across your application, otherwise, requests
|
|
53
36
|
may be fulfilled by the wrong handler.
|
|
54
37
|
|
|
55
|
-
There is also an optional constructor argument, `cache`, which can be used to
|
|
56
|
-
provide a custom cache for use with data the handler fulfills. Custom caches
|
|
57
|
-
must implement the `ICache<TOptions, TData>` interface. If this is omitted, the
|
|
58
|
-
core Wonder Blocks Data in-memory cache will be used. If you want to avoid
|
|
59
|
-
caching in memory, see `NoCache`, which is a caching strategy that eliminates
|
|
60
|
-
the use of caching entirely.
|
|
61
|
-
|
|
62
38
|
The `fulfillRequest` method of this class is not implemented and will throw if
|
|
63
39
|
called. Subclasses will need to implement this method.
|
|
64
40
|
|
|
@@ -73,11 +49,3 @@ SSR process is tracking the data for hydration. An example of setting this to
|
|
|
73
49
|
false might be when you are using Apollo Client. In that scenario, you may use
|
|
74
50
|
Apollo Cache to store and hydrate the data, while using Wonder Blocks Data to
|
|
75
51
|
track and fulfill any query requests made via Apollo Client.
|
|
76
|
-
|
|
77
|
-
Finally, the `shouldRefreshCache` method is provided for cases where a handler
|
|
78
|
-
may want control over cache freshness. By default, this will return `true` for
|
|
79
|
-
error results or a missing value. However, in some cases, handlers may want to
|
|
80
|
-
make sure cached entries are not stale, and so may return `true` from the
|
|
81
|
-
`shouldRefreshCache` method to instruct the framework to make a new request.
|
|
82
|
-
The existing cached value will still be used, but an updated value will be
|
|
83
|
-
requested.
|
|
@@ -25,9 +25,8 @@ type RequestCache = {
|
|
|
25
25
|
*
|
|
26
26
|
* INTERNAL USE ONLY
|
|
27
27
|
*/
|
|
28
|
-
export const TrackerContext: React.Context<?TrackerFn> =
|
|
29
|
-
null
|
|
30
|
-
);
|
|
28
|
+
export const TrackerContext: React.Context<?TrackerFn> =
|
|
29
|
+
new React.createContext<?TrackerFn>(null);
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
32
|
* The default instance is stored here.
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
3
3
|
import MemoryCache from "./memory-cache.js";
|
|
4
|
-
import NoCache from "./no-cache.js";
|
|
5
4
|
|
|
6
5
|
import type {
|
|
7
6
|
ValidData,
|
|
8
7
|
CacheEntry,
|
|
9
8
|
Cache,
|
|
10
|
-
ICache,
|
|
11
9
|
IRequestHandler,
|
|
12
10
|
ResponseCache as ResCache,
|
|
13
11
|
} from "./types.js";
|
|
@@ -31,32 +29,17 @@ export class ResponseCache {
|
|
|
31
29
|
return _default;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
_hydrationCache: MemoryCache<any, any>;
|
|
35
33
|
_ssrOnlyCache: ?MemoryCache<any, any>;
|
|
36
34
|
|
|
37
35
|
constructor(
|
|
38
|
-
|
|
36
|
+
hydrationCache: ?MemoryCache<any, any> = null,
|
|
39
37
|
ssrOnlyCache: ?MemoryCache<any, any> = null,
|
|
40
38
|
) {
|
|
41
39
|
this._ssrOnlyCache = Server.isServerSide()
|
|
42
40
|
? ssrOnlyCache || new MemoryCache()
|
|
43
41
|
: undefined;
|
|
44
|
-
this.
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Returns the default cache to use for the given handler.
|
|
49
|
-
*/
|
|
50
|
-
_defaultCache<TOptions, TData: ValidData>(
|
|
51
|
-
handler: IRequestHandler<TOptions, TData>,
|
|
52
|
-
): ICache<TOptions, TData> {
|
|
53
|
-
if (handler.hydrate) {
|
|
54
|
-
return this._hydrationAndDefaultCache;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// If the handler doesn't want to hydrate, we return the SSR-only cache.
|
|
58
|
-
// If we are client-side, we return our non-caching implementation.
|
|
59
|
-
return this._ssrOnlyCache || NoCache.Default;
|
|
42
|
+
this._hydrationCache = hydrationCache || new MemoryCache();
|
|
60
43
|
}
|
|
61
44
|
|
|
62
45
|
_setCacheEntry<TOptions, TData: ValidData>(
|
|
@@ -65,15 +48,14 @@ export class ResponseCache {
|
|
|
65
48
|
entry: CacheEntry<TData>,
|
|
66
49
|
): CacheEntry<TData> {
|
|
67
50
|
const frozenEntry = Object.freeze(entry);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// We
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
this._defaultCache(handler).store(handler, options, frozenEntry);
|
|
51
|
+
if (this._ssrOnlyCache != null) {
|
|
52
|
+
// We are server-side.
|
|
53
|
+
// We need to store this value.
|
|
54
|
+
if (handler.hydrate) {
|
|
55
|
+
this._hydrationCache.store(handler, options, frozenEntry);
|
|
56
|
+
} else {
|
|
57
|
+
this._ssrOnlyCache.store(handler, options, frozenEntry);
|
|
58
|
+
}
|
|
77
59
|
}
|
|
78
60
|
return frozenEntry;
|
|
79
61
|
}
|
|
@@ -84,14 +66,14 @@ export class ResponseCache {
|
|
|
84
66
|
* This can only be called if the cache is not already in use.
|
|
85
67
|
*/
|
|
86
68
|
initialize: (source: ResCache) => void = (source) => {
|
|
87
|
-
if (this.
|
|
69
|
+
if (this._hydrationCache.inUse) {
|
|
88
70
|
throw new Error(
|
|
89
71
|
"Cannot initialize data response cache more than once",
|
|
90
72
|
);
|
|
91
73
|
}
|
|
92
74
|
|
|
93
75
|
try {
|
|
94
|
-
this.
|
|
76
|
+
this._hydrationCache = new MemoryCache(source);
|
|
95
77
|
} catch (e) {
|
|
96
78
|
throw new Error(
|
|
97
79
|
`An error occurred trying to initialize the data response cache: ${e}`,
|
|
@@ -101,6 +83,8 @@ export class ResponseCache {
|
|
|
101
83
|
|
|
102
84
|
/**
|
|
103
85
|
* Cache data for a specific response.
|
|
86
|
+
*
|
|
87
|
+
* This is a noop when client-side.
|
|
104
88
|
*/
|
|
105
89
|
cacheData: <TOptions, TData: ValidData>(
|
|
106
90
|
handler: IRequestHandler<TOptions, TData>,
|
|
@@ -110,12 +94,12 @@ export class ResponseCache {
|
|
|
110
94
|
handler: IRequestHandler<TOptions, TData>,
|
|
111
95
|
options: TOptions,
|
|
112
96
|
data: TData,
|
|
113
|
-
): CacheEntry<TData> => {
|
|
114
|
-
return this._setCacheEntry(handler, options, {data});
|
|
115
|
-
};
|
|
97
|
+
): CacheEntry<TData> => this._setCacheEntry(handler, options, {data});
|
|
116
98
|
|
|
117
99
|
/**
|
|
118
100
|
* Cache an error for a specific response.
|
|
101
|
+
*
|
|
102
|
+
* This is a noop when client-side.
|
|
119
103
|
*/
|
|
120
104
|
cacheError: <TOptions, TData: ValidData>(
|
|
121
105
|
handler: IRequestHandler<TOptions, TData>,
|
|
@@ -140,42 +124,27 @@ export class ResponseCache {
|
|
|
140
124
|
handler: IRequestHandler<TOptions, TData>,
|
|
141
125
|
options: TOptions,
|
|
142
126
|
): ?$ReadOnly<CacheEntry<TData>> => {
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Get the internal entry for the handler.
|
|
154
|
-
// This allows us to use our hydrated cache during hydration.
|
|
155
|
-
// If we just returned null when the custom cache didn't have it,
|
|
156
|
-
// we would never hydrate properly.
|
|
157
|
-
const internalEntry = this._defaultCache<TOptions, TData>(
|
|
158
|
-
handler,
|
|
159
|
-
).retrieve(handler, options);
|
|
160
|
-
|
|
161
|
-
// If we are not server-side and we hydrated something that the custom
|
|
162
|
-
// cache didn't have, we need to make sure the custom cache contains
|
|
163
|
-
// that value.
|
|
164
|
-
if (
|
|
165
|
-
this._ssrOnlyCache == null &&
|
|
166
|
-
handler.cache != null &&
|
|
167
|
-
internalEntry != null
|
|
168
|
-
) {
|
|
169
|
-
// Yes, if this throws, we will have a problem. We want that.
|
|
170
|
-
// Bad cache implementations should be overt.
|
|
171
|
-
handler.cache.store(handler, options, internalEntry);
|
|
127
|
+
// Get the cached entry for this value.
|
|
128
|
+
// If the handler wants WB Data to hydrate (i.e. handler.hydrate is
|
|
129
|
+
// true), we use our hydration cache. Otherwise, if we're server-side
|
|
130
|
+
// we use our SSR-only cache. Otherwise, there's no entry to return.
|
|
131
|
+
const cache = handler.hydrate
|
|
132
|
+
? this._hydrationCache
|
|
133
|
+
: Server.isServerSide()
|
|
134
|
+
? this._ssrOnlyCache
|
|
135
|
+
: undefined;
|
|
136
|
+
const internalEntry = cache?.retrieve(handler, options);
|
|
172
137
|
|
|
173
|
-
|
|
138
|
+
// If we are not server-side and we hydrated something, let's clear
|
|
139
|
+
// that from the hydration cache to save memory.
|
|
140
|
+
if (this._ssrOnlyCache == null && internalEntry != null) {
|
|
141
|
+
// We now delete this from our hydration cache as we don't need it.
|
|
174
142
|
// This does mean that if another handler of the same type but
|
|
175
|
-
// without
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
143
|
+
// without some sort of linked cache won't get the value, but
|
|
144
|
+
// that's not an expected use-case. If two different places use the
|
|
145
|
+
// same handler and options (i.e. the same request), then the
|
|
146
|
+
// handler should cater to that to ensure they share the result.
|
|
147
|
+
this._hydrationCache.remove(handler, options);
|
|
179
148
|
}
|
|
180
149
|
return internalEntry;
|
|
181
150
|
};
|
|
@@ -199,31 +168,19 @@ export class ResponseCache {
|
|
|
199
168
|
// to match the key of the entry we're removing, but that's an
|
|
200
169
|
// inefficient way to remove a single item, so let's not do that.
|
|
201
170
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// Delete the entry from our internal cache.
|
|
208
|
-
// Even if we have a custom cache, we want to make sure we still
|
|
209
|
-
// removed the same value from internal cache since this could be
|
|
210
|
-
// getting called before hydration for some complex advanced usage
|
|
211
|
-
// reason.
|
|
212
|
-
return (
|
|
213
|
-
this._defaultCache(handler).remove(handler, options) ||
|
|
214
|
-
removedCustom
|
|
215
|
-
);
|
|
171
|
+
// Delete the entry from the appropriate cache.
|
|
172
|
+
return handler.hydrate
|
|
173
|
+
? this._hydrationCache.remove(handler, options)
|
|
174
|
+
: this._ssrOnlyCache?.remove(handler, options) ?? false;
|
|
216
175
|
};
|
|
217
176
|
|
|
218
177
|
/**
|
|
219
178
|
* Remove from cache, any entries matching the given handler and predicate.
|
|
220
179
|
*
|
|
221
|
-
* This will, if present therein, remove matching values from the
|
|
222
|
-
*
|
|
180
|
+
* This will, if present therein, remove matching values from the framework
|
|
181
|
+
* in-memory cache.
|
|
223
182
|
*
|
|
224
|
-
* It returns a count of all records removed.
|
|
225
|
-
* keys, but of unique entries. So if the same key is removed from both the
|
|
226
|
-
* framework and custom caches, that will be 2 records removed.
|
|
183
|
+
* It returns a count of all records removed.
|
|
227
184
|
*/
|
|
228
185
|
removeAll: <TOptions, TData: ValidData>(
|
|
229
186
|
handler: IRequestHandler<TOptions, TData>,
|
|
@@ -238,36 +195,19 @@ export class ResponseCache {
|
|
|
238
195
|
cachedEntry: $ReadOnly<CacheEntry<TData>>,
|
|
239
196
|
) => boolean,
|
|
240
197
|
): number => {
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
customCache?.removeAll(handler, predicate) || 0;
|
|
246
|
-
|
|
247
|
-
// Apply the predicate to what we have in our internal cached.
|
|
248
|
-
// Even if we have a custom cache, we want to make sure we still
|
|
249
|
-
// removed the same value from internal cache since this could be
|
|
250
|
-
// getting called before hydration for some complex advanced usage
|
|
251
|
-
// reason.
|
|
252
|
-
const removedCount = this._defaultCache(handler).removeAll(
|
|
253
|
-
handler,
|
|
254
|
-
predicate,
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
// We have no idea which keys were removed from which caches,
|
|
258
|
-
// so we can't dedupe the remove counts based on keys.
|
|
259
|
-
// That's why we return the total records deleted rather than the
|
|
260
|
-
// total keys deleted.
|
|
261
|
-
return removedCount + removedCountCustom;
|
|
198
|
+
// Apply the predicate to what we have in the appropriate cache.
|
|
199
|
+
return handler.hydrate
|
|
200
|
+
? this._hydrationCache.removeAll(handler, predicate)
|
|
201
|
+
: this._ssrOnlyCache?.removeAll(handler, predicate) ?? 0;
|
|
262
202
|
};
|
|
263
203
|
|
|
264
204
|
/**
|
|
265
205
|
* Deep clone the hydration cache.
|
|
266
206
|
*
|
|
267
|
-
* By design, this
|
|
207
|
+
* By design, this only clones the data that is to be used for hydration.
|
|
268
208
|
*/
|
|
269
209
|
cloneHydratableData: () => $ReadOnly<Cache> = (): $ReadOnly<Cache> => {
|
|
270
210
|
// We return our hydration cache only.
|
|
271
|
-
return this.
|
|
211
|
+
return this._hydrationCache.cloneData();
|
|
272
212
|
};
|
|
273
213
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {ValidData, CacheEntry, Result} from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Turns a cache entry into a stateful result.
|
|
6
|
+
*/
|
|
7
|
+
export const resultFromCacheEntry = <TData: ValidData>(
|
|
8
|
+
cacheEntry: ?CacheEntry<TData>,
|
|
9
|
+
): Result<TData> => {
|
|
10
|
+
// No cache entry means we didn't load one yet.
|
|
11
|
+
if (cacheEntry == null) {
|
|
12
|
+
return {
|
|
13
|
+
status: "loading",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {data, error} = cacheEntry;
|
|
18
|
+
|
|
19
|
+
if (data != null) {
|
|
20
|
+
return {
|
|
21
|
+
status: "success",
|
|
22
|
+
data,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (error == null) {
|
|
27
|
+
// We should never get here ever.
|
|
28
|
+
return {
|
|
29
|
+
status: "error",
|
|
30
|
+
error: "Loaded result has invalid state where data and error are missing",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
status: "error",
|
|
36
|
+
error,
|
|
37
|
+
};
|
|
38
|
+
};
|
package/src/util/types.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
export type ValidData = string | boolean | number | {...};
|
|
3
3
|
|
|
4
|
+
export type Status = "loading" | "success" | "error";
|
|
5
|
+
|
|
4
6
|
export type Result<TData: ValidData> =
|
|
5
7
|
| {|
|
|
6
|
-
|
|
7
|
-
data?: void,
|
|
8
|
-
error?: void,
|
|
8
|
+
status: "loading",
|
|
9
9
|
|}
|
|
10
10
|
| {|
|
|
11
|
-
|
|
11
|
+
status: "success",
|
|
12
12
|
data?: TData,
|
|
13
|
-
|
|
13
|
+
|}
|
|
14
|
+
| {|
|
|
15
|
+
status: "error",
|
|
16
|
+
error: string,
|
|
14
17
|
|};
|
|
15
18
|
|
|
16
19
|
export type CacheEntry<TData: ValidData> =
|
|
@@ -28,24 +31,12 @@ type HandlerSubcache = {
|
|
|
28
31
|
...
|
|
29
32
|
};
|
|
30
33
|
|
|
31
|
-
export type InterceptCacheFn<TOptions, TData: ValidData> = (
|
|
32
|
-
options: TOptions,
|
|
33
|
-
cacheEntry: ?$ReadOnly<CacheEntry<TData>>,
|
|
34
|
-
) => ?$ReadOnly<CacheEntry<TData>>;
|
|
35
|
-
|
|
36
34
|
export type InterceptFulfillRequestFn<TOptions, TData: ValidData> = (
|
|
37
35
|
options: TOptions,
|
|
38
36
|
) => ?Promise<TData>;
|
|
39
37
|
|
|
40
|
-
export type InterceptShouldRefreshCacheFn<TOptions, TData: ValidData> = (
|
|
41
|
-
options: TOptions,
|
|
42
|
-
cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
|
|
43
|
-
) => ?boolean;
|
|
44
|
-
|
|
45
38
|
export type Interceptor = {|
|
|
46
|
-
|
|
47
|
-
fulfillRequest?: ?InterceptFulfillRequestFn<any, any>,
|
|
48
|
-
shouldRefreshCache?: ?InterceptShouldRefreshCacheFn<any, any>,
|
|
39
|
+
fulfillRequest: InterceptFulfillRequestFn<any, any>,
|
|
49
40
|
|};
|
|
50
41
|
|
|
51
42
|
export type InterceptContextData = {
|
|
@@ -120,31 +111,19 @@ export interface IRequestHandler<TOptions, TData: ValidData> {
|
|
|
120
111
|
*/
|
|
121
112
|
get type(): string;
|
|
122
113
|
|
|
123
|
-
/**
|
|
124
|
-
* A custom cache to use with data that this handler requests.
|
|
125
|
-
* This only affects client-side caching of data.
|
|
126
|
-
*/
|
|
127
|
-
get cache(): ?ICache<TOptions, TData>;
|
|
128
|
-
|
|
129
114
|
/**
|
|
130
115
|
* When true, server-side results are cached and hydrated in the client.
|
|
131
116
|
* When false, the server-side cache is not used and results are not
|
|
132
117
|
* hydrated.
|
|
133
118
|
* This should only be set to false if something is ensuring that the
|
|
134
119
|
* hydrated client result will match the server result.
|
|
135
|
-
*/
|
|
136
|
-
get hydrate(): boolean;
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Determine if the cached data should be refreshed.
|
|
140
120
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
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.
|
|
143
125
|
*/
|
|
144
|
-
|
|
145
|
-
options: TOptions,
|
|
146
|
-
cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
|
|
147
|
-
): boolean;
|
|
126
|
+
get hydrate(): boolean;
|
|
148
127
|
|
|
149
128
|
/**
|
|
150
129
|
* Get the key to use for a given request. This should be idempotent for a
|