UNPKG

falcor

Version:

A JavaScript library for efficient data fetching.

283 lines (249 loc) 11.2 kB
var complement = require("./complement"); var flushGetRequest = require("./flushGetRequest"); var incrementVersion = require("../support/incrementVersion"); var currentCacheVersion = require("../support/currentCacheVersion"); var REQUEST_ID = 0; var GetRequestType = require("./RequestTypes").GetRequest; var setJSONGraphs = require("./../set/setJSONGraphs"); var setPathValues = require("./../set/setPathValues"); var $error = require("./../types/error"); var emptyArray = []; var InvalidSourceError = require("./../errors/InvalidSourceError"); /** * Creates a new GetRequest. This GetRequest takes a scheduler and * the request queue. Once the scheduler fires, all batched requests * will be sent to the server. Upon request completion, the data is * merged back into the cache and all callbacks are notified. * * @param {Scheduler} scheduler - * @param {RequestQueueV2} requestQueue - * @param {number} attemptCount */ var GetRequestV2 = function(scheduler, requestQueue, attemptCount) { this.sent = false; this.scheduled = false; this.requestQueue = requestQueue; this.id = ++REQUEST_ID; this.type = GetRequestType; this._scheduler = scheduler; this._attemptCount = attemptCount; this._pathMap = {}; this._optimizedPaths = []; this._requestedPaths = []; this._callbacks = []; this._count = 0; this._disposable = null; this._collapsed = null; this._disposed = false; }; GetRequestV2.prototype = { /** * batches the paths that are passed in. Once the request is complete, * all callbacks will be called and the request will be removed from * parent queue. * @param {Array} requestedPaths - * @param {Array} optimizedPaths - * @param {Function} callback - */ batch: function(requestedPaths, optimizedPaths, callback) { var self = this; var batchedOptPathSets = self._optimizedPaths; var batchedReqPathSets = self._requestedPaths; var batchedCallbacks = self._callbacks; var batchIx = batchedOptPathSets.length; // If its not sent, simply add it to the requested paths // and callbacks. batchedOptPathSets[batchIx] = optimizedPaths; batchedReqPathSets[batchIx] = requestedPaths; batchedCallbacks[batchIx] = callback; ++self._count; // If it has not been scheduled, then schedule the action if (!self.scheduled) { self.scheduled = true; var flushedDisposable; var scheduleDisposable = self._scheduler.schedule(function() { flushedDisposable = flushGetRequest(self, batchedOptPathSets, function(err, data) { var i, fn, len; var model = self.requestQueue.model; self.requestQueue.removeRequest(self); self._disposed = true; if (model._treatDataSourceErrorsAsJSONGraphErrors ? err instanceof InvalidSourceError : !!err) { for (i = 0, len = batchedCallbacks.length; i < len; ++i) { fn = batchedCallbacks[i]; if (fn) { fn(err); } } return; } // If there is at least one callback remaining, then // callback the callbacks. if (self._count) { // currentVersion will get added to each inserted // node as node.$_version inside of self._merge. // // atom values just downloaded with $expires: 0 // (now-expired) will get assigned $_version equal // to currentVersion, and checkCacheAndReport will // later consider those nodes to not have expired // for the duration of current event loop tick // // we unset currentCacheVersion after all callbacks // have been called, to ensure that only these // particular callbacks and any synchronous model.get // callbacks inside of these, get the now-expired // values var currentVersion = incrementVersion.getCurrentVersion(); currentCacheVersion.setVersion(currentVersion); var mergeContext = { hasInvalidatedResult: false }; var pathsErr = model._useServerPaths && data && data.paths === undefined ? new Error("Server responses must include a 'paths' field when Model._useServerPaths === true") : undefined; if (!pathsErr) { self._merge(batchedReqPathSets, err, data, mergeContext); } // Call the callbacks. The first one inserts all // the data so that the rest do not have consider // if their data is present or not. for (i = 0, len = batchedCallbacks.length; i < len; ++i) { fn = batchedCallbacks[i]; if (fn) { fn(pathsErr || err, data, mergeContext.hasInvalidatedResult); } } currentCacheVersion.setVersion(null); } }); self._disposable = flushedDisposable; }); // If the scheduler is sync then `flushedDisposable` will be // defined, and we want to use it, because that's what aborts an // in-flight XHR request, for example. // But if the scheduler is async, then `flushedDisposable` won't be // defined yet, and so we must use the scheduler's disposable until // `flushedDisposable` is defined. Since we want to still use // `flushedDisposable` once it is defined (to be able to abort in- // flight XHR requests), hence the reassignment of `_disposable` // above. self._disposable = flushedDisposable || scheduleDisposable; } // Disposes this batched request. This does not mean that the // entire request has been disposed, but just the local one, if all // requests are disposed, then the outer disposable will be removed. return createDisposable(self, batchIx); }, /** * Attempts to add paths to the outgoing request. If there are added * paths then the request callback will be added to the callback list. * Handles adding partial paths as well * * @returns {Array} - whether new requested paths were inserted in this * request, the remaining paths that could not be added, * and disposable for the inserted requested paths. */ add: function(requested, optimized, callback) { // uses the length tree complement calculator. var self = this; var complementResult = complement(requested, optimized, self._pathMap); var inserted = false; var disposable = false; // If we found an intersection, then just add new callback // as one of the dependents of that request if (complementResult.intersection.length) { inserted = true; var batchIx = self._callbacks.length; self._callbacks[batchIx] = callback; self._requestedPaths[batchIx] = complementResult.intersection; self._optimizedPaths[batchIx] = []; ++self._count; disposable = createDisposable(self, batchIx); } return [inserted, complementResult.requestedComplement, complementResult.optimizedComplement, disposable]; }, /** * merges the response into the model"s cache. */ _merge: function(requested, err, data, mergeContext) { var self = this; var model = self.requestQueue.model; var modelRoot = model._root; var errorSelector = modelRoot.errorSelector; var comparator = modelRoot.comparator; var boundPath = model._path; model._path = emptyArray; // flatten all the requested paths, adds them to the var nextPaths = model._useServerPaths ? data.paths : flattenRequestedPaths(requested); // Insert errors in every requested position. if (err && model._treatDataSourceErrorsAsJSONGraphErrors) { var error = err; // Converts errors to objects, a more friendly storage // of errors. if (error instanceof Error) { error = { message: error.message }; } // Not all errors are value $types. if (!error.$type) { error = { $type: $error, value: error }; } var pathValues = nextPaths.map(function(x) { return { path: x, value: error }; }); setPathValues(model, pathValues, null, errorSelector, comparator, mergeContext); } // Insert the jsonGraph from the dataSource. else { setJSONGraphs(model, [{ paths: nextPaths, jsonGraph: data.jsonGraph }], null, errorSelector, comparator, mergeContext); } // return the model"s boundPath model._path = boundPath; } }; // Creates a more efficient closure of the things that are // needed. So the request and the batch index. Also prevents code // duplication. function createDisposable(request, batchIx) { var disposed = false; return function() { if (disposed || request._disposed) { return; } disposed = true; request._callbacks[batchIx] = null; request._optimizedPaths[batchIx] = []; request._requestedPaths[batchIx] = []; // If there are no more requests, then dispose all of the request. var count = --request._count; var disposable = request._disposable; if (count === 0) { // looking for unsubscribe here to support more data sources (Rx) if (disposable.unsubscribe) { disposable.unsubscribe(); } else { disposable.dispose(); } request.requestQueue.removeRequest(request); } }; } function flattenRequestedPaths(requested) { var out = []; var outLen = -1; for (var i = 0, len = requested.length; i < len; ++i) { var paths = requested[i]; for (var j = 0, innerLen = paths.length; j < innerLen; ++j) { out[++outLen] = paths[j]; } } return out; } module.exports = GetRequestV2;