@rangojs/router 0.17.0 → 0.18.0
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/README.md +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +98 -14
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-snapshot.d.ts +4 -3
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/types.d.ts +207 -43
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
- package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
- package/dist/types/rsc/shell-capture.d.ts +146 -15
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/types.d.ts +7 -0
- package/dist/types/server/context.d.ts +109 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/request-context.d.ts +159 -33
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +46 -9
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +123 -51
- package/skills/caching/SKILL.md +253 -23
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +11 -6
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +44 -32
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +8 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/state.md +3 -1
- package/skills/loader/SKILL.md +161 -39
- package/skills/middleware/SKILL.md +16 -9
- package/skills/migrate-nextjs/SKILL.md +67 -36
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +64 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +655 -363
- package/skills/prerender/SKILL.md +27 -14
- package/skills/rango/SKILL.md +52 -33
- package/skills/route/SKILL.md +12 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +25 -21
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/loader.md +18 -1
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/theme/SKILL.md +52 -4
- package/skills/typesafety/env-and-bindings.md +10 -2
- package/skills/use-cache/SKILL.md +39 -16
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/rsc-router.tsx +5 -0
- package/src/cache/cache-key-utils.ts +22 -2
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +34 -7
- package/src/cache/cache-scope.ts +345 -33
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/handle-snapshot.ts +4 -3
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +104 -19
- package/src/cache/types.ts +211 -43
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/prerender/build-shell-capture.ts +65 -11
- package/src/route-definition/dsl-helpers.ts +4 -3
- package/src/router/handler-context.ts +38 -4
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +19 -7
- package/src/router/loader-resolution.ts +63 -32
- package/src/router/match-api.ts +28 -9
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/cache-lookup.ts +17 -0
- package/src/router/match-middleware/cache-store.ts +31 -11
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +23 -4
- package/src/router/prerender-match.ts +6 -0
- package/src/router/segment-resolution/fresh.ts +18 -9
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +227 -56
- package/src/router/segment-resolution/loader-snapshot.ts +203 -51
- package/src/router/segment-resolution/mask-nested.ts +136 -42
- package/src/router/segment-resolution/revalidation.ts +27 -10
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/rsc-rendering.ts +601 -181
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +60 -6
- package/src/rsc/shell-capture.ts +1001 -250
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/types.ts +7 -0
- package/src/server/context.ts +219 -28
- package/src/server/cookie-store.ts +87 -62
- package/src/server/request-context.ts +263 -38
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/run-loader.ts +63 -5
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/types/handler-context.ts +14 -1
- package/src/urls/pattern-types.ts +22 -18
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/router-discovery.ts +14 -4
|
@@ -53,30 +53,34 @@ export interface PartialPrerenderProps {
|
|
|
53
53
|
/**
|
|
54
54
|
* Operational tags attached to the captured shell entry for
|
|
55
55
|
* `updateTag()`/`revalidateTag()`-driven eviction. UNIONED with the tags the
|
|
56
|
-
* capture
|
|
56
|
+
* capture collects: its doc record's tags (what the handler layer and the
|
|
57
|
+
* server components it rendered recorded) and its bake-lane loaders'.
|
|
57
58
|
*/
|
|
58
59
|
tags?: string[];
|
|
59
60
|
/**
|
|
60
|
-
* Upper bound (serialized UTF-8 bytes) on the
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
61
|
+
* Upper bound (serialized UTF-8 bytes) on the loader pins in the capture
|
|
62
|
+
* data snapshot: the loader records and the cache values loaders read,
|
|
63
|
+
* duplicated into the shell entry. The doc record (the handler layer every
|
|
64
|
+
* HIT replays) is exempt. Over the cap the pins are dropped: the shell is
|
|
65
|
+
* still stored and every HIT still replays the handler layer, but the
|
|
66
|
+
* pinned loaders read the live store, so drifted cached content can
|
|
67
|
+
* hydration-mismatch and be repaired client-side. Reported once per key.
|
|
68
|
+
* Defaults to 8 MiB. The whole entry is bounded separately by the store's
|
|
69
|
+
* value limit (Cloudflare KV: 25 MiB); an entry over it is refused.
|
|
68
70
|
*/
|
|
69
71
|
maxSnapshotBytes?: number;
|
|
70
72
|
/**
|
|
71
|
-
* Capture settle budget in MILLISECONDS (default 15000).
|
|
72
|
-
* background capture: the
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
73
|
+
* Capture settle budget in MILLISECONDS (default 15000). One deadline for
|
|
74
|
+
* the whole background capture: the match (the handlers, and the loaders
|
|
75
|
+
* they await), the wait for handler output (handle pushes and the promises
|
|
76
|
+
* nested in them, bake-lane loaders, and the doc record's encode, which
|
|
77
|
+
* awaits handler promises and async server components), AND the fizz
|
|
78
|
+
* prerender. Everything a handler produces is
|
|
79
|
+
* awaited and baked into the stored shell. Declare it to tighten the
|
|
80
|
+
* budget below the default or when a route's shell material takes longer
|
|
81
|
+
* than 15s to settle. A budget that expires before the doc record is
|
|
82
|
+
* written REFUSES the capture (the route stays MISS with the once-per-key
|
|
83
|
+
* warning): a shell with missing handler output is never stored. Capture is background work
|
|
80
84
|
* (waitUntil), so a longer budget costs latency-to-HIT only, never a served
|
|
81
85
|
* response; the platform waitUntil lifetime (workerd: ~30s past response
|
|
82
86
|
* completion) is the physical ceiling — the default's derivation lives in
|
|
@@ -5,9 +5,36 @@
|
|
|
5
5
|
* environment. Delegates to extracted modules for discovery, route types
|
|
6
6
|
* generation, virtual module codegen, and bundle post-processing.
|
|
7
7
|
*/
|
|
8
|
-
import type { Plugin } from "vite";
|
|
9
|
-
import { VIRTUAL_ROUTES_MANIFEST_ID, type PluginOptions } from "./discovery/state.js";
|
|
8
|
+
import type { Plugin, ViteDevServer } from "vite";
|
|
9
|
+
import { VIRTUAL_ROUTES_MANIFEST_ID, type DiscoveryState, type PluginOptions } from "./discovery/state.js";
|
|
10
10
|
export { VIRTUAL_ROUTES_MANIFEST_ID };
|
|
11
|
+
/**
|
|
12
|
+
* Create a minimal Vite server for router discovery.
|
|
13
|
+
*
|
|
14
|
+
* Both dev-mode prerender and build-mode discovery need a temp RSC server
|
|
15
|
+
* to import user router files via module runner. This factory centralizes
|
|
16
|
+
* the shared config and the mode-specific differences:
|
|
17
|
+
* - Dev: path-based IDs (no forceBuild), separate cacheDir
|
|
18
|
+
* - Build: hashed IDs (forceBuild), hashClientRefs for production bundles
|
|
19
|
+
*
|
|
20
|
+
* Returns the ViteDevServer instance. Callers access .environments.rsc as needed.
|
|
21
|
+
* @internal Exported for tests.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createTempRscServer(state: DiscoveryState, options?: {
|
|
24
|
+
forceBuild?: boolean;
|
|
25
|
+
cacheDir?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Serve the REAL rango SSR entry (getVirtualEntrySSR) for
|
|
28
|
+
* "virtual:entry-ssr" instead of the discovery stub, so the dev
|
|
29
|
+
* /__rsc_shell endpoint can drive captureShellHTML in this server's SSR
|
|
30
|
+
* realm. Dev-correct by construction: the entry's bootstrap resolves to
|
|
31
|
+
* plugin-rsc's stable virtual browser-entry URL, which the MAIN dev
|
|
32
|
+
* server serves to the browser. Loaded lazily — discovery never imports
|
|
33
|
+
* the SSR entry, so the temp server stays as light as before until a
|
|
34
|
+
* shell capture actually runs.
|
|
35
|
+
*/
|
|
36
|
+
realSsrEntry?: boolean;
|
|
37
|
+
}): Promise<ViteDevServer>;
|
|
11
38
|
/**
|
|
12
39
|
* Plugin that discovers router instances at dev/build time via the RSC environment.
|
|
13
40
|
*
|
package/dist/vite/index.js
CHANGED
|
@@ -3802,7 +3802,7 @@ import { resolve } from "node:path";
|
|
|
3802
3802
|
// package.json
|
|
3803
3803
|
var package_default = {
|
|
3804
3804
|
name: "@rangojs/router",
|
|
3805
|
-
version: "0.
|
|
3805
|
+
version: "0.18.0",
|
|
3806
3806
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
3807
3807
|
keywords: [
|
|
3808
3808
|
"react",
|
|
@@ -6147,6 +6147,9 @@ import React from "react";
|
|
|
6147
6147
|
// src/server/context.ts
|
|
6148
6148
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
6149
6149
|
|
|
6150
|
+
// src/rsc/shell-capture-constants.ts
|
|
6151
|
+
var SHELL_CAPTURE_MAX_WAIT_MS = 15e3;
|
|
6152
|
+
|
|
6150
6153
|
// src/cache/cache-exec-scope.ts
|
|
6151
6154
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6152
6155
|
|
|
@@ -6170,19 +6173,23 @@ var LOADER_SCOPE_KEY = /* @__PURE__ */ Symbol.for("rangojs-router:loader-scope")
|
|
|
6170
6173
|
var loaderScopeALS = globalThis[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage2();
|
|
6171
6174
|
var LOADER_BODY_SCOPE_KEY = /* @__PURE__ */ Symbol.for("rangojs-router:loader-body-scope");
|
|
6172
6175
|
var loaderBodyScopeALS = globalThis[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage2();
|
|
6176
|
+
function getLoaderBodyTags() {
|
|
6177
|
+
return loaderBodyScopeALS.getStore()?.tags;
|
|
6178
|
+
}
|
|
6179
|
+
function getCurrentLoaderBodyId() {
|
|
6180
|
+
return loaderBodyScopeALS.getStore()?.loaderId;
|
|
6181
|
+
}
|
|
6173
6182
|
var PUSH_CALLBACK_SCOPE_KEY = /* @__PURE__ */ Symbol.for(
|
|
6174
6183
|
"rangojs-router:push-callback-scope"
|
|
6175
6184
|
);
|
|
6176
6185
|
var pushCallbackScopeALS = globalThis[PUSH_CALLBACK_SCOPE_KEY] ??= new AsyncLocalStorage2();
|
|
6177
6186
|
|
|
6178
|
-
// src/rsc/shell-capture-constants.ts
|
|
6179
|
-
var SHELL_CAPTURE_MAX_WAIT_MS = 15e3;
|
|
6180
|
-
|
|
6181
6187
|
// src/rsc/shell-serve.ts
|
|
6182
6188
|
var DEV_SHELL_PROBE_TIMEOUT_MS = 1e4;
|
|
6183
6189
|
function normalizeCaptureTimeout(value) {
|
|
6184
6190
|
return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.min(value, SHELL_CAPTURE_MAX_WAIT_MS) : void 0;
|
|
6185
6191
|
}
|
|
6192
|
+
var SHELL_PRELUDE_CHUNK_BYTES = 32 * 1024;
|
|
6186
6193
|
|
|
6187
6194
|
// src/vite/inject-client-debug.ts
|
|
6188
6195
|
function isRouterInternalDebugId(id) {
|
|
@@ -9133,18 +9140,36 @@ var ALL_USE_ITEM_TYPES = new Set(
|
|
|
9133
9140
|
);
|
|
9134
9141
|
|
|
9135
9142
|
// src/server/request-context.ts
|
|
9136
|
-
import { AsyncLocalStorage as
|
|
9143
|
+
import { AsyncLocalStorage as AsyncLocalStorage5 } from "node:async_hooks";
|
|
9137
9144
|
init_cookie_name();
|
|
9138
9145
|
|
|
9139
9146
|
// src/server/handle-store.ts
|
|
9140
9147
|
var LOADER_SLOT = Object.freeze({ loader: true });
|
|
9141
9148
|
|
|
9149
|
+
// src/cache/cache-tag.ts
|
|
9150
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
9151
|
+
var cacheTagStorage = new AsyncLocalStorage3();
|
|
9152
|
+
var segmentOwnerStorage = new AsyncLocalStorage3();
|
|
9153
|
+
var recordedTagCapture = globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:recorded-tag-capture")] ??= new AsyncLocalStorage3();
|
|
9154
|
+
var identityReads = globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:identity-reads")] ??= /* @__PURE__ */ new WeakMap();
|
|
9155
|
+
function activeTagSet() {
|
|
9156
|
+
const capture = recordedTagCapture.getStore();
|
|
9157
|
+
const body = getLoaderBodyTags();
|
|
9158
|
+
if (capture && capture.outer === body) return capture.into;
|
|
9159
|
+
return body ?? capture?.into;
|
|
9160
|
+
}
|
|
9161
|
+
globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:identity-read-recorder")] = (surface, verb) => {
|
|
9162
|
+
const set = activeTagSet();
|
|
9163
|
+
if (set === void 0 || identityReads.has(set)) return;
|
|
9164
|
+
identityReads.set(set, { surface, verb, bodyId: getCurrentLoaderBodyId() });
|
|
9165
|
+
};
|
|
9166
|
+
|
|
9142
9167
|
// src/router/router-context.ts
|
|
9143
|
-
import { AsyncLocalStorage as
|
|
9144
|
-
var routerContext = new
|
|
9168
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
9169
|
+
var routerContext = new AsyncLocalStorage4();
|
|
9145
9170
|
|
|
9146
9171
|
// src/server/request-context.ts
|
|
9147
|
-
var requestContextStorage = new
|
|
9172
|
+
var requestContextStorage = new AsyncLocalStorage5();
|
|
9148
9173
|
|
|
9149
9174
|
// src/route-definition/redirect.ts
|
|
9150
9175
|
init_redirect_origin();
|
|
@@ -9727,6 +9752,8 @@ async function runShellPrerenderPhase(s, builder) {
|
|
|
9727
9752
|
const candidates = s.shellCandidates;
|
|
9728
9753
|
const startTotal = performance.now();
|
|
9729
9754
|
console.log(`[rango] Shell-prerendering ${candidates.length} URL(s)...`);
|
|
9755
|
+
const keepAlive = setInterval(() => {
|
|
9756
|
+
}, 6e4);
|
|
9730
9757
|
let restoreClientRequire;
|
|
9731
9758
|
try {
|
|
9732
9759
|
const rscEnv = tempServer.environments?.rsc;
|
|
@@ -9934,6 +9961,7 @@ async function runShellPrerenderPhase(s, builder) {
|
|
|
9934
9961
|
} finally {
|
|
9935
9962
|
delete globalThis.__loadPrerenderManifestModule;
|
|
9936
9963
|
restoreClientRequire?.();
|
|
9964
|
+
clearInterval(keepAlive);
|
|
9937
9965
|
}
|
|
9938
9966
|
}
|
|
9939
9967
|
function writeShellManifest(s, rscOutDir, staged) {
|
|
@@ -10603,7 +10631,16 @@ async function createTempRscServer(state, options = {}) {
|
|
|
10603
10631
|
return createViteServer({
|
|
10604
10632
|
root: state.projectRoot,
|
|
10605
10633
|
configFile: false,
|
|
10606
|
-
server: {
|
|
10634
|
+
server: {
|
|
10635
|
+
middlewareMode: true,
|
|
10636
|
+
// Build mode: no file watching. buildStart installs the per-router tries
|
|
10637
|
+
// into this realm and runShellPrerenderPhase reuses it after the bundles
|
|
10638
|
+
// are written. A watched write in between (writeRouteTypesFiles' own
|
|
10639
|
+
// <router>.named-routes.gen.ts, which every router file imports) can
|
|
10640
|
+
// make Vite full-reload the RSC runner; the shell phase then matches in
|
|
10641
|
+
// a fresh realm with no tries and a root path("/*") wins (#947).
|
|
10642
|
+
...options.forceBuild ? { watch: null } : {}
|
|
10643
|
+
},
|
|
10607
10644
|
appType: "custom",
|
|
10608
10645
|
logLevel: "silent",
|
|
10609
10646
|
resolve: resolveConfig,
|
package/package.json
CHANGED
|
@@ -103,13 +103,16 @@ Delivery follows the loader race model: a push that beats the handler barrier
|
|
|
103
103
|
is in the SSR'd document; a push after a slow fetch streams and applies
|
|
104
104
|
client-side (`useHandle` re-renders when it lands). To guarantee document
|
|
105
105
|
delivery, register the loader as `loader(Def, { ssr: false })` —
|
|
106
|
-
see `/loader` → "Writing Handles from Loaders".
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
`
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
see `/loader` → "Writing Handles from Loaders". On a `ppr` route the race is
|
|
107
|
+
decided at capture: a loader without `ssr: false` never runs there, so its
|
|
108
|
+
crumb is not in the shell and applies client-side on every HIT; with
|
|
109
|
+
`ssr: false` the crumb bakes into the shell (`/ppr` → "On a shell HIT").
|
|
110
|
+
Under `cache()` loader crumbs are not stored with the cached segments; the
|
|
111
|
+
loader re-pushes them on every hit, so they appear once even without an `href`.
|
|
112
|
+
A loader with its own `cache()` keeps its crumbs on its own hits too: they are
|
|
113
|
+
replayed from the loader's cache entry. A crumb from a loader it awaits appears
|
|
114
|
+
once even when a sibling loader or the handler reads that loader too; that live
|
|
115
|
+
run's crumb replaces the cached one in the trail.
|
|
113
116
|
|
|
114
117
|
## Async Content
|
|
115
118
|
|
|
@@ -134,6 +137,13 @@ path("/product/:id", async (ctx) => {
|
|
|
134
137
|
Async content is a `Promise<ReactNode>`. Resolve it in your component
|
|
135
138
|
with React's `use()` hook wrapped in `<Suspense>`.
|
|
136
139
|
|
|
140
|
+
A crumb the HANDLER pushes is handler output. In a route `cache()` entry and
|
|
141
|
+
in a `ppr` shell it is stored resolved: the cache write, or the shell capture
|
|
142
|
+
(within `ppr.captureTimeout`), waits for a `content` promise and for a
|
|
143
|
+
`.defer()` slot (below), and every HIT replays the crumb as it was resolved
|
|
144
|
+
then, without running the handler. For per-request crumb content, push it
|
|
145
|
+
from a loader without `ssr: false` (see "Pushing from Loaders").
|
|
146
|
+
|
|
137
147
|
### Deferred content (decide now, resolve from a deep component)
|
|
138
148
|
|
|
139
149
|
When the handler should DECIDE to push a crumb early but the value is produced
|
|
@@ -18,15 +18,15 @@ of the request live on a hit. The runtime layers share the app-level store
|
|
|
18
18
|
`revalidateTag`); `Prerender()`/`Static()` output is built into the server
|
|
19
19
|
bundle instead.
|
|
20
20
|
|
|
21
|
-
| Layer | Declared with | Stores
|
|
22
|
-
| -------------------- | ---------------------------------------------- |
|
|
23
|
-
| Function / component | `"use cache"` | one function's return value
|
|
24
|
-
| Loader data | `loader(L, () => [cache({...})])` | one loader's result
|
|
25
|
-
| Segments, runtime | `cache({...}, () => [...])` | rendered Flight segments of a subtree
|
|
26
|
-
| Segments, build time | `Prerender()` / `Static()` | Flight segments rendered at build
|
|
27
|
-
| HTML shell | `ppr` path option | HTML prelude + React postponed state
|
|
28
|
-
| Whole response (app) | `createDocumentCacheMiddleware()` + `s-maxage` | final response in the app store
|
|
29
|
-
| Whole response (CDN) | `Cache-Control: s-maxage` read by the platform | final response outside the app
|
|
21
|
+
| Layer | Declared with | Stores | Still runs on a hit | Skill |
|
|
22
|
+
| -------------------- | ---------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------ | --------------------- |
|
|
23
|
+
| Function / component | `"use cache"` | one function's return value | everything around the call | `/use-cache` |
|
|
24
|
+
| Loader data | `loader(L, () => [cache({...})])` | one loader's result | other loaders, handlers, rendering | `/caching`, `/loader` |
|
|
25
|
+
| Segments, runtime | `cache({...}, () => [...])` | rendered Flight segments of a subtree | middleware, segments above it, loaders, HTML render | `/caching` |
|
|
26
|
+
| Segments, build time | `Prerender()` / `Static()` | Flight segments rendered at build | middleware, loaders, HTML render | `/prerender` |
|
|
27
|
+
| HTML shell | `ppr` path option | HTML prelude + React postponed state + the handler layer | middleware and live loaders (no handler); holes resume | `/ppr` |
|
|
28
|
+
| Whole response (app) | `createDocumentCacheMiddleware()` + `s-maxage` | final response in the app store | middleware above it; nothing below | `/document-cache` |
|
|
29
|
+
| Whole response (CDN) | `Cache-Control: s-maxage` read by the platform | final response outside the app | nothing — the app is not invoked | `/deployment-caching` |
|
|
30
30
|
|
|
31
31
|
Quick rules:
|
|
32
32
|
|
|
@@ -37,7 +37,10 @@ Quick rules:
|
|
|
37
37
|
- **Whole response** public and identical for everyone → `/document-cache` or
|
|
38
38
|
CDN caching (`/deployment-caching`).
|
|
39
39
|
- A value that must be fresh on **every** request → a loader (never cached
|
|
40
|
-
unless you opt in)
|
|
40
|
+
unless you opt in), read with `useLoader()`; on a `ppr` route, keep it off
|
|
41
|
+
`ssr: false`. A handler inside a `cache()` or `ppr` boundary that awaits
|
|
42
|
+
`ctx.use(Loader)` renders the value it got on the miss, and every hit
|
|
43
|
+
replays that output.
|
|
41
44
|
|
|
42
45
|
The layers compose: `"use cache"` inside a `cache()` subtree, a cached loader
|
|
43
46
|
under a `cache()` boundary, `cache()` segments replayed inside a `ppr` shell
|
|
@@ -53,25 +56,38 @@ execution model, and runtime control.
|
|
|
53
56
|
|
|
54
57
|
## Two axes — do not conflate
|
|
55
58
|
|
|
56
|
-
Everything on this page is **
|
|
57
|
-
|
|
59
|
+
Everything on this page is **stored-value freshness** — _is a cached value
|
|
60
|
+
still good?_ There is a second, orthogonal concern it is easy to mistake for
|
|
58
61
|
caching:
|
|
59
62
|
|
|
60
|
-
1. **Stored-value freshness** — _is a cached value still good?_
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`cacheTag(...tags)` — inside `"use cache"` it tags that entry; called during
|
|
64
|
-
request render outside `"use cache"` it tags the
|
|
65
|
-
|
|
66
|
-
`
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
1. **Stored-value freshness** — _is a cached value still good?_ → `"use cache"`
|
|
64
|
+
(fn/component), `cache()` (segment), loader `cache()` (loader data). Entries
|
|
65
|
+
expire by **TTL/SWR** and can be tagged (`cache({ tags })` or runtime
|
|
66
|
+
`cacheTag(...tags)` — inside `"use cache"` it tags that entry; called during
|
|
67
|
+
a request render outside `"use cache"` it tags what the render stores: the
|
|
68
|
+
enclosing route `cache()` entry, a cached loader's own entry when called in
|
|
69
|
+
its body, and the `ppr` shell or document-cache entry built from them; on a
|
|
70
|
+
`ppr` route it tags the shell only when shell material calls it). All
|
|
71
|
+
built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`,
|
|
72
|
+
`VercelCacheStore`) index by tag; invalidate on demand with
|
|
73
|
+
`updateTag(...tags)` (awaitable) or `revalidateTag(...tags)` (background,
|
|
74
|
+
non-blocking). Both evict rather than mark stale, and the request that calls
|
|
75
|
+
either reads its own writes; `updateTag()` also waits for the durable write,
|
|
76
|
+
`revalidateTag()` leaves it in the background. `CFCacheStore` and
|
|
77
|
+
`VercelCacheStore` serve PPR shell reads through per-isolate memos: the
|
|
78
|
+
invalidating user skips them via the fresh-reads cookie, other users see the
|
|
79
|
+
invalidation once the memo refreshes and, on `CFCacheStore` with KV, once KV
|
|
80
|
+
propagates the marker to their colo (`/caching` → "Tag-Based Invalidation"
|
|
81
|
+
and "The fresh-reads cookie").
|
|
69
82
|
2. **Client-update selection** — _should this segment re-run and stream to the
|
|
70
83
|
client on this navigation/action?_
|
|
71
84
|
→ `revalidate()`. Covered in `/loader` and `/route`, **not here**.
|
|
72
85
|
|
|
73
86
|
They are orthogonal and compose: a segment selected by `revalidate()` still
|
|
74
|
-
consults its cache (hit → no recompute)
|
|
87
|
+
consults its cache (hit → no recompute), except that a server action's
|
|
88
|
+
revalidation render skips the route `cache()` lookup and re-renders those
|
|
89
|
+
segments fresh (a loader's own `cache()` and `"use cache"` are still read); a
|
|
90
|
+
cache bust does **not** force a client
|
|
75
91
|
update, and `revalidate()` never reads, writes, or expires a cached value. If you
|
|
76
92
|
know React Router, `revalidate()` is `shouldRevalidate`, not `Cache-Control`. See
|
|
77
93
|
`/rango` → "Coming from another framework" for the cross-framework mapping.
|
|
@@ -111,7 +127,14 @@ else they allow:
|
|
|
111
127
|
would be baked in), and response side effects (`ctx.headers.set()`,
|
|
112
128
|
`setCookie()`, `setStatus()`, `onResponse()`) throw. `ctx.set()` of a cacheable var is
|
|
113
129
|
**allowed** — children are cached too and can read it. **Loaders are exempt**
|
|
114
|
-
(
|
|
130
|
+
(a route `cache()` does not store their values) — read request data inside a
|
|
131
|
+
loader.
|
|
132
|
+
- **Loader `cache()` guard** (a loader bound with its own `cache()`) — its value
|
|
133
|
+
is stored under a key that names no user, so a miss whose body read
|
|
134
|
+
`cookies()`, `headers()`, the theme or a non-cacheable `ctx.get()` fails
|
|
135
|
+
unless the binding has a `key()` or its store a `keyGenerator`. Either one
|
|
136
|
+
switches the check off, so put what the body reads in it (`/loader` →
|
|
137
|
+
"Cache Key").
|
|
115
138
|
- **`"use cache"` exec-guard** (function-level) — the same request-scoped APIs
|
|
116
139
|
throw inside the cached function (`cookies()`, `headers()`, `ctx.set()`,
|
|
117
140
|
`ctx.headers.set()` and other response writes); additionally, tainted
|
|
@@ -119,6 +142,12 @@ else they allow:
|
|
|
119
142
|
the cached path: with no item-capable store configured the function runs
|
|
120
143
|
uncached and nothing throws.
|
|
121
144
|
|
|
145
|
+
The `ppr` shell capture has its own, stricter guard: `cookies()`, `headers()`, a
|
|
146
|
+
theme read, a `{ cache: false }` variable read, and `ctx.dynamic()` refuse the
|
|
147
|
+
capture anywhere it waits — handlers, promises they pass or push, async server
|
|
148
|
+
components, `ssr: false` loaders, and loaders a handler awaits (no loader
|
|
149
|
+
exemption there). The route then serves uncached; see `/ppr`.
|
|
150
|
+
|
|
122
151
|
### Cross-deploy safety: version-segmented store keys
|
|
123
152
|
|
|
124
153
|
`CFCacheStore` prefixes every **physical** store key (the CF Cache API URL and
|
|
@@ -203,7 +232,7 @@ local/dev behavior, not as proof that segment SWR is active.
|
|
|
203
232
|
| **Side effects** | Response side effects throw inside the boundary | `ctx.headers.set()`, `ctx.set()`, etc. throw |
|
|
204
233
|
| **Handle data** | Handler pushes replayed; loader pushes re-run live | Captured and replayed when it receives `ctx` |
|
|
205
234
|
| **Loaders** | Always fresh — excluded from cache, opt-in per loader | Can be used inside loaders |
|
|
206
|
-
| **Nesting** |
|
|
235
|
+
| **Nesting** | Inner TTLs override; `key()` partitions compose | Compose by calling cached functions from uncached |
|
|
207
236
|
|
|
208
237
|
### cache() Cache Key
|
|
209
238
|
|
|
@@ -216,8 +245,14 @@ honors `createRouter({ cache: { searchParams } })`.
|
|
|
216
245
|
|
|
217
246
|
A custom `key` function replaces the whole default key (e.g., to key by user role
|
|
218
247
|
or locale); it also bypasses the store's `keyGenerator` and the search-param
|
|
219
|
-
filter.
|
|
220
|
-
|
|
248
|
+
filter. It runs once per request and may read `cookies()`. A nested `cache()`
|
|
249
|
+
keys its records within the enclosing `key()` partition (`/caching` → "Keys
|
|
250
|
+
nest"). Never return raw request input from `key()`: normalize it to the
|
|
251
|
+
values you serve or prefix it. On a `ppr` route, `key()` (and a store
|
|
252
|
+
`keyGenerator` that returns a non-default key) also partitions the shell: one
|
|
253
|
+
shell per value, and a partitioned request never reads a build-time shell.
|
|
254
|
+
Keep the values to a small set. `condition` can disable caching entirely at
|
|
255
|
+
runtime (e.g., skip for authenticated users).
|
|
221
256
|
|
|
222
257
|
### "use cache" Cache Key
|
|
223
258
|
|
|
@@ -294,6 +329,11 @@ export const urlpatterns = urls(({ path, cache }) => [
|
|
|
294
329
|
]);
|
|
295
330
|
```
|
|
296
331
|
|
|
332
|
+
On a `ppr` route, `cache(false)` or a `condition()` that returns false also
|
|
333
|
+
means no shell: that route (or that request) renders like a cache miss, with
|
|
334
|
+
no shell served or captured. A `ppr` route cannot opt out of a layout's
|
|
335
|
+
`cache()` and still get a shell.
|
|
336
|
+
|
|
297
337
|
## When to Use "use cache"
|
|
298
338
|
|
|
299
339
|
Use the `"use cache"` directive when:
|
|
@@ -364,8 +404,20 @@ does **not** keep a value fresher than its parent:
|
|
|
364
404
|
So `"use cache: short"` (60s) inside `cache({ ttl: 600 })` yields ~600s freshness
|
|
365
405
|
on hits, **not** 60s. This is not a bug: setting `cache({ ttl: 600 })` declares
|
|
366
406
|
"this subtree may be ~600s stale." **If a value must be fresher than its
|
|
367
|
-
enclosing segment, put it in a loader**
|
|
368
|
-
|
|
407
|
+
enclosing segment, put it in a loader** read with `useLoader()` (without
|
|
408
|
+
`ssr: false` on a `ppr` route). `debugPerformance` prints cache hits per
|
|
409
|
+
layer, so the actual per-request behavior is observable.
|
|
410
|
+
|
|
411
|
+
Keys and tags cross the nesting too:
|
|
412
|
+
|
|
413
|
+
- An inner `cache()` without `key` keys its records within the outer `key()`
|
|
414
|
+
partition; one with its own `key` composes both (`/caching` → "Keys nest").
|
|
415
|
+
- Tags that `cacheTag()` calls and `"use cache"` reads record inside a
|
|
416
|
+
`cache()` subtree are stored on that `cache()` entry, so `updateTag()` of one
|
|
417
|
+
of them evicts it (and a `ppr` shell or document built from it) instead of
|
|
418
|
+
waiting for it to expire.
|
|
419
|
+
- A `ppr` shell never outlives the route `cache()` entry it was captured
|
|
420
|
+
from: its ttl and ttl + swr are capped to what that entry has left.
|
|
369
421
|
|
|
370
422
|
## Headers and Cookies
|
|
371
423
|
|
|
@@ -381,6 +433,10 @@ Neither mechanism caches response headers or cookies.
|
|
|
381
433
|
segment outside the cache boundary, or a loader.
|
|
382
434
|
- **"use cache"**: `cookies()` and `headers()` throw inside the cached function
|
|
383
435
|
(both reads and writes), and so do `ctx.headers` mutations. Move them outside.
|
|
436
|
+
- **`ctx.theme`** (handler and middleware) and **`getRequestContext().theme`**
|
|
437
|
+
are the visitor's theme cookie, so a read throws in both scopes, like
|
|
438
|
+
`cookies()`. Read it with `useTheme()` in a client component, or in a
|
|
439
|
+
loader (see the `/theme` skill).
|
|
384
440
|
|
|
385
441
|
```typescript
|
|
386
442
|
// Set headers that must appear on every response in middleware
|
|
@@ -413,14 +469,16 @@ specifies `cache: false`, the value is non-cacheable.
|
|
|
413
469
|
|
|
414
470
|
**Behavior inside a `cache()` boundary:**
|
|
415
471
|
|
|
416
|
-
| Operation
|
|
417
|
-
|
|
|
418
|
-
| `cookies()` / `headers()` (read or write)
|
|
419
|
-
| `ctx.
|
|
420
|
-
| `ctx.get(
|
|
421
|
-
| `ctx.
|
|
422
|
-
| `ctx.
|
|
423
|
-
|
|
|
472
|
+
| Operation | Inside a `cache()` boundary |
|
|
473
|
+
| -------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
474
|
+
| `cookies()` / `headers()` (read or write) | Throws (request-scoped, would poison the shared entry) |
|
|
475
|
+
| `ctx.theme`, `getRequestContext().theme` | Throws, like `cookies()` (#971) |
|
|
476
|
+
| `ctx.get(cacheableVar)` | Allowed |
|
|
477
|
+
| `ctx.get(nonCacheableVar)` | Throws (would be baked in) |
|
|
478
|
+
| `ctx.set(var, value)` (cacheable) | Allowed |
|
|
479
|
+
| `ctx.headers.set()` / cookie writes | Throws (response side effect would be lost on hit) |
|
|
480
|
+
| Any of the above **inside a loader** | Allowed (loaders always run fresh) |
|
|
481
|
+
| The reads above in a loader with its own `cache()` | The miss fails without a `key()`; any `key()` allows them, so it must include them |
|
|
424
482
|
|
|
425
483
|
(Both scopes block the same request-scoped APIs — `cookies()`, `headers()`,
|
|
426
484
|
response side effects, and non-cacheable `ctx.get()` — because each would leak
|
|
@@ -476,7 +534,8 @@ non-cacheable value in as an argument so it becomes part of the key. The pattern
|
|
|
476
534
|
**read tainted context at the point of use, in the path that needs it (a loader or
|
|
477
535
|
live segment) — never extract user data into a plain value and cache that.**
|
|
478
536
|
Loaders are exempt because they run outside the cache scope and resolve fresh
|
|
479
|
-
every request.
|
|
537
|
+
every request. A loader with its own `cache()` does not: its value is stored,
|
|
538
|
+
so it falls under the loader `cache()` guard above.
|
|
480
539
|
|
|
481
540
|
## Loaders Are Always Fresh
|
|
482
541
|
|
|
@@ -489,8 +548,16 @@ fresh on every request. This is enforced at two levels:
|
|
|
489
548
|
loaders after yielding its cached UI segments (the loaders above the boundary
|
|
490
549
|
run with their live segments), ensuring fresh data regardless of cache state.
|
|
491
550
|
|
|
492
|
-
This means `cache()` gives you cached UI + fresh data by default
|
|
493
|
-
|
|
551
|
+
This means `cache()` gives you cached UI + fresh data by default, where the
|
|
552
|
+
data is read outside the cached output: `useLoader()` in a client component.
|
|
553
|
+
A handler inside the boundary that awaits `ctx.use(Loader)` renders the value
|
|
554
|
+
it got on the miss, and every hit replays that output. On a `ppr` route an
|
|
555
|
+
`ssr: false` loader is the other exception: it bakes into the shell (`/ppr`).
|
|
556
|
+
To also cache a loader's data, explicitly opt in with
|
|
557
|
+
`loader(Fn, () => [cache({...})])`.
|
|
558
|
+
That entry is keyed by loader, host, path and params, not by the route's
|
|
559
|
+
`key()`, so give it a `key()` that includes any request data its body reads;
|
|
560
|
+
without one the miss fails.
|
|
494
561
|
|
|
495
562
|
## cache() Placement Patterns
|
|
496
563
|
|
|
@@ -513,16 +580,16 @@ path("/dashboard", DashboardPage, { name: "dashboard" }, () => [
|
|
|
513
580
|
```
|
|
514
581
|
|
|
515
582
|
On hit: DashboardPage, DashboardSidebar, StatsPanel, and ActivityFeed are all
|
|
516
|
-
served from cache, and DashboardLoader runs fresh. On miss: all handlers run
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
`
|
|
583
|
+
served from cache, and DashboardLoader runs fresh. On miss: all handlers run and
|
|
584
|
+
the path's segments are cached together. DashboardPage runs inside the boundary,
|
|
585
|
+
so the guards apply to it: a header write there throws. The wrapper form
|
|
586
|
+
`cache({ ttl: 300 }, () => [layout(DashboardSidebar)])` inside a path does the
|
|
587
|
+
same, and `cache(false)` there opts the path out of an enclosing `cache()` (on a
|
|
588
|
+
`ppr` route, out of the shell too). The same form caches a response route:
|
|
522
589
|
`path.json("/api/feed", handler, { name: "feed" }, () => [cache({ ttl: 60 })])`.
|
|
523
|
-
A `cache()` inside a `layout()` or `middleware()` wrapper in the path caches
|
|
524
|
-
|
|
525
|
-
|
|
590
|
+
A `cache()` inside a `layout()` or `middleware()` wrapper in the path caches the
|
|
591
|
+
path too: `layout(DashboardSidebar, () => [cache({ ttl: 300 })])` is the same
|
|
592
|
+
unit as the example above, not the sidebar alone.
|
|
526
593
|
|
|
527
594
|
### Bare cache() among a layout's children
|
|
528
595
|
|
|
@@ -566,8 +633,8 @@ RootLayout renders fresh every request, and a header it writes lands on every
|
|
|
566
633
|
response. ProductPage, ReviewsPanel, and RelatedProducts are inside the cache
|
|
567
634
|
boundary and served from cache on hit. This is useful when the root layout
|
|
568
635
|
depends on request-specific data (user session, theme) but the product content
|
|
569
|
-
is cacheable. On a `ppr` route the
|
|
570
|
-
`cache()`
|
|
636
|
+
is cacheable. On a `ppr` route the shell bakes RootLayout too: a shell HIT runs
|
|
637
|
+
no layout, and the shell expires no later than this `cache()` entry.
|
|
571
638
|
|
|
572
639
|
### Loader-level caching
|
|
573
640
|
|
|
@@ -589,7 +656,12 @@ This attaches the cache config directly to the loader entry. The loader's
|
|
|
589
656
|
data is cached independently from the route's segment cache, together with
|
|
590
657
|
the handle pushes its body made (replayed on every hit). Loader caching
|
|
591
658
|
supports custom keys, tags, SWR, conditional bypass, and per-loader store
|
|
592
|
-
overrides — see `/loader` for the full reference.
|
|
659
|
+
overrides — see `/loader` for the full reference. A body that reads
|
|
660
|
+
`cookies()`, `headers()` or a non-cacheable variable needs a `key()`, which
|
|
661
|
+
must include the value: without one the miss fails, and any `key()` switches
|
|
662
|
+
that check off. The entry also carries the tags its body recorded
|
|
663
|
+
(`cacheTag()` calls, `"use cache"` reads, and those of the loaders it reads
|
|
664
|
+
with `ctx.use()`), so `updateTag()` of any of them drops it.
|
|
593
665
|
|
|
594
666
|
## See Also
|
|
595
667
|
|