diff --git a/.ado/jobs/desktop.yml b/.ado/jobs/desktop.yml index 86ba196b7f9..cb5b4af022a 100644 --- a/.ado/jobs/desktop.yml +++ b/.ado/jobs/desktop.yml @@ -63,6 +63,7 @@ jobs: - name: Desktop.IntegrationTests.Filter value: > (FullyQualifiedName!=RNTesterIntegrationTests::AsyncStorage)& + (FullyQualifiedName!=RNTesterIntegrationTests::Blob)& (FullyQualifiedName!=RNTesterIntegrationTests::IntegrationTestHarness)& (FullyQualifiedName!=WebSocketResourcePerformanceTest::ProcessThreadsPerResource)& (FullyQualifiedName!=Microsoft::React::Test::HttpOriginPolicyIntegrationTest) diff --git a/change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json b/change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json new file mode 100644 index 00000000000..9c6be33fb71 --- /dev/null +++ b/change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement Blob module", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop.DLL/react-native-win32.x64.def b/vnext/Desktop.DLL/react-native-win32.x64.def index a0833d14490..f8d5742909f 100644 --- a/vnext/Desktop.DLL/react-native-win32.x64.def +++ b/vnext/Desktop.DLL/react-native-win32.x64.def @@ -56,7 +56,6 @@ EXPORTS ?makeChakraRuntime@JSI@Microsoft@@YA?AV?$unique_ptr@VRuntime@jsi@facebook@@U?$default_delete@VRuntime@jsi@facebook@@@std@@@std@@$$QEAUChakraRuntimeArgs@12@@Z ?Make@IHttpResource@Networking@React@Microsoft@@SA?AV?$shared_ptr@UIHttpResource@Networking@React@Microsoft@@@std@@XZ ?CreateTimingModule@react@facebook@@YA?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@AEBV?$shared_ptr@VMessageQueueThread@react@facebook@@@4@@Z -??0WebSocketModule@React@Microsoft@@QEAA@XZ ??0NetworkingModule@React@Microsoft@@QEAA@XZ ?MakeJSQueueThread@ReactNative@Microsoft@@YA?AV?$shared_ptr@VMessageQueueThread@react@facebook@@@std@@XZ ?Hash128@SpookyHashV2@hash@folly@@SAXPEBX_KPEA_K2@Z diff --git a/vnext/Desktop.DLL/react-native-win32.x86.def b/vnext/Desktop.DLL/react-native-win32.x86.def index 0365f31dd07..d899f933715 100644 --- a/vnext/Desktop.DLL/react-native-win32.x86.def +++ b/vnext/Desktop.DLL/react-native-win32.x86.def @@ -51,7 +51,6 @@ EXPORTS ?GetRuntimeOptionString@React@Microsoft@@YA?BV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@ABV34@@Z ?makeChakraRuntime@JSI@Microsoft@@YG?AV?$unique_ptr@VRuntime@jsi@facebook@@U?$default_delete@VRuntime@jsi@facebook@@@std@@@std@@$$QAUChakraRuntimeArgs@12@@Z ?CreateTimingModule@react@facebook@@YG?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@ABV?$shared_ptr@VMessageQueueThread@react@facebook@@@4@@Z -??0WebSocketModule@React@Microsoft@@QAE@XZ ?Make@IHttpResource@Networking@React@Microsoft@@SG?AV?$shared_ptr@UIHttpResource@Networking@React@Microsoft@@@std@@XZ ??0NetworkingModule@React@Microsoft@@QAE@XZ ?MakeJSQueueThread@ReactNative@Microsoft@@YG?AV?$shared_ptr@VMessageQueueThread@react@facebook@@@std@@XZ diff --git a/vnext/Desktop.IntegrationTests/DesktopTestRunner.cpp b/vnext/Desktop.IntegrationTests/DesktopTestRunner.cpp index 7cacdf8c25c..b1ad4b1540b 100644 --- a/vnext/Desktop.IntegrationTests/DesktopTestRunner.cpp +++ b/vnext/Desktop.IntegrationTests/DesktopTestRunner.cpp @@ -3,9 +3,6 @@ #include -#include -#include -#include #include #include #include "ChakraRuntimeHolder.h" @@ -42,6 +39,7 @@ shared_ptr TestRunner::GetInstance( auto nativeQueue = Microsoft::ReactNative::MakeJSQueueThread(); auto jsQueue = Microsoft::ReactNative::MakeJSQueueThread(); + // See InstanceImpl::GetDefaultNativeModules at OInstance.cpp vector>> extraModules{ {"AsyncLocalStorage", []() -> unique_ptr { diff --git a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp index ce9fb266d61..640383f71e6 100644 --- a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp +++ b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp @@ -142,8 +142,9 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest) resource->SendRequest( string{http::to_string(clientArgs.Method).data()}, string{server1Args.Url}, + 0, /*requestId*/ std::move(clientArgs.RequestHeaders), - { IHttpResource::BodyData::Type::String, "REQUEST_CONTENT" }, + {}, /*data*/ "text", false, /*useIncrementalUpdates*/ 1000, /*timeout*/ @@ -195,8 +196,9 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest) resource->SendRequest( string{http::to_string(clientArgs.Method).data()}, string{serverArgs.Url}, + 0, /*requestId*/ std::move(clientArgs.RequestHeaders), - { IHttpResource::BodyData::Type::String, "REQUEST_CONTENT" }, + {}, /*data*/ "text", false, /*useIncrementalUpdates*/ 1000, /*timeout*/ @@ -290,10 +292,12 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest) resource->SendRequest( "TRACE", url, + 0, /*requestId*/ { {"ValidHeader", "AnyValue"} }, - {} /*bodyData*/, + {}, /*data*/ + //{} /*bodyData*/, "text", false /*useIncrementalUpdates*/, 1000 /*timeout*/, diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index 6d73e68aa63..2d2e7dac901 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -73,8 +73,9 @@ TEST_CLASS (HttpResourceIntegrationTest) { resource->SendRequest( "GET", std::move(url), - {} /*header*/, - {} /*bodyData*/, + 0, /*requestId*/ + {}, /*header*/ + {}, /*data*/ "text", false, 1000 /*timeout*/, @@ -128,13 +129,14 @@ TEST_CLASS (HttpResourceIntegrationTest) { resource->SendRequest( "GET", std::move(url), + 0, /*requestId*/ { {"Content-Type", "application/json"}, {"Content-Encoding", "ASCII"}, {"name3", "value3"}, {"name4", "value4"}, }, - {} /*bodyData*/, + {}, /*data*/ "text", false, 1000 /*timeout*/, @@ -170,7 +172,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { promise.set_value(); }); - resource->SendRequest("GET", "http://nonexistinghost", {}, {}, "text", false, 1000, false, [](int64_t) {}); + resource->SendRequest("GET", "http://nonexistinghost", 0, {}, {}, "text", false, 1000, false, [](int64_t) {}); promise.get_future().wait(); @@ -238,8 +240,9 @@ TEST_CLASS (HttpResourceIntegrationTest) { resource->SendRequest( "OPTIONS", string{url}, - {} /*headers*/, - {} /*bodyData*/, + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ "text", false, 1000 /*timeout*/, @@ -248,8 +251,9 @@ TEST_CLASS (HttpResourceIntegrationTest) { resource->SendRequest( "GET", std::move(url), - {} /*headers*/, - {} /*bodyData*/, + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ "text", false, 1000 /*timeout*/, @@ -330,8 +334,9 @@ TEST_CLASS (HttpResourceIntegrationTest) { resource->SendRequest( "GET", std::move(url), - {} /*headers*/, - {} /*bodyData*/, + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ "text", false, /*useIncrementalUpdates*/ 1000 /*timeout*/, diff --git a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp index 260fb494f10..ea1183a6a96 100644 --- a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp @@ -188,10 +188,12 @@ TEST_CLASS (RNTesterIntegrationTests) { TEST_METHOD(WebSocket) { // Should behave the same as IntegrationTests/websocket_integration_test_server.js auto server = std::make_shared(5555, false /*useTLS*/); - server->SetMessageFactory([](std::string &&message) -> std::string { return message + "_response"; }); + server->SetMessageFactory([](string &&message) -> string { return message + "_response"; }); server->Start(); TestComponent("WebSocketTest"); + + server->Stop(); } BEGIN_TEST_METHOD_ATTRIBUTE(AccessibilityManager) @@ -216,5 +218,20 @@ 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()); + } + + BEGIN_TEST_METHOD_ATTRIBUTE(Blob) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(Blob) { + auto result = m_runner.RunTest("IntegrationTests/BlobTest", "BlobTest"); + Assert::AreEqual(TestStatus::Passed, result.Status, result.Message.c_str()); + } + #pragma endregion Extended Tests }; diff --git a/vnext/Desktop.UnitTests/WebSocketModuleTest.cpp b/vnext/Desktop.UnitTests/WebSocketModuleTest.cpp index 5fec25b8e13..295c1f117bf 100644 --- a/vnext/Desktop.UnitTests/WebSocketModuleTest.cpp +++ b/vnext/Desktop.UnitTests/WebSocketModuleTest.cpp @@ -26,7 +26,7 @@ TEST_CLASS (WebSocketModuleTest) { "connect", "close", "send", "sendBinary", "ping"}; TEST_METHOD(CreateModule) { - auto module = make_unique(); + auto module = make_unique(nullptr /*inspectableProperties*/); Assert::IsFalse(module == nullptr); Assert::AreEqual(string("WebSocketModule"), module->getName()); @@ -40,7 +40,7 @@ TEST_CLASS (WebSocketModuleTest) { } TEST_METHOD(ConnectEmptyUriFails) { - auto module = make_unique(); + auto module = make_unique(nullptr /*inspectableProperties*/); module->getMethods() .at(WebSocketModule::MethodId::Connect) @@ -70,7 +70,7 @@ TEST_CLASS (WebSocketModuleTest) { }; auto instance = CreateMockInstance(jsef); - auto module = make_unique(); + auto module = make_unique(nullptr /*inspectableProperties*/); module->setInstance(instance); module->SetResourceFactory([](const string &) { auto rc = make_shared(); diff --git a/vnext/Microsoft.ReactNative.Cxx/Microsoft.ReactNative.Cxx.vcxitems.filters b/vnext/Microsoft.ReactNative.Cxx/Microsoft.ReactNative.Cxx.vcxitems.filters index 38880c96c7c..c8626e0d355 100644 --- a/vnext/Microsoft.ReactNative.Cxx/Microsoft.ReactNative.Cxx.vcxitems.filters +++ b/vnext/Microsoft.ReactNative.Cxx/Microsoft.ReactNative.Cxx.vcxitems.filters @@ -27,7 +27,7 @@ JSI - + diff --git a/vnext/Microsoft.ReactNative.Managed.UnitTests/packages.lock.json b/vnext/Microsoft.ReactNative.Managed.UnitTests/packages.lock.json index 29155030c86..0f5ab5eac44 100644 --- a/vnext/Microsoft.ReactNative.Managed.UnitTests/packages.lock.json +++ b/vnext/Microsoft.ReactNative.Managed.UnitTests/packages.lock.json @@ -38,6 +38,11 @@ "resolved": "2.1.2", "contentHash": "DXTDuBumPC4oo9KKZMt5zgOuLdfUjqcsLRLyqeubaIxfl7ZBfk8wfsKRWYd1m5aCL3ekifW5pwT3rwuB2mDdLw==" }, + "boost": { + "type": "Transitive", + "resolved": "1.76.0", + "contentHash": "p+w3YvNdXL8Cu9Fzrmexssu0tZbWxuf6ywsQqHjDlKFE5ojXHof1HIyMC3zDLfLnh80dIeFcEUAuR2Asg/XHRA==" + }, "Microsoft.Net.Native.Compiler": { "type": "Transitive", "resolved": "2.2.7-rel-27913-00", @@ -69,10 +74,25 @@ "resolved": "1.0.1", "contentHash": "rkn+fKobF/cbWfnnfBOQHKVKIOpxMZBvlSHkqDWgBpwGDcLRduvs3D9OLGeV6GWGvVwNlVi2CBbTjuPmtHvyNw==" }, + "Microsoft.UI.Xaml": { + "type": "Transitive", + "resolved": "2.7.0", + "contentHash": "dB4im13tfmMgL/V3Ei+3kD2rUF+/lTxAmR4gjJ45l577eljHfdo/KUrxpq/3I1Vp6e5GCDG1evDaEGuDxypLMg==" + }, + "Microsoft.Windows.CppWinRT": { + "type": "Transitive", + "resolved": "2.0.211028.7", + "contentHash": "JBGI0c3WLoU6aYJRy9Qo0MLDQfObEp+d4nrhR95iyzf7+HOgjRunHDp/6eGFREd7xq3OI1mll9ecJrMfzBvlyg==" + }, + "Microsoft.Windows.SDK.BuildTools": { + "type": "Transitive", + "resolved": "10.0.22000.194", + "contentHash": "4L0P3zqut466SIqT3VBeLTNUQTxCBDOrTRymRuROCRJKazcK7ibLz9yAO1nKWRt50ttCj39oAa2Iuz9ZTDmLlg==" + }, "NETStandard.Library": { "type": "Transitive", "resolved": "2.0.3", - "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "contentHash": "548M6mnBSJWxsIlkQHfbzoYxpiYFXZZSL00p4GHYv8PkiqFBnnT68mW5mGEsA/ch9fDO9GkPgkFQpWiXZN7mAQ==", "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0" } @@ -82,6 +102,11 @@ "resolved": "9.0.1", "contentHash": "U82mHQSKaIk+lpSVCbWYKNavmNH1i5xrExDEquU1i6I5pV6UMOqRnJRSlKO3cMPfcpp0RgDY+8jUXHdQ4IfXvw==" }, + "ReactNative.Hermes.Windows": { + "type": "Transitive", + "resolved": "0.11.0-ms.6", + "contentHash": "WAVLsSZBV4p/3hNC3W67su7xu3f/ZMSKxu0ON7g2GaKRbkJmH0Qyif1IlzcJwtvR48kuOdfgPu7Bgtz3AY+gqg==" + }, "runtime.win10-arm.Microsoft.Net.Native.Compiler": { "type": "Transitive", "resolved": "2.2.7-rel-27913-00", @@ -268,15 +293,45 @@ "System.Runtime": "4.1.0" } }, - "microsoft.reactnative": { + "common": { + "type": "Project" + }, + "fmt": { "type": "Project" }, + "folly": { + "type": "Project", + "dependencies": { + "boost": "1.76.0", + "fmt": "1.0.0" + } + }, + "microsoft.reactnative": { + "type": "Project", + "dependencies": { + "Common": "1.0.0", + "Folly": "1.0.0", + "Microsoft.UI.Xaml": "2.7.0", + "Microsoft.Windows.CppWinRT": "2.0.211028.7", + "Microsoft.Windows.SDK.BuildTools": "10.0.22000.194", + "ReactCommon": "1.0.0", + "ReactNative.Hermes.Windows": "0.11.0-ms.6", + "boost": "1.76.0" + } + }, "microsoft.reactnative.managed": { "type": "Project", "dependencies": { "Microsoft.NETCore.UniversalWindowsPlatform": "6.2.9", "Microsoft.ReactNative": "1.0.0" } + }, + "reactcommon": { + "type": "Project", + "dependencies": { + "Folly": "1.0.0", + "boost": "1.76.0" + } } }, "UAP,Version=v10.0.16299/win10-arm": { diff --git a/vnext/Microsoft.ReactNative.Managed/packages.lock.json b/vnext/Microsoft.ReactNative.Managed/packages.lock.json index 14d1ec0d868..decd73cbdce 100644 --- a/vnext/Microsoft.ReactNative.Managed/packages.lock.json +++ b/vnext/Microsoft.ReactNative.Managed/packages.lock.json @@ -24,6 +24,11 @@ "Microsoft.SourceLink.Common": "1.0.0" } }, + "boost": { + "type": "Transitive", + "resolved": "1.76.0", + "contentHash": "p+w3YvNdXL8Cu9Fzrmexssu0tZbWxuf6ywsQqHjDlKFE5ojXHof1HIyMC3zDLfLnh80dIeFcEUAuR2Asg/XHRA==" + }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "1.0.0", @@ -53,21 +58,41 @@ "Microsoft.NETCore.Platforms": { "type": "Transitive", "resolved": "2.1.0", - "contentHash": "ok+RPAtESz/9MUXeIEz6Lv5XAGQsaNmEYXMsgVALj4D7kqC8gveKWXWXbufLySR2fWrwZf8smyN5RmHu0e4BHA==" + "contentHash": "GmkKfoyerqmsHMn7OZj0AKpcBabD+GaafqphvX2Mw406IwiJRy1pKcKqdCfKJfYmkRyJ6+e+RaUylgdJoDa1jQ==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "1.0.0", "contentHash": "G8DuQY8/DK5NN+3jm5wcMcd9QYD90UV7MiLmdljSJixi3U/vNaeBKmmXUqI4DJCOeWizIUEh4ALhSt58mR+5eg==" }, + "Microsoft.UI.Xaml": { + "type": "Transitive", + "resolved": "2.7.0", + "contentHash": "dB4im13tfmMgL/V3Ei+3kD2rUF+/lTxAmR4gjJ45l577eljHfdo/KUrxpq/3I1Vp6e5GCDG1evDaEGuDxypLMg==" + }, + "Microsoft.Windows.CppWinRT": { + "type": "Transitive", + "resolved": "2.0.211028.7", + "contentHash": "JBGI0c3WLoU6aYJRy9Qo0MLDQfObEp+d4nrhR95iyzf7+HOgjRunHDp/6eGFREd7xq3OI1mll9ecJrMfzBvlyg==" + }, + "Microsoft.Windows.SDK.BuildTools": { + "type": "Transitive", + "resolved": "10.0.22000.194", + "contentHash": "4L0P3zqut466SIqT3VBeLTNUQTxCBDOrTRymRuROCRJKazcK7ibLz9yAO1nKWRt50ttCj39oAa2Iuz9ZTDmLlg==" + }, "NETStandard.Library": { "type": "Transitive", "resolved": "2.0.3", - "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "contentHash": "548M6mnBSJWxsIlkQHfbzoYxpiYFXZZSL00p4GHYv8PkiqFBnnT68mW5mGEsA/ch9fDO9GkPgkFQpWiXZN7mAQ==", "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0" } }, + "ReactNative.Hermes.Windows": { + "type": "Transitive", + "resolved": "0.11.0-ms.6", + "contentHash": "WAVLsSZBV4p/3hNC3W67su7xu3f/ZMSKxu0ON7g2GaKRbkJmH0Qyif1IlzcJwtvR48kuOdfgPu7Bgtz3AY+gqg==" + }, "runtime.win10-arm.Microsoft.Net.Native.Compiler": { "type": "Transitive", "resolved": "2.2.7-rel-27913-00", @@ -135,8 +160,38 @@ "resolved": "2.2.9", "contentHash": "qF6RRZKaflI+LR1YODNyWYjq5YoX8IJ2wx5y8O+AW2xO+1t/Q6Mm+jQ38zJbWnmXbrcOqUYofn7Y3/KC6lTLBQ==" }, - "microsoft.reactnative": { + "common": { "type": "Project" + }, + "fmt": { + "type": "Project" + }, + "folly": { + "type": "Project", + "dependencies": { + "boost": "1.76.0", + "fmt": "1.0.0" + } + }, + "microsoft.reactnative": { + "type": "Project", + "dependencies": { + "Common": "1.0.0", + "Folly": "1.0.0", + "Microsoft.UI.Xaml": "2.7.0", + "Microsoft.Windows.CppWinRT": "2.0.211028.7", + "Microsoft.Windows.SDK.BuildTools": "10.0.22000.194", + "ReactCommon": "1.0.0", + "ReactNative.Hermes.Windows": "0.11.0-ms.6", + "boost": "1.76.0" + } + }, + "reactcommon": { + "type": "Project", + "dependencies": { + "Folly": "1.0.0", + "boost": "1.76.0" + } } }, "UAP,Version=v10.0.16299/win10-arm": { diff --git a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp index 4c1b23037f9..a2a2c9d3b36 100644 --- a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp +++ b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp @@ -46,7 +46,9 @@ std::vector GetCoreModules( std::vector modules; modules.emplace_back( - "Networking", []() { return Microsoft::React::CreateHttpModule(); }, jsMessageQueue); + "Networking", + [props = context->Properties()]() { return Microsoft::React::CreateHttpModule(props); }, + jsMessageQueue); modules.emplace_back( "Timing", diff --git a/vnext/Shared/CreateModules.h b/vnext/Shared/CreateModules.h index 1cfe1c7d594..535f26e0273 100644 --- a/vnext/Shared/CreateModules.h +++ b/vnext/Shared/CreateModules.h @@ -3,9 +3,14 @@ #pragma once +// React Native #include #include +// Windows API +#include + +// Standard Library #include // Forward declarations. Desktop projects can not access @@ -30,9 +35,19 @@ extern std::unique_ptr CreateTimingModule( namespace Microsoft::React { extern const char *GetHttpModuleName() noexcept; -extern std::unique_ptr CreateHttpModule() noexcept; +extern std::unique_ptr CreateHttpModule( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; extern const char *GetWebSocketModuleName() noexcept; -extern std::unique_ptr CreateWebSocketModule() noexcept; +extern std::unique_ptr CreateWebSocketModule( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; + +extern const char *GetBlobModuleName() noexcept; +extern std::unique_ptr CreateBlobModule( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; + +extern const char *GetFileReaderModuleName() noexcept; +extern std::unique_ptr CreateFileReaderModule( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/BlobModule.cpp b/vnext/Shared/Modules/BlobModule.cpp new file mode 100644 index 00000000000..b03b2e44269 --- /dev/null +++ b/vnext/Shared/Modules/BlobModule.cpp @@ -0,0 +1,376 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "BlobModule.h" + +#include +#include +#include +#include +#include + +// React Native +#include + +// Windows API +#include +#include + +// Standard Library +#include +#include +#include + +using namespace facebook::xplat; + +using folly::dynamic; +using std::scoped_lock; +using std::shared_ptr; +using std::string; +using std::vector; +using std::weak_ptr; +using winrt::Microsoft::ReactNative::IReactPropertyBag; +using winrt::Microsoft::ReactNative::ReactNonAbiValue; +using winrt::Microsoft::ReactNative::ReactPropertyBag; +using winrt::Microsoft::ReactNative::ReactPropertyId; +using winrt::Windows::Foundation::GuidHelper; +using winrt::Windows::Foundation::IInspectable; +using winrt::Windows::Foundation::Uri; +using winrt::Windows::Security::Cryptography::CryptographicBuffer; + +namespace fs = std::filesystem; + +namespace { +constexpr char moduleName[] = "BlobModule"; +constexpr char blobKey[] = "blob"; +constexpr char blobIdKey[] = "blobId"; +constexpr char offsetKey[] = "offset"; +constexpr char sizeKey[] = "size"; +constexpr char typeKey[] = "type"; +constexpr char dataKey[] = "data"; +} // namespace + +namespace Microsoft::React { + +#pragma region BlobModule + +BlobModule::BlobModule(winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept + : m_sharedState{std::make_shared()}, + m_blobPersistor{std::make_shared()}, + m_contentHandler{std::make_shared(m_blobPersistor)}, + m_requestBodyHandler{std::make_shared(m_blobPersistor)}, + m_responseHandler{std::make_shared(m_blobPersistor)}, + m_inspectableProperties{inspectableProperties} { + auto propBag = ReactPropertyBag{m_inspectableProperties.try_as()}; + + auto contentHandlerPropId = + ReactPropertyId>>{L"BlobModule.ContentHandler"}; + auto contentHandler = weak_ptr{m_contentHandler}; + propBag.Set(contentHandlerPropId, std::move(contentHandler)); + + auto blobPersistorPropId = ReactPropertyId>>{L"Blob.Persistor"}; + auto blobPersistor = weak_ptr{m_blobPersistor}; + propBag.Set(blobPersistorPropId, std::move(blobPersistor)); + + m_sharedState->Module = this; +} + +BlobModule::~BlobModule() noexcept /*override*/ { + m_sharedState->Module = nullptr; +} + +#pragma region CxxModule + +string BlobModule::getName() { + return moduleName; +} + +std::map BlobModule::getConstants() { + return {{"BLOB_URI_SCHEME", blobKey}, {"BLOB_URI_HOST", {}}}; +} + +vector BlobModule::getMethods() { + return { + {"addNetworkingHandler", + [propBag = ReactPropertyBag{m_inspectableProperties.try_as()}, + requestBodyHandler = m_requestBodyHandler, + responseHandler = m_responseHandler](dynamic args) { + auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; + + if (auto prop = propBag.Get(propId)) { + if (auto httpHandler = prop.Value().lock()) { + httpHandler->AddRequestBodyHandler(requestBodyHandler); + httpHandler->AddResponseHandler(responseHandler); + } + } + // TODO: else emit error? + }}, + + {"addWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Register(id); + }}, + + {"removeWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Unregister(id); + }}, + + {"sendOverSocket", + [weakState = weak_ptr(m_sharedState), + persistor = m_blobPersistor, + propBag = ReactPropertyBag{m_inspectableProperties.try_as()}](dynamic args) { + auto propId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; + shared_ptr wsProxy; + if (auto prop = propBag.Get(propId)) { + wsProxy = prop.Value().lock(); + } + if (!wsProxy) { + return; + } + + auto blob = jsArgAsObject(args, 0); + auto blobId = blob[blobIdKey].getString(); + auto offset = blob[offsetKey].getInt(); + auto size = blob[sizeKey].getInt(); + auto socketID = jsArgAsInt(args, 1); + + winrt::array_view data; + try { + data = persistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + } + return; + } + + auto buffer = CryptographicBuffer::CreateFromByteArray(data); + auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); + auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); + + wsProxy->SendBinary(std::move(base64String), socketID); + }}, + + {"createFromParts", + // As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. + // Use getInstance() directly once + // https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. + [persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { + auto parts = jsArgAsArray(args, 0); // Array + auto blobId = jsArgAsString(args, 1); + vector buffer{}; + + for (const auto &part : parts) { + auto type = part[typeKey].asString(); + if (blobKey == type) { + auto blob = part[dataKey]; + winrt::array_view bufferPart; + try { + bufferPart = persistor->ResolveMessage( + blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); + } catch (const std::exception &e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + } + return; + } + + buffer.reserve(buffer.size() + bufferPart.size()); + buffer.insert(buffer.end(), bufferPart.begin(), bufferPart.end()); + } else if ("string" == type) { + auto data = part[dataKey].asString(); + + buffer.reserve(buffer.size() + data.size()); + buffer.insert(buffer.end(), data.begin(), data.end()); + } else { + if (auto state = weakState.lock()) { + auto message = "Invalid type for blob: " + type; + Modules::SendEvent(state->Module->getInstance(), "blobFailed", std::move(message)); + } + return; + } + } + + persistor->StoreMessage(std::move(buffer), std::move(blobId)); + }}, + + {"release", + [persistor = m_blobPersistor](dynamic args) // blobId: string + { + auto blobId = jsArgAsString(args, 0); + + persistor->RemoveMessage(std::move(blobId)); + }}}; +} + +#pragma endregion CxxModule + +#pragma endregion BlobModule + +#pragma region MemoryBlobPersistor + +#pragma region IBlobPersistor + +winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&blobId, int64_t offset, int64_t size) { + if (size < 1) + return {}; + + scoped_lock lock{m_mutex}; + + auto dataItr = m_blobs.find(std::move(blobId)); + // Not found. + if (dataItr == m_blobs.cend()) + throw std::invalid_argument("Blob object not found"); + + auto &bytes = (*dataItr).second; + auto endBound = static_cast(offset + size); + // Out of bounds. + if (endBound > bytes.size() || offset >= static_cast(bytes.size()) || offset < 0) + throw std::out_of_range("Offset or size out of range"); + + return winrt::array_view(bytes.data() + offset, bytes.data() + endBound); +} + +void MemoryBlobPersistor::RemoveMessage(string &&blobId) noexcept { + scoped_lock lock{m_mutex}; + + m_blobs.erase(std::move(blobId)); +} + +void MemoryBlobPersistor::StoreMessage(vector &&message, string &&blobId) noexcept { + scoped_lock lock{m_mutex}; + + m_blobs.insert_or_assign(std::move(blobId), std::move(message)); +} + +string MemoryBlobPersistor::StoreMessage(vector &&message) noexcept { + // substr(1, 36) strips curly braces from a GUID. + auto blobId = winrt::to_string(winrt::to_hstring(GuidHelper::CreateNewGuid())).substr(1, 36); + + scoped_lock lock{m_mutex}; + m_blobs.insert_or_assign(blobId, std::move(message)); + + return blobId; +} + +#pragma endregion IBlobPersistor + +#pragma endregion MemoryBlobPersistor + +#pragma region BlobWebSocketModuleContentHandler + +BlobWebSocketModuleContentHandler::BlobWebSocketModuleContentHandler(shared_ptr blobPersistor) noexcept + : m_blobPersistor{blobPersistor} {} + +#pragma region IWebSocketModuleContentHandler + +void BlobWebSocketModuleContentHandler::ProcessMessage(string &&message, dynamic ¶ms) /*override*/ { + params[dataKey] = std::move(message); +} + +void BlobWebSocketModuleContentHandler::ProcessMessage(vector &&message, dynamic ¶ms) /*override*/ { + auto blob = dynamic::object(); + blob(offsetKey, 0); + blob(sizeKey, message.size()); + blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(message))); + + params[dataKey] = std::move(blob); + params[typeKey] = blobKey; +} +#pragma endregion IWebSocketModuleContentHandler + +void BlobWebSocketModuleContentHandler::Register(int64_t socketID) noexcept { + scoped_lock lock{m_mutex}; + m_socketIds.insert(socketID); +} + +void BlobWebSocketModuleContentHandler::Unregister(int64_t socketID) noexcept { + scoped_lock lock{m_mutex}; + + auto itr = m_socketIds.find(socketID); + if (itr != m_socketIds.end()) + m_socketIds.erase(itr); +} + +#pragma endregion BlobWebSocketModuleContentHandler + +#pragma region BlobModuleRequestBodyHandler + +BlobModuleRequestBodyHandler::BlobModuleRequestBodyHandler(shared_ptr blobPersistor) noexcept + : m_blobPersistor{blobPersistor} {} + +#pragma region IRequestBodyHandler + +bool BlobModuleRequestBodyHandler::Supports(dynamic &data) /*override*/ { + auto itr = data.find(blobKey); + + return itr != data.items().end() && !(*itr).second.empty(); +} + +dynamic BlobModuleRequestBodyHandler::ToRequestBody(dynamic &data, string &contentType) /*override*/ { + auto type = contentType; + if (!data[typeKey].isNull() && !data[typeKey].asString().empty()) { + type = data[typeKey].asString(); + } + if (type.empty()) { + type = "application/octet-stream"; + } + + auto blob = data[blobKey]; + auto blobId = blob[blobIdKey].asString(); + auto bytes = m_blobPersistor->ResolveMessage(std::move(blobId), blob[offsetKey].asInt(), blob[sizeKey].asInt()); + + auto result = dynamic::object(); + result(typeKey, type); + result(sizeKey, bytes.size()); + result("bytes", dynamic(bytes.cbegin(), bytes.cend())); + + return result; +} + +#pragma endregion IRequestBodyHandler + +#pragma endregion BlobModuleRequestBodyHandler + +#pragma region BlobModuleResponseHandler + +BlobModuleResponseHandler::BlobModuleResponseHandler(shared_ptr blobPersistor) noexcept + : m_blobPersistor{blobPersistor} {} + +#pragma region IResponseHandler + +bool BlobModuleResponseHandler::Supports(string &responseType) /*override*/ { + return blobKey == responseType; +} + +dynamic BlobModuleResponseHandler::ToResponseData(vector &&content) /*override*/ { + auto blob = dynamic::object(); + blob(offsetKey, 0); + blob(sizeKey, content.size()); + blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(content))); + + return blob; +} + +#pragma endregion IResponseHandler + +#pragma endregion BlobModuleResponseHandler + +/*extern*/ const char *GetBlobModuleName() noexcept { + return moduleName; +} + +/*extern*/ std::unique_ptr CreateBlobModule( + IInspectable const &inspectableProperties) noexcept { + if (auto properties = inspectableProperties.try_as()) + return std::make_unique(properties); + + return nullptr; +} + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/BlobModule.h b/vnext/Shared/Modules/BlobModule.h new file mode 100644 index 00000000000..06e9b0b4695 --- /dev/null +++ b/vnext/Shared/Modules/BlobModule.h @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include +#include + +// React Native +#include + +// Windows API +#include + +// Standard Library +#include +#include +#include +#include +#include + +namespace Microsoft::React { + +class MemoryBlobPersistor final : public IBlobPersistor { + std::unordered_map> m_blobs; + std::mutex m_mutex; + + public: +#pragma region IBlobPersistor + + winrt::array_view ResolveMessage(std::string &&blobId, int64_t offset, int64_t size) override; + + void RemoveMessage(std::string &&blobId) noexcept override; + + void StoreMessage(std::vector &&message, std::string &&blobId) noexcept override; + + std::string StoreMessage(std::vector &&message) noexcept override; + +#pragma endregion IBlobPersistor +}; + +class BlobWebSocketModuleContentHandler final : public IWebSocketModuleContentHandler { + std::unordered_set m_socketIds; + std::mutex m_mutex; + std::shared_ptr m_blobPersistor; + + public: + BlobWebSocketModuleContentHandler(std::shared_ptr blobPersistor) noexcept; + +#pragma region IWebSocketModuleContentHandler + + void ProcessMessage(std::string &&message, folly::dynamic ¶ms) override; + + void ProcessMessage(std::vector &&message, folly::dynamic ¶ms) override; + +#pragma endregion IWebSocketModuleContentHandler + + void Register(int64_t socketID) noexcept; + + void Unregister(int64_t socketID) noexcept; +}; + +class BlobModuleRequestBodyHandler final : public IRequestBodyHandler { + std::shared_ptr m_blobPersistor; + + public: + BlobModuleRequestBodyHandler(std::shared_ptr blobPersistor) noexcept; + +#pragma region IRequestBodyHandler + + bool Supports(folly::dynamic &data) override; + + folly::dynamic ToRequestBody(folly::dynamic &data, std::string &contentType) override; + +#pragma endregion IRequestBodyHandler +}; + +class BlobModuleResponseHandler final : public IResponseHandler { + std::shared_ptr m_blobPersistor; + + public: + BlobModuleResponseHandler(std::shared_ptr blobPersistor) noexcept; + +#pragma region IResponseHandler + + bool Supports(std::string &responseType) override; + + folly::dynamic ToResponseData(std::vector &&content) override; + +#pragma endregion IResponseHandler +}; + +class BlobModule : public facebook::xplat::module::CxxModule { + std::shared_ptr m_blobPersistor; + std::shared_ptr m_contentHandler; + std::shared_ptr m_requestBodyHandler; + std::shared_ptr m_responseHandler; + + // Property bag high level reference. + winrt::Windows::Foundation::IInspectable m_inspectableProperties; + + public: + enum class MethodId { + AddNetworkingHandler = 0, + AddWebSocketHandler = 1, + RemoveWebSocketHandler = 2, + SendOverSocket = 3, + CreateFromParts = 4, + Release = 5, + SIZE = 6 + }; + + BlobModule(winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; + + ~BlobModule() noexcept override; + + struct SharedState { + /// + /// Keeps a raw reference to the module object to lazily retrieve the React Instance as needed. + /// + CxxModule *Module{nullptr}; + }; + +#pragma region CxxModule + + /// + /// + /// + std::string getName() override; + + /// + /// + /// + std::map getConstants() override; + + /// + /// + /// + /// See See react-native/Libraries/WebSocket/WebSocket.js + std::vector getMethods() override; + +#pragma endregion CxxModule + + private: + /// + /// Keeps members that can be accessed threads other than this module's owner accessible. + /// + std::shared_ptr m_sharedState; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/CxxModuleUtilities.cpp b/vnext/Shared/Modules/CxxModuleUtilities.cpp new file mode 100644 index 00000000000..45c48dc7405 --- /dev/null +++ b/vnext/Shared/Modules/CxxModuleUtilities.cpp @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "CxxModuleUtilities.h" + +using facebook::react::Instance; +using folly::dynamic; +using std::string; +using std::weak_ptr; + +namespace Microsoft::React::Modules { + +void SendEvent(weak_ptr weakReactInstance, string &&eventName, dynamic &&args) { + if (auto instance = weakReactInstance.lock()) { + instance->callJSFunction("RCTDeviceEventEmitter", "emit", dynamic::array(std::move(eventName), std::move(args))); + } +} + +} // namespace Microsoft::React::Modules diff --git a/vnext/Shared/Modules/CxxModuleUtilities.h b/vnext/Shared/Modules/CxxModuleUtilities.h new file mode 100644 index 00000000000..00c869a953e --- /dev/null +++ b/vnext/Shared/Modules/CxxModuleUtilities.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +// Folly +#include + +// React Native +#include + +// Standard Library +#include +#include + +namespace Microsoft::React::Modules { + +void SendEvent( + std::weak_ptr weakReactInstance, + std::string &&eventName, + folly::dynamic &&args); + +} // namespace Microsoft::React::Modules diff --git a/vnext/Shared/Modules/FileReaderModule.cpp b/vnext/Shared/Modules/FileReaderModule.cpp new file mode 100644 index 00000000000..a23328c0e15 --- /dev/null +++ b/vnext/Shared/Modules/FileReaderModule.cpp @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "FileReaderModule.h" + +#include + +// Boost Library +#include +#include +#include + +// React Native +#include + +// Windows API +#include + +using namespace facebook::xplat; + +using folly::dynamic; +using std::string; +using std::weak_ptr; +using winrt::Microsoft::ReactNative::IReactPropertyBag; +using winrt::Microsoft::ReactNative::ReactNonAbiValue; +using winrt::Microsoft::ReactNative::ReactPropertyBag; +using winrt::Microsoft::ReactNative::ReactPropertyId; +using winrt::Windows::Foundation::IInspectable; + +namespace { +constexpr char moduleName[] = "FileReaderModule"; +} // namespace + +namespace Microsoft::React { + +#pragma region FileReaderModule + +FileReaderModule::FileReaderModule(weak_ptr weakBlobPersistor) noexcept + : m_weakBlobPersistor{weakBlobPersistor} {} + +FileReaderModule::~FileReaderModule() noexcept /*override*/ +{} + +#pragma region CxxModule + +string FileReaderModule::getName() { + return moduleName; +} + +std::map FileReaderModule::getConstants() { + return {}; +} + +std::vector FileReaderModule::getMethods() { + return { + {/// + /// + /// Array of arguments passed from the JavaScript layer. + /// [0] - dynamic blob object { blobId, offset, size[, type] } + /// + /// + "readAsDataURL", + [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { + if (!blobPersistor) { + return reject({"Could not find Blob persistor"}); + } + + auto blob = jsArgAsObject(args, 0); + + auto blobId = blob["blobId"].asString(); + auto offset = blob["offset"].asInt(); + auto size = blob["size"].asInt(); + + winrt::array_view bytes; + try { + bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + return reject({e.what()}); + } + + auto result = string{"data:"}; + auto typeItr = blob.find("type"); + if (typeItr == blob.items().end()) { + result += "application/octet-stream"; + } else { + result += (*typeItr).second.asString(); + } + result += ";base64,"; + + // https://www.boost.org/doc/libs/1_76_0/libs/serialization/doc/dataflow.html + using namespace boost::archive::iterators; + typedef base64_from_binary> encode_base64; + std::ostringstream oss; + std::copy(encode_base64(bytes.cbegin()), encode_base64(bytes.cend()), ostream_iterator(oss)); + result += oss.str(); + + resolve({std::move(result)}); + }}, + {/// + /// + /// Array of arguments passed from the JavaScript layer. + /// [0] - dynamic blob object { blobId, offset, size } + /// [1] - string encoding + /// + /// + "readAsText", + [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { + if (!blobPersistor) { + return reject({"Could not find Blob persistor"}); + } + + auto blob = jsArgAsObject(args, 0); + auto encoding = jsArgAsString(args, 1); // Default: "UTF-8" + + auto blobId = blob["blobId"].asString(); + auto offset = blob["offset"].asInt(); + auto size = blob["size"].asInt(); + + winrt::array_view bytes; + try { + bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + return reject({e.what()}); + } + + // #9982 - Handle non-UTF8 encodings + // See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/charset/Charset.html + auto result = string{bytes.cbegin(), bytes.cend()}; + + resolve({std::move(result)}); + }}}; +} + +#pragma endregion CxxModule + +#pragma endregion FileReaderModule + +/*extern*/ const char *GetFileReaderModuleName() noexcept { + return moduleName; +} + +/*extern*/ std::unique_ptr CreateFileReaderModule( + IInspectable const &inspectableProperties) noexcept { + auto propId = ReactPropertyId>>{L"Blob.Persistor"}; + auto propBag = ReactPropertyBag{inspectableProperties.try_as()}; + + if (auto prop = propBag.Get(propId)) { + auto weakBlobPersistor = prop.Value(); + + return std::make_unique(weakBlobPersistor); + } + + return nullptr; +} + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/FileReaderModule.h b/vnext/Shared/Modules/FileReaderModule.h new file mode 100644 index 00000000000..bde28680a45 --- /dev/null +++ b/vnext/Shared/Modules/FileReaderModule.h @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "IBlobPersistor.h" + +// React Native +#include + +// Folly +#include + +// Standard Library +#include +#include +#include +#include + +namespace Microsoft::React { + +class FileReaderModule : public facebook::xplat::module::CxxModule { + public: + enum class MethodId { ReadAsDataURL = 0, ReadAsText = 1, SIZE = 2 }; + + FileReaderModule(std::weak_ptr weakBlobPersistor) noexcept; + + ~FileReaderModule() noexcept override; + +#pragma region CxxModule + + /// + /// + /// + std::string getName() override; + + /// + /// + /// + std::map getConstants() override; + + /// + /// + /// + /// See See react-native/Libraries/WebSocket/WebSocket.js + std::vector getMethods() override; + +#pragma endregion CxxModule + + private: + std::weak_ptr m_weakBlobPersistor; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/HttpModule.cpp b/vnext/Shared/Modules/HttpModule.cpp index 61e57a724cd..0c9f2947af2 100644 --- a/vnext/Shared/Modules/HttpModule.cpp +++ b/vnext/Shared/Modules/HttpModule.cpp @@ -5,6 +5,9 @@ #include "HttpModule.h" +#include +#include + // React Native #include #include @@ -14,21 +17,34 @@ using folly::dynamic; using std::shared_ptr; using std::string; using std::weak_ptr; +using winrt::Microsoft::ReactNative::IReactPropertyBag; +using winrt::Microsoft::ReactNative::ReactNonAbiValue; +using winrt::Microsoft::ReactNative::ReactPropertyBag; +using winrt::Microsoft::ReactNative::ReactPropertyId; +using winrt::Windows::Foundation::IInspectable; namespace { +using Microsoft::React::Modules::SendEvent; using Microsoft::React::Networking::IHttpResource; constexpr char moduleName[] = "Networking"; -static void SendEvent(weak_ptr weakReactInstance, string &&eventName, dynamic &&args) { - if (auto instance = weakReactInstance.lock()) { - instance->callJSFunction("RCTDeviceEventEmitter", "emit", dynamic::array(std::move(eventName), std::move(args))); - } -} +// React event names +constexpr char completedResponse[] = "didCompleteNetworkResponse"; +constexpr char receivedResponse[] = "didReceiveNetworkResponse"; +constexpr char receivedData[] = "didReceiveNetworkData"; +constexpr char receivedDataProgress[] = "didReceiveNetworkDataProgress"; -static shared_ptr CreateHttpResource(weak_ptr weakReactInstance) { - auto resource = IHttpResource::Make(); +static void SetUpHttpResource( + shared_ptr resource, + weak_ptr weakReactInstance, + IInspectable &inspectableProperties) { + resource->SetOnRequestSuccess([weakReactInstance](int64_t requestId) { + auto args = dynamic::array(requestId); + + SendEvent(weakReactInstance, completedResponse, std::move(args)); + }); resource->SetOnResponse([weakReactInstance](int64_t requestId, IHttpResource::Response &&response) { dynamic headers = dynamic::object(); @@ -39,33 +55,39 @@ static shared_ptr CreateHttpResource(weak_ptr weakReact // TODO: Test response content. dynamic args = dynamic::array(requestId, response.StatusCode, headers, response.Url); - SendEvent(weakReactInstance, "didReceiveNetworkResponse", std::move(args)); + SendEvent(weakReactInstance, receivedResponse, std::move(args)); }); - resource->SetOnData([weakReactInstance](int64_t requestId, std::string &&responseData) { - dynamic args = dynamic::array(requestId, std::move(responseData)); - - SendEvent(weakReactInstance, "didReceiveNetworkData", std::move(args)); + resource->SetOnData([weakReactInstance](int64_t requestId, string &&responseData) { + SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); // TODO: Move into separate method IF not executed right after onData() - SendEvent(weakReactInstance, "didCompleteNetworkResponse", dynamic::array(requestId)); + SendEvent(weakReactInstance, completedResponse, dynamic::array(requestId)); }); + // Explicitly declaring function type to avoid type inference ambiguity. + std::function onDataDynamic = [weakReactInstance]( + int64_t requestId, dynamic &&responseData) { + SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); + }; + resource->SetOnData(std::move(onDataDynamic)); + resource->SetOnError([weakReactInstance](int64_t requestId, string &&message) { dynamic args = dynamic::array(requestId, std::move(message)); // TODO: isTimeout errorArgs.push_back(true); - SendEvent(weakReactInstance, "didCompleteNetworkResponse", std::move(args)); + SendEvent(weakReactInstance, completedResponse, std::move(args)); }); - - return resource; } } // namespace namespace Microsoft::React { -HttpModule::HttpModule() noexcept : m_holder{std::make_shared()} { +HttpModule::HttpModule(IInspectable const &inspectableProperties) noexcept + : m_holder{std::make_shared()}, + m_inspectableProperties{inspectableProperties}, + m_resource{IHttpResource::Make(inspectableProperties)} { m_holder->Module = this; } @@ -86,10 +108,6 @@ std::map HttpModule::getConstants() { // clang-format off std::vector HttpModule::getMethods() { - auto weakHolder = weak_ptr(m_holder); - auto holder = weakHolder.lock(); - auto weakReactInstance = weak_ptr(holder->Module->getInstance()); - return { { @@ -102,53 +120,32 @@ std::vector HttpModule::getMethods() } auto resource = holder->Module->m_resource; - if (resource || (resource = CreateHttpResource(holder->Module->getInstance()))) + if (!holder->Module->m_isResourceSetup) { - IHttpResource::BodyData bodyData; - auto params = facebook::xplat::jsArgAsObject(args, 0); - auto data = params["data"]; - auto stringData = data["string"]; - if (!stringData.empty()) - { - bodyData = {IHttpResource::BodyData::Type::String, stringData.getString()}; - } - else - { - auto base64Data = data["base64"]; - if (!base64Data.empty()) - { - bodyData = {IHttpResource::BodyData::Type::Base64, base64Data.getString()}; - } - else - { - auto uriData = data["uri"]; - if (!uriData.empty()) - { - bodyData = {IHttpResource::BodyData::Type::Uri, uriData.getString()}; - } - } - } - //TODO: Support FORM data + SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties); + holder->Module->m_isResourceSetup = true; + } - IHttpResource::Headers headers; - for (auto& header : params["headers"].items()) { - headers.emplace(header.first.getString(), header.second.getString()); - } + auto params = facebook::xplat::jsArgAsObject(args, 0); + IHttpResource::Headers headers; + for (auto& header : params["headers"].items()) { + headers.emplace(header.first.getString(), header.second.getString()); + } - resource->SendRequest( - params["method"].asString(), - params["url"].asString(), - std::move(headers), - std::move(bodyData), - params["responseType"].asString(), - params["incrementalUpdates"].asBool(), - static_cast(params["timeout"].asDouble()), - false,//withCredentials, - [cxxCallback = std::move(cxxCallback)](int64_t requestId) { - cxxCallback({requestId}); - } - ); - } // If resource available + resource->SendRequest( + params["method"].asString(), + params["url"].asString(), + params["requestId"].asInt(), + std::move(headers), + std::move(params["data"]), + params["responseType"].asString(), + params["incrementalUpdates"].asBool(), + static_cast(params["timeout"].asDouble()), + params["withCredentials"].asBool(), + [cxxCallback = std::move(cxxCallback)](int64_t requestId) { + cxxCallback({requestId}); + } + ); } }, { @@ -162,10 +159,13 @@ std::vector HttpModule::getMethods() } auto resource = holder->Module->m_resource; - if (resource || (resource = CreateHttpResource(holder->Module->getInstance()))) + if (!holder->Module->m_isResourceSetup) { - resource->AbortRequest(facebook::xplat::jsArgAsInt(args, 0)); + SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties); + holder->Module->m_isResourceSetup = true; } + + resource->AbortRequest(facebook::xplat::jsArgAsInt(args, 0)); } }, { @@ -179,10 +179,13 @@ std::vector HttpModule::getMethods() } auto resource = holder->Module->m_resource; - if (resource || (resource = CreateHttpResource(holder->Module->getInstance()))) + if (!holder->Module->m_isResourceSetup) { - resource->ClearCookies(); + SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties); + holder->Module->m_isResourceSetup = true; } + + resource->ClearCookies(); } } }; diff --git a/vnext/Shared/Modules/HttpModule.h b/vnext/Shared/Modules/HttpModule.h index cb870835547..103818c03fe 100644 --- a/vnext/Shared/Modules/HttpModule.h +++ b/vnext/Shared/Modules/HttpModule.h @@ -8,6 +8,9 @@ // React Native #include +// Windows API +#include + namespace Microsoft::React { /// @@ -18,7 +21,7 @@ class HttpModule : public facebook::xplat::module::CxxModule { public: enum MethodId { SendRequest = 0, AbortRequest = 1, ClearCookies = 2, LAST = ClearCookies }; - HttpModule() noexcept; + HttpModule(winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; ~HttpModule() noexcept override; @@ -49,5 +52,9 @@ class HttpModule : public facebook::xplat::module::CxxModule { std::shared_ptr m_resource; std::shared_ptr m_holder; + bool m_isResourceSetup{false}; + + // Property bag high level reference. + winrt::Windows::Foundation::IInspectable m_inspectableProperties; }; } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/IBlobPersistor.h b/vnext/Shared/Modules/IBlobPersistor.h new file mode 100644 index 00000000000..b1035fc461e --- /dev/null +++ b/vnext/Shared/Modules/IBlobPersistor.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +// Windows API +#include + +// Standard Library +#include +#include + +namespace Microsoft::React { + +struct IBlobPersistor { + /// + /// + /// When an entry for blobId cannot be found. + /// + /// + virtual winrt::array_view ResolveMessage(std::string &&blobId, int64_t offset, int64_t size) = 0; + + virtual void RemoveMessage(std::string &&blobId) noexcept = 0; + + virtual void StoreMessage(std::vector &&message, std::string &&blobId) noexcept = 0; + + virtual std::string StoreMessage(std::vector &&message) noexcept = 0; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/IHttpModuleProxy.h b/vnext/Shared/Modules/IHttpModuleProxy.h new file mode 100644 index 00000000000..0c0f3c9b381 --- /dev/null +++ b/vnext/Shared/Modules/IHttpModuleProxy.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "IRequestBodyHandler.h" +#include "IResponseHandler.h" +#include "IUriHandler.h" + +// Standard Library +#include + +namespace Microsoft::React { + +/// +/// Provides partial access to HttpModule methods directly to other native modules +/// without switching to the JavaScript queue thread. +/// +struct IHttpModuleProxy { + virtual ~IHttpModuleProxy() noexcept {} + + // TODO: Implement custom URI handlers. + virtual void AddUriHandler(std::shared_ptr uriHandler) noexcept = 0; + + virtual void AddRequestBodyHandler(std::shared_ptr requestBodyHandler) noexcept = 0; + + virtual void AddResponseHandler(std::shared_ptr responseHandler) noexcept = 0; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/IRequestBodyHandler.h b/vnext/Shared/Modules/IRequestBodyHandler.h new file mode 100644 index 00000000000..34fe0d3405c --- /dev/null +++ b/vnext/Shared/Modules/IRequestBodyHandler.h @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +// Folly +#include + +// Standard Library +#include + +namespace Microsoft::React { + +/// +/// Allows adding custom handling to build the {@link RequestBody} from the JS body payload. +/// +struct IRequestBodyHandler { + /// + /// Returns if the handler should be used for a JS body payload. + /// + /// + /// folly object potentially containing a blob reference. + /// "blob" - folly object holding blob metadata. Optional. + /// + /// + /// true - contains a blob reference. + /// false - does not contain a blob reference. + /// + virtual bool Supports(folly::dynamic &data) = 0; + + /// + /// Returns the {@link RequestBody} for the JS body payload. + /// + /// + /// Incoming folly object containing the blob metadada. + /// Structure: + /// "blob" - folly object info + /// "blobId" - Blob unique identifier + /// "offset" - Start index to read the blob + /// "size" - Amount of bytes to read from the blob + /// + /// + /// folly::dynamic object with the following entries: + /// "type" - Request content type + /// "size" - Amount of bytes + /// "bytes" - Raw body content + /// NOTE: This is an arbitrary key. Pending non-folly structured object to model request body. + /// + virtual folly::dynamic ToRequestBody(folly::dynamic &data, std::string &contentType) = 0; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/IResponseHandler.h b/vnext/Shared/Modules/IResponseHandler.h new file mode 100644 index 00000000000..abbc78f7e60 --- /dev/null +++ b/vnext/Shared/Modules/IResponseHandler.h @@ -0,0 +1,27 @@ +#pragma once + +// Folly +#include + +// Standard Library +#include +#include + +namespace Microsoft::React { + +/// +/// Allows adding custom handling to build the JS body payload from the {@link ResponseBody}. +/// +struct IResponseHandler { + /// + /// Returns if the handler should be used for a response type. + /// + virtual bool Supports(std::string &responseType) = 0; + + /// + /// Returns the JS body payload for the {@link ResponseBody}. + /// + virtual folly::dynamic ToResponseData(std::vector &&content) = 0; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/IUriHandler.h b/vnext/Shared/Modules/IUriHandler.h new file mode 100644 index 00000000000..fa96c251cef --- /dev/null +++ b/vnext/Shared/Modules/IUriHandler.h @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +// Folly +#include + +// Standard Library +#include + +namespace Microsoft::React { + +/// +/// Allows to implement a custom fetching process for specific URIs. It is the handler's job to +/// fetch the URI and return the JS body payload. +/// +struct IUriHandler { + /// + /// Returns whether the handler should be used for a given URI. + /// + virtual bool Supports(std::string &uri, std::string &responseType) = 0; + + /// + /// Fetch the URI and return the JS body payload. + /// + /// + /// Blob representation in a dynamic object with the folliwing structure: + /// "blobId" - Blob unique identifier + /// "offset" - Blob segment starting offset + /// "size" - Number of bytes fetched from blob + /// "name" - File name obtained from the URI + /// "lastModified - Last write to local file in milliseconds + virtual folly::dynamic Fetch(std::string &uri) = 0; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/IWebSocketModuleContentHandler.h b/vnext/Shared/Modules/IWebSocketModuleContentHandler.h new file mode 100644 index 00000000000..97c06f158f9 --- /dev/null +++ b/vnext/Shared/Modules/IWebSocketModuleContentHandler.h @@ -0,0 +1,26 @@ +// 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 { + virtual ~IWebSocketModuleContentHandler() noexcept {} + + virtual void ProcessMessage(std::string &&message, folly::dynamic ¶ms) = 0; + + virtual void ProcessMessage(std::vector &&message, folly::dynamic ¶ms) = 0; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/IWebSocketModuleProxy.h b/vnext/Shared/Modules/IWebSocketModuleProxy.h new file mode 100644 index 00000000000..61320097d8f --- /dev/null +++ b/vnext/Shared/Modules/IWebSocketModuleProxy.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +// Standard Library +#include +#include + +namespace Microsoft::React { + +/// +/// Provides partial access to WebSocketModule methods directly to other native modules +/// without switching to the JavaScript queue thread. +/// +struct IWebSocketModuleProxy { + virtual ~IWebSocketModuleProxy() noexcept {} + + virtual void SendBinary(std::string &&base64String, int64_t id) noexcept = 0; +}; + +} // namespace Microsoft::React diff --git a/vnext/Shared/Modules/NetworkingModule.cpp b/vnext/Shared/Modules/NetworkingModule.cpp index c2006257804..3bdceb93ca6 100644 --- a/vnext/Shared/Modules/NetworkingModule.cpp +++ b/vnext/Shared/Modules/NetworkingModule.cpp @@ -261,7 +261,7 @@ void NetworkingModule::NetworkingHelper::SendRequest( int64_t requestId = ++s_lastRequestId; // Enforce supported args - assert(responseType == "text" || responseType == "base64"); + assert(responseType == "text" || responseType == "base64" || responseType == "blob"); // Callback with the requestId cb({requestId}); diff --git a/vnext/Shared/Modules/WebSocketModule.cpp b/vnext/Shared/Modules/WebSocketModule.cpp index 3c9a974df20..2ccb180e26e 100644 --- a/vnext/Shared/Modules/WebSocketModule.cpp +++ b/vnext/Shared/Modules/WebSocketModule.cpp @@ -5,10 +5,16 @@ #include -#include +#include +#include +#include + +// React Native #include #include -#include "Unicode.h" + +// Windows API +#include // Standard Libriary #include @@ -18,25 +24,26 @@ using namespace facebook::xplat; using facebook::react::Instance; using folly::dynamic; -using Microsoft::Common::Unicode::Utf16ToUtf8; -using Microsoft::Common::Unicode::Utf8ToUtf16; - using std::shared_ptr; using std::string; using std::weak_ptr; +using winrt::Microsoft::ReactNative::IReactPropertyBag; +using winrt::Microsoft::ReactNative::ReactNonAbiValue; +using winrt::Microsoft::ReactNative::ReactPropertyBag; +using winrt::Microsoft::ReactNative::ReactPropertyId; + +using winrt::Windows::Foundation::IInspectable; +using winrt::Windows::Security::Cryptography::CryptographicBuffer; + namespace { +using Microsoft::React::IWebSocketModuleProxy; using Microsoft::React::WebSocketModule; +using Microsoft::React::Modules::SendEvent; using Microsoft::React::Networking::IWebSocketResource; constexpr char moduleName[] = "WebSocketModule"; -static void SendEvent(weak_ptr weakInstance, string &&eventName, dynamic &&args) { - if (auto instance = weakInstance.lock()) { - instance->callJSFunction("RCTDeviceEventEmitter", "emit", dynamic::array(std::move(eventName), std::move(args))); - } -} - static shared_ptr GetOrCreateWebSocket(int64_t id, string &&url, weak_ptr weakState) { auto state = weakState.lock(); @@ -91,14 +98,37 @@ GetOrCreateWebSocket(int64_t id, string &&url, weak_ptrSetOnMessage([id, weakInstance](size_t length, const string &message, bool isBinary) { - auto strongInstance = weakInstance.lock(); - if (!strongInstance) - return; + ws->SetOnMessage( + [id, weakInstance, propBag = ReactPropertyBag{state->InspectableProps.try_as()}]( + size_t length, const string &message, bool isBinary) { + auto strongInstance = weakInstance.lock(); + if (!strongInstance) + return; + + dynamic args = dynamic::object("id", id)("type", isBinary ? "binary" : "text"); + shared_ptr contentHandler; + auto propId = ReactPropertyId>>{ + L"BlobModule.ContentHandler"}; + if (auto prop = propBag.Get(propId)) + contentHandler = prop.Value().lock(); + + if (contentHandler) { + if (isBinary) { + auto buffer = CryptographicBuffer::DecodeFromBase64String(winrt::to_hstring(message)); + winrt::com_array arr; + CryptographicBuffer::CopyToByteArray(buffer, arr); + auto data = std::vector(arr.begin(), arr.end()); + + contentHandler->ProcessMessage(std::move(data), args); + } else { + contentHandler->ProcessMessage(string{message}, args); + } + } else { + args["data"] = message; + } - auto args = dynamic::object("id", id)("data", message)("type", isBinary ? "binary" : "text"); - SendEvent(weakInstance, "websocketMessage", std::move(args)); - }); + SendEvent(weakInstance, "websocketMessage", std::move(args)); + }); ws->SetOnClose([id, weakInstance](IWebSocketResource::CloseCode code, const string &reason) { auto strongInstance = weakInstance.lock(); if (!strongInstance) @@ -119,9 +149,24 @@ GetOrCreateWebSocket(int64_t id, string &&url, weak_ptr()} { +#pragma region WebSocketModule + +WebSocketModule::WebSocketModule(winrt::Windows::Foundation::IInspectable const &inspectableProperties) + : m_sharedState{std::make_shared()}, + m_proxy{std::make_shared(inspectableProperties)} { m_sharedState->ResourceFactory = [](string &&url) { return IWebSocketResource::Make(); }; m_sharedState->Module = this; + m_sharedState->InspectableProps = inspectableProperties; + + auto propBag = ReactPropertyBag{m_sharedState->InspectableProps.try_as()}; + + auto proxyPropId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; + auto proxy = weak_ptr{m_proxy}; + propBag.Set(proxyPropId, std::move(proxy)); + + auto statePropId = ReactPropertyId>>{L"WebSocketModule.SharedState"}; + auto state = weak_ptr{m_sharedState}; + propBag.Set(statePropId, std::move(state)); } WebSocketModule::~WebSocketModule() noexcept /*override*/ { @@ -167,7 +212,7 @@ std::vector WebSocketModule::getMeth const auto& headersDynamic = optionsDynamic["headers"]; for (const auto& header : headersDynamic.items()) { - options.emplace(Utf8ToUtf16(header.first.getString()), header.second.getString()); + options.emplace(winrt::to_hstring(header.first.getString()), header.second.getString()); } } @@ -246,12 +291,37 @@ std::vector WebSocketModule::getMeth } // getMethods // clang-format on +#pragma endregion WebSocketModule + +#pragma region WebSocketModuleProxy + +WebSocketModuleProxy::WebSocketModuleProxy(IInspectable const &inspectableProperties) noexcept + : m_inspectableProps{inspectableProperties} {} + +void WebSocketModuleProxy::SendBinary(std::string &&base64String, int64_t id) noexcept /*override*/ { + auto propBag = ReactPropertyBag{m_inspectableProps.try_as()}; + auto sharedPropId = + ReactPropertyId>>{L"WebSocketModule.SharedState"}; + auto state = propBag.Get(sharedPropId).Value(); + + weak_ptr weakWs = GetOrCreateWebSocket(id, {}, std::move(state)); + if (auto sharedWs = weakWs.lock()) { + sharedWs->SendBinary(std::move(base64String)); + } +} + +#pragma endregion WebSocketModuleProxy + /*extern*/ const char *GetWebSocketModuleName() noexcept { return moduleName; } -/*extern*/ std::unique_ptr CreateWebSocketModule() noexcept { - return std::make_unique(); +/*extern*/ std::unique_ptr CreateWebSocketModule( + IInspectable const &inspectableProperties) noexcept { + if (auto properties = inspectableProperties.try_as()) + return std::make_unique(properties); + + return nullptr; } } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/WebSocketModule.h b/vnext/Shared/Modules/WebSocketModule.h index 07030752b52..5a10ac5990a 100644 --- a/vnext/Shared/Modules/WebSocketModule.h +++ b/vnext/Shared/Modules/WebSocketModule.h @@ -3,13 +3,31 @@ #pragma once +#include #include // React Native #include +// Windows API +#include + namespace Microsoft::React { +class WebSocketModuleProxy final : public IWebSocketModuleProxy { + // Property bag high level reference. + winrt::Windows::Foundation::IInspectable m_inspectableProps; + + public: + WebSocketModuleProxy(winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; + +#pragma region IWebSocketModuleProxy + + void SendBinary(std::string &&base64String, int64_t id) noexcept override; + +#pragma endregion +}; + /// /// Realizes NativeModules projection. /// See react-native/Libraries/WebSocket/WebSocket.js @@ -18,7 +36,7 @@ class WebSocketModule : public facebook::xplat::module::CxxModule { public: enum MethodId { Connect = 0, Close = 1, Send = 2, SendBinary = 3, Ping = 4, SIZE = 5 }; - WebSocketModule(); + WebSocketModule(winrt::Windows::Foundation::IInspectable const &inspectableProperties); ~WebSocketModule() noexcept override; @@ -38,6 +56,9 @@ class WebSocketModule : public facebook::xplat::module::CxxModule { /// Keeps a raw reference to the module object to lazily retrieve the React Instance as needed. /// CxxModule *Module{nullptr}; + + // Property bag high level reference. + winrt::Windows::Foundation::IInspectable InspectableProps; }; #pragma region CxxModule overrides @@ -74,6 +95,11 @@ class WebSocketModule : public facebook::xplat::module::CxxModule { /// Keeps members that can be accessed threads other than this module's owner accessible. /// std::shared_ptr m_sharedState; + + /// + /// Exposes a subset of the module's methods. + /// + std::shared_ptr m_proxy; }; } // namespace Microsoft::React diff --git a/vnext/Shared/Networking/IHttpResource.h b/vnext/Shared/Networking/IHttpResource.h index 2e999bb5755..7a8434e9f6f 100644 --- a/vnext/Shared/Networking/IHttpResource.h +++ b/vnext/Shared/Networking/IHttpResource.h @@ -3,6 +3,12 @@ #pragma once +// Folly +#include + +// Windows API +#include + // Standard Library #include #include @@ -14,6 +20,7 @@ namespace Microsoft::React::Networking { struct IHttpResource { typedef std::unordered_map Headers; + // TODO: Implement Form data struct BodyData { enum class Type : size_t { Empty, String, Base64, Uri, Form } Type = Type::Empty; std::string Data; @@ -27,13 +34,53 @@ struct IHttpResource { static std::shared_ptr Make() noexcept; + static std::shared_ptr Make( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; + virtual ~IHttpResource() noexcept {} + /// + /// Initiates an HTTP request. + /// + /// + /// HTTP verb to send in de request. + /// GET | POST | PUT | DELETE | OPTIONS + /// + /// + /// Server/service remote endpoint to send the request to. + /// + /// + /// Request unique identifier. + /// + /// + /// HTTP request header map. + /// + /// + /// Dynamic map containing request payload. + /// The payload may be an empty request body or one of the following: + /// "string" - UTF-8 string payload + /// "base64" - Base64-encoded data string + /// "uri" - URI data reference + /// "form" - Form-encoded data + /// + /// + /// text | binary | blob + /// + /// + /// Response body to be retrieved in several iterations. + /// + /// + /// Request timeout in miliseconds. + /// + /// + /// Allow including credentials in request. + /// virtual void SendRequest( std::string &&method, std::string &&url, + int64_t requestId, Headers &&headers, - BodyData &&bodyData, + folly::dynamic &&data, std::string &&responseType, bool useIncrementalUpdates, int64_t timeout, @@ -43,9 +90,10 @@ struct IHttpResource { virtual void ClearCookies() noexcept = 0; - virtual void SetOnRequest(std::function &&handler) noexcept = 0; + virtual void SetOnRequestSuccess(std::function &&handler) noexcept = 0; virtual void SetOnResponse(std::function &&handler) noexcept = 0; virtual void SetOnData(std::function &&handler) noexcept = 0; + virtual void SetOnData(std::function &&handler) noexcept = 0; virtual void SetOnError( std::function &&handler) noexcept = 0; }; diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index aa3ed5e853e..388d75e8c89 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -4,6 +4,7 @@ #include "WinRTHttpResource.h" #include +#include #include #include #include @@ -17,10 +18,14 @@ #include #include +using folly::dynamic; + using std::function; using std::scoped_lock; using std::shared_ptr; using std::string; +using std::vector; +using std::weak_ptr; using winrt::fire_and_forget; using winrt::hresult_error; @@ -45,29 +50,25 @@ namespace Microsoft::React::Networking { #pragma region WinRTHttpResource -// TODO: Check for multi-thread issues if there are multiple instances. -/*static*/ int64_t WinRTHttpResource::s_lastRequestId = 0; - WinRTHttpResource::WinRTHttpResource(IHttpClient &&client) noexcept : m_client{std::move(client)} {} -WinRTHttpResource::WinRTHttpResource() noexcept : WinRTHttpResource(winrt::Windows::Web::Http::HttpClient()) {} +WinRTHttpResource::WinRTHttpResource() noexcept : WinRTHttpResource(winrt::Windows::Web::Http::HttpClient{}) {} #pragma region IHttpResource void WinRTHttpResource::SendRequest( string &&method, string &&url, + int64_t requestId, Headers &&headers, - BodyData &&bodyData, + dynamic &&data, string &&responseType, bool useIncrementalUpdates, int64_t timeout, bool withCredentials, std::function &&callback) noexcept /*override*/ { - auto requestId = ++s_lastRequestId; - // Enforce supported args - assert(responseType == "text" || responseType == "base64"); + assert(responseType == "text" || responseType == "base64" | responseType == "blob"); if (callback) { callback(requestId); @@ -82,10 +83,10 @@ void WinRTHttpResource::SendRequest( auto concreteArgs = args.as(); concreteArgs->RequestId = requestId; concreteArgs->Headers = std::move(headers); - concreteArgs->Body = std::move(bodyData); + concreteArgs->Data = std::move(data); concreteArgs->IncrementalUpdates = useIncrementalUpdates; concreteArgs->WithCredentials = withCredentials; - concreteArgs->IsText = responseType == "text"; + concreteArgs->ResponseType = std::move(responseType); concreteArgs->Timeout = timeout; PerformSendRequest(std::move(request), args); @@ -126,8 +127,8 @@ void WinRTHttpResource::ClearCookies() noexcept /*override*/ { // NOT IMPLEMENTED } -void WinRTHttpResource::SetOnRequest(function &&handler) noexcept /*override*/ { - m_onRequest = std::move(handler); +void WinRTHttpResource::SetOnRequestSuccess(function &&handler) noexcept /*override*/ { + m_onRequestSuccess = std::move(handler); } void WinRTHttpResource::SetOnResponse(function &&handler) noexcept @@ -140,6 +141,12 @@ void WinRTHttpResource::SetOnData(function &&handler) noexcept +/*override*/ +{ + m_onDataDynamic = std::move(handler); +} + void WinRTHttpResource::SetOnError(function &&handler) noexcept /*override*/ { m_onError = std::move(handler); @@ -167,6 +174,28 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque // Ensure background thread co_await winrt::resume_background(); + // If URI handler is available, it takes over request processing. + if (auto uriHandler = self->m_uriHandler.lock()) { + auto uri = winrt::to_string(coRequest.RequestUri().ToString()); + try { + if (uriHandler->Supports(uri, coReqArgs->ResponseType)) { + auto blob = uriHandler->Fetch(uri); + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(coReqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(coReqArgs->RequestId); + } + + co_return; + } + } catch (const hresult_error &e) { + if (self->m_onError) + co_return self->m_onError(coReqArgs->RequestId, Utilities::HResultToString(e)); + } catch (const std::exception &e) { + if (self->m_onError) + co_return self->m_onError(coReqArgs->RequestId, e.what()); + } + } + HttpMediaTypeHeaderValue contentType{nullptr}; string contentEncoding; string contentLength; @@ -201,20 +230,44 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque } IHttpContent content{nullptr}; - if (BodyData::Type::String == coReqArgs->Body.Type) { - content = HttpStringContent{to_hstring(coReqArgs->Body.Data)}; - } else if (BodyData::Type::Base64 == coReqArgs->Body.Type) { - auto buffer = CryptographicBuffer::DecodeFromBase64String(to_hstring(coReqArgs->Body.Data)); - content = HttpBufferContent{buffer}; - } else if (BodyData::Type::Uri == coReqArgs->Body.Type) { - auto file = co_await StorageFile::GetFileFromApplicationUriAsync(Uri{to_hstring(coReqArgs->Body.Data)}); - auto stream = co_await file.OpenReadAsync(); - content = HttpStreamContent{stream}; - } else if (BodyData::Type::Form == coReqArgs->Body.Type) { - // #9535 - HTTP form data support - } else { - // BodyData::Type::Empty - // TODO: Error => unsupported?? + auto &data = coReqArgs->Data; + if (!data.isNull()) { + auto bodyHandler = self->m_requestBodyHandler.lock(); + if (bodyHandler && bodyHandler->Supports(data)) { + auto contentTypeString = contentType ? winrt::to_string(contentType.ToString()) : ""; + dynamic blob; + try { + blob = bodyHandler->ToRequestBody(data, contentTypeString); + } catch (const std::invalid_argument &e) { + if (self->m_onError) { + self->m_onError(coReqArgs->RequestId, e.what()); + } + co_return; + } + auto bytes = blob["bytes"]; + auto byteVector = vector(bytes.size()); + for (auto &byte : bytes) { + byteVector.push_back(static_cast(byte.asInt())); + } + auto view = winrt::array_view{byteVector}; + auto buffer = CryptographicBuffer::CreateFromByteArray(view); + content = HttpBufferContent{std::move(buffer)}; + } else if (!data["string"].empty()) { + content = HttpStringContent{to_hstring(data["string"].asString())}; + } else if (!data["base64"].empty()) { + auto buffer = CryptographicBuffer::DecodeFromBase64String(to_hstring(data["base64"].asString())); + content = HttpBufferContent{std::move(buffer)}; + } else if (!data["uri"].empty()) { + auto file = co_await StorageFile::GetFileFromApplicationUriAsync(Uri{to_hstring(data["uri"].asString())}); + auto stream = co_await file.OpenReadAsync(); + content = HttpStreamContent{std::move(stream)}; + } else if (!data["form"].empty()) { + // #9535 - HTTP form data support + // winrt::Windows::Web::Http::HttpMultipartFormDataContent() + } else { + // Assume empty request body. + // content = HttpStringContent{L""}; + } } if (content != nullptr) { @@ -224,12 +277,13 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque } if (!contentEncoding.empty()) { if (!content.Headers().ContentEncoding().TryParseAdd(to_hstring(contentEncoding))) { - if (m_onError) { - m_onError(coReqArgs->RequestId, "Failed to parse Content-Encoding"); - } + if (self->m_onError) + self->m_onError(coReqArgs->RequestId, "Failed to parse Content-Encoding"); + co_return; } } + if (!contentLength.empty()) { const auto contentLengthHeader = _atoi64(contentLength.c_str()); content.Headers().ContentLength(contentLengthHeader); @@ -255,7 +309,7 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque auto response = sendRequestOp.GetResults(); if (response) { if (self->m_onResponse) { - string url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); + auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); // Gather headers for both the response content and the response itself // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling @@ -278,7 +332,27 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); auto reader = DataReader{inputStream}; - if (coReqArgs->IsText) { + // #9510 - 10mb limit on fetch + co_await reader.LoadAsync(10 * 1024 * 1024); + + // Let response handler take over, if set + if (auto responseHandler = self->m_responseHandler.lock()) { + if (responseHandler->Supports(coReqArgs->ResponseType)) { + auto bytes = vector(reader.UnconsumedBufferLength()); + reader.ReadBytes(bytes); + auto blob = responseHandler->ToResponseData(std::move(bytes)); + + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(coReqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(coReqArgs->RequestId); + } + + co_return; + } + } + + auto isText = coReqArgs->ResponseType == "text"; + if (isText) { reader.UnicodeEncoding(UnicodeEncoding::Utf8); } @@ -291,7 +365,7 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque co_await reader.LoadAsync(segmentSize); length = reader.UnconsumedBufferLength(); - if (coReqArgs->IsText) { + if (isText) { auto data = std::vector(length); reader.ReadBytes(data); @@ -327,23 +401,63 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque } self->UntrackResponse(coReqArgs->RequestId); +} // PerformSendRequest + +#pragma region IHttpModuleProxy + +void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ +{ + // TODO: Implement custom URI handling. } +void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ +{ + m_requestBodyHandler = weak_ptr(requestBodyHandler); +} + +void WinRTHttpResource::AddResponseHandler(shared_ptr responseHandler) noexcept /*override*/ +{ + m_responseHandler = weak_ptr(responseHandler); +} + +#pragma endregion IHttpModuleProxy + #pragma endregion WinRTHttpResource #pragma region IHttpResource -/*static*/ shared_ptr IHttpResource::Make() noexcept { +/*static*/ shared_ptr IHttpResource::Make( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { + using namespace winrt::Microsoft::ReactNative; + using winrt::Windows::Web::Http::HttpClient; + + shared_ptr result; + if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { - return std::make_shared(); + result = std::make_shared(); } else { auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); auto opFilter = winrt::make(); - auto client = winrt::Windows::Web::Http::HttpClient{opFilter}; + auto client = HttpClient{opFilter}; - return std::make_shared(std::move(client)); + result = std::make_shared(std::move(client)); } + + // Register resource as HTTP module proxy. + if (inspectableProperties) { + auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; + auto propBag = ReactPropertyBag{inspectableProperties.try_as()}; + auto moduleProxy = weak_ptr{result}; + propBag.Set(propId, std::move(moduleProxy)); + } + + return result; +} + +/*static*/ shared_ptr IHttpResource::Make() noexcept { + auto inspectableProperties = IInspectable{nullptr}; + return Make(inspectableProperties); } #pragma endregion IHttpResource diff --git a/vnext/Shared/Networking/WinRTHttpResource.h b/vnext/Shared/Networking/WinRTHttpResource.h index 86615fdb6a1..38d030f8264 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.h +++ b/vnext/Shared/Networking/WinRTHttpResource.h @@ -5,6 +5,7 @@ #include "IHttpResource.h" +#include #include "WinRTTypes.h" // Windows API @@ -15,18 +16,24 @@ namespace Microsoft::React::Networking { -class WinRTHttpResource : public IHttpResource, public std::enable_shared_from_this { - static int64_t s_lastRequestId; - +class WinRTHttpResource : public IHttpResource, + public IHttpModuleProxy, + public std::enable_shared_from_this { winrt::Windows::Web::Http::IHttpClient m_client; std::mutex m_mutex; std::unordered_map m_responses; - std::function m_onRequest; + std::function m_onRequestSuccess; std::function m_onResponse; std::function m_onData; + std::function m_onDataDynamic; std::function m_onError; + // Used for IHttpModuleProxy + std::weak_ptr m_uriHandler; + std::weak_ptr m_requestBodyHandler; + std::weak_ptr m_responseHandler; + void TrackResponse(int64_t requestId, ResponseOperation response) noexcept; void UntrackResponse(int64_t requestId) noexcept; @@ -45,8 +52,9 @@ class WinRTHttpResource : public IHttpResource, public std::enable_shared_from_t void SendRequest( std::string &&method, std::string &&url, + int64_t requestId, Headers &&headers, - BodyData &&bodyData, + folly::dynamic &&data, std::string &&responseType, bool useIncrementalUpdates, int64_t timeout, @@ -55,13 +63,24 @@ class WinRTHttpResource : public IHttpResource, public std::enable_shared_from_t void AbortRequest(int64_t requestId) noexcept override; void ClearCookies() noexcept override; -#pragma endregion IHttpResource - - void SetOnRequest(std::function &&handler) noexcept override; + void SetOnRequestSuccess(std::function &&handler) noexcept override; void SetOnResponse(std::function &&handler) noexcept override; void SetOnData(std::function &&handler) noexcept override; + void SetOnData(std::function &&handler) noexcept override; void SetOnError(std::function &&handler) noexcept override; + +#pragma endregion IHttpResource + +#pragma region IHttpModuleProxy + + void AddUriHandler(std::shared_ptr uriHandler) noexcept override; + + void AddRequestBodyHandler(std::shared_ptr requestBodyHandler) noexcept override; + + void AddResponseHandler(std::shared_ptr responseHandler) noexcept override; + +#pragma endregion IHttpModuleProxy }; } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/Networking/WinRTTypes.h b/vnext/Shared/Networking/WinRTTypes.h index 3542776ed16..d0bbafc27a0 100644 --- a/vnext/Shared/Networking/WinRTTypes.h +++ b/vnext/Shared/Networking/WinRTTypes.h @@ -5,6 +5,9 @@ #include "IHttpResource.h" +// Folly +#include + // Windows API #include @@ -16,10 +19,10 @@ namespace Microsoft::React::Networking { struct RequestArgs : public winrt::implements { int64_t RequestId; IHttpResource::Headers Headers; - IHttpResource::BodyData Body; + folly::dynamic Data; bool IncrementalUpdates; bool WithCredentials; - bool IsText; + std::string ResponseType; int64_t Timeout; }; diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index 76a4d41f71d..fc40263f557 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -67,14 +67,16 @@ using namespace facebook; using namespace Microsoft::JSI; using std::make_shared; +using winrt::Microsoft::ReactNative::ReactPropertyBagHelper; namespace Microsoft::React { -/*extern*/ std::unique_ptr CreateHttpModule() noexcept { +/*extern*/ std::unique_ptr CreateHttpModule( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { if (GetRuntimeOptionBool("Http.UseMonolithicModule")) { return std::make_unique(); } else { - return std::make_unique(); + return std::make_unique(inspectableProperties); } } @@ -540,18 +542,21 @@ InstanceImpl::~InstanceImpl() { std::vector> InstanceImpl::GetDefaultNativeModules( std::shared_ptr nativeQueue) { std::vector> modules; + auto transitionalProps{ReactPropertyBagHelper::CreatePropertyBag()}; modules.push_back(std::make_unique( m_innerInstance, Microsoft::React::GetHttpModuleName(), - [nativeQueue]() -> std::unique_ptr { return Microsoft::React::CreateHttpModule(); }, + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateHttpModule(transitionalProps); + }, nativeQueue)); modules.push_back(std::make_unique( m_innerInstance, Microsoft::React::GetWebSocketModuleName(), - [nativeQueue]() -> std::unique_ptr { - return Microsoft::React::CreateWebSocketModule(); + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateWebSocketModule(transitionalProps); }, nativeQueue)); @@ -613,6 +618,18 @@ std::vector> InstanceImpl::GetDefaultNativeModules []() { return std::make_unique(); }, nativeQueue)); + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetBlobModuleName(), + [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetFileReaderModuleName(), + [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, + nativeQueue)); + return modules; } diff --git a/vnext/Shared/OInstance.h b/vnext/Shared/OInstance.h index 6803816d1b6..c6452311ff0 100644 --- a/vnext/Shared/OInstance.h +++ b/vnext/Shared/OInstance.h @@ -5,14 +5,18 @@ #pragma once +#include +#include +#include "InstanceManager.h" + +// React Native +#include + +// Standard Libriary #include #include #include -#include -#include -#include "InstanceManager.h" - namespace facebook { namespace react { diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index 8b4936dcf19..6041031a617 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -44,7 +44,10 @@ true + + + @@ -87,6 +90,16 @@ + + + + + + + + + + diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index e2d387b6809..3976b01efbf 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -127,6 +127,9 @@ Source Files + + Source Files\Modules + Source Files\Modules @@ -143,6 +146,12 @@ Source Files\Networking + + Source Files\Modules + + + Source Files\Modules + @@ -388,6 +397,15 @@ Header Files + + Header Files\Modules + + + Header Files\Modules + + + Header Files\Modules + Header Files\Modules @@ -418,6 +436,27 @@ Header Files + + Header Files\Modules + + + Header Files\Modules + + + Header Files\Modules + + + Header Files\Modules + + + Header Files\Modules + + + Header Files\Modules + + + Header Files\Modules + diff --git a/vnext/overrides.json b/vnext/overrides.json index 924670f92b2..4bdfbe0af30 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -115,6 +115,10 @@ "baseFile": "index.js", "baseHash": "2a0bd511c691be2ac3da45a3c77250aaf55414e1" }, + { + "type": "platform", + "file": "src/IntegrationTests/BlobTest.js" + }, { "type": "platform", "file": "src/IntegrationTests/DummyTest.js" @@ -127,10 +131,18 @@ "type": "platform", "file": "src/IntegrationTests/websocket_integration_test_server_binary.js" }, + { + "type": "platform", + "file": "src/IntegrationTests/websocket_integration_test_server_blob.js" + }, { "type": "platform", "file": "src/IntegrationTests/WebSocketBinaryTest.js" }, + { + "type": "platform", + "file": "src/IntegrationTests/WebSocketBlobTest.js" + }, { "type": "platform", "file": "src/IntegrationTests/XHRTest.js" @@ -502,4 +514,4 @@ "file": "src/typings-index.ts" } ] -} \ No newline at end of file +} diff --git a/vnext/src/IntegrationTests/BlobTest.js b/vnext/src/IntegrationTests/BlobTest.js new file mode 100644 index 00000000000..a9a87a365d5 --- /dev/null +++ b/vnext/src/IntegrationTests/BlobTest.js @@ -0,0 +1,199 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * @format + * @flow + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); + +const {AppRegistry, View} = ReactNative; + +const {TestModule} = ReactNative.NativeModules; + +type State = { + statusCode: number, + xhr: XMLHttpRequest, + expected: String, +}; + +class BlobTest extends React.Component<{...}, State> { + state: State = { + statusCode: 0, + xhr: new XMLHttpRequest(), + // https://www.facebook.com/favicon.ico + expected: new String( + 'data:application/octet-stream;base64,' + + 'AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAA' + + 'AAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOFl' + + 'BiviZgKP4WYB1f//////////4WUA1eJmAI/hawYrAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAA/4ArBuNpA5TkawP942kC/+NpAv///////////+NpAv/jaQL/5GoD/eNp' + + 'A5T/gCsGAAAAAAAAAAAAAAAA/4ArBuVvBL3lbgT/5W4E/+VuBP/lbgT/////////' + + '///lbgT/5W4E/+VuBP/lbgT/5W8Evf+AKwYAAAAAAAAAAOlzBZTncwX/53MF/+dz' + + 'Bf/ncwX/53MF////////////53MF/+dzBf/ncwb/53MF/+dzBv/pcweUAAAAAO19' + + 'DCvpeAf96HcH/+l4B//odwf/6XgH/+l4B////////////+h3B//odwf/6XgH/+h3' + + 'B//peAf/6nkH/e19DCvrfQmP630J/+t9Cf/rfAn/630J/+t8Cf/rfAn/////////' + + '///rfQn/630J/+p8CP/rfQn/6nwI/+p8CP/rfQuP7YEL1e2BCv/tgQr/7IEK/+2B' + + 'Cv/////////////////////////////////uiRj/7IEK/+2CCv/tggr/7YIM1e6G' + + 'DfPvhgz/74YM/++HDP/vhgz/////////////////////////////////8Zw4/++G' + + 'DP/uhgz/7oYM/+6GDPPwiw7z8IsN//CLDf/wiw3/8IsN//CLDf/wiw3/////////' + + '///wig3/8IoN//CLDf/wig3/8IsN//CLDf/xjA/z85EQ1fOQD//zkA//85AP//OQ' + + 'D//zkA//85AP////////////8o8P//KPD//ykA//8o8P//KQD//ykA//85EQ1fSU' + + 'EI/1lRH/9ZUR//SUEP/1lRH/9JQQ//SUEP/+9uz///////jDev/0mRz/9ZUR//SV' + + 'Ef/1lRH/9ZUR//SUEI/5mhgr95kS/faZEv/2mRL/9pkS//aZEv/2mRL//Nqo////' + + '//////////////rLhv/3mhL/9pkS//eZEv35mhgrAAAAAPifFZT4nhT/+Z8U//ie' + + 'FP/5nxT/+Z8U//ikI//83a3//vjw//78+f/7yX3/+J4T//ieFP/4nxWUAAAAAAAA' + + 'AAD/qisG+6MWvfqjFf/6oxX/+qMV//qjFf/6oxX/+qMV//qjFf/6oxX/+qIV//qj' + + 'Ff/7oxa9/6orBgAAAAAAAAAAAAAAAP+qKwb9qRiU/agW/fyoF//8qBf//agX//yo' + + 'F//9qBf//agX//2oF/39qRiU/6orBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+y' + + 'Hiv/rRmP/6wZ1f+tGPP/rBjz/64Z1f+vGY//sh4rAAAAAAAAAAAAAAAAAAAAAPAP' + + 'AADAAwAAgAEAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAB' + + 'AACAAQAAwAMAAPAPAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+A' + + 'AAbiZQRH4GMAlf//////////////////////////4GQAv+BjAJXiZQBH/4AABgAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpqCxjiZgKW4WYB8eJmAf/hZQH/////////' + + '///////////////////hZgH/4mYB/+FmAf/iZwHx4mYClupqCxgAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP9t' + + 'JAfkagSC42kC9ONoAv/jaAL/4mgC/+NoAv///////////////////////////+Jo' + + 'Af/iaAL/4mgB/+JoAv/iaAL/42kC9ORqBIL/bSQHAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqagsY5GoEx+NqA//jagP/42oD/+Nq' + + 'A//kawP/42oD////////////////////////////42oD/+RrA//jagP/5GsD/+Rr' + + 'A//kawP/5GsD/+RsBMfqdQsYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAA6HEGLeVuBOPlbQT/5GwD/+VtBP/kbAP/5GwD/+VtBP/kbAP/////////' + + '///////////////////kbAP/5G0D/+RsA//kbQP/5G0D/+RtA//kbQP/5G0D/+Ru' + + 'A+PocQYtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOp1CxjmcAbj5W8E/+Vv' + + 'BP/lbwT/5W8E/+VvBP/lbwT/5W8E/+VvBP///////////////////////////+Vv' + + 'BP/mcAX/5W8E/+ZwBf/mcAX/5W8E/+ZwBf/lbwT/5W8E/+ZwBuPqdQsYAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAD/kiQH53IFx+ZyBf/mcQX/5nEF/+ZxBf/mcQX/5nEF/+Zx' + + 'Bf/ncgX/5nEF////////////////////////////53IG/+ZxBf/ncgb/5nEF/+Zx' + + 'Bf/mcgX/5nEF/+ZyBf/mcgX/5nEF/+dyBcf/kiQHAAAAAAAAAAAAAAAAAAAAAOd2' + + 'CILodAb/53QG/+h0Bv/odAb/6HQG/+h0Bv/odAb/6HQG/+d0Bv/odAb/////////' + + '///////////////////ndAX/53QG/+d0Bf/ndAb/53QG/+h1Bv/ndAb/6HUG/+h1' + + 'Bv/ndAX/6HUG/+d2BoIAAAAAAAAAAAAAAADqgAsY6HYG9Oh2B//odgb/6HYH/+h2' + + 'B//odgb/6HYH/+h2Bv/odgb/6HYH/+h2Bv///////////////////////////+l3' + + 'B//odgb/6XcH/+h2Bv/odgb/6HYH/+h2Bv/odgf/6HYH/+h2Bv/odgf/6HYG9OqA' + + 'CxgAAAAAAAAAAOt6CZbpeQj/6nkI/+l5CP/qeQj/6nkI/+l5B//qeQj/6XkH/+l5' + + 'B//peQf/6XkH////////////////////////////6XkI/+l5B//peQj/6XkH/+l5' + + 'B//peQj/6XkH/+l5CP/peQj/6XkH/+l5CP/peQf/63oJlgAAAAD/gCsG7H0K8et8' + + 'Cf/qewj/63wJ/+p7CP/qewj/6nsI/+p7CP/qewj/6nsI/+t8Cf/qewj/////////' + + '///////////////////qewj/6nwJ/+p7CP/qfAn/6nwJ/+p7CP/qfAn/6nsI/+p7' + + 'CP/rfAn/6nsI/+t8Cf/sfQrx/4ArBu2BC0frfQn/630J/+t+Cf/rfQn/634J/+t+' + + 'Cf/rfgn/634J////////////////////////////////////////////////////' + + '///////////////////zs27/634J/+x+Cf/rfgn/634J/+t+Cf/rfgn/634J/+t+' + + 'Cf/tgQtH7IAKleyACv/sgAr/7IAK/+yACv/sgAr/7IAK/+yACv/sgAr/////////' + + '//////////////////////////////////////////////////////////////XC' + + 'iv/sgAr/7IAK/+yACv/sgAr/7IAJ/+yACv/sgAn/7IAJ/+yACpXugwu/7YML/+2D' + + 'C//tggr/7YML/+2CCv/tggr/7YIK/+2CCv//////////////////////////////' + + '////////////////////////////////////////+NKn/+2DC//tggr/7YML/+2D' + + 'C//tgwv/7YML/+2DC//tgwv/7oMLv++GDNnuhQv/7oUL/+6FC//uhQv/7oUL/+6F' + + 'C//vhQz/7oUL////////////////////////////////////////////////////' + + '///////////////////64cT/7oUL/+6FC//uhQv/7oUL/+6EC//uhQv/7oQL/+6E' + + 'C//vhgzZ74gO8++IDP/viAz/74cM/++IDP/vhwz/74cM/++HDP/vhwz/////////' + + '//////////////////////////////////////////////////////////////3w' + + '4f/viA3/74cM/++IDf/viA3/74cM/++IDf/vhwz/74cM/++HDfPwiw7z8IoN//CK' + + 'Df/wig3/8IoN//CKDf/wig3/8IkN//CKDf/wiQ3/8IkN//CKDf/wiQ3/////////' + + '///////////////////wiQ3/8IoN//CJDf/wig3/8IoN//CJDf/wig3/8IkN//CJ' + + 'Df/wiQ3/8IkN//CJDf/wiQ3/8IsO8/KNDtnxjA7/8YwO//GMDf/xjA7/8YwN//GM' + + 'Df/xjA3/8YwN//GMDf/xjA3/8YwO//GMDf////////////////////////////GM' + + 'Dv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GM' + + 'Dv/yjQ7Z8o8Pv/KPD//yjw//8o8P//KPD//yjw//8o8P//KPD//yjw//8o8P//KP' + + 'D//yjg7/8o8P////////////////////////////8Y4O//KODv/xjg7/8o4O//KO' + + 'Dv/yjg7/8o4O//KODv/yjg7/8o8P//KODv/yjw//8o8P//OQEL/zkQ+V85EP//OR' + + 'D//zkQ//85EP//ORD//zkQ//85EP//ORD//zkQ//85EP//OREP/zkQ///vr0////' + + '///////////////////0myb/85EQ//ORD//zkRD/85EQ//ORD//zkRD/85EP//OR' + + 'D//zkQ//85EP//ORD//zkQ//85EPlfSXEkf0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + + 'EP/zkxD/9JQQ//OTEP/zkxD/9JQQ//OTEP/86tD///////////////////////rV' + + 'ov/1nSb/85MQ//STEP/0kxD/9JMQ//STEP/0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + + 'EP/0kxJH/6orBvWWEvH1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYR//WW' + + 'Ef/1lhH/9ZYR//vZq///////////////////////////////////////////////' + + '///1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYS8f+qKwYAAAAA95kTlvaY' + + 'Ev/2mBH/9pgS//aYEf/2mBH/9ZgR//aYEf/1mBH/9ZgR//aYEv/1mBH/+LFN////' + + '//////////////////////////////////////////////aYEv/1mBH/9pgS//aY' + + 'Ev/1mBH/9pgS//WYEf/3mRGWAAAAAAAAAAD/nxUY+JwU9PebE//3mxP/95sT//eb' + + 'E//3mxP/95sT//ebE//3mxP/95oS//ebE//3mhL//OK7////////////////////' + + '////////////////////////95sT//ebE//3mxP/95sT//aaEv/3mxP/95sT9P+f' + + 'FRgAAAAAAAAAAAAAAAD5nxSC+J0T//idE//4nRP/+J0T//ecE//4nRP/95wT//ec' + + 'E//4nRP/95wT//idE//4pSf//efF////////////////////////////////////' + + '///4nRP/950T//idE//4nRP/+J0T//idE//5nxSCAAAAAAAAAAAAAAAAAAAAAP+2' + + 'JAf6oBXH+aAU//mgFP/5oBT/+J8U//mgFP/4nxT/+J8U//mfFP/4nxT/+Z8U//mf' + + 'FP/5oRf/+86H//7w2v/+/Pj//v36//758f/+8+P//evQ//mgFP/5nxT/+aAU//mg' + + 'FP/4nxT/+qAVx/+2JAcAAAAAAAAAAAAAAAAAAAAAAAAAAP+qFRj7oxXj+aEU//mh' + + 'FP/6ohX/+aEU//qiFf/6ohX/+qIV//qiFf/6ohX/+qIV//mhFP/6ohX/+aEU//mh' + + 'FP/6ohX/+aEU//qiFf/6ohX/+aEU//qiFf/5oRT/+aEU//ujFeP/qhUYAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+mFi78pBXj+6QV//ukFv/7pBX/+6QW//uk' + + 'Fv/6pBX/+6QW//qkFf/6pBX/+6QW//qkFf/7pBb/+6QW//ulFv/7pBb/+6UW//ul' + + 'Fv/7pBX/+6UW//ukFf/8pBXj/6QXLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAP+qIBj8qBfH/KcW//ynF//8pxb//KcW//ynFv/8pxb//KcW//yn' + + 'Fv/7phb//KcW//umFv/7phb/+6YW//umFv/7phb/+6YW//ynFv/7phb//KgXx/+q' + + 'IBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+2' + + 'JAf9qxiC/akY9PypF//8qRf//KgX//ypF//8qBf//KgX//2pF//8qBf//akX//2p' + + 'F//9qRf//akX//2pF//9qRf//qkY9P2rGIL/tiQHAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/tSAY/60alv+s' + + 'GPH+rBj//qwY//6sGP/+rBj//asY//6sGP/9qxj//asY//2rF//9qxj//qsX8f2s' + + 'GJb/tSAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9UrBv+wGUf/rxqV/68Zv/+v' + + 'Gtn/rhnz/64Z8/+vGtn/rxm//68alf+wGUf/1SsGAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AA///AAD//AAAP/gAAB/wAAAP4AAAB8AA' + + 'AAPAAAADgAAAAYAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAACAAAABgAAAAcAAAAPAAAAD4AAAB/AAAA/4AAAf/AAAP/8A' + + 'AP//wAP/', + ), + }; + + _get = () => { + this.state.xhr.onloadend = () => { + this.setState({ + statusCode: this.state.xhr.status, + }); + }; + this.state.xhr.open('GET', 'https://www.facebook.com/favicon.ico'); + this.state.xhr.setRequestHeader('Accept-Encoding', 'utf-8'); + this.state.xhr.responseType = 'blob'; + this.state.xhr.send(); + }; + + _getSucceeded = () => { + return this.state.statusCode === 200 && this.state.xhr.response !== null; + }; + + _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); + }; + + componentDidMount() { + this._get(); + this._waitFor(this._getSucceeded, 6, doneSucceeded => { + let reader = new FileReader(); + reader.readAsDataURL(this.state.xhr.response); + reader.onload = () => { + TestModule.markTestPassed( + doneSucceeded && this.state.expected === reader.result, + ); + }; + }); + } + + render(): React.Node { + return ; + } +} + +AppRegistry.registerComponent('BlobTest', () => BlobTest); + +module.exports = BlobTest; diff --git a/vnext/src/IntegrationTests/WebSocketBlobTest.js b/vnext/src/IntegrationTests/WebSocketBlobTest.js new file mode 100644 index 00000000000..2f975d719f4 --- /dev/null +++ b/vnext/src/IntegrationTests/WebSocketBlobTest.js @@ -0,0 +1,158 @@ +/** + * 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: Uint8Array) => { + if (!this.state.socket) { + return; + } + this.state.socket.send(message); + }; + + _sendTestMessage = () => { + this._sendBinary(this.state.testMessage); + }; + + _receivedTestExpectedResponse = () => { + // Can't iterate through Blob response. Blob.arrayBuffer() not supported. + return ( + this.state.lastMessage?.size === this.state.testExpectedResponse.length + ); + }; + + 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 ; + } +} // class WebSocketBlobTest + +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..59e64f678bb --- /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]); + }); +}); diff --git a/vnext/src/Libraries/Network/RCTNetworkingWinShared.js b/vnext/src/Libraries/Network/RCTNetworkingWinShared.js index cc73cb6e534..e60babf60b0 100644 --- a/vnext/src/Libraries/Network/RCTNetworkingWinShared.js +++ b/vnext/src/Libraries/Network/RCTNetworkingWinShared.js @@ -61,6 +61,11 @@ type RCTNetworkingEventDefinitions = $ReadOnly<{ ], }>; +let _requestId = 1; +function generateRequestId(): number { + return _requestId++; +} + const RCTNetworking = { addListener>( eventType: K, @@ -82,11 +87,13 @@ const RCTNetworking = { callback: (requestId: number) => void, withCredentials: boolean, ) { + const requestId = generateRequestId(); const body = convertRequestBody(data); RCTNetworkingNative.sendRequest( { method, url, + requestId, data: {...body, trackingName}, headers, responseType,