From 5702b39ef1a90969dcb6aad87d799d67973e4911 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 20 Nov 2025 17:14:37 -0800 Subject: [PATCH] runtime: implement CompressionStream/DecompressionStream (#24757) Closes https://github.com/oven-sh/bun/issues/1723 Closes https://github.com/oven-sh/bun/pull/22214 Closes https://github.com/oven-sh/bun/pull/24241 also supports the `"brotli"` and `"zstd"` formats image --- bench/runner.mjs | 5 +- bench/snippets/compression-streams.mjs | 156 ++++++++++++++++++ docs/runtime/nodejs-compat.mdx | 4 +- src/bun.js/bindings/JSCompressionStream.cpp | 139 ++++++++++++++++ src/bun.js/bindings/JSCompressionStream.h | 40 +++++ src/bun.js/bindings/JSDecompressionStream.cpp | 139 ++++++++++++++++ src/bun.js/bindings/JSDecompressionStream.h | 40 +++++ src/bun.js/bindings/ZigGlobalObject.cpp | 4 + src/bun.js/bindings/ZigGlobalObject.lut.txt | 6 +- src/bun.js/bindings/js_classes.ts | 2 + .../bindings/webcore/DOMClientIsoSubspaces.h | 2 + src/bun.js/bindings/webcore/DOMConstructors.h | 4 +- src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 2 + src/codegen/bundle-modules.ts | 2 +- src/js/builtins/CompressionStream.ts | 33 ++++ src/js/builtins/DecompressionStream.ts | 33 ++++ src/js/node/stream.web.ts | 4 +- test/integration/bun-types/fixture/streams.ts | 2 + .../test/parallel/test-global-webstreams.js | 24 +++ .../test-whatwg-webstreams-compression.js | 70 ++++++++ 20 files changed, 699 insertions(+), 12 deletions(-) create mode 100644 bench/snippets/compression-streams.mjs create mode 100644 src/bun.js/bindings/JSCompressionStream.cpp create mode 100644 src/bun.js/bindings/JSCompressionStream.h create mode 100644 src/bun.js/bindings/JSDecompressionStream.cpp create mode 100644 src/bun.js/bindings/JSDecompressionStream.h create mode 100644 src/js/builtins/CompressionStream.ts create mode 100644 src/js/builtins/DecompressionStream.ts create mode 100644 test/js/node/test/parallel/test-global-webstreams.js create mode 100644 test/js/node/test/parallel/test-whatwg-webstreams-compression.js diff --git a/bench/runner.mjs b/bench/runner.mjs index 9f6bcee16f..b9715232f0 100644 --- a/bench/runner.mjs +++ b/bench/runner.mjs @@ -13,7 +13,4 @@ export function run(opts = {}) { } export const bench = Mitata.bench; - -export function group(_name, fn) { - return Mitata.group(fn); -} +export const group = Mitata.group; diff --git a/bench/snippets/compression-streams.mjs b/bench/snippets/compression-streams.mjs new file mode 100644 index 0000000000..b8f3d34cd5 --- /dev/null +++ b/bench/snippets/compression-streams.mjs @@ -0,0 +1,156 @@ +import { bench, group, run } from "../runner.mjs"; + +const runAll = !process.argv.includes("--simple"); + +const small = new Uint8Array(1024); +const medium = new Uint8Array(1024 * 100); +const large = new Uint8Array(1024 * 1024); + +for (let i = 0; i < large.length; i++) { + const value = Math.floor(Math.sin(i / 100) * 128 + 128); + if (i < small.length) small[i] = value; + if (i < medium.length) medium[i] = value; + large[i] = value; +} + +const format = new Intl.NumberFormat("en-US", { notation: "compact", unit: "byte" }); + +async function compress(data, format) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + const reader = cs.readable.getReader(); + + writer.write(data); + writer.close(); + + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + + const result = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + return result; +} + +async function decompress(data, format) { + const ds = new DecompressionStream(format); + const writer = ds.writable.getWriter(); + const reader = ds.readable.getReader(); + + writer.write(data); + writer.close(); + + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + + const result = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + return result; +} + +async function roundTrip(data, format) { + const compressed = await compress(data, format); + return await decompress(compressed, format); +} + +const formats = ["deflate", "gzip", "deflate-raw"]; +if (runAll) formats.push("brotli", "zstd"); + +// Small data benchmarks (1KB) +group(`CompressionStream ${format.format(small.length)}`, () => { + for (const fmt of formats) { + try { + new CompressionStream(fmt); + bench(fmt, async () => await compress(small, fmt)); + } catch (e) { + // Skip unsupported formats + } + } +}); + +// Medium data benchmarks (100KB) +group(`CompressionStream ${format.format(medium.length)}`, () => { + for (const fmt of formats) { + try { + new CompressionStream(fmt); + bench(fmt, async () => await compress(medium, fmt)); + } catch (e) {} + } +}); + +// Large data benchmarks (1MB) +group(`CompressionStream ${format.format(large.length)}`, () => { + for (const fmt of formats) { + try { + new CompressionStream(fmt); + bench(fmt, async () => await compress(large, fmt)); + } catch (e) { + // Skip unsupported formats + } + } +}); + +const compressedData = {}; +for (const fmt of formats) { + try { + compressedData[fmt] = { + small: await compress(small, fmt), + medium: await compress(medium, fmt), + large: await compress(large, fmt), + }; + } catch (e) { + // Skip unsupported formats + } +} + +group(`DecompressionStream ${format.format(small.length)}`, () => { + for (const fmt of formats) { + if (compressedData[fmt]) { + bench(fmt, async () => await decompress(compressedData[fmt].small, fmt)); + } + } +}); + +group(`DecompressionStream ${format.format(medium.length)}`, () => { + for (const fmt of formats) { + if (compressedData[fmt]) { + bench(fmt, async () => await decompress(compressedData[fmt].medium, fmt)); + } + } +}); + +group(`DecompressionStream ${format.format(large.length)}`, () => { + for (const fmt of formats) { + if (compressedData[fmt]) { + bench(fmt, async () => await decompress(compressedData[fmt].large, fmt)); + } + } +}); + +group(`roundtrip ${format.format(large.length)}`, () => { + for (const fmt of formats) { + try { + new CompressionStream(fmt); + bench(fmt, async () => await roundTrip(large, fmt)); + } catch (e) { + // Skip unsupported formats + } + } +}); + +await run(); diff --git a/docs/runtime/nodejs-compat.mdx b/docs/runtime/nodejs-compat.mdx index ff5b489d99..0a382f97bd 100644 --- a/docs/runtime/nodejs-compat.mdx +++ b/docs/runtime/nodejs-compat.mdx @@ -245,7 +245,7 @@ The table below lists all globals implemented by Node.js and Bun's current compa ### [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) -🔴 Not implemented. +🟢 Fully implemented. ### [`console`](https://developer.mozilla.org/en-US/docs/Web/API/console) @@ -273,7 +273,7 @@ The table below lists all globals implemented by Node.js and Bun's current compa ### [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) -🔴 Not implemented. +🟢 Fully implemented. ### [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event) diff --git a/src/bun.js/bindings/JSCompressionStream.cpp b/src/bun.js/bindings/JSCompressionStream.cpp new file mode 100644 index 0000000000..5fa7be0e09 --- /dev/null +++ b/src/bun.js/bindings/JSCompressionStream.cpp @@ -0,0 +1,139 @@ +#include "config.h" +#include "JSCompressionStream.h" + +#include "JSDOMBuiltinConstructor.h" +#include "JSDOMGlobalObjectInlines.h" +#include "WebCoreJSClientData.h" +#include "WebCoreJSBuiltins.h" +#include +#include + +namespace WebCore { + +using namespace JSC; + +static JSC_DECLARE_CUSTOM_GETTER(jsCompressionStreamConstructor); + +class JSCompressionStreamPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSCompressionStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSCompressionStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSCompressionStreamPrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCompressionStreamPrototype, Base); + return &vm.plainObjectSpace(); + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSCompressionStreamPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCompressionStreamPrototype, JSCompressionStreamPrototype::Base); + +using JSCompressionStreamDOMConstructor = JSDOMBuiltinConstructor; + +template<> const ClassInfo JSCompressionStreamDOMConstructor::s_info = { "CompressionStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCompressionStreamDOMConstructor) }; + +template<> JSValue JSCompressionStreamDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + UNUSED_PARAM(vm); + return globalObject.functionPrototype(); +} + +template<> void JSCompressionStreamDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "CompressionStream"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSCompressionStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +template<> FunctionExecutable* JSCompressionStreamDOMConstructor::initializeExecutable(VM& vm) +{ + return compressionStreamInitializeCompressionStreamCodeGenerator(vm); +} + +static const HashTableValue JSCompressionStreamPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsCompressionStreamConstructor, 0 } }, + { "readable"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, compressionStreamReadableCodeGenerator, 0 } }, + { "writable"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, compressionStreamWritableCodeGenerator, 0 } }, +}; + +const ClassInfo JSCompressionStreamPrototype::s_info = { "CompressionStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCompressionStreamPrototype) }; + +void JSCompressionStreamPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSCompressionStream::info(), JSCompressionStreamPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +const ClassInfo JSCompressionStream::s_info = { "CompressionStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCompressionStream) }; + +JSC::GCClient::IsoSubspace* JSCompressionStream::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForCompressionStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCompressionStream = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForCompressionStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForCompressionStream = std::forward(space); }); +} + +JSCompressionStream::JSCompressionStream(Structure* structure, JSDOMGlobalObject& globalObject) + : JSDOMObject(structure, globalObject) +{ +} + +void JSCompressionStream::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSObject* JSCompressionStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSCompressionStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSCompressionStreamPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSCompressionStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSCompressionStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *jsCast(globalObject)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsCompressionStreamConstructor, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* prototype = jsDynamicCast(JSValue::decode(thisValue)); + if (!prototype) + return throwVMTypeError(lexicalGlobalObject, throwScope); + return JSValue::encode(JSCompressionStream::getConstructor(vm, lexicalGlobalObject)); +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/JSCompressionStream.h b/src/bun.js/bindings/JSCompressionStream.h new file mode 100644 index 0000000000..99833ab99d --- /dev/null +++ b/src/bun.js/bindings/JSCompressionStream.h @@ -0,0 +1,40 @@ +#pragma once + +#include "JSDOMWrapper.h" + +namespace WebCore { + +class JSCompressionStream : public JSDOMObject { +public: + using Base = JSDOMObject; + static JSCompressionStream* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) + { + JSCompressionStream* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSCompressionStream(structure, *globalObject); + ptr->finishCreation(globalObject->vm()); + return ptr; + } + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); + + JSCompressionStream(JSC::Structure*, JSDOMGlobalObject&); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/JSDecompressionStream.cpp b/src/bun.js/bindings/JSDecompressionStream.cpp new file mode 100644 index 0000000000..160e01eb47 --- /dev/null +++ b/src/bun.js/bindings/JSDecompressionStream.cpp @@ -0,0 +1,139 @@ +#include "config.h" +#include "JSDecompressionStream.h" + +#include "JSDOMBuiltinConstructor.h" +#include "JSDOMGlobalObjectInlines.h" +#include "WebCoreJSClientData.h" +#include "WebCoreJSBuiltins.h" +#include +#include + +namespace WebCore { + +using namespace JSC; + +static JSC_DECLARE_CUSTOM_GETTER(jsDecompressionStreamConstructor); + +class JSDecompressionStreamPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSDecompressionStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSDecompressionStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSDecompressionStreamPrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSDecompressionStreamPrototype, Base); + return &vm.plainObjectSpace(); + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSDecompressionStreamPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSDecompressionStreamPrototype, JSDecompressionStreamPrototype::Base); + +using JSDecompressionStreamDOMConstructor = JSDOMBuiltinConstructor; + +template<> const ClassInfo JSDecompressionStreamDOMConstructor::s_info = { "DecompressionStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDecompressionStreamDOMConstructor) }; + +template<> JSValue JSDecompressionStreamDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + UNUSED_PARAM(vm); + return globalObject.functionPrototype(); +} + +template<> void JSDecompressionStreamDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "DecompressionStream"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSDecompressionStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +template<> FunctionExecutable* JSDecompressionStreamDOMConstructor::initializeExecutable(VM& vm) +{ + return decompressionStreamInitializeDecompressionStreamCodeGenerator(vm); +} + +static const HashTableValue JSDecompressionStreamPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsDecompressionStreamConstructor, 0 } }, + { "readable"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, decompressionStreamReadableCodeGenerator, 0 } }, + { "writable"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, decompressionStreamWritableCodeGenerator, 0 } }, +}; + +const ClassInfo JSDecompressionStreamPrototype::s_info = { "DecompressionStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDecompressionStreamPrototype) }; + +void JSDecompressionStreamPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSDecompressionStream::info(), JSDecompressionStreamPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +const ClassInfo JSDecompressionStream::s_info = { "DecompressionStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDecompressionStream) }; + +JSC::GCClient::IsoSubspace* JSDecompressionStream::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForDecompressionStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForDecompressionStream = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForDecompressionStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForDecompressionStream = std::forward(space); }); +} + +JSDecompressionStream::JSDecompressionStream(Structure* structure, JSDOMGlobalObject& globalObject) + : JSDOMObject(structure, globalObject) +{ +} + +void JSDecompressionStream::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSObject* JSDecompressionStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSDecompressionStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSDecompressionStreamPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSDecompressionStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSDecompressionStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *jsCast(globalObject)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsDecompressionStreamConstructor, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* prototype = jsDynamicCast(JSValue::decode(thisValue)); + if (!prototype) + return throwVMTypeError(lexicalGlobalObject, throwScope); + return JSValue::encode(JSDecompressionStream::getConstructor(vm, lexicalGlobalObject)); +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/JSDecompressionStream.h b/src/bun.js/bindings/JSDecompressionStream.h new file mode 100644 index 0000000000..b35119606a --- /dev/null +++ b/src/bun.js/bindings/JSDecompressionStream.h @@ -0,0 +1,40 @@ +#pragma once + +#include "JSDOMWrapper.h" + +namespace WebCore { + +class JSDecompressionStream : public JSDOMObject { +public: + using Base = JSDOMObject; + static JSDecompressionStream* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) + { + JSDecompressionStream* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSDecompressionStream(structure, *globalObject); + ptr->finishCreation(globalObject->vm()); + return ptr; + } + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); + + JSDecompressionStream(JSC::Structure*, JSDOMGlobalObject&); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index caf3c26aa2..7e22705369 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -78,6 +78,8 @@ #include "JSAbortAlgorithm.h" #include "JSAbortController.h" #include "JSAbortSignal.h" +#include "JSCompressionStream.h" +#include "JSDecompressionStream.h" #include "JSBroadcastChannel.h" #include "JSBuffer.h" #include "JSBufferList.h" @@ -962,9 +964,11 @@ WEBCORE_GENERATED_CONSTRUCTOR_GETTER(AbortSignal); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(BroadcastChannel); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(ByteLengthQueuingStrategy) WEBCORE_GENERATED_CONSTRUCTOR_GETTER(CloseEvent); +WEBCORE_GENERATED_CONSTRUCTOR_GETTER(CompressionStream); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(CountQueuingStrategy) WEBCORE_GENERATED_CONSTRUCTOR_GETTER(CryptoKey); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(CustomEvent); +WEBCORE_GENERATED_CONSTRUCTOR_GETTER(DecompressionStream); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(DOMException); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(DOMFormData); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(DOMURL); diff --git a/src/bun.js/bindings/ZigGlobalObject.lut.txt b/src/bun.js/bindings/ZigGlobalObject.lut.txt index 49b22f1412..9fbf2e00f0 100644 --- a/src/bun.js/bindings/ZigGlobalObject.lut.txt +++ b/src/bun.js/bindings/ZigGlobalObject.lut.txt @@ -21,9 +21,9 @@ setInterval functionSetInterval Function 1 setTimeout functionSetTimeout Function 1 structuredClone WebCore::jsFunctionStructuredClone Function 2 - + global GlobalObject_getGlobalThis PropertyCallback - + Bun GlobalObject::m_bunObject CellProperty|DontDelete|ReadOnly File GlobalObject::m_JSDOMFileConstructor CellProperty crypto GlobalObject::m_cryptoObject CellProperty @@ -48,9 +48,11 @@ BroadcastChannel BroadcastChannelConstructorCallback PropertyCallback ByteLengthQueuingStrategy ByteLengthQueuingStrategyConstructorCallback PropertyCallback CloseEvent CloseEventConstructorCallback PropertyCallback + CompressionStream CompressionStreamConstructorCallback PropertyCallback CountQueuingStrategy CountQueuingStrategyConstructorCallback PropertyCallback CryptoKey CryptoKeyConstructorCallback PropertyCallback CustomEvent CustomEventConstructorCallback PropertyCallback + DecompressionStream DecompressionStreamConstructorCallback PropertyCallback DOMException DOMExceptionConstructorCallback PropertyCallback ErrorEvent ErrorEventConstructorCallback PropertyCallback Event EventConstructorCallback PropertyCallback diff --git a/src/bun.js/bindings/js_classes.ts b/src/bun.js/bindings/js_classes.ts index 564fbb39bc..989f69cbb4 100644 --- a/src/bun.js/bindings/js_classes.ts +++ b/src/bun.js/bindings/js_classes.ts @@ -8,4 +8,6 @@ export default [ ["WritableStream", "JSWritableStream.h"], ["TransformStream", "JSTransformStream.h"], ["ArrayBuffer"], + ["CompressionStream", "JSCompressionStream.h"], + ["DecompressionStream", "JSDecompressionStream.h"], ]; diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index ce662148a5..b19ae4faa9 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -283,6 +283,8 @@ public: std::unique_ptr m_clientSubspaceForReadableStreamSource; std::unique_ptr m_clientSubspaceForTransformStream; std::unique_ptr m_clientSubspaceForTransformStreamDefaultController; + std::unique_ptr m_clientSubspaceForCompressionStream; + std::unique_ptr m_clientSubspaceForDecompressionStream; std::unique_ptr m_clientSubspaceForWritableStream; std::unique_ptr m_clientSubspaceForWritableStreamDefaultController; std::unique_ptr m_clientSubspaceForWritableStreamDefaultWriter; diff --git a/src/bun.js/bindings/webcore/DOMConstructors.h b/src/bun.js/bindings/webcore/DOMConstructors.h index d83ef6cfe3..78b38e131a 100644 --- a/src/bun.js/bindings/webcore/DOMConstructors.h +++ b/src/bun.js/bindings/webcore/DOMConstructors.h @@ -198,6 +198,8 @@ enum class DOMConstructorID : uint16_t { ReadableStreamSource, TransformStream, TransformStreamDefaultController, + CompressionStream, + DecompressionStream, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter, @@ -860,7 +862,7 @@ enum class DOMConstructorID : uint16_t { EventEmitter, }; -static constexpr unsigned numberOfDOMConstructorsBase = 846; +static constexpr unsigned numberOfDOMConstructorsBase = 848; static constexpr unsigned bunExtraConstructors = 3; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index cce908c751..049b161b8f 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -266,6 +266,8 @@ public: std::unique_ptr m_subspaceForReadableStreamSource; std::unique_ptr m_subspaceForTransformStream; std::unique_ptr m_subspaceForTransformStreamDefaultController; + std::unique_ptr m_subspaceForCompressionStream; + std::unique_ptr m_subspaceForDecompressionStream; std::unique_ptr m_subspaceForWritableStream; std::unique_ptr m_subspaceForWritableStreamDefaultController; std::unique_ptr m_subspaceForWritableStreamDefaultWriter; diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index f539171ef6..c282da4560 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -532,7 +532,7 @@ declare module "module" { `; for (const [name] of jsclasses) { - dts += `\ndeclare function $inherits${name}(value: any): boolean;`; + dts += `\ndeclare function $inherits${name}(value: any): value is ${name};`; } return dts; diff --git a/src/js/builtins/CompressionStream.ts b/src/js/builtins/CompressionStream.ts new file mode 100644 index 0000000000..a777b35a53 --- /dev/null +++ b/src/js/builtins/CompressionStream.ts @@ -0,0 +1,33 @@ +export function initializeCompressionStream(this, format) { + const zlib = require("node:zlib"); + const stream = require("node:stream"); + + const builders = { + "deflate": zlib.createDeflate, + "deflate-raw": zlib.createDeflateRaw, + "gzip": zlib.createGzip, + "brotli": zlib.createBrotliCompress, + "zstd": zlib.createZstdCompress, + }; + + if (!(format in builders)) + throw $ERR_INVALID_ARG_VALUE("format", format, "must be one of: " + Object.keys(builders).join(", ")); + + const handle = builders[format](); + $putByIdDirectPrivate(this, "readable", stream.Readable.toWeb(handle)); + $putByIdDirectPrivate(this, "writable", stream.Writable.toWeb(handle)); + + return this; +} + +$getter; +export function readable(this) { + if (!$inheritsCompressionStream(this)) throw $makeGetterTypeError("CompressionStream", "readable"); + return $getByIdDirectPrivate(this, "readable"); +} + +$getter; +export function writable(this) { + if (!$inheritsCompressionStream(this)) throw $makeGetterTypeError("CompressionStream", "writable"); + return $getByIdDirectPrivate(this, "writable"); +} diff --git a/src/js/builtins/DecompressionStream.ts b/src/js/builtins/DecompressionStream.ts new file mode 100644 index 0000000000..bf608d03fd --- /dev/null +++ b/src/js/builtins/DecompressionStream.ts @@ -0,0 +1,33 @@ +export function initializeDecompressionStream(this, format) { + const zlib = require("node:zlib"); + const stream = require("node:stream"); + + const builders = { + "deflate": zlib.createInflate, + "deflate-raw": zlib.createInflateRaw, + "gzip": zlib.createGunzip, + "brotli": zlib.createBrotliDecompress, + "zstd": zlib.createZstdDecompress, + }; + + if (!(format in builders)) + throw $ERR_INVALID_ARG_VALUE("format", format, "must be one of: " + Object.keys(builders).join(", ")); + + const handle = builders[format](); + $putByIdDirectPrivate(this, "readable", stream.Readable.toWeb(handle)); + $putByIdDirectPrivate(this, "writable", stream.Writable.toWeb(handle)); + + return this; +} + +$getter; +export function readable(this) { + if (!$inheritsDecompressionStream(this)) throw $makeGetterTypeError("DecompressionStream", "readable"); + return $getByIdDirectPrivate(this, "readable"); +} + +$getter; +export function writable(this) { + if (!$inheritsDecompressionStream(this)) throw $makeGetterTypeError("DecompressionStream", "writable"); + return $getByIdDirectPrivate(this, "writable"); +} diff --git a/src/js/node/stream.web.ts b/src/js/node/stream.web.ts index a0d92215bd..8de228b824 100644 --- a/src/js/node/stream.web.ts +++ b/src/js/node/stream.web.ts @@ -15,6 +15,6 @@ export default { CountQueuingStrategy, TextEncoderStream, TextDecoderStream, - CompressionStream: undefined, - DecompressionStream: undefined, + CompressionStream, + DecompressionStream, }; diff --git a/test/integration/bun-types/fixture/streams.ts b/test/integration/bun-types/fixture/streams.ts index ad64910184..f53338482a 100644 --- a/test/integration/bun-types/fixture/streams.ts +++ b/test/integration/bun-types/fixture/streams.ts @@ -59,6 +59,8 @@ expectType(node_stream.blob()).is>(); Bun.file("./foo.csv").stream().pipeThrough(new TextDecoderStream()).pipeThrough(new TextEncoderStream()); +Bun.file("./foo.csv").stream().pipeThrough(new CompressionStream("gzip")).pipeThrough(new DecompressionStream("gzip")); + Bun.file("./foo.csv") .stream() .pipeThrough(new TextDecoderStream()) diff --git a/test/js/node/test/parallel/test-global-webstreams.js b/test/js/node/test/parallel/test-global-webstreams.js new file mode 100644 index 0000000000..ab20e376b7 --- /dev/null +++ b/test/js/node/test/parallel/test-global-webstreams.js @@ -0,0 +1,24 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const webstreams = require('stream/web'); + +assert.strictEqual(ReadableStream, webstreams.ReadableStream); +assert.strictEqual(ReadableStreamDefaultReader, webstreams.ReadableStreamDefaultReader); +assert.strictEqual(ReadableStreamBYOBReader, webstreams.ReadableStreamBYOBReader); +assert.strictEqual(ReadableStreamBYOBRequest, webstreams.ReadableStreamBYOBRequest); +assert.strictEqual(ReadableByteStreamController, webstreams.ReadableByteStreamController); +assert.strictEqual(ReadableStreamDefaultController, webstreams.ReadableStreamDefaultController); +assert.strictEqual(TransformStream, webstreams.TransformStream); +assert.strictEqual(TransformStreamDefaultController, webstreams.TransformStreamDefaultController); +assert.strictEqual(WritableStream, webstreams.WritableStream); +assert.strictEqual(WritableStreamDefaultWriter, webstreams.WritableStreamDefaultWriter); +assert.strictEqual(WritableStreamDefaultController, webstreams.WritableStreamDefaultController); +assert.strictEqual(ByteLengthQueuingStrategy, webstreams.ByteLengthQueuingStrategy); +assert.strictEqual(CountQueuingStrategy, webstreams.CountQueuingStrategy); +assert.strictEqual(TextEncoderStream, webstreams.TextEncoderStream); +assert.strictEqual(TextDecoderStream, webstreams.TextDecoderStream); +assert.strictEqual(CompressionStream, webstreams.CompressionStream); +assert.strictEqual(DecompressionStream, webstreams.DecompressionStream); diff --git a/test/js/node/test/parallel/test-whatwg-webstreams-compression.js b/test/js/node/test/parallel/test-whatwg-webstreams-compression.js new file mode 100644 index 0000000000..a6f2e1b425 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-webstreams-compression.js @@ -0,0 +1,70 @@ +// Flags: --no-warnings +'use strict'; + +const common = require('../common'); + +const { + CompressionStream, + DecompressionStream, +} = require('stream/web'); + +const assert = require('assert'); +const dec = new TextDecoder(); + +async function test(format) { + const gzip = new CompressionStream(format); + const gunzip = new DecompressionStream(format); + + assert.strictEqual(gzip[Symbol.toStringTag], 'CompressionStream'); + assert.strictEqual(gunzip[Symbol.toStringTag], 'DecompressionStream'); + + gzip.readable.pipeTo(gunzip.writable).then(common.mustCall()); + + const reader = gunzip.readable.getReader(); + const writer = gzip.writable.getWriter(); + + const compressed_data = []; + const reader_function = ({ value, done }) => { + if (value) + compressed_data.push(value); + if (!done) + return reader.read().then(reader_function); + assert.strictEqual(dec.decode(Buffer.concat(compressed_data)), 'hello'); + }; + const reader_promise = reader.read().then(reader_function); + + await Promise.all([ + reader_promise, + reader_promise.then(() => reader.read().then(({ done }) => assert(done))), + writer.write('hello'), + writer.close(), + ]); +} + +Promise.all(['gzip', 'deflate', 'deflate-raw', 'brotli', 'zstd'].map((i) => test(i))).then(common.mustCall()); + +[1, 'hello', false, {}].forEach((i) => { + assert.throws(() => new CompressionStream(i), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.throws(() => new DecompressionStream(i), { + code: 'ERR_INVALID_ARG_VALUE', + }); +}); + +assert.throws( + () => Reflect.get(CompressionStream.prototype, 'readable', {}), { + name: 'TypeError', + }); +assert.throws( + () => Reflect.get(CompressionStream.prototype, 'writable', {}), { + name: 'TypeError', + }); +assert.throws( + () => Reflect.get(DecompressionStream.prototype, 'readable', {}), { + name: 'TypeError', + }); +assert.throws( + () => Reflect.get(DecompressionStream.prototype, 'writable', {}), { + name: 'TypeError', + });