package gobackend import ( "sync" "time" ) // RateLimiter implements a sliding window rate limiter type RateLimiter struct { mu sync.Mutex maxRequests int window time.Duration timestamps []time.Time } // NewRateLimiter creates a new rate limiter with specified max requests per window func NewRateLimiter(maxRequests int, window time.Duration) *RateLimiter { return &RateLimiter{ maxRequests: maxRequests, window: window, timestamps: make([]time.Time, 0, maxRequests), } } // WaitForSlot blocks until a request is allowed under the rate limit // Returns immediately if under the limit, otherwise waits until a slot is available func (r *RateLimiter) WaitForSlot() { r.mu.Lock() defer r.mu.Unlock() now := time.Now() // Remove timestamps outside the window r.cleanOldTimestamps(now) // If under limit, record and return immediately if len(r.timestamps) < r.maxRequests { r.timestamps = append(r.timestamps, now) return } // Calculate wait time until oldest timestamp expires oldestTimestamp := r.timestamps[0] waitUntil := oldestTimestamp.Add(r.window) waitDuration := waitUntil.Sub(now) if waitDuration > 0 { // Release lock while waiting r.mu.Unlock() time.Sleep(waitDuration) r.mu.Lock() // Clean again after waiting r.cleanOldTimestamps(time.Now()) } // Record this request r.timestamps = append(r.timestamps, time.Now()) } // cleanOldTimestamps removes timestamps that are outside the current window func (r *RateLimiter) cleanOldTimestamps(now time.Time) { cutoff := now.Add(-r.window) validStart := 0 for i, ts := range r.timestamps { if ts.After(cutoff) { validStart = i break } validStart = i + 1 } if validStart > 0 { r.timestamps = r.timestamps[validStart:] } } // TryAcquire attempts to acquire a slot without blocking // Returns true if successful, false if rate limit would be exceeded func (r *RateLimiter) TryAcquire() bool { r.mu.Lock() defer r.mu.Unlock() now := time.Now() r.cleanOldTimestamps(now) if len(r.timestamps) < r.maxRequests { r.timestamps = append(r.timestamps, now) return true } return false } // Available returns the number of requests available in the current window func (r *RateLimiter) Available() int { r.mu.Lock() defer r.mu.Unlock() r.cleanOldTimestamps(time.Now()) return r.maxRequests - len(r.timestamps) } // Global SongLink rate limiter - 9 requests per minute (to be safe, limit is 10) var songLinkRateLimiter = NewRateLimiter(9, time.Minute) // GetSongLinkRateLimiter returns the global SongLink rate limiter func GetSongLinkRateLimiter() *RateLimiter { return songLinkRateLimiter }