Skip to content

Commit

Permalink
feat: enable zlib compression when available
Browse files Browse the repository at this point in the history
  • Loading branch information
magne4000 authored Oct 16, 2024
1 parent 4592ba1 commit d2f6509
Show file tree
Hide file tree
Showing 10 changed files with 50 additions and 66 deletions.
3 changes: 0 additions & 3 deletions docs/middlewares/compress.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
> [!NOTE]
> **Cloudflare** middlewares are not generated, as Cloudflare compresses responses by default.
> [!NOTE]
> **Elysia** middleware does not compress right now as Bun lacks support for `CompressionStream`
## Usage

```ts twoslash
Expand Down
13 changes: 6 additions & 7 deletions packages/compress/src/compress.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { COMPRESSIBLE_CONTENT_TYPE_REGEX, SUPPORTED_ENCODINGS } from "./const";
import { COMPRESSIBLE_CONTENT_TYPE_REGEX } from "./const";
import { chooseBestEncoding } from "./encoding-header";
import type { CompressionOptions } from "./types";

type SupportedEncodings = (typeof SUPPORTED_ENCODINGS)[number];
import { supportedEncodings } from "./runtime";
import type { CompressionAlgorithm, CompressionOptions } from "./types";

const cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/i;

export class EncodingGuesser {
public readonly encoding: SupportedEncodings | null;
public readonly encoding: CompressionAlgorithm | null;

constructor(
private request: Request,
Expand All @@ -28,13 +27,13 @@ export class EncodingGuesser {
return null;
}

const chosenEncoding = chooseBestEncoding(this.request, SUPPORTED_ENCODINGS);
const chosenEncoding = chooseBestEncoding(this.request, supportedEncodings);

if (!chosenEncoding || chosenEncoding === "identity") {
return null;
}

return chosenEncoding as SupportedEncodings;
return chosenEncoding as CompressionAlgorithm;
}

guessEncoding(response: Response) {
Expand Down
6 changes: 0 additions & 6 deletions packages/compress/src/const.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
export const BROTLI = "br";
export const DEFLATE = "deflate";
export const GZIP = "gzip";

export const SUPPORTED_ENCODINGS = [/*BROTLI, */ GZIP, DEFLATE] as const;

/**
* Match for compressible content type.
*/
Expand Down
4 changes: 3 additions & 1 deletion packages/compress/src/encoding-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ function parseEncoding(str: string) {
export function chooseBestEncoding(request: Request, availableEncodings: readonly string[]) {
let bestEncoding: AcceptEncoding | null = null;

if (availableEncodings.length === 0) return null;

const header = request.headers.get("Accept-Encoding");
if (!header) return bestEncoding;
if (!header) return null;

const parsed = parseAcceptEncodingHeader(header);

Expand Down
8 changes: 1 addition & 7 deletions packages/compress/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,11 @@ import type { CompressionOptions } from "./types";

const compressMiddleware = ((options?: CompressionOptions) => (request) => {
const guesser = new EncodingGuesser(request);
let disabled = false;

if (typeof CompressionStream === "undefined") {
console.warn("Your platform does not support CompressionStream. Compression is disabled");
disabled = true;
}

return function universalMiddlewareCompress(response) {
const encoding = guesser.guessEncoding(response);

if (disabled || !encoding) return response;
if (!encoding) return response;

return handleCompression(encoding, response, options);
};
Expand Down
42 changes: 16 additions & 26 deletions packages/compress/src/response.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
import type { SUPPORTED_ENCODINGS } from "./const";
import { compressStream } from "./stream/stream";
import type { CompressionOptions } from "./types";

// async function guessCompressor(
// compressionMethod: CompressionOptions["compressionMethod"],
// encoding: (typeof SUPPORTED_ENCODINGS)[number],
// ): Promise<Compressor> {
// if (compressionMethod === "auto" || !compressionMethod) {
// // biome-ignore lint/style/noParameterAssign: <explanation>
// compressionMethod = encoding === "br" ? "zlib" : "stream";
// }
// if (compressionMethod === "zlib") {
// const { compressStream } = await import("./zlib/stream.js");
//
// return (input) => compressStream(input, encoding);
// }
// if (compressionMethod === "stream") {
// const { compressStream } = await import("./stream/stream.js");
//
// return (input) => compressStream(input, encoding as CompressionFormat);
// }
// throw new Error('Unsupported compressionMethod. Possible values are "auto", "zlib" or "stream".');
// }
import { isCompressionStreamAvailable } from "./runtime";
import type { CompressionAlgorithm, CompressionOptions, Compressor } from "./types";

async function guessCompressor(encoding: CompressionAlgorithm): Promise<Compressor> {
if (encoding === "br" || !isCompressionStreamAvailable) {
const { compressStream } = await import("./zlib/stream.js");

return (input) => compressStream(input, encoding);
}
const { compressStream } = await import("./stream/stream.js");

return (input) => compressStream(input, encoding as CompressionFormat);
}

export const handleCompression = async (
encoding: (typeof SUPPORTED_ENCODINGS)[number],
encoding: CompressionAlgorithm,
input: Response,
options?: CompressionOptions & ResponseInit,
): Promise<Response> => {
Expand All @@ -40,7 +29,8 @@ export const handleCompression = async (
if (!(input.headers.get(header) ?? "").includes(value)) input.headers.append(header, value);
}

const body = compressStream(input.body, encoding);
const compressor = await guessCompressor(encoding);
const body = await compressor(input.body);

if (body !== null) {
input.headers.append("Content-Encoding", encoding);
Expand Down
16 changes: 16 additions & 0 deletions packages/compress/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const isCompressionStreamAvailable = typeof CompressionStream !== "undefined";
export const isZlibAvailable = await isNodeZlibAvailable();
export const supportedEncodings = isZlibAvailable
? ["br", "gzip", "deflate"]
: isCompressionStreamAvailable
? ["gzip", "deflate"]
: [];

async function isNodeZlibAvailable() {
try {
await import(/* @vite-ignore */ "node:zlib");
return true;
} catch {
return false;
}
}
2 changes: 1 addition & 1 deletion packages/compress/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ export type Compressor = (
) => ReadableStream<Uint8Array> | null | Promise<ReadableStream<Uint8Array> | null>;
export type CompressionAlgorithm = "br" | "gzip" | "deflate";
export interface CompressionOptions {
// compressionMethod?: "auto" | "zlib" | "stream";
threshold?: number;
}
export type SupportedEncodings = CompressionAlgorithm[];
19 changes: 7 additions & 12 deletions packages/compress/test/compression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { decompressResponse } from "./utils";

const hugeStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".repeat(2 * 1024 * 1024);

describe.each([{ encoding: "gzip" }, { encoding: "deflate" }] as const)(
describe.each([{ encoding: "gzip" }, { encoding: "deflate" }, { encoding: "br" }] as const)(
"handleCompression: encoding: $encoding",
({ encoding }) => {
it("should not compress again if input is compressed already", async () => {
Expand All @@ -29,7 +29,9 @@ describe.each([{ encoding: "gzip" }, { encoding: "deflate" }] as const)(
expect(output.headers.get("Vary")).toStrictEqual("Accept-Encoding");
expect(output.headers.get("Content-Length")).toBeNull();

await expect(decompressResponse(output, encoding)).resolves.toBe("Test Response");
if (encoding !== "br") {
await expect(decompressResponse(output, encoding)).resolves.toBe("Test Response");
}
});

it('should append Accept-Encoding to "Vary" header if already present on intermediate Response', async () => {
Expand Down Expand Up @@ -98,16 +100,9 @@ describe.each([{ encoding: "gzip" }, { encoding: "deflate" }] as const)(
expect(output.headers.get("Content-Encoding")).toStrictEqual(encoding);
expect(output.headers.get("Content-Length")).toBeNull();

await expect(decompressResponse(output, encoding)).resolves.toBe(hugeStr);
if (encoding !== "br") {
await expect(decompressResponse(output, encoding)).resolves.toBe(hugeStr);
}
});
},
);

describe("handleCompression: encoding: 'br', compressionMethod: 'stream'", () => {
it("chould throw because CompressionStream does not support brotli", async () => {
const input = new Response("Test Response");

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
await expect(handleCompression("br" as any, input)).rejects.toThrow("compressionMethod");
});
});
3 changes: 0 additions & 3 deletions tests-examples/tests-tool/.testRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ export function testRun(
expect(response.headers.has("x-universal-hello")).toBe(true);

if (
// Bun does not support CompressionStream yet
// https://github.com/oven-sh/bun/issues/1723
!cmd.startsWith("pnpm run dev:elysia") &&
// Cloudflare already compresses data, so the compress middleware is not built for those targets
!cmd.startsWith("pnpm run dev:pages") &&
!cmd.startsWith("pnpm run dev:worker")
Expand Down

0 comments on commit d2f6509

Please sign in to comment.