From c5eff075b4b31818b5f23a855be3ab415fc7b21b Mon Sep 17 00:00:00 2001 From: kylekz Date: Sun, 7 Dec 2025 01:31:47 +1300 Subject: [PATCH 1/4] add brotli and zstd to CompressionFormat type --- packages/bun-types/globals.d.ts | 30 +++++++++---------- test/integration/bun-types/bun-types.test.ts | 20 +++++++++++++ test/integration/bun-types/fixture/streams.ts | 2 ++ 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index f2d211d5ff38..0bf1cc986da1 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -14,6 +14,12 @@ declare module "bun" { ? {} : import("node:stream/web").DecompressionStream; + /** + * Compression formats supported by Bun's CompressionStream and DecompressionStream. + * Bun extends the standard web formats with brotli and zstd support. + */ + type CompressionFormat = "gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd"; + type LibPerformanceOrNodePerfHooksPerformance = LibDomIsLoaded extends true ? {} : import("perf_hooks").Performance; type LibEmptyOrPerformanceEntry = LibDomIsLoaded extends true ? {} : import("node:perf_hooks").PerformanceEntry; type LibEmptyOrPerformanceMark = LibDomIsLoaded extends true ? {} : import("node:perf_hooks").PerformanceMark; @@ -278,28 +284,22 @@ declare var Event: { new (type: string, eventInitDict?: Bun.EventInit): Event; }; -/** - * Unimplemented in Bun - */ interface CompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebCompressionStream {} -/** - * Unimplemented in Bun - */ declare var CompressionStream: Bun.__internal.UseLibDomIfAvailable< "CompressionStream", - typeof import("node:stream/web").CompressionStream + { + prototype: CompressionStream; + new (format: Bun.__internal.CompressionFormat): CompressionStream; + } >; -/** - * Unimplemented in Bun - */ -interface DecompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebCompressionStream {} -/** - * Unimplemented in Bun - */ +interface DecompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebDecompressionStream {} declare var DecompressionStream: Bun.__internal.UseLibDomIfAvailable< "DecompressionStream", - typeof import("node:stream/web").DecompressionStream + { + prototype: DecompressionStream; + new (format: Bun.__internal.CompressionFormat): DecompressionStream; + } >; interface EventTarget { diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index a48d0c17b8b9..47a3a1f66ead 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -525,6 +525,26 @@ describe("@types/bun integration test", () => { "line": "streams.ts:49:19", "message": "Property 'blob' does not exist on type 'ReadableStream>'.", }, + { + code: 2345, + line: "streams.ts:63:66", + message: "Argument of type '\"brotli\"' is not assignable to parameter of type 'CompressionFormat'.", + }, + { + code: 2345, + line: "streams.ts:63:113", + message: "Argument of type '\"brotli\"' is not assignable to parameter of type 'CompressionFormat'.", + }, + { + code: 2345, + line: "streams.ts:64:66", + message: "Argument of type '\"zstd\"' is not assignable to parameter of type 'CompressionFormat'.", + }, + { + code: 2345, + line: "streams.ts:64:111", + message: "Argument of type '\"zstd\"' is not assignable to parameter of type 'CompressionFormat'.", + }, { code: 2353, line: "websocket.ts:25:5", diff --git a/test/integration/bun-types/fixture/streams.ts b/test/integration/bun-types/fixture/streams.ts index f53338482abe..a1e5aacf045b 100644 --- a/test/integration/bun-types/fixture/streams.ts +++ b/test/integration/bun-types/fixture/streams.ts @@ -60,6 +60,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 CompressionStream("brotli")).pipeThrough(new DecompressionStream("brotli")); +Bun.file("./foo.csv").stream().pipeThrough(new CompressionStream("zstd")).pipeThrough(new DecompressionStream("zstd")); Bun.file("./foo.csv") .stream() From 2b23b5b4bd54169536efa1c790997cec3007c507 Mon Sep 17 00:00:00 2001 From: kylekz Date: Sun, 7 Dec 2025 11:02:21 +1300 Subject: [PATCH 2/4] move CompressionFormat out of Bun.__internal --- packages/bun-types/globals.d.ts | 48 ++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 0bf1cc986da1..8906367edd23 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -14,12 +14,6 @@ declare module "bun" { ? {} : import("node:stream/web").DecompressionStream; - /** - * Compression formats supported by Bun's CompressionStream and DecompressionStream. - * Bun extends the standard web formats with brotli and zstd support. - */ - type CompressionFormat = "gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd"; - type LibPerformanceOrNodePerfHooksPerformance = LibDomIsLoaded extends true ? {} : import("perf_hooks").Performance; type LibEmptyOrPerformanceEntry = LibDomIsLoaded extends true ? {} : import("node:perf_hooks").PerformanceEntry; type LibEmptyOrPerformanceMark = LibDomIsLoaded extends true ? {} : import("node:perf_hooks").PerformanceMark; @@ -89,6 +83,30 @@ declare var WritableStream: Bun.__internal.UseLibDomIfAvailable< } >; +/** + * Compression formats supported by Bun's CompressionStream and DecompressionStream. + * Bun extends the standard web formats with brotli and zstd support. + */ +type CompressionFormat = "gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd"; + +interface CompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebCompressionStream {} +declare var CompressionStream: Bun.__internal.UseLibDomIfAvailable< + "CompressionStream", + { + prototype: CompressionStream; + new (format: CompressionFormat): CompressionStream; + } +>; + +interface DecompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebDecompressionStream {} +declare var DecompressionStream: Bun.__internal.UseLibDomIfAvailable< + "DecompressionStream", + { + prototype: DecompressionStream; + new (format: CompressionFormat): DecompressionStream; + } +>; + interface Worker extends Bun.__internal.LibWorkerOrBunWorker {} declare var Worker: Bun.__internal.UseLibDomIfAvailable< "Worker", @@ -284,24 +302,6 @@ declare var Event: { new (type: string, eventInitDict?: Bun.EventInit): Event; }; -interface CompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebCompressionStream {} -declare var CompressionStream: Bun.__internal.UseLibDomIfAvailable< - "CompressionStream", - { - prototype: CompressionStream; - new (format: Bun.__internal.CompressionFormat): CompressionStream; - } ->; - -interface DecompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebDecompressionStream {} -declare var DecompressionStream: Bun.__internal.UseLibDomIfAvailable< - "DecompressionStream", - { - prototype: DecompressionStream; - new (format: Bun.__internal.CompressionFormat): DecompressionStream; - } ->; - interface EventTarget { /** * Adds a new handler for the `type` event. Any given `listener` is added only once per `type` and per `capture` option value. From 33029ad05f800c96324bf73961665b129b549b57 Mon Sep 17 00:00:00 2001 From: kylekz Date: Sun, 7 Dec 2025 13:10:27 +1300 Subject: [PATCH 3/4] add compression tests --- test/js/web/streams/compression.test.ts | 251 ++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 test/js/web/streams/compression.test.ts diff --git a/test/js/web/streams/compression.test.ts b/test/js/web/streams/compression.test.ts new file mode 100644 index 000000000000..63fae21bf4a6 --- /dev/null +++ b/test/js/web/streams/compression.test.ts @@ -0,0 +1,251 @@ +import { describe, expect, test } from "bun:test"; + +describe("CompressionStream and DecompressionStream", () => { + describe("brotli", () => { + test("compresses data with brotli", async () => { + const input = "Hello, Bun! This is a test string for brotli compression."; + const encoder = new TextEncoder(); + const data = encoder.encode(input); + + const compressionStream = new CompressionStream("brotli"); + const writer = compressionStream.writable.getWriter(); + writer.write(data); + writer.close(); + + const compressedChunks: Uint8Array[] = []; + const reader = compressionStream.readable.getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + compressedChunks.push(value); + } + + expect(compressedChunks.length).toBeGreaterThan(0); + const totalLength = compressedChunks.reduce((acc, chunk) => acc + chunk.length, 0); + expect(totalLength).toBeGreaterThan(0); + }); + + test("decompresses brotli data", async () => { + const input = "Hello, Bun! This is a test string for brotli decompression."; + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const data = encoder.encode(input); + + // First compress + const compressionStream = new CompressionStream("brotli"); + const writer = compressionStream.writable.getWriter(); + writer.write(data); + writer.close(); + + const compressedChunks: Uint8Array[] = []; + const reader = compressionStream.readable.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + compressedChunks.push(value); + } + + // Concatenate compressed chunks + const totalLength = compressedChunks.reduce((acc, chunk) => acc + chunk.length, 0); + const compressed = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of compressedChunks) { + compressed.set(chunk, offset); + offset += chunk.length; + } + + // Then decompress + const decompressionStream = new DecompressionStream("brotli"); + const decompWriter = decompressionStream.writable.getWriter(); + decompWriter.write(compressed); + decompWriter.close(); + + const decompressedChunks: Uint8Array[] = []; + const decompReader = decompressionStream.readable.getReader(); + while (true) { + const { done, value } = await decompReader.read(); + if (done) break; + decompressedChunks.push(value); + } + + const decompressedLength = decompressedChunks.reduce((acc, chunk) => acc + chunk.length, 0); + const decompressed = new Uint8Array(decompressedLength); + offset = 0; + for (const chunk of decompressedChunks) { + decompressed.set(chunk, offset); + offset += chunk.length; + } + + const output = decoder.decode(decompressed); + expect(output).toBe(input); + }); + + test("round-trip compression with brotli", async () => { + const testData = [ + "Simple string", + Buffer.alloc(1000, "A").toString(), + "Mixed 123 !@# symbols", + "", + JSON.stringify({ nested: { object: "value" } }), + ]; + + for (const input of testData) { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const data = encoder.encode(input); + + // Compress and decompress + const compressed = await new Response( + new Blob([data]).stream().pipeThrough(new CompressionStream("brotli")), + ).arrayBuffer(); + + const decompressed = await new Response( + new Blob([compressed]).stream().pipeThrough(new DecompressionStream("brotli")), + ).arrayBuffer(); + + const output = decoder.decode(decompressed); + expect(output).toBe(input); + } + }); + }); + + describe("zstd", () => { + test("compresses data with zstd", async () => { + const input = "Hello, Bun! This is a test string for zstd compression."; + const encoder = new TextEncoder(); + const data = encoder.encode(input); + + const compressionStream = new CompressionStream("zstd"); + const writer = compressionStream.writable.getWriter(); + writer.write(data); + writer.close(); + + const compressedChunks: Uint8Array[] = []; + const reader = compressionStream.readable.getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + compressedChunks.push(value); + } + + expect(compressedChunks.length).toBeGreaterThan(0); + const totalLength = compressedChunks.reduce((acc, chunk) => acc + chunk.length, 0); + expect(totalLength).toBeGreaterThan(0); + }); + + test("decompresses zstd data", async () => { + const input = "Hello, Bun! This is a test string for zstd decompression."; + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const data = encoder.encode(input); + + // First compress + const compressionStream = new CompressionStream("zstd"); + const writer = compressionStream.writable.getWriter(); + writer.write(data); + writer.close(); + + const compressedChunks: Uint8Array[] = []; + const reader = compressionStream.readable.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + compressedChunks.push(value); + } + + // Concatenate compressed chunks + const totalLength = compressedChunks.reduce((acc, chunk) => acc + chunk.length, 0); + const compressed = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of compressedChunks) { + compressed.set(chunk, offset); + offset += chunk.length; + } + + // Then decompress + const decompressionStream = new DecompressionStream("zstd"); + const decompWriter = decompressionStream.writable.getWriter(); + decompWriter.write(compressed); + decompWriter.close(); + + const decompressedChunks: Uint8Array[] = []; + const decompReader = decompressionStream.readable.getReader(); + while (true) { + const { done, value } = await decompReader.read(); + if (done) break; + decompressedChunks.push(value); + } + + const decompressedLength = decompressedChunks.reduce((acc, chunk) => acc + chunk.length, 0); + const decompressed = new Uint8Array(decompressedLength); + offset = 0; + for (const chunk of decompressedChunks) { + decompressed.set(chunk, offset); + offset += chunk.length; + } + + const output = decoder.decode(decompressed); + expect(output).toBe(input); + }); + + test("round-trip compression with zstd", async () => { + const testData = [ + "Simple string", + Buffer.alloc(1000, "A").toString(), + "Mixed 123 !@# symbols", + "", + JSON.stringify({ nested: { object: "value" } }), + ]; + + for (const input of testData) { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const data = encoder.encode(input); + + // Compress and decompress + const compressed = await new Response( + new Blob([data]).stream().pipeThrough(new CompressionStream("zstd")), + ).arrayBuffer(); + + const decompressed = await new Response( + new Blob([compressed]).stream().pipeThrough(new DecompressionStream("zstd")), + ).arrayBuffer(); + + const output = decoder.decode(decompressed); + expect(output).toBe(input); + } + }); + }); + + describe("all formats", () => { + test("works with all compression formats", async () => { + const formats: Array<"gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd"> = [ + "gzip", + "deflate", + "deflate-raw", + "brotli", + "zstd", + ]; + + const input = "Test data for all compression formats!"; + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const data = encoder.encode(input); + + for (const format of formats) { + const compressed = await new Response( + new Blob([data]).stream().pipeThrough(new CompressionStream(format)), + ).arrayBuffer(); + + const decompressed = await new Response( + new Blob([compressed]).stream().pipeThrough(new DecompressionStream(format)), + ).arrayBuffer(); + + const output = decoder.decode(decompressed); + expect(output).toBe(input); + } + }); + }); +}); From 519d98143d72902866b49c7b20f2e170c0d23149 Mon Sep 17 00:00:00 2001 From: kylekz Date: Tue, 9 Dec 2025 14:37:19 +1300 Subject: [PATCH 4/4] move CompressionFormat into bun namespace --- packages/bun-types/bun.d.ts | 3 +++ packages/bun-types/globals.d.ts | 10 ++-------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d510becbda55..0039515dde81 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3887,6 +3887,9 @@ declare module "bun" { static readonly byteLength: 32; } + /** Extends the standard web formats with `brotli` and `zstd` support. */ + type CompressionFormat = "gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd"; + /** Compression options for `Bun.deflateSync` and `Bun.gzipSync` */ interface ZlibCompressionOptions { /** diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 8906367edd23..b2c4b7d5f2ed 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -83,18 +83,12 @@ declare var WritableStream: Bun.__internal.UseLibDomIfAvailable< } >; -/** - * Compression formats supported by Bun's CompressionStream and DecompressionStream. - * Bun extends the standard web formats with brotli and zstd support. - */ -type CompressionFormat = "gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd"; - interface CompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebCompressionStream {} declare var CompressionStream: Bun.__internal.UseLibDomIfAvailable< "CompressionStream", { prototype: CompressionStream; - new (format: CompressionFormat): CompressionStream; + new (format: Bun.CompressionFormat): CompressionStream; } >; @@ -103,7 +97,7 @@ declare var DecompressionStream: Bun.__internal.UseLibDomIfAvailable< "DecompressionStream", { prototype: DecompressionStream; - new (format: CompressionFormat): DecompressionStream; + new (format: Bun.CompressionFormat): DecompressionStream; } >;