11/**
22 * This special AbortController is used to wait for all the abort handlers to finish before resolving the promise.
33 */
4+ type AbortListener = EventListenerOrEventListenerObject
5+
6+ type ListenerRecord = {
7+ wrapped : EventListener
8+ cleanup : ( ) => void
9+ }
10+
411export class AsyncAbortController extends AbortController {
5- protected promises : Promise < any > [ ] = [ ]
12+ protected runningPromises = new Set < Promise < void > > ( )
13+ protected abortListeners = new WeakMap < AbortListener , Map < boolean , ListenerRecord > > ( )
614 protected _nextGroup ?: AsyncAbortController
715
816 constructor ( ) {
917 super ( )
1018
11- const originalEventListener = this . signal . addEventListener
19+ const originalAddEventListener = this . signal . addEventListener . bind ( this . signal )
20+ const originalRemoveEventListener = this . signal . removeEventListener . bind ( this . signal )
1221
1322 // Patch event addEventListener to keep track of listeners and their promises
14- this . signal . addEventListener = ( type : string , listener : any , options : any ) => {
23+ this . signal . addEventListener = (
24+ type : string ,
25+ listener : EventListenerOrEventListenerObject | null ,
26+ options ?: boolean | AddEventListenerOptions
27+ ) => {
28+ if ( ! listener ) {
29+ return
30+ }
31+
1532 if ( type !== 'abort' ) {
16- return originalEventListener . call ( this . signal , type , listener , options )
33+ return originalAddEventListener ( type , listener , options )
34+ }
35+
36+ if ( this . signal . aborted ) {
37+ return originalAddEventListener ( type , listener , options )
38+ }
39+
40+ const capture = getCaptureOption ( options )
41+ const existingRecord = this . getAbortListenerRecord ( listener , capture )
42+ if ( existingRecord ) {
43+ return originalAddEventListener ( type , existingRecord . wrapped , options )
44+ }
45+
46+ const registrationSignal = getRegistrationSignal ( options )
47+ if ( registrationSignal ?. aborted ) {
48+ return
49+ }
50+
51+ let wrapped ! : EventListener
52+ const cleanupRegistrationSignal = this . watchListenerRemovalSignal (
53+ registrationSignal ,
54+ listener ,
55+ capture
56+ )
57+
58+ wrapped = ( event : Event ) => {
59+ this . deleteAbortListenerRecord ( listener , capture )
60+ originalRemoveEventListener ( type , wrapped , capture )
61+
62+ const runningPromise = this . invokeAbortListener ( listener , event )
63+ this . runningPromises . add ( runningPromise )
64+ void runningPromise . finally ( ( ) => {
65+ this . runningPromises . delete ( runningPromise )
66+ } )
1767 }
1868
19- let resolving : undefined | ( ( ) => Promise < void > ) = undefined
20- const promise = new Promise < void > ( ( resolve , reject ) => {
21- resolving = async ( ) : Promise < void > => {
22- return Promise . resolve ( )
23- . then ( ( ) => listener ( ) )
24- . then ( ( ) => {
25- resolve ( )
26- } )
27- . catch ( ( error ) => {
28- reject ( error )
29- } )
30- }
69+ this . setAbortListenerRecord ( listener , capture , {
70+ wrapped,
71+ cleanup : cleanupRegistrationSignal ,
3172 } )
32- this . promises . push ( promise )
3373
34- if ( ! resolving ) {
35- throw new Error ( 'resolve is undefined' )
74+ return originalAddEventListener ( type , wrapped , options )
75+ }
76+
77+ this . signal . removeEventListener = (
78+ type : string ,
79+ listener : EventListenerOrEventListenerObject | null ,
80+ options ?: boolean | EventListenerOptions
81+ ) => {
82+ if ( ! listener ) {
83+ return
84+ }
85+
86+ if ( type !== 'abort' ) {
87+ return originalRemoveEventListener ( type , listener , options )
88+ }
89+
90+ const capture = getCaptureOption ( options )
91+ const record = this . getAbortListenerRecord ( listener , capture )
92+ if ( ! record ) {
93+ return originalRemoveEventListener ( type , listener , options )
3694 }
3795
38- return originalEventListener . call ( this . signal , type , resolving , options )
96+ this . deleteAbortListenerRecord ( listener , capture )
97+ return originalRemoveEventListener ( type , record . wrapped , options )
3998 }
4099 }
41100
@@ -50,8 +109,8 @@ export class AsyncAbortController extends AbortController {
50109
51110 async abortAsync ( ) {
52111 this . abort ( )
53- while ( this . promises . length > 0 ) {
54- const promises = this . promises . splice ( 0 , 100 )
112+ while ( this . runningPromises . size > 0 ) {
113+ const promises = Array . from ( this . runningPromises )
55114 await Promise . allSettled ( promises )
56115 }
57116 await this . abortNextGroup ( )
@@ -62,4 +121,108 @@ export class AsyncAbortController extends AbortController {
62121 await this . _nextGroup . abortAsync ( )
63122 }
64123 }
124+
125+ protected invokeAbortListener ( listener : AbortListener , event : Event ) : Promise < void > {
126+ try {
127+ const result =
128+ typeof listener === 'function'
129+ ? listener . call ( this . signal , event )
130+ : listener . handleEvent ( event )
131+
132+ return Promise . resolve ( result ) . then ( ( ) => undefined )
133+ } catch ( error ) {
134+ return Promise . reject ( error )
135+ }
136+ }
137+
138+ protected getAbortListenerRecord (
139+ listener : AbortListener ,
140+ capture : boolean
141+ ) : ListenerRecord | undefined {
142+ return this . abortListeners . get ( listener ) ?. get ( capture )
143+ }
144+
145+ protected setAbortListenerRecord (
146+ listener : AbortListener ,
147+ capture : boolean ,
148+ record : ListenerRecord
149+ ) {
150+ const records = this . abortListeners . get ( listener ) ?? new Map < boolean , ListenerRecord > ( )
151+ records . set ( capture , record )
152+ this . abortListeners . set ( listener , records )
153+ }
154+
155+ protected deleteAbortListenerRecord ( listener : AbortListener , capture : boolean ) {
156+ const records = this . abortListeners . get ( listener )
157+ const record = records ?. get ( capture )
158+ if ( ! records || ! record ) {
159+ return
160+ }
161+
162+ record . cleanup ( )
163+ records . delete ( capture )
164+
165+ if ( records . size === 0 ) {
166+ this . abortListeners . delete ( listener )
167+ }
168+ }
169+
170+ protected watchListenerRemovalSignal (
171+ signal : AbortSignal | undefined ,
172+ listener : AbortListener ,
173+ capture : boolean
174+ ) : ( ) => void {
175+ if ( ! signal ) {
176+ return ( ) => { }
177+ }
178+
179+ const onAbort = ( ) => {
180+ this . deleteAbortListenerRecord ( listener , capture )
181+ }
182+
183+ addNativeEventListener ( signal , 'abort' , onAbort , { once : true } )
184+
185+ return ( ) => {
186+ removeNativeEventListener ( signal , 'abort' , onAbort , { capture : false } )
187+ }
188+ }
189+ }
190+
191+ const nativeAddEventListener = EventTarget . prototype . addEventListener
192+ const nativeRemoveEventListener = EventTarget . prototype . removeEventListener
193+
194+ function addNativeEventListener (
195+ target : EventTarget ,
196+ type : string ,
197+ listener : EventListenerOrEventListenerObject ,
198+ options ?: boolean | AddEventListenerOptions
199+ ) {
200+ nativeAddEventListener . call ( target , type , listener , options )
201+ }
202+
203+ function removeNativeEventListener (
204+ target : EventTarget ,
205+ type : string ,
206+ listener : EventListenerOrEventListenerObject ,
207+ options ?: boolean | EventListenerOptions
208+ ) {
209+ nativeRemoveEventListener . call ( target , type , listener , options )
210+ }
211+
212+ function getCaptureOption ( options ?: boolean | EventListenerOptions ) : boolean {
213+ if ( typeof options === 'boolean' ) {
214+ return options
215+ }
216+
217+ return options ?. capture ?? false
218+ }
219+
220+ function getRegistrationSignal (
221+ options ?: boolean | AddEventListenerOptions
222+ ) : AbortSignal | undefined {
223+ if ( typeof options === 'boolean' ) {
224+ return undefined
225+ }
226+
227+ return options ?. signal
65228}
0 commit comments