Skip to content

Commit 4f3cea2

Browse files
committed
fix: drop any in abort controller
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
1 parent 41cfe75 commit 4f3cea2

2 files changed

Lines changed: 94 additions & 14 deletions

File tree

src/internal/concurrency/async-abort-controller.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,44 @@
22
* This special AbortController is used to wait for all the abort handlers to finish before resolving the promise.
33
*/
44
export class AsyncAbortController extends AbortController {
5-
protected promises: Promise<any>[] = []
5+
protected promises: Promise<unknown>[] = []
66
protected _nextGroup?: AsyncAbortController
77

88
constructor() {
99
super()
1010

11-
const originalEventListener = this.signal.addEventListener
11+
const originalEventListener = this.signal.addEventListener.bind(this.signal)
1212

1313
// Patch event addEventListener to keep track of listeners and their promises
14-
this.signal.addEventListener = (type: string, listener: any, options: any) => {
14+
this.signal.addEventListener = (
15+
type: string,
16+
listener: EventListenerOrEventListenerObject | null,
17+
options?: boolean | AddEventListenerOptions
18+
) => {
19+
if (!listener) {
20+
return
21+
}
22+
1523
if (type !== 'abort') {
16-
return originalEventListener.call(this.signal, type, listener, options)
24+
return originalEventListener(type, listener, options)
1725
}
1826

19-
let resolving: undefined | (() => Promise<void>) = undefined
27+
let resolving: ((event: Event) => Promise<void>) | undefined
2028
const promise = new Promise<void>((resolve, reject) => {
21-
resolving = async (): Promise<void> => {
22-
return Promise.resolve()
23-
.then(() => listener())
24-
.then(() => {
29+
resolving = (event: Event): Promise<void> => {
30+
try {
31+
const result =
32+
typeof listener === 'function'
33+
? listener.call(this.signal, event)
34+
: listener.handleEvent(event)
35+
36+
return Promise.resolve(result).then(() => {
2537
resolve()
26-
})
27-
.catch((error) => {
28-
reject(error)
29-
})
38+
}, reject)
39+
} catch (error) {
40+
reject(error)
41+
return Promise.resolve()
42+
}
3043
}
3144
})
3245
this.promises.push(promise)
@@ -35,7 +48,7 @@ export class AsyncAbortController extends AbortController {
3548
throw new Error('resolve is undefined')
3649
}
3750

38-
return originalEventListener.call(this.signal, type, resolving, options)
51+
return originalEventListener(type, resolving as EventListener, options)
3952
}
4053
}
4154

src/test/async-abort-controller.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,71 @@ describe('AsyncAbortController', () => {
6969

7070
expect(order).toEqual(['root:start', 'root:end', 'child', 'grandchild'])
7171
})
72+
73+
it('forwards the real abort event to function listeners with the signal as context', async () => {
74+
const controller = new AsyncAbortController()
75+
const seen: {
76+
target: EventTarget | null
77+
currentTarget: EventTarget | null
78+
context: unknown
79+
} = {
80+
target: null,
81+
currentTarget: null,
82+
context: undefined,
83+
}
84+
85+
controller.signal.addEventListener('abort', function (event) {
86+
seen.target = event.target
87+
seen.currentTarget = event.currentTarget
88+
seen.context = this
89+
})
90+
91+
await controller.abortAsync()
92+
93+
expect(seen.target).toBe(controller.signal)
94+
expect(seen.currentTarget).toBe(controller.signal)
95+
expect(seen.context).toBe(controller.signal)
96+
})
97+
98+
it('waits for handleEvent listeners before aborting nested groups', async () => {
99+
const controller = new AsyncAbortController()
100+
const childGroup = controller.nextGroup
101+
const order: string[] = []
102+
let releaseRootAbort!: () => void
103+
const rootAbortDone = new Promise<void>((resolve) => {
104+
releaseRootAbort = resolve
105+
})
106+
const listener = {
107+
target: null as EventTarget | null,
108+
async handleEvent(event: Event) {
109+
this.target = event.target
110+
order.push('root:start')
111+
await rootAbortDone
112+
order.push('root:end')
113+
},
114+
}
115+
116+
controller.signal.addEventListener('abort', listener)
117+
childGroup.signal.addEventListener('abort', () => {
118+
order.push('child')
119+
})
120+
121+
const abortPromise = controller.abortAsync()
122+
123+
await Promise.resolve()
124+
expect(order).toEqual(['root:start'])
125+
126+
releaseRootAbort()
127+
await abortPromise
128+
129+
expect(listener.target).toBe(controller.signal)
130+
expect(order).toEqual(['root:start', 'root:end', 'child'])
131+
})
132+
133+
it('ignores null abort listeners', async () => {
134+
const controller = new AsyncAbortController()
135+
136+
expect(() => controller.signal.addEventListener('abort', null)).not.toThrow()
137+
await expect(controller.abortAsync()).resolves.toBeUndefined()
138+
})
72139
})

0 commit comments

Comments
 (0)