1/**
2 * HTTP response compression backed by `node:zlib`, shared by the Node.js, Bun,
3 * and Deno platforms.
4 *
5 * Byte-array bodies are compressed in one shot with the asynchronous
6 * `node:zlib` APIs, preserving an exact `Content-Length`. Streaming bodies go
7 * through `node:zlib` transform streams that flush each input chunk.
8 *
9 * @since 4.0.0
10 */
11import * as Effect from "effect/Effect"
12import * as HttpBody from "effect/http/HttpBody"
13import type * as Platform from "effect/http/HttpPlatform"
14import * as Response from "effect/http/HttpServerResponse"
15import type { Duplex } from "node:stream"
16import { Readable } from "node:stream"
17import * as Zlib from "node:zlib"
18
19/**
20 * The compression algorithms supported by the runtime's `node:zlib`. `zstd`
21 * requires Node.js 22.15 or newer.
22 *
23 * @category constants
24 * @since 4.0.0
25 */
26export const algorithms: ReadonlySet<Platform.CompressionAlgorithm> = new Set(
27 typeof Zlib.zstdCompress === "function"
28 ? ["gzip", "deflate", "br", "zstd"]
29 : ["gzip", "deflate", "br"]
30)
31
32const brotliParams = (level: number | undefined, sizeHint?: number): Zlib.BrotliOptions => {
33 const params: Record<number, number> = {}
34 if (level !== undefined) {
35 params[Zlib.constants.BROTLI_PARAM_QUALITY] = level
36 }
37 if (sizeHint !== undefined) {
38 params[Zlib.constants.BROTLI_PARAM_SIZE_HINT] = sizeHint
39 }
40 return { params }
41}
42
43const zstdParams = (level: number | undefined): Zlib.ZstdOptions | undefined =>
44 level === undefined || level === 3 ? undefined : { params: { [Zlib.constants.ZSTD_c_compressionLevel]: level } }
45
46const compress = (
47 data: Uint8Array,
48 algorithm: Platform.CompressionAlgorithm,
49 options?: Platform.CompressionOptions | undefined
50): Effect.Effect<Uint8Array> =>
51 Effect.callback((resume) => {
52 const complete = (error: Error | null, result: Uint8Array) =>
53 resume(error === null ? Effect.succeed(result) : Effect.die(error))
54 switch (algorithm) {
55 case "gzip": {
56 Zlib.gzip(data, { level: options?.level }, complete)
57 break
58 }
59 case "deflate": {
60 Zlib.deflate(data, { level: options?.level }, complete)
61 break
62 }
63 case "br": {
64 Zlib.brotliCompress(data, brotliParams(options?.level, data.byteLength), complete)
65 break
66 }
67 case "zstd": {
68 const params = zstdParams(options?.level)
69 if (params === undefined) {
70 Zlib.zstdCompress(data, complete)
71 } else {
72 Zlib.zstdCompress(data, params, complete)
73 }
74 break
75 }
76 }
77 })
78
79/**
80 * Creates a `Compression` that compresses byte-array bodies in one shot with
81 * the asynchronous `node:zlib` APIs, setting the exact `Content-Length` of the
82 * compressed body. All other bodies are delegated to `fallback`.
83 *
84 * @category constructors
85 * @since 4.0.0
86 */
87export const make = (fallback: Platform.Compression): Platform.Compression => ({
88 algorithms: fallback.algorithms,
89 compressResponse(response, algorithm, options) {
90 const body = response.body
91 if (body._tag !== "Uint8Array") {
92 return fallback.compressResponse(response, algorithm, options)
93 }
94 return Effect.map(compress(body.body, algorithm, options), (result) =>
95 Response.setHeader(
96 Response.setBody(response, HttpBody.uint8Array(result, response.headers["content-type"] ?? body.contentType)),
97 "content-length",
98 result.byteLength.toString()
99 ))
100 }
101})
102
103/**
104 * Creates a `node:zlib` compression transform stream that flushes each input
105 * chunk, for streaming response bodies.
106 *
107 * @category constructors
108 * @since 4.0.0
109 */
110export const compressTransform = (
111 algorithm: Platform.CompressionAlgorithm,
112 options?: Platform.CompressionOptions | undefined
113): Duplex => {
114 switch (algorithm) {
115 case "gzip": {
116 return Zlib.createGzip({ level: options?.level, flush: Zlib.constants.Z_SYNC_FLUSH })
117 }
118 case "deflate": {
119 return Zlib.createDeflate({ level: options?.level, flush: Zlib.constants.Z_SYNC_FLUSH })
120 }
121 case "br": {
122 return Zlib.createBrotliCompress({
123 ...brotliParams(options?.level),
124 flush: Zlib.constants.BROTLI_OPERATION_FLUSH
125 })
126 }
127 case "zstd": {
128 return Zlib.createZstdCompress({
129 ...zstdParams(options?.level),
130 flush: Zlib.constants.ZSTD_e_flush
131 })
132 }
133 }
134}
135
136/**
137 * A Web `ReadableStream` version of `compressTransform`, for platforms that
138 * stream response bodies as Web streams.
139 *
140 * @category constructors
141 * @since 4.0.0
142 */
143export const compressTransformWeb = (
144 algorithm: Platform.CompressionAlgorithm,
145 options?: Platform.CompressionOptions | undefined
146) =>
147(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> => {
148 const transform = compressTransform(algorithm, options)
149 const source = Readable.fromWeb(stream as any)
150 source.on("error", (cause) => transform.destroy(cause))
151 transform.on("close", () => source.destroy())
152 return Readable.toWeb(source.pipe(transform)) as unknown as ReadableStream<Uint8Array>
153}