Skip to main content
Glama
github.rate.limiter.ts8.04 kB
/** * GitHub Rate Limiter Module * ======================== * * This module provides rate limiting functionality for GitHub API requests * to prevent hitting rate limits and handle rate limit responses gracefully. */ import { getApplicationConfiguration } from '../../configuration/index.export.js'; import { StructuredLoggingUtility } from '../../utilities/structured.logging.utility.js'; import { ApplicationErrorHandlingUtility } from '../../utilities/error.handling.utility.js'; import { asRecord } from '../../utilities/type.casting.utility.js'; /** * Simple mutex implementation to prevent race conditions */ class AsyncMutex { private _locking: Promise<void> = Promise.resolve(); private _locked = false; /** * Acquires the mutex lock * @returns Promise that resolves when the lock is acquired */ async acquire(): Promise<() => void> { // Wait for any ongoing operations to complete await this._locking; // Create a new promise to represent this lock let releaseFn: () => void; this._locked = true; // Set up the next locking promise this._locking = new Promise<void>(resolve => { releaseFn = () => { this._locked = false; resolve(); }; }); // Return the release function return releaseFn!; } /** * Checks if the mutex is currently locked * @returns Whether the mutex is locked */ get isLocked(): boolean { return this._locked; } } /** * Rate limit information */ interface RateLimitInfo { /** Number of requests remaining in the rate limit window */ remaining: number; /** Timestamp when the rate limit resets (in ms since epoch) */ resetTime: number; /** Maximum requests allowed per rate limit window */ limit: number; } /** * Rate limiting service for GitHub API requests */ export class GitHubRateLimiter { private mutex = new AsyncMutex(); private rateLimitInfo: RateLimitInfo = { remaining: 5000, // GitHub's default rate limit resetTime: Date.now() + 3600000, // Default assume 1 hour limit: 5000 }; private config = getApplicationConfiguration().rateLimiting; /** * Check if we should throttle requests based on rate limit state * * @returns Promise that resolves when it's safe to proceed * @throws Error if rate limit prevents the request */ async checkRateLimit(): Promise<void> { // Acquire lock to prevent race conditions const release = await this.mutex.acquire(); try { if (!this.config.enabled) { return; // Rate limiting disabled } const now = Date.now(); const resetTime = this.rateLimitInfo.resetTime; // If we've exceeded our safety threshold if (this.rateLimitInfo.remaining <= this.config.minRemaining) { const waitTime = resetTime - now + this.config.resetBufferMs; // If reset time is in the future, wait until reset if (waitTime > 0) { StructuredLoggingUtility.recordWarnEntry('Rate limit approached, throttling request', { remaining: this.rateLimitInfo.remaining, limit: this.rateLimitInfo.limit, resetInMs: waitTime }); // If wait time is too long, throw an error instead of waiting if (waitTime > 60000) { // Don't wait more than 1 minute throw ApplicationErrorHandlingUtility.createGithubApiError( 'GitHub API rate limit exceeded, cannot proceed with request', { remaining: this.rateLimitInfo.remaining, resetTime: new Date(resetTime).toISOString(), waitTime } ); } // Wait until rate limit resets // Store timeout reference to prevent memory leaks const timeoutPromise = new Promise(resolve => { const timeoutId = setTimeout(() => { resolve(null); clearTimeout(timeoutId); // Cleanup the timeout }, waitTime); }); await timeoutPromise; // Don't reset rate limit info manually // The next API call will update rate limit info from response headers StructuredLoggingUtility.recordInfoEntry('Waited for rate limit reset', { waitTimeMs: waitTime }); } } } finally { // Always release the lock release(); } } /** * Update rate limit information from API response headers * * @param headers - Response headers from GitHub API */ updateRateLimitFromHeaders(headers: Record<string, any>): void { // For updates, we need to ensure thread safety // We'll use a non-blocking approach by creating and then applying the update const remaining = headers['x-ratelimit-remaining']; const resetTime = headers['x-ratelimit-reset']; const limit = headers['x-ratelimit-limit']; if (remaining !== undefined && resetTime !== undefined && limit !== undefined) { // Create the new rate limit info const newRateLimitInfo = { remaining: parseInt(remaining, 10), resetTime: parseInt(resetTime, 10) * 1000, // Convert to milliseconds limit: parseInt(limit, 10) }; // Apply the update safely in an async context this.safelyUpdateRateLimitInfo(newRateLimitInfo); } } /** * Safely updates rate limit info with mutex protection * * @param newInfo - New rate limit information */ private async safelyUpdateRateLimitInfo(newInfo: RateLimitInfo): Promise<void> { // Use mutex to prevent race conditions const release = await this.mutex.acquire(); try { // Update the rate limit info this.rateLimitInfo = newInfo; StructuredLoggingUtility.recordDebugEntry('Rate limit info updated', asRecord(this.rateLimitInfo)); } finally { // Always release the lock release(); } } /** * Handle rate limit exceeded error * * @param retryAfter - Optional retry-after header value * @returns Promise that resolves when it's safe to retry * @throws Error if retry isn't possible */ async handleRateLimitExceeded(retryAfter?: string): Promise<void> { // Acquire lock to prevent race conditions const release = await this.mutex.acquire(); try { let waitTime = this.config.resetBufferMs; // Use retry-after header if available if (retryAfter) { waitTime = parseInt(retryAfter, 10) * 1000; } else if (this.rateLimitInfo.resetTime > Date.now()) { waitTime = this.rateLimitInfo.resetTime - Date.now() + this.config.resetBufferMs; } StructuredLoggingUtility.recordWarnEntry('Rate limit exceeded, waiting to retry', { waitTimeMs: waitTime }); // If wait time is too long, throw an error instead of waiting if (waitTime > 120000) { // Don't wait more than 2 minutes throw ApplicationErrorHandlingUtility.createGithubApiError( 'GitHub API rate limit exceeded, retry not possible', { resetTime: new Date(this.rateLimitInfo.resetTime).toISOString(), waitTime } ); } // Wait until we can retry with proper cleanup const timeoutPromise = new Promise(resolve => { const timeoutId = setTimeout(() => { resolve(null); clearTimeout(timeoutId); // Cleanup the timeout }, waitTime); }); await timeoutPromise; } finally { // Always release the lock release(); } } } /** * Singleton instance of the GitHub rate limiter */ let limiterInstance: GitHubRateLimiter | null = null; /** * Gets the GitHub rate limiter instance * @returns GitHub rate limiter instance */ export function getGitHubRateLimiter(): GitHubRateLimiter { if (!limiterInstance) { limiterInstance = new GitHubRateLimiter(); } return limiterInstance; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/github-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server