diff --git a/.vscode/settings.json b/.vscode/settings.json
index 4295a933d1f..1e2e92eba7b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,7 +10,7 @@
"**/lib/**/*.js": true,
"**/dist/**/*.js": true
},
- "editor.formatOnSave": true,
+ "editor.formatOnSave": false,
"eslint.format.enable": true,
"eslint.packageManager": "yarn",
"eslint.enable": true,
diff --git a/change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json b/change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json
new file mode 100644
index 00000000000..cda201f8577
--- /dev/null
+++ b/change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Allow fetching HTTP content by segments",
+ "packageName": "react-native-windows",
+ "email": "julio.rocha@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/@react-native-windows/tester/overrides.json b/packages/@react-native-windows/tester/overrides.json
index 65a0a651fd4..ca338fe1574 100644
--- a/packages/@react-native-windows/tester/overrides.json
+++ b/packages/@react-native-windows/tester/overrides.json
@@ -14,6 +14,14 @@
"baseHash": "a1d1c6638e815f6dd53504983813fda92f3b5478",
"issue": 6341
},
+ {
+ "type": "platform",
+ "file": "src/js/examples/HTTP/HTTPExample.js"
+ },
+ {
+ "type": "platform",
+ "file": "src/js/examples/HTTP/HTTPExampleMultiPartFormData.js"
+ },
{
"type": "patch",
"file": "src/js/examples/Pressable/PressableExample.windows.js",
@@ -48,4 +56,4 @@
"baseHash": "d12b4947135ada2dcb1c68835f53d7f0beff8a4e"
}
]
-}
\ No newline at end of file
+}
diff --git a/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js
new file mode 100644
index 00000000000..95311de2d10
--- /dev/null
+++ b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT License.
+ *
+ * @flow
+ * @format
+ */
+'use strict';
+
+const React = require('react');
+const HTTPExampleMultiPartFormData = require('./HTTPExampleMultiPartFormData');
+const XHRExampleFetch = require('../XHR/XHRExampleFetch');
+
+exports.framework = 'React';
+exports.title = 'HTTP';
+exports.category = 'Basic';
+exports.description =
+ ('Example that demonstrates direct and indirect HTTP native module usage.': string);
+exports.examples = [
+ {
+ title: 'multipart/form-data POST',
+ render(): React.Node {
+ return ;
+ },
+ },
+ {
+ title: 'Fetch Test',
+ render(): React.Node {
+ return ;
+ },
+ },
+];
diff --git a/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js
new file mode 100644
index 00000000000..9f80487bf32
--- /dev/null
+++ b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js
@@ -0,0 +1,164 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT License.
+ *
+ * @flow
+ * @format
+ */
+'use strict';
+
+const React = require('react');
+
+const {StyleSheet, Text, TextInput, View} = require('react-native');
+
+/**
+ * See https://www.w3schools.com/php/php_form_validation.asp
+ */
+class HTTPExampleMultiPartFormData extends React.Component {
+ responseURL: ?string;
+ responseHeaders: ?Object;
+
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ responseText: null,
+ };
+ this.responseURL = null;
+ this.responseHeaders = null;
+ }
+
+ submit(uri: string) {
+ const formData = new FormData();
+
+ formData.append('name', {
+ string: 'Name',
+ type: 'application/text',
+ });
+ formData.append('email', {
+ string: 'me@mail.com',
+ type: 'application/text',
+ });
+ formData.append('website', {
+ string: 'http://aweb.com',
+ type: 'application/text',
+ });
+ formData.append('comment', {
+ string: 'Hello',
+ type: 'application/text',
+ });
+ formData.append('gender', {
+ string: 'Other',
+ type: 'application/text',
+ });
+
+ fetch(uri, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ body: formData,
+ })
+ .then(response => {
+ this.responseURL = response.url;
+ this.responseHeaders = response.headers;
+
+ return response.text();
+ })
+ .then(body => {
+ this.setState({responseText: body});
+ });
+ }
+
+ _renderHeaders(): null | Array {
+ if (!this.responseHeaders) {
+ return null;
+ }
+
+ const responseHeaders = [];
+ const keys = Object.keys(this.responseHeaders.map);
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ // $FlowFixMe[incompatible-use]
+ const value = this.responseHeaders.get(key);
+ responseHeaders.push(
+
+ {key}: {value}
+ ,
+ );
+ }
+ return responseHeaders;
+ }
+
+ render(): React.Node {
+ const responseURL = this.responseURL ? (
+
+ Server response URL:
+ {this.responseURL}
+
+ ) : null;
+
+ const responseHeaders = this.responseHeaders ? (
+
+ Server response headers:
+ {this._renderHeaders()}
+
+ ) : null;
+
+ const response = this.state.responseText ? (
+
+ Server response:
+
+
+ ) : null;
+
+ return (
+
+ Edit URL to submit:
+ {
+ this.submit(event.nativeEvent.text);
+ }}
+ style={styles.textInput}
+ />
+ {responseURL}
+ {responseHeaders}
+ {response}
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ textInput: {
+ flex: 1,
+ borderRadius: 3,
+ borderColor: 'grey',
+ borderWidth: 1,
+ height: 30,
+ paddingLeft: 8,
+ },
+ label: {
+ flex: 1,
+ color: '#aaa',
+ fontWeight: '500',
+ height: 20,
+ },
+ textOutput: {
+ flex: 1,
+ fontSize: 17,
+ borderRadius: 3,
+ borderColor: 'grey',
+ borderWidth: 1,
+ height: 200,
+ paddingLeft: 8,
+ },
+});
+
+module.exports = HTTPExampleMultiPartFormData;
diff --git a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js
index a450e5df622..f9b6d45246c 100644
--- a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js
+++ b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js
@@ -12,6 +12,14 @@ import type {RNTesterModuleInfo} from '../types/RNTesterTypes';
import ReactNativeFeatureFlags from 'react-native/Libraries/ReactNative/ReactNativeFeatureFlags';
const Components: Array = [
+ {
+ key: 'HTTPExample',
+ module: require('../examples/HTTP/HTTPExample'),
+ },
+ {
+ key: 'XHRExample',
+ module: require('../examples/XHR/XHRExample'),
+ },
{
key: 'ActivityIndicatorExample',
category: 'UI',
diff --git a/vnext/Desktop.DLL/react-native-win32.x64.def b/vnext/Desktop.DLL/react-native-win32.x64.def
index f8d5742909f..eec2862ff77 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
-??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
??1Instance@react@facebook@@QEAA@XZ
diff --git a/vnext/Desktop.DLL/react-native-win32.x86.def b/vnext/Desktop.DLL/react-native-win32.x86.def
index d899f933715..f9b316cb1b0 100644
--- a/vnext/Desktop.DLL/react-native-win32.x86.def
+++ b/vnext/Desktop.DLL/react-native-win32.x86.def
@@ -52,7 +52,6 @@ EXPORTS
?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
?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
?Hash128@SpookyHashV2@hash@folly@@SGXPBXIPA_K1@Z
?assertionFailure@detail@folly@@YGXPBD00I0H@Z
diff --git a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp
index 0813355bf0a..4ed441749b3 100644
--- a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp
+++ b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp
@@ -27,7 +27,6 @@ TEST_MODULE_INITIALIZE(InitModule) {
SetRuntimeOptionBool("WebSocket.AcceptSelfSigned", true);
SetRuntimeOptionBool("UseBeastWebSocket", false);
- SetRuntimeOptionBool("Http.UseMonolithicModule", false);
SetRuntimeOptionBool("Blob.EnableModule", true);
// WebSocketJSExecutor can't register native log hooks.
diff --git a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp
index 9522dc4b59a..cc0cfc04438 100644
--- a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp
+++ b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp
@@ -19,12 +19,8 @@
namespace Microsoft::ReactNative {
-using winrt::Microsoft::ReactNative::ReactPropertyBag;
-
namespace {
-using winrt::Microsoft::ReactNative::ReactPropertyId;
-
bool HasPackageIdentity() noexcept {
static const bool hasPackageIdentity = []() noexcept {
auto packageStatics = winrt::get_activation_factory(
@@ -39,13 +35,6 @@ bool HasPackageIdentity() noexcept {
return hasPackageIdentity;
}
-ReactPropertyId HttpUseMonolithicModuleProperty() noexcept {
- static ReactPropertyId propId{
- L"ReactNative.Http"
- L"UseMonolithicModule"};
- return propId;
-}
-
} // namespace
std::vector GetCoreModules(
@@ -61,17 +50,15 @@ std::vector GetCoreModules(
[props = context->Properties()]() { return Microsoft::React::CreateHttpModule(props); },
jsMessageQueue);
- if (!ReactPropertyBag(context->Properties()).Get(HttpUseMonolithicModuleProperty())) {
- modules.emplace_back(
- Microsoft::React::GetBlobModuleName(),
- [props = context->Properties()]() { return Microsoft::React::CreateBlobModule(props); },
- batchingUIMessageQueue);
-
- modules.emplace_back(
- Microsoft::React::GetFileReaderModuleName(),
- [props = context->Properties()]() { return Microsoft::React::CreateFileReaderModule(props); },
- batchingUIMessageQueue);
- }
+ modules.emplace_back(
+ Microsoft::React::GetBlobModuleName(),
+ [props = context->Properties()]() { return Microsoft::React::CreateBlobModule(props); },
+ batchingUIMessageQueue);
+
+ modules.emplace_back(
+ Microsoft::React::GetFileReaderModuleName(),
+ [props = context->Properties()]() { return Microsoft::React::CreateFileReaderModule(props); },
+ batchingUIMessageQueue);
modules.emplace_back(
"Timing",
diff --git a/vnext/Shared/Modules/FileReaderModule.cpp b/vnext/Shared/Modules/FileReaderModule.cpp
index 9f7919e31fc..67cb040b52e 100644
--- a/vnext/Shared/Modules/FileReaderModule.cpp
+++ b/vnext/Shared/Modules/FileReaderModule.cpp
@@ -4,6 +4,7 @@
#include "FileReaderModule.h"
#include
+#include
// Boost Library
#include
diff --git a/vnext/Shared/Modules/HttpModule.cpp b/vnext/Shared/Modules/HttpModule.cpp
index 20f4ea0fd04..326f677d843 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) {
@@ -108,90 +123,90 @@ std::map HttpModule::getConstants() {
}
// clang-format off
-std::vector HttpModule::getMethods() {
+ std::vector HttpModule::getMethods() {
- return
- {
+ return
{
- "sendRequest",
- [weakHolder = weak_ptr(m_holder)](dynamic args, Callback cxxCallback)
{
- auto holder = weakHolder.lock();
- if (!holder) {
- return;
- }
-
- auto resource = holder->Module->m_resource;
- if (!holder->Module->m_isResourceSetup)
+ "sendRequest",
+ [weakHolder = weak_ptr(m_holder)](dynamic args, Callback cxxCallback)
{
- SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
- holder->Module->m_isResourceSetup = true;
- }
+ auto holder = weakHolder.lock();
+ if (!holder) {
+ return;
+ }
- auto params = facebook::xplat::jsArgAsObject(args, 0);
- IHttpResource::Headers headers;
- for (auto& header : params["headers"].items()) {
- headers.emplace(header.first.getString(), header.second.getString());
- }
+ auto resource = holder->Module->m_resource;
+ if (!holder->Module->m_isResourceSetup)
+ {
+ SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
+ holder->Module->m_isResourceSetup = true;
+ }
- 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});
+ auto params = facebook::xplat::jsArgAsObject(args, 0);
+ IHttpResource::Headers headers;
+ for (auto& header : params["headers"].items()) {
+ headers.emplace(header.first.getString(), header.second.getString());
}
- );
- }
- },
- {
- "abortRequest",
- [weakHolder = weak_ptr(m_holder)](dynamic args)
+
+ 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});
+ }
+ );
+ }
+ },
{
- auto holder = weakHolder.lock();
- if (!holder)
+ "abortRequest",
+ [weakHolder = weak_ptr(m_holder)](dynamic args)
{
- return;
- }
+ auto holder = weakHolder.lock();
+ if (!holder)
+ {
+ return;
+ }
- auto resource = holder->Module->m_resource;
- if (!holder->Module->m_isResourceSetup)
- {
- SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
- holder->Module->m_isResourceSetup = true;
- }
+ auto resource = holder->Module->m_resource;
+ if (!holder->Module->m_isResourceSetup)
+ {
+ SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
+ holder->Module->m_isResourceSetup = true;
+ }
- resource->AbortRequest(facebook::xplat::jsArgAsInt(args, 0));
- }
- },
- {
- "clearCookies",
- [weakHolder = weak_ptr(m_holder)](dynamic args)
+ resource->AbortRequest(facebook::xplat::jsArgAsInt(args, 0));
+ }
+ },
{
- auto holder = weakHolder.lock();
- if (!holder)
+ "clearCookies",
+ [weakHolder = weak_ptr(m_holder)](dynamic args)
{
- return;
- }
+ auto holder = weakHolder.lock();
+ if (!holder)
+ {
+ return;
+ }
- auto resource = holder->Module->m_resource;
- if (!holder->Module->m_isResourceSetup)
- {
- SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
- holder->Module->m_isResourceSetup = true;
- }
+ auto resource = holder->Module->m_resource;
+ if (!holder->Module->m_isResourceSetup)
+ {
+ SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
+ holder->Module->m_isResourceSetup = true;
+ }
- resource->ClearCookies();
+ resource->ClearCookies();
+ }
}
- }
- };
-}
+ };
+ }
// clang-format on
#pragma endregion CxxModule
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/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp
index 15d23ec1b5b..cd01c56d7dd 100644
--- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp
+++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp
@@ -120,12 +120,12 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co
}
/*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept {
- // Ensure header is in Simple CORS white list
+ // Ensure header is in Simple CORS allowlist
for (const auto &header : request.Headers()) {
if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend())
return false;
- // Ensure Content-Type value is in Simple CORS white list, if present
+ // Ensure Content-Type value is in Simple CORS allowlist, if present
if (boost::iequals(header.Key(), L"Content-Type")) {
if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend())
return false;
@@ -135,12 +135,12 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co
// WinRT separates request headers from request content headers
if (auto content = request.Content()) {
for (const auto &header : content.Headers()) {
- // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it.
+ // WinRT automatically appends non-allowlisted header Content-Length when Content-Type is set. Skip it.
if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() &&
!boost::iequals(header.Key(), "Content-Length"))
return false;
- // Ensure Content-Type value is in Simple CORS white list, if present
+ // Ensure Content-Type value is in Simple CORS allowlist, if present
if (boost::iequals(header.Key(), L"Content-Type")) {
if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend())
return false;
@@ -148,7 +148,7 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co
}
}
- // Ensure method is in Simple CORS white list
+ // Ensure method is in Simple CORS allowlist
return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend();
}
@@ -599,7 +599,7 @@ void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const &respons
}
if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) {
- // Filter out response headers that are not in the Simple CORS whitelist
+ // Filter out response headers that are not in the Simple CORS allowlist
std::queue nonSimpleNames;
for (const auto &header : response.Headers().GetView()) {
if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend())
@@ -651,21 +651,26 @@ ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage
preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString());
auto headerNames = wstring{};
- auto headerItr = coRequest.Headers().begin();
- if (headerItr != coRequest.Headers().end()) {
- headerNames += (*headerItr).Key();
+ auto writeSeparator = false;
+ for (const auto &header : coRequest.Headers()) {
+ if (writeSeparator) {
+ headerNames += L", ";
+ } else {
+ writeSeparator = true;
+ }
- while (++headerItr != coRequest.Headers().end())
- headerNames += L", " + (*headerItr).Key();
+ headerNames += header.Key();
}
if (coRequest.Content()) {
- headerItr = coRequest.Content().Headers().begin();
- if (headerItr != coRequest.Content().Headers().end()) {
- headerNames += (*headerItr).Key();
+ for (const auto &header : coRequest.Content().Headers()) {
+ if (writeSeparator) {
+ headerNames += L", ";
+ } else {
+ writeSeparator = true;
+ }
- while (++headerItr != coRequest.Content().Headers().end())
- headerNames += L", " + (*headerItr).Key();
+ headerNames += header.Key();
}
}
diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp
index 56e9899531f..6022450205b 100644
--- a/vnext/Shared/Networking/WinRTHttpResource.cpp
+++ b/vnext/Shared/Networking/WinRTHttpResource.cpp
@@ -51,8 +51,51 @@ 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
+void AttachMultipartHeaders(IHttpContent content, const dynamic &headers) {
+ HttpMediaTypeHeaderValue contentType{nullptr};
+
+ // Headers are generally case-insensitive
+ // https://www.ietf.org/rfc/rfc2616.txt section 4.2
+ // TODO: Consolidate with PerformRequest's header parsing.
+ for (auto &header : headers.items()) {
+ auto &name = header.first.getString();
+ auto &value = header.second.getString();
+
+ if (boost::iequals(name.c_str(), "Content-Type")) {
+ contentType = HttpMediaTypeHeaderValue::Parse(to_hstring(value));
+ } else if (boost::iequals(name.c_str(), "Authorization")) {
+ bool success = content.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value));
+ if (!success) {
+ throw hresult_error{E_INVALIDARG, L"Failed to append Authorization"};
+ }
+ } else {
+ content.Headers().Append(to_hstring(name), to_hstring(value));
+ }
+ }
+
+ if (contentType) {
+ content.Headers().ContentType(contentType);
+ }
+}
+
#pragma region WinRTHttpResource
WinRTHttpResource::WinRTHttpResource(IHttpClient &&client) noexcept : m_client{std::move(client)} {}
@@ -81,20 +124,23 @@ IAsyncOperation WinRTHttpResource::CreateRequest(
// Headers are generally case-insensitive
// https://www.ietf.org/rfc/rfc2616.txt section 4.2
for (auto &header : reqArgs->Headers) {
- if (boost::iequals(header.first.c_str(), "Content-Type")) {
- bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(header.second), contentType);
+ auto &name = header.first;
+ auto &value = header.second;
+
+ if (boost::iequals(name.c_str(), "Content-Type")) {
+ bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(value), contentType);
if (!success) {
if (self->m_onError) {
self->m_onError(reqArgs->RequestId, "Failed to parse Content-Type", false);
}
co_return nullptr;
}
- } else if (boost::iequals(header.first.c_str(), "Content-Encoding")) {
- contentEncoding = header.second;
- } else if (boost::iequals(header.first.c_str(), "Content-Length")) {
- contentLength = header.second;
- } else if (boost::iequals(header.first.c_str(), "Authorization")) {
- bool success = request.Headers().TryAppendWithoutValidation(to_hstring(header.first), to_hstring(header.second));
+ } else if (boost::iequals(name.c_str(), "Content-Encoding")) {
+ contentEncoding = value;
+ } else if (boost::iequals(name.c_str(), "Content-Length")) {
+ contentLength = value;
+ } else if (boost::iequals(name.c_str(), "Authorization")) {
+ bool success = request.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value));
if (!success) {
if (self->m_onError) {
self->m_onError(reqArgs->RequestId, "Failed to append Authorization", false);
@@ -103,7 +149,7 @@ IAsyncOperation WinRTHttpResource::CreateRequest(
}
} else {
try {
- request.Headers().Append(to_hstring(header.first), to_hstring(header.second));
+ request.Headers().Append(to_hstring(name), to_hstring(value));
} catch (hresult_error const &e) {
if (self->m_onError) {
self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false);
@@ -146,9 +192,31 @@ IAsyncOperation WinRTHttpResource::CreateRequest(
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 if (!data["formData"].empty()) {
+ winrt::Windows::Web::Http::HttpMultipartFormDataContent multiPartContent;
+ auto formData = data["formData"];
+
+ // #6046 - Overwriting WinRT's HttpMultipartFormDataContent implicit Content-Type clears the generated boundary
+ contentType = nullptr;
+
+ for (auto &formDataPart : formData) {
+ IHttpContent formContent{nullptr};
+ if (!formDataPart["string"].isNull()) {
+ formContent = HttpStringContent{to_hstring(formDataPart["string"].asString())};
+ } else if (!formDataPart["uri"].empty()) {
+ auto filePath = to_hstring(formDataPart["uri"].asString());
+ auto file = co_await StorageFile::GetFileFromPathAsync(filePath);
+ auto stream = co_await file.OpenReadAsync();
+ formContent = HttpStreamContent{stream};
+ }
+
+ if (formContent) {
+ AttachMultipartHeaders(formContent, formDataPart["headers"]);
+ multiPartContent.Add(formContent, to_hstring(formDataPart["fieldName"].asString()));
+ }
+ } // foreach form data part
+
+ content = multiPartContent;
}
}
@@ -205,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);
@@ -283,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*/ {
@@ -316,11 +400,18 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect
auto props = winrt::multi_threaded_map();
props.Insert(L"RequestArgs", coArgs);
- auto coRequest = co_await CreateRequest(std::move(coMethod), std::move(coUri), props);
- if (!coRequest) {
- co_return;
+ auto coRequestOp = CreateRequest(std::move(coMethod), std::move(coUri), props);
+ co_await lessthrow_await_adapter>{coRequestOp};
+ auto coRequestOpHR = coRequestOp.ErrorCode();
+ if (coRequestOpHR < 0) {
+ if (self->m_onError) {
+ self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(coRequestOpHR)), false);
+ }
+ co_return self->UntrackResponse(reqArgs->RequestId);
}
+ auto coRequest = coRequestOp.GetResults();
+
// 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());
@@ -332,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) {
@@ -345,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) {
@@ -411,55 +509,86 @@ WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspect
auto inputStream = co_await response.Content().ReadAsInputStreamAsync();
auto reader = DataReader{inputStream};
- // #9510 - 10mb limit on fetch
- co_await reader.LoadAsync(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)) {
- auto bytes = vector(reader.UnconsumedBufferLength());
- reader.ReadBytes(bytes);
- auto blob = responseHandler->ToResponseData(std::move(bytes));
+ vector responseData{};
+ while (auto loaded = co_await reader.LoadAsync(segmentSize)) {
+ auto length = reader.UnconsumedBufferLength();
+ auto data = vector(length);
+ reader.ReadBytes(data);
+
+ responseData.insert(responseData.cend(), data.cbegin(), data.cend());
+ }
+
+ auto blob = responseHandler->ToResponseData(std::move(responseData));
if (self->m_onDataDynamic && self->m_onRequestSuccess) {
self->m_onDataDynamic(reqArgs->RequestId, std::move(blob));
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 - We currently accumulate all incoming request data in 10MB chunks.
- uint32_t segmentSize = 10 * 1024 * 1024;
+ int64_t receivedBytes = 0;
string responseData;
winrt::Windows::Storage::Streams::IBuffer buffer;
- uint32_t length;
- do {
- co_await reader.LoadAsync(segmentSize);
- length = reader.UnconsumedBufferLength();
+ while (auto loaded = co_await reader.LoadAsync(segmentSize)) {
+ auto length = reader.UnconsumedBufferLength();
+ receivedBytes += length;
if (isText) {
- auto data = std::vector(length);
+ 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*/);
+ }
}
- } while (length > 0);
+ }
- 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;
diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp
index 59fa2e23a8f..ecf7874d0ca 100644
--- a/vnext/Shared/OInstance.cpp
+++ b/vnext/Shared/OInstance.cpp
@@ -27,7 +27,6 @@
#include
#include
-#include
#include
#include
#include
@@ -73,11 +72,7 @@ namespace Microsoft::React {
/*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(inspectableProperties);
- }
+ return std::make_unique(inspectableProperties);
}
} // namespace Microsoft::React
@@ -638,8 +633,7 @@ std::vector> InstanceImpl::GetDefaultNativeModules
// If this code is enabled, we will have unused module instances.
// Also, MSRN has a different property bag mechanism incompatible with this method's transitionalProps variable.
#if (defined(_MSC_VER) && !defined(WINRT))
- if (Microsoft::React::GetRuntimeOptionBool("Blob.EnableModule") &&
- !Microsoft::React::GetRuntimeOptionBool("Http.UseMonolithicModule")) {
+ if (Microsoft::React::GetRuntimeOptionBool("Blob.EnableModule")) {
modules.push_back(std::make_unique(
m_innerInstance,
Microsoft::React::GetBlobModuleName(),
diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems
index b08b35b58ea..ebca7b446c0 100644
--- a/vnext/Shared/Shared.vcxitems
+++ b/vnext/Shared/Shared.vcxitems
@@ -50,7 +50,9 @@
-
+
+ true
+
diff --git a/vnext/src/IntegrationTests/BlobTest.js b/vnext/src/IntegrationTests/BlobTest.js
index a9a87a365d5..4070b8f8176 100644
--- a/vnext/src/IntegrationTests/BlobTest.js
+++ b/vnext/src/IntegrationTests/BlobTest.js
@@ -16,7 +16,7 @@ const {TestModule} = ReactNative.NativeModules;
type State = {
statusCode: number,
xhr: XMLHttpRequest,
- expected: String,
+ expected: string,
};
class BlobTest extends React.Component<{...}, State> {
@@ -24,123 +24,122 @@ class BlobTest extends React.Component<{...}, State> {
statusCode: 0,
xhr: new XMLHttpRequest(),
// https://www.facebook.com/favicon.ico
- expected: new String(
+ expected:
'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/',
- ),
+ '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 = () => {