@timber-js/app 0.1.44 → 0.1.46

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.
@@ -30,6 +30,8 @@ export declare function logRequestCompleted(data: {
30
30
  path: string;
31
31
  status: number;
32
32
  durationMs: number;
33
+ /** Number of concurrent in-flight requests (including this one) at completion time. */
34
+ concurrency?: number;
33
35
  }): void;
34
36
  /** Log request received. Level: debug. */
35
37
  export declare function logRequestReceived(data: {
@@ -42,6 +44,8 @@ export declare function logSlowRequest(data: {
42
44
  path: string;
43
45
  durationMs: number;
44
46
  threshold: number;
47
+ /** Number of concurrent in-flight requests at the time the slow request completed. */
48
+ concurrency?: number;
45
49
  }): void;
46
50
  /** Log middleware short-circuit. Level: debug. */
47
51
  export declare function logMiddlewareShortCircuit(data: {
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/server/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1D;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAEpD;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,YAAY,GAAG,IAAI,CAE/C;AAsBD,4CAA4C;AAC5C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,IAAI,CAEP;AAED,0CAA0C;AAC1C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE/E;AAED,+CAA+C;AAC/C,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,IAAI,CAEP;AAED,kDAAkD;AAClD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,IAAI,CAEP;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAM/F;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAQ3F;AAED,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAM5D;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED,sDAAsD;AACtD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAEnE;AAED,6DAA6D;AAC7D,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAEpF;AAED,oCAAoC;AACpC,wBAAgB,YAAY,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE7D"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/server/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1D;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAEpD;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,YAAY,GAAG,IAAI,CAE/C;AAsBD,4CAA4C;AAC5C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,IAAI,CAEP;AAED,0CAA0C;AAC1C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE/E;AAED,+CAA+C;AAC/C,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,IAAI,CAEP;AAED,kDAAkD;AAClD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,IAAI,CAEP;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAM/F;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAQ3F;AAED,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAM5D;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED,sDAAsD;AACtD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAEnE;AAED,6DAA6D;AAC7D,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAEpF;AAED,oCAAoC;AACpC,wBAAgB,YAAY,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/server/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAY,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAoC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAItD,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,uDAAuD;IACvD,UAAU,CAAC,EAAE,YAAY,CAAC;CAC3B;AAED,6DAA6D;AAC7D,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAAC;AAEnE,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,CACjC,QAAQ,EAAE,MAAM,KACb,OAAO,oBAAoB,EAAE,kBAAkB,GAAG,IAAI,CAAC;AAE5D,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,6DAA6D;AAC7D,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,YAAY,CAAC,EAAE,mBAAmB,KAC/B,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElC,+DAA+D;AAC/D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,KACrB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAI1B,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACtD,qEAAqE;IACrE,UAAU,EAAE,YAAY,CAAC;IACzB,iGAAiG;IACjG,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C,kEAAkE;IAClE,MAAM,EAAE,aAAa,CAAC;IACtB,kEAAkE;IAClE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzF,kFAAkF;IAClF,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yGAAyG;IACzG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,2BAA2B,EAAE,mBAAmB,EAAE,CAAC;IACjF;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,EAAE,CACpB,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,KACrB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAuU1F"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/server/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAY,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAoC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAItD,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,uDAAuD;IACvD,UAAU,CAAC,EAAE,YAAY,CAAC;CAC3B;AAED,6DAA6D;AAC7D,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAAC;AAEnE,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,CACjC,QAAQ,EAAE,MAAM,KACb,OAAO,oBAAoB,EAAE,kBAAkB,GAAG,IAAI,CAAC;AAE5D,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,6DAA6D;AAC7D,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,YAAY,CAAC,EAAE,mBAAmB,KAC/B,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElC,+DAA+D;AAC/D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,KACrB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAI1B,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACtD,qEAAqE;IACrE,UAAU,EAAE,YAAY,CAAC;IACzB,iGAAiG;IACjG,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C,kEAAkE;IAClE,MAAM,EAAE,aAAa,CAAC;IACtB,kEAAkE;IAClE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzF,kFAAkF;IAClF,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yGAAyG;IACzG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,2BAA2B,EAAE,mBAAmB,EAAE,CAAC;IACjF;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,EAAE,CACpB,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,KACrB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA+V1F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -4,7 +4,9 @@
4
4
  // written to the build output during adapter buildOutput(). It's imported
5
5
  // by the Nitro entry and preview server at runtime.
6
6
  //
7
- // Uses CompressionStream (Web API) for gzip and node:zlib for brotli.
7
+ // Uses CompressionStream (Web API) for gzip. Brotli is left to CDNs/reverse
8
+ // proxies — at streaming quality levels its ratio advantage is marginal and
9
+ // node:zlib buffers output internally, breaking streaming.
8
10
  // Cloudflare Workers don't need this — the edge auto-compresses.
9
11
  //
10
12
  // See design/25-production-deployments.md.
@@ -20,10 +22,9 @@
20
22
  export function generateCompressModule(): string {
21
23
  return `// Generated by @timber-js/app — response compression for self-hosted deployments.
22
24
  // Do not edit — this file is regenerated on each build.
23
- // Uses CompressionStream (Web API) for gzip, node:zlib for brotli.
24
-
25
- import { createBrotliCompress, constants as zlibConstants } from 'node:zlib';
26
- import { Readable } from 'node:stream';
25
+ // Uses CompressionStream (Web API) for gzip. Brotli is left to CDNs/reverse
26
+ // proxies — at streaming quality levels its ratio advantage is marginal and
27
+ // node:zlib buffers output internally, breaking streaming.
27
28
 
28
29
  const COMPRESSIBLE_TYPES = new Set([
29
30
  'text/html', 'text/css', 'text/plain', 'text/xml', 'text/javascript',
@@ -37,7 +38,10 @@ const NO_COMPRESS_STATUSES = new Set([204, 304]);
37
38
  function negotiateEncoding(acceptEncoding) {
38
39
  if (!acceptEncoding) return null;
39
40
  const tokens = acceptEncoding.split(',').map(s => s.split(';')[0].trim().toLowerCase());
40
- if (tokens.includes('br')) return 'br';
41
+ // Brotli (br) is intentionally not handled at the application level.
42
+ // At streaming-friendly quality levels (0-4), brotli's ratio advantage over
43
+ // gzip is marginal, and node:zlib's brotli transform buffers output internally.
44
+ // CDNs/reverse proxies apply brotli at higher quality levels on cached responses.
41
45
  if (tokens.includes('gzip')) return 'gzip';
42
46
  return null;
43
47
  }
@@ -57,36 +61,12 @@ function compressWithGzip(body) {
57
61
  return body.pipeThrough(new CompressionStream('gzip'));
58
62
  }
59
63
 
60
- function compressWithBrotli(body) {
61
- const brotli = createBrotliCompress({
62
- params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 4 },
63
- });
64
- const reader = body.getReader();
65
- const pump = async () => {
66
- try {
67
- while (true) {
68
- const { done, value } = await reader.read();
69
- if (done) { brotli.end(); return; }
70
- if (!brotli.write(value)) {
71
- await new Promise(resolve => brotli.once('drain', resolve));
72
- }
73
- }
74
- } catch (err) {
75
- brotli.destroy(err instanceof Error ? err : new Error(String(err)));
76
- }
77
- };
78
- pump();
79
- return Readable.toWeb(brotli);
80
- }
81
-
82
64
  export function compressResponse(request, response) {
83
65
  if (!shouldCompress(response)) return response;
84
66
  const acceptEncoding = request.headers.get('Accept-Encoding') || '';
85
67
  const encoding = negotiateEncoding(acceptEncoding);
86
68
  if (!encoding) return response;
87
- const compressedBody = encoding === 'br'
88
- ? compressWithBrotli(response.body)
89
- : compressWithGzip(response.body);
69
+ const compressedBody = compressWithGzip(response.body);
90
70
  const headers = new Headers(response.headers);
91
71
  headers.set('Content-Encoding', encoding);
92
72
  headers.delete('Content-Length');
@@ -1,14 +1,13 @@
1
1
  // Response compression for self-hosted deployments (dev server, Nitro preview).
2
2
  //
3
- // Uses CompressionStream (Web Platform API) for gzip and node:zlib for
4
- // brotli (CompressionStream doesn't support brotli). Cloudflare Workers
5
- // auto-compress at the edge this module is only used on Node.js/Bun.
3
+ // Uses CompressionStream (Web Platform API) for gzip. Brotli is intentionally
4
+ // left to CDNs and reverse proxies — at streaming-friendly quality levels its
5
+ // ratio advantage is marginal, and node:zlib's brotli transform buffers output
6
+ // internally, breaking streaming. Cloudflare Workers auto-compress at the edge —
7
+ // this module is only used on Node.js/Bun.
6
8
  //
7
9
  // See design/25-production-deployments.md.
8
10
 
9
- import { createBrotliCompress, constants as zlibConstants } from 'node:zlib';
10
- import { Readable } from 'node:stream';
11
-
12
11
  // ─── Constants ────────────────────────────────────────────────────────────
13
12
 
14
13
  /**
@@ -41,21 +40,25 @@ const NO_COMPRESS_STATUSES = new Set([204, 304]);
41
40
 
42
41
  /**
43
42
  * Parse Accept-Encoding and return the best supported encoding.
44
- * Prefers brotli (br) over gzip. Returns null if no supported encoding.
43
+ * Returns 'gzip' if the client accepts it, null otherwise.
44
+ *
45
+ * Brotli (br) is intentionally not handled at the application level.
46
+ * At the streaming-friendly quality levels (0–4), brotli's compression
47
+ * ratio advantage over gzip is marginal, and node:zlib's brotli transform
48
+ * buffers output internally — turning smooth streaming responses into
49
+ * large infrequent bursts. Brotli's real wins come from offline/static
50
+ * compression at higher quality levels (5–11), which CDNs and reverse
51
+ * proxies (Cloudflare, nginx, Caddy) apply on cached responses.
45
52
  *
46
- * We always prefer brotli regardless of quality values because:
47
- * 1. Brotli achieves better compression ratios than gzip
48
- * 2. All modern browsers that send br in Accept-Encoding support it well
49
- * 3. Respecting q-values for br vs gzip adds complexity with no real benefit
53
+ * See design/25-production-deployments.md.
50
54
  */
51
- export function negotiateEncoding(acceptEncoding: string): 'br' | 'gzip' | null {
55
+ export function negotiateEncoding(acceptEncoding: string): 'gzip' | null {
52
56
  if (!acceptEncoding) return null;
53
57
 
54
58
  // Parse tokens from the Accept-Encoding header (ignore quality values).
55
59
  // e.g. "gzip;q=1.0, br;q=0.8, deflate" → ['gzip', 'br', 'deflate']
56
60
  const tokens = acceptEncoding.split(',').map((s) => s.split(';')[0].trim().toLowerCase());
57
61
 
58
- if (tokens.includes('br')) return 'br';
59
62
  if (tokens.includes('gzip')) return 'gzip';
60
63
  return null;
61
64
  }
@@ -112,10 +115,8 @@ export function compressResponse(request: Request, response: Response): Response
112
115
  const encoding = negotiateEncoding(acceptEncoding);
113
116
  if (!encoding) return response;
114
117
 
115
- // Compress the body stream
116
- const compressedBody = encoding === 'br'
117
- ? compressWithBrotli(response.body!)
118
- : compressWithGzip(response.body!);
118
+ // Compress the body stream with gzip via the Web Platform CompressionStream API.
119
+ const compressedBody = compressWithGzip(response.body!);
119
120
 
120
121
  // Build new headers: copy originals, add compression headers, remove Content-Length
121
122
  // (compressed size is unknown until streaming completes).
@@ -153,48 +154,3 @@ function compressWithGzip(body: ReadableStream<Uint8Array>): ReadableStream<Uint
153
154
  return body.pipeThrough(compressionStream as unknown as TransformStream<Uint8Array, Uint8Array>);
154
155
  }
155
156
 
156
- // ─── Brotli (node:zlib) ──────────────────────────────────────────────────
157
-
158
- /**
159
- * Compress a ReadableStream with brotli using node:zlib.
160
- *
161
- * CompressionStream doesn't support brotli — it only handles gzip and deflate.
162
- * We use node:zlib's createBrotliCompress() and bridge between Web streams
163
- * and Node streams.
164
- */
165
- function compressWithBrotli(body: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
166
- const brotli = createBrotliCompress({
167
- params: {
168
- // Quality 4 balances compression ratio and CPU time for streaming.
169
- // Default (11) is too slow for real-time responses.
170
- [zlibConstants.BROTLI_PARAM_QUALITY]: 4,
171
- },
172
- });
173
-
174
- // Pipe the Web ReadableStream into the Node brotli transform.
175
- const reader = body.getReader();
176
-
177
- // Pump chunks from the Web ReadableStream into the Node transform.
178
- const pump = async (): Promise<void> => {
179
- try {
180
- while (true) {
181
- const { done, value } = await reader.read();
182
- if (done) {
183
- brotli.end();
184
- return;
185
- }
186
- // Write to brotli, wait for drain if buffer is full
187
- if (!brotli.write(value)) {
188
- await new Promise<void>((resolve) => brotli.once('drain', resolve));
189
- }
190
- }
191
- } catch (err) {
192
- brotli.destroy(err instanceof Error ? err : new Error(String(err)));
193
- }
194
- };
195
- // Start pumping (fire and forget — errors propagate via brotli stream)
196
- pump();
197
-
198
- // Convert the Node readable (brotli output) to a Web ReadableStream.
199
- return Readable.toWeb(brotli) as ReadableStream<Uint8Array>;
200
- }
@@ -67,6 +67,8 @@ export function logRequestCompleted(data: {
67
67
  path: string;
68
68
  status: number;
69
69
  durationMs: number;
70
+ /** Number of concurrent in-flight requests (including this one) at completion time. */
71
+ concurrency?: number;
70
72
  }): void {
71
73
  _logger?.info('request completed', withTraceContext(data));
72
74
  }
@@ -82,6 +84,8 @@ export function logSlowRequest(data: {
82
84
  path: string;
83
85
  durationMs: number;
84
86
  threshold: number;
87
+ /** Number of concurrent in-flight requests at the time the slow request completed. */
88
+ concurrency?: number;
85
89
  }): void {
86
90
  _logger?.warn('slow request exceeded threshold', withTraceContext(data));
87
91
  }
@@ -173,11 +173,16 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
173
173
  onPipelineError,
174
174
  } = config;
175
175
 
176
+ // Concurrent request counter — tracks how many requests are in-flight.
177
+ // Logged with each request for diagnosing resource contention.
178
+ let activeRequests = 0;
179
+
176
180
  return async (req: Request): Promise<Response> => {
177
181
  const url = new URL(req.url);
178
182
  const method = req.method;
179
183
  const path = url.pathname;
180
184
  const startTime = performance.now();
185
+ activeRequests++;
181
186
 
182
187
  // Establish per-request trace ID scope (design/17-logging.md §"trace_id is Always Set").
183
188
  // This runs before runWithRequestContext so traceId() is available from the
@@ -215,9 +220,9 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
215
220
  // DevSpanProcessor reads this for tree/summary output.
216
221
  await setSpanAttribute('http.response.status_code', result.status);
217
222
 
218
- // Append Server-Timing header in dev mode.
219
- // At this point, pre-flush phases (proxy, middleware, render)
220
- // have completed and their timing entries are collected.
223
+ // Append Server-Timing header.
224
+ // In dev mode: detailed per-phase breakdown (proxy, middleware, render).
225
+ // In production: single total duration safe to expose, no phase names.
221
226
  // Response.redirect() creates immutable headers, so we must
222
227
  // ensure mutability before writing Server-Timing.
223
228
  if (enableServerTiming) {
@@ -226,6 +231,13 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
226
231
  result = ensureMutableResponse(result);
227
232
  result.headers.set('Server-Timing', serverTiming);
228
233
  }
234
+ } else {
235
+ // Production: emit total request duration only.
236
+ // No phase breakdown — prevents information disclosure
237
+ // while giving browser DevTools useful timing data.
238
+ const totalMs = Math.round(performance.now() - startTime);
239
+ result = ensureMutableResponse(result);
240
+ result.headers.set('Server-Timing', `total;dur=${totalMs}`);
229
241
  }
230
242
 
231
243
  return result;
@@ -235,10 +247,12 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
235
247
  // Post-span: structured production logging
236
248
  const durationMs = Math.round(performance.now() - startTime);
237
249
  const status = response.status;
238
- logRequestCompleted({ method, path, status, durationMs });
250
+ const concurrency = activeRequests;
251
+ activeRequests--;
252
+ logRequestCompleted({ method, path, status, durationMs, concurrency });
239
253
 
240
254
  if (slowRequestMs > 0 && durationMs > slowRequestMs) {
241
- logSlowRequest({ method, path, durationMs, threshold: slowRequestMs });
255
+ logSlowRequest({ method, path, durationMs, threshold: slowRequestMs, concurrency });
242
256
  }
243
257
 
244
258
  return response;
@@ -474,6 +488,16 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
474
488
  markResponseFlushed();
475
489
  return response;
476
490
  } catch (error) {
491
+ // DenySignal leaked from render (e.g. notFound() in metadata()).
492
+ // Return the deny status code instead of 500.
493
+ if (error instanceof DenySignal) {
494
+ return new Response(null, { status: error.status });
495
+ }
496
+ // RedirectSignal leaked from render — honour the redirect.
497
+ if (error instanceof RedirectSignal) {
498
+ responseHeaders.set('Location', error.location);
499
+ return new Response(null, { status: error.status, headers: responseHeaders });
500
+ }
477
501
  logRenderError({ method, path, error });
478
502
  await fireOnRequestError(error, req, 'render');
479
503
  if (onPipelineError && error instanceof Error) onPipelineError(error, 'render');