UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

86 lines (85 loc) 3.58 kB
/** * Executes a promise with a timeout. If the promise doesn't resolve or reject within * the specified timeout, returns the provided default value. * * @param operation The promise to execute * @param timeoutMs Timeout in milliseconds * @param operationName Name of the operation (for logs) * @param defaultValue Value to return if the operation times out * @returns Promise that resolves with the operation result or the default value on timeout */ export function withTimeout(operation, timeoutMs, operationName, defaultValue) { // Don't sanitize operation name for logs - only telemetry will sanitize if needed return new Promise((resolve, reject) => { let isCompleted = false; // Set up timeout const timeoutId = setTimeout(() => { if (!isCompleted) { isCompleted = true; if (defaultValue !== null) { resolve(defaultValue); } else { // Keep the original operation name in the error message // Telemetry sanitization happens at the capture level reject(`__ERROR__: ${operationName} timed out after ${timeoutMs / 1000} seconds`); } } }, timeoutMs); // Execute the operation operation .then(result => { if (!isCompleted) { isCompleted = true; clearTimeout(timeoutId); resolve(result); } }) .catch(error => { if (!isCompleted) { isCompleted = true; clearTimeout(timeoutId); if (defaultValue !== null) { resolve(defaultValue); } else { // Pass the original error unchanged - sanitization for telemetry happens in capture reject(error); } } }); }); } /** * Run an operation under a timeout WITH real cancellation. * * Unlike withTimeout (which only races a timer and leaves the underlying work * running — holding its libuv thread/fd until the OS call returns), this passes * an AbortSignal into the operation and aborts it when the timeout fires, so a * read/stream that honors the signal is cancelled and its resources released. * * Rejects with an Error whose `.code` is 'ETIMEDOUT' on timeout (so existing * ETIMEDOUT handling / permission-error mapping keeps working). * * Caveat: an operation wedged inside a single un-interruptible syscall only * observes the abort once that syscall returns; library reads that ignore the * signal (e.g. Excel/PDF parsers) still get the timeout rejection but keep * running in the background until they finish on their own. */ export function runWithAbortableTimeout(operation, timeoutMs, operationName) { const controller = new AbortController(); let timeoutId; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => { controller.abort(); const error = new Error(`${operationName} timed out after ${timeoutMs / 1000} seconds`); error.code = 'ETIMEDOUT'; reject(error); }, timeoutMs); }); const op = operation(controller.signal); // Swallow the late abort rejection so it can't surface as an unhandled // rejection after the timeout has already settled the race. op.catch(() => { }); return Promise.race([op, timeout]).finally(() => clearTimeout(timeoutId)); }