crawlforge-mcp-server
Version:
CrawlForge MCP Server - Professional Model Context Protocol server with 19 comprehensive web scraping, crawling, and content processing tools.
196 lines (168 loc) • 4.63 kB
JavaScript
export class RateLimiter {
constructor(options = {}) {
const {
requestsPerSecond = 10,
requestsPerMinute = 100,
perDomain = true
} = options;
this.requestsPerSecond = requestsPerSecond;
this.requestsPerMinute = requestsPerMinute;
this.perDomain = perDomain;
this.windowMs = 1000; // 1 second window
this.limits = new Map(); // domain -> { count, resetTime }
}
async checkLimit(urlOrDomain) {
const domain = this.extractDomain(urlOrDomain);
const now = Date.now();
const key = this.perDomain ? domain : 'global';
let limit = this.limits.get(key);
if (!limit) {
limit = {
secondCount: 0,
secondReset: now + 1000,
minuteCount: 0,
minuteReset: now + 60000
};
this.limits.set(key, limit);
}
// Reset counters if windows have passed
if (now > limit.secondReset) {
limit.secondCount = 0;
limit.secondReset = now + 1000;
}
if (now > limit.minuteReset) {
limit.minuteCount = 0;
limit.minuteReset = now + 60000;
}
// Check rate limits
if (limit.secondCount >= this.requestsPerSecond) {
const waitTime = limit.secondReset - now;
await this.delay(waitTime);
return this.checkLimit(urlOrDomain);
}
if (limit.minuteCount >= this.requestsPerMinute) {
const waitTime = limit.minuteReset - now;
await this.delay(waitTime);
return this.checkLimit(urlOrDomain);
}
// Increment counters
limit.secondCount++;
limit.minuteCount++;
return true;
}
extractDomain(urlOrDomain) {
try {
if (urlOrDomain.startsWith('http://') || urlOrDomain.startsWith('https://')) {
const url = new URL(urlOrDomain);
return url.hostname;
}
return urlOrDomain;
} catch {
return urlOrDomain;
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
reset(domain) {
if (domain) {
this.limits.delete(domain);
} else {
this.limits.clear();
}
}
getStats() {
const stats = {};
for (const [domain, limit] of this.limits.entries()) {
stats[domain] = {
secondCount: limit.secondCount,
minuteCount: limit.minuteCount,
secondsUntilReset: Math.max(0, Math.ceil((limit.secondReset - Date.now()) / 1000)),
minutesUntilReset: Math.max(0, Math.ceil((limit.minuteReset - Date.now()) / 60000))
};
}
return stats;
}
}
export class CircuitBreaker {
constructor(options = {}) {
const {
threshold = 5,
timeout = 60000,
resetTimeout = 120000
} = options;
this.threshold = threshold;
this.timeout = timeout;
this.resetTimeout = resetTimeout;
this.failures = new Map(); // domain -> { count, state, nextAttempt }
}
async execute(domain, fn) {
const breaker = this.getBreaker(domain);
if (breaker.state === 'OPEN') {
if (Date.now() < breaker.nextAttempt) {
throw new Error(`Circuit breaker is OPEN for ${domain}`);
}
breaker.state = 'HALF_OPEN';
}
try {
const result = await Promise.race([
fn(),
this.timeoutPromise()
]);
this.onSuccess(domain);
return result;
} catch (error) {
this.onFailure(domain);
throw error;
}
}
getBreaker(domain) {
if (!this.failures.has(domain)) {
this.failures.set(domain, {
count: 0,
state: 'CLOSED',
nextAttempt: Date.now()
});
}
return this.failures.get(domain);
}
onSuccess(domain) {
const breaker = this.getBreaker(domain);
breaker.count = 0;
breaker.state = 'CLOSED';
}
onFailure(domain) {
const breaker = this.getBreaker(domain);
breaker.count++;
if (breaker.count >= this.threshold) {
breaker.state = 'OPEN';
breaker.nextAttempt = Date.now() + this.resetTimeout;
}
}
timeoutPromise() {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Operation timeout')), this.timeout);
});
}
reset(domain) {
if (domain) {
this.failures.delete(domain);
} else {
this.failures.clear();
}
}
getStats() {
const stats = {};
for (const [domain, breaker] of this.failures.entries()) {
stats[domain] = {
failureCount: breaker.count,
state: breaker.state,
nextAttemptIn: breaker.state === 'OPEN'
? Math.max(0, Math.ceil((breaker.nextAttempt - Date.now()) / 1000))
: 0
};
}
return stats;
}
}
export default RateLimiter;