@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
201 lines • 6.49 kB
JavaScript
/**
* Token Bucket Rate Limiter for URL Downloads
*
* Implements a token bucket algorithm to limit concurrent URL downloads.
* This prevents DoS attacks from rapid URL download requests.
*
* Default configuration: 10 downloads per second
*/
import { logger } from "./logger.js";
import { ErrorFactory } from "./errorHandling.js";
/**
* Default configuration: 10 downloads per second
*/
const DEFAULT_CONFIG = {
maxTokens: 10,
refillIntervalMs: 1000,
tokensPerRefill: 10,
maxQueueSize: 100,
queueTimeoutMs: 30000,
};
/**
* Token Bucket Rate Limiter
*
* Uses a token bucket algorithm where:
* - Tokens are consumed when a download is requested
* - Tokens are refilled at a fixed rate
* - Requests that exceed the limit are queued
*/
export class TokenBucketRateLimiter {
tokens;
config;
queue = [];
refillTimer = null;
lastRefillTime;
constructor(config = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.tokens = this.config.maxTokens;
this.lastRefillTime = Date.now();
this.startRefillTimer();
}
/**
* Start the token refill timer
*/
startRefillTimer() {
if (this.refillTimer) {
return;
}
this.refillTimer = setInterval(() => {
this.refillTokens();
this.processQueue();
}, this.config.refillIntervalMs);
// Unref to prevent keeping the process alive
this.refillTimer.unref();
}
/**
* Refill tokens based on elapsed time
*/
refillTokens() {
const now = Date.now();
const elapsed = now - this.lastRefillTime;
const intervalsElapsed = Math.floor(elapsed / this.config.refillIntervalMs);
if (intervalsElapsed > 0) {
const tokensToAdd = intervalsElapsed * this.config.tokensPerRefill;
this.tokens = Math.min(this.config.maxTokens, this.tokens + tokensToAdd);
this.lastRefillTime = now;
}
}
/**
* Process queued requests when tokens become available
*/
processQueue() {
const now = Date.now();
// Remove timed out requests
while (this.queue.length > 0) {
const request = this.queue[0];
if (now - request.timestamp > this.config.queueTimeoutMs) {
this.queue.shift();
if (request.timeoutTimer) {
clearTimeout(request.timeoutTimer);
}
request.reject(ErrorFactory.rateLimiterQueueTimeout(this.config.queueTimeoutMs));
}
else {
break;
}
}
// Process requests while we have tokens
while (this.tokens > 0 && this.queue.length > 0) {
const request = this.queue.shift();
if (request) {
if (request.timeoutTimer) {
clearTimeout(request.timeoutTimer);
}
this.tokens--;
request.resolve();
}
}
}
/**
* Acquire a token for a download
* Returns immediately if token is available, otherwise queues the request
*
* @throws NeuroLinkError if queue is full or request times out
*/
async acquire() {
// Refill tokens based on elapsed time
this.refillTokens();
// If token available, consume it immediately
if (this.tokens > 0) {
this.tokens--;
return;
}
// Check queue size limit
if (this.queue.length >= this.config.maxQueueSize) {
logger.warn(`Rate limiter queue full (${this.config.maxQueueSize} requests pending)`);
throw ErrorFactory.rateLimiterQueueFull(this.config.maxQueueSize);
}
// Queue the request with per-request timeout handling
return new Promise((resolve, reject) => {
// Create per-request timeout timer for robust timeout handling
const timeoutTimer = setTimeout(() => {
// Remove from queue if still present
const index = this.queue.findIndex((req) => req.timeoutTimer === timeoutTimer);
if (index !== -1) {
this.queue.splice(index, 1);
reject(ErrorFactory.rateLimiterQueueTimeout(this.config.queueTimeoutMs));
}
}, this.config.queueTimeoutMs);
this.queue.push({
resolve,
reject,
timestamp: Date.now(),
timeoutTimer,
});
});
}
/**
* Get current rate limiter statistics
*/
getStats() {
return {
availableTokens: this.tokens,
queueLength: this.queue.length,
maxTokens: this.config.maxTokens,
maxQueueSize: this.config.maxQueueSize,
};
}
/**
* Reset the rate limiter to initial state
*/
reset() {
this.tokens = this.config.maxTokens;
this.lastRefillTime = Date.now();
// Reject all queued requests and clear timeout timers
while (this.queue.length > 0) {
const request = this.queue.shift();
if (request) {
if (request.timeoutTimer) {
clearTimeout(request.timeoutTimer);
}
request.reject(ErrorFactory.rateLimiterReset());
}
}
}
/**
* Stop the rate limiter and clean up resources
*/
stop() {
if (this.refillTimer) {
clearInterval(this.refillTimer);
this.refillTimer = null;
}
this.reset();
}
}
/**
* Global rate limiter instance for URL downloads
* Default: 10 downloads per second
*/
export const urlDownloadRateLimiter = new TokenBucketRateLimiter({
maxTokens: 10,
refillIntervalMs: 1000,
tokensPerRefill: 10,
maxQueueSize: 100,
queueTimeoutMs: 30000,
});
/**
* Rate-limited wrapper for async functions
* Ensures the function is rate-limited using the provided rate limiter
*
* @param fn - The async function to wrap
* @param rateLimiter - The rate limiter to use (defaults to urlDownloadRateLimiter)
* @returns A rate-limited version of the function
*/
export function withRateLimit(fn, rateLimiter = urlDownloadRateLimiter) {
return async (...args) => {
await rateLimiter.acquire();
return fn(...args);
};
}
//# sourceMappingURL=rateLimiter.js.map