diff --git a/packages/react-native/src/private/webapis/dom/abort-api/AbortController.js b/packages/react-native/src/private/webapis/dom/abort-api/AbortController.js index 2dcac12de310..f093e25ee54e 100644 --- a/packages/react-native/src/private/webapis/dom/abort-api/AbortController.js +++ b/packages/react-native/src/private/webapis/dom/abort-api/AbortController.js @@ -72,10 +72,7 @@ function getSignal(controller: AbortController): AbortSignal { const signal = controller[SIGNAL_KEY]; if (signal == null) { throw new TypeError( - `Expected 'this' to be an 'AbortController' object, but got ${ - // $FlowExpectedError[invalid-compare] - controller === null ? 'null' : typeof controller - }`, + `Expected 'this' to be an 'AbortController' object, but got ${typeof controller}`, ); } return signal; diff --git a/packages/react-native/src/private/webapis/dom/abort-api/AbortSignal.js b/packages/react-native/src/private/webapis/dom/abort-api/AbortSignal.js index d45a8dae2e1e..9b0e42cd30db 100644 --- a/packages/react-native/src/private/webapis/dom/abort-api/AbortSignal.js +++ b/packages/react-native/src/private/webapis/dom/abort-api/AbortSignal.js @@ -91,30 +91,36 @@ export class AbortSignal extends EventTarget { throw new TypeError('The signals value must be an instance of Array'); } - const controller = new AbortController(); - const listeners = []; - const cleanup = () => listeners.forEach(unsubscribe => unsubscribe()); - + // First pass: validate that every item is an AbortSignal. for (let i = 0; i < signals.length; i++) { const signal = signals[i]; - - // Validate that each item is an AbortSignal if (!(signal instanceof AbortSignal)) { - cleanup(); // Remove all listeners added so far throw new TypeError( 'The "signals[' + i + ']" argument must be an instance of AbortSignal', ); } + } - // Abort immediately if one of the signals is already aborted + // Second pass: short-circuit if any of the signals is already aborted. No + // listeners have been registered yet, so there is nothing to clean up if + // we return early. + for (let i = 0; i < signals.length; i++) { + const signal = signals[i]; if (signal.aborted) { - cleanup(); // Remove all listeners added so far - controller.abort(signal.reason); - break; + return AbortSignal.abort(signal.reason); } + } + + // Third pass: none of the signals is aborted, so subscribe to all of them + // and abort the resulting signal when any of them aborts. + const controller = new AbortController(); + const listeners = []; + const cleanup = () => listeners.forEach(unsubscribe => unsubscribe()); + for (let i = 0; i < signals.length; i++) { + const signal = signals[i]; const onAbort = () => { controller.abort(signal.reason); cleanup(); diff --git a/packages/react-native/src/private/webapis/dom/abort-api/__tests__/AbortController-itest.js b/packages/react-native/src/private/webapis/dom/abort-api/__tests__/AbortController-itest.js index 81a80f907aaf..4d5fe296a3f6 100644 --- a/packages/react-native/src/private/webapis/dom/abort-api/__tests__/AbortController-itest.js +++ b/packages/react-native/src/private/webapis/dom/abort-api/__tests__/AbortController-itest.js @@ -373,6 +373,26 @@ describe('AbortController', () => { 'The "signals[1]" argument must be an instance of AbortSignal', ); }); + + it('should validate every element before short-circuiting on an already-aborted signal', () => { + const aborted = new AbortController(); + aborted.abort(new Error('already aborted')); + + let thrown; + try { + // $FlowExpectedError[incompatible-type] + AbortSignal.any([aborted.signal, {}]); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(TypeError); + // $FlowExpectedError[incompatible-type] + const typeError: TypeError = thrown; + expect(typeError.message).toBe( + 'The "signals[1]" argument must be an instance of AbortSignal', + ); + }); }); describe("'timeout' static method", () => {