/// Retry policy with exponential backoff for network requests library retry_policy; import 'dart:async'; import 'dart:io'; import 'dart:math'; /// Configuration for retry behavior class RetryConfig { /// Maximum number of retry attempts final int maxAttempts; /// Initial delay before first retry (milliseconds) final int initialDelayMs; /// Maximum delay between retries (milliseconds) final int maxDelayMs; /// Multiplier for exponential backoff final double backoffMultiplier; /// Whether to add jitter to retry delays final bool useJitter; const RetryConfig({ this.maxAttempts = 3, this.initialDelayMs = 1000, this.maxDelayMs = 30000, this.backoffMultiplier = 2.0, this.useJitter = true, }); /// Default configuration for standard API calls static const standard = RetryConfig(); /// Configuration for critical operations static const critical = RetryConfig( maxAttempts: 5, initialDelayMs: 500, maxDelayMs: 60000, ); /// Configuration for background sync static const backgroundSync = RetryConfig( maxAttempts: 10, initialDelayMs: 2000, maxDelayMs: 120000, ); } /// Retry policy implementation with exponential backoff class RetryPolicy { final RetryConfig config; final Random _random = Random(); RetryPolicy({RetryConfig? config}) : config = config ?? RetryConfig.standard; /// Executes an operation with retry logic /// /// [operation]: The async operation to execute /// [shouldRetry]: Optional function to determine if error is retryable /// [onRetry]: Optional callback when retry attempt is made /// /// Returns the result of the operation /// Throws the last error if all retries fail Future execute({ required Future Function() operation, bool Function(dynamic error)? shouldRetry, void Function(int attempt, dynamic error, Duration delay)? onRetry, }) async { int attempt = 0; dynamic lastError; while (attempt < config.maxAttempts) { try { return await operation(); } catch (error) { lastError = error; attempt++; // Check if we should retry this error final retryable = shouldRetry?.call(error) ?? _isRetryableError(error); if (!retryable || attempt >= config.maxAttempts) { throw error; } // Calculate delay with exponential backoff final delay = _calculateDelay(attempt); // Notify about retry onRetry?.call(attempt, error, delay); // Wait before next attempt await Future.delayed(delay); } } // Should never reach here, but throw last error just in case throw lastError!; } /// Calculates delay for given attempt number Duration _calculateDelay(int attempt) { // Exponential backoff: initialDelay * (multiplier ^ (attempt - 1)) final exponentialDelay = config.initialDelayMs * pow(config.backoffMultiplier, attempt - 1).toInt(); // Cap at max delay var delayMs = min(exponentialDelay, config.maxDelayMs); // Add jitter to prevent thundering herd if (config.useJitter) { final jitter = _random.nextDouble() * 0.3; // ±30% jitter delayMs = (delayMs * (1 + jitter - 0.15)).toInt(); } return Duration(milliseconds: delayMs); } /// Determines if an error is retryable bool _isRetryableError(dynamic error) { // Network errors are retryable if (error is TimeoutException) return true; if (error is SocketException) return true; // HTTP status codes that are retryable if (error.toString().contains('500')) return true; // Internal Server Error if (error.toString().contains('502')) return true; // Bad Gateway if (error.toString().contains('503')) return true; // Service Unavailable if (error.toString().contains('504')) return true; // Gateway Timeout if (error.toString().contains('429')) return true; // Too Many Requests // Client errors (4xx) are generally not retryable if (error.toString().contains('400')) return false; // Bad Request if (error.toString().contains('401')) return false; // Unauthorized if (error.toString().contains('403')) return false; // Forbidden if (error.toString().contains('404')) return false; // Not Found // Default: don't retry unknown errors return false; } } /// Extension to add retry capability to Future operations extension RetryExtension on Future Function() { /// Executes this operation with retry logic Future withRetry({ RetryConfig? config, bool Function(dynamic error)? shouldRetry, void Function(int attempt, dynamic error, Duration delay)? onRetry, }) { final policy = RetryPolicy(config: config); return policy.execute( operation: this, shouldRetry: shouldRetry, onRetry: onRetry, ); } }