@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.
- package/dist/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
- package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
- package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
- package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
- package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
- package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-DJDdyEae.js} +157 -313
- package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
- package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
- package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
- package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
- package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
- package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
- package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
- package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
- package/dist/_chunks/logger-CM0clugc.js.map +1 -0
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
- package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
- package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
- package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
- package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
- package/dist/_chunks/tree-match-D2l830j2.js +102 -0
- package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
- package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-DvWDj453.js} +154 -88
- package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +36 -14
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/compress-module.d.ts +5 -4
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +1 -3
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +14 -134
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +3 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +20 -235
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/invalidation-epoch.d.ts +43 -0
- package/dist/cache/invalidation-epoch.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +13 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +17 -9
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli-schema-sync.d.ts +21 -1
- package/dist/cli-schema-sync.d.ts.map +1 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -31
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/hmr.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +13 -4
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +8 -1
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -17
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +30 -33
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +9 -2
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +0 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +0 -5
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +10 -5
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -4
- package/dist/cookies/index.js.map +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +1 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
- package/dist/dev-tools/hydration-overlay.d.ts +11 -0
- package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +1 -2
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/fonts/ast-walk.d.ts +68 -0
- package/dist/fonts/ast-walk.d.ts.map +1 -0
- package/dist/fonts/ast.d.ts +23 -11
- package/dist/fonts/ast.d.ts.map +1 -1
- package/dist/fonts/bundle.d.ts +3 -1
- package/dist/fonts/bundle.d.ts.map +1 -1
- package/dist/fonts/dev-middleware.d.ts +1 -1
- package/dist/fonts/dev-middleware.d.ts.map +1 -1
- package/dist/fonts/fallbacks.d.ts +0 -7
- package/dist/fonts/fallbacks.d.ts.map +1 -1
- package/dist/fonts/google.d.ts +4 -2
- package/dist/fonts/google.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +3 -3
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/fonts/paths.d.ts +43 -0
- package/dist/fonts/paths.d.ts.map +1 -0
- package/dist/fonts/pipeline.d.ts +7 -21
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts +1 -18
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +609 -406
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/params/resolve-schema.d.ts.map +1 -1
- package/dist/plugin-context.d.ts +28 -5
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +13 -5
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +1 -2
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +4 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/file-cache.d.ts +8 -0
- package/dist/routing/file-cache.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +0 -5
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +32 -0
- package/dist/routing/manifest-codegen.d.ts.map +1 -0
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +15 -0
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +7 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/actions.d.ts +0 -14
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +0 -29
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/canonicalize.d.ts +10 -0
- package/dist/server/canonicalize.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +27 -2
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +8 -0
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.d.ts +1 -5
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +238 -349
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +0 -11
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +25 -0
- package/dist/server/render-utils.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -25
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -18
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +6 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/utils/mdx-file.d.ts +15 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -0
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +3 -3
- package/src/adapters/cloudflare-kv-cache.ts +56 -19
- package/src/adapters/compress-module.ts +27 -118
- package/src/adapters/nitro.ts +3 -41
- package/src/adapters/raw-imports.d.ts +5 -0
- package/src/cache/cache-api.ts +9 -1
- package/src/cache/index.ts +5 -2
- package/src/cache/invalidation-epoch.ts +93 -0
- package/src/cache/redis-handler.ts +33 -2
- package/src/cache/timber-cache.ts +109 -53
- package/src/cli-schema-sync.ts +123 -30
- package/src/cli.ts +33 -37
- package/src/client/browser-entry/hmr.ts +0 -11
- package/src/client/browser-entry/hydrate.ts +39 -5
- package/src/client/browser-entry/post-hydration.ts +6 -15
- package/src/client/browser-entry/router-init.ts +98 -3
- package/src/client/error-boundary.tsx +14 -0
- package/src/client/error-reconstituter.tsx +15 -1
- package/src/client/history.ts +10 -24
- package/src/client/navigation-api.ts +14 -9
- package/src/client/navigation-context.ts +1 -21
- package/src/client/navigation-root.tsx +0 -8
- package/src/client/router.ts +18 -8
- package/src/client/rsc-fetch.ts +7 -2
- package/src/client/use-search-params.ts +10 -5
- package/src/config-validation.ts +33 -25
- package/src/cookies/define-cookie.ts +2 -18
- package/src/dev-tools/dev-page-shell.ts +6 -2
- package/src/dev-tools/hydration-overlay.ts +158 -0
- package/src/dev-tools/index.ts +3 -16
- package/src/fonts/ast-walk.ts +235 -0
- package/src/fonts/ast.ts +91 -249
- package/src/fonts/bundle.ts +16 -9
- package/src/fonts/dev-middleware.ts +16 -5
- package/src/fonts/fallbacks.ts +0 -23
- package/src/fonts/google.ts +7 -3
- package/src/fonts/local.ts +7 -6
- package/src/fonts/paths.ts +58 -0
- package/src/fonts/pipeline.ts +27 -48
- package/src/fonts/transform.ts +27 -0
- package/src/fonts/virtual-modules.ts +1 -57
- package/src/index.ts +5 -11
- package/src/params/resolve-schema.ts +2 -34
- package/src/plugin-context.ts +95 -17
- package/src/plugins/cache.ts +64 -17
- package/src/plugins/dev-server.ts +61 -9
- package/src/plugins/fonts.ts +21 -6
- package/src/plugins/routing.ts +5 -348
- package/src/routing/codegen-types.ts +4 -0
- package/src/routing/codegen.ts +66 -8
- package/src/routing/convention-lint.ts +4 -3
- package/src/routing/file-cache.ts +23 -0
- package/src/routing/interception.ts +2 -21
- package/src/routing/manifest-codegen.ts +290 -0
- package/src/routing/scanner.ts +85 -80
- package/src/routing/schema-validation.ts +2 -2
- package/src/schema-bridge.ts +63 -2
- package/src/search-params/define.ts +34 -2
- package/src/search-params/wrappers.ts +21 -3
- package/src/segment-params/define.ts +2 -62
- package/src/server/access-gate.tsx +8 -0
- package/src/server/actions.ts +8 -31
- package/src/server/build-manifest.ts +0 -56
- package/src/server/canonicalize.ts +6 -3
- package/src/server/deny-boundary.ts +106 -13
- package/src/server/deny-renderer.ts +46 -10
- package/src/server/error-boundary-wrapper.ts +1 -14
- package/src/server/html-injector-core.ts +19 -0
- package/src/server/html-injectors.ts +14 -4
- package/src/server/internal.ts +1 -18
- package/src/server/node-stream-transforms.ts +5 -72
- package/src/server/render-utils.ts +31 -0
- package/src/server/route-element-builder.ts +77 -47
- package/src/server/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +2 -12
- package/src/server/rsc-entry/helpers.ts +6 -34
- package/src/server/rsc-entry/index.ts +11 -0
- package/src/server/rsc-entry/render-route.ts +7 -9
- package/src/server/rsc-entry/rsc-payload.ts +2 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/ssr-render.ts +1 -11
- package/src/server/status-code-resolver.ts +3 -2
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/src/shared/href-validation.ts +8 -0
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-DH-M8Poy.js +0 -2
- package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
- package/dist/_chunks/define-DMVhsipD.js.map +0 -1
- package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
- package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
- package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
- package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
- package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
- package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
- package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
- package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
- package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
- package/dist/_chunks/warnings-CNPXaypM.js +0 -125
- package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
- package/dist/client/link-pending-store.d.ts +0 -65
- package/dist/client/link-pending-store.d.ts.map +0 -1
- package/dist/client/nav-link-store.d.ts +0 -27
- package/dist/client/nav-link-store.d.ts.map +0 -1
- package/dist/dev-tools/warnings.d.ts +0 -109
- package/dist/dev-tools/warnings.d.ts.map +0 -1
- package/dist/server/flush.d.ts +0 -74
- package/dist/server/flush.d.ts.map +0 -1
- package/src/client/link-pending-store.ts +0 -116
- package/src/client/nav-link-store.ts +0 -37
- package/src/dev-tools/warnings.ts +0 -238
- package/src/server/flush.ts +0 -183
|
@@ -1,127 +1,36 @@
|
|
|
1
|
-
//
|
|
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
|
-
//
|
|
4
|
-
// written
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
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
|
-
*
|
|
18
|
+
* Compile server/compress.ts into a standalone ESM module.
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
|
|
24
|
-
|
|
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
|
}
|
package/src/adapters/nitro.ts
CHANGED
|
@@ -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
|
-
|
|
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. */
|
package/src/cache/cache-api.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/src/cache/index.ts
CHANGED
|
@@ -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
|
|