From ab3b117680c56030163253a6e332928775fa086b Mon Sep 17 00:00:00 2001 From: Tianyu Yao Date: Thu, 27 Feb 2025 12:55:14 -0800 Subject: [PATCH] Pass operation availability to the network layer for loadQuery Reviewed By: lynnshaoyu Differential Revision: D70144365 fbshipit-source-id: d97370b3087987baff2a59183dca613cc223ba59 --- .../relay-hooks/__tests__/loadQuery-test.js | 56 ++++++++++++++++++- .../__tests__/preloadQuery_DEPRECATED-test.js | 35 ++++++++++-- packages/react-relay/relay-hooks/loadQuery.js | 21 ++++++- .../relay-hooks/preloadQuery_DEPRECATED.js | 20 ++++++- 4 files changed, 120 insertions(+), 12 deletions(-) diff --git a/packages/react-relay/relay-hooks/__tests__/loadQuery-test.js b/packages/react-relay/relay-hooks/__tests__/loadQuery-test.js index 9e8b9b05acaed..a83f8e43c22d1 100644 --- a/packages/react-relay/relay-hooks/__tests__/loadQuery-test.js +++ b/packages/react-relay/relay-hooks/__tests__/loadQuery-test.js @@ -18,6 +18,7 @@ import type { } from './__generated__/loadQueryTestQuery.graphql'; import type { CacheConfig, + INetwork, LogRequestInfoFunction, Query, RequestParameters, @@ -96,6 +97,7 @@ describe('loadQuery', () => { let mockAvailability: {fetchTime?: number, status: string}; let disposeOnloadCallback; let executeOnloadCallback; + let checkOperation; beforeEach(() => { fetch = jest.fn( @@ -122,7 +124,17 @@ describe('loadQuery', () => { return observable; }, ); - environment = createMockEnvironment({network: Network.create(fetch)}); + function wrapNetworkExecute(network: INetwork): INetwork { + return { + execute: (_1, _2, _3, _4, _5, _6, _7, _checkOperation) => { + checkOperation = _checkOperation; + return network.execute(_1, _2, _3, _4, _5, _6, _7, _checkOperation); + }, + }; + } + environment = createMockEnvironment({ + network: wrapNetworkExecute(Network.create(fetch)), + }); jest.clearAllTimers(); jest.useFakeTimers(); @@ -397,6 +409,48 @@ describe('loadQuery', () => { expect(disposeEnvironmentRetain).toHaveBeenCalledTimes(1); }); }); + + describe("with fetchPolicy === 'store-and-network'", () => { + it('should call fetch if the query can be fulfilled by the store', () => { + const {source} = loadQuery( + environment, + preloadableConcreteRequest, + variables, + { + fetchPolicy: 'store-and-network', + }, + ); + expect(fetch).toHaveBeenCalled(); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + expect(environment.executeWithSource).toHaveBeenCalled(); + expect(source).toBeDefined(); + // Query should still be retained even if we don't fetch + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + expect(environment.retain).toHaveBeenCalled(); + }); + + it('returns the correct operation availability (available)', () => { + loadQuery(environment, preloadableConcreteRequest, variables, { + fetchPolicy: 'store-and-network', + }); + expect(fetch).toHaveBeenCalled(); + expect(checkOperation != null && checkOperation().status).toEqual( + 'available', + ); + }); + + it('returns the correct operation availability (missing)', () => { + mockAvailability = {status: 'missing'}; + + loadQuery(environment, preloadableConcreteRequest, variables, { + fetchPolicy: 'store-and-network', + }); + expect(fetch).toHaveBeenCalled(); + expect(checkOperation != null && checkOperation().status).toEqual( + 'missing', + ); + }); + }); }); describe('when the query AST is unavailable synchronously', () => { diff --git a/packages/react-relay/relay-hooks/__tests__/preloadQuery_DEPRECATED-test.js b/packages/react-relay/relay-hooks/__tests__/preloadQuery_DEPRECATED-test.js index 3d9d6c5040ad8..9b3354f822fc4 100644 --- a/packages/react-relay/relay-hooks/__tests__/preloadQuery_DEPRECATED-test.js +++ b/packages/react-relay/relay-hooks/__tests__/preloadQuery_DEPRECATED-test.js @@ -11,7 +11,10 @@ 'use strict'; -import type {GraphQLResponse} from 'relay-runtime/network/RelayNetworkTypes'; +import type { + GraphQLResponse, + INetwork, +} from 'relay-runtime/network/RelayNetworkTypes'; const preloadQuery_DEPRECATED = require('../preloadQuery_DEPRECATED'); const { @@ -76,19 +79,37 @@ describe.each(['RelayModernEnvironment', 'MultiActorEnvironment'])( let sink; let variables; let operation; + let checkOperation; beforeEach(() => { // $FlowFixMe[missing-local-annot] error found when enabling Flow LTI mode - fetch = jest.fn((_query, _variables, _cacheConfig) => { + fetch = jest.fn((_query, _variables, _cacheConfig, _4, _5) => { // $FlowFixMe[missing-local-annot] error found when enabling Flow LTI mode return Observable.create(_sink => { sink = _sink; }); }); - + function wrapNetworkExecute(network: INetwork): INetwork { + return { + execute: (_1, _2, _3, _4, _5, _6, _7, _checkOperation) => { + checkOperation = _checkOperation; + return network.execute( + _1, + _2, + _3, + _4, + _5, + _6, + _7, + _checkOperation, + ); + }, + }; + } const multiActorEnvironment = new MultiActorEnvironment({ // $FlowFixMe[invalid-tuple-arity] Error found while enabling LTI on this file - createNetworkForActor: _actorID => Network.create(fetch), + createNetworkForActor: _actorID => + wrapNetworkExecute(Network.create(fetch)), createStoreForActor: _actorID => new Store(new RecordSource(), { gcReleaseBufferSize: 1, @@ -99,7 +120,7 @@ describe.each(['RelayModernEnvironment', 'MultiActorEnvironment'])( ? multiActorEnvironment.forActor(getActorIdentifier('actor:1234')) : new Environment({ // $FlowFixMe[invalid-tuple-arity] Error found while enabling LTI on this file - network: Network.create(fetch), + network: wrapNetworkExecute(Network.create(fetch)), store: new Store(new RecordSource(), { gcReleaseBufferSize: 1, }), @@ -535,6 +556,10 @@ describe.each(['RelayModernEnvironment', 'MultiActorEnvironment'])( expect(fetch.mock.calls[0][0]).toBe(query.params); expect(fetch.mock.calls[0][1]).toEqual(variables); expect(fetch.mock.calls[0][2]).toEqual({force: true}); + expect(checkOperation && checkOperation()).toEqual({ + status: 'available', + fetchTime, + }); const [events, observer] = createObserver(); if (preloaded.source) { diff --git a/packages/react-relay/relay-hooks/loadQuery.js b/packages/react-relay/relay-hooks/loadQuery.js index 978b54f89c132..32f952d4a0e1c 100644 --- a/packages/react-relay/relay-hooks/loadQuery.js +++ b/packages/react-relay/relay-hooks/loadQuery.js @@ -28,6 +28,7 @@ import type { RequestIdentifier, RequestParameters, } from 'relay-runtime'; +import type {OperationAvailability} from 'relay-runtime/store/RelayStoreTypes'; const invariant = require('invariant'); const { @@ -130,6 +131,7 @@ function loadQuery< let didMakeNetworkRequest = false; const makeNetworkRequest = ( params: RequestParameters, + checkOperation?: () => OperationAvailability, ): Observable => { // N.B. this function is called synchronously or not at all // didMakeNetworkRequest is safe to rely on in the returned value @@ -160,7 +162,16 @@ function loadQuery< 'raw-network-request-' + getRequestIdentifier(params, variables); const observable = fetchQueryDeduped(environment, identifier, () => { const network = environment.getNetwork(); - return network.execute(params, variables, networkCacheConfig); + return network.execute( + params, + variables, + networkCacheConfig, + undefined, + undefined, + undefined, + undefined, + checkOperation, + ); }); const {unsubscribe} = observable.subscribe({ @@ -245,15 +256,19 @@ function loadQuery< // N.B. If the fetch policy allows fulfillment from the store but the // environment already has the data for that operation cached in the store, // then we do nothing. + const operationAvailability = environment.check(operation); const shouldFetch = fetchPolicy !== 'store-or-network' || - environment.check(operation).status !== 'available'; + operationAvailability.status !== 'available'; if (shouldFetch) { executeDeduped(operation, () => { // N.B. Since we have the operation synchronously available here, // we can immediately fetch and execute the operation. - const networkObservable = makeNetworkRequest(concreteRequest.params); + const networkObservable = makeNetworkRequest( + concreteRequest.params, + () => operationAvailability, + ); const executeObservable = executeWithNetworkSource( operation, networkObservable, diff --git a/packages/react-relay/relay-hooks/preloadQuery_DEPRECATED.js b/packages/react-relay/relay-hooks/preloadQuery_DEPRECATED.js index e6ab0ec4f89ff..4cddf74dbdce5 100644 --- a/packages/react-relay/relay-hooks/preloadQuery_DEPRECATED.js +++ b/packages/react-relay/relay-hooks/preloadQuery_DEPRECATED.js @@ -167,12 +167,17 @@ function preloadQueryDeduped( }`; const prevQueryEntry = pendingQueries.get(cacheKey); - const availability = - fetchPolicy === STORE_OR_NETWORK_DEFAULT && query != null && query != null + function checkOperation() { + return query != null ? environment.check( createOperationDescriptor(query, variables, networkCacheConfig), ) : {status: 'missing'}; + } + const availability = + fetchPolicy === STORE_OR_NETWORK_DEFAULT + ? checkOperation() + : {status: 'missing'}; let nextQueryEntry: ?PendingQueryEntry; if (availability.status === 'available' && query != null) { @@ -203,7 +208,16 @@ function preloadQueryDeduped( } } else if (prevQueryEntry == null || prevQueryEntry.kind !== 'network') { // Should fetch but we're not already fetching: fetch! - const source = network.execute(params, variables, networkCacheConfig, null); + const source = network.execute( + params, + variables, + networkCacheConfig, + null, + undefined, + undefined, + undefined, + checkOperation, + ); const subject = new ReplaySubject(); nextQueryEntry = { cacheKey,