axios-retryer
Version:
TypeScript-first Axios retry library with concurrency limits, request priority, token refresh, response caching, and circuit breaker plugins.
465 lines (455 loc) • 18.3 kB
TypeScript
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;
};
/**
* 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;
}
/**
* 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 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;
export { MetricsCollector, MetricsPlugin, createMetricsPlugin };
export type { MetricsPluginEvents, MetricsRecorder };