Просмотр исходного кода

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

GitOrigin-RevId: d0fc041054e17f50b5b19343e06e857bd9635902
Alf Eaton 1 год назад
Родитель
Сommit
9d290ae234

+ 1 - 0
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'
 

+ 2 - 22
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)])
 }

+ 54 - 0
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

+ 43 - 0
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')
+      })
+    })
+  })
+})