abortController.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { setMaxListeners } from 'events'
  2. /**
  3. * Default max listeners for standard operations
  4. */
  5. const DEFAULT_MAX_LISTENERS = 50
  6. /**
  7. * Creates an AbortController with proper event listener limits set.
  8. * This prevents MaxListenersExceededWarning when multiple listeners
  9. * are attached to the abort signal.
  10. *
  11. * @param maxListeners - Maximum number of listeners (default: 50)
  12. * @returns AbortController with configured listener limit
  13. */
  14. export function createAbortController(
  15. maxListeners: number = DEFAULT_MAX_LISTENERS,
  16. ): AbortController {
  17. const controller = new AbortController()
  18. setMaxListeners(maxListeners, controller.signal)
  19. return controller
  20. }
  21. /**
  22. * Propagates abort from a parent to a weakly-referenced child controller.
  23. * Both parent and child are weakly held — neither direction creates a
  24. * strong reference that could prevent GC.
  25. * Module-scope function avoids per-call closure allocation.
  26. */
  27. function propagateAbort(
  28. this: WeakRef<AbortController>,
  29. weakChild: WeakRef<AbortController>,
  30. ): void {
  31. const parent = this.deref()
  32. weakChild.deref()?.abort(parent?.signal.reason)
  33. }
  34. /**
  35. * Removes an abort handler from a weakly-referenced parent signal.
  36. * Both parent and handler are weakly held — if either has been GC'd
  37. * or the parent already aborted ({once: true}), this is a no-op.
  38. * Module-scope function avoids per-call closure allocation.
  39. */
  40. function removeAbortHandler(
  41. this: WeakRef<AbortController>,
  42. weakHandler: WeakRef<(...args: unknown[]) => void>,
  43. ): void {
  44. const parent = this.deref()
  45. const handler = weakHandler.deref()
  46. if (parent && handler) {
  47. parent.signal.removeEventListener('abort', handler)
  48. }
  49. }
  50. /**
  51. * Creates a child AbortController that aborts when its parent aborts.
  52. * Aborting the child does NOT affect the parent.
  53. *
  54. * Memory-safe: Uses WeakRef so the parent doesn't retain abandoned children.
  55. * If the child is dropped without being aborted, it can still be GC'd.
  56. * When the child IS aborted, the parent listener is removed to prevent
  57. * accumulation of dead handlers.
  58. *
  59. * @param parent - The parent AbortController
  60. * @param maxListeners - Maximum number of listeners (default: 50)
  61. * @returns Child AbortController
  62. */
  63. export function createChildAbortController(
  64. parent: AbortController,
  65. maxListeners?: number,
  66. ): AbortController {
  67. const child = createAbortController(maxListeners)
  68. // Fast path: parent already aborted, no listener setup needed
  69. if (parent.signal.aborted) {
  70. child.abort(parent.signal.reason)
  71. return child
  72. }
  73. // WeakRef prevents the parent from keeping an abandoned child alive.
  74. // If all strong references to child are dropped without aborting it,
  75. // the child can still be GC'd — the parent only holds a dead WeakRef.
  76. const weakChild = new WeakRef(child)
  77. const weakParent = new WeakRef(parent)
  78. const handler = propagateAbort.bind(weakParent, weakChild)
  79. parent.signal.addEventListener('abort', handler, { once: true })
  80. // Auto-cleanup: remove parent listener when child is aborted (from any source).
  81. // Both parent and handler are weakly held — if either has been GC'd or the
  82. // parent already aborted ({once: true}), the cleanup is a harmless no-op.
  83. child.signal.addEventListener(
  84. 'abort',
  85. removeAbortHandler.bind(weakParent, new WeakRef(handler)),
  86. { once: true },
  87. )
  88. return child
  89. }