import { calculateBackoffDelay } from "@utils/helpers"; import { logger } from "@utils/logger"; import { ApiClientConfig, ApiError, ApiRequest } from "@utils/types"; export class RetryStrategy { private config: ApiClientConfig["retry"]; constructor(config: ApiClientConfig) { this.config = config.retry; logger.log( "RetryStrategy initialized with maxAttempts:", this.config.maxAttempts ); } /** * Checks if a failed request should be retried based on its error/response * and the current retry count. * @param request The current ApiRequest object. * @param error The potential ApiError that occurred. * @returns {boolean} True if the request should be retried. */ public shouldRetry(request: ApiRequest, error: ApiError): boolean { if (request.options.retryable === false) { logger.log( `Request ${request.id} skipped retry: explicitly set to non-retryable.` ); return false; } if (request.retryCount >= this.config.maxAttempts) { logger.log( `Request ${request.id} failed retry limit: ${request.retryCount}/${this.config.maxAttempts}` ); return false; } let isRetryableError = false; if (error.isNetworkError || error.isTimeoutError) { isRetryableError = true; } else if (error.response?.status) { isRetryableError = this.config.statusCodes.includes( error.response.status ); } if (!isRetryableError) { logger.log( `Request ${request.id} skipped retry: non-retryable error/status code.`, error.classification ); } return isRetryableError; } /** * Calculates the delay before the next attempt using exponential backoff. * @param request The current ApiRequest object. * @returns {number} The delay in milliseconds. */ public getNextDelay(request: ApiRequest): number { const delay = calculateBackoffDelay( request.retryCount + 1, this.config.delayMs ); logger.warn( `Request ${request.id} retrying in ${Math.round( delay / 1000 )}s (Attempt ${request.retryCount + 1})` ); return delay; } }