UNPKG

axios-useful

Version:

Some expand functions for axios, like retry, cache, etc.

545 lines (536 loc) 17.5 kB
'use strict'; 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;