/** * Abort Signal Utilities * * Provides utilities for consistent AbortSignal propagation across the application. * These utilities help ensure that async operations can be properly cancelled * when needed (e.g., user stops generation, navigates away, etc.). */ /** * Throws an AbortError if the signal is aborted. * Use this at the start of async operations to fail fast. * * @param signal - Optional AbortSignal to check * @throws DOMException with name 'AbortError' if signal is aborted * * @example * ```ts * async function fetchData(signal?: AbortSignal) { * throwIfAborted(signal); * // ... proceed with operation * } * ``` */ export function throwIfAborted(signal?: AbortSignal): void { if (signal?.aborted) { throw new DOMException('Operation was aborted', 'AbortError'); } } /** * Checks if an error is an AbortError. * Use this to distinguish between user-initiated cancellation and actual errors. * * @param error - Error to check * @returns true if the error is an AbortError * * @example * ```ts * try { * await fetchData(signal); * } catch (error) { * if (isAbortError(error)) { * // User cancelled - no error dialog needed * return; * } * // Handle actual error * } * ``` */ export function isAbortError(error: unknown): boolean { if (error instanceof DOMException && error.name === 'AbortError') { return true; } if (error instanceof Error && error.name === 'AbortError') { return true; } return false; } /** * Creates a new AbortController that is linked to one or more parent signals. * When any parent signal aborts, the returned controller also aborts. * * Useful for creating child operations that should be cancelled when * either the parent operation or their own timeout/condition triggers. * * @param signals - Parent signals to link to (undefined signals are ignored) * @returns A new AbortController linked to all provided signals * * @example * ```ts * // Link to user's abort signal and add a timeout * const linked = createLinkedController(userSignal, timeoutSignal); * await fetch(url, { signal: linked.signal }); * ``` */ export function createLinkedController(...signals: (AbortSignal | undefined)[]): AbortController { const controller = new AbortController(); for (const signal of signals) { if (!signal) continue; // If already aborted, abort immediately if (signal.aborted) { controller.abort(signal.reason); return controller; } // Link to parent signal signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true }); } return controller; } /** * Creates an AbortSignal that times out after the specified duration. * * @param ms - Timeout duration in milliseconds * @returns AbortSignal that will abort after the timeout * * @example * ```ts * const signal = createTimeoutSignal(5000); // 5 second timeout * await fetch(url, { signal }); * ``` */ export function createTimeoutSignal(ms: number): AbortSignal { return AbortSignal.timeout(ms); } /** * Wraps a promise to reject if the signal is aborted. * Useful for making non-abortable promises respect an AbortSignal. * * @param promise - Promise to wrap * @param signal - AbortSignal to respect * @returns Promise that rejects with AbortError if signal aborts * * @example * ```ts * // Make a non-abortable operation respect abort signal * const result = await withAbortSignal( * someNonAbortableOperation(), * signal * ); * ``` */ export async function withAbortSignal(promise: Promise, signal?: AbortSignal): Promise { if (!signal) return promise; throwIfAborted(signal); return new Promise((resolve, reject) => { const abortHandler = () => { reject(new DOMException('Operation was aborted', 'AbortError')); }; signal.addEventListener('abort', abortHandler, { once: true }); promise .then((value) => { signal.removeEventListener('abort', abortHandler); resolve(value); }) .catch((error) => { signal.removeEventListener('abort', abortHandler); reject(error); }); }); }