smart_retry 1.0.0 copy "smart_retry: ^1.0.0" to clipboard
smart_retry: ^1.0.0 copied to clipboard

A production-ready retry library for Dart and Flutter featuring exponential backoff, advanced jitter strategies, conditional retries, typed exceptions, event streams, and zero-boilerplate async resilience.

example/main.dart

// ignore_for_file: avoid_print
import 'dart:io';

import 'package:smart_retry/smart_retry.dart';

// ════════════════════════════════════════════════════════════════════════════
// smart_retry — Runnable Examples
// ════════════════════════════════════════════════════════════════════════════
//
// Run with:  dart run example/main.dart
// ════════════════════════════════════════════════════════════════════════════

Future<void> main() async {
  print('╔══════════════════════════════════════════════════════════╗');
  print('║           smart_retry  —  runnable examples             ║');
  print('╚══════════════════════════════════════════════════════════╝\n');

  await example1SimpleRetry();
  await example2CustomOptions();
  await example3RetryIf();
  await example4OnRetryCallback();
  await example5ReusableOptions();
  await example6EventStream();
  await example7PreviewSchedule();
  await example8NonRetryableHandling();
  await example9ExhaustedHandling();

  SmartRetry.dispose();
  print('\n✅  All examples completed.');
}

// ────────────────────────────────────────────────────────────────────────────
// Example 1 — Simplest possible call
// ────────────────────────────────────────────────────────────────────────────
Future<void> example1SimpleRetry() async {
  _header('1', 'Simple retry — succeeds on 3rd attempt');

  var calls = 0;
  final result = await SmartRetry.run<String>(
    () async {
      calls++;
      if (calls < 3) throw SocketException('transient error (call $calls)');
      return 'user_data_42';
    },
    maxAttempts: 5,
    initialDelay: const Duration(milliseconds: 50),
    jitterStrategy: JitterStrategy.none, // deterministic for demo
  );

  print('  Result  : $result');
  print('  Calls   : $calls');
}

// ────────────────────────────────────────────────────────────────────────────
// Example 2 — Custom full options
// ────────────────────────────────────────────────────────────────────────────
Future<void> example2CustomOptions() async {
  _header('2', 'Custom options — exponential + equal jitter');

  var callCount2 = 0;
  try {
    await SmartRetry.run<void>(
      () async {
        callCount2++;
        if (callCount2 > 4) return; // satisfy unused-local-variable
        throw const SocketException('server overloaded');
      },
      maxAttempts: 4,
      initialDelay: const Duration(milliseconds: 20),
      maxDelay: const Duration(milliseconds: 200),
      factor: 2.0,
      backoffStrategy: BackoffStrategy.exponential,
      jitterStrategy: JitterStrategy.equal,
      debugMode: true, // ← prints each attempt to stdout
    );
  } on MaxAttemptsExceededException catch (e) {
    print('  Gave up : ${e.attempts} attempts, '
        '${e.totalElapsed.inMilliseconds}ms elapsed');
  }
}

// ────────────────────────────────────────────────────────────────────────────
// Example 3 — retryIf: only retry specific exceptions
// ────────────────────────────────────────────────────────────────────────────
Future<void> example3RetryIf() async {
  _header('3', 'retryIf — skip retry for non-transient errors');

  // Scenario A: SocketException → retryable
  var callsA = 0;
  try {
    await SmartRetry.run<void>(
      () async {
        callsA++;
        throw const SocketException('network blip');
      },
      maxAttempts: 3,
      initialDelay: Duration.zero,
      retryIf: (e) => e is SocketException,
    );
  } on MaxAttemptsExceededException catch (e) {
    print('  Scenario A — exhausted after ${e.attempts} attempts '
        '(SocketException is retryable)');
  }

  // Scenario B: FormatException → NOT retryable → abort on first try
  var callsB = 0;
  try {
    await SmartRetry.run<void>(
      () async {
        callsB++;
        throw const FormatException('invalid JSON from server');
      },
      maxAttempts: 10, // high maxAttempts — but retryIf will block them
      initialDelay: Duration.zero,
      retryIf: (e) => e is SocketException, // FormatException won't match
    );
  } on NonRetryableException catch (e) {
    print('  Scenario B — aborted immediately on attempt 1: ${e.cause}');
  }

  print('  callsA=$callsA (all used), callsB=$callsB (only 1 used)');
}

