package tools import ( "sync" "time" ) // RateLimiter tracks tool call counts per key (typically roomID) using a // sliding window. It is safe for concurrent use. type RateLimiter struct { maxCalls int window time.Duration mu sync.Mutex buckets map[string][]time.Time } // NewRateLimiter creates a rate limiter that allows maxCalls per window per key. func NewRateLimiter(maxCalls int, window time.Duration) *RateLimiter { return &RateLimiter{ maxCalls: maxCalls, window: window, buckets: make(map[string][]time.Time), } } // Allow checks whether a call for the given key is within the rate limit. // If allowed, it records the call and returns true. Otherwise returns false. func (rl *RateLimiter) Allow(key string) bool { rl.mu.Lock() defer rl.mu.Unlock() now := time.Now() cutoff := now.Add(-rl.window) // Trim expired entries calls := rl.buckets[key] start := 0 for start < len(calls) && calls[start].Before(cutoff) { start++ } calls = calls[start:] if len(calls) >= rl.maxCalls { rl.buckets[key] = calls return false } rl.buckets[key] = append(calls, now) return true } // Cleanup removes stale entries for keys that have no recent calls. // Should be called periodically to prevent memory growth. func (rl *RateLimiter) Cleanup() { rl.mu.Lock() defer rl.mu.Unlock() cutoff := time.Now().Add(-rl.window) for key, calls := range rl.buckets { start := 0 for start < len(calls) && calls[start].Before(cutoff) { start++ } if start >= len(calls) { delete(rl.buckets, key) } else { rl.buckets[key] = calls[start:] } } }