UNPKG

react-relay

Version:

A framework for building GraphQL-driven React applications.

380 lines (354 loc) • 13.5 kB
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall relay */ 'use strict'; import type { EnvironmentProviderOptions, LoadQueryOptions, PreloadableConcreteRequest, PreloadedQueryInner, } from './EntryPointTypes.flow'; import type { ConcreteRequest, GraphQLResponse, GraphQLTaggedNode, IEnvironment, OperationDescriptor, OperationType, Query, RequestIdentifier, RequestParameters, } from 'relay-runtime'; const invariant = require('invariant'); const { __internal: {fetchQueryDeduped}, Observable, PreloadableQueryRegistry, ReplaySubject, createOperationDescriptor, getRequest, getRequestIdentifier, } = require('relay-runtime'); let fetchKey = 100001; type QueryType<T> = T extends Query<infer V, infer D, infer RR> ? { variables: V, response: D, rawResponse?: $NonMaybeType<RR>, } : [+t: T] extends [+t: PreloadableConcreteRequest<infer V>] ? V : empty; declare function loadQuery< T, TEnvironmentProviderOptions = EnvironmentProviderOptions, >( environment: IEnvironment, preloadableRequest: T, variables: QueryType<T>['variables'], options?: ?LoadQueryOptions, environmentProviderOptions?: ?TEnvironmentProviderOptions, ): PreloadedQueryInner<QueryType<T>, TEnvironmentProviderOptions>; function loadQuery< TQuery: OperationType, TEnvironmentProviderOptions = EnvironmentProviderOptions, >( environment: IEnvironment, preloadableRequest: PreloadableConcreteRequest<TQuery>, variables: TQuery['variables'], options?: ?LoadQueryOptions, environmentProviderOptions?: ?TEnvironmentProviderOptions, ): PreloadedQueryInner<TQuery, TEnvironmentProviderOptions> { // Every time you call loadQuery, we will generate a new fetchKey. // This will ensure that every query reference that is created and // passed to usePreloadedQuery is independently evaluated, // even if they are for the same query/variables. // Specifically, we want to avoid a case where we try to refetch a // query by calling loadQuery a second time, and have the Suspense // cache in usePreloadedQuery reuse the cached result instead of // re-evaluating the new query ref and triggering a refetch if // necessary. fetchKey++; const fetchPolicy = options?.fetchPolicy ?? 'store-or-network'; const networkCacheConfig = { ...options?.networkCacheConfig, force: true, }; // executeWithNetworkSource will retain and execute an operation // against the Relay store, given an Observable that would provide // the network events for the operation. let retainReference; let didExecuteNetworkSource = false; const executeWithNetworkSource = ( operation: OperationDescriptor, networkObservable: Observable<GraphQLResponse>, ): Observable<GraphQLResponse> => { didExecuteNetworkSource = true; return environment.executeWithSource({ operation, source: networkObservable, }); }; // N.B. For loadQuery, we unconventionally want to return an Observable // that isn't lazily executed, meaning that we don't want to wait // until the returned Observable is subscribed to to actually start // fetching and executing an operation; i.e. we want to execute the // operation eagerly, when loadQuery is called. // For this reason, we use an intermediate executionSubject which // allows us to capture the events that occur during the eager execution // of the operation, and then replay them to the Observable we // ultimately return. const executionSubject = new ReplaySubject<GraphQLResponse>(); const returnedObservable = Observable.create<GraphQLResponse>(sink => executionSubject.subscribe(sink), ); let unsubscribeFromNetworkRequest; let networkError = null; // makeNetworkRequest will immediately start a raw network request if // one isn't already in flight and return an Observable that when // subscribed to will replay the network events that have occured so far, // as well as subsequent events. let didMakeNetworkRequest = false; const makeNetworkRequest = ( params: RequestParameters, ): Observable<GraphQLResponse> => { // N.B. this function is called synchronously or not at all // didMakeNetworkRequest is safe to rely on in the returned value // Even if the request gets deduped below, we still wan't to return an // observable that provides the replayed network events for the query, // so we set this to true before deduping, to guarantee that the // `source` observable is returned. didMakeNetworkRequest = true; const subject = new ReplaySubject<GraphQLResponse>(); // Here, we are calling fetchQueryDeduped at the network layer level, // which ensures that only a single network request is active for a given // (environment, identifier) pair. // Since network requests can be started /before/ we have the query ast // necessary to process the results, we need to dedupe the raw requests // separately from deduping the operation execution; specifically, // if `loadQuery` is called multiple times before the query ast is available, // we still want the network request to be deduped. // - If a duplicate active network request is found, it will return an // Observable that replays the events of the already active request. // - If no duplicate active network request is found, it will call the fetchFn // to start the request, and return an Observable that will replay // the events from the network request. // We provide an extra key to the identifier to distinguish deduping // of raw network requests vs deduping of operation executions. const identifier: RequestIdentifier = 'raw-network-request-' + getRequestIdentifier(params, variables); const observable = fetchQueryDeduped(environment, identifier, () => { const network = environment.getNetwork(); return network.execute(params, variables, networkCacheConfig); }); const {unsubscribe} = observable.subscribe({ error(err) { networkError = err; subject.error(err); }, next(data) { subject.next(data); }, complete() { subject.complete(); }, }); unsubscribeFromNetworkRequest = unsubscribe; return Observable.create(sink => { const subjectSubscription = subject.subscribe(sink); return () => { subjectSubscription.unsubscribe(); unsubscribeFromNetworkRequest(); }; }); }; let unsubscribeFromExecution; const executeDeduped = ( operation: OperationDescriptor, fetchFn: () => Observable<GraphQLResponse>, ) => { // N.B. at this point, if we're calling execute with a query ast (OperationDescriptor), // we are guaranteed to have started a network request. We set this to // true here as well since `makeNetworkRequest` might get skipped in the case // where the query ast is already available and the query executions get deduped. // Even if the execution gets deduped below, we still wan't to return // an observable that provides the replayed network events for the query, // so we set this to true before deduping, to guarantee that the `source` // observable is returned. didMakeNetworkRequest = true; // Here, we are calling fetchQueryDeduped, which ensures that only // a single operation is active for a given (environment, identifier) pair, // and also tracks the active state of the operation, which is necessary // for our Suspense infra to later be able to suspend (or not) on // active operations. Even though we already dedupe raw network requests, // we also need to dedupe and keep track operation execution for our Suspense // infra, and we also want to avoid processing responses more than once, for // the cases where `loadQuery` might be called multiple times after the query ast // is available. // - If a duplicate active operation is found, it will return an // Observable that replays the events of the already active operation. // - If no duplicate active operation is found, it will call the fetchFn // to execute the operation, and return an Observable that will provide // the events for executing the operation. ({unsubscribe: unsubscribeFromExecution} = fetchQueryDeduped( environment, operation.request.identifier, fetchFn, ).subscribe({ error(err) { executionSubject.error(err); }, next(data) { executionSubject.next(data); }, complete() { executionSubject.complete(); }, })); }; const checkAvailabilityAndExecute = (concreteRequest: ConcreteRequest) => { const operation = createOperationDescriptor( concreteRequest, variables, networkCacheConfig, ); retainReference = environment.retain(operation); if (fetchPolicy === 'store-only') { return; } // 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 shouldFetch = fetchPolicy !== 'store-or-network' || environment.check(operation).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 executeObservable = executeWithNetworkSource( operation, networkObservable, ); return executeObservable; }); } }; let params; let cancelOnLoadCallback: () => void; let queryId; if (preloadableRequest.kind === 'PreloadableConcreteRequest') { const preloadableConcreteRequest: PreloadableConcreteRequest<TQuery> = (preloadableRequest: $FlowFixMe); ({params} = preloadableConcreteRequest); ({id: queryId} = params); invariant( queryId !== null, 'Relay: `loadQuery` requires that preloadable query `%s` has a persisted query id', params.name, ); const module = PreloadableQueryRegistry.get(queryId); if (module != null) { checkAvailabilityAndExecute(module); } else { // If the module isn't synchronously available, we launch the // network request immediately if the fetchPolicy might produce // a network fetch, regardless of the state of the store cache. We // do this because we can't check if a query is cached without the // ast, and we know that if we don't have the query ast // available, then this query could've never been written to the // store in the first place, so it couldn't have been cached. const networkObservable = fetchPolicy === 'store-only' ? null : makeNetworkRequest(params); // $FlowFixMe[method-unbinding] added when improving typing for this parameters ({dispose: cancelOnLoadCallback} = PreloadableQueryRegistry.onLoad( queryId, preloadedModule => { cancelOnLoadCallback(); const operation = createOperationDescriptor( preloadedModule, variables, networkCacheConfig, ); retainReference = environment.retain(operation); if (networkObservable != null) { executeDeduped(operation, () => executeWithNetworkSource(operation, networkObservable), ); } }, )); } } else { const graphQlTaggedNode: GraphQLTaggedNode = (preloadableRequest: $FlowFixMe); const request = getRequest(graphQlTaggedNode); params = request.params; queryId = params.cacheID != null ? params.cacheID : params.id; checkAvailabilityAndExecute(request); } let isDisposed = false; let isReleased = false; let isNetworkRequestCancelled = false; const releaseQuery = () => { if (isReleased) { return; } retainReference && retainReference.dispose(); isReleased = true; }; const cancelNetworkRequest = () => { if (isNetworkRequestCancelled) { return; } if (didExecuteNetworkSource) { unsubscribeFromExecution && unsubscribeFromExecution(); } else { unsubscribeFromNetworkRequest && unsubscribeFromNetworkRequest(); } cancelOnLoadCallback && cancelOnLoadCallback(); isNetworkRequestCancelled = true; }; return { kind: 'PreloadedQuery', environment, environmentProviderOptions, dispose() { if (isDisposed) { return; } releaseQuery(); cancelNetworkRequest(); isDisposed = true; }, releaseQuery, cancelNetworkRequest, fetchKey, id: queryId, // $FlowFixMe[unsafe-getters-setters] - this has no side effects get isDisposed() { return isDisposed || isReleased; }, // $FlowFixMe[unsafe-getters-setters] - this has no side effects get networkError() { return networkError; }, name: params.name, networkCacheConfig, fetchPolicy, source: didMakeNetworkRequest ? returnedObservable : undefined, variables, }; } module.exports = { loadQuery, };