@khanacademy/wonder-blocks-data 7.0.0 → 8.0.1
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 +32 -0
- package/dist/es/index.js +321 -759
- package/dist/index.js +1188 -802
- package/package.json +3 -3
- package/src/__docs__/_overview_ssr_.stories.mdx +13 -13
- package/src/__docs__/exports.abort-inflight-requests.stories.mdx +20 -0
- package/src/__docs__/exports.data.stories.mdx +3 -3
- package/src/__docs__/{exports.fulfill-all-data-requests.stories.mdx → exports.fetch-tracked-requests.stories.mdx} +5 -5
- package/src/__docs__/exports.get-gql-request-id.stories.mdx +24 -0
- package/src/__docs__/{exports.has-unfulfilled-requests.stories.mdx → exports.has-tracked-requests-to-be-fetched.stories.mdx} +4 -4
- package/src/__docs__/exports.intialize-hydration-cache.stories.mdx +29 -0
- package/src/__docs__/exports.purge-caches.stories.mdx +23 -0
- package/src/__docs__/{exports.remove-all-from-cache.stories.mdx → exports.purge-hydration-cache.stories.mdx} +4 -4
- package/src/__docs__/{exports.clear-shared-cache.stories.mdx → exports.purge-shared-cache.stories.mdx} +4 -4
- package/src/__docs__/exports.track-data.stories.mdx +4 -4
- package/src/__docs__/exports.use-cached-effect.stories.mdx +7 -4
- package/src/__docs__/exports.use-gql.stories.mdx +1 -33
- package/src/__docs__/exports.use-server-effect.stories.mdx +1 -1
- package/src/__docs__/exports.use-shared-cache.stories.mdx +2 -2
- package/src/__docs__/types.fetch-policy.stories.mdx +44 -0
- package/src/__docs__/types.response-cache.stories.mdx +1 -1
- package/src/__tests__/generated-snapshot.test.js +5 -5
- package/src/components/__tests__/data.test.js +2 -6
- package/src/hooks/__tests__/use-cached-effect.test.js +341 -100
- package/src/hooks/__tests__/use-hydratable-effect.test.js +15 -9
- package/src/hooks/__tests__/use-shared-cache.test.js +6 -6
- package/src/hooks/use-cached-effect.js +169 -93
- package/src/hooks/use-hydratable-effect.js +8 -1
- package/src/hooks/use-shared-cache.js +2 -2
- package/src/index.js +14 -78
- package/src/util/__tests__/get-gql-request-id.test.js +74 -0
- package/src/util/__tests__/graphql-document-node-parser.test.js +542 -0
- package/src/util/__tests__/hydration-cache-api.test.js +35 -0
- package/src/util/__tests__/purge-caches.test.js +29 -0
- package/src/util/__tests__/request-api.test.js +188 -0
- package/src/util/__tests__/request-fulfillment.test.js +42 -0
- package/src/util/__tests__/ssr-cache.test.js +68 -60
- package/src/util/__tests__/to-gql-operation.test.js +42 -0
- package/src/util/data-error.js +6 -0
- package/src/util/get-gql-request-id.js +50 -0
- package/src/util/graphql-document-node-parser.js +133 -0
- package/src/util/graphql-types.js +30 -0
- package/src/util/hydration-cache-api.js +28 -0
- package/src/util/purge-caches.js +15 -0
- package/src/util/request-api.js +66 -0
- package/src/util/request-fulfillment.js +32 -12
- package/src/util/request-tracking.js +1 -1
- package/src/util/ssr-cache.js +11 -24
- package/src/util/to-gql-operation.js +44 -0
- package/src/util/types.js +31 -0
- package/src/__docs__/exports.intialize-cache.stories.mdx +0 -29
- package/src/__docs__/exports.remove-from-cache.stories.mdx +0 -25
- package/src/__docs__/exports.request-fulfillment.stories.mdx +0 -36
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import {useForceUpdate} from "@khanacademy/wonder-blocks-core";
|
|
4
|
+
import {DataError, DataErrors} from "../util/data-error.js";
|
|
4
5
|
|
|
5
6
|
import {RequestFulfillment} from "../util/request-fulfillment.js";
|
|
6
7
|
import {Status} from "../util/status.js";
|
|
@@ -10,7 +11,21 @@ import {useRequestInterception} from "./use-request-interception.js";
|
|
|
10
11
|
|
|
11
12
|
import type {Result, ValidCacheData} from "../util/types.js";
|
|
12
13
|
|
|
14
|
+
// TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
|
|
15
|
+
// have fixed:
|
|
16
|
+
// https://github.com/import-js/eslint-plugin-import/issues/2073
|
|
17
|
+
// eslint-disable-next-line import/named
|
|
18
|
+
import {FetchPolicy} from "../util/types.js";
|
|
19
|
+
|
|
13
20
|
type CachedEffectOptions<TData: ValidCacheData> = {|
|
|
21
|
+
/**
|
|
22
|
+
* The policy to use when determining how to retrieve the request data from
|
|
23
|
+
* cache and network.
|
|
24
|
+
*
|
|
25
|
+
* Defaults to `FetchPolicy.CacheBeforeNetwork`.
|
|
26
|
+
*/
|
|
27
|
+
fetchPolicy?: FetchPolicy,
|
|
28
|
+
|
|
14
29
|
/**
|
|
15
30
|
* When `true`, the effect will not be executed; otherwise, the effect will
|
|
16
31
|
* be executed.
|
|
@@ -81,8 +96,9 @@ export const useCachedEffect = <TData: ValidCacheData>(
|
|
|
81
96
|
options: CachedEffectOptions<TData> = ({}: $Shape<
|
|
82
97
|
CachedEffectOptions<TData>,
|
|
83
98
|
>),
|
|
84
|
-
): Result<TData
|
|
99
|
+
): [Result<TData>, () => void] => {
|
|
85
100
|
const {
|
|
101
|
+
fetchPolicy = FetchPolicy.CacheBeforeNetwork,
|
|
86
102
|
skip: hardSkip = false,
|
|
87
103
|
retainResultOnChange = false,
|
|
88
104
|
onResultChanged,
|
|
@@ -104,107 +120,166 @@ export const useCachedEffect = <TData: ValidCacheData>(
|
|
|
104
120
|
// that all calls when the request is in-flight will update once that
|
|
105
121
|
// request is done, we want the cache to be empty until that point.
|
|
106
122
|
);
|
|
107
|
-
|
|
108
|
-
// Build a function that will update the cache and either invoke the
|
|
109
|
-
// callback provided in options, or force an update.
|
|
110
123
|
const forceUpdate = useForceUpdate();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
// For the NetworkOnly fetch policy, we ignore the cached value.
|
|
125
|
+
// So we need somewhere else to store the network value.
|
|
126
|
+
const networkResultRef = React.useRef();
|
|
127
|
+
|
|
128
|
+
// Set up the function that will do the fetching.
|
|
129
|
+
const currentRequestRef = React.useRef();
|
|
130
|
+
const fetchRequest = React.useMemo(() => {
|
|
131
|
+
// We aren't using useCallback here because we need to make sure that
|
|
132
|
+
// if we are rememo-izing, we cancel any inflight request for the old
|
|
133
|
+
// callback.
|
|
134
|
+
currentRequestRef.current?.cancel();
|
|
135
|
+
currentRequestRef.current = null;
|
|
136
|
+
networkResultRef.current = null;
|
|
137
|
+
|
|
138
|
+
const fetchFn = () => {
|
|
139
|
+
if (fetchPolicy === FetchPolicy.CacheOnly) {
|
|
140
|
+
throw new DataError(
|
|
141
|
+
"Cannot fetch with CacheOnly policy",
|
|
142
|
+
DataErrors.NotAllowed,
|
|
143
|
+
);
|
|
121
144
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
// We use our request fulfillment here so that in-flight
|
|
146
|
+
// requests are shared. In order to ensure that we don't share
|
|
147
|
+
// in-flight requests for different scopes, we add the scope to the
|
|
148
|
+
// requestId.
|
|
149
|
+
// We do this as a courtesy to simplify usage in sandboxed
|
|
150
|
+
// uses like storybook where we want each story to perform their
|
|
151
|
+
// own requests from scratch and not share inflight requests across
|
|
152
|
+
// stories.
|
|
153
|
+
// Since this only occurs here, nothing else will care about this
|
|
154
|
+
// change except the request tracking.
|
|
155
|
+
const request = RequestFulfillment.Default.fulfill(
|
|
156
|
+
`${requestId}|${scope}`,
|
|
157
|
+
{
|
|
158
|
+
handler: interceptedHandler,
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (request === currentRequestRef.current?.request) {
|
|
163
|
+
// The request inflight is the same, so do nothing.
|
|
164
|
+
// NOTE: Perhaps if invoked via a refetch, we will want to
|
|
165
|
+
// override this behavior and force a new request?
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Clear the last network result.
|
|
170
|
+
networkResultRef.current = null;
|
|
171
|
+
|
|
172
|
+
// Cancel the previous request.
|
|
173
|
+
currentRequestRef.current?.cancel();
|
|
174
|
+
|
|
175
|
+
// TODO(somewhatabstract, FEI-4276):
|
|
176
|
+
// Until our RequestFulfillment API supports cancelling/aborting, we
|
|
177
|
+
// will have to do it.
|
|
178
|
+
let cancel = false;
|
|
179
|
+
|
|
180
|
+
// NOTE: Our request fulfillment handles the error cases here.
|
|
181
|
+
// Catching shouldn't serve a purpose.
|
|
182
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
183
|
+
request.then((result) => {
|
|
184
|
+
currentRequestRef.current = null;
|
|
185
|
+
if (cancel) {
|
|
186
|
+
// We don't modify our result if the request was cancelled
|
|
187
|
+
// as it means that this hook no longer cares about that old
|
|
188
|
+
// request.
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Now we need to update the cache and notify or force a rerender.
|
|
193
|
+
setMostRecentResult(result);
|
|
194
|
+
networkResultRef.current = result;
|
|
195
|
+
|
|
196
|
+
if (onResultChanged != null) {
|
|
197
|
+
// If we have a callback, call it to let our caller know we
|
|
198
|
+
// got a result.
|
|
199
|
+
onResultChanged(result);
|
|
200
|
+
} else {
|
|
201
|
+
// If there's no callback, and this is using cache in some
|
|
202
|
+
// capacity, just force a rerender.
|
|
203
|
+
forceUpdate();
|
|
204
|
+
}
|
|
205
|
+
return; // Shut up eslint always-return rule.
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
currentRequestRef.current = {
|
|
209
|
+
requestId,
|
|
210
|
+
request,
|
|
211
|
+
cancel() {
|
|
212
|
+
cancel = true;
|
|
213
|
+
RequestFulfillment.Default.abort(requestId);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Now we can return the new fetch function.
|
|
219
|
+
return fetchFn;
|
|
220
|
+
|
|
221
|
+
// We deliberately ignore the handler here because we want folks to use
|
|
222
|
+
// interceptor functions inline in props for simplicity. This is OK
|
|
223
|
+
// since changing the handler without changing the requestId doesn't
|
|
224
|
+
// really make sense - the same requestId should be handled the same as
|
|
225
|
+
// each other.
|
|
226
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
227
|
+
}, [
|
|
228
|
+
requestId,
|
|
229
|
+
onResultChanged,
|
|
230
|
+
forceUpdate,
|
|
231
|
+
setMostRecentResult,
|
|
232
|
+
fetchPolicy,
|
|
233
|
+
]);
|
|
125
234
|
|
|
126
235
|
// We need to trigger a re-render when the request ID changes as that
|
|
127
|
-
// indicates its a different request.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const softSkip = React.useMemo(() => {
|
|
138
|
-
if (requestId === previousRequestId) {
|
|
139
|
-
// If the requestId is unchanged, it means we already rendered at
|
|
140
|
-
// least once and so we already made the request at least once. So
|
|
141
|
-
// we can bail out right here.
|
|
142
|
-
return true;
|
|
236
|
+
// indicates its a different request.
|
|
237
|
+
const requestIdRef = React.useRef(requestId);
|
|
238
|
+
|
|
239
|
+
// Calculate if we want to fetch the result or not.
|
|
240
|
+
// If this is true, we will do a new fetch, cancelling the previous fetch
|
|
241
|
+
// if there is one inflight.
|
|
242
|
+
const shouldFetch = React.useMemo(() => {
|
|
243
|
+
if (hardSkip) {
|
|
244
|
+
// We don't fetch if we've been told to hard skip.
|
|
245
|
+
return false;
|
|
143
246
|
}
|
|
144
247
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
248
|
+
switch (fetchPolicy) {
|
|
249
|
+
case FetchPolicy.CacheOnly:
|
|
250
|
+
// Don't want to do a network request if we're only
|
|
251
|
+
// interested in the cache.
|
|
252
|
+
return false;
|
|
253
|
+
|
|
254
|
+
case FetchPolicy.CacheBeforeNetwork:
|
|
255
|
+
// If we don't have a cached value or this is a new requestId,
|
|
256
|
+
// then we need to fetch.
|
|
257
|
+
return (
|
|
258
|
+
mostRecentResult == null ||
|
|
259
|
+
requestId !== requestIdRef.current
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
case FetchPolicy.CacheAndNetwork:
|
|
263
|
+
case FetchPolicy.NetworkOnly:
|
|
264
|
+
// We don't care about the cache. If we don't have a network
|
|
265
|
+
// result, then we need to fetch one.
|
|
266
|
+
return networkResultRef.current == null;
|
|
148
267
|
}
|
|
268
|
+
}, [requestId, mostRecentResult, fetchPolicy, hardSkip]);
|
|
149
269
|
|
|
150
|
-
|
|
151
|
-
|
|
270
|
+
// Let's make sure our ref is set to the most recent requestId.
|
|
271
|
+
requestIdRef.current = requestId;
|
|
152
272
|
|
|
153
|
-
// So now we make sure the client-side request happens per our various
|
|
154
|
-
// options.
|
|
155
273
|
React.useEffect(() => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// We don't do anything if we've been told to hard skip (a hard skip
|
|
159
|
-
// means we should cancel the previous request and is therefore a
|
|
160
|
-
// dependency on that), or we have determined we have already done
|
|
161
|
-
// enough and can soft skip (a soft skip doesn't trigger the request
|
|
162
|
-
// to re-run; we don't want to cancel the in progress effect if we're
|
|
163
|
-
// soft skipping.
|
|
164
|
-
if (hardSkip || softSkip) {
|
|
274
|
+
if (!shouldFetch) {
|
|
165
275
|
return;
|
|
166
276
|
}
|
|
167
|
-
|
|
168
|
-
// If we got here, we're going to perform the request.
|
|
169
|
-
// Let's make sure our ref is set to the most recent requestId.
|
|
170
|
-
requestIdRef.current = requestId;
|
|
171
|
-
|
|
172
|
-
// OK, we've done all our checks and things. It's time to make the
|
|
173
|
-
// request. We use our request fulfillment here so that in-flight
|
|
174
|
-
// requests are shared.
|
|
175
|
-
// NOTE: Our request fulfillment handles the error cases here.
|
|
176
|
-
// Catching shouldn't serve a purpose.
|
|
177
|
-
// eslint-disable-next-line promise/catch-or-return
|
|
178
|
-
RequestFulfillment.Default.fulfill(requestId, {
|
|
179
|
-
handler: interceptedHandler,
|
|
180
|
-
}).then((result) => {
|
|
181
|
-
if (cancel) {
|
|
182
|
-
// We don't modify our result if an earlier effect was
|
|
183
|
-
// cancelled as it means that this hook no longer cares about
|
|
184
|
-
// that old request.
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
setCacheAndNotify(result);
|
|
189
|
-
return; // Shut up eslint always-return rule.
|
|
190
|
-
});
|
|
191
|
-
|
|
277
|
+
fetchRequest();
|
|
192
278
|
return () => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// (Of course, we will only want to abort them if no one is waiting
|
|
196
|
-
// on them)
|
|
197
|
-
// For now, we just block cancelled requests from changing our
|
|
198
|
-
// cache.
|
|
199
|
-
cancel = true;
|
|
279
|
+
currentRequestRef.current?.cancel();
|
|
280
|
+
currentRequestRef.current = null;
|
|
200
281
|
};
|
|
201
|
-
|
|
202
|
-
// change. These are the only two things that should affect the
|
|
203
|
-
// cancellation of a pending request. We do not update if the handler
|
|
204
|
-
// changes, in order to simplify the API - otherwise, callers would
|
|
205
|
-
// not be able to use inline functions with this hook.
|
|
206
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
207
|
-
}, [hardSkip, requestId]);
|
|
282
|
+
}, [shouldFetch, fetchRequest]);
|
|
208
283
|
|
|
209
284
|
// We track the last result we returned in order to support the
|
|
210
285
|
// "retainResultOnChange" option.
|
|
@@ -215,11 +290,12 @@ export const useCachedEffect = <TData: ValidCacheData>(
|
|
|
215
290
|
|
|
216
291
|
// Loading is a transient state, so we only use it here; it's not something
|
|
217
292
|
// we cache.
|
|
218
|
-
const result =
|
|
219
|
-
(
|
|
220
|
-
|
|
221
|
-
|
|
293
|
+
const result =
|
|
294
|
+
(fetchPolicy === FetchPolicy.NetworkOnly
|
|
295
|
+
? networkResultRef.current
|
|
296
|
+
: mostRecentResult) ?? loadingResult;
|
|
222
297
|
lastResultAgnosticOfIdRef.current = result;
|
|
223
298
|
|
|
224
|
-
return result
|
|
299
|
+
// We return the result and a function for triggering a refetch.
|
|
300
|
+
return [result, fetchRequest];
|
|
225
301
|
};
|
|
@@ -5,6 +5,11 @@ import {useServerEffect} from "./use-server-effect.js";
|
|
|
5
5
|
import {useSharedCache} from "./use-shared-cache.js";
|
|
6
6
|
import {useCachedEffect} from "./use-cached-effect.js";
|
|
7
7
|
|
|
8
|
+
// TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
|
|
9
|
+
// have fixed:
|
|
10
|
+
// https://github.com/import-js/eslint-plugin-import/issues/2073
|
|
11
|
+
// eslint-disable-next-line import/named
|
|
12
|
+
import {FetchPolicy} from "../util/types.js";
|
|
8
13
|
import type {Result, ValidCacheData} from "../util/types.js";
|
|
9
14
|
|
|
10
15
|
/**
|
|
@@ -191,11 +196,13 @@ export const useHydratableEffect = <TData: ValidCacheData>(
|
|
|
191
196
|
);
|
|
192
197
|
|
|
193
198
|
// When we're client-side, we ultimately want the result from this call.
|
|
194
|
-
const clientResult = useCachedEffect(requestId, handler, {
|
|
199
|
+
const [clientResult] = useCachedEffect(requestId, handler, {
|
|
195
200
|
skip,
|
|
196
201
|
onResultChanged,
|
|
197
202
|
retainResultOnChange,
|
|
198
203
|
scope,
|
|
204
|
+
// Be explicit about our fetch policy for clarity.
|
|
205
|
+
fetchPolicy: FetchPolicy.CacheBeforeNetwork,
|
|
199
206
|
});
|
|
200
207
|
|
|
201
208
|
// OK, now which result do we return.
|
|
@@ -17,9 +17,9 @@ type CacheValueFn<TValue: ValidCacheData> = (value: ?TValue) => void;
|
|
|
17
17
|
const cache = new ScopedInMemoryCache();
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Purge the in-memory cache or a single scope within it.
|
|
21
21
|
*/
|
|
22
|
-
export const
|
|
22
|
+
export const purgeSharedCache = (scope: string = "") => {
|
|
23
23
|
// If we have a valid scope (empty string is falsy), then clear that scope.
|
|
24
24
|
if (scope && typeof scope === "string") {
|
|
25
25
|
cache.purgeScope(scope);
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
ValidCacheData,
|
|
8
|
-
CachedResponse,
|
|
9
|
-
ResponseCache,
|
|
10
|
-
} from "./util/types.js";
|
|
11
|
-
|
|
2
|
+
// TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
|
|
3
|
+
// have fixed:
|
|
4
|
+
// https://github.com/import-js/eslint-plugin-import/issues/2073
|
|
5
|
+
// eslint-disable-next-line import/named
|
|
6
|
+
export {FetchPolicy} from "./util/types.js";
|
|
12
7
|
export type {
|
|
13
8
|
ErrorOptions,
|
|
14
9
|
ResponseCache,
|
|
@@ -18,79 +13,16 @@ export type {
|
|
|
18
13
|
ValidCacheData,
|
|
19
14
|
} from "./util/types.js";
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
* @param {ResponseCache} source The cache content to use for initializing the
|
|
25
|
-
* cache.
|
|
26
|
-
* @throws {Error} If the cache is already initialized.
|
|
27
|
-
*/
|
|
28
|
-
export const initializeCache = (source: ResponseCache): void =>
|
|
29
|
-
SsrCache.Default.initialize(source);
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Fulfill all tracked data requests.
|
|
33
|
-
*
|
|
34
|
-
* This is for use with the `TrackData` component during server-side rendering.
|
|
35
|
-
*
|
|
36
|
-
* @throws {Error} If executed outside of server-side rendering.
|
|
37
|
-
* @returns {Promise<void>} A promise that resolves when all tracked requests
|
|
38
|
-
* have been fulfilled.
|
|
39
|
-
*/
|
|
40
|
-
export const fulfillAllDataRequests = (): Promise<ResponseCache> => {
|
|
41
|
-
if (!Server.isServerSide()) {
|
|
42
|
-
return Promise.reject(
|
|
43
|
-
new Error("Data requests are not tracked when client-side"),
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
return RequestTracker.Default.fulfillTrackedRequests();
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Indicate if there are unfulfilled tracked requests.
|
|
51
|
-
*
|
|
52
|
-
* This is used in conjunction with `TrackData`.
|
|
53
|
-
*
|
|
54
|
-
* @throws {Error} If executed outside of server-side rendering.
|
|
55
|
-
* @returns {boolean} `true` if there are unfulfilled tracked requests;
|
|
56
|
-
* otherwise, `false`.
|
|
57
|
-
*/
|
|
58
|
-
export const hasUnfulfilledRequests = (): boolean => {
|
|
59
|
-
if (!Server.isServerSide()) {
|
|
60
|
-
throw new Error("Data requests are not tracked when client-side");
|
|
61
|
-
}
|
|
62
|
-
return RequestTracker.Default.hasUnfulfilledRequests;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Remove the request identified from the cached hydration responses.
|
|
67
|
-
*
|
|
68
|
-
* @param {string} id The request ID of the response to remove from the cache.
|
|
69
|
-
*/
|
|
70
|
-
export const removeFromCache = (id: string): boolean =>
|
|
71
|
-
SsrCache.Default.remove(id);
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Remove all cached hydration responses that match the given predicate.
|
|
75
|
-
*
|
|
76
|
-
* @param {(id: string) => boolean} [predicate] The predicate to match against
|
|
77
|
-
* the cached hydration responses. If no predicate is provided, all cached
|
|
78
|
-
* hydration responses will be removed.
|
|
79
|
-
*/
|
|
80
|
-
export const removeAllFromCache = (
|
|
81
|
-
predicate?: (
|
|
82
|
-
key: string,
|
|
83
|
-
cacheEntry: ?$ReadOnly<CachedResponse<ValidCacheData>>,
|
|
84
|
-
) => boolean,
|
|
85
|
-
): void => SsrCache.Default.removeAll(predicate);
|
|
86
|
-
|
|
16
|
+
export * from "./util/hydration-cache-api.js";
|
|
17
|
+
export * from "./util/request-api.js";
|
|
18
|
+
export {purgeCaches} from "./util/purge-caches.js";
|
|
87
19
|
export {default as TrackData} from "./components/track-data.js";
|
|
88
20
|
export {default as Data} from "./components/data.js";
|
|
89
21
|
export {default as InterceptRequests} from "./components/intercept-requests.js";
|
|
90
22
|
export {DataError, DataErrors} from "./util/data-error.js";
|
|
91
23
|
export {useServerEffect} from "./hooks/use-server-effect.js";
|
|
92
24
|
export {useCachedEffect} from "./hooks/use-cached-effect.js";
|
|
93
|
-
export {useSharedCache,
|
|
25
|
+
export {useSharedCache, purgeSharedCache} from "./hooks/use-shared-cache.js";
|
|
94
26
|
export {
|
|
95
27
|
useHydratableEffect,
|
|
96
28
|
// TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
|
|
@@ -101,10 +33,14 @@ export {
|
|
|
101
33
|
} from "./hooks/use-hydratable-effect.js";
|
|
102
34
|
export {ScopedInMemoryCache} from "./util/scoped-in-memory-cache.js";
|
|
103
35
|
export {SerializableInMemoryCache} from "./util/serializable-in-memory-cache.js";
|
|
104
|
-
export {RequestFulfillment} from "./util/request-fulfillment.js";
|
|
105
36
|
export {Status} from "./util/status.js";
|
|
106
37
|
|
|
38
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
107
39
|
// GraphQL
|
|
40
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
41
|
+
export {getGqlRequestId} from "./util/get-gql-request-id.js";
|
|
42
|
+
export {graphQLDocumentNodeParser} from "./util/graphql-document-node-parser.js";
|
|
43
|
+
export {toGqlOperation} from "./util/to-gql-operation.js";
|
|
108
44
|
export {GqlRouter} from "./components/gql-router.js";
|
|
109
45
|
export {useGql} from "./hooks/use-gql.js";
|
|
110
46
|
export {GqlError, GqlErrors} from "./util/gql-error.js";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import {getGqlRequestId} from "../get-gql-request-id.js";
|
|
4
|
+
|
|
5
|
+
describe("#getGqlRequestId", () => {
|
|
6
|
+
it("should include the id of the query", () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const operation = {
|
|
9
|
+
type: "query",
|
|
10
|
+
id: "myQuery",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Act
|
|
14
|
+
const requestId = getGqlRequestId(operation, null, {
|
|
15
|
+
module: "MODULE",
|
|
16
|
+
curriculum: "CURRICULUM",
|
|
17
|
+
targetLocale: "LOCALE",
|
|
18
|
+
});
|
|
19
|
+
const result = new Set(requestId.split("|"));
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
expect(result).toContain("myQuery");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should include the context values sorted by key", () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const operation = {
|
|
28
|
+
type: "query",
|
|
29
|
+
id: "myQuery",
|
|
30
|
+
};
|
|
31
|
+
const context = {
|
|
32
|
+
context3: "value3",
|
|
33
|
+
context2: "value2",
|
|
34
|
+
context1: "value1",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
const requestId = getGqlRequestId(operation, null, context);
|
|
39
|
+
const result = new Set(requestId.split("|"));
|
|
40
|
+
|
|
41
|
+
// Assert
|
|
42
|
+
expect(result).toContain(
|
|
43
|
+
`context1=value1&context2=value2&context3=value3`,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should include the variables, sorted by key, if present", () => {
|
|
48
|
+
// Arrange
|
|
49
|
+
const operation = {
|
|
50
|
+
type: "query",
|
|
51
|
+
id: "myQuery",
|
|
52
|
+
};
|
|
53
|
+
const variables = {
|
|
54
|
+
variable4: null,
|
|
55
|
+
variable2: 42,
|
|
56
|
+
variable1: "value1",
|
|
57
|
+
variable5: true,
|
|
58
|
+
variable3: undefined,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Act
|
|
62
|
+
const requestId = getGqlRequestId(operation, variables, {
|
|
63
|
+
module: "MODULE",
|
|
64
|
+
curriculum: "CURRICULUM",
|
|
65
|
+
targetLocale: "LOCALE",
|
|
66
|
+
});
|
|
67
|
+
const result = new Set(requestId.split("|"));
|
|
68
|
+
|
|
69
|
+
// Assert
|
|
70
|
+
expect(result).toContain(
|
|
71
|
+
`variable1=value1&variable2=42&variable3=&variable4=null&variable5=true`,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|