axios-ex
Version:
A integration solution of axios for large enterprise projects
364 lines (358 loc) • 12.3 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var tslib = require('tslib');
var axios = require('axios');
var isRetryAllowed = require('is-retry-allowed');
var jsCool = require('js-cool');
var SAFE_HTTP_METHODS = ['get', 'head', 'options'];
var IDEMPOTENT_HTTP_METHODS = SAFE_HTTP_METHODS.concat(['put', 'delete']);
/**
* Get the default delay time in milliseconds
*
* @private
* @returns number - delay in milliseconds, always 0
*/
function noRetryDelay() {
return 0;
}
/**
* the config for retry when initialize and return
*
* @param config - AxiosExtendRequestOptions
* @return currentState
*/
function getCurrentState(config) {
var currentState = config['axios-extend'] || {};
currentState.retryCount = currentState.retryCount || 0;
config['axios-extend'] = currentState;
return currentState;
}
/**
* Get the request data
*
* @param config - AxiosExtendRequestOptions
* @param defaultOptions - AxiosExtendConfig
* @return options
*/
function getRequestOptions(config, defaultOptions) {
return Object.assign({}, defaultOptions, config['axios-extend']);
}
/**
* Clean up agent to prevent dead loops
*
* @param axios - any
* @param config - any
*/
function fixConfig(axios, config) {
if (axios.defaults.agent === config.agent) {
delete config.agent;
}
if (axios.defaults.httpAgent === config.httpAgent) {
delete config.httpAgent;
}
if (axios.defaults.httpsAgent === config.httpsAgent) {
delete config.httpsAgent;
}
}
/**
* @param error - Error
* @return boolean
*/
function isNetworkError(error) {
return !error.response && Boolean(error.code) &&
// Prevents retrying cancelled requests
error.code !== 'ECONNABORTED' &&
// Prevents retrying timed out requests
isRetryAllowed(error); // Prevents retrying unsafe errors
}
/**
* @param error - Error
* @return boolean
*/
function isSafeRequestError(error) {
// Cannot determine if the request can be retried
if (!error.config) return false;
return isRetryableError(error) && SAFE_HTTP_METHODS.includes(error.config.method);
}
/**
* @param error - Error
* @return boolean
*/
function isIdempotentRequestError(error) {
// Cannot determine if the request can be retried
if (!error.config) return false;
return isRetryableError(error) && IDEMPOTENT_HTTP_METHODS.includes(error.config.method);
}
/**
* @param error - Error
* @return boolean
*/
function isNetworkOrIdempotentRequestError(error) {
return isNetworkError(error) || isIdempotentRequestError(error);
}
/**
* @param retryNumber - default: 0
* @return delay milliseconds
*/
function exponentialDelay(retryNumber) {
if (retryNumber === void 0) {
retryNumber = 0;
}
var delay = Math.pow(2, retryNumber) * 1000;
var randomSum = delay * 0.5 * Math.random(); // 0-50% of the delay
return delay + randomSum;
}
/**
* @param error - Error
* @return boolean
*/
function isRetryableError(error) {
return error.code !== 'ECONNABORTED' && (!error.response || error.response.status >= 500 && error.response.status <= 599);
}
/**
* AxiosExtend class
*
* @return Promise
*/
var AxiosExtend = /** @class */function () {
function AxiosExtend(_a) {
var orderly = _a.orderly,
unique = _a.unique,
retries = _a.retries,
onCancel = _a.onCancel,
defaultOptions = tslib.__rest(_a, ["orderly", "unique", "retries", "onCancel"]);
this.axiosInstance = null;
this.waiting = {}; // Request Queue
this.unique = false; // Whether to cancel the previous similar requests, default: false
this.orderly = true; // Whether to return in order, default: true
this.onCancel = null; // Callback when request is cancelled
this.orderly = orderly !== null && orderly !== void 0 ? orderly : true;
this.unique = unique !== null && unique !== void 0 ? unique : false;
this.retries = retries !== null && retries !== void 0 ? retries : 0;
this.onCancel = onCancel !== null && onCancel !== void 0 ? onCancel : null;
// Initialization method
this.init(defaultOptions);
return this;
}
/**
* Initialization
*/
AxiosExtend.prototype.init = function (defaultOptions) {
var _this = this;
var setHeaders = defaultOptions.setHeaders,
onRequest = defaultOptions.onRequest,
onRequestError = defaultOptions.onRequestError,
onResponse = defaultOptions.onResponse,
onResponseError = defaultOptions.onResponseError,
onError = defaultOptions.onError,
options = tslib.__rest(defaultOptions, ["setHeaders", "onRequest", "onRequestError", "onResponse", "onResponseError", "onError"]);
if (!this.axiosInstance) this.axiosInstance = axios.create(options);
// Set request headers
setHeaders && setHeaders(axios);
// Adding a request interceptor
onRequest && axios.interceptors.request.use(function (config) {
var currentState = getCurrentState(config);
currentState.lastRequestTime = Date.now();
if (currentState.retryCount > 0) return config; // retry re-requests the interface without executing onRequest again
return onRequest(config, config.requestOptions);
}, function (err) {
onRequestError && onRequestError(err);
onError && onError(err);
return Promise.reject(err);
});
// Adding a response interceptor
onResponse && axios.interceptors.response.use(function (res) {
return onResponse(res, res.config.requestOptions);
}, function (err) {
var config = err.config;
// No request config
if (!config) {
onResponseError && onResponseError(err);
onError && onError(err);
return Promise.reject(err);
}
var _a = getRequestOptions(config, defaultOptions),
_b = _a.retries,
retries = _b === void 0 ? _this.retries : _b,
_c = _a.retryCondition,
retryCondition = _c === void 0 ? isNetworkOrIdempotentRequestError : _c,
_d = _a.retryDelay,
retryDelay = _d === void 0 ? noRetryDelay : _d,
_e = _a.shouldResetTimeout,
shouldResetTimeout = _e === void 0 ? false : _e;
var currentState = getCurrentState(config);
var shouldRetry = retryCondition(err) && currentState.retryCount < retries;
if (shouldRetry) {
currentState.retryCount += 1;
var delay_1 = retryDelay(currentState.retryCount, err);
// Clean up agent to prevent dead loops
fixConfig(axios, config);
if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
var lastRequestDuration = Date.now() - currentState.lastRequestTime;
// Minimum 1ms timeout (passing 0 or less to XHR means no timeout)
config.timeout = Math.max(config.timeout - lastRequestDuration - delay_1, 1);
}
// Initialize request data
config.transformRequest = [function (data) {
return data;
}];
return new Promise(function (resolve) {
return setTimeout(function () {
return resolve(axios(config));
}, delay_1);
});
}
onResponseError && onResponseError(err);
onError && onError(err);
return Promise.reject(err);
});
};
/**
* Create request
*/
AxiosExtend.prototype.create = function (
// url: string | AxiosExtendRequestOptions<D>,
config) {
var _this = this;
var _a = config.unique,
unique = _a === void 0 ? this.unique : _a,
_b = config.orderly,
orderly = _b === void 0 ? this.orderly : _b,
_c = config.url,
url = _c === void 0 ? '' : _c;
var promiseKey = Symbol('promiseKey');
var source = axios.CancelToken.source();
var abortController;
config.requestOptions = jsCool.extend(true, {}, config);
config.cancelToken = source.token;
if (typeof AbortController === 'function') {
abortController = new AbortController();
config.signal = abortController.signal;
}
// Interface must return in order or need to cancel url same request
unique && this.clear(url);
var promise = new Promise(function (resolve, reject) {
axios(config).then(function (res) {
if (!orderly) resolve(res);else _this.wait(url, promiseKey).then(function () {
resolve(res);
});
}).catch(function (err) {
// Request cancelled
if (axios.isCancel(err)) _this.onCancel && _this.onCancel(err);
// Request error
else reject(err);
}).finally(function () {
var index = _this.waiting[url].findIndex(function (el) {
return el.promiseKey === promiseKey;
});
index > -1 && _this.waiting[url].splice(index, 1);
});
});
this.add(url, {
promiseKey: promiseKey,
url: url,
promise: promise,
source: source,
abortController: abortController
});
return promise;
};
/**
* Drop all un-need requests
*
* @param key - the key of waiting line, usually to be the request url
*/
AxiosExtend.prototype.clear = function (key) {
var e_1, _a;
for (var url in this.waiting) {
// no key => clean all
if (!key || url === key) {
var waitingList = this.waiting[url] || [];
try {
for (var waitingList_1 = (e_1 = void 0, tslib.__values(waitingList)), waitingList_1_1 = waitingList_1.next(); !waitingList_1_1.done; waitingList_1_1 = waitingList_1.next()) {
var item = waitingList_1_1.value;
item.source.cancel('request canceled');
item.abortController && item.abortController.abort();
}
} catch (e_1_1) {
e_1 = {
error: e_1_1
};
} finally {
try {
if (waitingList_1_1 && !waitingList_1_1.done && (_a = waitingList_1.return)) _a.call(waitingList_1);
} finally {
if (e_1) throw e_1.error;
}
}
this.waiting[url] = [];
}
}
};
/**
* Waiting to resolve the item before this request
*
* @param key - the key of waiting line, usually to be the request url
* @param promiseKey - the unique promise key
* @returns - Promise<void>
*/
AxiosExtend.prototype.wait = function (key, promiseKey) {
return tslib.__awaiter(this, void 0, void 0, function () {
var waitingList, index;
return tslib.__generator(this, function (_b) {
switch (_b.label) {
case 0:
if (!this.orderly) return [2 /*return*/, Promise.resolve()];
waitingList = this.waiting[key] || [];
index = waitingList.findIndex(function (item) {
return item.promiseKey === promiseKey;
});
_b.label = 1;
case 1:
if (!(index > 0)) return [3 /*break*/, 7];
index--;
if (!(waitingList[index] && waitingList[index].promiseKey !== promiseKey)) return [3 /*break*/, 6];
_b.label = 2;
case 2:
_b.trys.push([2, 4,, 5]);
return [4 /*yield*/, waitingList[index].promise
// await waitingList.splice(index, 1)[0].promise
];
case 3:
_b.sent();
return [3 /*break*/, 5];
case 4:
_b.sent();
console.info('The task has been dropped');
return [3 /*break*/, 5];
case 5:
waitingList.splice(index, 1);
_b.label = 6;
case 6:
return [3 /*break*/, 1];
case 7:
return [2 /*return*/];
}
});
});
};
/**
* set item to waiting list
*
* @param key - the key of waiting line, usually to be the request url
* @param item - waiting object
*/
AxiosExtend.prototype.add = function (key, item) {
if (!(key in this.waiting)) this.waiting[key] = [];
this.waiting[key].push(item);
};
return AxiosExtend;
}();
exports.AxiosExtend = AxiosExtend;
exports.default = AxiosExtend;
exports.exponentialDelay = exponentialDelay;
exports.isIdempotentRequestError = isIdempotentRequestError;
exports.isNetworkError = isNetworkError;
exports.isNetworkOrIdempotentRequestError = isNetworkOrIdempotentRequestError;
exports.isRetryableError = isRetryableError;
exports.isSafeRequestError = isSafeRequestError;