diff --git a/services/web/frontend/js/features/pdf-preview/util/pdf-js.ts b/services/web/frontend/js/features/pdf-preview/util/pdf-js.ts index 51a3bc9891..d2217b11bf 100644 --- a/services/web/frontend/js/features/pdf-preview/util/pdf-js.ts +++ b/services/web/frontend/js/features/pdf-preview/util/pdf-js.ts @@ -1,3 +1,4 @@ +import '@/utils/abortsignal-polyfill' import * as PDFJS from 'pdfjs-dist' import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api' diff --git a/services/web/frontend/js/utils/abort-signal.ts b/services/web/frontend/js/utils/abort-signal.ts index 98e9ce49ab..c6c9cb9883 100644 --- a/services/web/frontend/js/utils/abort-signal.ts +++ b/services/web/frontend/js/utils/abort-signal.ts @@ -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)]) } diff --git a/services/web/frontend/js/utils/abortsignal-polyfill.ts b/services/web/frontend/js/utils/abortsignal-polyfill.ts new file mode 100644 index 0000000000..00b214a560 --- /dev/null +++ b/services/web/frontend/js/utils/abortsignal-polyfill.ts @@ -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 diff --git a/services/web/test/frontend/utils/abortsignal-polyfill.spec.ts b/services/web/test/frontend/utils/abortsignal-polyfill.spec.ts new file mode 100644 index 0000000000..2aa06f123f --- /dev/null +++ b/services/web/test/frontend/utils/abortsignal-polyfill.spec.ts @@ -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') + }) + }) + }) +})