Files
personal_site/frontend/node_modules/relay-runtime/lib/query/fetchQueryInternal.js
T

288 lines
9.0 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
// flowlint ambiguous-object-type:error
'use strict';
var Observable = require('../network/RelayObservable');
var RelayReplaySubject = require('../util/RelayReplaySubject');
var invariant = require('invariant');
var WEAKMAP_SUPPORTED = typeof WeakMap === 'function';
var requestCachesByEnvironment = WEAKMAP_SUPPORTED ? new WeakMap() : new Map();
/**
* Fetches the given query and variables on the provided environment,
* and de-dupes identical in-flight requests.
*
* Observing a request:
* ====================
* fetchQuery returns an Observable which you can call .subscribe()
* on. subscribe() takes an Observer, which you can provide to
* observe network events:
*
* ```
* fetchQuery(environment, query, variables).subscribe({
* // Called when network requests starts
* start: (subscription) => {},
*
* // Called after a payload is received and written to the local store
* next: (payload) => {},
*
* // Called when network requests errors
* error: (error) => {},
*
* // Called when network requests fully completes
* complete: () => {},
*
* // Called when network request is unsubscribed
* unsubscribe: (subscription) => {},
* });
* ```
*
* In-flight request de-duping:
* ============================
* By default, calling fetchQuery multiple times with the same
* environment, query and variables will not initiate a new request if a request
* for those same parameters is already in flight.
*
* A request is marked in-flight from the moment it starts until the moment it
* fully completes, regardless of error or successful completion.
*
* NOTE: If the request completes _synchronously_, calling fetchQuery
* a second time with the same arguments in the same tick will _NOT_ de-dupe
* the request given that it will no longer be in-flight.
*
*
* Data Retention:
* ===============
* This function will not retain any query data outside the scope of the
* request, which means it is not guaranteed that it won't be garbage
* collected after the request completes.
* If you need to retain data, you can do so manually with environment.retain().
*
* Cancelling requests:
* ====================
* If the subscription returned by subscribe is called while the
* request is in-flight, the request will be cancelled.
*
* ```
* const subscription = fetchQuery(...).subscribe(...);
*
* // This will cancel the request if it is in-flight.
* subscription.unsubscribe();
* ```
*/
function fetchQuery(environment, operation) {
return fetchQueryDeduped(environment, operation.request.identifier, function () {
return environment.execute({
operation: operation
});
});
}
/**
* Low-level implementation details of `fetchQuery`.
*
* `fetchQueryDeduped` can also be used to share a single cache for
* requests that aren't using `fetchQuery` directly (e.g. because they don't
* have an `OperationDescriptor` when they are called).
*/
function fetchQueryDeduped(environment, identifier, fetchFn) {
return Observable.create(function (sink) {
var requestCache = getRequestCache(environment);
var cachedRequest = requestCache.get(identifier);
if (!cachedRequest) {
fetchFn()["finally"](function () {
return requestCache["delete"](identifier);
}).subscribe({
start: function start(subscription) {
cachedRequest = {
identifier: identifier,
subject: new RelayReplaySubject(),
subjectForInFlightStatus: new RelayReplaySubject(),
subscription: subscription
};
requestCache.set(identifier, cachedRequest);
},
next: function next(response) {
var cachedReq = getCachedRequest(requestCache, identifier);
cachedReq.subject.next(response);
cachedReq.subjectForInFlightStatus.next(response);
},
error: function error(_error) {
var cachedReq = getCachedRequest(requestCache, identifier);
cachedReq.subject.error(_error);
cachedReq.subjectForInFlightStatus.error(_error);
},
complete: function complete() {
var cachedReq = getCachedRequest(requestCache, identifier);
cachedReq.subject.complete();
cachedReq.subjectForInFlightStatus.complete();
},
unsubscribe: function unsubscribe(subscription) {
var cachedReq = getCachedRequest(requestCache, identifier);
cachedReq.subject.unsubscribe();
cachedReq.subjectForInFlightStatus.unsubscribe();
}
});
}
!(cachedRequest != null) ? process.env.NODE_ENV !== "production" ? invariant(false, '[fetchQueryInternal] fetchQueryDeduped: Expected `start` to be ' + 'called synchronously') : invariant(false) : void 0;
return getObservableForCachedRequest(requestCache, cachedRequest).subscribe(sink);
});
}
/**
* @private
*/
function getObservableForCachedRequest(requestCache, cachedRequest) {
return Observable.create(function (sink) {
var subscription = cachedRequest.subject.subscribe(sink);
return function () {
subscription.unsubscribe();
var cachedRequestInstance = requestCache.get(cachedRequest.identifier);
if (cachedRequestInstance) {
var requestSubscription = cachedRequestInstance.subscription;
if (requestSubscription != null && cachedRequestInstance.subject.getObserverCount() === 0) {
requestSubscription.unsubscribe();
requestCache["delete"](cachedRequest.identifier);
}
}
};
});
}
/**
* @private
*/
function getActiveStatusObservableForCachedRequest(environment, requestCache, cachedRequest) {
return Observable.create(function (sink) {
var subscription = cachedRequest.subjectForInFlightStatus.subscribe({
error: sink.error,
next: function next(response) {
if (!environment.isRequestActive(cachedRequest.identifier)) {
sink.complete();
return;
}
sink.next();
},
complete: sink.complete,
unsubscribe: sink.complete
});
return function () {
subscription.unsubscribe();
};
});
}
/**
* If a request is active for the given query, variables and environment,
* this function will return a Promise that will resolve when that request
* stops being active (receives a final payload), and the data has been saved
* to the store.
* If no request is active, null will be returned
*/
function getPromiseForActiveRequest(environment, request) {
var requestCache = getRequestCache(environment);
var cachedRequest = requestCache.get(request.identifier);
if (!cachedRequest) {
return null;
}
if (!environment.isRequestActive(cachedRequest.identifier)) {
return null;
}
return new Promise(function (resolve, reject) {
var resolveOnNext = false;
getActiveStatusObservableForCachedRequest(environment, requestCache, cachedRequest).subscribe({
complete: resolve,
error: reject,
next: function next(response) {
/*
* The underlying `RelayReplaySubject` will synchronously replay events
* as soon as we subscribe, but since we want the *next* asynchronous
* one, we'll ignore them until the replay finishes.
*/
if (resolveOnNext) {
resolve(response);
}
}
});
resolveOnNext = true;
});
}
/**
* If there is a pending request for the given query, returns an Observable of
* *all* its responses. Existing responses are published synchronously and
* subsequent responses are published asynchronously. Returns null if there is
* no pending request. This is similar to fetchQuery() except that it will not
* issue a fetch if there isn't already one pending.
*/
function getObservableForActiveRequest(environment, request) {
var requestCache = getRequestCache(environment);
var cachedRequest = requestCache.get(request.identifier);
if (!cachedRequest) {
return null;
}
if (!environment.isRequestActive(cachedRequest.identifier)) {
return null;
}
return getActiveStatusObservableForCachedRequest(environment, requestCache, cachedRequest);
}
/**
* @private
*/
function getRequestCache(environment) {
var cached = requestCachesByEnvironment.get(environment);
if (cached != null) {
return cached;
}
var requestCache = new Map();
requestCachesByEnvironment.set(environment, requestCache);
return requestCache;
}
/**
* @private
*/
function getCachedRequest(requestCache, identifier) {
var cached = requestCache.get(identifier);
!(cached != null) ? process.env.NODE_ENV !== "production" ? invariant(false, '[fetchQueryInternal] getCachedRequest: Expected request to be cached') : invariant(false) : void 0;
return cached;
}
module.exports = {
fetchQuery: fetchQuery,
fetchQueryDeduped: fetchQueryDeduped,
getPromiseForActiveRequest: getPromiseForActiveRequest,
getObservableForActiveRequest: getObservableForActiveRequest
};