// ────────────────────────────────────────────────────────────────────────────
// Example 4 — onRetry callback for rich logging
// ────────────────────────────────────────────────────────────────────────────
Future<void> example4OnRetryCallback() async {
  _header('4', 'onRetry callback — full RetryContext');

  var callCount4 = 0;
  try {
    await SmartRetry.run<void>(
      () async {
        callCount4++;
        if (callCount4 > 4) return; // satisfy unused-local-variable
        throw const SocketException('temporary failure');
      },
      maxAttempts: 4,
      initialDelay: const Duration(milliseconds: 30),
      jitterStrategy: JitterStrategy.none,
      onRetry: (ctx) {
        print(
          '  [retry ${ctx.attemptNumber}/${ctx.maxAttempts}] '
          'next in ${ctx.nextDelay.inMilliseconds}ms | '
          'elapsed ${ctx.elapsedTime.inMilliseconds}ms | '
          '${ctx.remainingAttempts} left | '
          'error: ${ctx.lastException.runtimeType}',
        );
      },
    );
  } on MaxAttemptsExceededException {
    // silently expected
  }
}

// ────────────────────────────────────────────────────────────────────────────
// Example 5 — Shared RetryOptions across multiple API calls
// ────────────────────────────────────────────────────────────────────────────
Future<void> example5ReusableOptions() async {
  _header('5', 'Shared RetryOptions — reused across call-sites');

  final networkOpts = RetryOptions(
    maxAttempts: 4,
    initialDelay: const Duration(milliseconds: 30),
    maxDelay: const Duration(milliseconds: 300),
    factor: 2.0,
    jitterStrategy: JitterStrategy.full,
    retryIf: (e) => e is SocketException || e is HttpException,
    onRetry: (ctx) =>
        print('  [shared] retry ${ctx.attemptNumber}: ${ctx.lastException}'),
  );

  // Override only what's needed for a specific endpoint
  final uploadOpts = networkOpts.copyWith(
    maxAttempts: 6, // uploads may need more attempts
    maxDelay: const Duration(seconds: 1),
  );

  var userCalls = 0;
  final user = await SmartRetry.runWithOptions(
    () async {
      userCalls++;
      if (userCalls < 2) throw const SocketException('blip');
      return {'id': 1, 'name': 'Alice'};
    },
    options: networkOpts,
  );
  print('  User fetched: $user ($userCalls calls)');
  print('  Upload opts : $uploadOpts');
}

// ────────────────────────────────────────────────────────────────────────────
// Example 6 — Event stream for centralised monitoring
// ────────────────────────────────────────────────────────────────────────────
Future<void> example6EventStream() async {
  _header('6', 'Event stream — centralised monitoring');

  final log = <String>[];
  final sub = SmartRetry.events.listen((event) {
    switch (event) {
      case AttemptStarted(:final attemptNumber, :final maxAttempts):
        log.add('▶ start  $attemptNumber/$maxAttempts');
      case AttemptFailed(
          :final attemptNumber,
          :final exception,
          :final nextDelay
        ):
        log.add('⚠ failed $attemptNumber — '
            '${exception.runtimeType} — '
            'next: ${nextDelay.inMilliseconds}ms');
      case AttemptSucceeded(:final attemptNumber, :final totalElapsed):
        log.add('✅ ok     $attemptNumber — ${totalElapsed.inMilliseconds}ms');
      case AllAttemptsFailed(:final totalAttempts, :final lastException):
        log.add('💀 dead   $totalAttempts — ${lastException.runtimeType}');
      case RetryAborted(:final exception):
        log.add('🚫 abort  ${exception.runtimeType}');
    }
  });

  var calls = 0;
  await SmartRetry.run<int>(
    () async {
      calls++;
      if (calls < 3) throw const SocketException('x');
      return 0;
    },
    maxAttempts: 5,
    initialDelay: Duration.zero,
    retryIf: (e) => e is SocketException,
  );

  await Future<void>.delayed(const Duration(milliseconds: 20));
  await sub.cancel();

  for (final entry in log) {
    print('  $entry');
  }
}

