davianspace_http_ratelimit 1.0.3 copy "davianspace_http_ratelimit: ^1.0.3" to clipboard
davianspace_http_ratelimit: ^1.0.3 copied to clipboard

Enterprise-grade HTTP rate limiting for Dart & Flutter with six algorithms, client/server pipelines, key-based admission control, and pluggable storage backends.

example/example.dart

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

import 'package:davianspace_http_ratelimit/davianspace_http_ratelimit.dart';
import 'package:davianspace_http_resilience/davianspace_http_resilience.dart';

/// ============================================================
/// davianspace_http_ratelimit — Production Usage Examples
/// ============================================================
///
/// This example demonstrates enterprise-ready patterns:
///
///  1. Token Bucket — burst-friendly client-side rate limiting
///  2. Fixed Window — simple quota enforcement
///  3. Sliding Window (counter) — approximate rolling window
///  4. Sliding Window Log — exact rolling window
///  5. Leaky Bucket — smooth output rate
///  6. Concurrency Limiter — cap in-flight requests
///  7. Server-side IP-based admission control
///  8. Server-side composite key (user + route)
///  9. Pipeline integration with HttpClientBuilder
///
/// Run with:
///   dart run example/example.dart
/// ============================================================

void main() async {
  await _example1TokenBucket();
  await _example2FixedWindow();
  await _example3SlidingWindowCounter();
  await _example4SlidingWindowLog();
  await _example5LeakyBucket();
  await _example6ConcurrencyLimiter();
  await _example7ServerSideIpRateLimit();
  await _example8ServerSideCompositeKey();
  _example9PipelineIntegration();
  print('\nAll examples completed.');
}

// ─────────────────────────────────────────────────────────────
// 1. Token Bucket — burst-friendly rate limiting
// ─────────────────────────────────────────────────────────────

Future<void> _example1TokenBucket() async {
  print('\n[Example 1] Token Bucket — burst up to capacity, then throttled');

  // Allow 100 requests/second with a burst of up to 200.
  final limiter = TokenBucketRateLimiter(
    capacity: 5,
    refillAmount: 2,
    refillInterval: const Duration(milliseconds: 100),
  );

  // Exhaust the initial burst.
  var allowed = 0;
  var rejected = 0;
  for (var i = 0; i < 8; i++) {
    if (limiter.tryAcquire()) {
      allowed++;
    } else {
      rejected++;
    }
  }
  print('  Burst phase: $allowed allowed, $rejected rejected');

  // Wait for one refill tick and try again.
  await Future<void>.delayed(const Duration(milliseconds: 120));
  if (limiter.tryAcquire()) {
    print('  After refill: 1 request allowed');
  }

  final s = limiter.statistics;
  print(
    '  Stats: acquired=${s.permitsAcquired}, '
    'rejected=${s.permitsRejected}, tokens=${s.currentPermits}',
  );
  limiter.dispose();
}

// ─────────────────────────────────────────────────────────────
// 2. Fixed Window — simple quota per time window
// ─────────────────────────────────────────────────────────────

Future<void> _example2FixedWindow() async {
  print('\n[Example 2] Fixed Window — 3 requests per 200 ms window');

  final limiter = FixedWindowRateLimiter(
    maxPermits: 3,
    windowDuration: const Duration(milliseconds: 200),
  );

  // Consume the window.
  for (var i = 1; i <= 4; i++) {
    final ok = limiter.tryAcquire();
    print('  Request $i: ${ok ? "ALLOWED" : "REJECTED (window exhausted)"}');
  }

  // Wait for the next window.
  await Future<void>.delayed(const Duration(milliseconds: 210));
  final ok = limiter.tryAcquire();
  print('  After window reset: ${ok ? "ALLOWED" : "REJECTED"}');
  limiter.dispose();
}

// ─────────────────────────────────────────────────────────────
// 3. Sliding Window Counter — approximate rolling window
// ─────────────────────────────────────────────────────────────

