alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
261 lines (228 loc) • 7.24 kB
text/typescript
import { Readable, type Transform } from "node:stream";
import { ReadableStream } from "node:stream/web";
import { promisify } from "node:util";
import * as zlib from "node:zlib";
import { $atom, $hook, $inject, $state, Alepha, type Static, z } from "alepha";
import type { ServerResponse } from "alepha/server";
const gzip = promisify(zlib.gzip);
const createGzip = zlib.createGzip;
const brotli = promisify(zlib.brotliCompress);
const createBrotliCompress = zlib.createBrotliCompress;
const zstd = zlib.zstdCompress ? promisify(zlib.zstdCompress) : undefined;
const createZstdCompress = zstd ? zlib.createZstdCompress : undefined;
// ---------------------------------------------------------------------------------------------------------------------
/**
* Compression configuration atom.
*/
export const compressOptions = $atom({
name: "alepha.server.compress.options",
schema: z.object({
disabled: z
.boolean()
.describe("Disable response compression entirely.")
.optional(),
allowedContentTypes: z
.array(z.string())
.describe("Content types eligible for compression."),
}),
default: {
allowedContentTypes: [
"application/json",
"text/html",
"application/javascript",
"text/plain",
"text/css",
],
},
});
export type CompressOptions = Static<typeof compressOptions.schema>;
declare module "alepha" {
interface State {
[compressOptions.key]: CompressOptions;
}
}
// ---------------------------------------------------------------------------------------------------------------------
export class ServerCompressProvider {
static compressors: Record<
string,
| {
compress: (...args: any[]) => Promise<Buffer>;
stream: (options?: any) => Transform;
}
| undefined
> = {
gzip: {
compress: gzip,
stream: createGzip,
},
br: {
compress: brotli,
stream: createBrotliCompress,
},
zstd:
zstd && createZstdCompress
? {
compress: zstd,
stream: createZstdCompress,
}
: undefined,
};
protected readonly alepha = $inject(Alepha);
protected readonly options = $state(compressOptions);
public readonly onResponse = $hook({
on: "server:onResponse",
handler: async ({ request, response }) => {
if (this.options.disabled) {
return;
}
// In serverless (Cloudflare Workers), skip compression entirely:
// Cloudflare's edge network automatically compresses responses
if (this.alepha.isServerless()) {
return;
}
// skip if already compressed
if (response.headers["content-encoding"]) {
return;
}
const acceptEncoding = request.headers["accept-encoding"]; // skip if no accept-encoding header
if (!acceptEncoding) {
return;
}
// skip if not json or html (for now)
if (!this.isAllowedContentType(response.headers["content-type"])) {
return;
}
for (const encoding of ["zstd", "br", "gzip"] as const) {
if (
acceptEncoding.includes(encoding) &&
ServerCompressProvider.compressors[encoding]
) {
await this.compress(encoding, response);
return;
}
}
},
});
protected isAllowedContentType(contentType: string | undefined): boolean {
if (!contentType) {
return false;
}
const lowerContentType = contentType.toLowerCase();
return !!this.options.allowedContentTypes.find((it) =>
lowerContentType.includes(it),
);
}
protected async compress(
encoding: keyof typeof ServerCompressProvider.compressors,
response: ServerResponse,
): Promise<void> {
const body = response.body; // can be string or Buffer or ArrayBuffer or Readable
const compressor = ServerCompressProvider.compressors[encoding];
if (!compressor) {
return;
}
const params = this.getParams(encoding);
if (
typeof body === "string" ||
Buffer.isBuffer(body) ||
body instanceof ArrayBuffer
) {
const compressed = await compressor.compress(body, {
params,
});
this.setHeaders(response, encoding);
response.headers["content-length"] = compressed.length.toString();
response.body = compressed;
return;
}
if (typeof body === "object" && body instanceof Readable) {
this.setHeaders(response, encoding);
response.body = body.pipe(compressor.stream({ params }));
return;
}
if (typeof body === "object" && body instanceof ReadableStream) {
this.setHeaders(response, encoding);
// For streaming responses, use flush mode to avoid buffering
response.body = this.createFlushingCompressStream(
body,
compressor.stream,
encoding,
params,
);
}
}
/**
* Create a compressed stream that flushes after each chunk.
* This is essential for streaming SSR - ensures each chunk is sent immediately.
*/
protected createFlushingCompressStream(
input: ReadableStream,
createCompressor: (options?: any) => Transform,
encoding: string,
params: Record<number, any>,
): ReadableStream<Uint8Array> {
const compressor = createCompressor({
params,
flush:
encoding === "br"
? zlib.constants.BROTLI_OPERATION_FLUSH
: zlib.constants.Z_SYNC_FLUSH,
});
const reader = Readable.fromWeb(input);
return new ReadableStream<Uint8Array>({
start(controller) {
compressor.on("data", (chunk: Buffer) => {
controller.enqueue(new Uint8Array(chunk));
});
compressor.on("end", () => {
controller.close();
});
compressor.on("error", (err) => {
controller.error(err);
});
reader.on("data", (chunk: Buffer) => {
compressor.write(chunk);
// Force flush after each chunk for streaming
// Cast to any because flush() exists on zlib streams but not in Transform type
const zlibStream = compressor as any;
if (encoding === "gzip") {
zlibStream.flush(zlib.constants.Z_SYNC_FLUSH);
} else if (encoding === "br") {
zlibStream.flush(zlib.constants.BROTLI_OPERATION_FLUSH);
} else if (encoding === "zstd") {
zlibStream.flush();
}
});
reader.on("end", () => {
compressor.end();
});
reader.on("error", (err) => {
controller.error(err);
});
},
});
}
protected getParams(
encoding: keyof typeof ServerCompressProvider.compressors,
): Record<number, any> {
if (encoding === "zstd") {
return {
[zlib.constants.ZSTD_c_compressionLevel]: 3, // default compression level for zstd
};
}
if (encoding === "br") {
return {};
}
if (encoding === "gzip") {
return {};
}
return {};
}
protected setHeaders(
response: ServerResponse,
encoding: keyof typeof ServerCompressProvider.compressors,
): void {
response.headers.vary = "accept-encoding";
response.headers["content-encoding"] = encoding;
}
}