Dmitry Petukhov 6b42d5c952 Updating Websocket readyState in case of websocketFailed event
Summary:
Fix for https://github.com/facebook/react-native/issues/9465

We are building a react-native based application which extensively uses WebSockets. The Android app crashes right after waking up being in suspended mode for a coupe of days and throws an exception:

"Cannot send a message. Unknown WebSocket id 1"

Before calling WebSocket.send(...) method from WebSocket.js we always check its readyState. I believe the problem is caused by not updating readyState if case of 'websocketFailed' event. this.close() cause the current used websocket ID to be removed from mWebSocketConnections HashMap (WebSocketModule.java), but readyState stays the same.
Closes https://github.com/facebook/react-native/pull/9487

Differential Revision: D3838675

Pulled By: mkonicek

fbshipit-source-id: e833cef9f1b94c6f7236077241cacf5a56f5824b
2016-09-08 17:13:45 -07:00

207 lines
5.3 KiB
JavaScript

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule WebSocket
* @flow
*/
'use strict';
const NativeEventEmitter = require('NativeEventEmitter');
const Platform = require('Platform');
const RCTWebSocketModule = require('NativeModules').WebSocketModule;
const WebSocketEvent = require('WebSocketEvent');
const EventTarget = require('event-target-shim');
const base64 = require('base64-js');
import type EventSubscription from 'EventSubscription';
type ArrayBufferView =
Int8Array |
Uint8Array |
Uint8ClampedArray |
Int16Array |
Uint16Array |
Int32Array |
Uint32Array |
Float32Array |
Float64Array |
DataView;
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;
const CLOSE_NORMAL = 1000;
const WEBSOCKET_EVENTS = [
'close',
'error',
'message',
'open',
];
let nextWebSocketId = 0;
/**
* 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) {
static CONNECTING = CONNECTING;
static OPEN = OPEN;
static CLOSING = CLOSING;
static CLOSED = CLOSED;
CONNECTING: number = CONNECTING;
OPEN: number = OPEN;
CLOSING: number = CLOSING;
CLOSED: number = CLOSED;
_socketId: number;
_eventEmitter: NativeEventEmitter;
_subscriptions: Array<EventSubscription>;
onclose: ?Function;
onerror: ?Function;
onmessage: ?Function;
onopen: ?Function;
binaryType: ?string;
bufferedAmount: number;
extension: ?string;
protocol: ?string;
readyState: number = CONNECTING;
url: ?string;
constructor(url: string, protocols: ?string | ?Array<string>, options: ?{origin?: string}) {
super();
if (typeof protocols === 'string') {
protocols = [protocols];
}
if (!Array.isArray(protocols)) {
protocols = null;
}
this._eventEmitter = new NativeEventEmitter(RCTWebSocketModule);
this._socketId = nextWebSocketId++;
RCTWebSocketModule.connect(url, protocols, options, this._socketId);
this._registerEvents();
}
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): void {
if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR');
}
if (typeof data === 'string') {
RCTWebSocketModule.send(data, this._socketId);
return;
}
if (ArrayBuffer.isView(data)) {
// $FlowFixMe: no way to assert that 'data' is indeed an ArrayBufferView now
data = data.buffer;
}
if (data instanceof ArrayBuffer) {
data = base64.fromByteArray(new Uint8Array(data));
RCTWebSocketModule.sendBinary(data, this._socketId);
return;
}
throw new Error('Unsupported data type');
}
ping(): void {
if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR');
}
RCTWebSocketModule.ping(this._socketId);
}
_close(code?: number, reason?: string): void {
if (Platform.OS === 'android') {
// 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 : '';
RCTWebSocketModule.close(statusCode, closeReason, this._socketId);
} else {
RCTWebSocketModule.close(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;
}
this.dispatchEvent(new WebSocketEvent('message', {
data: (ev.type === 'binary') ? base64.toByteArray(ev.data).buffer : ev.data
}));
}),
this._eventEmitter.addListener('websocketOpen', ev => {
if (ev.id !== this._socketId) {
return;
}
this.readyState = this.OPEN;
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,
}));
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', {
message: ev.message,
}));
this._unregisterEvents();
this.close();
})
];
}
}
module.exports = WebSocket;