@khanacademy/wonder-blocks-data 13.0.11 → 13.0.12
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 +8 -0
- package/package.json +3 -3
- package/src/components/__tests__/data.test.tsx +0 -832
- package/src/components/__tests__/gql-router.test.tsx +0 -63
- package/src/components/__tests__/intercept-requests.test.tsx +0 -57
- package/src/components/__tests__/track-data.test.tsx +0 -56
- package/src/components/data.ts +0 -73
- package/src/components/gql-router.tsx +0 -63
- package/src/components/intercept-context.ts +0 -19
- package/src/components/intercept-requests.tsx +0 -67
- package/src/components/track-data.tsx +0 -28
- package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.ts.snap +0 -17
- package/src/hooks/__tests__/use-cached-effect.test.tsx +0 -789
- package/src/hooks/__tests__/use-gql-router-context.test.tsx +0 -132
- package/src/hooks/__tests__/use-gql.test.tsx +0 -204
- package/src/hooks/__tests__/use-hydratable-effect.test.ts +0 -708
- package/src/hooks/__tests__/use-request-interception.test.tsx +0 -254
- package/src/hooks/__tests__/use-server-effect.test.ts +0 -293
- package/src/hooks/__tests__/use-shared-cache.test.ts +0 -263
- package/src/hooks/use-cached-effect.ts +0 -297
- package/src/hooks/use-gql-router-context.ts +0 -49
- package/src/hooks/use-gql.ts +0 -58
- package/src/hooks/use-hydratable-effect.ts +0 -201
- package/src/hooks/use-request-interception.ts +0 -53
- package/src/hooks/use-server-effect.ts +0 -75
- package/src/hooks/use-shared-cache.ts +0 -107
- package/src/index.ts +0 -46
- package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.ts.snap +0 -19
- package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.ts.snap +0 -19
- package/src/util/__tests__/get-gql-data-from-response.test.ts +0 -186
- package/src/util/__tests__/get-gql-request-id.test.ts +0 -132
- package/src/util/__tests__/graphql-document-node-parser.test.ts +0 -535
- package/src/util/__tests__/hydration-cache-api.test.ts +0 -34
- package/src/util/__tests__/merge-gql-context.test.ts +0 -73
- package/src/util/__tests__/purge-caches.test.ts +0 -28
- package/src/util/__tests__/request-api.test.ts +0 -176
- package/src/util/__tests__/request-fulfillment.test.ts +0 -146
- package/src/util/__tests__/request-tracking.test.tsx +0 -321
- package/src/util/__tests__/result-from-cache-response.test.ts +0 -79
- package/src/util/__tests__/scoped-in-memory-cache.test.ts +0 -316
- package/src/util/__tests__/serializable-in-memory-cache.test.ts +0 -397
- package/src/util/__tests__/ssr-cache.test.ts +0 -636
- package/src/util/__tests__/to-gql-operation.test.ts +0 -41
- package/src/util/data-error.ts +0 -63
- package/src/util/get-gql-data-from-response.ts +0 -65
- package/src/util/get-gql-request-id.ts +0 -106
- package/src/util/gql-error.ts +0 -43
- package/src/util/gql-router-context.ts +0 -9
- package/src/util/gql-types.ts +0 -64
- package/src/util/graphql-document-node-parser.ts +0 -132
- package/src/util/graphql-types.ts +0 -28
- package/src/util/hydration-cache-api.ts +0 -30
- package/src/util/merge-gql-context.ts +0 -35
- package/src/util/purge-caches.ts +0 -14
- package/src/util/request-api.ts +0 -65
- package/src/util/request-fulfillment.ts +0 -121
- package/src/util/request-tracking.ts +0 -211
- package/src/util/result-from-cache-response.ts +0 -30
- package/src/util/scoped-in-memory-cache.ts +0 -121
- package/src/util/serializable-in-memory-cache.ts +0 -44
- package/src/util/ssr-cache.ts +0 -193
- package/src/util/status.ts +0 -35
- package/src/util/to-gql-operation.ts +0 -43
- package/src/util/types.ts +0 -145
- package/tsconfig-build.json +0 -12
- package/tsconfig-build.tsbuildinfo +0 -1
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
// eslint-disable-next-line import/no-unassigned-import
|
|
2
|
-
import "jest-extended";
|
|
3
|
-
import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
|
|
4
|
-
|
|
5
|
-
import {useSharedCache, SharedCache} from "../use-shared-cache";
|
|
6
|
-
|
|
7
|
-
describe("#useSharedCache", () => {
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
SharedCache.purgeAll();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it.each`
|
|
13
|
-
id
|
|
14
|
-
${null}
|
|
15
|
-
${""}
|
|
16
|
-
${5}
|
|
17
|
-
${() => "BOO"}
|
|
18
|
-
`("should throw if the id is $id", ({id}: any) => {
|
|
19
|
-
// Arrange
|
|
20
|
-
|
|
21
|
-
// Act
|
|
22
|
-
const {result} = clientRenderHook(() => useSharedCache(id, "scope"));
|
|
23
|
-
|
|
24
|
-
// Assert
|
|
25
|
-
expect(result.error).toMatchSnapshot();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it.each`
|
|
29
|
-
scope
|
|
30
|
-
${null}
|
|
31
|
-
${""}
|
|
32
|
-
${5}
|
|
33
|
-
${() => "BOO"}
|
|
34
|
-
`("should throw if the scope is $scope", ({scope}: any) => {
|
|
35
|
-
// Arrange
|
|
36
|
-
|
|
37
|
-
// Act
|
|
38
|
-
const {result} = clientRenderHook(() => useSharedCache("id", scope));
|
|
39
|
-
|
|
40
|
-
// Assert
|
|
41
|
-
expect(result.error).toMatchSnapshot();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("should return a tuple of two items", () => {
|
|
45
|
-
// Arrange
|
|
46
|
-
|
|
47
|
-
// Act
|
|
48
|
-
const {
|
|
49
|
-
result: {current: result},
|
|
50
|
-
} = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
51
|
-
|
|
52
|
-
// Assert
|
|
53
|
-
expect(result).toBeArrayOfSize(2);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("tuple[0] - currentValue", () => {
|
|
57
|
-
it("should be null if nothing is cached", () => {
|
|
58
|
-
// Arrange
|
|
59
|
-
|
|
60
|
-
// Act
|
|
61
|
-
const {
|
|
62
|
-
result: {current: result},
|
|
63
|
-
} = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
64
|
-
|
|
65
|
-
// Assert
|
|
66
|
-
expect(result[0]).toBeNull();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("should match initialValue when provided as a non-function", () => {
|
|
70
|
-
// Arrange
|
|
71
|
-
|
|
72
|
-
// Act
|
|
73
|
-
const {
|
|
74
|
-
result: {current: result},
|
|
75
|
-
} = clientRenderHook(() =>
|
|
76
|
-
useSharedCache("id", "scope", "INITIAL VALUE"),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
// Assert
|
|
80
|
-
expect(result[0]).toBe("INITIAL VALUE");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("should match the return of initialValue when provided as non-function", () => {
|
|
84
|
-
// Arrange
|
|
85
|
-
|
|
86
|
-
// Act
|
|
87
|
-
const {
|
|
88
|
-
result: {current: result},
|
|
89
|
-
} = clientRenderHook(() =>
|
|
90
|
-
useSharedCache("id", "scope", () => "INITIAL VALUE"),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// Assert
|
|
94
|
-
expect(result[0]).toBe("INITIAL VALUE");
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe("tuple[1] - setValue", () => {
|
|
99
|
-
it("should be a function", () => {
|
|
100
|
-
// Arrange
|
|
101
|
-
|
|
102
|
-
// Act
|
|
103
|
-
const {
|
|
104
|
-
result: {current: result},
|
|
105
|
-
} = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
106
|
-
|
|
107
|
-
// Assert
|
|
108
|
-
expect(result[1]).toBeFunction();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("should be the same function if the id and scope remain the same", () => {
|
|
112
|
-
// Arrange
|
|
113
|
-
const wrapper = clientRenderHook(
|
|
114
|
-
({id, scope}: any) => useSharedCache(id, scope),
|
|
115
|
-
{initialProps: {id: "id", scope: "scope"}},
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// Act
|
|
119
|
-
wrapper.rerender({
|
|
120
|
-
id: "id",
|
|
121
|
-
scope: "scope",
|
|
122
|
-
});
|
|
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";
|
|
127
|
-
|
|
128
|
-
// Assert
|
|
129
|
-
expect(result1).toBe(result2);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("should be a new function if the id changes", () => {
|
|
133
|
-
// Arrange
|
|
134
|
-
const wrapper = clientRenderHook(
|
|
135
|
-
({id}: any) => useSharedCache(id, "scope"),
|
|
136
|
-
{
|
|
137
|
-
initialProps: {id: "id"},
|
|
138
|
-
},
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
// Act
|
|
142
|
-
wrapper.rerender({id: "new-id"});
|
|
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";
|
|
147
|
-
|
|
148
|
-
// Assert
|
|
149
|
-
expect(result1).not.toBe(result2);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("should be a new function if the scope changes", () => {
|
|
153
|
-
// Arrange
|
|
154
|
-
const wrapper = clientRenderHook(
|
|
155
|
-
({scope}: any) => useSharedCache("id", scope),
|
|
156
|
-
{
|
|
157
|
-
initialProps: {scope: "scope"},
|
|
158
|
-
},
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
// Act
|
|
162
|
-
wrapper.rerender({scope: "new-scope"});
|
|
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";
|
|
167
|
-
|
|
168
|
-
// Assert
|
|
169
|
-
expect(result1).not.toBe(result2);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("should set the value in the cache", () => {
|
|
173
|
-
// Arrange
|
|
174
|
-
const wrapper = clientRenderHook(() =>
|
|
175
|
-
useSharedCache("id", "scope"),
|
|
176
|
-
);
|
|
177
|
-
const setValue = wrapper.result.current[1];
|
|
178
|
-
|
|
179
|
-
// Act
|
|
180
|
-
setValue("CACHED_VALUE");
|
|
181
|
-
// Rerender so the hook retrieves this new value.
|
|
182
|
-
wrapper.rerender();
|
|
183
|
-
const result = wrapper.result.current[0];
|
|
184
|
-
|
|
185
|
-
// Assert
|
|
186
|
-
expect(result).toBe("CACHED_VALUE");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it.each`
|
|
190
|
-
value
|
|
191
|
-
${undefined}
|
|
192
|
-
${null}
|
|
193
|
-
`("should purge the value from the cache if $value", ({value}: any) => {
|
|
194
|
-
// Arrange
|
|
195
|
-
const wrapper = clientRenderHook(() =>
|
|
196
|
-
useSharedCache("id", "scope"),
|
|
197
|
-
);
|
|
198
|
-
const setValue = wrapper.result.current[1];
|
|
199
|
-
setValue("CACHED_VALUE");
|
|
200
|
-
|
|
201
|
-
// Act
|
|
202
|
-
// Rerender so the result has the cached value.
|
|
203
|
-
wrapper.rerender();
|
|
204
|
-
setValue(value);
|
|
205
|
-
// Rerender so the hook retrieves this new value.
|
|
206
|
-
wrapper.rerender();
|
|
207
|
-
const result = wrapper.result.current[0];
|
|
208
|
-
|
|
209
|
-
// Assert
|
|
210
|
-
expect(result).toBeNull();
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("should share cache across all uses", () => {
|
|
215
|
-
// Arrange
|
|
216
|
-
const hook1 = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
217
|
-
const hook2 = clientRenderHook(() => useSharedCache("id", "scope"));
|
|
218
|
-
hook1.result.current[1]("VALUE_1");
|
|
219
|
-
|
|
220
|
-
// Act
|
|
221
|
-
hook2.rerender();
|
|
222
|
-
const result = hook2.result.current[0];
|
|
223
|
-
|
|
224
|
-
// Assert
|
|
225
|
-
expect(result).toBe("VALUE_1");
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it.each`
|
|
229
|
-
id
|
|
230
|
-
${"id1"}
|
|
231
|
-
${"id2"}
|
|
232
|
-
`("should not share cache if scope is different", ({id}: any) => {
|
|
233
|
-
// Arrange
|
|
234
|
-
const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
|
|
235
|
-
const hook2 = clientRenderHook(() => useSharedCache(id, "scope2"));
|
|
236
|
-
hook1.result.current[1]("VALUE_1");
|
|
237
|
-
|
|
238
|
-
// Act
|
|
239
|
-
hook2.rerender();
|
|
240
|
-
const result = hook2.result.current[0];
|
|
241
|
-
|
|
242
|
-
// Assert
|
|
243
|
-
expect(result).toBeNull();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it.each`
|
|
247
|
-
scope
|
|
248
|
-
${"scope1"}
|
|
249
|
-
${"scope2"}
|
|
250
|
-
`("should not share cache if id is different", ({scope}: any) => {
|
|
251
|
-
// Arrange
|
|
252
|
-
const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
|
|
253
|
-
const hook2 = clientRenderHook(() => useSharedCache("id2", scope));
|
|
254
|
-
hook1.result.current[1]("VALUE_1");
|
|
255
|
-
|
|
256
|
-
// Act
|
|
257
|
-
hook2.rerender();
|
|
258
|
-
const result = hook2.result.current[0];
|
|
259
|
-
|
|
260
|
-
// Assert
|
|
261
|
-
expect(result).toBeNull();
|
|
262
|
-
});
|
|
263
|
-
});
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {useForceUpdate} from "@khanacademy/wonder-blocks-core";
|
|
3
|
-
import {DataError, DataErrors} from "../util/data-error";
|
|
4
|
-
|
|
5
|
-
import {RequestFulfillment} from "../util/request-fulfillment";
|
|
6
|
-
import {Status} from "../util/status";
|
|
7
|
-
|
|
8
|
-
import {useSharedCache} from "./use-shared-cache";
|
|
9
|
-
import {useRequestInterception} from "./use-request-interception";
|
|
10
|
-
|
|
11
|
-
import type {Result, ValidCacheData} from "../util/types";
|
|
12
|
-
|
|
13
|
-
import {FetchPolicy} from "../util/types";
|
|
14
|
-
|
|
15
|
-
type CachedEffectOptions<TData extends ValidCacheData> = {
|
|
16
|
-
/**
|
|
17
|
-
* The policy to use when determining how to retrieve the request data from
|
|
18
|
-
* cache and network.
|
|
19
|
-
*
|
|
20
|
-
* Defaults to `FetchPolicy.CacheBeforeNetwork`.
|
|
21
|
-
*/
|
|
22
|
-
fetchPolicy?: typeof FetchPolicy[keyof typeof FetchPolicy];
|
|
23
|
-
/**
|
|
24
|
-
* When `true`, the effect will not be executed; otherwise, the effect will
|
|
25
|
-
* be executed.
|
|
26
|
-
*
|
|
27
|
-
* If this is set to `true` while the effect is still pending, the pending
|
|
28
|
-
* effect will be cancelled.
|
|
29
|
-
*
|
|
30
|
-
* Default is `false`.
|
|
31
|
-
*/
|
|
32
|
-
skip?: boolean;
|
|
33
|
-
/**
|
|
34
|
-
* When `true`, the effect will not reset the result to the loading status
|
|
35
|
-
* while executing if the requestId changes, instead, returning
|
|
36
|
-
* the existing result from before the change; otherwise, the result will
|
|
37
|
-
* be set to loading status.
|
|
38
|
-
*
|
|
39
|
-
* If the status is loading when the changes are made, it will remain as
|
|
40
|
-
* loading; old pending effects are discarded on changes and as such this
|
|
41
|
-
* value has no effect in that case.
|
|
42
|
-
*/
|
|
43
|
-
retainResultOnChange?: boolean;
|
|
44
|
-
/**
|
|
45
|
-
* Callback that is invoked if the result for the given hook has changed.
|
|
46
|
-
*
|
|
47
|
-
* When defined, the hook will invoke this callback whenever it has reason
|
|
48
|
-
* to change the result and will not otherwise affect component rendering
|
|
49
|
-
* directly.
|
|
50
|
-
*
|
|
51
|
-
* When not defined, the hook will ensure the component re-renders to pick
|
|
52
|
-
* up the latest result.
|
|
53
|
-
*/
|
|
54
|
-
onResultChanged?: (result: Result<TData>) => void;
|
|
55
|
-
/**
|
|
56
|
-
* Scope to use with the shared cache.
|
|
57
|
-
*
|
|
58
|
-
* When specified, the given scope will be used to isolate this hook's
|
|
59
|
-
* cached results. Otherwise, a shared default scope will be used.
|
|
60
|
-
*
|
|
61
|
-
* Changing this value after the first call is not supported.
|
|
62
|
-
*/
|
|
63
|
-
scope?: string;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
type InflightRequest<TData extends ValidCacheData> = {
|
|
67
|
-
requestId: string;
|
|
68
|
-
request: Promise<Result<TData>>;
|
|
69
|
-
cancel(): void;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const DefaultScope = "useCachedEffect";
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Hook to execute and cache an async operation on the client.
|
|
76
|
-
*
|
|
77
|
-
* This hook executes the given handler on the client if there is no
|
|
78
|
-
* cached result to use.
|
|
79
|
-
*
|
|
80
|
-
* Results are cached so they can be shared between equivalent invocations.
|
|
81
|
-
* In-flight requests are also shared, so that concurrent calls will
|
|
82
|
-
* behave as one might exect. Cache updates invoked by one hook instance
|
|
83
|
-
* do not trigger renders in components that use the same requestID; however,
|
|
84
|
-
* that should not matter since concurrent requests will share the same
|
|
85
|
-
* in-flight request, and subsequent renders will grab from the cache.
|
|
86
|
-
*
|
|
87
|
-
* Once the request has been tried once and a non-loading response has been
|
|
88
|
-
* cached, the request will not executed made again.
|
|
89
|
-
*/
|
|
90
|
-
export const useCachedEffect = <TData extends ValidCacheData>(
|
|
91
|
-
requestId: string,
|
|
92
|
-
handler: () => Promise<TData>,
|
|
93
|
-
options: CachedEffectOptions<TData> = {} as Partial<
|
|
94
|
-
CachedEffectOptions<TData>
|
|
95
|
-
>,
|
|
96
|
-
): [Result<TData>, () => void] => {
|
|
97
|
-
const {
|
|
98
|
-
fetchPolicy = FetchPolicy.CacheBeforeNetwork,
|
|
99
|
-
skip: hardSkip = false,
|
|
100
|
-
retainResultOnChange = false,
|
|
101
|
-
onResultChanged,
|
|
102
|
-
scope = DefaultScope,
|
|
103
|
-
} = options;
|
|
104
|
-
|
|
105
|
-
// Plug in to the request interception framework for code that wants
|
|
106
|
-
// to use that.
|
|
107
|
-
const interceptedHandler = useRequestInterception(requestId, handler);
|
|
108
|
-
|
|
109
|
-
// Instead of using state, which would be local to just this hook instance,
|
|
110
|
-
// we use a shared in-memory cache.
|
|
111
|
-
const [mostRecentResult, setMostRecentResult] = useSharedCache<
|
|
112
|
-
Result<TData>
|
|
113
|
-
>( // The key of the cached item
|
|
114
|
-
requestId, // The scope of the cached items
|
|
115
|
-
// No default value. We don't want the loading status there; to ensure
|
|
116
|
-
// that all calls when the request is in-flight will update once that
|
|
117
|
-
// request is done, we want the cache to be empty until that point.
|
|
118
|
-
scope,
|
|
119
|
-
);
|
|
120
|
-
const forceUpdate = useForceUpdate();
|
|
121
|
-
// For the NetworkOnly fetch policy, we ignore the cached value.
|
|
122
|
-
// So we need somewhere else to store the network value.
|
|
123
|
-
const networkResultRef = React.useRef<Result<TData> | null>();
|
|
124
|
-
|
|
125
|
-
// Set up the function that will do the fetching.
|
|
126
|
-
const currentRequestRef = React.useRef<InflightRequest<TData> | null>();
|
|
127
|
-
const fetchRequest = React.useMemo(() => {
|
|
128
|
-
// We aren't using useCallback here because we need to make sure that
|
|
129
|
-
// if we are rememo-izing, we cancel any inflight request for the old
|
|
130
|
-
// callback.
|
|
131
|
-
currentRequestRef.current?.cancel();
|
|
132
|
-
currentRequestRef.current = null;
|
|
133
|
-
networkResultRef.current = null;
|
|
134
|
-
|
|
135
|
-
const fetchFn = () => {
|
|
136
|
-
if (fetchPolicy === FetchPolicy.CacheOnly) {
|
|
137
|
-
throw new DataError(
|
|
138
|
-
"Cannot fetch with CacheOnly policy",
|
|
139
|
-
DataErrors.NotAllowed,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
// We use our request fulfillment here so that in-flight
|
|
143
|
-
// requests are shared. In order to ensure that we don't share
|
|
144
|
-
// in-flight requests for different scopes, we add the scope to the
|
|
145
|
-
// requestId.
|
|
146
|
-
// We do this as a courtesy to simplify usage in sandboxed
|
|
147
|
-
// uses like storybook where we want each story to perform their
|
|
148
|
-
// own requests from scratch and not share inflight requests across
|
|
149
|
-
// stories.
|
|
150
|
-
// Since this only occurs here, nothing else will care about this
|
|
151
|
-
// change except the request tracking.
|
|
152
|
-
const request = RequestFulfillment.Default.fulfill(
|
|
153
|
-
`${requestId}|${scope}`,
|
|
154
|
-
{
|
|
155
|
-
handler: interceptedHandler,
|
|
156
|
-
},
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
if (request === currentRequestRef.current?.request) {
|
|
160
|
-
// The request inflight is the same, so do nothing.
|
|
161
|
-
// NOTE: Perhaps if invoked via a refetch, we will want to
|
|
162
|
-
// override this behavior and force a new request?
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Clear the last network result.
|
|
167
|
-
networkResultRef.current = null;
|
|
168
|
-
|
|
169
|
-
// Cancel the previous request.
|
|
170
|
-
currentRequestRef.current?.cancel();
|
|
171
|
-
|
|
172
|
-
// TODO(somewhatabstract, FEI-4276):
|
|
173
|
-
// Until our RequestFulfillment API supports cancelling/aborting, we
|
|
174
|
-
// will have to do it.
|
|
175
|
-
let cancel = false;
|
|
176
|
-
|
|
177
|
-
// NOTE: Our request fulfillment handles the error cases here.
|
|
178
|
-
// Catching shouldn't serve a purpose.
|
|
179
|
-
// eslint-disable-next-line promise/catch-or-return
|
|
180
|
-
request.then((result) => {
|
|
181
|
-
currentRequestRef.current = null;
|
|
182
|
-
if (cancel) {
|
|
183
|
-
// We don't modify our result if the request was cancelled
|
|
184
|
-
// as it means that this hook no longer cares about that old
|
|
185
|
-
// request.
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Now we need to update the cache and notify or force a rerender.
|
|
190
|
-
setMostRecentResult(result);
|
|
191
|
-
networkResultRef.current = result;
|
|
192
|
-
|
|
193
|
-
if (onResultChanged != null) {
|
|
194
|
-
// If we have a callback, call it to let our caller know we
|
|
195
|
-
// got a result.
|
|
196
|
-
onResultChanged(result);
|
|
197
|
-
} else {
|
|
198
|
-
// If there's no callback, and this is using cache in some
|
|
199
|
-
// capacity, just force a rerender.
|
|
200
|
-
forceUpdate();
|
|
201
|
-
}
|
|
202
|
-
return; // Shut up eslint always-return rule.
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
currentRequestRef.current = {
|
|
206
|
-
requestId,
|
|
207
|
-
request,
|
|
208
|
-
cancel() {
|
|
209
|
-
cancel = true;
|
|
210
|
-
RequestFulfillment.Default.abort(requestId);
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Now we can return the new fetch function.
|
|
216
|
-
return fetchFn;
|
|
217
|
-
|
|
218
|
-
// We deliberately ignore the handler here because we want folks to use
|
|
219
|
-
// interceptor functions inline in props for simplicity. This is OK
|
|
220
|
-
// since changing the handler without changing the requestId doesn't
|
|
221
|
-
// really make sense - the same requestId should be handled the same as
|
|
222
|
-
// each other.
|
|
223
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
224
|
-
}, [
|
|
225
|
-
requestId,
|
|
226
|
-
onResultChanged,
|
|
227
|
-
forceUpdate,
|
|
228
|
-
setMostRecentResult,
|
|
229
|
-
fetchPolicy,
|
|
230
|
-
]);
|
|
231
|
-
|
|
232
|
-
// Calculate if we want to fetch the result or not.
|
|
233
|
-
// If this is true, we will do a new fetch, cancelling the previous fetch
|
|
234
|
-
// if there is one inflight.
|
|
235
|
-
const shouldFetch = React.useMemo(() => {
|
|
236
|
-
if (hardSkip) {
|
|
237
|
-
// We don't fetch if we've been told to hard skip.
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
switch (fetchPolicy) {
|
|
242
|
-
case FetchPolicy.CacheOnly:
|
|
243
|
-
// Don't want to do a network request if we're only
|
|
244
|
-
// interested in the cache.
|
|
245
|
-
return false;
|
|
246
|
-
|
|
247
|
-
case FetchPolicy.CacheBeforeNetwork:
|
|
248
|
-
// If we don't have a cached value then we need to fetch.
|
|
249
|
-
return mostRecentResult == null;
|
|
250
|
-
|
|
251
|
-
case FetchPolicy.CacheAndNetwork:
|
|
252
|
-
case FetchPolicy.NetworkOnly:
|
|
253
|
-
// We don't care about the cache. If we don't have a network
|
|
254
|
-
// result, then we need to fetch one.
|
|
255
|
-
return networkResultRef.current == null;
|
|
256
|
-
}
|
|
257
|
-
}, [mostRecentResult, fetchPolicy, hardSkip]);
|
|
258
|
-
|
|
259
|
-
React.useEffect(() => {
|
|
260
|
-
if (!shouldFetch) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
fetchRequest();
|
|
264
|
-
return () => {
|
|
265
|
-
currentRequestRef.current?.cancel();
|
|
266
|
-
currentRequestRef.current = null;
|
|
267
|
-
};
|
|
268
|
-
}, [shouldFetch, fetchRequest]);
|
|
269
|
-
|
|
270
|
-
// We track the last result we returned in order to support the
|
|
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).
|
|
281
|
-
const loadingResult = retainResultOnChange
|
|
282
|
-
? lastResultAgnosticOfIdRef.current
|
|
283
|
-
: shouldFetch
|
|
284
|
-
? Status.loading<TData>()
|
|
285
|
-
: Status.noData<TData>();
|
|
286
|
-
|
|
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> =
|
|
290
|
-
(fetchPolicy === FetchPolicy.NetworkOnly
|
|
291
|
-
? networkResultRef.current
|
|
292
|
-
: mostRecentResult) ?? loadingResult;
|
|
293
|
-
lastResultAgnosticOfIdRef.current = result;
|
|
294
|
-
|
|
295
|
-
// We return the result and a function for triggering a refetch.
|
|
296
|
-
return [result, fetchRequest];
|
|
297
|
-
};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import {useContext, useRef, useMemo} from "react";
|
|
2
|
-
|
|
3
|
-
import {mergeGqlContext} from "../util/merge-gql-context";
|
|
4
|
-
import {GqlRouterContext} from "../util/gql-router-context";
|
|
5
|
-
import {GqlError, GqlErrors} from "../util/gql-error";
|
|
6
|
-
|
|
7
|
-
import type {GqlRouterConfiguration, GqlContext} from "../util/gql-types";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Construct a GqlRouterContext from the current one and partial context.
|
|
11
|
-
*/
|
|
12
|
-
export const useGqlRouterContext = <TContext extends GqlContext>(
|
|
13
|
-
contextOverrides: Partial<TContext> = {} as Partial<TContext>,
|
|
14
|
-
): GqlRouterConfiguration<TContext> => {
|
|
15
|
-
// This hook only works if the `GqlRouter` has been used to setup context.
|
|
16
|
-
const gqlRouterContext = useContext(GqlRouterContext);
|
|
17
|
-
if (gqlRouterContext == null) {
|
|
18
|
-
throw new GqlError("No GqlRouter", GqlErrors.Internal);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const {fetch, defaultContext} = gqlRouterContext;
|
|
22
|
-
const contextRef = useRef<TContext>(defaultContext);
|
|
23
|
-
const mergedContext = mergeGqlContext(defaultContext, contextOverrides);
|
|
24
|
-
|
|
25
|
-
// Now, we can see if this represents a new context and if so,
|
|
26
|
-
// update our ref and return the merged value.
|
|
27
|
-
const refKeys = Object.keys(contextRef.current);
|
|
28
|
-
const mergedKeys = Object.keys(mergedContext);
|
|
29
|
-
const shouldWeUpdateRef =
|
|
30
|
-
refKeys.length !== mergedKeys.length ||
|
|
31
|
-
mergedKeys.every(
|
|
32
|
-
(key) => contextRef.current[key] !== mergedContext[key],
|
|
33
|
-
);
|
|
34
|
-
if (shouldWeUpdateRef) {
|
|
35
|
-
contextRef.current = mergedContext;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// OK, now we're up-to-date, let's memoize our final result.
|
|
39
|
-
const finalContext = contextRef.current;
|
|
40
|
-
const finalRouterContext = useMemo(
|
|
41
|
-
() => ({
|
|
42
|
-
fetch,
|
|
43
|
-
defaultContext: finalContext,
|
|
44
|
-
}),
|
|
45
|
-
[fetch, finalContext],
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
return finalRouterContext;
|
|
49
|
-
};
|
package/src/hooks/use-gql.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import {useCallback} from "react";
|
|
2
|
-
|
|
3
|
-
import {mergeGqlContext} from "../util/merge-gql-context";
|
|
4
|
-
import {useGqlRouterContext} from "./use-gql-router-context";
|
|
5
|
-
import {getGqlDataFromResponse} from "../util/get-gql-data-from-response";
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
GqlContext,
|
|
9
|
-
GqlOperation,
|
|
10
|
-
GqlFetchOptions,
|
|
11
|
-
} from "../util/gql-types";
|
|
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
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Hook to obtain a gqlFetch function for performing GraphQL requests.
|
|
22
|
-
*
|
|
23
|
-
* The fetch function will resolve null if the request was aborted, otherwise
|
|
24
|
-
* it will resolve the data returned by the GraphQL server.
|
|
25
|
-
*
|
|
26
|
-
* Context is merged with the default context provided to the GqlRouter.
|
|
27
|
-
* Values in the partial context given to the returned fetch function will
|
|
28
|
-
* only be included if they have a value other than undefined.
|
|
29
|
-
*/
|
|
30
|
-
export const useGql = <TContext extends GqlContext>(
|
|
31
|
-
context: Partial<TContext> = {} as Partial<TContext>,
|
|
32
|
-
): GqlFetchFn<TContext> => {
|
|
33
|
-
// This hook only works if the `GqlRouter` has been used to setup context.
|
|
34
|
-
const gqlRouterContext = useGqlRouterContext(context);
|
|
35
|
-
|
|
36
|
-
// Let's memoize the gqlFetch function we create based off our context.
|
|
37
|
-
// That way, even if the context happens to change, if its values don't
|
|
38
|
-
// we give the same function instance back to our callers instead of
|
|
39
|
-
// making a new one. That then means they can safely use the return value
|
|
40
|
-
// in hooks deps without fear of it triggering extra renders.
|
|
41
|
-
const gqlFetch: GqlFetchFn<TContext> = useCallback(
|
|
42
|
-
<TData, TVariables extends Record<any, any>>(
|
|
43
|
-
operation: GqlOperation<TData, TVariables>,
|
|
44
|
-
options: GqlFetchOptions<TVariables, TContext> = Object.freeze({}),
|
|
45
|
-
): Promise<TData> => {
|
|
46
|
-
const {fetch, defaultContext} = gqlRouterContext;
|
|
47
|
-
const {variables, context = {}} = options;
|
|
48
|
-
const finalContext = mergeGqlContext(defaultContext, context);
|
|
49
|
-
|
|
50
|
-
// Invoke the fetch and extract the data.
|
|
51
|
-
return fetch(operation, variables, finalContext).then((response) =>
|
|
52
|
-
getGqlDataFromResponse<TData>(response),
|
|
53
|
-
);
|
|
54
|
-
},
|
|
55
|
-
[gqlRouterContext],
|
|
56
|
-
);
|
|
57
|
-
return gqlFetch;
|
|
58
|
-
};
|