mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
d80284c607
commit
ea1260accb
+8
-174
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
+237
-229
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user