UNPKG

axios-retryer

Version:

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

638 lines (627 loc) 24.2 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; }; /** * 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; } /** * 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; export { CachingPlugin, InMemoryCacheStorage, InvalidCacheKeyError, createCachePlugin }; export type { CacheInvalidationMatcher, CacheKeyBuilder, CacheKeyBuilderContext, CacheStorage, CacheStorageEntry, CachedItem, CachingPluginEvents, CachingPluginOptions, CachingRequestOptions };