Skip to content
Open
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
7 changes: 5 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,12 @@ 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('../vendor/abort-controller/WithTimeoutAndAnyPolyfill') // flowlint-line untyped-import:off
.AbortController,
);
polyfillGlobal(
'AbortSignal',
() => require('abort-controller/dist/abort-controller').AbortSignal, // flowlint-line untyped-import:off
() =>
require('../vendor/abort-controller/WithTimeoutAndAnyPolyfill').AbortSignal, // flowlint-line untyped-import:off
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* The abort-controller polyfill does not implement parts of the modern API:
* - AbortSignal.timeout — https://github.com/mysticatea/abort-controller/issues/35
* - AbortSignal.any — https://github.com/mysticatea/abort-controller/issues/40
* - AbortSignal::reason — https://github.com/mysticatea/abort-controller/issues/36
*
* The package has not been updated for 7 years, so I (retyui) decided to patch it locally.
*/
import {
AbortController,
AbortSignal,
} from "abort-controller/dist/abort-controller";


const defineProperty = (obj, key, value) =>
Object.defineProperty(obj, key, {
writable: true,
enumerable: true,
configurable: true,
value,
});

const isReasonSupported = () => {
try {
const controller = new AbortController();
controller.abort("test reason");
return controller.signal.reason === "test reason";
} catch {
return false;
}
};


// 1. AbortSignal::reason polyfill
// Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/reason
// Spec: https://dom.spec.whatwg.org/#dom-abortsignal-reason
if (!isReasonSupported()) {
const originalAbort = AbortController.prototype.abort;
const reasonsMap = new WeakMap();

Object.defineProperty(AbortSignal.prototype, "reason", {
enumerable: true,
configurable: true,
get() {
return reasonsMap.get(this);
},
});

defineProperty(AbortController.prototype, "abort", function (reason) {
if (this.signal.aborted) {
return; // already aborted
}
// AbortError: https://developer.mozilla.org/en-US/docs/Web/API/DOMException#aborterror
const abortError = new Error("signal is aborted without reason");
abortError.name = "AbortError";
abortError.code = 20;
reasonsMap.set(this.signal, reason === undefined ? abortError : reason);
originalAbort.call(this);
});
}

// 2. AbortSignal.timeout static method polyfill
// Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
// Spec: https://dom.spec.whatwg.org/#dom-abortsignal-timeout
if (typeof AbortSignal.timeout !== "function") {
defineProperty(AbortSignal, "timeout", function (timeInMs) {
const isPositiveNumber = timeInMs >= 0;

if (!isPositiveNumber) {
throw new TypeError(
"Failed to execute 'timeout' on 'AbortSignal': The provided value have to be a non-negative number.",
);
}

const controller = new AbortController();

setTimeout(() => {
// TimeoutError: https://developer.mozilla.org/en-US/docs/Web/API/DOMException#timeouterror
const timeoutError = new Error("signal timed out");
timeoutError.name = "TimeoutError";
timeoutError.code = 23;
controller.abort(timeoutError);
}, timeInMs);

return controller.signal;
});
}


// 3. AbortSignal.any static method polyfill
// Docs: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static
// Spec: https://dom.spec.whatwg.org/#dom-abortsignal-any
if (typeof AbortSignal.any !== "function") {
defineProperty(AbortSignal, "any", function (signals) {
if (!Array.isArray(signals)) {
throw new Error("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);
signal.addEventListener("abort", onAbort, { once: true });
listeners.push(() => signal.removeEventListener("abort", onAbort));
}

return controller.signal;
});
}

export { AbortController, AbortSignal };
22 changes: 21 additions & 1 deletion packages/react-native/src/types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,13 @@ declare global {
*/
readonly aborted: boolean;

/**
* The **`reason`** read-only property returns a JavaScript value that indicates the abort reason.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason)
*/
readonly reason: any;

onabort: (event: AbortEvent) => void;

addEventListener: (
Expand All @@ -626,6 +633,19 @@ declare global {
capture?: boolean | undefined;
},
) => void;

/**
* The **`AbortSignal.any()`** static method takes an iterable of abort signals and returns an AbortSignal.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static)
*/
static any(signals: AbortSignal[]): AbortSignal;
/**
* The **`AbortSignal.timeout()`** static method returns an AbortSignal that will automatically abort after a specified time.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static)
*/
static timeout(milliseconds: number): AbortSignal;
}

class AbortController {
Expand All @@ -640,7 +660,7 @@ declare global {
/**
* Abort and signal to any observers that the associated activity is to be aborted.
*/
abort(): void;
abort(reason?: any): void;
}

interface FileReaderEventMap {
Expand Down
4 changes: 3 additions & 1 deletion packages/react-native/types/__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
Loading