| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- 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
- }
|