mcp-ynab
Version:
Model Context Protocol server for YNAB integration
192 lines (160 loc) • 5.42 kB
JavaScript
/**
* 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 };