Skip to content

Commit 6f3375a

Browse files
retyuimeta-codesync[bot]
authored andcommitted
Replace abort-controller that has no update 7 years with React Native fork (#57230)
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`](https://github.com/mysticatea/abort-controller/tree/master/src) files to the `packages/react-native/src/private/webapis/dom/abort-api/` folder 3. Adapt existing [tests](https://github.com/mysticatea/abort-controller/blob/master/test/index.ts) to react-native env. 4. Add missing methods: - `AbortSignal.abort(reason)`, - `AbortSignal.any(signals)`, - `AbortSignal.timeout(time)`, - `signal.throwIfAborted()`; 6. Cover new methods with tests 7. Bundle size is smaller as a removed `event-target-shim` has duplicate logic from `packages/react-native/src/private/webapis/dom/events/EventTarget.js` 8. Typescript types has updated ## Changelog: [GENERAL] [CHANGED] - Replace `abort-controller` with fork version from react-native Pull Request resolved: #57230 Reviewed By: javache Differential Revision: D110042076 Pulled By: rubennorte fbshipit-source-id: 1dfdc92089c893add9831ffcda94e0dd05779543
1 parent d833014 commit 6f3375a

8 files changed

Lines changed: 804 additions & 17 deletions

File tree

packages/react-native/Libraries/Core/setUpXHR.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,13 @@ polyfillGlobal('URL', () => require('../Blob/URL').URL);
3636
polyfillGlobal('URLSearchParams', () => require('../Blob/URL').URLSearchParams);
3737
polyfillGlobal(
3838
'AbortController',
39-
() => require('abort-controller/dist/abort-controller').AbortController, // flowlint-line untyped-import:off
39+
() =>
40+
require('../../src/private/webapis/dom/abort-api/AbortController')
41+
.AbortController, // flowlint-line untyped-import:off
4042
);
4143
polyfillGlobal(
4244
'AbortSignal',
43-
() => require('abort-controller/dist/abort-controller').AbortSignal, // flowlint-line untyped-import:off
45+
() =>
46+
require('../../src/private/webapis/dom/abort-api/AbortSignal')
47+
.AbortSignal_public, // flowlint-line untyped-import:off
4448
);

packages/react-native/__typetests__/globals.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,13 @@ const fetchCopy: WindowOrWorkerGlobalScope['fetch'] = fetch;
142142
const myHeaders = new Headers();
143143
myHeaders.append('Content-Type', 'image/jpeg');
144144

145+
const controller = new AbortController();
146+
145147
const myInit: RequestInit = {
146148
method: 'GET',
147149
headers: myHeaders,
148150
mode: 'cors',
149-
signal: new AbortSignal(),
151+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]),
150152
};
151153

152154
const myRequest = new Request('flowers.jpg');

packages/react-native/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@
152152
"@react-native/js-polyfills": "0.87.0-main",
153153
"@react-native/normalize-colors": "0.87.0-main",
154154
"@react-native/virtualized-lists": "0.87.0-main",
155-
"abort-controller": "^3.0.0",
156155
"anser": "^1.4.9",
157156
"ansi-regex": "^5.0.0",
158157
"babel-plugin-syntax-hermes-parser": "0.36.1",
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Based on abort-controller by Toru Nagashima
3+
* https://github.com/mysticatea/abort-controller
4+
*
5+
* Original work Copyright (c) 2017 Toru Nagashima
6+
* Modified work Copyright (c) Meta Platforms, Inc. and affiliates.
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in all
16+
* copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
* SOFTWARE.
25+
*
26+
* @flow strict
27+
* @format
28+
*/
29+
30+
// flowlint unsafe-getters-setters:off
31+
32+
import {AbortSignal, abortSignal, createAbortSignal} from './AbortSignal';
33+
34+
const SIGNAL_KEY: symbol = Symbol('aborted');
35+
36+
/**
37+
* The AbortController.
38+
* @see https://dom.spec.whatwg.org/#abortcontroller
39+
*/
40+
export class AbortController {
41+
/**
42+
* Initialize this controller.
43+
*/
44+
// $FlowExpectedError[unsupported-syntax]
45+
[SIGNAL_KEY]: AbortSignal;
46+
47+
constructor() {
48+
// $FlowExpectedError[prop-missing]
49+
this[SIGNAL_KEY] = createAbortSignal();
50+
}
51+
52+
/**
53+
* Returns the `AbortSignal` object associated with this object.
54+
*/
55+
get signal(): AbortSignal {
56+
return getSignal(this);
57+
}
58+
59+
/**
60+
* Abort and signal to any observers that the associated activity is to be aborted.
61+
*/
62+
abort(reason: unknown): void {
63+
abortSignal(reason, getSignal(this));
64+
}
65+
}
66+
67+
/**
68+
* Get the associated signal of a given controller.
69+
*/
70+
function getSignal(controller: AbortController): AbortSignal {
71+
// $FlowExpectedError[prop-missing]
72+
const signal = controller[SIGNAL_KEY];
73+
if (signal == null) {
74+
throw new TypeError(
75+
`Expected 'this' to be an 'AbortController' object, but got ${
76+
// $FlowExpectedError[invalid-compare]
77+
controller === null ? 'null' : typeof controller
78+
}`,
79+
);
80+
}
81+
return signal;
82+
}
83+
84+
// Properties should be enumerable.
85+
//$FlowExpectedError[cannot-write]
86+
Object.defineProperties(AbortController.prototype, {
87+
signal: {enumerable: true},
88+
abort: {enumerable: true},
89+
});
90+
91+
//$FlowExpectedError[cannot-write]
92+
Object.defineProperty(AbortController.prototype, Symbol.toStringTag, {
93+
configurable: true,
94+
value: 'AbortController',
95+
});
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/**
2+
* Based on abort-controller by Toru Nagashima
3+
* https://github.com/mysticatea/abort-controller
4+
*
5+
* Original work Copyright (c) 2017 Toru Nagashima
6+
* Modified work Copyright (c) Meta Platforms, Inc. and affiliates.
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in all
16+
* copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
* SOFTWARE.
25+
*
26+
* @flow strict
27+
* @format
28+
*/
29+
30+
// flowlint unsafe-getters-setters:off
31+
32+
import type {EventCallback} from '../events/EventTarget';
33+
34+
import DOMException from '../../errors/DOMException';
35+
import Event from '../events/Event';
36+
import {
37+
getEventHandlerAttribute,
38+
setEventHandlerAttribute,
39+
} from '../events/EventHandlerAttributes';
40+
import EventTarget from '../events/EventTarget';
41+
import {dispatchTrustedEvent} from '../events/internals/EventTargetInternals';
42+
import {AbortController} from './AbortController';
43+
44+
const ABORTED_KEY: symbol = Symbol('aborted');
45+
const REASON_KEY: symbol = Symbol('reason');
46+
47+
/**
48+
* The signal class.
49+
* @see https://dom.spec.whatwg.org/#abortsignal
50+
*/
51+
export class AbortSignal extends EventTarget {
52+
/**
53+
*
54+
* Returns an AbortSignal instance whose abort reason is set to reason if not undefined; otherwise to an "AbortError" DOMException.
55+
* Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort_static
56+
* Spec: https://dom.spec.whatwg.org/#dom-abortsignal-abort
57+
*/
58+
static abort(reason: unknown): AbortSignal {
59+
const signal = createAbortSignal();
60+
abortSignal(reason, signal);
61+
return signal;
62+
}
63+
64+
/**
65+
* AbortSignal.timeout static method
66+
* Docs: https:developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
67+
* Spec: https://dom.spec.whatwg.org/#dom-abortsignal-timeout
68+
*/
69+
static timeout(timeInMs: number): AbortSignal {
70+
if (!(timeInMs >= 0)) {
71+
throw new TypeError(
72+
"Failed to execute 'timeout' on 'AbortSignal': The provided value has to be a non-negative number.",
73+
);
74+
}
75+
const controller = new AbortController();
76+
setTimeout(
77+
() =>
78+
controller.abort(new DOMException('signal timed out', 'TimeoutError')),
79+
timeInMs,
80+
);
81+
return controller.signal;
82+
}
83+
84+
/**
85+
* 3. AbortSignal.any static method
86+
* Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static
87+
* Spec: https://dom.spec.whatwg.org/#dom-abortsignal-any
88+
*/
89+
static any(signals: AbortSignal[]): AbortSignal {
90+
if (!Array.isArray(signals)) {
91+
throw new TypeError('The signals value must be an instance of Array');
92+
}
93+
94+
const controller = new AbortController();
95+
const listeners = [];
96+
const cleanup = () => listeners.forEach(unsubscribe => unsubscribe());
97+
98+
for (let i = 0; i < signals.length; i++) {
99+
const signal = signals[i];
100+
101+
// Validate that each item is an AbortSignal
102+
if (!(signal instanceof AbortSignal)) {
103+
cleanup(); // Remove all listeners added so far
104+
throw new TypeError(
105+
'The "signals[' +
106+
i +
107+
']" argument must be an instance of AbortSignal',
108+
);
109+
}
110+
111+
// Abort immediately if one of the signals is already aborted
112+
if (signal.aborted) {
113+
cleanup(); // Remove all listeners added so far
114+
controller.abort(signal.reason);
115+
break;
116+
}
117+
118+
const onAbort = () => {
119+
controller.abort(signal.reason);
120+
cleanup();
121+
};
122+
signal.addEventListener('abort', onAbort);
123+
listeners.push(() => signal.removeEventListener('abort', onAbort));
124+
}
125+
return controller.signal;
126+
}
127+
128+
// $FlowExpectedError[unsupported-syntax]
129+
[ABORTED_KEY]: boolean = false;
130+
// $FlowExpectedError[unsupported-syntax]
131+
[REASON_KEY]: unknown;
132+
133+
/**
134+
* Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise.
135+
*/
136+
get aborted(): boolean {
137+
// $FlowExpectedError[prop-missing]
138+
const aborted = this[ABORTED_KEY];
139+
if (typeof aborted !== 'boolean') {
140+
throw new TypeError(
141+
`Expected 'this' to be an 'AbortSignal' object, but got ${
142+
// $FlowExpectedError[invalid-compare]
143+
this === null ? 'null' : typeof this
144+
}`,
145+
);
146+
}
147+
return aborted;
148+
}
149+
150+
get reason(): unknown {
151+
// $FlowExpectedError[prop-missing]
152+
return this[REASON_KEY];
153+
}
154+
155+
get onabort(): EventCallback | null {
156+
return getEventHandlerAttribute(this, 'abort');
157+
}
158+
159+
set onabort(listener: ?EventCallback): void {
160+
setEventHandlerAttribute(this, 'abort', listener);
161+
}
162+
163+
throwIfAborted(): void {
164+
if (this.aborted) {
165+
throw this.reason;
166+
}
167+
}
168+
}
169+
170+
/**
171+
* Create an AbortSignal object.
172+
*/
173+
export function createAbortSignal(): AbortSignal {
174+
return new AbortSignal();
175+
}
176+
177+
/**
178+
* Abort a given signal.
179+
*/
180+
export function abortSignal(
181+
reason: unknown | void = new DOMException(
182+
'signal is aborted without reason',
183+
'AbortError',
184+
),
185+
signal: AbortSignal,
186+
): void {
187+
// $FlowExpectedError[invalid-compare]
188+
// $FlowExpectedError[prop-missing]
189+
if (signal[ABORTED_KEY] !== false) {
190+
return;
191+
}
192+
// $FlowExpectedError[prop-missing]
193+
signal[ABORTED_KEY] = true;
194+
// $FlowExpectedError[prop-missing]
195+
signal[REASON_KEY] = reason;
196+
dispatchTrustedEvent(signal, new Event('abort'));
197+
}
198+
199+
// Properties should be enumerable.
200+
//$FlowExpectedError[cannot-write]
201+
Object.defineProperties(AbortSignal.prototype, {
202+
aborted: {enumerable: true},
203+
reason: {enumerable: true},
204+
onabort: {enumerable: true},
205+
throwIfAborted: {enumerable: true},
206+
});
207+
208+
// `toString()` should return `"[object AbortSignal]"`
209+
Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
210+
configurable: true,
211+
value: 'AbortSignal',
212+
});
213+
214+
/**
215+
* AbortSignal cannot be constructed directly.
216+
* So this wrapper is used to achieve such behavior
217+
*/
218+
export const AbortSignal_public: typeof AbortSignal =
219+
/* eslint-disable no-shadow */
220+
// $FlowExpectedError[incompatible-type]
221+
function AbortSignal() {
222+
throw new TypeError(
223+
"Failed to construct 'AbortSignal': Illegal constructor",
224+
);
225+
};
226+
227+
// 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.
228+
// $FlowFixMe[unsafe-object-assign]
229+
// $FlowFixMe[not-an-object]
230+
Object.getOwnPropertyNames(AbortSignal).forEach(methodName => {
231+
Object.defineProperty(
232+
AbortSignal_public,
233+
methodName,
234+
// $FlowExpectedError[incompatible-type]
235+
Object.getOwnPropertyDescriptor(AbortSignal, methodName),
236+
);
237+
});

0 commit comments

Comments
 (0)