Skip to main content
Glama
rateLimit.js5.16 kB
/** * Rate limit handling for YNAB API * YNAB API has a limit of 200 requests per hour per token */ const fs = require('fs-extra'); const path = require('path'); const { logger } = require('./logger'); const { RateLimitError } = require('./errorHandler'); class RateLimiter { constructor(options = {}) { this.requestCounts = {}; this.resetInterval = options.resetInterval || 3600000; // 1 hour in milliseconds this.limit = options.limit || 200; // YNAB limit is 200 requests per hour this.bufferPercentage = options.bufferPercentage || 10; // Keep 10% buffer for critical operations this.effectiveLimit = Math.floor(this.limit * (100 - this.bufferPercentage) / 100); this.storePath = options.storePath || path.join(process.cwd(), 'data', 'rate-limits.json'); this.persistenceEnabled = options.persistenceEnabled !== false; // Create the directory if it doesn't exist if (this.persistenceEnabled) { const dir = path.dirname(this.storePath); fs.ensureDirSync(dir); this.loadState(); } // Clean up expired entries periodically setInterval(() => this.cleanupExpiredEntries(), 60000); // Every minute } loadState() { try { if (fs.existsSync(this.storePath)) { const data = fs.readFileSync(this.storePath, 'utf8'); this.requestCounts = JSON.parse(data); logger.debug('Rate limiter state loaded from disk'); } } catch (error) { logger.warn('Failed to load rate limiter state:', error); } } saveState() { if (!this.persistenceEnabled) return; try { fs.writeFileSync(this.storePath, JSON.stringify(this.requestCounts, null, 2)); logger.debug('Rate limiter state saved to disk'); } catch (error) { logger.warn('Failed to save rate limiter state:', error); } } canMakeRequest(accountId, priority = 'normal') { const now = Date.now(); if (!this.requestCounts[accountId]) { this.requestCounts[accountId] = { count: 0, resetTime: now + this.resetInterval }; } const account = this.requestCounts[accountId]; // Reset counter if the hour has passed if (now > account.resetTime) { account.count = 0; account.resetTime = now + this.resetInterval; } // For high priority requests, use full limit const limitToUse = priority === 'high' ? this.limit : this.effectiveLimit; return account.count < limitToUse; } incrementCounter(accountId) { if (this.requestCounts[accountId]) { this.requestCounts[accountId].count++; this.saveState(); } } decrementCounter(accountId) { if (this.requestCounts[accountId] && this.requestCounts[accountId].count > 0) { this.requestCounts[accountId].count--; this.saveState(); } } getRemainingRequests(accountId, priority = 'normal') { if (!this.requestCounts[accountId]) { return priority === 'high' ? this.limit : this.effectiveLimit; } const limitToUse = priority === 'high' ? this.limit : this.effectiveLimit; return Math.max(0, limitToUse - this.requestCounts[accountId].count); } getResetTime(accountId) { if (!this.requestCounts[accountId]) { return Date.now() + this.resetInterval; } return this.requestCounts[accountId].resetTime; } cleanupExpiredEntries() { const now = Date.now(); let changed = false; Object.keys(this.requestCounts).forEach(accountId => { if (now > this.requestCounts[accountId].resetTime) { this.requestCounts[accountId].count = 0; this.requestCounts[accountId].resetTime = now + this.resetInterval; changed = true; } }); if (changed && this.persistenceEnabled) { this.saveState(); } } async executeWithRateLimit(accountId, func, priority = 'normal') { if (!this.canMakeRequest(accountId, priority)) { const resetTime = new Date(this.getResetTime(accountId)); const resetInMinutes = Math.ceil((resetTime - new Date()) / 60000); throw new RateLimitError( `Rate limit exceeded for ${accountId}. Resets in ${resetInMinutes} minutes.`, 'RATE_LIMIT_EXCEEDED' ); } this.incrementCounter(accountId); try { return await func(); } catch (error) { // If the error is a 429 from YNAB API, adjust our rate limiter if (error.response && error.response.status === 429) { // Force reset the counter to match YNAB's tracking this.requestCounts[accountId].count = this.limit; this.saveState(); // Extract retry-after header if available const retryAfter = error.response.headers['retry-after'] || 3600; const resetInSeconds = parseInt(retryAfter, 10); throw new RateLimitError( `YNAB API rate limit exceeded. Retry after ${resetInSeconds} seconds.`, 'YNAB_RATE_LIMIT_EXCEEDED' ); } throw error; } } } // Create singleton instance const rateLimiter = new RateLimiter(); module.exports = { RateLimiter, rateLimiter };

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/mattweg/ynab-mcp'

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