Add polyfills for AbortSignal.any and AbortSignal.timeout (#24958)

GitOrigin-RevId: d0fc041054e17f50b5b19343e06e857bd9635902
This commit is contained in:
Alf Eaton
2025-04-25 09:10:08 +01:00
committed by Copybot
parent ed9844b2ec
commit 9d290ae234
4 changed files with 100 additions and 22 deletions

View File

@@ -1,3 +1,4 @@
import '@/utils/abortsignal-polyfill'
import * as PDFJS from 'pdfjs-dist'
import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api'

View File

@@ -1,25 +1,5 @@
export const supportsModernAbortSignal =
typeof AbortSignal.any === 'function' &&
typeof AbortSignal.timeout === 'function'
import './abortsignal-polyfill'
export const signalWithTimeout = (signal: AbortSignal, timeout: number) => {
if (supportsModernAbortSignal) {
return AbortSignal.any([signal, AbortSignal.timeout(timeout)])
}
const abortController = new AbortController()
const abort = () => {
window.clearTimeout(timer)
signal.removeEventListener('abort', abort)
abortController.abort()
}
// abort after timeout has expired
const timer = window.setTimeout(abort, timeout)
// abort when the original signal is aborted
signal.addEventListener('abort', abort)
return abortController.signal
return AbortSignal.any([signal, AbortSignal.timeout(timeout)])
}

View File

@@ -0,0 +1,54 @@
if (typeof AbortSignal.timeout !== 'function') {
AbortSignal.timeout = (time: number) => {
const controller = new AbortController()
function abort() {
controller.abort(new DOMException('Timed out', 'TimeoutError'))
}
function clean() {
window.clearTimeout(timer)
controller.signal.removeEventListener('abort', clean)
}
controller.signal.addEventListener('abort', clean)
const timer = window.setTimeout(abort, time)
return controller.signal
}
}
if (typeof AbortSignal.any !== 'function') {
AbortSignal.any = (signals: AbortSignal[]) => {
const controller = new AbortController()
// return immediately if any of the signals are already aborted.
for (const signal of signals) {
if (signal.aborted) {
controller.abort(signal.reason)
return controller.signal
}
}
function abort() {
controller.abort()
clean()
}
function clean() {
for (const signal of signals) {
signal.removeEventListener('abort', abort)
}
}
// abort the controller (and clean up) when any of the signals aborts
for (const signal of signals) {
signal.addEventListener('abort', abort)
}
return controller.signal
}
}
export default null // show that this is a module

View File

@@ -0,0 +1,43 @@
describe('AbortSignal polyfills', function () {
before(function () {
// @ts-expect-error deleting a required method
delete AbortSignal.any
// @ts-expect-error deleting a required method
delete AbortSignal.timeout
// this polyfill provides the required methods
cy.wrap(import('@/utils/abortsignal-polyfill'))
})
describe('AbortSignal.any', function () {
it('aborts the new signal immediately if one of the signals is aborted already', function () {
const controller1 = new AbortController()
const controller2 = new AbortController()
controller1.abort()
const signal = AbortSignal.any([controller1.signal, controller2.signal])
cy.wrap(signal.aborted).should('be.true')
})
it('aborts the new signal asynchronously if one of the signals is aborted later', function () {
const controller1 = new AbortController()
const controller2 = new AbortController()
const signal = AbortSignal.any([controller1.signal, controller2.signal])
controller1.abort()
cy.wrap(signal.aborted).should('be.true')
})
})
describe('AbortSignal.timeout', function () {
it('aborts the signal after the timeout', function () {
cy.clock().then(clock => {
const signal = AbortSignal.timeout(1000)
cy.wrap(signal.aborted).should('be.false')
clock.tick(1000)
cy.wrap(signal.aborted).should('be.true')
})
})
})
})