diff --git a/CMakeLists.txt b/CMakeLists.txt index 481e848f..0d27daf7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,7 @@ option(JSRUNTIMEHOST_POLYFILL_FILE "Include JsRuntimeHost Polyfill File and File option(JSRUNTIMEHOST_POLYFILL_PERFORMANCE "Include JsRuntimeHost Polyfill Performance." ON) option(JSRUNTIMEHOST_POLYFILL_TEXTDECODER "Include JsRuntimeHost Polyfill TextDecoder." ON) option(JSRUNTIMEHOST_POLYFILL_TEXTENCODER "Include JsRuntimeHost Polyfill TextEncoder." ON) +option(JSRUNTIMEHOST_POLYFILL_STREAMS "Include JsRuntimeHost Polyfill Web Streams." ON) # Sanitizers option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF) diff --git a/Polyfills/Blob/CMakeLists.txt b/Polyfills/Blob/CMakeLists.txt index 1b77ef6d..7577e668 100644 --- a/Polyfills/Blob/CMakeLists.txt +++ b/Polyfills/Blob/CMakeLists.txt @@ -1,5 +1,6 @@ set(SOURCES "Include/Babylon/Polyfills/Blob.h" + "README.md" "Source/Blob.cpp" "Source/Blob.h") diff --git a/Polyfills/Blob/README.md b/Polyfills/Blob/README.md new file mode 100644 index 00000000..2206f9c4 --- /dev/null +++ b/Polyfills/Blob/README.md @@ -0,0 +1,12 @@ +# Blob + +Provides the browser `Blob` constructor, byte/text readers, zero-copy Blob +composition and slicing, and a lazily pulled byte `ReadableStream`. + +Call `Babylon::Polyfills::Streams::Initialize` before using `Blob.stream()` on +engines that do not provide Web Streams. Stream reads copy only the requested +chunk into JavaScript-owned memory; composing Blobs and slicing share immutable +native byte segments. + +The focused tests are adapted from WPT `FileAPI/blob`, WebKit's Blob stream +chunk/crash regressions, and Firefox's large Blob `pipeTo` regression. diff --git a/Polyfills/Blob/Source/Blob.cpp b/Polyfills/Blob/Source/Blob.cpp index 789c346c..4dcb4832 100644 --- a/Polyfills/Blob/Source/Blob.cpp +++ b/Polyfills/Blob/Source/Blob.cpp @@ -2,8 +2,63 @@ #include #include +#include +#include +#include + namespace Babylon::Polyfills::Internal { + struct Blob::Segment + { + std::shared_ptr> Bytes; + size_t Offset{}; + size_t Length{}; + }; + + struct Blob::Data + { + std::vector Segments; + size_t Size{}; + + void Append(const Segment& segment) + { + if (segment.Length == 0) + { + return; + } + + Segments.emplace_back(segment); + Size += segment.Length; + } + + void CopyTo(size_t position, std::byte* destination, size_t length) const + { + size_t segmentStart{}; + for (const auto& segment : Segments) + { + const auto segmentEnd = segmentStart + segment.Length; + if (position < segmentEnd && length > 0) + { + const auto localOffset = position > segmentStart ? position - segmentStart : 0; + const auto copyLength = std::min(segment.Length - localOffset, length); + std::memcpy(destination, segment.Bytes->data() + segment.Offset + localOffset, copyLength); + destination += copyLength; + position += copyLength; + length -= copyLength; + } + segmentStart = segmentEnd; + } + } + }; + + struct Blob::StreamState + { + std::shared_ptr BlobData; + size_t Position{}; + size_t SegmentIndex{}; + size_t SegmentOffset{}; + }; + void Blob::Initialize(Napi::Env env) { static constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob"; @@ -18,8 +73,14 @@ namespace Babylon::Polyfills::Internal InstanceMethod("text", &Blob::Text), InstanceMethod("arrayBuffer", &Blob::ArrayBuffer), InstanceMethod("bytes", &Blob::Bytes), + InstanceMethod("slice", &Blob::Slice), + InstanceMethod("stream", &Blob::Stream), }); + auto descriptor = Napi::Object::New(env); + descriptor.Set("configurable", true); + descriptor.Set("value", JS_BLOB_CONSTRUCTOR_NAME); + env.Global().Get("Object").As().Get("defineProperty").As().Call(env.Global().Get("Object"), {func.Get("prototype"), Napi::Symbol::WellKnown(env, "toStringTag"), descriptor}); env.Global().Set(JS_BLOB_CONSTRUCTOR_NAME, func); } } @@ -27,36 +88,122 @@ namespace Babylon::Polyfills::Internal Blob::Blob(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { - if (info.Length() > 0) + auto env = Env(); + auto data = std::make_shared(); + std::vector stringSegmentIndices; + bool convertLineEndings{}; + + if (info.Length() > 0 && !info[0].IsUndefined()) { - const auto blobParts = info[0].As(); + if (!info[0].IsObject() && !info[0].IsFunction()) + { + throw Napi::TypeError::New(env, "Blob parts must be an iterable object."); + } + + const auto blobParts = info[0].As(); + if (blobParts.IsArray()) + { + data->Segments.reserve(blobParts.As().Length()); + } - if (blobParts.Length() > 0) + const auto reflect = env.Global().Get("Reflect").As(); + const auto iteratorMethod = reflect.Get("get").As().Call( + reflect, + {blobParts, Napi::Symbol::WellKnown(env, "iterator")}); + if (!iteratorMethod.IsFunction()) { - const auto firstPart = blobParts.Get(uint32_t{0}); - ProcessBlobPart(firstPart); + throw Napi::TypeError::New(env, "Blob parts must be iterable."); + } - if (blobParts.Length() > 1) + const auto iterator = iteratorMethod.As().Call(blobParts, {}).As(); + const auto next = iterator.Get("next").As(); + try + { + while (true) { - throw Napi::Error::New(Env(), "Using multiple BlobParts in Blob constructor is not implemented."); + const auto result = next.Call(iterator, {}).As(); + if (result.Get("done").ToBoolean().Value()) + { + break; + } + + if (AppendBlobPart(*data, result.Get("value"))) + { + stringSegmentIndices.emplace_back(data->Segments.size() - 1); + } } } + catch (...) + { + try + { + const auto returnMethod = iterator.Get("return"); + if (returnMethod.IsFunction()) + { + returnMethod.As().Call(iterator, {}); + } + } + catch (...) + { + // Preserve the exception that interrupted part conversion. + } + throw; + } } - if (info.Length() > 1) + if (info.Length() > 1 && !info[1].IsUndefined() && !info[1].IsNull()) { + if (!info[1].IsObject() && !info[1].IsFunction()) + { + throw Napi::TypeError::New(env, "Blob options must be an object."); + } + const auto options = info[1].As(); + const auto endings = options.Get("endings"); + if (!endings.IsUndefined()) + { + const auto value = endings.ToString().Utf8Value(); + if (value != "transparent" && value != "native") + { + throw Napi::TypeError::New(env, "Blob endings must be 'transparent' or 'native'."); + } + convertLineEndings = value == "native"; + } - if (options.Has("type")) + const auto type = options.Get("type"); + if (!type.IsUndefined()) { - m_type = options.Get("type").As().Utf8Value(); + m_type = NormalizeType(type.ToString().Utf8Value()); } } + + if (convertLineEndings) + { + for (const auto segmentIndex : stringSegmentIndices) + { + auto& segment = data->Segments[segmentIndex]; + auto value = std::string{ + reinterpret_cast(segment.Bytes->data() + segment.Offset), + segment.Length}; + value = NormalizeLineEndings(std::move(value)); + + auto bytes = std::make_shared>(value.size()); + if (!value.empty()) + { + std::memcpy(bytes->data(), value.data(), value.size()); + } + data->Size -= segment.Length; + segment = {std::move(bytes), 0, value.size()}; + data->Size += segment.Length; + } + } + + m_data = std::move(data); } Napi::Value Blob::GetSize(const Napi::CallbackInfo&) { - return Napi::Value::From(Env(), m_data.size()); + return Napi::Value::From(Env(), m_data->Size); } Napi::Value Blob::GetType(const Napi::CallbackInfo&) @@ -67,8 +214,11 @@ namespace Babylon::Polyfills::Internal Napi::Value Blob::Text(const Napi::CallbackInfo&) { // NOTE: This will not check for UTF-8 validity - const auto begin = reinterpret_cast(m_data.data()); - std::string text(begin, m_data.size()); + std::string text(m_data->Size, '\0'); + if (!text.empty()) + { + m_data->CopyTo(0, reinterpret_cast(text.data()), text.size()); + } const auto deferred = Napi::Promise::Deferred::New(Env()); deferred.Resolve(Napi::String::New(Env(), text)); @@ -77,59 +227,334 @@ namespace Babylon::Polyfills::Internal Napi::Value Blob::ArrayBuffer(const Napi::CallbackInfo&) { - const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size()); - if (m_data.data()) - { - std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size()); - } - const auto deferred = Napi::Promise::Deferred::New(Env()); - deferred.Resolve(arrayBuffer); + deferred.Resolve(CreateArrayBuffer()); return deferred.Promise(); } Napi::Value Blob::Bytes(const Napi::CallbackInfo&) { - const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size()); - if (m_data.data()) - { - std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size()); - } - const auto uint8Array = Napi::Uint8Array::New(Env(), m_data.size(), arrayBuffer, 0); + const auto arrayBuffer = CreateArrayBuffer(); + const auto uint8Array = Napi::Uint8Array::New(Env(), m_data->Size, arrayBuffer, 0); const auto deferred = Napi::Promise::Deferred::New(Env()); deferred.Resolve(uint8Array); return deferred.Promise(); } - void Blob::ProcessBlobPart(const Napi::Value& blobPart) + Napi::Value Blob::Slice(const Napi::CallbackInfo& info) { + const auto start = NormalizeSliceIndex( + info.Length() > 0 && !info[0].IsUndefined() ? info[0].ToNumber().DoubleValue() : 0, + m_data->Size); + const auto end = NormalizeSliceIndex( + info.Length() > 1 && !info[1].IsUndefined() ? info[1].ToNumber().DoubleValue() : static_cast(m_data->Size), + m_data->Size); + const auto sliceEnd = std::max(start, end); + + auto sliceData = std::make_shared(); + size_t segmentStart{}; + for (const auto& segment : m_data->Segments) + { + const auto segmentEnd = segmentStart + segment.Length; + const auto overlapStart = std::max(start, segmentStart); + const auto overlapEnd = std::min(sliceEnd, segmentEnd); + if (overlapStart < overlapEnd) + { + sliceData->Append({segment.Bytes, segment.Offset + overlapStart - segmentStart, overlapEnd - overlapStart}); + } + segmentStart = segmentEnd; + if (segmentStart >= sliceEnd) + { + break; + } + } + + std::string contentType; + if (info.Length() > 2 && !info[2].IsUndefined()) + { + contentType = NormalizeType(info[2].ToString().Utf8Value()); + } + + const auto blobConstructor = Env().Global().Get("Blob").As(); + const auto result = blobConstructor.New({Napi::Array::New(Env())}); + auto resultBlob = Napi::ObjectWrap::Unwrap(result); + resultBlob->m_data = std::move(sliceData); + resultBlob->m_type = std::move(contentType); + return result; + } + + Napi::Value Blob::Stream(const Napi::CallbackInfo& info) + { + auto env = info.Env(); + const auto readableStreamValue = env.Global().Get("ReadableStream"); + if (!readableStreamValue.IsFunction()) + { + throw Napi::TypeError::New(env, "Blob.stream() requires ReadableStream to be installed."); + } + + auto state = std::make_shared(); + state->BlobData = m_data; + + auto source = Napi::Object::New(env); + source.Set("type", "bytes"); + source.Set("pull", Napi::Function::New(env, [state](const Napi::CallbackInfo& callbackInfo) -> Napi::Value { + auto callbackEnv = callbackInfo.Env(); + auto controller = callbackInfo[0].As(); + if (!state->BlobData || state->Position >= state->BlobData->Size) + { + controller.Get("close").As().Call(controller, {}); + state->BlobData.reset(); + return callbackEnv.Undefined(); + } + + constexpr size_t CHUNK_SIZE = 64 * 1024; + const auto copyInto = [&state](std::byte* destination, size_t outputLength) { + auto segmentIndex = state->SegmentIndex; + auto segmentOffset = state->SegmentOffset; + size_t remaining = outputLength; + + while (remaining > 0) + { + const auto& segment = state->BlobData->Segments[segmentIndex]; + const auto copyLength = std::min(segment.Length - segmentOffset, remaining); + std::memcpy(destination, segment.Bytes->data() + segment.Offset + segmentOffset, copyLength); + destination += copyLength; + remaining -= copyLength; + segmentOffset += copyLength; + if (segmentOffset == segment.Length) + { + ++segmentIndex; + segmentOffset = 0; + } + } + + state->Position += outputLength; + state->SegmentIndex = segmentIndex; + state->SegmentOffset = segmentOffset; + }; + + const auto byobRequestValue = controller.Get("byobRequest"); + if (!byobRequestValue.IsUndefined() && !byobRequestValue.IsNull()) + { + const auto byobRequest = byobRequestValue.As(); + const auto viewValue = byobRequest.Get("view"); + if (!viewValue.IsTypedArray()) + { + throw Napi::TypeError::New(callbackEnv, "Blob.stream() received an invalid BYOB request view."); + } + + const auto view = viewValue.As(); + const auto outputLength = std::min({CHUNK_SIZE, view.ByteLength(), state->BlobData->Size - state->Position}); + const auto outputBuffer = view.ArrayBuffer(); + auto destination = static_cast(outputBuffer.Data()) + view.ByteOffset(); + copyInto(destination, outputLength); + byobRequest.Get("respond").As().Call(byobRequest, {Napi::Number::New(callbackEnv, outputLength)}); + } + else + { + const auto outputLength = std::min(CHUNK_SIZE, state->BlobData->Size - state->Position); + auto outputBuffer = Napi::ArrayBuffer::New(callbackEnv, outputLength); + copyInto(static_cast(outputBuffer.Data()), outputLength); + auto output = Napi::Uint8Array::New(callbackEnv, outputLength, outputBuffer, 0); + controller.Get("enqueue").As().Call(controller, {output}); + } + + if (state->Position == state->BlobData->Size) + { + controller.Get("close").As().Call(controller, {}); + state->BlobData.reset(); + } + + return callbackEnv.Undefined(); + })); + source.Set("cancel", Napi::Function::New(env, [state](const Napi::CallbackInfo& callbackInfo) -> Napi::Value { + state->BlobData.reset(); + return callbackInfo.Env().Undefined(); + })); + + return readableStreamValue.As().New({source}); + } + + bool Blob::AppendBlobPart(Data& data, const Napi::Value& blobPart) + { + const std::byte* source{}; + size_t length{}; + bool isBufferSource{}; + if (blobPart.IsArrayBuffer()) { const auto buffer = blobPart.As(); - const auto begin = static_cast(buffer.Data()); - m_data.assign(begin, begin + buffer.ByteLength()); + source = static_cast(buffer.Data()); + length = buffer.ByteLength(); + isBufferSource = true; } - else if (blobPart.IsTypedArray() || blobPart.IsDataView()) + else if (blobPart.IsTypedArray()) { const auto array = blobPart.As(); const auto buffer = array.ArrayBuffer(); - const auto begin = static_cast(buffer.Data()) + array.ByteOffset(); - m_data.assign(begin, begin + array.ByteLength()); + const auto bufferData = static_cast(buffer.Data()); + source = bufferData == nullptr ? nullptr : bufferData + array.ByteOffset(); + length = array.ByteLength(); + isBufferSource = true; + } + else if (blobPart.IsDataView()) + { + const auto view = blobPart.As(); + const auto buffer = view.ArrayBuffer(); + const auto bufferData = static_cast(buffer.Data()); + source = bufferData == nullptr ? nullptr : bufferData + view.ByteOffset(); + length = view.ByteLength(); + isBufferSource = true; + } + else if (blobPart.IsObject()) + { + const auto object = blobPart.As(); + const auto blobConstructor = Env().Global().Get("Blob").As(); + if (object.InstanceOf(blobConstructor)) + { + auto nativeBlob = object; + if (!object.Get("constructor").StrictEquals(blobConstructor)) + { + nativeBlob = object.Get("slice").As().Call(object, {Napi::Number::New(Env(), 0), object.Get("size")}).As(); + } + + const auto blob = Napi::ObjectWrap::Unwrap(nativeBlob); + data.Segments.reserve(data.Segments.size() + blob->m_data->Segments.size()); + for (const auto& segment : blob->m_data->Segments) + { + data.Append(segment); + } + return false; + } + } + + if (isBufferSource) + { + auto bytes = std::make_shared>(length); + if (length > 0) + { + std::memcpy(bytes->data(), source, length); + } + data.Append({std::move(bytes), 0, length}); + return false; + } + + auto value = blobPart.ToString().Utf8Value(); + auto bytes = std::make_shared>(value.size()); + if (!value.empty()) + { + std::memcpy(bytes->data(), value.data(), value.size()); + } + data.Append({std::move(bytes), 0, value.size()}); + return !value.empty(); + } + + Napi::ArrayBuffer Blob::CreateArrayBuffer() const + { + auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data->Size); + if (m_data->Size > 0) + { + m_data->CopyTo(0, static_cast(arrayBuffer.Data()), m_data->Size); } - else if (blobPart.IsString()) + return arrayBuffer; + } + + std::string Blob::NormalizeType(std::string type) + { + for (auto& character : type) { - const auto str = blobPart.As().Utf8Value(); - const auto begin = reinterpret_cast(str.data()); - m_data.assign(begin, begin + str.length()); + const auto value = static_cast(character); + if (value < 0x20 || value > 0x7E) + { + return {}; + } + if (character >= 'A' && character <= 'Z') + { + character = static_cast(character - 'A' + 'a'); + } } - else + return type; + } + + std::string Blob::NormalizeLineEndings(std::string value) + { +#ifdef _WIN32 + static constexpr auto nativeNewline = "\r\n"; +#else + static constexpr auto nativeNewline = "\n"; +#endif + size_t outputLength{}; + for (size_t index{}; index < value.size(); ++index) { - // Assume it's another Blob object - const auto obj = blobPart.As(); - const auto blobObj = Napi::ObjectWrap::Unwrap(obj); - m_data.assign(blobObj->m_data.begin(), blobObj->m_data.end()); + if (value[index] == '\r') + { + if (index + 1 < value.size() && value[index + 1] == '\n') + { + ++index; + } + outputLength += std::char_traits::length(nativeNewline); + } + else if (value[index] == '\n') + { + outputLength += std::char_traits::length(nativeNewline); + } + else + { + ++outputLength; + } } + + std::string result; + result.reserve(outputLength); + for (size_t index{}; index < value.size(); ++index) + { + if (value[index] == '\r') + { + if (index + 1 < value.size() && value[index + 1] == '\n') + { + ++index; + } + result.append(nativeNewline); + } + else if (value[index] == '\n') + { + result.append(nativeNewline); + } + else + { + result.push_back(value[index]); + } + } + return result; + } + + size_t Blob::NormalizeSliceIndex(double value, size_t size) + { + if (std::isnan(value)) + { + return 0; + } + + const auto magnitude = std::abs(value); + const auto lower = std::floor(magnitude); + const auto fraction = magnitude - lower; + auto rounded = lower; + if (fraction > 0.5 || (fraction == 0.5 && std::fmod(lower, 2.0) != 0.0)) + { + rounded += 1.0; + } + + if (value < 0) + { + return !std::isfinite(rounded) || rounded >= static_cast(size) + ? 0 + : size - static_cast(rounded); + } + + return !std::isfinite(rounded) || rounded >= static_cast(size) + ? size + : static_cast(rounded); } } diff --git a/Polyfills/Blob/Source/Blob.h b/Polyfills/Blob/Source/Blob.h index df04e834..11979e1c 100644 --- a/Polyfills/Blob/Source/Blob.h +++ b/Polyfills/Blob/Source/Blob.h @@ -2,8 +2,9 @@ #include -#include +#include #include +#include namespace Babylon::Polyfills::Internal { @@ -20,10 +21,21 @@ namespace Babylon::Polyfills::Internal Napi::Value Text(const Napi::CallbackInfo& info); Napi::Value ArrayBuffer(const Napi::CallbackInfo& info); Napi::Value Bytes(const Napi::CallbackInfo& info); + Napi::Value Slice(const Napi::CallbackInfo& info); + Napi::Value Stream(const Napi::CallbackInfo& info); + + struct Segment; + struct Data; + struct StreamState; + + bool AppendBlobPart(Data& data, const Napi::Value& blobPart); + Napi::ArrayBuffer CreateArrayBuffer() const; - void ProcessBlobPart(const Napi::Value& blobPart); + static std::string NormalizeType(std::string type); + static std::string NormalizeLineEndings(std::string value); + static size_t NormalizeSliceIndex(double value, size_t size); - std::vector m_data; + std::shared_ptr m_data; std::string m_type; }; -} \ No newline at end of file +} diff --git a/Polyfills/CMakeLists.txt b/Polyfills/CMakeLists.txt index a44765fb..c952cf27 100644 --- a/Polyfills/CMakeLists.txt +++ b/Polyfills/CMakeLists.txt @@ -44,4 +44,8 @@ endif() if(JSRUNTIMEHOST_POLYFILL_TEXTENCODER) add_subdirectory(TextEncoder) -endif() \ No newline at end of file +endif() + +if(JSRUNTIMEHOST_POLYFILL_STREAMS) + add_subdirectory(Streams) +endif() diff --git a/Polyfills/Fetch/CMakeLists.txt b/Polyfills/Fetch/CMakeLists.txt index 62b46175..24346db0 100644 --- a/Polyfills/Fetch/CMakeLists.txt +++ b/Polyfills/Fetch/CMakeLists.txt @@ -1,12 +1,25 @@ +set(FETCH_POLYFILL_FILE "${CMAKE_CURRENT_SOURCE_DIR}/Source/FetchPolyfill.js") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${FETCH_POLYFILL_FILE}") +file(READ "${FETCH_POLYFILL_FILE}" FETCH_POLYFILL_SOURCE) +configure_file( + "Source/FetchScripts.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/Generated/FetchScripts.h" + @ONLY) + set(SOURCES "Include/Babylon/Polyfills/Fetch.h" + "Readme.md" "Source/Fetch.h" - "Source/Fetch.cpp") + "Source/Fetch.cpp" + "Source/FetchPolyfill.js" + "Source/FetchScripts.h.in") add_library(Fetch ${SOURCES}) warnings_as_errors(Fetch) -target_include_directories(Fetch PUBLIC "Include") +target_include_directories(Fetch + PUBLIC "Include" + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/Generated") target_link_libraries(Fetch PUBLIC JsRuntime diff --git a/Polyfills/Fetch/Readme.md b/Polyfills/Fetch/Readme.md index 00c41cfc..b3d21a4e 100644 --- a/Polyfills/Fetch/Readme.md +++ b/Polyfills/Fetch/Readme.md @@ -1,5 +1,7 @@ # Fetch -Minimal implementation of the [WHATWG `fetch()`](https://fetch.spec.whatwg.org/) API. Like `XMLHttpRequest`, it is implemented on top of the platform-specific transports in the `UrlLib` dependency, so network behavior (libcurl / WinHTTP / etc.) is identical between the two polyfills. +Implementation of the [WHATWG `fetch()`](https://fetch.spec.whatwg.org/) API. +Like `XMLHttpRequest`, it uses the platform-specific transport from `UrlLib`, so +network behavior is shared between the two polyfills. ```js const response = await fetch("https://example.com/data.json"); @@ -8,16 +10,24 @@ if (response.ok) { } ``` -## Response -`fetch()` returns a `Promise` that resolves to a `Response`-like object exposing: -* `ok`, `status`, `statusText`, `url`, `redirected`, `type`, `bodyUsed` -* `headers` with `get(name)`, `has(name)`, and `forEach(callback)` (header names are matched case-insensitively) -* `text()`, `arrayBuffer()`, `json()`, `blob()` (each returns a `Promise`) -* `clone()` +## Headers and Response +The module installs `Headers` and `Response` when the host does not already +provide them. Response bodies use a `ReadableStream` and follow the browser's +single-consumption behavior through `bodyUsed`. The supported body readers are +`arrayBuffer()`, `blob()`, `bytes()`, `json()`, and `text()`. -The response body is fully buffered before the promise resolves. The body accessors may therefore be called more than once (`bodyUsed` is always reported as `false`), which is a deliberate, lenient deviation from the spec's single-use semantics. +Initialize the Streams, Blob, TextEncoder, TextDecoder, and URL polyfills before +using body-bearing responses on engines that do not provide those globals. -`blob()` requires the `Blob` polyfill to be initialized; otherwise the returned promise rejects. +A completed native response is copied once into JavaScript-owned memory and +exposed as a byte stream. Body readers retain chunk references while consuming +a stream and allocate a contiguous result at most once when that result requires +one. Repeated header iteration reuses a mutation-versioned normalized view, and +header deletion and replacement compact the field list in place. + +The focused conformance tests are adapted from WPT `fetch/api/headers` and +`fetch/api/response`, plus WebKit, Firefox, and Chromium response-body +regressions. ## Local files Like `XMLHttpRequest`, `fetch()` supports loading local resources: @@ -60,4 +70,3 @@ The rejection's `stack` is captured synchronously at the `fetch()` call site (be is handed to a worker thread), so crash reports can attribute the failing call rather than an empty scheduler tick. (Engines that only materialize `.stack` when an error is thrown may omit the frames.) - diff --git a/Polyfills/Fetch/Source/Fetch.cpp b/Polyfills/Fetch/Source/Fetch.cpp index 7a500df7..0168a429 100644 --- a/Polyfills/Fetch/Source/Fetch.cpp +++ b/Polyfills/Fetch/Source/Fetch.cpp @@ -1,4 +1,5 @@ #include "Fetch.h" +#include "FetchScripts.h" #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -20,16 +22,6 @@ namespace Babylon::Polyfills::Internal { namespace { - // Buffered response payload shared between the Response object and any clones it produces. - struct ResponseData - { - int statusCode{}; - std::string statusText; - std::string url; - std::vector> headers; - std::vector body; - }; - // Shared state for honoring an AbortSignal passed via init.signal. Co-owned by the "abort" // listener (which sets the flag, captures the reason, and cancels the transport) and the // completion continuation (which reports the AbortError and tears the listener down). @@ -63,10 +55,202 @@ namespace Babylon::Polyfills::Internal }); } + struct DataUrlResponse + { + std::string contentType; + std::string url; + std::vector body; + }; + + bool IsAsciiWhitespace(uint8_t value) + { + return value == 0x09 || value == 0x0A || value == 0x0C || value == 0x0D || value == 0x20; + } + + void TrimAsciiWhitespace(std::string& value) + { + const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char character) { + return IsAsciiWhitespace(character); + }); + const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char character) { + return IsAsciiWhitespace(character); + }).base(); + value = first < last ? std::string{first, last} : std::string{}; + } + + int HexDigitValue(char value) + { + if (value >= '0' && value <= '9') + { + return value - '0'; + } + if (value >= 'a' && value <= 'f') + { + return value - 'a' + 10; + } + if (value >= 'A' && value <= 'F') + { + return value - 'A' + 10; + } + return -1; + } + + template + void ForEachPercentDecodedByte(std::string_view value, TCallback&& callback) + { + for (size_t index = 0; index < value.size(); ++index) + { + if (value[index] == '%' && index + 2 < value.size()) + { + const int high = HexDigitValue(value[index + 1]); + const int low = HexDigitValue(value[index + 2]); + if (high >= 0 && low >= 0) + { + callback(static_cast((high << 4) | low)); + index += 2; + continue; + } + } + callback(static_cast(value[index])); + } + } + + std::vector PercentDecode(std::string_view value) + { + std::vector decoded; + decoded.reserve(value.size()); + ForEachPercentDecodedByte(value, [&decoded](uint8_t byte) { + decoded.push_back(byte); + }); + return decoded; + } + + int Base64DigitValue(uint8_t value) + { + if (value >= 'A' && value <= 'Z') + { + return value - 'A'; + } + if (value >= 'a' && value <= 'z') + { + return value - 'a' + 26; + } + if (value >= '0' && value <= '9') + { + return value - '0' + 52; + } + if (value == '+') + { + return 62; + } + if (value == '/') + { + return 63; + } + return -1; + } + + std::vector ForgivingBase64Decode(std::string_view input) + { + size_t digitCount{}; + size_t paddingCount{}; + bool sawPadding{}; + ForEachPercentDecodedByte(input, [&](uint8_t value) { + if (IsAsciiWhitespace(value)) + { + return; + } + if (value == '=') + { + sawPadding = true; + ++paddingCount; + return; + } + if (sawPadding || Base64DigitValue(value) < 0) + { + throw std::runtime_error{"fetch: invalid base64 data URL"}; + } + ++digitCount; + }); + + const auto encodedCount = digitCount + paddingCount; + if ((paddingCount > 0 && (encodedCount % 4 != 0 || paddingCount > 2)) || digitCount % 4 == 1) + { + throw std::runtime_error{"fetch: invalid base64 data URL"}; + } + + std::vector decoded(digitCount / 4 * 3 + digitCount % 4 * 3 / 4); + size_t outputIndex{}; + uint32_t accumulator{}; + size_t availableBits{}; + ForEachPercentDecodedByte(input, [&](uint8_t value) { + if (IsAsciiWhitespace(value) || value == '=') + { + return; + } + accumulator = (accumulator << 6) | static_cast(Base64DigitValue(value)); + availableBits += 6; + if (availableBits >= 8) + { + availableBits -= 8; + decoded[outputIndex++] = static_cast(accumulator >> availableBits); + accumulator &= (uint32_t{1} << availableBits) - 1; + } + }); + return decoded; + } + + std::optional ParseDataUrl(std::string_view url) + { + if (url.size() < 5 || !EqualsIgnoreCase(url.substr(0, 4), "data") || url[4] != ':') + { + return std::nullopt; + } + + const auto comma = url.find(',', 5); + if (comma == std::string_view::npos) + { + throw std::runtime_error{"fetch: malformed data URL"}; + } + + std::string mediaType{url.substr(5, comma - 5)}; + TrimAsciiWhitespace(mediaType); + bool base64 = false; + if (const auto semicolon = mediaType.rfind(';'); semicolon != std::string::npos) + { + std::string finalParameter{mediaType.substr(semicolon + 1)}; + TrimAsciiWhitespace(finalParameter); + if (EqualsIgnoreCase(finalParameter, "base64")) + { + mediaType.resize(semicolon); + TrimAsciiWhitespace(mediaType); + base64 = true; + } + } + if (mediaType.empty()) + { + mediaType = "text/plain;charset=US-ASCII"; + } + else if (mediaType.front() == ';') + { + mediaType.insert(0, "text/plain"); + } + + const auto fragment = url.find('#', comma + 1); + const auto payload = url.substr(comma + 1, fragment == std::string_view::npos ? std::string_view::npos : fragment - comma - 1); + auto decodedPayload = base64 ? ForgivingBase64Decode(payload) : PercentDecode(payload); + + return DataUrlResponse{ + std::move(mediaType), + std::string{url.substr(0, fragment)}, + std::move(decodedPayload)}; + } + // Stable message used for every transport-failure rejection. Browsers and Node both keep // this constant (the variable detail rides on `cause`) so crash-report grouping stays // intact; we follow Node/undici's "fetch failed" spelling. constexpr const char* FETCH_FAILED_MESSAGE = "fetch failed"; + constexpr const char* JS_FETCH_POLYFILL_EXPORTS_NAME = "fetchPolyfillExports"; // Snapshot the JS call-site stack synchronously, inside fetch(), before SendAsync() hands // the request to a worker thread. The transport-failure rejection is otherwise built in a @@ -157,18 +341,6 @@ namespace Babylon::Polyfills::Internal throw std::runtime_error{"Unsupported fetch method: " + method + " (only GET and POST are supported)"}; } - std::optional FindHeader(const ResponseData& data, std::string_view name) - { - for (const auto& header : data.headers) - { - if (EqualsIgnoreCase(header.first, name)) - { - return header.second; - } - } - return std::nullopt; - } - void ApplyRequestHeaders(UrlLib::UrlRequest& request, const Napi::Value& headers) { if (headers.IsUndefined() || headers.IsNull()) @@ -176,169 +348,85 @@ namespace Babylon::Polyfills::Internal return; } - Napi::Env env = headers.Env(); - - // Array of [name, value] pairs. - if (headers.IsArray()) + const auto env = headers.Env(); + const auto headersConstructor = env.Global().Get("Headers"); + if (headersConstructor.IsUndefined() || headersConstructor.IsNull()) { - const auto array = headers.As(); - for (uint32_t i = 0; i < array.Length(); ++i) - { - const auto pair = array.Get(i); - if (pair.IsArray()) - { - const auto entry = pair.As(); - request.SetRequestHeader(entry.Get(0u).ToString().Utf8Value(), entry.Get(1u).ToString().Utf8Value()); - } - } - return; + throw Napi::TypeError::New(env, "fetch requires Headers to be installed."); } - if (headers.IsObject()) - { - const auto object = headers.As(); - - // Headers / Map instances expose forEach((value, key) => ...). - const auto forEach = object.Get("forEach"); - if (forEach.IsFunction()) - { - const auto callback = Napi::Function::New(env, [&request](const Napi::CallbackInfo& info) { - if (info.Length() >= 2) - { - request.SetRequestHeader(info[1].ToString().Utf8Value(), info[0].ToString().Utf8Value()); - } - }); - forEach.As().Call(object, {callback}); - return; - } - - // Plain object of name/value properties. - const auto names = object.GetPropertyNames(); - for (uint32_t i = 0; i < names.Length(); ++i) + auto normalizedHeaders = headersConstructor.As().New({headers}); + const auto callback = Napi::Function::New(env, [&request](const Napi::CallbackInfo& info) { + if (info.Length() >= 2) { - const auto key = names.Get(i); - request.SetRequestHeader(key.ToString().Utf8Value(), object.Get(key).ToString().Utf8Value()); + request.SetRequestHeader(info[1].ToString().Utf8Value(), info[0].ToString().Utf8Value()); } - } + }); + normalizedHeaders.Get("forEach").As().Call(normalizedHeaders, {callback}); } - Napi::Object BuildHeaders(Napi::Env env, const std::shared_ptr& data) + void InitializeFetchClasses(Napi::Env env) { - Napi::Object headers = Napi::Object::New(env); - - headers.Set("get", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto value = FindHeader(*data, info[0].ToString().Utf8Value()); - return value ? Napi::Value{Napi::String::New(env, *value)} : Napi::Value{env.Null()}; - }, "get")); - - headers.Set("has", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - return Napi::Boolean::New(info.Env(), FindHeader(*data, info[0].ToString().Utf8Value()).has_value()); - }, "has")); - - headers.Set("forEach", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto callback = info[0].As(); - const auto thisArg = info.Length() > 1 ? info[1] : env.Undefined(); - for (const auto& header : data->headers) - { - callback.Call(thisArg, {Napi::String::New(env, header.second), Napi::String::New(env, header.first)}); - } - return env.Undefined(); - }, "forEach")); - - return headers; + auto global = env.Global(); + auto nativeObject = JsRuntime::NativeObject::GetFromJavaScript(env); + auto exportsValue = nativeObject.Get(JS_FETCH_POLYFILL_EXPORTS_NAME); + if (exportsValue.IsUndefined() || exportsValue.IsNull()) + { + exportsValue = Napi::Eval(env, FetchScripts::Polyfill, "jsruntimehost://fetch-polyfill.js"); + nativeObject.Set(JS_FETCH_POLYFILL_EXPORTS_NAME, exportsValue); + } + const auto exports = exportsValue.As(); + const auto headers = global.Get("Headers"); + const auto response = global.Get("Response"); + if (headers.IsUndefined() || headers.IsNull() || response.IsUndefined() || response.IsNull()) + { + global.Set("Headers", exports.Get("Headers")); + global.Set("Response", exports.Get("Response")); + } + nativeObject.Set("createFetchResponse", exports.Get("createFetchResponse")); } - Napi::Object BuildResponse(Napi::Env env, const std::shared_ptr& data) + template + Napi::Value CreateFetchResponse( + Napi::Env env, + const void* body, + size_t bodySize, + const THeaders& headers, + int status, + std::string_view statusText, + std::string_view url, + bool redirected) { - Napi::Object response = Napi::Object::New(env); - - const bool ok = data->statusCode >= 200 && data->statusCode < 300; - response.Set("ok", Napi::Boolean::New(env, ok)); - response.Set("status", Napi::Number::New(env, data->statusCode)); - response.Set("statusText", Napi::String::New(env, data->statusText)); - response.Set("url", Napi::String::New(env, data->url)); - response.Set("redirected", Napi::Boolean::New(env, false)); - response.Set("type", Napi::String::New(env, "basic")); - response.Set("bodyUsed", Napi::Boolean::New(env, false)); - response.Set("headers", BuildHeaders(env, data)); - - response.Set("text", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto deferred = Napi::Promise::Deferred::New(env); - std::string text{reinterpret_cast(data->body.data()), data->body.size()}; - deferred.Resolve(Napi::String::New(env, text)); - return deferred.Promise(); - }, "text")); - - response.Set("arrayBuffer", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto deferred = Napi::Promise::Deferred::New(env); - const auto arrayBuffer = Napi::ArrayBuffer::New(env, data->body.size()); - if (!data->body.empty()) - { - std::memcpy(arrayBuffer.Data(), data->body.data(), data->body.size()); - } - deferred.Resolve(arrayBuffer); - return deferred.Promise(); - }, "arrayBuffer")); - - response.Set("json", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto deferred = Napi::Promise::Deferred::New(env); - std::string text{reinterpret_cast(data->body.data()), data->body.size()}; - const auto json = env.Global().Get("JSON").As(); - const auto parse = json.Get("parse").As(); - try - { - deferred.Resolve(parse.Call(json, {Napi::String::New(env, text)})); - } - catch (const Napi::Error& error) - { - deferred.Reject(error.Value()); - } - return deferred.Promise(); - }, "json")); - - response.Set("blob", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto deferred = Napi::Promise::Deferred::New(env); - - // Use IsUndefined()/IsNull() rather than IsFunction() to detect the Blob - // polyfill: some JavaScriptCore/JSI builds classify constructor functions as - // typeof 'object', so napi_typeof reports napi_object and IsFunction() would - // incorrectly reject even when the Blob polyfill is installed. - const auto blobConstructor = env.Global().Get("Blob"); - if (blobConstructor.IsUndefined() || blobConstructor.IsNull()) - { - deferred.Reject(Napi::Error::New(env, "fetch: Blob is not available in this environment").Value()); - return deferred.Promise(); - } - - const auto arrayBuffer = Napi::ArrayBuffer::New(env, data->body.size()); - if (!data->body.empty()) - { - std::memcpy(arrayBuffer.Data(), data->body.data(), data->body.size()); - } - const auto bytes = Napi::Uint8Array::New(env, data->body.size(), arrayBuffer, 0); - - Napi::Array parts = Napi::Array::New(env, 1); - parts.Set(0u, bytes); + auto arrayBuffer = Napi::ArrayBuffer::New(env, bodySize); + if (bodySize > 0) + { + std::memcpy(arrayBuffer.Data(), body, bodySize); + } + const auto bytes = Napi::Uint8Array::New(env, bodySize, arrayBuffer, 0); - Napi::Object options = Napi::Object::New(env); - const auto contentType = FindHeader(*data, "content-type"); - options.Set("type", Napi::String::New(env, contentType.value_or(""))); + auto responseHeaders = Napi::Array::New(env, headers.size()); + uint32_t headerIndex{}; + for (const auto& header : headers) + { + auto pair = Napi::Array::New(env, 2); + pair.Set(uint32_t{0}, Napi::String::New(env, header.first)); + pair.Set(uint32_t{1}, Napi::String::New(env, header.second)); + responseHeaders.Set(headerIndex++, pair); + } - deferred.Resolve(blobConstructor.As().New({parts, options})); - return deferred.Promise(); - }, "blob")); + auto init = Napi::Object::New(env); + init.Set("headers", responseHeaders); + init.Set("status", Napi::Number::New(env, status)); + init.Set("statusText", Napi::String::New(env, statusText.data(), statusText.size())); - response.Set("clone", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - return BuildResponse(info.Env(), data); - }, "clone")); + auto metadata = Napi::Object::New(env); + metadata.Set("redirected", Napi::Boolean::New(env, redirected)); + metadata.Set("type", Napi::String::New(env, "basic")); + metadata.Set("url", Napi::String::New(env, url.data(), url.size())); - return response; + const auto nativeObject = JsRuntime::NativeObject::GetFromJavaScript(env); + const auto createResponse = nativeObject.Get("createFetchResponse").As(); + return createResponse.Call(nativeObject, {env.Global().Get("Response"), bytes, init, metadata}); } } @@ -347,6 +435,7 @@ namespace Babylon::Polyfills::Internal void Initialize(Napi::Env env) { static constexpr auto JS_FETCH_NAME = "fetch"; + InitializeFetchClasses(env); auto fetchFunction = Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { Napi::Env env = info.Env(); @@ -404,6 +493,47 @@ namespace Babylon::Polyfills::Internal signal = init.Get("signal"); } + if (signal.IsObject()) + { + const auto signalObject = signal.As(); + if (signalObject.Get("aborted").ToBoolean().Value()) + { + deferred.Reject(GetAbortReason(env, signalObject)); + return deferred.Promise(); + } + } + + std::optional dataUrl; + try + { + dataUrl = ParseDataUrl(url); + } + catch (const std::runtime_error& error) + { + deferred.Reject(Napi::TypeError::New(env, error.what()).Value()); + return deferred.Promise(); + } + + if (dataUrl) + { + if (method != UrlLib::UrlMethod::Get || body.has_value()) + { + throw std::runtime_error{"fetch: data URLs only support GET requests"}; + } + const std::vector> responseHeaders{ + {"content-type", dataUrl->contentType}}; + deferred.Resolve(CreateFetchResponse( + env, + dataUrl->body.data(), + dataUrl->body.size(), + responseHeaders, + 200, + "OK", + dataUrl->url, + false)); + return deferred.Promise(); + } + auto request = std::make_shared(); request->Open(method, url); request->ResponseType(UrlLib::UrlResponseType::Buffer); @@ -494,18 +624,19 @@ namespace Babylon::Polyfills::Internal return; } - auto data = std::make_shared(); - data->statusCode = status; - data->statusText = std::string{request->StatusText()}; - data->url = std::string{request->ResponseUrl()}; - for (const auto& header : request->GetAllResponseHeaders()) - { - data->headers.emplace_back(header.first, header.second); - } const auto responseBuffer = request->ResponseBuffer(); - data->body.assign(responseBuffer.begin(), responseBuffer.end()); - - deferred.Resolve(BuildResponse(env, data)); + const auto statusText = request->StatusText(); + const std::string responseUrl{request->ResponseUrl()}; + const std::string_view finalUrl = responseUrl.empty() ? std::string_view{url} : std::string_view{responseUrl}; + deferred.Resolve(CreateFetchResponse( + env, + responseBuffer.data(), + responseBuffer.size(), + request->GetAllResponseHeaders(), + status, + statusText, + finalUrl, + !responseUrl.empty() && responseUrl != url)); }) .then(*scheduler, arcana::cancellation::none(), [deferred, env, scheduler](const arcana::expected& result) { @@ -525,8 +656,7 @@ namespace Babylon::Polyfills::Internal deferred.Reject(Napi::Error::New(env, std::current_exception()).Value()); } - return deferred.Promise(); - }, JS_FETCH_NAME); + return deferred.Promise(); }, JS_FETCH_NAME); if (env.Global().Get(JS_FETCH_NAME).IsUndefined()) { diff --git a/Polyfills/Fetch/Source/FetchPolyfill.js b/Polyfills/Fetch/Source/FetchPolyfill.js new file mode 100644 index 00000000..4435d1ef --- /dev/null +++ b/Polyfills/Fetch/Source/FetchPolyfill.js @@ -0,0 +1,704 @@ +(function(global) { + "use strict"; + + var headerStates = new WeakMap(); + var responseStates = new WeakMap(); + var streamTrackers = new WeakMap(); + var readerTrackers = new WeakMap(); + var iteratorTrackers = new WeakMap(); + var headerNamePattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + var nullBodyStatuses = [204, 205, 304]; + + function wrapMethod(target, name, createWrapper) { + var descriptor = Object.getOwnPropertyDescriptor(target, name); + var original = descriptor ? descriptor.value : target[name]; + if (typeof original !== "function") { + return; + } + if (!descriptor) { + descriptor = { + configurable: true, + enumerable: false, + writable: true + }; + } + descriptor.value = createWrapper(original); + Object.defineProperty(target, name, descriptor); + } + + function trackStream(stream) { + var tracker = streamTrackers.get(stream); + if (!tracker) { + tracker = { disturbed: false }; + streamTrackers.set(stream, tracker); + } + return tracker; + } + + function trackReader(reader, tracker) { + if (readerTrackers.has(reader)) { + return reader; + } + readerTrackers.set(reader, tracker); + for (var name of ["read", "cancel"]) { + wrapMethod(reader, name, function(original) { + return function() { + tracker.disturbed = true; + return original.apply(this, arguments); + }; + }); + } + return reader; + } + + function trackIterator(iterator, tracker) { + if (iteratorTrackers.has(iterator)) { + return iterator; + } + iteratorTrackers.set(iterator, tracker); + for (var name of ["next", "return", "throw"]) { + wrapMethod(iterator, name, function(original) { + return function() { + tracker.disturbed = true; + return original.apply(this, arguments); + }; + }); + } + return iterator; + } + + function instrumentReadableStreams() { + if (global.ReadableStream === undefined || global.ReadableStream === null) { + return; + } + var prototype = global.ReadableStream.prototype; + wrapMethod(prototype, "getReader", function(original) { + return function() { + return trackReader(original.apply(this, arguments), trackStream(this)); + }; + }); + for (var name of ["cancel", "pipeTo", "pipeThrough", "tee"]) { + wrapMethod(prototype, name, function(original) { + return function() { + var result = original.apply(this, arguments); + trackStream(this).disturbed = true; + return result; + }; + }); + } + wrapMethod(prototype, "values", function(original) { + return function() { + return trackIterator(original.apply(this, arguments), trackStream(this)); + }; + }); + if (typeof Symbol.asyncIterator === "symbol") { + wrapMethod(prototype, Symbol.asyncIterator, function(original) { + return function() { + return trackIterator(original.apply(this, arguments), trackStream(this)); + }; + }); + } + } + + instrumentReadableStreams(); + + function requireHeaderState(value) { + var state = headerStates.get(value); + if (!state) { + throw new TypeError("Illegal invocation"); + } + return state; + } + + function requireResponseState(value) { + var state = responseStates.get(value); + if (!state) { + throw new TypeError("Illegal invocation"); + } + return state; + } + + function toByteString(value, description) { + var string = String(value); + for (var index = 0; index < string.length; ++index) { + if (string.charCodeAt(index) > 255) { + throw new TypeError(description + " is not a valid ByteString"); + } + } + return string; + } + + function normalizeHeaderName(value) { + var name = toByteString(value, "Header name"); + if (!headerNamePattern.test(name)) { + throw new TypeError("Invalid header name"); + } + return name.toLowerCase(); + } + + function normalizeHeaderValue(value) { + var normalized = toByteString(value, "Header value") + .replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, ""); + if (/[\0\r\n]/.test(normalized)) { + throw new TypeError("Invalid header value"); + } + return normalized; + } + + function isForbiddenResponseHeader(name) { + return name === "set-cookie" || name === "set-cookie2"; + } + + function appendHeader(state, name, value) { + if (state.guard === "response" && isForbiddenResponseHeader(name)) { + return; + } + state.list.push([name, value]); + state.version++; + } + + function removeHeader(state, name) { + var writeIndex = 0; + var removed = false; + for (var readIndex = 0; readIndex < state.list.length; ++readIndex) { + var entry = state.list[readIndex]; + if (entry[0] === name) { + removed = true; + } else { + state.list[writeIndex++] = entry; + } + } + state.list.length = writeIndex; + return removed; + } + + function fillHeaders(target, init) { + var state = requireHeaderState(target); + if (init === undefined) { + return; + } + if ((typeof init !== "object" && typeof init !== "function") || init === null) { + throw new TypeError("Headers initializer must be an object"); + } + + var iteratorMethod = init[Symbol.iterator]; + if (iteratorMethod !== undefined) { + if (typeof iteratorMethod !== "function") { + throw new TypeError("Headers initializer is not iterable"); + } + for (var entry of init) { + if ((typeof entry !== "object" && typeof entry !== "function") || entry === null) { + throw new TypeError("Each header pair must be iterable"); + } + var pair = Array.from(entry); + if (pair.length !== 2) { + throw new TypeError("Each header pair must contain exactly two items"); + } + appendHeader(state, normalizeHeaderName(pair[0]), normalizeHeaderValue(pair[1])); + } + return; + } + + for (var name of Object.keys(init)) { + appendHeader(state, normalizeHeaderName(name), normalizeHeaderValue(init[name])); + } + } + + function combinedHeaderEntries(state) { + if (state.cacheVersion === state.version) { + return state.cache; + } + var valuesByName = new Map(); + for (var entry of state.list) { + var values = valuesByName.get(entry[0]); + if (!values) { + values = []; + valuesByName.set(entry[0], values); + } + values.push(entry[1]); + } + + var result = []; + var names = Array.from(valuesByName.keys()).sort(); + for (var name of names) { + var values = valuesByName.get(name); + if (name === "set-cookie") { + for (var value of values) { + result.push([name, value]); + } + } else { + result.push([name, values.join(", ")]); + } + } + state.cache = result; + state.cacheVersion = state.version; + return state.cache; + } + + function* iterateHeaders(headers, kind) { + var index = 0; + while (true) { + var entries = combinedHeaderEntries(requireHeaderState(headers)); + if (index >= entries.length) { + return; + } + var entry = entries[index++]; + if (kind === "key") { + yield entry[0]; + } else if (kind === "value") { + yield entry[1]; + } else { + yield [entry[0], entry[1]]; + } + } + } + + class Headers { + constructor(init) { + headerStates.set(this, { + cache: [], + cacheVersion: -1, + guard: "none", + list: [], + version: 0 + }); + fillHeaders(this, init); + } + + append(name, value) { + var state = requireHeaderState(this); + appendHeader(state, normalizeHeaderName(name), normalizeHeaderValue(value)); + } + + delete(name) { + var state = requireHeaderState(this); + name = normalizeHeaderName(name); + if (state.guard === "response" && isForbiddenResponseHeader(name)) { + return; + } + if (removeHeader(state, name)) { + state.version++; + } + } + + get(name) { + var state = requireHeaderState(this); + name = normalizeHeaderName(name); + var value = null; + for (var entry of state.list) { + if (entry[0] === name) { + value = value === null ? entry[1] : value + ", " + entry[1]; + } + } + return value; + } + + getSetCookie() { + var state = requireHeaderState(this); + var values = []; + for (var entry of state.list) { + if (entry[0] === "set-cookie") { + values.push(entry[1]); + } + } + return values; + } + + has(name) { + var state = requireHeaderState(this); + name = normalizeHeaderName(name); + return state.list.some(function(entry) { return entry[0] === name; }); + } + + set(name, value) { + var state = requireHeaderState(this); + name = normalizeHeaderName(name); + value = normalizeHeaderValue(value); + if (state.guard === "response" && isForbiddenResponseHeader(name)) { + return; + } + removeHeader(state, name); + state.list.push([name, value]); + state.version++; + } + + entries() { + requireHeaderState(this); + return iterateHeaders(this, "entry"); + } + + keys() { + requireHeaderState(this); + return iterateHeaders(this, "key"); + } + + values() { + requireHeaderState(this); + return iterateHeaders(this, "value"); + } + + forEach(callback, thisArg) { + requireHeaderState(this); + if (typeof callback !== "function") { + throw new TypeError("Headers.forEach callback must be a function"); + } + for (var entry of this) { + callback.call(thisArg, entry[1], entry[0], this); + } + } + + [Symbol.iterator]() { + return this.entries(); + } + } + + Object.defineProperty(Headers.prototype, Symbol.toStringTag, { + configurable: true, + value: "Headers" + }); + + function createResponseHeaders(init) { + var headers = new Headers(init); + var state = requireHeaderState(headers); + state.guard = "response"; + var removedForbidden = removeHeader(state, "set-cookie"); + removedForbidden = removeHeader(state, "set-cookie2") || removedForbidden; + if (removedForbidden) { + state.version++; + } + return headers; + } + + function isReadableStream(value) { + return global.ReadableStream !== undefined && global.ReadableStream !== null && + value instanceof global.ReadableStream; + } + + function streamFromBytes(bytes) { + var chunk = bytes; + return new global.ReadableStream({ + type: "bytes", + pull: function(controller) { + if (chunk !== null) { + var value = chunk; + chunk = null; + if (value.byteLength !== 0) { + controller.enqueue(value); + } + } + controller.close(); + }, + cancel: function() { + chunk = null; + } + }); + } + + function copyBufferSource(value) { + var source; + if (value instanceof ArrayBuffer) { + source = new Uint8Array(value); + } else { + source = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + } + var copy = new Uint8Array(source.byteLength); + copy.set(source); + return copy; + } + + function extractBody(body) { + if (body === null || body === undefined) { + return { stream: null, contentType: null }; + } + if (isReadableStream(body)) { + if (body.locked || trackStream(body).disturbed) { + throw new TypeError("Response body stream is already disturbed or locked"); + } + return { stream: body, contentType: null }; + } + if (global.Blob !== undefined && global.Blob !== null && body instanceof global.Blob) { + return { stream: body.stream(), contentType: body.type || null }; + } + if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) { + return { stream: streamFromBytes(copyBufferSource(body)), contentType: null }; + } + if (global.URLSearchParams !== undefined && global.URLSearchParams !== null && body instanceof global.URLSearchParams) { + return { + stream: streamFromBytes(new global.TextEncoder().encode(body.toString())), + contentType: "application/x-www-form-urlencoded;charset=UTF-8" + }; + } + if (global.FormData !== undefined && global.FormData !== null && body instanceof global.FormData) { + throw new TypeError("FormData response bodies are not supported by this runtime"); + } + + return { + stream: streamFromBytes(new global.TextEncoder().encode(String(body))), + contentType: "text/plain;charset=UTF-8" + }; + } + + function bodyIsUnusable(state) { + return state.used || (state.body !== null && (state.body.locked || state.bodyTracker.disturbed)); + } + + async function readBodyChunks(state) { + if (state.body === null) { + return [[], 0]; + } + if (bodyIsUnusable(state)) { + throw new TypeError("Response body is already used"); + } + + state.used = true; + var chunks = []; + var total = 0; + var reader = state.body.getReader(); + while (true) { + var result = await reader.read(); + if (result.done) { + return [chunks, total]; + } + if (!(result.value instanceof Uint8Array)) { + throw new TypeError("Response body stream yielded a non-Uint8Array chunk"); + } + if (result.value.byteLength === 0) { + continue; + } + if (total > Number.MAX_SAFE_INTEGER - result.value.byteLength) { + throw new RangeError("Response body is too large"); + } + chunks.push(result.value); + total += result.value.byteLength; + } + } + + function joinChunks(chunksAndTotal) { + var chunks = chunksAndTotal[0]; + var total = chunksAndTotal[1]; + if (chunks.length === 0) { + return new Uint8Array(0); + } + if (chunks.length === 1) { + return chunks[0]; + } + + var bytes = new Uint8Array(total); + var offset = 0; + for (var chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return bytes; + } + + async function consumeText(state) { + if (state.body === null) { + return ""; + } + if (bodyIsUnusable(state)) { + throw new TypeError("Response body is already used"); + } + + state.used = true; + var decoder = new global.TextDecoder(); + var pieces = []; + var reader = state.body.getReader(); + while (true) { + var result = await reader.read(); + if (result.done) { + pieces.push(decoder.decode()); + return pieces.join(""); + } + if (!(result.value instanceof Uint8Array)) { + throw new TypeError("Response body stream yielded a non-Uint8Array chunk"); + } + if (result.value.byteLength !== 0) { + pieces.push(decoder.decode(result.value, { stream: true })); + } + } + } + + function validateResponseInit(init) { + if (init === undefined || init === null) { + init = {}; + } else if (typeof init !== "object" && typeof init !== "function") { + throw new TypeError("Response init must be an object"); + } + + var status = init.status === undefined ? 200 : Math.trunc(Number(init.status)); + if (!Number.isFinite(status) || status < 200 || status > 599) { + throw new RangeError("Response status must be between 200 and 599"); + } + var statusText = init.statusText === undefined ? "" : + toByteString(init.statusText, "Response statusText"); + if (/[\r\n]/.test(statusText)) { + throw new TypeError("Invalid Response statusText"); + } + return { + headers: createResponseHeaders(init.headers), + status: status, + statusText: statusText + }; + } + + function initializeResponse(response, body, init, metadata) { + var normalizedInit = validateResponseInit(init); + var extracted = extractBody(body); + if (extracted.stream !== null && nullBodyStatuses.indexOf(normalizedInit.status) !== -1) { + throw new TypeError("Response with this status cannot have a body"); + } + if (extracted.contentType !== null && !normalizedInit.headers.has("content-type")) { + normalizedInit.headers.append("content-type", extracted.contentType); + } + + responseStates.set(response, { + body: extracted.stream, + bodyTracker: extracted.stream === null ? null : trackStream(extracted.stream), + headers: normalizedInit.headers, + redirected: metadata && metadata.redirected === true, + status: normalizedInit.status, + statusText: normalizedInit.statusText, + type: metadata && metadata.type ? String(metadata.type) : "default", + url: metadata && metadata.url ? String(metadata.url) : "", + used: false + }); + } + + class Response { + constructor(body, init) { + initializeResponse(this, body === undefined ? null : body, init, null); + } + + get body() { return requireResponseState(this).body; } + get bodyUsed() { + var state = requireResponseState(this); + return state.used || (state.body !== null && state.bodyTracker.disturbed); + } + get headers() { return requireResponseState(this).headers; } + get ok() { + var status = requireResponseState(this).status; + return status >= 200 && status <= 299; + } + get redirected() { return requireResponseState(this).redirected; } + get status() { return requireResponseState(this).status; } + get statusText() { return requireResponseState(this).statusText; } + get type() { return requireResponseState(this).type; } + get url() { return requireResponseState(this).url; } + + async arrayBuffer() { + var bytes = joinChunks(await readBodyChunks(requireResponseState(this))); + if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) { + return bytes.buffer; + } + return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + } + + async blob() { + var state = requireResponseState(this); + var bytes = joinChunks(await readBodyChunks(state)); + return new global.Blob([bytes], { type: state.headers.get("content-type") || "" }); + } + + async bytes() { + return joinChunks(await readBodyChunks(requireResponseState(this))); + } + + clone() { + var state = requireResponseState(this); + if (bodyIsUnusable(state)) { + throw new TypeError("Cannot clone a used Response"); + } + + var cloneBody = null; + if (state.body !== null) { + var branches = state.body.tee(); + state.body = branches[0]; + state.bodyTracker = trackStream(state.body); + cloneBody = branches[1]; + } + var clone = new Response(cloneBody, { + headers: state.headers, + status: state.status, + statusText: state.statusText + }); + var cloneState = requireResponseState(clone); + cloneState.redirected = state.redirected; + cloneState.type = state.type; + cloneState.url = state.url; + return clone; + } + + async json() { + return JSON.parse(await consumeText(requireResponseState(this))); + } + + async text() { + return consumeText(requireResponseState(this)); + } + + static error() { + var response = Object.create(Response.prototype); + responseStates.set(response, { + body: null, + headers: createResponseHeaders(), + redirected: false, + status: 0, + statusText: "", + type: "error", + url: "", + used: false + }); + return response; + } + + static json(data, init) { + var text = JSON.stringify(data); + if (text === undefined) { + throw new TypeError("Response.json data is not JSON serializable"); + } + var hasExplicitContentType = init !== undefined && init !== null && + new Headers(init.headers).has("content-type"); + var response = new Response(text, init); + if (!hasExplicitContentType) { + response.headers.set("content-type", "application/json"); + } + return response; + } + + static redirect(url, status) { + status = status === undefined ? 302 : Math.trunc(Number(status)); + if ([301, 302, 303, 307, 308].indexOf(status) === -1) { + throw new RangeError("Invalid redirect status"); + } + var location = global.URL !== undefined && global.URL !== null + ? new global.URL(String(url), global.location && global.location.href || undefined).toString() + : String(url); + return new Response(null, { status: status, headers: { location: location } }); + } + } + + Object.defineProperty(Response.prototype, Symbol.toStringTag, { + configurable: true, + value: "Response" + }); + + function createFetchResponse(ResponseConstructor, bytes, init, metadata) { + var status = init && init.status; + var body = nullBodyStatuses.indexOf(status) === -1 ? streamFromBytes(bytes) : null; + var response = new ResponseConstructor(body, init); + var state = responseStates.get(response); + if (state) { + state.redirected = metadata.redirected === true; + state.type = metadata.type || "basic"; + state.url = metadata.url || ""; + } + return response; + } + + return { + Headers: Headers, + Response: Response, + createFetchResponse: createFetchResponse + }; +})(globalThis) diff --git a/Polyfills/Fetch/Source/FetchScripts.h.in b/Polyfills/Fetch/Source/FetchScripts.h.in new file mode 100644 index 00000000..8cc0e511 --- /dev/null +++ b/Polyfills/Fetch/Source/FetchScripts.h.in @@ -0,0 +1,8 @@ +#pragma once + +namespace Babylon::Polyfills::Internal::FetchScripts +{ + inline constexpr char Polyfill[] = R"JSRHFETCH( +@FETCH_POLYFILL_SOURCE@ +)JSRHFETCH"; +} diff --git a/Polyfills/File/Source/File.cpp b/Polyfills/File/Source/File.cpp index 5cc3de02..2bc19e62 100644 --- a/Polyfills/File/Source/File.cpp +++ b/Polyfills/File/Source/File.cpp @@ -50,6 +50,8 @@ namespace Babylon::Polyfills::Internal InstanceMethod("arrayBuffer", &File::ArrayBuffer), InstanceMethod("text", &File::Text), InstanceMethod("bytes", &File::Bytes), + InstanceMethod("slice", &File::Slice), + InstanceMethod("stream", &File::Stream), }); global.Set(JS_FILE_CONSTRUCTOR_NAME, func); @@ -59,12 +61,16 @@ namespace Babylon::Polyfills::Internal // a Blob subtype; BJS core (fileTools, Offline/database, // abstractEngine, thinNativeEngine) branches on `instanceof Blob` // and needs File inputs to satisfy that check. - auto setPrototypeOf = env.Global().Get("Object").As() - .Get("setPrototypeOf").As(); + auto setPrototypeOf = env.Global().Get("Object").As().Get("setPrototypeOf").As(); setPrototypeOf.Call({ func.Get("prototype"), blob.As().Get("prototype"), }); + + auto descriptor = Napi::Object::New(env); + descriptor.Set("configurable", true); + descriptor.Set("value", JS_FILE_CONSTRUCTOR_NAME); + global.Get("Object").As().Get("defineProperty").As().Call(global.Get("Object"), {func.Get("prototype"), Napi::Symbol::WellKnown(env, "toStringTag"), descriptor}); } File::File(const Napi::CallbackInfo& info) @@ -79,7 +85,7 @@ namespace Babylon::Polyfills::Internal { throw Napi::TypeError::New(env, "Failed to construct 'File': 2 arguments required, but only " + - std::to_string(info.Length()) + " present."); + std::to_string(info.Length()) + " present."); } Napi::Value parts = info[0]; @@ -118,21 +124,11 @@ namespace Babylon::Polyfills::Internal } } - Napi::Value partsArray; - if (parts.IsArray()) - { - partsArray = parts; - } - else - { - partsArray = Napi::Array::New(env, 0); - } - // Delegate byte-buffer construction to the native Blob polyfill so // we benefit from its existing BlobPart handling (ArrayBuffer, // typed array, string, Blob). auto blobCtor = env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).As(); - auto blobInstance = blobCtor.New({partsArray, blobOptions}); + auto blobInstance = blobCtor.New({parts, blobOptions}); m_blob = Napi::Persistent(blobInstance); } @@ -173,6 +169,29 @@ namespace Babylon::Polyfills::Internal auto blob = m_blob.Value(); return blob.Get("bytes").As().Call(blob, {}); } + + Napi::Value File::Slice(const Napi::CallbackInfo& info) + { + auto blob = m_blob.Value(); + const auto slice = blob.Get("slice").As(); + switch (info.Length()) + { + case 0: + return slice.Call(blob, {}); + case 1: + return slice.Call(blob, {info[0]}); + case 2: + return slice.Call(blob, {info[0], info[1]}); + default: + return slice.Call(blob, {info[0], info[1], info[2]}); + } + } + + Napi::Value File::Stream(const Napi::CallbackInfo&) + { + auto blob = m_blob.Value(); + return blob.Get("stream").As().Call(blob, {}); + } } namespace Babylon::Polyfills::File diff --git a/Polyfills/File/Source/File.h b/Polyfills/File/Source/File.h index 9d73edb2..49473666 100644 --- a/Polyfills/File/Source/File.h +++ b/Polyfills/File/Source/File.h @@ -22,6 +22,8 @@ namespace Babylon::Polyfills::Internal Napi::Value ArrayBuffer(const Napi::CallbackInfo& info); Napi::Value Text(const Napi::CallbackInfo& info); Napi::Value Bytes(const Napi::CallbackInfo& info); + Napi::Value Slice(const Napi::CallbackInfo& info); + Napi::Value Stream(const Napi::CallbackInfo& info); Napi::ObjectReference m_blob; std::string m_name; diff --git a/Polyfills/Streams/CMakeLists.txt b/Polyfills/Streams/CMakeLists.txt new file mode 100644 index 00000000..6c00ed81 --- /dev/null +++ b/Polyfills/Streams/CMakeLists.txt @@ -0,0 +1,30 @@ +set(WEB_STREAMS_POLYFILL_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/ThirdParty/web-streams-polyfill/ponyfill.es5.js") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${WEB_STREAMS_POLYFILL_FILE}") +file(READ "${WEB_STREAMS_POLYFILL_FILE}" WEB_STREAMS_POLYFILL_SOURCE) +string(SUBSTRING "${WEB_STREAMS_POLYFILL_SOURCE}" 0 60000 WEB_STREAMS_POLYFILL_SOURCE_1) +string(SUBSTRING "${WEB_STREAMS_POLYFILL_SOURCE}" 60000 -1 WEB_STREAMS_POLYFILL_SOURCE_2) +configure_file( + "Source/StreamsScripts.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/Generated/StreamsScripts.h" + @ONLY) + +set(SOURCES + "Include/Babylon/Polyfills/Streams.h" + "Source/Streams.cpp" + "Source/StreamsScripts.h.in" + "ThirdParty/web-streams-polyfill/LICENSE" + "ThirdParty/web-streams-polyfill/ponyfill.es5.js") + +add_library(Streams ${SOURCES}) +warnings_as_errors(Streams) + +target_include_directories(Streams + PUBLIC "Include" + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/Generated") + +target_link_libraries(Streams PUBLIC JsRuntime) + +set_property(TARGET Streams PROPERTY FOLDER Polyfills) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Polyfills/Streams/Include/Babylon/Polyfills/Streams.h b/Polyfills/Streams/Include/Babylon/Polyfills/Streams.h new file mode 100644 index 00000000..01e5a15c --- /dev/null +++ b/Polyfills/Streams/Include/Babylon/Polyfills/Streams.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace Babylon::Polyfills::Streams +{ + // Installs the WHATWG Streams constructors that are not already supplied + // by the JavaScript engine. + void BABYLON_API Initialize(Napi::Env env); +} diff --git a/Polyfills/Streams/README.md b/Polyfills/Streams/README.md new file mode 100644 index 00000000..c84a38e3 --- /dev/null +++ b/Polyfills/Streams/README.md @@ -0,0 +1,14 @@ +# Web Streams + +Installs the standard `ReadableStream`, `WritableStream`, `TransformStream`, +reader, writer, controller, and queuing-strategy globals when the selected +JavaScript engine does not provide them. + +The implementation is the ES5 ponyfill bundle from +[`web-streams-polyfill` 4.3.0](https://github.com/MattiasBuelens/web-streams-polyfill/tree/add69059ed08eae6a18559aba49575a280d1529e), +which is based on the WHATWG reference implementation. The vendored project +tests this release against the Streams portion of WPT at revision +[`c05b4473`](https://github.com/web-platform-tests/wpt/tree/c05b447326585237713013c66341eab2cdf967b6/streams). + +`Initialize` preserves any Streams constructors already supplied by the host +engine and fills only missing globals. diff --git a/Polyfills/Streams/Source/Streams.cpp b/Polyfills/Streams/Source/Streams.cpp new file mode 100644 index 00000000..e742344f --- /dev/null +++ b/Polyfills/Streams/Source/Streams.cpp @@ -0,0 +1,53 @@ +#include + +#include "StreamsScripts.h" + +#include +#include + +namespace Babylon::Polyfills::Streams +{ + void BABYLON_API Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + auto global = env.Global(); + + static constexpr std::array constructorNames{ + "ReadableStream", + "ReadableStreamDefaultController", + "ReadableByteStreamController", + "ReadableStreamBYOBRequest", + "ReadableStreamDefaultReader", + "ReadableStreamBYOBReader", + "WritableStream", + "WritableStreamDefaultController", + "WritableStreamDefaultWriter", + "ByteLengthQueuingStrategy", + "CountQueuingStrategy", + "TransformStream", + "TransformStreamDefaultController", + }; + + bool needsPonyfill{}; + for (const auto name : constructorNames) + { + const auto constructor = global.Get(name.data()); + if (constructor.IsUndefined() || constructor.IsNull()) + { + needsPonyfill = true; + break; + } + } + + if (!needsPonyfill) + { + return; + } + + const auto exports = Napi::Eval(env, Internal::StreamsScripts::Ponyfill.data(), "jsruntimehost://web-streams-polyfill.js").As(); + for (const auto name : constructorNames) + { + global.Set(name.data(), exports.Get(name.data())); + } + } +} diff --git a/Polyfills/Streams/Source/StreamsScripts.h.in b/Polyfills/Streams/Source/StreamsScripts.h.in new file mode 100644 index 00000000..126a23f7 --- /dev/null +++ b/Polyfills/Streams/Source/StreamsScripts.h.in @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace Babylon::Polyfills::Internal::StreamsScripts +{ + inline constexpr char PonyfillPart1[] = R"JSRHSTREAM( +(function() { + var exports = {}; + var module = { exports: exports }; +@WEB_STREAMS_POLYFILL_SOURCE_1@)JSRHSTREAM"; + + // Keep the split transparent: inserting whitespace here can divide an + // identifier or string literal when the vendored bundle is regenerated. + inline constexpr char PonyfillPart2[] = R"JSRHSTREAM(@WEB_STREAMS_POLYFILL_SOURCE_2@ + return module.exports; +})() +)JSRHSTREAM"; + + template + consteval auto Join(const char (&part1)[Part1Size], const char (&part2)[Part2Size]) + { + std::array result{}; + for (std::size_t index{}; index < Part1Size - 1; ++index) + { + result[index] = part1[index]; + } + for (std::size_t index{}; index < Part2Size; ++index) + { + result[Part1Size - 1 + index] = part2[index]; + } + return result; + } + + inline constexpr auto Ponyfill = Join(PonyfillPart1, PonyfillPart2); +} diff --git a/Polyfills/Streams/ThirdParty/web-streams-polyfill/LICENSE b/Polyfills/Streams/ThirdParty/web-streams-polyfill/LICENSE new file mode 100644 index 00000000..7adbcf57 --- /dev/null +++ b/Polyfills/Streams/ThirdParty/web-streams-polyfill/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2026 Mattias Buelens +Copyright (c) 2016 Diwank Singh Tomer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Polyfills/Streams/ThirdParty/web-streams-polyfill/ponyfill.es5.js b/Polyfills/Streams/ThirdParty/web-streams-polyfill/ponyfill.es5.js new file mode 100644 index 00000000..18d63683 --- /dev/null +++ b/Polyfills/Streams/ThirdParty/web-streams-polyfill/ponyfill.es5.js @@ -0,0 +1,8 @@ +/** + * @license + * web-streams-polyfill v4.3.0 + * Copyright 2026 Mattias Buelens, Diwank Singh Tomer and other contributors. + * This code is released under the MIT license. + * SPDX-License-Identifier: MIT + */ +!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r((e="undefined"!=typeof globalThis?globalThis:e||self).WebStreamsPolyfill={})}(this,function(e){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol:function(e){return"Symbol(".concat(e,")")};function t(){}function o(e){return"object"==typeof e&&null!==e||"function"==typeof e}"function"==typeof SuppressedError&&SuppressedError;var n=t;function i(e,r){try{Object.defineProperty(e,"name",{value:r,configurable:!0})}catch(e){}}var a=Promise,u=Promise.resolve.bind(a),l=Promise.prototype.then,s=Promise.reject.bind(a),c=u;function f(e){return new a(e)}function d(e){return f(function(r){return r(e)})}function p(e){return s(e)}function b(e,r,t){return l.call(e,r,t)}function h(e,r,t){b(b(e,r,t),void 0,n)}function _(e,r){h(e,r)}function m(e,r){h(e,void 0,r)}function v(e,r,t){return b(e,r,t)}function y(e){b(e,void 0,n)}var g=function(e){if("function"==typeof queueMicrotask)g=queueMicrotask;else{var r=d(void 0);g=function(e){return b(r,e)}}return g(e)};function S(e,r,t){if("function"!=typeof e)throw new TypeError("Argument is not a function");return Function.prototype.apply.call(e,r,t)}function w(e,r,t){try{return d(S(e,r,t))}catch(e){return p(e)}}var R=function(){function e(){this._cursor=0,this._size=0,this._front={_elements:[],_next:void 0},this._back=this._front,this._cursor=0,this._size=0}return Object.defineProperty(e.prototype,"length",{get:function(){return this._size},enumerable:!1,configurable:!0}),e.prototype.push=function(e){var r=this._back,t=r;16383===r._elements.length&&(t={_elements:[],_next:void 0}),r._elements.push(e),t!==r&&(this._back=t,r._next=t),++this._size},e.prototype.shift=function(){var e=this._front,r=e,t=this._cursor,o=t+1,n=e._elements,i=n[t];return 16384===o&&(r=e._next,o=0),--this._size,this._cursor=o,e!==r&&(this._front=r),n[t]=void 0,i},e.prototype.forEach=function(e){for(var r=this._cursor,t=this._front,o=t._elements;!(r===o.length&&void 0===t._next||r===o.length&&(r=0,0===(o=(t=t._next)._elements).length));)e(o[r]),++r},e.prototype.peek=function(){var e=this._front,r=this._cursor;return e._elements[r]},e}(),T=r("[[AbortSteps]]"),P=r("[[ErrorSteps]]"),C=r("[[CancelSteps]]"),q=r("[[PullSteps]]"),E=r("[[CanPullSyncSteps]]"),W=r("[[ReleaseSteps]]");function O(e,r){e._ownerReadableStream=r,r._reader=e,"readable"===r._state?A(e):"closed"===r._state?function(e){A(e),F(e)}(e):z(e,r._storedError)}function j(e,r){return eo(e._ownerReadableStream,r)}function B(e){var r=e._ownerReadableStream;"readable"===r._state?D(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")):function(e,r){z(e,r)}(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")),r._readableStreamController[W](),r._reader=void 0,e._ownerReadableStream=void 0}function k(e){return new TypeError("Cannot "+e+" a stream using a released reader")}function A(e){e._closedPromise=f(function(r,t){e._closedPromise_resolve=r,e._closedPromise_reject=t})}function z(e,r){A(e),D(e,r)}function D(e,r){void 0!==e._closedPromise_reject&&(y(e._closedPromise),e._closedPromise_reject(r),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}function F(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}var L=Number.isFinite||function(e){return"number"==typeof e&&isFinite(e)},I=Math.trunc||function(e){return e<0?Math.ceil(e):Math.floor(e)};function M(e,r){if(void 0!==e&&("object"!=typeof(t=e)&&"function"!=typeof t))throw new TypeError("".concat(r," is not an object."));var t}function x(e,r){if("function"!=typeof e)throw new TypeError("".concat(r," is not a function."))}function Y(e,r){if(!function(e){return"object"==typeof e&&null!==e||"function"==typeof e}(e))throw new TypeError("".concat(r," is not an object."))}function Q(e,r,t){if(void 0===e)throw new TypeError("Parameter ".concat(r," is required in '").concat(t,"'."))}function N(e,r,t){if(void 0===e)throw new TypeError("".concat(r," is required in '").concat(t,"'."))}function H(e){return Number(e)}function V(e){return 0===e?0:e}function U(e,r){var t=Number.MAX_SAFE_INTEGER,o=Number(e);if(o=V(o),!L(o))throw new TypeError("".concat(r," is not a finite number"));if((o=function(e){return V(I(e))}(o))<0||o>t)throw new TypeError("".concat(r," is outside the accepted range of ").concat(0," to ").concat(t,", inclusive"));return L(o)&&0!==o?o:0}function G(e,r){if(!Zt(e))throw new TypeError("".concat(r," is not a ReadableStream."))}function X(e){return new ee(e)}function J(e,r){e._reader._readRequests.push(r)}function K(e,r,t){var o=e._reader._readRequests.shift();t?o._closeSteps():o._chunkSteps(r)}function Z(e){return e._reader._readRequests.length}function $(e){var r=e._reader;return void 0!==r&&!!ae(r)}var ee=function(){function ReadableStreamDefaultReader(e){if(Q(e,1,"ReadableStreamDefaultReader"),G(e,"First parameter"),$t(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");O(this,e),this._readRequests=new R}return Object.defineProperty(ReadableStreamDefaultReader.prototype,"closed",{get:function(){return ae(this)?this._closedPromise:p(ce("closed"))},enumerable:!1,configurable:!0}),ReadableStreamDefaultReader.prototype.cancel=function(e){return void 0===e&&(e=void 0),ae(this)?void 0===this._ownerReadableStream?p(k("cancel")):j(this,e):p(ce("cancel"))},ReadableStreamDefaultReader.prototype.read=function(){if(!ae(this))return p(ce("read"));if(void 0===this._ownerReadableStream)return p(k("read from"));var e=le(this)?new ie:new ne;return ue(this,e),e._promise},ReadableStreamDefaultReader.prototype.releaseLock=function(){if(!ae(this))throw ce("releaseLock");void 0!==this._ownerReadableStream&&function(e){B(e);var r=new TypeError("Reader was released");se(e,r)}(this)},ReadableStreamDefaultReader}();Object.defineProperties(ee.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),i(ee.prototype.cancel,"cancel"),i(ee.prototype.read,"read"),i(ee.prototype.releaseLock,"releaseLock"),"symbol"==typeof r.toStringTag&&Object.defineProperty(ee.prototype,r.toStringTag,{value:"ReadableStreamDefaultReader",configurable:!0});var re,te,oe,ne=function(){function e(){var e=this;this._promise=f(function(r,t){e._resolvePromise=r,e._rejectPromise=t})}return e.prototype._chunkSteps=function(e){this._resolvePromise({value:e,done:!1})},e.prototype._closeSteps=function(){this._resolvePromise({value:void 0,done:!0})},e.prototype._errorSteps=function(e){this._rejectPromise(e)},e}(),ie=function(){function e(){this._promise=void 0}return e.prototype._chunkSteps=function(e){this._promise=c({value:e,done:!1})},e.prototype._closeSteps=function(){this._promise=c({value:void 0,done:!0})},e.prototype._errorSteps=function(e){this._promise=p(e)},e}();function ae(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_readRequests")&&e instanceof ee)}function ue(e,r){var t=e._ownerReadableStream;t._disturbed=!0,"closed"===t._state?r._closeSteps():"errored"===t._state?r._errorSteps(t._storedError):t._readableStreamController[q](r)}function le(e){var r=e._ownerReadableStream;return"closed"===r._state||("errored"===r._state||r._readableStreamController[E]())}function se(e,r){var t=e._readRequests;e._readRequests=new R,t.forEach(function(e){e._errorSteps(r)})}function ce(e){return new TypeError("ReadableStreamDefaultReader.prototype.".concat(e," can only be used on a ReadableStreamDefaultReader"))}function fe(e){return e.slice()}function de(e,r,t,o,n){new Uint8Array(e).set(new Uint8Array(t,o,n),r)}var pe=function(e){return(pe="function"==typeof e.transfer?function(e){return e.transfer()}:"function"==typeof structuredClone?function(e){return structuredClone(e,{transfer:[e]})}:function(e){return e})(e)},be=function(e){return(be="boolean"==typeof e.detached?function(e){return e.detached}:function(e){return 0===e.byteLength})(e)};function he(e,r,t){if(e.slice)return e.slice(r,t);var o=t-r,n=new ArrayBuffer(o);return de(n,0,e,r,o),n}function _e(e,r){var t=e[r];if(null!=t){if("function"!=typeof t)throw new TypeError("".concat(String(r)," is not a function"));return t}}function me(e){try{var r=e.done,t=e.value;return b(c(t),function(e){return{done:r,value:e}})}catch(e){return p(e)}}var ve,ye=null!==(oe=null!==(re=r.asyncIterator)&&void 0!==re?re:null===(te=r.for)||void 0===te?void 0:te.call(r,"Symbol.asyncIterator"))&&void 0!==oe?oe:"@@asyncIterator";function ge(e,t,n){if(void 0===t&&(t="sync"),void 0===n)if("async"===t){if(void 0===(n=_e(e,ye)))return function(e){var r={next:function(){var r;try{r=Se(e)}catch(e){return p(e)}return me(r)},return:function(r){var t;try{var n=_e(e.iterator,"return");if(void 0===n)return d({done:!0,value:r});t=S(n,e.iterator,[r])}catch(e){return p(e)}return o(t)?me(t):p(new TypeError("The iterator.return() method must return an object"))}};return{iterator:r,nextMethod:r.next,done:!1}}(ge(e,"sync",_e(e,r.iterator)))}else n=_e(e,r.iterator);if(void 0===n)throw new TypeError("The object is not iterable");var i=S(n,e,[]);if(!o(i))throw new TypeError("The iterator method must return an object");return{iterator:i,nextMethod:i.next,done:!1}}function Se(e){var r=S(e.nextMethod,e.iterator,[]);if(!o(r))throw new TypeError("The iterator.next() method must return an object");return r}var we=function(){function e(e,r){this._ongoingPromise=void 0,this._isFinished=!1,this._reader=e,this._preventCancel=r}return e.prototype.next=function(){var e=this,r=function(){return e._nextSteps()};return this._ongoingPromise=this._ongoingPromise?v(this._ongoingPromise,r,r):r(),this._ongoingPromise},e.prototype.return=function(e){var r=this,t=function(){return r._returnSteps(e)};return this._ongoingPromise=this._ongoingPromise?v(this._ongoingPromise,t,t):t(),this._ongoingPromise},e.prototype._nextSteps=function(){if(this._isFinished)return Promise.resolve({value:void 0,done:!0});var e=this._reader,r=new Re(this);return ue(e,r),r._promise},e.prototype._returnSteps=function(e){if(this._isFinished)return Promise.resolve({value:e,done:!0});this._isFinished=!0;var r=this._reader;if(!this._preventCancel){var t=j(r,e);return B(r),v(t,function(){return{value:e,done:!0}})}return B(r),d({value:e,done:!0})},e}(),Re=function(){function e(e){var r=this;this._iterator=e,this._promise=f(function(e,t){r._resolvePromise=e,r._rejectPromise=t})}return e.prototype._chunkSteps=function(e){var r=this;this._iterator._ongoingPromise=void 0,g(function(){return r._resolvePromise({value:e,done:!1})})},e.prototype._closeSteps=function(){var e=this._iterator;e._ongoingPromise=void 0,e._isFinished=!0,B(e._reader),this._resolvePromise({value:void 0,done:!0})},e.prototype._errorSteps=function(e){var r=this._iterator;r._ongoingPromise=void 0,r._isFinished=!0,B(r._reader),this._rejectPromise(e)},e}(),Te=((ve={next:function(){return Pe(this)?this._asyncIteratorImpl.next():p(Ce("next"))},return:function(e){return Pe(this)?this._asyncIteratorImpl.return(e):p(Ce("return"))}})[ye]=function(){return this},ve);function Pe(e){if(!o(e))return!1;if(!Object.prototype.hasOwnProperty.call(e,"_asyncIteratorImpl"))return!1;try{return e._asyncIteratorImpl instanceof we}catch(e){return!1}}function Ce(e){return new TypeError("ReadableStreamAsyncIterator.".concat(e," can only be used on a ReadableSteamAsyncIterator"))}Object.defineProperty(Te,ye,{enumerable:!1});var qe=Number.isNaN||function(e){return e!=e};function Ee(e){var r=he(e.buffer,e.byteOffset,e.byteOffset+e.byteLength);return new Uint8Array(r)}function We(e){var r=e._queue.shift();return e._queueTotalSize-=r.size,e._queueTotalSize<0&&(e._queueTotalSize=0),r.value}function Oe(e,r,t){if("number"!=typeof(o=t)||qe(o)||o<0||t===1/0)throw new RangeError("Size must be a finite, non-NaN, non-negative number.");var o;e._queue.push({value:r,size:t}),e._queueTotalSize+=t}function je(e){e._queue=new R,e._queueTotalSize=0}function Be(e){return e===DataView}function ke(e){return Be(e)?1:e.BYTES_PER_ELEMENT}var Ae=function(){function ReadableStreamBYOBRequest(){throw new TypeError("Illegal constructor")}return Object.defineProperty(ReadableStreamBYOBRequest.prototype,"view",{get:function(){if(!Fe(this))throw sr("view");return this._view},enumerable:!1,configurable:!0}),ReadableStreamBYOBRequest.prototype.respond=function(e){if(!Fe(this))throw sr("respond");if(Q(e,1,"respond"),e=U(e,"First parameter"),void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");if(be(this._view.buffer))throw new TypeError("The BYOB request's buffer has been detached and so cannot be used as a response");ar(this._associatedReadableByteStreamController,e)},ReadableStreamBYOBRequest.prototype.respondWithNewView=function(e){if(!Fe(this))throw sr("respondWithNewView");if(Q(e,1,"respondWithNewView"),!ArrayBuffer.isView(e))throw new TypeError("You can only respond with array buffer views");if(void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");if(be(e.buffer))throw new TypeError("The given view's buffer has been detached and so cannot be used as a response");ur(this._associatedReadableByteStreamController,e)},ReadableStreamBYOBRequest}();Object.defineProperties(Ae.prototype,{respond:{enumerable:!0},respondWithNewView:{enumerable:!0},view:{enumerable:!0}}),i(Ae.prototype.respond,"respond"),i(Ae.prototype.respondWithNewView,"respondWithNewView"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Ae.prototype,r.toStringTag,{value:"ReadableStreamBYOBRequest",configurable:!0});var ze=function(){function ReadableByteStreamController(){throw new TypeError("Illegal constructor")}return Object.defineProperty(ReadableByteStreamController.prototype,"byobRequest",{get:function(){if(!De(this))throw cr("byobRequest");return nr(this)},enumerable:!1,configurable:!0}),Object.defineProperty(ReadableByteStreamController.prototype,"desiredSize",{get:function(){if(!De(this))throw cr("desiredSize");return ir(this)},enumerable:!1,configurable:!0}),ReadableByteStreamController.prototype.close=function(){if(!De(this))throw cr("close");if(this._closeRequested)throw new TypeError("The stream has already been closed; do not close it again!");var e=this._controlledReadableByteStream._state;if("readable"!==e)throw new TypeError("The stream (in ".concat(e," state) is not in the readable state and cannot be closed"));er(this)},ReadableByteStreamController.prototype.enqueue=function(e){if(!De(this))throw cr("enqueue");if(Q(e,1,"enqueue"),!ArrayBuffer.isView(e))throw new TypeError("chunk must be an array buffer view");if(0===e.byteLength)throw new TypeError("chunk must have non-zero byteLength");if(0===e.buffer.byteLength)throw new TypeError("chunk's buffer must have non-zero byteLength");if(this._closeRequested)throw new TypeError("stream is closed or draining");var r=this._controlledReadableByteStream._state;if("readable"!==r)throw new TypeError("The stream (in ".concat(r," state) is not in the readable state and cannot be enqueued to"));rr(this,e)},ReadableByteStreamController.prototype.error=function(e){if(void 0===e&&(e=void 0),!De(this))throw cr("error");tr(this,e)},ReadableByteStreamController.prototype[C]=function(e){Ie(this),je(this);var r=this._cancelAlgorithm(e);return $e(this),r},ReadableByteStreamController.prototype[q]=function(e){var r=this._controlledReadableByteStream;if(this._queueTotalSize>0)or(this,e);else{var t=this._autoAllocateChunkSize;if(void 0!==t){var o=void 0;try{o=new ArrayBuffer(t)}catch(r){return void e._errorSteps(r)}var n={buffer:o,bufferByteLength:t,byteOffset:0,byteLength:t,bytesFilled:0,minimumFill:1,elementSize:1,viewConstructor:Uint8Array,readerType:"default"};this._pendingPullIntos.push(n)}J(r,e),Le(this)}},ReadableByteStreamController.prototype[E]=function(){return this._queueTotalSize>0},ReadableByteStreamController.prototype[W]=function(){if(this._pendingPullIntos.length>0){var e=this._pendingPullIntos.peek();e.readerType="none",this._pendingPullIntos=new R,this._pendingPullIntos.push(e)}},ReadableByteStreamController}();function De(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableByteStream")&&e instanceof ze)}function Fe(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_associatedReadableByteStreamController")&&e instanceof Ae)}function Le(e){var r=function(e){var r=e._controlledReadableByteStream;if("readable"!==r._state)return!1;if(e._closeRequested)return!1;if(!e._started)return!1;if($(r)&&Z(r)>0)return!0;if(hr(r)&&br(r)>0)return!0;var t=ir(e);if(t>0)return!0;return!1}(e);r&&(e._pulling?e._pullAgain=!0:(e._pulling=!0,h(e._pullAlgorithm(),function(){return e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,Le(e)),null},function(r){return tr(e,r),null})))}function Ie(e){Xe(e),e._pendingPullIntos=new R}function Me(e,r){var t=!1;"closed"===e._state&&(t=!0);var o=Ye(r);"default"===r.readerType?K(e,o,t):function(e,r,t){var o=e._reader,n=o._readIntoRequests.shift();t?n._closeSteps(r):n._chunkSteps(r)}(e,o,t)}function xe(e,r){for(var t=0;t0&&Ne(e,r.buffer,r.byteOffset,r.bytesFilled),Ze(e)}function Ve(e,r){var t=Math.min(e._queueTotalSize,r.byteLength-r.bytesFilled),o=r.bytesFilled+t,n=t,i=!1,a=o-o%r.elementSize;a>=r.minimumFill&&(n=a-r.bytesFilled,i=!0);for(var u=e._queue;n>0;){var l=u.peek(),s=Math.min(n,l.byteLength),c=r.byteOffset+r.bytesFilled;de(r.buffer,c,l.buffer,l.byteOffset,s),l.byteLength===s?u.shift():(l.byteOffset+=s,l.byteLength-=s),e._queueTotalSize-=s,Ue(e,s,r),n-=s}return i}function Ue(e,r,t){t.bytesFilled+=r}function Ge(e){0===e._queueTotalSize&&e._closeRequested?($e(e),ro(e._controlledReadableByteStream)):Le(e)}function Xe(e){null!==e._byobRequest&&(e._byobRequest._associatedReadableByteStreamController=void 0,e._byobRequest._view=null,e._byobRequest=null)}function Je(e){for(var r=[];e._pendingPullIntos.length>0&&0!==e._queueTotalSize;){var t=e._pendingPullIntos.peek();Ve(e,t)&&(Ze(e),r.push(t))}return r}function Ke(e,r){var t=e._pendingPullIntos.peek();Xe(e),"closed"===e._controlledReadableByteStream._state?function(e,r){"none"===r.readerType&&Ze(e);var t=e._controlledReadableByteStream;if(hr(t)){for(var o=[];o.length0){var n=t.byteOffset+t.bytesFilled;Ne(e,t.buffer,n-o,o)}t.bytesFilled-=o;var i=Je(e);Me(e._controlledReadableByteStream,t),xe(e._controlledReadableByteStream,i)}}else{He(e,t);var a=Je(e);xe(e._controlledReadableByteStream,a)}}(e,r,t),Le(e)}function Ze(e){return e._pendingPullIntos.shift()}function $e(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0}function er(e){var r=e._controlledReadableByteStream;if(!e._closeRequested&&"readable"===r._state)if(e._queueTotalSize>0)e._closeRequested=!0;else{if(e._pendingPullIntos.length>0){var t=e._pendingPullIntos.peek();if(t.bytesFilled%t.elementSize!==0){var o=new TypeError("Insufficient bytes to fill elements in the given buffer");throw tr(e,o),o}}$e(e),ro(r)}}function rr(e,r){var t=e._controlledReadableByteStream;if(!e._closeRequested&&"readable"===t._state){var o=r.buffer,n=r.byteOffset,i=r.byteLength;if(be(o))throw new TypeError("chunk's buffer is detached and so cannot be enqueued");var a=pe(o);if(e._pendingPullIntos.length>0){var u=e._pendingPullIntos.peek();if(be(u.buffer))throw new TypeError("The BYOB request's buffer has been detached and so cannot be filled with an enqueued chunk");Xe(e),u.buffer=pe(u.buffer),"none"===u.readerType&&He(e,u)}if($(t))if(function(e){for(var r=e._controlledReadableByteStream._reader;r._readRequests.length>0;){if(0===e._queueTotalSize)return;or(e,r._readRequests.shift())}}(e),0===Z(t))Qe(e,a,n,i);else e._pendingPullIntos.length>0&&Ze(e),K(t,new Uint8Array(a,n,i),!1);else if(hr(t)){Qe(e,a,n,i),xe(t,Je(e))}else Qe(e,a,n,i);Le(e)}}function tr(e,r){var t=e._controlledReadableByteStream;"readable"===t._state&&(Ie(e),je(e),$e(e),to(t,r))}function or(e,r){var t=e._queue.shift();e._queueTotalSize-=t.byteLength,Ge(e);var o=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);r._chunkSteps(o)}function nr(e){if(null===e._byobRequest&&e._pendingPullIntos.length>0){var r=e._pendingPullIntos.peek(),t=new Uint8Array(r.buffer,r.byteOffset+r.bytesFilled,r.byteLength-r.bytesFilled),o=Object.create(Ae.prototype);!function(e,r,t){e._associatedReadableByteStreamController=r,e._view=t}(o,e,t),e._byobRequest=o}return e._byobRequest}function ir(e){var r=e._controlledReadableByteStream._state;return"errored"===r?null:"closed"===r?0:e._strategyHWM-e._queueTotalSize}function ar(e,r){var t=e._pendingPullIntos.peek();if("closed"===e._controlledReadableByteStream._state){if(0!==r)throw new TypeError("bytesWritten must be 0 when calling respond() on a closed stream")}else{if(0===r)throw new TypeError("bytesWritten must be greater than 0 when calling respond() on a readable stream");if(t.bytesFilled+r>t.byteLength)throw new RangeError("bytesWritten out of range")}t.buffer=pe(t.buffer),Ke(e,r)}function ur(e,r){var t=e._pendingPullIntos.peek();if("closed"===e._controlledReadableByteStream._state){if(0!==r.byteLength)throw new TypeError("The view's length must be 0 when calling respondWithNewView() on a closed stream")}else if(0===r.byteLength)throw new TypeError("The view's length must be greater than 0 when calling respondWithNewView() on a readable stream");if(t.byteOffset+t.bytesFilled!==r.byteOffset)throw new RangeError("The region specified by view does not match byobRequest");if(t.bufferByteLength!==r.buffer.byteLength)throw new RangeError("The buffer of view has different capacity than byobRequest");if(t.bytesFilled+r.byteLength>t.byteLength)throw new RangeError("The region specified by view is larger than byobRequest");var o=r.byteLength;t.buffer=pe(r.buffer),Ke(e,o)}function lr(e,r,t,o,n,i,a){r._controlledReadableByteStream=e,r._pullAgain=!1,r._pulling=!1,r._byobRequest=null,r._queue=r._queueTotalSize=void 0,je(r),r._closeRequested=!1,r._started=!1,r._strategyHWM=i,r._pullAlgorithm=o,r._cancelAlgorithm=n,r._autoAllocateChunkSize=a,r._pendingPullIntos=new R,e._readableStreamController=r,h(d(t()),function(){return r._started=!0,Le(r),null},function(e){return tr(r,e),null})}function sr(e){return new TypeError("ReadableStreamBYOBRequest.prototype.".concat(e," can only be used on a ReadableStreamBYOBRequest"))}function cr(e){return new TypeError("ReadableByteStreamController.prototype.".concat(e," can only be used on a ReadableByteStreamController"))}function fr(e,r){if("byob"!==(e="".concat(e)))throw new TypeError("".concat(r," '").concat(e,"' is not a valid enumeration value for ReadableStreamReaderMode"));return e}function dr(e){return new _r(e)}function pr(e,r){e._reader._readIntoRequests.push(r)}function br(e){return e._reader._readIntoRequests.length}function hr(e){var r=e._reader;return void 0!==r&&!!yr(r)}Object.defineProperties(ze.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},byobRequest:{enumerable:!0},desiredSize:{enumerable:!0}}),i(ze.prototype.close,"close"),i(ze.prototype.enqueue,"enqueue"),i(ze.prototype.error,"error"),"symbol"==typeof r.toStringTag&&Object.defineProperty(ze.prototype,r.toStringTag,{value:"ReadableByteStreamController",configurable:!0});var _r=function(){function ReadableStreamBYOBReader(e){if(Q(e,1,"ReadableStreamBYOBReader"),G(e,"First parameter"),$t(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");if(!De(e._readableStreamController))throw new TypeError("Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source");O(this,e),this._readIntoRequests=new R}return Object.defineProperty(ReadableStreamBYOBReader.prototype,"closed",{get:function(){return yr(this)?this._closedPromise:p(wr("closed"))},enumerable:!1,configurable:!0}),ReadableStreamBYOBReader.prototype.cancel=function(e){return void 0===e&&(e=void 0),yr(this)?void 0===this._ownerReadableStream?p(k("cancel")):j(this,e):p(wr("cancel"))},ReadableStreamBYOBReader.prototype.read=function(e,r){if(void 0===r&&(r={}),!yr(this))return p(wr("read"));if(!ArrayBuffer.isView(e))return p(new TypeError("view must be an array buffer view"));if(0===e.byteLength)return p(new TypeError("view must have non-zero byteLength"));if(0===e.buffer.byteLength)return p(new TypeError("view's buffer must have non-zero byteLength"));if(be(e.buffer))return p(new TypeError("view's buffer has been detached"));var t;try{t=function(e,r){var t;return M(e,r),{min:U(null!==(t=null==e?void 0:e.min)&&void 0!==t?t:1,"".concat(r," has member 'min' that"))}}(r,"options")}catch(e){return p(e)}var o=t.min;if(0===o)return p(new TypeError("options.min must be greater than 0"));if(function(e){return Be(e.constructor)}(e)){if(o>e.byteLength)return p(new RangeError("options.min must be less than or equal to view's byteLength"))}else if(o>e.length)return p(new RangeError("options.min must be less than or equal to view's length"));if(void 0===this._ownerReadableStream)return p(k("read from"));var n=function(e,r,t){var o=e._ownerReadableStream;return"errored"===o._state||function(e,r,t){var o=e._controlledReadableByteStream,n=ke(r.constructor);r.byteLength;var i=t*n;return!(e._pendingPullIntos.length>0)&&("closed"===o._state||e._queueTotalSize>=i)}(o._readableStreamController,r,t)}(this,e,o)?new vr:new mr;return gr(this,e,o,n),n._promise},ReadableStreamBYOBReader.prototype.releaseLock=function(){if(!yr(this))throw wr("releaseLock");void 0!==this._ownerReadableStream&&function(e){B(e);var r=new TypeError("Reader was released");Sr(e,r)}(this)},ReadableStreamBYOBReader}();Object.defineProperties(_r.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),i(_r.prototype.cancel,"cancel"),i(_r.prototype.read,"read"),i(_r.prototype.releaseLock,"releaseLock"),"symbol"==typeof r.toStringTag&&Object.defineProperty(_r.prototype,r.toStringTag,{value:"ReadableStreamBYOBReader",configurable:!0});var mr=function(){function e(){var e=this;this._promise=f(function(r,t){e._resolvePromise=r,e._rejectPromise=t})}return e.prototype._chunkSteps=function(e){this._resolvePromise({value:e,done:!1})},e.prototype._closeSteps=function(e){this._resolvePromise({value:e,done:!0})},e.prototype._errorSteps=function(e){this._rejectPromise(e)},e}(),vr=function(){function e(){this._promise=void 0}return e.prototype._chunkSteps=function(e){this._promise=c({value:e,done:!1})},e.prototype._closeSteps=function(e){this._promise=c({value:e,done:!0})},e.prototype._errorSteps=function(e){this._promise=p(e)},e}();function yr(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_readIntoRequests")&&e instanceof _r)}function gr(e,r,t,o){var n=e._ownerReadableStream;n._disturbed=!0,"errored"===n._state?o._errorSteps(n._storedError):function(e,r,t,o){var n,i=e._controlledReadableByteStream,a=r.constructor,u=ke(a),l=r.byteOffset,s=r.byteLength,c=t*u;try{n=pe(r.buffer)}catch(p){return void o._errorSteps(p)}var f={buffer:n,bufferByteLength:n.byteLength,byteOffset:l,byteLength:s,bytesFilled:0,minimumFill:c,elementSize:u,viewConstructor:a,readerType:"byob"};if(e._pendingPullIntos.length>0)return e._pendingPullIntos.push(f),void pr(i,o);if("closed"!==i._state){if(e._queueTotalSize>0){if(Ve(e,f)){var d=Ye(f);return Ge(e),void o._chunkSteps(d)}if(e._closeRequested){var p=new TypeError("Insufficient bytes to fill elements in the given buffer");return tr(e,p),void o._errorSteps(p)}}e._pendingPullIntos.push(f),pr(i,o),Le(e)}else{var b=new a(f.buffer,f.byteOffset,0);o._closeSteps(b)}}(n._readableStreamController,r,t,o)}function Sr(e,r){var t=e._readIntoRequests;e._readIntoRequests=new R,t.forEach(function(e){e._errorSteps(r)})}function wr(e){return new TypeError("ReadableStreamBYOBReader.prototype.".concat(e," can only be used on a ReadableStreamBYOBReader"))}function Rr(e,r){var t=e.highWaterMark;if(void 0===t)return r;if(qe(t)||t<0)throw new RangeError("Invalid highWaterMark");return t}function Tr(e){var r=e.size;return r||function(){return 1}}function Pr(e,r){M(e,r);var t=null==e?void 0:e.highWaterMark,o=null==e?void 0:e.size;return{highWaterMark:void 0===t?void 0:H(t),size:void 0===o?void 0:Cr(o,"".concat(r," has member 'size' that"))}}function Cr(e,r){return x(e,r),function(r){return H(e(r))}}function qr(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}function Er(e,r,t){return x(e,t),function(){return w(e,r,[])}}function Wr(e,r,t){return x(e,t),function(t){return S(e,r,[t])}}function Or(e,r,t){return x(e,t),function(t,o){return w(e,r,[t,o])}}function jr(e,r){if(!zr(e))throw new TypeError("".concat(r," is not a WritableStream."))}var Br=function(){function WritableStream(e,r){void 0===e&&(e={}),void 0===r&&(r={}),void 0===e?e=null:Y(e,"First parameter");var t=Pr(r,"Second parameter"),o=function(e,r){M(e,r);var t=null==e?void 0:e.abort,o=null==e?void 0:e.close,n=null==e?void 0:e.start,i=null==e?void 0:e.type,a=null==e?void 0:e.write;return{abort:void 0===t?void 0:qr(t,e,"".concat(r," has member 'abort' that")),close:void 0===o?void 0:Er(o,e,"".concat(r," has member 'close' that")),start:void 0===n?void 0:Wr(n,e,"".concat(r," has member 'start' that")),write:void 0===a?void 0:Or(a,e,"".concat(r," has member 'write' that")),type:i}}(e,"First parameter");if(Ar(this),void 0!==o.type)throw new RangeError("Invalid type is specified");var n=Tr(t);!function(e,r,t,o){var n,i,a,u,l=Object.create($r.prototype);n=void 0!==r.start?function(){return r.start(l)}:function(){};i=void 0!==r.write?function(e){return r.write(e,l)}:function(){return d(void 0)};a=void 0!==r.close?function(){return r.close()}:function(){return d(void 0)};u=void 0!==r.abort?function(e){return r.abort(e)}:function(){return d(void 0)};rt(e,l,n,i,a,u,t,o)}(this,o,Rr(t,1),n)}return Object.defineProperty(WritableStream.prototype,"locked",{get:function(){if(!zr(this))throw lt("locked");return Dr(this)},enumerable:!1,configurable:!0}),WritableStream.prototype.abort=function(e){return void 0===e&&(e=void 0),zr(this)?Dr(this)?p(new TypeError("Cannot abort a stream that already has a writer")):Fr(this,e):p(lt("abort"))},WritableStream.prototype.close=function(){return zr(this)?Dr(this)?p(new TypeError("Cannot close a stream that already has a writer")):Yr(this)?p(new TypeError("Cannot close an already-closing stream")):Lr(this):p(lt("close"))},WritableStream.prototype.getWriter=function(){if(!zr(this))throw lt("getWriter");return kr(this)},WritableStream}();function kr(e){return new Hr(e)}function Ar(e){e._state="writable",e._storedError=void 0,e._writer=void 0,e._writableStreamController=void 0,e._writeRequests=new R,e._inFlightWriteRequest=void 0,e._closeRequest=void 0,e._inFlightCloseRequest=void 0,e._pendingAbortRequest=void 0,e._backpressure=!1}function zr(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_writableStreamController")&&e instanceof Br)}function Dr(e){return void 0!==e._writer}function Fr(e,r){var t;if("closed"===e._state||"errored"===e._state)return d(void 0);e._writableStreamController._abortReason=r,null===(t=e._writableStreamController._abortController)||void 0===t||t.abort(r);var o=e._state;if("closed"===o||"errored"===o)return d(void 0);if(void 0!==e._pendingAbortRequest)return e._pendingAbortRequest._promise;var n=!1;"erroring"===o&&(n=!0,r=void 0);var i=f(function(t,o){e._pendingAbortRequest={_promise:void 0,_resolve:t,_reject:o,_reason:r,_wasAlreadyErroring:n}});return e._pendingAbortRequest._promise=i,n||Mr(e,r),i}function Lr(e){var r=e._state;if("closed"===r||"errored"===r)return p(new TypeError("The stream (in ".concat(r," state) is not in the writable state and cannot be closed")));var t,o=f(function(r,t){var o={_resolve:r,_reject:t};e._closeRequest=o}),n=e._writer;return void 0!==n&&e._backpressure&&"writable"===r&>(n),Oe(t=e._writableStreamController,Zr,0),nt(t),o}function Ir(e,r){"writable"!==e._state?xr(e):Mr(e,r)}function Mr(e,r){var t=e._writableStreamController;e._state="erroring",e._storedError=r;var o=e._writer;void 0!==o&&Xr(o,r),!function(e){if(void 0===e._inFlightWriteRequest&&void 0===e._inFlightCloseRequest)return!1;return!0}(e)&&t._started&&xr(e)}function xr(e){e._state="errored",e._writableStreamController[P]();var r=e._storedError;if(e._writeRequests.forEach(function(e){e._reject(r)}),e._writeRequests=new R,void 0!==e._pendingAbortRequest){var t=e._pendingAbortRequest;if(e._pendingAbortRequest=void 0,t._wasAlreadyErroring)return t._reject(r),void Qr(e);h(e._writableStreamController[T](t._reason),function(){return t._resolve(),Qr(e),null},function(r){return t._reject(r),Qr(e),null})}else Qr(e)}function Yr(e){return void 0!==e._closeRequest||void 0!==e._inFlightCloseRequest}function Qr(e){void 0!==e._closeRequest&&(e._closeRequest._reject(e._storedError),e._closeRequest=void 0);var r=e._writer;void 0!==r&&bt(r,e._storedError)}function Nr(e,r){var t=e._writer;void 0!==t&&r!==e._backpressure&&(r?function(e){_t(e)}(t):gt(t)),e._backpressure=r}Object.defineProperties(Br.prototype,{abort:{enumerable:!0},close:{enumerable:!0},getWriter:{enumerable:!0},locked:{enumerable:!0}}),i(Br.prototype.abort,"abort"),i(Br.prototype.close,"close"),i(Br.prototype.getWriter,"getWriter"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Br.prototype,r.toStringTag,{value:"WritableStream",configurable:!0});var Hr=function(){function WritableStreamDefaultWriter(e){if(Q(e,1,"WritableStreamDefaultWriter"),jr(e,"First parameter"),Dr(e))throw new TypeError("This stream has already been locked for exclusive writing by another writer");this._ownerWritableStream=e,e._writer=this;var r,t=e._state;if("writable"===t)!Yr(e)&&e._backpressure?_t(this):vt(this),dt(this);else if("erroring"===t)mt(this,e._storedError),dt(this);else if("closed"===t)vt(this),dt(r=this),ht(r);else{var o=e._storedError;mt(this,o),pt(this,o)}}return Object.defineProperty(WritableStreamDefaultWriter.prototype,"closed",{get:function(){return Vr(this)?this._closedPromise:p(ct("closed"))},enumerable:!1,configurable:!0}),Object.defineProperty(WritableStreamDefaultWriter.prototype,"desiredSize",{get:function(){if(!Vr(this))throw ct("desiredSize");if(void 0===this._ownerWritableStream)throw ft("desiredSize");return function(e){var r=e._ownerWritableStream,t=r._state;if("errored"===t||"erroring"===t)return null;if("closed"===t)return 0;return ot(r._writableStreamController)}(this)},enumerable:!1,configurable:!0}),Object.defineProperty(WritableStreamDefaultWriter.prototype,"ready",{get:function(){return Vr(this)?this._readyPromise:p(ct("ready"))},enumerable:!1,configurable:!0}),WritableStreamDefaultWriter.prototype.abort=function(e){return void 0===e&&(e=void 0),Vr(this)?void 0===this._ownerWritableStream?p(ft("abort")):function(e,r){return Fr(e._ownerWritableStream,r)}(this,e):p(ct("abort"))},WritableStreamDefaultWriter.prototype.close=function(){if(!Vr(this))return p(ct("close"));var e=this._ownerWritableStream;return void 0===e?p(ft("close")):Yr(e)?p(new TypeError("Cannot close an already-closing stream")):Ur(this)},WritableStreamDefaultWriter.prototype.releaseLock=function(){if(!Vr(this))throw ct("releaseLock");void 0!==this._ownerWritableStream&&Jr(this)},WritableStreamDefaultWriter.prototype.write=function(e){return void 0===e&&(e=void 0),Vr(this)?void 0===this._ownerWritableStream?p(ft("write to")):Kr(this,e):p(ct("write"))},WritableStreamDefaultWriter}();function Vr(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_ownerWritableStream")&&e instanceof Hr)}function Ur(e){return Lr(e._ownerWritableStream)}function Gr(e,r){"pending"===e._closedPromiseState?bt(e,r):function(e,r){pt(e,r)}(e,r)}function Xr(e,r){"pending"===e._readyPromiseState?yt(e,r):function(e,r){mt(e,r)}(e,r)}function Jr(e){var r=e._ownerWritableStream,t=new TypeError("Writer was released and can no longer be used to monitor the stream's closedness");Xr(e,t),Gr(e,t),r._writer=void 0,e._ownerWritableStream=void 0}function Kr(e,r){var t=e._ownerWritableStream,o=t._writableStreamController,n=function(e,r){if(void 0===e._strategySizeAlgorithm)return 1;try{return e._strategySizeAlgorithm(r)}catch(r){return it(e,r),1}}(o,r);if(t!==e._ownerWritableStream)return p(ft("write to"));var i=t._state;if("errored"===i)return p(t._storedError);if(Yr(t)||"closed"===i)return p(new TypeError("The stream is closing or closed and cannot be written to"));if("erroring"===i)return p(t._storedError);var a=function(e){return f(function(r,t){var o={_resolve:r,_reject:t};e._writeRequests.push(o)})}(t);return function(e,r,t){try{Oe(e,r,t)}catch(r){return void it(e,r)}var o=e._controlledWritableStream;if(!Yr(o)&&"writable"===o._state){Nr(o,at(e))}nt(e)}(o,r,n),a}Object.defineProperties(Hr.prototype,{abort:{enumerable:!0},close:{enumerable:!0},releaseLock:{enumerable:!0},write:{enumerable:!0},closed:{enumerable:!0},desiredSize:{enumerable:!0},ready:{enumerable:!0}}),i(Hr.prototype.abort,"abort"),i(Hr.prototype.close,"close"),i(Hr.prototype.releaseLock,"releaseLock"),i(Hr.prototype.write,"write"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Hr.prototype,r.toStringTag,{value:"WritableStreamDefaultWriter",configurable:!0});var Zr={},$r=function(){function WritableStreamDefaultController(){throw new TypeError("Illegal constructor")}return Object.defineProperty(WritableStreamDefaultController.prototype,"abortReason",{get:function(){if(!et(this))throw st("abortReason");return this._abortReason},enumerable:!1,configurable:!0}),Object.defineProperty(WritableStreamDefaultController.prototype,"signal",{get:function(){if(!et(this))throw st("signal");if(void 0===this._abortController)throw new TypeError("WritableStreamDefaultController.prototype.signal is not supported");return this._abortController.signal},enumerable:!1,configurable:!0}),WritableStreamDefaultController.prototype.error=function(e){if(void 0===e&&(e=void 0),!et(this))throw st("error");"writable"===this._controlledWritableStream._state&&ut(this,e)},WritableStreamDefaultController.prototype[T]=function(e){var r=this._abortAlgorithm(e);return tt(this),r},WritableStreamDefaultController.prototype[P]=function(){je(this)},WritableStreamDefaultController}();function et(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledWritableStream")&&e instanceof $r)}function rt(e,r,t,o,n,i,a,u){r._controlledWritableStream=e,e._writableStreamController=r,r._queue=void 0,r._queueTotalSize=void 0,je(r),r._abortReason=void 0,r._abortController=function(){if("function"==typeof AbortController)return new AbortController}(),r._started=!1,r._strategySizeAlgorithm=u,r._strategyHWM=a,r._writeAlgorithm=o,r._closeAlgorithm=n,r._abortAlgorithm=i;var l=at(r);Nr(e,l),h(d(t()),function(){return r._started=!0,nt(r),null},function(t){return r._started=!0,Ir(e,t),null})}function tt(e){e._writeAlgorithm=void 0,e._closeAlgorithm=void 0,e._abortAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function ot(e){return e._strategyHWM-e._queueTotalSize}function nt(e){var r=e._controlledWritableStream;if(e._started&&void 0===r._inFlightWriteRequest)if("erroring"!==r._state){if(0!==e._queue.length){var t=e._queue.peek().value;t===Zr?function(e){var r=e._controlledWritableStream;(function(e){e._inFlightCloseRequest=e._closeRequest,e._closeRequest=void 0})(r),We(e);var t=e._closeAlgorithm();tt(e),h(t,function(){return function(e){e._inFlightCloseRequest._resolve(void 0),e._inFlightCloseRequest=void 0,"erroring"===e._state&&(e._storedError=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._resolve(),e._pendingAbortRequest=void 0)),e._state="closed";var r=e._writer;void 0!==r&&ht(r)}(r),null},function(e){return function(e,r){e._inFlightCloseRequest._reject(r),e._inFlightCloseRequest=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._reject(r),e._pendingAbortRequest=void 0),Ir(e,r)}(r,e),null})}(e):function(e,r){var t=e._controlledWritableStream;!function(e){e._inFlightWriteRequest=e._writeRequests.shift()}(t);var o=e._writeAlgorithm(r);h(o,function(){!function(e){e._inFlightWriteRequest._resolve(void 0),e._inFlightWriteRequest=void 0}(t);var r=t._state;if(We(e),!Yr(t)&&"writable"===r){var o=at(e);Nr(t,o)}return nt(e),null},function(r){return"writable"===t._state&&tt(e),function(e,r){e._inFlightWriteRequest._reject(r),e._inFlightWriteRequest=void 0,Ir(e,r)}(t,r),null})}(e,t)}}else xr(r)}function it(e,r){"writable"===e._controlledWritableStream._state&&ut(e,r)}function at(e){return ot(e)<=0}function ut(e,r){var t=e._controlledWritableStream;tt(e),Mr(t,r)}function lt(e){return new TypeError("WritableStream.prototype.".concat(e," can only be used on a WritableStream"))}function st(e){return new TypeError("WritableStreamDefaultController.prototype.".concat(e," can only be used on a WritableStreamDefaultController"))}function ct(e){return new TypeError("WritableStreamDefaultWriter.prototype.".concat(e," can only be used on a WritableStreamDefaultWriter"))}function ft(e){return new TypeError("Cannot "+e+" a stream using a released writer")}function dt(e){e._closedPromise=f(function(r,t){e._closedPromise_resolve=r,e._closedPromise_reject=t,e._closedPromiseState="pending"})}function pt(e,r){dt(e),bt(e,r)}function bt(e,r){void 0!==e._closedPromise_reject&&(y(e._closedPromise),e._closedPromise_reject(r),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="rejected")}function ht(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="resolved")}function _t(e){e._readyPromise=f(function(r,t){e._readyPromise_resolve=r,e._readyPromise_reject=t}),e._readyPromiseState="pending"}function mt(e,r){_t(e),yt(e,r)}function vt(e){_t(e),gt(e)}function yt(e,r){void 0!==e._readyPromise_reject&&(y(e._readyPromise),e._readyPromise_reject(r),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="rejected")}function gt(e){void 0!==e._readyPromise_resolve&&(e._readyPromise_resolve(void 0),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="fulfilled")}Object.defineProperties($r.prototype,{abortReason:{enumerable:!0},signal:{enumerable:!0},error:{enumerable:!0}}),"symbol"==typeof r.toStringTag&&Object.defineProperty($r.prototype,r.toStringTag,{value:"WritableStreamDefaultController",configurable:!0});var St="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof global?global:void 0;var wt,Rt=(function(e){if("function"!=typeof e&&"object"!=typeof e)return!1;if("DOMException"!==e.name)return!1;try{return new e,!0}catch(e){return!1}}(wt=null==St?void 0:St.DOMException)?wt:void 0)||function(){var e=function(e,r){this.message=e||"",this.name=r||"Error",Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)};return i(e,"DOMException"),e.prototype=Object.create(Error.prototype),Object.defineProperty(e.prototype,"constructor",{value:e,writable:!0,configurable:!0}),e}();function Tt(e,r,t,o,n,i){var a=X(e),u=kr(r);e._disturbed=!0;var l=new Pt(u),s=new qt(l);return f(function(c,m){var v,g,S,w;if(void 0!==i){if(v=function(){var t=void 0!==i.reason?i.reason:new Rt("Aborted","AbortError"),a=[];o||a.push(function(){return"writable"===r._state?Fr(r,t):d(void 0)}),n||a.push(function(){return"readable"===e._state?eo(e,t):d(void 0)}),P(function(){return Promise.all(a.map(function(e){return e()}))},!0,t)},i.aborted)return void v();i.addEventListener("abort",v)}function R(){for(;!l._shuttingDown&&!r._backpressure&&"writable"===r._state&&!Yr(r)&&"readable"===e._state&&le(a);)ue(a,s);if(l._shuttingDown)return d(!0);if(r._backpressure)return b(u._readyPromise,R);var t=new Ct(l);return ue(a,t),t._promise}if(Et(e,a._closedPromise,function(e){return o?C(!0,e):P(function(){return Fr(r,e)},!0,e),null}),Et(r,u._closedPromise,function(r){return n?C(!0,r):P(function(){return eo(e,r)},!0,r),null}),g=e,S=a._closedPromise,w=function(){return t?C():P(function(){return function(e){var r=e._ownerWritableStream,t=r._state;return Yr(r)||"closed"===t?d(void 0):"errored"===t?p(r._storedError):Ur(e)}(u)}),null},"closed"===g._state?w():_(S,w),Yr(r)||"closed"===r._state){var T=new TypeError("the destination writable stream closed before all data could be piped to it");n?C(!0,T):P(function(){return eo(e,T)},!0,T)}function P(e,t,o){function n(){return h(e(),function(){return q(t,o)},function(e){return q(!0,e)}),null}l._shuttingDown||(l._shuttingDown=!0,"writable"!==r._state||Yr(r)?n():_(l._waitForWritesToFinish(),n))}function C(e,t){l._shuttingDown||(l._shuttingDown=!0,"writable"!==r._state||Yr(r)?q(e,t):_(l._waitForWritesToFinish(),function(){return q(e,t)}))}function q(e,r){return Jr(u),B(a),void 0!==i&&i.removeEventListener("abort",v),e?m(r):c(void 0),null}y(f(function(e,r){!function t(o){o?e():b(R(),t,r)}(!1)}))})}var Pt=function(){function e(e){this._writer=e,this._shuttingDown=!1,this._currentWrite=d(void 0)}return e.prototype._waitForWritesToFinish=function(){var e=this,r=this._currentWrite;return b(this._currentWrite,function(){return r!==e._currentWrite?e._waitForWritesToFinish():void 0})},e}(),Ct=function(){function e(e){var r=this;this._state=e,this._promise=f(function(e,t){r._resolvePromise=e,r._rejectPromise=t})}return e.prototype._chunkSteps=function(e){this._state._currentWrite=b(Kr(this._state._writer,e),void 0,t),this._resolvePromise(!1)},e.prototype._closeSteps=function(){this._resolvePromise(!0)},e.prototype._errorSteps=function(e){this._rejectPromise(e)},e}(),qt=function(){function e(e){this._state=e}return e.prototype._chunkSteps=function(e){this._state._currentWrite=b(Kr(this._state._writer,e),void 0,t)},e.prototype._closeSteps=function(){},e.prototype._errorSteps=function(e){},e}();function Et(e,r,t){"errored"===e._state?t(e._storedError):m(r,t)}var Wt=function(){function ReadableStreamDefaultController(){throw new TypeError("Illegal constructor")}return Object.defineProperty(ReadableStreamDefaultController.prototype,"desiredSize",{get:function(){if(!Ot(this))throw Mt("desiredSize");return Ft(this)},enumerable:!1,configurable:!0}),ReadableStreamDefaultController.prototype.close=function(){if(!Ot(this))throw Mt("close");if(!Lt(this))throw new TypeError("The stream is not in a state that permits close");At(this)},ReadableStreamDefaultController.prototype.enqueue=function(e){if(void 0===e&&(e=void 0),!Ot(this))throw Mt("enqueue");if(!Lt(this))throw new TypeError("The stream is not in a state that permits enqueue");return zt(this,e)},ReadableStreamDefaultController.prototype.error=function(e){if(void 0===e&&(e=void 0),!Ot(this))throw Mt("error");Dt(this,e)},ReadableStreamDefaultController.prototype[C]=function(e){je(this);var r=this._cancelAlgorithm(e);return kt(this),r},ReadableStreamDefaultController.prototype[q]=function(e){var r=this._controlledReadableStream;if(this._queue.length>0){var t=We(this);this._closeRequested&&0===this._queue.length?(kt(this),ro(r)):jt(this),e._chunkSteps(t)}else J(r,e),jt(this)},ReadableStreamDefaultController.prototype[E]=function(){return this._queue.length>0},ReadableStreamDefaultController.prototype[W]=function(){},ReadableStreamDefaultController}();function Ot(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableStream")&&e instanceof Wt)}function jt(e){Bt(e)&&(e._pulling?e._pullAgain=!0:(e._pulling=!0,h(e._pullAlgorithm(),function(){return e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,jt(e)),null},function(r){return Dt(e,r),null})))}function Bt(e){var r=e._controlledReadableStream;return!!Lt(e)&&(!!e._started&&(!!($t(r)&&Z(r)>0)||Ft(e)>0))}function kt(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function At(e){if(Lt(e)){var r=e._controlledReadableStream;e._closeRequested=!0,0===e._queue.length&&(kt(e),ro(r))}}function zt(e,r){if(Lt(e)){var t=e._controlledReadableStream;if($t(t)&&Z(t)>0)K(t,r,!1);else{var o=void 0;try{o=e._strategySizeAlgorithm(r)}catch(r){throw Dt(e,r),r}try{Oe(e,r,o)}catch(r){throw Dt(e,r),r}}jt(e)}}function Dt(e,r){var t=e._controlledReadableStream;"readable"===t._state&&(je(e),kt(e),to(t,r))}function Ft(e){var r=e._controlledReadableStream._state;return"errored"===r?null:"closed"===r?0:e._strategyHWM-e._queueTotalSize}function Lt(e){var r=e._controlledReadableStream._state;return!e._closeRequested&&"readable"===r}function It(e,r,t,o,n,i,a){r._controlledReadableStream=e,r._queue=void 0,r._queueTotalSize=void 0,je(r),r._started=!1,r._closeRequested=!1,r._pullAgain=!1,r._pulling=!1,r._strategySizeAlgorithm=a,r._strategyHWM=i,r._pullAlgorithm=o,r._cancelAlgorithm=n,e._readableStreamController=r,h(d(t()),function(){return r._started=!0,jt(r),null},function(e){return Dt(r,e),null})}function Mt(e){return new TypeError("ReadableStreamDefaultController.prototype.".concat(e," can only be used on a ReadableStreamDefaultController"))}function xt(e,r){return De(e._readableStreamController)?function(e){var r,t,o,n,i,a=X(e),u=!1,l=!1,s=!1,c=!1,p=!1,b=f(function(e){i=e});function h(e){m(e._closedPromise,function(r){return e!==a||(tr(o._readableStreamController,r),tr(n._readableStreamController,r),c&&p||i(void 0)),null})}function _(){yr(a)&&(B(a),h(a=X(e))),ue(a,{_chunkSteps:function(r){g(function(){l=!1,s=!1;var t=r,a=r;if(!c&&!p)try{a=Ee(r)}catch(r){return tr(o._readableStreamController,r),tr(n._readableStreamController,r),void i(eo(e,r))}c||rr(o._readableStreamController,t),p||rr(n._readableStreamController,a),u=!1,l?y():s&&S()})},_closeSteps:function(){u=!1,c||er(o._readableStreamController),p||er(n._readableStreamController),o._readableStreamController._pendingPullIntos.length>0&&ar(o._readableStreamController,0),n._readableStreamController._pendingPullIntos.length>0&&ar(n._readableStreamController,0),c&&p||i(void 0)},_errorSteps:function(){u=!1}})}function v(r,t){ae(a)&&(B(a),h(a=dr(e)));var f=t?n:o,d=t?o:n;gr(a,r,1,{_chunkSteps:function(r){g(function(){l=!1,s=!1;var o=t?p:c;if(t?c:p)o||ur(f._readableStreamController,r);else{var n=void 0;try{n=Ee(r)}catch(r){return tr(f._readableStreamController,r),tr(d._readableStreamController,r),void i(eo(e,r))}o||ur(f._readableStreamController,r),rr(d._readableStreamController,n)}u=!1,l?y():s&&S()})},_closeSteps:function(e){u=!1;var r=t?p:c,o=t?c:p;r||er(f._readableStreamController),o||er(d._readableStreamController),void 0!==e&&(r||ur(f._readableStreamController,e),!o&&d._readableStreamController._pendingPullIntos.length>0&&ar(d._readableStreamController,0)),r&&o||i(void 0)},_errorSteps:function(){u=!1}})}function y(){if(u)return l=!0,d(void 0);u=!0;var e=nr(o._readableStreamController);return null===e?_():v(e._view,!1),d(void 0)}function S(){if(u)return s=!0,d(void 0);u=!0;var e=nr(n._readableStreamController);return null===e?_():v(e._view,!0),d(void 0)}function w(o){if(c=!0,r=o,p){var n=fe([r,t]),a=eo(e,n);i(a)}return b}function R(o){if(p=!0,t=o,c){var n=fe([r,t]),a=eo(e,n);i(a)}return b}function T(){}return o=Jt(T,y,w),n=Jt(T,S,R),h(a),[o,n]}(e):function(e){var r,t,o,n,i,a=X(e),u=!1,l=!1,s=!1,c=!1,p=f(function(e){i=e});function b(){return u?(l=!0,d(void 0)):(u=!0,ue(a,{_chunkSteps:function(e){g(function(){l=!1;var r=e,t=e;s||zt(o._readableStreamController,r),c||zt(n._readableStreamController,t),u=!1,l&&b()})},_closeSteps:function(){u=!1,s||At(o._readableStreamController),c||At(n._readableStreamController),s&&c||i(void 0)},_errorSteps:function(){u=!1}}),d(void 0))}function h(o){if(s=!0,r=o,c){var n=fe([r,t]),a=eo(e,n);i(a)}return p}function _(o){if(c=!0,t=o,s){var n=fe([r,t]),a=eo(e,n);i(a)}return p}function v(){}return o=Xt(v,b,h),n=Xt(v,b,_),m(a._closedPromise,function(e){return Dt(o._readableStreamController,e),Dt(n._readableStreamController,e),s&&c||i(void 0),null}),[o,n]}(e)}function Yt(e){return o(r=e)&&void 0!==r.getReader?function(e){var r;function n(){var t;try{t=e.read()}catch(e){return p(e)}return v(t,function(e){if(!o(e))throw new TypeError("The promise returned by the reader.read() method must fulfill with an object");if(e.done)At(r._readableStreamController);else{var t=e.value;zt(r._readableStreamController,t)}})}function i(r){try{return d(e.cancel(r))}catch(e){return p(e)}}return r=Xt(t,n,i,0),r}(e.getReader()):function(e){var r,n=ge(e,"async");function i(){var e;try{e=Se(n)}catch(e){return p(e)}return v(d(e),function(e){if(!o(e))throw new TypeError("The promise returned by the iterator.next() method must fulfill with an object");if(e.done)At(r._readableStreamController);else{var t=e.value;zt(r._readableStreamController,t)}})}function a(e){var r,t=n.iterator;try{r=_e(t,"return")}catch(e){return p(e)}return void 0===r?d(void 0):v(w(r,t,[e]),function(e){if(!o(e))throw new TypeError("The promise returned by the iterator.return() method must fulfill with an object")})}return r=Xt(t,i,a,0),r}(e);var r}function Qt(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}function Nt(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}function Ht(e,r,t){return x(e,t),function(t){return S(e,r,[t])}}function Vt(e,r){if("bytes"!==(e="".concat(e)))throw new TypeError("".concat(r," '").concat(e,"' is not a valid enumeration value for ReadableStreamType"));return e}function Ut(e,r){M(e,r);var t=null==e?void 0:e.preventAbort,o=null==e?void 0:e.preventCancel,n=null==e?void 0:e.preventClose,i=null==e?void 0:e.signal;return void 0!==i&&function(e,r){if(!function(e){if("object"!=typeof e||null===e)return!1;try{return"boolean"==typeof e.aborted}catch(e){return!1}}(e))throw new TypeError("".concat(r," is not an AbortSignal."))}(i,"".concat(r," has member 'signal' that")),{preventAbort:Boolean(t),preventCancel:Boolean(o),preventClose:Boolean(n),signal:i}}Object.defineProperties(Wt.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},desiredSize:{enumerable:!0}}),i(Wt.prototype.close,"close"),i(Wt.prototype.enqueue,"enqueue"),i(Wt.prototype.error,"error"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Wt.prototype,r.toStringTag,{value:"ReadableStreamDefaultController",configurable:!0});var Gt=function(){function ReadableStream(e,r){void 0===e&&(e={}),void 0===r&&(r={}),void 0===e?e=null:Y(e,"First parameter");var t=Pr(r,"Second parameter"),o=function(e,r){M(e,r);var t=e,o=null==t?void 0:t.autoAllocateChunkSize,n=null==t?void 0:t.cancel,i=null==t?void 0:t.pull,a=null==t?void 0:t.start,u=null==t?void 0:t.type;return{autoAllocateChunkSize:void 0===o?void 0:U(o,"".concat(r," has member 'autoAllocateChunkSize' that")),cancel:void 0===n?void 0:Qt(n,t,"".concat(r," has member 'cancel' that")),pull:void 0===i?void 0:Nt(i,t,"".concat(r," has member 'pull' that")),start:void 0===a?void 0:Ht(a,t,"".concat(r," has member 'start' that")),type:void 0===u?void 0:Vt(u,"".concat(r," has member 'type' that"))}}(e,"First parameter");if(Kt(this),"bytes"===o.type){if(void 0!==t.size)throw new RangeError("The strategy for a byte stream cannot have a size function");!function(e,r,t){var o,n,i,a=Object.create(ze.prototype);o=void 0!==r.start?function(){return r.start(a)}:function(){},n=void 0!==r.pull?function(){return r.pull(a)}:function(){return d(void 0)},i=void 0!==r.cancel?function(e){return r.cancel(e)}:function(){return d(void 0)};var u=r.autoAllocateChunkSize;if(0===u)throw new TypeError("autoAllocateChunkSize must be greater than 0");lr(e,a,o,n,i,t,u)}(this,o,Rr(t,0))}else{var n=Tr(t);!function(e,r,t,o){var n,i,a,u=Object.create(Wt.prototype);n=void 0!==r.start?function(){return r.start(u)}:function(){},i=void 0!==r.pull?function(){return r.pull(u)}:function(){return d(void 0)},a=void 0!==r.cancel?function(e){return r.cancel(e)}:function(){return d(void 0)},It(e,u,n,i,a,t,o)}(this,o,Rr(t,1),n)}}return Object.defineProperty(ReadableStream.prototype,"locked",{get:function(){if(!Zt(this))throw oo("locked");return $t(this)},enumerable:!1,configurable:!0}),ReadableStream.prototype.cancel=function(e){return void 0===e&&(e=void 0),Zt(this)?$t(this)?p(new TypeError("Cannot cancel a stream that already has a reader")):eo(this,e):p(oo("cancel"))},ReadableStream.prototype.getReader=function(e){if(void 0===e&&(e=void 0),!Zt(this))throw oo("getReader");return void 0===function(e,r){M(e,r);var t=null==e?void 0:e.mode;return{mode:void 0===t?void 0:fr(t,"".concat(r," has member 'mode' that"))}}(e,"First parameter").mode?X(this):dr(this)},ReadableStream.prototype.pipeThrough=function(e,r){if(void 0===r&&(r={}),!Zt(this))throw oo("pipeThrough");Q(e,1,"pipeThrough");var t=function(e,r){M(e,r);var t=null==e?void 0:e.readable;N(t,"readable","ReadableWritablePair"),G(t,"".concat(r," has member 'readable' that"));var o=null==e?void 0:e.writable;return N(o,"writable","ReadableWritablePair"),jr(o,"".concat(r," has member 'writable' that")),{readable:t,writable:o}}(e,"First parameter"),o=Ut(r,"Second parameter");if($t(this))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream");if(Dr(t.writable))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream");return y(Tt(this,t.writable,o.preventClose,o.preventAbort,o.preventCancel,o.signal)),t.readable},ReadableStream.prototype.pipeTo=function(e,r){if(void 0===r&&(r={}),!Zt(this))return p(oo("pipeTo"));if(void 0===e)return p("Parameter 1 is required in 'pipeTo'.");if(!zr(e))return p(new TypeError("ReadableStream.prototype.pipeTo's first argument must be a WritableStream"));var t;try{t=Ut(r,"Second parameter")}catch(e){return p(e)}return $t(this)?p(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream")):Dr(e)?p(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream")):Tt(this,e,t.preventClose,t.preventAbort,t.preventCancel,t.signal)},ReadableStream.prototype.tee=function(){if(!Zt(this))throw oo("tee");return fe(xt(this))},ReadableStream.prototype.values=function(e){if(void 0===e&&(e=void 0),!Zt(this))throw oo("values");var r,t,o,n,i,a=function(e,r){M(e,r);var t=null==e?void 0:e.preventCancel;return{preventCancel:Boolean(t)}}(e,"First parameter");return r=this,t=a.preventCancel,o=X(r),n=new we(o,t),(i=Object.create(Te))._asyncIteratorImpl=n,i},ReadableStream.prototype[ye]=function(e){return this.values(e)},ReadableStream.from=function(e){return Yt(e)},ReadableStream}();function Xt(e,r,t,o,n){void 0===o&&(o=1),void 0===n&&(n=function(){return 1});var i=Object.create(Gt.prototype);return Kt(i),It(i,Object.create(Wt.prototype),e,r,t,o,n),i}function Jt(e,r,t){var o=Object.create(Gt.prototype);return Kt(o),lr(o,Object.create(ze.prototype),e,r,t,0,void 0),o}function Kt(e){e._state="readable",e._reader=void 0,e._storedError=void 0,e._disturbed=!1}function Zt(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_readableStreamController")&&e instanceof Gt)}function $t(e){return void 0!==e._reader}function eo(e,r){if(e._disturbed=!0,"closed"===e._state)return d(void 0);if("errored"===e._state)return p(e._storedError);ro(e);var o=e._reader;if(void 0!==o&&yr(o)){var n=o._readIntoRequests;o._readIntoRequests=new R,n.forEach(function(e){e._closeSteps(void 0)})}return v(e._readableStreamController[C](r),t)}function ro(e){e._state="closed";var r=e._reader;if(void 0!==r&&(F(r),ae(r))){var t=r._readRequests;r._readRequests=new R,t.forEach(function(e){e._closeSteps()})}}function to(e,r){e._state="errored",e._storedError=r;var t=e._reader;void 0!==t&&(D(t,r),ae(t)?se(t,r):Sr(t,r))}function oo(e){return new TypeError("ReadableStream.prototype.".concat(e," can only be used on a ReadableStream"))}function no(e,r){M(e,r);var t=null==e?void 0:e.highWaterMark;return N(t,"highWaterMark","QueuingStrategyInit"),{highWaterMark:H(t)}}Object.defineProperties(Gt,{from:{enumerable:!0}}),Object.defineProperties(Gt.prototype,{cancel:{enumerable:!0},getReader:{enumerable:!0},pipeThrough:{enumerable:!0},pipeTo:{enumerable:!0},tee:{enumerable:!0},values:{enumerable:!0},locked:{enumerable:!0}}),i(Gt.from,"from"),i(Gt.prototype.cancel,"cancel"),i(Gt.prototype.getReader,"getReader"),i(Gt.prototype.pipeThrough,"pipeThrough"),i(Gt.prototype.pipeTo,"pipeTo"),i(Gt.prototype.tee,"tee"),i(Gt.prototype.values,"values"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Gt.prototype,r.toStringTag,{value:"ReadableStream",configurable:!0}),Object.defineProperty(Gt.prototype,ye,{value:Gt.prototype.values,writable:!0,configurable:!0});var io=function(e){return e.byteLength};i(io,"size");var ao=function(){function ByteLengthQueuingStrategy(e){Q(e,1,"ByteLengthQueuingStrategy"),e=no(e,"First parameter"),this._byteLengthQueuingStrategyHighWaterMark=e.highWaterMark}return Object.defineProperty(ByteLengthQueuingStrategy.prototype,"highWaterMark",{get:function(){if(!lo(this))throw uo("highWaterMark");return this._byteLengthQueuingStrategyHighWaterMark},enumerable:!1,configurable:!0}),Object.defineProperty(ByteLengthQueuingStrategy.prototype,"size",{get:function(){if(!lo(this))throw uo("size");return io},enumerable:!1,configurable:!0}),ByteLengthQueuingStrategy}();function uo(e){return new TypeError("ByteLengthQueuingStrategy.prototype.".concat(e," can only be used on a ByteLengthQueuingStrategy"))}function lo(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_byteLengthQueuingStrategyHighWaterMark")&&e instanceof ao)}Object.defineProperties(ao.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof r.toStringTag&&Object.defineProperty(ao.prototype,r.toStringTag,{value:"ByteLengthQueuingStrategy",configurable:!0});var so=function(){return 1};i(so,"size");var co=function(){function CountQueuingStrategy(e){Q(e,1,"CountQueuingStrategy"),e=no(e,"First parameter"),this._countQueuingStrategyHighWaterMark=e.highWaterMark}return Object.defineProperty(CountQueuingStrategy.prototype,"highWaterMark",{get:function(){if(!po(this))throw fo("highWaterMark");return this._countQueuingStrategyHighWaterMark},enumerable:!1,configurable:!0}),Object.defineProperty(CountQueuingStrategy.prototype,"size",{get:function(){if(!po(this))throw fo("size");return so},enumerable:!1,configurable:!0}),CountQueuingStrategy}();function fo(e){return new TypeError("CountQueuingStrategy.prototype.".concat(e," can only be used on a CountQueuingStrategy"))}function po(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_countQueuingStrategyHighWaterMark")&&e instanceof co)}function bo(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}function ho(e,r,t){return x(e,t),function(t){return S(e,r,[t])}}function _o(e,r,t){return x(e,t),function(t,o){return w(e,r,[t,o])}}function mo(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}Object.defineProperties(co.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof r.toStringTag&&Object.defineProperty(co.prototype,r.toStringTag,{value:"CountQueuingStrategy",configurable:!0});var vo=function(){function TransformStream(e,r,t){void 0===e&&(e={}),void 0===r&&(r={}),void 0===t&&(t={}),void 0===e&&(e=null);var o=Pr(r,"Second parameter"),n=Pr(t,"Third parameter"),i=function(e,r){M(e,r);var t=null==e?void 0:e.cancel,o=null==e?void 0:e.flush,n=null==e?void 0:e.readableType,i=null==e?void 0:e.start,a=null==e?void 0:e.transform,u=null==e?void 0:e.writableType;return{cancel:void 0===t?void 0:mo(t,e,"".concat(r," has member 'cancel' that")),flush:void 0===o?void 0:bo(o,e,"".concat(r," has member 'flush' that")),readableType:n,start:void 0===i?void 0:ho(i,e,"".concat(r," has member 'start' that")),transform:void 0===a?void 0:_o(a,e,"".concat(r," has member 'transform' that")),writableType:u}}(e,"First parameter");if(void 0!==i.readableType)throw new RangeError("Invalid readableType specified");if(void 0!==i.writableType)throw new RangeError("Invalid writableType specified");var a,u=Rr(n,0),l=Tr(n),s=Rr(o,1),c=Tr(o);!function(e,r,t,o,n,i){function a(){return r}function u(r){return function(e,r){var t=e._transformStreamController;if(e._backpressure){return v(e._backpressureChangePromise,function(){var o=e._writable;if("erroring"===o._state)throw o._storedError;return Eo(t,r)})}return Eo(t,r)}(e,r)}function l(r){return function(e,r){var t=e._transformStreamController;if(void 0!==t._finishPromise)return t._finishPromise;var o=e._readable;t._finishPromise=f(function(e,r){t._finishPromise_resolve=e,t._finishPromise_reject=r});var n=t._cancelAlgorithm(r);return Co(t),h(n,function(){return"errored"===o._state?jo(t,o._storedError):(Dt(o._readableStreamController,r),Oo(t)),null},function(e){return Dt(o._readableStreamController,e),jo(t,e),null}),t._finishPromise}(e,r)}function s(){return function(e){var r=e._transformStreamController;if(void 0!==r._finishPromise)return r._finishPromise;var t=e._readable;r._finishPromise=f(function(e,t){r._finishPromise_resolve=e,r._finishPromise_reject=t});var o=r._flushAlgorithm();return Co(r),h(o,function(){return"errored"===t._state?jo(r,t._storedError):(At(t._readableStreamController),Oo(r)),null},function(e){return Dt(t._readableStreamController,e),jo(r,e),null}),r._finishPromise}(e)}function c(){return function(e){return Ro(e,!1),e._backpressureChangePromise}(e)}function d(r){return function(e,r){var t=e._transformStreamController;if(void 0!==t._finishPromise)return t._finishPromise;var o=e._writable;t._finishPromise=f(function(e,r){t._finishPromise_resolve=e,t._finishPromise_reject=r});var n=t._cancelAlgorithm(r);return Co(t),h(n,function(){return"errored"===o._state?jo(t,o._storedError):(it(o._writableStreamController,r),wo(e),Oo(t)),null},function(r){return it(o._writableStreamController,r),wo(e),jo(t,r),null}),t._finishPromise}(e,r)}e._writable=function(e,r,t,o,n,i){void 0===n&&(n=1),void 0===i&&(i=function(){return 1});var a=Object.create(Br.prototype);return Ar(a),rt(a,Object.create($r.prototype),e,r,t,o,n,i),a}(a,u,s,l,t,o),e._readable=Xt(a,c,d,n,i),e._backpressure=void 0,e._backpressureChangePromise=void 0,e._backpressureChangePromise_resolve=void 0,Ro(e,!0),e._transformStreamController=void 0}(this,f(function(e){a=e}),s,c,u,l),function(e,r){var t,o,n,i=Object.create(To.prototype);t=void 0!==r.transform?function(e){return r.transform(e,i)}:function(e){try{return qo(i,e),d(void 0)}catch(e){return p(e)}};o=void 0!==r.flush?function(){return r.flush(i)}:function(){return d(void 0)};n=void 0!==r.cancel?function(e){return r.cancel(e)}:function(){return d(void 0)};!function(e,r,t,o,n){r._controlledTransformStream=e,e._transformStreamController=r,r._transformAlgorithm=t,r._flushAlgorithm=o,r._cancelAlgorithm=n,r._finishPromise=void 0,r._finishPromise_resolve=void 0,r._finishPromise_reject=void 0}(e,i,t,o,n)}(this,i),void 0!==i.start?a(i.start(this._transformStreamController)):a(void 0)}return Object.defineProperty(TransformStream.prototype,"readable",{get:function(){if(!yo(this))throw Bo("readable");return this._readable},enumerable:!1,configurable:!0}),Object.defineProperty(TransformStream.prototype,"writable",{get:function(){if(!yo(this))throw Bo("writable");return this._writable},enumerable:!1,configurable:!0}),TransformStream}();function yo(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_transformStreamController")&&e instanceof vo)}function go(e,r){Dt(e._readable._readableStreamController,r),So(e,r)}function So(e,r){Co(e._transformStreamController),it(e._writable._writableStreamController,r),wo(e)}function wo(e){e._backpressure&&Ro(e,!1)}function Ro(e,r){void 0!==e._backpressureChangePromise&&e._backpressureChangePromise_resolve(),e._backpressureChangePromise=f(function(r){e._backpressureChangePromise_resolve=r}),e._backpressure=r}Object.defineProperties(vo.prototype,{readable:{enumerable:!0},writable:{enumerable:!0}}),"symbol"==typeof r.toStringTag&&Object.defineProperty(vo.prototype,r.toStringTag,{value:"TransformStream",configurable:!0});var To=function(){function TransformStreamDefaultController(){throw new TypeError("Illegal constructor")}return Object.defineProperty(TransformStreamDefaultController.prototype,"desiredSize",{get:function(){if(!Po(this))throw Wo("desiredSize");return Ft(this._controlledTransformStream._readable._readableStreamController)},enumerable:!1,configurable:!0}),TransformStreamDefaultController.prototype.enqueue=function(e){if(void 0===e&&(e=void 0),!Po(this))throw Wo("enqueue");qo(this,e)},TransformStreamDefaultController.prototype.error=function(e){if(void 0===e&&(e=void 0),!Po(this))throw Wo("error");var r;r=e,go(this._controlledTransformStream,r)},TransformStreamDefaultController.prototype.terminate=function(){if(!Po(this))throw Wo("terminate");!function(e){var r=e._controlledTransformStream;At(r._readable._readableStreamController);var t=new TypeError("TransformStream terminated");So(r,t)}(this)},TransformStreamDefaultController}();function Po(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledTransformStream")&&e instanceof To)}function Co(e){e._transformAlgorithm=void 0,e._flushAlgorithm=void 0,e._cancelAlgorithm=void 0}function qo(e,r){var t=e._controlledTransformStream,o=t._readable._readableStreamController;if(!Lt(o))throw new TypeError("Readable side is not in a state that permits enqueue");try{zt(o,r)}catch(e){throw So(t,e),t._readable._storedError}var n=function(e){return!Bt(e)}(o);n!==t._backpressure&&Ro(t,!0)}function Eo(e,r){return v(e._transformAlgorithm(r),void 0,function(r){throw go(e._controlledTransformStream,r),r})}function Wo(e){return new TypeError("TransformStreamDefaultController.prototype.".concat(e," can only be used on a TransformStreamDefaultController"))}function Oo(e){void 0!==e._finishPromise_resolve&&(e._finishPromise_resolve(),e._finishPromise_resolve=void 0,e._finishPromise_reject=void 0)}function jo(e,r){void 0!==e._finishPromise_reject&&(y(e._finishPromise),e._finishPromise_reject(r),e._finishPromise_resolve=void 0,e._finishPromise_reject=void 0)}function Bo(e){return new TypeError("TransformStream.prototype.".concat(e," can only be used on a TransformStream"))}Object.defineProperties(To.prototype,{enqueue:{enumerable:!0},error:{enumerable:!0},terminate:{enumerable:!0},desiredSize:{enumerable:!0}}),i(To.prototype.enqueue,"enqueue"),i(To.prototype.error,"error"),i(To.prototype.terminate,"terminate"),"symbol"==typeof r.toStringTag&&Object.defineProperty(To.prototype,r.toStringTag,{value:"TransformStreamDefaultController",configurable:!0}),e.ByteLengthQueuingStrategy=ao,e.CountQueuingStrategy=co,e.ReadableByteStreamController=ze,e.ReadableStream=Gt,e.ReadableStreamBYOBReader=_r,e.ReadableStreamBYOBRequest=Ae,e.ReadableStreamDefaultController=Wt,e.ReadableStreamDefaultReader=ee,e.TransformStream=vo,e.TransformStreamDefaultController=To,e.WritableStream=Br,e.WritableStreamDefaultController=$r,e.WritableStreamDefaultWriter=Hr}); diff --git a/Tests/UnitTests/CMakeLists.txt b/Tests/UnitTests/CMakeLists.txt index 2dbc7619..686b79a5 100644 --- a/Tests/UnitTests/CMakeLists.txt +++ b/Tests/UnitTests/CMakeLists.txt @@ -70,6 +70,7 @@ target_link_libraries(UnitTests PRIVATE Performance PRIVATE TextDecoder PRIVATE TextEncoder + PRIVATE Streams ${ADDITIONAL_LIBRARIES}) # See https://gitlab.kitware.com/cmake/cmake/-/issues/23543 diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index b286edf8..c8ef3b64 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -293,11 +293,321 @@ describe("XMLHTTPRequest", function () { }); }); +describe("Headers", function () { + // Focused ports from WPT fetch/api/headers. + it("normalizes names and values and combines repeated fields", function () { + const headers = new Headers([ + ["X-Test", " first\t"], + ["x-test", "second"], + ["X-Other", "\r\n value \n"] + ]); + expect(headers.get("X-TEST")).to.equal("first, second"); + expect(headers.get("x-other")).to.equal("value"); + expect(Array.from(headers.keys())).to.deep.equal(["x-other", "x-test"]); + }); + + it("validates sequence shape and HTTP ByteStrings", function () { + expect(() => new Headers(null as any)).to.throw(); + expect(() => new Headers([["missing-value"]] as any)).to.throw(); + expect(() => new Headers([["too", "many", "values"]] as any)).to.throw(); + expect(() => new Headers([["invalid name", "value"]])).to.throw(); + expect(() => new Headers([["valid", "a\0b"]])).to.throw(); + expect(() => new Headers([["invalidĀ", "value"]])).to.throw(); + }); + + it("preserves Set-Cookie fields while combining other duplicates", function () { + const headers = new Headers([ + ["set-cookie", "a=1"], + ["x-value", "first"], + ["Set-Cookie", "b=2"], + ["X-Value", "second"] + ]); + expect(headers.getSetCookie()).to.deep.equal(["a=1", "b=2"]); + expect(Array.from(headers)).to.deep.equal([ + ["set-cookie", "a=1"], + ["set-cookie", "b=2"], + ["x-value", "first, second"] + ]); + }); + + it("keeps iteration live when headers are changed", function () { + const headers = new Headers({ bar: "0", baz: "1", foo: "2" }); + const seen: string[] = []; + for (const [name] of headers) { + seen.push(name); + headers.delete("foo"); + } + expect(seen).to.deep.equal(["bar", "baz"]); + }); + + it("copies an initializer and honors a custom iterator", function () { + const source = new Headers({ ignored: "value" }); + source[Symbol.iterator] = function* () { + yield ["custom", "value"]; + }; + const copy = new Headers(source); + source.set("custom", "changed"); + expect(Array.from(copy)).to.deep.equal([["custom", "value"]]); + }); +}); + +describe("Response", function () { + // Focused ports from WPT fetch/api/response. + it("exposes browser defaults and validates response metadata", function () { + const response = new Response(); + expect(String(response)).to.equal("[object Response]"); + expect(response.status).to.equal(200); + expect(response.statusText).to.equal(""); + expect(response.ok).to.equal(true); + expect(response.type).to.equal("default"); + expect(response.url).to.equal(""); + expect(response.body).to.equal(null); + expect(response.headers).to.equal(response.headers); + + expect(() => new Response("", { status: 199 })).to.throw(RangeError); + expect(() => new Response("", { status: 600 })).to.throw(RangeError); + expect(() => new Response("", { statusText: "bad\ntext" })).to.throw(TypeError); + expect(() => new Response("body", { status: 204 })).to.throw(TypeError); + }); + + it("streams strings and consumes a body only once", async function () { + const response = new Response("streamed text"); + expect(response.body).to.be.instanceOf(ReadableStream); + expect(response.bodyUsed).to.equal(false); + expect(await response.text()).to.equal("streamed text"); + expect(response.bodyUsed).to.equal(true); + + let rejected = false; + try { + await response.arrayBuffer(); + } catch (error) { + rejected = error instanceof TypeError; + } + expect(rejected).to.equal(true); + }); + + // Ported from WPT fetch/api/response/response-consume-empty.any.js. + it("consumes a null body as empty without disturbing it", async function () { + const textResponse = new Response(); + expect(await textResponse.text()).to.equal(""); + expect(textResponse.bodyUsed).to.equal(false); + + const bufferResponse = new Response(); + expect((await bufferResponse.arrayBuffer()).byteLength).to.equal(0); + expect(bufferResponse.bodyUsed).to.equal(false); + + const bytesResponse = new Response(); + expect((await bytesResponse.bytes()).byteLength).to.equal(0); + expect(bytesResponse.bodyUsed).to.equal(false); + + const blobResponse = new Response(); + expect((await blobResponse.blob()).size).to.equal(0); + expect(blobResponse.bodyUsed).to.equal(false); + + const jsonResponse = new Response(); + let jsonRejected = false; + try { + await jsonResponse.json(); + } catch { + jsonRejected = true; + } + expect(jsonRejected).to.equal(true); + expect(jsonResponse.bodyUsed).to.equal(false); + }); + + // Byte streams reject zero-length enqueues. An empty BufferSource still + // represents a non-null body, but its stream must close without a chunk. + it("consumes an empty BufferSource without enqueueing an empty byte chunk", async function () { + const response = new Response(new Uint8Array(0)); + expect(response.body).to.be.instanceOf(ReadableStream); + expect(response.bodyUsed).to.equal(false); + expect(new Uint8Array(await response.arrayBuffer())).to.eql(new Uint8Array(0)); + expect(response.bodyUsed).to.equal(true); + }); + + it("snapshots mutable BufferSource input once", async function () { + const input = new Uint8Array([80, 65, 83, 83]); + const response = new Response(input); + input.fill(0); + expect(await response.text()).to.equal("PASS"); + }); + + // Adapted from WebKit LayoutTests/fetch/body-init.html. + it("stringifies integer and object bodies", async function () { + expect(await new Response(1 as any).text()).to.equal("1"); + expect(await new Response({} as any).text()).to.equal("[object Object]"); + }); + + it("sets inferred content types without replacing explicit headers", async function () { + const text = new Response("text"); + expect(text.headers.get("content-type")).to.equal("text/plain;charset=UTF-8"); + + const blob = new Response(new Blob(["blob"], { type: "application/example" })); + expect(blob.headers.get("content-type")).to.equal("application/example"); + + const explicit = new Response("text", { headers: { "content-type": "text/custom" } }); + expect(explicit.headers.get("content-type")).to.equal("text/custom"); + }); + + it("clones stream branches for independent consumption", async function () { + const response = new Response("clone body", { + headers: { "x-test": "value" }, + status: 201, + statusText: "Created" + }); + const clone = response.clone(); + expect(clone.status).to.equal(201); + expect(clone.headers.get("x-test")).to.equal("value"); + expect(await response.text()).to.equal("clone body"); + expect(await clone.text()).to.equal("clone body"); + }); + + function withoutPrivateDisturbedState(stream: ReadableStream): ReadableStream { + Object.defineProperty(stream, "_disturbed", { + configurable: true, + get() { return undefined; }, + set() {} + }); + return stream; + } + + it("tracks direct stream reads and rejects invalid body chunks", async function () { + const directStream = withoutPrivateDisturbedState(new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("body")); + controller.close(); + } + })); + const direct = new Response(directStream); + const reader = direct.body!.getReader(); + expect(direct.bodyUsed).to.equal(false); + await reader.read(); + expect(direct.bodyUsed).to.equal(true); + + const invalid = new Response(new ReadableStream({ + start(controller) { + controller.enqueue("not bytes"); + controller.close(); + } + }) as any); + let rejected = false; + try { + await invalid.bytes(); + } catch (error) { + rejected = error instanceof TypeError; + } + expect(rejected).to.equal(true); + }); + + it("rejects a host-shaped stream disturbed before Response construction", async function () { + const stream = withoutPrivateDisturbedState(new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([1])); + controller.close(); + } + })); + const reader = stream.getReader(); + await reader.read(); + reader.releaseLock(); + + expect(() => new Response(stream)).to.throw(TypeError); + }); + + // Focused ports from WPT response-stream-disturbed-6.any.js and + // response-stream-disturbed-by-pipe.any.js. These must not depend on a + // private field supplied by one particular Streams implementation. + it("tracks cancellation and piping through standard stream methods", async function () { + const cancelled = new Response(withoutPrivateDisturbedState(new ReadableStream())); + const cancelledReader = cancelled.body!.getReader(); + expect(cancelled.bodyUsed).to.equal(false); + await cancelledReader.cancel(); + expect(cancelled.bodyUsed).to.equal(true); + + const piped = new Response(withoutPrivateDisturbedState(new ReadableStream({ + start(controller) { + controller.close(); + } + }))); + const pipePromise = piped.body!.pipeTo(new WritableStream({}, { highWaterMark: 0 })); + expect(piped.bodyUsed).to.equal(true); + await pipePromise; + + const pipedThrough = new Response(withoutPrivateDisturbedState(new ReadableStream({ + start(controller) { + controller.close(); + } + }))); + const output = pipedThrough.body!.pipeThrough(new TransformStream()); + expect(pipedThrough.bodyUsed).to.equal(true); + await output.cancel(); + }); + + // Adapted from Firefox dom/fetch/tests/crashtests/1939295.html. The + // unresolved read must remain safe through runtime teardown. + it("does not crash while consuming an open empty stream", function () { + const pending = new Response(new ReadableStream()).text(); + expect(pending).to.be.instanceOf(Promise); + }); + + // Adapted from WebKit's imported many-empty-chunks-crash.html. + it("consumes many empty chunks without retaining growing byte buffers", async function () { + const response = new Response(new ReadableStream({ + start(controller) { + for (let index = 0; index < 40000; ++index) { + controller.enqueue(new Uint8Array()); + } + controller.close(); + } + })); + expect((await response.arrayBuffer()).byteLength).to.equal(0); + }); + + // Bounded adaptation of Chromium's call-extra-crash-is-disturbed.html. + // QuickJS terminates before a JS-defined getter can run after a real + // native stack overflow, so retain the deep-call regression portably. + it("reads bodyUsed from a deep call stack without recursion", function () { + const response = new Response(new ReadableStream()); + function readAtDepth(depth: number): boolean { + return depth === 0 ? response.bodyUsed : readAtDepth(depth - 1); + } + expect(readAtDepth(128)).to.equal(false); + }); + + it("provides error, redirect, and JSON factories", async function () { + const error = Response.error(); + expect(error.type).to.equal("error"); + expect(error.status).to.equal(0); + + const redirect = Response.redirect("https://example.com/path", 307); + expect(redirect.status).to.equal(307); + expect(redirect.headers.get("location")).to.equal("https://example.com/path"); + + const json = Response.json({ value: 42 }); + expect(json.headers.get("content-type")).to.equal("application/json"); + expect(await json.json()).to.deep.equal({ value: 42 }); + }); + + it("filters forbidden response Set-Cookie fields", function () { + const response = new Response(null, { + headers: { + "set-cookie": "secret=value", + "set-cookie2": "legacy=value" + } + }); + response.headers.append("Set-Cookie", "other=value"); + expect(response.headers.getSetCookie()).to.deep.equal([]); + expect(response.headers.has("set-cookie2")).to.equal(false); + }); +}); + describe("fetch", function () { this.timeout(30000); it("should resolve with ok=true and status=200 for a resource that exists", async function () { const response = await fetch("https://github.com/"); + expect(response).to.be.instanceOf(Response); + expect(response.headers).to.be.instanceOf(Headers); + expect(response.body).to.be.instanceOf(ReadableStream); expect(response.ok).to.equal(true); expect(response.status).to.equal(200); }); @@ -320,6 +630,71 @@ describe("fetch", function () { expect(await response.text()).to.equal("var symlink_target_js = true;"); }); + it("should resolve percent-encoded data URLs locally", async function () { + const url = "data:text/plain;charset=utf-8,hello%20native%20fetch%21"; + const response = await fetch(url); + expect(response.ok).to.equal(true); + expect(response.status).to.equal(200); + expect(response.url).to.equal(url); + expect(response.headers.get("content-type")).to.equal("text/plain;charset=utf-8"); + expect(await response.text()).to.equal("hello native fetch!"); + }); + + it("should decode base64 data URLs without using the network transport", async function () { + const response = await fetch("data:application/octet-stream;base64,AAEC/w=="); + const clone = response.clone(); + expect(new Uint8Array(await response.arrayBuffer())).to.eql(new Uint8Array([0, 1, 2, 255])); + const blob = await clone.blob(); + expect(blob.type).to.equal("application/octet-stream"); + expect(new Uint8Array(await blob.arrayBuffer())).to.eql(new Uint8Array([0, 1, 2, 255])); + }); + + // Adapted from WPT fetch/data-urls/processing.any.js and resources/data-urls.json. + const dataUrlCases: Array<[string, string, number[]]> = [ + ["data:,", "text/plain;charset=US-ASCII", []], + ["data:,%FF", "text/plain;charset=US-ASCII", [255]], + ["data:text/plain,X", "text/plain", [88]], + ["data:,X#fragment", "text/plain;charset=US-ASCII", [88]], + ["data:;BASe64,WA", "text/plain;charset=US-ASCII", [88]], + ["data: ;charset=x ; base64,W%20A", "text/plain;charset=x", [88]] + ]; + + for (const [url, expectedType, expectedBody] of dataUrlCases) { + it(`should process WPT data URL case ${JSON.stringify(url)}`, async function () { + const response = await fetch(url); + expect(response.headers.get("content-type")).to.equal(expectedType); + expect(Array.from(new Uint8Array(await response.arrayBuffer()))).to.eql(expectedBody); + }); + } + + // Adapted from WPT's forgiving-base64 vectors and Chromium's DataURL tests. + const base64Cases: Array<[string, number[]]> = [ + ["abcd", [105, 183, 29]], + ["ab%09%0A%0C%0D%20cd", [105, 183, 29]], + ["ab==", [105]], + ["/A", [252]], + ["YR", [97]] + ]; + + for (const [encoded, expectedBody] of base64Cases) { + it(`should forgiving-base64 decode ${JSON.stringify(encoded)}`, async function () { + const response = await fetch(`data:application/octet-stream;base64,${encoded}`); + expect(Array.from(new Uint8Array(await response.arrayBuffer()))).to.eql(expectedBody); + }); + } + + for (const encoded of ["a", "ab===", "ab%0Bcd", "=a", "a=b"]) { + it(`should reject invalid WPT base64 case ${JSON.stringify(encoded)}`, async function () { + let error: unknown; + try { + await fetch(`data:application/octet-stream;base64,${encoded}`); + } catch (caught) { + error = caught; + } + expect(error).to.be.instanceOf(TypeError); + }); + } + it("arrayBuffer() should return the body as bytes", async function () { const response = await fetch("app:///Scripts/symlink_target.js"); const expected = new Uint8Array("var symlink_target_js = true;".split("").map(x => x.charCodeAt(0))); @@ -1371,9 +1746,181 @@ describe("Console", function () { }); }); +describe("Web Streams", function () { + // Focused ports from the WHATWG Streams WPT suites at c05b4473: + // readable-streams/general.any.js and tee.any.js, + // writable-streams/write.any.js, and transform-streams/general.any.js. + it("installs the standard stream constructors", function () { + expect(ReadableStream).to.be.a("function"); + expect(WritableStream).to.be.a("function"); + expect(TransformStream).to.be.a("function"); + expect(ByteLengthQueuingStrategy).to.be.a("function"); + expect(CountQueuingStrategy).to.be.a("function"); + }); + + it("delivers queued chunks in order and closes the reader", async function () { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue("a"); + controller.enqueue("b"); + controller.close(); + } + }); + const reader = stream.getReader(); + + expect(await reader.read()).to.deep.equal({ value: "a", done: false }); + expect(await reader.read()).to.deep.equal({ value: "b", done: false }); + expect(await reader.read()).to.deep.equal({ value: undefined, done: true }); + await reader.closed; + }); + + it("propagates a rejected pull to read and closed", async function () { + const failure = new Error("pull failed"); + const reader = new ReadableStream({ + pull() { + return Promise.reject(failure); + } + }).getReader(); + + let readFailure: unknown; + let closedFailure: unknown; + try { await reader.read(); } catch (error) { readFailure = error; } + try { await reader.closed; } catch (error) { closedFailure = error; } + expect(readFailure).to.equal(failure); + expect(closedFailure).to.equal(failure); + }); + + it("tees without one branch consuming the other", async function () { + const [first, second] = new ReadableStream({ + start(controller) { + controller.enqueue("a"); + controller.enqueue("b"); + controller.close(); + } + }).tee(); + const firstReader = first.getReader(); + const secondReader = second.getReader(); + + expect(await firstReader.read()).to.deep.equal({ value: "a", done: false }); + expect(await firstReader.read()).to.deep.equal({ value: "b", done: false }); + expect(await firstReader.read()).to.deep.equal({ value: undefined, done: true }); + expect(await secondReader.read()).to.deep.equal({ value: "a", done: false }); + }); + + it("waits for asynchronous writes before closing", async function () { + const stored: number[] = []; + const writable = new WritableStream({ + write(chunk) { + return Promise.resolve().then(() => stored.push(chunk)); + } + }); + const writer = writable.getWriter(); + + writer.write(1); + writer.write(2); + await writer.close(); + expect(stored).to.deep.equal([1, 2]); + }); + + it("applies transform output and backpressure", async function () { + const transform = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk.toUpperCase()); + } + }); + const writer = transform.writable.getWriter(); + const reader = transform.readable.getReader(); + const write = writer.write("native"); + + expect(await reader.read()).to.deep.equal({ value: "NATIVE", done: false }); + await write; + await writer.close(); + expect(await reader.read()).to.deep.equal({ value: undefined, done: true }); + }); + + it("supports BYOB reads from byte streams", async function () { + let sent = false; + const stream = new ReadableStream({ + type: "bytes", + pull(controller) { + if (!sent) { + sent = true; + controller.enqueue(new Uint8Array([8, 241, 48, 123, 151])); + controller.close(); + } + } + } as any); + const reader = stream.getReader({ mode: "byob" }); + const result = await reader.read(new Uint8Array(8)); + + expect(result.done).to.equal(false); + expect(Array.from(result.value!)).to.deep.equal([8, 241, 48, 123, 151]); + expect((await reader.read(new Uint8Array(8))).done).to.equal(true); + }); + + // Ported from Firefox's dom/streams/test/xpcshell/subclassing.js. + it("supports subclassed streams, readers, and queuing strategies", async function () { + class SubclassedStream extends ReadableStream {} + class SubclassedStrategy extends CountQueuingStrategy {} + const stream = new SubclassedStream({ + start(controller) { + controller.enqueue("first"); + controller.close(); + } + }); + const Reader = stream.getReader().constructor as typeof ReadableStreamDefaultReader; + class SubclassedReader extends Reader {} + + expect(stream).to.be.instanceOf(ReadableStream); + expect(new SubclassedStrategy({ highWaterMark: 4 }).highWaterMark).to.equal(4); + + const secondStream = new ReadableStream({ + start(controller) { + controller.enqueue("second"); + controller.close(); + } + }); + const reader = new SubclassedReader(secondStream); + expect(await reader.read()).to.deep.equal({ value: "second", done: false }); + }); + + // Ported from Chromium's http/tests/streams/chromium/transform-stream-enqueue.html. + it("rejects enqueues after a transform is terminated or errored", function () { + expect(() => new TransformStream({ + start(controller) { + controller.terminate(); + controller.enqueue("late"); + } + })).to.throw(TypeError); + + expect(() => new TransformStream({ + start(controller) { + controller.error(new Error("failed")); + controller.enqueue("late"); + } + })).to.throw(TypeError); + }); +}); + describe("Blob", function () { + this.timeout(10000); + let emptyBlobs: Blob[], helloBlobs: Blob[], stringBlob: Blob, typedArrayBlob: Blob, arrayBufferBlob: Blob, blobBlob: Blob; + async function readStream(stream: ReadableStream, mode?: "byob"): Promise { + const reader: any = stream.getReader(mode === undefined ? undefined : { mode }); + const bytes: number[] = []; + while (true) { + const result = mode === "byob" + ? await reader.read(new Uint8Array(64)) + : await reader.read(); + if (result.done) { + return bytes; + } + bytes.push(...Array.from(result.value as Uint8Array)); + } + } + before(function () { emptyBlobs = [new Blob([]), new Blob([])]; stringBlob = new Blob(["Hello"]); @@ -1416,6 +1963,120 @@ describe("Blob", function () { expect(modelGltfJson.type).to.equal("model/gltf+json"); }); + // Focused ports from WPT FileAPI/blob/Blob-constructor.any.js. + it("accepts iterable parts and preserves their order", async function () { + const parts = { + *[Symbol.iterator]() { + yield "foo"; + yield new Uint8Array([98, 97, 114]); + yield new Blob(["baz"]); + } + }; + const blob = new Blob(parts as any); + expect(blob.size).to.equal(9); + expect(await blob.text()).to.equal("foobarbaz"); + }); + + it("uses an Array's overridden iterator", async function () { + const parts = ["ignored"]; + parts[Symbol.iterator] = function* () { + yield "custom"; + }; + + expect(await new Blob(parts).text()).to.equal("custom"); + }); + + it("closes an iterator when BlobPart conversion throws", function () { + let closed = false; + const badPart = { + toString() { + throw new Error("part conversion failed"); + } + }; + const parts = (function* () { + try { + yield badPart; + } finally { + closed = true; + } + })(); + + // QuickJS currently wraps an exception rethrown through a native + // constructor as its generic JS error type, so assert the observable + // iterator-close behavior separately from the adapter's error text. + expect(() => new Blob(parts as any)).to.throw(); + expect(closed).to.equal(true); + }); + + it("observes BlobPart array mutations during iteration", async function () { + const parts: any[] = [ + { + toString() { + parts.pop(); + return "PASS"; + } + }, + { + toString() { + throw new Error("removed part was converted"); + } + } + ]; + + expect(await new Blob(parts).text()).to.equal("PASS"); + }); + + it("converts parts before reading options in WebIDL order", function () { + const accesses: string[] = []; + const part = { + toString() { + accesses.push("part"); + return "data"; + } + }; + new Blob([part], { + get type() { + accesses.push("type"); + return "TEXT/PLAIN"; + }, + get endings() { + accesses.push("endings"); + return "transparent" as EndingType; + } + }); + + expect(accesses).to.deep.equal(["part", "endings", "type"]); + }); + + it("validates the endings enum and options dictionary", function () { + expect(() => new Blob([], { endings: "NATIVE" as EndingType })).to.throw(); + expect(() => new Blob([], { endings: "invalid" as EndingType })).to.throw(); + for (const value of [123, true, "abc"]) { + expect(() => new Blob([], value as any)).to.throw(); + } + expect(() => new Blob([], null as any)).not.to.throw(); + expect(() => new Blob([], undefined)).not.to.throw(); + }); + + it("exposes browser-compatible class tags", function () { + expect(String(new Blob())).to.equal("[object Blob]"); + expect(String(new File([], "empty.txt"))).to.equal("[object File]"); + }); + + it("rejects primitive parts containers", function () { + for (const value of [null, true, 7, "not a sequence"]) { + // Some Node-API adapters currently wrap a Napi::TypeError thrown + // by a constructor as their generic JS error type. + expect(() => new Blob(value as any)).to.throw(); + } + }); + + it("normalizes valid MIME types and clears invalid types", function () { + expect(new Blob([], { type: "TEXT/PLAIN" }).type).to.equal("text/plain"); + expect(new Blob([], { type: "te\x09xt/plain" }).type).to.equal(""); + expect(new Blob([], { type: "text/\x7fplain" }).type).to.equal(""); + }); + // -------------------------------- Blob.text() -------------------------------- it("returns empty string for empty blobs", async function () { for (const blob of emptyBlobs) { @@ -1437,6 +2098,11 @@ describe("Blob", function () { expect(text).to.equal("你好, 世界"); }); + it("replaces invalid UTF-8 bytes", async function () { + const invalid = new Uint8Array([192, 193, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]); + expect(await new Blob([invalid]).text()).to.equal("\ufffd".repeat(invalid.length)); + }); + it("preserves line endings like default transparent mode", async function () { const lineEndingsBlob = new Blob(["Hello\nWorld"]); const text = await lineEndingsBlob.text(); @@ -1483,6 +2149,109 @@ describe("Blob", function () { } }); + + // Focused ports from WPT FileAPI/blob/Blob-slice.any.js. + it("slices across part boundaries and applies clamp rounding", async function () { + const blob = new Blob(["foo", new Blob(["bar"]), "baz"]); + expect(await blob.slice(2, 7).text()).to.equal("obarb"); + expect(await new Blob(["abcd"]).slice(1.5).text()).to.equal("cd"); + expect(await new Blob(["abcd"]).slice(2.5).text()).to.equal("cd"); + expect(await blob.slice(-3, undefined, "TEXT/PLAIN").text()).to.equal("baz"); + expect(blob.slice(-3, undefined, "TEXT/PLAIN").type).to.equal("text/plain"); + }); + + // Focused ports from WPT FileAPI/blob/Blob-stream.any.js. + it("streams binary data through default and BYOB readers", async function () { + const input = [8, 241, 48, 123, 151]; + const blob = new Blob([new Uint8Array(input)]); + expect(await readStream(blob.stream())).to.deep.equal(input); + expect(await readStream(blob.stream(), "byob")).to.deep.equal(input); + expect(await readStream(new Blob().stream())).to.deep.equal([]); + }); + + // Adapted from WPT streams/readable-byte-streams/general.any.js BYOB + // coverage. Small, offset views exercise the caller-owned output path. + it("fills bounded BYOB views across Blob segment boundaries", async function () { + const input = new Uint8Array(97); + for (let index = 0; index < input.length; ++index) { + input[index] = (index * 31) & 0xff; + } + + const blob = new Blob([input.subarray(0, 11), input.subarray(11, 53), input.subarray(53)]); + const reader = blob.stream().getReader({ mode: "byob" }); + const output: number[] = []; + const requestSizes = [1, 3, 7, 16]; + let requestIndex = 0; + while (true) { + const requestSize = requestSizes[requestIndex++ % requestSizes.length]; + const request = new Uint8Array(new ArrayBuffer(requestSize + 4), 2, requestSize); + const result = await reader.read(request); + if (result.done) { + break; + } + expect(result.value!.byteLength).to.be.at.most(requestSize); + output.push(...Array.from(result.value!)); + } + + expect(output).to.deep.equal(Array.from(input)); + }); + + it("keeps independent stream cursors after the Blob reference is dropped", async function () { + let blob: Blob | null = new Blob(["PASS"]); + const first = blob.stream(); + const second = blob.stream(); + blob = null; + expect(await readStream(first)).to.deep.equal([80, 65, 83, 83]); + expect(await readStream(second)).to.deep.equal([80, 65, 83, 83]); + }); + + // Adapted from WebKit's fast/files/blob-stream-chunks.html. + it("chunks a large Blob and supports cancellation without retaining work", async function () { + const blob = new Blob([new Uint8Array(5 * 1024 * 1024)]); + const reader = blob.stream().getReader(); + const first = await reader.read(); + expect(first.done).to.equal(false); + expect(first.value!.byteLength).to.be.at.most(64 * 1024); + await reader.cancel(); + await reader.closed; + }); + + // Adapted from WebKit's blob-stream crash regression and exercises + // teardown of the C++ pull-state closures under repeated construction. + it("constructs and cancels many empty streams without crashing", async function () { + for (let index = 0; index < 1000; ++index) { + await new Blob().stream().cancel(); + } + }); + + // Scaled port of Firefox's dom/streams/test/xpcshell/large-pipeto.js. + it("pipes nested shared Blob parts without corrupting chunk boundaries", async function () { + const pattern = new Uint8Array(256 * 1024); + for (let index = 0; index < pattern.length; ++index) { + pattern[index] = index % 256; + } + const pair = new Blob([pattern, pattern]); + const nested = new Blob([pair, pair, pair, pair, pair, pair]); + let position = 0; + + await nested.stream().pipeTo(new WritableStream({ + write(chunk: Uint8Array) { + for (const value of chunk) { + const expected = position % pattern.length % 256; + if (value !== expected) { + throw new Error(`Blob stream byte ${position}: expected ${expected}, received ${value}`); + } + ++position; + } + } + })); + expect(position).to.equal(pattern.length * 12); + }); + + it("uses a File's Blob bytes when composing parts", async function () { + const file = new File(["a", "b"], "letters.txt"); + expect(await new Blob(["<", file, ">"]).text()).to.equal(""); + }); }); describe("napi class prototype isolation (#172)", function () { @@ -1804,6 +2573,22 @@ describe("File", function () { expect(text).to.equal("你好, 世界"); }); + // Adapted from WebKit's fast/files/blob-stream-crash-2.html. + it("streams and slices multiple File parts through the Blob API", async function () { + const file = new File(["a", new Blob(), "b", new Blob(), "c", new Blob(), "d"], "letters.txt"); + const reader = file.stream().getReader(); + const bytes: number[] = []; + while (true) { + const result = await reader.read(); + if (result.done) { + break; + } + bytes.push(...Array.from(result.value as Uint8Array)); + } + expect(bytes).to.deep.equal([97, 98, 99, 100]); + expect(await file.slice(1, 3).text()).to.equal("bc"); + }); + // -------------------------------- Blob inheritance -------------------------------- it("is an instance of Blob (prototype chain wired up)", function () { // BJS core (fileTools, Offline/database, abstractEngine, diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index a920fa1f..23ca47b5 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -85,11 +86,12 @@ TEST(JavaScript, All) Babylon::Polyfills::URL::Initialize(env); Babylon::Polyfills::WebSocket::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); - Babylon::Polyfills::Fetch::Initialize(env); + Babylon::Polyfills::Streams::Initialize(env); Babylon::Polyfills::Blob::Initialize(env); Babylon::Polyfills::File::Initialize(env); Babylon::Polyfills::TextDecoder::Initialize(env); Babylon::Polyfills::TextEncoder::Initialize(env); + Babylon::Polyfills::Fetch::Initialize(env); auto setExitCodeCallback = Napi::Function::New( env, [&exitCodePromise](const Napi::CallbackInfo& info) { @@ -111,6 +113,138 @@ TEST(JavaScript, All) EXPECT_EQ(exitCode, 0); } +TEST(Streams, ReplacesPartialHostSuiteAndIsIdempotent) +{ + std::promise done; + std::atomic_bool completed{}; + const auto complete = [&done, &completed](std::string result) { + if (!completed.exchange(true)) + { + done.set_value(std::move(result)); + } + }; + + Babylon::AppRuntime::Options options{}; + options.UnhandledExceptionHandler = [&complete](const Napi::Error& error) { + complete(Napi::GetErrorString(error)); + }; + Babylon::AppRuntime runtime{options}; + + runtime.Dispatch([&complete](Napi::Env env) { + auto global = env.Global(); + const auto hostReadableStream = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostReadableStream"); + global.Set("ReadableStream", hostReadableStream); + global.Set("TransformStream", env.Null()); + + Babylon::Polyfills::Streams::Initialize(env); + const auto installedReadableStream = global.Get("ReadableStream"); + const auto installedTransformStream = global.Get("TransformStream"); + EXPECT_FALSE(installedReadableStream.StrictEquals(hostReadableStream)); + if (!installedReadableStream.IsFunction() || !installedTransformStream.IsFunction()) + { + complete("Streams::Initialize did not install a complete constructor suite."); + return; + } + + const auto transform = installedTransformStream.As().New({}); + EXPECT_TRUE(transform.Get("readable").As().InstanceOf(installedReadableStream.As())); + + Babylon::Polyfills::Streams::Initialize(env); + EXPECT_TRUE(global.Get("ReadableStream").StrictEquals(installedReadableStream)); + EXPECT_TRUE(global.Get("TransformStream").StrictEquals(installedTransformStream)); + complete({}); + }); + + const auto error = done.get_future().get(); + EXPECT_TRUE(error.empty()) << error; +} + +TEST(Fetch, PreservesCompleteHostClassesAndIsIdempotent) +{ + std::promise done; + std::atomic_bool completed{}; + const auto complete = [&done, &completed](std::string result) { + if (!completed.exchange(true)) + { + done.set_value(std::move(result)); + } + }; + + Babylon::AppRuntime::Options options{}; + options.UnhandledExceptionHandler = [&complete](const Napi::Error& error) { + complete(Napi::GetErrorString(error)); + }; + Babylon::AppRuntime runtime{options}; + + runtime.Dispatch([&complete](Napi::Env env) { + auto global = env.Global(); + const auto hostHeaders = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostHeaders"); + const auto hostResponse = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostResponse"); + global.Set("Headers", hostHeaders); + global.Set("Response", hostResponse); + + Babylon::Polyfills::Fetch::Initialize(env); + EXPECT_TRUE(global.Get("Headers").StrictEquals(hostHeaders)); + EXPECT_TRUE(global.Get("Response").StrictEquals(hostResponse)); + + const auto installedFetch = global.Get("fetch"); + Babylon::Polyfills::Fetch::Initialize(env); + EXPECT_TRUE(global.Get("Headers").StrictEquals(hostHeaders)); + EXPECT_TRUE(global.Get("Response").StrictEquals(hostResponse)); + EXPECT_TRUE(global.Get("fetch").StrictEquals(installedFetch)); + complete({}); + }); + + const auto error = done.get_future().get(); + EXPECT_TRUE(error.empty()) << error; +} + +TEST(Fetch, ReplacesPartialOrNullHostClassesAsACompletePair) +{ + std::promise done; + std::atomic_bool completed{}; + const auto complete = [&done, &completed](std::string result) { + if (!completed.exchange(true)) + { + done.set_value(std::move(result)); + } + }; + + Babylon::AppRuntime::Options options{}; + options.UnhandledExceptionHandler = [&complete](const Napi::Error& error) { + complete(Napi::GetErrorString(error)); + }; + Babylon::AppRuntime runtime{options}; + + runtime.Dispatch([&complete](Napi::Env env) { + auto global = env.Global(); + const auto hostHeaders = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostHeaders"); + global.Set("Headers", hostHeaders); + global.Set("Response", env.Null()); + + Babylon::Polyfills::Fetch::Initialize(env); + const auto installedHeaders = global.Get("Headers"); + const auto installedResponse = global.Get("Response"); + EXPECT_FALSE(installedHeaders.StrictEquals(hostHeaders)); + if (!installedHeaders.IsFunction() || !installedResponse.IsFunction()) + { + complete("Fetch::Initialize did not install a complete Headers/Response pair."); + return; + } + + const auto response = installedResponse.As().New({}); + EXPECT_TRUE(response.Get("headers").As().InstanceOf(installedHeaders.As())); + + Babylon::Polyfills::Fetch::Initialize(env); + EXPECT_TRUE(global.Get("Headers").StrictEquals(installedHeaders)); + EXPECT_TRUE(global.Get("Response").StrictEquals(installedResponse)); + complete({}); + }); + + const auto error = done.get_future().get(); + EXPECT_TRUE(error.empty()) << error; +} + TEST(Console, Log) { Babylon::AppRuntime runtime{};