Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/react-native/Libraries/Core/setUpXHR.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ polyfillGlobal('URL', () => require('../Blob/URL').URL);
polyfillGlobal('URLSearchParams', () => require('../Blob/URL').URLSearchParams);
polyfillGlobal(
'AbortController',
() => require('abort-controller/dist/abort-controller').AbortController, // flowlint-line untyped-import:off
() =>
require('../../src/private/webapis/dom/abort-api/AbortController')
.AbortController, // flowlint-line untyped-import:off
);
polyfillGlobal(
'AbortSignal',
() => require('abort-controller/dist/abort-controller').AbortSignal, // flowlint-line untyped-import:off
() =>
require('../../src/private/webapis/dom/abort-api/AbortSignal')
.AbortSignal_public, // flowlint-line untyped-import:off

@retyui retyui Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RN uses a public interface , same as tests do
https://github.com/react/react-native/pull/57230/changes#r3491521247

);
4 changes: 3 additions & 1 deletion packages/react-native/__typetests__/globals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,13 @@ const fetchCopy: WindowOrWorkerGlobalScope['fetch'] = fetch;
const myHeaders = new Headers();
myHeaders.append('Content-Type', 'image/jpeg');

const controller = new AbortController();

const myInit: RequestInit = {
method: 'GET',
headers: myHeaders,
mode: 'cors',
signal: new AbortSignal(),
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]),
};

const myRequest = new Request('flowers.jpg');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Based on abort-controller by Toru Nagashima
* https://github.com/mysticatea/abort-controller
*
* Original work Copyright (c) 2017 Toru Nagashima
* Modified work Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @flow strict

@rubennorte rubennorte Jun 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a proper license here. Given that you copied the implementation from an external package, you should keep the original license in the header for this file. Something like:

/**
 * Based on abort-controller by Toru Nagashima
 * https://github.com/mysticatea/abort-controller
 * 
 * Original work Copyright (c) 2017 Toru Nagashima
 * Modified work Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * @flow strict
 * @format
 */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

* @format
*/

// flowlint unsafe-getters-setters:off

import {AbortSignal, abortSignal, createAbortSignal} from './AbortSignal';

const SIGNAL_KEY: symbol = Symbol('aborted');

/**
* The AbortController.
* @see https://dom.spec.whatwg.org/#abortcontroller
*/
export class AbortController {
/**
* Initialize this controller.
*/
// $FlowExpectedError[unsupported-syntax]
[SIGNAL_KEY]: AbortSignal;

constructor() {
// $FlowExpectedError[prop-missing]
this[SIGNAL_KEY] = createAbortSignal();
}

/**
* Returns the `AbortSignal` object associated with this object.
*/
get signal(): AbortSignal {
return getSignal(this);
}

/**
* Abort and signal to any observers that the associated activity is to be aborted.
*/
abort(reason: unknown): void {
abortSignal(reason, getSignal(this));
}
}

