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
JavaScript
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;