using Predictalytics.Application.Interfaces;
using Predictalytics.Domain.Enums;
using System.Collections.Concurrent;
namespace Predictalytics.Application.Services;
///
/// Token-bucket rate limiter with per-platform configuration.
///
public class RateLimiterService : IRateLimiter
{
private readonly ConcurrentDictionary _semaphores = new();
private readonly ConcurrentDictionary _lastRequest = new();
private readonly ConcurrentDictionary _blockedUntil = new();
// Minimum delay between requests per platform (milliseconds)
private static readonly Dictionary PlatformDelays = new()
{
{ PlatformType.Polymarket, 200 },
{ PlatformType.Limitless, 500 },
{ PlatformType.Azuro, 1000 },
{ PlatformType.Myriad, 1000 },
{ PlatformType.PredictFun, 1000 },
{ PlatformType.Kalshi, 500 },
{ PlatformType.Stake, 1000 }
};
public async Task WaitAsync(PlatformType platform, CancellationToken ct = default)
{
var sem = _semaphores.GetOrAdd(platform, _ => new SemaphoreSlim(1, 1));
await sem.WaitAsync(ct);
try
{
// 1. Check if we are currently blocked due to a 429
if (_blockedUntil.TryGetValue(platform, out var blockedUntil))
{
var waitTime = blockedUntil - DateTime.UtcNow;
if (waitTime > TimeSpan.Zero)
{
await Task.Delay(waitTime, ct);
}
}
// 2. Normal token bucket delay
if (_lastRequest.TryGetValue(platform, out var last))
{
var delayMs = PlatformDelays.GetValueOrDefault(platform, 1000);
var elapsed = (DateTime.UtcNow - last).TotalMilliseconds;
if (elapsed < delayMs)
await Task.Delay((int)(delayMs - elapsed), ct);
}
_lastRequest[platform] = DateTime.UtcNow;
}
finally { sem.Release(); }
}
public bool CanMakeRequest(PlatformType platform)
{
if (_blockedUntil.TryGetValue(platform, out var blockedUntil) && blockedUntil > DateTime.UtcNow)
return false;
if (!_lastRequest.TryGetValue(platform, out var last)) return true;
var delayMs = PlatformDelays.GetValueOrDefault(platform, 1000);
return (DateTime.UtcNow - last).TotalMilliseconds >= delayMs;
}
public void ReportRateLimitExceeded(PlatformType platform, TimeSpan? retryAfter = null)
{
var penalty = retryAfter ?? TimeSpan.FromSeconds(30);
_blockedUntil[platform] = DateTime.UtcNow.Add(penalty);
}
}