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, weakChild: WeakRef, ): 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, 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 }