compression-streams

js/globals/compression-streams.ts

CompressionStream and DecompressionStream globals (WHATWG Compression Streams).

Both classes are installed on globalThis, so application code uses them without importing anything. Each instance is a transform pair — a writable side that accepts raw bytes and a readable side that emits the transformed bytes — designed to be dropped into a stream pipeline with pipeThrough().

Supported formats are the three the spec defines — gzip, deflate (zlib-wrapped DEFLATE), and deflate-raw (bare DEFLATE with no wrapper) — plus brotli as a runtime extension when the system Brotli library is available. Constructing a stream with 'brotli' on a machine without the backend throws; portable code should check brotliAvailable from fino:compress before selecting it.

DecompressionStream mirrors the web API and does not expose a maximum output size option. Consumers that handle untrusted compressed input should read the decompressed stream with an application byte budget and cancel it when the budget is exceeded.

Bridging the two streaming APIs

All actual compression work lives in fino:compress, whose streaming shape is factory-based: createCompressor(opts) returns an object with transform(asyncIterable) → asyncIterable. That API does not speak Web Streams. To bridge:

1. A simple async-iterable input channel is created. 2. The compression factory is connected to it: factory().transform(channel). 3. The output iterable is wrapped in ReadableStream.from(). 4. A WritableStream sink feeds incoming writes into the channel.

This keeps all the actual compression work in the existing FFI-backed generators and avoids duplicating the zlib/brotli logic here.

Example

const source = new Blob(['hello']).stream();
const compressed = source.pipeThrough(new CompressionStream('gzip'));
const restored = compressed.pipeThrough(new DecompressionStream('gzip'));

const text = await new Response(restored).text();
console.log(text); // 'hello'

WHATWG Compression Streams specification: https://compression.spec.whatwg.org/

Classes

class CompressionStream {

Transforms a stream of bytes by compressing it using gzip, deflate, deflate-raw (raw DEFLATE without a wrapper), or Brotli.

Available as a global — no import required. The usual shape is source.pipeThrough(new CompressionStream(format)), which yields a compressed byte stream ready for further piping:

const cs = new CompressionStream('gzip');
readableSource.pipeThrough(cs);  // cs.readable yields compressed chunks

For buffered use, write chunks through writable and collect readable:

const cs = new CompressionStream('gzip');
const writer = cs.writable.getWriter();
await writer.write(new TextEncoder().encode('hello '.repeat(1000)));
await writer.close();
const compressed = await new Response(cs.readable).bytes();

Output is standard for each format: anything framed as gzip, deflate, or deflate-raw here is interchangeable with the one-shot compress()/decompress() functions in fino:compress and with any other conforming implementation.

Constructors

constructor(format: CompressionFormat)

Create a compression transform for the selected format.

The writable side accepts BufferSource chunks and the readable side emits compressed Uint8Array chunks.

Throws TypeError if the format is not one of the supported names. Throws if the format is 'brotli' and the system Brotli library is unavailable — portable code should check brotliAvailable from fino:compress first.

const gzip = new CompressionStream('gzip');
await new Blob(['hello']).stream().pipeTo(gzip.writable);

Getters

get readable()

Readable side that yields compressed bytes as Uint8Array chunks.

Chunks become available as the compressor produces them. The stream closes after the writable side is closed and all compressor output — including the final flush block — has been emitted.

const cs = new CompressionStream('deflate');
const compressed = cs.readable;
get writable()

Writable side that accepts uncompressed BufferSource chunks.

Writing anything other than an ArrayBuffer or ArrayBufferView rejects the write with TypeError. Closing this side completes the compression stream and flushes final bytes; aborting it errors the readable side with the abort reason.

const cs = new CompressionStream('gzip');
const writer = cs.writable.getWriter();
await writer.write(new Uint8Array([1, 2, 3]));
await writer.close();

class DecompressionStream {

Transforms a stream of compressed bytes (gzip, deflate, deflate-raw, or Brotli) into the original uncompressed data.

Available as a global — no import required. Corrupt or truncated input does not throw synchronously: the error surfaces on the readable side when the backend decompressor detects it, rejecting the pending read or pipeTo promise.

Matching the web API, there is no maximum output size option. When decompressing untrusted input, count bytes as you read and cancel the stream if an application budget is exceeded.

const ds = new DecompressionStream('gzip');
compressedReadable.pipeThrough(ds);  // ds.readable yields decompressed chunks

Decoding a fetched gzip payload to text:

const response = await fetch('https://example.com/logs.gz');
const restored = response.body.pipeThrough(new DecompressionStream('gzip'));
const text = await new Response(restored).text();

Constructors

constructor(format: CompressionFormat)

Create a decompression transform for the selected format.

The format must match how the input was actually compressed — the stream does not sniff; mismatched or invalid compressed input causes the readable side to error when the backend decompressor detects it.

Throws TypeError if the format is not one of the supported names. Throws if the format is 'brotli' and the system Brotli library is unavailable.

const gunzip = new DecompressionStream('gzip');
compressedReadable.pipeThrough(gunzip);

Getters

get readable()

Readable side that yields decompressed bytes as Uint8Array chunks.

Errors here if the compressed input is invalid for the selected format.

const ds = new DecompressionStream('deflate-raw');
const output = ds.readable;
get writable()

Writable side that accepts compressed BufferSource chunks.

Chunk boundaries do not matter — compressed input may be split anywhere. Close the writer to finish the decompressor and surface final output or truncation errors.

const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
await writer.write(compressedBytes);
await writer.close();