// ────────────────────────────────────────────────────────────────────────────
// Example 7 — previewSchedule: see delays without running
// ────────────────────────────────────────────────────────────────────────────
Future<void> example7PreviewSchedule() async {
  _header('7', 'previewSchedule — inspect delay table');

  const opts = RetryOptions(
    maxAttempts: 6,
    initialDelay: Duration(milliseconds: 500),
    maxDelay: Duration(seconds: 20),
    factor: 2.0,
    backoffStrategy: BackoffStrategy.exponential,
    jitterStrategy: JitterStrategy.none, // deterministic for preview
  );

  final schedule = SmartRetry.previewSchedule(options: opts);

  print('  Retry | Base delay | Actual delay');
  print('  ------+------------+-------------');
  for (final row in schedule) {
    print(
      '   #${row.attempt.toString().padRight(4)}'
      '| ${_padMs(row.baseDelay)}  '
      '| ${_padMs(row.actualDelay)}',
    );
  }
}

String _padMs(Duration d) => '${d.inMilliseconds}ms'.padLeft(8);

// ────────────────────────────────────────────────────────────────────────────
// Example 8 — Handling NonRetryableException gracefully
// ────────────────────────────────────────────────────────────────────────────
Future<void> example8NonRetryableHandling() async {
  _header('8', 'NonRetryableException — graceful degradation');

  Future<String> fetchProfile(int userId) =>
      SmartRetry.run<String>(
        () async {
          // FormatException extends IOException → extends Exception — safe to catch
          if (userId == 0) throw const FormatException('userId must be > 0');
          throw const SocketException('server down');
        },
        maxAttempts: 3,
        initialDelay: Duration.zero,
        retryIf: (e) => e is SocketException, // FormatException → non-retryable
      );

  // Case A: programming error → NonRetryableException (abort immediately)
  try {
    await fetchProfile(0);
  } on NonRetryableException catch (e) {
    print('  ✋ Validation error, not retrying: ${e.cause}');
  } on MaxAttemptsExceededException {
    print('  This should not happen for userId=0');
  }

  // Case B: transient error → MaxAttemptsExceededException (retried 3 times)
  try {
    await fetchProfile(99);
  } on MaxAttemptsExceededException catch (e) {
    print('  🌐 Network down, gave up after ${e.attempts} attempts.');
  } on NonRetryableException {
    print('  This should not happen for userId=99');
  }
}

// ────────────────────────────────────────────────────────────────────────────
// Example 9 — Full exhaustion with detailed error reporting
// ────────────────────────────────────────────────────────────────────────────
Future<void> example9ExhaustedHandling() async {
  _header('9', 'Full exhaustion — detailed reporting');

  final retries = <RetryContext>[];

  try {
    await SmartRetry.run<void>(
      () async => throw const SocketException('backend unreachable'),
      maxAttempts: 3,
      initialDelay: const Duration(milliseconds: 10),
      jitterStrategy: JitterStrategy.none,
      onRetry: retries.add,
    );
  } on MaxAttemptsExceededException catch (e) {
    print('  Final report:');
    print('    • Attempts     : ${e.attempts}');
    print('    • Total time   : ${e.totalElapsed.inMilliseconds}ms');
    print('    • Last error   : ${e.lastException}');
    print('    • Retry delays : ${retries.map((c) => '${c.nextDelay.inMilliseconds}ms').join(', ')}');
  }
}

// ────────────────────────────────────────────────────────────────────────────
// Helpers
// ────────────────────────────────────────────────────────────────────────────
void _header(String n, String title) {
  print('─── Example $n: $title');
}
0
likes
150
points
6
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A production-ready retry library for Dart and Flutter featuring exponential backoff, advanced jitter strategies, conditional retries, typed exceptions, event streams, and zero-boilerplate async resilience.

Repository (GitHub)
View/report issues

Topics

#retry #backoff #network #resilience #http

License

MIT (license)

Dependencies

meta

More

Packages that depend on smart_retry