Skip to content

Instantly share code, notes, and snippets.

@aabccd021
Created October 30, 2024 01:26
Show Gist options
  • Save aabccd021/2c244a8700910255180207b415127903 to your computer and use it in GitHub Desktop.
Save aabccd021/2c244a8700910255180207b415127903 to your computer and use it in GitHub Desktop.
Bun compression middleware
import zlib from "node:zlib";
import type { Transform } from "stream";
import mimeDb from "mime-db";
function getQValue(qValue: string | undefined): number {
if (qValue === undefined) {
return 1;
}
const [_key, value] = qValue.split("=");
if (value === undefined) {
return 1;
}
return parseFloat(value);
}
const defaultTransforms = {
gzip: (): Transform => zlib.createGzip(),
br: (): Transform => zlib.createBrotliCompress(),
deflate: (): Transform => zlib.createDeflate(),
} satisfies CompressionMethods;
const defaultCompression: Compression<typeof defaultTransforms> = {
transforms: defaultTransforms,
priority: ["deflate", "br", "gzip"],
};
interface Encoding {
readonly name: string;
readonly qValue: number;
}
function parseEncoding(encoding: string): Encoding | undefined {
const [name, qValue] = encoding
.trim()
.split(";")
.map((s) => s.trim());
if (name === undefined) {
return undefined;
}
return { name, qValue: getQValue(qValue) };
}
function encodingByPriority(
compressionPriority: readonly string[],
a: Encoding,
b: Encoding,
): number {
const qDiff = b.qValue - a.qValue;
if (qDiff !== 0) {
return qDiff;
}
const bIndex = compressionPriority.indexOf(b.name);
const aIndex = compressionPriority.indexOf(a.name);
return bIndex - aIndex;
}
function buildCompressionPipe(transform: Transform): ReadableWritablePair {
const readable = new ReadableStream({
start(controller: ReadableStreamDefaultController<Uint8Array>): void {
transform.on("data", (chunk: unknown) => {
if (!(chunk instanceof Uint8Array) && typeof chunk !== "undefined") {
throw new Error("Unexpected chunk type");
}
controller.enqueue(chunk);
});
transform.once("end", () => {
controller.close();
});
},
});
const writable = new WritableStream({
write: (chunk: unknown): void => {
transform.write(chunk);
},
close: (): void => {
transform.end();
},
});
return { readable, writable };
}
type RequestHandler = (req: Request) => Promise<Response>;
type CompressionMethods = Record<string, () => Transform>;
interface Compression<T extends CompressionMethods = CompressionMethods> {
readonly transforms: T;
// Righter value has higher priority (1)
// Lefter value has lower priority (0)
// Value not in the list will be considered as lowest priority (-1)
readonly priority: readonly (keyof T)[];
}
type Middleware = (handle: RequestHandler) => RequestHandler;
function shouldTransform(cacheControl: string | null): boolean {
if (cacheControl === null) {
return true;
}
const noTransform = cacheControl
.split(",")
.some((directive) => directive.trim() === "no-transform");
return !noTransform;
}
export function makeMiddleware(option: {
readonly compression?: Compression;
}): Middleware {
return (handle) => async (req) => {
const response = await handle(req);
if (response.body === null) {
return response;
}
const acceptEncs = req.headers.get("Accept-Encoding");
if (acceptEncs === null) {
return response;
}
const mime = response.headers.get("Content-Type")?.split(";")[0];
if (mime === undefined) {
return response;
}
// Don't compress for Cache-Control: no-transform
// https://tools.ietf.org/html/rfc7234#section-5.2.2.4
const cacheControl = response.headers.get("Cache-Control");
if (!shouldTransform(cacheControl)) {
return response;
}
// Make sure text/event-stream is not compressed
if (mimeDb[mime]?.compressible !== true) {
return response;
}
const alreadyCompressed = response.headers.get("Content-Encoding") !== null;
if (alreadyCompressed) {
return response;
}
const compressionTransforms: Record<string, () => Transform> =
option.compression?.transforms ?? defaultCompression.transforms;
const supportedEncs = Object.keys(compressionTransforms);
const compressionPriority =
option.compression?.priority ?? defaultCompression.priority;
const [preferredEncoding] = acceptEncs
.split(",")
.map(parseEncoding)
.filter((enc): enc is Encoding => enc !== undefined)
.filter((enc) => supportedEncs.includes(enc.name))
.toSorted((a, b) => encodingByPriority(compressionPriority, a, b));
if (preferredEncoding === undefined) {
return response;
}
const transform = compressionTransforms[preferredEncoding.name];
if (transform === undefined) {
return response;
}
const compresionPipe = buildCompressionPipe(transform());
const compressed = response.body.pipeThrough(compresionPipe);
const newResponse = new Response(compressed, response);
newResponse.headers.set("Content-Encoding", preferredEncoding.name);
newResponse.headers.set("Vary", "Accept-Encoding");
return newResponse;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment