Spaces:
Running on Zero
Running on Zero
File size: 4,846 Bytes
ef6e870 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | /**
* 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.).
*/
// the standard DOMException name for a cancelled operation
const ABORT_ERROR_NAME = 'AbortError';
// browser specific TypeError messages emitted when a fetch reader is cut by page unload,
// navigation, or a transient network drop. functionally aborts, not actionable errors
const ABORT_LIKE_MESSAGE_PATTERNS = [
/input stream/i, // Firefox: stream cut at unload
/network connection was lost/i, // Safari: transient network drop
/load failed/i // Safari: page navigation during fetch
];
/**
* 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', ABORT_ERROR_NAME);
}
}
/**
* 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 === ABORT_ERROR_NAME) {
return true;
}
if (error instanceof Error) {
if (error.name === ABORT_ERROR_NAME) {
return true;
}
// these patterns are functionally aborts, keep them out of the red console
if (error instanceof TypeError) {
const msg = error.message ?? '';
if (ABORT_LIKE_MESSAGE_PATTERNS.some((re) => re.test(msg))) 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<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
if (!signal) return promise;
throwIfAborted(signal);
return new Promise<T>((resolve, reject) => {
const abortHandler = () => {
reject(new DOMException('Operation was aborted', ABORT_ERROR_NAME));
};
signal.addEventListener('abort', abortHandler, { once: true });
promise
.then((value) => {
signal.removeEventListener('abort', abortHandler);
resolve(value);
})
.catch((error) => {
signal.removeEventListener('abort', abortHandler);
reject(error);
});
});
}
|