/ utils / combinedAbortSignal.ts
combinedAbortSignal.ts
 1  import { createAbortController } from './abortController.js'
 2  
 3  /**
 4   * Creates a combined AbortSignal that aborts when the input signal aborts,
 5   * an optional second signal aborts, or an optional timeout elapses.
 6   * Returns both the signal and a cleanup function that removes event listeners
 7   * and clears the internal timeout timer.
 8   *
 9   * Use `timeoutMs` instead of passing `AbortSignal.timeout(ms)` as a signal —
10   * under Bun, `AbortSignal.timeout` timers are finalized lazily and accumulate
11   * in native memory until they fire (measured ~2.4KB/call held for the full
12   * timeout duration). This implementation uses `setTimeout` + `clearTimeout`
13   * so the timer is freed immediately on cleanup.
14   */
15  export function createCombinedAbortSignal(
16    signal: AbortSignal | undefined,
17    opts?: { signalB?: AbortSignal; timeoutMs?: number },
18  ): { signal: AbortSignal; cleanup: () => void } {
19    const { signalB, timeoutMs } = opts ?? {}
20    const combined = createAbortController()
21  
22    if (signal?.aborted || signalB?.aborted) {
23      combined.abort()
24      return { signal: combined.signal, cleanup: () => {} }
25    }
26  
27    let timer: ReturnType<typeof setTimeout> | undefined
28    const abortCombined = () => {
29      if (timer !== undefined) clearTimeout(timer)
30      combined.abort()
31    }
32  
33    if (timeoutMs !== undefined) {
34      timer = setTimeout(abortCombined, timeoutMs)
35      timer.unref?.()
36    }
37    signal?.addEventListener('abort', abortCombined)
38    signalB?.addEventListener('abort', abortCombined)
39  
40    const cleanup = () => {
41      if (timer !== undefined) clearTimeout(timer)
42      signal?.removeEventListener('abort', abortCombined)
43      signalB?.removeEventListener('abort', abortCombined)
44    }
45  
46    return { signal: combined.signal, cleanup }
47  }