diff --git a/change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json b/change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json new file mode 100644 index 00000000000..b1668063e0e --- /dev/null +++ b/change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement HTTP incremental updates", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Shared/Modules/HttpModule.cpp b/vnext/Shared/Modules/HttpModule.cpp index 20f4ea0fd04..7995320d961 100644 --- a/vnext/Shared/Modules/HttpModule.cpp +++ b/vnext/Shared/Modules/HttpModule.cpp @@ -33,8 +33,10 @@ constexpr char moduleName[] = "Networking"; // React event names constexpr char completedResponse[] = "didCompleteNetworkResponse"; constexpr char receivedResponse[] = "didReceiveNetworkResponse"; -constexpr char receivedData[] = "didReceiveNetworkData"; +constexpr char sentData[] = "didSendNetworkData"; +constexpr char receivedIncrementalData[] = "didReceiveNetworkIncrementalData"; constexpr char receivedDataProgress[] = "didReceiveNetworkDataProgress"; +constexpr char receivedData[] = "didReceiveNetworkData"; static void SetUpHttpResource( shared_ptr resource, @@ -60,9 +62,6 @@ static void SetUpHttpResource( 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, completedResponse, dynamic::array(requestId)); }); // Explicitly declaring function type to avoid type inference ambiguity. @@ -72,6 +71,22 @@ static void SetUpHttpResource( }; resource->SetOnData(std::move(onDataDynamic)); + resource->SetOnIncrementalData( + [weakReactInstance](int64_t requestId, string &&responseData, int64_t progress, int64_t total) { + SendEvent( + weakReactInstance, + receivedIncrementalData, + dynamic::array(requestId, std::move(responseData), progress, total)); + }); + + resource->SetOnDataProgress([weakReactInstance](int64_t requestId, int64_t progress, int64_t total) { + SendEvent(weakReactInstance, receivedDataProgress, dynamic::array(requestId, progress, total)); + }); + + resource->SetOnResponseComplete([weakReactInstance](int64_t requestId) { + SendEvent(weakReactInstance, completedResponse, dynamic::array(requestId)); + }); + resource->SetOnError([weakReactInstance](int64_t requestId, string &&message, bool isTimeout) { dynamic args = dynamic::array(requestId, std::move(message)); if (isTimeout) { diff --git a/vnext/Shared/Networking/IHttpResource.h b/vnext/Shared/Networking/IHttpResource.h index 17d791d1868..c590e91aac7 100644 --- a/vnext/Shared/Networking/IHttpResource.h +++ b/vnext/Shared/Networking/IHttpResource.h @@ -91,10 +91,136 @@ struct IHttpResource { virtual void ClearCookies() noexcept = 0; + /// + /// Sets a function to be invoked when a request has been successfully responded. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// virtual void SetOnRequestSuccess(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when a response arrives and its headers are received. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Object containing basic response data + /// + /// virtual void SetOnResponse(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when response content data has been received. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Response content payload (plain text or Base64-encoded) + /// + /// virtual void SetOnData(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when response content data has been received. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Structured response content payload (i.e. Blob data) + /// + /// virtual void SetOnData(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when a response content increment has been received. + /// + /// + /// The handler set by this method will only be called if the request sets the incremental updates flag. + /// The handler is also mutually exclusive with those set by `SetOnData`, which are used for one pass, non-incremental + /// updates. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Partial response content data increment (non-accumulative) + /// + /// + /// Number of bytes received so far + /// + /// + /// Number of total bytes to receive + /// + /// + virtual void SetOnIncrementalData( + std::function + &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when response content download progress is reported. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Number of bytes received so far + /// + /// + /// Number of total bytes to receive + /// + /// + virtual void SetOnDataProgress( + std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when a response has been fully handled (either succeeded or failed). + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + virtual void SetOnResponseComplete(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when an error condition is found. + /// + /// + /// The handler's purpose is not to report any given HTTP error status (i.e. 403, 501). + /// It is meant to report application errors when executing HTTP requests. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// virtual void SetOnError( std::function &&handler) noexcept = 0; }; diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 7c2ec167f67..6022450205b 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -51,6 +51,21 @@ using winrt::Windows::Web::Http::IHttpClient; using winrt::Windows::Web::Http::IHttpContent; using winrt::Windows::Web::Http::Headers::HttpMediaTypeHeaderValue; +namespace { + +constexpr uint32_t operator""_KiB(unsigned long long int x) { + return static_cast(1024 * x); +} + +constexpr uint32_t operator""_MiB(unsigned long long int x) { + return static_cast(1024_KiB * x); +} + +constexpr char responseTypeText[] = "text"; +constexpr char responseTypeBase64[] = "base64"; +constexpr char responseTypeBlob[] = "blob"; + +} // namespace namespace Microsoft::React::Networking { // May throw winrt::hresult_error @@ -258,7 +273,7 @@ void WinRTHttpResource::SendRequest( bool withCredentials, std::function &&callback) noexcept /*override*/ { // Enforce supported args - assert(responseType == "text" || responseType == "base64" || responseType == "blob"); + assert(responseType == responseTypeText || responseType == responseTypeBase64 || responseType == responseTypeBlob); if (callback) { callback(requestId); @@ -336,6 +351,22 @@ void WinRTHttpResource::SetOnData(function &&handler) noexcept +/*override*/ { + m_onIncrementalData = std::move(handler); +} + +void WinRTHttpResource::SetOnDataProgress( + function &&handler) noexcept +/*override*/ { + m_onDataProgress = std::move(handler); +} + +void WinRTHttpResource::SetOnResponseComplete(function &&handler) noexcept /*override*/ { + m_onComplete = std::move(handler); +} + void WinRTHttpResource::SetOnError( function &&handler) noexcept /*override*/ { @@ -392,6 +423,10 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect self->m_onRequestSuccess(reqArgs->RequestId); } + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); + } + co_return; } } catch (const hresult_error &e) { @@ -405,6 +440,9 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect try { auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); + + auto isText = reqArgs->ResponseType == responseTypeText; + self->TrackResponse(reqArgs->RequestId, sendRequestOp); if (reqArgs->Timeout > 0) { @@ -471,13 +509,14 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); auto reader = DataReader{inputStream}; - // #9510 - We currently accumulate all incoming request data in 10MB chunks - const uint32_t segmentSize = 10 * 1024 * 1024; + // Accumulate all incoming request data in 8MB chunks + // Note, the minimum apparent valid chunk size is 128 KB + // Apple's implementation appears to grab 5-8 KB chunks + const uint32_t segmentSize = reqArgs->IncrementalUpdates ? 128_KiB : 8_MiB; // Let response handler take over, if set if (auto responseHandler = self->m_responseHandler.lock()) { if (responseHandler->Supports(reqArgs->ResponseType)) { - // #9510 vector responseData{}; while (auto loaded = co_await reader.LoadAsync(segmentSize)) { auto length = reader.UnconsumedBufferLength(); @@ -494,37 +533,62 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect self->m_onRequestSuccess(reqArgs->RequestId); } + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); + } co_return; } } - auto isText = reqArgs->ResponseType == "text"; if (isText) { reader.UnicodeEncoding(UnicodeEncoding::Utf8); } - // #9510 + int64_t receivedBytes = 0; string responseData; winrt::Windows::Storage::Streams::IBuffer buffer; while (auto loaded = co_await reader.LoadAsync(segmentSize)) { auto length = reader.UnconsumedBufferLength(); + receivedBytes += length; if (isText) { auto data = vector(length); reader.ReadBytes(data); - responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); + auto incrementData = string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); + // #9534 - Send incremental updates. + // See https://github.com/facebook/react-native/blob/v0.70.6/Libraries/Network/RCTNetworking.mm#L561 + if (reqArgs->IncrementalUpdates) { + responseData = std::move(incrementData); + + if (self->m_onIncrementalData) { + // For total, see #10849 + self->m_onIncrementalData(reqArgs->RequestId, std::move(responseData), receivedBytes, 0 /*total*/); + } + } else { + responseData += std::move(incrementData); + } } else { buffer = reader.ReadBuffer(length); auto data = CryptographicBuffer::EncodeToBase64String(buffer); responseData += winrt::to_string(std::wstring_view(data)); + + if (self->m_onDataProgress) { + // For total, see #10849 + self->m_onDataProgress(reqArgs->RequestId, receivedBytes, 0 /*total*/); + } } } - if (self->m_onData) { + // If dealing with text-incremental response data, use m_onIncrementalData instead + if (self->m_onData && !(reqArgs->IncrementalUpdates && isText)) { self->m_onData(reqArgs->RequestId, std::move(responseData)); } + + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); + } } else { if (self->m_onError) { self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); diff --git a/vnext/Shared/Networking/WinRTHttpResource.h b/vnext/Shared/Networking/WinRTHttpResource.h index 23cc68dd299..7b18c69c6aa 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.h +++ b/vnext/Shared/Networking/WinRTHttpResource.h @@ -30,6 +30,10 @@ class WinRTHttpResource : public IHttpResource, std::function m_onData; std::function m_onDataDynamic; std::function m_onError; + std::function + m_onIncrementalData; + std::function m_onDataProgress; + std::function m_onComplete; // Used for IHttpModuleProxy std::weak_ptr m_uriHandler; @@ -80,6 +84,12 @@ class WinRTHttpResource : public IHttpResource, void SetOnResponse(std::function &&handler) noexcept override; void SetOnData(std::function &&handler) noexcept override; void SetOnData(std::function &&handler) noexcept override; + void SetOnIncrementalData( + std::function + &&handler) noexcept override; + void SetOnDataProgress( + std::function &&handler) noexcept override; + void SetOnResponseComplete(std::function &&handler) noexcept override; void SetOnError( std::function &&handler) noexcept override;