UNPKG

okanjo

Version:

Integrate your application with the Okanjo API.

285 lines (246 loc) 8.48 kB
/* * Date: 10/20/16 4:30 PM * * ---- * * (c) Okanjo Partners Inc * https://okanjo.com * support@okanjo.com * * https://github.com/okanjo/okanjo-nodejs * * ---- * * TL;DR? see: http://www.tldrlegal.com/license/mit-license * * The MIT License (MIT) * Copyright (c) 2013 Okanjo Partners Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ var util = require('util'), timers = require('timers'), setImmediate = global.setImmediate || /* istanbul ignore next */ timers.setImmediate, Provider = require('../provider'); /** * Request handler * @param {Client} [client] * @constructor */ function FetchProvider(client) { Provider.call(this, client); /** * Where to send requests to * @type {string} */ this.rpcHost = client.config.rpcHost || "/rpc"; /** * What method is the RPC router expecting * @type {string} */ this.rpcMethod = client.config.rpcMethod || 'POST'; /** * How many requests can be run in parallel at any given time. Additional requests will be queued. * @type {*|number} */ this.maxConcurrency = client.config.maxConcurrency || 5; /** * Active request counter * @type {number} * @private */ this._activeRequests = 0; /** * Request queue * @type {Array} * @private */ this._requestQueue = []; this._handleRequest = this._handleRequest.bind(this); this._completeRequest = this._completeRequest.bind(this); this._runQueueIfAble = this._runQueueIfAble.bind(this); } util.inherits(FetchProvider, Provider); /** * Returns whether the request pipeline is full (true) or not (false) * @returns {boolean} */ FetchProvider.prototype.areRequestsSaturated = function() { return this._activeRequests >= this.maxConcurrency; }; /** * Queues a new request. Will run it if able * @param query * @param callback * @returns {Promise<any>} * @private */ FetchProvider.prototype._queueRequest = function(query, callback) { var queue = this._requestQueue; var _runQueueIfAble = this._runQueueIfAble; return new Promise(function (resolve, reject) { queue.push({ query: query, callback: callback, resolve: resolve, reject: reject }); _runQueueIfAble(); }); }; /** * Runs the next available item in the queue if concurrency not met * @private */ FetchProvider.prototype._runQueueIfAble = function() { var _handleRequest = this._handleRequest; // Run any queued requests if able if (this._requestQueue.length > 0 && !this.areRequestsSaturated()) { // Bump request counter this._activeRequests++; // Take the one off the top var queuedRequest = this._requestQueue.shift(); // Execute return setImmediate(function () { _handleRequest(queuedRequest); }); } }; /** * Hook for when a request completes. Will try to run the next task in the queue if able * @private */ FetchProvider.prototype._completeRequest = function() { // Decrement request counter this._activeRequests--; // Handle the next available request this._runQueueIfAble(); }; /** * Executes the query * @param {Query} query - The query to execute * @param callback – Callback to fire when request is completed * @returns {Promise<any>} * @abstract */ FetchProvider.prototype.execute = function(query, callback) { // Queue this request (returns a promise, resolved when the req completes) return this._queueRequest(query, callback); }; /* istanbul ignore next: taken from MDN, like it's the gospel */ /** * Object.assign polyfill from MDN * @param target * @return {any} */ function assign(target/*, varArgs*/) { // .length of function is 2 'use strict'; if (target === null || target === undefined) { throw new TypeError('Cannot convert undefined or null to object'); } var to = Object(target); for (var index = 1; index < arguments.length; index++) { var nextSource = arguments[index]; if (nextSource !== null && nextSource !== undefined) { for (var nextKey in nextSource) { // Avoid bugs when hasOwnProperty is shadowed if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; } } } } return to; } /** * Handles the request like execute() used to do * @param queuedRequest * @return {Promise<any>} * @private */ FetchProvider.prototype._handleRequest = function(queuedRequest) { // shallow copy the query so we can safely mutate it var payload = assign({}, queuedRequest.query); var options = payload.options; delete payload.options; var headers = assign({}, queuedRequest.query.headers); headers['Accept'] = 'application/json'; headers['Content-Type'] = 'application/json; charset=utf-8'; var req = { method: this.rpcMethod, body: JSON.stringify(queuedRequest.query), credentials: 'same-origin', // preserve authentication headers: headers }; // Hook for making fetch abortable, see: https://developers.google.com/web/updates/2017/09/abortable-fetch if (options.signal) req.signal = options.signal; var _completeRequest = this._completeRequest; return fetch(this.rpcHost + '?a=' + encodeURIComponent(queuedRequest.query.action), req) .then(function(res) { return res.json(); }) .then(function(res) { if (res.error) { // Error response from API return Promise.reject(res); } else { // Browserify should polyfill setImmediate if (queuedRequest.callback) { return setImmediate(function() { _completeRequest(); queuedRequest.callback(null, res); }); } _completeRequest(); queuedRequest.resolve(res); // this goes back to caller return Promise.resolve(res); // internally resolve } }) .catch(function(err) { if (!err || !err.statusCode) { err = { statusCode: 503, error: (err instanceof Error ? err.message : /* istanbul ignore next: not worth testing err vs stats */ err), message: "Something went wrong", attributes: { source: 'okanjo.providers.FetchProvider', wrappedError: err } }; } // Check for unauthorized hook case if (err.statusCode === 401) this._unauthorizedHook(err, queuedRequest.query); if (queuedRequest.callback) { return setImmediate(function() { _completeRequest(); queuedRequest.callback(err, null); }); } _completeRequest(); queuedRequest.reject(err); // this goes back to caller return Promise.resolve(err); // internally resolve }.bind(this)) ; }; /** * @callback requestCallback * @param {object|null} error * @param {object|null} response */ module.exports = FetchProvider;