UNPKG

leshan-mcp-server

Version:

A standards-compliant MCP server for Leshan LwM2M, exposing Leshan as Model Context Protocol tools.

180 lines (155 loc) 4.98 kB
import { validateEnvironment } from "../config/env.js"; import logger from "./loggerConfig.js"; // Get environment configuration const env = validateEnvironment(); class RateLimiter { constructor() { this.maxConcurrent = env.MAX_CONCURRENT_REQUESTS; this.activeRequests = 0; this.queue = []; this.requestHistory = []; this.windowSize = 60000; // 1 minute window this.maxRequestsPerWindow = this.maxConcurrent * 10; // Allow bursts } /** * Acquire a slot for request execution * @returns {Promise<void>} */ async acquire() { return new Promise((resolve, reject) => { const requestId = this.generateRequestId(); const timestamp = Date.now(); // Check rate limiting window if (!this.isWithinRateLimit()) { reject(new Error("Rate limit exceeded. Too many requests in the current window.")); return; } if (this.activeRequests < this.maxConcurrent) { this.activeRequests++; this.addToHistory(requestId, timestamp); logger.debug("Request slot acquired immediately", { requestId, activeRequests: this.activeRequests, queueLength: this.queue.length }); resolve(); } else { // Add to queue with timeout const timeoutId = setTimeout(() => { const index = this.queue.findIndex(item => item.requestId === requestId); if (index !== -1) { this.queue.splice(index, 1); reject(new Error("Request timeout while waiting in queue")); } }, 30000); // 30 second timeout this.queue.push({ requestId, timestamp, resolve, reject, timeoutId }); logger.debug("Request queued", { requestId, queuePosition: this.queue.length, activeRequests: this.activeRequests }); } }); } /** * Release a request slot */ release() { this.activeRequests = Math.max(0, this.activeRequests - 1); if (this.queue.length > 0) { const next = this.queue.shift(); clearTimeout(next.timeoutId); this.activeRequests++; this.addToHistory(next.requestId, next.timestamp); logger.debug("Request slot released and assigned to queued request", { requestId: next.requestId, activeRequests: this.activeRequests, remainingQueue: this.queue.length }); next.resolve(); } else { logger.debug("Request slot released", { activeRequests: this.activeRequests }); } } /** * Execute function with rate limiting * @param {Function} fn - Function to execute * @returns {Promise<any>} Function result */ async withLimit(fn) { await this.acquire(); try { return await fn(); } finally { this.release(); } } /** * Check if request is within rate limit window * @returns {boolean} True if within limit */ isWithinRateLimit() { const now = Date.now(); const windowStart = now - this.windowSize; // Clean old requests from history this.requestHistory = this.requestHistory.filter(req => req.timestamp > windowStart); return this.requestHistory.length < this.maxRequestsPerWindow; } /** * Add request to history for rate limiting * @param {string} requestId - Request identifier * @param {number} timestamp - Request timestamp */ addToHistory(requestId, timestamp) { this.requestHistory.push({ requestId, timestamp }); // Keep history size manageable if (this.requestHistory.length > this.maxRequestsPerWindow * 2) { this.requestHistory = this.requestHistory.slice(-this.maxRequestsPerWindow); } } /** * Generate unique request ID * @returns {string} Request ID */ generateRequestId() { return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Get current rate limiter statistics * @returns {Object} Statistics */ getStats() { const now = Date.now(); const windowStart = now - this.windowSize; const recentRequests = this.requestHistory.filter(req => req.timestamp > windowStart); return { activeRequests: this.activeRequests, queueLength: this.queue.length, maxConcurrent: this.maxConcurrent, requestsInWindow: recentRequests.length, maxRequestsPerWindow: this.maxRequestsPerWindow, utilizationPercent: Math.round((this.activeRequests / this.maxConcurrent) * 100), windowUtilizationPercent: Math.round((recentRequests.length / this.maxRequestsPerWindow) * 100) }; } /** * Clear all queued requests (for shutdown) */ clearQueue() { this.queue.forEach(item => { clearTimeout(item.timeoutId); item.reject(new Error("Server shutting down")); }); this.queue = []; } } const rateLimiter = new RateLimiter(); export default rateLimiter;