UNPKG

axios-retryer

Version:

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

1,358 lines (1,331 loc) 54 kB
import { Method, AxiosRequestConfig, AxiosError, AxiosInstance, AxiosResponse } from 'axios'; type AxiosRetryerHttpMethod = Method; declare const AXIOS_RETRYER_REQUEST_PRIORITIES: { readonly CRITICAL: 4; readonly HIGHEST: 3; readonly HIGH: 2; readonly MEDIUM: 1; readonly LOW: 0; }; type AxiosRetryerRequestPriority = (typeof AXIOS_RETRYER_REQUEST_PRIORITIES)[keyof typeof AXIOS_RETRYER_REQUEST_PRIORITIES]; type RetryEventArgs<TEvents extends object, K extends keyof TEvents> = NonNullable<TEvents[K]> extends (...args: infer TArgs) => unknown ? TArgs : never; type RetryEventListener<TEvents extends object, K extends keyof TEvents> = (...args: RetryEventArgs<TEvents, K>) => void; /** * Terminal request error payload emitted by `onRequestError`. */ interface AxiosRetryerRequestErrorEvent { /** Final Axios error object that caused request failure. */ error: AxiosError; /** Final Axios request config that failed. */ config: AxiosRequestConfig; /** HTTP status if available, otherwise `null` for network-level failures. */ status: number | null; /** Request identifier if available. */ requestId?: string; /** Total attempts performed including the initial attempt. */ attempts: number; /** Whether the final error shape is considered retryable by the active strategy. */ retryable: boolean; } /** * Queue-entry payload emitted by `onRequestQueued`. */ interface AxiosRetryerRequestQueuedEvent { /** Request identifier generated or assigned by RetryManager. */ requestId: string; /** Request config entering the queue. */ config: AxiosRequestConfig; /** Resolved priority used for queue ordering. */ priority: AxiosRetryerRequestPriority; /** Queue size immediately after this request was enqueued. */ queueSize: number; } /** * Queue-dispatch payload emitted by `onRequestDispatched`. */ interface AxiosRetryerRequestDispatchedEvent { /** Request identifier generated or assigned by RetryManager. */ requestId: string; /** Request config dispatched from the queue. */ config: AxiosRequestConfig; /** Resolved priority used for queue ordering. */ priority: AxiosRetryerRequestPriority; /** Time spent waiting in the queue before dispatch (milliseconds). */ queuedForMs: number; } /** * Success payload emitted by `onRequestSucceeded`. */ interface AxiosRetryerRequestSucceededEvent { /** Request identifier generated or assigned by RetryManager. */ requestId?: string; /** Final request config that succeeded. */ config: AxiosRequestConfig; /** Final HTTP status code. */ status: number; /** Total attempts performed including the initial attempt. */ attempts: number; } /** * Core events exposed by RetryManager without any plugins attached. */ interface CoreRetryEvents { /** * Triggered when the retry process begins. */ onRetryProcessStarted?: () => void; /** * Triggered before each retry attempt. * @param config The Axios request configuration being retried. */ beforeRetry?: (config: AxiosRequestConfig) => void; /** * Triggered after a retry attempt. * @param config The Axios request configuration being retried. * @param success Whether the retry was successful. * @param error If the retry failed, the error that caused the failure. */ afterRetry?: (config: AxiosRequestConfig, success: boolean, error?: AxiosError) => void; /** * Triggered when a retry is scheduled and waiting for the specified delay. * @param delayMs The delay in milliseconds. * @param config The Axios request configuration. */ onRetryScheduled?: (delayMs: number, config: AxiosRequestConfig) => void; /** * Triggered for each failed retry attempt. * @param config The failed Axios request configuration. */ onFailure?: (config: AxiosRequestConfig) => void; /** * Triggered when a request enters the queue. * * @param payload Queue entry metadata for this request. */ onRequestQueued?: (payload: AxiosRetryerRequestQueuedEvent) => void; /** * Triggered when a queued request is dispatched from the queue to the network layer. * * @param payload Dispatch metadata including queue wait duration. */ onRequestDispatched?: (payload: AxiosRetryerRequestDispatchedEvent) => void; /** * Triggered when a request succeeds (initial attempt or after retries). * * @param payload Success metadata for this request. */ onRequestSucceeded?: (payload: AxiosRetryerRequestSucceededEvent) => void; /** * Triggered once when a request fails terminally (all retries exhausted or no-retry terminal path). * Unlike `onFailure`, this event is emitted only for the final failure. * * @param payload Terminal error context for application-level handling. */ onRequestError?: (payload: AxiosRetryerRequestErrorEvent) => void; /** * Triggered when all retries are completed. */ onRetryProcessFinished?: () => void; /** * Triggered when an in-flight retry delay timer is cancelled — either because * the user aborted the request (`source: 'user'`) or because the system shut * the request down (`source: 'system'`, e.g. plugin destroy, queue clear). */ onRetryTimerCancelled?: (payload: { requestId: string; source: 'user' | 'system'; }) => void; /** * Triggered when a request cancelled. * @param requestId Id of the cancelled request. */ onRequestCancelled?: (requestId: string) => void; /** * Called when a request fails due to network or connection issues, meaning * no valid server response was received (e.g., user is offline). * * @param request - The Axios request config that encountered a connection error. */ onInternetConnectionError?: (request: AxiosRequestConfig) => void; /** * Triggered when a blocking request (at or above `blockingPriorityThreshold`) fails terminally. * Only fires when `blockingPriorityThreshold` is configured. * * @param config The Axios request config of the failed blocking request. */ onBlockingRequestFailed?: (config: AxiosRequestConfig) => void; /** * Triggered when every in-flight blocking request (at or above `blockingPriorityThreshold`) * has **succeeded** (terminal success) and none remain in the internal blocker set. * Not emitted when a blocker fails (`onBlockingRequestFailed`) or is cancelled. * Only fires when `blockingPriorityThreshold` is configured. */ onAllBlockingRequestsResolved?: () => void; } type RetryManagerEvents<TPluginEvents extends object = Record<never, never>> = { [K in keyof CoreRetryEvents | keyof TPluginEvents]: K extends keyof TPluginEvents ? K extends keyof CoreRetryEvents ? CoreRetryEvents[K] & TPluginEvents[K] : TPluginEvents[K] : K extends keyof CoreRetryEvents ? CoreRetryEvents[K] : never; }; /** * AxiosRetryer metrics * */ interface AxiosRetryerMetrics { totalRequests: number; successfulRetries: number; failedRetries: number; completelyFailedRequests: number; canceledRequests: number; completelyFailedCriticalRequests: number; errorTypes: { network: number; server5xx: number; client4xx: number; cancelled: number; }; retryAttemptsDistribution: Record<string, number>; requestCountsByPriority: Record<string, number>; retryPrioritiesDistribution: Record<string, { total: number; successes: number; failures: number; }>; queueWaitDuration: number; retryDelayDuration: number; } /** * Represents the distribution of different error types encountered */ interface ErrorTypesDistribution { /** Number of network-related errors (e.g., connection failures) */ network: number; /** Number of 5xx server errors */ server5xx: number; /** Number of 4xx client errors */ client4xx: number; /** Number of canceled requests */ cancelled: number; } /** * Represents metrics for a specific request priority level */ interface PriorityMetrics { /** The priority level (higher numbers indicate higher priority) */ priority: number; /** Total number of retry attempts for this priority */ total: number; /** Number of successful retries for this priority */ successes: number; /** Number of failed retries for this priority */ failures: number; /** Success rate percentage for this priority (0-100) */ successRate: number; /** Failure rate percentage for this priority (0-100) */ failureRate: number; } /** * AxiosRetryer detailed metrics * */ interface AxiosRetryerDetailedMetrics { /** Total number of requests made through the retryer */ totalRequests: number; /** Number of successfully completed retries */ successfulRetries: number; /** Number of failed retry attempts */ failedRetries: number; /** Requests that failed all retry attempts */ completelyFailedRequests: number; /** Requests canceled before completion */ canceledRequests: number; /** Critical priority requests that failed all retries */ completelyFailedCriticalRequests: number; /** Distribution of error types encountered */ errorTypesDistribution: ErrorTypesDistribution; /** Distribution of retry attempts across all requests */ retryAttemptsDistribution: Record<number, number>; /** Count of requests by priority level */ requestCountsByPriority: Record<number, number>; /** Average time spent in queue (seconds) */ avgQueueWait: number; /** Average delay between retry attempts (seconds) */ avgRetryDelay: number; /** Detailed metrics grouped by request priority */ priorityMetrics: PriorityMetrics[]; /** Timer health and accumulation metrics */ timerHealth: { /** Number of active internal timers */ activeTimers: number; /** Number of active retry timers */ activeRetryTimers: number; /** Health score (0 = excellent, 100+ = potential issues) */ healthScore: number; }; } /** * Interface for pluggable metrics recording. * The core library ships with no-op metrics by default. * Use MetricsPlugin for full metrics collection. */ interface MetricsRecorder { reset(): void; buildDetailedMetrics(timerStats: { activeTimers: number; activeRetryTimers: number; }): AxiosRetryerDetailedMetrics; emitMetricsUpdated?(): void; } /** * Logger interface used by RetryManager and its collaborators. * Supply a custom implementation via {@link RetryManagerOptions.logger} * to redirect or suppress log output. */ interface Logger { log(message: string, data?: unknown): void; error(message: string, error?: unknown): void; warn(message: string, data?: unknown): void; debug(message: string, meta?: unknown): void; } /** * By implementing this interface, we can write our own custom request store * */ interface RequestStore { /** * Add a request config to the store * */ add(request: AxiosRequestConfig): void; /** * Remove a request config to the store * */ remove(request: AxiosRequestConfig): void; /** * Get all request configs from the store * */ getAll(): AxiosRequestConfig[]; /** * Clear request store * */ clear(): void; } /** * Context object passed to plugins during initialization and teardown. * Provides the plugin-facing view of RetryManager capabilities including * plugin-only wiring hooks that are not part of the public manager API. */ interface PluginContext<TPluginEvents extends object = Record<never, never>> { /** The Axios instance managed by RetryManager. */ readonly axiosInstance: AxiosInstance; /** Returns the configured logger. */ getLogger(): Logger; /** Subscribe to a manager or plugin event. */ on<K extends keyof RetryManagerEvents<TPluginEvents>>(event: K, listener: RetryEventListener<RetryManagerEvents<TPluginEvents>, K>): void; /** Unsubscribe from a manager or plugin event. */ off<K extends keyof RetryManagerEvents<TPluginEvents>>(event: K, listener: RetryEventListener<RetryManagerEvents<TPluginEvents>, K>): boolean; /** Fire all listeners registered for this event. */ emit<K extends keyof RetryManagerEvents<TPluginEvents>>(event: K, ...args: RetryEventArgs<RetryManagerEvents<TPluginEvents>, K>): void; /** * Identical to `emit` — fires all listeners registered for this event. * * Kept for backward compatibility with existing plugins. There is no semantic * distinction from `emit`: prefer `emit` in new code. */ triggerAndEmit<K extends keyof RetryManagerEvents<TPluginEvents>>(event: K, ...args: RetryEventArgs<RetryManagerEvents<TPluginEvents>, K>): void; /** Cancel a specific in-flight or queued request by its ID. */ cancelRequest(requestId: string): void; /** Cancel all active and queued requests. */ cancelAllRequests(): void; /** Cancel only requests currently waiting in the queue. */ cancelQueuedRequests(): void; /** * Register a queue gate that must approve each request before it is dispatched. * Used by plugins that need to block request processing under certain conditions. */ registerQueueGate(name: string, canProcess: (request: AxiosRequestConfig) => boolean): void; /** Remove a previously registered queue gate. */ unregisterQueueGate(name: string): boolean; /** Trigger a queue drain pass. Useful after a gate condition changes. */ refreshQueue(): void; /** * Register or unregister a metrics recorder. * Pass `null` to detach. Used by MetricsPlugin to expose metric data to the RetryManager's getMetrics() method. */ registerMetricsRecorder(recorder: MetricsRecorder | null): void; /** * Return active timer counts. * Used by MetricsPlugin to populate the timerHealth section of detailed metrics. */ getTimerStats(): { activeTimers: number; activeRetryTimers: number; }; /** * Release lifecycle tracking for a request config and mark its queue slot complete. * Used by TokenRefreshPlugin when a tracked request is intercepted for token refresh. */ releaseRequestTracking(config: AxiosRequestConfig): void; } /** * AxiosRetryer plugin interface that can be attached with {@link RetryManager.use} and removed with {@link RetryManager.unuse} * */ interface RetryPlugin<TPluginEvents extends object = Record<never, never>> { /** * Plugin name. Should be unique * */ name: string; /** * Plugin version (e.g. 1.0.0) * */ version: string; /** * Phantom covariant marker for TypeScript to infer `TPluginEvents` at call sites * such as `manager.use(plugin)`. Never set this at runtime; implementations may * simply omit it (it is always `undefined`). * */ readonly _events?: Readonly<TPluginEvents>; /** * Called when the plugin is attached and initialized. * @param context Plugin context providing manager capabilities and plugin-only wiring hooks. * */ initialize: (context: PluginContext<TPluginEvents>) => void; /** * Called before the plugin is removed. * @param context Plugin context providing manager capabilities and plugin-only wiring hooks. * */ onBeforeDestroyed?: (context: PluginContext<TPluginEvents>) => void; } type MaybePromise<T> = T | Promise<T>; /** * Represents a cached item containing the AxiosResponse and the timestamp it was cached. */ interface CachedItem { response: AxiosResponse<unknown>; timestamp: number; ttr?: number; lastAccessedAt?: number; } interface CacheStorageEntry { readonly key: string; readonly value: CachedItem; } interface CacheStorage { get(key: string): MaybePromise<CachedItem | undefined>; set(key: string, value: CachedItem): MaybePromise<void>; delete(key: string): MaybePromise<void>; clear(): MaybePromise<void>; /** * Returns the adapter's full cache index. * Cleanup and non-exact invalidation operate on this index. */ entries(): MaybePromise<readonly CacheStorageEntry[]>; } /** * Options for the CachingPlugin. */ interface CachingPluginOptions { /** * Response header names that are stripped before a response is written to the cache. * This prevents sensitive per-user or session-establishing headers (e.g. `Set-Cookie`) * from being replayed to different callers who receive a cached response. * * Matching is case-insensitive. * * @default ['set-cookie'] */ sensitiveResponseHeaders?: readonly string[]; /** * If true, include the entire headers object in the cache key. * @default false */ compareHeaders?: boolean; /** * Duration (in milliseconds) a cached entry is considered fresh. * If 0, the cache never expires. * @default 0 */ timeToRevalidate?: number; /** * HTTP methods to cache. By default, only GET requests are cached. * @default [AXIOS_RETRYER_HTTP_METHODS.GET] */ cacheMethods?: readonly AxiosRetryerHttpMethod[]; /** * Interval in milliseconds to run cache cleanup. * If 0, periodic cleanup is disabled. * @default 0 */ cleanupInterval?: number; /** * Maximum age in milliseconds for cached items. * Items older than this will be removed during cleanup. * If 0, items don't expire based on age. * @default 0 */ maxAge?: number; /** * Maximum number of items to keep in cache. * If exceeded, oldest items will be removed first. * If 0, no limit is applied. * @default 1000 */ maxItems?: number; /** * If true, only requests that are retried will be cached. * Requests that are not retried will not be cached even if they are cacheable. * @default false */ cacheOnlyRetriedRequests?: boolean; /** * Indexed storage backend used for cache entries. * Custom adapters must implement `entries()` so cleanup and invalidation can * operate on the adapter's source of truth after restart or across processes. * Defaults to the built-in in-memory storage. */ storage?: CacheStorage; /** * If true, concurrent identical cacheable requests share the same in-flight response. * @default true */ dedupeConcurrentRequests?: boolean; /** * Allows custom cache key composition from canonical request parts. * The default builder uses normalized method, URL, params, body, and optional headers. */ cacheKeyBuilder?: CacheKeyBuilder; /** * When true, requests carrying authentication headers (Authorization, Cookie, * Proxy-Authorization, X-Auth-Token, X-API-Key) are excluded from caching. * This prevents cross-principal cache collisions on shared retryer instances. * * Set to `false` only for legitimate shared-cache use cases where all * principals should receive the same cached response. * * @default true */ skipWhenAuthPresent?: boolean; /** * Header names whose values are folded into the cache key, binding each * cache entry to the identity or context carried by those headers. * * Use this instead of (or together with) `skipWhenAuthPresent: false` * when you need per-principal caching rather than skipping the cache entirely. * * Header name matching is case-insensitive. * * @default [] * * @example * ```ts * // Cache responses per-user based on their Authorization header: * new CachingPlugin({ skipWhenAuthPresent: false, varyHeaders: ['Authorization'] }); * ``` */ varyHeaders?: readonly string[]; /** * Maximum byte size of a response body (measured via `JSON.stringify` length) that * will be written to the cache. Responses whose serialized body exceeds this limit * are served normally but not stored. * * Use this to prevent a single large response from consuming excessive memory. * Set to `0` to disable the check (no limit). * * @default 0 */ maxEntrySize?: number; } interface CachingRequestOptions { cache?: boolean; ttr?: number; } interface CacheKeyBuilderContext { readonly config: AxiosRequestConfig; readonly method: string; readonly normalizedUrl: string; readonly normalizedParams: string; readonly normalizedData: string; readonly normalizedHeaders: string; } type CacheKeyBuilder = (context: CacheKeyBuilderContext) => string; interface CachingPluginEvents { onCacheHit?: (payload: { keyFingerprint: string; config: AxiosRequestConfig; ageMs: number; }) => void; onCacheMiss?: (payload: { keyFingerprint: string; config: AxiosRequestConfig; reason: 'empty' | 'stale'; }) => void; onCacheInvalidated?: (payload: { count: number; matcher: 'all' | 'custom'; }) => void; } type CacheInvalidationMatcher = string | RegExp | { exact: string; } | { prefix: string; }; declare class InMemoryCacheStorage implements CacheStorage { private readonly storage; get(key: string): CachedItem | undefined; set(key: string, value: CachedItem): void; delete(key: string): void; clear(): void; entries(): readonly CacheStorageEntry[]; } declare class CachingPlugin implements RetryPlugin<CachingPluginEvents> { name: string; version: string; readonly _events?: Readonly<CachingPluginEvents>; private context; private interceptorIdReq; private interceptorIdRes; private static readonly CACHE_CLEANUP_TIMEOUT_MS; private static readonly CACHE_CLEANUP_DISABLE_AFTER; private readonly cache; private readonly inflight; private readonly cleanup; private readonly options; private readonly storage; constructor(options?: CachingPluginOptions); initialize(context: PluginContext<CachingPluginEvents>): void; onBeforeDestroyed(): void; private getCacheKeyFingerprint; /** * Checks if there is a fresh cached response and handles the request accordingly. */ private handleRequest; /** * Handles successful responses by caching them when appropriate. */ private handleResponseSuccess; private handleResponseError; /** * Generates a unique cache key based on the request configuration. */ buildCacheKey(config: AxiosRequestConfig): string; private runCacheCleanup; /** * Manually clears all cache entries. */ clearCache(): void | Promise<void>; /** * Invalidates cache entries using explicit exact-key, prefix, or RegExp matching. * * Plain string input is treated as an exact-key match. * * @param matcher The exact key, prefix matcher, or RegExp to match for invalidation * @returns The number of invalidated cache entries */ invalidateCache(matcher: CacheInvalidationMatcher): number | Promise<number>; /** * Returns current cache statistics. */ getCacheStats(): { size: number; oldestItemAge: number; newestItemAge: number; averageAge: number; }; private upsertCacheEntry; private touchCacheEntry; private deleteCacheEntry; private getRequestCachingOptions; private readCacheEntriesForScan; private syncLocalCache; private enforceMaxItemsBeforeUpsert; private persistCacheTouchIfNeeded; private invalidateCacheEntries; private buildCacheKeyContext; } declare class InvalidCacheKeyError extends Error { constructor(); } declare module 'axios' { interface AxiosRequestConfig { __cachingOptions?: CachingRequestOptions; } } /** * Creates a CachingPlugin instance. * Functional alternative to using the `new CachingPlugin()` constructor. * * The caching plugin stores responses from successful requests and returns them * for identical requests, reducing network traffic and improving performance. * * @param options Configuration options for the CachingPlugin * @returns A configured CachingPlugin instance * * @example * ```typescript * import { AXIOS_RETRYER_HTTP_METHODS } from 'axios-retryer'; * * const cachePlugin = createCachePlugin({ * timeToRevalidate: 60000, // Cache responses for 60 seconds * cacheMethods: [AXIOS_RETRYER_HTTP_METHODS.GET], // Only cache GET requests * cleanupInterval: 300000, // Run cleanup every 5 minutes * maxItems: 100, // Store at most 100 responses * compareHeaders: false // Don't include headers in cache key * }); * * manager.use(cachePlugin); * ``` */ declare function createCachePlugin(options?: CachingPluginOptions): CachingPlugin; declare const CIRCUIT_BREAKER_STATES: { readonly CLOSED: "CLOSED"; readonly OPEN: "OPEN"; readonly HALF_OPEN: "HALF_OPEN"; }; type CircuitBreakerState = (typeof CIRCUIT_BREAKER_STATES)[keyof typeof CIRCUIT_BREAKER_STATES]; declare const CIRCUIT_BREAKER_SCOPES: { readonly HOST: "host"; readonly URL: "url"; readonly HOST_AND_URL: "host+url"; }; type CircuitBreakerScope = (typeof CIRCUIT_BREAKER_SCOPES)[keyof typeof CIRCUIT_BREAKER_SCOPES]; /** * Configuration options for the Circuit Breaker behavior. */ interface CircuitBreakerOptions { /** * Number of consecutive failures required to trip the circuit. * Once this threshold is exceeded, the circuit transitions from `CLOSED` to `OPEN`. */ failureThreshold: number; /** * Duration (in milliseconds) the circuit remains in the `OPEN` state * before allowing a test request in the `HALF_OPEN` state. */ openTimeout: number; /** * Maximum number of test requests allowed in `HALF_OPEN` state * before deciding to either reset (back to `CLOSED`) or trip again to `OPEN`. */ halfOpenMax: number; /** * Number of successful test requests required in HALF_OPEN state to reset the circuit. * Must be <= halfOpenMax. */ successThreshold?: number; /** * If true, uses a sliding window approach to count failures over time rather than consecutive failures. */ useSlidingWindow?: boolean; /** * The duration (in milliseconds) of the sliding window when useSlidingWindow is true. */ slidingWindowSize?: number; /** * Callback function to determine which errors should contribute to circuit breaking. * If not provided, all errors count. */ shouldCountError?: (error: AxiosError) => boolean; /** * Adaptive timeout configuration. When true, the circuit breaker will track response times * and adjust timeouts accordingly. */ adaptiveTimeout?: boolean; /** * Percentile (0-1) to use for adaptive timeout calculation. Default is 0.95 (95th percentile). */ adaptiveTimeoutPercentile?: number; /** * Number of historical response times to track for adaptive timeout calculation. */ adaptiveTimeoutSampleSize?: number; /** * Timeout multiplier (e.g., 1.5 = 150% of the calculated percentile). */ adaptiveTimeoutMultiplier?: number; /** * Maximum number of unique scope keys tracked in the adaptive-timeout response-metrics map. * Prevents unbounded memory growth. When the cap is reached, the oldest entry is evicted. * Default: 500. */ maxTrackedScopes?: number; /** * Allow specific endpoints to be excluded from circuit breaking. * * String patterns use exact URL equality. `RegExp` patterns are tested against * the full request URL on every request — prefer string patterns when possible. * * **Security note (ReDoS):** Avoid catastrophically backtracking patterns such as * `/(a+)+$/` combined with long non-matching URLs. JavaScript's regex engine is * single-threaded and synchronous; a pathological pattern will block the event loop. * Validate any user-controlled or externally-sourced patterns before use. */ excludeUrls?: readonly (string | RegExp)[]; /** * Controls how circuit state is grouped. * `host+url` scopes by host and normalized URL, which is the default. */ scope?: CircuitBreakerScope | ((config: AxiosRequestConfig) => string); /** * Optional adapter for sharing circuit state across processes or hosts. */ stateAdapter?: CircuitBreakerStateAdapter; } interface CircuitBreakerFailureRecord { timestamp: number; url: string; status?: number; errorCode?: string; } interface CircuitBreakerScopeState { state: CircuitBreakerState; failureCount: number; successCount: number; halfOpenCount: number; nextAttempt: number; recentFailures: CircuitBreakerFailureRecord[]; lastFailureStatus?: number; lastFailureCode?: string; } interface CircuitBreakerScopeMetrics { scopeKey: string; url: string; host?: string; state: CircuitBreakerState; failureCount: number; halfOpenCount: number; successCount: number; nextAttemptIn: number; failuresInWindow: number; } interface CircuitBreakerAdaptiveTimeoutMetrics { scopeKey: string; url: string; host?: string; timeoutMs: number; p95ResponseTimeMs: number; samplesCount: number; } interface CircuitBreakerMetrics { state: CircuitBreakerState; failureCount: number; halfOpenCount: number; successCount: number; nextAttemptIn: number; failuresInWindow: number; adaptiveTimeouts: CircuitBreakerAdaptiveTimeoutMetrics[]; scopeMetrics: CircuitBreakerScopeMetrics[]; } interface CircuitBreakerStateAdapter { get(key: string): CircuitBreakerScopeState | undefined | Promise<CircuitBreakerScopeState | undefined>; set(key: string, state: CircuitBreakerScopeState): void | Promise<void>; delete(key: string): void | Promise<void>; clear(): void | Promise<void>; } interface CircuitBreakerPluginEvents { onCircuitStateChanged?: (payload: { scopeKey: string; from: CircuitBreakerState; to: CircuitBreakerState; reason: 'failure-threshold' | 'half-open-failure' | 'open-timeout-elapsed' | 'success-threshold-reached' | 'manual-reset'; nextAttemptIn?: number; }) => void; } interface ResponseTimeMetrics { times: number[]; sampleSize: number; lastCalculated: number; currentPercentileMs: number; scopeKey: string; normalizedUrl: string; host?: string; } /** * Default in-memory state adapter. Stores circuit state as deep clones to * prevent accidental mutation of internal state from external references. */ declare class InMemoryCircuitBreakerStateAdapter implements CircuitBreakerStateAdapter { private readonly state; get(key: string): CircuitBreakerScopeState | undefined; set(key: string, state: CircuitBreakerScopeState): void; delete(key: string): void; clear(): void; } /** * Enhanced CircuitBreakerPlugin with sliding window failure counting, * selective error monitoring, adaptive timeout management, granular recovery, * URL exclusion, scoped circuit state, and optional distributed state adapters. * * @implements {RetryPlugin} */ declare class CircuitBreakerPlugin implements RetryPlugin<CircuitBreakerPluginEvents> { static STATES: { readonly CLOSED: "CLOSED"; readonly OPEN: "OPEN"; readonly HALF_OPEN: "HALF_OPEN"; }; readonly name = "CircuitBreakerPlugin"; readonly version = "2.0.0"; readonly _events?: Readonly<CircuitBreakerPluginEvents>; private readonly _options; private _requestInterceptorId?; private _responseInterceptorId?; private _context; private readonly _adaptiveTimeoutTracker; private readonly _scopeManager; private readonly _failureWindow; private readonly _metricBaselines; constructor(options?: Partial<CircuitBreakerOptions>); /** @internal */ get _responseMetrics(): Record<string, ResponseTimeMetrics>; /** @internal */ get _scopeStateCache(): Map<string, CircuitBreakerScopeState>; /** @internal */ get _knownScopes(): Map<string, { scopeKey: string; normalizedUrl: string; host?: string; }>; /** @internal */ _normalizeUrl(url: string): string; /** @internal */ _resolveScopeKey(config: AxiosRequestConfig, normalizedUrl: string, host?: string): string; /** @internal */ _getScopeDetails(config: AxiosRequestConfig): { scopeKey: string; normalizedUrl: string; host?: string; }; /** @internal */ _writeScopeState(scopeKey: string, state: CircuitBreakerScopeState): Promise<void>; initialize(context: PluginContext<CircuitBreakerPluginEvents>): void; onBeforeDestroyed(context: PluginContext<CircuitBreakerPluginEvents>): void; getState(scopeKey?: string): CircuitBreakerState; manualReset(scopeKey?: string): void; /** * Backwards-compatible alias retained for existing tests and internal callers. * @internal */ _reset(scopeKey?: string): void; /** * @internal Exposed for test inspection only; not part of the public API. */ _trackResponseTime(response: AxiosResponse): void; resetMetrics(): void; getAdaptiveTimeoutMetrics(): CircuitBreakerMetrics['adaptiveTimeouts']; getMetrics(): CircuitBreakerMetrics; private _tripScope; private _resetScope; private _transitionToHalfOpen; private _emitStateChange; private _shouldCountError; private _isUrlExcluded; private _createCircuitStateError; private _getScopeMetrics; private _getVisibleCounter; private _getVisibleFailuresInWindow; } declare class CircuitBreakerStateError extends AxiosError { readonly circuitState: CircuitBreakerState; constructor(message: string, circuitState: CircuitBreakerState, request: AxiosRequestConfig, code?: string, response?: AxiosResponse); } /** * Creates a CircuitBreakerPlugin instance. * Functional alternative to using the `new CircuitBreakerPlugin()` constructor. * * The circuit breaker pattern prevents repeated requests to a failing service by * temporarily blocking requests after a threshold of failures is reached. * * @param options Configuration options for the CircuitBreakerPlugin * @returns A configured CircuitBreakerPlugin instance * * @example * ```typescript * const circuitBreaker = createCircuitBreaker({ * failureThreshold: 5, // Trip circuit after 5 consecutive failures * openTimeout: 30000, // Remain open for 30s before allowing half-open test * halfOpenMax: 1, // Allow 1 test request in half-open state * useSlidingWindow: true, // Use sliding window for failure analysis * slidingWindowSize: 60000 // 60-second sliding window * }); * * manager.use(circuitBreaker); * ``` */ declare function createCircuitBreaker(options?: Partial<CircuitBreakerOptions>): CircuitBreakerPlugin; interface SanitizeOptions { sensitiveHeaders?: readonly string[]; sensitiveFields?: readonly string[]; redactionChar?: string; sanitizeRequestData?: boolean; sanitizeResponseData?: boolean; sanitizeUrlParams?: boolean; allowedFields?: readonly string[]; allowlistOnly?: boolean; } interface DebugSanitizationPluginOptions { sanitizeOptions?: SanitizeOptions; } /** * Plugin that adds sanitized debug logging for requests and errors. * * Lazy-loads the sanitization module and logs sanitized request URLs, headers, * data, and error responses via the manager's logger. * * @example * ```typescript * import { DebugSanitizationPlugin } from 'axios-retryer/plugins/DebugSanitizationPlugin'; * * const debugPlugin = new DebugSanitizationPlugin({ * sanitizeOptions: { sensitiveHeaders: ['x-custom-secret'] } * }); * manager.use(debugPlugin); * ``` */ declare class DebugSanitizationPlugin implements RetryPlugin { name: string; version: string; private context; private interceptorIdReq; private interceptorIdRes; private readonly sanitizeOptions; constructor(options?: DebugSanitizationPluginOptions); initialize(context: PluginContext): void; onBeforeDestroyed(context: PluginContext): void; private logSanitizedRequest; private logSanitizedError; } /** * Creates a DebugSanitizationPlugin instance. * Functional alternative to using the `new DebugSanitizationPlugin()` constructor. * * The debug sanitization plugin adds sanitized debug logging for requests and errors, * redacting sensitive information like tokens, passwords, and API keys. * * @param options Configuration options for the DebugSanitizationPlugin * @returns A configured DebugSanitizationPlugin instance * * @example * ```typescript * const debugPlugin = createDebugSanitizationPlugin({ * sanitizeOptions: { * sensitiveHeaders: ['x-custom-secret'], * redactionChar: '#' * } * }); * * manager.use(debugPlugin); * ``` */ declare function createDebugSanitizationPlugin(options?: DebugSanitizationPluginOptions): DebugSanitizationPlugin; interface ManualRetryPluginEvents { onManualRetryProcessStarted?: () => void; onRequestRemovedFromStore?: (request: AxiosRequestConfig) => void; } interface ManualRetryPluginOptions { maxRequestsToStore?: number; manualRetryMaxAge?: number; storeNonIdempotent?: boolean; storeAuthRequests?: boolean; beforeRetry?: (config: AxiosRequestConfig) => AxiosRequestConfig | null; prepareRequestForStore?: (config: AxiosRequestConfig) => AxiosRequestConfig | null; rehydrateAuth?: (config: AxiosRequestConfig) => AxiosRequestConfig | null; requestStore?: RequestStore; } /** * Plugin that stores failed requests and allows replaying them later via `retryFailedRequests()`. * * Listens to the `onFailure` event to capture terminal failures and strips auth headers * before storage. Auth material is NOT re-applied on replay unless an explicit * `rehydrateAuth` hook is provided, preventing cross-principal replay. * * @example * ```typescript * import { ManualRetryPlugin } from 'axios-retryer/plugins/ManualRetryPlugin'; * * const manualRetry = new ManualRetryPlugin({ manualRetryMaxAge: 60_000 }); * manager.use(manualRetry); * * // Later, after failures: * const results = await manualRetry.retryFailedRequests(); * ``` */ declare class ManualRetryPlugin implements RetryPlugin<ManualRetryPluginEvents> { name: string; version: string; readonly _events?: Readonly<ManualRetryPluginEvents>; private context; private store; private readonly maxRequestsToStore; private readonly maxAge; private readonly storeNonIdempotent; private readonly storeAuthRequests; private readonly beforeRetryCallback?; private readonly prepareRequestForStoreCallback?; private readonly rehydrateAuthCallback?; private readonly customStore?; private onFailureHandler; constructor(options?: ManualRetryPluginOptions); initialize(context: PluginContext<ManualRetryPluginEvents>): void; onBeforeDestroyed(context: PluginContext<ManualRetryPluginEvents>): void; /** * Retries all stored failed requests that have not expired. * Requests older than `manualRetryMaxAge` are discarded. * * Replay is fail-fast: if any replayed request fails, the promise rejects * with that error and remaining stored requests are not replayed. * * @returns Array of replay responses. */ retryFailedRequests<T = unknown>(): Promise<AxiosResponse<T>[]>; /** * Returns a copy of all currently stored failed requests. */ getStoredRequests(): AxiosRequestConfig[]; /** * Clears all stored failed requests without retrying them. */ clearStoredRequests(): void; private isEligible; private prepareStoredRequest; /** * Prevents Axios from merging auth headers from `defaults.headers.common` * into the replayed request. Without this, a request that failed under user A * could be replayed with user B's token if the defaults changed between * failure and replay. */ private neutralizeDefaultAuthHeaders; } /** * Creates a ManualRetryPlugin instance. * Functional alternative to using the `new ManualRetryPlugin()` constructor. * * The manual retry plugin stores failed requests and allows replaying them * later via `retryFailedRequests()`. * * @param options Configuration options for the ManualRetryPlugin * @returns A configured ManualRetryPlugin instance * * @example * ```typescript * const manualRetry = createManualRetryPlugin({ * manualRetryMaxAge: 60000, // Discard requests older than 1 minute * maxRequestsToStore: 100, // Store at most 100 failed requests * storeNonIdempotent: false, // Only store idempotent requests * }); * * manager.use(manualRetry); * * // Later: * const results = await manualRetry.retryFailedRequests(); * ``` */ declare function createManualRetryPlugin(options?: ManualRetryPluginOptions): ManualRetryPlugin; interface MetricsPluginEvents { onMetricsUpdated?: (metrics: AxiosRetryerDetailedMetrics) => void; } /** * Plugin that enables detailed metrics collection for the RetryManager. * * Without this plugin, `getMetrics()` returns empty/zero metrics. * Install this plugin to track request counts, retry distributions, * error types, queue wait times, and priority breakdowns. * * @example * ```typescript * import { MetricsPlugin } from 'axios-retryer/plugins/MetricsPlugin'; * * const metricsPlugin = new MetricsPlugin(); * manager.use(metricsPlugin); * * // Later: read collected metrics * const metrics = manager.getMetrics(); * ``` */ declare class MetricsPlugin implements RetryPlugin<MetricsPluginEvents> { name: string; version: string; readonly _events?: Readonly<MetricsPluginEvents>; private context; private metrics; private collector; private readonly recorder; private readonly onRequestQueuedListener; private readonly onRequestDispatchedListener; private readonly onRequestCancelledListener; private readonly beforeRetryListener; private readonly afterRetryListener; private readonly onRetryScheduledListener; private readonly onFailureListener; private readonly onBlockingRequestFailedListener; private readonly onRequestSucceededListener; constructor(); initialize(context: PluginContext<MetricsPluginEvents>): void; onBeforeDestroyed(context: PluginContext<MetricsPluginEvents>): void; getMetrics(): AxiosRetryerDetailedMetrics; resetMetrics(): void; private emitMetricsUpdated; } declare class MetricsCollector implements MetricsRecorder { private readonly getMetricsState; private queueWaitHistory; private retryDelayHistory; constructor(getMetricsState: () => AxiosRetryerMetrics); recordRequestStart(priority: AxiosRetryerRequestPriority): void; recordQueueWait(durationMs: number): void; recordRetrySuccess(priority: AxiosRetryerRequestPriority): void; recordRetryFailure(priority: AxiosRetryerRequestPriority, error: AxiosError): void; recordRetryAttempt(attempt: number, priority: AxiosRetryerRequestPriority): void; recordRetryDelay(durationMs: number): void; reset(): void; recordCancellation(includeErrorType?: boolean): void; recordTerminalFailure(isCritical: boolean): void; buildDetailedMetrics(timerStats: { activeTimers: number; activeRetryTimers: number; }): AxiosRetryerDetailedMetrics; private getPriorityMetrics; private recordDuration; private pruneDurationHistory; private getWindowAverage; } /** * Creates a MetricsPlugin instance. * Functional alternative to using the `new MetricsPlugin()` constructor. * * @returns A configured MetricsPlugin instance * * @example * ```typescript * import { createMetricsPlugin } from 'axios-retryer/plugins/MetricsPlugin'; * * const metricsPlugin = createMetricsPlugin(); * manager.use(metricsPlugin); * ``` */ declare function createMetricsPlugin(): MetricsPlugin; interface TokenRefreshResult { /** * New access token. When `null` or `undefined` (or omitted), the plugin treats the refresh as a no-op: * no header update, no `onTokenRefreshed` / `onTokenRefreshFailed`, and no “failed refresh” short-circuit. * Concurrent waiters are released without forcing `TokenRefreshFailedError`. */ token?: string | null; } type TokenRefreshHandler = (axiosInst: AxiosInstance) => Promise<TokenRefreshResult>; interface TokenRefreshPluginEvents { onTokenRefreshed?: (newToken: string) => void; onTokenRefreshFailed?: () => void; onBeforeTokenRefresh?: () => void; } interface TokenRefreshPluginOptions { /** If true, allow multiple refresh attempts up to maxRefreshAttempts on failure. */ retryOnRefreshFail?: boolean; /** Maximum number of refresh attempts (1 => 1 total attempt, 2 => 2 attempts, etc.). */ maxRefreshAttempts?: number; /** Timeout in ms for each refresh call. */ refreshTimeout?: number; /** The HTTP header name to set with the new token (e.g. "Authorization"). */ authHeaderName?: string; /** A prefix for your token (commonly "Bearer "). */ tokenPrefix?: string; /** HTTP status codes that trigger a token refresh (e.g., [401, 419]). */ refreshStatusCodes?: readonly number[]; /** * Maximum backoff delay in ms between refresh retry attempts. * Caps the exponential backoff to prevent multi-minute stalls with high maxRefreshAttempts. * Default: 30_000 (30 seconds). */ maxRefreshBackoffMs?: number; /** * Maximum number of requests that may queue while a token refresh is in flight. * Excess requests reject with TokenRefreshQueueOverflowError. Default: 500. * Set to 0 or a negative number to disable the cap (not recommended). */ maxQueuedRequests?: number; /** * Optional function to detect auth errors in response bodies (for APIs that return 200 with error in body) * Return true if response contains an auth error that should trigger token refresh */ customErrorDetector?: (response: unknown) => boolean; } /** * A RetryPlugin that manages token refresh on certain status codes (e.g., 401). * It intercepts failed requests, attempts to refresh the token, * and re-dispatches any queued requests if refresh succeeds. * * Can also detect custom auth errors in response bodies for APIs that return 200 OK * with error messages in the body (like GraphQL). */ declare class TokenRefreshPlugin implements RetryPlugin<TokenRefreshPluginEvents> { name: string; version: string; readonly _events?: Readonly<TokenRefreshPluginEvents>; private context; private requestInterceptorId; private interceptorId; private responseInterceptorId; private refreshAxios; private isRefreshing; private refreshQueue; private readonly teardown; private timerManager; private failedAuthHeaderValue; private readonly refreshToken; private readonly options; private logger; constructor(refreshToken: TokenRefreshHandler, options?: TokenRefreshPluginOptions); /** * Called by RetryManager when we register this plugin via manager.use(plugin). * Attaches a response interceptor to the manager's axios instance and * creates a dedicated axios instance for refresh calls. */ initialize(context: PluginContext<TokenRefreshPluginEvents>): void; /** * Called when the plugin is removed. */ onBeforeDestroyed(context: PluginContext<TokenRefreshPluginEvents>): void; private withTeardown; private ensureActive; private bindRefreshErrorToRequest; /** * Returns true when the queue is at or above `maxQueuedRequests`. * A value <= 0 disables the cap. */ private isQueueOverflowing; private buildQueueOverflowError; private rejectQueueEntryWithBoundError; private dispose; /** * Checks successful responses for custom auth errors in the response body */ private handleSuccessResponse; /** * Intercepts a failed response. If the error status is refreshable and the request * hasn't already been retried, then either queues the request (if refresh is in progress) * or starts a new refresh cycle. */ private handleResponseError; /** * Checks if the error status code is in the list of refreshable status codes. */ private isRefreshableError; /** * Main token refresh flow: * 1) Set isRefreshing = true. * 2) Attempt to refresh the token. * 3) On success, update the auth header and retry both queued and original requests. * 4) On failure, clear the queue and reject. */ private handleTokenRefresh; /** * Attempts token refresh up to maxRefreshAttempts times if retryOnRefreshFail is true. * Each attempt is subject to a timeout defined in refreshTimeout. Throw a non-Axios * error from the refresh handler to stop remaining refresh retries immediately, for * example when no refresh token is available in storage. */ private executeTokenRefresh; /** * Updates the manager's default auth header so subsequent requests automatically carry the new token. */ private updateAuthHeader; /** * Retries the given request through the retry manager pipeline, * marking it with __isRetryRefreshRequest to avoid loops. */ private retryRequest; /** * If a 401 is encountered while a refresh is already in progress, queue the