@timber-js/app 0.2.0-alpha.146 → 0.2.0-alpha.148

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.
Files changed (297) hide show
  1. package/dist/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
  2. package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
  3. package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
  4. package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
  5. package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
  6. package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
  7. package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-DJDdyEae.js} +157 -313
  8. package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
  9. package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
  10. package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
  11. package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
  12. package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
  13. package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
  14. package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
  15. package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
  16. package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
  17. package/dist/_chunks/logger-CM0clugc.js.map +1 -0
  18. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
  19. package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
  20. package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
  21. package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
  22. package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
  23. package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
  24. package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
  25. package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
  26. package/dist/_chunks/tree-match-D2l830j2.js +102 -0
  27. package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
  28. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
  29. package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-DvWDj453.js} +154 -88
  30. package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
  31. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  32. package/dist/adapters/cloudflare-kv-cache.js +36 -14
  33. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  34. package/dist/adapters/compress-module.d.ts +5 -4
  35. package/dist/adapters/compress-module.d.ts.map +1 -1
  36. package/dist/adapters/nitro.d.ts +1 -3
  37. package/dist/adapters/nitro.d.ts.map +1 -1
  38. package/dist/adapters/nitro.js +14 -134
  39. package/dist/adapters/nitro.js.map +1 -1
  40. package/dist/cache/cache-api.d.ts.map +1 -1
  41. package/dist/cache/index.d.ts +3 -1
  42. package/dist/cache/index.d.ts.map +1 -1
  43. package/dist/cache/index.js +20 -235
  44. package/dist/cache/index.js.map +1 -1
  45. package/dist/cache/invalidation-epoch.d.ts +43 -0
  46. package/dist/cache/invalidation-epoch.d.ts.map +1 -0
  47. package/dist/cache/redis-handler.d.ts +13 -0
  48. package/dist/cache/redis-handler.d.ts.map +1 -1
  49. package/dist/cache/timber-cache.d.ts +17 -9
  50. package/dist/cache/timber-cache.d.ts.map +1 -1
  51. package/dist/cli-schema-sync.d.ts +21 -1
  52. package/dist/cli-schema-sync.d.ts.map +1 -1
  53. package/dist/cli.d.ts +12 -0
  54. package/dist/cli.d.ts.map +1 -1
  55. package/dist/cli.js +26 -31
  56. package/dist/cli.js.map +1 -1
  57. package/dist/client/browser-entry/hmr.d.ts.map +1 -1
  58. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  59. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  60. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  61. package/dist/client/error-boundary.d.ts.map +1 -1
  62. package/dist/client/error-boundary.js +13 -4
  63. package/dist/client/error-boundary.js.map +1 -1
  64. package/dist/client/error-reconstituter.d.ts +8 -1
  65. package/dist/client/error-reconstituter.d.ts.map +1 -1
  66. package/dist/client/history.d.ts +10 -17
  67. package/dist/client/history.d.ts.map +1 -1
  68. package/dist/client/index.js +1 -1
  69. package/dist/client/index.js.map +1 -1
  70. package/dist/client/internal.js +30 -33
  71. package/dist/client/internal.js.map +1 -1
  72. package/dist/client/navigation-api.d.ts +9 -2
  73. package/dist/client/navigation-api.d.ts.map +1 -1
  74. package/dist/client/navigation-context.d.ts +0 -11
  75. package/dist/client/navigation-context.d.ts.map +1 -1
  76. package/dist/client/navigation-root.d.ts +0 -5
  77. package/dist/client/navigation-root.d.ts.map +1 -1
  78. package/dist/client/router.d.ts.map +1 -1
  79. package/dist/client/rsc-fetch.d.ts.map +1 -1
  80. package/dist/client/use-search-params.d.ts +10 -5
  81. package/dist/client/use-search-params.d.ts.map +1 -1
  82. package/dist/codec.js +1 -1
  83. package/dist/config-validation.d.ts.map +1 -1
  84. package/dist/cookies/define-cookie.d.ts.map +1 -1
  85. package/dist/cookies/index.js +2 -4
  86. package/dist/cookies/index.js.map +1 -1
  87. package/dist/dev-tools/dev-page-shell.d.ts +1 -0
  88. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
  89. package/dist/dev-tools/hydration-overlay.d.ts +11 -0
  90. package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
  91. package/dist/dev-tools/index.d.ts +1 -2
  92. package/dist/dev-tools/index.d.ts.map +1 -1
  93. package/dist/fonts/ast-walk.d.ts +68 -0
  94. package/dist/fonts/ast-walk.d.ts.map +1 -0
  95. package/dist/fonts/ast.d.ts +23 -11
  96. package/dist/fonts/ast.d.ts.map +1 -1
  97. package/dist/fonts/bundle.d.ts +3 -1
  98. package/dist/fonts/bundle.d.ts.map +1 -1
  99. package/dist/fonts/dev-middleware.d.ts +1 -1
  100. package/dist/fonts/dev-middleware.d.ts.map +1 -1
  101. package/dist/fonts/fallbacks.d.ts +0 -7
  102. package/dist/fonts/fallbacks.d.ts.map +1 -1
  103. package/dist/fonts/google.d.ts +4 -2
  104. package/dist/fonts/google.d.ts.map +1 -1
  105. package/dist/fonts/local.d.ts +3 -3
  106. package/dist/fonts/local.d.ts.map +1 -1
  107. package/dist/fonts/paths.d.ts +43 -0
  108. package/dist/fonts/paths.d.ts.map +1 -0
  109. package/dist/fonts/pipeline.d.ts +7 -21
  110. package/dist/fonts/pipeline.d.ts.map +1 -1
  111. package/dist/fonts/transform.d.ts.map +1 -1
  112. package/dist/fonts/virtual-modules.d.ts +1 -18
  113. package/dist/fonts/virtual-modules.d.ts.map +1 -1
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +609 -406
  116. package/dist/index.js.map +1 -1
  117. package/dist/params/index.js +1 -1
  118. package/dist/params/resolve-schema.d.ts.map +1 -1
  119. package/dist/plugin-context.d.ts +28 -5
  120. package/dist/plugin-context.d.ts.map +1 -1
  121. package/dist/plugins/cache.d.ts +13 -5
  122. package/dist/plugins/cache.d.ts.map +1 -1
  123. package/dist/plugins/dev-server.d.ts.map +1 -1
  124. package/dist/plugins/fonts.d.ts +1 -2
  125. package/dist/plugins/fonts.d.ts.map +1 -1
  126. package/dist/plugins/routing.d.ts.map +1 -1
  127. package/dist/routing/codegen-types.d.ts +4 -0
  128. package/dist/routing/codegen-types.d.ts.map +1 -1
  129. package/dist/routing/codegen.d.ts.map +1 -1
  130. package/dist/routing/convention-lint.d.ts.map +1 -1
  131. package/dist/routing/file-cache.d.ts +8 -0
  132. package/dist/routing/file-cache.d.ts.map +1 -0
  133. package/dist/routing/index.js +2 -2
  134. package/dist/routing/interception.d.ts +0 -5
  135. package/dist/routing/interception.d.ts.map +1 -1
  136. package/dist/routing/manifest-codegen.d.ts +32 -0
  137. package/dist/routing/manifest-codegen.d.ts.map +1 -0
  138. package/dist/routing/scanner.d.ts.map +1 -1
  139. package/dist/schema-bridge.d.ts +15 -0
  140. package/dist/schema-bridge.d.ts.map +1 -1
  141. package/dist/search-params/define.d.ts.map +1 -1
  142. package/dist/search-params/index.js +7 -3
  143. package/dist/search-params/index.js.map +1 -1
  144. package/dist/search-params/wrappers.d.ts.map +1 -1
  145. package/dist/segment-params/define.d.ts.map +1 -1
  146. package/dist/segment-params/index.js +1 -1
  147. package/dist/server/access-gate.d.ts.map +1 -1
  148. package/dist/server/actions.d.ts +0 -14
  149. package/dist/server/actions.d.ts.map +1 -1
  150. package/dist/server/build-manifest.d.ts +0 -29
  151. package/dist/server/build-manifest.d.ts.map +1 -1
  152. package/dist/server/canonicalize.d.ts +10 -0
  153. package/dist/server/canonicalize.d.ts.map +1 -1
  154. package/dist/server/deny-boundary.d.ts +27 -2
  155. package/dist/server/deny-boundary.d.ts.map +1 -1
  156. package/dist/server/deny-renderer.d.ts +1 -1
  157. package/dist/server/deny-renderer.d.ts.map +1 -1
  158. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  159. package/dist/server/html-injector-core.d.ts +8 -0
  160. package/dist/server/html-injector-core.d.ts.map +1 -1
  161. package/dist/server/html-injectors.d.ts.map +1 -1
  162. package/dist/server/index.js +2 -2
  163. package/dist/server/internal.d.ts +1 -5
  164. package/dist/server/internal.d.ts.map +1 -1
  165. package/dist/server/internal.js +238 -349
  166. package/dist/server/internal.js.map +1 -1
  167. package/dist/server/node-stream-transforms.d.ts +0 -11
  168. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  169. package/dist/server/render-utils.d.ts +25 -0
  170. package/dist/server/render-utils.d.ts.map +1 -0
  171. package/dist/server/route-element-builder.d.ts +11 -25
  172. package/dist/server/route-element-builder.d.ts.map +1 -1
  173. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  174. package/dist/server/rsc-entry/helpers.d.ts +2 -18
  175. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  179. package/dist/server/ssr-render.d.ts.map +1 -1
  180. package/dist/server/status-code-resolver.d.ts +6 -0
  181. package/dist/server/status-code-resolver.d.ts.map +1 -1
  182. package/dist/server/utils/mdx-file.d.ts +15 -0
  183. package/dist/server/utils/mdx-file.d.ts.map +1 -0
  184. package/dist/shared/href-validation.d.ts.map +1 -1
  185. package/dist/shims/image.d.ts +15 -15
  186. package/package.json +3 -3
  187. package/src/adapters/cloudflare-kv-cache.ts +56 -19
  188. package/src/adapters/compress-module.ts +27 -118
  189. package/src/adapters/nitro.ts +3 -41
  190. package/src/adapters/raw-imports.d.ts +5 -0
  191. package/src/cache/cache-api.ts +9 -1
  192. package/src/cache/index.ts +5 -2
  193. package/src/cache/invalidation-epoch.ts +93 -0
  194. package/src/cache/redis-handler.ts +33 -2
  195. package/src/cache/timber-cache.ts +109 -53
  196. package/src/cli-schema-sync.ts +123 -30
  197. package/src/cli.ts +33 -37
  198. package/src/client/browser-entry/hmr.ts +0 -11
  199. package/src/client/browser-entry/hydrate.ts +39 -5
  200. package/src/client/browser-entry/post-hydration.ts +6 -15
  201. package/src/client/browser-entry/router-init.ts +98 -3
  202. package/src/client/error-boundary.tsx +14 -0
  203. package/src/client/error-reconstituter.tsx +15 -1
  204. package/src/client/history.ts +10 -24
  205. package/src/client/navigation-api.ts +14 -9
  206. package/src/client/navigation-context.ts +1 -21
  207. package/src/client/navigation-root.tsx +0 -8
  208. package/src/client/router.ts +18 -8
  209. package/src/client/rsc-fetch.ts +7 -2
  210. package/src/client/use-search-params.ts +10 -5
  211. package/src/config-validation.ts +33 -25
  212. package/src/cookies/define-cookie.ts +2 -18
  213. package/src/dev-tools/dev-page-shell.ts +6 -2
  214. package/src/dev-tools/hydration-overlay.ts +158 -0
  215. package/src/dev-tools/index.ts +3 -16
  216. package/src/fonts/ast-walk.ts +235 -0
  217. package/src/fonts/ast.ts +91 -249
  218. package/src/fonts/bundle.ts +16 -9
  219. package/src/fonts/dev-middleware.ts +16 -5
  220. package/src/fonts/fallbacks.ts +0 -23
  221. package/src/fonts/google.ts +7 -3
  222. package/src/fonts/local.ts +7 -6
  223. package/src/fonts/paths.ts +58 -0
  224. package/src/fonts/pipeline.ts +27 -48
  225. package/src/fonts/transform.ts +27 -0
  226. package/src/fonts/virtual-modules.ts +1 -57
  227. package/src/index.ts +5 -11
  228. package/src/params/resolve-schema.ts +2 -34
  229. package/src/plugin-context.ts +95 -17
  230. package/src/plugins/cache.ts +64 -17
  231. package/src/plugins/dev-server.ts +61 -9
  232. package/src/plugins/fonts.ts +21 -6
  233. package/src/plugins/routing.ts +5 -348
  234. package/src/routing/codegen-types.ts +4 -0
  235. package/src/routing/codegen.ts +66 -8
  236. package/src/routing/convention-lint.ts +4 -3
  237. package/src/routing/file-cache.ts +23 -0
  238. package/src/routing/interception.ts +2 -21
  239. package/src/routing/manifest-codegen.ts +290 -0
  240. package/src/routing/scanner.ts +85 -80
  241. package/src/routing/schema-validation.ts +2 -2
  242. package/src/schema-bridge.ts +63 -2
  243. package/src/search-params/define.ts +34 -2
  244. package/src/search-params/wrappers.ts +21 -3
  245. package/src/segment-params/define.ts +2 -62
  246. package/src/server/access-gate.tsx +8 -0
  247. package/src/server/actions.ts +8 -31
  248. package/src/server/build-manifest.ts +0 -56
  249. package/src/server/canonicalize.ts +6 -3
  250. package/src/server/deny-boundary.ts +106 -13
  251. package/src/server/deny-renderer.ts +46 -10
  252. package/src/server/error-boundary-wrapper.ts +1 -14
  253. package/src/server/html-injector-core.ts +19 -0
  254. package/src/server/html-injectors.ts +14 -4
  255. package/src/server/internal.ts +1 -18
  256. package/src/server/node-stream-transforms.ts +5 -72
  257. package/src/server/render-utils.ts +31 -0
  258. package/src/server/route-element-builder.ts +77 -47
  259. package/src/server/route-matcher.ts +22 -6
  260. package/src/server/rsc-entry/error-renderer.ts +2 -12
  261. package/src/server/rsc-entry/helpers.ts +6 -34
  262. package/src/server/rsc-entry/index.ts +11 -0
  263. package/src/server/rsc-entry/render-route.ts +7 -9
  264. package/src/server/rsc-entry/rsc-payload.ts +2 -1
  265. package/src/server/rsc-entry/ssr-renderer.ts +2 -2
  266. package/src/server/ssr-render.ts +1 -11
  267. package/src/server/status-code-resolver.ts +3 -2
  268. package/src/server/tree-match.ts +15 -5
  269. package/src/server/utils/mdx-file.ts +22 -0
  270. package/src/shared/href-validation.ts +8 -0
  271. package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
  272. package/dist/_chunks/cli-schema-sync-DH-M8Poy.js +0 -2
  273. package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
  274. package/dist/_chunks/define-DMVhsipD.js.map +0 -1
  275. package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
  276. package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
  277. package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
  278. package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
  279. package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
  280. package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
  281. package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
  282. package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
  283. package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
  284. package/dist/_chunks/warnings-CNPXaypM.js +0 -125
  285. package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
  286. package/dist/client/link-pending-store.d.ts +0 -65
  287. package/dist/client/link-pending-store.d.ts.map +0 -1
  288. package/dist/client/nav-link-store.d.ts +0 -27
  289. package/dist/client/nav-link-store.d.ts.map +0 -1
  290. package/dist/dev-tools/warnings.d.ts +0 -109
  291. package/dist/dev-tools/warnings.d.ts.map +0 -1
  292. package/dist/server/flush.d.ts +0 -74
  293. package/dist/server/flush.d.ts.map +0 -1
  294. package/src/client/link-pending-store.ts +0 -116
  295. package/src/client/nav-link-store.ts +0 -37
  296. package/src/dev-tools/warnings.ts +0 -238
  297. package/src/server/flush.ts +0 -183
