@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and
340 lines (339 loc) • 12.1 kB
JavaScript
/**
* Timeout utilities for NeuroLink
*
* Provides flexible timeout parsing and error handling for AI operations.
* Supports multiple time formats: milliseconds, seconds, minutes, hours.
*/
/**
* Custom error class for timeout operations
*/
export class TimeoutError extends Error {
timeout;
provider;
operation;
constructor(message, timeout, provider, operation) {
super(message);
this.timeout = timeout;
this.provider = provider;
this.operation = operation;
this.name = "TimeoutError";
// Maintains proper stack trace for where error was thrown
if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, TimeoutError);
}
}
}
/**
* Parse timeout value from various formats
* @param timeout - Can be number (ms), string with unit, or undefined
* @returns Parsed timeout in milliseconds or undefined
* @throws Error if format is invalid
*
* Examples:
* - parseTimeout(5000) => 5000
* - parseTimeout('30s') => 30000
* - parseTimeout('2m') => 120000
* - parseTimeout('1.5h') => 5400000
* - parseTimeout(undefined) => undefined
*/
export function parseTimeout(timeout) {
if (timeout === undefined) {
return undefined;
}
if (typeof timeout === "number") {
if (timeout <= 0) {
throw new Error(`Timeout must be positive, got: ${timeout}`);
}
return timeout; // Assume milliseconds
}
if (typeof timeout === "string") {
// Match number (including decimals) followed by optional unit
const match = timeout.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
if (!match) {
throw new Error(`Invalid timeout format: ${timeout}. Use formats like '30s', '2m', '500ms', or '1.5h'`);
}
const value = parseFloat(match[1]);
if (value <= 0) {
throw new Error(`Timeout must be positive, got: ${value}`);
}
const unit = match[2] || "ms";
switch (unit) {
case "ms":
return value;
case "s":
return value * 1000;
case "m":
return value * 60 * 1000;
case "h":
return value * 60 * 60 * 1000;
default:
return value; // Should never reach here due to regex
}
}
throw new Error(`Invalid timeout type: ${typeof timeout}`);
}
/**
* Default timeout configurations for different providers and operations
*/
export const DEFAULT_TIMEOUTS = {
global: "30s", // Default for all providers
streaming: "2m", // Longer timeout for streaming operations
providers: {
openai: "30s", // OpenAI typically responds quickly
bedrock: "45s", // AWS can be slower, especially for cold starts
vertex: "60s", // Google Cloud can be slower
anthropic: "60s", // Increased timeout for Anthropic API stability
azure: "30s", // Azure OpenAI similar to OpenAI
"google-ai": "30s", // Google AI Studio is fast
huggingface: "2m", // Open source models vary significantly
ollama: "5m", // Local models need more time, especially large ones
mistral: "45s", // Mistral AI moderate speed
},
tools: {
default: "10s", // Default timeout for MCP tool execution
filesystem: "5s", // File operations should be quick
network: "30s", // Network requests might take longer
computation: "2m", // Heavy computation tools need more time
},
};
/**
* Get default timeout for a specific provider
* @param provider - Provider name
* @param operation - Operation type (generate or stream)
* @returns Default timeout string
*/
export function getDefaultTimeout(provider, operation = "generate") {
if (operation === "stream") {
return DEFAULT_TIMEOUTS.streaming;
}
const providerKey = provider.toLowerCase().replace("_", "-");
return (DEFAULT_TIMEOUTS.providers[providerKey] || DEFAULT_TIMEOUTS.global);
}
/**
* Create a timeout promise that rejects after specified duration
* @param timeout - Timeout duration
* @param provider - Provider name for error message
* @param operation - Operation type for error message
* @returns Promise that rejects with TimeoutError
*/
export function createTimeoutPromise(timeout, provider, operation) {
const timeoutMs = parseTimeout(timeout);
if (!timeoutMs) {
return null; // No timeout
}
return new Promise((_, reject) => {
const timer = setTimeout(() => {
reject(new TimeoutError(`${provider} ${operation} operation timed out after ${timeout}`, timeoutMs, provider, operation));
}, timeoutMs);
// Unref the timer so it doesn't keep the process alive (Node.js only)
if (typeof timer === "object" &&
timer &&
"unref" in timer &&
typeof timer.unref === "function") {
timer.unref();
}
});
}
/**
* Enhanced timeout manager with proper cleanup and abort controller integration
* Consolidated from timeout-manager.ts
*/
export class TimeoutManager {
activeTimeouts = new Map();
/**
* Execute operation with timeout and proper cleanup
*/
async executeWithTimeout(operation, config) {
const startTime = Date.now();
const operationId = this.generateOperationId(config.operation);
let retriesUsed = 0;
const maxRetries = config.retryOnTimeout ? (config.maxRetries ?? 1) : 0;
while (retriesUsed <= maxRetries) {
try {
const result = await this.performSingleOperation(operation, config, operationId);
return {
success: true,
data: result,
timedOut: false,
executionTime: Date.now() - startTime,
retriesUsed,
};
}
catch (error) {
this.cleanup(operationId);
if (error instanceof TimeoutError && retriesUsed < maxRetries) {
retriesUsed++;
continue;
}
return {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
timedOut: error instanceof TimeoutError,
executionTime: Date.now() - startTime,
retriesUsed,
};
}
}
return {
success: false,
error: new Error("Maximum retries exceeded"),
timedOut: true,
executionTime: Date.now() - startTime,
retriesUsed,
};
}
async performSingleOperation(operation, config, operationId) {
const timeoutMs = this.getTimeoutMs(config);
if (!timeoutMs) {
return await operation();
}
const controller = new AbortController();
const existingSignal = config.abortSignal;
if (existingSignal) {
existingSignal.addEventListener("abort", () => {
controller.abort(existingSignal.reason);
});
if (existingSignal.aborted) {
throw new Error("Operation aborted before execution");
}
}
const timeoutPromise = this.createTimeoutPromise(timeoutMs, operationId);
this.registerTimeout(operationId, timeoutPromise.timer, controller, () => {
clearTimeout(timeoutPromise.timer);
});
try {
return await Promise.race([operation(), timeoutPromise.promise]);
}
finally {
this.cleanup(operationId);
}
}
getTimeoutMs(config) {
return parseTimeout(config.timeout);
}
generateOperationId(operation) {
return `${operation}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
createTimeoutPromise(timeoutMs, operationId) {
let timer;
const promise = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new TimeoutError(`Operation timeout after ${timeoutMs}ms`, timeoutMs));
}, timeoutMs);
});
return { promise, timer: timer };
}
registerTimeout(operationId, timer, controller, cleanup) {
this.activeTimeouts.set(operationId, { timer, controller, cleanup });
}
cleanup(operationId) {
const timeoutInfo = this.activeTimeouts.get(operationId);
if (timeoutInfo) {
timeoutInfo.cleanup();
this.activeTimeouts.delete(operationId);
}
}
gracefulShutdown() {
for (const [operationId] of this.activeTimeouts) {
this.cleanup(operationId);
}
}
}
/**
* Wrapper functions consolidated from timeout-wrapper.ts
*/
/**
* Wrap a promise with timeout
* @param promise - The promise to wrap
* @param timeout - Timeout duration (number in ms or string with unit)
* @param provider - Provider name for error messages
* @param operation - Operation type (generate or stream)
* @returns The result of the promise or throws TimeoutError
*/
export async function withTimeout(promise, timeout, provider, operation) {
const timeoutMs = parseTimeout(timeout);
if (!timeoutMs) {
return promise;
}
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new TimeoutError(`${provider} ${operation} operation timed out after ${timeoutMs}ms`, timeoutMs, provider, operation));
}, timeoutMs);
});
return Promise.race([promise, timeoutPromise]);
}
/**
* Wrap a streaming async generator with timeout
* @param generator - The async generator to wrap
* @param timeout - Timeout duration for the entire stream
* @param provider - Provider name for error messages
* @returns Wrapped async generator that respects timeout
*/
export async function* withStreamingTimeout(generator, timeout, provider) {
const timeoutMs = parseTimeout(timeout);
if (!timeoutMs) {
yield* generator;
return;
}
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new TimeoutError(`${provider} streaming operation timed out after ${timeoutMs}ms`, timeoutMs, provider, "stream"));
}, timeoutMs);
});
try {
for await (const item of generator) {
const raceResult = await Promise.race([
Promise.resolve(item),
timeoutPromise,
]);
yield raceResult;
}
}
finally {
clearTimeout(timeoutId);
}
}
/**
* Create an abort controller with timeout
* @param timeout - Timeout duration
* @param provider - Provider name for error messages
* @param operation - Operation type
* @returns AbortController and cleanup function
*/
export function createTimeoutController(timeout, provider, operation) {
const timeoutMs = parseTimeout(timeout);
if (!timeoutMs) {
return null;
}
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort(new TimeoutError(`${provider} ${operation} operation timed out after ${timeout}`, timeoutMs, provider, operation));
}, timeoutMs);
const cleanup = () => {
clearTimeout(timer);
};
return { controller, cleanup, timeoutMs };
}
/**
* Merge abort signals (for combining user abort with timeout)
* @param signals - Array of abort signals to merge
* @returns Combined abort controller
*/
export function mergeAbortSignals(signals) {
const controller = new AbortController();
for (const signal of signals) {
if (signal && !signal.aborted) {
signal.addEventListener("abort", () => {
if (!controller.signal.aborted) {
controller.abort(signal.reason);
}
});
}
if (signal?.aborted) {
controller.abort(signal.reason);
break;
}
}
return controller;
}