UNPKG

libzotero

Version:
342 lines (304 loc) 10.2 kB
'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var log = require('./Log.js').Logger('libZotero:FetchNet', 3); var Deferred = function Deferred() { var d = this; d.promise = new Promise(function (resolve, reject) { d.resolve = resolve; d.reject = reject; }); }; //var Deferred = require('deferred'); /* * Make concurrent and sequential network requests, respecting backoff/retry-after * headers, and keeping concurrent requests below a certain limit. * * Push onto the queue individual or arrays of requestConfig objects * If there is room for requests and we are not currently backing off: * start a sequential series, or individual request * When any request or set of requests finishes, we preprocess the response, * looking for backoff/retry-after to obey, and putting sequential responses * into an array. We then trigger the next waiting request. * */ var Net = function Net() { this.deferredQueue = []; this.numRunning = 0; this.numConcurrent = 3; this.backingOff = false; }; Net.prototype.queueDeferred = function () { var net = this; var d = new Deferred(); net.deferredQueue.push(d); return d.promise; }; //add a request to the end of the queue, so that previously queue requests run first //if requestObject is an array of requests, run them sequentially Net.prototype.queueRequest = function (requestObject) { log.debug('Zotero.Net.queueRequest', 3); var net = this; var resultPromise; if (Array.isArray(requestObject)) { resultPromise = net.queueDeferred().then(function () { log.debug('running sequential after queued deferred resolved', 4); return net.runSequential(requestObject); }).then(function (response) { net.queuedRequestDone(); return response; }, function (response) { net.queuedRequestDone(); throw response; }); } else { resultPromise = net.queueDeferred().then(function () { log.debug('running concurrent after queued deferred resolved', 4); return net.runConcurrent(requestObject); }).then(function (response) { net.queuedRequestDone(); return response; }, function (response) { net.queuedRequestDone(); throw response; }); } net.runNext(); return resultPromise; }; //run a request without waiting for any other requests to complete Net.prototype.runConcurrent = function (requestObject) { log.debug('Zotero.Net.runConcurrent', 3); return this.ajaxRequest(requestObject).then(function (response) { log.debug('done with runConcurrent request', 3); return response; }); }; //run the set of requests serially //chaining each request onto the .then of the previous one, after //adding the previous response to a responses array that will be //returned via promise to the caller when all requests are complete Net.prototype.runSequential = function (requestObjects) { log.debug('Zotero.Net.runSequential', 3); var net = this; var responses = []; var seqPromise = Promise.resolve(); var _loop = function _loop() { var requestObject = requestObjects[i]; seqPromise = seqPromise.then(function () { var p = net.ajaxRequest(requestObject).then(function (response) { log.debug('pushing sequential response into result array', 3); responses.push(response); }, function (response) { //return error responses too responses.push(response); }); return p; }); }; for (var i = 0; i < requestObjects.length; i++) { _loop(); } return seqPromise.then(function () { log.debug('done with sequential aggregator promise - returning responses', 4); return responses; }); }; //when one concurrent call, or a sequential series finishes, subtract it from the running //count and run the next if there is something waiting to be run Net.prototype.individualRequestDone = function (response) { log.debug('Zotero.Net.individualRequestDone', 3); var net = this; //check if we need to back off before making more requests var wait = net.checkDelay(response); if (wait > 0) { var waitms = wait * 1000; net.backingOff = true; var waitExpiration = Date.now() + waitms; if (waitExpiration > net.waitingExpires) { net.waitingExpires = waitExpiration; } setTimeout(net.runNext, waitms); } return response; }; Net.prototype.queuedRequestDone = function () { log.debug('queuedRequestDone', 3); var net = this; net.numRunning--; net.runNext(); }; Net.prototype.runNext = function () { log.debug('Zotero.Net.runNext', 4); var net = this; var nowms = Date.now(); //check if we're backing off and need to remain backing off, //or if we should now continue if (net.backingOff && net.waitingExpires > nowms - 100) { log.debug('currently backing off', 3); var waitms = net.waitingExpires - nowms; setTimeout(net.runNext, waitms); return; } else if (net.backingOff && net.waitingExpires <= nowms - 100) { net.backingOff = false; } //continue making requests up to the concurrent limit log.debug(net.numRunning + '/' + net.numConcurrent + ' Running. ' + net.deferredQueue.length + ' queued.', 4); while (net.deferredQueue.length > 0 && net.numRunning < net.numConcurrent) { net.numRunning++; var nextD = net.deferredQueue.shift(); nextD.resolve(); log.debug(net.numRunning + '/' + net.numConcurrent + ' Running. ' + net.deferredQueue.length + ' queued.', 4); } }; Net.prototype.checkDelay = function (response) { log.debug('Zotero.Net.checkDelay', 4); var net = this; var wait = 0; if (Array.isArray(response)) { for (var i = 0; i < response.length; i++) { var iwait = net.checkDelay(response[i]); if (iwait > wait) { wait = iwait; } } } else { if (response.status == 429) { wait = response.retryAfter; } else if (response.backoff) { wait = response.backoff; } } return wait; }; //perform a network request defined by requestConfig //convert the Response into a Zotero.ApiResponse, and attach the passed in //success/failure handlers to the promise chain before returning (or default error logger //if no failure handler is defined) Net.prototype.ajaxRequest = function (requestConfig) { log.debug('Zotero.Net.ajaxRequest', 3); var net = this; var defaultConfig = { type: 'GET', headers: { 'Zotero-API-Version': Zotero.config.apiVersion, 'Content-Type': 'application/json' }, success: function success(response) { return response; }, error: function error(response) { if (!response instanceof Zotero.ApiResponse) { log.error('Response is not a Zotero.ApiResponse: ' + response); } else if (response.rawResponse) { log.error('ajaxRequest rejected:' + response.rawResponse.status + ' - ' + response.rawResponse.statusText); } else { log.error('ajaxRequest rejected: No rawResponse set. (likely network error)'); log.error(response.error); } throw response; } //cache:false }; var headers = Z.extend({}, defaultConfig.headers, requestConfig.headers); if (requestConfig.key) { headers = Z.extend(headers, { 'Zotero-API-Key': requestConfig.key }); delete requestConfig.key; } var config = Z.extend({}, defaultConfig, requestConfig); config.headers = headers; if (_typeof(config.url) == 'object') { config.url = Zotero.ajax.apiRequestString(config.url); } config.url = Zotero.ajax.proxyWrapper(config.url, config.type); if (!config.url) { throw 'No url specified in Zotero.Net.ajaxRequest'; } log.debug('AJAX config', 4); log.debug(config, 4); var handleSuccessCallback = function handleSuccessCallback(response) { return new Promise(function (resolve) { if (config.success) { var maybePromise = config.success(response); if (maybePromise && 'then' in maybePromise) { maybePromise.then(function () { resolve(); }); } else { resolve(); } } else { resolve(); } }); }; var ajaxpromise = new Promise(function (resolve, reject) { net.ajax(config).then(function (response) { var ar = new Zotero.ApiResponse(response); if ('processData' in config && config.processData === false) { handleSuccessCallback(response).then(function () { return resolve(response); }); } else { response.json().then(function (data) { ar.data = data; handleSuccessCallback(ar).then(function () { return resolve(ar); }); }, function (err) { log.error(err); ar.isError = true; ar.error = err; reject(ar); //reject promise on malformed json }); } }, function (response) { var ar; if (response instanceof Error) { ar = new Zotero.ApiResponse(); ar.isError = true; ar.error = response; } else { ar = new Zotero.ApiResponse(response); } resolve(ar); }); }).then(net.individualRequestDone.bind(net)).then(function (response) { //now that we're done handling, reject if (response.isError) { //re-throw ApiResponse that was a rejection throw response; } return response; }).catch(config.error); return ajaxpromise; }; //perform a network request defined by config, and return a promise for a Response //resolve with a successful status (200-300) reject, but with the same Response object otherwise Net.prototype.ajax = function (config) { config = Zotero.extend({ type: 'GET' }, config); var headersInit = config.headers || {}; var headers = new Headers(headersInit); var request = new Request(config.url, { method: config.type, headers: headers, mode: 'cors', body: config.data }); return fetch(request).then(function (response) { log.debug('fetch done', 4); log.debug(request, 4); if (response.status >= 200 && response.status < 300) { log.debug('200-300 response: resolving Net.ajax promise', 3); // Performs the function "resolve" when this.status is equal to 2xx return response; } else { log.debug('not 200-300 response: rejecting Net.ajax promise', 3); // Performs the function "reject" when this.status is different than 2xx throw response; } }, function (err) { log.error(err); throw err; }); }; module.exports = new Net();