@@ -1,127 +1,36 @@
1
- // Generated compression module template for self-hosted deployments.
1
+ // Compiles server/compress.ts into a standalone ESM module (_compress.mjs)
2
+ // for self-hosted deployments. The adapter writes this to the build output
3
+ // during buildOutput(). It's imported by the Nitro entry and preview server
4
+ // at runtime.
2
5
  //
3
- // This file generates a standalone ESM module (_compress.mjs) that is
4
- // written to the build output during adapter buildOutput(). It's imported
5
- // by the Nitro entry and preview server at runtime.
6
- //
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.
10
- // Cloudflare Workers don't need this — the edge auto-compresses.
11
- //
12
- // See design/25-production-deployments.md.
6
+ // By compiling the actual TypeScript source rather than maintaining a
7
+ // hand-written string template, any fix to server/compress.ts automatically
8
+ // flows to the production module. See design/18-build-system.md ("entry
9
+ // modules are real TypeScript files, not codegen strings").
10
+
11
+ import { transformWithOxc } from 'vite';
12
+ // Vite's ?raw import embeds the file as a string at build time — works
13
+ // in both dev (Vite resolves the TS source) and the published package
14
+ // (the string is inlined into the compiled adapter entry).
15
+ import compressSource from '../server/compress.ts?raw';
13
16
 
