File size: 3,364 Bytes
064bfd6 | 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 | import { setMaxListeners } from 'events'
/**
* Default max listeners for standard operations
*/
const DEFAULT_MAX_LISTENERS = 50
/**
* Creates an AbortController with proper event listener limits set.
* This prevents MaxListenersExceededWarning when multiple listeners
* are attached to the abort signal.
*
* @param maxListeners - Maximum number of listeners (default: 50)
* @returns AbortController with configured listener limit
*/
export function createAbortController(
maxListeners: number = DEFAULT_MAX_LISTENERS,
): AbortController {
const controller = new AbortController()
setMaxListeners(maxListeners, controller.signal)
return controller
}
/**
* Propagates abort from a parent to a weakly-referenced child controller.
* Both parent and child are weakly held — neither direction creates a
* strong reference that could prevent GC.
* Module-scope function avoids per-call closure allocation.
*/
function propagateAbort(
this: WeakRef<AbortController>,
weakChild: WeakRef<AbortController>,
): void {
const parent = this.deref()
weakChild.deref()?.abort(parent?.signal.reason)
}
/**
* Removes an abort handler from a weakly-referenced parent signal.
* Both parent and handler are weakly held — if either has been GC'd
* or the parent already aborted ({once: true}), this is a no-op.
* Module-scope function avoids per-call closure allocation.
*/
function removeAbortHandler(
this: WeakRef<AbortController>,
weakHandler: WeakRef<(...args: unknown[]) => void>,
): void {
const parent = this.deref()
const handler = weakHandler.deref()
if (parent && handler) {
parent.signal.removeEventListener('abort', handler)
}
}
/**
* Creates a child AbortController that aborts when its parent aborts.
* Aborting the child does NOT affect the parent.
*
* Memory-safe: Uses WeakRef so the parent doesn't retain abandoned children.
* If the child is dropped without being aborted, it can still be GC'd.
* When the child IS aborted, the parent listener is removed to prevent
* accumulation of dead handlers.
*
* @param parent - The parent AbortController
* @param maxListeners - Maximum number of listeners (default: 50)
* @returns Child AbortController
*/
export function createChildAbortController(
parent: AbortController,
maxListeners?: number,
): AbortController {
const child = createAbortController(maxListeners)
// Fast path: parent already aborted, no listener setup needed
if (parent.signal.aborted) {
child.abort(parent.signal.reason)
return child
}
// WeakRef prevents the parent from keeping an abandoned child alive.
// If all strong references to child are dropped without aborting it,
// the child can still be GC'd — the parent only holds a dead WeakRef.
const weakChild = new WeakRef(child)
const weakParent = new WeakRef(parent)
const handler = propagateAbort.bind(weakParent, weakChild)
parent.signal.addEventListener('abort', handler, { once: true })
// Auto-cleanup: remove parent listener when child is aborted (from any source).
// Both parent and handler are weakly held — if either has been GC'd or the
// parent already aborted ({once: true}), the cleanup is a harmless no-op.
child.signal.addEventListener(
'abort',
removeAbortHandler.bind(weakParent, new WeakRef(handler)),
{ once: true },
)
return child
}
|