diff --git a/packages/react-native/Libraries/Core/setUpXHR.js b/packages/react-native/Libraries/Core/setUpXHR.js index 5b7e03cbfbce..52ed8f28e386 100644 --- a/packages/react-native/Libraries/Core/setUpXHR.js +++ b/packages/react-native/Libraries/Core/setUpXHR.js @@ -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 ); diff --git a/packages/react-native/src/private/webapis/dom/abort-api/AbortController.js b/packages/react-native/src/private/webapis/dom/abort-api/AbortController.js new file mode 100644 index 000000000000..8917107c17ce --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/abort-api/AbortController.js @@ -0,0 +1,93 @@ +/** + * 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 {AbortSignal, abortSignal, createAbortSignal} from './AbortSignal'; + +/** + * The AbortController. + * @see https://dom.spec.whatwg.org/#abortcontroller + */ +export class AbortController { + /** + * Initialize this controller. + */ + constructor() { + signals.set(this, 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)); + } +} + +/** + * Associated signals. + */ +const signals = new WeakMap(); + +/** + * Get the associated signal of a given controller. + */ +function getSignal(controller: AbortController): AbortSignal { + const signal = signals.get(controller); + 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', +}); diff --git a/packages/react-native/src/private/webapis/dom/abort-api/AbortSignal.js b/packages/react-native/src/private/webapis/dom/abort-api/AbortSignal.js new file mode 100644 index 000000000000..497eda90795a --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/abort-api/AbortSignal.js @@ -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 reasons = new WeakMap(); + +/** + * 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 { + 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; + } + + /** + * AbortSignal cannot be constructed directly. + */ + constructor() { + super(); + abortedFlags.set(this, false); + } + + /** + * Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise. + */ + get aborted(): boolean { + const aborted = abortedFlags.get(this); + 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 { + return reasons.get(this); + } + + 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 { + if (abortedFlags.get(signal) !== false) { + return; + } + + abortedFlags.set(signal, true); + reasons.set(signal, reason); + signal.dispatchEvent(new Event('abort')); +} + +/** + * Aborted flag for each instances. + */ +const abortedFlags = new WeakMap(); + +// 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', +}); + +export const AbortSignal_public: typeof AbortSignal = + /* eslint-disable no-shadow */ + // $FlowExpectedError[incompatible-type] + function AbortSignal() { + throw new TypeError( + "Failed to construct 'AbortSignal': Illegal constructor", + ); + }; + +// $FlowExpectedError[prop-missing] +AbortSignal_public.prototype = AbortSignal.prototype; +// 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 => { + Object.defineProperty( + AbortSignal_public, + methodName, + // $FlowExpectedError[incompatible-type] + Object.getOwnPropertyDescriptor(AbortSignal, methodName), + ); +}); diff --git a/packages/react-native/src/private/webapis/dom/abort-api/__tests__/abort-api-itest.js b/packages/react-native/src/private/webapis/dom/abort-api/__tests__/abort-api-itest.js new file mode 100644 index 000000000000..06d0fc70e06b --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/abort-api/__tests__/abort-api-itest.js @@ -0,0 +1,326 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import DOMException from '../../../errors/DOMException'; +import {AbortController} from '../AbortController'; +import {AbortSignal_public as AbortSignal} from '../AbortSignal'; +import Event from 'react-native/src/private/webapis/dom/events/Event'; +import EventTarget from 'react-native/src/private/webapis/dom/events/EventTarget'; + +let listenerCallOrder = 0; + +type EventRecordingListener = JestMockFn<[Event], void> & { + eventData?: { + callOrder: number, + composedPath: ReadonlyArray, + currentTarget: Event['currentTarget'], + eventPhase: Event['eventPhase'], + target: Event['target'], + }, + ... +}; + +function createListener( + implementation?: Event => void, +): EventRecordingListener { + // $FlowExpectedError[incompatible-type] + const listener: EventRecordingListener = jest.fn((event: Event) => { + listener.eventData = { + callOrder: listenerCallOrder++, + composedPath: event.composedPath(), + currentTarget: event.currentTarget, + eventPhase: event.eventPhase, + target: event.target, + }; + + if (implementation) { + implementation(event); + } + }); + + return listener; +} + +describe('AbortController', () => { + let controller: AbortController; + + beforeEach(() => { + controller = new AbortController(); + }); + + it('should not be callable', () => { + expect(() => { + // $FlowExpectedError[prop-missing] + // $FlowExpectedError[constructor-as-function] + AbortController(); + }).toThrow('Cannot call a class as a function'); + }); + + it('should have 2 properties', () => { + const keys = new Set(['signal', 'abort']); + + for (const key in controller) { + expect(keys.has(key)).toBe(true); + keys.delete(key); + } + + expect(keys.size).toBe(0); + }); + + it('should be stringified as [object AbortController]', () => { + // $FlowExpectedError[method-unbinding] + expect(Object.prototype.toString.call(controller)).toBe( + '[object AbortController]', + ); + }); + + describe("'signal' property", () => { + let signal: AbortSignal; + + beforeEach(() => { + signal = controller.signal; + }); + + it('should return the same instance always', () => { + expect(controller.signal).toBe(signal); + }); + + it('should be an AbortSignal object', () => { + expect(signal).toBeInstanceOf(AbortSignal); + }); + + it('should be an EventTarget object', () => { + expect(signal).toBeInstanceOf(EventTarget); + }); + + it('should have required properties', () => { + const keys = new Set([ + 'aborted', + 'onabort', + 'reason', + 'throwIfAborted', + 'addEventListener', + 'dispatchEvent', + 'removeEventListener', + ]); + + for (const key in signal) { + expect(keys.has(key)).toBe(true); + keys.delete(key); + } + + expect(keys.size).toBe(0); + }); + + it("should have 'aborted' property which is false by default", () => { + expect(signal.aborted).toBe(false); + }); + + it("should have 'reason' property which is undefined by default", () => { + expect(signal.reason).toBe(undefined); + }); + + it("should have 'onabort' property which is null by default", () => { + // $FlowExpectedError[prop-missing] + expect(signal.onabort).toBe(null); + }); + + it("should throw a TypeError if 'signal.aborted' getter is called with non AbortSignal object", () => { + const proto = Object.getPrototypeOf(signal); + const descriptor = Object.getOwnPropertyDescriptor(proto, 'aborted'); + const getAborted = descriptor?.get; + + expect(() => { + if (getAborted) { + getAborted.call({}); + } else { + throw new TypeError(); + } + }).toThrow( + "Expected 'this' to be an 'AbortSignal' object, but got object", + ); + }); + + it('should be stringified as [object AbortSignal]', () => { + // $FlowExpectedError[method-unbinding] + expect(Object.prototype.toString.call(signal)).toBe( + '[object AbortSignal]', + ); + }); + }); + + describe("'abort' method", () => { + it("should set true to 'signal.aborted' property", () => { + controller.abort(); + expect(controller.signal.aborted).toBe(true); + }); + + it("should set default 'reason' when called without an argument", () => { + controller.abort(); + + const reason = controller.signal.reason; + expect(reason).toBeInstanceOf(DOMException); + //$FlowExpectedError[unclear-type] + const domException = reason as any as DOMException; + expect(domException.name).toBe('AbortError'); + expect(domException.message).toBe('signal is aborted without reason'); + }); + + it("should set the provided 'reason' when called with an argument", () => { + const reason = new Error('boom'); + + controller.abort(reason); + + expect(controller.signal.reason).toBe(reason); + }); + + it("should make 'throwIfAborted' throw the abort reason", () => { + const reason = {message: 'boom'}; + + controller.abort(reason); + + let thrown; + try { + controller.signal.throwIfAborted(); + } catch (error) { + thrown = error; + } + + expect(thrown).toBe(reason); + }); + + it("should not throw from 'throwIfAborted' before aborting", () => { + expect(() => { + controller.signal.throwIfAborted(); + }).not.toThrow(); + }); + + it("should fire 'abort' event on 'signal' (addEventListener)", () => { + const listener = createListener(); + controller.signal.addEventListener('abort', listener); + controller.abort(); + + expect(listener).toHaveBeenCalledTimes(1); + }); + + it("should fire 'abort' event on 'signal' (onabort)", () => { + const listener = createListener(); + // $FlowExpectedError[incompatible-type] + // $FlowExpectedError[prop-missing] + controller.signal.onabort = listener; + controller.abort(); + + expect(listener).toHaveBeenCalledTimes(1); + }); + + it("should not fire 'abort' event twice", () => { + const listener = createListener(); + controller.signal.addEventListener('abort', listener); + + controller.abort(); + controller.abort(); + controller.abort(); + + expect(listener).toHaveBeenCalledTimes(1); + }); + + it("should throw a TypeError if 'this' is not an AbortController object", () => { + expect(() => { + // $FlowExpectedError[method-unbinding] + controller.abort.call({}); + }).toThrow( + "Expected 'this' to be an 'AbortController' object, but got object", + ); + }); + }); + + describe("'any' static method", () => { + it('should abort when one of the provided signals aborts', () => { + const first = new AbortController(); + const second = new AbortController(); + const reason = new Error('stop'); + + const signal = AbortSignal.any([first.signal, second.signal]); + + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal.aborted).toBe(false); + + second.abort(reason); + + expect(signal.aborted).toBe(true); + expect(signal.reason).toBe(reason); + }); + + it('should abort immediately if one of the provided signals is already aborted', () => { + const first = new AbortController(); + const second = new AbortController(); + const reason = new Error('already aborted'); + + second.abort(reason); + + const signal = AbortSignal.any([first.signal, second.signal]); + + expect(signal.aborted).toBe(true); + expect(signal.reason).toBe(reason); + }); + }); + + describe("'timeout' static method", () => { + beforeEach(() => { + //jest.useFakeTimers(); + }); + + afterEach(() => { + //jest.useRealTimers(); + }); + + it('should abort after the timeout with a TimeoutError reason', async () => { + const signal = AbortSignal.timeout(10); + + expect(signal.aborted).toBe(false); + + await new Promise(resolve => setTimeout(resolve, 20)); + + expect(signal.aborted).toBe(true); + expect(signal.reason).toBeInstanceOf(DOMException); + expect(signal.reason).toMatchObject({ + name: 'TimeoutError', + message: 'signal timed out', + }); + }); + + it('should throw a TypeError for a negative timeout', () => { + expect(() => { + AbortSignal.timeout(-1); + }).toThrow( + "Failed to execute 'timeout' on 'AbortSignal': The provided value has to be a non-negative number.", + ); + }); + }); +}); + +describe('AbortSignal', () => { + it('should not be callable', () => { + expect(() => { + // $FlowExpectedError[prop-missing] + // $FlowExpectedError[constructor-as-function] + AbortSignal(); + }).toThrow("Failed to construct 'AbortSignal': Illegal constructor"); + }); + + it("should throw a TypeError when it's constructed directly", () => { + expect(() => { + // $FlowExpectedError[cannot-new] + // eslint-disable-next-line no-new + new AbortSignal(); + }).toThrow("Failed to construct 'AbortSignal': Illegal constructor"); + }); +}); diff --git a/packages/react-native/src/types/globals.d.ts b/packages/react-native/src/types/globals.d.ts index 4082da352804..76f45042ee28 100644 --- a/packages/react-native/src/types/globals.d.ts +++ b/packages/react-native/src/types/globals.d.ts @@ -600,6 +600,12 @@ declare global { * Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise. */ 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; @@ -626,6 +632,32 @@ declare global { capture?: boolean | undefined; }, ) => void; + + /** + * Throws the abort reason if the signal has been aborted. + * Otherwise, does nothing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted) + */ + throwIfAborted(): void; + /** + * The **`AbortSignal.abort(reason)`** static method returns an AbortSignal that is already set as aborted (and which does not trigger an abort event). + * + * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort_static) + */ + static abort(reason: any): AbortSignal; + /** + * 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 { @@ -640,7 +672,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 { diff --git a/packages/react-native/types/__typetests__/globals.tsx b/packages/react-native/types/__typetests__/globals.tsx index 8bacebf8c5f7..7d95b048a3d5 100644 --- a/packages/react-native/types/__typetests__/globals.tsx +++ b/packages/react-native/types/__typetests__/globals.tsx @@ -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'); diff --git a/yarn.lock b/yarn.lock index 1a7a2ec0229a..ff2a459e94cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2578,13 +2578,6 @@ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz#00d1dd940b218dff2e49309d410d8bb212159225" integrity sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw== -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - accepts@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" @@ -4509,11 +4502,6 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - eventemitter3@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4"