Tikfinity/Runtime/TikfinityRateLimiter.cs
using System;
using System.Collections.Generic;
namespace TikfinitySbox;
sealed class TikfinityRateLimiter
{
sealed class Counter
{
public long Window;
public int Count;
}
readonly Dictionary<string, Counter> _channels = new( StringComparer.OrdinalIgnoreCase );
readonly Dictionary<string, Counter> _users = new( StringComparer.OrdinalIgnoreCase );
long _lastPruneWindow;
public bool TryTake( string channelKey, string userKey, double now, int globalLimit, int userLimit )
{
var window = (long)Math.Floor( now );
var normalizedChannel = string.IsNullOrWhiteSpace( channelKey ) ? "unknown" : channelKey;
if ( !_channels.TryGetValue( normalizedChannel, out var channel ) )
{
channel = new Counter();
_channels[normalizedChannel] = channel;
}
if ( !Take( channel, window, globalLimit ) ) return false;
var normalizedUser = $"{normalizedChannel}|{(string.IsNullOrWhiteSpace( userKey ) ? "anonymous" : userKey)}";
if ( !_users.TryGetValue( normalizedUser, out var counter ) )
{
counter = new Counter();
_users[normalizedUser] = counter;
}
if ( !Take( counter, window, userLimit ) )
{
if ( channel.Count > 0 ) channel.Count--;
return false;
}
if ( window - _lastPruneWindow >= 10 )
{
_lastPruneWindow = window;
Prune( window );
}
return true;
}
static bool Take( Counter counter, long window, int limit )
{
if ( counter.Window != window )
{
counter.Window = window;
counter.Count = 0;
}
var safeLimit = limit < 1 ? 1 : limit;
if ( counter.Count >= safeLimit ) return false;
counter.Count++;
return true;
}
void Prune( long window )
{
var remove = new List<string>();
foreach ( var pair in _users )
{
if ( window - pair.Value.Window > 10 ) remove.Add( pair.Key );
}
foreach ( var key in remove ) _users.Remove( key );
remove.Clear();
foreach ( var pair in _channels )
{
if ( window - pair.Value.Window > 10 ) remove.Add( pair.Key );
}
foreach ( var key in remove ) _channels.Remove( key );
}
}