@khanacademy/wonder-blocks-data 4.0.0 → 6.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 +31 -0
- package/dist/es/index.js +793 -375
- package/dist/index.js +1203 -523
- package/legacy-docs.md +3 -0
- package/package.json +2 -2
- package/src/__docs__/_overview_.stories.mdx +18 -0
- package/src/__docs__/_overview_graphql.stories.mdx +35 -0
- package/src/__docs__/_overview_ssr_.stories.mdx +185 -0
- package/src/__docs__/_overview_testing_.stories.mdx +123 -0
- package/src/__docs__/exports.clear-shared-cache.stories.mdx +20 -0
- package/src/__docs__/exports.data-error.stories.mdx +23 -0
- package/src/__docs__/exports.data-errors.stories.mdx +23 -0
- package/src/{components/data.md → __docs__/exports.data.stories.mdx} +15 -18
- package/src/__docs__/exports.fulfill-all-data-requests.stories.mdx +24 -0
- package/src/__docs__/exports.gql-error.stories.mdx +23 -0
- package/src/__docs__/exports.gql-errors.stories.mdx +20 -0
- package/src/__docs__/exports.gql-router.stories.mdx +29 -0
- package/src/__docs__/exports.has-unfulfilled-requests.stories.mdx +20 -0
- package/src/__docs__/exports.intercept-requests.stories.mdx +69 -0
- package/src/__docs__/exports.intialize-cache.stories.mdx +29 -0
- package/src/__docs__/exports.remove-all-from-cache.stories.mdx +24 -0
- package/src/__docs__/exports.remove-from-cache.stories.mdx +25 -0
- package/src/__docs__/exports.request-fulfillment.stories.mdx +36 -0
- package/src/__docs__/exports.scoped-in-memory-cache.stories.mdx +92 -0
- package/src/__docs__/exports.serializable-in-memory-cache.stories.mdx +112 -0
- package/src/__docs__/exports.status.stories.mdx +31 -0
- package/src/{components/track-data.md → __docs__/exports.track-data.stories.mdx} +15 -0
- package/src/__docs__/exports.use-cached-effect.stories.mdx +41 -0
- package/src/__docs__/exports.use-gql.stories.mdx +73 -0
- package/src/__docs__/exports.use-hydratable-effect.stories.mdx +43 -0
- package/src/__docs__/exports.use-server-effect.stories.mdx +38 -0
- package/src/__docs__/exports.use-shared-cache.stories.mdx +30 -0
- package/src/__docs__/exports.when-client-side.stories.mdx +33 -0
- package/src/__docs__/types.cached-response.stories.mdx +29 -0
- package/src/__docs__/types.error-options.stories.mdx +21 -0
- package/src/__docs__/types.gql-context.stories.mdx +20 -0
- package/src/__docs__/types.gql-fetch-fn.stories.mdx +24 -0
- package/src/__docs__/types.gql-fetch-options.stories.mdx +24 -0
- package/src/__docs__/types.gql-operation-type.stories.mdx +24 -0
- package/src/__docs__/types.gql-operation.stories.mdx +67 -0
- package/src/__docs__/types.response-cache.stories.mdx +33 -0
- package/src/__docs__/types.result.stories.mdx +39 -0
- package/src/__docs__/types.scoped-cache.stories.mdx +27 -0
- package/src/__docs__/types.valid-cache-data.stories.mdx +23 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -80
- package/src/__tests__/generated-snapshot.test.js +7 -31
- package/src/components/__tests__/data.test.js +160 -154
- package/src/components/__tests__/intercept-requests.test.js +58 -0
- package/src/components/data.js +22 -126
- package/src/components/intercept-context.js +4 -5
- package/src/components/intercept-requests.js +69 -0
- package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +8 -8
- package/src/hooks/__tests__/use-cached-effect.test.js +507 -0
- package/src/hooks/__tests__/use-gql-router-context.test.js +133 -0
- package/src/hooks/__tests__/use-gql.test.js +1 -30
- package/src/hooks/__tests__/use-hydratable-effect.test.js +708 -0
- package/src/hooks/__tests__/use-request-interception.test.js +255 -0
- package/src/hooks/__tests__/use-server-effect.test.js +39 -11
- package/src/hooks/use-cached-effect.js +225 -0
- package/src/hooks/use-gql-router-context.js +50 -0
- package/src/hooks/use-gql.js +22 -52
- package/src/hooks/use-hydratable-effect.js +206 -0
- package/src/hooks/use-request-interception.js +51 -0
- package/src/hooks/use-server-effect.js +14 -7
- package/src/hooks/use-shared-cache.js +13 -11
- package/src/index.js +54 -2
- package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.js.snap +19 -0
- package/src/util/__tests__/merge-gql-context.test.js +74 -0
- package/src/util/__tests__/request-fulfillment.test.js +23 -42
- package/src/util/__tests__/request-tracking.test.js +26 -7
- package/src/util/__tests__/result-from-cache-response.test.js +19 -5
- package/src/util/__tests__/scoped-in-memory-cache.test.js +6 -85
- package/src/util/__tests__/serializable-in-memory-cache.test.js +398 -0
- package/src/util/__tests__/ssr-cache.test.js +52 -52
- package/src/util/abort-error.js +15 -0
- package/src/util/data-error.js +58 -0
- package/src/util/get-gql-data-from-response.js +3 -2
- package/src/util/gql-error.js +19 -11
- package/src/util/merge-gql-context.js +34 -0
- package/src/util/request-fulfillment.js +49 -46
- package/src/util/request-tracking.js +69 -15
- package/src/util/result-from-cache-response.js +12 -16
- package/src/util/scoped-in-memory-cache.js +24 -47
- package/src/util/serializable-in-memory-cache.js +49 -0
- package/src/util/ssr-cache.js +9 -8
- package/src/util/status.js +30 -0
- package/src/util/types.js +18 -1
- package/docs.md +0 -122
- package/src/components/__tests__/intercept-data.test.js +0 -63
- package/src/components/intercept-data.js +0 -66
- package/src/components/intercept-data.md +0 -51
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import {
|
|
2
|
+
import type {Result, ValidCacheData} from "./types.js";
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import {DataError, DataErrors} from "./data-error.js";
|
|
5
5
|
|
|
6
6
|
type RequestCache = {
|
|
7
|
-
[id: string]: Promise<any
|
|
7
|
+
[id: string]: Promise<Result<any>>,
|
|
8
8
|
...
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
let _default: RequestFulfillment;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* This fulfills a request, making sure that in-flight requests are shared.
|
|
15
|
+
*/
|
|
13
16
|
export class RequestFulfillment {
|
|
14
17
|
static get Default(): RequestFulfillment {
|
|
15
18
|
if (!_default) {
|
|
@@ -18,13 +21,8 @@ export class RequestFulfillment {
|
|
|
18
21
|
return _default;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
_responseCache: SsrCache;
|
|
22
24
|
_requests: RequestCache = {};
|
|
23
25
|
|
|
24
|
-
constructor(responseCache: ?SsrCache = undefined) {
|
|
25
|
-
this._responseCache = responseCache || SsrCache.Default;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
26
|
/**
|
|
29
27
|
* Get a promise of a request for a given handler and options.
|
|
30
28
|
*
|
|
@@ -34,19 +32,19 @@ export class RequestFulfillment {
|
|
|
34
32
|
fulfill: <TData: ValidCacheData>(
|
|
35
33
|
id: string,
|
|
36
34
|
options: {|
|
|
37
|
-
handler: () => Promise
|
|
35
|
+
handler: () => Promise<TData>,
|
|
38
36
|
hydrate?: boolean,
|
|
39
37
|
|},
|
|
40
|
-
) => Promise
|
|
38
|
+
) => Promise<Result<TData>> = <TData: ValidCacheData>(
|
|
41
39
|
id: string,
|
|
42
40
|
{
|
|
43
41
|
handler,
|
|
44
42
|
hydrate = true,
|
|
45
43
|
}: {|
|
|
46
|
-
handler: () => Promise
|
|
44
|
+
handler: () => Promise<TData>,
|
|
47
45
|
hydrate?: boolean,
|
|
48
46
|
|},
|
|
49
|
-
): Promise
|
|
47
|
+
): Promise<Result<TData>> => {
|
|
50
48
|
/**
|
|
51
49
|
* If we have an inflight request, we'll provide that.
|
|
52
50
|
*/
|
|
@@ -58,40 +56,45 @@ export class RequestFulfillment {
|
|
|
58
56
|
/**
|
|
59
57
|
* We don't have an inflight request, so let's set one up.
|
|
60
58
|
*/
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
const request = handler()
|
|
60
|
+
.then((data: TData): Result<TData> => ({
|
|
61
|
+
status: "success",
|
|
62
|
+
data,
|
|
63
|
+
}))
|
|
64
|
+
.catch((error: string | Error): Result<TData> => {
|
|
65
|
+
const actualError =
|
|
66
|
+
typeof error === "string"
|
|
67
|
+
? new DataError("Request failed", DataErrors.Unknown, {
|
|
68
|
+
metadata: {
|
|
69
|
+
unexpectedError: error,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
: error;
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
// Return aborted result if the request was aborted.
|
|
75
|
+
// The only way to detect this reliably, it seems, is to
|
|
76
|
+
// check the error name and see if it's "AbortError" (this
|
|
77
|
+
// is also what Apollo does).
|
|
78
|
+
// Even then, it's reliant on the handler supporting aborts.
|
|
79
|
+
// TODO(somewhatabstract, FEI-4276): Add first class abort
|
|
80
|
+
// support to the handler API.
|
|
81
|
+
if (actualError.name === "AbortError") {
|
|
82
|
+
return {
|
|
83
|
+
status: "aborted",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
status: "error",
|
|
88
|
+
error: actualError,
|
|
89
|
+
};
|
|
90
|
+
})
|
|
91
|
+
.finally(() => {
|
|
92
|
+
delete this._requests[id];
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Store the request in our cache.
|
|
96
|
+
this._requests[id] = request;
|
|
97
|
+
|
|
98
|
+
return request;
|
|
96
99
|
};
|
|
97
100
|
}
|
|
@@ -7,14 +7,14 @@ import type {ResponseCache, ValidCacheData} from "./types.js";
|
|
|
7
7
|
|
|
8
8
|
type TrackerFn = <TData: ValidCacheData>(
|
|
9
9
|
id: string,
|
|
10
|
-
handler: () => Promise
|
|
10
|
+
handler: () => Promise<TData>,
|
|
11
11
|
hydrate: boolean,
|
|
12
12
|
) => void;
|
|
13
13
|
|
|
14
14
|
type RequestCache = {
|
|
15
15
|
[id: string]: {|
|
|
16
|
-
hydrate
|
|
17
|
-
handler: () => Promise
|
|
16
|
+
hydrate: boolean,
|
|
17
|
+
handler: () => Promise<any>,
|
|
18
18
|
|},
|
|
19
19
|
...
|
|
20
20
|
};
|
|
@@ -55,7 +55,7 @@ export class RequestTracker {
|
|
|
55
55
|
|
|
56
56
|
constructor(responseCache: ?SsrCache = undefined) {
|
|
57
57
|
this._responseCache = responseCache || SsrCache.Default;
|
|
58
|
-
this._requestFulfillment = new RequestFulfillment(
|
|
58
|
+
this._requestFulfillment = new RequestFulfillment();
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -66,11 +66,11 @@ export class RequestTracker {
|
|
|
66
66
|
*/
|
|
67
67
|
trackDataRequest: <TData: ValidCacheData>(
|
|
68
68
|
id: string,
|
|
69
|
-
handler: () => Promise
|
|
69
|
+
handler: () => Promise<TData>,
|
|
70
70
|
hydrate: boolean,
|
|
71
71
|
) => void = <TData: ValidCacheData>(
|
|
72
72
|
id: string,
|
|
73
|
-
handler: () => Promise
|
|
73
|
+
handler: () => Promise<TData>,
|
|
74
74
|
hydrate: boolean,
|
|
75
75
|
): void => {
|
|
76
76
|
/**
|
|
@@ -114,13 +114,68 @@ export class RequestTracker {
|
|
|
114
114
|
fulfillTrackedRequests: () => Promise<ResponseCache> =
|
|
115
115
|
(): Promise<ResponseCache> => {
|
|
116
116
|
const promises = [];
|
|
117
|
+
const {cacheData, cacheError} = this._responseCache;
|
|
117
118
|
|
|
118
119
|
for (const requestKey of Object.keys(this._trackedRequests)) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
const options = this._trackedRequests[requestKey];
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
promises.push(
|
|
124
|
+
this._requestFulfillment
|
|
125
|
+
.fulfill(requestKey, {...options})
|
|
126
|
+
.then((result) => {
|
|
127
|
+
switch (result.status) {
|
|
128
|
+
case "success":
|
|
129
|
+
/**
|
|
130
|
+
* Let's cache the data!
|
|
131
|
+
*
|
|
132
|
+
* NOTE: This only caches when we're
|
|
133
|
+
* server side.
|
|
134
|
+
*/
|
|
135
|
+
cacheData(
|
|
136
|
+
requestKey,
|
|
137
|
+
result.data,
|
|
138
|
+
options.hydrate,
|
|
139
|
+
);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case "error":
|
|
143
|
+
/**
|
|
144
|
+
* Let's cache the error!
|
|
145
|
+
*
|
|
146
|
+
* NOTE: This only caches when we're
|
|
147
|
+
* server side.
|
|
148
|
+
*/
|
|
149
|
+
cacheError(
|
|
150
|
+
requestKey,
|
|
151
|
+
result.error,
|
|
152
|
+
options.hydrate,
|
|
153
|
+
);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// For status === "loading":
|
|
158
|
+
// Could never get here unless we wrote
|
|
159
|
+
// the code wrong. Rather than bloat
|
|
160
|
+
// code with useless error, just ignore.
|
|
161
|
+
|
|
162
|
+
// For status === "aborted":
|
|
163
|
+
// We won't cache this.
|
|
164
|
+
// We don't hydrate aborted requests,
|
|
165
|
+
// so the client would just see them
|
|
166
|
+
// as unfulfilled data.
|
|
167
|
+
return;
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
// This captures if there are problems in the code that
|
|
172
|
+
// begins the requests.
|
|
173
|
+
promises.push(
|
|
174
|
+
Promise.resolve(
|
|
175
|
+
cacheError(requestKey, e, options.hydrate),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
124
179
|
}
|
|
125
180
|
|
|
126
181
|
/**
|
|
@@ -129,16 +184,15 @@ export class RequestTracker {
|
|
|
129
184
|
* We call this now for a simpler API.
|
|
130
185
|
*
|
|
131
186
|
* If we reset the tracked calls after all promises resolve, any
|
|
132
|
-
*
|
|
187
|
+
* request tracking done while promises are in flight would be lost.
|
|
133
188
|
*
|
|
134
189
|
* If we don't reset at all, then we have to expose the `reset` call
|
|
135
190
|
* for consumers to use, or they'll only ever be able to accumulate
|
|
136
191
|
* more and more tracked requests, having to fulfill them all every
|
|
137
192
|
* time.
|
|
138
193
|
*
|
|
139
|
-
* Calling it here means we can have multiple "track -> request"
|
|
140
|
-
* in a row and in an easy to reason about manner.
|
|
141
|
-
*
|
|
194
|
+
* Calling it here means we can have multiple "track -> request"
|
|
195
|
+
* cycles in a row and in an easy to reason about manner.
|
|
142
196
|
*/
|
|
143
197
|
this.reset();
|
|
144
198
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
|
+
import {Status} from "./status.js";
|
|
3
|
+
import {DataError, DataErrors} from "./data-error.js";
|
|
2
4
|
import type {ValidCacheData, CachedResponse, Result} from "./types.js";
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -6,30 +8,24 @@ import type {ValidCacheData, CachedResponse, Result} from "./types.js";
|
|
|
6
8
|
*/
|
|
7
9
|
export const resultFromCachedResponse = <TData: ValidCacheData>(
|
|
8
10
|
cacheEntry: ?CachedResponse<TData>,
|
|
9
|
-
): Result<TData> => {
|
|
10
|
-
// No cache entry means
|
|
11
|
+
): ?Result<TData> => {
|
|
12
|
+
// No cache entry means no result to be hydrated.
|
|
11
13
|
if (cacheEntry == null) {
|
|
12
|
-
return
|
|
13
|
-
status: "loading",
|
|
14
|
-
};
|
|
14
|
+
return null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const {data, error} = cacheEntry;
|
|
18
18
|
if (error != null) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// Let's hydrate the error. We don't persist everything about the
|
|
20
|
+
// original error on the server, hence why we only superficially
|
|
21
|
+
// hydrate it to a GqlHydratedError.
|
|
22
|
+
return Status.error(new DataError(error, DataErrors.Hydrated));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
if (data != null) {
|
|
26
|
-
return
|
|
27
|
-
status: "success",
|
|
28
|
-
data,
|
|
29
|
-
};
|
|
26
|
+
return Status.success(data);
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
29
|
+
// We shouldn't get here since we don't actually cache null data.
|
|
30
|
+
return Status.aborted();
|
|
35
31
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
2
|
+
import {DataError, DataErrors} from "./data-error.js";
|
|
3
|
+
import type {ScopedCache, ValidCacheData} from "./types.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Describe an in-memory cache.
|
|
@@ -8,15 +8,8 @@ import type {ValidCacheData, ScopedCache} from "./types.js";
|
|
|
8
8
|
export class ScopedInMemoryCache {
|
|
9
9
|
_cache: ScopedCache;
|
|
10
10
|
|
|
11
|
-
constructor(initialCache: ScopedCache =
|
|
12
|
-
|
|
13
|
-
this._cache = clone(initialCache);
|
|
14
|
-
} catch (e) {
|
|
15
|
-
throw new KindError(
|
|
16
|
-
`An error occurred trying to initialize from a response cache snapshot: ${e}`,
|
|
17
|
-
Errors.InvalidInput,
|
|
18
|
-
);
|
|
19
|
-
}
|
|
11
|
+
constructor(initialCache: ScopedCache = {}) {
|
|
12
|
+
this._cache = initialCache;
|
|
20
13
|
}
|
|
21
14
|
|
|
22
15
|
/**
|
|
@@ -31,50 +24,47 @@ export class ScopedInMemoryCache {
|
|
|
31
24
|
/**
|
|
32
25
|
* Set a value in the cache.
|
|
33
26
|
*/
|
|
34
|
-
set
|
|
27
|
+
set<TValue: ValidCacheData>(
|
|
35
28
|
scope: string,
|
|
36
29
|
id: string,
|
|
37
30
|
value: TValue,
|
|
38
|
-
)
|
|
31
|
+
): void {
|
|
39
32
|
if (!id || typeof id !== "string") {
|
|
40
|
-
throw new
|
|
33
|
+
throw new DataError(
|
|
41
34
|
"id must be non-empty string",
|
|
42
|
-
|
|
35
|
+
DataErrors.InvalidInput,
|
|
43
36
|
);
|
|
44
37
|
}
|
|
45
38
|
|
|
46
39
|
if (!scope || typeof scope !== "string") {
|
|
47
|
-
throw new
|
|
40
|
+
throw new DataError(
|
|
48
41
|
"scope must be non-empty string",
|
|
49
|
-
|
|
42
|
+
DataErrors.InvalidInput,
|
|
50
43
|
);
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
if (typeof value === "function") {
|
|
54
|
-
throw new
|
|
47
|
+
throw new DataError(
|
|
55
48
|
"value must be a non-function value",
|
|
56
|
-
|
|
49
|
+
DataErrors.InvalidInput,
|
|
57
50
|
);
|
|
58
51
|
}
|
|
59
52
|
|
|
60
53
|
this._cache[scope] = this._cache[scope] ?? {};
|
|
61
|
-
this._cache[scope][id] =
|
|
62
|
-
}
|
|
54
|
+
this._cache[scope][id] = value;
|
|
55
|
+
}
|
|
63
56
|
|
|
64
57
|
/**
|
|
65
58
|
* Retrieve a value from the cache.
|
|
66
59
|
*/
|
|
67
|
-
get
|
|
68
|
-
scope,
|
|
69
|
-
id,
|
|
70
|
-
): ?ValidCacheData => {
|
|
60
|
+
get(scope: string, id: string): ?ValidCacheData {
|
|
71
61
|
return this._cache[scope]?.[id] ?? null;
|
|
72
|
-
}
|
|
62
|
+
}
|
|
73
63
|
|
|
74
64
|
/**
|
|
75
65
|
* Purge an item from the cache.
|
|
76
66
|
*/
|
|
77
|
-
purge
|
|
67
|
+
purge(scope: string, id: string): void {
|
|
78
68
|
if (!this._cache[scope]?.[id]) {
|
|
79
69
|
return;
|
|
80
70
|
}
|
|
@@ -82,17 +72,17 @@ export class ScopedInMemoryCache {
|
|
|
82
72
|
if (Object.keys(this._cache[scope]).length === 0) {
|
|
83
73
|
delete this._cache[scope];
|
|
84
74
|
}
|
|
85
|
-
}
|
|
75
|
+
}
|
|
86
76
|
|
|
87
77
|
/**
|
|
88
78
|
* Purge a scope of items that match the given predicate.
|
|
89
79
|
*
|
|
90
80
|
* If the predicate is omitted, then all items in the scope are purged.
|
|
91
81
|
*/
|
|
92
|
-
purgeScope
|
|
82
|
+
purgeScope(
|
|
93
83
|
scope: string,
|
|
94
84
|
predicate?: (id: string, value: ValidCacheData) => boolean,
|
|
95
|
-
)
|
|
85
|
+
): void {
|
|
96
86
|
if (!this._cache[scope]) {
|
|
97
87
|
return;
|
|
98
88
|
}
|
|
@@ -110,20 +100,20 @@ export class ScopedInMemoryCache {
|
|
|
110
100
|
if (Object.keys(this._cache[scope]).length === 0) {
|
|
111
101
|
delete this._cache[scope];
|
|
112
102
|
}
|
|
113
|
-
}
|
|
103
|
+
}
|
|
114
104
|
|
|
115
105
|
/**
|
|
116
106
|
* Purge all items from the cache that match the given predicate.
|
|
117
107
|
*
|
|
118
108
|
* If the predicate is omitted, then all items in the cache are purged.
|
|
119
109
|
*/
|
|
120
|
-
purgeAll
|
|
110
|
+
purgeAll(
|
|
121
111
|
predicate?: (
|
|
122
112
|
scope: string,
|
|
123
113
|
id: string,
|
|
124
114
|
value: ValidCacheData,
|
|
125
115
|
) => boolean,
|
|
126
|
-
)
|
|
116
|
+
): void {
|
|
127
117
|
if (predicate == null) {
|
|
128
118
|
this._cache = {};
|
|
129
119
|
return;
|
|
@@ -132,18 +122,5 @@ export class ScopedInMemoryCache {
|
|
|
132
122
|
for (const scope of Object.keys(this._cache)) {
|
|
133
123
|
this.purgeScope(scope, (id, value) => predicate(scope, id, value));
|
|
134
124
|
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Clone the cache.
|
|
139
|
-
*/
|
|
140
|
-
clone: () => ScopedCache = () => {
|
|
141
|
-
try {
|
|
142
|
-
return clone(this._cache);
|
|
143
|
-
} catch (e) {
|
|
144
|
-
throw new Error(
|
|
145
|
-
`An error occurred while trying to clone the cache: ${e}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
};
|
|
125
|
+
}
|
|
149
126
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {clone} from "@khanacademy/wonder-stuff-core";
|
|
3
|
+
import {DataError, DataErrors} from "./data-error.js";
|
|
4
|
+
import {ScopedInMemoryCache} from "./scoped-in-memory-cache.js";
|
|
5
|
+
import type {ValidCacheData, ScopedCache} from "./types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Describe a serializable in-memory cache.
|
|
9
|
+
*/
|
|
10
|
+
export class SerializableInMemoryCache extends ScopedInMemoryCache {
|
|
11
|
+
constructor(initialCache: ScopedCache = {}) {
|
|
12
|
+
try {
|
|
13
|
+
super(clone(initialCache));
|
|
14
|
+
} catch (e) {
|
|
15
|
+
throw new DataError(
|
|
16
|
+
`An error occurred trying to initialize from a response cache snapshot: ${e}`,
|
|
17
|
+
DataErrors.InvalidInput,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set a value in the cache.
|
|
24
|
+
*/
|
|
25
|
+
set<TValue: ValidCacheData>(
|
|
26
|
+
scope: string,
|
|
27
|
+
id: string,
|
|
28
|
+
value: TValue,
|
|
29
|
+
): void {
|
|
30
|
+
super.set(scope, id, Object.freeze(clone(value)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clone the cache.
|
|
35
|
+
*/
|
|
36
|
+
clone(): ScopedCache {
|
|
37
|
+
try {
|
|
38
|
+
return clone(this._cache);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
throw new DataError(
|
|
41
|
+
"An error occurred while trying to clone the cache",
|
|
42
|
+
DataErrors.Internal,
|
|
43
|
+
{
|
|
44
|
+
cause: e,
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/util/ssr-cache.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
3
|
-
import {
|
|
3
|
+
import {SerializableInMemoryCache} from "./serializable-in-memory-cache.js";
|
|
4
4
|
|
|
5
5
|
import type {ValidCacheData, CachedResponse, ResponseCache} from "./types.js";
|
|
6
6
|
|
|
@@ -25,17 +25,18 @@ export class SsrCache {
|
|
|
25
25
|
return _default;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
_hydrationCache:
|
|
29
|
-
_ssrOnlyCache: ?
|
|
28
|
+
_hydrationCache: SerializableInMemoryCache;
|
|
29
|
+
_ssrOnlyCache: ?SerializableInMemoryCache;
|
|
30
30
|
|
|
31
31
|
constructor(
|
|
32
|
-
hydrationCache: ?
|
|
33
|
-
ssrOnlyCache: ?
|
|
32
|
+
hydrationCache: ?SerializableInMemoryCache = null,
|
|
33
|
+
ssrOnlyCache: ?SerializableInMemoryCache = null,
|
|
34
34
|
) {
|
|
35
35
|
this._ssrOnlyCache = Server.isServerSide()
|
|
36
|
-
? ssrOnlyCache || new
|
|
36
|
+
? ssrOnlyCache || new SerializableInMemoryCache()
|
|
37
37
|
: undefined;
|
|
38
|
-
this._hydrationCache =
|
|
38
|
+
this._hydrationCache =
|
|
39
|
+
hydrationCache || new SerializableInMemoryCache();
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
_setCachedResponse<TData: ValidCacheData>(
|
|
@@ -70,7 +71,7 @@ export class SsrCache {
|
|
|
70
71
|
"Cannot initialize data response cache more than once",
|
|
71
72
|
);
|
|
72
73
|
}
|
|
73
|
-
this._hydrationCache = new
|
|
74
|
+
this._hydrationCache = new SerializableInMemoryCache({
|
|
74
75
|
// $FlowIgnore[incompatible-call]
|
|
75
76
|
[DefaultScope]: source,
|
|
76
77
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {Result, ValidCacheData} from "./types.js";
|
|
3
|
+
|
|
4
|
+
const loadingStatus = Object.freeze({
|
|
5
|
+
status: "loading",
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const abortedStatus = Object.freeze({
|
|
9
|
+
status: "aborted",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create Result<TData> instances with specific statuses.
|
|
14
|
+
*/
|
|
15
|
+
export const Status = Object.freeze({
|
|
16
|
+
loading: <TData: ValidCacheData = ValidCacheData>(): Result<TData> =>
|
|
17
|
+
loadingStatus,
|
|
18
|
+
aborted: <TData: ValidCacheData = ValidCacheData>(): Result<TData> =>
|
|
19
|
+
abortedStatus,
|
|
20
|
+
success: <TData: ValidCacheData>(data: TData): Result<TData> => ({
|
|
21
|
+
status: "success",
|
|
22
|
+
data,
|
|
23
|
+
}),
|
|
24
|
+
error: <TData: ValidCacheData = ValidCacheData>(
|
|
25
|
+
error: Error,
|
|
26
|
+
): Result<TData> => ({
|
|
27
|
+
status: "error",
|
|
28
|
+
error,
|
|
29
|
+
}),
|
|
30
|
+
});
|
package/src/util/types.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
|
+
import type {Metadata} from "@khanacademy/wonder-stuff-core";
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* Define what can be cached.
|
|
4
6
|
*
|
|
@@ -25,7 +27,7 @@ export type Result<TData: ValidCacheData> =
|
|
|
25
27
|
|}
|
|
26
28
|
| {|
|
|
27
29
|
status: "error",
|
|
28
|
-
error:
|
|
30
|
+
error: Error,
|
|
29
31
|
|}
|
|
30
32
|
| {|
|
|
31
33
|
status: "aborted",
|
|
@@ -68,3 +70,18 @@ export type ScopedCache = {
|
|
|
68
70
|
},
|
|
69
71
|
...
|
|
70
72
|
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Options to pass to error construction.
|
|
76
|
+
*/
|
|
77
|
+
export type ErrorOptions = {|
|
|
78
|
+
/**
|
|
79
|
+
* Metadata to attach to the error.
|
|
80
|
+
*/
|
|
81
|
+
metadata?: ?Metadata,
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The error that caused the error being constructed.
|
|
85
|
+
*/
|
|
86
|
+
cause?: ?Error,
|
|
87
|
+
|};
|