UNPKG

axios-retryer

Version:

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

420 lines (411 loc) 16.7 kB
import { AxiosRequestConfig, AxiosError, AxiosInstance } from 'axios'; 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; } 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; export { DebugSanitizationPlugin, createDebugSanitizationPlugin }; export type { DebugSanitizationPluginOptions, SanitizeOptions };