Set up test to validate refactor of XHR, FileReader and WebSocket classes to use the built-in EventTarget implementation (#48930)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48930

Changelog: [internal]

This creates new versions of `XMLHttpRequest`, `FileReader` and `WebSocket` that extend the new built-in `EventTarget` implementation, instead of the implementation from the `event-target-shim` package.

It also sets up a test to choose between the 2 implementations at runtime to verify correctness and performance. This doesn't use the RN feature flags infra because we use this flag very early on startup, before we have a chance to set overrides. We could use a native feature flag instead but it'd slow down the rollout of the test.

Reviewed By: yungsters

Differential Revision: D68625226

fbshipit-source-id: bff715c43a237b65d5a02a3fdb56f3275689ea46
This commit is contained in:
Rubén Norte
2025-01-29 10:06:08 -08:00
committed by Facebook GitHub Bot
parent d80284c607
commit ea1260accb
15 changed files with 3266 additions and 1402 deletions
+8 -174
View File
@@ -8,179 +8,13 @@
* @format
*/
import type Blob from './Blob';
import typeof FileReader from './FileReader_old';
import NativeFileReaderModule from './NativeFileReaderModule';
import {toByteArray} from 'base64-js';
import EventTarget from 'event-target-shim';
// Use a global instead of a flag from ReactNativeFeatureFlags because this will
// be read before apps have a chance to set overrides.
const useBuiltInEventTarget = global.RN$useBuiltInEventTarget?.();
type ReadyState =
| 0 // EMPTY
| 1 // LOADING
| 2; // DONE
type ReaderResult = string | ArrayBuffer;
const READER_EVENTS = [
'abort',
'error',
'load',
'loadstart',
'loadend',
'progress',
];
const EMPTY = 0;
const LOADING = 1;
const DONE = 2;
class FileReader extends (EventTarget(...READER_EVENTS): typeof EventTarget) {
static EMPTY: number = EMPTY;
static LOADING: number = LOADING;
static DONE: number = DONE;
EMPTY: number = EMPTY;
LOADING: number = LOADING;
DONE: number = DONE;
_readyState: ReadyState;
_error: ?Error;
_result: ?ReaderResult;
_aborted: boolean = false;
constructor() {
super();
this._reset();
}
_reset(): void {
this._readyState = EMPTY;
this._error = null;
this._result = null;
}
_setReadyState(newState: ReadyState) {
this._readyState = newState;
this.dispatchEvent({type: 'readystatechange'});
if (newState === DONE) {
if (this._aborted) {
this.dispatchEvent({type: 'abort'});
} else if (this._error) {
this.dispatchEvent({type: 'error'});
} else {
this.dispatchEvent({type: 'load'});
}
this.dispatchEvent({type: 'loadend'});
}
}
readAsArrayBuffer(blob: ?Blob): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsArrayBuffer' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (this._aborted) {
return;
}
const base64 = text.split(',')[1];
const typedArray = toByteArray(base64);
this._result = typedArray.buffer;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
readAsDataURL(blob: ?Blob): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
readAsText(blob: ?Blob, encoding: string = 'UTF-8'): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsText(blob.data, encoding).then(
(text: string) => {
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
abort() {
this._aborted = true;
// only call onreadystatechange if there is something to abort, as per spec
if (this._readyState !== EMPTY && this._readyState !== DONE) {
this._reset();
this._setReadyState(DONE);
}
// Reset again after, in case modified in handler
this._reset();
}
get readyState(): ReadyState {
return this._readyState;
}
get error(): ?Error {
return this._error;
}
get result(): ?ReaderResult {
return this._result;
}
}
export default FileReader;
export default (useBuiltInEventTarget
? // $FlowExpectedError[incompatible-cast]
require('./FileReader_new').default
: require('./FileReader_old').default) as FileReader;
+231
View File
@@ -0,0 +1,231 @@
/**
* 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
* @format
*/
import type {EventCallback} from '../../src/private/webapis/dom/events/EventTarget';
import type Blob from './Blob';
import Event from '../../src/private/webapis/dom/events/Event';
import {
getEventHandlerAttribute,
setEventHandlerAttribute,
} from '../../src/private/webapis/dom/events/EventHandlerAttributes';
import EventTarget from '../../src/private/webapis/dom/events/EventTarget';
import NativeFileReaderModule from './NativeFileReaderModule';
import {toByteArray} from 'base64-js';
type ReadyState =
| 0 // EMPTY
| 1 // LOADING
| 2; // DONE
type ReaderResult = string | ArrayBuffer;
const EMPTY = 0;
const LOADING = 1;
const DONE = 2;
class FileReader extends EventTarget {
static EMPTY: number = EMPTY;
static LOADING: number = LOADING;
static DONE: number = DONE;
EMPTY: number = EMPTY;
LOADING: number = LOADING;
DONE: number = DONE;
_readyState: ReadyState;
_error: ?Error;
_result: ?ReaderResult;
_aborted: boolean = false;
constructor() {
super();
this._reset();
}
_reset(): void {
this._readyState = EMPTY;
this._error = null;
this._result = null;
}
_setReadyState(newState: ReadyState) {
this._readyState = newState;
this.dispatchEvent(new Event('readystatechange'));
if (newState === DONE) {
if (this._aborted) {
this.dispatchEvent(new Event('abort'));
} else if (this._error) {
this.dispatchEvent(new Event('error'));
} else {
this.dispatchEvent(new Event('load'));
}
this.dispatchEvent(new Event('loadend'));
}
}
readAsArrayBuffer(blob: ?Blob): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsArrayBuffer' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (this._aborted) {
return;
}
const base64 = text.split(',')[1];
const typedArray = toByteArray(base64);
this._result = typedArray.buffer;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
readAsDataURL(blob: ?Blob): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
readAsText(blob: ?Blob, encoding: string = 'UTF-8'): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsText(blob.data, encoding).then(
(text: string) => {
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
abort() {
this._aborted = true;
// only call onreadystatechange if there is something to abort, as per spec
if (this._readyState !== EMPTY && this._readyState !== DONE) {
this._reset();
this._setReadyState(DONE);
}
// Reset again after, in case modified in handler
this._reset();
}
get readyState(): ReadyState {
return this._readyState;
}
get error(): ?Error {
return this._error;
}
get result(): ?ReaderResult {
return this._result;
}
get onabort(): EventCallback | null {
return getEventHandlerAttribute(this, 'abort');
}
set onabort(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'abort', listener);
}
get onerror(): EventCallback | null {
return getEventHandlerAttribute(this, 'error');
}
set onerror(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'error', listener);
}
get onload(): EventCallback | null {
return getEventHandlerAttribute(this, 'load');
}
set onload(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'load', listener);
}
get onloadstart(): EventCallback | null {
return getEventHandlerAttribute(this, 'loadstart');
}
set onloadstart(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'loadstart', listener);
}
get onloadend(): EventCallback | null {
return getEventHandlerAttribute(this, 'loadend');
}
set onloadend(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'loadend', listener);
}
get onprogress(): EventCallback | null {
return getEventHandlerAttribute(this, 'progress');
}
set onprogress(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'progress', listener);
}
}
export default FileReader;
+186
View File
@@ -0,0 +1,186 @@
/**
* 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
* @format
*/
import type Blob from './Blob';
import NativeFileReaderModule from './NativeFileReaderModule';
import {toByteArray} from 'base64-js';
import EventTarget from 'event-target-shim';
type ReadyState =
| 0 // EMPTY
| 1 // LOADING
| 2; // DONE
type ReaderResult = string | ArrayBuffer;
const READER_EVENTS = [
'abort',
'error',
'load',
'loadstart',
'loadend',
'progress',
];
const EMPTY = 0;
const LOADING = 1;
const DONE = 2;
class FileReader extends (EventTarget(...READER_EVENTS): typeof EventTarget) {
static EMPTY: number = EMPTY;
static LOADING: number = LOADING;
static DONE: number = DONE;
EMPTY: number = EMPTY;
LOADING: number = LOADING;
DONE: number = DONE;
_readyState: ReadyState;
_error: ?Error;
_result: ?ReaderResult;
_aborted: boolean = false;
constructor() {
super();
this._reset();
}
_reset(): void {
this._readyState = EMPTY;
this._error = null;
this._result = null;
}
_setReadyState(newState: ReadyState) {
this._readyState = newState;
this.dispatchEvent({type: 'readystatechange'});
if (newState === DONE) {
if (this._aborted) {
this.dispatchEvent({type: 'abort'});
} else if (this._error) {
this.dispatchEvent({type: 'error'});
} else {
this.dispatchEvent({type: 'load'});
}
this.dispatchEvent({type: 'loadend'});
}
}
readAsArrayBuffer(blob: ?Blob): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsArrayBuffer' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (this._aborted) {
return;
}
const base64 = text.split(',')[1];
const typedArray = toByteArray(base64);
this._result = typedArray.buffer;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
readAsDataURL(blob: ?Blob): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
readAsText(blob: ?Blob, encoding: string = 'UTF-8'): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsText(blob.data, encoding).then(
(text: string) => {
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
abort() {
this._aborted = true;
// only call onreadystatechange if there is something to abort, as per spec
if (this._readyState !== EMPTY && this._readyState !== DONE) {
this._reset();
this._setReadyState(DONE);
}
// Reset again after, in case modified in handler
this._reset();
}
get readyState(): ReadyState {
return this._readyState;
}
get error(): ?Error {
return this._error;
}
get result(): ?ReaderResult {
return this._result;
}
}
export default FileReader;
@@ -10,6 +10,9 @@
'use strict';
const Blob = require('../Blob').default;
const FileReader = require('../FileReader').default;
jest.unmock('event-target-shim').setMock('../../BatchedBridge/NativeModules', {
__esModule: true,
default: {
@@ -18,39 +21,43 @@ jest.unmock('event-target-shim').setMock('../../BatchedBridge/NativeModules', {
},
});
const Blob = require('../Blob').default;
const FileReader = require('../FileReader').default;
describe('FileReader', function () {
it('should read blob as text', async () => {
const e = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = resolve;
reader.onerror = reject;
reader.readAsText(new Blob());
[false, true].forEach(enableModern => {
describe(`FileReader (${enableModern ? 'modern' : 'legacy'})`, function () {
beforeAll(() => {
jest.resetModules();
global.RN$useBuiltInEventTarget = () => enableModern;
});
expect(e.target.result).toBe('');
});
it('should read blob as data URL', async () => {
const e = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = resolve;
reader.onerror = reject;
reader.readAsDataURL(new Blob());
it('should read blob as text', async () => {
const e = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = resolve;
reader.onerror = reject;
reader.readAsText(new Blob());
});
expect(e.target.result).toBe('');
});
expect(e.target.result).toBe('data:text/plain;base64,NDI=');
});
it('should read blob as ArrayBuffer', async () => {
const e = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = resolve;
reader.onerror = reject;
reader.readAsArrayBuffer(new Blob());
it('should read blob as data URL', async () => {
const e = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = resolve;
reader.onerror = reject;
reader.readAsDataURL(new Blob());
});
expect(e.target.result).toBe('data:text/plain;base64,NDI=');
});
it('should read blob as ArrayBuffer', async () => {
const e = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = resolve;
reader.onerror = reject;
reader.readAsArrayBuffer(new Blob());
});
const ab = e.target.result;
expect(ab.byteLength).toBe(2);
expect(new TextDecoder().decode(ab)).toBe('42');
});
const ab = e.target.result;
expect(ab.byteLength).toBe(2);
expect(new TextDecoder().decode(ab)).toBe('42');
});
});
+11 -685
View File
@@ -8,691 +8,17 @@
* @flow
*/
'use strict';
import typeof XMLHttpRequest from './XMLHttpRequest_old';
import type {IPerformanceLogger} from '../Utilities/createPerformanceLogger';
export type * from './XMLHttpRequest_old';
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
import EventTarget from 'event-target-shim';
// Use a global instead of a flag from ReactNativeFeatureFlags because this will
// be read before apps have a chance to set overrides.
const useBuiltInEventTarget = global.RN$useBuiltInEventTarget?.();
const BlobManager = require('../Blob/BlobManager').default;
const GlobalPerformanceLogger = require('../Utilities/GlobalPerformanceLogger');
const RCTNetworking = require('./RCTNetworking').default;
const base64 = require('base64-js');
const invariant = require('invariant');
const DEBUG_NETWORK_SEND_DELAY: false = false; // Set to a number of milliseconds when debugging
const LABEL_FOR_MISSING_URL_FOR_PROFILING = 'Unknown URL';
export type NativeResponseType = 'base64' | 'blob' | 'text';
export type ResponseType =
| ''
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text';
export type Response = ?Object | string;
type XHRInterceptor = interface {
requestSent(id: number, url: string, method: string, headers: Object): void,
responseReceived(
id: number,
url: string,
status: number,
headers: Object,
): void,
dataReceived(id: number, data: string): void,
loadingFinished(id: number, encodedDataLength: number): void,
loadingFailed(id: number, error: string): void,
};
// The native blob module is optional so inject it here if available.
if (BlobManager.isAvailable) {
BlobManager.addNetworkingHandler();
}
const UNSENT = 0;
const OPENED = 1;
const HEADERS_RECEIVED = 2;
const LOADING = 3;
const DONE = 4;
const SUPPORTED_RESPONSE_TYPES = {
arraybuffer: typeof global.ArrayBuffer === 'function',
blob: typeof global.Blob === 'function',
document: false,
json: true,
text: true,
'': true,
};
const REQUEST_EVENTS = [
'abort',
'error',
'load',
'loadstart',
'progress',
'timeout',
'loadend',
];
const XHR_EVENTS = REQUEST_EVENTS.concat('readystatechange');
class XMLHttpRequestEventTarget extends (EventTarget(
...REQUEST_EVENTS,
): typeof EventTarget) {
onload: ?Function;
onloadstart: ?Function;
onprogress: ?Function;
ontimeout: ?Function;
onerror: ?Function;
onabort: ?Function;
onloadend: ?Function;
}
/**
* Shared base for platform-specific XMLHttpRequest implementations.
*/
class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) {
static UNSENT: number = UNSENT;
static OPENED: number = OPENED;
static HEADERS_RECEIVED: number = HEADERS_RECEIVED;
static LOADING: number = LOADING;
static DONE: number = DONE;
static _interceptor: ?XHRInterceptor = null;
static _profiling: boolean = false;
UNSENT: number = UNSENT;
OPENED: number = OPENED;
HEADERS_RECEIVED: number = HEADERS_RECEIVED;
LOADING: number = LOADING;
DONE: number = DONE;
// EventTarget automatically initializes these to `null`.
onload: ?Function;
onloadstart: ?Function;
onprogress: ?Function;
ontimeout: ?Function;
onerror: ?Function;
onabort: ?Function;
onloadend: ?Function;
onreadystatechange: ?Function;
readyState: number = UNSENT;
responseHeaders: ?Object;
status: number = 0;
timeout: number = 0;
responseURL: ?string;
withCredentials: boolean = true;
upload: XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
_requestId: ?number;
_subscriptions: Array<EventSubscription>;
_aborted: boolean = false;
_cachedResponse: Response;
_hasError: boolean = false;
_headers: Object;
_lowerCaseResponseHeaders: Object;
_method: ?string = null;
_perfKey: ?string = null;
_responseType: ResponseType;
_response: string = '';
_sent: boolean;
_url: ?string = null;
_timedOut: boolean = false;
_trackingName: string = 'unknown';
_incrementalEvents: boolean = false;
_startTime: ?number = null;
_performanceLogger: IPerformanceLogger = GlobalPerformanceLogger;
static setInterceptor(interceptor: ?XHRInterceptor) {
XMLHttpRequest._interceptor = interceptor;
}
static enableProfiling(enableProfiling: boolean): void {
XMLHttpRequest._profiling = enableProfiling;
}
constructor() {
super();
this._reset();
}
_reset(): void {
this.readyState = this.UNSENT;
this.responseHeaders = undefined;
this.status = 0;
delete this.responseURL;
this._requestId = null;
this._cachedResponse = undefined;
this._hasError = false;
this._headers = {};
this._response = '';
this._responseType = '';
this._sent = false;
this._lowerCaseResponseHeaders = {};
this._clearSubscriptions();
this._timedOut = false;
}
get responseType(): ResponseType {
return this._responseType;
}
set responseType(responseType: ResponseType): void {
if (this._sent) {
throw new Error(
"Failed to set the 'responseType' property on 'XMLHttpRequest': The " +
'response type cannot be set after the request has been sent.',
);
}
if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) {
console.warn(
`The provided value '${responseType}' is not a valid 'responseType'.`,
);
return;
}
// redboxes early, e.g. for 'arraybuffer' on ios 7
invariant(
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
`The provided value '${responseType}' is unsupported in this environment.`,
);
if (responseType === 'blob') {
invariant(
BlobManager.isAvailable,
'Native module BlobModule is required for blob support',
);
}
this._responseType = responseType;
}
get responseText(): string {
if (this._responseType !== '' && this._responseType !== 'text') {
throw new Error(
"The 'responseText' property is only available if 'responseType' " +
`is set to '' or 'text', but it is '${this._responseType}'.`,
);
}
if (this.readyState < LOADING) {
return '';
}
return this._response;
}
get response(): Response {
const {responseType} = this;
if (responseType === '' || responseType === 'text') {
return this.readyState < LOADING || this._hasError ? '' : this._response;
}
if (this.readyState !== DONE) {
return null;
}
if (this._cachedResponse !== undefined) {
return this._cachedResponse;
}
switch (responseType) {
case 'document':
this._cachedResponse = null;
break;
case 'arraybuffer':
this._cachedResponse = base64.toByteArray(this._response).buffer;
break;
case 'blob':
if (typeof this._response === 'object' && this._response) {
this._cachedResponse = BlobManager.createFromOptions(this._response);
} else if (this._response === '') {
this._cachedResponse = BlobManager.createFromParts([]);
} else {
throw new Error(
'Invalid response for blob - expecting object, was ' +
`${typeof this._response}: ${this._response.trim()}`,
);
}
break;
case 'json':
try {
this._cachedResponse = JSON.parse(this._response);
} catch (_) {
this._cachedResponse = null;
}
break;
default:
this._cachedResponse = null;
}
return this._cachedResponse;
}
// exposed for testing
__didCreateRequest(requestId: number): void {
this._requestId = requestId;
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.requestSent(
requestId,
this._url || '',
this._method || 'GET',
this._headers,
);
}
// exposed for testing
__didUploadProgress(
requestId: number,
progress: number,
total: number,
): void {
if (requestId === this._requestId) {
this.upload.dispatchEvent({
type: 'progress',
lengthComputable: true,
loaded: progress,
total,
});
}
}
__didReceiveResponse(
requestId: number,
status: number,
responseHeaders: ?Object,
responseURL: ?string,
): void {
if (requestId === this._requestId) {
this._perfKey != null &&
this._performanceLogger.stopTimespan(this._perfKey);
this.status = status;
this.setResponseHeaders(responseHeaders);
this.setReadyState(this.HEADERS_RECEIVED);
if (responseURL || responseURL === '') {
this.responseURL = responseURL;
} else {
delete this.responseURL;
}
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.responseReceived(
requestId,
responseURL || this._url || '',
status,
responseHeaders || {},
);
}
}
__didReceiveData(requestId: number, response: string): void {
if (requestId !== this._requestId) {
return;
}
this._response = response;
this._cachedResponse = undefined; // force lazy recomputation
this.setReadyState(this.LOADING);
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.dataReceived(requestId, response);
}
__didReceiveIncrementalData(
requestId: number,
responseText: string,
progress: number,
total: number,
) {
if (requestId !== this._requestId) {
return;
}
if (!this._response) {
this._response = responseText;
} else {
this._response += responseText;
}
if (XMLHttpRequest._profiling) {
performance.mark(
'Track:XMLHttpRequest:Incremental Data: ' + this._getMeasureURL(),
);
}
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.dataReceived(requestId, responseText);
this.setReadyState(this.LOADING);
this.__didReceiveDataProgress(requestId, progress, total);
}
__didReceiveDataProgress(
requestId: number,
loaded: number,
total: number,
): void {
if (requestId !== this._requestId) {
return;
}
this.dispatchEvent({
type: 'progress',
lengthComputable: total >= 0,
loaded,
total,
});
}
// exposed for testing
__didCompleteResponse(
requestId: number,
error: string,
timeOutError: boolean,
): void {
if (requestId === this._requestId) {
if (error) {
if (this._responseType === '' || this._responseType === 'text') {
this._response = error;
}
this._hasError = true;
if (timeOutError) {
this._timedOut = true;
}
}
this._clearSubscriptions();
this._requestId = null;
this.setReadyState(this.DONE);
if (XMLHttpRequest._profiling && this._startTime != null) {
const start = this._startTime;
performance.measure('Track:XMLHttpRequest:' + this._getMeasureURL(), {
start,
end: performance.now(),
});
}
if (error) {
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.loadingFailed(requestId, error);
} else {
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.loadingFinished(
requestId,
this._response.length,
);
}
}
}
_clearSubscriptions(): void {
(this._subscriptions || []).forEach(sub => {
if (sub) {
sub.remove();
}
});
this._subscriptions = [];
}
getAllResponseHeaders(): ?string {
if (!this.responseHeaders) {
// according to the spec, return null if no response has been received
return null;
}
// Assign to non-nullable local variable.
const responseHeaders = this.responseHeaders;
const unsortedHeaders: Map<
string,
{lowerHeaderName: string, upperHeaderName: string, headerValue: string},
> = new Map();
for (const rawHeaderName of Object.keys(responseHeaders)) {
const headerValue = responseHeaders[rawHeaderName];
const lowerHeaderName = rawHeaderName.toLowerCase();
const header = unsortedHeaders.get(lowerHeaderName);
if (header) {
header.headerValue += ', ' + headerValue;
unsortedHeaders.set(lowerHeaderName, header);
} else {
unsortedHeaders.set(lowerHeaderName, {
lowerHeaderName,
upperHeaderName: rawHeaderName.toUpperCase(),
headerValue,
});
}
}
// Sort in ascending order, with a being less than b if a's name is legacy-uppercased-byte less than b's name.
const sortedHeaders = [...unsortedHeaders.values()].sort((a, b) => {
if (a.upperHeaderName < b.upperHeaderName) {
return -1;
}
if (a.upperHeaderName > b.upperHeaderName) {
return 1;
}
return 0;
});
// Combine into single text response.
return (
sortedHeaders
.map(header => {
return header.lowerHeaderName + ': ' + header.headerValue;
})
.join('\r\n') + '\r\n'
);
}
getResponseHeader(header: string): ?string {
const value = this._lowerCaseResponseHeaders[header.toLowerCase()];
return value !== undefined ? value : null;
}
setRequestHeader(header: string, value: any): void {
if (this.readyState !== this.OPENED) {
throw new Error('Request has not been opened');
}
this._headers[header.toLowerCase()] = String(value);
}
/**
* Custom extension for tracking origins of request.
*/
setTrackingName(trackingName: string): XMLHttpRequest {
this._trackingName = trackingName;
return this;
}
/**
* Custom extension for setting a custom performance logger
*/
setPerformanceLogger(performanceLogger: IPerformanceLogger): XMLHttpRequest {
this._performanceLogger = performanceLogger;
return this;
}
open(method: string, url: string, async: ?boolean): void {
/* Other optional arguments are not supported yet */
if (this.readyState !== this.UNSENT) {
throw new Error('Cannot open, already sending');
}
if (async !== undefined && !async) {
// async is default
throw new Error('Synchronous http requests are not supported');
}
if (!url) {
throw new Error('Cannot load an empty url');
}
this._method = method.toUpperCase();
this._url = url;
this._aborted = false;
this.setReadyState(this.OPENED);
}
send(data: any): void {
if (this.readyState !== this.OPENED) {
throw new Error('Request has not been opened');
}
if (this._sent) {
throw new Error('Request has already been sent');
}
this._sent = true;
const incrementalEvents =
this._incrementalEvents || !!this.onreadystatechange || !!this.onprogress;
this._subscriptions.push(
RCTNetworking.addListener('didSendNetworkData', args =>
this.__didUploadProgress(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkResponse', args =>
this.__didReceiveResponse(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkData', args =>
this.__didReceiveData(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkIncrementalData', args =>
this.__didReceiveIncrementalData(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkDataProgress', args =>
this.__didReceiveDataProgress(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didCompleteNetworkResponse', args =>
this.__didCompleteResponse(...args),
),
);
let nativeResponseType: NativeResponseType = 'text';
if (this._responseType === 'arraybuffer') {
nativeResponseType = 'base64';
}
if (this._responseType === 'blob') {
nativeResponseType = 'blob';
}
const doSend = () => {
const friendlyName =
this._trackingName !== 'unknown' ? this._trackingName : this._url;
this._perfKey = 'network_XMLHttpRequest_' + String(friendlyName);
this._performanceLogger.startTimespan(this._perfKey);
this._startTime = performance.now();
invariant(
this._method,
'XMLHttpRequest method needs to be defined (%s).',
friendlyName,
);
invariant(
this._url,
'XMLHttpRequest URL needs to be defined (%s).',
friendlyName,
);
RCTNetworking.sendRequest(
this._method,
this._trackingName,
this._url,
this._headers,
data,
/* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
* when making Flow check .android.js files. */
nativeResponseType,
incrementalEvents,
this.timeout,
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this.__didCreateRequest.bind(this),
this.withCredentials,
);
};
if (DEBUG_NETWORK_SEND_DELAY) {
setTimeout(doSend, DEBUG_NETWORK_SEND_DELAY);
} else {
doSend();
}
}
abort(): void {
this._aborted = true;
if (this._requestId) {
RCTNetworking.abortRequest(this._requestId);
}
// only call onreadystatechange if there is something to abort,
// below logic is per spec
if (
!(
this.readyState === this.UNSENT ||
(this.readyState === this.OPENED && !this._sent) ||
this.readyState === this.DONE
)
) {
this._reset();
this.setReadyState(this.DONE);
}
// Reset again after, in case modified in handler
this._reset();
}
setResponseHeaders(responseHeaders: ?Object): void {
this.responseHeaders = responseHeaders || null;
const headers = responseHeaders || {};
this._lowerCaseResponseHeaders = Object.keys(headers).reduce<{
[string]: any,
}>((lcaseHeaders, headerName) => {
// $FlowFixMe[invalid-computed-prop]
lcaseHeaders[headerName.toLowerCase()] = headers[headerName];
return lcaseHeaders;
}, {});
}
setReadyState(newState: number): void {
this.readyState = newState;
this.dispatchEvent({type: 'readystatechange'});
if (newState === this.DONE) {
if (this._aborted) {
this.dispatchEvent({type: 'abort'});
} else if (this._hasError) {
if (this._timedOut) {
this.dispatchEvent({type: 'timeout'});
} else {
this.dispatchEvent({type: 'error'});
}
} else {
this.dispatchEvent({type: 'load'});
}
this.dispatchEvent({type: 'loadend'});
}
}
/* global EventListener */
addEventListener(type: string, listener: EventListener): void {
// If we dont' have a 'readystatechange' event handler, we don't
// have to send repeated LOADING events with incremental updates
// to responseText, which will avoid a bunch of native -> JS
// bridge traffic.
if (type === 'readystatechange' || type === 'progress') {
this._incrementalEvents = true;
}
super.addEventListener(type, listener);
}
_getMeasureURL(): string {
return (
this._trackingName ?? this._url ?? LABEL_FOR_MISSING_URL_FOR_PROFILING
);
}
}
module.exports = XMLHttpRequest;
module.exports = (
useBuiltInEventTarget
? // $FlowExpectedError[incompatible-cast]
require('./XMLHttpRequest_new')
: require('./XMLHttpRequest_old')
) as XMLHttpRequest;
@@ -0,0 +1,791 @@
/**
* 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.
*
* @format
* @flow
*/
'use strict';
import type {
EventCallback,
EventListener,
} from '../../src/private/webapis/dom/events/EventTarget';
import type {IPerformanceLogger} from '../Utilities/createPerformanceLogger';
import Event from '../../src/private/webapis/dom/events/Event';
import {
getEventHandlerAttribute,
setEventHandlerAttribute,
} from '../../src/private/webapis/dom/events/EventHandlerAttributes';
import EventTarget from '../../src/private/webapis/dom/events/EventTarget';
import {dispatchTrustedEvent} from '../../src/private/webapis/dom/events/internals/EventTargetInternals';
import ProgressEvent from '../../src/private/webapis/xhr/events/ProgressEvent';
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
const BlobManager = require('../Blob/BlobManager').default;
const GlobalPerformanceLogger = require('../Utilities/GlobalPerformanceLogger');
const RCTNetworking = require('./RCTNetworking').default;
const base64 = require('base64-js');
const invariant = require('invariant');
const DEBUG_NETWORK_SEND_DELAY: false = false; // Set to a number of milliseconds when debugging
const LABEL_FOR_MISSING_URL_FOR_PROFILING = 'Unknown URL';
export type NativeResponseType = 'base64' | 'blob' | 'text';
export type ResponseType =
| ''
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text';
export type Response = ?Object | string;
type XHRInterceptor = interface {
requestSent(id: number, url: string, method: string, headers: Object): void,
responseReceived(
id: number,
url: string,
status: number,
headers: Object,
): void,
dataReceived(id: number, data: string): void,
loadingFinished(id: number, encodedDataLength: number): void,
loadingFailed(id: number, error: string): void,
};
// The native blob module is optional so inject it here if available.
if (BlobManager.isAvailable) {
BlobManager.addNetworkingHandler();
}
const UNSENT = 0;
const OPENED = 1;
const HEADERS_RECEIVED = 2;
const LOADING = 3;
const DONE = 4;
const SUPPORTED_RESPONSE_TYPES = {
arraybuffer: typeof global.ArrayBuffer === 'function',
blob: typeof global.Blob === 'function',
document: false,
json: true,
text: true,
'': true,
};
class XMLHttpRequestEventTarget extends EventTarget {
get onload(): EventCallback | null {
return getEventHandlerAttribute(this, 'load');
}
set onload(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'load', listener);
}
get onloadstart(): EventCallback | null {
return getEventHandlerAttribute(this, 'loadstart');
}
set onloadstart(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'loadstart', listener);
}
get onprogress(): EventCallback | null {
return getEventHandlerAttribute(this, 'progress');
}
set onprogress(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'progress', listener);
}
get ontimeout(): EventCallback | null {
return getEventHandlerAttribute(this, 'timeout');
}
set ontimeout(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'timeout', listener);
}
get onerror(): EventCallback | null {
return getEventHandlerAttribute(this, 'error');
}
set onerror(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'error', listener);
}
get onabort(): EventCallback | null {
return getEventHandlerAttribute(this, 'abort');
}
set onabort(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'abort', listener);
}
get onloadend(): EventCallback | null {
return getEventHandlerAttribute(this, 'loadend');
}
set onloadend(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'loadend', listener);
}
}
/**
* Shared base for platform-specific XMLHttpRequest implementations.
*/
class XMLHttpRequest extends EventTarget {
static UNSENT: number = UNSENT;
static OPENED: number = OPENED;
static HEADERS_RECEIVED: number = HEADERS_RECEIVED;
static LOADING: number = LOADING;
static DONE: number = DONE;
static _interceptor: ?XHRInterceptor = null;
static _profiling: boolean = false;
UNSENT: number = UNSENT;
OPENED: number = OPENED;
HEADERS_RECEIVED: number = HEADERS_RECEIVED;
LOADING: number = LOADING;
DONE: number = DONE;
readyState: number = UNSENT;
responseHeaders: ?Object;
status: number = 0;
timeout: number = 0;
responseURL: ?string;
withCredentials: boolean = true;
upload: XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
_requestId: ?number;
_subscriptions: Array<EventSubscription>;
_aborted: boolean = false;
_cachedResponse: Response;
_hasError: boolean = false;
_headers: Object;
_lowerCaseResponseHeaders: Object;
_method: ?string = null;
_perfKey: ?string = null;
_responseType: ResponseType;
_response: string = '';
_sent: boolean;
_url: ?string = null;
_timedOut: boolean = false;
_trackingName: string = 'unknown';
_incrementalEvents: boolean = false;
_startTime: ?number = null;
_performanceLogger: IPerformanceLogger = GlobalPerformanceLogger;
static setInterceptor(interceptor: ?XHRInterceptor) {
XMLHttpRequest._interceptor = interceptor;
}
static enableProfiling(enableProfiling: boolean): void {
XMLHttpRequest._profiling = enableProfiling;
}
constructor() {
super();
this._reset();
}
_reset(): void {
this.readyState = this.UNSENT;
this.responseHeaders = undefined;
this.status = 0;
delete this.responseURL;
this._requestId = null;
this._cachedResponse = undefined;
this._hasError = false;
this._headers = {};
this._response = '';
this._responseType = '';
this._sent = false;
this._lowerCaseResponseHeaders = {};
this._clearSubscriptions();
this._timedOut = false;
}
get responseType(): ResponseType {
return this._responseType;
}
set responseType(responseType: ResponseType): void {
if (this._sent) {
throw new Error(
"Failed to set the 'responseType' property on 'XMLHttpRequest': The " +
'response type cannot be set after the request has been sent.',
);
}
if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) {
console.warn(
`The provided value '${responseType}' is not a valid 'responseType'.`,
);
return;
}
// redboxes early, e.g. for 'arraybuffer' on ios 7
invariant(
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
`The provided value '${responseType}' is unsupported in this environment.`,
);
if (responseType === 'blob') {
invariant(
BlobManager.isAvailable,
'Native module BlobModule is required for blob support',
);
}
this._responseType = responseType;
}
get responseText(): string {
if (this._responseType !== '' && this._responseType !== 'text') {
throw new Error(
"The 'responseText' property is only available if 'responseType' " +
`is set to '' or 'text', but it is '${this._responseType}'.`,
);
}
if (this.readyState < LOADING) {
return '';
}
return this._response;
}
get response(): Response {
const {responseType} = this;
if (responseType === '' || responseType === 'text') {
return this.readyState < LOADING || this._hasError ? '' : this._response;
}
if (this.readyState !== DONE) {
return null;
}
if (this._cachedResponse !== undefined) {
return this._cachedResponse;
}
switch (responseType) {
case 'document':
this._cachedResponse = null;
break;
case 'arraybuffer':
this._cachedResponse = base64.toByteArray(this._response).buffer;
break;
case 'blob':
if (typeof this._response === 'object' && this._response) {
this._cachedResponse = BlobManager.createFromOptions(this._response);
} else if (this._response === '') {
this._cachedResponse = BlobManager.createFromParts([]);
} else {
throw new Error(
'Invalid response for blob - expecting object, was ' +
`${typeof this._response}: ${this._response.trim()}`,
);
}
break;
case 'json':
try {
this._cachedResponse = JSON.parse(this._response);
} catch (_) {
this._cachedResponse = null;
}
break;
default:
this._cachedResponse = null;
}
return this._cachedResponse;
}
// exposed for testing
__didCreateRequest(requestId: number): void {
this._requestId = requestId;
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.requestSent(
requestId,
this._url || '',
this._method || 'GET',
this._headers,
);
}
// exposed for testing
__didUploadProgress(
requestId: number,
progress: number,
total: number,
): void {
if (requestId === this._requestId) {
dispatchTrustedEvent(
this.upload,
new ProgressEvent('progress', {
lengthComputable: true,
loaded: progress,
total,
}),
);
}
}
__didReceiveResponse(
requestId: number,
status: number,
responseHeaders: ?Object,
responseURL: ?string,
): void {
if (requestId === this._requestId) {
this._perfKey != null &&
this._performanceLogger.stopTimespan(this._perfKey);
this.status = status;
this.setResponseHeaders(responseHeaders);
this.setReadyState(this.HEADERS_RECEIVED);
if (responseURL || responseURL === '') {
this.responseURL = responseURL;
} else {
delete this.responseURL;
}
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.responseReceived(
requestId,
responseURL || this._url || '',
status,
responseHeaders || {},
);
}
}
__didReceiveData(requestId: number, response: string): void {
if (requestId !== this._requestId) {
return;
}
this._response = response;
this._cachedResponse = undefined; // force lazy recomputation
this.setReadyState(this.LOADING);
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.dataReceived(requestId, response);
}
__didReceiveIncrementalData(
requestId: number,
responseText: string,
progress: number,
total: number,
) {
if (requestId !== this._requestId) {
return;
}
if (!this._response) {
this._response = responseText;
} else {
this._response += responseText;
}
if (XMLHttpRequest._profiling) {
performance.mark(
'Track:XMLHttpRequest:Incremental Data: ' + this._getMeasureURL(),
);
}
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.dataReceived(requestId, responseText);
this.setReadyState(this.LOADING);
this.__didReceiveDataProgress(requestId, progress, total);
}
__didReceiveDataProgress(
requestId: number,
loaded: number,
total: number,
): void {
if (requestId !== this._requestId) {
return;
}
dispatchTrustedEvent(
this,
new ProgressEvent('progress', {
lengthComputable: total >= 0,
loaded,
total,
}),
);
}
// exposed for testing
__didCompleteResponse(
requestId: number,
error: string,
timeOutError: boolean,
): void {
if (requestId === this._requestId) {
if (error) {
if (this._responseType === '' || this._responseType === 'text') {
this._response = error;
}
this._hasError = true;
if (timeOutError) {
this._timedOut = true;
}
}
this._clearSubscriptions();
this._requestId = null;
this.setReadyState(this.DONE);
if (XMLHttpRequest._profiling && this._startTime != null) {
const start = this._startTime;
performance.measure('Track:XMLHttpRequest:' + this._getMeasureURL(), {
start,
end: performance.now(),
});
}
if (error) {
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.loadingFailed(requestId, error);
} else {
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.loadingFinished(
requestId,
this._response.length,
);
}
}
}
_clearSubscriptions(): void {
(this._subscriptions || []).forEach(sub => {
if (sub) {
sub.remove();
}
});
this._subscriptions = [];
}
getAllResponseHeaders(): ?string {
if (!this.responseHeaders) {
// according to the spec, return null if no response has been received
return null;
}
// Assign to non-nullable local variable.
const responseHeaders = this.responseHeaders;
const unsortedHeaders: Map<
string,
{lowerHeaderName: string, upperHeaderName: string, headerValue: string},
> = new Map();
for (const rawHeaderName of Object.keys(responseHeaders)) {
const headerValue = responseHeaders[rawHeaderName];
const lowerHeaderName = rawHeaderName.toLowerCase();
const header = unsortedHeaders.get(lowerHeaderName);
if (header) {
header.headerValue += ', ' + headerValue;
unsortedHeaders.set(lowerHeaderName, header);
} else {
unsortedHeaders.set(lowerHeaderName, {
lowerHeaderName,
upperHeaderName: rawHeaderName.toUpperCase(),
headerValue,
});
}
}
// Sort in ascending order, with a being less than b if a's name is legacy-uppercased-byte less than b's name.
const sortedHeaders = [...unsortedHeaders.values()].sort((a, b) => {
if (a.upperHeaderName < b.upperHeaderName) {
return -1;
}
if (a.upperHeaderName > b.upperHeaderName) {
return 1;
}
return 0;
});
// Combine into single text response.
return (
sortedHeaders
.map(header => {
return header.lowerHeaderName + ': ' + header.headerValue;
})
.join('\r\n') + '\r\n'
);
}
getResponseHeader(header: string): ?string {
const value = this._lowerCaseResponseHeaders[header.toLowerCase()];
return value !== undefined ? value : null;
}
setRequestHeader(header: string, value: any): void {
if (this.readyState !== this.OPENED) {
throw new Error('Request has not been opened');
}
this._headers[header.toLowerCase()] = String(value);
}
/**
* Custom extension for tracking origins of request.
*/
setTrackingName(trackingName: string): XMLHttpRequest {
this._trackingName = trackingName;
return this;
}
/**
* Custom extension for setting a custom performance logger
*/
setPerformanceLogger(performanceLogger: IPerformanceLogger): XMLHttpRequest {
this._performanceLogger = performanceLogger;
return this;
}
open(method: string, url: string, async: ?boolean): void {
/* Other optional arguments are not supported yet */
if (this.readyState !== this.UNSENT) {
throw new Error('Cannot open, already sending');
}
if (async !== undefined && !async) {
// async is default
throw new Error('Synchronous http requests are not supported');
}
if (!url) {
throw new Error('Cannot load an empty url');
}
this._method = method.toUpperCase();
this._url = url;
this._aborted = false;
this.setReadyState(this.OPENED);
}
send(data: any): void {
if (this.readyState !== this.OPENED) {
throw new Error('Request has not been opened');
}
if (this._sent) {
throw new Error('Request has already been sent');
}
this._sent = true;
const incrementalEvents =
this._incrementalEvents || !!this.onreadystatechange || !!this.onprogress;
this._subscriptions.push(
RCTNetworking.addListener('didSendNetworkData', args =>
this.__didUploadProgress(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkResponse', args =>
this.__didReceiveResponse(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkData', args =>
this.__didReceiveData(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkIncrementalData', args =>
this.__didReceiveIncrementalData(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkDataProgress', args =>
this.__didReceiveDataProgress(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didCompleteNetworkResponse', args =>
this.__didCompleteResponse(...args),
),
);
let nativeResponseType: NativeResponseType = 'text';
if (this._responseType === 'arraybuffer') {
nativeResponseType = 'base64';
}
if (this._responseType === 'blob') {
nativeResponseType = 'blob';
}
const doSend = () => {
const friendlyName =
this._trackingName !== 'unknown' ? this._trackingName : this._url;
this._perfKey = 'network_XMLHttpRequest_' + String(friendlyName);
this._performanceLogger.startTimespan(this._perfKey);
this._startTime = performance.now();
invariant(
this._method,
'XMLHttpRequest method needs to be defined (%s).',
friendlyName,
);
invariant(
this._url,
'XMLHttpRequest URL needs to be defined (%s).',
friendlyName,
);
RCTNetworking.sendRequest(
this._method,
this._trackingName,
this._url,
this._headers,
data,
/* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
* when making Flow check .android.js files. */
nativeResponseType,
incrementalEvents,
this.timeout,
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this.__didCreateRequest.bind(this),
this.withCredentials,
);
};
if (DEBUG_NETWORK_SEND_DELAY) {
setTimeout(doSend, DEBUG_NETWORK_SEND_DELAY);
} else {
doSend();
}
}
abort(): void {
this._aborted = true;
if (this._requestId) {
RCTNetworking.abortRequest(this._requestId);
}
// only call onreadystatechange if there is something to abort,
// below logic is per spec
if (
!(
this.readyState === this.UNSENT ||
(this.readyState === this.OPENED && !this._sent) ||
this.readyState === this.DONE
)
) {
this._reset();
this.setReadyState(this.DONE);
}
// Reset again after, in case modified in handler
this._reset();
}
setResponseHeaders(responseHeaders: ?Object): void {
this.responseHeaders = responseHeaders || null;
const headers = responseHeaders || {};
this._lowerCaseResponseHeaders = Object.keys(headers).reduce<{
[string]: any,
}>((lcaseHeaders, headerName) => {
// $FlowFixMe[invalid-computed-prop]
lcaseHeaders[headerName.toLowerCase()] = headers[headerName];
return lcaseHeaders;
}, {});
}
setReadyState(newState: number): void {
this.readyState = newState;
dispatchTrustedEvent(this, new Event('readystatechange'));
if (newState === this.DONE) {
if (this._aborted) {
dispatchTrustedEvent(this, new Event('abort'));
} else if (this._hasError) {
if (this._timedOut) {
dispatchTrustedEvent(this, new Event('timeout'));
} else {
dispatchTrustedEvent(this, new Event('error'));
}
} else {
dispatchTrustedEvent(this, new Event('load'));
}
dispatchTrustedEvent(this, new Event('loadend'));
}
}
addEventListener(type: string, listener: EventListener | null): void {
// If we dont' have a 'readystatechange' event handler, we don't
// have to send repeated LOADING events with incremental updates
// to responseText, which will avoid a bunch of native -> JS
// bridge traffic.
if (type === 'readystatechange' || type === 'progress') {
this._incrementalEvents = true;
}
super.addEventListener(type, listener);
}
_getMeasureURL(): string {
return (
this._trackingName ?? this._url ?? LABEL_FOR_MISSING_URL_FOR_PROFILING
);
}
/*
* `on<event>` event handling (without JS prototype magic).
*/
get onabort(): EventCallback | null {
return getEventHandlerAttribute(this, 'abort');
}
set onabort(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'abort', listener);
}
get onerror(): EventCallback | null {
return getEventHandlerAttribute(this, 'error');
}
set onerror(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'error', listener);
}
get onload(): EventCallback | null {
return getEventHandlerAttribute(this, 'load');
}
set onload(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'load', listener);
}
get onloadstart(): EventCallback | null {
return getEventHandlerAttribute(this, 'loadstart');
}
set onloadstart(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'loadstart', listener);
}
get onprogress(): EventCallback | null {
return getEventHandlerAttribute(this, 'progress');
}
set onprogress(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'progress', listener);
}
get ontimeout(): EventCallback | null {
return getEventHandlerAttribute(this, 'timeout');
}
set ontimeout(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'timeout', listener);
}
get onloadend(): EventCallback | null {
return getEventHandlerAttribute(this, 'loadend');
}
set onloadend(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'loadend', listener);
}
get onreadystatechange(): EventCallback | null {
return getEventHandlerAttribute(this, 'readystatechange');
}
set onreadystatechange(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'readystatechange', listener);
}
}
module.exports = XMLHttpRequest;
@@ -0,0 +1,698 @@
/**
* 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.
*
* @format
* @flow
*/
'use strict';
import type {IPerformanceLogger} from '../Utilities/createPerformanceLogger';
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
import EventTarget from 'event-target-shim';
const BlobManager = require('../Blob/BlobManager').default;
const GlobalPerformanceLogger = require('../Utilities/GlobalPerformanceLogger');
const RCTNetworking = require('./RCTNetworking').default;
const base64 = require('base64-js');
const invariant = require('invariant');
const DEBUG_NETWORK_SEND_DELAY: false = false; // Set to a number of milliseconds when debugging
const LABEL_FOR_MISSING_URL_FOR_PROFILING = 'Unknown URL';
export type NativeResponseType = 'base64' | 'blob' | 'text';
export type ResponseType =
| ''
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text';
export type Response = ?Object | string;
type XHRInterceptor = interface {
requestSent(id: number, url: string, method: string, headers: Object): void,
responseReceived(
id: number,
url: string,
status: number,
headers: Object,
): void,
dataReceived(id: number, data: string): void,
loadingFinished(id: number, encodedDataLength: number): void,
loadingFailed(id: number, error: string): void,
};
// The native blob module is optional so inject it here if available.
if (BlobManager.isAvailable) {
BlobManager.addNetworkingHandler();
}
const UNSENT = 0;
const OPENED = 1;
const HEADERS_RECEIVED = 2;
const LOADING = 3;
const DONE = 4;
const SUPPORTED_RESPONSE_TYPES = {
arraybuffer: typeof global.ArrayBuffer === 'function',
blob: typeof global.Blob === 'function',
document: false,
json: true,
text: true,
'': true,
};
const REQUEST_EVENTS = [
'abort',
'error',
'load',
'loadstart',
'progress',
'timeout',
'loadend',
];
const XHR_EVENTS = REQUEST_EVENTS.concat('readystatechange');
class XMLHttpRequestEventTarget extends (EventTarget(
...REQUEST_EVENTS,
): typeof EventTarget) {
onload: ?Function;
onloadstart: ?Function;
onprogress: ?Function;
ontimeout: ?Function;
onerror: ?Function;
onabort: ?Function;
onloadend: ?Function;
}
/**
* Shared base for platform-specific XMLHttpRequest implementations.
*/
class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) {
static UNSENT: number = UNSENT;
static OPENED: number = OPENED;
static HEADERS_RECEIVED: number = HEADERS_RECEIVED;
static LOADING: number = LOADING;
static DONE: number = DONE;
static _interceptor: ?XHRInterceptor = null;
static _profiling: boolean = false;
UNSENT: number = UNSENT;
OPENED: number = OPENED;
HEADERS_RECEIVED: number = HEADERS_RECEIVED;
LOADING: number = LOADING;
DONE: number = DONE;
// EventTarget automatically initializes these to `null`.
onload: ?Function;
onloadstart: ?Function;
onprogress: ?Function;
ontimeout: ?Function;
onerror: ?Function;
onabort: ?Function;
onloadend: ?Function;
onreadystatechange: ?Function;
readyState: number = UNSENT;
responseHeaders: ?Object;
status: number = 0;
timeout: number = 0;
responseURL: ?string;
withCredentials: boolean = true;
upload: XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
_requestId: ?number;
_subscriptions: Array<EventSubscription>;
_aborted: boolean = false;
_cachedResponse: Response;
_hasError: boolean = false;
_headers: Object;
_lowerCaseResponseHeaders: Object;
_method: ?string = null;
_perfKey: ?string = null;
_responseType: ResponseType;
_response: string = '';
_sent: boolean;
_url: ?string = null;
_timedOut: boolean = false;
_trackingName: string = 'unknown';
_incrementalEvents: boolean = false;
_startTime: ?number = null;
_performanceLogger: IPerformanceLogger = GlobalPerformanceLogger;
static setInterceptor(interceptor: ?XHRInterceptor) {
XMLHttpRequest._interceptor = interceptor;
}
static enableProfiling(enableProfiling: boolean): void {
XMLHttpRequest._profiling = enableProfiling;
}
constructor() {
super();
this._reset();
}
_reset(): void {
this.readyState = this.UNSENT;
this.responseHeaders = undefined;
this.status = 0;
delete this.responseURL;
this._requestId = null;
this._cachedResponse = undefined;
this._hasError = false;
this._headers = {};
this._response = '';
this._responseType = '';
this._sent = false;
this._lowerCaseResponseHeaders = {};
this._clearSubscriptions();
this._timedOut = false;
}
get responseType(): ResponseType {
return this._responseType;
}
set responseType(responseType: ResponseType): void {
if (this._sent) {
throw new Error(
"Failed to set the 'responseType' property on 'XMLHttpRequest': The " +
'response type cannot be set after the request has been sent.',
);
}
if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) {
console.warn(
`The provided value '${responseType}' is not a valid 'responseType'.`,
);
return;
}
// redboxes early, e.g. for 'arraybuffer' on ios 7
invariant(
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
`The provided value '${responseType}' is unsupported in this environment.`,
);
if (responseType === 'blob') {
invariant(
BlobManager.isAvailable,
'Native module BlobModule is required for blob support',
);
}
this._responseType = responseType;
}
get responseText(): string {
if (this._responseType !== '' && this._responseType !== 'text') {
throw new Error(
"The 'responseText' property is only available if 'responseType' " +
`is set to '' or 'text', but it is '${this._responseType}'.`,
);
}
if (this.readyState < LOADING) {
return '';
}
return this._response;
}
get response(): Response {
const {responseType} = this;
if (responseType === '' || responseType === 'text') {
return this.readyState < LOADING || this._hasError ? '' : this._response;
}
if (this.readyState !== DONE) {
return null;
}
if (this._cachedResponse !== undefined) {
return this._cachedResponse;
}
switch (responseType) {
case 'document':
this._cachedResponse = null;
break;
case 'arraybuffer':
this._cachedResponse = base64.toByteArray(this._response).buffer;
break;
case 'blob':
if (typeof this._response === 'object' && this._response) {
this._cachedResponse = BlobManager.createFromOptions(this._response);
} else if (this._response === '') {
this._cachedResponse = BlobManager.createFromParts([]);
} else {
throw new Error(
'Invalid response for blob - expecting object, was ' +
`${typeof this._response}: ${this._response.trim()}`,
);
}
break;
case 'json':
try {
this._cachedResponse = JSON.parse(this._response);
} catch (_) {
this._cachedResponse = null;
}
break;
default:
this._cachedResponse = null;
}
return this._cachedResponse;
}
// exposed for testing
__didCreateRequest(requestId: number): void {
this._requestId = requestId;
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.requestSent(
requestId,
this._url || '',
this._method || 'GET',
this._headers,
);
}
// exposed for testing
__didUploadProgress(
requestId: number,
progress: number,
total: number,
): void {
if (requestId === this._requestId) {
this.upload.dispatchEvent({
type: 'progress',
lengthComputable: true,
loaded: progress,
total,
});
}
}
__didReceiveResponse(
requestId: number,
status: number,
responseHeaders: ?Object,
responseURL: ?string,
): void {
if (requestId === this._requestId) {
this._perfKey != null &&
this._performanceLogger.stopTimespan(this._perfKey);
this.status = status;
this.setResponseHeaders(responseHeaders);
this.setReadyState(this.HEADERS_RECEIVED);
if (responseURL || responseURL === '') {
this.responseURL = responseURL;
} else {
delete this.responseURL;
}
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.responseReceived(
requestId,
responseURL || this._url || '',
status,
responseHeaders || {},
);
}
}
__didReceiveData(requestId: number, response: string): void {
if (requestId !== this._requestId) {
return;
}
this._response = response;
this._cachedResponse = undefined; // force lazy recomputation
this.setReadyState(this.LOADING);
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.dataReceived(requestId, response);
}
__didReceiveIncrementalData(
requestId: number,
responseText: string,
progress: number,
total: number,
) {
if (requestId !== this._requestId) {
return;
}
if (!this._response) {
this._response = responseText;
} else {
this._response += responseText;
}
if (XMLHttpRequest._profiling) {
performance.mark(
'Track:XMLHttpRequest:Incremental Data: ' + this._getMeasureURL(),
);
}
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.dataReceived(requestId, responseText);
this.setReadyState(this.LOADING);
this.__didReceiveDataProgress(requestId, progress, total);
}
__didReceiveDataProgress(
requestId: number,
loaded: number,
total: number,
): void {
if (requestId !== this._requestId) {
return;
}
this.dispatchEvent({
type: 'progress',
lengthComputable: total >= 0,
loaded,
total,
});
}
// exposed for testing
__didCompleteResponse(
requestId: number,
error: string,
timeOutError: boolean,
): void {
if (requestId === this._requestId) {
if (error) {
if (this._responseType === '' || this._responseType === 'text') {
this._response = error;
}
this._hasError = true;
if (timeOutError) {
this._timedOut = true;
}
}
this._clearSubscriptions();
this._requestId = null;
this.setReadyState(this.DONE);
if (XMLHttpRequest._profiling && this._startTime != null) {
const start = this._startTime;
performance.measure('Track:XMLHttpRequest:' + this._getMeasureURL(), {
start,
end: performance.now(),
});
}
if (error) {
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.loadingFailed(requestId, error);
} else {
XMLHttpRequest._interceptor &&
XMLHttpRequest._interceptor.loadingFinished(
requestId,
this._response.length,
);
}
}
}
_clearSubscriptions(): void {
(this._subscriptions || []).forEach(sub => {
if (sub) {
sub.remove();
}
});
this._subscriptions = [];
}
getAllResponseHeaders(): ?string {
if (!this.responseHeaders) {
// according to the spec, return null if no response has been received
return null;
}
// Assign to non-nullable local variable.
const responseHeaders = this.responseHeaders;
const unsortedHeaders: Map<
string,
{lowerHeaderName: string, upperHeaderName: string, headerValue: string},
> = new Map();
for (const rawHeaderName of Object.keys(responseHeaders)) {
const headerValue = responseHeaders[rawHeaderName];
const lowerHeaderName = rawHeaderName.toLowerCase();
const header = unsortedHeaders.get(lowerHeaderName);
if (header) {
header.headerValue += ', ' + headerValue;
unsortedHeaders.set(lowerHeaderName, header);
} else {
unsortedHeaders.set(lowerHeaderName, {
lowerHeaderName,
upperHeaderName: rawHeaderName.toUpperCase(),
headerValue,
});
}
}
// Sort in ascending order, with a being less than b if a's name is legacy-uppercased-byte less than b's name.
const sortedHeaders = [...unsortedHeaders.values()].sort((a, b) => {
if (a.upperHeaderName < b.upperHeaderName) {
return -1;
}
if (a.upperHeaderName > b.upperHeaderName) {
return 1;
}
return 0;
});
// Combine into single text response.
return (
sortedHeaders
.map(header => {
return header.lowerHeaderName + ': ' + header.headerValue;
})
.join('\r\n') + '\r\n'
);
}
getResponseHeader(header: string): ?string {
const value = this._lowerCaseResponseHeaders[header.toLowerCase()];
return value !== undefined ? value : null;
}
setRequestHeader(header: string, value: any): void {
if (this.readyState !== this.OPENED) {
throw new Error('Request has not been opened');
}
this._headers[header.toLowerCase()] = String(value);
}
/**
* Custom extension for tracking origins of request.
*/
setTrackingName(trackingName: string): XMLHttpRequest {
this._trackingName = trackingName;
return this;
}
/**
* Custom extension for setting a custom performance logger
*/
setPerformanceLogger(performanceLogger: IPerformanceLogger): XMLHttpRequest {
this._performanceLogger = performanceLogger;
return this;
}
open(method: string, url: string, async: ?boolean): void {
/* Other optional arguments are not supported yet */
if (this.readyState !== this.UNSENT) {
throw new Error('Cannot open, already sending');
}
if (async !== undefined && !async) {
// async is default
throw new Error('Synchronous http requests are not supported');
}
if (!url) {
throw new Error('Cannot load an empty url');
}
this._method = method.toUpperCase();
this._url = url;
this._aborted = false;
this.setReadyState(this.OPENED);
}
send(data: any): void {
if (this.readyState !== this.OPENED) {
throw new Error('Request has not been opened');
}
if (this._sent) {
throw new Error('Request has already been sent');
}
this._sent = true;
const incrementalEvents =
this._incrementalEvents || !!this.onreadystatechange || !!this.onprogress;
this._subscriptions.push(
RCTNetworking.addListener('didSendNetworkData', args =>
this.__didUploadProgress(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkResponse', args =>
this.__didReceiveResponse(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkData', args =>
this.__didReceiveData(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkIncrementalData', args =>
this.__didReceiveIncrementalData(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didReceiveNetworkDataProgress', args =>
this.__didReceiveDataProgress(...args),
),
);
this._subscriptions.push(
RCTNetworking.addListener('didCompleteNetworkResponse', args =>
this.__didCompleteResponse(...args),
),
);
let nativeResponseType: NativeResponseType = 'text';
if (this._responseType === 'arraybuffer') {
nativeResponseType = 'base64';
}
if (this._responseType === 'blob') {
nativeResponseType = 'blob';
}
const doSend = () => {
const friendlyName =
this._trackingName !== 'unknown' ? this._trackingName : this._url;
this._perfKey = 'network_XMLHttpRequest_' + String(friendlyName);
this._performanceLogger.startTimespan(this._perfKey);
this._startTime = performance.now();
invariant(
this._method,
'XMLHttpRequest method needs to be defined (%s).',
friendlyName,
);
invariant(
this._url,
'XMLHttpRequest URL needs to be defined (%s).',
friendlyName,
);
RCTNetworking.sendRequest(
this._method,
this._trackingName,
this._url,
this._headers,
data,
/* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
* when making Flow check .android.js files. */
nativeResponseType,
incrementalEvents,
this.timeout,
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this.__didCreateRequest.bind(this),
this.withCredentials,
);
};
if (DEBUG_NETWORK_SEND_DELAY) {
setTimeout(doSend, DEBUG_NETWORK_SEND_DELAY);
} else {
doSend();
}
}
abort(): void {
this._aborted = true;
if (this._requestId) {
RCTNetworking.abortRequest(this._requestId);
}
// only call onreadystatechange if there is something to abort,
// below logic is per spec
if (
!(
this.readyState === this.UNSENT ||
(this.readyState === this.OPENED && !this._sent) ||
this.readyState === this.DONE
)
) {
this._reset();
this.setReadyState(this.DONE);
}
// Reset again after, in case modified in handler
this._reset();
}
setResponseHeaders(responseHeaders: ?Object): void {
this.responseHeaders = responseHeaders || null;
const headers = responseHeaders || {};
this._lowerCaseResponseHeaders = Object.keys(headers).reduce<{
[string]: any,
}>((lcaseHeaders, headerName) => {
// $FlowFixMe[invalid-computed-prop]
lcaseHeaders[headerName.toLowerCase()] = headers[headerName];
return lcaseHeaders;
}, {});
}
setReadyState(newState: number): void {
this.readyState = newState;
this.dispatchEvent({type: 'readystatechange'});
if (newState === this.DONE) {
if (this._aborted) {
this.dispatchEvent({type: 'abort'});
} else if (this._hasError) {
if (this._timedOut) {
this.dispatchEvent({type: 'timeout'});
} else {
this.dispatchEvent({type: 'error'});
}
} else {
this.dispatchEvent({type: 'load'});
}
this.dispatchEvent({type: 'loadend'});
}
}
/* global EventListener */
addEventListener(type: string, listener: EventListener): void {
// If we dont' have a 'readystatechange' event handler, we don't
// have to send repeated LOADING events with incremental updates
// to responseText, which will avoid a bunch of native -> JS
// bridge traffic.
if (type === 'readystatechange' || type === 'progress') {
this._incrementalEvents = true;
}
super.addEventListener(type, listener);
}
_getMeasureURL(): string {
return (
this._trackingName ?? this._url ?? LABEL_FOR_MISSING_URL_FOR_PROFILING
);
}
}
module.exports = XMLHttpRequest;
@@ -49,267 +49,275 @@ jest
},
});
describe('XMLHttpRequest', function () {
let xhr;
let handleTimeout;
let handleError;
let handleLoad;
let handleReadyStateChange;
let handleLoadEnd;
[false, true].forEach(enableModern => {
describe(`XMLHttpRequest (${enableModern ? 'modern' : 'legacy'})`, function () {
beforeAll(() => {
jest.resetModules();
global.RN$useBuiltInEventTarget = () => enableModern;
});
beforeEach(() => {
xhr = new XMLHttpRequest();
let xhr;
let handleTimeout;
let handleError;
let handleLoad;
let handleReadyStateChange;
let handleLoadEnd;
xhr.ontimeout = jest.fn();
xhr.onerror = jest.fn();
xhr.onload = jest.fn();
xhr.onloadend = jest.fn();
xhr.onreadystatechange = jest.fn();
beforeEach(() => {
xhr = new XMLHttpRequest();
handleTimeout = jest.fn();
handleError = jest.fn();
handleLoad = jest.fn();
handleLoadEnd = jest.fn();
handleReadyStateChange = jest.fn();
xhr.ontimeout = jest.fn();
xhr.onerror = jest.fn();
xhr.onload = jest.fn();
xhr.onloadend = jest.fn();
xhr.onreadystatechange = jest.fn();
xhr.addEventListener('timeout', handleTimeout);
xhr.addEventListener('error', handleError);
xhr.addEventListener('load', handleLoad);
xhr.addEventListener('loadend', handleLoadEnd);
xhr.addEventListener('readystatechange', handleReadyStateChange);
handleTimeout = jest.fn();
handleError = jest.fn();
handleLoad = jest.fn();
handleLoadEnd = jest.fn();
handleReadyStateChange = jest.fn();
jest.clearAllMocks();
});
xhr.addEventListener('timeout', handleTimeout);
xhr.addEventListener('error', handleError);
xhr.addEventListener('load', handleLoad);
xhr.addEventListener('loadend', handleLoadEnd);
xhr.addEventListener('readystatechange', handleReadyStateChange);
afterEach(() => {
xhr = null;
handleTimeout = null;
handleError = null;
handleLoad = null;
handleLoadEnd = null;
handleReadyStateChange = null;
});
jest.clearAllMocks();
});
it('should transition readyState correctly', function () {
expect(xhr.readyState).toBe(xhr.UNSENT);
afterEach(() => {
xhr = null;
handleTimeout = null;
handleError = null;
handleLoad = null;
handleLoadEnd = null;
handleReadyStateChange = null;
});
xhr.open('GET', 'blabla');
it('should transition readyState correctly', function () {
expect(xhr.readyState).toBe(xhr.UNSENT);
expect(xhr.onreadystatechange.mock.calls.length).toBe(1);
expect(handleReadyStateChange.mock.calls.length).toBe(1);
expect(xhr.readyState).toBe(xhr.OPENED);
});
xhr.open('GET', 'blabla');
it('should expose responseType correctly', function () {
expect(xhr.responseType).toBe('');
expect(xhr.onreadystatechange.mock.calls.length).toBe(1);
expect(handleReadyStateChange.mock.calls.length).toBe(1);
expect(xhr.readyState).toBe(xhr.OPENED);
});
jest.spyOn(console, 'warn').mockReturnValue(undefined);
it('should expose responseType correctly', function () {
expect(xhr.responseType).toBe('');
// Setting responseType to an unsupported value has no effect.
xhr.responseType = 'arrayblobbuffertextfile';
expect(xhr.responseType).toBe('');
jest.spyOn(console, 'warn').mockReturnValue(undefined);
expect(console.warn).toBeCalledWith(
"The provided value 'arrayblobbuffertextfile' is not a valid 'responseType'.",
);
console.warn.mockRestore();
// Setting responseType to an unsupported value has no effect.
xhr.responseType = 'arrayblobbuffertextfile';
expect(xhr.responseType).toBe('');
xhr.responseType = 'arraybuffer';
expect(xhr.responseType).toBe('arraybuffer');
expect(console.warn).toBeCalledWith(
"The provided value 'arrayblobbuffertextfile' is not a valid 'responseType'.",
);
console.warn.mockRestore();
xhr.responseType = 'arraybuffer';
expect(xhr.responseType).toBe('arraybuffer');
// Can't change responseType after first data has been received.
xhr.open('GET', 'blabla');
xhr.send();
expect(() => {
xhr.responseType = 'text';
}).toThrow();
});
it('should expose responseText correctly', function () {
xhr.responseType = '';
expect(xhr.responseText).toBe('');
expect(xhr.response).toBe('');
xhr.responseType = 'arraybuffer';
expect(() => xhr.responseText).toThrow();
expect(xhr.response).toBe(null);
// Can't change responseType after first data has been received.
xhr.open('GET', 'blabla');
xhr.send();
expect(() => {
xhr.responseType = 'text';
}).toThrow();
});
expect(xhr.responseText).toBe('');
expect(xhr.response).toBe('');
it('should expose responseText correctly', function () {
xhr.responseType = '';
expect(xhr.responseText).toBe('');
expect(xhr.response).toBe('');
// responseText is read-only.
expect(() => {
xhr.responseText = 'hi';
}).toThrow();
expect(xhr.responseText).toBe('');
expect(xhr.response).toBe('');
xhr.responseType = 'arraybuffer';
expect(() => xhr.responseText).toThrow();
expect(xhr.response).toBe(null);
xhr.responseType = 'text';
expect(xhr.responseText).toBe('');
expect(xhr.response).toBe('');
// responseText is read-only.
expect(() => {
xhr.responseText = 'hi';
}).toThrow();
expect(xhr.responseText).toBe('');
expect(xhr.response).toBe('');
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(2);
xhr.__didReceiveData(requestId, 'Some data');
expect(xhr.responseText).toBe('Some data');
});
it('should call ontimeout function when the request times out', function () {
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(3);
xhr.__didCompleteResponse(requestId, 'Timeout', true);
xhr.__didCompleteResponse(requestId, 'Timeout', true);
expect(xhr.readyState).toBe(xhr.DONE);
expect(xhr.ontimeout.mock.calls.length).toBe(1);
expect(xhr.onloadend.mock.calls.length).toBe(1);
expect(xhr.onerror).not.toBeCalled();
expect(xhr.onload).not.toBeCalled();
expect(handleTimeout.mock.calls.length).toBe(1);
expect(handleLoadEnd.mock.calls.length).toBe(1);
expect(handleError).not.toBeCalled();
expect(handleLoad).not.toBeCalled();
});
it('should call onerror function when the request times out', function () {
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(4);
xhr.__didCompleteResponse(requestId, 'Generic error');
expect(xhr.readyState).toBe(xhr.DONE);
expect(xhr.onreadystatechange.mock.calls.length).toBe(2);
expect(xhr.onerror.mock.calls.length).toBe(1);
expect(xhr.onloadend.mock.calls.length).toBe(1);
expect(xhr.ontimeout).not.toBeCalled();
expect(xhr.onload).not.toBeCalled();
expect(handleReadyStateChange.mock.calls.length).toBe(2);
expect(handleError.mock.calls.length).toBe(1);
expect(handleLoadEnd.mock.calls.length).toBe(1);
expect(handleTimeout).not.toBeCalled();
expect(handleLoad).not.toBeCalled();
});
it('should call onload function when there is no error', function () {
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(5);
xhr.__didCompleteResponse(requestId, null);
expect(xhr.readyState).toBe(xhr.DONE);
expect(xhr.onreadystatechange.mock.calls.length).toBe(2);
expect(xhr.onload.mock.calls.length).toBe(1);
expect(xhr.onloadend.mock.calls.length).toBe(1);
expect(xhr.onerror).not.toBeCalled();
expect(xhr.ontimeout).not.toBeCalled();
expect(handleReadyStateChange.mock.calls.length).toBe(2);
expect(handleLoad.mock.calls.length).toBe(1);
expect(handleLoadEnd.mock.calls.length).toBe(1);
expect(handleError).not.toBeCalled();
expect(handleTimeout).not.toBeCalled();
});
it('should call upload onprogress', function () {
xhr.open('GET', 'blabla');
xhr.send();
xhr.upload.onprogress = jest.fn();
const handleProgress = jest.fn();
xhr.upload.addEventListener('progress', handleProgress);
setRequestId(6);
xhr.__didUploadProgress(requestId, 42, 100);
expect(xhr.upload.onprogress.mock.calls.length).toBe(1);
expect(handleProgress.mock.calls.length).toBe(1);
expect(xhr.upload.onprogress.mock.calls[0][0].loaded).toBe(42);
expect(xhr.upload.onprogress.mock.calls[0][0].total).toBe(100);
expect(handleProgress.mock.calls[0][0].loaded).toBe(42);
expect(handleProgress.mock.calls[0][0].total).toBe(100);
});
it('should combine response headers with CRLF', function () {
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(7);
xhr.__didReceiveResponse(requestId, 200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '32',
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(2);
xhr.__didReceiveData(requestId, 'Some data');
expect(xhr.responseText).toBe('Some data');
});
expect(xhr.getAllResponseHeaders()).toBe(
'content-length: 32\r\n' + 'content-type: text/plain; charset=utf-8\r\n',
);
});
it('should call ontimeout function when the request times out', function () {
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(3);
xhr.__didCompleteResponse(requestId, 'Timeout', true);
xhr.__didCompleteResponse(requestId, 'Timeout', true);
it('should log to GlobalPerformanceLogger if a custom performance logger is not set', () => {
xhr.open('GET', 'blabla');
xhr.send();
expect(xhr.readyState).toBe(xhr.DONE);
expect(GlobalPerformanceLogger.startTimespan).toHaveBeenCalledWith(
'network_XMLHttpRequest_blabla',
);
expect(GlobalPerformanceLogger.stopTimespan).not.toHaveBeenCalled();
expect(xhr.ontimeout.mock.calls.length).toBe(1);
expect(xhr.onloadend.mock.calls.length).toBe(1);
expect(xhr.onerror).not.toBeCalled();
expect(xhr.onload).not.toBeCalled();
setRequestId(8);
xhr.__didReceiveResponse(requestId, 200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '32',
expect(handleTimeout.mock.calls.length).toBe(1);
expect(handleLoadEnd.mock.calls.length).toBe(1);
expect(handleError).not.toBeCalled();
expect(handleLoad).not.toBeCalled();
});
expect(GlobalPerformanceLogger.stopTimespan).toHaveBeenCalledWith(
'network_XMLHttpRequest_blabla',
);
});
it('should call onerror function when the request times out', function () {
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(4);
xhr.__didCompleteResponse(requestId, 'Generic error');
it('should log to a custom performance logger if set', () => {
const performanceLogger = createPerformanceLogger();
jest.spyOn(performanceLogger, 'startTimespan');
jest.spyOn(performanceLogger, 'stopTimespan');
expect(xhr.readyState).toBe(xhr.DONE);
xhr.setPerformanceLogger(performanceLogger);
expect(xhr.onreadystatechange.mock.calls.length).toBe(2);
expect(xhr.onerror.mock.calls.length).toBe(1);
expect(xhr.onloadend.mock.calls.length).toBe(1);
expect(xhr.ontimeout).not.toBeCalled();
expect(xhr.onload).not.toBeCalled();
xhr.open('GET', 'blabla');
xhr.send();
expect(performanceLogger.startTimespan).toHaveBeenCalledWith(
'network_XMLHttpRequest_blabla',
);
expect(GlobalPerformanceLogger.startTimespan).not.toHaveBeenCalled();
expect(performanceLogger.stopTimespan).not.toHaveBeenCalled();
setRequestId(9);
xhr.__didReceiveResponse(requestId, 200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '32',
expect(handleReadyStateChange.mock.calls.length).toBe(2);
expect(handleError.mock.calls.length).toBe(1);
expect(handleLoadEnd.mock.calls.length).toBe(1);
expect(handleTimeout).not.toBeCalled();
expect(handleLoad).not.toBeCalled();
});
expect(performanceLogger.stopTimespan).toHaveBeenCalledWith(
'network_XMLHttpRequest_blabla',
);
expect(GlobalPerformanceLogger.stopTimespan).not.toHaveBeenCalled();
});
it('should call onload function when there is no error', function () {
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(5);
xhr.__didCompleteResponse(requestId, null);
it('should sort and lowercase response headers', function () {
// Derived from XHR Web Platform Test: https://github.com/web-platform-tests/wpt/blob/master/xhr/getallresponseheaders.htm
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(10);
xhr.__didReceiveResponse(requestId, 200, {
'foo-TEST': '1',
'FOO-test': '2',
__Custom: 'token',
'ALSO-here': 'Mr. PB',
ewok: 'lego',
expect(xhr.readyState).toBe(xhr.DONE);
expect(xhr.onreadystatechange.mock.calls.length).toBe(2);
expect(xhr.onload.mock.calls.length).toBe(1);
expect(xhr.onloadend.mock.calls.length).toBe(1);
expect(xhr.onerror).not.toBeCalled();
expect(xhr.ontimeout).not.toBeCalled();
expect(handleReadyStateChange.mock.calls.length).toBe(2);
expect(handleLoad.mock.calls.length).toBe(1);
expect(handleLoadEnd.mock.calls.length).toBe(1);
expect(handleError).not.toBeCalled();
expect(handleTimeout).not.toBeCalled();
});
expect(xhr.getAllResponseHeaders()).toBe(
'also-here: Mr. PB\r\newok: lego\r\nfoo-test: 1, 2\r\n__custom: token\r\n',
);
it('should call upload onprogress', function () {
xhr.open('GET', 'blabla');
xhr.send();
xhr.upload.onprogress = jest.fn();
const handleProgress = jest.fn();
xhr.upload.addEventListener('progress', handleProgress);
setRequestId(6);
xhr.__didUploadProgress(requestId, 42, 100);
expect(xhr.upload.onprogress.mock.calls.length).toBe(1);
expect(handleProgress.mock.calls.length).toBe(1);
expect(xhr.upload.onprogress.mock.calls[0][0].loaded).toBe(42);
expect(xhr.upload.onprogress.mock.calls[0][0].total).toBe(100);
expect(handleProgress.mock.calls[0][0].loaded).toBe(42);
expect(handleProgress.mock.calls[0][0].total).toBe(100);
});
it('should combine response headers with CRLF', function () {
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(7);
xhr.__didReceiveResponse(requestId, 200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '32',
});
expect(xhr.getAllResponseHeaders()).toBe(
'content-length: 32\r\n' +
'content-type: text/plain; charset=utf-8\r\n',
);
});
it('should log to GlobalPerformanceLogger if a custom performance logger is not set', () => {
xhr.open('GET', 'blabla');
xhr.send();
expect(GlobalPerformanceLogger.startTimespan).toHaveBeenCalledWith(
'network_XMLHttpRequest_blabla',
);
expect(GlobalPerformanceLogger.stopTimespan).not.toHaveBeenCalled();
setRequestId(8);
xhr.__didReceiveResponse(requestId, 200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '32',
});
expect(GlobalPerformanceLogger.stopTimespan).toHaveBeenCalledWith(
'network_XMLHttpRequest_blabla',
);
});
it('should log to a custom performance logger if set', () => {
const performanceLogger = createPerformanceLogger();
jest.spyOn(performanceLogger, 'startTimespan');
jest.spyOn(performanceLogger, 'stopTimespan');
xhr.setPerformanceLogger(performanceLogger);
xhr.open('GET', 'blabla');
xhr.send();
expect(performanceLogger.startTimespan).toHaveBeenCalledWith(
'network_XMLHttpRequest_blabla',
);
expect(GlobalPerformanceLogger.startTimespan).not.toHaveBeenCalled();
expect(performanceLogger.stopTimespan).not.toHaveBeenCalled();
setRequestId(9);
xhr.__didReceiveResponse(requestId, 200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '32',
});
expect(performanceLogger.stopTimespan).toHaveBeenCalledWith(
'network_XMLHttpRequest_blabla',
);
expect(GlobalPerformanceLogger.stopTimespan).not.toHaveBeenCalled();
});
it('should sort and lowercase response headers', function () {
// Derived from XHR Web Platform Test: https://github.com/web-platform-tests/wpt/blob/master/xhr/getallresponseheaders.htm
xhr.open('GET', 'blabla');
xhr.send();
setRequestId(10);
xhr.__didReceiveResponse(requestId, 200, {
'foo-TEST': '1',
'FOO-test': '2',
__Custom: 'token',
'ALSO-here': 'Mr. PB',
ewok: 'lego',
});
expect(xhr.getAllResponseHeaders()).toBe(
'also-here: Mr. PB\r\newok: lego\r\nfoo-test: 1, 2\r\n__custom: token\r\n',
);
});
});
});
+8 -285
View File
@@ -8,290 +8,13 @@
* @flow
*/
import type {BlobData} from '../Blob/BlobTypes';
import type {EventSubscription} from '../vendor/emitter/EventEmitter';
import typeof WebSocket from './WebSocket_old';
import Blob from '../Blob/Blob';
import BlobManager from '../Blob/BlobManager';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import binaryToBase64 from '../Utilities/binaryToBase64';
import Platform from '../Utilities/Platform';
import NativeWebSocketModule from './NativeWebSocketModule';
import WebSocketEvent from './WebSocketEvent';
import base64 from 'base64-js';
import EventTarget from 'event-target-shim';
import invariant from 'invariant';
// Use a global instead of a flag from ReactNativeFeatureFlags because this will
// be read before apps have a chance to set overrides.
const useBuiltInEventTarget = global.RN$useBuiltInEventTarget?.();
type ArrayBufferView =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| DataView;
type BinaryType = 'blob' | 'arraybuffer';
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;
const CLOSE_NORMAL = 1000;
// Abnormal closure where no code is provided in a control frame
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5
const CLOSE_ABNORMAL = 1006;
const WEBSOCKET_EVENTS = ['close', 'error', 'message', 'open'];
let nextWebSocketId = 0;
type WebSocketEventDefinitions = {
websocketOpen: [{id: number, protocol: string}],
websocketClosed: [{id: number, code: number, reason: string}],
websocketMessage: [
| {type: 'binary', id: number, data: string}
| {type: 'text', id: number, data: string}
| {type: 'blob', id: number, data: BlobData},
],
websocketFailed: [{id: number, message: string}],
};
/**
* Browser-compatible WebSockets implementation.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
* See https://github.com/websockets/ws
*/
class WebSocket extends (EventTarget(...WEBSOCKET_EVENTS): typeof EventTarget) {
static CONNECTING: number = CONNECTING;
static OPEN: number = OPEN;
static CLOSING: number = CLOSING;
static CLOSED: number = CLOSED;
CONNECTING: number = CONNECTING;
OPEN: number = OPEN;
CLOSING: number = CLOSING;
CLOSED: number = CLOSED;
_socketId: number;
_eventEmitter: NativeEventEmitter<WebSocketEventDefinitions>;
_subscriptions: Array<EventSubscription>;
_binaryType: ?BinaryType;
onclose: ?Function;
onerror: ?Function;
onmessage: ?Function;
onopen: ?Function;
bufferedAmount: number;
extension: ?string;
protocol: ?string;
readyState: number = CONNECTING;
url: ?string;
constructor(
url: string,
protocols: ?string | ?Array<string>,
options: ?{headers?: {origin?: string, ...}, ...},
) {
super();
this.url = url;
if (typeof protocols === 'string') {
protocols = [protocols];
}
const {headers = {}, ...unrecognized} = options || {};
// Preserve deprecated backwards compatibility for the 'origin' option
// $FlowFixMe[prop-missing]
if (unrecognized && typeof unrecognized.origin === 'string') {
console.warn(
'Specifying `origin` as a WebSocket connection option is deprecated. Include it under `headers` instead.',
);
/* $FlowFixMe[prop-missing] (>=0.54.0 site=react_native_fb,react_native_
* oss) This comment suppresses an error found when Flow v0.54 was
* deployed. To see the error delete this comment and run Flow. */
headers.origin = unrecognized.origin;
/* $FlowFixMe[prop-missing] (>=0.54.0 site=react_native_fb,react_native_
* oss) This comment suppresses an error found when Flow v0.54 was
* deployed. To see the error delete this comment and run Flow. */
delete unrecognized.origin;
}
// Warn about and discard anything else
if (Object.keys(unrecognized).length > 0) {
console.warn(
'Unrecognized WebSocket connection option(s) `' +
Object.keys(unrecognized).join('`, `') +
'`. ' +
'Did you mean to put these under `headers`?',
);
}
if (!Array.isArray(protocols)) {
protocols = null;
}
this._eventEmitter = new NativeEventEmitter(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeWebSocketModule,
);
this._socketId = nextWebSocketId++;
this._registerEvents();
NativeWebSocketModule.connect(url, protocols, {headers}, this._socketId);
}
get binaryType(): ?BinaryType {
return this._binaryType;
}
set binaryType(binaryType: BinaryType): void {
if (binaryType !== 'blob' && binaryType !== 'arraybuffer') {
throw new Error("binaryType must be either 'blob' or 'arraybuffer'");
}
if (this._binaryType === 'blob' || binaryType === 'blob') {
invariant(
BlobManager.isAvailable,
'Native module BlobModule is required for blob support',
);
if (binaryType === 'blob') {
BlobManager.addWebSocketHandler(this._socketId);
} else {
BlobManager.removeWebSocketHandler(this._socketId);
}
}
this._binaryType = binaryType;
}
close(code?: number, reason?: string): void {
if (this.readyState === this.CLOSING || this.readyState === this.CLOSED) {
return;
}
this.readyState = this.CLOSING;
this._close(code, reason);
}
send(data: string | ArrayBuffer | ArrayBufferView | Blob): void {
if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR');
}
if (data instanceof Blob) {
invariant(
BlobManager.isAvailable,
'Native module BlobModule is required for blob support',
);
BlobManager.sendOverSocket(data, this._socketId);
return;
}
if (typeof data === 'string') {
NativeWebSocketModule.send(data, this._socketId);
return;
}
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
NativeWebSocketModule.sendBinary(binaryToBase64(data), this._socketId);
return;
}
throw new Error('Unsupported data type');
}
ping(): void {
if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR');
}
NativeWebSocketModule.ping(this._socketId);
}
_close(code?: number, reason?: string): void {
// See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
const statusCode = typeof code === 'number' ? code : CLOSE_NORMAL;
const closeReason = typeof reason === 'string' ? reason : '';
NativeWebSocketModule.close(statusCode, closeReason, this._socketId);
if (BlobManager.isAvailable && this._binaryType === 'blob') {
BlobManager.removeWebSocketHandler(this._socketId);
}
}
_unregisterEvents(): void {
this._subscriptions.forEach(e => e.remove());
this._subscriptions = [];
}
_registerEvents(): void {
this._subscriptions = [
this._eventEmitter.addListener('websocketMessage', ev => {
if (ev.id !== this._socketId) {
return;
}
let data: Blob | BlobData | ArrayBuffer | string = ev.data;
switch (ev.type) {
case 'binary':
data = base64.toByteArray(ev.data).buffer;
break;
case 'blob':
data = BlobManager.createFromOptions(ev.data);
break;
}
this.dispatchEvent(new WebSocketEvent('message', {data}));
}),
this._eventEmitter.addListener('websocketOpen', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.OPEN;
this.protocol = ev.protocol;
this.dispatchEvent(new WebSocketEvent('open'));
}),
this._eventEmitter.addListener('websocketClosed', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.CLOSED;
this.dispatchEvent(
new WebSocketEvent('close', {
code: ev.code,
reason: ev.reason,
// TODO: missing `wasClean` (exposed on iOS as `clean` but missing on Android)
}),
);
this._unregisterEvents();
this.close();
}),
this._eventEmitter.addListener('websocketFailed', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.CLOSED;
this.dispatchEvent(
new WebSocketEvent('error', {
message: ev.message,
}),
);
this.dispatchEvent(
new WebSocketEvent('close', {
code: CLOSE_ABNORMAL,
reason: ev.message,
// TODO: Expose `wasClean`
}),
);
this._unregisterEvents();
this.close();
}),
];
}
}
export default WebSocket;
export default (useBuiltInEventTarget
? // $FlowExpectedError[incompatible-cast]
require('./WebSocket_new').default
: require('./WebSocket_old').default) as WebSocket;
@@ -0,0 +1,325 @@
/**
* 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.
*
* @format
* @flow
*/
import type {EventCallback} from '../../src/private/webapis/dom/events/EventTarget';
import type {BlobData} from '../Blob/BlobTypes';
import type {EventSubscription} from '../vendor/emitter/EventEmitter';
import Event from '../../src/private/webapis/dom/events/Event';
import {
getEventHandlerAttribute,
setEventHandlerAttribute,
} from '../../src/private/webapis/dom/events/EventHandlerAttributes';
import EventTarget from '../../src/private/webapis/dom/events/EventTarget';
import MessageEvent from '../../src/private/webapis/html/events/MessageEvent';
import CloseEvent from '../../src/private/webapis/websockets/events/CloseEvent';
import Blob from '../Blob/Blob';
import BlobManager from '../Blob/BlobManager';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import binaryToBase64 from '../Utilities/binaryToBase64';
import Platform from '../Utilities/Platform';
import NativeWebSocketModule from './NativeWebSocketModule';
import base64 from 'base64-js';
import invariant from 'invariant';
type ArrayBufferView =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| DataView;
type BinaryType = 'blob' | 'arraybuffer';
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;
const CLOSE_NORMAL = 1000;
// Abnormal closure where no code is provided in a control frame
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5
const CLOSE_ABNORMAL = 1006;
let nextWebSocketId = 0;
type WebSocketEventDefinitions = {
websocketOpen: [{id: number, protocol: string}],
websocketClosed: [{id: number, code: number, reason: string}],
websocketMessage: [
| {type: 'binary', id: number, data: string}
| {type: 'text', id: number, data: string}
| {type: 'blob', id: number, data: BlobData},
],
websocketFailed: [{id: number, message: string}],
};
/**
* Browser-compatible WebSockets implementation.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
* See https://github.com/websockets/ws
*/
class WebSocket extends EventTarget {
static CONNECTING: number = CONNECTING;
static OPEN: number = OPEN;
static CLOSING: number = CLOSING;
static CLOSED: number = CLOSED;
CONNECTING: number = CONNECTING;
OPEN: number = OPEN;
CLOSING: number = CLOSING;
CLOSED: number = CLOSED;
_socketId: number;
_eventEmitter: NativeEventEmitter<WebSocketEventDefinitions>;
_subscriptions: Array<EventSubscription>;
_binaryType: ?BinaryType;
bufferedAmount: number;
extension: ?string;
protocol: ?string;
readyState: number = CONNECTING;
url: ?string;
constructor(
url: string,
protocols: ?string | ?Array<string>,
options: ?{headers?: {origin?: string, ...}, ...},
) {
super();
this.url = url;
if (typeof protocols === 'string') {
protocols = [protocols];
}
const {headers = {}, ...unrecognized} = options || {};
// Preserve deprecated backwards compatibility for the 'origin' option
// $FlowFixMe[prop-missing]
if (unrecognized && typeof unrecognized.origin === 'string') {
console.warn(
'Specifying `origin` as a WebSocket connection option is deprecated. Include it under `headers` instead.',
);
/* $FlowFixMe[prop-missing] (>=0.54.0 site=react_native_fb,react_native_
* oss) This comment suppresses an error found when Flow v0.54 was
* deployed. To see the error delete this comment and run Flow. */
headers.origin = unrecognized.origin;
/* $FlowFixMe[prop-missing] (>=0.54.0 site=react_native_fb,react_native_
* oss) This comment suppresses an error found when Flow v0.54 was
* deployed. To see the error delete this comment and run Flow. */
delete unrecognized.origin;
}
// Warn about and discard anything else
if (Object.keys(unrecognized).length > 0) {
console.warn(
'Unrecognized WebSocket connection option(s) `' +
Object.keys(unrecognized).join('`, `') +
'`. ' +
'Did you mean to put these under `headers`?',
);
}
if (!Array.isArray(protocols)) {
protocols = null;
}
this._eventEmitter = new NativeEventEmitter(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeWebSocketModule,
);
this._socketId = nextWebSocketId++;
this._registerEvents();
NativeWebSocketModule.connect(url, protocols, {headers}, this._socketId);
}
get binaryType(): ?BinaryType {
return this._binaryType;
}
set binaryType(binaryType: BinaryType): void {
if (binaryType !== 'blob' && binaryType !== 'arraybuffer') {
throw new Error("binaryType must be either 'blob' or 'arraybuffer'");
}
if (this._binaryType === 'blob' || binaryType === 'blob') {
invariant(
BlobManager.isAvailable,
'Native module BlobModule is required for blob support',
);
if (binaryType === 'blob') {
BlobManager.addWebSocketHandler(this._socketId);
} else {
BlobManager.removeWebSocketHandler(this._socketId);
}
}
this._binaryType = binaryType;
}
close(code?: number, reason?: string): void {
if (this.readyState === this.CLOSING || this.readyState === this.CLOSED) {
return;
}
this.readyState = this.CLOSING;
this._close(code, reason);
}
send(data: string | ArrayBuffer | ArrayBufferView | Blob): void {
if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR');
}
if (data instanceof Blob) {
invariant(
BlobManager.isAvailable,
'Native module BlobModule is required for blob support',
);
BlobManager.sendOverSocket(data, this._socketId);
return;
}
if (typeof data === 'string') {
NativeWebSocketModule.send(data, this._socketId);
return;
}
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
NativeWebSocketModule.sendBinary(binaryToBase64(data), this._socketId);
return;
}
throw new Error('Unsupported data type');
}
ping(): void {
if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR');
}
NativeWebSocketModule.ping(this._socketId);
}
_close(code?: number, reason?: string): void {
// See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
const statusCode = typeof code === 'number' ? code : CLOSE_NORMAL;
const closeReason = typeof reason === 'string' ? reason : '';
NativeWebSocketModule.close(statusCode, closeReason, this._socketId);
if (BlobManager.isAvailable && this._binaryType === 'blob') {
BlobManager.removeWebSocketHandler(this._socketId);
}
}
_unregisterEvents(): void {
this._subscriptions.forEach(e => e.remove());
this._subscriptions = [];
}
_registerEvents(): void {
this._subscriptions = [
this._eventEmitter.addListener('websocketMessage', ev => {
if (ev.id !== this._socketId) {
return;
}
let data: Blob | BlobData | ArrayBuffer | string = ev.data;
switch (ev.type) {
case 'binary':
data = base64.toByteArray(ev.data).buffer;
break;
case 'blob':
data = BlobManager.createFromOptions(ev.data);
break;
}
this.dispatchEvent(new MessageEvent('message', {data}));
}),
this._eventEmitter.addListener('websocketOpen', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.OPEN;
this.protocol = ev.protocol;
this.dispatchEvent(new Event('open'));
}),
this._eventEmitter.addListener('websocketClosed', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.CLOSED;
this.dispatchEvent(
new CloseEvent('close', {
code: ev.code,
reason: ev.reason,
// TODO: missing `wasClean` (exposed on iOS as `clean` but missing on Android)
}),
);
this._unregisterEvents();
this.close();
}),
this._eventEmitter.addListener('websocketFailed', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.CLOSED;
this.dispatchEvent(new Event('error'));
this.dispatchEvent(
new CloseEvent('close', {
code: CLOSE_ABNORMAL,
reason: ev.message,
// TODO: Expose `wasClean`
}),
);
this._unregisterEvents();
this.close();
}),
];
}
get onclose(): EventCallback | null {
return getEventHandlerAttribute(this, 'close');
}
set onclose(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'close', listener);
}
get onerror(): EventCallback | null {
return getEventHandlerAttribute(this, 'error');
}
set onerror(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'error', listener);
}
get onmessage(): EventCallback | null {
return getEventHandlerAttribute(this, 'message');
}
set onmessage(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'message', listener);
}
get onopen(): EventCallback | null {
return getEventHandlerAttribute(this, 'open');
}
set onopen(listener: ?EventCallback) {
setEventHandlerAttribute(this, 'open', listener);
}
}
export default WebSocket;
@@ -0,0 +1,297 @@
/**
* 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.
*
* @format
* @flow
*/
import type {BlobData} from '../Blob/BlobTypes';
import type {EventSubscription} from '../vendor/emitter/EventEmitter';
import Blob from '../Blob/Blob';
import BlobManager from '../Blob/BlobManager';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import binaryToBase64 from '../Utilities/binaryToBase64';
import Platform from '../Utilities/Platform';
import NativeWebSocketModule from './NativeWebSocketModule';
import WebSocketEvent from './WebSocketEvent';
import base64 from 'base64-js';
import EventTarget from 'event-target-shim';
import invariant from 'invariant';
type ArrayBufferView =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| DataView;
type BinaryType = 'blob' | 'arraybuffer';
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;
const CLOSE_NORMAL = 1000;
// Abnormal closure where no code is provided in a control frame
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5
const CLOSE_ABNORMAL = 1006;
const WEBSOCKET_EVENTS = ['close', 'error', 'message', 'open'];
let nextWebSocketId = 0;
type WebSocketEventDefinitions = {
websocketOpen: [{id: number, protocol: string}],
websocketClosed: [{id: number, code: number, reason: string}],
websocketMessage: [
| {type: 'binary', id: number, data: string}
| {type: 'text', id: number, data: string}
| {type: 'blob', id: number, data: BlobData},
],
websocketFailed: [{id: number, message: string}],
};
/**
* Browser-compatible WebSockets implementation.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
* See https://github.com/websockets/ws
*/
class WebSocket extends (EventTarget(...WEBSOCKET_EVENTS): typeof EventTarget) {
static CONNECTING: number = CONNECTING;
static OPEN: number = OPEN;
static CLOSING: number = CLOSING;
static CLOSED: number = CLOSED;
CONNECTING: number = CONNECTING;
OPEN: number = OPEN;
CLOSING: number = CLOSING;
CLOSED: number = CLOSED;
_socketId: number;
_eventEmitter: NativeEventEmitter<WebSocketEventDefinitions>;
_subscriptions: Array<EventSubscription>;
_binaryType: ?BinaryType;
onclose: ?Function;
onerror: ?Function;
onmessage: ?Function;
onopen: ?Function;
bufferedAmount: number;
extension: ?string;
protocol: ?string;
readyState: number = CONNECTING;
url: ?string;
constructor(
url: string,
protocols: ?string | ?Array<string>,
options: ?{headers?: {origin?: string, ...}, ...},
) {
super();
this.url = url;
if (typeof protocols === 'string') {
protocols = [protocols];
}
const {headers = {}, ...unrecognized} = options || {};
// Preserve deprecated backwards compatibility for the 'origin' option
// $FlowFixMe[prop-missing]
if (unrecognized && typeof unrecognized.origin === 'string') {
console.warn(
'Specifying `origin` as a WebSocket connection option is deprecated. Include it under `headers` instead.',
);
/* $FlowFixMe[prop-missing] (>=0.54.0 site=react_native_fb,react_native_
* oss) This comment suppresses an error found when Flow v0.54 was
* deployed. To see the error delete this comment and run Flow. */
headers.origin = unrecognized.origin;
/* $FlowFixMe[prop-missing] (>=0.54.0 site=react_native_fb,react_native_
* oss) This comment suppresses an error found when Flow v0.54 was
* deployed. To see the error delete this comment and run Flow. */
delete unrecognized.origin;
}
// Warn about and discard anything else
if (Object.keys(unrecognized).length > 0) {
console.warn(
'Unrecognized WebSocket connection option(s) `' +
Object.keys(unrecognized).join('`, `') +
'`. ' +
'Did you mean to put these under `headers`?',
);
}
if (!Array.isArray(protocols)) {
protocols = null;
}
this._eventEmitter = new NativeEventEmitter(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeWebSocketModule,
);
this._socketId = nextWebSocketId++;
this._registerEvents();
NativeWebSocketModule.connect(url, protocols, {headers}, this._socketId);
}
get binaryType(): ?BinaryType {
return this._binaryType;
}
set binaryType(binaryType: BinaryType): void {
if (binaryType !== 'blob' && binaryType !== 'arraybuffer') {
throw new Error("binaryType must be either 'blob' or 'arraybuffer'");
}
if (this._binaryType === 'blob' || binaryType === 'blob') {
invariant(
BlobManager.isAvailable,
'Native module BlobModule is required for blob support',
);
if (binaryType === 'blob') {
BlobManager.addWebSocketHandler(this._socketId);
} else {
BlobManager.removeWebSocketHandler(this._socketId);
}
}
this._binaryType = binaryType;
}
close(code?: number, reason?: string): void {
if (this.readyState === this.CLOSING || this.readyState === this.CLOSED) {
return;
}
this.readyState = this.CLOSING;
this._close(code, reason);
}
send(data: string | ArrayBuffer | ArrayBufferView | Blob): void {
if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR');
}
if (data instanceof Blob) {
invariant(
BlobManager.isAvailable,
'Native module BlobModule is required for blob support',
);
BlobManager.sendOverSocket(data, this._socketId);
return;
}
if (typeof data === 'string') {
NativeWebSocketModule.send(data, this._socketId);
return;
}
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
NativeWebSocketModule.sendBinary(binaryToBase64(data), this._socketId);
return;
}
throw new Error('Unsupported data type');
}
ping(): void {
if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR');
}
NativeWebSocketModule.ping(this._socketId);
}
_close(code?: number, reason?: string): void {
// See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
const statusCode = typeof code === 'number' ? code : CLOSE_NORMAL;
const closeReason = typeof reason === 'string' ? reason : '';
NativeWebSocketModule.close(statusCode, closeReason, this._socketId);
if (BlobManager.isAvailable && this._binaryType === 'blob') {
BlobManager.removeWebSocketHandler(this._socketId);
}
}
_unregisterEvents(): void {
this._subscriptions.forEach(e => e.remove());
this._subscriptions = [];
}
_registerEvents(): void {
this._subscriptions = [
this._eventEmitter.addListener('websocketMessage', ev => {
if (ev.id !== this._socketId) {
return;
}
let data: Blob | BlobData | ArrayBuffer | string = ev.data;
switch (ev.type) {
case 'binary':
data = base64.toByteArray(ev.data).buffer;
break;
case 'blob':
data = BlobManager.createFromOptions(ev.data);
break;
}
this.dispatchEvent(new WebSocketEvent('message', {data}));
}),
this._eventEmitter.addListener('websocketOpen', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.OPEN;
this.protocol = ev.protocol;
this.dispatchEvent(new WebSocketEvent('open'));
}),
this._eventEmitter.addListener('websocketClosed', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.CLOSED;
this.dispatchEvent(
new WebSocketEvent('close', {
code: ev.code,
reason: ev.reason,
// TODO: missing `wasClean` (exposed on iOS as `clean` but missing on Android)
}),
);
this._unregisterEvents();
this.close();
}),
this._eventEmitter.addListener('websocketFailed', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.CLOSED;
this.dispatchEvent(
new WebSocketEvent('error', {
message: ev.message,
}),
);
this.dispatchEvent(
new WebSocketEvent('close', {
code: CLOSE_ABNORMAL,
reason: ev.message,
// TODO: Expose `wasClean`
}),
);
this._unregisterEvents();
this.close();
}),
];
}
}
export default WebSocket;
@@ -1453,6 +1453,52 @@ declare export default typeof File;
`;
exports[`public API should not change unintentionally Libraries/Blob/FileReader.js 1`] = `
"declare export default FileReader;
"
`;
exports[`public API should not change unintentionally Libraries/Blob/FileReader_new.js 1`] = `
"type ReadyState = 0 | 1 | 2;
type ReaderResult = string | ArrayBuffer;
declare class FileReader extends EventTarget {
static EMPTY: number;
static LOADING: number;
static DONE: number;
EMPTY: number;
LOADING: number;
DONE: number;
_readyState: ReadyState;
_error: ?Error;
_result: ?ReaderResult;
_aborted: boolean;
constructor(): void;
_reset(): void;
_setReadyState(newState: ReadyState): void;
readAsArrayBuffer(blob: ?Blob): void;
readAsDataURL(blob: ?Blob): void;
readAsText(blob: ?Blob, encoding: string): void;
abort(): void;
get readyState(): ReadyState;
get error(): ?Error;
get result(): ?ReaderResult;
get onabort(): EventCallback | null;
set onabort(listener: ?EventCallback): void;
get onerror(): EventCallback | null;
set onerror(listener: ?EventCallback): void;
get onload(): EventCallback | null;
set onload(listener: ?EventCallback): void;
get onloadstart(): EventCallback | null;
set onloadstart(listener: ?EventCallback): void;
get onloadend(): EventCallback | null;
set onloadend(listener: ?EventCallback): void;
get onprogress(): EventCallback | null;
set onprogress(listener: ?EventCallback): void;
}
declare export default typeof FileReader;
"
`;
exports[`public API should not change unintentionally Libraries/Blob/FileReader_old.js 1`] = `
"type ReadyState = 0 | 1 | 2;
type ReaderResult = string | ArrayBuffer;
declare class FileReader extends EventTarget {
@@ -6529,6 +6575,155 @@ exports[`public API should not change unintentionally Libraries/Network/RCTNetwo
`;
exports[`public API should not change unintentionally Libraries/Network/XMLHttpRequest.js 1`] = `
"export type * from \\"./XMLHttpRequest_old\\";
declare module.exports: XMLHttpRequest;
"
`;
exports[`public API should not change unintentionally Libraries/Network/XMLHttpRequest_new.js 1`] = `
"export type NativeResponseType = \\"base64\\" | \\"blob\\" | \\"text\\";
export type ResponseType =
| \\"\\"
| \\"arraybuffer\\"
| \\"blob\\"
| \\"document\\"
| \\"json\\"
| \\"text\\";
export type Response = ?Object | string;
type XHRInterceptor = interface {
requestSent(id: number, url: string, method: string, headers: Object): void,
responseReceived(
id: number,
url: string,
status: number,
headers: Object
): void,
dataReceived(id: number, data: string): void,
loadingFinished(id: number, encodedDataLength: number): void,
loadingFailed(id: number, error: string): void,
};
declare class XMLHttpRequestEventTarget extends EventTarget {
get onload(): EventCallback | null;
set onload(listener: ?EventCallback): void;
get onloadstart(): EventCallback | null;
set onloadstart(listener: ?EventCallback): void;
get onprogress(): EventCallback | null;
set onprogress(listener: ?EventCallback): void;
get ontimeout(): EventCallback | null;
set ontimeout(listener: ?EventCallback): void;
get onerror(): EventCallback | null;
set onerror(listener: ?EventCallback): void;
get onabort(): EventCallback | null;
set onabort(listener: ?EventCallback): void;
get onloadend(): EventCallback | null;
set onloadend(listener: ?EventCallback): void;
}
declare class XMLHttpRequest extends EventTarget {
static UNSENT: number;
static OPENED: number;
static HEADERS_RECEIVED: number;
static LOADING: number;
static DONE: number;
static _interceptor: ?XHRInterceptor;
static _profiling: boolean;
UNSENT: number;
OPENED: number;
HEADERS_RECEIVED: number;
LOADING: number;
DONE: number;
readyState: number;
responseHeaders: ?Object;
status: number;
timeout: number;
responseURL: ?string;
withCredentials: boolean;
upload: XMLHttpRequestEventTarget;
_requestId: ?number;
_subscriptions: Array<EventSubscription>;
_aborted: boolean;
_cachedResponse: Response;
_hasError: boolean;
_headers: Object;
_lowerCaseResponseHeaders: Object;
_method: ?string;
_perfKey: ?string;
_responseType: ResponseType;
_response: string;
_sent: boolean;
_url: ?string;
_timedOut: boolean;
_trackingName: string;
_incrementalEvents: boolean;
_startTime: ?number;
_performanceLogger: IPerformanceLogger;
static setInterceptor(interceptor: ?XHRInterceptor): void;
static enableProfiling(enableProfiling: boolean): void;
constructor(): void;
_reset(): void;
get responseType(): ResponseType;
set responseType(responseType: ResponseType): void;
get responseText(): string;
get response(): Response;
__didCreateRequest(requestId: number): void;
__didUploadProgress(requestId: number, progress: number, total: number): void;
__didReceiveResponse(
requestId: number,
status: number,
responseHeaders: ?Object,
responseURL: ?string
): void;
__didReceiveData(requestId: number, response: string): void;
__didReceiveIncrementalData(
requestId: number,
responseText: string,
progress: number,
total: number
): void;
__didReceiveDataProgress(
requestId: number,
loaded: number,
total: number
): void;
__didCompleteResponse(
requestId: number,
error: string,
timeOutError: boolean
): void;
_clearSubscriptions(): void;
getAllResponseHeaders(): ?string;
getResponseHeader(header: string): ?string;
setRequestHeader(header: string, value: any): void;
setTrackingName(trackingName: string): XMLHttpRequest;
setPerformanceLogger(performanceLogger: IPerformanceLogger): XMLHttpRequest;
open(method: string, url: string, async: ?boolean): void;
send(data: any): void;
abort(): void;
setResponseHeaders(responseHeaders: ?Object): void;
setReadyState(newState: number): void;
addEventListener(type: string, listener: EventListener | null): void;
_getMeasureURL(): string;
get onabort(): EventCallback | null;
set onabort(listener: ?EventCallback): void;
get onerror(): EventCallback | null;
set onerror(listener: ?EventCallback): void;
get onload(): EventCallback | null;
set onload(listener: ?EventCallback): void;
get onloadstart(): EventCallback | null;
set onloadstart(listener: ?EventCallback): void;
get onprogress(): EventCallback | null;
set onprogress(listener: ?EventCallback): void;
get ontimeout(): EventCallback | null;
set ontimeout(listener: ?EventCallback): void;
get onloadend(): EventCallback | null;
set onloadend(listener: ?EventCallback): void;
get onreadystatechange(): EventCallback | null;
set onreadystatechange(listener: ?EventCallback): void;
}
declare module.exports: XMLHttpRequest;
"
`;
exports[`public API should not change unintentionally Libraries/Network/XMLHttpRequest_old.js 1`] = `
"export type NativeResponseType = \\"base64\\" | \\"blob\\" | \\"text\\";
export type ResponseType =
| \\"\\"
@@ -9189,6 +9384,78 @@ declare export default typeof NativeWebSocketModule;
`;
exports[`public API should not change unintentionally Libraries/WebSocket/WebSocket.js 1`] = `
"declare export default WebSocket;
"
`;
exports[`public API should not change unintentionally Libraries/WebSocket/WebSocket_new.js 1`] = `
"type ArrayBufferView =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| DataView;
type BinaryType = \\"blob\\" | \\"arraybuffer\\";
type WebSocketEventDefinitions = {
websocketOpen: [{ id: number, protocol: string }],
websocketClosed: [{ id: number, code: number, reason: string }],
websocketMessage: [
| { type: \\"binary\\", id: number, data: string }
| { type: \\"text\\", id: number, data: string }
| { type: \\"blob\\", id: number, data: BlobData },
],
websocketFailed: [{ id: number, message: string }],
};
declare class WebSocket extends EventTarget {
static CONNECTING: number;
static OPEN: number;
static CLOSING: number;
static CLOSED: number;
CONNECTING: number;
OPEN: number;
CLOSING: number;
CLOSED: number;
_socketId: number;
_eventEmitter: NativeEventEmitter<WebSocketEventDefinitions>;
_subscriptions: Array<EventSubscription>;
_binaryType: ?BinaryType;
bufferedAmount: number;
extension: ?string;
protocol: ?string;
readyState: number;
url: ?string;
constructor(
url: string,
protocols: ?string | ?Array<string>,
options: ?{ headers?: { origin?: string, ... }, ... }
): void;
get binaryType(): ?BinaryType;
set binaryType(binaryType: BinaryType): void;
close(code?: number, reason?: string): void;
send(data: string | ArrayBuffer | ArrayBufferView | Blob): void;
ping(): void;
_close(code?: number, reason?: string): void;
_unregisterEvents(): void;
_registerEvents(): void;
get onclose(): EventCallback | null;
set onclose(listener: ?EventCallback): void;
get onerror(): EventCallback | null;
set onerror(listener: ?EventCallback): void;
get onmessage(): EventCallback | null;
set onmessage(listener: ?EventCallback): void;
get onopen(): EventCallback | null;
set onopen(listener: ?EventCallback): void;
}
declare export default typeof WebSocket;
"
`;
exports[`public API should not change unintentionally Libraries/WebSocket/WebSocket_old.js 1`] = `
"type ArrayBufferView =
| Int8Array
| Uint8Array
@@ -0,0 +1,63 @@
/**
* 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
* @format
*/
/**
* This module implements the `MessageEvent` interface from the HTML spec.
* See https://html.spec.whatwg.org/multipage/comms.html#messageevent.
*/
// flowlint unsafe-getters-setters:off
import type {EventInit} from '../../dom/events/Event';
import Event from '../../dom/events/Event';
export type MessageEventInit = $ReadOnly<{
...EventInit,
data?: mixed,
origin?: string,
lastEventId?: string,
// Unsupported
// source?: MessageEventSource,
// Unsupported
// ports?: Array<MessagePort>,
}>;
export default class MessageEvent extends Event {
_data: mixed;
_origin: string;
_lastEventId: string;
constructor(type: string, options?: ?MessageEventInit) {
const {
data,
origin = '',
lastEventId = '',
...eventOptions
} = options ?? {};
super(type, eventOptions);
this._data = data;
this._origin = String(origin);
this._lastEventId = String(lastEventId);
}
get data(): mixed {
return this._data;
}
get origin(): string {
return this._origin;
}
get lastEventId(): string {
return this._lastEventId;
}
}
@@ -0,0 +1,54 @@
/**
* 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
* @format
*/
/**
* This module implements the `CloseEvent` interface from `WebSocket`.
* See https://websockets.spec.whatwg.org/#the-closeevent-interface.
*/
// flowlint unsafe-getters-setters:off
import type {EventInit} from '../../dom/events/Event';
import Event from '../../dom/events/Event';
export type CloseEventInit = $ReadOnly<{
...EventInit,
wasClean?: boolean,
code?: number,
reason?: string,
}>;
export default class CloseEvent extends Event {
_wasClean: boolean;
_code: number;
_reason: string;
constructor(type: string, options?: ?CloseEventInit) {
const {wasClean, code, reason, ...eventOptions} = options ?? {};
super(type, eventOptions);
this._wasClean = Boolean(wasClean);
this._code = Number(code) || 0;
this._reason = reason != null ? String(reason) : '';
}
get wasClean(): boolean {
return this._wasClean;
}
get code(): number {
return this._code;
}
get reason(): string {
return this._reason;
}
}
@@ -0,0 +1,54 @@
/**
* 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
* @format
*/
/**
* This module implements the `ProgressEvent` interface from `XMLHttpRequest`.
* See https://xhr.spec.whatwg.org/#interface-progressevent.
*/
// flowlint unsafe-getters-setters:off
import type {EventInit} from '../../dom/events/Event';
import Event from '../../dom/events/Event';
export type ProgressEventInit = $ReadOnly<{
...EventInit,
lengthComputable: boolean,
loaded: number,
total: number,
}>;
export default class ProgressEvent extends Event {
_lengthComputable: boolean;
_loaded: number;
_total: number;
constructor(type: string, options?: ?ProgressEventInit) {
const {lengthComputable, loaded, total, ...eventOptions} = options ?? {};
super(type, eventOptions);
this._lengthComputable = Boolean(lengthComputable);
this._loaded = Number(loaded) || 0;
this._total = Number(total) || 0;
}
get lengthComputable(): boolean {
return this._lengthComputable;
}
get loaded(): number {
return this._loaded;
}
get total(): number {
return this._total;
}
}