14
17
  /**
15
- * Generate a standalone ESM module that exports compressResponse().
18
+ * Compile server/compress.ts into a standalone ESM module.
16
19
  *
17
- * Written to `_compress.mjs` during buildOutput. Imported by the Nitro entry
18
- * and preview server.
20
+ * Uses Vite's OXC transform to strip TypeScript types from the
21
+ * embedded source. The result is written to `_compress.mjs` during
22
+ * buildOutput.
19
23
  *
20
24
  * @internal Exported for testing.
21
25
  */
22
- export function generateCompressModule(): string {
23
- return `// Generated by @timber-js/app — response compression for self-hosted deployments.
24
- // Do not edit — this file is regenerated on each build.
25
- // Uses node:zlib createGzip() (C++ native) on Node.js, falls back to
26
- // CompressionStream (Web API) on other runtimes. Brotli is left to CDNs/reverse
27
- // proxies — at streaming quality levels its ratio advantage is marginal.
28
- import { Readable, pipeline } from 'node:stream';
29
- import { createGzip, constants } from 'node:zlib';
30
-
31
- const COMPRESSIBLE_TYPES = new Set([
32
- 'text/html', 'text/css', 'text/plain', 'text/xml', 'text/javascript',
33
- 'text/x-component', 'application/json', 'application/javascript',
34
- 'application/xml', 'application/xhtml+xml', 'application/rss+xml',
35
- 'application/atom+xml', 'image/svg+xml',
36
- ]);
37
-
38
- const NO_COMPRESS_STATUSES = new Set([204, 304]);
39
-
40
- function negotiateEncoding(acceptEncoding) {
41
- if (!acceptEncoding) return null;
42
- // Parse tokens with quality values. Per RFC 9110 §12.5.3, q=0 means
43
- // "not acceptable" — the client explicitly rejects that encoding.
44
- // Brotli (br) is intentionally not handled at the application level.
45
- const parts = acceptEncoding.split(',');
46
- for (const part of parts) {
47
- const [token, ...params] = part.split(';');
48
- const name = token.trim().toLowerCase();
49
- if (name !== 'gzip') continue;
50
- let qValue = 1;
51
- for (const param of params) {
52
- const trimmed = param.trim().toLowerCase();
53
- if (trimmed.startsWith('q=')) {
54
- qValue = parseFloat(trimmed.slice(2));
55
- if (Number.isNaN(qValue)) qValue = 1;
56
- break;
57
- }
58
- }
59
- if (qValue > 0) return 'gzip';
60
- }
61
- return null;
62
- }
63
-
64
- function shouldCompress(response) {
65
- if (!response.body) return false;
66
- if (NO_COMPRESS_STATUSES.has(response.status)) return false;
67
- if (response.headers.has('Content-Encoding')) return false;
68
- const contentType = response.headers.get('Content-Type');
69
- if (!contentType) return false;
70
- const mimeType = contentType.split(';')[0].trim().toLowerCase();
71
- if (mimeType === 'text/event-stream') return false;
72
- return COMPRESSIBLE_TYPES.has(mimeType);
73
- }
74
-
75
- function compressWithGzip(body) {
76
- // Use node:zlib (C++ native) for gzip compression. The Web Streams
77
- // CompressionStream works but every chunk crosses the JS/Promise boundary.
78
- // node:zlib.createGzip() compresses entirely in C++ with zero per-chunk
79
- // Promise overhead.
80
- //
81
- // Convert: Web ReadableStream → Node Readable → pipe through gzip →
82
- // Node Readable → Readable.toWeb() → Web ReadableStream
83
- try {
84
- const nodeReadable = Readable.fromWeb(body);
85
- // Z_SYNC_FLUSH ensures each chunk is flushed immediately so the browser
86
- // receives the HTML shell before Suspense boundaries resolve. Without it,
87
- // gzip buffers internally and breaks streaming.
88
- const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });
89
- // pipeline() instead of bare pipe(): pipe() does not forward source
90
- // errors, so an erroring body would emit an unhandled 'error' event and
91
- // crash the process. pipeline() destroys gzip on source error — the web
92
- // stream rejects — and propagates consumer cancel back to the source
93
- // body. Errors reach the consumer via the web stream; the callback only
94
- // marks them handled at the Node layer.
95
- pipeline(nodeReadable, gzip, () => {});
96
- return Readable.toWeb(gzip);
97
- } catch {
98
- // Fallback: CompressionStream (CF Workers, or if node:stream unavailable)
99
- return body.pipeThrough(new CompressionStream('gzip'));
100
- }
101
- }
102
-
103
- export function compressResponse(request, response) {
104
- if (!shouldCompress(response)) return response;
105
- const acceptEncoding = request.headers.get('Accept-Encoding') || '';
106
- const encoding = negotiateEncoding(acceptEncoding);
107
- if (!encoding) return response;
108
- const compressedBody = compressWithGzip(response.body);
109
- const headers = new Headers(response.headers);
110
- headers.set('Content-Encoding', encoding);
111
- headers.delete('Content-Length');
112
- const existingVary = headers.get('Vary');
113
- if (existingVary) {
114
- if (!existingVary.toLowerCase().includes('accept-encoding')) {
115
- headers.set('Vary', existingVary + ', Accept-Encoding');
116
- }
117
- } else {
118
- headers.set('Vary', 'Accept-Encoding');
119
- }
120
- return new Response(compressedBody, {
121
- status: response.status,
122
- statusText: response.statusText,
123
- headers,
26
+ export async function generateCompressModule(): Promise<string> {
27
+ const result = await transformWithOxc(compressSource, 'compress.ts', {
28
+ lang: 'ts',
124
29
  });
125
- }
126
- `;
30
+
31
+ return (
32
+ '// Generated by @timber-js/app — compiled from server/compress.ts.\n' +
33
+ '// Do not edit — this file is regenerated on each build.\n' +
34
+ result.code
35
+ );
127
36
  }
