// // RateLimiter.swift // SportsTime // // Rate limiting for API providers to respect their rate limits. // import Foundation // MARK: - Rate Limiter /// Per-provider rate limiting to avoid hitting API limits actor RateLimiter { // MARK: - Types struct ProviderConfig { let name: String let minInterval: TimeInterval // Minimum time between requests let burstLimit: Int // Max requests in burst window let burstWindow: TimeInterval // Window for burst counting } // MARK: - Properties private var lastRequestTimes: [String: Date] = [:] private var requestCounts: [String: [Date]] = [:] private var configs: [String: ProviderConfig] = [:] // MARK: - Default Configurations /// Default provider rate limit configurations private static let defaultConfigs: [ProviderConfig] = [ ProviderConfig(name: "mlb_stats", minInterval: 0.1, burstLimit: 30, burstWindow: 60), // 10 req/sec ProviderConfig(name: "nhl_stats", minInterval: 0.2, burstLimit: 20, burstWindow: 60), // 5 req/sec ProviderConfig(name: "nba_stats", minInterval: 0.5, burstLimit: 10, burstWindow: 60), // 2 req/sec ProviderConfig(name: "espn", minInterval: 1.0, burstLimit: 30, burstWindow: 60), // 1 req/sec ProviderConfig(name: "sports_reference", minInterval: 3.0, burstLimit: 10, burstWindow: 60) // 1 req/3 sec ] // MARK: - Singleton static let shared = RateLimiter() private init() { // Load default configs for config in Self.defaultConfigs { configs[config.name] = config } } // MARK: - Configuration /// Configure rate limiting for a provider func configureProvider(_ config: ProviderConfig) { configs[config.name] = config } // MARK: - Rate Limiting /// Wait if needed to respect rate limits for a provider /// Returns immediately if rate limit allows, otherwise sleeps until allowed func waitIfNeeded(for provider: String) async { let config = configs[provider] ?? ProviderConfig( name: provider, minInterval: 1.0, burstLimit: 60, burstWindow: 60 ) await enforceMinInterval(for: provider, interval: config.minInterval) await enforceBurstLimit(for: provider, limit: config.burstLimit, window: config.burstWindow) recordRequest(for: provider) } /// Check if a request can be made without waiting func canMakeRequest(for provider: String) -> Bool { let config = configs[provider] ?? ProviderConfig( name: provider, minInterval: 1.0, burstLimit: 60, burstWindow: 60 ) // Check min interval if let lastRequest = lastRequestTimes[provider] { let elapsed = Date().timeIntervalSince(lastRequest) if elapsed < config.minInterval { return false } } // Check burst limit let now = Date() let windowStart = now.addingTimeInterval(-config.burstWindow) if let requests = requestCounts[provider] { let recentRequests = requests.filter { $0 > windowStart } if recentRequests.count >= config.burstLimit { return false } } return true } /// Get estimated wait time until next request is allowed func estimatedWaitTime(for provider: String) -> TimeInterval { let config = configs[provider] ?? ProviderConfig( name: provider, minInterval: 1.0, burstLimit: 60, burstWindow: 60 ) var maxWait: TimeInterval = 0 // Check min interval wait if let lastRequest = lastRequestTimes[provider] { let elapsed = Date().timeIntervalSince(lastRequest) if elapsed < config.minInterval { maxWait = max(maxWait, config.minInterval - elapsed) } } // Check burst limit wait let now = Date() let windowStart = now.addingTimeInterval(-config.burstWindow) if let requests = requestCounts[provider] { let recentRequests = requests.filter { $0 > windowStart }.sorted() if recentRequests.count >= config.burstLimit { // Need to wait until oldest request falls out of window if let oldestInWindow = recentRequests.first { let waitUntil = oldestInWindow.addingTimeInterval(config.burstWindow) let wait = waitUntil.timeIntervalSince(now) maxWait = max(maxWait, wait) } } } return maxWait } /// Reset rate limit tracking for a provider func reset(for provider: String) { lastRequestTimes.removeValue(forKey: provider) requestCounts.removeValue(forKey: provider) } /// Reset all rate limit tracking func resetAll() { lastRequestTimes.removeAll() requestCounts.removeAll() } // MARK: - Private Helpers private func enforceMinInterval(for provider: String, interval: TimeInterval) async { if let lastRequest = lastRequestTimes[provider] { let elapsed = Date().timeIntervalSince(lastRequest) if elapsed < interval { let waitTime = interval - elapsed try? await Task.sleep(nanoseconds: UInt64(waitTime * 1_000_000_000)) } } } private func enforceBurstLimit(for provider: String, limit: Int, window: TimeInterval) async { let now = Date() let windowStart = now.addingTimeInterval(-window) // Clean up old requests if var requests = requestCounts[provider] { requests = requests.filter { $0 > windowStart } requestCounts[provider] = requests // Check if at limit if requests.count >= limit { // Wait until oldest request falls out of window if let oldestInWindow = requests.sorted().first { let waitUntil = oldestInWindow.addingTimeInterval(window) let waitTime = waitUntil.timeIntervalSince(now) if waitTime > 0 { try? await Task.sleep(nanoseconds: UInt64(waitTime * 1_000_000_000)) } } } } } private func recordRequest(for provider: String) { let now = Date() lastRequestTimes[provider] = now if requestCounts[provider] == nil { requestCounts[provider] = [] } requestCounts[provider]?.append(now) // Clean up old requests periodically if let requests = requestCounts[provider], requests.count > 1000 { let oneHourAgo = now.addingTimeInterval(-3600) requestCounts[provider] = requests.filter { $0 > oneHourAgo } } } }