diff --git a/change/react-native-windows-2020-09-23-19-56-04-issues-3178-blob.json b/change/react-native-windows-2020-09-23-19-56-04-issues-3178-blob.json new file mode 100644 index 00000000000..be04b1ad71a --- /dev/null +++ b/change/react-native-windows-2020-09-23-19-56-04-issues-3178-blob.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "Implement BlobModule", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-09-24T02:56:04.067Z" +} diff --git a/vnext/Desktop.DLL/react-native-win32.x64.def b/vnext/Desktop.DLL/react-native-win32.x64.def index 65ff04896b6..489ee53c6f0 100644 --- a/vnext/Desktop.DLL/react-native-win32.x64.def +++ b/vnext/Desktop.DLL/react-native-win32.x64.def @@ -61,6 +61,7 @@ EXPORTS ??0WebSocketModule@React@Microsoft@@QEAA@XZ ??0NetworkingModule@React@Microsoft@@QEAA@XZ ?CreateAsyncStorageModule@react@facebook@@YA?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@PEB_W@Z +?CreateBlobModule@React@Microsoft@@YA?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@XZ YGAssert YGAssertWithConfig diff --git a/vnext/Desktop.DLL/react-native-win32.x86.def b/vnext/Desktop.DLL/react-native-win32.x86.def index c7b0c9f89ed..e764787bf18 100644 --- a/vnext/Desktop.DLL/react-native-win32.x86.def +++ b/vnext/Desktop.DLL/react-native-win32.x86.def @@ -60,6 +60,7 @@ EXPORTS ?CreateAsyncStorageModule@react@facebook@@YG?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@PB_W@Z ?Make@IHttpResource@React@Microsoft@@SG?AV?$unique_ptr@UIHttpResource@React@Microsoft@@U?$default_delete@UIHttpResource@React@Microsoft@@@std@@@std@@XZ ??0NetworkingModule@React@Microsoft@@QAE@XZ +?CreateBlobModule@React@Microsoft@@YG?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@XZ _YGAlignToString@4 _YGAssert@8 diff --git a/vnext/Desktop.IntegrationTests/DesktopTestRunner.cpp b/vnext/Desktop.IntegrationTests/DesktopTestRunner.cpp index 42de7b57629..fa281260981 100644 --- a/vnext/Desktop.IntegrationTests/DesktopTestRunner.cpp +++ b/vnext/Desktop.IntegrationTests/DesktopTestRunner.cpp @@ -72,7 +72,8 @@ shared_ptr TestRunner::GetInstance( make_tuple( TestDeviceInfoModule::name, []() -> unique_ptr { return make_unique(); }, - nativeQueue)}; + nativeQueue), + make_tuple("BlobModule", []() -> unique_ptr { return CreateBlobModule(); }, nativeQueue)}; // <0> string // <1> CxxModule::Provider diff --git a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp index 5ae6ee31190..71545168be6 100644 --- a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp @@ -199,5 +199,13 @@ TEST_CLASS (RNTesterIntegrationTests) { Assert::AreEqual(TestStatus::Passed, result.Status, result.Message.c_str()); } + BEGIN_TEST_METHOD_ATTRIBUTE(WebSocketBlob) + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(WebSocketBlob) { + auto result = m_runner.RunTest("IntegrationTests/WebSocketBlobTest", "WebSocketBlobTest"); + Assert::AreEqual(TestStatus::Passed, result.Status, result.Message.c_str()); + } + #pragma endregion Extended Tests }; diff --git a/vnext/Desktop/HttpResource.cpp b/vnext/Desktop/HttpResource.cpp index ac687526cf4..2e8a67ec351 100644 --- a/vnext/Desktop/HttpResource.cpp +++ b/vnext/Desktop/HttpResource.cpp @@ -35,7 +35,7 @@ void HttpResource::SendRequest( int64_t timeout, std::function &&callback) noexcept { // Enforce supported args - assert(responseType == "text" || responseType == "base64"); + assert(responseType == "text" || responseType == "base64" || responseType == "blob"); assert(!useIncrementalUpdates); // ISS:2306365 - Callback with the requestId diff --git a/vnext/Desktop/Modules/BlobModule.cpp b/vnext/Desktop/Modules/BlobModule.cpp new file mode 100644 index 00000000000..50957320e44 --- /dev/null +++ b/vnext/Desktop/Modules/BlobModule.cpp @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "BlobModule.h" + +#include + +// React Native +#include +#include +#include + +// Windows API +#include +#include + +using namespace facebook::xplat; + +using folly::dynamic; +using std::lock_guard; +using std::mutex; +using std::string; +using std::vector; +using std::weak_ptr; +using winrt::Windows::Foundation::GuidHelper; +using winrt::Windows::Security::Cryptography::CryptographicBuffer; + +namespace { +constexpr char moduleName[] = "BlobModule"; +constexpr char blobURIScheme[] = "blob"; + +weak_ptr s_contentHandler; +} // namespace + +namespace Microsoft::React { + +BlobModule::BlobModule() noexcept { + if (!m_contentHandler) { + m_contentHandler = std::make_shared(); + s_contentHandler = m_contentHandler; + } +} + +#pragma region CxxModule overrides + +string BlobModule::getName() { + return moduleName; +} + +std::map BlobModule::getConstants() { + return {{"BLOB_URI_SCHEME", blobURIScheme}, {"BLOB_URI_HOST", {}}}; +} + +std::vector BlobModule::getMethods() { + return {Method( + "addNetworkingHandler", + [this](dynamic args) { + // TODO: Implement #6081 + }), + + Method( + "addWebSocketHandler", + [this](dynamic args) { + auto id = jsArgAsInt(args, 0); + + m_contentHandler->Register(id); + }), + + Method( + "removeWebSocketHandler", + [this](dynamic args) { + auto id = jsArgAsInt(args, 0); + + m_contentHandler->Unregister(id); + }), + + Method( + "sendOverSocket", + [this](dynamic args) { + auto blob = jsArgAsObject(args, 0); + auto blobId = blob["blobId"].getString(); + auto offset = blob["offset"].getInt(); + auto size = blob["size"].getInt(); + auto socketID = jsArgAsInt(args, 1); + + auto data = m_contentHandler->ResolveMessage(std::move(blobId), offset, size); + + if (auto instance = getInstance().lock()) { + auto buffer = CryptographicBuffer::CreateFromByteArray(data); + auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); + auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); + + auto sendArgs = dynamic::array(std::move(base64String), socketID); + instance->callJSFunction("WebSocketModule", "sendBinary", std::move(sendArgs)); + } + }), + + Method( + "createFromParts", + [this](dynamic args) { + auto parts = jsArgAsArray(args, 0); // Array + auto blobId = jsArgAsString(args, 1); + vector buffer{}; + + for (const auto &part : parts) { + auto type = part["type"]; + if (type == "blob") { + auto blob = part["data"]; + auto bufferPart = m_contentHandler->ResolveMessage( + blob["blobId"].asString(), blob["offset"].asInt(), blob["size"].asInt()); + buffer.reserve(buffer.size() + bufferPart.size()); + buffer.insert(buffer.end(), bufferPart.begin(), bufferPart.end()); + } else if (type == "string") { + auto data = part["data"].asString(); + auto bufferPart = vector(data.begin(), data.end()); + + buffer.reserve(buffer.size() + bufferPart.size()); + buffer.insert(buffer.end(), bufferPart.begin(), bufferPart.end()); + } else { + // TODO: Send error message to instance? + return; + } + + m_contentHandler->StoreMessage(std::move(buffer), std::move(blobId)); + } + }), + + Method( + "release", + [this](dynamic args) // blobId: string + { + auto blobId = jsArgAsString(args, 0); + + m_contentHandler->RemoveMessage(std::move(blobId)); + })}; +} + +#pragma endregion CxxModule overrides + +#pragma region IWebSocketModuleContentHandler overrides + +void BlobWebSocketModuleContentHandler::Register(int64_t socketID) noexcept /*override*/ +{ + lock_guard lock{m_socketIDsMutex}; + m_socketIDs.insert(socketID); +} + +void BlobWebSocketModuleContentHandler::Unregister(int64_t socketID) noexcept /*override*/ +{ + lock_guard lock{m_socketIDsMutex}; + if (m_socketIDs.find(socketID) != m_socketIDs.end()) + m_socketIDs.erase(socketID); +} + +const bool BlobWebSocketModuleContentHandler::IsRegistered(int64_t socketID) noexcept /*override*/ +{ + lock_guard lock{m_socketIDsMutex}; + return m_socketIDs.find(socketID) != m_socketIDs.end(); +} + +vector +BlobWebSocketModuleContentHandler::ResolveMessage(string &&blobId, int64_t offset, int64_t size) noexcept +/*override*/ +{ + lock_guard lock{m_blobsMutex}; + + auto data = m_blobs.at(std::move(blobId)); + auto start = data.cbegin() + static_cast(offset); + auto end = start + static_cast(size); + + return vector(start, end); +} + +void BlobWebSocketModuleContentHandler::RemoveMessage(string &&blobId) noexcept { + lock_guard lock{m_blobsMutex}; + + m_blobs.erase(std::move(blobId)); +} + +void BlobWebSocketModuleContentHandler::ProcessMessage(string &&message, dynamic ¶ms) /*override*/ +{ + params["data"] = std::move(message); +} + +void BlobWebSocketModuleContentHandler::ProcessMessage(vector &&message, dynamic ¶ms) /*override*/ +{ + auto blob = dynamic::object(); + blob("offset", 0); + blob("size", message.size()); + + // substr(1, 36) strips curly braces from a GUID. + string blobId = winrt::to_string(winrt::to_hstring(GuidHelper::CreateNewGuid())).substr(1, 36); + StoreMessage(std::move(message), std::move(blobId)); + + params["data"] = std::move(blob); + params["type"] = "blob"; +} + +void BlobWebSocketModuleContentHandler::StoreMessage(vector &&message, std::string &&blobId) noexcept +/*override*/ +{ + lock_guard lock{m_blobsMutex}; + m_blobs.insert_or_assign(std::move(blobId), std::move(message)); +} + +#pragma endregion IWebSocketModuleContentHandler overrides + +/*static*/ weak_ptr IWebSocketModuleContentHandler::GetInstance() noexcept { + return s_contentHandler; +} + +/*extern*/ std::unique_ptr CreateBlobModule() noexcept { + return std::make_unique(); +} + +} // namespace Microsoft::React diff --git a/vnext/Desktop/Modules/BlobModule.h b/vnext/Desktop/Modules/BlobModule.h new file mode 100644 index 00000000000..448e1cf7378 --- /dev/null +++ b/vnext/Desktop/Modules/BlobModule.h @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +// React Native +#include + +// Standard Library +#include +#include +#include +#include +#include + +namespace Microsoft::React { + +class BlobModule : public facebook::xplat::module::CxxModule { + std::shared_ptr m_contentHandler; + + public: + enum class MethodId { + AddNetworkingHandler = 0, + AddWebSocketHandler = 1, + RemoveWebSocketHandler = 2, + SendOverSocket = 3, + CreateFromParts = 4, + Release = 5, + SIZE = 6 + }; + + BlobModule() noexcept; + +#pragma region CxxModule overrides + + /// + /// + /// + std::string getName() override; + + /// + /// + /// + std::map getConstants() override; + + /// + /// + /// + /// See See react-native/Libraries/WebSocket/WebSocket.js + std::vector getMethods() override; + +#pragma endregion CxxModule overrides +}; + +class BlobWebSocketModuleContentHandler final : public IWebSocketModuleContentHandler { + std::unordered_map> m_blobs; + std::mutex m_blobsMutex; + std::unordered_set m_socketIDs; + std::mutex m_socketIDsMutex; + + public: +#pragma region IWebSocketModuleContentHandler overrides + + void Register(std::int64_t socketID) noexcept override; + + void Unregister(std::int64_t socketID) noexcept override; + + const bool IsRegistered(std::int64_t socketID) noexcept override; + + std::vector + ResolveMessage(std::string &&blobId, std::int64_t offset, std::int64_t size) noexcept override; + + void RemoveMessage(std::string &&blobId) noexcept override; + + void ProcessMessage(std::string &&message, folly::dynamic ¶ms) override; + + void ProcessMessage(std::vector &&message, folly::dynamic ¶ms) override; + + void StoreMessage(std::vector &&message, std::string &&blobId) noexcept override; + +#pragma endregion IWebSocketModuleContentHandler overrides +}; + +} // namespace Microsoft::React diff --git a/vnext/Desktop/Modules/WebSocketModule.cpp b/vnext/Desktop/Modules/WebSocketModule.cpp index f7f53318568..275a62e30d8 100644 --- a/vnext/Desktop/Modules/WebSocketModule.cpp +++ b/vnext/Desktop/Modules/WebSocketModule.cpp @@ -5,10 +5,17 @@ #include +#include #include +#include "Unicode.h" + +// React Native #include #include -#include "Unicode.h" + +// Windows API +#include +#include using namespace facebook::xplat; using namespace folly; @@ -16,9 +23,11 @@ using namespace folly; using Microsoft::Common::Unicode::Utf16ToUtf8; using Microsoft::Common::Unicode::Utf8ToUtf16; +// Standard Library using std::shared_ptr; using std::string; using std::weak_ptr; +using winrt::Windows::Security::Cryptography::CryptographicBuffer; namespace { constexpr char moduleName[] = "WebSocketModule"; @@ -204,7 +213,29 @@ shared_ptr WebSocketModule::GetOrCreateWebSocket(int64_t id, if (!strongInstance) return; - auto args = dynamic::object("id", id)("data", message)("type", isBinary ? "binary" : "text"); + dynamic args = dynamic::object("id", id)("data", message)("type", isBinary ? "binary" : "text"); + + auto contentHandler = IWebSocketModuleContentHandler::GetInstance().lock(); + if (isBinary) + { + auto buffer = CryptographicBuffer::DecodeFromBase64String(Utf8ToUtf16(message)); + winrt::com_array arr; + CryptographicBuffer::CopyToByteArray(buffer, arr); + auto data = std::vector(arr.begin(), arr.end()); + + if (contentHandler) + contentHandler->ProcessMessage(std::move(data), args); + else + args["data"] = message; + } + else + { + if (contentHandler) + contentHandler->ProcessMessage(string{message}, args); + else + args["data"] = message; + } + this->SendEvent("websocketMessage", std::move(args)); }); ws->SetOnClose([this, id, weakInstance](IWebSocketResource::CloseCode code, const string& reason) diff --git a/vnext/Desktop/NativeModuleFactories.h b/vnext/Desktop/NativeModuleFactories.h index babe17f4e03..cb958afca7b 100644 --- a/vnext/Desktop/NativeModuleFactories.h +++ b/vnext/Desktop/NativeModuleFactories.h @@ -16,7 +16,13 @@ extern std::unique_ptr CreateAsyncStorageMod extern std::unique_ptr CreateTimingModule( const std::shared_ptr &nativeThread) noexcept; -extern std::unique_ptr CreateWebSocketModule() noexcept; - } // namespace react } // namespace facebook + +namespace Microsoft::React { + +extern std::unique_ptr CreateWebSocketModule() noexcept; + +extern std::unique_ptr CreateBlobModule() noexcept; + +} // namespace Microsoft::React diff --git a/vnext/Desktop/React.Windows.Desktop.vcxproj b/vnext/Desktop/React.Windows.Desktop.vcxproj index 607956bdc9c..3be9d93dc4b 100644 --- a/vnext/Desktop/React.Windows.Desktop.vcxproj +++ b/vnext/Desktop/React.Windows.Desktop.vcxproj @@ -216,6 +216,7 @@ + @@ -284,6 +285,7 @@ + diff --git a/vnext/Desktop/React.Windows.Desktop.vcxproj.filters b/vnext/Desktop/React.Windows.Desktop.vcxproj.filters index 24338eb9a51..94b742fce5c 100644 --- a/vnext/Desktop/React.Windows.Desktop.vcxproj.filters +++ b/vnext/Desktop/React.Windows.Desktop.vcxproj.filters @@ -10,9 +10,6 @@ {63472e53-239b-4816-af09-ed2789b8fa95} - - {57577307-7ee7-4a55-8d17-c142b3f6f9cb} - {ae049213-3bdf-4cbe-83f4-39625a414539} @@ -22,9 +19,6 @@ {13b0d9c2-9c50-4245-b23f-e120dc6442cf} - - {fe8806a2-f72f-4dc3-9996-3a0ee66101ef} - {56988676-220f-43a7-b8b1-8ecc51158498} @@ -164,6 +158,9 @@ Source Files + + Source Files\Modules + @@ -184,6 +181,9 @@ Header Files + + Header Files\Modules + diff --git a/vnext/IntegrationTests/TestRunner.cpp b/vnext/IntegrationTests/TestRunner.cpp index e57e60ae0b8..f968cffdb76 100644 --- a/vnext/IntegrationTests/TestRunner.cpp +++ b/vnext/IntegrationTests/TestRunner.cpp @@ -6,10 +6,14 @@ #include #include -#include +#include #include "TestInstance.h" #include "TestModule.h" +// React Native +#include + +// Windows API #include #include @@ -69,7 +73,8 @@ TestResult TestRunner::RunTest(string &&bundlePath, string &&appName, NativeLogg // Note, further configuration should be done in each Windows variant's // TestRunner implementation. shared_ptr devSettings = make_shared(); - devSettings->useWebDebugger = false; // WebSocketJSExecutor can't register native log hooks. + // WebSocketJSExecutor can't register native log hooks. Default to false. + devSettings->useWebDebugger = GetRuntimeOptionBool("TestRunner.UseWebDebugger"); devSettings->liveReloadCallback = []() {}; // Enables ChakraExecutor devSettings->errorCallback = [&result](string message) { result.Message = Microsoft::Common::Unicode::Utf8ToUtf16(message); diff --git a/vnext/Shared/Modules/IWebSocketModuleContentHandler.h b/vnext/Shared/Modules/IWebSocketModuleContentHandler.h new file mode 100644 index 00000000000..cdb4592acef --- /dev/null +++ b/vnext/Shared/Modules/IWebSocketModuleContentHandler.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +// React Native +#include + +// Standard Library +#include +#include + +namespace Microsoft::React { + +/// +/// See https://github.com/facebook/react-native/blob/v0.63.2/React/CoreModules/RCTWebSocketModule.h#L12 +/// +struct IWebSocketModuleContentHandler { + static std::weak_ptr GetInstance() noexcept; + + virtual ~IWebSocketModuleContentHandler() noexcept {} + + virtual void Register(std::int64_t socketID) noexcept = 0; + + virtual void Unregister(std::int64_t socketID) noexcept = 0; + + virtual const bool IsRegistered(std::int64_t socketID) noexcept = 0; + + virtual std::vector + ResolveMessage(std::string &&blobId, std::int64_t offset, std::int64_t size) noexcept = 0; + + virtual void RemoveMessage(std::string &&blobId) noexcept = 0; + + virtual void ProcessMessage(std::string &&message, folly::dynamic ¶ms) = 0; + + virtual void ProcessMessage(std::vector &&message, folly::dynamic ¶ms) = 0; + + virtual void StoreMessage(std::vector &&message, std::string &&blobId) noexcept = 0; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/WebSocketModule.h b/vnext/Shared/Modules/WebSocketModule.h index 25fa33493b7..6285ca442b9 100644 --- a/vnext/Shared/Modules/WebSocketModule.h +++ b/vnext/Shared/Modules/WebSocketModule.h @@ -55,7 +55,7 @@ class WebSocketModule : public facebook::xplat::module::CxxModule { /// Keeps IWebSocketResource instances identified by id. /// As defined in WebSocket.js. /// - std::map> m_webSockets; + std::map> m_webSockets; /// /// Generates IWebSocketResource instances, defaulting to IWebSocketResource::Make. diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index 45c3a1624eb..7b45e37ad60 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -64,6 +64,7 @@ + diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index b9c58bf92f7..a0885614bfe 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -306,6 +306,7 @@ Header Files + @@ -321,4 +322,4 @@ Source Files\etw - + \ No newline at end of file diff --git a/vnext/overrides.json b/vnext/overrides.json index 4eee38febce..39769d0027f 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -10,7 +10,10 @@ "DeforkingPatches/README.md", "src/tsconfig.json", "**/*.windesktop.js", - "src/RNTester/js/examples-win/**" + "src/RNTester/js/examples-win/**", + "src/IntegrationTests/WebSocketBlobTest.js", + "src/IntegrationTests/websocket_integration_test_server_binary.js", + "src/IntegrationTests/websocket_integration_test_server_blob.js" ], "overrides": [ { @@ -84,10 +87,18 @@ "type": "platform", "file": "src/IntegrationTests/LoggingTest.js" }, + { + "type": "platform", + "file": "src/IntegrationTests/websocket_integration_test_server_blob.js" + }, { "type": "platform", "file": "src/IntegrationTests/websocket_integration_test_server_binary.js" }, + { + "type": "platform", + "file": "src/IntegrationTests/WebSocketBlobTest.js" + }, { "type": "platform", "file": "src/IntegrationTests/WebSocketBinaryTest.js" diff --git a/vnext/src/IntegrationTests/WebSocketBlobTest.js b/vnext/src/IntegrationTests/WebSocketBlobTest.js new file mode 100644 index 00000000000..94a184ed3a5 --- /dev/null +++ b/vnext/src/IntegrationTests/WebSocketBlobTest.js @@ -0,0 +1,168 @@ +/** + * Copyright (c) Facebook, Inc. and its 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'; + +const React = require('react'); +const ReactNative = require('react-native'); +const {AppRegistry, View} = ReactNative; +const {TestModule} = ReactNative.NativeModules; + +const DEFAULT_WS_URL = 'ws://localhost:5557/'; + +const WS_EVENTS = ['close', 'error', 'message', 'open']; + +type State = { + url: string, + fetchStatus: ?string, + socket: ?WebSocket, + socketState: ?number, + lastSocketEvent: ?string, + lastMessage: ?Blob, + testMessage: Uint8Array, + testExpectedResponse: Uint8Array, + ... +}; + +class WebSocketBlobTest extends React.Component<{}, State> { + state: State = { + url: DEFAULT_WS_URL, + fetchStatus: null, + socket: null, + socketState: null, + lastSocketEvent: null, + lastMessage: null, + testMessage: new Uint8Array([1, 2, 3]), + testExpectedResponse: new Uint8Array([4, 5, 6, 7]), + }; + + _waitFor = (condition: any, timeout: any, callback: any) => { + let remaining = timeout; + const timeoutFunction = function() { + if (condition()) { + callback(true); + return; + } + remaining--; + if (remaining === 0) { + callback(false); + } else { + setTimeout(timeoutFunction, 1000); + } + }; + setTimeout(timeoutFunction, 1000); + }; + + _connect = () => { + const socket = new WebSocket(this.state.url); + socket.binaryType = 'blob'; + WS_EVENTS.forEach(ev => socket.addEventListener(ev, this._onSocketEvent)); + this.setState({ + socket, + socketState: socket.readyState, + }); + }; + + _socketIsConnected = () => { + return this.state.socketState === 1; //'OPEN' + }; + + _socketIsDisconnected = () => { + return this.state.socketState === 3; //'CLOSED' + }; + + _disconnect = () => { + if (!this.state.socket) { + return; + } + this.state.socket.close(); + }; + + _onSocketEvent = (event: any) => { + const state: any = { + socketState: event.target.readyState, + lastSocketEvent: event.type, + }; + if (event.type === 'message') { + state.lastMessage = event.data; + } + this.setState(state); + }; + + _sendBinary = (message: Blob) => { + if (!this.state.socket) { + return; + } + this.state.socket.send(message); + }; + + _sendTestMessage = () => { + this._sendBinary(this.state.testMessage); + }; + + _receivedTestExpectedResponse = () => { + if ( + this.state.lastMessage?.size !== this.state.testExpectedResponse.length + ) { + return false; + } + + //for (var i = 0; i < expected.length; i++) { + // if (expected[i] !== result[i]) { + // return false; + // } + //} + + return true; + }; + + componentDidMount() { + this.testConnect(); + } + + testConnect: () => void = () => { + this._connect(); + this._waitFor(this._socketIsConnected, 5, connectSucceeded => { + if (!connectSucceeded) { + TestModule.markTestPassed(false); + return; + } + this.testSendAndReceive(); + }); + }; + + testSendAndReceive: () => void = () => { + this._sendTestMessage(); + this._waitFor(this._receivedTestExpectedResponse, 5, messageReceived => { + if (!messageReceived) { + TestModule.markTestPassed(false); + return; + } + this.testDisconnect(); + }); + }; + + testDisconnect: () => void = () => { + this._disconnect(); + this._waitFor(this._socketIsDisconnected, 5, disconnectSucceeded => { + TestModule.markTestPassed(disconnectSucceeded); + }); + }; + + render(): React.Node { + return ; + } +} + +WebSocketBlobTest.displayName = 'WebSocketBlobTest'; + +AppRegistry.registerComponent('WebSocketBlobTest', () => WebSocketBlobTest); + +module.exports = WebSocketBlobTest; diff --git a/vnext/src/IntegrationTests/websocket_integration_test_server_blob.js b/vnext/src/IntegrationTests/websocket_integration_test_server_blob.js new file mode 100644 index 00000000000..f415043d917 --- /dev/null +++ b/vnext/src/IntegrationTests/websocket_integration_test_server_blob.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/** + * Copyright (c) Facebook, Inc. and its 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 + */ + +'use strict'; + +/* eslint-env node */ + +const WebSocket = require('ws'); +const Blob = require('node-fetch'); + +console.log(`\ +WebSocket binary integration test server + +This will send each incoming message back, in binary form. + +`); + +const server = new WebSocket.Server({port: 5557}); +server.on('connection', ws => { + ws.binaryType = "blob"; + ws.on('message', message => { + console.log(message); + + ws.send([4, 5, 6, 7]); + }); +});