@@ -154,117 +154,6 @@ module.exports = class GraphQL {
154154 this . emit ( 'reset' , { exceptCacheKey } ) ;
155155 } ;
156156
157- /**
158- * Fetches a GraphQL operation.
159- * @param {GraphQLFetchOptions } fetchOptions URL and options for [`fetch`](https://developer.mozilla.org/docs/Web/API/Fetch_API).
160- * @param {GraphQLCacheKey } cacheKey [GraphQL cache]{@link GraphQL#cache} [key]{@link GraphQLCacheKey}.
161- * @returns {Promise<GraphQLCacheValue> } A promise that resolves the [GraphQL cache]{@link GraphQL#cache} [value]{@link GraphQLCacheValue}.
162- * @fires GraphQL#event:fetch
163- * @fires GraphQL#event:cache
164- * @ignore
165- */
166- fetch = ( { url, ...options } , cacheKey ) => {
167- let fetchResponse ;
168-
169- const fetcher =
170- typeof fetch === 'function'
171- ? fetch
172- : ( ) =>
173- Promise . reject (
174- new TypeError ( 'Global fetch API or polyfill unavailable.' )
175- ) ;
176- const cacheValue = { } ;
177- const cacheValuePromise = fetcher ( url , options )
178- . then (
179- ( response ) => {
180- fetchResponse = response ;
181-
182- if ( ! response . ok )
183- cacheValue . httpError = {
184- status : response . status ,
185- statusText : response . statusText ,
186- } ;
187-
188- return response . json ( ) . then (
189- ( { errors, data } ) => {
190- // JSON parse ok.
191- if ( ! errors && ! data )
192- cacheValue . parseError = 'Malformed payload.' ;
193- if ( errors ) cacheValue . graphQLErrors = errors ;
194- if ( data ) cacheValue . data = data ;
195- } ,
196- ( { message } ) => {
197- // JSON parse error.
198- cacheValue . parseError = message ;
199- }
200- ) ;
201- } ,
202- ( { message } ) => {
203- cacheValue . fetchError = message ;
204- }
205- )
206- . then ( ( ) => {
207- // If there’s earlier GraphQL operation(s) loading for the same cache
208- // key, wait for them to complete so the cache is updated and the
209- // `cache` events are emitted in the order the operations were
210- // initiated. This prevents a slow earlier operation from overwriting
211- // the cache from a faster later operation.
212- if ( this . operations [ cacheKey ] . length > 1 ) {
213- const operationIndex = this . operations [ cacheKey ] . indexOf (
214- cacheValuePromise
215- ) ;
216-
217- if ( operationIndex )
218- // There are earlier GraphQL operations.
219- return Promise . all (
220- // The earlier GraphQL operations.
221- this . operations [ cacheKey ] . slice ( 0 , operationIndex )
222- ) ;
223- }
224- } )
225- . then ( ( ) => {
226- // The cache value promise should resolve after the cache has been
227- // updated, it’s cleared from the map of loading GraphQL operations,
228- // and the `cache` event has been emitted (in that order).
229-
230- // Update the cache.
231- this . cache [ cacheKey ] = cacheValue ;
232-
233- // Clear this operation from the map of loading GraphQL operations.
234- this . operations [ cacheKey ] . splice (
235- // The `>>> 0` is a clever way to defend against `indexOf` returning
236- // `-1` if the cache value promise is not in the array for some
237- // unexpected reason. It leaves non-negative integers alone, but
238- // converts `-1` into the largest possible unsigned 32-bit integer,
239- // which happens to be the ECMAScript spec max length of an array;
240- // resulting in a harmless splice.
241- this . operations [ cacheKey ] . indexOf ( cacheValuePromise ) >>> 0 ,
242- 1
243- ) ;
244- if ( ! this . operations [ cacheKey ] . length ) delete this . operations [ cacheKey ] ;
245-
246- // Emit the `cache` event.
247- this . emit ( 'cache' , {
248- cacheKey,
249- cacheValue,
250-
251- // May be undefined if there was a fetch error.
252- response : fetchResponse ,
253- } ) ;
254-
255- return cacheValue ;
256- } ) ;
257-
258- if ( ! this . operations [ cacheKey ] ) this . operations [ cacheKey ] = [ ] ;
259- this . operations [ cacheKey ] . push ( cacheValuePromise ) ;
260-
261- // Emit the `fetch` event after the cache value promise has been added to
262- // the map of loading GraphQL operations.
263- this . emit ( 'fetch' , { cacheKey, cacheValuePromise } ) ;
264-
265- return cacheValuePromise ;
266- } ;
267-
268157 /**
269158 * Loads a GraphQL operation, visible in
270159 * [GraphQL operations]{@link GraphQL#operations}. Emits a
@@ -300,10 +189,133 @@ module.exports = class GraphQL {
300189 'operate() options “reloadOnLoad” and “resetOnLoad” can’t both be true.'
301190 ) ;
302191
192+ // The `fetch` global not being defined correctly results in a `fetchError`
193+ // within the cache value; not the `operate` method throwing an error.
194+ const fetcher =
195+ typeof fetch === 'function'
196+ ? fetch
197+ : ( ) =>
198+ Promise . reject (
199+ new TypeError ( 'Global fetch API or polyfill unavailable.' )
200+ ) ;
201+
202+ // Create the fetch options.
303203 const fetchOptions = graphqlFetchOptions ( operation ) ;
304204 if ( fetchOptionsOverride ) fetchOptionsOverride ( fetchOptions ) ;
205+ const { url, ...options } = fetchOptions ;
206+
207+ // Create the cache key.
305208 const cacheKey = cacheKeyCreator ( fetchOptions ) ;
306- const cacheValuePromise = this . fetch ( fetchOptions , cacheKey ) ;
209+
210+ // The `operations` property must be updated for sync code following this
211+ // `operate` method call, as well as async code using `cacheValuePromise`.
212+ let resolveOperationsUpdated ;
213+ const operationsUpdatedPromise = new Promise ( ( resolve ) => {
214+ resolveOperationsUpdated = resolve ;
215+ } ) ;
216+
217+ // Start the fetch sync within the `operate` method rather than within the
218+ // `cacheValuePromise` chain to ensure fetches are sent in the order of
219+ // multiple `operate` method calls within sync code.
220+ const responsePromise = fetcher ( url , options ) ;
221+
222+ let fetchResponse ;
223+
224+ const cacheValue = { } ;
225+ const cacheValuePromise = operationsUpdatedPromise . then ( ( ) =>
226+ responsePromise
227+ . then (
228+ ( response ) => {
229+ fetchResponse = response ;
230+
231+ if ( ! response . ok )
232+ cacheValue . httpError = {
233+ status : response . status ,
234+ statusText : response . statusText ,
235+ } ;
236+
237+ return response . json ( ) . then (
238+ ( { errors, data } ) => {
239+ // JSON parse ok.
240+ if ( ! errors && ! data )
241+ cacheValue . parseError = 'Malformed payload.' ;
242+ if ( errors ) cacheValue . graphQLErrors = errors ;
243+ if ( data ) cacheValue . data = data ;
244+ } ,
245+ ( { message } ) => {
246+ // JSON parse error.
247+ cacheValue . parseError = message ;
248+ }
249+ ) ;
250+ } ,
251+ ( { message } ) => {
252+ cacheValue . fetchError = message ;
253+ }
254+ )
255+ . then ( ( ) => {
256+ // If there’s earlier GraphQL operation(s) loading for the same cache
257+ // key, wait for them to complete so the cache is updated and the
258+ // `cache` events are emitted in the order the operations were
259+ // initiated. This prevents a slow earlier operation from overwriting
260+ // the cache from a faster later operation.
261+ if ( this . operations [ cacheKey ] . length > 1 ) {
262+ const operationIndex = this . operations [ cacheKey ] . indexOf (
263+ cacheValuePromise
264+ ) ;
265+
266+ if ( operationIndex )
267+ // There are earlier GraphQL operations.
268+ return Promise . all (
269+ // The earlier GraphQL operations.
270+ this . operations [ cacheKey ] . slice ( 0 , operationIndex )
271+ ) ;
272+ }
273+ } )
274+ . then ( ( ) => {
275+ // The cache value promise should resolve after the cache has been
276+ // updated, it’s cleared from the map of loading GraphQL operations,
277+ // and the `cache` event has been emitted (in that order).
278+
279+ // Update the cache.
280+ this . cache [ cacheKey ] = cacheValue ;
281+
282+ // Clear this operation from the map of loading GraphQL operations.
283+ this . operations [ cacheKey ] . splice (
284+ // The `>>> 0` is a clever way to defend against `indexOf` returning
285+ // `-1` if the cache value promise is not in the array for some
286+ // unexpected reason. It leaves non-negative integers alone, but
287+ // converts `-1` into the largest possible unsigned 32-bit integer,
288+ // which happens to be the ECMAScript spec max length of an array;
289+ // resulting in a harmless splice.
290+ this . operations [ cacheKey ] . indexOf ( cacheValuePromise ) >>> 0 ,
291+ 1
292+ ) ;
293+
294+ // If there are no more operations loading for this cache key, delete
295+ // the empty array from the `operations` property.
296+ if ( ! this . operations [ cacheKey ] . length )
297+ delete this . operations [ cacheKey ] ;
298+
299+ // Emit the `cache` event.
300+ this . emit ( 'cache' , {
301+ cacheKey,
302+ cacheValue,
303+
304+ // May be undefined if there was a fetch error.
305+ response : fetchResponse ,
306+ } ) ;
307+
308+ return cacheValue ;
309+ } )
310+ ) ;
311+
312+ // Add this operation to the map of loading GraphQL operations.
313+ if ( ! this . operations [ cacheKey ] ) this . operations [ cacheKey ] = [ ] ;
314+ this . operations [ cacheKey ] . push ( cacheValuePromise ) ;
315+ resolveOperationsUpdated ( ) ;
316+
317+ // Emit the `fetch` event after the `operations` property has been updated.
318+ this . emit ( 'fetch' , { cacheKey, cacheValuePromise } ) ;
307319
308320 // A reload or reset happens after the cache is updated as a side effect.
309321 if ( reloadOnLoad )
0 commit comments