UNPKG

mcp-ynab

Version:

Model Context Protocol server for YNAB integration

192 lines (160 loc) 5.42 kB
/** * RequestTracker - Monitor YNAB API rate limits (200 requests/hour) * Part of GlobalCacheManager system for Phase 2.5 */ class RequestTracker { constructor() { this.requests = []; // Array of {timestamp, endpoint, budgetId, tool} this.hourLimit = 200; // YNAB API limit: 200 requests per hour this.warningThreshold = 0.8; // Warning at 80% usage (160 requests) this.blockThreshold = 0.95; // Block at 95% usage (190 requests) // Cleanup expired requests every 5 minutes this.cleanupInterval = setInterval(() => { this.cleanup(); }, 5 * 60 * 1000); } /** * Track a new API request */ trackRequest(endpoint, budgetId = null, tool = null) { const request = { timestamp: Date.now(), endpoint, budgetId, tool, id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}` }; this.requests.push(request); // Log the request for monitoring console.error(`[RequestTracker] API call tracked: ${endpoint} (${this.getCurrentUsage().used}/${this.hourLimit})`); return request.id; } /** * Get current usage statistics */ getCurrentUsage() { this.cleanup(); // Ensure we have current data const now = Date.now(); const oneHourAgo = now - (60 * 60 * 1000); const recentRequests = this.requests.filter(req => req.timestamp > oneHourAgo); const used = recentRequests.length; const remaining = Math.max(0, this.hourLimit - used); const percentUsed = (used / this.hourLimit) * 100; return { used, remaining, limit: this.hourLimit, percentUsed, isNearLimit: percentUsed >= (this.warningThreshold * 100), shouldBlock: percentUsed >= (this.blockThreshold * 100) }; } /** * Check if a request should be allowed */ canMakeRequest() { const usage = this.getCurrentUsage(); return !usage.shouldBlock; } /** * Get rate limit status for logging/debugging */ getStatus() { const usage = this.getCurrentUsage(); const now = Date.now(); const oneHourAgo = now - (60 * 60 * 1000); // Get recent requests grouped by endpoint const recentRequests = this.requests.filter(req => req.timestamp > oneHourAgo); const endpointStats = {}; recentRequests.forEach(req => { if (!endpointStats[req.endpoint]) { endpointStats[req.endpoint] = 0; } endpointStats[req.endpoint]++; }); // Detect session activity const last5MinRequests = this.requests.filter(req => req.timestamp > (now - 5 * 60 * 1000)); const isActiveSession = last5MinRequests.length >= 3; return { ...usage, endpointStats, isActiveSession, totalRequestsLogged: this.requests.length, oldestRequest: this.requests.length > 0 ? new Date(Math.min(...this.requests.map(r => r.timestamp))) : null }; } /** * Get time until requests are freed up */ getTimeUntilCapacityAvailable(requestsNeeded = 1) { this.cleanup(); const usage = this.getCurrentUsage(); if (usage.remaining >= requestsNeeded) { return 0; // Capacity available now } // Find when enough requests will expire to free up capacity const now = Date.now(); const oneHourAgo = now - (60 * 60 * 1000); const recentRequests = this.requests .filter(req => req.timestamp > oneHourAgo) .sort((a, b) => a.timestamp - b.timestamp); // How many requests need to expire to free up capacity? const requestsToExpire = (usage.used + requestsNeeded) - this.hourLimit; if (requestsToExpire <= 0 || requestsToExpire > recentRequests.length) { return 0; } // When will the Nth oldest request expire? const expiryRequest = recentRequests[requestsToExpire - 1]; const expiryTime = expiryRequest.timestamp + (60 * 60 * 1000); // 1 hour after request const waitTime = Math.max(0, expiryTime - now); return waitTime; } /** * Remove requests older than 1 hour */ cleanup() { const oneHourAgo = Date.now() - (60 * 60 * 1000); const before = this.requests.length; this.requests = this.requests.filter(req => req.timestamp > oneHourAgo); const after = this.requests.length; if (before > after) { console.error(`[RequestTracker] Cleaned up ${before - after} expired request records`); } } /** * Get requests by time period for analysis */ getRequestsByPeriod(minutes = 60) { const now = Date.now(); const periodAgo = now - (minutes * 60 * 1000); return this.requests .filter(req => req.timestamp > periodAgo) .sort((a, b) => b.timestamp - a.timestamp); // Most recent first } /** * Estimate optimal delay between requests */ getOptimalDelay() { const usage = this.getCurrentUsage(); if (usage.percentUsed < 50) { return 0; // No delay needed } else if (usage.percentUsed < 80) { return 1000; // 1 second delay } else if (usage.percentUsed < 95) { return 3000; // 3 second delay } else { return 10000; // 10 second delay when near limit } } /** * Destroy the tracker and cleanup interval */ destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.requests = []; } } export { RequestTracker };