@andrew_eragon/mcp-server-salesforce
Version:
A SaaS-ready Salesforce connector MCP Server with connection pooling and multi-user support.
142 lines (141 loc) • 4.38 kB
JavaScript
/**
* Utilities for parallel execution and rate limiting
*/
/**
* Execute promises with concurrency limit
*/
export async function parallelLimit(items, concurrency, fn) {
const results = [];
const executing = [];
for (const [index, item] of items.entries()) {
const promise = fn(item).then(result => {
results[index] = result;
});
executing.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing);
executing.splice(executing.findIndex(p => {
// Remove completed promises
return p === promise || p.settled;
}), 1);
}
}
await Promise.all(executing);
return results;
}
/**
* Batch items and process in parallel
*/
export async function batchProcess(items, batchSize, fn) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await fn(batch);
results.push(...batchResults);
}
return results;
}
/**
* Rate limiter
*/
export class RateLimiter {
/**
* @param maxRequests Maximum number of requests
* @param timeWindow Time window in milliseconds
*/
constructor(maxRequests, timeWindow) {
this.queue = [];
this.currentRequests = 0;
this.requestTimestamps = [];
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
}
/**
* Execute function with rate limiting
*/
async execute(fn) {
await this.waitForSlot();
try {
return await fn();
}
finally {
this.releaseSlot();
}
}
async waitForSlot() {
const now = Date.now();
// Remove old timestamps
this.requestTimestamps = this.requestTimestamps.filter(ts => now - ts < this.timeWindow);
// If we have capacity, proceed immediately
if (this.requestTimestamps.length < this.maxRequests) {
this.requestTimestamps.push(now);
this.currentRequests++;
return;
}
// Wait for the oldest request to expire
const oldestTimestamp = this.requestTimestamps[0];
const waitTime = this.timeWindow - (now - oldestTimestamp);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime + 10));
}
// Try again
return this.waitForSlot();
}
releaseSlot() {
this.currentRequests--;
}
/**
* Get current rate limit status
*/
getStatus() {
const now = Date.now();
this.requestTimestamps = this.requestTimestamps.filter(ts => now - ts < this.timeWindow);
return {
current: this.requestTimestamps.length,
max: this.maxRequests,
available: this.maxRequests - this.requestTimestamps.length
};
}
}
/**
* Retry function with exponential backoff
*/
export async function retry(fn, options = {}) {
const { maxAttempts = 3, initialDelay = 1000, maxDelay = 10000, backoffMultiplier = 2, onRetry } = options;
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === maxAttempts) {
throw lastError;
}
const delay = Math.min(initialDelay * Math.pow(backoffMultiplier, attempt - 1), maxDelay);
if (onRetry) {
onRetry(lastError, attempt);
}
console.error(`⚠️ Retry attempt ${attempt}/${maxAttempts} after ${delay}ms: ${lastError.message}`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
/**
* Timeout wrapper for promises
*/
export async function withTimeout(promise, timeoutMs, errorMessage = 'Operation timed out') {
let timeoutHandle;
const timeoutPromise = new Promise((_, reject) => {
timeoutHandle = setTimeout(() => {
reject(new Error(errorMessage));
}, timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
}
finally {
clearTimeout(timeoutHandle);
}
}