/**
* Timeout utilities with AbortSignal support
*
* Prevents long-running operations from blocking the MCP connection.
*/
import { TimeoutError } from './errors.js';
/**
* Execute a function with a timeout
*
* @example
* ```typescript
* const result = await withTimeout(
* () => longRunningOperation(),
* 5000,
* 'data-fetch'
* );
* ```
*/
export async function withTimeout<T>(
fn: (signal: AbortSignal) => Promise<T>,
timeoutMs: number,
operationName: string = 'operation'
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const result = await fn(controller.signal);
clearTimeout(timeoutId);
return result;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
throw new TimeoutError(operationName, timeoutMs);
}
throw error;
}
}
/**
* Create a promise that rejects after a timeout
*/
export function timeoutPromise(ms: number, message?: string): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new TimeoutError(message ?? 'operation', ms));
}, ms);
});
}
/**
* Race a promise against a timeout
*
* @example
* ```typescript
* const result = await raceTimeout(fetchData(), 5000, 'fetch');
* ```
*/
export async function raceTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
operationName: string = 'operation'
): Promise<T> {
return Promise.race([promise, timeoutPromise(timeoutMs, operationName)]);
}
/**
* Create an AbortSignal that times out after specified duration
*/
export function createTimeoutSignal(timeoutMs: number): AbortSignal {
const controller = new AbortController();
setTimeout(() => controller.abort(), timeoutMs);
return controller.signal;
}
/**
* Combine multiple AbortSignals into one
*/
export function combineSignals(...signals: AbortSignal[]): AbortSignal {
const controller = new AbortController();
for (const signal of signals) {
if (signal.aborted) {
controller.abort(signal.reason);
return controller.signal;
}
signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
}
return controller.signal;
}