@@ -21,6 +21,7 @@ const {
21
21
createOperationDescriptor,
22
22
getRequest,
23
23
Observable,
24
+ __internal : { fetchQueryDeduped} ,
24
25
} = require ( 'relay-runtime' ) ;
25
26
26
27
import type {
@@ -30,6 +31,7 @@ import type {
30
31
} from './EntryPointTypes.flow' ;
31
32
import type {
32
33
IEnvironment ,
34
+ OperationDescriptor ,
33
35
OperationType ,
34
36
GraphQLTaggedNode ,
35
37
GraphQLResponse ,
@@ -57,8 +59,7 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
57
59
options ? : LoadQueryOptions ,
58
60
environmentProviderOptions ? : TEnvironmentProviderOptions ,
59
61
) : PreloadedQueryInner < TQuery , TEnvironmentProviderOptions > {
60
- // Flow does not know of React internals (rightly so), but we need to
61
- // ensure here that this function isn't called inside render.
62
+ // This code ensures that we don't call loadQuery during render.
62
63
const CurrentDispatcher =
63
64
// $FlowFixMe[prop-missing]
64
65
React . __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
@@ -74,6 +75,9 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
74
75
force : true ,
75
76
} ;
76
77
78
+ // makeNetworkRequest will immediately start a raw network request and
79
+ // return an Observable that when subscribing to it, will replay the
80
+ // network events that have occured so far, as well as subsequent events.
77
81
let madeNetworkRequest = false ;
78
82
const makeNetworkRequest = ( params ) : Observable < GraphQLResponse > => {
79
83
// N.B. this function is called synchronously or not at all
@@ -102,45 +106,90 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
102
106
return Observable . create ( sink => subject . subscribe ( sink ) ) ;
103
107
} ;
104
108
105
- const normalizationSubject = new ReplaySubject ( ) ;
109
+ // executeWithNetworkSource will retain and execute an operation
110
+ // against the Relay store, given an Observable that would provide
111
+ // the network events for the operation.
112
+ let retainReference ;
113
+ const executeWithNetworkSource = (
114
+ operation : OperationDescriptor ,
115
+ networkObservable : Observable < GraphQLResponse > ,
116
+ ) : Observable < GraphQLResponse > => {
117
+ retainReference = environment . retain ( operation ) ;
118
+ return environment . executeWithSource ( {
119
+ operation,
120
+ source : networkObservable ,
121
+ } ) ;
122
+ } ;
123
+
124
+ // N.B. For loadQuery, we unconventionally want to return an Observable
125
+ // that isn't lazily executed, meaning that we don't want to wait
126
+ // until the returned Observable is subscribed to to actually start
127
+ // fetching and executing an operation; i.e. we want to execute the
128
+ // operation eagerly, when loadQuery is called.
129
+ // For this reason, we use an intermediate executionSubject which
130
+ // allows us to capture the events that occur during the eager execution
131
+ // of the operation, and then replay them to the Observable we
132
+ // ultimately return.
133
+ const executionSubject = new ReplaySubject ( ) ;
106
134
const returnedObservable = Observable . create ( sink =>
107
- normalizationSubject . subscribe ( sink ) ,
135
+ executionSubject . subscribe ( sink ) ,
108
136
) ;
109
137
110
- let unsubscribeFromExecute ;
111
- let retainReference ;
112
- const executeWithSource = ( operation , sourceObservable ) = > {
113
- retainReference = environment . retain ( operation ) ;
114
- ( { unsubscribe : unsubscribeFromExecute } = environment
115
- . executeWithSource ( {
116
- operation,
117
- source : sourceObservable ,
118
- } )
119
- . subscribe ( {
120
- error ( err ) {
121
- normalizationSubject . error ( err ) ;
122
- } ,
123
- next ( data ) {
124
- normalizationSubject . next ( data ) ;
125
- } ,
126
- complete ( ) {
127
- normalizationSubject . complete ( ) ;
128
- } ,
129
- } ) ) ;
138
+ let unsubscribeFromExecution ;
139
+ const executeDeduped = (
140
+ operation : OperationDescriptor ,
141
+ fetchFn : ( ) = > Observable < GraphQLResponse > ,
142
+ ) = > {
143
+ // N.B.
144
+ // Here, we are calling fetchQueryDeduped, which ensures that only
145
+ // a single operation is active for a given (environment, identifier) pair,
146
+ // and also tracks the active state of the operation, which is necessary
147
+ // for our Suspense infra to later be able to suspend (or not) on
148
+ // active operations.
149
+ // - If a duplicate active operation is found, it will return an
150
+ // Observable that replays the events of the already active operation.
151
+ // - If no duplicate active operation is found, it will call the fetchFn
152
+ // to execute the operation, and return an Observable that will provide
153
+ // the events for executing the operation.
154
+ ( { unsubscribe : unsubscribeFromExecution } = fetchQueryDeduped (
155
+ environment ,
156
+ operation . request . identifier ,
157
+ fetchFn ,
158
+ ) . subscribe ( {
159
+ error ( err ) {
160
+ executionSubject . error ( err ) ;
161
+ } ,
162
+ next ( data ) {
163
+ executionSubject . next ( data ) ;
164
+ } ,
165
+ complete ( ) {
166
+ executionSubject . complete ( ) ;
167
+ } ,
168
+ } ) ) ;
130
169
} ;
131
170
132
171
const checkAvailabilityAndExecute = concreteRequest => {
133
172
const operation = createOperationDescriptor ( concreteRequest , variables ) ;
173
+
174
+ // N.B. If the fetch policy allows fulfillment from the store but the
175
+ // environment already has the data for that operation cached in the store,
176
+ // then we do nothing.
134
177
const shouldFetch =
135
178
fetchPolicy !== 'store-or-network' ||
136
179
environment . check ( operation ) . status !== 'available' ;
137
180
138
181
if ( shouldFetch ) {
139
- const source = makeNetworkRequest ( concreteRequest . params ) ;
140
- executeWithSource ( operation , source ) ;
182
+ executeDeduped ( operation , ( ) => {
183
+ // N.B. Since we have the operation synchronously available here,
184
+ // we can immediately fetch and execute the operation.
185
+ const networkObservable = makeNetworkRequest ( concreteRequest . params ) ;
186
+ const executeObservable = executeWithNetworkSource (
187
+ operation ,
188
+ networkObservable ,
189
+ ) ;
190
+ return executeObservable ;
191
+ } ) ;
141
192
}
142
- // if the fetch policy allows fulfillment from the store and the environment
143
- // has the appropriate data, we do nothing.
144
193
} ;
145
194
146
195
let params ;
@@ -163,9 +212,13 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
163
212
if ( module != null ) {
164
213
checkAvailabilityAndExecute ( module ) ;
165
214
} else {
166
- // If the module isn't synchronously available, we launch the network request
167
- // immediately and ignore the fetch policy.
168
- const source = makeNetworkRequest ( params ) ;
215
+ // If the module isn't synchronously available, we launch the
216
+ // network request immediately and ignore the fetch policy.
217
+ // Note that without the operation module we can't reliably
218
+ // dedupe network requests, since the request identifier is
219
+ // based on the variables the operation expects, and not
220
+ // just the variables passed as input.
221
+ const networkObservable = makeNetworkRequest ( params ) ;
169
222
( { dispose : cancelOnLoadCallback } = PreloadableQueryRegistry . onLoad (
170
223
moduleId ,
171
224
preloadedModule => {
@@ -175,7 +228,9 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
175
228
preloadedModule ,
176
229
variables ,
177
230
) ;
178
- executeWithSource ( operation , source ) ;
231
+ executeDeduped ( operation , ( ) =>
232
+ executeWithNetworkSource ( operation , networkObservable ) ,
233
+ ) ;
179
234
} ,
180
235
) ) ;
181
236
if ( ! environment . isServer ( ) ) {
@@ -187,15 +242,14 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
187
242
}
188
243
// complete() the subject so that the observer knows no (additional) payloads
189
244
// will be delivered
190
- normalizationSubject . complete ( ) ;
245
+ executionSubject . complete ( ) ;
191
246
} , LOAD_QUERY_AST_MAX_TIMEOUT ) ;
192
247
}
193
248
}
194
249
} else {
195
250
const graphQlTaggedNode : GraphQLTaggedNode = ( preloadableRequest : $FlowFixMe ) ;
196
251
const request = getRequest ( graphQlTaggedNode ) ;
197
252
params = request . params ;
198
-
199
253
checkAvailabilityAndExecute ( request ) ;
200
254
}
201
255
@@ -208,7 +262,7 @@ function loadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
208
262
if ( isDisposed ) {
209
263
return ;
210
264
}
211
- unsubscribeFromExecute && unsubscribeFromExecute ( ) ;
265
+ unsubscribeFromExecution && unsubscribeFromExecution ( ) ;
212
266
retainReference && retainReference . dispose ( ) ;
213
267
cancelOnLoadCallback && cancelOnLoadCallback ( ) ;
214
268
loadQueryAstTimeoutId != null && clearTimeout ( loadQueryAstTimeoutId ) ;
0 commit comments