@stacksjs/bun-router 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/index.js +53 -8
- package/dist/response/compression.d.ts +43 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -2057,6 +2057,8 @@ function isCompressible(contentType) {
|
|
|
2057
2057
|
if (!contentType)
|
|
2058
2058
|
return false;
|
|
2059
2059
|
const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
2060
|
+
if (type === "text/event-stream")
|
|
2061
|
+
return false;
|
|
2060
2062
|
if (type.startsWith("text/"))
|
|
2061
2063
|
return true;
|
|
2062
2064
|
return [
|
|
@@ -2099,10 +2101,10 @@ function shouldCompress(response, encoding, threshold) {
|
|
|
2099
2101
|
return false;
|
|
2100
2102
|
if (!isCompressible(response.headers.get("content-type")))
|
|
2101
2103
|
return false;
|
|
2102
|
-
|
|
2103
|
-
if (!Number.isFinite(length))
|
|
2104
|
+
if (!response.body)
|
|
2104
2105
|
return false;
|
|
2105
|
-
|
|
2106
|
+
const length = Number(response.headers.get("content-length") ?? Number.NaN);
|
|
2107
|
+
return Number.isFinite(length) ? length >= threshold : true;
|
|
2106
2108
|
}
|
|
2107
2109
|
async function compressResponse(response, request2, options = {}) {
|
|
2108
2110
|
const settings = { ...DEFAULT_COMPRESSION, ...options };
|
|
@@ -2114,19 +2116,61 @@ async function compressResponse(response, request2, options = {}) {
|
|
|
2114
2116
|
appendVary(response.headers, "Accept-Encoding");
|
|
2115
2117
|
return response;
|
|
2116
2118
|
}
|
|
2117
|
-
const
|
|
2118
|
-
const
|
|
2119
|
-
if (
|
|
2119
|
+
const known = Number(response.headers.get("content-length") ?? Number.NaN);
|
|
2120
|
+
const body = Number.isFinite(known) ? { stream: response.body, size: known, ended: false } : await peekBody(response.body, settings.threshold);
|
|
2121
|
+
if (body.ended && body.size < settings.threshold) {
|
|
2120
2122
|
const headers2 = new Headers(response.headers);
|
|
2121
2123
|
appendVary(headers2, "Accept-Encoding");
|
|
2122
|
-
return new Response(body, { status: response.status, statusText: response.statusText, headers: headers2 });
|
|
2124
|
+
return new Response(body.stream, { status: response.status, statusText: response.statusText, headers: headers2 });
|
|
2123
2125
|
}
|
|
2124
2126
|
const headers = new Headers(response.headers);
|
|
2125
2127
|
headers.set("Content-Encoding", encoding);
|
|
2126
|
-
headers.set("Content-Length", String(compressed.byteLength));
|
|
2127
2128
|
appendVary(headers, "Accept-Encoding");
|
|
2129
|
+
headers.delete("Content-Length");
|
|
2130
|
+
const compressed = body.stream.pipeThrough(new CompressionStream(encoding === "gzip" ? "gzip" : "deflate"));
|
|
2128
2131
|
return new Response(compressed, { status: response.status, statusText: response.statusText, headers });
|
|
2129
2132
|
}
|
|
2133
|
+
async function peekBody(source, limit) {
|
|
2134
|
+
const reader = source.getReader();
|
|
2135
|
+
const prefix = [];
|
|
2136
|
+
let size = 0;
|
|
2137
|
+
let ended = false;
|
|
2138
|
+
while (size < limit) {
|
|
2139
|
+
const { value, done } = await reader.read();
|
|
2140
|
+
if (done) {
|
|
2141
|
+
ended = true;
|
|
2142
|
+
break;
|
|
2143
|
+
}
|
|
2144
|
+
if (value) {
|
|
2145
|
+
prefix.push(value);
|
|
2146
|
+
size += value.byteLength;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
const stream = new ReadableStream({
|
|
2150
|
+
start(controller) {
|
|
2151
|
+
for (const chunk of prefix)
|
|
2152
|
+
controller.enqueue(chunk);
|
|
2153
|
+
if (ended)
|
|
2154
|
+
controller.close();
|
|
2155
|
+
},
|
|
2156
|
+
async pull(controller) {
|
|
2157
|
+
if (ended)
|
|
2158
|
+
return;
|
|
2159
|
+
const { value, done } = await reader.read();
|
|
2160
|
+
if (done) {
|
|
2161
|
+
ended = true;
|
|
2162
|
+
controller.close();
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
if (value)
|
|
2166
|
+
controller.enqueue(value);
|
|
2167
|
+
},
|
|
2168
|
+
cancel(reason) {
|
|
2169
|
+
return reader.cancel(reason);
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
return { stream, size, ended };
|
|
2173
|
+
}
|
|
2130
2174
|
function appendVary(headers, value) {
|
|
2131
2175
|
const existing = headers.get("vary");
|
|
2132
2176
|
if (!existing) {
|
|
@@ -13785,6 +13829,7 @@ export {
|
|
|
13785
13829
|
performanceDashboard,
|
|
13786
13830
|
performanceAssertions,
|
|
13787
13831
|
performanceAlerting,
|
|
13832
|
+
peekBody,
|
|
13788
13833
|
parseThrottleString,
|
|
13789
13834
|
oauth2Auth,
|
|
13790
13835
|
normalizePath,
|
|
@@ -7,25 +7,39 @@
|
|
|
7
7
|
* HTML and compresses to about a tenth of that, on every request, for every
|
|
8
8
|
* visitor - and the same is true of any JSON list long enough to matter.
|
|
9
9
|
*
|
|
10
|
-
* Three rules
|
|
10
|
+
* Three rules:
|
|
11
11
|
*
|
|
12
12
|
* - **Only when the client asked.** `Accept-Encoding` decides, and `Vary` says
|
|
13
13
|
* so, because a cache that hands a gzipped body to a client that did not ask
|
|
14
14
|
* is a cache that breaks that client.
|
|
15
|
-
* - **Only when the length is known.** A response with no `Content-Length` is a
|
|
16
|
-
* stream, and buffering one to compress it would turn a progressive render
|
|
17
|
-
* into a wait - the diff manifest that streams a hundred files as they parse
|
|
18
|
-
* would arrive all at once, at the end. Streams are left alone.
|
|
19
15
|
* - **Only when it is worth it.** Below a kilobyte the header costs more than
|
|
20
16
|
* the saving, and already-compressed bytes (an image, a woff2, a zip) get
|
|
21
17
|
* bigger rather than smaller.
|
|
18
|
+
* - **Never by buffering.** The body is piped through a `CompressionStream`
|
|
19
|
+
* rather than read into memory, so a response that streams keeps streaming:
|
|
20
|
+
* the diff manifest that sends a hundred files as they parse still arrives a
|
|
21
|
+
* file at a time, compressed. Buffering to compress would have turned this
|
|
22
|
+
* product's progressive render into a wait, which is a worse trade than the
|
|
23
|
+
* bytes are worth.
|
|
24
|
+
*
|
|
25
|
+
* A piped body has no length until it ends, so `Content-Length` comes off and
|
|
26
|
+
* the response goes out chunked. That is the ordinary shape for a compressed
|
|
27
|
+
* response and every client handles it; a browser showing a page as it arrives
|
|
28
|
+
* is not waiting for a number.
|
|
22
29
|
*/
|
|
23
30
|
/** zlib's own range, so a level Bun would reject is a type error rather than a throw. */
|
|
24
31
|
export type CompressionLevel = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
25
32
|
export interface CompressionOptions {
|
|
26
33
|
/** Off turns the whole thing off, for a proxy that already compresses. */
|
|
27
34
|
enabled?: boolean;
|
|
28
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Kept for callers that set it, and unused by the stream path.
|
|
37
|
+
*
|
|
38
|
+
* `CompressionStream` has no level knob - it is the platform's, at its own
|
|
39
|
+
* default - and piping is what keeps a streamed response streaming. A level
|
|
40
|
+
* would mean going back to buffering, which is the trade this deliberately
|
|
41
|
+
* does not make.
|
|
42
|
+
*/
|
|
29
43
|
level?: CompressionLevel;
|
|
30
44
|
/** Bodies below this many bytes are sent as they are. */
|
|
31
45
|
threshold?: number;
|
|
@@ -53,7 +67,16 @@ export declare function isCompressible(contentType: string | null): boolean;
|
|
|
53
67
|
* response path is a different change from this one.
|
|
54
68
|
*/
|
|
55
69
|
export declare function negotiateEncoding(header: string | null): 'gzip' | 'deflate' | null;
|
|
56
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Whether this response should be compressed at all.
|
|
72
|
+
*
|
|
73
|
+
* `Content-Length` is a *hint here rather than a requirement*: Bun does not put
|
|
74
|
+
* one on a `Response` built from a string, so treating its absence as "this is
|
|
75
|
+
* a stream, leave it alone" left every server-rendered page uncompressed - the
|
|
76
|
+
* exact case this exists for. When there is a length, it decides against
|
|
77
|
+
* bodies too small to be worth it; when there is not, the body is piped and
|
|
78
|
+
* the threshold cannot apply.
|
|
79
|
+
*/
|
|
57
80
|
export declare function shouldCompress(response: Response, encoding: string | null, threshold: number): boolean;
|
|
58
81
|
/**
|
|
59
82
|
* The response, compressed when that is the right thing to do.
|
|
@@ -62,5 +85,18 @@ export declare function shouldCompress(response: Response, encoding: string | nu
|
|
|
62
85
|
* unconditionally at the end of the pipeline.
|
|
63
86
|
*/
|
|
64
87
|
export declare function compressResponse(response: Response, request: Request, options?: CompressionOptions): Promise<Response>;
|
|
88
|
+
/**
|
|
89
|
+
* Read up to `limit` bytes, and hand back a stream that still has them.
|
|
90
|
+
*
|
|
91
|
+
* The prefix is re-emitted before the rest of the source, so nothing is lost
|
|
92
|
+
* and nothing is held beyond the limit. `ended` says the body finished inside
|
|
93
|
+
* the prefix, which is what makes a small answer distinguishable from the
|
|
94
|
+
* beginning of a large one.
|
|
95
|
+
*/
|
|
96
|
+
export declare function peekBody(source: ReadableStream<Uint8Array>, limit: number): Promise<{
|
|
97
|
+
stream: ReadableStream<Uint8Array>;
|
|
98
|
+
size: number;
|
|
99
|
+
ended: boolean;
|
|
100
|
+
}>;
|
|
65
101
|
/** Add to `Vary` without dropping what is already there. */
|
|
66
102
|
export declare function appendVary(headers: Headers, value: string): void;
|