using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.RedditComments.Services;
///
/// Sliding-window rate limiter. Guarantees no more than N requests per rolling 60 seconds,
/// no matter how many callers are waiting.
///
public class RateLimiter
{
private readonly object _lock = new object();
private readonly Queue _requestTimestamps = new Queue();
private readonly int? _maxPerMinuteOverride;
///
/// Initializes a new instance of the class.
///
/// Optional fixed limit (used by tests); otherwise the plugin configuration is used.
public RateLimiter(int? maxPerMinuteOverride = null)
{
_maxPerMinuteOverride = maxPerMinuteOverride;
}
private int MaxPerMinute
{
get
{
if (_maxPerMinuteOverride.HasValue)
{
return _maxPerMinuteOverride.Value;
}
return ReadConfiguredLimit();
}
}
private static int ReadConfiguredLimit()
{
var configured = Plugin.Instance?.Configuration.MaxRequestsPerMinute ?? 60;
// Reddit's own hard limit for OAuth clients is 100 req/min; never exceed it.
return Math.Clamp(configured, 1, 100);
}
///
/// Waits until a request slot is available, then reserves it.
///
/// Cancellation token.
/// A task that completes when the caller may perform one request.
public async Task WaitAsync(CancellationToken cancellationToken = default)
{
while (true)
{
TimeSpan delay;
lock (_lock)
{
var now = DateTime.UtcNow;
while (_requestTimestamps.Count > 0 && now - _requestTimestamps.Peek() >= TimeSpan.FromMinutes(1))
{
_requestTimestamps.Dequeue();
}
if (_requestTimestamps.Count < MaxPerMinute)
{
_requestTimestamps.Enqueue(now);
return;
}
// Window is full: wait until the oldest request falls out of the window.
delay = TimeSpan.FromMinutes(1) - (now - _requestTimestamps.Peek()) + TimeSpan.FromMilliseconds(100);
}
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
}
}
}