From e62fabe9d2cfed5d87f94eb6962de550a1e25f0b Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Tue, 21 Jul 2026 21:55:02 -0700 Subject: [PATCH 1/8] Add optional WHATWG Streams polyfill Vendor the ES5 web-streams-polyfill 4.3.0 ponyfill and expose an idempotent Streams initializer that preserves constructors supplied by the selected JavaScript engine. Cover readable, writable, transform, BYOB, error, tee, subclassing, and host-constructor behavior with focused ports from WPT plus Firefox and Chromium regression tests. Validate the implementation on JavaScriptCore under ASan/UBSan and QuickJS Release. --- CMakeLists.txt | 1 + Polyfills/CMakeLists.txt | 6 +- Polyfills/Streams/CMakeLists.txt | 30 ++++ .../Include/Babylon/Polyfills/Streams.h | 11 ++ Polyfills/Streams/README.md | 14 ++ Polyfills/Streams/Source/Streams.cpp | 55 ++++++ Polyfills/Streams/Source/StreamsScripts.h.in | 36 ++++ .../ThirdParty/web-streams-polyfill/LICENSE | 22 +++ .../web-streams-polyfill/ponyfill.es5.js | 8 + Tests/UnitTests/CMakeLists.txt | 1 + Tests/UnitTests/Scripts/tests.ts | 156 ++++++++++++++++++ Tests/UnitTests/Shared/Shared.cpp | 26 +++ 12 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 Polyfills/Streams/CMakeLists.txt create mode 100644 Polyfills/Streams/Include/Babylon/Polyfills/Streams.h create mode 100644 Polyfills/Streams/README.md create mode 100644 Polyfills/Streams/Source/Streams.cpp create mode 100644 Polyfills/Streams/Source/StreamsScripts.h.in create mode 100644 Polyfills/Streams/ThirdParty/web-streams-polyfill/LICENSE create mode 100644 Polyfills/Streams/ThirdParty/web-streams-polyfill/ponyfill.es5.js 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/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/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..4552ed07 --- /dev/null +++ b/Polyfills/Streams/Source/Streams.cpp @@ -0,0 +1,55 @@ +#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) + { + if (global.Get(name.data()).IsUndefined()) + { + 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) + { + if (global.Get(name.data()).IsUndefined()) + { + 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..9bf7f5bf --- /dev/null +++ b/Polyfills/Streams/Source/StreamsScripts.h.in @@ -0,0 +1,36 @@ +#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"; + + 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..973c6420 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -1371,6 +1371,162 @@ 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 () { let emptyBlobs: Blob[], helloBlobs: Blob[], stringBlob: Blob, typedArrayBlob: Blob, arrayBufferBlob: Blob, blobBlob: Blob; diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index a920fa1f..e08d1f4d 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -90,6 +91,7 @@ TEST(JavaScript, All) Babylon::Polyfills::File::Initialize(env); Babylon::Polyfills::TextDecoder::Initialize(env); Babylon::Polyfills::TextEncoder::Initialize(env); + Babylon::Polyfills::Streams::Initialize(env); auto setExitCodeCallback = Napi::Function::New( env, [&exitCodePromise](const Napi::CallbackInfo& info) { @@ -111,6 +113,30 @@ TEST(JavaScript, All) EXPECT_EQ(exitCode, 0); } +TEST(Streams, PreservesHostConstructorsAndIsIdempotent) +{ + Babylon::AppRuntime runtime{}; + std::promise done; + + runtime.Dispatch([&done](Napi::Env env) { + auto global = env.Global(); + const auto hostReadableStream = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostReadableStream"); + global.Set("ReadableStream", hostReadableStream); + + Babylon::Polyfills::Streams::Initialize(env); + EXPECT_TRUE(global.Get("ReadableStream").StrictEquals(hostReadableStream)); + EXPECT_TRUE(global.Get("TransformStream").IsFunction()); + + const auto installedTransformStream = global.Get("TransformStream"); + Babylon::Polyfills::Streams::Initialize(env); + EXPECT_TRUE(global.Get("ReadableStream").StrictEquals(hostReadableStream)); + EXPECT_TRUE(global.Get("TransformStream").StrictEquals(installedTransformStream)); + done.set_value(); + }); + + done.get_future().get(); +} + TEST(Console, Log) { Babylon::AppRuntime runtime{}; From 4529fc45e9fddfefb68583761fcf8bddd87e561e Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Tue, 21 Jul 2026 22:19:21 -0700 Subject: [PATCH 2/8] Complete Blob streaming and slicing support Implement iterable BlobPart conversion, MIME and endings normalization, zero-copy Blob composition and slicing, and lazy 64 KiB byte streams. Delegate File streaming and slicing to its backing Blob while preserving browser class identity.\n\nUse immutable shared segments so nested Blob and slice construction do not duplicate payload bytes. Add focused WPT, WebKit, and Firefox regression coverage for constructor ordering, iterator closure, UTF-8 decoding, BYOB reads, cancellation, lifetime safety, and large nested streams. --- Polyfills/Blob/CMakeLists.txt | 1 + Polyfills/Blob/README.md | 12 + Polyfills/Blob/Source/Blob.cpp | 484 ++++++++++++++++++++++++++++--- Polyfills/Blob/Source/Blob.h | 20 +- Polyfills/File/Source/File.cpp | 47 ++- Polyfills/File/Source/File.h | 2 + Tests/UnitTests/Scripts/tests.ts | 227 +++++++++++++++ 7 files changed, 734 insertions(+), 59 deletions(-) create mode 100644 Polyfills/Blob/README.md 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..2c7ccc0a 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."); + } + + const auto iterator = iteratorMethod.As().Call(blobParts, {}).As(); + const auto next = iterator.Get("next").As(); + try + { + while (true) + { + const auto result = next.Call(iterator, {}).As(); + if (result.Get("done").ToBoolean().Value()) + { + break; + } - if (blobParts.Length() > 1) + 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 (...) { - throw Napi::Error::New(Env(), "Using multiple BlobParts in Blob constructor is not implemented."); + // 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"; + } + + const auto type = options.Get("type"); + if (!type.IsUndefined()) + { + m_type = NormalizeType(type.ToString().Utf8Value()); + } + } - if (options.Has("type")) + if (convertLineEndings) + { + for (const auto segmentIndex : stringSegmentIndices) { - m_type = options.Get("type").As().Utf8Value(); + 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,311 @@ 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 outputLength = std::min(CHUNK_SIZE, state->BlobData->Size - state->Position); + auto outputBuffer = Napi::ArrayBuffer::New(callbackEnv, outputLength); + auto destination = static_cast(outputBuffer.Data()); + 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; + } + } + + auto output = Napi::Uint8Array::New(callbackEnv, outputLength, outputBuffer, 0); + controller.Get("enqueue").As().Call(controller, {output}); + state->Position += outputLength; + state->SegmentIndex = segmentIndex; + state->SegmentOffset = segmentOffset; + + 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; } - else if (blobPart.IsString()) + + auto value = blobPart.ToString().Utf8Value(); + auto bytes = std::make_shared>(value.size()); + if (!value.empty()) { - const auto str = blobPart.As().Utf8Value(); - const auto begin = reinterpret_cast(str.data()); - m_data.assign(begin, begin + str.length()); + std::memcpy(bytes->data(), value.data(), value.size()); } - else + 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) { - // 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()); + m_data->CopyTo(0, static_cast(arrayBuffer.Data()), m_data->Size); } + return arrayBuffer; + } + + std::string Blob::NormalizeType(std::string type) + { + for (auto& character : type) + { + const auto value = static_cast(character); + if (value < 0x20 || value > 0x7E) + { + return {}; + } + if (character >= 'A' && character <= 'Z') + { + character = static_cast(character - 'A' + 'a'); + } + } + 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) + { + 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/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/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index 973c6420..0df6e9de 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -1528,8 +1528,24 @@ describe("Web Streams", function () { }); 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"]); @@ -1572,6 +1588,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) { @@ -1593,6 +1723,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(); @@ -1639,6 +1774,82 @@ 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([]); + }); + + 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 () { @@ -1960,6 +2171,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, From 075ab25a93708070bdc67b77271198469dc6b7b4 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 22 Jul 2026 00:26:50 -0700 Subject: [PATCH 3/8] Keep Streams implementations internally consistent Replace a partial or null host Streams surface as a complete constructor suite so stream products retain compatible instanceof relationships. Preserve a complete suite on repeated initialization.\n\nHarden the native initialization test so N-API failures complete the test promise instead of hanging, and cover partial host replacement plus null handling and cross-constructor compatibility. --- Polyfills/Streams/Source/Streams.cpp | 8 ++---- Tests/UnitTests/Shared/Shared.cpp | 42 +++++++++++++++++++++------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Polyfills/Streams/Source/Streams.cpp b/Polyfills/Streams/Source/Streams.cpp index 4552ed07..e742344f 100644 --- a/Polyfills/Streams/Source/Streams.cpp +++ b/Polyfills/Streams/Source/Streams.cpp @@ -31,7 +31,8 @@ namespace Babylon::Polyfills::Streams bool needsPonyfill{}; for (const auto name : constructorNames) { - if (global.Get(name.data()).IsUndefined()) + const auto constructor = global.Get(name.data()); + if (constructor.IsUndefined() || constructor.IsNull()) { needsPonyfill = true; break; @@ -46,10 +47,7 @@ namespace Babylon::Polyfills::Streams const auto exports = Napi::Eval(env, Internal::StreamsScripts::Ponyfill.data(), "jsruntimehost://web-streams-polyfill.js").As(); for (const auto name : constructorNames) { - if (global.Get(name.data()).IsUndefined()) - { - global.Set(name.data(), exports.Get(name.data())); - } + global.Set(name.data(), exports.Get(name.data())); } } } diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index e08d1f4d..1c3b93a0 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -113,28 +113,50 @@ TEST(JavaScript, All) EXPECT_EQ(exitCode, 0); } -TEST(Streams, PreservesHostConstructorsAndIsIdempotent) +TEST(Streams, ReplacesPartialHostSuiteAndIsIdempotent) { - Babylon::AppRuntime runtime{}; - std::promise done; + 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([&done](Napi::Env env) { + 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); - EXPECT_TRUE(global.Get("ReadableStream").StrictEquals(hostReadableStream)); - EXPECT_TRUE(global.Get("TransformStream").IsFunction()); - + 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(hostReadableStream)); + EXPECT_TRUE(global.Get("ReadableStream").StrictEquals(installedReadableStream)); EXPECT_TRUE(global.Get("TransformStream").StrictEquals(installedTransformStream)); - done.set_value(); + complete({}); }); - done.get_future().get(); + const auto error = done.get_future().get(); + EXPECT_TRUE(error.empty()) << error; } TEST(Console, Log) From 2a35a0d493886fe67a16303351ccc6966c590858 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 22 Jul 2026 00:29:36 -0700 Subject: [PATCH 4/8] Write Blob BYOB reads into caller buffers Use ReadableByteStreamController.byobRequest views directly and respond with the produced byte count instead of allocating and enqueueing a 64 KiB intermediate chunk. Keep the existing bounded allocation path for default readers.\n\nAdd WPT-derived coverage for small offset BYOB views that cross immutable Blob segment boundaries. --- Polyfills/Blob/Source/Blob.cpp | 69 +++++++++++++++++++++----------- Tests/UnitTests/Scripts/tests.ts | 27 +++++++++++++ 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/Polyfills/Blob/Source/Blob.cpp b/Polyfills/Blob/Source/Blob.cpp index 2c7ccc0a..4dcb4832 100644 --- a/Polyfills/Blob/Source/Blob.cpp +++ b/Polyfills/Blob/Source/Blob.cpp @@ -309,33 +309,56 @@ namespace Babylon::Polyfills::Internal } constexpr size_t CHUNK_SIZE = 64 * 1024; - const auto outputLength = std::min(CHUNK_SIZE, state->BlobData->Size - state->Position); - auto outputBuffer = Napi::ArrayBuffer::New(callbackEnv, outputLength); - auto destination = static_cast(outputBuffer.Data()); - auto segmentIndex = state->SegmentIndex; - auto segmentOffset = state->SegmentOffset; - size_t remaining = outputLength; - - while (remaining > 0) + 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& 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) + const auto byobRequest = byobRequestValue.As(); + const auto viewValue = byobRequest.Get("view"); + if (!viewValue.IsTypedArray()) { - ++segmentIndex; - segmentOffset = 0; + throw Napi::TypeError::New(callbackEnv, "Blob.stream() received an invalid BYOB request view."); } - } - auto output = Napi::Uint8Array::New(callbackEnv, outputLength, outputBuffer, 0); - controller.Get("enqueue").As().Call(controller, {output}); - state->Position += outputLength; - state->SegmentIndex = segmentIndex; - state->SegmentOffset = segmentOffset; + 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) { diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index 0df6e9de..85ffd980 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -1794,6 +1794,33 @@ describe("Blob", function () { 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(); From 8fe0b9ea982fc4341c567acabcefd0d50ed2b257 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 26 Jul 2026 02:14:50 -0700 Subject: [PATCH 5/8] Preserve Streams bundle bytes across source splits --- Polyfills/Streams/Source/StreamsScripts.h.in | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Polyfills/Streams/Source/StreamsScripts.h.in b/Polyfills/Streams/Source/StreamsScripts.h.in index 9bf7f5bf..126a23f7 100644 --- a/Polyfills/Streams/Source/StreamsScripts.h.in +++ b/Polyfills/Streams/Source/StreamsScripts.h.in @@ -9,9 +9,10 @@ namespace Babylon::Polyfills::Internal::StreamsScripts (function() { var exports = {}; var module = { exports: exports }; -@WEB_STREAMS_POLYFILL_SOURCE_1@ -)JSRHSTREAM"; +@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; })() From 35d40c96574bc6ad8fd0a060fccdb1fc9e4e8200 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 26 Jul 2026 04:49:27 -0700 Subject: [PATCH 6/8] Avoid per-stream Blob callback wrappers --- Polyfills/Blob/Source/Blob.cpp | 151 +++++++++++++++++++-------------- Polyfills/Blob/Source/Blob.h | 2 + 2 files changed, 88 insertions(+), 65 deletions(-) diff --git a/Polyfills/Blob/Source/Blob.cpp b/Polyfills/Blob/Source/Blob.cpp index 4dcb4832..8fc318a9 100644 --- a/Polyfills/Blob/Source/Blob.cpp +++ b/Polyfills/Blob/Source/Blob.cpp @@ -293,87 +293,108 @@ namespace Babylon::Polyfills::Internal 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); + auto stateOwner = Napi::External::New( + env, + new StreamState{m_data}, + [](Napi::Env, StreamState* state) { + delete state; + }); + auto state = stateOwner.Data(); + + // ReadableStream retains its underlying source as the receiver for the + // pull/cancel algorithms, so let that object own the callback state. + // The compile-time callback overload passes the state directly through + // napi_create_function. In contrast, a capturing Function::New allocates + // node-addon-api CallbackData and attaches it as a hidden property to + // every function. Besides two avoidable allocations per Blob stream, + // that path creates extra JSC Function structure transitions and exposed + // a system-WebKitGTK LeakSanitizer allocation during Worker teardown. + source.Set(Napi::Symbol::New(env, "Blob stream state"), stateOwner); 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(); - } + source.Set("pull", Napi::Function::New<&Blob::PullStream>(env, "pull", state)); + source.Set("cancel", Napi::Function::New<&Blob::CancelStream>(env, "cancel", state)); - 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; + return readableStreamValue.As().New({source}); + } - 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; - } - } + Napi::Value Blob::PullStream(const Napi::CallbackInfo& info) + { + auto env = info.Env(); + auto state = static_cast(info.Data()); + auto controller = info[0].As(); + if (!state->BlobData || state->Position >= state->BlobData->Size) + { + controller.Get("close").As().Call(controller, {}); + state->BlobData.reset(); + return env.Undefined(); + } - state->Position += outputLength; - state->SegmentIndex = segmentIndex; - state->SegmentOffset = segmentOffset; - }; + 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; - const auto byobRequestValue = controller.Get("byobRequest"); - if (!byobRequestValue.IsUndefined() && !byobRequestValue.IsNull()) + while (remaining > 0) { - const auto byobRequest = byobRequestValue.As(); - const auto viewValue = byobRequest.Get("view"); - if (!viewValue.IsTypedArray()) + 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) { - throw Napi::TypeError::New(callbackEnv, "Blob.stream() received an invalid BYOB request view."); + ++segmentIndex; + segmentOffset = 0; } - - 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) + 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()) { - controller.Get("close").As().Call(controller, {}); - state->BlobData.reset(); + throw Napi::TypeError::New(env, "Blob.stream() received an invalid BYOB request view."); } - return callbackEnv.Undefined(); - })); - source.Set("cancel", Napi::Function::New(env, [state](const Napi::CallbackInfo& callbackInfo) -> Napi::Value { + 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(env, outputLength)}); + } + else + { + const auto outputLength = std::min(CHUNK_SIZE, state->BlobData->Size - state->Position); + auto outputBuffer = Napi::ArrayBuffer::New(env, outputLength); + copyInto(static_cast(outputBuffer.Data()), outputLength); + auto output = Napi::Uint8Array::New(env, 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 callbackInfo.Env().Undefined(); - })); + } - return readableStreamValue.As().New({source}); + return env.Undefined(); + } + + Napi::Value Blob::CancelStream(const Napi::CallbackInfo& info) + { + static_cast(info.Data())->BlobData.reset(); + return info.Env().Undefined(); } bool Blob::AppendBlobPart(Data& data, const Napi::Value& blobPart) diff --git a/Polyfills/Blob/Source/Blob.h b/Polyfills/Blob/Source/Blob.h index 11979e1c..5035bf36 100644 --- a/Polyfills/Blob/Source/Blob.h +++ b/Polyfills/Blob/Source/Blob.h @@ -23,6 +23,8 @@ namespace Babylon::Polyfills::Internal Napi::Value Bytes(const Napi::CallbackInfo& info); Napi::Value Slice(const Napi::CallbackInfo& info); Napi::Value Stream(const Napi::CallbackInfo& info); + static Napi::Value PullStream(const Napi::CallbackInfo& info); + static Napi::Value CancelStream(const Napi::CallbackInfo& info); struct Segment; struct Data; From bbecd3d589a51d7870ad68891bd8f755134fc9c5 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 26 Jul 2026 05:06:43 -0700 Subject: [PATCH 7/8] Use JSC-compatible Blob stream state key --- Polyfills/Blob/Source/Blob.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Polyfills/Blob/Source/Blob.cpp b/Polyfills/Blob/Source/Blob.cpp index 8fc318a9..60d8d9de 100644 --- a/Polyfills/Blob/Source/Blob.cpp +++ b/Polyfills/Blob/Source/Blob.cpp @@ -310,7 +310,11 @@ namespace Babylon::Polyfills::Internal // every function. Besides two avoidable allocations per Blob stream, // that path creates extra JSC Function structure transitions and exposed // a system-WebKitGTK LeakSanitizer allocation during Worker teardown. - source.Set(Napi::Symbol::New(env, "Blob stream state"), stateOwner); + // Keep the owner on the internal source object with a string key. + // The legacy JSC Node-API adapter stringifies property keys in + // napi_set_property, so passing a Symbol here throws even though + // modern engines accept it. + source.Set("__jsRuntimeHostBlobStreamState", stateOwner); source.Set("type", "bytes"); source.Set("pull", Napi::Function::New<&Blob::PullStream>(env, "pull", state)); source.Set("cancel", Napi::Function::New<&Blob::CancelStream>(env, "cancel", state)); From 2bfc18c951a33b073acf8539f708b0e390276398 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 26 Jul 2026 05:39:02 -0700 Subject: [PATCH 8/8] Share Blob stream source callbacks --- Polyfills/Blob/Source/Blob.cpp | 234 ++++++++++++++++++--------------- Polyfills/Blob/Source/Blob.h | 4 +- 2 files changed, 127 insertions(+), 111 deletions(-) diff --git a/Polyfills/Blob/Source/Blob.cpp b/Polyfills/Blob/Source/Blob.cpp index 60d8d9de..5b29f730 100644 --- a/Polyfills/Blob/Source/Blob.cpp +++ b/Polyfills/Blob/Source/Blob.cpp @@ -51,12 +51,119 @@ namespace Babylon::Polyfills::Internal } }; - struct Blob::StreamState + class Blob::StreamSource final : public Napi::ObjectWrap { - std::shared_ptr BlobData; - size_t Position{}; - size_t SegmentIndex{}; - size_t SegmentOffset{}; + public: + static Napi::Function Define(Napi::Env env) + { + return DefineClass( + env, + "BlobStreamSource", + { + InstanceMethod("pull", &StreamSource::Pull), + InstanceMethod("cancel", &StreamSource::Cancel), + }); + } + + explicit StreamSource(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + if (info.Length() != 1 || !info[0].IsObject()) + { + throw Napi::TypeError::New(info.Env(), "Blob stream source requires a Blob."); + } + + const auto blob = Napi::ObjectWrap::Unwrap(info[0].As()); + if (blob == nullptr) + { + throw Napi::TypeError::New(info.Env(), "Blob stream source requires a Blob."); + } + m_blobData = blob->m_data; + } + + private: + Napi::Value Pull(const Napi::CallbackInfo& info) + { + auto env = info.Env(); + auto controller = info[0].As(); + if (!m_blobData || m_position >= m_blobData->Size) + { + controller.Get("close").As().Call(controller, {}); + m_blobData.reset(); + return env.Undefined(); + } + + constexpr size_t CHUNK_SIZE = 64 * 1024; + const auto copyInto = [this](std::byte* destination, size_t outputLength) { + auto segmentIndex = m_segmentIndex; + auto segmentOffset = m_segmentOffset; + size_t remaining = outputLength; + + while (remaining > 0) + { + const auto& segment = m_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; + } + } + + m_position += outputLength; + m_segmentIndex = segmentIndex; + m_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(env, "Blob.stream() received an invalid BYOB request view."); + } + + const auto view = viewValue.As(); + const auto outputLength = std::min({CHUNK_SIZE, view.ByteLength(), m_blobData->Size - m_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(env, outputLength)}); + } + else + { + const auto outputLength = std::min(CHUNK_SIZE, m_blobData->Size - m_position); + auto outputBuffer = Napi::ArrayBuffer::New(env, outputLength); + copyInto(static_cast(outputBuffer.Data()), outputLength); + auto output = Napi::Uint8Array::New(env, outputLength, outputBuffer, 0); + controller.Get("enqueue").As().Call(controller, {output}); + } + + if (m_position == m_blobData->Size) + { + controller.Get("close").As().Call(controller, {}); + m_blobData.reset(); + } + + return env.Undefined(); + } + + Napi::Value Cancel(const Napi::CallbackInfo& info) + { + m_blobData.reset(); + return info.Env().Undefined(); + } + + std::shared_ptr m_blobData; + size_t m_position{}; + size_t m_segmentIndex{}; + size_t m_segmentOffset{}; }; void Blob::Initialize(Napi::Env env) @@ -81,6 +188,17 @@ namespace Babylon::Polyfills::Internal 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}); + + // Underlying stream sources are native ObjectWrap instances so + // pull/cancel live on one shared prototype. Creating two native + // functions for every Blob.stream() caused avoidable JSC Function + // structure transitions and exposed a WebKitGTK LeakSanitizer + // allocation during Worker teardown. + auto sourceDescriptor = Napi::Object::New(env); + sourceDescriptor.Set("value", StreamSource::Define(env)); + env.Global().Get("Object").As().Get("defineProperty").As().Call( + env.Global().Get("Object"), + {func, Napi::String::New(env, "__jsRuntimeHostBlobStreamSource"), sourceDescriptor}); env.Global().Set(JS_BLOB_CONSTRUCTOR_NAME, func); } } @@ -293,114 +411,14 @@ namespace Babylon::Polyfills::Internal throw Napi::TypeError::New(env, "Blob.stream() requires ReadableStream to be installed."); } - auto source = Napi::Object::New(env); - auto stateOwner = Napi::External::New( - env, - new StreamState{m_data}, - [](Napi::Env, StreamState* state) { - delete state; - }); - auto state = stateOwner.Data(); - - // ReadableStream retains its underlying source as the receiver for the - // pull/cancel algorithms, so let that object own the callback state. - // The compile-time callback overload passes the state directly through - // napi_create_function. In contrast, a capturing Function::New allocates - // node-addon-api CallbackData and attaches it as a hidden property to - // every function. Besides two avoidable allocations per Blob stream, - // that path creates extra JSC Function structure transitions and exposed - // a system-WebKitGTK LeakSanitizer allocation during Worker teardown. - // Keep the owner on the internal source object with a string key. - // The legacy JSC Node-API adapter stringifies property keys in - // napi_set_property, so passing a Symbol here throws even though - // modern engines accept it. - source.Set("__jsRuntimeHostBlobStreamState", stateOwner); + const auto blobConstructor = env.Global().Get("Blob").As(); + const auto sourceConstructor = blobConstructor.Get("__jsRuntimeHostBlobStreamSource").As(); + auto source = sourceConstructor.New({info.This()}); source.Set("type", "bytes"); - source.Set("pull", Napi::Function::New<&Blob::PullStream>(env, "pull", state)); - source.Set("cancel", Napi::Function::New<&Blob::CancelStream>(env, "cancel", state)); return readableStreamValue.As().New({source}); } - Napi::Value Blob::PullStream(const Napi::CallbackInfo& info) - { - auto env = info.Env(); - auto state = static_cast(info.Data()); - auto controller = info[0].As(); - if (!state->BlobData || state->Position >= state->BlobData->Size) - { - controller.Get("close").As().Call(controller, {}); - state->BlobData.reset(); - return env.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(env, "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(env, outputLength)}); - } - else - { - const auto outputLength = std::min(CHUNK_SIZE, state->BlobData->Size - state->Position); - auto outputBuffer = Napi::ArrayBuffer::New(env, outputLength); - copyInto(static_cast(outputBuffer.Data()), outputLength); - auto output = Napi::Uint8Array::New(env, 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 env.Undefined(); - } - - Napi::Value Blob::CancelStream(const Napi::CallbackInfo& info) - { - static_cast(info.Data())->BlobData.reset(); - return info.Env().Undefined(); - } - bool Blob::AppendBlobPart(Data& data, const Napi::Value& blobPart) { const std::byte* source{}; diff --git a/Polyfills/Blob/Source/Blob.h b/Polyfills/Blob/Source/Blob.h index 5035bf36..6174d0af 100644 --- a/Polyfills/Blob/Source/Blob.h +++ b/Polyfills/Blob/Source/Blob.h @@ -23,12 +23,10 @@ namespace Babylon::Polyfills::Internal Napi::Value Bytes(const Napi::CallbackInfo& info); Napi::Value Slice(const Napi::CallbackInfo& info); Napi::Value Stream(const Napi::CallbackInfo& info); - static Napi::Value PullStream(const Napi::CallbackInfo& info); - static Napi::Value CancelStream(const Napi::CallbackInfo& info); struct Segment; struct Data; - struct StreamState; + class StreamSource; bool AppendBlobPart(Data& data, const Napi::Value& blobPart); Napi::ArrayBuffer CreateArrayBuffer() const;