react-relay
Version:
A framework for building data-driven React applications.
283 lines (227 loc) • 9.45 kB
JavaScript
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule RelayPendingQueryTracker
* @typechecks
*
*/
'use strict';
var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default'];
var _Object$keys = require('babel-runtime/core-js/object/keys')['default'];
var Deferred = require('fbjs/lib/Deferred');
var DliteFetchModeConstants = require('./DliteFetchModeConstants');
var GraphQLDeferredQueryTracker = require('./GraphQLDeferredQueryTracker');
var Promise = require('fbjs/lib/Promise');
var PromiseMap = require('fbjs/lib/PromiseMap');
var RelayTaskScheduler = require('./RelayTaskScheduler');
var containsRelayQueryRootCall = require('./containsRelayQueryRootCall');
var everyObject = require('fbjs/lib/everyObject');
var fetchRelayQuery = require('./fetchRelayQuery');
var invariant = require('fbjs/lib/invariant');
var subtractRelayQuery = require('./subtractRelayQuery');
var pendingFetchMap = {};
// Asynchronous mapping from preload query IDs to results.
var preloadQueryMap = new PromiseMap();
var PendingFetch = (function () {
function PendingFetch(_ref) {
var fetchMode = _ref.fetchMode;
var forceIndex = _ref.forceIndex;
var query = _ref.query;
var storeData = _ref.storeData;
_classCallCheck(this, PendingFetch);
var queryID = query.getID();
this._storeData = storeData;
this._query = query;
this._forceIndex = forceIndex;
this._resolvedSubtractedQuery = false;
this._resolvedDeferred = new Deferred();
this._dependents = [];
this._pendingDependencyMap = {};
var subtractedQuery;
if (fetchMode === DliteFetchModeConstants.FETCH_MODE_PRELOAD) {
subtractedQuery = query;
this._fetchSubtractedQueryPromise = preloadQueryMap.get(queryID);
} else {
subtractedQuery = this._subtractPending(query);
this._fetchSubtractedQueryPromise = subtractedQuery ? fetchRelayQuery(subtractedQuery) : Promise.resolve();
}
this._fetchedSubtractedQuery = !subtractedQuery;
this._errors = [];
if (subtractedQuery) {
pendingFetchMap[queryID] = {
fetch: this,
query: subtractedQuery
};
GraphQLDeferredQueryTracker.recordQuery(subtractedQuery);
this._fetchSubtractedQueryPromise.done(this._handleSubtractedQuerySuccess.bind(this, subtractedQuery), this._handleSubtractedQueryFailure.bind(this, subtractedQuery));
} else {
this._markSubtractedQueryAsResolved();
}
}
/**
* Subtracts all pending queries from the supplied `query` and returns the
* resulting difference. The difference can be null if the entire query is
* pending.
*
* If any pending queries were subtracted, they will be added as dependencies
* and the query will only resolve once the subtracted query and all
* dependencies have resolved.
*
* This, combined with our use of diff queries (see `diffRelayQuery`) means
* that we only go to the server for things that are not in (or not on their
* way to) the cache (`RelayRecordStore`).
*/
PendingFetch.prototype._subtractPending = function _subtractPending(query) {
var _this = this;
everyObject(pendingFetchMap, function (pending) {
// Stop if the entire query is subtracted.
if (!query) {
return false;
}
if (containsRelayQueryRootCall(pending.query, query)) {
var subtractedQuery = subtractRelayQuery(query, pending.query);
if (subtractedQuery !== query) {
query = subtractedQuery;
_this._addPendingDependency(pending.fetch);
}
}
return true;
});
return query;
};
PendingFetch.prototype._addPendingDependency = function _addPendingDependency(pendingFetch) {
var queryID = pendingFetch.getQuery().getID();
this._pendingDependencyMap[queryID] = pendingFetch;
pendingFetch._addDependent(this);
};
PendingFetch.prototype._addDependent = function _addDependent(pendingFetch) {
this._dependents.push(pendingFetch);
};
PendingFetch.prototype._handleSubtractedQuerySuccess = function _handleSubtractedQuerySuccess(subtractedQuery, result) {
var _this2 = this;
this._fetchedSubtractedQuery = true;
RelayTaskScheduler.await(function () {
var response = result.response;
!(response && typeof response === 'object') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayPendingQueryTracker: Expected response to be an object, got ' + '`%s`.', response ? typeof response : response) : invariant(false) : undefined;
_this2._storeData.handleQueryPayload(subtractedQuery, response, _this2._forceIndex);
GraphQLDeferredQueryTracker.resolveQuery(subtractedQuery, response, result.ref_params);
}).done(this._markSubtractedQueryAsResolved.bind(this), this._markAsRejected.bind(this));
};
PendingFetch.prototype._handleSubtractedQueryFailure = function _handleSubtractedQueryFailure(subtractedQuery, error) {
GraphQLDeferredQueryTracker.rejectQuery(subtractedQuery, error);
this._markAsRejected(error);
};
PendingFetch.prototype._markSubtractedQueryAsResolved = function _markSubtractedQueryAsResolved() {
var queryID = this.getQuery().getID();
delete pendingFetchMap[queryID];
this._resolvedSubtractedQuery = true;
this._updateResolvedDeferred();
this._dependents.forEach(function (dependent) {
return dependent._markDependencyAsResolved(queryID);
});
};
PendingFetch.prototype._markAsRejected = function _markAsRejected(error) {
var queryID = this.getQuery().getID();
delete pendingFetchMap[queryID];
console.error(error.message);
this._errors.push(error);
this._updateResolvedDeferred();
this._dependents.forEach(function (dependent) {
return dependent._markDependencyAsRejected(queryID, error);
});
};
PendingFetch.prototype._markDependencyAsResolved = function _markDependencyAsResolved(dependencyQueryID) {
delete this._pendingDependencyMap[dependencyQueryID];
this._updateResolvedDeferred();
};
PendingFetch.prototype._markDependencyAsRejected = function _markDependencyAsRejected(dependencyQueryID, error) {
delete this._pendingDependencyMap[dependencyQueryID];
this._errors.push(error);
this._updateResolvedDeferred();
// Dependencies further down the graph are either not affected or informed
// by `dependencyQueryID`.
};
PendingFetch.prototype._updateResolvedDeferred = function _updateResolvedDeferred() {
if (this._isSettled() && !this._resolvedDeferred.isSettled()) {
if (this._errors.length) {
this._resolvedDeferred.reject(this._errors[0]);
} else {
this._resolvedDeferred.resolve(undefined);
}
}
};
PendingFetch.prototype._isSettled = function _isSettled() {
return this._errors.length > 0 || this._resolvedSubtractedQuery && !hasItems(this._pendingDependencyMap);
};
PendingFetch.prototype.getQuery = function getQuery() {
return this._query;
};
PendingFetch.prototype.getResolvedPromise = function getResolvedPromise() {
return this._resolvedDeferred.getPromise();
};
/**
* A pending query is resolvable if it is already resolved or will be resolved
* imminently (i.e. its subtracted query and the subtracted queries of all its
* pending dependencies have been fetched).
*/
PendingFetch.prototype.isResolvable = function isResolvable() {
if (this._fetchedSubtractedQuery) {
return everyObject(this._pendingDependencyMap, function (pendingDependency) {
return pendingDependency._fetchedSubtractedQuery;
});
// Pending dependencies further down the graph either don't affect the
// result or are already in `_pendingDependencyMap`.
}
return false;
};
return PendingFetch;
})();
function hasItems(map) {
return !!_Object$keys(map).length;
}
/**
* @internal
*
* Tracks pending (in-flight) queries.
*
* In order to send minimal queries and avoid re-retrieving data,
* `RelayPendingQueryTracker` maintains a registry of pending queries, and
* "subtracts" those from any new queries that callers enqueue.
*/
var RelayPendingQueryTracker = {
/**
* Used by `GraphQLQueryRunner` to enqueue new queries.
*/
add: function add(params) {
return new PendingFetch(params);
},
hasPendingQueries: function hasPendingQueries() {
return hasItems(pendingFetchMap);
},
/**
* Clears all pending query tracking. Does not cancel the queries themselves.
*/
resetPending: function resetPending() {
pendingFetchMap = {};
GraphQLDeferredQueryTracker.reset();
},
resolvePreloadQuery: function resolvePreloadQuery(queryID, result) {
preloadQueryMap.resolveKey(queryID, result);
},
rejectPreloadQuery: function rejectPreloadQuery(queryID, error) {
preloadQueryMap.rejectKey(queryID, error);
},
// TODO: Use `export type`.
PendingFetch: PendingFetch
};
module.exports = RelayPendingQueryTracker;
/**
* Error(s) in fetching/handleUpdate-ing its or one of its pending
* dependency's subtracted query. There may be more than one error. However,
* `_resolvedDeferred` is rejected with the earliest encountered error.
*/