Skip to content

Commit 4527a5b

Browse files
Juan Tejadafacebook-github-bot
Juan Tejada
authored andcommittedDec 15, 2020
loadQuery dedupes requests even before ast is available
Reviewed By: josephsavona Differential Revision: D25540639 fbshipit-source-id: dc5ec9190f84e1ad3777eefdacfb229139934735
1 parent 37436b5 commit 4527a5b

File tree

6 files changed

+83
-32
lines changed

6 files changed

+83
-32
lines changed
 

‎packages/relay-experimental/__tests__/loadQuery-source-behavior-test.js

+17-11
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,12 @@ describe('when passed a PreloadableConcreteRequest', () => {
153153

154154
it('should dedupe network request if called multiple times', () => {
155155
PreloadableQueryRegistry.set(ID, query);
156-
callLoadQuery(preloadableConcreteRequest);
157-
callLoadQuery(preloadableConcreteRequest);
156+
const res1 = callLoadQuery(preloadableConcreteRequest);
157+
const res2 = callLoadQuery(preloadableConcreteRequest);
158158

159159
expect(fetch).toHaveBeenCalledTimes(1);
160+
expect(res1.source).toBeDefined();
161+
expect(res2.source).toBeDefined();
160162
});
161163

162164
it('should pass network errors onto source', () => {
@@ -193,20 +195,24 @@ describe('when passed a PreloadableConcreteRequest', () => {
193195
});
194196

195197
describe('when the query is unavailable synchronously', () => {
196-
it('should dedupe operation execution if called multiple times', () => {
197-
callLoadQuery(preloadableConcreteRequest);
198-
callLoadQuery(preloadableConcreteRequest);
198+
it('should dedupe network request if called multiple times', () => {
199+
const res1 = callLoadQuery(preloadableConcreteRequest);
200+
const res2 = callLoadQuery(preloadableConcreteRequest);
201+
expect(fetch).toHaveBeenCalledTimes(1);
199202

200-
// Note: before we have the operation module is available
201-
// we can't reliably dedupe network requests, since the
202-
// request identifier is based on the variables the
203-
// operation expects, and not just the variables passed as
204-
// input.
205-
expect(fetch).toHaveBeenCalledTimes(2);
203+
expect(res1.source).toBeDefined();
204+
expect(res2.source).toBeDefined();
205+
});
206+
it('should dedupe operation execution if called multiple times', () => {
207+
const res1 = callLoadQuery(preloadableConcreteRequest);
208+
const res2 = callLoadQuery(preloadableConcreteRequest);
209+
expect(fetch).toHaveBeenCalledTimes(1);
206210

207211
PreloadableQueryRegistry.set(ID, query);
208212
// We only process the network request once.
209213
expect(environment.executeWithSource).toBeCalledTimes(1);
214+
expect(res1.source).toBeDefined();
215+
expect(res2.source).toBeDefined();
210216
});
211217

212218
describe('when the query AST is available before the network response', () => {

‎packages/relay-experimental/__tests__/loadQuery-test.js

-1
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,6 @@ describe('when passed a PreloadableConcreteRequest', () => {
304304
sink.next(response);
305305
expect(nextCallback).toHaveBeenCalledWith(response);
306306
});
307-
308307
it('should mark failed network requests', () => {
309308
const preloadedQuery = loadQuery(
310309
environment,

‎packages/relay-experimental/loadQuery.js

+62-19
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ const {
2020
ReplaySubject,
2121
createOperationDescriptor,
2222
getRequest,
23+
getRequestIdentifier,
2324
Observable,
25+
RelayFeatureFlags,
2426
__internal: {fetchQueryDeduped},
2527
} = require('relay-runtime');
2628

@@ -35,6 +37,7 @@ import type {
3537
OperationType,
3638
GraphQLTaggedNode,
3739
GraphQLResponse,
40+
RequestIdentifier,
3841
} from 'relay-runtime';
3942

4043
let RenderDispatcher = null;
@@ -83,25 +86,50 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
8386

8487
let unsubscribeFromNetworkRequest;
8588
let networkError = null;
86-
87-
// makeNetworkRequest will immediately start a raw network request and
88-
// return an Observable that when subscribing to it, will replay the
89-
// network events that have occured so far, as well as subsequent events.
89+
// makeNetworkRequest will immediately start a raw network request if
90+
// one isn't already in flight and return an Observable that when
91+
// subscribed to will replay the network events that have occured so far,
92+
// as well as subsequent events.
9093
let madeNetworkRequest = false;
9194
const makeNetworkRequest = (params): Observable<GraphQLResponse> => {
9295
// N.B. this function is called synchronously or not at all
9396
// madeNetworkRequest is safe to rely on in the returned value
94-
97+
// Even if the request gets deduped below, we still wan't to return an
98+
// observable that provides the replayed network events for the query,
99+
// so we set this to true before deduping, to guarantee that the
100+
// `source` observable is returned.
95101
madeNetworkRequest = true;
96-
const network = environment.getNetwork();
97-
const sourceObservable = network.execute(
98-
params,
99-
variables,
100-
networkCacheConfig,
101-
);
102102

103+
let observable;
103104
const subject = new ReplaySubject();
104-
({unsubscribe: unsubscribeFromNetworkRequest} = sourceObservable.subscribe({
105+
if (RelayFeatureFlags.ENABLE_LOAD_QUERY_REQUEST_DEDUPING === true) {
106+
// Here, we are calling fetchQueryDeduped at the network layer level,
107+
// which ensures that only a single network request is active for a given
108+
// (environment, identifier) pair.
109+
// Since network requests can be started /before/ we have the query ast
110+
// necessary to process the results, we need to dedupe the raw requests
111+
// separately from deduping the operation execution; specifically,
112+
// if `loadQuery` is called multiple times before the query ast is available,
113+
// we still want the network request to be deduped.
114+
// - If a duplicate active network request is found, it will return an
115+
// Observable that replays the events of the already active request.
116+
// - If no duplicate active network request is found, it will call the fetchFn
117+
// to start the request, and return an Observable that will replay
118+
// the events from the network request.
119+
// We provide an extra key to the identifier to distinguish deduping
120+
// of raw network requests vs deduping of operation executions.
121+
const identifier: RequestIdentifier =
122+
'raw-network-request-' + getRequestIdentifier(params, variables);
123+
observable = fetchQueryDeduped(environment, identifier, () => {
124+
const network = environment.getNetwork();
125+
return network.execute(params, variables, networkCacheConfig);
126+
});
127+
} else {
128+
const network = environment.getNetwork();
129+
observable = network.execute(params, variables, networkCacheConfig);
130+
}
131+
132+
({unsubscribe: unsubscribeFromNetworkRequest} = observable.subscribe({
105133
error(err) {
106134
networkError = err;
107135
subject.error(err);
@@ -150,12 +178,27 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
150178
operation: OperationDescriptor,
151179
fetchFn: () => Observable<GraphQLResponse>,
152180
) => {
153-
// N.B.
181+
if (RelayFeatureFlags.ENABLE_LOAD_QUERY_REQUEST_DEDUPING === true) {
182+
// N.B. at this point, if we're calling execute with a query ast (OperationDescriptor),
183+
// we are guaranteed to have started a network request. We set this to
184+
// true here as well since `makeNetworkRequest` might get skipped in the case
185+
// where the query ast is already available and the query executions get deduped.
186+
// Even if the execution gets deduped below, we still wan't to return
187+
// an observable that provides the replayed network events for the query,
188+
// so we set this to true before deduping, to guarantee that the `source`
189+
// observable is returned.
190+
madeNetworkRequest = true;
191+
}
192+
154193
// Here, we are calling fetchQueryDeduped, which ensures that only
155194
// a single operation is active for a given (environment, identifier) pair,
156195
// and also tracks the active state of the operation, which is necessary
157196
// for our Suspense infra to later be able to suspend (or not) on
158-
// active operations.
197+
// active operations. Even though we already dedupe raw network requests,
198+
// we also need to dedupe and keep track operation execution for our Suspense
199+
// infra, and we also want to avoid processing responses more than once, for
200+
// the cases where `loadQuery` might be called multiple times after the query ast
201+
// is available.
159202
// - If a duplicate active operation is found, it will return an
160203
// Observable that replays the events of the already active operation.
161204
// - If no duplicate active operation is found, it will call the fetchFn
@@ -222,11 +265,11 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
222265
checkAvailabilityAndExecute(module);
223266
} else {
224267
// If the module isn't synchronously available, we launch the
225-
// network request immediately and ignore the fetch policy.
226-
// Note that without the operation module we can't reliably
227-
// dedupe network requests, since the request identifier is
228-
// based on the variables the operation expects, and not
229-
// just the variables passed as input.
268+
// network request immediately, ignoring the fetchPolicy. We
269+
// do this because we can't check if a query is cached without the
270+
// ast, and we know that if we don't have the query ast
271+
// available, then this query could've never been written to the
272+
// store in the first place, so it couldn't have been cached.
230273
const networkObservable = makeNetworkRequest(params);
231274
({dispose: cancelOnLoadCallback} = PreloadableQueryRegistry.onLoad(
232275
moduleId,

‎packages/relay-runtime/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export type {
202202
VariablesOf,
203203
} from './util/RelayRuntimeTypes';
204204
export type {Local3DPayload} from './util/createPayloadFor3DField';
205+
export type {RequestIdentifier} from './util/getRequestIdentifier';
205206

206207
// As early as possible, check for the existence of the JavaScript globals which
207208
// Relay Runtime relies upon, and produce a clear message if they do not exist.

‎packages/relay-runtime/util/RelayFeatureFlags.js

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type FeatureFlags = {|
2222
ENABLE_GETFRAGMENTIDENTIFIER_OPTIMIZATION: boolean,
2323
ENABLE_FRIENDLY_QUERY_NAME_GQL_URL: boolean,
2424
ENABLE_STORE_SUBSCRIPTIONS_REFACTOR: boolean,
25+
ENABLE_LOAD_QUERY_REQUEST_DEDUPING: boolean,
2526
|};
2627

2728
const RelayFeatureFlags: FeatureFlags = {
@@ -34,6 +35,7 @@ const RelayFeatureFlags: FeatureFlags = {
3435
ENABLE_GETFRAGMENTIDENTIFIER_OPTIMIZATION: false,
3536
ENABLE_FRIENDLY_QUERY_NAME_GQL_URL: false,
3637
ENABLE_STORE_SUBSCRIPTIONS_REFACTOR: false,
38+
ENABLE_LOAD_QUERY_REQUEST_DEDUPING: true,
3739
};
3840

3941
module.exports = RelayFeatureFlags;

‎packages/relay-runtime/util/getRequestIdentifier.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const stableCopy = require('./stableCopy');
1818
import type {RequestParameters} from './RelayConcreteNode';
1919
import type {Variables} from './RelayRuntimeTypes';
2020

21-
export opaque type RequestIdentifier: string = string;
21+
export type RequestIdentifier = string;
2222

2323
/**
2424
* Returns a stable identifier for the given pair of `RequestParameters` +

0 commit comments

Comments
 (0)
Please sign in to comment.