tokenWindow = $windowFactory->createNumericWindow( identifier: "token_bucket:{$this->identifier}", windowSize: Duration::fromMinutes(1), // Short window for token tracking persistent: $this->persistent ); // Window for analytics and threat detection (longer window) $this->analyticsWindow = $windowFactory->createRateLimitWindow( identifier: "token_analytics:{$this->identifier}", windowSize: Duration::fromMinutes(5), // Longer window for pattern analysis persistent: $this->persistent ); } /** * Attempt to consume tokens */ public function consume(int $tokens = 1): RateLimitResult { $now = Timestamp::fromFloat($this->timeProvider->getCurrentTime()); // Record consumption attempt for analytics $this->analyticsWindow->record($tokens, $now); // Get current bucket state $currentTokens = $this->getCurrentTokens($now); if ($currentTokens >= $tokens) { // Consume tokens by recording the consumption $this->tokenWindow->record($tokens, $now); $remainingTokens = $currentTokens - $tokens; return RateLimitResult::allowed($this->capacity, $remainingTokens); } // Calculate retry after based on refill rate $tokensNeeded = $tokens - $currentTokens; $retryAfter = intval(ceil($tokensNeeded / $this->refillRate) * $this->refillInterval->toSeconds()); return RateLimitResult::exceeded($this->capacity, $currentTokens, $retryAfter); } /** * Consume tokens with advanced analytics */ public function consumeWithAnalysis(int $tokens = 1, array $requestContext = []): RateLimitResult { $basicResult = $this->consume($tokens); // Get analytics from the analytics window $now = Timestamp::fromFloat($this->timeProvider->getCurrentTime()); $stats = $this->analyticsWindow->getStats($now); $analysisData = [ 'token_consumption' => [ 'requested' => $tokens, 'available' => $basicResult->getCurrent(), 'capacity' => $this->capacity, 'refill_rate' => $this->refillRate, ], 'sliding_window_stats' => [ 'window_start' => $stats->windowStart->toFloat(), 'window_end' => $stats->windowEnd->toFloat(), 'total_requests' => $stats->totalCount, ], ]; // Add advanced analytics if available if (isset($stats->aggregatedData['rate_limit'])) { $rateLimitResult = $stats->aggregatedData['rate_limit']; if (method_exists($rateLimitResult, 'getBurstAnalysis')) { $analysisData['burst_analysis'] = $rateLimitResult->getBurstAnalysis(); $analysisData['threat_analysis'] = $rateLimitResult->threatAnalysis; } } // Enhance with request context if (! empty($requestContext)) { $analysisData = array_merge($analysisData, [ 'client_ip' => $requestContext['client_ip'] ?? null, 'user_agent' => $requestContext['user_agent'] ?? null, 'request_path' => $requestContext['request_path'] ?? null, ]); } return RateLimitResult::withThreatAnalysis( $basicResult->isAllowed(), $basicResult->getLimit(), $basicResult->getCurrent(), $analysisData, $basicResult->getRetryAfter() ); } /** * Check current token availability without consuming */ public function peek(): int { $now = Timestamp::fromFloat($this->timeProvider->getCurrentTime()); return $this->getCurrentTokens($now); } /** * Reset the token bucket */ public function reset(): void { $this->tokenWindow->clear(); $this->analyticsWindow->clear(); } /** * Get bucket capacity */ public function getCapacity(): int { return $this->capacity; } /** * Get refill rate (tokens per second) */ public function getRefillRate(): int { return $this->refillRate; } /** * Get analytics data */ public function getAnalytics(): array { $now = Timestamp::fromFloat($this->timeProvider->getCurrentTime()); $stats = $this->analyticsWindow->getStats($now); return [ 'identifier' => $this->identifier, 'capacity' => $this->capacity, 'refill_rate' => $this->refillRate, 'current_tokens' => $this->getCurrentTokens($now), 'analytics_window' => [ 'total_requests' => $stats->totalCount, 'window_start' => $stats->windowStart->toFloat(), 'window_end' => $stats->windowEnd->toFloat(), 'aggregated_data' => $stats->aggregatedData, ], ]; } /** * Calculate current available tokens based on refill rate and consumption */ private function getCurrentTokens(Timestamp $now): int { // Get consumption in the last refill period $refillWindowSize = Duration::fromSeconds( intval($this->capacity / $this->refillRate) + 60 // Buffer for precision ); $tokenStats = $this->tokenWindow->getStats($now); // Calculate tokens consumed in the refill window $tokensConsumed = 0; if (isset($tokenStats->aggregatedData['numeric'])) { $numericResult = $tokenStats->aggregatedData['numeric']; if (method_exists($numericResult, 'getSum')) { $tokensConsumed = intval($numericResult->getSum()); } } // Calculate tokens refilled since the oldest consumption $windowStart = $tokenStats->windowStart->toFloat(); $timeSinceWindowStart = $now->toFloat() - $windowStart; $tokensRefilled = min( $this->capacity, intval($timeSinceWindowStart * $this->refillRate) ); // Available tokens = capacity - consumed + refilled, capped at capacity $availableTokens = min( $this->capacity, max(0, $this->capacity - $tokensConsumed + $tokensRefilled) ); return $availableTokens; } }