UNPKG

axios-retryer

Version:

TypeScript-first Axios retry library with concurrency limits, request priority, token refresh, response caching, and circuit breaker plugins.

2 lines (1 loc) 19.6 kB
const e="GET",t=new Set(["retryAttempt","requestRetries","requestMode","requestId","correlationId","isRetrying","priority","timestamp","backoffType","retryableStatuses","extra","isRetryRefreshRequest","manualReplayAttempt","retryAfterMs","silentlyCancelled","cachingOptions"]);function i(e){return t.has(e)&&"__proto__"!==e&&"constructor"!==e&&"prototype"!==e}function n(e){const t={};if(Object.defineProperty(t,"toJSON",{value:()=>{},enumerable:!1,writable:!1,configurable:!1}),e)for(const n of Object.keys(e))i(n)&&(t[n]=e[n]);return t}function r(e){if(e)return e.__axiosRetryer}class s extends Error{constructor(){super("URL is required for cache key generation"),this.name="InvalidCacheKeyError"}}class a extends Error{constructor(e,t){super(e),this.name=new.target.name,this.code=t,Object.setPrototypeOf(this,new.target.prototype)}}class o extends a{constructor(e,t,i){super(e,"EINVALID_CONFIG"),this.optionName=t,this.optionValue=i}}class c{constructor(){this.storage=new Map}get(e){return this.storage.get(e)}set(e,t){this.storage.set(e,t)}delete(e){this.storage.delete(e)}clear(){this.storage.clear()}entries(){return Array.from(this.storage,([e,t])=>({key:e,value:t}))}}function h(e){let t=2166136261;for(let i=0;i<e.length;i++)t^=e.charCodeAt(i),t=Math.imul(t,16777619);return`fp_${(t>>>0).toString(16).padStart(8,"0")}`}function l([e,t],[i,n]){return e.localeCompare(i)||t.localeCompare(n)}function u(e){const t=e.indexOf("#"),i=-1===t?e:e.slice(0,t),n=i.indexOf("?");if(-1===n)return i;const r=i.slice(0,n),s=i.slice(n+1);if(!s)return r;const a=Array.from(new URLSearchParams(s).entries()).sort(l);return 0===a.length?r:`${r}?${a.map(([e,t])=>`${encodeURIComponent(e)}=${encodeURIComponent(t)}`).join("&")}`}function g(e,t=!1){if(void 0!==e){if(null===e||"string"==typeof e||"number"==typeof e||"boolean"==typeof e)return e;if(e instanceof Date)return e.toISOString();if("undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams)return Array.from(e.entries()).sort(l);if(e instanceof Map)return Array.from(e.entries()).map(([e,i])=>[String(e),g(i,t)]).sort(([e],[t])=>e.localeCompare(t));if(e instanceof Set)return Array.from(e.values()).map(e=>g(e,t));if(Array.isArray(e))return e.map(e=>g(e,t));if("object"==typeof e){const i="function"==typeof e.toJSON?e.toJSON():e;if(i!==e)return g(i,t);const n={};return Object.entries(i).map(([e,i])=>[t?e.toLowerCase():e,g(i,t)]).sort(([e],[t])=>e.localeCompare(t)).forEach(([e,t])=>{n[e]=t}),n}return String(e)}}function f(e,t=!1){if(null==e)return"";if("string"==typeof e){const i=e.trim();if(i.startsWith("{")&&i.endsWith("}")||i.startsWith("[")&&i.endsWith("]"))try{return JSON.stringify(g(JSON.parse(i),t))}catch(e){}return e}return"number"==typeof e||"boolean"==typeof e?String(e):JSON.stringify(g(e,t))}function d(e){return[e.method,e.normalizedUrl,e.normalizedParams,e.normalizedData,e.normalizedHeaders].join("|")}function p(e){return e instanceof RegExp?{type:"regexp",fingerprint:h(String(e))}:"string"==typeof e?{type:"exact",fingerprint:h(e)}:{type:"exact"in e?"exact":"prefix",fingerprint:h("exact"in e?e.exact:e.prefix)}}const m=new Set(["authorization","proxy-authorization","cookie","x-auth-token","x-api-key"]);function y(e){if(!e.headers)return!1;const t=e.headers;for(const e of Object.keys(t))if(m.has(e.toLowerCase()))return!0;return!1}function v(e){return!!e&&"function"==typeof e.then}function C(e){return e instanceof Error?{errorName:e.name}:{}}function x(e){var t;return null!==(t=e.lastAccessedAt)&&void 0!==t?t:e.timestamp}function w(e){return[...e].sort((e,t)=>x(e.value)-x(t.value)||e.key.localeCompare(t.key))}function I(e,t=new WeakMap){if(null===e||"object"!=typeof e)return e;if(e instanceof Date)return new Date(e.getTime());if("undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams)return new URLSearchParams(e.toString());if("undefined"!=typeof ArrayBuffer){if(e instanceof ArrayBuffer)return e.slice(0);if(ArrayBuffer.isView(e))return function(e){const t="undefined"!=typeof globalThis?globalThis.Buffer:void 0;return(null==t?void 0:t.isBuffer(e))?t.from(e):e instanceof DataView?new DataView(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength)):"function"==typeof e.slice?e.slice():new Uint8Array(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength))}(e)}if("undefined"!=typeof Blob&&e instanceof Blob)return e.slice(0,e.size,e.type);const i=t.get(e);if(void 0!==i)return i;if(Array.isArray(e)){const i=[];return t.set(e,i),e.forEach((e,n)=>{i[n]=I(e,t)}),i}if(e instanceof Map){const i=new Map;return t.set(e,i),e.forEach((e,n)=>{i.set(I(n,t),I(e,t))}),i}if(e instanceof Set){const i=new Set;return t.set(e,i),e.forEach(e=>{i.add(I(e,t))}),i}let n=e;if(function(e){return"object"==typeof e&&null!==e&&"function"==typeof e.toJSON}(e))try{n=e.toJSON()}catch{}if(n!==e)return I(n,t);const r=Object.getPrototypeOf(e),s=Object.create(null===r?null:r);return t.set(e,s),Reflect.ownKeys(e).forEach(i=>{const n=Object.getOwnPropertyDescriptor(e,i);(null==n?void 0:n.enumerable)&&(s[i]=I(e[i],t))}),s}function A(e){if("function"==typeof structuredClone)try{return structuredClone(e)}catch(e){}return I(e)}function R(e,t){const i=A(e.headers);if(t.length>0){const e=new Set(t.map(e=>e.toLowerCase()));for(const t of Object.keys(i))e.has(t.toLowerCase())&&delete i[t]}return{config:{},data:A(e.data),headers:i,status:e.status,statusText:e.statusText}}function b(e,t){return{config:t,data:A(e.data),headers:A(e.headers),status:e.status,statusText:e.statusText}}class S{constructor(e){this.options=e,this.timer=null,this.consecutiveFailures=0}start(){this.timer||this.options.intervalMs<=0||(this.timer=setInterval(()=>this.runOnce(),this.options.intervalMs))}stop(){this.timer&&(clearInterval(this.timer),this.timer=null)}get consecutiveFailureCount(){return this.consecutiveFailures}runOnce(){let e;const t=new Promise((t,i)=>{e=setTimeout(()=>i(new Error(`Cache cleanup exceeded ${this.options.timeoutMs} ms timeout`)),this.options.timeoutMs)});Promise.race([this.options.runCleanup(),t]).then(()=>{this.consecutiveFailures=0}).catch(e=>{var t,i;this.consecutiveFailures+=1,null===(t=this.options.getLogger())||void 0===t||t.warn("[CachingPlugin] Failed to run cache cleanup",{...C(e),consecutiveFailures:this.consecutiveFailures}),this.consecutiveFailures>=this.options.disableAfterFailures&&(null===(i=this.options.getLogger())||void 0===i||i.error("[CachingPlugin] Disabling cleanup after repeated failures",{consecutiveFailures:this.consecutiveFailures}),this.stop())}).finally(()=>{void 0!==e&&clearTimeout(e)})}}class E{constructor(e){this.options=e,this.inflightRequests=new Map,this.inflightLeaders=new Map,this.inflightFollowers=new Map,this.servedFromCache=new Set,this.trackingIdFallback=new WeakMap}getOrAssignTrackingId(e){var t,i;const n=null===(t=r(e))||void 0===t?void 0:t.requestId;if(n)return n;let s=this.trackingIdFallback.get(e);return s||(s=`ct_${Math.random().toString(36).slice(2)}`,this.trackingIdFallback.set(e,s),null===(i=this.options.getLogger())||void 0===i||i.warn("[CachingPlugin] Request lacks requestId; falling back to WeakMap-keyed tracking id. Register CachingPlugin AFTER any plugin that mutates the request config.")),s}peekInflight(e){return this.inflightRequests.get(e)}registerLeader(e,t){const i=function(){let e,t;const i=new Promise((i,n)=>{e=i,t=n});return i.catch(()=>{}),{promise:i,resolve:e,reject:t}}();return this.inflightRequests.set(t,i),this.inflightLeaders.set(this.getOrAssignTrackingId(e),t),i}registerFollower(e,t){this.inflightFollowers.set(this.getOrAssignTrackingId(e),t)}consumeFollower(e){if(!e)return!1;const t=this.getOrAssignTrackingId(e);return!!this.inflightFollowers.has(t)&&(this.inflightFollowers.delete(t),!0)}clearFollower(e){this.inflightFollowers.delete(this.getOrAssignTrackingId(e))}markServedFromCache(e){this.servedFromCache.add(this.getOrAssignTrackingId(e))}consumeServedFromCache(e){if(!e)return!1;const t=this.getOrAssignTrackingId(e);return!!this.servedFromCache.has(t)&&(this.servedFromCache.delete(t),!0)}resolve(e,t){if(!e)return;const i=this.getOrAssignTrackingId(e),n=this.inflightLeaders.get(i);if(!n)return;const r=this.inflightRequests.get(n);r&&(r.resolve(t),this.inflightRequests.delete(n)),this.inflightLeaders.delete(i)}reject(e,t){const i=this.getOrAssignTrackingId(e),n=this.inflightLeaders.get(i);if(!n)return;const r=this.inflightRequests.get(n);r&&(r.reject(t),this.inflightRequests.delete(n)),this.inflightLeaders.delete(i)}clearInflightOnly(){this.inflightRequests.clear()}clearAll(){this.inflightRequests.clear(),this.inflightLeaders.clear(),this.inflightFollowers.clear(),this.servedFromCache.clear()}}class F{constructor(t){this.name="CachingPlugin",this.version="1.0.0",this.interceptorIdReq=null,this.interceptorIdRes=null,this.cache=new Map,this.options=function(t){var i,n;return{sensitiveResponseHeaders:["set-cookie"],compareHeaders:!1,timeToRevalidate:0,cacheMethods:[e],cleanupInterval:0,maxAge:0,maxItems:1e3,maxEntrySize:0,cacheOnlyRetriedRequests:!1,storage:null!==(i=null==t?void 0:t.storage)&&void 0!==i?i:new c,dedupeConcurrentRequests:!0,cacheKeyBuilder:null!==(n=null==t?void 0:t.cacheKeyBuilder)&&void 0!==n?n:d,skipWhenAuthPresent:!0,varyHeaders:[],...t}}(t),this.storage=this.options.storage,function(e){if(!Number.isInteger(e.cleanupInterval)||e.cleanupInterval<0)throw new o("cleanupInterval must be a non-negative integer","cleanupInterval",e.cleanupInterval);if(!Number.isInteger(e.maxAge)||e.maxAge<0)throw new o("maxAge must be a non-negative integer","maxAge",e.maxAge);if(!Number.isInteger(e.maxItems)||e.maxItems<0)throw new o("maxItems must be a non-negative integer","maxItems",e.maxItems);if(!Number.isInteger(e.maxEntrySize)||e.maxEntrySize<0)throw new o("maxEntrySize must be a non-negative integer","maxEntrySize",e.maxEntrySize);if(!Number.isInteger(e.timeToRevalidate)||e.timeToRevalidate<0)throw new o("timeToRevalidate must be a non-negative integer","timeToRevalidate",e.timeToRevalidate)}(this.options);const i=()=>{var e,t;return null!==(t=null===(e=this.context)||void 0===e?void 0:e.getLogger())&&void 0!==t?t:null};this.inflight=new E({getLogger:i}),this.cleanup=new S({intervalMs:this.options.cleanupInterval,timeoutMs:F.CACHE_CLEANUP_TIMEOUT_MS,disableAfterFailures:F.CACHE_CLEANUP_DISABLE_AFTER,runCleanup:()=>this.runCacheCleanup(),getLogger:i})}initialize(e){this.context=e;const t=e.axiosInstance;this.interceptorIdReq=t.interceptors.request.use(e=>this.handleRequest(e),e=>Promise.reject(e)),this.interceptorIdRes=t.interceptors.response.use(e=>this.handleResponseSuccess(e),e=>this.handleResponseError(e)),this.options.cleanupInterval>0&&this.cleanup.start()}onBeforeDestroyed(){null!==this.interceptorIdReq&&this.context.axiosInstance.interceptors.request.eject(this.interceptorIdReq),null!==this.interceptorIdRes&&this.context.axiosInstance.interceptors.response.eject(this.interceptorIdRes),this.cleanup.stop(),this.inflight.clearAll()}getCacheKeyFingerprint(e){return h(e)}async handleRequest(t){var i,r,s,a,o,c,h;const l=function(e){const t=e;return t.__axiosRetryer?"function"!=typeof t.__axiosRetryer.toJSON&&(t.__axiosRetryer=n(t.__axiosRetryer)):t.__axiosRetryer=n(),t.__axiosRetryer}(t),u=this.getRequestCachingOptions(t),g=(t.method||e).toUpperCase();if(u&&(l.cachingOptions=u),!1===(null==u?void 0:u.cache))return null===(i=this.context.getLogger())||void 0===i||i.debug("[CachingPlugin] Skipping cache for request (explicitly disabled)"),t;if(this.options.skipWhenAuthPresent&&y(t))return null===(r=this.context.getLogger())||void 0===r||r.debug("[CachingPlugin] Skipping cache for authenticated request"),t;if(!0!==(null==u?void 0:u.cache)&&!this.options.cacheMethods.includes(g))return t;if(this.options.cacheOnlyRetriedRequests&&!l.isRetrying)return t;const f=this.buildCacheKey(t),d=this.getCacheKeyFingerprint(f);let p=this.cache.get(f);if(!p)try{p=await this.storage.get(f)}catch(e){return null===(s=this.context.getLogger())||void 0===s||s.warn("[CachingPlugin] Failed to read cache entry",{cacheKeyFingerprint:d,...C(e)}),t}if(p){const e=Date.now(),i=e-p.timestamp,n=null!==(a=p.ttr)&&void 0!==a?a:this.options.timeToRevalidate;if(0===n||i<n){const n=this.touchCacheEntry(f,p,e);return await this.persistCacheTouchIfNeeded(f,n,d),null===(o=this.context.getLogger())||void 0===o||o.debug("[CachingPlugin] Cache hit",{cacheKeyFingerprint:d,ageMs:i}),this.context.triggerAndEmit("onCacheHit",{keyFingerprint:d,config:t,ageMs:i}),this.inflight.markServedFromCache(t),{...t,adapter:()=>Promise.resolve(b(n.response,t))}}null===(c=this.context.getLogger())||void 0===c||c.debug("[CachingPlugin] Cache stale",{cacheKeyFingerprint:d,ageMs:i}),this.context.triggerAndEmit("onCacheMiss",{keyFingerprint:d,config:t,reason:"stale"}),await this.deleteCacheEntry(f)}else this.context.triggerAndEmit("onCacheMiss",{keyFingerprint:d,config:t,reason:"empty"});if(!this.options.dedupeConcurrentRequests)return t;const m=this.inflight.peekInflight(f);return m?(this.inflight.registerFollower(t,f),null===(h=this.context.getLogger())||void 0===h||h.debug("[CachingPlugin] Piggybacking on in-flight request",{cacheKeyFingerprint:d}),{...t,adapter:async()=>b(await m.promise,t)}):(this.inflight.registerLeader(t,f),t)}async handleResponseSuccess(t){var i,n,s,a,o,c;const h=t.config?r(t.config):void 0;if(this.inflight.consumeFollower(t.config))return t;if(this.inflight.consumeServedFromCache(t.config))return t;const l=this.getRequestCachingOptions(t.config);if(!1===(null==l?void 0:l.cache))return this.inflight.resolve(t.config,t),t;if(this.options.skipWhenAuthPresent&&y(t.config))return this.inflight.resolve(t.config,t),t;if(!0!==(null==l?void 0:l.cache)){const n=((null===(i=t.config)||void 0===i?void 0:i.method)||e).toUpperCase();if(!this.options.cacheMethods.includes(n))return this.inflight.resolve(t.config,t),t;if(this.options.cacheOnlyRetriedRequests&&t.config&&!(null==h?void 0:h.isRetrying))return this.inflight.resolve(t.config,t),t}if(t.status>=200&&t.status<300){const e=this.buildCacheKey(t.config),i=this.getCacheKeyFingerprint(e),r=null==l?void 0:l.ttr;if(this.options.maxEntrySize>0)try{const e=null!==(s=null===(n=JSON.stringify(t.data))||void 0===n?void 0:n.length)&&void 0!==s?s:0;if(e>this.options.maxEntrySize)return null===(a=this.context.getLogger())||void 0===a||a.debug("[CachingPlugin] Skipping oversized response",{cacheKeyFingerprint:i,estimatedSize:e,maxEntrySize:this.options.maxEntrySize}),this.inflight.resolve(t.config,t),t}catch{return this.inflight.resolve(t.config,t),t}try{null===(o=this.context.getLogger())||void 0===o||o.debug("[CachingPlugin] Caching response",{cacheKeyFingerprint:i,...r?{ttrMs:r}:{}}),await this.upsertCacheEntry(e,{response:R(t,this.options.sensitiveResponseHeaders),timestamp:Date.now(),ttr:r,lastAccessedAt:Date.now()})}catch(e){null===(c=this.context.getLogger())||void 0===c||c.warn("[CachingPlugin] Failed to cache response",{cacheKeyFingerprint:i,...C(e)})}finally{this.inflight.resolve(t.config,t)}}else this.inflight.resolve(t.config,t);return t}handleResponseError(e){return e.config&&(this.inflight.reject(e.config,e),this.inflight.clearFollower(e.config)),Promise.reject(e)}buildCacheKey(e){if(!e.url)throw new s;return this.options.cacheKeyBuilder(this.buildCacheKeyContext(e))}async runCacheCleanup(){var e;const t=await this.readCacheEntriesForScan();this.syncLocalCache(t);const i=Date.now(),n=new Set;if(this.options.maxAge>0&&t.forEach(({key:e,value:t})=>{i-t.timestamp>this.options.maxAge&&n.add(e)}),this.options.maxItems>0&&t.length>this.options.maxItems){const e=t.length-this.options.maxItems,i=w(t);for(let t=0;t<i.length&&n.size<e;t++)n.add(i[t].key)}n.size>0&&(await Promise.all(Array.from(n,e=>this.deleteCacheEntry(e))),null===(e=this.context.getLogger())||void 0===e||e.debug(`[CachingPlugin] Cleaned up ${n.size} cached items`))}clearCache(){var e;const t=this.cache.size;this.cache.clear(),this.inflight.clearInflightOnly(),null===(e=this.context.getLogger())||void 0===e||e.debug("[CachingPlugin] Cache cleared."),this.context.triggerAndEmit("onCacheInvalidated",{count:t,matcher:"all"});const i=this.storage.clear();if(v(i))return i.then(()=>{})}invalidateCache(e){const t=this.storage.entries();return v(t)?Promise.resolve(t).then(t=>this.invalidateCacheEntries(e,t)):this.invalidateCacheEntries(e,t)}getCacheStats(){const e=Date.now(),t=Array.from(this.cache.values());if(0===t.length)return{size:0,oldestItemAge:0,newestItemAge:0,averageAge:0};const i=t.map(t=>e-t.timestamp);return{size:this.cache.size,oldestItemAge:Math.max(...i),newestItemAge:Math.min(...i),averageAge:i.reduce((e,t)=>e+t,0)/i.length}}async upsertCacheEntry(e,t){var i;await this.enforceMaxItemsBeforeUpsert(e);const n=this.touchCacheEntry(e,t,null!==(i=t.lastAccessedAt)&&void 0!==i?i:t.timestamp);await this.storage.set(e,n)}touchCacheEntry(e,t,i=Date.now()){const n=t.lastAccessedAt===i?t:{...t,lastAccessedAt:i};return this.cache.delete(e),this.cache.set(e,n),n}deleteCacheEntry(e){this.cache.delete(e);const t=this.storage.delete(e);if(v(t))return t.then(()=>{})}getRequestCachingOptions(e){var t,i;return null!==(t=e.__cachingOptions)&&void 0!==t?t:null===(i=r(e))||void 0===i?void 0:i.cachingOptions}async readCacheEntriesForScan(){return Array.from(await this.storage.entries())}syncLocalCache(e){this.cache.clear(),w(e).forEach(({key:e,value:t})=>{this.cache.set(e,t)})}async enforceMaxItemsBeforeUpsert(e){if(0===this.options.maxItems)return;if(this.cache.size>0){if(this.cache.has(e))return;if(this.cache.size<this.options.maxItems)return;const t=this.cache.size-this.options.maxItems+1,i=w(Array.from(this.cache,([e,t])=>({key:e,value:t})));return void await Promise.all(i.slice(0,t).map(({key:e})=>this.deleteCacheEntry(e)))}const t=await this.readCacheEntriesForScan();if(this.syncLocalCache(t),t.some(t=>t.key===e))return;const i=t.length-this.options.maxItems+1;if(i<=0)return;const n=w(t).slice(0,i).map(e=>e.key);await Promise.all(n.map(e=>this.deleteCacheEntry(e)))}async persistCacheTouchIfNeeded(e,t,i){var n;if(0!==this.options.maxItems)try{await this.storage.set(e,t)}catch(e){null===(n=this.context.getLogger())||void 0===n||n.warn("[CachingPlugin] Failed to persist cache access metadata",{cacheKeyFingerprint:i,...C(e)})}}invalidateCacheEntries(e,t){this.syncLocalCache(t);const i=t.filter(({key:t})=>function(e,t){return t instanceof RegExp?t.test(e):"string"==typeof t?e===t:"exact"in t?e===t.exact:e.startsWith(t.prefix)}(t,e)).map(({key:e})=>e);if(0===i.length)return 0;const n=i.map(e=>this.deleteCacheEntry(e)),r=()=>{var t;return null===(t=this.context.getLogger())||void 0===t||t.debug("[CachingPlugin] Invalidated cache entries",{count:i.length,matcher:p(e)}),this.context.triggerAndEmit("onCacheInvalidated",{count:i.length,matcher:"custom"}),i.length};return n.some(e=>v(e))?Promise.all(n.map(e=>Promise.resolve(e))).then(()=>r()):r()}buildCacheKeyContext(t){return function(t,i){var n;let r;if(i.compareHeaders&&t.headers)r=f(t.headers,!0);else if(i.varyHeaders.length>0&&t.headers){const e=t.headers,n=new Set(i.varyHeaders.map(e=>e.toLowerCase())),s=[];for(const t of Object.keys(e))n.has(t.toLowerCase())&&s.push([t.toLowerCase(),String(e[t])]);s.sort(l),r=s.length>0?JSON.stringify(s):""}else r="";return{config:t,method:(t.method||e).toUpperCase(),normalizedUrl:u(null!==(n=t.url)&&void 0!==n?n:""),normalizedParams:f(t.params),normalizedData:f(t.data),normalizedHeaders:r}}(t,this.options)}}function k(e){return new F(e)}F.CACHE_CLEANUP_TIMEOUT_MS=3e4,F.CACHE_CLEANUP_DISABLE_AFTER=5;export{F as CachingPlugin,c as InMemoryCacheStorage,s as InvalidCacheKeyError,k as createCachePlugin};