@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
|
@@ -37,6 +37,12 @@ export interface TrieLeaf {
|
|
|
37
37
|
pr?: true;
|
|
38
38
|
/** Passthrough: handler kept in bundle for live fallback on unknown params */
|
|
39
39
|
pt?: true;
|
|
40
|
+
/**
|
|
41
|
+
* On-demand prerender eligible: the route opted into router.prerender()
|
|
42
|
+
* refresh. Distinct from `pr` — an od route may have no build-baked entry yet
|
|
43
|
+
* still needs a writable-overlay lookup at serve time.
|
|
44
|
+
*/
|
|
45
|
+
od?: true;
|
|
40
46
|
/** Response type for non-RSC routes (json, text, image, any) */
|
|
41
47
|
rt?: string;
|
|
42
48
|
/** Negotiate variants: response-type routes sharing this path */
|
|
@@ -72,6 +78,7 @@ export interface TrieNode {
|
|
|
72
78
|
* @param prerenderRouteNames - Optional set of prerendered route names (sets leaf.pr)
|
|
73
79
|
* @param passthroughRouteNames - Optional set of passthrough route names (sets leaf.pt)
|
|
74
80
|
* @param responseTypeRoutes - Optional map of route name to response type (sets leaf.rt)
|
|
81
|
+
* @param onDemandRouteNames - Optional set of on-demand-eligible route names (sets leaf.od)
|
|
75
82
|
*/
|
|
76
83
|
export function buildRouteTrie(
|
|
77
84
|
routeManifest: Record<string, string>,
|
|
@@ -80,6 +87,7 @@ export function buildRouteTrie(
|
|
|
80
87
|
prerenderRouteNames?: Set<string>,
|
|
81
88
|
passthroughRouteNames?: Set<string>,
|
|
82
89
|
responseTypeRoutes?: Record<string, string>,
|
|
90
|
+
onDemandRouteNames?: Set<string>,
|
|
83
91
|
): TrieNode {
|
|
84
92
|
const root: TrieNode = {};
|
|
85
93
|
|
|
@@ -99,6 +107,7 @@ export function buildRouteTrie(
|
|
|
99
107
|
...(trailingSlash ? { ts: trailingSlash } : {}),
|
|
100
108
|
...(prerenderRouteNames?.has(routeName) ? { pr: true } : {}),
|
|
101
109
|
...(passthroughRouteNames?.has(routeName) ? { pt: true } : {}),
|
|
110
|
+
...(onDemandRouteNames?.has(routeName) ? { od: true } : {}),
|
|
102
111
|
...(responseType ? { rt: responseType } : {}),
|
|
103
112
|
});
|
|
104
113
|
}
|
|
@@ -225,6 +225,29 @@ export interface Rango<
|
|
|
225
225
|
* ```
|
|
226
226
|
*/
|
|
227
227
|
fetch(request: Request, input?: RouterRequestInput<TEnv>): Promise<Response>;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Bind `{ env, ctx }` and get the on-demand (ISR-style) prerender runner, to refresh from a running app —
|
|
231
|
+
* fetch handler, cron, queue, workflow, webhook, or server action.
|
|
232
|
+
*
|
|
233
|
+
* Requestless: the producer renders with route params and env only, never the
|
|
234
|
+
* caller's cookies/headers/session. Only routes that opted in via
|
|
235
|
+
* `Prerender(..., { onDemand })` are persistable; others return a skipped
|
|
236
|
+
* result. Configure the store via the `prerender` router option.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```ts
|
|
240
|
+
* const prerender = router.prerender({ env, ctx });
|
|
241
|
+
* await prerender("/products/42");
|
|
242
|
+
* await prerender(
|
|
243
|
+
* { route: "products.detail", params: { id: "42" } },
|
|
244
|
+
* { throwOnError: true },
|
|
245
|
+
* );
|
|
246
|
+
* await prerender.many(targets, { concurrency: 4 });
|
|
247
|
+
* await prerender.markStale(["product:42"]);
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
prerender: import("../prerender/on-demand.js").PrerenderFn<TEnv, TRoutes>;
|
|
228
251
|
}
|
|
229
252
|
|
|
230
253
|
/**
|
|
@@ -332,6 +355,13 @@ export interface RangoInternal<
|
|
|
332
355
|
*/
|
|
333
356
|
readonly cache?: RangoOptions<TEnv>["cache"];
|
|
334
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Prerender store configuration (the writable durable overlay for on-demand
|
|
360
|
+
* prerender). Stored separately from the `prerender` trigger method so the RSC
|
|
361
|
+
* handler can resolve it per request. @internal
|
|
362
|
+
*/
|
|
363
|
+
readonly _prerenderConfig?: RangoOptions<TEnv>["prerender"];
|
|
364
|
+
|
|
335
365
|
/**
|
|
336
366
|
* Not found component to render when no route matches
|
|
337
367
|
*/
|
|
@@ -620,6 +650,9 @@ export interface RangoInternal<
|
|
|
620
650
|
* Handle an RSC request.
|
|
621
651
|
*/
|
|
622
652
|
fetch(request: Request, input?: RouterRequestInput<TEnv>): Promise<Response>;
|
|
653
|
+
|
|
654
|
+
/** On-demand prerender binder: router.prerender({ env, ctx }) returns the runner. */
|
|
655
|
+
prerender: import("../prerender/on-demand.js").PrerenderFn<TEnv, TRoutes>;
|
|
623
656
|
}
|
|
624
657
|
|
|
625
658
|
/**
|
|
@@ -341,6 +341,36 @@ export interface RangoOptions<TEnv = any> {
|
|
|
341
341
|
searchParams?: CacheSearchParams;
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Writable prerender store for on-demand (ISR-style) prerender refresh.
|
|
346
|
+
*
|
|
347
|
+
* A durable overlay in front of the bundled build manifest. When provided,
|
|
348
|
+
* `router.prerender(target, { env, ctx })` renders a route requestlessly and
|
|
349
|
+
* writes a versioned payload here; the serve path reads the overlay before the
|
|
350
|
+
* bundled manifest. Same union shape as `cache`: a plain config object or a
|
|
351
|
+
* factory resolved per request/per call (never at `createRouter()` time), so
|
|
352
|
+
* multi-tenant apps never memoize one request's binding for another.
|
|
353
|
+
*
|
|
354
|
+
* @example Cloudflare KV overlay with SWR revalidation
|
|
355
|
+
* ```typescript
|
|
356
|
+
* import { createKVPrerenderStore } from "@rangojs/router/prerender/cloudflare";
|
|
357
|
+
*
|
|
358
|
+
* const router = createRouter<AppEnv>({
|
|
359
|
+
* prerender: (env) => ({
|
|
360
|
+
* store: createKVPrerenderStore(env.PRERENDER_KV),
|
|
361
|
+
* ttl: 3600,
|
|
362
|
+
* onRevalidate: (target, e) => e.PRERENDER_QUEUE.send({ target }),
|
|
363
|
+
* }),
|
|
364
|
+
* });
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
prerender?:
|
|
368
|
+
| import("../prerender/on-demand.js").PrerenderConfig<TEnv>
|
|
369
|
+
| ((
|
|
370
|
+
env: TEnv,
|
|
371
|
+
ctx?: ExecutionContext,
|
|
372
|
+
) => import("../prerender/on-demand.js").PrerenderConfig<TEnv>);
|
|
373
|
+
|
|
344
374
|
/**
|
|
345
375
|
* Named cache profiles for "use cache" directive.
|
|
346
376
|
* Profile names map to TTL/SWR configuration.
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
37
37
|
import { _getRequestContext } from "../../server/request-context.js";
|
|
38
38
|
import { getRouterContext } from "../router-context.js";
|
|
39
|
-
import {
|
|
39
|
+
import { trackStreamedHandler } from "./streamed-handler-boundary.js";
|
|
40
40
|
import { observeSegmentHandler } from "../instrument.js";
|
|
41
41
|
import {
|
|
42
42
|
track,
|
|
@@ -280,6 +280,13 @@ export interface ResolveSegmentOptions {
|
|
|
280
280
|
* #918: resolved twice on a MISS, once and discarded on a HIT).
|
|
281
281
|
*/
|
|
282
282
|
chain?: readonly EntryData[];
|
|
283
|
+
/**
|
|
284
|
+
* The matched chain entry being resolved: where the sync path catches a
|
|
285
|
+
* handler error, so a streamed handler (or slot owned by a routeless layout)
|
|
286
|
+
* recovers into the same boundary. resolveSegment sets it for the entry it
|
|
287
|
+
* resolves; nested resolutions keep the outer value.
|
|
288
|
+
*/
|
|
289
|
+
boundaryEntry?: EntryData;
|
|
283
290
|
}
|
|
284
291
|
|
|
285
292
|
/**
|
|
@@ -298,6 +305,8 @@ export async function resolveSegment<TEnv>(
|
|
|
298
305
|
options?: ResolveSegmentOptions,
|
|
299
306
|
): Promise<ResolvedSegment[]> {
|
|
300
307
|
const segments: ResolvedSegment[] = [];
|
|
308
|
+
const boundaryEntry = options?.boundaryEntry ?? entry;
|
|
309
|
+
if (!options?.boundaryEntry) options = { ...options, boundaryEntry };
|
|
301
310
|
|
|
302
311
|
if (entry.type === "layout" || entry.type === "cache") {
|
|
303
312
|
if (!options?.skipLoaders) {
|
|
@@ -398,19 +407,15 @@ export async function resolveSegment<TEnv>(
|
|
|
398
407
|
if (result instanceof Promise) {
|
|
399
408
|
warnOnStreamedResponse(result, entry.id);
|
|
400
409
|
result.finally(doneRouteHandler).catch(() => {});
|
|
401
|
-
|
|
410
|
+
component = trackStreamedHandler(deps, result, {
|
|
411
|
+
boundaryEntry,
|
|
402
412
|
segmentId: entry.shortCode,
|
|
403
413
|
segmentType: entry.type,
|
|
404
|
-
|
|
405
|
-
observeStreamedHandler(
|
|
406
|
-
tracked,
|
|
407
|
-
entry.shortCode,
|
|
408
|
-
entry.type,
|
|
409
|
-
context.pathname,
|
|
414
|
+
context,
|
|
410
415
|
routeKey,
|
|
411
416
|
params,
|
|
412
|
-
|
|
413
|
-
|
|
417
|
+
recover: !options?.throwOnError,
|
|
418
|
+
});
|
|
414
419
|
} else {
|
|
415
420
|
doneRouteHandler();
|
|
416
421
|
component = result;
|
|
@@ -679,19 +684,15 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
679
684
|
: handler;
|
|
680
685
|
if (result instanceof Promise) {
|
|
681
686
|
result.finally(doneParallelHandler).catch(() => {});
|
|
682
|
-
|
|
687
|
+
component = trackStreamedHandler(deps, result, {
|
|
688
|
+
boundaryEntry: options?.boundaryEntry ?? parallelEntry,
|
|
683
689
|
segmentId: `${parentShortCode}.${slot}`,
|
|
684
690
|
segmentType: "parallel",
|
|
685
|
-
|
|
686
|
-
observeStreamedHandler(
|
|
687
|
-
tracked,
|
|
688
|
-
`${parentShortCode}.${slot}`,
|
|
689
|
-
"parallel",
|
|
690
|
-
context.pathname,
|
|
691
|
+
context,
|
|
691
692
|
routeKey,
|
|
692
693
|
params,
|
|
693
|
-
|
|
694
|
-
|
|
694
|
+
recover: !options?.throwOnError,
|
|
695
|
+
});
|
|
695
696
|
} else {
|
|
696
697
|
doneParallelHandler();
|
|
697
698
|
component = result as ReactNode;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* 1. options.key(requestCtx) — full override, stored namespaced as
|
|
10
10
|
* loader:{loaderId}:key:{encodeURIComponent(result)} (#1009)
|
|
11
11
|
* 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
|
|
12
|
-
* 3. loader:{loaderId}:{host}{pathname}:{sortedParams} — default
|
|
12
|
+
* 3. loader:{loaderId}:{routerId}@{host}{pathname}:{sortedParams} — default
|
|
13
|
+
* (a loader's ctx is one router's: cache-key-utils.ts, the router rule)
|
|
13
14
|
*
|
|
14
15
|
* Values are serialized via RSC Flight (serializeResult/deserializeResult),
|
|
15
16
|
* supporting ReactNode, Promises, null, and all RSC-serializable types.
|
|
@@ -77,7 +78,7 @@ import {
|
|
|
77
78
|
import { observePhase, PHASES } from "../instrument.js";
|
|
78
79
|
import {
|
|
79
80
|
KEY_PART_PREFIX,
|
|
80
|
-
|
|
81
|
+
requestKeyBase,
|
|
81
82
|
} from "../../cache/cache-key-utils.js";
|
|
82
83
|
import {
|
|
83
84
|
resolveTtl,
|
|
@@ -88,6 +89,7 @@ import {
|
|
|
88
89
|
DEFAULT_ROUTE_TTL,
|
|
89
90
|
} from "../../cache/cache-policy.js";
|
|
90
91
|
import { readThroughItem } from "../../cache/read-through-swr.js";
|
|
92
|
+
import { isWarmReplace, noteWarmWrite } from "../../prerender/warm-request.js";
|
|
91
93
|
import {
|
|
92
94
|
executionStart,
|
|
93
95
|
predatesInvalidation,
|
|
@@ -138,17 +140,6 @@ function debugLoaderCacheLog(message: string): void {
|
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
function getDefaultLoaderCacheKey(
|
|
142
|
-
loaderId: string,
|
|
143
|
-
host: string,
|
|
144
|
-
pathname: string,
|
|
145
|
-
params: Record<string, string>,
|
|
146
|
-
): string {
|
|
147
|
-
const paramStr = sortedRouteParams(params);
|
|
148
|
-
const base = paramStr ? `${pathname}:${paramStr}` : pathname;
|
|
149
|
-
return `loader:${loaderId}:${host}${base}`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
143
|
/**
|
|
153
144
|
* A loader's own `key()` result as its entry key (#1009). The result is
|
|
154
145
|
* often request input, and the item family (`getItem`/`setItem`) has no
|
|
@@ -159,7 +150,7 @@ function getDefaultLoaderCacheKey(
|
|
|
159
150
|
* - an encoded result holds no `:`, so the key reads unambiguously from
|
|
160
151
|
* the right (result, `key:`, loader id), and two namespaced keys are
|
|
161
152
|
* equal only for the same loader and result;
|
|
162
|
-
* - a default key `loader:<id
|
|
153
|
+
* - a default key `loader:<id>:[<router>@]<host><pathname>...` holds a `/` past its
|
|
163
154
|
* id, and an encoded result holds none, so the two are equal only if one
|
|
164
155
|
* loader's id is literally `<other id>:<host>/<path>...`. Loader ids can
|
|
165
156
|
* hold `:` (a dev id is a root-relative path, absolute `D:/...` on
|
|
@@ -188,8 +179,9 @@ async function resolveLoaderKey(
|
|
|
188
179
|
// (cache-runtime pushes ctx.url.host). Without it, a multi-tenant host router
|
|
189
180
|
// serving the same pathname for different hosts would leak one host's cached
|
|
190
181
|
// loader data to another.
|
|
191
|
-
const
|
|
192
|
-
const
|
|
182
|
+
const requestCtx = getRequestContext();
|
|
183
|
+
const host = requestCtx?.url?.host ?? "localhost";
|
|
184
|
+
const defaultKey = `loader:${loaderId}:${requestKeyBase(requestCtx, host, pathname, undefined, params)}`;
|
|
193
185
|
if (options === false) return { key: defaultKey, declared: false };
|
|
194
186
|
const keyFn = options.key;
|
|
195
187
|
return {
|
|
@@ -768,7 +760,11 @@ function executeLoaderData<TEnv>(
|
|
|
768
760
|
};
|
|
769
761
|
|
|
770
762
|
const data = await readThroughItem({
|
|
771
|
-
|
|
763
|
+
// A router.prerender() warm takes the miss path: the body runs and
|
|
764
|
+
// setItem replaces the entry.
|
|
765
|
+
getItem: isWarmReplace(requestCtxForExecute)
|
|
766
|
+
? async () => null
|
|
767
|
+
: (k) => store.getItem!(k),
|
|
772
768
|
// Handles ride the entry like "use cache" (encodeHandles: Flight, pending
|
|
773
769
|
// pushes awaited up to its timeout, the whole blob dropped on a timeout
|
|
774
770
|
// or a thrown encode). Encoded here, inside the deferred write, so a
|
|
@@ -795,7 +791,9 @@ function executeLoaderData<TEnv>(
|
|
|
795
791
|
...o,
|
|
796
792
|
tags: entryTags.length > 0 ? entryTags : undefined,
|
|
797
793
|
...(handles ? { handles } : {}),
|
|
794
|
+
startedAt: start?.at,
|
|
798
795
|
});
|
|
796
|
+
noteWarmWrite(requestCtxForExecute, "item");
|
|
799
797
|
}),
|
|
800
798
|
key,
|
|
801
799
|
execute: async () => {
|
|
@@ -47,7 +47,7 @@ import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
|
47
47
|
import { getRouterContext } from "../router-context.js";
|
|
48
48
|
import { observeEvent, observeSegmentHandler } from "../instrument.js";
|
|
49
49
|
import { runInSegmentTagScope } from "../../cache/cache-tag.js";
|
|
50
|
-
import {
|
|
50
|
+
import { trackStreamedHandler } from "./streamed-handler-boundary.js";
|
|
51
51
|
import {
|
|
52
52
|
track,
|
|
53
53
|
RangoContext,
|
|
@@ -441,6 +441,8 @@ async function resolveParallelSlotComponent<TEnv>(args: {
|
|
|
441
441
|
deps: SegmentResolutionDeps<TEnv>;
|
|
442
442
|
routeKey: string;
|
|
443
443
|
params: Record<string, string>;
|
|
444
|
+
/** Matched chain entry (where the sync path catches), else the slot's owner. */
|
|
445
|
+
boundaryEntry: EntryData;
|
|
444
446
|
}): Promise<{ component: ReactNode | undefined; handlerRan: boolean }> {
|
|
445
447
|
const {
|
|
446
448
|
shouldResolve,
|
|
@@ -452,6 +454,7 @@ async function resolveParallelSlotComponent<TEnv>(args: {
|
|
|
452
454
|
deps,
|
|
453
455
|
routeKey,
|
|
454
456
|
params,
|
|
457
|
+
boundaryEntry,
|
|
455
458
|
} = args;
|
|
456
459
|
|
|
457
460
|
let component: ReactNode | undefined;
|
|
@@ -484,19 +487,15 @@ async function resolveParallelSlotComponent<TEnv>(args: {
|
|
|
484
487
|
: handler;
|
|
485
488
|
if (result instanceof Promise) {
|
|
486
489
|
warnOnStreamedResponse(result, parallelId);
|
|
487
|
-
|
|
490
|
+
component = trackStreamedHandler(deps, result, {
|
|
491
|
+
boundaryEntry,
|
|
488
492
|
segmentId: parallelId,
|
|
489
493
|
segmentType: "parallel",
|
|
490
|
-
|
|
491
|
-
observeStreamedHandler(
|
|
492
|
-
tracked,
|
|
493
|
-
parallelId,
|
|
494
|
-
"parallel",
|
|
495
|
-
context.pathname,
|
|
494
|
+
context,
|
|
496
495
|
routeKey,
|
|
497
496
|
params,
|
|
498
|
-
|
|
499
|
-
|
|
497
|
+
recover: true,
|
|
498
|
+
});
|
|
500
499
|
} else {
|
|
501
500
|
component = result as ReactNode;
|
|
502
501
|
}
|
|
@@ -547,6 +546,8 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
547
546
|
* to it: loader sub-ids are filtered out and slots are re-grouped by parent).
|
|
548
547
|
*/
|
|
549
548
|
loaderOrder?: "after" | "before";
|
|
549
|
+
/** Matched chain entry being resolved; see ResolveSegmentOptions.boundaryEntry. */
|
|
550
|
+
boundaryEntry?: EntryData;
|
|
550
551
|
},
|
|
551
552
|
): Promise<SegmentRevalidationResult> {
|
|
552
553
|
const segments: ResolvedSegment[] = [];
|
|
@@ -696,6 +697,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
696
697
|
deps,
|
|
697
698
|
routeKey,
|
|
698
699
|
params,
|
|
700
|
+
boundaryEntry: options?.boundaryEntry ?? entry,
|
|
699
701
|
});
|
|
700
702
|
|
|
701
703
|
segments.push({
|
|
@@ -865,19 +867,17 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
865
867
|
if (result instanceof Promise) {
|
|
866
868
|
warnOnStreamedResponse(result, routeEntry.id);
|
|
867
869
|
result.finally(doneHandler).catch(() => {});
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
);
|
|
880
|
-
return { content: tracked };
|
|
870
|
+
return {
|
|
871
|
+
content: trackStreamedHandler(deps, result, {
|
|
872
|
+
boundaryEntry: entry,
|
|
873
|
+
segmentId: entry.shortCode,
|
|
874
|
+
segmentType: entry.type,
|
|
875
|
+
context,
|
|
876
|
+
routeKey,
|
|
877
|
+
params,
|
|
878
|
+
recover: true,
|
|
879
|
+
}),
|
|
880
|
+
};
|
|
881
881
|
}
|
|
882
882
|
doneHandler();
|
|
883
883
|
return { content: result };
|
|
@@ -1098,6 +1098,8 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
1098
1098
|
deps,
|
|
1099
1099
|
actionContext,
|
|
1100
1100
|
stale,
|
|
1101
|
+
undefined,
|
|
1102
|
+
entry,
|
|
1101
1103
|
);
|
|
1102
1104
|
segments.push(...orphanResult.segments);
|
|
1103
1105
|
matchedIds.push(...orphanResult.matchedIds);
|
|
@@ -1126,6 +1128,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1126
1128
|
stale?: boolean,
|
|
1127
1129
|
/** Parent route entry — its loaders are inherited so parallel slots can access them. */
|
|
1128
1130
|
parentRouteEntry?: EntryData,
|
|
1131
|
+
/** Matched chain entry being resolved (see ResolveSegmentOptions.boundaryEntry); defaults to parentRouteEntry. */
|
|
1132
|
+
boundaryEntry: EntryData | undefined = parentRouteEntry,
|
|
1129
1133
|
): Promise<SegmentRevalidationResult> {
|
|
1130
1134
|
invariant(
|
|
1131
1135
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -1286,7 +1290,11 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1286
1290
|
deps,
|
|
1287
1291
|
actionContext,
|
|
1288
1292
|
stale,
|
|
1289
|
-
{
|
|
1293
|
+
{
|
|
1294
|
+
parentChainDefault: "force-render",
|
|
1295
|
+
loaderOrder: "before",
|
|
1296
|
+
boundaryEntry,
|
|
1297
|
+
},
|
|
1290
1298
|
);
|
|
1291
1299
|
segments.push(...parallelResult.segments);
|
|
1292
1300
|
matchedIds.push(...parallelResult.matchedIds);
|
|
@@ -1307,6 +1315,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1307
1315
|
deps,
|
|
1308
1316
|
actionContext,
|
|
1309
1317
|
stale,
|
|
1318
|
+
undefined,
|
|
1319
|
+
boundaryEntry,
|
|
1310
1320
|
);
|
|
1311
1321
|
segments.push(...nestedResult.segments);
|
|
1312
1322
|
matchedIds.push(...nestedResult.matchedIds);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streamed handlers (loading()): track, observe, and recover a rejection into
|
|
3
|
+
* the declared errorBoundary() / notFoundBoundary() fallback.
|
|
4
|
+
*
|
|
5
|
+
* Invariants:
|
|
6
|
+
* - No status write and no onError call: the status is committed at first
|
|
7
|
+
* flush, and trackHandler already reports each rejection once.
|
|
8
|
+
* - The recovered promise resolves, so Flight reports no error. The writers
|
|
9
|
+
* that refuse a failed render through Flight errors need the failure
|
|
10
|
+
* recorded here: `_renderErrors` for the document cache and a prerender
|
|
11
|
+
* warm, `_recoveredHandlerErrors` for cacheRoute (cache(), and the doc record
|
|
12
|
+
* a PPR capture needs). The shell capture and the SWR re-render own both,
|
|
13
|
+
* so a derived render never writes into (or reads) the foreground's.
|
|
14
|
+
* - Nothing to render to rethrows the original rejection: no boundary found, a
|
|
15
|
+
* thrown Response (redirect), a Skip, a throwing fallback.
|
|
16
|
+
* - No recovery when `recover` is false (prerender throwOnError, the intercept
|
|
17
|
+
* background re-render): those lanes must reject so nothing is baked/cached.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ReactNode } from "react";
|
|
21
|
+
import { isDataNotFoundError } from "../../errors.js";
|
|
22
|
+
import {
|
|
23
|
+
createErrorInfo,
|
|
24
|
+
createNotFoundInfo,
|
|
25
|
+
renderErrorFallback,
|
|
26
|
+
renderNotFoundFallback,
|
|
27
|
+
resolveNotFoundFallback,
|
|
28
|
+
} from "../error-handling.js";
|
|
29
|
+
import { _getRequestContext } from "../../server/request-context.js";
|
|
30
|
+
import type { EntryData } from "../../server/context.js";
|
|
31
|
+
import type { ErrorInfo, HandlerContext } from "../../types/index.js";
|
|
32
|
+
import type { SegmentResolutionDeps } from "../types.js";
|
|
33
|
+
import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
|
|
34
|
+
|
|
35
|
+
export interface StreamedHandlerOptions<TEnv> {
|
|
36
|
+
/**
|
|
37
|
+
* Entry to look boundaries up from: the matched chain entry being resolved
|
|
38
|
+
* (where the sync path catches, whatever routeless layout owns a slot), or
|
|
39
|
+
* the intercept's declaring entry.
|
|
40
|
+
*/
|
|
41
|
+
boundaryEntry: EntryData;
|
|
42
|
+
segmentId: string;
|
|
43
|
+
segmentType: ErrorInfo["segmentType"];
|
|
44
|
+
context: HandlerContext<any, TEnv>;
|
|
45
|
+
routeKey?: string;
|
|
46
|
+
params: Record<string, string>;
|
|
47
|
+
/** False where the rejection must reach the caller (prerender, intercept background re-render). */
|
|
48
|
+
recover: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const NOOP = (): void => {};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Track a streamed handler promise and return the segment component: the
|
|
55
|
+
* tracked promise itself when there is nothing to recover to, else a promise
|
|
56
|
+
* that resolves to the declared fallback on rejection.
|
|
57
|
+
*/
|
|
58
|
+
export function trackStreamedHandler<TEnv>(
|
|
59
|
+
deps: SegmentResolutionDeps<TEnv>,
|
|
60
|
+
result: Promise<unknown>,
|
|
61
|
+
options: StreamedHandlerOptions<TEnv>,
|
|
62
|
+
): ReactNode {
|
|
63
|
+
const { segmentId, segmentType, context } = options;
|
|
64
|
+
const tracked = deps.trackHandler(result as Promise<ReactNode>, {
|
|
65
|
+
segmentId,
|
|
66
|
+
segmentType,
|
|
67
|
+
});
|
|
68
|
+
observeStreamedHandler(
|
|
69
|
+
tracked,
|
|
70
|
+
segmentId,
|
|
71
|
+
segmentType,
|
|
72
|
+
context.pathname,
|
|
73
|
+
options.routeKey,
|
|
74
|
+
options.params,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (!options.recover) return tracked as ReactNode;
|
|
78
|
+
const { boundaryEntry } = options;
|
|
79
|
+
const hasBoundary =
|
|
80
|
+
deps.findNearestErrorBoundary(boundaryEntry) != null ||
|
|
81
|
+
deps.findNearestNotFoundBoundary(boundaryEntry) != null ||
|
|
82
|
+
deps.notFoundComponent !== undefined;
|
|
83
|
+
if (!hasBoundary) return tracked as ReactNode;
|
|
84
|
+
|
|
85
|
+
// Captured now: the rejection can settle outside the request's ALS frame.
|
|
86
|
+
const reqCtx = _getRequestContext();
|
|
87
|
+
const recovered = tracked.catch((error: unknown): ReactNode => {
|
|
88
|
+
const node = renderBoundary(error, deps, options, boundaryEntry);
|
|
89
|
+
if (node === undefined) throw error;
|
|
90
|
+
if (reqCtx)
|
|
91
|
+
(reqCtx._recoveredHandlerErrors ??= new Map()).set(segmentId, error);
|
|
92
|
+
reqCtx?._renderErrors?.push(error);
|
|
93
|
+
return node;
|
|
94
|
+
});
|
|
95
|
+
// A dropped segment (never rendered) must not become an unhandledRejection.
|
|
96
|
+
recovered.catch(NOOP);
|
|
97
|
+
return recovered as ReactNode;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderBoundary<TEnv>(
|
|
101
|
+
error: unknown,
|
|
102
|
+
deps: SegmentResolutionDeps<TEnv>,
|
|
103
|
+
options: StreamedHandlerOptions<TEnv>,
|
|
104
|
+
boundaryEntry: EntryData,
|
|
105
|
+
): ReactNode | undefined {
|
|
106
|
+
if (error instanceof Response) return undefined;
|
|
107
|
+
if ((error as { name?: string } | null)?.name === "Skip") return undefined;
|
|
108
|
+
const { segmentId, segmentType, context } = options;
|
|
109
|
+
try {
|
|
110
|
+
if (isDataNotFoundError(error)) {
|
|
111
|
+
const boundary = deps.findNearestNotFoundBoundary(boundaryEntry);
|
|
112
|
+
if (boundary == null && deps.notFoundComponent === undefined) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return renderNotFoundFallback(
|
|
116
|
+
resolveNotFoundFallback(
|
|
117
|
+
boundary,
|
|
118
|
+
deps.notFoundComponent,
|
|
119
|
+
context.pathname,
|
|
120
|
+
),
|
|
121
|
+
createNotFoundInfo(error, segmentId, segmentType, context.pathname),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const fallback = deps.findNearestErrorBoundary(boundaryEntry);
|
|
125
|
+
if (fallback == null) return undefined;
|
|
126
|
+
return renderErrorFallback(
|
|
127
|
+
fallback,
|
|
128
|
+
createErrorInfo(error, segmentId, segmentType),
|
|
129
|
+
);
|
|
130
|
+
} catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -25,6 +25,8 @@ export interface TrieMatchResult {
|
|
|
25
25
|
pr?: true;
|
|
26
26
|
/** Passthrough: handler kept for live fallback on unknown params */
|
|
27
27
|
pt?: true;
|
|
28
|
+
/** On-demand prerender eligible (router.prerender() refreshable) */
|
|
29
|
+
od?: true;
|
|
28
30
|
/** Response type for non-RSC routes (json, text, image, any) */
|
|
29
31
|
responseType?: string;
|
|
30
32
|
/** Negotiate variants: response-type routes sharing this path */
|
|
@@ -343,6 +345,7 @@ function validateAndBuild(
|
|
|
343
345
|
if (redirectTo) result.redirectTo = redirectTo;
|
|
344
346
|
if (leaf.pr) result.pr = true;
|
|
345
347
|
if (leaf.pt) result.pt = true;
|
|
348
|
+
if (leaf.od) result.od = true;
|
|
346
349
|
if (leaf.rt) result.responseType = leaf.rt;
|
|
347
350
|
if (leaf.nv) result.negotiateVariants = leaf.nv;
|
|
348
351
|
if (leaf.rf) result.rscFirst = true;
|