Future<void> _example3SlidingWindowCounter() async {
  print('\n[Example 3] Sliding Window Counter — approximate rolling window');

  // 5 requests per 500 ms rolling window.
  final limiter = SlidingWindowRateLimiter(
    maxPermits: 5,
    windowDuration: const Duration(milliseconds: 500),
  );

  var allowed = 0;
  var rejected = 0;
  for (var i = 0; i < 8; i++) {
    if (limiter.tryAcquire()) {
      allowed++;
    } else {
      rejected++;
    }
  }
  print('  $allowed allowed, $rejected rejected out of 8 attempts');

  // Partial window slide: wait ~260 ms (just over half the window).
  await Future<void>.delayed(const Duration(milliseconds: 260));
  final ok = limiter.tryAcquire();
  print(
    '  After ~half-window slide: ${ok ? "ALLOWED (window partially refreshed)" : "still REJECTED"}',
  );
  limiter.dispose();
}

// ─────────────────────────────────────────────────────────────
// 4. Sliding Window Log — exact rolling window
// ─────────────────────────────────────────────────────────────

Future<void> _example4SlidingWindowLog() async {
  print('\n[Example 4] Sliding Window Log — exact per-timestamp counting');

  // Exactly 3 requests per 300 ms rolling window.
  final limiter = SlidingWindowLogRateLimiter(
    maxPermits: 3,
    windowDuration: const Duration(milliseconds: 300),
  );

  limiter.tryAcquire(); // t=0
  limiter.tryAcquire(); // t≈0
  limiter.tryAcquire(); // t≈0 — window now full
  final rejected = !limiter.tryAcquire();
  print('  4th immediate request rejected: $rejected');

  // Wait for all three timestamps to expire.
  await Future<void>.delayed(const Duration(milliseconds: 310));
  final ok = limiter.tryAcquire();
  print('  After full window expiry: ${ok ? "ALLOWED" : "REJECTED"}');
  limiter.dispose();
}

// ─────────────────────────────────────────────────────────────
// 5. Leaky Bucket — smooth output rate
// ─────────────────────────────────────────────────────────────

Future<void> _example5LeakyBucket() async {
  print('\n[Example 5] Leaky Bucket — drains 1 request every 100 ms');

  // Queue capacity 3; one request leaks per 100 ms.
  final limiter = LeakyBucketRateLimiter(
    capacity: 3,
    leakInterval: const Duration(milliseconds: 100),
  );

  var allowed = 0;
  var rejected = 0;
  for (var i = 0; i < 5; i++) {
    if (limiter.tryAcquire()) {
      allowed++;
    } else {
      rejected++;
    }
  }
  print(
    '  Burst: $allowed queued, $rejected rejected (queue full after capacity)',
  );

  // Wait for one leak and try again.
  await Future<void>.delayed(const Duration(milliseconds: 110));
  if (limiter.tryAcquire()) {
    print('  After one leak: 1 request queued');
  }
  limiter.dispose();
}

// ─────────────────────────────────────────────────────────────
// 6. Concurrency Limiter — cap simultaneous in-flight requests
// ─────────────────────────────────────────────────────────────

Future<void> _example6ConcurrencyLimiter() async {
  print('\n[Example 6] Concurrency Limiter — at most 2 simultaneous requests');

  final limiter = ConcurrencyLimiter(maxConcurrency: 2);

  Future<void> simulateRequest(int id) async {
    print('  Request $id: acquiring...');
    await limiter.acquire();
    try {
      print('  Request $id: in-flight');
      await Future<void>.delayed(const Duration(milliseconds: 50));
      print('  Request $id: done');
    } finally {
      limiter.release();
    }
  }

  // Launch 4 requests concurrently; only 2 run at a time.
  await Future.wait([
    simulateRequest(1),
    simulateRequest(2),
    simulateRequest(3),
    simulateRequest(4),
  ]);

  print('  Final stats: ${limiter.statistics}');
  limiter.dispose();
}

// ─────────────────────────────────────────────────────────────
// 7. Server-side IP-based admission control
// ─────────────────────────────────────────────────────────────

Future<void> _example7ServerSideIpRateLimit() async {
  print('\n[Example 7] Server-side rate limiting — per client IP');

  // Allow 5 requests per minute per IP, using a Token Bucket.
  final server = ServerRateLimiter(
    limiterFactory: () => TokenBucketRateLimiter(
      capacity: 5,
      refillAmount: 5,
      refillInterval: const Duration(minutes: 1),
    ),
    repository: InMemoryRateLimiterRepository(),
    acquireTimeout: Duration.zero, // non-blocking
  );

  const extractor = IpKeyExtractor();

  // Simulate two different client IPs.
  final headersIp1 = {'x-forwarded-for': '203.0.113.1'};
  final headersIp2 = {'x-forwarded-for': '203.0.113.2'};
  final uri = Uri.parse('https://api.example.com/data');

  for (var i = 1; i <= 6; i++) {
    final key = extractor.extractKey(headersIp1, uri);
    final allowed = server.tryAllow(key);
    print('  IP-1 request $i: ${allowed ? "ALLOWED" : "RATE LIMITED"}');
  }

  // Second IP has its own independent quota.
  final key2 = extractor.extractKey(headersIp2, uri);
  final allowed = server.tryAllow(key2);
  print(
    '  IP-2 request 1: ${allowed ? "ALLOWED (separate quota)" : "RATE LIMITED"}',
  );

  server.dispose();
}

