Files
Ensemble/lib/services/retry_helper.dart
Claude db0cefe612 Add retry logic with exponential backoff for network resilience
- Created RetryHelper service with three retry strategies:
  - retry(): Generic retry with configurable backoff
  - retryNetwork(): Optimized for network operations (4 attempts, 2-16s backoff)
  - retryCritical(): Aggressive retry for critical operations (5 attempts, 1-10s backoff)

- Applied retry logic to critical API methods:
  - getPlayers(): Essential for player list loading
  - getQueue(): Critical for now playing bar
  - getAlbumTracks(): Required for album playback
  - getPlaylistTracks(): Required for playlist playback
  - playTracks(): Critical for playback operations
  - search(): User-facing search functionality

- Network-aware retry detection:
  - Automatically retries on socket/network/timeout/connection errors
  - Smart error filtering to avoid retrying non-transient errors
  - Exponential backoff to prevent server overload

- Comprehensive logging:
  - Logs each retry attempt with delay duration
  - Logs final failure after all retries exhausted
  - Maintains visibility into retry behavior

This improves app reliability under poor network conditions and handles
transient network failures gracefully.
2025-11-22 23:42:41 +00:00

88 lines
2.7 KiB
Dart

import 'dart:async';
import 'debug_logger.dart';
class RetryHelper {
static final _logger = DebugLogger();
/// Retry a function with exponential backoff
///
/// [operation] - The async function to retry
/// [maxAttempts] - Maximum number of attempts (default: 3)
/// [initialDelay] - Initial delay in seconds (default: 2)
/// [maxDelay] - Maximum delay in seconds (default: 16)
/// [shouldRetry] - Optional function to determine if we should retry based on error
static Future<T> retry<T>({
required Future<T> Function() operation,
int maxAttempts = 3,
int initialDelaySeconds = 2,
int maxDelaySeconds = 16,
bool Function(dynamic error)? shouldRetry,
}) async {
int attempt = 0;
int delaySeconds = initialDelaySeconds;
while (true) {
attempt++;
try {
return await operation();
} catch (error) {
// Check if we should retry this error
final canRetry = shouldRetry?.call(error) ?? true;
// If this was the last attempt or we shouldn't retry, rethrow
if (attempt >= maxAttempts || !canRetry) {
_logger.log('❌ Operation failed after $attempt attempts: $error');
rethrow;
}
// Log the retry attempt
_logger.log(
'⚠️ Attempt $attempt/$maxAttempts failed: $error. Retrying in ${delaySeconds}s...'
);
// Wait before retrying (exponential backoff)
await Future.delayed(Duration(seconds: delaySeconds));
// Double the delay for next attempt, but cap at maxDelay
delaySeconds = (delaySeconds * 2).clamp(initialDelaySeconds, maxDelaySeconds);
}
}
}
/// Retry specifically for network operations
/// Uses default settings optimized for network calls
static Future<T> retryNetwork<T>({
required Future<T> Function() operation,
int maxAttempts = 4, // More attempts for network
}) {
return retry(
operation: operation,
maxAttempts: maxAttempts,
initialDelaySeconds: 2,
maxDelaySeconds: 16,
shouldRetry: (error) {
// Retry on network-related errors
final errorString = error.toString().toLowerCase();
return errorString.contains('socket') ||
errorString.contains('network') ||
errorString.contains('timeout') ||
errorString.contains('connection');
},
);
}
/// Retry for critical operations that should almost never fail
/// Uses more aggressive retry strategy
static Future<T> retryCritical<T>({
required Future<T> Function() operation,
}) {
return retry(
operation: operation,
maxAttempts: 5,
initialDelaySeconds: 1,
maxDelaySeconds: 10,
);
}
}