Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down
42 changes: 18 additions & 24 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ declare var WritableStream: Bun.__internal.UseLibDomIfAvailable<
}
>;

interface CompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebCompressionStream {}
declare var CompressionStream: Bun.__internal.UseLibDomIfAvailable<
"CompressionStream",
{
prototype: CompressionStream;
new (format: Bun.CompressionFormat): CompressionStream;
}
>;

interface DecompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebDecompressionStream {}
declare var DecompressionStream: Bun.__internal.UseLibDomIfAvailable<
"DecompressionStream",
{
prototype: DecompressionStream;
new (format: Bun.CompressionFormat): DecompressionStream;
}
>;
Comment thread
alii marked this conversation as resolved.

interface Worker extends Bun.__internal.LibWorkerOrBunWorker {}
declare var Worker: Bun.__internal.UseLibDomIfAvailable<
"Worker",
Expand Down Expand Up @@ -278,30 +296,6 @@ 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
>;

/**
* Unimplemented in Bun
*/
interface DecompressionStream extends Bun.__internal.LibEmptyOrNodeStreamWebCompressionStream {}
/**
* Unimplemented in Bun
*/
declare var DecompressionStream: Bun.__internal.UseLibDomIfAvailable<
"DecompressionStream",
typeof import("node:stream/web").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.
Expand Down
20 changes: 20 additions & 0 deletions test/integration/bun-types/bun-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,26 @@ describe("@types/bun integration test", () => {
"line": "streams.ts:49:19",
"message": "Property 'blob' does not exist on type 'ReadableStream<Uint8Array<ArrayBufferLike>>'.",
},
{
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'.",
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
code: 2353,
line: "websocket.ts:25:5",
Expand Down
2 changes: 2 additions & 0 deletions test/integration/bun-types/fixture/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ expectType(node_stream.blob()).is<Promise<Blob>>();
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()
Expand Down
251 changes: 251 additions & 0 deletions test/js/web/streams/compression.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
Comment thread
alii marked this conversation as resolved.

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);
}
});
});
Comment on lines +222 to +250

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Good comprehensive format coverage.

The test validates all compression formats work correctly. Since the PR adds CompressionFormat as a global type, consider using it instead of the inline union:

-    const formats: Array<"gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd"> = [
+    const formats: CompressionFormat[] = [
       "gzip",
       "deflate",
       "deflate-raw",
       "brotli",
       "zstd",
     ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
});
});
describe("all formats", () => {
test("works with all compression formats", async () => {
const formats: CompressionFormat[] = [
"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);
}
});
});
🤖 Prompt for AI Agents
In test/js/web/streams/compression.test.ts around lines 222 to 250, replace the
inline union type Array<"gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd">
with the global CompressionFormat type; update the variable declaration to use
Array<CompressionFormat> (or CompressionFormat[]) and keep the rest of the test
unchanged so it leverages the new global type while preserving runtime behavior.

});