Skip to content

Commit 14ae1b0

Browse files
committed
feat: Add AbortSignal.any(signals), signal.throwIfAborted() and AbortSignal.timeout(time)
1 parent 0bbb4f1 commit 14ae1b0

6 files changed

Lines changed: 377 additions & 84 deletions

File tree

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ polyfillGlobal('URL', () => require('../Blob/URL').URL);
3636
polyfillGlobal('URLSearchParams', () => require('../Blob/URL').URLSearchParams);
3737
polyfillGlobal(
3838
'AbortController',
39-
() => require('../../src/private/webapis/dom/abort-api/AbortController').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',
4345
() =>
44-
require('../../src/private/webapis/dom/abort-api/AbortSignal').AbortSignal, // flowlint-line untyped-import:off
46+
require('../../src/private/webapis/dom/abort-api/AbortSignal')
47+
.AbortSignal_public, // flowlint-line untyped-import:off
4548
);
Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,34 @@
11
/**
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+
*
226
* @flow strict
327
* @format
428
*/
29+
30+
// flowlint unsafe-getters-setters:off
31+
532
import {AbortSignal, abortSignal, createAbortSignal} from './AbortSignal';
633

734
/**
@@ -19,29 +46,28 @@ export class AbortController {
1946
/**
2047
* Returns the `AbortSignal` object associated with this object.
2148
*/
22-
// $FlowExpectedError[unsafe-getters-setters]
2349
get signal(): AbortSignal {
2450
return getSignal(this);
2551
}
2652

2753
/**
2854
* Abort and signal to any observers that the associated activity is to be aborted.
2955
*/
30-
abort(): void {
31-
abortSignal(getSignal(this));
56+
abort(reason: unknown): void {
57+
abortSignal(reason, getSignal(this));
3258
}
3359
}
3460

3561
/**
3662
* Associated signals.
3763
*/
38-
const signals = new WeakMap<AbortController, AbortSignal>()
64+
const signals = new WeakMap<AbortController, AbortSignal>();
3965

4066
/**
4167
* Get the associated signal of a given controller.
4268
*/
4369
function getSignal(controller: AbortController): AbortSignal {
44-
const signal = signals.get(controller)
70+
const signal = signals.get(controller);
4571
if (signal == null) {
4672
throw new TypeError(
4773
`Expected 'this' to be an 'AbortController' object, but got ${
@@ -50,20 +76,18 @@ function getSignal(controller: AbortController): AbortSignal {
5076
}`,
5177
);
5278
}
53-
return signal
79+
return signal;
5480
}
5581

5682
// Properties should be enumerable.
5783
//$FlowExpectedError[cannot-write]
5884
Object.defineProperties(AbortController.prototype, {
59-
signal: { enumerable: true },
60-
abort: { enumerable: true },
61-
})
85+
signal: {enumerable: true},
86+
abort: {enumerable: true},
87+
});
6288

63-
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
64-
//$FlowExpectedError[cannot-write]
65-
Object.defineProperty(AbortController.prototype, Symbol.toStringTag, {
66-
configurable: true,
67-
value: 'AbortController',
68-
});
69-
}
89+
//$FlowExpectedError[cannot-write]
90+
Object.defineProperty(AbortController.prototype, Symbol.toStringTag, {
91+
configurable: true,
92+
value: 'AbortController',
93+
});
Lines changed: 171 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,139 @@
11
/**
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+
*
226
* @flow strict
327
* @format
428
*/
5-
import Event from '../events/Event';
6-
import EventTarget from '../events/EventTarget'
729

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 {AbortController} from './AbortController';
842

43+
const reasons = new WeakMap<AbortSignal, unknown>();
944

