Code/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 );
    }
}