Skip to content

Replace abort-controller that has no update 7 years with React Native fork#57230

Open
retyui wants to merge 2 commits into
react:mainfrom
retyui:feat/retyui/abort-api
Open

Replace abort-controller that has no update 7 years with React Native fork#57230
retyui wants to merge 2 commits into
react:mainfrom
retyui:feat/retyui/abort-api

Conversation

@retyui

@retyui retyui commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary:

Issue: #55247

We had a similar situation with promise package that wasn't maintained for long time, so Hermes team simply forked it https://github.com/facebook/hermes/blob/adf76f44a5175455a43c984e7d792015ebc3be2a/lib/InternalJavaScript/01-Promise.js#L53 to fix impl. issues and add more features

Changes:

  1. Remove abort-controller (and event-target-shim as subdep.) packages
  2. Move abort-controller/src files to the packages/react-native/src/private/webapis/dom/abort-api/ folder
  3. Adapt existing tests to react-native env.
  4. Add missing methods:
    • AbortSignal.abort(reason),
    • AbortSignal.any(signals),
    • AbortSignal.timeout(time),
    • signal.throwIfAborted();
  5. Cover new methods with tests
  6. Bundle size is smaller as a removed event-target-shim has duplicate logic from packages/react-native/src/private/webapis/dom/events/EventTarget.js
  7. Typescript types has updated

Changelog:

[GENERAL] [CHANGED] - Replace abort-controller with fork version from react-native

Test Plan:

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 16, 2026
@retyui retyui force-pushed the feat/retyui/abort-api branch from c73abf7 to bc9cea8 Compare June 16, 2026 14:44
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jun 16, 2026
Comment thread packages/react-native/src/types/globals.d.ts
Comment thread packages/react-native/src/private/webapis/dom/abort-api/AbortController.js Outdated
@retyui

retyui commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@rubennorte could you please review this PR ?

1 similar comment
@retyui

retyui commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@rubennorte could you please review this PR ?

@@ -0,0 +1,318 @@
/**

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've soft-deprecated normal Jest tests for React Native runtime APIs. Please migrate to Fantom (using the -itest suffix).

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.

Also, can we just call this AbortController-itest?

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.

yes

@meta-codesync

meta-codesync Bot commented Jun 29, 2026

Copy link
Copy Markdown

@rubennorte has imported this pull request. If you are a Meta employee, you can view this in D110042076.

@rubennorte rubennorte left a comment

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.

Thanks for working on this. Please address the feedback so we can merge it.

* 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

static timeout(timeInMs: number): AbortSignal {
if (!(timeInMs >= 0)) {
throw new TypeError(
"Failed to execute 'timeout' on 'AbortSignal': The provided value have to be a non-negative number.",

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.

Nit: ...has to be...

*/
static any(signals: AbortSignal[]): AbortSignal {
if (!Array.isArray(signals)) {
throw new Error('The signals value must be an instance of Array');

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 TypeError

}

const listeners = new WeakMap<AbortSignal, () => void>();
Object.defineProperty(AbortSignal.prototype, `onabort`, {

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.

Please check "EventHandlerAttributes" to implement this.

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.

Cool, much simpler way to define a onabort

});

// `toString()` should return `"[object AbortSignal]"`
if (typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol') {

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.

Yeah, this check is unnecessary

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.

removed

@@ -0,0 +1,69 @@
/**
* @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

/**
* Returns the `AbortSignal` object associated with this object.
*/
// $FlowExpectedError[unsafe-getters-setters]

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.

Could you please disable the lint instead at the top of the file via // flowlint unsafe-getters-setters:off, like we do in

*/
constructor() {
super();
throw new TypeError('AbortSignal cannot be constructed directly');

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.

Could you please use the same pattern that we use in other DOM classes in RN to prevent them to be instantiated externally while being allowed internally? That would avoid the Flow issues in other parts of the code.

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.

Could you please give an example ?

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.

export const DOMRectList_public: typeof DOMRectList =

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.

thank you

* Create an AbortSignal object.
*/
export function createAbortSignal(): AbortSignal {
const signal = Object.create(AbortSignal.prototype);

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.

Please use const signal = new AbortSignal(). See my comment above about the pattern of private constructors.


abortedFlags.set(signal, true);
reasons.set(signal, reason);
// $FlowExpectedError[incompatible-type]

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.

Why is this Flow suppression necessary?

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.

doesn't need anymore, removed

@retyui retyui force-pushed the feat/retyui/abort-api branch from c64a914 to 14ae1b0 Compare June 29, 2026 11:55
@retyui retyui requested a review from rubennorte June 29, 2026 11:55
@retyui

retyui commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@rubennorte All comments were fixed please review again

@retyui retyui force-pushed the feat/retyui/abort-api branch from 14ae1b0 to d7756d6 Compare June 29, 2026 12:00

import DOMException from '../../../errors/DOMException';
import {AbortController} from '../AbortController';
import {AbortSignal_public as AbortSignal} from '../AbortSignal';

@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.

tests based on AbortSignal_public class

() => 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

// Copy static properties so that callers accessing them via the public constructor (e.g. `AbortSignal.timeout(0)`) still work.
// $FlowFixMe[unsafe-object-assign]
// $FlowFixMe[not-an-object]
['timeout', 'abort', 'any'].forEach(methodName => {

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.

Might be better to use Object. getOwnPropertyNames() here

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.

sure, code looks clear now

Object. getOwnPropertyNames(AbortSignal) // ['length', 'name', 'prototype', 'abort', 'any', 'timeout']

});
});

describe("'any' static method", () => {

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.

Could you please also add tests for the static abort method?

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.

yes, tests were added

}

/**
* AbortSignal cannot be constructed directly.

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 comment is outdated. We should indicate this is the internal/private constructor

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

@retyui retyui force-pushed the feat/retyui/abort-api branch from d7756d6 to ed56991 Compare June 29, 2026 14:37
@retyui retyui requested a review from rubennorte June 29, 2026 14:38

constructor() {
super();
abortedFlags.set(this, false);

@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.

Might just be easier to convert this into a WeakSet and call it abortedSignals.

Edit: actually, why don't we store this state in a private field in the signal itself? I think it'd be more efficient. Same for reasons.

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.

Edit: actually, why don't we store this state in a private field in the signal itself? I think it'd be more efficient.

yes we can use private fields, I tried to keep this fork close to the orignal imlp. to avoid any unexpected breaking changes. so 7 years ago there wasn't any private fields and WeakMap works as alternative to make a private field.

Let me know if you agree to convert it to private fields

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.

Yeah, let's convert them

@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.

@rubennorte actually, I have no idea how to abort a signal from an AbortController::abort() method
without exposing a public method to mutate that this.#aborted filed

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.

Oh, that's fair. You can use a symbol property instead of a private method, the same way we did in EventTarget. Check EventInternals. As an added benefit, that method is actually faster than private properties atm.

@rubennorte

Copy link
Copy Markdown
Contributor

Sorry I missed some details in the original review about the weakmaps for signal state. Can you take a look please?

@retyui retyui force-pushed the feat/retyui/abort-api branch 2 times, most recently from 8c8e3e5 to d754299 Compare June 29, 2026 16:06
@retyui retyui force-pushed the feat/retyui/abort-api branch from d754299 to b3594af Compare June 30, 2026 07:16
@retyui

retyui commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@rubennorte would you mind to review it again ?

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).

@rubennorte rubennorte left a comment

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.

There's just one detail that I can fix on my end. Thanks for the contribution and iterating on this, @retyui !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Callstack Partner: Callstack Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants