@stacksjs/bun-router 0.0.23 → 0.0.24

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 CHANGED
@@ -2047,7 +2047,7 @@ function getPrefix() {
2047
2047
  }
2048
2048
  var prefix = getPrefix();
2049
2049
  // package.json
2050
- var version = "0.0.23";
2050
+ var version = "0.0.24";
2051
2051
 
2052
2052
  // src/cli/middleware.ts
2053
2053
  import fs2 from "fs/promises";
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export * as Errors from './errors';
20
20
  export * from './middleware';
21
21
  export { enableRequestContext, getCurrentRequest, request, runWithRequest, setCurrentRequest } from './request/context';
22
22
  export * from './session';
23
+ export * from './response/compression';
23
24
  export * from './response/response-factory';
24
25
  export * from './router';
25
26
  export { MiddlewareGroupRegistry, middlewareGroups } from './router/middleware-groups';
package/dist/index.js CHANGED
@@ -2047,6 +2047,97 @@ function request() {
2047
2047
  }
2048
2048
  return req;
2049
2049
  }
2050
+ // src/response/compression.ts
2051
+ var DEFAULT_COMPRESSION = {
2052
+ enabled: true,
2053
+ level: 6,
2054
+ threshold: 1024
2055
+ };
2056
+ function isCompressible(contentType) {
2057
+ if (!contentType)
2058
+ return false;
2059
+ const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
2060
+ if (type.startsWith("text/"))
2061
+ return true;
2062
+ return [
2063
+ "application/json",
2064
+ "application/ld+json",
2065
+ "application/manifest+json",
2066
+ "application/javascript",
2067
+ "application/x-javascript",
2068
+ "application/xml",
2069
+ "application/xhtml+xml",
2070
+ "application/rss+xml",
2071
+ "application/atom+xml",
2072
+ "application/graphql",
2073
+ "application/x-ndjson",
2074
+ "image/svg+xml"
2075
+ ].includes(type);
2076
+ }
2077
+ function negotiateEncoding(header) {
2078
+ if (!header)
2079
+ return null;
2080
+ const offers = new Map;
2081
+ for (const part of header.split(",")) {
2082
+ const [name = "", ...parameters] = part.trim().split(";");
2083
+ const quality = parameters.map((parameter) => /^\s*q=([\d.]+)\s*$/i.exec(parameter)).find(Boolean);
2084
+ offers.set(name.trim().toLowerCase(), quality ? Number(quality[1]) : 1);
2085
+ }
2086
+ const wanted = (name) => (offers.get(name) ?? offers.get("*") ?? 0) > 0;
2087
+ if (wanted("gzip"))
2088
+ return "gzip";
2089
+ if (wanted("deflate"))
2090
+ return "deflate";
2091
+ return null;
2092
+ }
2093
+ function shouldCompress(response, encoding, threshold) {
2094
+ if (!encoding)
2095
+ return false;
2096
+ if (response.status === 204 || response.status === 304 || response.status === 206)
2097
+ return false;
2098
+ if (response.headers.has("content-encoding"))
2099
+ return false;
2100
+ if (!isCompressible(response.headers.get("content-type")))
2101
+ return false;
2102
+ const length = Number(response.headers.get("content-length") ?? Number.NaN);
2103
+ if (!Number.isFinite(length))
2104
+ return false;
2105
+ return length >= threshold;
2106
+ }
2107
+ async function compressResponse(response, request2, options = {}) {
2108
+ const settings = { ...DEFAULT_COMPRESSION, ...options };
2109
+ if (!settings.enabled)
2110
+ return response;
2111
+ const encoding = negotiateEncoding(request2.headers.get("accept-encoding"));
2112
+ if (!shouldCompress(response, encoding, settings.threshold)) {
2113
+ if (isCompressible(response.headers.get("content-type")) && !response.headers.has("content-encoding"))
2114
+ appendVary(response.headers, "Accept-Encoding");
2115
+ return response;
2116
+ }
2117
+ const body = new Uint8Array(await response.arrayBuffer());
2118
+ const compressed = encoding === "gzip" ? Bun.gzipSync(body, { level: settings.level }) : Bun.deflateSync(body, { level: settings.level });
2119
+ if (compressed.byteLength >= body.byteLength) {
2120
+ const headers2 = new Headers(response.headers);
2121
+ appendVary(headers2, "Accept-Encoding");
2122
+ return new Response(body, { status: response.status, statusText: response.statusText, headers: headers2 });
2123
+ }
2124
+ const headers = new Headers(response.headers);
2125
+ headers.set("Content-Encoding", encoding);
2126
+ headers.set("Content-Length", String(compressed.byteLength));
2127
+ appendVary(headers, "Accept-Encoding");
2128
+ return new Response(compressed, { status: response.status, statusText: response.statusText, headers });
2129
+ }
2130
+ function appendVary(headers, value) {
2131
+ const existing = headers.get("vary");
2132
+ if (!existing) {
2133
+ headers.set("Vary", value);
2134
+ return;
2135
+ }
2136
+ const parts = existing.split(",").map((part) => part.trim().toLowerCase());
2137
+ if (parts.includes("*") || parts.includes(value.toLowerCase()))
2138
+ return;
2139
+ headers.set("Vary", `${existing}, ${value}`);
2140
+ }
2050
2141
  // src/response/response-factory.ts
2051
2142
  var response = {
2052
2143
  json: (data, options = {}) => {
@@ -9563,7 +9654,8 @@ function registerServerHandling(RouterClass) {
9563
9654
  handleRequest: {
9564
9655
  async value(req) {
9565
9656
  const self = this;
9566
- return runWithRequest(req, () => self.handleRequestImpl(req));
9657
+ const response2 = await runWithRequest(req, () => self.handleRequestImpl(req));
9658
+ return await compressResponse(response2, req, self.config?.compression ?? {});
9567
9659
  },
9568
9660
  writable: true,
9569
9661
  configurable: true
@@ -13664,6 +13756,7 @@ export {
13664
13756
  spyOnMiddleware,
13665
13757
  singleFileUpload,
13666
13758
  signRequest,
13759
+ shouldCompress,
13667
13760
  setCurrentRequest,
13668
13761
  setContainer,
13669
13762
  session,
@@ -13695,6 +13788,7 @@ export {
13695
13788
  parseThrottleString,
13696
13789
  oauth2Auth,
13697
13790
  normalizePath,
13791
+ negotiateEncoding,
13698
13792
  multipleFileUpload,
13699
13793
  modelMocks,
13700
13794
  mockMiddleware,
@@ -13706,6 +13800,7 @@ export {
13706
13800
  jsonBody,
13707
13801
  joinPaths,
13708
13802
  isRouteHandler,
13803
+ isCompressible,
13709
13804
  isActionClass,
13710
13805
  inputValidation,
13711
13806
  injectQueryPreservationScript,
@@ -13763,6 +13858,7 @@ export {
13763
13858
  contentSecurityPolicy,
13764
13859
  constraintHelpers,
13765
13860
  config,
13861
+ compressResponse,
13766
13862
  clearNamedRoutes,
13767
13863
  buildUrlWithPreservedParams,
13768
13864
  benchmarkTests,
@@ -13773,6 +13869,7 @@ export {
13773
13869
  authHelpers,
13774
13870
  auth,
13775
13871
  applyRequestEnhancements,
13872
+ appendVary,
13776
13873
  apiKeyAuth,
13777
13874
  WebSocketTester,
13778
13875
  Validator,
@@ -13854,6 +13951,7 @@ export {
13854
13951
  DefaultServiceProviderManager,
13855
13952
  DatabaseSessionStore,
13856
13953
  DatabaseRules,
13954
+ DEFAULT_COMPRESSION,
13857
13955
  DDoSProtection,
13858
13956
  Csrf,
13859
13957
  Cors,
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Compressing what goes back, which nothing was doing.
3
+ *
4
+ * Static files have been served gzipped for a long time; every response a
5
+ * *route* produced went out whole. A server-rendered page is the shape that
6
+ * costs: the landing page of the application this was found in is 253 KB of
7
+ * HTML and compresses to about a tenth of that, on every request, for every
8
+ * visitor - and the same is true of any JSON list long enough to matter.
9
+ *
10
+ * Three rules, and the middle one is the important one:
11
+ *
12
+ * - **Only when the client asked.** `Accept-Encoding` decides, and `Vary` says
13
+ * so, because a cache that hands a gzipped body to a client that did not ask
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
+ * - **Only when it is worth it.** Below a kilobyte the header costs more than
20
+ * the saving, and already-compressed bytes (an image, a woff2, a zip) get
21
+ * bigger rather than smaller.
22
+ */
23
+ /** zlib's own range, so a level Bun would reject is a type error rather than a throw. */
24
+ export type CompressionLevel = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
25
+ export interface CompressionOptions {
26
+ /** Off turns the whole thing off, for a proxy that already compresses. */
27
+ enabled?: boolean;
28
+ /** zlib level. 6 is the usual balance; 9 costs noticeably more CPU for a few percent. */
29
+ level?: CompressionLevel;
30
+ /** Bodies below this many bytes are sent as they are. */
31
+ threshold?: number;
32
+ }
33
+ export declare const DEFAULT_COMPRESSION: Required<CompressionOptions>;
34
+ /**
35
+ * Content types worth compressing.
36
+ *
37
+ * A prefix list rather than a regex over everything: `image/svg+xml` is text
38
+ * and compresses well, `image/png` is already compressed and grows. When in
39
+ * doubt the answer is no, because sending something slightly larger is a
40
+ * wasted CPU cycle and a wasted byte, while failing to compress is only a
41
+ * missed saving.
42
+ */
43
+ export declare function isCompressible(contentType: string | null): boolean;
44
+ /**
45
+ * The encoding to use, from what the client offered.
46
+ *
47
+ * gzip before deflate, and `q=0` respected - a client that says
48
+ * `gzip;q=0` means it, and sending gzip anyway is how you get an unreadable
49
+ * page in the one browser that asked.
50
+ *
51
+ * Brotli is not offered even though Bun can produce it: `Bun.gzipSync` is
52
+ * synchronous and brotli is not, and a compression step that awaits inside the
53
+ * response path is a different change from this one.
54
+ */
55
+ export declare function negotiateEncoding(header: string | null): 'gzip' | 'deflate' | null;
56
+ /** Whether this response should be compressed at all, and why not when it should not. */
57
+ export declare function shouldCompress(response: Response, encoding: string | null, threshold: number): boolean;
58
+ /**
59
+ * The response, compressed when that is the right thing to do.
60
+ *
61
+ * Returns the response it was given when it is not, so a caller can apply this
62
+ * unconditionally at the end of the pipeline.
63
+ */
64
+ export declare function compressResponse(response: Response, request: Request, options?: CompressionOptions): Promise<Response>;
65
+ /** Add to `Vary` without dropping what is already there. */
66
+ export declare function appendVary(headers: Headers, value: string): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/bun-router",
3
3
  "type": "module",
4
- "version": "0.0.23",
4
+ "version": "0.0.24",
5
5
  "description": "A fast, type-safe router for Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",