/**
* Get the associated signal of a given controller.
*/
function getSignal(controller: AbortController): AbortSignal {
// $FlowExpectedError[prop-missing]
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
}`,
);
}
return signal;
}

// Properties should be enumerable.
//$FlowExpectedError[cannot-write]
Object.defineProperties(AbortController.prototype, {
signal: {enumerable: true},
abort: {enumerable: true},
});

//$FlowExpectedError[cannot-write]
Object.defineProperty(AbortController.prototype, Symbol.toStringTag, {
configurable: true,
value: 'AbortController',
});
236 changes: 236 additions & 0 deletions packages/react-native/src/private/webapis/dom/abort-api/AbortSignal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/**
* Based on abort-controller by Toru Nagashima
* https://github.com/mysticatea/abort-controller
*
* Original work Copyright (c) 2017 Toru Nagashima
* Modified work Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @flow strict
* @format
*/

// flowlint unsafe-getters-setters:off

import type {EventCallback} from '../events/EventTarget';

import DOMException from '../../errors/DOMException';
import Event from '../events/Event';
import {
getEventHandlerAttribute,
setEventHandlerAttribute,
} from '../events/EventHandlerAttributes';
import EventTarget from '../events/EventTarget';
import {AbortController} from './AbortController';

const ABORTED_KEY: symbol = Symbol('aborted');
const REASON_KEY: symbol = Symbol('reason');

/**
* The signal class.
* @see https://dom.spec.whatwg.org/#abortsignal
*/
export class AbortSignal extends EventTarget {
/**
*
* Returns an AbortSignal instance whose abort reason is set to reason if not undefined; otherwise to an "AbortError" DOMException.
* Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort_static
* Spec: https://dom.spec.whatwg.org/#dom-abortsignal-abort
*/
static abort(reason: unknown): AbortSignal {
const signal = createAbortSignal();
abortSignal(reason, signal);
return signal;
}

/**
* AbortSignal.timeout static method
* Docs: https:developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
* Spec: https://dom.spec.whatwg.org/#dom-abortsignal-timeout
*/
static timeout(timeInMs: number): AbortSignal {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also have a static abort method on AbortSignal

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right, I missed it

if (!(timeInMs >= 0)) {
throw new TypeError(
"Failed to execute 'timeout' on 'AbortSignal': The provided value has to be a non-negative number.",
);
}
const controller = new AbortController();
setTimeout(
() =>
controller.abort(new DOMException('signal timed out', 'TimeoutError')),
timeInMs,
);
return controller.signal;
}

/**
* 3. AbortSignal.any static method
* Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static
* Spec: https://dom.spec.whatwg.org/#dom-abortsignal-any
*/
static any(signals: AbortSignal[]): AbortSignal {
if (!Array.isArray(signals)) {
throw new TypeError('The signals value must be an instance of Array');
}

const controller = new AbortController();
const listeners = [];
const cleanup = () => listeners.forEach(unsubscribe => unsubscribe());

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 Error(
'The "signals[' +
i +
']" argument must be an instance of AbortSignal',
);
}

// Abort immediately if one of the signals is already aborted
if (signal.aborted) {
cleanup(); // Remove all listeners added so far
controller.abort(signal.reason);
break;
}

const onAbort = () => {
controller.abort(signal.reason);
cleanup();
};
signal.addEventListener('abort', onAbort);
listeners.push(() => signal.removeEventListener('abort', onAbort));
}
return controller.signal;
}

// $FlowExpectedError[unsupported-syntax]
[ABORTED_KEY]: boolean = false;
// $FlowExpectedError[unsupported-syntax]
[REASON_KEY]: unknown;

/**
* Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise.
*/
get aborted(): boolean {
// $FlowExpectedError[prop-missing]
const aborted = this[ABORTED_KEY];
if (typeof aborted !== 'boolean') {
throw new TypeError(
`Expected 'this' to be an 'AbortSignal' object, but got ${
// $FlowExpectedError[invalid-compare]
this === null ? 'null' : typeof this
}`,
);
}
return aborted;
}

get reason(): unknown {
// $FlowExpectedError[prop-missing]
return this[REASON_KEY];
}

get onabort(): EventCallback | null {
return getEventHandlerAttribute(this, 'abort');
}

set onabort(listener: ?EventCallback): void {
setEventHandlerAttribute(this, 'abort', listener);
}

throwIfAborted(): void {
if (this.aborted) {
throw this.reason;
}
}
}

/**
* Create an AbortSignal object.
*/
export function createAbortSignal(): AbortSignal {
return new AbortSignal();
}

/**
* Abort a given signal.
*/
export function abortSignal(
reason: unknown | void = new DOMException(
'signal is aborted without reason',
'AbortError',
),
signal: AbortSignal,
): void {
// $FlowExpectedError[invalid-compare]
// $FlowExpectedError[prop-missing]
if (signal[ABORTED_KEY] !== false) {
return;
}
// $FlowExpectedError[prop-missing]
signal[ABORTED_KEY] = true;
// $FlowExpectedError[prop-missing]
signal[REASON_KEY] = reason;
signal.dispatchEvent(new Event('abort'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a trusted event, so it should be dispatched via dispatchTrustedEvent (see EventTargetInternals).

}

// Properties should be enumerable.
//$FlowExpectedError[cannot-write]
Object.defineProperties(AbortSignal.prototype, {
aborted: {enumerable: true},
reason: {enumerable: true},
onabort: {enumerable: true},
throwIfAborted: {enumerable: true},
});

// `toString()` should return `"[object AbortSignal]"`
Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
configurable: true,
value: 'AbortSignal',
});

/**
* AbortSignal cannot be constructed directly.
* So this wrapper is used to achieve such behavior
*/
export const AbortSignal_public: typeof AbortSignal =
/* eslint-disable no-shadow */
// $FlowExpectedError[incompatible-type]
function AbortSignal() {
throw new TypeError(
"Failed to construct 'AbortSignal': Illegal constructor",
);
};

// Copy static properties ('length', 'name', 'prototype', 'abort', 'any', 'timeout') so that callers accessing them via the public constructor (e.g. `AbortSignal.timeout(0)`) still work.
// $FlowFixMe[unsafe-object-assign]
// $FlowFixMe[not-an-object]
Object.getOwnPropertyNames(AbortSignal).forEach(methodName => {
Object.defineProperty(
AbortSignal_public,
methodName,
// $FlowExpectedError[incompatible-type]
Object.getOwnPropertyDescriptor(AbortSignal, methodName),
);
});
Loading
Loading