@khanacademy/wonder-blocks-data 12.0.0 → 13.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 +11 -0
- package/dist/components/data.d.ts +2 -2
- package/dist/es/index.js +10 -6
- package/dist/hooks/use-gql.d.ts +5 -1
- package/dist/index.js +10 -6
- package/dist/util/status.d.ts +4 -3
- package/dist/util/types.d.ts +2 -0
- package/package.json +2 -2
- package/src/components/__tests__/data.test.tsx +6 -13
- package/src/components/data.ts +2 -4
- package/src/hooks/__tests__/use-cached-effect.test.tsx +79 -40
- package/src/hooks/__tests__/use-gql-router-context.test.tsx +1 -2
- package/src/hooks/__tests__/use-hydratable-effect.test.ts +1 -2
- package/src/hooks/__tests__/use-request-interception.test.tsx +2 -5
- package/src/hooks/__tests__/use-server-effect.test.ts +3 -6
- package/src/hooks/__tests__/use-shared-cache.test.ts +17 -13
- package/src/hooks/use-cached-effect.ts +24 -20
- package/src/hooks/use-gql.ts +12 -9
- package/src/hooks/use-request-interception.ts +13 -11
- package/src/hooks/use-shared-cache.ts +4 -2
- package/src/util/__tests__/request-api.test.ts +2 -1
- package/src/util/__tests__/request-tracking.test.tsx +5 -9
- package/src/util/__tests__/result-from-cache-response.test.ts +2 -2
- package/src/util/__tests__/serializable-in-memory-cache.test.ts +1 -2
- package/src/util/__tests__/ssr-cache.test.ts +2 -4
- package/src/util/__tests__/to-gql-operation.test.ts +2 -4
- package/src/util/graphql-document-node-parser.ts +6 -6
- package/src/util/merge-gql-context.ts +2 -1
- package/src/util/request-tracking.ts +6 -2
- package/src/util/ssr-cache.ts +11 -8
- package/src/util/status.ts +6 -0
- package/src/util/types.ts +3 -0
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -68,7 +68,7 @@ describe("#useRequestInterception", () => {
|
|
|
68
68
|
const handler = jest.fn();
|
|
69
69
|
const interceptor1 = jest.fn();
|
|
70
70
|
const interceptor2 = jest.fn();
|
|
71
|
-
const Wrapper = ({children, interceptor}: any) => (
|
|
71
|
+
const Wrapper = ({children, interceptor}: any): React.ReactElement => (
|
|
72
72
|
<InterceptRequests interceptor={interceptor}>
|
|
73
73
|
{children}
|
|
74
74
|
</InterceptRequests>
|
|
@@ -80,8 +80,7 @@ describe("#useRequestInterception", () => {
|
|
|
80
80
|
{wrapper: Wrapper, initialProps: {interceptor: interceptor1}},
|
|
81
81
|
);
|
|
82
82
|
const result1 = wrapper.result.current;
|
|
83
|
-
|
|
84
|
-
wrapper.rerender({wrapper: Wrapper, interceptor: interceptor2});
|
|
83
|
+
wrapper.rerender({interceptor: interceptor2});
|
|
85
84
|
const result2 = wrapper.result.current;
|
|
86
85
|
|
|
87
86
|
// Assert
|
|
@@ -126,7 +125,6 @@ describe("#useRequestInterception", () => {
|
|
|
126
125
|
interceptedHandler();
|
|
127
126
|
|
|
128
127
|
// Assert
|
|
129
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'toHaveBeenCalledBefore' does not exist on type 'JestMatchers<Mock<null, [], any>>'.
|
|
130
128
|
expect(interceptorNearest).toHaveBeenCalledBefore(
|
|
131
129
|
interceptorFurthest,
|
|
132
130
|
);
|
|
@@ -154,7 +152,6 @@ describe("#useRequestInterception", () => {
|
|
|
154
152
|
interceptedHandler();
|
|
155
153
|
|
|
156
154
|
// Assert
|
|
157
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'toHaveBeenCalledBefore' does not exist on type 'JestMatchers<Mock<null, [], any>>'.
|
|
158
155
|
expect(interceptorFurthest).toHaveBeenCalledBefore(handler);
|
|
159
156
|
});
|
|
160
157
|
|
|
@@ -139,8 +139,7 @@ describe("#useServerEffect", () => {
|
|
|
139
139
|
const interceptedHandler = jest.fn();
|
|
140
140
|
jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
|
|
141
141
|
data: "DATA",
|
|
142
|
-
|
|
143
|
-
error: null,
|
|
142
|
+
error: undefined,
|
|
144
143
|
});
|
|
145
144
|
jest.spyOn(
|
|
146
145
|
UseRequestInterception,
|
|
@@ -165,8 +164,7 @@ describe("#useServerEffect", () => {
|
|
|
165
164
|
const fakeHandler = jest.fn();
|
|
166
165
|
jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
|
|
167
166
|
data: "DATA",
|
|
168
|
-
|
|
169
|
-
error: null,
|
|
167
|
+
error: undefined,
|
|
170
168
|
});
|
|
171
169
|
|
|
172
170
|
// Act
|
|
@@ -221,8 +219,7 @@ describe("#useServerEffect", () => {
|
|
|
221
219
|
const fakeHandler = jest.fn();
|
|
222
220
|
jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
|
|
223
221
|
data: "DATA",
|
|
224
|
-
|
|
225
|
-
error: null,
|
|
222
|
+
error: undefined,
|
|
226
223
|
});
|
|
227
224
|
|
|
228
225
|
// Act
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-unassigned-import
|
|
2
|
+
import "jest-extended";
|
|
1
3
|
import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
|
|
2
4
|
|
|
3
5
|
import {useSharedCache, SharedCache} from "../use-shared-cache";
|
|
@@ -48,7 +50,6 @@ describe("#useSharedCache", () => {
|
|
|
48
50
|
} = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
49
51
|
|
|
50
52
|
// Assert
|
|
51
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'toBeArrayOfSize' does not exist on type 'JestMatchers<[ValidCacheData | null | undefined, CacheValueFn<ValidCacheData>]>'.
|
|
52
53
|
expect(result).toBeArrayOfSize(2);
|
|
53
54
|
});
|
|
54
55
|
|
|
@@ -119,12 +120,13 @@ describe("#useSharedCache", () => {
|
|
|
119
120
|
id: "id",
|
|
120
121
|
scope: "scope",
|
|
121
122
|
});
|
|
122
|
-
const
|
|
123
|
-
const
|
|
123
|
+
const value1 = wrapper.result.all[wrapper.result.all.length - 2];
|
|
124
|
+
const value2 = wrapper.result.current;
|
|
125
|
+
const result1 = Array.isArray(value1) ? value1[1] : "BAD1";
|
|
126
|
+
const result2 = Array.isArray(value2) ? value2[1] : "BAD2";
|
|
124
127
|
|
|
125
128
|
// Assert
|
|
126
|
-
|
|
127
|
-
expect(result1[1]).toBe(result2[1]);
|
|
129
|
+
expect(result1).toBe(result2);
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
it("should be a new function if the id changes", () => {
|
|
@@ -138,12 +140,13 @@ describe("#useSharedCache", () => {
|
|
|
138
140
|
|
|
139
141
|
// Act
|
|
140
142
|
wrapper.rerender({id: "new-id"});
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
+
const value1 = wrapper.result.all[wrapper.result.all.length - 2];
|
|
144
|
+
const value2 = wrapper.result.current;
|
|
145
|
+
const result1 = Array.isArray(value1) ? value1[1] : "BAD1";
|
|
146
|
+
const result2 = Array.isArray(value2) ? value2[1] : "BAD2";
|
|
143
147
|
|
|
144
148
|
// Assert
|
|
145
|
-
|
|
146
|
-
expect(result1[1]).not.toBe(result2[1]);
|
|
149
|
+
expect(result1).not.toBe(result2);
|
|
147
150
|
});
|
|
148
151
|
|
|
149
152
|
it("should be a new function if the scope changes", () => {
|
|
@@ -157,12 +160,13 @@ describe("#useSharedCache", () => {
|
|
|
157
160
|
|
|
158
161
|
// Act
|
|
159
162
|
wrapper.rerender({scope: "new-scope"});
|
|
160
|
-
const
|
|
161
|
-
const
|
|
163
|
+
const value1 = wrapper.result.all[wrapper.result.all.length - 2];
|
|
164
|
+
const value2 = wrapper.result.current;
|
|
165
|
+
const result1 = Array.isArray(value1) ? value1[1] : "BAD1";
|
|
166
|
+
const result2 = Array.isArray(value2) ? value2[1] : "BAD2";
|
|
162
167
|
|
|
163
168
|
// Assert
|
|
164
|
-
|
|
165
|
-
expect(result1[1]).not.toBe(result2[1]);
|
|
169
|
+
expect(result1).not.toBe(result2);
|
|
166
170
|
});
|
|
167
171
|
|
|
168
172
|
it("should set the value in the cache", () => {
|
|
@@ -63,6 +63,12 @@ type CachedEffectOptions<TData extends ValidCacheData> = {
|
|
|
63
63
|
scope?: string;
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
type InflightRequest<TData extends ValidCacheData> = {
|
|
67
|
+
requestId: string;
|
|
68
|
+
request: Promise<Result<TData>>;
|
|
69
|
+
cancel(): void;
|
|
70
|
+
};
|
|
71
|
+
|
|
66
72
|
const DefaultScope = "useCachedEffect";
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -114,19 +120,16 @@ export const useCachedEffect = <TData extends ValidCacheData>(
|
|
|
114
120
|
const forceUpdate = useForceUpdate();
|
|
115
121
|
// For the NetworkOnly fetch policy, we ignore the cached value.
|
|
116
122
|
// So we need somewhere else to store the network value.
|
|
117
|
-
const networkResultRef = React.useRef();
|
|
123
|
+
const networkResultRef = React.useRef<Result<TData> | null>();
|
|
118
124
|
|
|
119
125
|
// Set up the function that will do the fetching.
|
|
120
|
-
const currentRequestRef = React.useRef();
|
|
126
|
+
const currentRequestRef = React.useRef<InflightRequest<TData> | null>();
|
|
121
127
|
const fetchRequest = React.useMemo(() => {
|
|
122
128
|
// We aren't using useCallback here because we need to make sure that
|
|
123
129
|
// if we are rememo-izing, we cancel any inflight request for the old
|
|
124
130
|
// callback.
|
|
125
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'cancel' does not exist on type 'never'.
|
|
126
131
|
currentRequestRef.current?.cancel();
|
|
127
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
|
|
128
132
|
currentRequestRef.current = null;
|
|
129
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
|
|
130
133
|
networkResultRef.current = null;
|
|
131
134
|
|
|
132
135
|
const fetchFn = () => {
|
|
@@ -153,7 +156,6 @@ export const useCachedEffect = <TData extends ValidCacheData>(
|
|
|
153
156
|
},
|
|
154
157
|
);
|
|
155
158
|
|
|
156
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'request' does not exist on type 'never'.
|
|
157
159
|
if (request === currentRequestRef.current?.request) {
|
|
158
160
|
// The request inflight is the same, so do nothing.
|
|
159
161
|
// NOTE: Perhaps if invoked via a refetch, we will want to
|
|
@@ -162,11 +164,9 @@ export const useCachedEffect = <TData extends ValidCacheData>(
|
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
// Clear the last network result.
|
|
165
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
|
|
166
167
|
networkResultRef.current = null;
|
|
167
168
|
|
|
168
169
|
// Cancel the previous request.
|
|
169
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'cancel' does not exist on type 'never'.
|
|
170
170
|
currentRequestRef.current?.cancel();
|
|
171
171
|
|
|
172
172
|
// TODO(somewhatabstract, FEI-4276):
|
|
@@ -178,7 +178,6 @@ export const useCachedEffect = <TData extends ValidCacheData>(
|
|
|
178
178
|
// Catching shouldn't serve a purpose.
|
|
179
179
|
// eslint-disable-next-line promise/catch-or-return
|
|
180
180
|
request.then((result) => {
|
|
181
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
|
|
182
181
|
currentRequestRef.current = null;
|
|
183
182
|
if (cancel) {
|
|
184
183
|
// We don't modify our result if the request was cancelled
|
|
@@ -189,7 +188,6 @@ export const useCachedEffect = <TData extends ValidCacheData>(
|
|
|
189
188
|
|
|
190
189
|
// Now we need to update the cache and notify or force a rerender.
|
|
191
190
|
setMostRecentResult(result);
|
|
192
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type 'Result<TData>' is not assignable to type 'undefined'.
|
|
193
191
|
networkResultRef.current = result;
|
|
194
192
|
|
|
195
193
|
if (onResultChanged != null) {
|
|
@@ -204,7 +202,6 @@ export const useCachedEffect = <TData extends ValidCacheData>(
|
|
|
204
202
|
return; // Shut up eslint always-return rule.
|
|
205
203
|
});
|
|
206
204
|
|
|
207
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type '{ requestId: string; request: Promise<Result<TData>>; cancel(): void; }' is not assignable to type 'undefined'.
|
|
208
205
|
currentRequestRef.current = {
|
|
209
206
|
requestId,
|
|
210
207
|
request,
|
|
@@ -265,29 +262,36 @@ export const useCachedEffect = <TData extends ValidCacheData>(
|
|
|
265
262
|
}
|
|
266
263
|
fetchRequest();
|
|
267
264
|
return () => {
|
|
268
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'cancel' does not exist on type 'never'.
|
|
269
265
|
currentRequestRef.current?.cancel();
|
|
270
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type 'null' is not assignable to type 'undefined'.
|
|
271
266
|
currentRequestRef.current = null;
|
|
272
267
|
};
|
|
273
268
|
}, [shouldFetch, fetchRequest]);
|
|
274
269
|
|
|
275
270
|
// We track the last result we returned in order to support the
|
|
276
|
-
// "retainResultOnChange" option.
|
|
277
|
-
const lastResultAgnosticOfIdRef = React.useRef(
|
|
271
|
+
// "retainResultOnChange" option. To begin, the last result is no-data.
|
|
272
|
+
const lastResultAgnosticOfIdRef = React.useRef<Result<TData>>(
|
|
273
|
+
Status.noData<TData>(),
|
|
274
|
+
);
|
|
275
|
+
// The default return value is:
|
|
276
|
+
// - The last result we returned if we're retaining results on change.
|
|
277
|
+
// - The no-data state if shouldFetch is false, and therefore there is no
|
|
278
|
+
// in-flight request.
|
|
279
|
+
// - Otherwise, the loading state (we can assume there's an inflight
|
|
280
|
+
// request if skip is not true).
|
|
278
281
|
const loadingResult = retainResultOnChange
|
|
279
282
|
? lastResultAgnosticOfIdRef.current
|
|
280
|
-
:
|
|
283
|
+
: shouldFetch
|
|
284
|
+
? Status.loading<TData>()
|
|
285
|
+
: Status.noData<TData>();
|
|
281
286
|
|
|
282
|
-
// Loading
|
|
283
|
-
// we cache.
|
|
284
|
-
const result =
|
|
287
|
+
// Loading and no-data are transient states, so we only use them here;
|
|
288
|
+
// they're not something we cache.
|
|
289
|
+
const result: Result<TData> =
|
|
285
290
|
(fetchPolicy === FetchPolicy.NetworkOnly
|
|
286
291
|
? networkResultRef.current
|
|
287
292
|
: mostRecentResult) ?? loadingResult;
|
|
288
293
|
lastResultAgnosticOfIdRef.current = result;
|
|
289
294
|
|
|
290
295
|
// We return the result and a function for triggering a refetch.
|
|
291
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type '{ status: "loading"; } | { status: "error"; error: Error; } | { status: "aborted"; } | { status: "success"; data: ValidCacheData; }' is not assignable to type 'Result<TData>'.
|
|
292
296
|
return [result, fetchRequest];
|
|
293
297
|
};
|
package/src/hooks/use-gql.ts
CHANGED
|
@@ -10,6 +10,13 @@ import type {
|
|
|
10
10
|
GqlFetchOptions,
|
|
11
11
|
} from "../util/gql-types";
|
|
12
12
|
|
|
13
|
+
interface GqlFetchFn<TContext extends GqlContext> {
|
|
14
|
+
<TData, TVariables extends Record<any, any>>(
|
|
15
|
+
operation: GqlOperation<TData, TVariables>,
|
|
16
|
+
options?: GqlFetchOptions<TVariables, TContext>,
|
|
17
|
+
): Promise<TData>;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* Hook to obtain a gqlFetch function for performing GraphQL requests.
|
|
15
22
|
*
|
|
@@ -22,10 +29,7 @@ import type {
|
|
|
22
29
|
*/
|
|
23
30
|
export const useGql = <TContext extends GqlContext>(
|
|
24
31
|
context: Partial<TContext> = {} as Partial<TContext>,
|
|
25
|
-
):
|
|
26
|
-
operation: GqlOperation<TData, TVariables>,
|
|
27
|
-
options?: GqlFetchOptions<TVariables, TContext>,
|
|
28
|
-
) => Promise<TData>) => {
|
|
32
|
+
): GqlFetchFn<TContext> => {
|
|
29
33
|
// This hook only works if the `GqlRouter` has been used to setup context.
|
|
30
34
|
const gqlRouterContext = useGqlRouterContext(context);
|
|
31
35
|
|
|
@@ -34,22 +38,21 @@ export const useGql = <TContext extends GqlContext>(
|
|
|
34
38
|
// we give the same function instance back to our callers instead of
|
|
35
39
|
// making a new one. That then means they can safely use the return value
|
|
36
40
|
// in hooks deps without fear of it triggering extra renders.
|
|
37
|
-
const gqlFetch = useCallback(
|
|
41
|
+
const gqlFetch: GqlFetchFn<TContext> = useCallback(
|
|
38
42
|
<TData, TVariables extends Record<any, any>>(
|
|
39
43
|
operation: GqlOperation<TData, TVariables>,
|
|
40
44
|
options: GqlFetchOptions<TVariables, TContext> = Object.freeze({}),
|
|
41
|
-
) => {
|
|
45
|
+
): Promise<TData> => {
|
|
42
46
|
const {fetch, defaultContext} = gqlRouterContext;
|
|
43
47
|
const {variables, context = {}} = options;
|
|
44
48
|
const finalContext = mergeGqlContext(defaultContext, context);
|
|
45
49
|
|
|
46
50
|
// Invoke the fetch and extract the data.
|
|
47
|
-
return fetch(operation, variables, finalContext).then(
|
|
48
|
-
getGqlDataFromResponse,
|
|
51
|
+
return fetch(operation, variables, finalContext).then((response) =>
|
|
52
|
+
getGqlDataFromResponse<TData>(response),
|
|
49
53
|
);
|
|
50
54
|
},
|
|
51
55
|
[gqlRouterContext],
|
|
52
56
|
);
|
|
53
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type '<TData, TVariables extends Record<any, any>>(operation: GqlOperation<TData, TVariables>, options?: GqlFetchOptions<TVariables, TContext>) => Promise<unknown>' is not assignable to type '<TData, TVariables extends Record<any, any>>(operation: GqlOperation<TData, TVariables>, options?: GqlFetchOptions<TVariables, TContext> | undefined) => Promise<...>'.
|
|
54
57
|
return gqlFetch;
|
|
55
58
|
};
|
|
@@ -30,20 +30,22 @@ export const useRequestInterception = <TData extends ValidCacheData>(
|
|
|
30
30
|
const interceptedHandler = React.useCallback((): Promise<TData> => {
|
|
31
31
|
// Call the interceptors from closest to furthest.
|
|
32
32
|
// If one returns a non-null result, then we keep that.
|
|
33
|
-
const interceptResponse =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
const interceptResponse: Promise<TData> | null | undefined =
|
|
34
|
+
interceptors.reduceRight(
|
|
35
|
+
(prev: Promise<TData> | null | undefined, interceptor) => {
|
|
36
|
+
if (prev != null) {
|
|
37
|
+
return prev;
|
|
38
|
+
}
|
|
39
|
+
return interceptor(requestId) as
|
|
40
|
+
| Promise<TData>
|
|
41
|
+
| null
|
|
42
|
+
| undefined;
|
|
43
|
+
},
|
|
44
|
+
null,
|
|
45
|
+
);
|
|
43
46
|
// If nothing intercepted this request, invoke the original handler.
|
|
44
47
|
// NOTE: We can't guarantee all interceptors return the same type
|
|
45
48
|
// as our handler, so how can TypeScript know? Let's just suppress that.
|
|
46
|
-
// @ts-expect-error [FEI-5019] - TS2739 - Type '(requestId: string) => Promise<ValidCacheData | null | undefined> | null | undefined' is missing the following properties from type 'Promise<TData>': then, catch, finally, [Symbol.toStringTag]
|
|
47
49
|
return interceptResponse ?? handler();
|
|
48
50
|
}, [handler, interceptors, requestId]);
|
|
49
51
|
|
|
@@ -81,8 +81,10 @@ export const useSharedCache = <TValue extends ValidCacheData>(
|
|
|
81
81
|
// since our last run through. Also, our cache does not know what type it
|
|
82
82
|
// stores, so we have to cast it to the type we're exporting. This is a
|
|
83
83
|
// dev time courtesy, rather than a runtime thing.
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
let currentValue: TValue | null | undefined = cache.get(scope, id) as
|
|
85
|
+
| TValue
|
|
86
|
+
| null
|
|
87
|
+
| undefined;
|
|
86
88
|
|
|
87
89
|
// If we have an initial value, we need to add it to the cache
|
|
88
90
|
// and use it as our current value.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-unassigned-import
|
|
2
|
+
import "jest-extended";
|
|
1
3
|
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
2
4
|
import {RequestFulfillment} from "../request-fulfillment";
|
|
3
5
|
import {RequestTracker} from "../request-tracking";
|
|
@@ -123,7 +125,6 @@ describe("#hasTrackedRequestsToBeFetched", () => {
|
|
|
123
125
|
const result = hasTrackedRequestsToBeFetched();
|
|
124
126
|
|
|
125
127
|
// Assert
|
|
126
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'toBeTrue' does not exist on type 'JestMatchers<boolean>'.
|
|
127
128
|
expect(result).toBeTrue();
|
|
128
129
|
});
|
|
129
130
|
});
|
|
@@ -162,13 +162,12 @@ describe("../request-tracking.js", () => {
|
|
|
162
162
|
it("should cache errors occurring in promises", async () => {
|
|
163
163
|
// Arrange
|
|
164
164
|
const requestTracker = createRequestTracker();
|
|
165
|
-
const fakeBadRequestHandler = () =>
|
|
165
|
+
const fakeBadRequestHandler = (): Promise<any> =>
|
|
166
166
|
new Promise((resolve: any, reject: any) =>
|
|
167
167
|
reject("OH NO!"),
|
|
168
168
|
);
|
|
169
169
|
requestTracker.trackDataRequest(
|
|
170
170
|
"ID",
|
|
171
|
-
// @ts-expect-error [FEI-5019] - TS2345 - Argument of type '() => Promise<unknown>' is not assignable to parameter of type '() => Promise<ValidCacheData>'.
|
|
172
171
|
fakeBadRequestHandler,
|
|
173
172
|
true,
|
|
174
173
|
);
|
|
@@ -192,23 +191,22 @@ describe("../request-tracking.js", () => {
|
|
|
192
191
|
* - Handlers that reject the promise
|
|
193
192
|
* - Handlers that resolve
|
|
194
193
|
*/
|
|
195
|
-
const fakeBadRequestHandler = () =>
|
|
194
|
+
const fakeBadRequestHandler = (): Promise<any> =>
|
|
196
195
|
new Promise((resolve: any, reject: any) =>
|
|
197
196
|
reject("OH NO!"),
|
|
198
197
|
);
|
|
199
|
-
const fakeBadHandler = () => {
|
|
198
|
+
const fakeBadHandler = (): Promise<any> => {
|
|
200
199
|
throw new Error("OH NO!");
|
|
201
200
|
};
|
|
202
|
-
const fakeValidHandler = (() => {
|
|
201
|
+
const fakeValidHandler = ((): (() => Promise<any>) => {
|
|
203
202
|
let counter = 0;
|
|
204
|
-
return (
|
|
203
|
+
return () => {
|
|
205
204
|
counter++;
|
|
206
205
|
return Promise.resolve(`DATA:${counter}`);
|
|
207
206
|
};
|
|
208
207
|
})();
|
|
209
208
|
requestTracker.trackDataRequest(
|
|
210
209
|
"BAD_REQUEST",
|
|
211
|
-
// @ts-expect-error [FEI-5019] - TS2345 - Argument of type '() => Promise<unknown>' is not assignable to parameter of type '() => Promise<ValidCacheData>'.
|
|
212
210
|
fakeBadRequestHandler,
|
|
213
211
|
true,
|
|
214
212
|
);
|
|
@@ -219,13 +217,11 @@ describe("../request-tracking.js", () => {
|
|
|
219
217
|
);
|
|
220
218
|
requestTracker.trackDataRequest(
|
|
221
219
|
"VALID_HANDLER1",
|
|
222
|
-
// @ts-expect-error [FEI-5019] - TS2345 - Argument of type '(o: any) => Promise<string>' is not assignable to parameter of type '() => Promise<string>'.
|
|
223
220
|
fakeValidHandler,
|
|
224
221
|
true,
|
|
225
222
|
);
|
|
226
223
|
requestTracker.trackDataRequest(
|
|
227
224
|
"VALID_HANDLER2",
|
|
228
|
-
// @ts-expect-error [FEI-5019] - TS2345 - Argument of type '(o: any) => Promise<string>' is not assignable to parameter of type '() => Promise<string>'.
|
|
229
225
|
fakeValidHandler,
|
|
230
226
|
true,
|
|
231
227
|
);
|
|
@@ -70,8 +70,8 @@ describe("#resultFromCachedResponse", () => {
|
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
// Act
|
|
73
|
-
|
|
74
|
-
const
|
|
73
|
+
const cacheResult = resultFromCachedResponse(cacheEntry);
|
|
74
|
+
const error = cacheResult?.status === "error" && cacheResult.error;
|
|
75
75
|
|
|
76
76
|
// Assert
|
|
77
77
|
expect(error).toMatchInlineSnapshot(`[HydratedDataError: ERROR]`);
|
|
@@ -9,12 +9,11 @@ describe("SerializableInMemoryCache", () => {
|
|
|
9
9
|
scope: {
|
|
10
10
|
key: "value",
|
|
11
11
|
},
|
|
12
|
-
}
|
|
12
|
+
};
|
|
13
13
|
|
|
14
14
|
// Act
|
|
15
15
|
const cache = new SerializableInMemoryCache(sourceData);
|
|
16
16
|
// Try to mutate the cache.
|
|
17
|
-
// @ts-expect-error [FEI-5019] - TS2540 - Cannot assign to 'scope' because it is a read-only property.
|
|
18
17
|
sourceData["scope"] = {key: "SOME_NEW_DATA"};
|
|
19
18
|
const result = cache.get("scope", "key");
|
|
20
19
|
|
|
@@ -125,12 +125,11 @@ describe("../ssr-cache.js", () => {
|
|
|
125
125
|
const cache = new SsrCache();
|
|
126
126
|
const sourceData = {
|
|
127
127
|
MY_KEY: {data: "THE_DATA"},
|
|
128
|
-
}
|
|
128
|
+
};
|
|
129
129
|
|
|
130
130
|
// Act
|
|
131
131
|
cache.initialize(sourceData);
|
|
132
132
|
// Try to mutate the cache.
|
|
133
|
-
// @ts-expect-error [FEI-5019] - TS2540 - Cannot assign to 'MY_KEY' because it is a read-only property.
|
|
134
133
|
sourceData["MY_KEY"] = {data: "SOME_NEW_DATA"};
|
|
135
134
|
const result = cache.getEntry("MY_KEY");
|
|
136
135
|
|
|
@@ -448,8 +447,7 @@ describe("../ssr-cache.js", () => {
|
|
|
448
447
|
const cloneSpy = jest
|
|
449
448
|
.spyOn(hydrationCache, "clone")
|
|
450
449
|
.mockReturnValue({
|
|
451
|
-
|
|
452
|
-
default: "CLONE!",
|
|
450
|
+
default: "CLONE!" as any,
|
|
453
451
|
});
|
|
454
452
|
const cache = new SsrCache(hydrationCache);
|
|
455
453
|
// Let's add to the initialized state to check that everything
|
|
@@ -9,11 +9,10 @@ describe("#toGqlOperation", () => {
|
|
|
9
9
|
const documentNode: any = {};
|
|
10
10
|
const parserSpy = jest
|
|
11
11
|
.spyOn(GDNP, "graphQLDocumentNodeParser")
|
|
12
|
-
// @ts-expect-error [FEI-5019] - TS2345 - Argument of type '{ name: string; type: string; }' is not assignable to parameter of type 'IDocumentDefinition'.
|
|
13
12
|
.mockReturnValue({
|
|
14
13
|
name: "operationName",
|
|
15
14
|
type: "query",
|
|
16
|
-
});
|
|
15
|
+
} as any);
|
|
17
16
|
|
|
18
17
|
// Act
|
|
19
18
|
toGqlOperation(documentNode);
|
|
@@ -25,11 +24,10 @@ describe("#toGqlOperation", () => {
|
|
|
25
24
|
it("should return the Wonder Blocks Data representation of the given document node", () => {
|
|
26
25
|
// Arrange
|
|
27
26
|
const documentNode: any = {};
|
|
28
|
-
// @ts-expect-error [FEI-5019] - TS2345 - Argument of type '{ name: string; type: string; }' is not assignable to parameter of type 'IDocumentDefinition'.
|
|
29
27
|
jest.spyOn(GDNP, "graphQLDocumentNodeParser").mockReturnValue({
|
|
30
28
|
name: "operationName",
|
|
31
29
|
type: "mutation",
|
|
32
|
-
});
|
|
30
|
+
} as any);
|
|
33
31
|
|
|
34
32
|
// Act
|
|
35
33
|
const result = toGqlOperation(documentNode);
|
|
@@ -59,20 +59,20 @@ export function graphQLDocumentNodeParser(
|
|
|
59
59
|
|
|
60
60
|
const queries = document.definitions.filter(
|
|
61
61
|
(x: DefinitionNode) =>
|
|
62
|
-
|
|
63
|
-
x
|
|
62
|
+
x.kind === "OperationDefinition" &&
|
|
63
|
+
(x as OperationDefinitionNode).operation === "query",
|
|
64
64
|
);
|
|
65
65
|
|
|
66
66
|
const mutations = document.definitions.filter(
|
|
67
67
|
(x: DefinitionNode) =>
|
|
68
|
-
|
|
69
|
-
x
|
|
68
|
+
x.kind === "OperationDefinition" &&
|
|
69
|
+
(x as OperationDefinitionNode).operation === "mutation",
|
|
70
70
|
);
|
|
71
71
|
|
|
72
72
|
const subscriptions = document.definitions.filter(
|
|
73
73
|
(x: DefinitionNode) =>
|
|
74
|
-
|
|
75
|
-
x
|
|
74
|
+
x.kind === "OperationDefinition" &&
|
|
75
|
+
(x as OperationDefinitionNode).operation === "subscription",
|
|
76
76
|
);
|
|
77
77
|
|
|
78
78
|
if (fragments.length && !queries.length && !mutations.length) {
|
|
@@ -23,7 +23,8 @@ export const mergeGqlContext = <TContext extends GqlContext>(
|
|
|
23
23
|
delete acc[key];
|
|
24
24
|
} else {
|
|
25
25
|
// Otherwise, we set it.
|
|
26
|
-
// @ts-expect-error
|
|
26
|
+
// @ts-expect-error TypeScript doesn't seem to see that
|
|
27
|
+
// TContext can have string keys.
|
|
27
28
|
acc[key] = overrides[key];
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -53,8 +53,7 @@ export class RequestTracker {
|
|
|
53
53
|
_responseCache: SsrCache;
|
|
54
54
|
_requestFulfillment: RequestFulfillment;
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
constructor(responseCache: SsrCache | null = undefined) {
|
|
56
|
+
constructor(responseCache?: SsrCache | null) {
|
|
58
57
|
this._responseCache = responseCache || SsrCache.Default;
|
|
59
58
|
this._requestFulfillment = new RequestFulfillment();
|
|
60
59
|
}
|
|
@@ -160,6 +159,11 @@ export class RequestTracker {
|
|
|
160
159
|
// the code wrong. Rather than bloat
|
|
161
160
|
// code with useless error, just ignore.
|
|
162
161
|
|
|
162
|
+
// For status === "no-data":
|
|
163
|
+
// Could never get here unless we wrote
|
|
164
|
+
// the code wrong. Rather than bloat
|
|
165
|
+
// code with useless error, just ignore.
|
|
166
|
+
|
|
163
167
|
// For status === "aborted":
|
|
164
168
|
// We won't cache this.
|
|
165
169
|
// We don't hydrate aborted requests,
|
package/src/util/ssr-cache.ts
CHANGED
|
@@ -124,7 +124,7 @@ export class SsrCache {
|
|
|
124
124
|
: null;
|
|
125
125
|
|
|
126
126
|
// Now we defer to the SSR value, and fallback to the hydration cache.
|
|
127
|
-
const internalEntry =
|
|
127
|
+
const internalEntry: ValidCacheData | null | undefined =
|
|
128
128
|
ssrEntry ?? this._hydrationCache.get(DefaultScope, id);
|
|
129
129
|
|
|
130
130
|
// If we are not server-side and we hydrated something, let's clear
|
|
@@ -140,8 +140,10 @@ export class SsrCache {
|
|
|
140
140
|
}
|
|
141
141
|
// Getting the typing right between the in-memory cache and this
|
|
142
142
|
// is hard. Just telling TypeScript it's OK.
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
return internalEntry as
|
|
144
|
+
| Readonly<CachedResponse<TData>>
|
|
145
|
+
| null
|
|
146
|
+
| undefined;
|
|
145
147
|
};
|
|
146
148
|
|
|
147
149
|
/**
|
|
@@ -161,9 +163,11 @@ export class SsrCache {
|
|
|
161
163
|
const realPredicate = predicate
|
|
162
164
|
? // We know what we're putting into the cache so let's assume it
|
|
163
165
|
// conforms.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
(_: string, key: string, cachedEntry: ValidCacheData) =>
|
|
167
|
+
predicate(
|
|
168
|
+
key,
|
|
169
|
+
cachedEntry as Readonly<CachedResponse<ValidCacheData>>,
|
|
170
|
+
)
|
|
167
171
|
: undefined;
|
|
168
172
|
|
|
169
173
|
// Apply the predicate to what we have in our caches.
|
|
@@ -184,7 +188,6 @@ export class SsrCache {
|
|
|
184
188
|
// to an empty object.
|
|
185
189
|
// We only need the default scope out of our scoped in-memory cache.
|
|
186
190
|
// We know that it conforms to our expectations.
|
|
187
|
-
|
|
188
|
-
return cache[DefaultScope] ?? {};
|
|
191
|
+
return (cache[DefaultScope] as ResponseCache) ?? {};
|
|
189
192
|
};
|
|
190
193
|
}
|
package/src/util/status.ts
CHANGED
|
@@ -4,6 +4,10 @@ const loadingStatus = Object.freeze({
|
|
|
4
4
|
status: "loading",
|
|
5
5
|
});
|
|
6
6
|
|
|
7
|
+
const noDataStatus = Object.freeze({
|
|
8
|
+
status: "no-data",
|
|
9
|
+
});
|
|
10
|
+
|
|
7
11
|
const abortedStatus = Object.freeze({
|
|
8
12
|
status: "aborted",
|
|
9
13
|
});
|
|
@@ -14,6 +18,8 @@ const abortedStatus = Object.freeze({
|
|
|
14
18
|
export const Status = Object.freeze({
|
|
15
19
|
loading: <TData extends ValidCacheData = ValidCacheData>(): Result<TData> =>
|
|
16
20
|
loadingStatus,
|
|
21
|
+
noData: <TData extends ValidCacheData = ValidCacheData>(): Result<TData> =>
|
|
22
|
+
noDataStatus,
|
|
17
23
|
aborted: <TData extends ValidCacheData = ValidCacheData>(): Result<TData> =>
|
|
18
24
|
abortedStatus,
|
|
19
25
|
success: <TData extends ValidCacheData>(data: TData): Result<TData> => ({
|