@rangojs/router 0.21.0 → 0.22.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/dist/bin/rango.js +13 -1
- package/dist/types/browser/event-controller.d.ts +30 -13
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +14 -0
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +23 -0
- package/dist/types/cache/index.d.ts +1 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +10 -1
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +63 -14
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +7 -0
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +30 -0
- package/dist/types/router/router-options.d.ts +24 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +9 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +63 -2
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/flight.entry.d.ts +2 -0
- package/dist/types/testing/index.d.ts +4 -0
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/serve-shell-request.d.ts +34 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +7 -0
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/state.d.ts +11 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/vite/index.js +179 -496
- package/package.json +12 -2
- package/skills/cache-guide/SKILL.md +18 -17
- package/skills/caching/SKILL.md +40 -4
- package/skills/catalog.json +1 -1
- package/skills/cloudflare/SKILL.md +131 -0
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +22 -0
- package/skills/hooks/handle-and-actions.md +3 -4
- package/skills/hooks/navigation.md +10 -0
- package/skills/hooks/state.md +5 -4
- package/skills/host-router/SKILL.md +18 -0
- package/skills/loader/SKILL.md +6 -4
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +31 -8
- package/skills/prerender/SKILL.md +645 -8
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/serve-shell-request.md +104 -8
- package/skills/use-cache/SKILL.md +9 -5
- package/skills/vercel/SKILL.md +2 -0
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/browser/event-controller.ts +69 -25
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +75 -35
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +9 -2
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +48 -12
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/react/Link.tsx +37 -42
- package/src/browser/react/NavigationProvider.tsx +63 -3
- package/src/browser/react/context.ts +17 -0
- package/src/browser/react/location-state.ts +5 -19
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +4 -20
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +79 -39
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +26 -6
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +38 -36
- package/src/cache/cf/cf-cache-store.ts +16 -11
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +50 -0
- package/src/cache/index.ts +1 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +17 -0
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +69 -14
- package/src/cache/vercel/vercel-cache-store.ts +20 -6
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +7 -2
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +230 -38
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +33 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +15 -17
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router.ts +107 -1
- package/src/rsc/handler.ts +51 -4
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +53 -11
- package/src/rsc/shell-build-manifest.ts +10 -8
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +108 -19
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +8 -0
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +15 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +13 -0
- package/src/server/request-context.ts +87 -1
- package/src/ssr/ssr-root.tsx +20 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +3 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/render-route.tsx +25 -12
- package/src/testing/serve-shell-request.ts +190 -34
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +7 -0
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/shell-prerender-phase.ts +5 -5
- package/src/vite/discovery/state.ts +17 -1
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugins/virtual-entries.ts +8 -1
- package/src/vite/router-discovery.ts +14 -0
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/browser/react/subscribe-to-nothing.ts +0 -7
- package/src/segment-content-promise.ts +0 -67
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mark and record of a `router.prerender()` warm request
|
|
3
|
+
* (docs/design/prerender-every-route.md, "The synthetic request"). The
|
|
4
|
+
* trigger marks the `Request` it dispatches, the handler puts the record on
|
|
5
|
+
* the request context (`RequestContext._prerenderWarm`), and each cache layer
|
|
6
|
+
* reads the context where it reads its store. No runtime imports: the cache
|
|
7
|
+
* layers import this without pulling the trigger.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { HandlerCacheConfig } from "../rsc/types.js";
|
|
11
|
+
import type { ShellCaptureDebugEvent } from "../rsc/shell-capture.js";
|
|
12
|
+
import type { PrerenderWarmCaches } from "./on-demand.js";
|
|
13
|
+
|
|
14
|
+
/** What one warm request did: written by the layers, read by the trigger. */
|
|
15
|
+
export interface PrerenderWarmRecord extends PrerenderWarmCaches {
|
|
16
|
+
/**
|
|
17
|
+
* `replace`: every runtime cache read misses and the writes replace the
|
|
18
|
+
* entries. `fill` (`onlyIfStale`): reads are normal; the record only reports.
|
|
19
|
+
*/
|
|
20
|
+
mode: "replace" | "fill";
|
|
21
|
+
/**
|
|
22
|
+
* The `createRouter({ cache })` config the trigger gated on its scope. The
|
|
23
|
+
* handler uses it instead of resolving the option again, so the store that
|
|
24
|
+
* was checked is the store that is written.
|
|
25
|
+
*/
|
|
26
|
+
cacheConfig: HandlerCacheConfig;
|
|
27
|
+
/** The surface an identity guard refused (server/context.ts guardIdentityRead). */
|
|
28
|
+
identity?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The request's render errors: the handler adopts this list as
|
|
31
|
+
* `RequestContext._renderErrors`, so what the layers push lands here.
|
|
32
|
+
*/
|
|
33
|
+
renderErrors: unknown[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What the helpers read; a derived capture context inherits the record. */
|
|
37
|
+
export interface WarmContext {
|
|
38
|
+
_prerenderWarm?: PrerenderWarmRecord;
|
|
39
|
+
_shellCaptureRun?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* In process on purpose: "miss every cache and overwrite it" reachable from a
|
|
44
|
+
* URL parameter, header or cookie would be a cache-busting amplifier. A client
|
|
45
|
+
* controls the bytes of a request, never this map's keys.
|
|
46
|
+
*/
|
|
47
|
+
const warmRequests = new WeakMap<Request, PrerenderWarmRecord>();
|
|
48
|
+
|
|
49
|
+
export function createWarmRecord(
|
|
50
|
+
mode: PrerenderWarmRecord["mode"],
|
|
51
|
+
cacheConfig: HandlerCacheConfig,
|
|
52
|
+
): PrerenderWarmRecord {
|
|
53
|
+
return {
|
|
54
|
+
mode,
|
|
55
|
+
cacheConfig,
|
|
56
|
+
renderErrors: [],
|
|
57
|
+
writes: { record: 0, item: 0, response: 0, shell: 0 },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The mark is the object: a clone or a rebuilt request carries none. */
|
|
62
|
+
export function markWarmRequest(
|
|
63
|
+
request: Request,
|
|
64
|
+
record: PrerenderWarmRecord,
|
|
65
|
+
): void {
|
|
66
|
+
warmRequests.set(request, record);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readWarmMark(
|
|
70
|
+
request: Request,
|
|
71
|
+
): PrerenderWarmRecord | undefined {
|
|
72
|
+
return warmRequests.get(request);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* True when this request must read every runtime cache as a miss. A flag read
|
|
77
|
+
* at each read point, not a store wrapper: a route's or loader's own
|
|
78
|
+
* `cache({ store })` bypasses the app store (CacheScope.getStore), and the
|
|
79
|
+
* "use cache" in-flight dedup lives outside the store, so a wrapper would
|
|
80
|
+
* miss both.
|
|
81
|
+
*
|
|
82
|
+
* False in the warm's own shell capture (`_shellCaptureRun`): it runs after
|
|
83
|
+
* the warm's writes settled and must replay them, or it would render every
|
|
84
|
+
* handler a second time and could bake another generation than the one just
|
|
85
|
+
* written.
|
|
86
|
+
*/
|
|
87
|
+
export function isWarmReplace(ctx: WarmContext | null | undefined): boolean {
|
|
88
|
+
return (
|
|
89
|
+
ctx?._prerenderWarm?.mode === "replace" && ctx._shellCaptureRun !== true
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Count a store write that landed. No-op outside a warm. */
|
|
94
|
+
export function noteWarmWrite(
|
|
95
|
+
ctx: WarmContext | null | undefined,
|
|
96
|
+
family: keyof PrerenderWarmCaches["writes"],
|
|
97
|
+
): void {
|
|
98
|
+
const record = ctx?._prerenderWarm;
|
|
99
|
+
if (record) record.writes[family] += 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Record the document cache's outcome; its store write counts through noteWarmWrite. */
|
|
103
|
+
export function noteWarmDocument(
|
|
104
|
+
ctx: WarmContext | null | undefined,
|
|
105
|
+
outcome: NonNullable<PrerenderWarmCaches["document"]>,
|
|
106
|
+
): void {
|
|
107
|
+
const record = ctx?._prerenderWarm;
|
|
108
|
+
if (!record) return;
|
|
109
|
+
record.document = outcome;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Record the surface an identity guard (guardIdentityRead) is about to refuse. */
|
|
113
|
+
export function noteWarmIdentityRead(
|
|
114
|
+
ctx: WarmContext | null | undefined,
|
|
115
|
+
surface: string,
|
|
116
|
+
): void {
|
|
117
|
+
const record = ctx?._prerenderWarm;
|
|
118
|
+
if (record) record.identity ??= surface;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const SHELL_EVENT_OUTCOMES: Partial<
|
|
122
|
+
Record<
|
|
123
|
+
ShellCaptureDebugEvent["outcome"],
|
|
124
|
+
NonNullable<PrerenderWarmCaches["shell"]>
|
|
125
|
+
>
|
|
126
|
+
> = {
|
|
127
|
+
stored: "stored",
|
|
128
|
+
refused: "refused",
|
|
129
|
+
"no-shell": "no-shell",
|
|
130
|
+
expired: "no-shell",
|
|
131
|
+
redirect: "no-shell",
|
|
132
|
+
error: "error",
|
|
133
|
+
"skip-capacity": "skipped-capacity",
|
|
134
|
+
"skip-queue-timeout": "skipped-queue-timeout",
|
|
135
|
+
"skip-inert-store": "not-eligible",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fold one capture event into the record: the last attempt's outcome stands.
|
|
140
|
+
* The skips a forced capture never takes and the `backoff` notice leave it.
|
|
141
|
+
*/
|
|
142
|
+
export function noteWarmShellEvent(
|
|
143
|
+
record: PrerenderWarmRecord,
|
|
144
|
+
event: ShellCaptureDebugEvent,
|
|
145
|
+
): void {
|
|
146
|
+
let shell = SHELL_EVENT_OUTCOMES[event.outcome];
|
|
147
|
+
if (shell === undefined) return;
|
|
148
|
+
// A putShell that threw still ends the attempt `stored` (the capture
|
|
149
|
+
// worked; shell-capture.ts reports the I/O error), but counted no write.
|
|
150
|
+
if (shell === "stored" && record.writes.shell === 0) shell = "error";
|
|
151
|
+
record.shell = shell;
|
|
152
|
+
if (shell === "stored") delete record.refusal;
|
|
153
|
+
else if (event.refusal !== undefined) record.refusal = event.refusal;
|
|
154
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One `router.prerender()` warm request: build the synthetic visitor request,
|
|
3
|
+
* dispatch it through the router's own handler, wait for everything it
|
|
4
|
+
* scheduled, and read back what it wrote
|
|
5
|
+
* (docs/design/prerender-every-route.md, "The synthetic request"). The handler
|
|
6
|
+
* is an injected `fetch`, so unit tests drive this with a fake.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
awaitLaterMillisecond,
|
|
11
|
+
raceDeadline,
|
|
12
|
+
settleGrowing,
|
|
13
|
+
type CollectingExecutionContext,
|
|
14
|
+
} from "../cache/background-task.js";
|
|
15
|
+
import { CAPTURE_QUEUE_WAIT_BUDGET_MS } from "../rsc/capture-queue.js";
|
|
16
|
+
import { SHELL_CAPTURE_TASK_HARD_CAP_MS } from "../rsc/shell-capture-constants.js";
|
|
17
|
+
import type { HandlerCacheConfig } from "../rsc/types.js";
|
|
18
|
+
import type { ExecutionContext } from "../types/request-scope.js";
|
|
19
|
+
import type { PrerenderResult, PrerenderWarmCaches } from "./on-demand.js";
|
|
20
|
+
import {
|
|
21
|
+
createWarmRecord,
|
|
22
|
+
markWarmRequest,
|
|
23
|
+
type PrerenderWarmRecord,
|
|
24
|
+
} from "./warm-request.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How long a warm waits for its response body and background work: a capture
|
|
28
|
+
* that waited out the queue budget, then ran to its hard cap.
|
|
29
|
+
*/
|
|
30
|
+
export const WARM_SETTLE_BUDGET_MS: number =
|
|
31
|
+
SHELL_CAPTURE_TASK_HARD_CAP_MS + CAPTURE_QUEUE_WAIT_BUDGET_MS;
|
|
32
|
+
|
|
33
|
+
export interface WarmRequestInput<TEnv> {
|
|
34
|
+
/** The absolute URL a visitor requests. */
|
|
35
|
+
url: URL;
|
|
36
|
+
mode: PrerenderWarmRecord["mode"];
|
|
37
|
+
/** The gated `createRouter({ cache })` config; the handler writes to its store. */
|
|
38
|
+
cacheConfig: HandlerCacheConfig;
|
|
39
|
+
env: TEnv;
|
|
40
|
+
/** The context `cacheConfig` was resolved with. */
|
|
41
|
+
ctx: CollectingExecutionContext;
|
|
42
|
+
/** `router.fetch`. */
|
|
43
|
+
fetch: (
|
|
44
|
+
request: Request,
|
|
45
|
+
input: { env: TEnv; ctx: ExecutionContext },
|
|
46
|
+
) => Promise<Response>;
|
|
47
|
+
/** Tests shorten the wait. */
|
|
48
|
+
settleBudgetMs?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WarmRequestOutcome {
|
|
52
|
+
record: PrerenderWarmRecord;
|
|
53
|
+
/** Absent when the handler threw before answering. */
|
|
54
|
+
responseStatus?: number;
|
|
55
|
+
/** The handler's throw, a body that failed mid-stream, or a wait that ran out. */
|
|
56
|
+
error?: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Dispatch one warm: a GET with `accept: text/html` and nothing else, so it
|
|
61
|
+
* lands in the partition a header-less visitor lands in. The response is
|
|
62
|
+
* drained and dropped: what it rendered reaches a visitor only through a
|
|
63
|
+
* store.
|
|
64
|
+
*/
|
|
65
|
+
export async function runWarmRequest<TEnv>(
|
|
66
|
+
input: WarmRequestInput<TEnv>,
|
|
67
|
+
): Promise<WarmRequestOutcome> {
|
|
68
|
+
// `await updateTag(tag)` then a warm is the content-refresh pattern, and a
|
|
69
|
+
// store refuses a shell whose capture started in the millisecond of an
|
|
70
|
+
// invalidation of one of its tags (putShell's gate compares whole
|
|
71
|
+
// milliseconds). Start in a later one, within the helper's bound.
|
|
72
|
+
await awaitLaterMillisecond(Date.now());
|
|
73
|
+
const record = createWarmRecord(input.mode, input.cacheConfig);
|
|
74
|
+
const request = new Request(input.url, { headers: { accept: "text/html" } });
|
|
75
|
+
markWarmRequest(request, record);
|
|
76
|
+
const deadline = Date.now() + (input.settleBudgetMs ?? WARM_SETTLE_BUDGET_MS);
|
|
77
|
+
|
|
78
|
+
let responseStatus: number | undefined;
|
|
79
|
+
let error: unknown;
|
|
80
|
+
try {
|
|
81
|
+
const response = await input.fetch(request, {
|
|
82
|
+
env: input.env,
|
|
83
|
+
ctx: input.ctx,
|
|
84
|
+
});
|
|
85
|
+
responseStatus = response.status;
|
|
86
|
+
// The render, and the tags and writes it records, finish with the body.
|
|
87
|
+
const drained = await raceDeadline(drainBody(response), deadline);
|
|
88
|
+
if (!drained.done) {
|
|
89
|
+
error = new Error(
|
|
90
|
+
`router.prerender(): the response for ${input.url.href} did not finish within the warm's wait`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
error = err ?? new Error("router.prerender(): the warm request failed");
|
|
95
|
+
}
|
|
96
|
+
await settleGrowing(input.ctx.tasks, deadline);
|
|
97
|
+
return { record, responseStatus, error };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Read the body to its end without keeping it. A non-200 is a failed warm
|
|
102
|
+
* whatever its body says, so it is cancelled instead.
|
|
103
|
+
*/
|
|
104
|
+
async function drainBody(response: Response): Promise<void> {
|
|
105
|
+
const { body } = response;
|
|
106
|
+
if (!body) return;
|
|
107
|
+
if (response.status !== 200) {
|
|
108
|
+
await body.cancel().catch(() => {});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const reader = body.getReader();
|
|
112
|
+
while (!(await reader.read()).done);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The per-cache detail a result carries. */
|
|
116
|
+
export function warmCaches(record: PrerenderWarmRecord): PrerenderWarmCaches {
|
|
117
|
+
return {
|
|
118
|
+
writes: { ...record.writes },
|
|
119
|
+
...(record.shell !== undefined ? { shell: record.shell } : {}),
|
|
120
|
+
...(record.refusal !== undefined ? { refusal: record.refusal } : {}),
|
|
121
|
+
...(record.document !== undefined ? { document: record.document } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A warm's status from what it recorded, first match wins. An identity
|
|
127
|
+
* refusal comes ahead of the render failure it usually also causes (the guard
|
|
128
|
+
* throws). `record.shell` is set only for a `ppr` route (shellServePlan).
|
|
129
|
+
*/
|
|
130
|
+
export function warmStatus(outcome: WarmRequestOutcome): WarmStatus {
|
|
131
|
+
const { record, responseStatus, error } = outcome;
|
|
132
|
+
if (record.identity !== undefined) return "skipped-personalized";
|
|
133
|
+
if (
|
|
134
|
+
error !== undefined ||
|
|
135
|
+
responseStatus !== 200 ||
|
|
136
|
+
record.renderErrors.length > 0
|
|
137
|
+
) {
|
|
138
|
+
return "render-failed";
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
record.shell !== undefined &&
|
|
142
|
+
record.shell !== "stored" &&
|
|
143
|
+
record.shell !== "fresh"
|
|
144
|
+
) {
|
|
145
|
+
return "shell-not-stored";
|
|
146
|
+
}
|
|
147
|
+
const { writes } = record;
|
|
148
|
+
if (writes.record + writes.item + writes.response + writes.shell === 0) {
|
|
149
|
+
return record.mode === "fill" ? "already-fresh" : "skipped-uncached";
|
|
150
|
+
}
|
|
151
|
+
return "warmed";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type WarmStatus =
|
|
155
|
+
| Extract<PrerenderResult, { ok: true; path: "warm" }>["status"]
|
|
156
|
+
| Extract<PrerenderResult, { ok: false }>["status"];
|
|
157
|
+
|
|
158
|
+
/** The `PrerenderResult` of a finished warm. */
|
|
159
|
+
export function composeWarmResult(
|
|
160
|
+
outcome: WarmRequestOutcome,
|
|
161
|
+
target: string,
|
|
162
|
+
routeName: string,
|
|
163
|
+
): PrerenderResult {
|
|
164
|
+
const status = warmStatus(outcome);
|
|
165
|
+
const caches = warmCaches(outcome.record);
|
|
166
|
+
if (status === "warmed" || status === "already-fresh") {
|
|
167
|
+
// Both are answered only for a 200 (warmStatus).
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
path: "warm",
|
|
171
|
+
status,
|
|
172
|
+
target,
|
|
173
|
+
routeName,
|
|
174
|
+
responseStatus: outcome.responseStatus!,
|
|
175
|
+
caches,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const error =
|
|
179
|
+
outcome.error ??
|
|
180
|
+
(status === "render-failed" ? outcome.record.renderErrors[0] : undefined);
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
path: "warm",
|
|
184
|
+
status,
|
|
185
|
+
target,
|
|
186
|
+
routeName,
|
|
187
|
+
...(outcome.responseStatus !== undefined
|
|
188
|
+
? { responseStatus: outcome.responseStatus }
|
|
189
|
+
: {}),
|
|
190
|
+
caches,
|
|
191
|
+
...(error !== undefined ? { error } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writable prerender store contract for on-demand (ISR-style) prerender refresh.
|
|
3
|
+
*
|
|
4
|
+
* The bundled build manifest (store.ts) is immutable and read-only. On-demand
|
|
5
|
+
* prerender adds a writable durable overlay in front of it: `router.prerender()`
|
|
6
|
+
* renders a route and writes a versioned envelope here; the serve path reads the
|
|
7
|
+
* overlay before the bundled manifest.
|
|
8
|
+
*
|
|
9
|
+
* This module is types + pure helpers only — no React/RSC/plugin-rsc imports —
|
|
10
|
+
* so it can be imported by the testing barrel and platform adapters alike.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PrerenderEntry } from "./store.js";
|
|
14
|
+
import { paramsEqual } from "../router/params-util.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Structured durable-overlay key. Includes the router's cache version so a
|
|
18
|
+
* deploy that changed the router's server code never reads Flight payloads
|
|
19
|
+
* that reference its previous build, and so Cloudflare gradual deployments
|
|
20
|
+
* stay correct (old and new worker versions each read their own namespace).
|
|
21
|
+
*/
|
|
22
|
+
export interface PrerenderKey {
|
|
23
|
+
routerId: string;
|
|
24
|
+
/**
|
|
25
|
+
* The owning router's cache version (its data version, or
|
|
26
|
+
* `createRouter({ version })`). Opaque: never set or read by app code; a
|
|
27
|
+
* store keys off it as given.
|
|
28
|
+
*/
|
|
29
|
+
version: string;
|
|
30
|
+
routeName: string;
|
|
31
|
+
/** DJB2 8-hex hash of the canonical params (see param-hash.ts). */
|
|
32
|
+
paramHash: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @internal What the trigger stamps on a write. TTL is soft staleness metadata
|
|
37
|
+
* (`staleAt`), never a hard store expiry: hard expiry would delete the entry
|
|
38
|
+
* a stale-while-revalidate serve needs, and an expired overlay would fall back
|
|
39
|
+
* to older bundled-manifest content.
|
|
40
|
+
*/
|
|
41
|
+
export interface StoredEntryOptions {
|
|
42
|
+
/** Seconds until the entry is considered stale. Absent = never stale. */
|
|
43
|
+
ttl?: number;
|
|
44
|
+
tags: string[];
|
|
45
|
+
/** Canonical params, stored for verify-on-read. */
|
|
46
|
+
params: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What a page and a "removed" marker both carry: a store reads either alike. */
|
|
50
|
+
interface StoredEnvelope {
|
|
51
|
+
v: 1;
|
|
52
|
+
meta: {
|
|
53
|
+
/**
|
|
54
|
+
* When the render started, not when the entry was written: a `markStale()`
|
|
55
|
+
* at or after it (strictly after in an in-process store) covers the entry.
|
|
56
|
+
* A `remove()` marker renders nothing and carries its write time.
|
|
57
|
+
*/
|
|
58
|
+
storedAt: number;
|
|
59
|
+
/**
|
|
60
|
+
* Absent = never stale. Soft metadata only; controls `onRevalidate`
|
|
61
|
+
* scheduling, not serving. Counts the ttl from the write. A store may
|
|
62
|
+
* lower it (tag markers do).
|
|
63
|
+
*/
|
|
64
|
+
staleAt?: number;
|
|
65
|
+
/** The configured ttl in seconds, as a result reports it. Absent = none. */
|
|
66
|
+
ttl?: number;
|
|
67
|
+
tags: string[];
|
|
68
|
+
/** The key's `version` at write time; verified on read. */
|
|
69
|
+
version: string;
|
|
70
|
+
/** Verified against the request's params on read (DJB2 collision guard). */
|
|
71
|
+
params: Record<string, string>;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type StoredPage = StoredEnvelope & { entry: PrerenderEntry; removed?: never };
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The "removed" marker (tombstone) stored in place of a page, by
|
|
79
|
+
* `prerender.remove()` or by a refresh whose render hit `notFound()` (or
|
|
80
|
+
* declined with `ctx.passthrough()`). A request that finds it is not served
|
|
81
|
+
* the build-time entry below the overlay. No `entry`: a router from before the
|
|
82
|
+
* marker rejects it as malformed (a miss) instead of serving an empty page.
|
|
83
|
+
* `meta` is stamped like a page's for a render outcome, so that marker goes
|
|
84
|
+
* stale and is rechecked; a `remove()` marker has no `staleAt` and no tags.
|
|
85
|
+
*/
|
|
86
|
+
type StoredTombstone = StoredEnvelope & { removed: true; entry?: never };
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Versioned envelope the router composes on a refresh or a removal and
|
|
90
|
+
* verifies on every read: a page, or the marker that the page was removed. A
|
|
91
|
+
* store persists either as given and returns it as stored.
|
|
92
|
+
*/
|
|
93
|
+
export type PrerenderStoredEntry = StoredPage | StoredTombstone;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The writable durable overlay: plain get/set. Backed in production by a
|
|
97
|
+
* platform adapter (Cloudflare KV, …) and in dev/tests by an in-memory store.
|
|
98
|
+
* The router composes the envelope before `set()` and verifies whatever `get()`
|
|
99
|
+
* returns (shape, `version`, params collision guard), so a store holds no
|
|
100
|
+
* policy of its own.
|
|
101
|
+
*
|
|
102
|
+
* Key everything off the `key` argument (`serializePrerenderKey(key)` carries
|
|
103
|
+
* `key.version`). Never call `getCacheVersions()` here: the trigger's `set()`
|
|
104
|
+
* runs outside the producer's request context, where it returns the
|
|
105
|
+
* whole-build fallback instead of the owning router's version.
|
|
106
|
+
*/
|
|
107
|
+
export interface WritablePrerenderStore {
|
|
108
|
+
/**
|
|
109
|
+
* The envelope stored under `key`, or null. Never memoize a miss: a refresh
|
|
110
|
+
* can write the key after a request missed it. May lower `meta.staleAt` (a
|
|
111
|
+
* tag marked stale after the entry was written).
|
|
112
|
+
*/
|
|
113
|
+
get(key: PrerenderKey): Promise<PrerenderStoredEntry | null>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Persist `stored` (a page, or the marker that the page was removed) under
|
|
117
|
+
* `key`, replacing any previous entry. No expiry.
|
|
118
|
+
*/
|
|
119
|
+
set(key: PrerenderKey, stored: PrerenderStoredEntry): Promise<void>;
|
|
120
|
+
|
|
121
|
+
delete?(key: PrerenderKey): Promise<void>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Mark the entries of router `routerId` (`PrerenderKey.routerId`) carrying
|
|
125
|
+
* any of `tags` stale (they keep serving). A mark covers an entry whose
|
|
126
|
+
* `meta.storedAt` it is at or after (strictly after for an in-process
|
|
127
|
+
* store), including one `set()` after the mark: a store that marks held
|
|
128
|
+
* entries in place has to remember its marks. Scope it to that router: routers
|
|
129
|
+
* behind a host router share one store, and a tag is only meaningful inside
|
|
130
|
+
* the router whose routes declared it. Optional: without it,
|
|
131
|
+
* `router.prerender().markStale()` is a no-op for this store.
|
|
132
|
+
*/
|
|
133
|
+
markStale?(routerId: string, tags: string[]): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Serialize a {@link PrerenderKey} to the design's string form:
|
|
138
|
+
* `prerender:{routerId}:{version}:{routeName}:{paramHash}`
|
|
139
|
+
*/
|
|
140
|
+
export function serializePrerenderKey(key: PrerenderKey): string {
|
|
141
|
+
return `prerender:${key.routerId}:${key.version}:${key.routeName}:${key.paramHash}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @internal Compose the versioned envelope the trigger hands to `store.set()`.
|
|
146
|
+
* `now` is `meta.storedAt`; `staleFrom` (default `now`) is where the ttl
|
|
147
|
+
* starts counting. Both are passed explicitly so tests stay deterministic.
|
|
148
|
+
*/
|
|
149
|
+
export function composeStoredEntry(
|
|
150
|
+
key: PrerenderKey,
|
|
151
|
+
entry: PrerenderEntry,
|
|
152
|
+
options: StoredEntryOptions,
|
|
153
|
+
now: number,
|
|
154
|
+
staleFrom: number = now,
|
|
155
|
+
): StoredPage {
|
|
156
|
+
// Only a finite, non-negative ttl produces a staleAt: NaN would never go
|
|
157
|
+
// stale and a negative ttl would be stale on every request.
|
|
158
|
+
const hasTtl =
|
|
159
|
+
options.ttl != null &&
|
|
160
|
+
Number.isFinite(options.ttl) &&
|
|
161
|
+
(options.ttl as number) >= 0;
|
|
162
|
+
return {
|
|
163
|
+
v: 1,
|
|
164
|
+
entry,
|
|
165
|
+
meta: {
|
|
166
|
+
storedAt: now,
|
|
167
|
+
...(hasTtl
|
|
168
|
+
? {
|
|
169
|
+
staleAt: staleFrom + (options.ttl as number) * 1000,
|
|
170
|
+
ttl: options.ttl as number,
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
173
|
+
tags: options.tags,
|
|
174
|
+
version: key.version,
|
|
175
|
+
params: options.params,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @internal Compose the "removed" marker stored in place of a page. Its meta
|
|
182
|
+
* is a page's (composeStoredEntry), so `ttl` and `tags` mean the same: the
|
|
183
|
+
* marker of a render outcome is given the route's and is rechecked like a
|
|
184
|
+
* page; `prerender.remove()` gives neither, and its marker never goes stale.
|
|
185
|
+
*/
|
|
186
|
+
export function composeStoredTombstone(
|
|
187
|
+
key: PrerenderKey,
|
|
188
|
+
options: StoredEntryOptions,
|
|
189
|
+
now: number,
|
|
190
|
+
staleFrom: number = now,
|
|
191
|
+
): StoredTombstone {
|
|
192
|
+
const { v, meta } = composeStoredEntry(
|
|
193
|
+
key,
|
|
194
|
+
{ segments: [], handles: "" },
|
|
195
|
+
options,
|
|
196
|
+
now,
|
|
197
|
+
staleFrom,
|
|
198
|
+
);
|
|
199
|
+
return { v, removed: true, meta };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @internal Verify a store's answer is safe to serve for this key: the
|
|
204
|
+
* envelope shape and version tag, the key version, and the stored params
|
|
205
|
+
* against the request's (the 8-hex DJB2 collision guard, param-hash.ts: a
|
|
206
|
+
* runtime write keyed off webhook-supplied params must not serve one page's
|
|
207
|
+
* content under another's URL, nor remove another's page). False means treat
|
|
208
|
+
* it as a miss. Run by the router on every read, so a third-party store
|
|
209
|
+
* cannot skip it.
|
|
210
|
+
*/
|
|
211
|
+
export function isStoredEntryValidFor(
|
|
212
|
+
stored: unknown,
|
|
213
|
+
key: PrerenderKey,
|
|
214
|
+
params: Record<string, string>,
|
|
215
|
+
): stored is PrerenderStoredEntry {
|
|
216
|
+
// A durable store can hold a parseable-but-malformed value (`"null"`,
|
|
217
|
+
// `{"v":1}`): a miss, never a TypeError on the serve path.
|
|
218
|
+
if (stored == null || typeof stored !== "object") return false;
|
|
219
|
+
const candidate = stored as Partial<PrerenderStoredEntry>;
|
|
220
|
+
const entry = candidate.entry;
|
|
221
|
+
const meta = candidate.meta;
|
|
222
|
+
if (
|
|
223
|
+
candidate.v !== 1 ||
|
|
224
|
+
// The "removed" marker has no entry; a page must have a whole one.
|
|
225
|
+
(candidate.removed !== true &&
|
|
226
|
+
(entry == null ||
|
|
227
|
+
typeof entry !== "object" ||
|
|
228
|
+
!Array.isArray(entry.segments) ||
|
|
229
|
+
typeof entry.handles !== "string")) ||
|
|
230
|
+
meta == null ||
|
|
231
|
+
typeof meta !== "object" ||
|
|
232
|
+
!Array.isArray(meta.tags) ||
|
|
233
|
+
typeof meta.storedAt !== "number"
|
|
234
|
+
) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
if (meta.version !== key.version) return false;
|
|
238
|
+
return paramsEqual(meta.params ?? {}, params);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** True once `now` has passed the entry's soft `staleAt`. Never-stale entries never go stale. */
|
|
242
|
+
export function isStoredEntryStale(
|
|
243
|
+
stored: PrerenderStoredEntry,
|
|
244
|
+
now: number,
|
|
245
|
+
): boolean {
|
|
246
|
+
return stored.meta.staleAt != null && now >= stored.meta.staleAt;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @internal Lower `meta.staleAt` to `at` when it is unset or later. Shared by
|
|
251
|
+
* the stores' tag marking so the rule cannot drift between them.
|
|
252
|
+
*/
|
|
253
|
+
export function lowerStoredEntryStaleAt(
|
|
254
|
+
stored: PrerenderStoredEntry,
|
|
255
|
+
at: number,
|
|
256
|
+
): void {
|
|
257
|
+
if (stored.meta.staleAt == null || stored.meta.staleAt > at) {
|
|
258
|
+
stored.meta.staleAt = at;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @internal `store.get(key)` verified for serving (isStoredEntryValidFor). A
|
|
264
|
+
* store error (e.g. a KV outage) reads as a miss: the serve path falls back to
|
|
265
|
+
* the bundled manifest or live handler, and the trigger's stale check renders.
|
|
266
|
+
*/
|
|
267
|
+
export async function readVerifiedStoredEntry(
|
|
268
|
+
store: WritablePrerenderStore,
|
|
269
|
+
key: PrerenderKey,
|
|
270
|
+
params: Record<string, string>,
|
|
271
|
+
): Promise<PrerenderStoredEntry | null> {
|
|
272
|
+
try {
|
|
273
|
+
const read = await store.get(key);
|
|
274
|
+
return isStoredEntryValidFor(read, key, params) ? read : null;
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
package/src/prerender.ts
CHANGED
|
@@ -123,6 +123,27 @@ export interface PrerenderOptions {
|
|
|
123
123
|
* ```
|
|
124
124
|
*/
|
|
125
125
|
concurrency?: number;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Opt this route into on-demand (ISR-style) prerender refresh via
|
|
129
|
+
* `router.prerender()`. `true` uses the router-level `prerender` defaults; an
|
|
130
|
+
* object overrides them per route.
|
|
131
|
+
*
|
|
132
|
+
* Any truthy value opts in — a literal, a spread, or an imported const:
|
|
133
|
+
* producer retention in the production bundle is driven by the evaluated
|
|
134
|
+
* route manifest (the same source that sets the runtime od flag), not a
|
|
135
|
+
* textual scan of the call.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* export const ProductPage = Prerender(
|
|
140
|
+
* async () => [{ id: "featured" }],
|
|
141
|
+
* async (ctx) => <Product data={await ctx.env.PRODUCTS.get(ctx.params.id)} />,
|
|
142
|
+
* { onDemand: { ttl: 3600, tags: ({ params }) => [`product:${params.id}`] } },
|
|
143
|
+
* );
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
onDemand?: import("./prerender/on-demand.js").OnDemandOption;
|
|
126
147
|
}
|
|
127
148
|
|
|
128
149
|
/**
|
|
@@ -144,6 +165,14 @@ export interface BuildContext<TParams> {
|
|
|
144
165
|
*/
|
|
145
166
|
dev: boolean;
|
|
146
167
|
|
|
168
|
+
/**
|
|
169
|
+
* True when this render is a requestless on-demand refresh driven by
|
|
170
|
+
* `router.prerender()` (rather than a build-time or dev prerender). The
|
|
171
|
+
* producer is still requestless — cookies/headers are unavailable — but
|
|
172
|
+
* `ctx.env` is the live trigger binding, not the shared build env.
|
|
173
|
+
*/
|
|
174
|
+
onDemand: boolean;
|
|
175
|
+
|
|
147
176
|
/**
|
|
148
177
|
* Build-time environment bindings (KV, D1, etc.) supplied by the Vite plugin.
|
|
149
178
|
* Only available when `buildEnv` is configured in rango() options.
|
package/src/reverse.ts
CHANGED
|
@@ -47,7 +47,7 @@ export type ParamsFor<TRoutes, TName extends keyof TRoutes> = ExtractParams<
|
|
|
47
47
|
/**
|
|
48
48
|
* Check if an object type has any keys
|
|
49
49
|
*/
|
|
50
|
-
type IsEmptyObject<T> = keyof T extends never ? true : false;
|
|
50
|
+
export type IsEmptyObject<T> = keyof T extends never ? true : false;
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Extract search schema from a route entry.
|