WHATWG Compression Streams
CompressionStream and DecompressionStream are TransformStream-shaped pairs that compress or
decompress chunks of BufferSource input as they flow through. The implementation tracks the
WHATWG Compression Streams Standard and is wired on top of
the same incremental codecs that back elide:compression.
Status
Implemented in Elide 1.2+, gated on a built-in deflate / gzip backend; Brotli ("brotli") and
Zstandard ("zstd") require the native compression library to be loaded (always present in the
shipped binary, available under ./builder run --jvm as well). Both are non-standard extensions to
the WHATWG format set.
CompressionStream
declare class CompressionStream {
constructor(format: "gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd");
readonly readable: ReadableStream<Uint8Array>;
readonly writable: WritableStream<BufferSource>;
}Construction parses the format argument with byte-exact string comparison — uppercase, trailing
whitespace, or any unrecognised value raises TypeError. The five recognised tokens map to:
"gzip"— RFC 1952 gzip stream (10-byte header, deflate body, 8-byte trailer)."deflate"— RFC 1950 zlib stream (2-byte header, deflate body, Adler-32 trailer)."deflate-raw"— raw deflate per RFC 1951, no wrapper."brotli"— RFC 7932 Brotli."zstd"— RFC 8878 Zstandard.
The internal compressor uses Z_NO_FLUSH between chunks (matching browser implementations) — a
single short input may not produce any output until the writable side is closed and the flush
algorithm emits the trailer. Chunks written to writable that are not BufferSource (ArrayBuffer,
typed arrays, DataView) cause writable.getWriter().write(...) to reject with TypeError.
DecompressionStream
declare class DecompressionStream {
constructor(format: "gzip" | "deflate" | "deflate-raw" | "brotli" | "zstd");
readonly readable: ReadableStream<Uint8Array>;
readonly writable: WritableStream<BufferSource>;
}The inverse direction. Malformed compressed input (truncated header, corrupted body) surfaces as a
TypeError rejection on readable.getReader().read(). Trailing un-consumed compressed bytes after
the format's end-of-stream marker are tolerated only if the input has been closed at exactly that
boundary; closing the writable side before the decoder has reached end-of-stream raises a
TypeError (Compression Streams Standard §3.2 step 5).
Decompression-bomb defence
A DecompressionStream enforces two parallel caps per stream:
- An absolute output cap of 1 GiB of decompressed bytes (
MAX_DECOMPRESSED_BYTES). - An expansion-ratio cap of 1024× of decompressed-bytes over compressed-bytes (
MAX_DECOMPRESSION_RATIO), evaluated only after at least 1 KiB of compressed input has been consumed (so a fixed-size header is not flagged as a bomb on its own).
When either cap is exceeded, the next read() rejects with TypeError. The caps prevent a
classic decompression-bomb attack — e.g. 1 KiB of compressed all-zeroes deflate expanding to
gigabytes — without rejecting legitimate text payloads (typical text peaks around 20:1).
Round-trip example
async function pumpThrough(stream, bytes) {
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
const chunks = [];
const drain = (async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(value);
}
})();
const writes = (async () => {
await writer.write(bytes);
await writer.close();
})();
await Promise.all([drain, writes]);
let total = 0;
for (const c of chunks) total += c.byteLength;
const out = new Uint8Array(total);
let off = 0;
for (const c of chunks) { out.set(c, off); off += c.byteLength; }
return out;
}
const source = new TextEncoder().encode("hello, compression streams!");
const compressed = await pumpThrough(new CompressionStream("gzip"), source);
const restored = await pumpThrough(new DecompressionStream("gzip"), compressed);
// `restored` byte-equals `source`.