Skip to content

encoders

Eugene Lazutkin edited this page Jul 10, 2026 · 3 revisions

Compression encoders

Request-side body compression: compress: 'gzip' (or true) compresses the outgoing body and sets Content-Encoding. The response side needs nothing — fetch decompresses gzip/br/zstd responses natively on every platform, and the browser owns Accept-Encoding.

import io from 'double-meh';

await io.post('/bulk', bigPayload, {compress: 'gzip'}); // Content-Encoding: gzip
await io.put('/doc/42', doc, {compress: true}); // true = gzip

The registry

io.encoders maps an encoder name to a function (source: ReadableStream, options) => ReadableStream (async allowed). Out of the box it holds gzip and deflate, built on the standard CompressionStream — available everywhere, browsers included. Add your own by assignment:

io.encoders.identity = source => source; // a (pointless) custom encoder
await io.post('/x', data, {compress: 'identity'});

An unknown name throws a TypeError naming the encoder.

br and zstd (CLI)

Brotli and zstandard compressors live in node:zlib, so they ship as an opt-in module — keeping node: references out of browser bundles (the same pattern as the SQLite storage backend):

import io from 'double-meh';
import {installZlibEncoders} from 'double-meh/encoders/zlib.js';

await installZlibEncoders(io); // registers br; zstd where the runtime ships it
await io.post('/bulk', bigPayload, {compress: 'br'});

Works on Node, Bun, and Deno. br always registers (brotli quality defaults to 5 — the on-the-fly convention; the zlib default of 11 is for static assets). zstd is feature-detected (zlib.createZstdCompress) and simply absent where the runtime lacks it.

Parameterizing encoders

Compression parameters are set at registration, not per request — pick a policy once, name it, and select it by name at call sites. Two mechanisms, both riding node:zlib options verbatim:

// 1. configure the default registrations
await installZlibEncoders(io, {br: {quality: 7}, zstd: {level: 10}});

// 2. factories — register named variants for distinct policies
import {brotliEncoder, gzipEncoder} from 'double-meh/encoders/zlib.js';

io.encoders.brMax = await brotliEncoder({quality: 11}); // static-asset grade
io.encoders.gzipFast = await gzipEncoder({level: 1}); // cheap telemetry uploads
await io.post('/telemetry', batch, {compress: 'gzipFast'});

Factories: gzipEncoder(options), deflateEncoder(options) (standard ZlibOptionslevel, memLevel, strategy, …), brotliEncoder(options) (quality is sugar for params[BROTLI_PARAM_QUALITY]; explicit params win), zstdEncoder(options) (level is sugar for params[ZSTD_c_compressionLevel]; throws where the runtime ships no zstd).

Honoring is best-effort by construction: the platform CompressionStream pair has no knobs at all (the web standard takes only the format name), so the out-of-the-box gzip/deflate ignore nothing — they simply can't be parameterized. Passing gzip/deflate options to installZlibEncoders replaces them with parameterized node:zlib versions (CLI runtimes only).

The inline escape hatch

compress also accepts an encoder function directly — (source, options) => stream — mirroring decode: fn. It runs one-off, without touching the registry, and since there is no name, no Content-Encoding is set automatically: the caller owns the header.

await io.post('/bulk', bigPayload, {
  compress: source => source.pipeThrough(new CompressionStream('gzip')),
  headers: {'Content-Encoding': 'gzip'}
});

Prefer named registry variants for anything used twice — configure at setup, select at call site.

Body handling rules

  • A buffered body (a string, bytes, a Blob, a JSON-encoded object) is compressed to completion and sent as bytes — Content-Length stays known, and it works over HTTP/1.1 everywhere.
  • A stream body ({readable} chains, io.stream duplexes) is compressed streaming — bytes flow through the encoder as they are produced.
  • A bodyless request (GET et al.) ignores compress entirely.
  • FormData / URLSearchParams bodies throw: their wire encoding (multipart boundaries, form encoding) belongs to the transport, and pre-consuming them would corrupt it.

Compression runs before user-registered request inspectors, so a body-signing inspector (say SigV4) sees the final compressed bytes.

The server must accept compressed requests — Content-Encoding on a request is a contract with your API, not something HTTP negotiates automatically.

See also

Clone this wiki locally