diff --git a/Libraries/WebSocket/Event.js b/Libraries/WebSocket/Event.js new file mode 100644 index 000000000000..46497d687eb6 --- /dev/null +++ b/Libraries/WebSocket/Event.js @@ -0,0 +1,21 @@ +/** + * 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 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..21a7055bcfec --- /dev/null +++ b/Libraries/WebSocket/MessageEvent.js @@ -0,0 +1,25 @@ +/** + * 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 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/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.android.js b/Libraries/WebSocket/WebSocket.android.js deleted file mode 100644 index 301f92f4915f..000000000000 --- a/Libraries/WebSocket/WebSocket.android.js +++ /dev/null @@ -1,39 +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 WebSocketBase = require('WebSocketBase'); - -class WebSocket extends WebSocketBase { - - connectToSocketImpl(url: string): void { - console.warn('WebSocket is not yet supported on Android'); - } - - closeConnectionImpl(): void{ - console.warn('WebSocket is not yet supported on Android'); - } - - cancelConnectionImpl(): void { - console.warn('WebSocket is not yet supported on Android'); - } - - sendStringImpl(message: string): void { - console.warn('WebSocket is not yet supported on Android'); - } - - sendArrayBufferImpl(): void { - console.warn('WebSocket is not yet supported on Android'); - } -} - -module.exports = WebSocket; diff --git a/Libraries/WebSocket/WebSocket.ios.js b/Libraries/WebSocket/WebSocket.ios.js deleted file mode 100644 index c2318b4976ec..000000000000 --- a/Libraries/WebSocket/WebSocket.ios.js +++ /dev/null @@ -1,126 +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'); - -class Event { - constructor(type) { - this.type = type.toString(); - } -} - -class MessageEvent extends Event { - constructor(type, eventInitDict) { - super(type); - Object.assign(this, eventInitDict); - } -} - -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.js b/Libraries/WebSocket/WebSocket.js new file mode 100644 index 000000000000..3d71ffc3a643 --- /dev/null +++ b/Libraries/WebSocket/WebSocket.js @@ -0,0 +1,127 @@ +/** + * 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 RCTWebSocketModule = require('NativeModules').WebSocketModule; + +var Platform = require('Platform'); + +var WebSocketBase = require('WebSocketBase'); + +var Event = require('Event'); +var MessageEvent = require('MessageEvent'); + +var WebSocketId = 0; + +var CLOSE_NORMAL = 1000; + +class WebSocket extends WebSocketBase { + _socketId: number; + _subs: any; + + connectToSocketImpl(url: string): void { + this._socketId = WebSocketId++; + + RCTWebSocketModule.connect(url, this._socketId); + + this._registerEvents(this._socketId); + } + + closeConnectionImpl(code?: number, reason?: string): void { + this._closeWebSocket(this._socketId, code, reason); + } + + cancelConnectionImpl(): void { + this._closeWebSocket(this._socketId); + } + + sendStringImpl(message: string): void { + RCTWebSocketModule.send(message, this._socketId); + } + + sendArrayBufferImpl(): void { + // TODO + 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 = []; + } + + _registerEvents(id: number): void { + this._subs = [ + RCTDeviceEventEmitter.addListener('websocketMessage', ev => { + if (ev.id !== id) { + return; + } + var event = new MessageEvent('message', { + data: ev.data + }); + this.onmessage && this.onmessage(event); + this.dispatchEvent(event); + }), + RCTDeviceEventEmitter.addListener('websocketOpen', ev => { + if (ev.id !== id) { + return; + } + this.readyState = this.OPEN; + var event = new Event('open'); + this.onopen && this.onopen(event); + this.dispatchEvent(event); + }), + RCTDeviceEventEmitter.addListener('websocketClosed', ev => { + if (ev.id !== id) { + return; + } + 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(); + 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(); + this._closeWebSocket(id); + }) + ]; + } +} + +module.exports = WebSocket; 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..d8347ad2d651 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java @@ -0,0 +1,181 @@ +/** + * 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("websocketFailed", 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("websocketFailed", 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)); } 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); +}); +```