1045
/**
1146
* The signal class.
1247
* @see https://dom.spec.whatwg.org/#abortsignal
1348
*/
1449
export class AbortSignal extends EventTarget {
50+
/**
51+
*
52+
* Returns an AbortSignal instance whose abort reason is set to reason if not undefined; otherwise to an "AbortError" DOMException.
53+
* Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort_static
54+
* Spec: https://dom.spec.whatwg.org/#dom-abortsignal-abort
55+
*/
56+
static abort(reason: unknown): AbortSignal {
57+
const signal = createAbortSignal();
58+
abortSignal(reason, signal);
59+
return signal;
60+
}
61+
62+
/**
63+
* AbortSignal.timeout static method
64+
* Docs: https:developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
65+
* Spec: https://dom.spec.whatwg.org/#dom-abortsignal-timeout
66+
*/
67+
static timeout(timeInMs: number): AbortSignal {
68+
if (!(timeInMs >= 0)) {
69+
throw new TypeError(
70+
"Failed to execute 'timeout' on 'AbortSignal': The provided value has to be a non-negative number.",
71+
);
72+
}
73+
const controller = new AbortController();
74+
setTimeout(
75+
() =>
76+
controller.abort(new DOMException('signal timed out', 'TimeoutError')),
77+
timeInMs,
78+
);
79+
return controller.signal;
80+
}
81+
82+
/**
83+
* 3. AbortSignal.any static method
84+
* Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static
85+
* Spec: https://dom.spec.whatwg.org/#dom-abortsignal-any
86+
*/
87+
static any(signals: AbortSignal[]): AbortSignal {
88+
if (!Array.isArray(signals)) {
89+
throw new TypeError('The signals value must be an instance of Array');
90+
}
91+
92+
const controller = new AbortController();
93+
const listeners = [];
94+
const cleanup = () => listeners.forEach(unsubscribe => unsubscribe());
95+
96+
for (let i = 0; i < signals.length; i++) {
97+
const signal = signals[i];
98+
99+
// Validate that each item is an AbortSignal
100+
if (!(signal instanceof AbortSignal)) {
101+
cleanup(); // Remove all listeners added so far
102+
throw new Error(
103+
'The "signals[' +
104+
i +
105+
']" argument must be an instance of AbortSignal',
106+
);
107+
}
108+
109+
// Abort immediately if one of the signals is already aborted
110+
if (signal.aborted) {
111+
cleanup(); // Remove all listeners added so far
112+
controller.abort(signal.reason);
113+
break;
114+
}
115+
116+
const onAbort = () => {
117+
controller.abort(signal.reason);
118+
cleanup();
119+
};
120+
signal.addEventListener('abort', onAbort);
121+
listeners.push(() => signal.removeEventListener('abort', onAbort));
122+
}
123+
return controller.signal;
124+
}
125+
15126
/**
16127
* AbortSignal cannot be constructed directly.
17128
*/
18129
constructor() {
19130
super();
20-
throw new TypeError('AbortSignal cannot be constructed directly');
131+
abortedFlags.set(this, false);
21132
}
22133

23134
/**
24135
* Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise.
25136
*/
26-
// $FlowExpectedError[unsafe-getters-setters]
27137
get aborted(): boolean {
28138
const aborted = abortedFlags.get(this);
29139
if (typeof aborted !== 'boolean') {
@@ -36,79 +146,91 @@ export class AbortSignal extends EventTarget {
36146
}
37147
return aborted;
38148
}
39-
}
40149

41-
const listeners = new WeakMap<AbortSignal, (()=> void)>();
42-
Object.defineProperty(AbortSignal.prototype, `onabort`, {
43-
enumerable: true,
44-
configurable: true,
45-
get() {
46-
// $FlowExpectedError[object-this-reference]
47-
return listeners.get(this) || null;
48-
},
49-
// $FlowExpectedError[missing-local-annot]
50-
set(value) {
51-
// $FlowExpectedError[object-this-reference]
52-
const currentListener = listeners.get(this);
53-
if (currentListener === value) return; // same handler? do nothing!
54-
if (currentListener) {
55-
// Before setting a new listener, remove the old one if exists
56-
// $FlowExpectedError[object-this-reference]
57-
this.removeEventListener('abort', currentListener);
58-
}
59-
if (typeof value === 'function') {
60-
// $FlowExpectedError[object-this-reference]
61-
listeners.set(this, value);
62-
// $FlowExpectedError[object-this-reference]
63-
this.addEventListener('abort', value);
64-
} else {
65-
// $FlowExpectedError[object-this-reference]
66-
listeners.delete(this);
67-
}
68-
},
69-
});
150+
get reason(): unknown {
151+
return reasons.get(this);
152+
}
153+
154+
get onabort(): EventCallback | null {
155+
return getEventHandlerAttribute(this, 'abort');
156+
}
70157

158+
set onabort(listener: ?EventCallback): void {
159+
setEventHandlerAttribute(this, 'abort', listener);
160+
}
161+
162+
throwIfAborted(): void {
163+
if (this.aborted) {
164+
throw this.reason;
165+
}
166+
}
167+
}
71168

