100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
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
|
|
}
|