@rangojs/router 0.21.0 → 0.22.1
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 +180 -497
- package/package.json +13 -3
- 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/loader-registry.ts +6 -1
- 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
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
createNavigationStore,
|
|
8
8
|
generateHistoryKey,
|
|
9
|
+
liveHandleDataForCache,
|
|
9
10
|
} from "./navigation-store.js";
|
|
10
11
|
import { createEventController } from "./event-controller.js";
|
|
11
12
|
import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
|
|
@@ -27,11 +28,14 @@ import { expandPayloadFragments } from "../segment-fragments.js";
|
|
|
27
28
|
import { initRangoState } from "./rango-state.js";
|
|
28
29
|
import { registerNavigationStore } from "./navigation-store-handle.js";
|
|
29
30
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
31
|
+
import {
|
|
32
|
+
auditTreeCause,
|
|
33
|
+
setSuspenseAuditStreamProbe,
|
|
34
|
+
} from "../suspense-audit.js";
|
|
30
35
|
import {
|
|
31
36
|
setPrefetchConcurrency,
|
|
32
37
|
setPrefetchDecoder,
|
|
33
38
|
} from "./prefetch/loader.js";
|
|
34
|
-
import { setDefaultPrefetchStrategy } from "./prefetch/default-strategy.js";
|
|
35
39
|
import { setAppVersion } from "./app-version.js";
|
|
36
40
|
import {
|
|
37
41
|
isInterceptSegment,
|
|
@@ -147,6 +151,12 @@ export interface BrowserAppContext {
|
|
|
147
151
|
* prefetch TTL are document-lifetime too (see AppShell).
|
|
148
152
|
*/
|
|
149
153
|
appShellRef?: import("./app-shell.js").AppShellRef;
|
|
154
|
+
/**
|
|
155
|
+
* Resolves when the document's stream has ended and every Suspense boundary
|
|
156
|
+
* in it has been revealed. NavigationProvider closes the hydration window
|
|
157
|
+
* then (NavigationProviderProps.hydration).
|
|
158
|
+
*/
|
|
159
|
+
documentRevealed: Promise<void>;
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
// Module-level state for the initialized app
|
|
@@ -255,6 +265,9 @@ export async function initBrowserApp(
|
|
|
255
265
|
const eventController = createEventController({
|
|
256
266
|
initialLocation: new URL(window.location.href),
|
|
257
267
|
});
|
|
268
|
+
if (process.env.NODE_ENV !== "production") {
|
|
269
|
+
setSuspenseAuditStreamProbe(() => eventController.getState().isStreaming);
|
|
270
|
+
}
|
|
258
271
|
|
|
259
272
|
// The document's entry, after the clearOnReload pass and under the version
|
|
260
273
|
// set above: what readers get once they have hydrated.
|
|
@@ -307,11 +320,9 @@ export async function initBrowserApp(
|
|
|
307
320
|
if (lateGenerator) {
|
|
308
321
|
void (async () => {
|
|
309
322
|
try {
|
|
310
|
-
// Applied
|
|
311
|
-
//
|
|
312
|
-
// the
|
|
313
|
-
// meanwhile; nothing is lost.
|
|
314
|
-
await hydrationCommitted;
|
|
323
|
+
// Applied as they arrive: during the hydration window (lockHydration
|
|
324
|
+
// below) the store keeps them in order, and readers see them after
|
|
325
|
+
// the commit that hydrates every boundary (NavigationProvider).
|
|
315
326
|
for await (const rawLateData of lateGenerator) {
|
|
316
327
|
// Handles are async by design: a push(promise) value resolves
|
|
317
328
|
// whenever it resolves — only SSR'd handles are awaited server-
|
|
@@ -337,10 +348,11 @@ export async function initBrowserApp(
|
|
|
337
348
|
} else {
|
|
338
349
|
bootLog("handles: none in payload");
|
|
339
350
|
}
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
|
|
351
|
+
// The hydration window: until NavigationProvider's barrier commit has
|
|
352
|
+
// hydrated every boundary, reads return the handle state the document's HTML
|
|
353
|
+
// was rendered with, and the late channel's and any navigation's handle
|
|
354
|
+
// writes wait in order (EventController.lockHydration).
|
|
355
|
+
eventController.lockHydration();
|
|
344
356
|
|
|
345
357
|
// Create composable utilities
|
|
346
358
|
const client = createNavigationClient(deps);
|
|
@@ -371,13 +383,10 @@ export async function initBrowserApp(
|
|
|
371
383
|
if (prefetchConcurrency !== undefined) {
|
|
372
384
|
setPrefetchConcurrency(prefetchConcurrency);
|
|
373
385
|
}
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
386
|
+
// The router-wide default Link prefetch strategy, for delegated plain
|
|
387
|
+
// anchors (navigation bridge). Undefined falls back to the environment
|
|
388
|
+
// default (default-strategy.ts).
|
|
377
389
|
const defaultPrefetch = initialPayload.metadata?.defaultPrefetch;
|
|
378
|
-
if (defaultPrefetch !== undefined) {
|
|
379
|
-
setDefaultPrefetchStrategy(defaultPrefetch);
|
|
380
|
-
}
|
|
381
390
|
|
|
382
391
|
// Wire the RSC decoder so prefetches decode eagerly and warm the route's
|
|
383
392
|
// client chunks (same createFromFetch the navigation client uses). Fragment
|
|
@@ -580,7 +589,11 @@ export async function initBrowserApp(
|
|
|
580
589
|
intercept: responseIsIntercept,
|
|
581
590
|
});
|
|
582
591
|
store.setHistoryKey(historyKey);
|
|
583
|
-
const currentHandleData =
|
|
592
|
+
const currentHandleData = liveHandleDataForCache(
|
|
593
|
+
eventController,
|
|
594
|
+
store,
|
|
595
|
+
historyKey,
|
|
596
|
+
);
|
|
584
597
|
store.cacheSegmentsForHistory(
|
|
585
598
|
historyKey,
|
|
586
599
|
segments,
|
|
@@ -588,6 +601,7 @@ export async function initBrowserApp(
|
|
|
588
601
|
);
|
|
589
602
|
|
|
590
603
|
const { main, intercept } = splitInterceptSegments(segments);
|
|
604
|
+
if (process.env.NODE_ENV !== "production") auditTreeCause("hmr");
|
|
591
605
|
store.emitUpdate({
|
|
592
606
|
root: renderSegments(main, {
|
|
593
607
|
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
@@ -627,6 +641,28 @@ export async function initBrowserApp(
|
|
|
627
641
|
linkInterceptionEnabled: linkInterception,
|
|
628
642
|
version,
|
|
629
643
|
appShellRef,
|
|
644
|
+
documentRevealed: new Promise<void>((resolve) => {
|
|
645
|
+
// Parsing ends with the stream, but React reveals a streamed boundary
|
|
646
|
+
// in batches up to 300 ms later (Fizz's $RC marks it `$~` and reveals
|
|
647
|
+
// it from a timer), so the boundary is still pending at
|
|
648
|
+
// DOMContentLoaded. A transition then would make React client-render
|
|
649
|
+
// it over its server HTML.
|
|
650
|
+
const start = (): void => {
|
|
651
|
+
// A reveal rewrites the same comment's data, or React removes the
|
|
652
|
+
// node; a boundary nested in revealed content joins the document
|
|
653
|
+
// only then, so resolve only once a full walk finds none.
|
|
654
|
+
let pending = unrevealedBoundaries();
|
|
655
|
+
const check = (): void => {
|
|
656
|
+
pending = pending.filter(isUnrevealed);
|
|
657
|
+
if (pending.length === 0) pending = unrevealedBoundaries();
|
|
658
|
+
if (pending.length > 0) requestAnimationFrame(check);
|
|
659
|
+
else resolve();
|
|
660
|
+
};
|
|
661
|
+
check();
|
|
662
|
+
};
|
|
663
|
+
if (document.readyState !== "loading") start();
|
|
664
|
+
else document.addEventListener("DOMContentLoaded", start, { once: true });
|
|
665
|
+
}),
|
|
630
666
|
};
|
|
631
667
|
browserAppContext = context;
|
|
632
668
|
|
|
@@ -634,27 +670,27 @@ export async function initBrowserApp(
|
|
|
634
670
|
return context;
|
|
635
671
|
}
|
|
636
672
|
|
|
673
|
+
/**
|
|
674
|
+
* A Suspense boundary of the document React has not revealed yet: Fizz marks
|
|
675
|
+
* one `$?` while its content streams and `$~` once its reveal is queued.
|
|
676
|
+
*/
|
|
677
|
+
function isUnrevealed(node: Comment): boolean {
|
|
678
|
+
return node.isConnected && (node.data === "$?" || node.data === "$~");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function unrevealedBoundaries(): Comment[] {
|
|
682
|
+
const found: Comment[] = [];
|
|
683
|
+
const walker = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
|
|
684
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
685
|
+
if (isUnrevealed(node as Comment)) found.push(node as Comment);
|
|
686
|
+
}
|
|
687
|
+
return found;
|
|
688
|
+
}
|
|
689
|
+
|
|
637
690
|
// Once-flag so the hydration-commit boot log fires a single time (StrictMode
|
|
638
691
|
// re-runs the root effect; the second flush is not a second hydration).
|
|
639
692
|
let hydrationCommitLogged = false;
|
|
640
693
|
|
|
641
|
-
// Hydration-commit barrier for the late handle channel: the late consumer
|
|
642
|
-
// applies nothing before the root's effects have run. A hydrating render
|
|
643
|
-
// reads the handle state frozen before hydrateRoot (useHandle,
|
|
644
|
-
// EventController.getHydrationHandleState), so an earlier application would
|
|
645
|
-
// no longer mismatch (it did while the initializer read the LIVE state: seen
|
|
646
|
-
// when a loader's push lost the handler-barrier race by milliseconds and its
|
|
647
|
-
// late yield landed mid-hydration). What the barrier still buys: a reader
|
|
648
|
-
// that hydrates after the live state moved on is rendered a second time
|
|
649
|
-
// right after, since React re-renders a reader whose hydration state is not
|
|
650
|
-
// the client one. Readers that hydrate with the root would all pay that;
|
|
651
|
-
// after the commit they are subscribed and take the update as an ordinary
|
|
652
|
-
// one. The root effect below resolves this.
|
|
653
|
-
let resolveHydrationCommitted!: () => void;
|
|
654
|
-
const hydrationCommitted: Promise<void> = new Promise((resolve) => {
|
|
655
|
-
resolveHydrationCommitted = resolve;
|
|
656
|
-
});
|
|
657
|
-
|
|
658
694
|
/**
|
|
659
695
|
* Get the browser app context. Throws if initBrowserApp hasn't been called.
|
|
660
696
|
*/
|
|
@@ -691,13 +727,18 @@ export interface RangoProps {}
|
|
|
691
727
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
692
728
|
*
|
|
693
729
|
* async function main() {
|
|
694
|
-
* await initBrowserApp({ rscStream, deps: rscBrowser });
|
|
730
|
+
* const { initialPayload } = await initBrowserApp({ rscStream, deps: rscBrowser });
|
|
731
|
+
* // Rejects when the action's state is not Flight-serializable.
|
|
732
|
+
* const formState = await Promise.resolve(initialPayload.formState).catch(
|
|
733
|
+
* () => undefined,
|
|
734
|
+
* );
|
|
695
735
|
*
|
|
696
736
|
* hydrateRoot(
|
|
697
737
|
* document,
|
|
698
738
|
* <React.StrictMode>
|
|
699
739
|
* <Rango />
|
|
700
|
-
* </React.StrictMode
|
|
740
|
+
* </React.StrictMode>,
|
|
741
|
+
* formState ? { formState } : undefined
|
|
701
742
|
* );
|
|
702
743
|
* }
|
|
703
744
|
* main();
|
|
@@ -716,6 +757,7 @@ export function Rango(_props: RangoProps): React.ReactElement {
|
|
|
716
757
|
version,
|
|
717
758
|
appShellRef,
|
|
718
759
|
linkInterceptionEnabled,
|
|
760
|
+
documentRevealed,
|
|
719
761
|
} = getBrowserAppContext();
|
|
720
762
|
|
|
721
763
|
// Signal that the React tree has hydrated. useEffect only fires after
|
|
@@ -730,9 +772,6 @@ export function Rango(_props: RangoProps): React.ReactElement {
|
|
|
730
772
|
hydrationCommitLogged = true;
|
|
731
773
|
bootLog("hydration commit (root effect flushed)");
|
|
732
774
|
}
|
|
733
|
-
// Release the late handle channel (see hydrationCommitted above).
|
|
734
|
-
// StrictMode double-invoke: resolving twice is a no-op.
|
|
735
|
-
resolveHydrationCommitted();
|
|
736
775
|
return cleanupPrefetch;
|
|
737
776
|
}, [bridge, linkInterceptionEnabled]);
|
|
738
777
|
|
|
@@ -748,6 +787,7 @@ export function Rango(_props: RangoProps): React.ReactElement {
|
|
|
748
787
|
version={version}
|
|
749
788
|
basename={initialPayload.metadata?.basename}
|
|
750
789
|
appShellRef={appShellRef}
|
|
790
|
+
hydration={{ settled: documentRevealed }}
|
|
751
791
|
/>
|
|
752
792
|
);
|
|
753
793
|
}
|
|
@@ -201,9 +201,9 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
201
201
|
// inside renderSegments, causing React to unmount the entire chain
|
|
202
202
|
// (LoaderBoundary > Suspense > LoaderResolver > RouteContentWrapper >
|
|
203
203
|
// Suspender) every time the user opened an intercept or navigated back
|
|
204
|
-
// to a cached page. The flicker is now prevented by renderSegments
|
|
205
|
-
//
|
|
206
|
-
//
|
|
204
|
+
// to a cached page. The flicker is now prevented by renderSegments
|
|
205
|
+
// handing the boundary settled content and loader data, so preserving
|
|
206
|
+
// `loading` keeps the element tree stable.
|
|
207
207
|
return fromCache;
|
|
208
208
|
})
|
|
209
209
|
.filter(Boolean) as ResolvedSegment[];
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
reconcileErrorSegments,
|
|
14
14
|
} from "./segment-reconciler.js";
|
|
15
15
|
import { startTransition } from "react";
|
|
16
|
+
import { liveHandleDataForCache } from "./navigation-store.js";
|
|
16
17
|
import type { EventController, ActionHandle } from "./event-controller.js";
|
|
17
18
|
import {
|
|
18
19
|
toNetworkError,
|
|
@@ -46,6 +47,7 @@ import {
|
|
|
46
47
|
type TransitionWhenActionInput,
|
|
47
48
|
} from "./transition-when.js";
|
|
48
49
|
import { CLIENT_REVALIDATION_HEADER } from "../client-urls/revalidation-protocol.js";
|
|
50
|
+
import { auditTreeCause } from "../suspense-audit.js";
|
|
49
51
|
|
|
50
52
|
// Polyfill Symbol.dispose/asyncDispose for Safari and older browsers
|
|
51
53
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -648,7 +650,11 @@ export function createServerActionBridge(
|
|
|
648
650
|
// handle pushed by the error-boundary render still applies — see the
|
|
649
651
|
// "normal" case below for why caching after onUpdate dropped it.
|
|
650
652
|
store.setSegmentIds(segmentIdsAfterError);
|
|
651
|
-
const currentHandleData =
|
|
653
|
+
const currentHandleData = liveHandleDataForCache(
|
|
654
|
+
eventController,
|
|
655
|
+
store,
|
|
656
|
+
currentKey,
|
|
657
|
+
);
|
|
652
658
|
store.cacheSegmentsForHistory(
|
|
653
659
|
currentKey,
|
|
654
660
|
errorResult.segments,
|
|
@@ -659,6 +665,7 @@ export function createServerActionBridge(
|
|
|
659
665
|
// Update UI with error boundary: urgent when transition({ when })
|
|
660
666
|
// gated it off, like every gated-off commit.
|
|
661
667
|
const errorUpdate = { root: errorTree, metadata: metadata! };
|
|
668
|
+
if (process.env.NODE_ENV !== "production") auditTreeCause("action");
|
|
662
669
|
if (errorGatedOff) onUpdate(errorUpdate);
|
|
663
670
|
else startTransition(() => onUpdate(errorUpdate));
|
|
664
671
|
|
|
@@ -850,7 +857,11 @@ export function createServerActionBridge(
|
|
|
850
857
|
// Location state already applied above (pre-switch); this action's
|
|
851
858
|
// UI render is skipped because a later sibling consolidates.
|
|
852
859
|
store.setSegmentIds(matched);
|
|
853
|
-
const currentHandleData =
|
|
860
|
+
const currentHandleData = liveHandleDataForCache(
|
|
861
|
+
eventController,
|
|
862
|
+
store,
|
|
863
|
+
currentKey,
|
|
864
|
+
);
|
|
854
865
|
store.cacheSegmentsForHistory(
|
|
855
866
|
currentKey,
|
|
856
867
|
fullSegments,
|
|
@@ -862,9 +873,13 @@ export function createServerActionBridge(
|
|
|
862
873
|
|
|
863
874
|
case "normal": {
|
|
864
875
|
// transition({ when }) for the action commit (kind "action"): the
|
|
865
|
-
// page does not move, so `to` is `from`. False commits urgently
|
|
866
|
-
//
|
|
867
|
-
//
|
|
876
|
+
// page does not move, so `to` is `from`. False commits urgently and
|
|
877
|
+
// switches every <ViewTransition> class to "none"; the tree below
|
|
878
|
+
// is built with isAction, which awaits segment content and loader
|
|
879
|
+
// data, so a boundary on screen is handed settled values. A build
|
|
880
|
+
// then shows no loading(); the dev server still does, because the
|
|
881
|
+
// client references of a fresh payload are still importing in that
|
|
882
|
+
// first render. True keeps the startTransition.
|
|
868
883
|
// This commit runs after awaits, outside the transition React opens
|
|
869
884
|
// for a form action / useActionState call, so React does not force
|
|
870
885
|
// a transition on it. The tree shape never changes, so
|
|
@@ -910,7 +925,11 @@ export function createServerActionBridge(
|
|
|
910
925
|
//
|
|
911
926
|
// Location state already applied above (pre-switch). Update store.
|
|
912
927
|
store.setSegmentIds(matched);
|
|
913
|
-
const currentHandleData =
|
|
928
|
+
const currentHandleData = liveHandleDataForCache(
|
|
929
|
+
eventController,
|
|
930
|
+
store,
|
|
931
|
+
currentKey,
|
|
932
|
+
);
|
|
914
933
|
store.cacheSegmentsForHistory(
|
|
915
934
|
currentKey,
|
|
916
935
|
fullSegments,
|
|
@@ -919,6 +938,7 @@ export function createServerActionBridge(
|
|
|
919
938
|
store.rememberDisplayedEntry(metadata?.routeName);
|
|
920
939
|
|
|
921
940
|
const actionUpdate = { root: newTree, metadata: metadata! };
|
|
941
|
+
if (process.env.NODE_ENV !== "production") auditTreeCause("action");
|
|
922
942
|
if (gatedOff) onUpdate(actionUpdate);
|
|
923
943
|
else startTransition(() => onUpdate(actionUpdate));
|
|
924
944
|
// Invalidation deferred to finalizeAction() (runs after this caches
|
package/src/browser/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode, ComponentType } from "react";
|
|
2
|
+
import type { ReactFormState } from "react-dom/client";
|
|
2
3
|
import type { ResolvedSegment, SlotState } from "../types.js";
|
|
3
4
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
4
5
|
import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
@@ -14,6 +15,11 @@ import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
|
14
15
|
export interface RscPayload<TMetadata = RscMetadata> {
|
|
15
16
|
metadata?: TMetadata;
|
|
16
17
|
returnValue?: ActionResult;
|
|
18
|
+
/**
|
|
19
|
+
* Form state of a PE POST document, for hydrateRoot's `formState` option.
|
|
20
|
+
* Rejects when the action's state is not Flight-serializable.
|
|
21
|
+
*/
|
|
22
|
+
formState?: Promise<ReactFormState>;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
/**
|
|
@@ -459,6 +465,8 @@ export interface NavigateOptionsInternal extends Omit<
|
|
|
459
465
|
state?: unknown;
|
|
460
466
|
/** Skip segment cache (used by redirect-with-state to force re-render) */
|
|
461
467
|
_skipCache?: boolean;
|
|
468
|
+
/** Server redirects already followed by this navigation chain (0 for a user navigation) */
|
|
469
|
+
_redirectHops?: number;
|
|
462
470
|
}
|
|
463
471
|
|
|
464
472
|
/**
|
|
@@ -554,6 +562,9 @@ export type StateListener = () => void;
|
|
|
554
562
|
* EventController owns the public navigation lifecycle exposed by hooks.
|
|
555
563
|
*/
|
|
556
564
|
export interface NavigationStore {
|
|
565
|
+
/** Client loader data for useLoader / useFetchLoader (loader-store.ts). */
|
|
566
|
+
readonly loaders: import("../loader-store.js").LoaderStore;
|
|
567
|
+
|
|
557
568
|
// Internal segment state (for bridges)
|
|
558
569
|
getSegmentState(): SegmentState;
|
|
559
570
|
setPath(path: string): void;
|
|
@@ -61,6 +61,8 @@ export interface GeneratedManifest {
|
|
|
61
61
|
prerenderRoutes?: string[];
|
|
62
62
|
/** Route names wrapped with Passthrough() (live handler for runtime fallback) */
|
|
63
63
|
passthroughRoutes?: string[];
|
|
64
|
+
/** Route names opted into on-demand prerender (Prerender(..., { onDemand })) */
|
|
65
|
+
onDemandRoutes?: string[];
|
|
64
66
|
/** Route name → response type for non-RSC routes */
|
|
65
67
|
responseTypeRoutes?: Record<string, string>;
|
|
66
68
|
/** Route name -> search schema descriptor for typed URL helpers */
|
|
@@ -172,6 +174,7 @@ async function buildPrefixTreeNode(
|
|
|
172
174
|
prerenderRoutes?: string[],
|
|
173
175
|
prerenderDefs?: Record<string, any>,
|
|
174
176
|
passthroughRoutes?: string[],
|
|
177
|
+
onDemandRoutes?: string[],
|
|
175
178
|
responseTypeRoutes?: Record<string, string>,
|
|
176
179
|
routeSearchSchemas?: Record<string, Record<string, string>>,
|
|
177
180
|
routerId?: string,
|
|
@@ -264,6 +267,13 @@ async function buildPrefixTreeNode(
|
|
|
264
267
|
passthroughRoutes.push(name);
|
|
265
268
|
}
|
|
266
269
|
}
|
|
270
|
+
if (
|
|
271
|
+
onDemandRoutes &&
|
|
272
|
+
entry.type === "route" &&
|
|
273
|
+
entry.isOnDemand === true
|
|
274
|
+
) {
|
|
275
|
+
onDemandRoutes.push(name);
|
|
276
|
+
}
|
|
267
277
|
}
|
|
268
278
|
}
|
|
269
279
|
|
|
@@ -289,6 +299,7 @@ async function buildPrefixTreeNode(
|
|
|
289
299
|
prerenderRoutes,
|
|
290
300
|
prerenderDefs,
|
|
291
301
|
passthroughRoutes,
|
|
302
|
+
onDemandRoutes,
|
|
292
303
|
responseTypeRoutes,
|
|
293
304
|
routeSearchSchemas,
|
|
294
305
|
routerId,
|
|
@@ -455,6 +466,7 @@ export async function generateManifestFull<TEnv>(
|
|
|
455
466
|
const prerenderRoutes: string[] = [];
|
|
456
467
|
const prerenderDefs: Record<string, any> = {};
|
|
457
468
|
const passthroughRoutes: string[] = [];
|
|
469
|
+
const onDemandRoutes: string[] = [];
|
|
458
470
|
const responseTypeRoutes: Record<string, string> = {};
|
|
459
471
|
for (const [name, entry] of manifest) {
|
|
460
472
|
if (entry.type === "route" && entry.isPrerender) {
|
|
@@ -465,6 +477,9 @@ export async function generateManifestFull<TEnv>(
|
|
|
465
477
|
if (entry.isPassthrough === true) {
|
|
466
478
|
passthroughRoutes.push(name);
|
|
467
479
|
}
|
|
480
|
+
if (entry.isOnDemand === true) {
|
|
481
|
+
onDemandRoutes.push(name);
|
|
482
|
+
}
|
|
468
483
|
}
|
|
469
484
|
if (entry.type === "route" && entry.responseType) {
|
|
470
485
|
responseTypeRoutes[name] = entry.responseType;
|
|
@@ -485,6 +500,7 @@ export async function generateManifestFull<TEnv>(
|
|
|
485
500
|
prerenderRoutes,
|
|
486
501
|
prerenderDefs,
|
|
487
502
|
passthroughRoutes,
|
|
503
|
+
onDemandRoutes,
|
|
488
504
|
responseTypeRoutes,
|
|
489
505
|
routeSearchSchemas,
|
|
490
506
|
options?.routerId,
|
|
@@ -502,6 +518,7 @@ export async function generateManifestFull<TEnv>(
|
|
|
502
518
|
prerenderRoutes: prerenderRoutes.length > 0 ? prerenderRoutes : undefined,
|
|
503
519
|
passthroughRoutes:
|
|
504
520
|
passthroughRoutes.length > 0 ? passthroughRoutes : undefined,
|
|
521
|
+
onDemandRoutes: onDemandRoutes.length > 0 ? onDemandRoutes : undefined,
|
|
505
522
|
responseTypeRoutes:
|
|
506
523
|
Object.keys(responseTypeRoutes).length > 0
|
|
507
524
|
? responseTypeRoutes
|
|
@@ -136,6 +136,11 @@ export function mergeFullManifests(
|
|
|
136
136
|
manifest.passthroughRoutes,
|
|
137
137
|
overwrittenRoutes,
|
|
138
138
|
),
|
|
139
|
+
onDemandRoutes: mergeRouteList(
|
|
140
|
+
merged.onDemandRoutes,
|
|
141
|
+
manifest.onDemandRoutes,
|
|
142
|
+
overwrittenRoutes,
|
|
143
|
+
),
|
|
139
144
|
responseTypeRoutes: mergeRouteRecord(
|
|
140
145
|
merged.responseTypeRoutes,
|
|
141
146
|
manifest.responseTypeRoutes,
|
package/src/build/route-trie.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* skipping.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { ExecutionContext } from "../types/request-scope.js";
|
|
11
|
+
|
|
10
12
|
interface WaitUntilHost {
|
|
11
13
|
waitUntil?: (fn: () => Promise<void>) => void;
|
|
12
14
|
}
|
|
@@ -85,3 +87,62 @@ export async function settleGrowing(
|
|
|
85
87
|
}
|
|
86
88
|
return true;
|
|
87
89
|
}
|
|
90
|
+
|
|
91
|
+
/** Macrotask turns `awaitLaterMillisecond` waits at most. */
|
|
92
|
+
export const LATER_MILLISECOND_MAX_TURNS = 10;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Yield until `Date.now()` passes `since`, so a store's whole-millisecond tag
|
|
96
|
+
* gate (putShell vs an invalidation made just before) sees a later
|
|
97
|
+
* millisecond. Bounded: on Cloudflare the clock stands still between I/O
|
|
98
|
+
* events, so an unbounded wait could hang. At the bound the caller proceeds
|
|
99
|
+
* and a store may refuse the write, which its result reports.
|
|
100
|
+
*/
|
|
101
|
+
export async function awaitLaterMillisecond(
|
|
102
|
+
since: number,
|
|
103
|
+
maxTurns: number = LATER_MILLISECOND_MAX_TURNS,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
for (let turn = 0; turn < maxTurns && Date.now() === since; turn++) {
|
|
106
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** An ExecutionContext that keeps every promise handed to `waitUntil`. */
|
|
111
|
+
export interface CollectingExecutionContext extends ExecutionContext {
|
|
112
|
+
readonly tasks: Promise<unknown>[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Wrap the caller's ExecutionContext (or none) so a caller can wait for the
|
|
117
|
+
* request's background work: the deferred cache writes, the shell capture, a
|
|
118
|
+
* store's own writes. Every other member reads through to `inner`, bound to
|
|
119
|
+
* it (workerd's methods reject a foreign `this`).
|
|
120
|
+
*/
|
|
121
|
+
export function createCollectingExecutionContext(
|
|
122
|
+
inner?: ExecutionContext,
|
|
123
|
+
): CollectingExecutionContext {
|
|
124
|
+
const tasks: Promise<unknown>[] = [];
|
|
125
|
+
const own: CollectingExecutionContext = {
|
|
126
|
+
tasks,
|
|
127
|
+
waitUntil(promise) {
|
|
128
|
+
const task = Promise.resolve(promise);
|
|
129
|
+
// The list only waits; the host (or the log below) owns the failure.
|
|
130
|
+
tasks.push(task.catch(() => {}));
|
|
131
|
+
if (inner) inner.waitUntil(task);
|
|
132
|
+
else {
|
|
133
|
+
task.catch((error) =>
|
|
134
|
+
console.error("[waitUntil] Background task failed:", error),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
passThroughOnException() {},
|
|
139
|
+
};
|
|
140
|
+
if (!inner) return own;
|
|
141
|
+
return new Proxy(own, {
|
|
142
|
+
get(target, prop) {
|
|
143
|
+
if (prop in target) return target[prop as keyof typeof target];
|
|
144
|
+
const value = (inner as unknown as Record<PropertyKey, unknown>)[prop];
|
|
145
|
+
return typeof value === "function" ? value.bind(inner) : value;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -22,7 +22,7 @@ import type { SearchParamsFilter } from "./search-params-filter.js";
|
|
|
22
22
|
*/
|
|
23
23
|
const RESERVED_SEARCH_PARAMS = new Set(["__no_cache", "__rsc", "__html"]);
|
|
24
24
|
|
|
25
|
-
function isReservedSearchParam(key: string): boolean {
|
|
25
|
+
export function isReservedSearchParam(key: string): boolean {
|
|
26
26
|
return key.startsWith("_rsc") || RESERVED_SEARCH_PARAMS.has(key);
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -65,20 +65,37 @@ export function sortedRouteParams(
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
68
|
+
* THE ROUTER RULE (#1065). A key that names one router's output for a host
|
|
69
|
+
* and path starts with the router part, `{encodeURIComponent(routerId)}@`. A
|
|
70
|
+
* key that names a function and its arguments, or a tag, does not.
|
|
69
71
|
*
|
|
70
|
-
*
|
|
71
|
-
* (
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
72
|
+
* - With the part: PPR shells and the build-shell manifest (buildShellKey,
|
|
73
|
+
* buildShellManifestKey); route cache() records, the document cache and
|
|
74
|
+
* response routes (cacheKeyBase); a loader's own cache() entry; a
|
|
75
|
+
* `"use cache"` entry of a call that takes a ctx (a ctx is one router's:
|
|
76
|
+
* its variables, env and reverse() map).
|
|
77
|
+
* - Without: a `"use cache"` entry of plain arguments or a bare Request
|
|
78
|
+
* (requestArgKey); tag markers; a key the app builds itself
|
|
79
|
+
* (`cache({ key })`, a `keyGenerator`).
|
|
76
80
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
81
|
+
* Why: a host and path do not name a router. A `hostOverride` cookie picks
|
|
82
|
+
* the router and forwards the request unmodified, and a `router.prerender()`
|
|
83
|
+
* warm requests whatever origin it resolved, so two routers on one store
|
|
84
|
+
* built one key and read each other's entry.
|
|
85
|
+
*
|
|
86
|
+
* The id (router.ts) has to be the same in every isolate and in build-time
|
|
87
|
+
* discovery: an explicit `id` and the injected `$$id` are, the `router_{n}`
|
|
88
|
+
* fallback is for a single router only (discover-routers.ts warns about
|
|
89
|
+
* more). Encoded, the id holds no `@` or `/`, and a host holds no `@`, so a
|
|
90
|
+
* key has a router part exactly when its first `@` comes before its first
|
|
91
|
+
* `/`: no id can be chosen to spell another router's key.
|
|
80
92
|
*/
|
|
81
|
-
export function
|
|
93
|
+
export function routerKeyPrefix(routerId: string): string {
|
|
94
|
+
return `${encodeURIComponent(routerId)}@`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** `{host}{pathname}[:params][?search]`: what follows the router part. */
|
|
98
|
+
function hostPathKey(
|
|
82
99
|
host: string,
|
|
83
100
|
pathname: string,
|
|
84
101
|
searchParams?: URLSearchParams,
|
|
@@ -96,6 +113,68 @@ export function cacheKeyBase(
|
|
|
96
113
|
return key;
|
|
97
114
|
}
|
|
98
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Key base of one router's output (the router rule, routerKeyPrefix):
|
|
118
|
+
* `${routerId}@${host}${pathname}[:params][?search]`.
|
|
119
|
+
*
|
|
120
|
+
* The ONE composition of the namespacing rule, shared by the segment tier
|
|
121
|
+
* (cache-scope.ts), the document tier (document-cache.ts), the response tier
|
|
122
|
+
* (rsc/response-cache-serve.ts) and the loader cache so the rule cannot drift
|
|
123
|
+
* between them. Host prefixing matters because VercelCacheStore /
|
|
124
|
+
* MemorySegmentCacheStore key by the raw string (only CFCacheStore adds host
|
|
125
|
+
* internally) -- on a single function serving multiple domains an
|
|
126
|
+
* un-namespaced key bleeds tenant A's cached response to tenant B.
|
|
127
|
+
*
|
|
128
|
+
* Output is BYTE-STABLE by contract: changing the composition silently
|
|
129
|
+
* invalidates every persisted cache entry on upgrade. Callers append their own
|
|
130
|
+
* tier-specific suffixes (`:rsc`/`:html`, segment hash) after this base.
|
|
131
|
+
*/
|
|
132
|
+
export function cacheKeyBase(
|
|
133
|
+
routerId: string,
|
|
134
|
+
host: string,
|
|
135
|
+
pathname: string,
|
|
136
|
+
searchParams?: URLSearchParams,
|
|
137
|
+
params?: Record<string, string>,
|
|
138
|
+
filter?: SearchParamsFilter,
|
|
139
|
+
): string {
|
|
140
|
+
return `${routerKeyPrefix(routerId)}${hostPathKey(host, pathname, searchParams, params, filter)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* cacheKeyBase for the request context `ctx`. rsc/handler.ts sets `_routerId`
|
|
145
|
+
* before the request scope opens and derived contexts inherit it, so a served
|
|
146
|
+
* request always has one. Only a hand-built context has none (a unit test,
|
|
147
|
+
* the runLoader / renderHandler primitives): its key has no router part,
|
|
148
|
+
* which no router's key equals (routerKeyPrefix).
|
|
149
|
+
*/
|
|
150
|
+
export function requestKeyBase(
|
|
151
|
+
ctx: { readonly _routerId?: string } | null | undefined,
|
|
152
|
+
host: string,
|
|
153
|
+
pathname: string,
|
|
154
|
+
searchParams?: URLSearchParams,
|
|
155
|
+
params?: Record<string, string>,
|
|
156
|
+
filter?: SearchParamsFilter,
|
|
157
|
+
): string {
|
|
158
|
+
const routerId = ctx?._routerId;
|
|
159
|
+
return routerId === undefined
|
|
160
|
+
? hostPathKey(host, pathname, searchParams, params, filter)
|
|
161
|
+
: cacheKeyBase(routerId, host, pathname, searchParams, params, filter);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The `"use cache"` key part of a bare Request argument: its URL, with no
|
|
166
|
+
* router part (the router rule, routerKeyPrefix).
|
|
167
|
+
*/
|
|
168
|
+
export function requestArgKey(url: URL, filter?: SearchParamsFilter): string {
|
|
169
|
+
return hostPathKey(
|
|
170
|
+
url.host,
|
|
171
|
+
url.pathname,
|
|
172
|
+
url.searchParams,
|
|
173
|
+
undefined,
|
|
174
|
+
filter,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
99
178
|
/**
|
|
100
179
|
* The prefix of a `key()` result's part in a record key (#975), and of a
|
|
101
180
|
* loader's own `key()` result after its `loader:<id>:` namespace (#1009,
|