72169
/**
73170
* Create an AbortSignal object.
74171
*/
75172
export function createAbortSignal(): AbortSignal {
76-
const signal = Object.create(AbortSignal.prototype);
77-
// $FlowExpectedError[incompatible-type]
78-
EventTarget.call(signal);
79-
abortedFlags.set(signal, false);
80-
return signal;
173+
return new AbortSignal();
81174
}
82175

83176
/**
84177
* Abort a given signal.
85178
*/
86-
export function abortSignal(signal: AbortSignal): void {
179+
export function abortSignal(
180+
reason: unknown | void = new DOMException(
181+
'signal is aborted without reason',
182+
'AbortError',
183+
),
184+
signal: AbortSignal,
185+
): void {
87186
if (abortedFlags.get(signal) !== false) {
88187
return;
89188
}
90189

91190
abortedFlags.set(signal, true);
92-
// $FlowExpectedError[incompatible-type]
191+
reasons.set(signal, reason);
93192
signal.dispatchEvent(new Event('abort'));
94193
}
95194

96195
/**
97196
* Aborted flag for each instances.
98197
*/
99-
const abortedFlags = new WeakMap<AbortSignal, boolean>()
198+
const abortedFlags = new WeakMap<AbortSignal, boolean>();
100199

101200
// Properties should be enumerable.
102201
//$FlowExpectedError[cannot-write]
103202
Object.defineProperties(AbortSignal.prototype, {
104203
aborted: {enumerable: true},
204+
reason: {enumerable: true},
205+
onabort: {enumerable: true},
206+
throwIfAborted: {enumerable: true},
105207
});
106208

107-
108209
// `toString()` should return `"[object AbortSignal]"`
109-
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
110-
Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
111-
configurable: true,
112-
value: "AbortSignal",
113-
})
114-
}
210+
Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
211+
configurable: true,
212+
value: 'AbortSignal',
213+
});
214+
215+
export const AbortSignal_public: typeof AbortSignal =
216+
/* eslint-disable no-shadow */
217+
// $FlowExpectedError[incompatible-type]
218+
function AbortSignal() {
219+
throw new TypeError(
220+
"Failed to construct 'AbortSignal': Illegal constructor",
221+
);
222+
};
223+
224+
// $FlowExpectedError[prop-missing]
225+
AbortSignal_public.prototype = AbortSignal.prototype;
226+
// Copy static properties so that callers accessing them via the public constructor (e.g. `AbortSignal.timeout(0)`) still work.
227+
// $FlowFixMe[unsafe-object-assign]
228+
// $FlowFixMe[not-an-object]
229+
['timeout', 'abort', 'any'].forEach(methodName => {
230+
Object.defineProperty(
231+
AbortSignal_public,
232+
methodName,
233+
// $FlowExpectedError[incompatible-type]
234+
Object.getOwnPropertyDescriptor(AbortSignal, methodName),
235+
);
236+
});

0 commit comments

Comments
 (0)