From 61583b38afbc92cd2e0b03e9e4e9d69bf058296f Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Fri, 18 Sep 2015 19:57:04 +0530 Subject: [PATCH 1/5] Implement WebSocket module for Android. Fixes #2837 Add a message property to error event Use okhttp-ws for WebSocket Added comments about the status code Specify reason to close socket on JavaScript side Pass extra parameters to close Change 'WebSocketManager' to 'WebSocketModule' Change reactContext to mReactContext Change timeouts as per react native JS debugger Better error handlng Explain executor shutdown --- Libraries/WebSocket/WebSocket.android.js | 91 ++++++++- .../modules/websocket/WebSocketModule.java | 182 ++++++++++++++++++ .../react/shell/MainReactPackage.java | 2 + 3 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java diff --git a/Libraries/WebSocket/WebSocket.android.js b/Libraries/WebSocket/WebSocket.android.js index 301f92f4915f..dcbf351b4510 100644 --- a/Libraries/WebSocket/WebSocket.android.js +++ b/Libraries/WebSocket/WebSocket.android.js @@ -11,28 +11,107 @@ */ 'use strict'; +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTWebSocketModule = require('NativeModules').WebSocketModule; + var WebSocketBase = require('WebSocketBase'); +var WebSocketId = 0; + +var CLOSE_NORMAL = 1000; + class WebSocket extends WebSocketBase { + _socketId: number; + _subs: any; connectToSocketImpl(url: string): void { - console.warn('WebSocket is not yet supported on Android'); + this._socketId = WebSocketId++; + + RCTWebSocketModule.connect(url, this._socketId); + + this._registerEvents(this._socketId); } - closeConnectionImpl(): void{ - console.warn('WebSocket is not yet supported on Android'); + closeConnectionImpl(code?: number, reason?: string): void { + /* + * The status code 1000 means 'CLOSE_NORMAL' + * Reason is empty string by to match browser behaviour + * More info: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + * More info: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + */ + var statusCode = typeof code === 'number' ? code : CLOSE_NORMAL; + var closeReason = typeof reason === 'string' ? reason : ''; + + RCTWebSocketModule.close(statusCode, closeReason, this._socketId); } cancelConnectionImpl(): void { - console.warn('WebSocket is not yet supported on Android'); + RCTWebSocketModule.close(CLOSE_NORMAL, '', this._socketId); } sendStringImpl(message: string): void { - console.warn('WebSocket is not yet supported on Android'); + RCTWebSocketModule.send(message, this._socketId); } sendArrayBufferImpl(): void { - console.warn('WebSocket is not yet supported on Android'); + // TODO + console.warn('Sending ArrayBuffers is not yet supported'); + } + + _unregisterEvents(): void { + this._subs.forEach(e => e.remove()); + this._subs = []; + } + + _registerEvents(id: number): void { + this._subs = [ + RCTDeviceEventEmitter.addListener('websocketMessage', ev => { + if (ev.id !== id) { + return; + } + + this.onmessage && this.onmessage({ + data: ev.data + }); + }), + RCTDeviceEventEmitter.addListener('websocketOpen', ev => { + if (ev.id !== id) { + return; + } + + this.readyState = this.OPEN; + this.onopen && this.onopen(); + }), + RCTDeviceEventEmitter.addListener('websocketClosed', ev => { + if (ev.id !== id) { + return; + } + + this.readyState = this.CLOSED; + this.onclose && this.onclose(ev); + + this._unregisterEvents(); + + RCTWebSocketModule.close(CLOSE_NORMAL, '', id); + }), + RCTDeviceEventEmitter.addListener('websocketFailed', ev => { + if (ev.id !== id) { + return; + } + + this.onerror && this.onerror(new Error(ev.message)); + this._unregisterEvents(); + + RCTWebSocketModule.close(CLOSE_NORMAL, '', id); + }), + RCTDeviceEventEmitter.addListener('websocketErrored', ev => { + if (ev.id !== id) { + return; + } + + this.onerror && this.onerror(new Error(ev.message)); + }) + ]; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java new file mode 100644 index 000000000000..7aa6b5e67d88 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java @@ -0,0 +1,182 @@ +/** + * 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. + */ + +package com.facebook.react.modules.websocket; + +import android.support.annotation.Nullable; + +import java.io.IOException; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.ws.WebSocket; +import com.squareup.okhttp.ws.WebSocketCall; +import com.squareup.okhttp.ws.WebSocketListener; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import okio.Buffer; +import okio.BufferedSource; + +public class WebSocketModule extends ReactContextBaseJavaModule { + + private Map mWebSocketConnections = new HashMap<>(); + private ReactContext mReactContext; + + public WebSocketModule(ReactApplicationContext ctx) { + super(ctx); + mReactContext = ctx; + } + + private void sendEvent(String eventName, @Nullable WritableMap params) { + mReactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); + } + + @Override + public String getName() { + return "WebSocketModule"; + } + + @ReactMethod + public void connect(final String url, final int id) { + OkHttpClient client = new OkHttpClient(); + + client.setConnectTimeout(10, TimeUnit.SECONDS); + client.setWriteTimeout(10, TimeUnit.SECONDS); + // Disable timeouts for read + client.setReadTimeout(0, TimeUnit.MINUTES); + + Request request = new Request.Builder() + .tag(id) + .url(url) + .build(); + + WebSocketCall.create(client, request).enqueue(new WebSocketListener() { + @Override + public void onOpen(final WebSocket webSocket, final Response response) { + mWebSocketConnections.put(id, webSocket); + + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + sendEvent("websocketOpen", params); + } + + @Override + public void onMessage( + final BufferedSource bufferedSource, + final WebSocket.PayloadType payloadType + ) { + String message; + + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + + try { + message = bufferedSource.readUtf8(); + } catch (IOException e) { + params.putString("message", e.getMessage()); + sendEvent("websocketErrored", params); + + return; + } + + params.putString("data", message); + + try { + bufferedSource.close(); + } catch (IOException e) { + FLog.e( + ReactConstants.TAG, + "Could not close BufferedSource for WebSocket message " + id, + e); + } + + sendEvent("websocketMessage", params); + } + + @Override + public void onClose(final int code, final String reason) { + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putInt("code", code); + params.putString("reason", reason); + sendEvent("websocketClosed", params); + } + + @Override + public void onFailure(final IOException e, final Response response) { + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putString("message", e.getMessage()); + sendEvent("websocketFailed", params); + } + + @Override + public void onPong(final Buffer buffer) { + } + }); + + // Trigger shutdown of the dispatcher's executor so this process can exit cleanly. + client.getDispatcher().getExecutorService().shutdown(); + } + + @ReactMethod + public void close(final int code, final String reason, final int id) { + WebSocket client = mWebSocketConnections.get(id); + + if (client == null) { + FLog.w( + ReactConstants.TAG, + "Could not find WebSocket connection for " + id); + return; + } + + try { + client.close(code, reason); + mWebSocketConnections.remove(id); + } catch (Exception e) { + FLog.e( + ReactConstants.TAG, + "Could not close WebSocket connection for " + id, + e); + } + } + + @ReactMethod + public void send(final String message, final int id) { + WebSocket client = mWebSocketConnections.get(id); + + try { + client.sendMessage( + WebSocket.PayloadType.TEXT, + new Buffer().writeUtf8(message) + ); + } catch (IOException e) { + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putString("message", e.getMessage()); + sendEvent("websocketErrored", params); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 65b7b38bcb53..50faadff6d95 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -21,6 +21,7 @@ import com.facebook.react.modules.network.NetworkingModule; import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.toast.ToastModule; +import com.facebook.react.modules.websocket.WebSocketModule; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.drawer.ReactDrawerLayoutManager; import com.facebook.react.views.image.ReactImageManager; @@ -46,6 +47,7 @@ public List createNativeModules(ReactApplicationContext reactConte new AsyncStorageModule(reactContext), new FrescoModule(reactContext), new NetworkingModule(reactContext), + new WebSocketModule(reactContext), new ToastModule(reactContext)); } From e853d3626875bbcaeebd06cbd6f685b7c5b18e12 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Mon, 28 Sep 2015 03:39:32 +0530 Subject: [PATCH 2/5] Implement EventTarget interface for WebSocket (Android) --- Libraries/WebSocket/Event.js | 29 ++++++++++++++++ Libraries/WebSocket/MessageEvent.js | 33 +++++++++++++++++++ Libraries/WebSocket/WebSocket.android.js | 33 ++++++++++++++++--- Libraries/WebSocket/WebSocket.ios.js | 14 ++------ .../modules/websocket/WebSocketModule.java | 2 +- 5 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 Libraries/WebSocket/Event.js create mode 100644 Libraries/WebSocket/MessageEvent.js diff --git a/Libraries/WebSocket/Event.js b/Libraries/WebSocket/Event.js new file mode 100644 index 000000000000..c3860a906a01 --- /dev/null +++ b/Libraries/WebSocket/Event.js @@ -0,0 +1,29 @@ +/** + * + * Copyright 2013-2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule Event + * + */ + +'use strict'; + +class Event { + constructor(type) { + this.type = type.toString(); + } +} + +module.exports = Event; diff --git a/Libraries/WebSocket/MessageEvent.js b/Libraries/WebSocket/MessageEvent.js new file mode 100644 index 000000000000..dc3dc9822710 --- /dev/null +++ b/Libraries/WebSocket/MessageEvent.js @@ -0,0 +1,33 @@ +/** + * + * Copyright 2013-2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule MessageEvent + * + */ + +'use strict'; + +var Event = require('Event'); + +class MessageEvent extends Event { + constructor(type, eventInitDict) { + super(type); + + Object.assign(this, eventInitDict); + } +} + +module.exports = MessageEvent; diff --git a/Libraries/WebSocket/WebSocket.android.js b/Libraries/WebSocket/WebSocket.android.js index dcbf351b4510..b074d4e27db6 100644 --- a/Libraries/WebSocket/WebSocket.android.js +++ b/Libraries/WebSocket/WebSocket.android.js @@ -16,6 +16,9 @@ var RCTWebSocketModule = require('NativeModules').WebSocketModule; var WebSocketBase = require('WebSocketBase'); +var Event = require('Event'); +var MessageEvent = require('MessageEvent'); + var WebSocketId = 0; var CLOSE_NORMAL = 1000; @@ -70,9 +73,12 @@ class WebSocket extends WebSocketBase { return; } - this.onmessage && this.onmessage({ + var event = new MessageEvent('message', { data: ev.data }); + + this.onmessage && this.onmessage(event); + this.dispatchEvent(event); }), RCTDeviceEventEmitter.addListener('websocketOpen', ev => { if (ev.id !== id) { @@ -80,7 +86,11 @@ class WebSocket extends WebSocketBase { } this.readyState = this.OPEN; - this.onopen && this.onopen(); + + var event = new Event('open'); + + this.onopen && this.onopen(event); + this.dispatchEvent(event); }), RCTDeviceEventEmitter.addListener('websocketClosed', ev => { if (ev.id !== id) { @@ -88,8 +98,11 @@ class WebSocket extends WebSocketBase { } this.readyState = this.CLOSED; - this.onclose && this.onclose(ev); + var event = new Event('close'); + + this.onclose && this.onclose(event); + this.dispatchEvent(event); this._unregisterEvents(); RCTWebSocketModule.close(CLOSE_NORMAL, '', id); @@ -99,7 +112,12 @@ class WebSocket extends WebSocketBase { return; } - this.onerror && this.onerror(new Error(ev.message)); + var event = new Event('error'); + + event.message = ev.message; + + this.onerror && this.onerror(event); + this.dispatchEvent(event); this._unregisterEvents(); RCTWebSocketModule.close(CLOSE_NORMAL, '', id); @@ -109,7 +127,12 @@ class WebSocket extends WebSocketBase { return; } - this.onerror && this.onerror(new Error(ev.message)); + var event = new Event('error'); + + event.message = ev.message; + + this.onerror && this.onerror(event); + this.dispatchEvent(event); }) ]; } diff --git a/Libraries/WebSocket/WebSocket.ios.js b/Libraries/WebSocket/WebSocket.ios.js index c2318b4976ec..c5f4cd9686e8 100644 --- a/Libraries/WebSocket/WebSocket.ios.js +++ b/Libraries/WebSocket/WebSocket.ios.js @@ -16,18 +16,8 @@ var RCTWebSocketManager = require('NativeModules').WebSocketManager; var WebSocketBase = require('WebSocketBase'); -class Event { - constructor(type) { - this.type = type.toString(); - } -} - -class MessageEvent extends Event { - constructor(type, eventInitDict) { - super(type); - Object.assign(this, eventInitDict); - } -} +var Event = require('Event'); +var MessageEvent = require('MessageEvent'); var WebSocketId = 0; diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java index 7aa6b5e67d88..b32ce19be6d3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java @@ -147,7 +147,7 @@ public void close(final int code, final String reason, final int id) { if (client == null) { FLog.w( - ReactConstants.TAG, + ReactConstants.TAG, "Could not find WebSocket connection for " + id); return; } From 110c6d483ddda70d94f718e1ed3a9011d7fd6677 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Fri, 2 Oct 2015 12:11:34 +0530 Subject: [PATCH 3/5] Use single WebSocket.js file to avoid code duplication --- Libraries/WebSocket/Event.js | 18 +-- Libraries/WebSocket/MessageEvent.js | 18 +-- .../RCTWebSocket.xcodeproj/project.pbxproj | 12 +- ...ebSocketManager.h => RCTWebSocketModule.h} | 2 +- ...ebSocketManager.m => RCTWebSocketModule.m} | 6 +- Libraries/WebSocket/WebSocket.ios.js | 116 ------------------ .../{WebSocket.android.js => WebSocket.js} | 60 ++++----- .../modules/websocket/WebSocketModule.java | 7 +- 8 files changed, 45 insertions(+), 194 deletions(-) rename Libraries/WebSocket/{RCTWebSocketManager.h => RCTWebSocketModule.h} (85%) rename Libraries/WebSocket/{RCTWebSocketManager.m => RCTWebSocketModule.m} (95%) delete mode 100644 Libraries/WebSocket/WebSocket.ios.js rename Libraries/WebSocket/{WebSocket.android.js => WebSocket.js} (73%) diff --git a/Libraries/WebSocket/Event.js b/Libraries/WebSocket/Event.js index c3860a906a01..46497d687eb6 100644 --- a/Libraries/WebSocket/Event.js +++ b/Libraries/WebSocket/Event.js @@ -1,18 +1,10 @@ /** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. * - * Copyright 2013-2014 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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 Event * diff --git a/Libraries/WebSocket/MessageEvent.js b/Libraries/WebSocket/MessageEvent.js index dc3dc9822710..21a7055bcfec 100644 --- a/Libraries/WebSocket/MessageEvent.js +++ b/Libraries/WebSocket/MessageEvent.js @@ -1,18 +1,10 @@ /** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. * - * Copyright 2013-2014 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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 MessageEvent * diff --git a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj index e1862d5818be..62dbee6a7606 100644 --- a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj +++ b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 1338BBE01B04ACC80064A9C9 /* RCTSRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 1338BBDD1B04ACC80064A9C9 /* RCTSRWebSocket.m */; }; 1338BBE11B04ACC80064A9C9 /* RCTWebSocketExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 1338BBDF1B04ACC80064A9C9 /* RCTWebSocketExecutor.m */; }; - 3C86DF7C1ADF695F0047B81A /* RCTWebSocketManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF7B1ADF695F0047B81A /* RCTWebSocketManager.m */; }; + 3C86DF7C1ADF695F0047B81A /* RCTWebSocketModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF7B1ADF695F0047B81A /* RCTWebSocketModule.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -18,8 +18,8 @@ 1338BBDE1B04ACC80064A9C9 /* RCTWebSocketExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketExecutor.h; sourceTree = ""; }; 1338BBDF1B04ACC80064A9C9 /* RCTWebSocketExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketExecutor.m; sourceTree = ""; }; 3C86DF461ADF2C930047B81A /* libRCTWebSocket.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTWebSocket.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 3C86DF7A1ADF695F0047B81A /* RCTWebSocketManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketManager.h; sourceTree = ""; }; - 3C86DF7B1ADF695F0047B81A /* RCTWebSocketManager.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketManager.m; sourceTree = ""; tabWidth = 2; }; + 3C86DF7A1ADF695F0047B81A /* RCTWebSocketModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketModule.h; sourceTree = ""; }; + 3C86DF7B1ADF695F0047B81A /* RCTWebSocketModule.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketModule.m; sourceTree = ""; tabWidth = 2; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -40,8 +40,8 @@ 1338BBDD1B04ACC80064A9C9 /* RCTSRWebSocket.m */, 1338BBDE1B04ACC80064A9C9 /* RCTWebSocketExecutor.h */, 1338BBDF1B04ACC80064A9C9 /* RCTWebSocketExecutor.m */, - 3C86DF7A1ADF695F0047B81A /* RCTWebSocketManager.h */, - 3C86DF7B1ADF695F0047B81A /* RCTWebSocketManager.m */, + 3C86DF7A1ADF695F0047B81A /* RCTWebSocketModule.h */, + 3C86DF7B1ADF695F0047B81A /* RCTWebSocketModule.m */, 3C86DF471ADF2C930047B81A /* Products */, ); indentWidth = 2; @@ -112,7 +112,7 @@ buildActionMask = 2147483647; files = ( 1338BBE01B04ACC80064A9C9 /* RCTSRWebSocket.m in Sources */, - 3C86DF7C1ADF695F0047B81A /* RCTWebSocketManager.m in Sources */, + 3C86DF7C1ADF695F0047B81A /* RCTWebSocketModule.m in Sources */, 1338BBE11B04ACC80064A9C9 /* RCTWebSocketExecutor.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Libraries/WebSocket/RCTWebSocketManager.h b/Libraries/WebSocket/RCTWebSocketModule.h similarity index 85% rename from Libraries/WebSocket/RCTWebSocketManager.h rename to Libraries/WebSocket/RCTWebSocketModule.h index 196486f256b9..ee176a9f246d 100644 --- a/Libraries/WebSocket/RCTWebSocketManager.h +++ b/Libraries/WebSocket/RCTWebSocketModule.h @@ -9,6 +9,6 @@ #import "RCTBridgeModule.h" -@interface RCTWebSocketManager : NSObject +@interface RCTWebSocketModule : NSObject @end diff --git a/Libraries/WebSocket/RCTWebSocketManager.m b/Libraries/WebSocket/RCTWebSocketModule.m similarity index 95% rename from Libraries/WebSocket/RCTWebSocketManager.m rename to Libraries/WebSocket/RCTWebSocketModule.m index 039dd7f22fef..24066439f029 100644 --- a/Libraries/WebSocket/RCTWebSocketManager.m +++ b/Libraries/WebSocket/RCTWebSocketModule.m @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTWebSocketManager.h" +#import "RCTWebSocketModule.h" #import "RCTBridge.h" #import "RCTEventDispatcher.h" @@ -29,11 +29,11 @@ - (void)setReactTag:(NSNumber *)reactTag @end -@interface RCTWebSocketManager () +@interface RCTWebSocketModule () @end -@implementation RCTWebSocketManager +@implementation RCTWebSocketModule { RCTSparseArray *_sockets; } diff --git a/Libraries/WebSocket/WebSocket.ios.js b/Libraries/WebSocket/WebSocket.ios.js deleted file mode 100644 index c5f4cd9686e8..000000000000 --- a/Libraries/WebSocket/WebSocket.ios.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * 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 - * - */ -'use strict'; - -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTWebSocketManager = require('NativeModules').WebSocketManager; - -var WebSocketBase = require('WebSocketBase'); - -var Event = require('Event'); -var MessageEvent = require('MessageEvent'); - -var WebSocketId = 0; - -class WebSocket extends WebSocketBase { - _socketId: number; - _subs: any; - - connectToSocketImpl(url: string): void { - this._socketId = WebSocketId++; - RCTWebSocketManager.connect(url, this._socketId); - this._registerEvents(this._socketId); - } - - closeConnectionImpl(): void { - RCTWebSocketManager.close(this._socketId); - } - - cancelConnectionImpl(): void { - RCTWebSocketManager.close(this._socketId); - } - - sendStringImpl(message: string): void { - RCTWebSocketManager.send(message, this._socketId); - } - - sendArrayBufferImpl(): void { - // TODO - console.warn('Sending ArrayBuffers is not yet supported'); - } - - _unregisterEvents(): void { - this._subs.forEach(e => e.remove()); - this._subs = []; - } - - _registerEvents(id: number): void { - this._subs = [ - RCTDeviceEventEmitter.addListener( - 'websocketMessage', - function(ev) { - if (ev.id !== id) { - return; - } - var event = new MessageEvent('message', { - data: ev.data - }); - this.onmessage && this.onmessage(event); - this.dispatchEvent(event); - }.bind(this) - ), - RCTDeviceEventEmitter.addListener( - 'websocketOpen', - function(ev) { - if (ev.id !== id) { - return; - } - this.readyState = this.OPEN; - var event = new Event('open'); - this.onopen && this.onopen(event); - this.dispatchEvent(event); - }.bind(this) - ), - RCTDeviceEventEmitter.addListener( - 'websocketClosed', - function(ev) { - if (ev.id !== id) { - return; - } - this.readyState = this.CLOSED; - var event = new Event('close'); - this.onclose && this.onclose(event); - this.dispatchEvent(event); - this._unregisterEvents(); - RCTWebSocketManager.close(id); - }.bind(this) - ), - RCTDeviceEventEmitter.addListener( - 'websocketFailed', - function(ev) { - if (ev.id !== id) { - return; - } - var event = new Event('error'); - event.message = ev.message; - this.onerror && this.onerror(event); - this.dispatchEvent(event); - this._unregisterEvents(); - RCTWebSocketManager.close(id); - }.bind(this) - ) - ]; - } - -} - -module.exports = WebSocket; diff --git a/Libraries/WebSocket/WebSocket.android.js b/Libraries/WebSocket/WebSocket.js similarity index 73% rename from Libraries/WebSocket/WebSocket.android.js rename to Libraries/WebSocket/WebSocket.js index b074d4e27db6..2a22a192a271 100644 --- a/Libraries/WebSocket/WebSocket.android.js +++ b/Libraries/WebSocket/WebSocket.js @@ -14,6 +14,8 @@ var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); var RCTWebSocketModule = require('NativeModules').WebSocketModule; +var Platform = require('Platform'); + var WebSocketBase = require('WebSocketBase'); var Event = require('Event'); @@ -36,20 +38,11 @@ class WebSocket extends WebSocketBase { } closeConnectionImpl(code?: number, reason?: string): void { - /* - * The status code 1000 means 'CLOSE_NORMAL' - * Reason is empty string by to match browser behaviour - * More info: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent - * More info: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent - */ - var statusCode = typeof code === 'number' ? code : CLOSE_NORMAL; - var closeReason = typeof reason === 'string' ? reason : ''; - - RCTWebSocketModule.close(statusCode, closeReason, this._socketId); + this._closeWebSocket(this._socketId, code, reason); } cancelConnectionImpl(): void { - RCTWebSocketModule.close(CLOSE_NORMAL, '', this._socketId); + this._closeWebSocket(this._socketId); } sendStringImpl(message: string): void { @@ -61,6 +54,22 @@ class WebSocket extends WebSocketBase { console.warn('Sending ArrayBuffers is not yet supported'); } + _closeWebSocket(id: number, code?: number, reason?: string): void { + if (Platform.OS === 'android') { + /* + * The status code 1000 means 'CLOSE_NORMAL' + * Reason is empty string by to match browser behaviour + * More info: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + */ + var statusCode = typeof code === 'number' ? code : CLOSE_NORMAL; + var closeReason = typeof reason === 'string' ? reason : ''; + + RCTWebSocketModule.close(statusCode, closeReason, id); + } else { + RCTWebSocketModule.close(id); + } + } + _unregisterEvents(): void { this._subs.forEach(e => e.remove()); this._subs = []; @@ -72,11 +81,9 @@ class WebSocket extends WebSocketBase { if (ev.id !== id) { return; } - var event = new MessageEvent('message', { data: ev.data }); - this.onmessage && this.onmessage(event); this.dispatchEvent(event); }), @@ -84,11 +91,8 @@ class WebSocket extends WebSocketBase { if (ev.id !== id) { return; } - this.readyState = this.OPEN; - var event = new Event('open'); - this.onopen && this.onopen(event); this.dispatchEvent(event); }), @@ -96,43 +100,23 @@ class WebSocket extends WebSocketBase { if (ev.id !== id) { return; } - this.readyState = this.CLOSED; - var event = new Event('close'); - this.onclose && this.onclose(event); this.dispatchEvent(event); this._unregisterEvents(); - - RCTWebSocketModule.close(CLOSE_NORMAL, '', id); + this._closeWebSocket(id); }), RCTDeviceEventEmitter.addListener('websocketFailed', ev => { if (ev.id !== id) { return; } - var event = new Event('error'); - event.message = ev.message; - this.onerror && this.onerror(event); this.dispatchEvent(event); this._unregisterEvents(); - - RCTWebSocketModule.close(CLOSE_NORMAL, '', id); - }), - RCTDeviceEventEmitter.addListener('websocketErrored', ev => { - if (ev.id !== id) { - return; - } - - var event = new Event('error'); - - event.message = ev.message; - - this.onerror && this.onerror(event); - this.dispatchEvent(event); + this._closeWebSocket(id); }) ]; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java index b32ce19be6d3..d8347ad2d651 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java @@ -96,7 +96,7 @@ public void onMessage( message = bufferedSource.readUtf8(); } catch (IOException e) { params.putString("message", e.getMessage()); - sendEvent("websocketErrored", params); + sendEvent("websocketFailed", params); return; } @@ -170,13 +170,12 @@ public void send(final String message, final int id) { try { client.sendMessage( WebSocket.PayloadType.TEXT, - new Buffer().writeUtf8(message) - ); + new Buffer().writeUtf8(message)); } catch (IOException e) { WritableMap params = Arguments.createMap(); params.putInt("id", id); params.putString("message", e.getMessage()); - sendEvent("websocketErrored", params); + sendEvent("websocketFailed", params); } } } From c9a5556dc956c885454d91cdd32140a29fd15081 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Fri, 2 Oct 2015 17:27:34 +0530 Subject: [PATCH 4/5] Added code and reason to closeEvent --- Libraries/WebSocket/WebSocket.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Libraries/WebSocket/WebSocket.js b/Libraries/WebSocket/WebSocket.js index 2a22a192a271..3d71ffc3a643 100644 --- a/Libraries/WebSocket/WebSocket.js +++ b/Libraries/WebSocket/WebSocket.js @@ -102,6 +102,8 @@ class WebSocket extends WebSocketBase { } this.readyState = this.CLOSED; var event = new Event('close'); + event.code = ev.code; + event.reason = ev.reason; this.onclose && this.onclose(event); this.dispatchEvent(event); this._unregisterEvents(); From 7dffbbfc7bc9a6eb3c066f520778459521fc8fb9 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Fri, 2 Oct 2015 17:29:26 +0530 Subject: [PATCH 5/5] Mention WebSocket under polyfills --- docs/Network.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/Network.md b/docs/Network.md index 5b26f51f9be7..682c15fc8d4d 100644 --- a/docs/Network.md +++ b/docs/Network.md @@ -7,7 +7,7 @@ permalink: docs/network.html next: timers --- -One of React Native's goals is to be a playground where we can experiment with different architectures and crazy ideas. Since browsers are not flexible enough, we had no choice but to reimplement the entire stack. In the places that we did not intend to change anything, we tried to be as faithful as possible to the browser APIs. The networking stack is a great example. +One of React Native's goals is to be a playground where we can experiment with different architectures and crazy ideas. Since browsers are not flexible enough, we had no choice but to re-implement the entire stack. In the places that we did not intend to change anything, we tried to be as faithful as possible to the browser APIs. The networking stack is a great example. ## XMLHttpRequest @@ -49,3 +49,32 @@ fetch('https://mywebsite.com/endpoint.php') console.warn(error); }); ``` + + +## WebSocket + +WebSocket is a protocol providing full-duplex communication channels over a single TCP connection. + +```javascript +var ws = new WebSocket('ws://host.com/path'); + +ws.on('open', function() { + // connection opened + ws.send('something'); +}); + +ws.on('message', function(e) { + // a message was received + console.log(e.data); +}); + +ws.on('error', function(e) { + // an error occurred + console.log(e.message); +}); + +ws.on('close', function(e) { + // connection closed + console.log(e.code, e.reason); +}); +```