// ─────────────────────────────────────────────────────────────
// 8. Server-side composite key — user + route
// ─────────────────────────────────────────────────────────────

Future<void> _example8ServerSideCompositeKey() async {
  print('\n[Example 8] Server-side rate limiting — user + route composite key');

  // 10 requests per minute per (user, route) pair.
  final server = ServerRateLimiter(
    limiterFactory: () => FixedWindowRateLimiter(
      maxPermits: 10,
      windowDuration: const Duration(minutes: 1),
    ),
    repository: InMemoryRateLimiterRepository(),
    acquireTimeout: Duration.zero,
  );

  // Composite key: user-id + path, joined by the default ':' separator.
  final extractor = CompositeKeyExtractor(
    [
      const UserKeyExtractor(),
      const RouteKeyExtractor(),
    ],
  );

  final headers = {'x-user-id': 'user-42'};
  final endpoint1 = Uri.parse('https://api.example.com/orders');
  final endpoint2 = Uri.parse('https://api.example.com/products');

  // Same user, different routes → different quotas.
  final key1 = extractor.extractKey(headers, endpoint1);
  final key2 = extractor.extractKey(headers, endpoint2);
  print('  Key for user-42 + /orders:   $key1');
  print('  Key for user-42 + /products: $key2');

  print('  /orders allowed: ${server.tryAllow(key1)}');
  print('  /products allowed: ${server.tryAllow(key2)}');

  server.dispose();
}

// ─────────────────────────────────────────────────────────────
// 9. Pipeline integration with HttpClientBuilder
// ─────────────────────────────────────────────────────────────

void _example9PipelineIntegration() {
  print('\n[Example 9] HttpClientBuilder.withRateLimit() pipeline integration');

  // Allow 100 req/s with a 200-burst token bucket.
  // Requests that cannot acquire a permit within 500 ms are rejected with
  // RateLimitExceededException; the onRejected callback logs the event.
  final limiter = TokenBucketRateLimiter(
    capacity: 200,
    refillAmount: 100,
    refillInterval: const Duration(seconds: 1),
  );

  final client = HttpClientBuilder()
      .withBaseUri(Uri.parse('https://api.example.com'))
      .withDefaultHeader('Accept', 'application/json')
      .withRateLimit(
        RateLimitPolicy(
          limiter: limiter,
          acquireTimeout: const Duration(milliseconds: 500),
          respectServerHeaders: true,
          onRejected: (ctx, e) {
            // ctx is typed Object? in the policy callback — cast for access.
            print('  [rate-limit] Request rejected: $e');
          },
        ),
      )
      .build();

  print('  Client built. Rate-limit handler wired into pipeline.');
  print(
    '  Limiter stats: ${limiter.statistics.currentPermits}/'
    '${limiter.statistics.maxPermits} tokens available',
  );
  // When the client processes a request, the handler acquires a token before
  // forwarding to the next handler and releases it afterwards.  If a token
  // cannot be acquired within acquireTimeout, the request is short-circuited
  // and onRejected is invoked.  When respectServerHeaders is true, parsed
  // RateLimitHeaders are stored on the HttpContext under
  // HttpRateLimitHandler.rateLimitHeadersPropertyKey after each response.

  client.dispose();
  limiter.dispose();
}
0
likes
160
points
129
downloads

Documentation

API reference

Publisher

verified publisherdavian.space

Weekly Downloads

Enterprise-grade HTTP rate limiting for Dart & Flutter with six algorithms, client/server pipelines, key-based admission control, and pluggable storage backends.

Repository (GitHub)
View/report issues
Contributing

Topics

#rate-limiting #throttling #flutter #architecture #observability

License

MIT (license)

Dependencies

davianspace_dependencyinjection, davianspace_http_resilience

More

Packages that depend on davianspace_http_ratelimit