axios-useful
Version:
Some expand functions for axios, like retry, cache, etc.
545 lines (536 loc) • 17.5 kB
JavaScript
;
var utils = require('@wang-yige/utils');
var axios$1 = require('axios');
var Methods;
(function(Methods2) {
Methods2["GET"] = "GET";
Methods2["POST"] = "POST";
Methods2["PUT"] = "PUT";
Methods2["DELETE"] = "DELETE";
})(Methods || (Methods = {}));
var SingleType;
(function(SingleType2) {
SingleType2["NEXT"] = "next";
SingleType2["PREV"] = "prev";
SingleType2["QUEUE"] = "queue";
})(SingleType || (SingleType = {}));
class ResponseCache {
response;
constructor(cache) {
this.response = cache;
}
}
class CacheController {
static isResponseCache(cache) {
return cache instanceof ResponseCache;
}
_axios;
cacheKeys;
cache;
constructor(axios) {
this._axios = axios;
}
request(config) {
const cacheConfig = config.cache;
if (!cacheConfig) {
return;
}
const { time } = utils.isBoolean(cacheConfig) ? { time: -1 } : cacheConfig;
if (!utils.isNumber(time) || time === 0) {
return;
}
if (!this.cache) {
this.cache = /* @__PURE__ */ new Map();
}
const cacheValue = this.getCache(config);
if (cacheValue) {
const { response, timestamp } = cacheValue;
if (utils.isNumber(time) && time > 0 && Date.now() - timestamp > time) {
this.deleteCache(config);
} else {
throw new ResponseCache(response);
}
}
}
response(config, response) {
const { cache = false } = config;
if (!cache) {
return;
}
if (!this.cache) {
this.cache = /* @__PURE__ */ new Map();
}
if (!this.hasCache(config)) {
const { time } = utils.isBoolean(cache) ? { time: -1 } : cache;
if (!utils.isNumber(time) || time === 0) {
return;
}
this.setCache(config, response);
}
}
hasCache(config) {
return this.cache.has(this.cacheKey(config));
}
getCache(config) {
if (this.hasCache(config)) {
return this.cache.get(this.cacheKey(config));
}
}
setCache(config, cache) {
return this.cache.set(this.cacheKey(config), { response: cache, timestamp: Date.now() });
}
deleteCache(config) {
return this.cache.delete(this.cacheKey(config));
}
cacheKey(config) {
if (!this.cacheKeys) {
this.cacheKeys = /* @__PURE__ */ new WeakMap();
}
if (this.cacheKeys.has(config)) {
return this.cacheKeys.get(config);
}
const { method = Methods.GET } = config;
const methodUpper = utils.upperCase(method);
if (methodUpper !== Methods.GET.toUpperCase()) {
throw new Error("Cache only supported for the `GET` method");
}
const key = `//${methodUpper}::${this._axios.getUri(config)}`;
this.cacheKeys.set(config, key);
return key;
}
}
const createAbortController = (() => {
if (utils.getGlobal().AbortController) {
return function(config) {
const controller = new AbortController();
config.signal = controller.signal;
return controller.abort.bind(controller);
};
} else {
return function(config) {
const CancellToken = axios$1.CancelToken;
const source = CancellToken.source();
config.cancelToken = source.token;
return source.cancel.bind(source);
};
}
})();
const DefaultRetryErrorCodes = [axios$1.AxiosError.ECONNABORTED, axios$1.AxiosError.ERR_NETWORK, axios$1.AxiosError.ETIMEDOUT, "ECONNREFUSED"];
const DefaultResponseCodes = [500, 404, 502];
const DefaultRequestCodes = [404];
function checkRetryCodeRange(range) {
return utils.isGeneralObject(range) && range.hasOwnProperty("from") && range.hasOwnProperty("to") && utils.isNumber(range.from) && utils.isNumber(range.to);
}
function trimString(s) {
return s.trim();
}
function parseCodeRange(range) {
const ranges = [];
if (utils.isNumber(range) || utils.isArray(range) && range.every(utils.isNumber)) {
const _range = utils.toArray(range).sort((a, b) => a - b);
ranges.push(..._range.reduce((prev, curr) => {
const last = prev[prev.length - 1];
if (!last || last.to + 1 !== curr) {
prev.push({ from: curr, to: curr });
return prev;
}
last.to = curr;
return prev;
}, []));
} else if (utils.isString(range)) {
ranges.push(...range.split(",").reduce((prev, curr) => {
const arr = curr.split("-").map(trimString).map(Number).slice(0, 2);
if (isNaN(arr[0])) {
return prev;
}
if (utils.isUndef(arr[1]) || isNaN(arr[1])) {
arr[1] = arr[0];
}
let [from, to] = arr;
if (from > to) {
[from, to] = [to, from];
}
prev.push({ from, to });
return prev;
}, []));
} else {
const _range = utils.toArray(range);
if (!_range.every(checkRetryCodeRange)) {
throw new Error("Invalid retry code range");
}
ranges.push(..._range);
}
const cache = /* @__PURE__ */ new Map();
return (code) => {
const _code = +code;
if (cache.has(_code)) {
return cache.get(_code);
}
for (const { from, to } of ranges) {
if (from <= _code && _code <= to) {
cache.set(_code, true);
return true;
}
}
cache.set(_code, false);
return false;
};
}
function parseErrorReasons(reasons) {
const _reasons = utils.toArray(reasons);
const cache = /* @__PURE__ */ new Map();
return (reason) => {
if (cache.has(reason)) {
return cache.get(reason);
}
cache.set(reason, _reasons.includes(reason));
return cache.get(reason);
};
}
function requestWithRetry(fn, rests, config) {
const retryConfig = config.retry;
if (!retryConfig) {
return fn(...rests, config);
}
const _config = utils.isBoolean(retryConfig) ? {} : retryConfig;
const { errorReasons = DefaultRetryErrorCodes, badResponseCodes = DefaultResponseCodes, badRequestCodes = DefaultRequestCodes, domains = void 0 } = _config;
const isErrorReasons = parseErrorReasons(errorReasons);
let isBadResponseCodes;
let isBadRequestCodes;
if (!isErrorReasons(axios$1.AxiosError.ERR_BAD_RESPONSE)) {
isBadResponseCodes = parseCodeRange(badResponseCodes);
}
if (!isErrorReasons(axios$1.AxiosError.ERR_BAD_REQUEST)) {
isBadRequestCodes = parseCodeRange(badRequestCodes);
}
const retryDelay = utils.isNumber(_config.delay) ? Math.max(_config.delay, 0) : 1e3;
const retryCount = utils.isNumber(_config.count) ? Math.max(_config.count, 1) : 5;
let changeDomain = false;
let domainIndex = -1;
let domainList;
if (utils.isArray(domains) && domains.length) {
changeDomain = true;
domainList = [...domains || []];
}
const useRetry = async (n = 0) => {
let requestConfig = config;
if (changeDomain) {
const index = domainIndex++;
if (domainIndex >= domainList.length) {
domainIndex = -1;
}
if (index >= 0) {
const target = domainList[index];
requestConfig = {
...config,
...utils.isDef(target) ? { baseURL: domainList[index] } : {}
};
}
}
return fn(...rests, requestConfig).catch((err) => {
if (axios$1.isCancel(err) || n >= retryCount) {
return Promise.reject(err);
}
if (isErrorReasons(err.code) || isBadResponseCodes && err.code === axios$1.AxiosError.ERR_BAD_RESPONSE && isBadResponseCodes(err == null ? void 0 : err.status) || isBadRequestCodes && err.code === axios$1.AxiosError.ERR_BAD_REQUEST && isBadRequestCodes(err == null ? void 0 : err.status)) {
return utils.delay(retryDelay).then(() => useRetry(n + 1));
}
return Promise.reject(err);
});
};
return useRetry();
}
const replacePrefixSlash = /^\/*([^\/].*)$/;
const replaceSuffixSlash = /^(.*[^\/])\/*$/;
function delayMicroQueue(cb) {
Promise.resolve().then(() => {
return Promise.resolve();
}).then(cb);
}
class SingleController {
_axios;
_pipeline;
singleTasks;
singleNext;
singlePrev;
constructor(axios2, pipeline) {
this._axios = axios2;
this._pipeline = pipeline;
}
request(fn, rests, url, config) {
const singleConfig = config.single;
if (singleConfig || utils.isUndef(singleConfig)) {
const { type = SingleType.QUEUE } = utils.isBoolean(singleConfig) || utils.isUndef(singleConfig) ? {} : singleConfig;
const _key = this.singleKey(url, config);
if (type === SingleType.QUEUE) {
const { promise, resolve, reject } = utils.createPromise();
if (!this.singleTasks) {
this.singleTasks = /* @__PURE__ */ new Map();
}
if (!this.singleTasks.has(_key)) {
this.singleTasks.set(_key, new utils.ParallelTask(1));
this.singleTasks.get(_key).onEmpty(() => {
this.singleTasks.delete(_key);
});
}
const tasks = this.singleTasks.get(_key);
const task = async () => {
await this.send(fn, rests, config).then(resolve, reject);
};
const useTask = tasks.add(task);
promise.abort = promise.cancel = () => {
useTask.cancel();
config.__abort && config.__abort();
};
return promise;
}
if (type === SingleType.NEXT) {
let isAbort = false;
if (!this.singleNext) {
this.singleNext = /* @__PURE__ */ new Map();
}
if (this.singleNext.has(_key)) {
this.singleNext.get(_key)();
}
const _promise = this.send(fn, rests, config);
this.singleNext.set(_key, () => {
isAbort = true;
_promise.abort();
});
const { promise, resolve, reject } = utils.createPromise();
promise.cancel = _promise.cancel;
promise.abort = _promise.abort;
_promise.then(resolve).catch((err) => {
if (isAbort) {
const tip = "This request has been canceled because of the next request is come.";
return reject(new axios$1.CanceledError(tip));
}
reject(err);
});
_promise.finally(() => this.singleNext.delete(_key));
return promise;
}
if (type === SingleType.PREV) {
if (!this.singlePrev) {
this.singlePrev = /* @__PURE__ */ new Set();
}
const { promise, resolve, reject } = utils.createPromise();
if (this.singlePrev.has(_key)) {
promise.cancel = promise.abort = utils.VOID_FUNCTION;
Promise.resolve().then(() => {
const tip = "This request has been canceled because of the previous request has not been completed.";
reject(new axios$1.CanceledError(tip));
});
} else {
this.singlePrev.add(_key);
const _promise = this.send(fn, rests, config);
promise.cancel = _promise.cancel;
promise.abort = _promise.abort;
_promise.then(resolve, reject);
_promise.finally(() => this.singlePrev.delete(_key));
}
return promise;
}
throw new Error("Unknown single type");
}
return this.send(fn, rests, config);
}
singleKey(url, config) {
const { method = Methods.GET } = config;
const baseURL = (this._axios.defaults.baseURL || "").replace(replaceSuffixSlash, "$1");
const path = (url || "").replace(replacePrefixSlash, "$1");
return `//${utils.upperCase(method)}::${baseURL}/${path}`;
}
send(fn, rests, config = {}) {
const abort = createAbortController(config);
config.__abort = abort;
const _promise = this._pipeline.add(async (config2) => {
return await requestWithRetry(fn, rests, config2);
}, config);
const { promise, resolve, reject } = utils.createPromise();
promise.abort = promise.cancel = () => {
_promise.cancel();
abort();
delayMicroQueue(() => {
reject(new axios$1.CanceledError("Abort request before send"));
});
};
_promise.then(resolve, reject);
return promise;
}
}
const noDataMethods = ["get", "delete", "head", "options"];
const withDataMethods = ["post", "put", "patch", "postForm", "putForm", "patchForm"];
const defineConfig = {
writable: false,
configurable: false
};
class AxiosRequestInstance {
/** The data of single types */
static Single = SingleType;
static create(baseURL, config) {
if (utils.isString(baseURL)) {
return new AxiosRequestInstance(baseURL, config);
}
return new AxiosRequestInstance(baseURL);
}
_maximum = 5;
_axios;
_pipeline;
_frequency = void 0;
// 拦截器重置
requestInterceptor;
requestInterceptorIndex;
responseInterceptorIndex;
// 控制器实例
cacheController;
singleController;
constructor(baseURL, config) {
if (!utils.isString(baseURL)) {
config = baseURL;
baseURL = baseURL == null ? void 0 : baseURL.baseURL;
}
const { maximumInOneTime = 5, limitInOneSecond = 50 } = config || {};
this._maximum = utils.isNumber(+maximumInOneTime) ? Math.max(1, +maximumInOneTime || 5) : 5;
this._pipeline = new utils.ParallelTask(this._maximum);
const _limit = +limitInOneSecond || 0;
if (utils.isNumber(_limit) && _limit > 0) {
this._frequency = utils.checkFrequency({ range: 1e3, maximum: _limit }, (_, current, path) => {
throw new Error(`The request frequency is over the limit in one second. Current count is ${current} with path '${path}'.
It's maybe an infinite loop, and if you want to continue, you can set the \`limitInOneSecond\` to a bigger number or zero.`);
});
}
const defaultConfig = { ...config };
delete defaultConfig.maximumInOneTime;
this._axios = axios$1.create({ ...defaultConfig, baseURL });
this.cacheController = new CacheController(this._axios);
this.singleController = new SingleController(this._axios, this._pipeline);
this.requestInterceptor = Object.freeze({
onFulfilled: (config2) => {
this.cacheController.request(config2);
return config2;
},
onRejected: (err) => {
return Promise.reject(err);
}
});
this.requestInterceptorIndex = this._axios.interceptors.request.use(this.requestInterceptor.onFulfilled, this.requestInterceptor.onRejected);
this.responseInterceptorIndex = this._axios.interceptors.response.use((response) => {
this.cacheController.response(response.config, response);
return response;
}, async (err) => {
if (CacheController.isResponseCache(err)) {
return Promise.resolve(err.response);
}
return Promise.reject(err);
});
noDataMethods.forEach((method) => {
const _method = this._axios[method].bind(this._axios);
Object.defineProperty(this, method, {
...defineConfig,
value: (url, config2) => {
return this.proxy(_method, [url], url, config2);
}
});
});
withDataMethods.forEach((method) => {
const _method = this._axios[method].bind(this._axios);
Object.defineProperty(this, method, {
...defineConfig,
value: (url, data, config2) => {
return this.proxy(_method, [url, data], url, config2);
}
});
});
}
get defaults() {
return this._axios.defaults;
}
/**
* Current axios instance, with don't have wrapper methods
*/
get axios() {
return this._axios;
}
/**
* Axios static object
*/
get Axios() {
return axios$1;
}
_interceptors;
/**
* Request interceptor
*/
get interceptors() {
if (this._interceptors) {
return this._interceptors;
}
this._interceptors = /* @__PURE__ */ Object.create(null);
const request = this._axios.interceptors.request;
const response = this._axios.interceptors.response;
Object.defineProperty(this._interceptors, "request", {
...defineConfig,
value: Object.freeze({
use: (...args) => {
request.eject(this.requestInterceptorIndex);
const index = request.use(...args);
this.requestInterceptorIndex = request.use(this.requestInterceptor.onFulfilled, this.requestInterceptor.onRejected);
return index;
},
eject: (id) => {
if (id === this.requestInterceptorIndex) {
return;
}
return request.eject(id);
},
clear: () => request.clear()
})
});
Object.defineProperty(this._interceptors, "response", {
...defineConfig,
value: Object.freeze({
use: (...args) => response.use(...args),
eject: (id) => {
if (id === this.responseInterceptorIndex) {
return;
}
return response.eject(id);
},
clear: () => response.clear()
})
});
return this._interceptors;
}
/**
* Change the maximum number of parallel requests pipeline.
*/
maximum(maximum) {
this._pipeline.changeMaxParallelCount(Math.max(1, +maximum || 5));
}
getUri(config) {
return this._axios.getUri(config);
}
// api methods
proxy(fn, rests, url, config = {}) {
this._frequency && this._frequency(1, url);
return this.singleController.request(fn, rests, url, { ...config });
}
}
let defaultInstance;
[...noDataMethods, ...withDataMethods].forEach((method) => {
Object.defineProperty(AxiosRequestInstance, method, {
...defineConfig,
value: (...args) => {
if (!defaultInstance) {
defaultInstance = new AxiosRequestInstance("");
}
return defaultInstance[method](...args);
}
});
});
const AxiosRequest = AxiosRequestInstance;
const APIRequest = AxiosRequest;
const axios = axios$1;
exports.APIRequest = APIRequest;
exports.AxiosRequest = AxiosRequest;
exports.axios = axios;