UNPKG

axios-retryer

Version:

axios-retryer is an advanced Axios request manager offering intelligent retry logic with token refresh, concurrency control, priority queuing, and a flexible plugin architecture, all built with TypeScript for robust HTTP client integrations.

2 lines (1 loc) 7.11 kB
class t{constructor(e={}){this.name="CircuitBreakerPlugin",this.version="2.0.0",this._state=t.STATES.CLOSED,this._recentFailures=[],this._responseMetrics={},this._options={failureThreshold:5,openTimeout:3e4,halfOpenMax:1,successThreshold:1,useSlidingWindow:!1,slidingWindowSize:6e4,adaptiveTimeout:!1,adaptiveTimeoutPercentile:.95,adaptiveTimeoutSampleSize:100,adaptiveTimeoutMultiplier:1.5,excludeUrls:[],...e},this._options.successThreshold&&this._options.successThreshold>this._options.halfOpenMax&&(this._options.successThreshold=this._options.halfOpenMax),this._state=t.STATES.CLOSED,this._failureCount=0,this._successCount=0,this._halfOpenCount=0,this._nextAttempt=Date.now()}initialize(e){this._manager=e;const i=e.axiosInstance;this._log("debug","Initializing CircuitBreakerPlugin with options:",{...this._options,state:this._state}),this._requestInterceptorId=i.interceptors.request.use((e=>{if(this._options.adaptiveTimeout&&e.url&&(e.__timestamp||(e.__timestamp=Date.now())),this._isUrlExcluded(e))return this._log("debug","URL excluded from circuit breaking: "+e.url),e;if(this._state===t.STATES.OPEN){if(Date.now()<this._nextAttempt){const t=this._nextAttempt-Date.now();return this._log("debug",`Circuit is OPEN: failing fast. Will retry in ${t}ms`),Promise.reject(Error("Circuit is open: failing fast."))}this._transitionToHalfOpen()}if(this._state===t.STATES.HALF_OPEN){if(this._halfOpenCount>=this._options.halfOpenMax)return this._log("debug","Circuit is HALF_OPEN: too many test requests."),Promise.reject(Error("Circuit is half-open: too many test requests."));this._halfOpenCount++,this._log("debug",`HALF_OPEN test request #${this._halfOpenCount} of ${this._options.halfOpenMax}`)}if(this._options.adaptiveTimeout&&e.url){const t=this._normalizeUrl(e.url);this._responseMetrics[t]&&this._responseMetrics[t].currentPercentileMs>0&&(e.timeout=Math.round(this._responseMetrics[t].currentPercentileMs*(this._options.adaptiveTimeoutMultiplier||1.5)),this._log("debug",`Setting adaptive timeout for ${t}: ${e.timeout}ms`))}return e})),this._responseInterceptorId=i.interceptors.response.use((e=>{if(this._options.adaptiveTimeout&&e.config.url&&this._trackResponseTime(e),this._state===t.STATES.HALF_OPEN){this._successCount++;const t=this._options.successThreshold||1;t>this._successCount?this._log("debug",`HALF_OPEN success: ${this._successCount}/${t} successful test requests`):(this._log("debug",`HALF_OPEN success threshold reached (${this._successCount}/${t})`),this._reset())}else this._state===t.STATES.CLOSED&&(this._failureCount=0);return e}),(e=>{if(e.config&&this._isUrlExcluded(e.config))return Promise.reject(e);if(this._options.shouldCountError&&!this._options.shouldCountError(e))return this._log("debug","Error excluded from circuit breaking by shouldCountError"),Promise.reject(e);if(this._options.useSlidingWindow){this._addFailureToSlidingWindow(e);const t=this._getFailureCountInWindow();this._options.failureThreshold>t||(this._log("debug",`Sliding window failure threshold reached: ${t} failures in window`),this._trip())}else this._failureCount++,this._log("debug",`Failure count increased: ${this._failureCount}/${this._options.failureThreshold}`),this._state!==t.STATES.HALF_OPEN&&this._options.failureThreshold>this._failureCount||this._trip();return Promise.reject(e)}))}onBeforeDestroyed(t){this._log("debug","Removing CircuitBreakerPlugin");const e=t.axiosInstance;void 0!==this._requestInterceptorId&&e.interceptors.request.eject(this._requestInterceptorId),void 0!==this._responseInterceptorId&&e.interceptors.response.eject(this._responseInterceptorId)}getState(){return this._state}getMetrics(){const t=this._options.useSlidingWindow?this._getFailureCountInWindow():this._failureCount;return this._options.useSlidingWindow&&this._cleanupOldFailures(),{state:this._state,failureCount:this._failureCount,halfOpenCount:this._halfOpenCount,successCount:this._successCount,nextAttemptIn:Math.max(0,this._nextAttempt-Date.now()),failuresInWindow:t,adaptiveTimeouts:this._options.adaptiveTimeout?Object.entries(this._responseMetrics).map((([t,e])=>({url:t,timeoutMs:Math.round(e.currentPercentileMs*(this._options.adaptiveTimeoutMultiplier||1.5)),p95ResponseTimeMs:e.currentPercentileMs,samplesCount:e.times.length}))):[]}}_trip(){this._state!==t.STATES.OPEN&&(this._state=t.STATES.OPEN,this._nextAttempt=Date.now()+this._options.openTimeout,this._successCount=0,this._log("error","Circuit tripped: entering OPEN state until "+new Date(this._nextAttempt).toISOString()))}_reset(){this._state=t.STATES.CLOSED,this._failureCount=0,this._successCount=0,this._halfOpenCount=0,this._log("debug","Circuit reset: entering CLOSED state.")}_transitionToHalfOpen(){this._state=t.STATES.HALF_OPEN,this._halfOpenCount=0,this._successCount=0,this._log("debug","Circuit transitioning to HALF_OPEN state.")}_addFailureToSlidingWindow(t){var e,i;const s={timestamp:Date.now(),url:(null===(e=t.config)||void 0===e?void 0:e.url)||"unknown",status:null===(i=t.response)||void 0===i?void 0:i.status,errorCode:t.code};this._recentFailures.push(s),this._failureCount++,this._cleanupOldFailures(),this._log("debug","Added failure to sliding window. Current count: "+this._recentFailures.length)}_cleanupOldFailures(){if(!this._options.useSlidingWindow)return;const t=Date.now()-(this._options.slidingWindowSize||6e4);this._recentFailures=this._recentFailures.filter((e=>e.timestamp>=t))}_getFailureCountInWindow(){return this._options.useSlidingWindow?(this._cleanupOldFailures(),this._recentFailures.length):this._failureCount}_trackResponseTime(t){if(!t.config.url||!this._options.adaptiveTimeout)return;const e=this._normalizeUrl(t.config.url);let i=0;t.headers&&t.headers["x-response-time"]?i=parseInt(t.headers["x-response-time"],10):t.config.__timestamp&&(i=Date.now()-t.config.__timestamp),i>0||(i=100),this._responseMetrics[e]||(this._responseMetrics[e]={times:[],sampleSize:this._options.adaptiveTimeoutSampleSize||100,lastCalculated:0,currentPercentileMs:0});const s=this._responseMetrics[e];s.times.push(i),s.times.length>s.sampleSize&&s.times.shift(),this._updateTimeoutPercentile(e)}_updateTimeoutPercentile(t){const e=this._responseMetrics[t];if(!e||0===e.times.length)return;const i=[...e.times].sort(((t,e)=>t-e)),s=this._options.adaptiveTimeoutPercentile||.95;e.currentPercentileMs=i[Math.max(0,Math.min(Math.ceil(i.length*s)-1,i.length-1))],e.lastCalculated=Date.now(),this._log("debug",`Updated adaptive timeout for ${t}: ${e.currentPercentileMs}ms at ${100*s}th percentile`)}_normalizeUrl(t){return t}_isUrlExcluded(t){return!(!t.url||!this._options.excludeUrls||0===this._options.excludeUrls.length)&&this._options.excludeUrls.some((e=>e instanceof RegExp?e.test(t.url||""):t.url===e))}_log(t,e,i){if(this._manager&&"function"==typeof this._manager.getLogger){const s=this._manager.getLogger(),n=`${this.name}: ${e}`;switch(t){case"debug":s.debug(n,i);break;case"error":s.error(n,i);break;case"warn":s.warn(n,i)}}}}function e(e={}){return new t(e)}t.STATES={CLOSED:"CLOSED",OPEN:"OPEN",HALF_OPEN:"HALF_OPEN"};export{t as CircuitBreakerPlugin,e as createCircuitBreaker};