@@ -200,7 +200,7 @@ export function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter
200
200
  // Write the compression helper module for runtime use.
201
201
  // See design/25-production-deployments.md — self-hosted deployments
202
202
  // need application-level compression (Cloudflare handles it at the edge).
203
- await writeFile(join(outDir, '_compress.mjs'), generateCompressModule());
203
+ await writeFile(join(outDir, '_compress.mjs'), await generateCompressModule());
204
204
 
205
205
  // Prepend the manifest assignment directly into the RSC entry so
206
206
  // globalThis.__TIMBER_BUILD_MANIFEST__ is set before any module reads it.
@@ -257,8 +257,7 @@ export function generateNitroEntry(
257
257
  buildDir: string,
258
258
  outDir: string,
259
259
  preset: NitroPreset,
260
- compress = true,
261
- hasManifestInit = false
260
+ compress = true
262
261
  ): string {
263
262
  // The RSC entry is the main request handler — it exports the fetch handler as default.
264
263
  // rsc/ is copied into the nitro dir so the import is local.
@@ -315,11 +314,6 @@ export function generateNitroEntry(
315
314
  handlerCall = ` const webResponse = await handler(webRequest)`;
316
315
  }
317
316
 
318
- // Build manifest init must be imported before the handler so that
319
- // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.
320
- // ESM guarantees imports are evaluated in order.
321
- const manifestImport = hasManifestInit ? "import './_timber-manifest-init.js'\n" : '';
322
-
323
317
  // Import runWithWaitUntil only when the preset supports it.
324
318
  const waitUntilImport = supportsWaitUntil ? ', runWithWaitUntil' : '';
325
319
 
@@ -329,7 +323,7 @@ export function generateNitroEntry(
329
323
  return `// Generated by @timber-js/app/adapters/nitro
330
324
  // Do not edit — this file is regenerated on each build.
331
325
 
332
- ${manifestImport}import handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'
326
+ import handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'
333
327
  ${compressImport}
334
328
  // Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
335
329
  // See design/25-production-deployments.md §"TIMBER_RUNTIME".
@@ -349,38 +343,6 @@ ${handlerCall}
349
343
  `;
350
344
  }
351
345
 
352
- /** @internal Exported for testing. */
353
- export function generateNitroConfig(
354
- preset: NitroPreset,
355
- userConfig?: Record<string, unknown>
356
- ): string {
357
- const presetConfig = PRESET_CONFIGS[preset];
358
-
359
- const config: Record<string, unknown> = {
360
- preset: presetConfig.nitroPreset,
361
- entry: './entry.ts',
362
- output: { dir: presetConfig.outputDir },
363
- // Static asset cache headers — hashed assets are immutable, others get 1h.
364
- // See design/25-production-deployments.md §"CDN / Edge Cache"
365
- routeRules: {
366
- '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },
367
- '/_timber/fonts/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },
368
- },
369
- ...presetConfig.extraConfig,
370
- ...userConfig,
371
- };
372
-
373
- const configJson = JSON.stringify(config, null, 2);
374
-
375
- return `// Generated by @timber-js/app/adapters/nitro
376
- // Do not edit — this file is regenerated on each build.
377
-
378
- import { defineNitroConfig } from 'nitro/config'
379
-
380
- export default defineNitroConfig(${configJson})
381
- `;
382
- }
383
-
384
346
  // ─── Preview ─────────────────────────────────────────────────────────────────
385
347
 
386
348
  /** Presets that produce a locally-runnable server entry. */
@@ -0,0 +1,5 @@
1
+ // Vite ?raw import — embeds a file as a string at build time.
2
+ declare module '*?raw' {
3
+ const content: string;
4
+ export default content;
5
+ }
@@ -1,6 +1,7 @@
1
1
  import type { CacheOptions } from './index';
2
2
  import { createCache } from './timber-cache';
3
3
  import { getCacheHandler } from './handler-store';
4
+ import { recordInvalidation } from './invalidation-epoch';
4
5
 
5
6
  /**
6
7
  * Public caching API: `cache(fn, opts)`.
@@ -23,7 +24,11 @@ export function cache<Fn extends (...args: any[]) => Promise<any>>(
23
24
  opts: CacheOptions<Fn>,
24
25
  stableId?: string
25
26
  ): Fn {
26
- return createCache(fn, opts, getCacheHandler(), stableId);
27
+ // Pass the getter, not a resolved handler: the wrapper re-resolves it on
28
+ // every call, so module-scope wrappers created before the framework's boot
29
+ // wiring calls setCacheHandler() still pick up the configured handler
30
+ // instead of permanently binding the in-memory fallback (TIM-1029).
31
+ return createCache(fn, opts, getCacheHandler, stableId);
27
32
  }
28
33
 
29
34
  /**
@@ -35,5 +40,8 @@ export function cache<Fn extends (...args: any[]) => Promise<any>>(
35
40
  * ```
36
41
  */
37
42
  cache.invalidate = async function invalidate(opts: { key?: string; tag?: string }): Promise<void> {
43
+ // Record before the handler delete so a cached fn in flight skips its
44
+ // post-resolve set instead of resurrecting the invalidated value (TIM-1028).
45
+ recordInvalidation(opts);
38
46
  await getCacheHandler().invalidate(opts);
39
47
  };
@@ -42,12 +42,14 @@ export class MemoryCacheHandler implements CacheHandler {
42
42
  private maxBytes: number | undefined;
43
43
  private maxEntryBytes: number | undefined;
44
44
  private currentBytes = 0;
45
+ private readonly _trackBytes: boolean;
45
46
 
46
47
  constructor(opts?: MemoryCacheHandlerOptions) {
47
48
  // maxEntries takes precedence over deprecated maxSize
48
49
  this.maxEntries = opts?.maxEntries ?? opts?.maxSize ?? 1000;
49
50
  this.maxBytes = opts?.maxBytes;
50
51
  this.maxEntryBytes = opts?.maxEntryBytes;
52
+ this._trackBytes = this.maxBytes !== undefined || this.maxEntryBytes !== undefined;
51
53
  }
52
54
 
53
55
  async get(key: string) {
@@ -63,7 +65,7 @@ export class MemoryCacheHandler implements CacheHandler {
63
65
  }
64
66
 
65
67
  async set(key: string, value: unknown, opts: { ttl: number; tags: string[] }) {
66
- const byteSize = estimateByteSize(value);
68
+ const byteSize = this._trackBytes ? estimateByteSize(value) : 0;
67
69
 
68
70
  // Reject entries exceeding per-entry byte limit
69
71
  if (this.maxEntryBytes !== undefined && byteSize > this.maxEntryBytes) {
@@ -125,7 +127,7 @@ export class MemoryCacheHandler implements CacheHandler {
125
127
  return this.store.size;
126
128
  }
127
129
 
128
- /** Estimated total byte size of all cached values. */
130
+ /** Estimated total byte size of all cached values. Only tracked when maxBytes or maxEntryBytes is configured; returns 0 otherwise. */
129
131
  get bytes(): number {
130
132
  return this.currentBytes;
131
133
  }
@@ -145,6 +147,7 @@ export { RedisCacheHandler } from './redis-handler';
145
147
  export type { RedisClient } from './redis-handler';
146
148
  export { cache } from './cache-api';
147
149
  export { setCacheHandler, getCacheHandler } from './handler-store';
150
+ export { setDefaultSingleflightTimeout } from './timber-cache';
148
151
  // NOTE: registerCachedFunction (runtime for 'use cache' directive) removed.
149
152
  // Future feature pending design doc. See design/06-caching.md.
150
153
  export { estimateByteSize } from './sizeof';
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Per-process invalidation epoch tracking for timber.cache (TIM-1028).
3
+ *
4
+ * handler.set for a cache miss runs after `fn()` resolves. An explicit
5
+ * `cache.invalidate({ tag })` / `revalidateTag` that lands while fn is in
6
+ * flight would otherwise be silently undone — the just-invalidated value
7
+ * re-stored as fresh for the full TTL, resurrecting pre-mutation data.
8
+ *
9
+ * Every invalidation bumps a monotonic epoch and records it against the
10
+ * invalidated key/tag. Before storing a result, the cache wrapper checks
11
+ * whether its key or any of its tags were invalidated after fn started and
12
+ * skips the write if so. Skipping is always safe: the only cost is one
13
+ * extra cache miss on the next call.
14
+ *
15
+ * Scope: per-process, like the singleflight map. An invalidation issued by
16
+ * another instance against a shared handler (Redis/KV) during fn flight is
17
+ * not observable here — closing that window fully would require store-level
18
+ * versioning, which no CacheHandler backend offers.
19
+ */
20
+
21
+ let epoch = 0;
22
+
23
+ /**
24
+ * Last-invalidated epoch per `key:<k>` / `tag:<t>` entry. Insertion order is
25
+ * epoch order (entries are re-inserted on update), so the oldest entry is
26
+ * always first — pruning evicts lowest epochs.
27
+ */
28
+ const lastInvalidated = new Map<string, number>();
29
+
30
+ /**
31
+ * Bound on tracked entries so high-cardinality tag invalidation (e.g.
32
+ * per-user tags) cannot grow this map without limit.
33
+ */
34
+ const MAX_TRACKED_ENTRIES = 10_000;
35
+
36
+ /**
37
+ * Highest epoch evicted from the map. Any execution that started at or
38
+ * before this epoch conservatively counts as invalidated — history was
39
+ * truncated, so we can no longer prove it wasn't.
40
+ */
41
+ let evictedEpochFloor = 0;
42
+
43
+ /** The current epoch. Capture before running fn(), pass to wasInvalidatedSince. */
44
+ export function currentInvalidationEpoch(): number {
45
+ return epoch;
46
+ }
47
+
48
+ /**
49
+ * Record an invalidation. Call BEFORE handler.invalidate() so an execution
50
+ * whose set-check races the invalidation errs toward skipping the write.
51
+ */
52
+ export function recordInvalidation(opts: { key?: string; tag?: string }): void {
53
+ epoch++;
54
+ if (opts.key) track(`key:${opts.key}`);
55
+ if (opts.tag) track(`tag:${opts.tag}`);
56
+ }
57
+
58
+ function track(entry: string): void {
59
+ lastInvalidated.delete(entry);
60
+ lastInvalidated.set(entry, epoch);
61
+ while (lastInvalidated.size > MAX_TRACKED_ENTRIES) {
62
+ const oldest = lastInvalidated.entries().next().value;
63
+ if (oldest === undefined) break;
64
+ evictedEpochFloor = Math.max(evictedEpochFloor, oldest[1]);
65
+ lastInvalidated.delete(oldest[0]);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Whether the key or any tag was invalidated after `startEpoch` (the epoch
71
+ * captured when fn started executing).
72
+ */
73
+ export function wasInvalidatedSince(startEpoch: number, key: string, tags: string[]): boolean {
74
+ return lastInvalidationEpochFor(key, tags) > startEpoch;
75
+ }
76
+
77
+ /**
78
+ * The highest epoch at which the key or any of the tags was invalidated
79
+ * (0 if never). Used to scope the singleflight key: callers arriving after
80
+ * an invalidation must not coalesce onto a flight that started before it —
81
+ * they would receive the pre-invalidation result the invalidation was
82
+ * issued to discard.
83
+ */
84
+ export function lastInvalidationEpochFor(key: string, tags: string[]): number {
85
+ let last = evictedEpochFloor;
86
+ const keyEpoch = lastInvalidated.get(`key:${key}`);
87
+ if (keyEpoch !== undefined && keyEpoch > last) last = keyEpoch;
88
+ for (const tag of tags) {
89
+ const tagEpoch = lastInvalidated.get(`tag:${tag}`);
90
+ if (tagEpoch !== undefined && tagEpoch > last) last = tagEpoch;
91
+ }
92
+ return last;
93
+ }
@@ -23,7 +23,9 @@ import type { CacheHandler } from './index';
23
23
  * expire: (k, s) => redis.expire(k, s),
24
24
  * del: (k) => redis.del(k),
25
25
  * sadd: (k, ...m) => redis.sadd(k, ...m),
26
+ * srem: (k, ...m) => redis.srem(k, ...m),
26
27
  * smembers: (k) => redis.smembers(k),
28
+ * ttl: (k) => redis.ttl(k),
27
29
  * };
28
30
  * ```
29
31
  *
@@ -38,7 +40,9 @@ import type { CacheHandler } from './index';
38
40
  * expire: (k, s) => redis.expire(k, s),
39
41
  * del: (k) => redis.del(k),
40
42
  * sadd: (k, ...m) => redis.sAdd(k, m),
43
+ * srem: (k, ...m) => redis.sRem(k, m),
41
44
  * smembers: (k) => redis.sMembers(k),
45
+ * ttl: (k) => redis.ttl(k),
42
46
  * };
43
47
  * ```
44
48
  *
@@ -54,7 +58,9 @@ import type { CacheHandler } from './index';
54
58
  * expire: (k, s) => redis.expire(k, s),
55
59
  * del: (k) => redis.del(k),
56
60
  * sadd: (k, ...m) => redis.sadd(k, m[0], ...m.slice(1)),
61
+ * srem: (k, ...m) => redis.srem(k, m[0], ...m.slice(1)),
57
62
  * smembers: (k) => redis.smembers(k),
63
+ * ttl: (k) => redis.ttl(k),
58
64
  * };
59
65
  * ```
60
66
  */
@@ -72,8 +78,15 @@ export interface RedisClient {
72
78
  del(key: string): Promise<unknown>;
73
79
  /** `SADD key member [member ...]` — add members to a set. */
74
80
  sadd(key: string, ...members: string[]): Promise<unknown>;
81
+ /** `SREM key member [member ...]` — remove members from a set. */
82
+ srem(key: string, ...members: string[]): Promise<unknown>;
75
83
  /** `SMEMBERS key` — all members of a set. */
76
84
  smembers(key: string): Promise<string[]>;
85
+ /**
86
+ * `TTL key` — remaining time-to-live in seconds. Returns -2 if the key does
87
+ * not exist, -1 if the key exists but has no expiry.
88
+ */
89
+ ttl(key: string): Promise<number>;
77
90
  }
78
91
 
79
92
  const KEY_PREFIX = 'timber:cache:';
@@ -119,7 +132,7 @@ export class RedisCacheHandler implements CacheHandler {
119
132
  async set(key: string, value: unknown, opts: { ttl: number; tags: string[] }): Promise<void> {
120
133
  const ck = this.cacheKey(key);
121
134
  const expiresAt = Date.now() + opts.ttl * 1000;
122
- const payload = JSON.stringify({ value, expiresAt });
135
+ const payload = JSON.stringify({ value, expiresAt, tags: opts.tags });
123
136
 
124
137
  // Redis TTL with generous margin beyond the logical TTL to allow SWR reads
125
138
  // on stale entries. The logical staleness is determined by expiresAt.
@@ -128,14 +141,32 @@ export class RedisCacheHandler implements CacheHandler {
128
141
  const redisTtlSeconds = Math.max(opts.ttl * 2 + 60, 120);
129
142
  await this.client.set(ck, payload, redisTtlSeconds);
130
143
 
131
- // Track key membership in each tag set
144
+ // Track key membership in each tag set, and expire the set so it doesn't
145
+ // outlive all its members. Only extend the TTL, never shorten — a short-TTL
146
+ // entry must not shrink a tag set that already contains long-TTL entries,
147
+ // otherwise invalidate({tag}) would miss the long-lived entries.
132
148
  for (const tag of opts.tags) {
133
149
  await this.client.sadd(this.tagKey(tag), key);
150
+ const currentTtl = await this.client.ttl(this.tagKey(tag));
151
+ // Only extend, never shorten. -2 = key doesn't exist (rare race after
152
+ // sadd), -1 = no expiry (new set or legacy — set initial TTL), positive
153
+ // but shorter = extend. A longer current TTL means a longer-lived entry
154
+ // set it first — don't shorten.
155
+ if (currentTtl < redisTtlSeconds) {
156
+ await this.client.expire(this.tagKey(tag), redisTtlSeconds);
157
+ }
134
158
  }
135
159
  }
136
160
 
137
161
  async invalidate(opts: { key?: string; tag?: string }): Promise<void> {
138
162
  if (opts.key) {
163
+ const raw = await this.client.get(this.cacheKey(opts.key));
164
+ if (raw !== null) {
165
+ const entry = JSON.parse(raw) as { tags?: string[] };
166
+ if (entry.tags) {
167
+ await Promise.all(entry.tags.map((tag) => this.client.srem(this.tagKey(tag), opts.key!)));
168
+ }
169
+ }
139
170
  await this.client.del(this.cacheKey(opts.key));
140
171
  }
141
172