@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResolvedSegment } from "./types.js";
|
|
2
|
-
import { debugLog } from "./logging.js";
|
|
2
|
+
import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Merge partial loader data from server with cached loader data.
|
|
@@ -19,9 +19,11 @@ export function mergeSegmentLoaders(
|
|
|
19
19
|
const serverLoaderIds = fromServer.loaderIds || [];
|
|
20
20
|
const cachedLoaderIds = fromCache.loaderIds || [];
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
if (IS_BROWSER_DEBUG) {
|
|
23
|
+
debugLog(
|
|
24
|
+
`[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
25
27
|
|
|
26
28
|
return {
|
|
27
29
|
...fromCache,
|
|
@@ -32,6 +32,7 @@ import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
|
32
32
|
import {
|
|
33
33
|
toNetworkError,
|
|
34
34
|
emitNetworkError,
|
|
35
|
+
emitNavigationError,
|
|
35
36
|
isBackgroundSuppressible,
|
|
36
37
|
} from "./network-error-handler.js";
|
|
37
38
|
import { debugLog } from "./logging.js";
|
|
@@ -215,17 +216,20 @@ export function createNavigationBridge(
|
|
|
215
216
|
store.setInterceptSourceUrl(null);
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
// Before navigating away, update the source page's cache with the latest
|
|
219
|
-
// This ensures the cache has correct handleData even if handles
|
|
219
|
+
// Before navigating away, update the source page's cache with the latest
|
|
220
|
+
// handleData. This ensures the cache has correct handleData even if handles
|
|
221
|
+
// were streaming. Use updateCacheHandleData (not cacheSegmentsForHistory):
|
|
222
|
+
// the source page's segments are unchanged, so this is a handleData refresh,
|
|
223
|
+
// not a commit. Critically it PRESERVES the entry's stale flag — when the
|
|
224
|
+
// source page has a deferred Meta still pending, its entry was marked stale
|
|
225
|
+
// (invalidate-on-pending) so a popstate return revalidates; re-committing it
|
|
226
|
+
// here would reset stale to false and serve the carried (pre-resolution)
|
|
227
|
+
// title as fresh. It also leaves the nav-instance token intact.
|
|
220
228
|
const sourceHistoryKey = store.getHistoryKey();
|
|
221
229
|
const sourceCached = store.getCachedSegments(sourceHistoryKey);
|
|
222
230
|
if (sourceCached?.segments && sourceCached.segments.length > 0) {
|
|
223
231
|
const currentHandleData = eventController.getHandleState().data;
|
|
224
|
-
store.
|
|
225
|
-
sourceHistoryKey,
|
|
226
|
-
sourceCached.segments,
|
|
227
|
-
currentHandleData,
|
|
228
|
-
);
|
|
232
|
+
store.updateCacheHandleData(sourceHistoryKey, currentHandleData);
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
// Check if we have cached segments for target URL
|
|
@@ -325,8 +329,15 @@ export function createNavigationBridge(
|
|
|
325
329
|
} as NavigateOptionsInternal);
|
|
326
330
|
}
|
|
327
331
|
|
|
328
|
-
|
|
329
|
-
|
|
332
|
+
// Aborted, or superseded by a newer navigation. A superseded nav may
|
|
333
|
+
// reject with a non-AbortError (e.g. a Flight decode that fails after its
|
|
334
|
+
// signal was aborted), so check the signal too -- otherwise we would
|
|
335
|
+
// render a boundary that clobbers the newer navigation's content.
|
|
336
|
+
if (
|
|
337
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
338
|
+
tx.handle.signal.aborted
|
|
339
|
+
) {
|
|
340
|
+
debugLog("[Browser] Navigation aborted or superseded");
|
|
330
341
|
return;
|
|
331
342
|
}
|
|
332
343
|
|
|
@@ -343,7 +354,13 @@ export function createNavigationBridge(
|
|
|
343
354
|
return;
|
|
344
355
|
}
|
|
345
356
|
|
|
346
|
-
|
|
357
|
+
// A response we could not process (undecodable Flight body, or an
|
|
358
|
+
// unanticipated failure building the response). Surface the route's
|
|
359
|
+
// error boundary rather than let the rejection abort the navigation
|
|
360
|
+
// silently. Prefetched responses funnel here too: a failed warm-prefetch
|
|
361
|
+
// payload rejects on consumption and propagates to this catch.
|
|
362
|
+
console.error("[Browser] Unprocessable navigation response:", error);
|
|
363
|
+
emitNavigationError(onUpdate, error, url);
|
|
347
364
|
} finally {
|
|
348
365
|
tx[Symbol.dispose]();
|
|
349
366
|
}
|
|
@@ -372,6 +389,14 @@ export function createNavigationBridge(
|
|
|
372
389
|
tx.with({ url: window.location.href, replace: true, scroll: false }),
|
|
373
390
|
);
|
|
374
391
|
} catch (error) {
|
|
392
|
+
// Aborted or superseded: bail without rendering a boundary (see navigate()).
|
|
393
|
+
if (
|
|
394
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
395
|
+
tx.handle.signal.aborted
|
|
396
|
+
) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
375
400
|
const networkError = toNetworkError(error, {
|
|
376
401
|
url: window.location.href,
|
|
377
402
|
operation: "revalidation",
|
|
@@ -384,7 +409,12 @@ export function createNavigationBridge(
|
|
|
384
409
|
emitNetworkError(onUpdate, networkError, window.location.href);
|
|
385
410
|
return;
|
|
386
411
|
}
|
|
387
|
-
|
|
412
|
+
|
|
413
|
+
// refresh() shares the fetchPartialUpdate chokepoint with navigate()/
|
|
414
|
+
// popstate, so an unprocessable response must surface the error boundary
|
|
415
|
+
// here too rather than become an uncaught rejection.
|
|
416
|
+
console.error("[Browser] Unprocessable refresh response:", error);
|
|
417
|
+
emitNavigationError(onUpdate, error, window.location.href);
|
|
388
418
|
} finally {
|
|
389
419
|
tx[Symbol.dispose]();
|
|
390
420
|
}
|
|
@@ -527,8 +557,19 @@ export function createNavigationBridge(
|
|
|
527
557
|
// SWR: If stale, trigger background revalidation
|
|
528
558
|
if (isStale) {
|
|
529
559
|
debugLog("[Browser] Cache is stale, background revalidating...");
|
|
530
|
-
// Background revalidation - don't await, just fire and forget
|
|
531
|
-
|
|
560
|
+
// Background revalidation - don't await, just fire and forget.
|
|
561
|
+
// When the entry's handles are incomplete (a deferred Meta was still
|
|
562
|
+
// pending when the user navigated away — see handlesPending), send NO
|
|
563
|
+
// segment IDs so the server returns a FULL re-render with the handle
|
|
564
|
+
// stream. A normal stale revalidation sends the cached IDs and the
|
|
565
|
+
// server returns a diff-only payload that omits unchanged segments'
|
|
566
|
+
// handles, so a deferred Meta would never re-stream and the title
|
|
567
|
+
// would stay the pre-resolution carry. handlesPending is set only for
|
|
568
|
+
// the deferred-Meta-aborted case, so action/cross-tab SWR keeps the
|
|
569
|
+
// cheap diff path.
|
|
570
|
+
const segmentIds = cached?.handlesPending
|
|
571
|
+
? []
|
|
572
|
+
: cachedSegments.map((s) => s.id);
|
|
532
573
|
|
|
533
574
|
const tx = createNavigationTransaction(
|
|
534
575
|
store,
|
|
@@ -602,8 +643,13 @@ export function createNavigationBridge(
|
|
|
602
643
|
// Restore scroll position after fetch completes
|
|
603
644
|
handleNavigationEnd({ restore: true, isStreaming });
|
|
604
645
|
} catch (error) {
|
|
605
|
-
|
|
606
|
-
|
|
646
|
+
// Aborted or superseded by a newer navigation: bail without clobbering
|
|
647
|
+
// content with a boundary (see navigate()).
|
|
648
|
+
if (
|
|
649
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
650
|
+
tx.handle.signal.aborted
|
|
651
|
+
) {
|
|
652
|
+
debugLog("[Browser] Popstate navigation aborted or superseded");
|
|
607
653
|
return;
|
|
608
654
|
}
|
|
609
655
|
|
|
@@ -620,7 +666,10 @@ export function createNavigationBridge(
|
|
|
620
666
|
return;
|
|
621
667
|
}
|
|
622
668
|
|
|
623
|
-
|
|
669
|
+
// Unprocessable response on a back/forward navigation: surface the
|
|
670
|
+
// error boundary instead of an uncaught rejection (see navigate()).
|
|
671
|
+
console.error("[Browser] Unprocessable popstate response:", error);
|
|
672
|
+
emitNavigationError(onUpdate, error, url);
|
|
624
673
|
} finally {
|
|
625
674
|
tx[Symbol.dispose]();
|
|
626
675
|
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { NetworkError, ServerRedirect, isNetworkError } from "../errors.js";
|
|
9
9
|
import {
|
|
10
10
|
browserDebugLog,
|
|
11
|
+
debugLog,
|
|
11
12
|
isBrowserDebugEnabled,
|
|
12
13
|
startBrowserTransaction,
|
|
13
14
|
} from "./logging.js";
|
|
@@ -222,7 +223,11 @@ export function createNavigationClient(
|
|
|
222
223
|
...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
|
|
223
224
|
headers: {
|
|
224
225
|
"X-RSC-Router-Client-Path": previousUrl,
|
|
225
|
-
|
|
226
|
+
// Reuse the single per-operation read (see rangoState above): the
|
|
227
|
+
// cache-key lookup and this header must agree on one value, and the
|
|
228
|
+
// cookie read has side effects (external-rotation notify) we do not
|
|
229
|
+
// want to fire twice per navigation.
|
|
230
|
+
"X-Rango-State": rangoState,
|
|
226
231
|
...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
|
|
227
232
|
...(interceptSourceUrl && {
|
|
228
233
|
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
@@ -260,6 +265,10 @@ export function createNavigationClient(
|
|
|
260
265
|
|
|
261
266
|
let payloadPromise: Promise<RscPayload>;
|
|
262
267
|
let streamCompletePromise: Promise<void>;
|
|
268
|
+
// True only for a prefetch-cache hit whose stream had already fully drained
|
|
269
|
+
// (complete === true). A still-streaming hit and the fresh path stay false,
|
|
270
|
+
// so only a fully-warmed prefetch commits in a transition (no fallback flash).
|
|
271
|
+
let fullyPrefetched = false;
|
|
263
272
|
|
|
264
273
|
if (cachedEntry) {
|
|
265
274
|
if (tx) {
|
|
@@ -270,6 +279,9 @@ export function createNavigationClient(
|
|
|
270
279
|
}
|
|
271
280
|
payloadPromise = cachedEntry.payload;
|
|
272
281
|
streamCompletePromise = cachedEntry.streamComplete;
|
|
282
|
+
// Only a hit whose stream already fully drained is "fully prefetched";
|
|
283
|
+
// a still-streaming hit must keep streaming its fallbacks like a cold load.
|
|
284
|
+
fullyPrefetched = cachedEntry.complete;
|
|
273
285
|
} else if (inflightEntryPromise) {
|
|
274
286
|
if (tx) {
|
|
275
287
|
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
@@ -299,6 +311,9 @@ export function createNavigationClient(
|
|
|
299
311
|
} else {
|
|
300
312
|
payloadPromise = entry.payload;
|
|
301
313
|
streamCompletePromise = entry.streamComplete;
|
|
314
|
+
// Adopted inflight is normally still streaming (false), but read the
|
|
315
|
+
// flag in case it completed between publish and adoption.
|
|
316
|
+
fullyPrefetched = entry.complete;
|
|
302
317
|
}
|
|
303
318
|
} else {
|
|
304
319
|
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
@@ -306,7 +321,18 @@ export function createNavigationClient(
|
|
|
306
321
|
}
|
|
307
322
|
|
|
308
323
|
try {
|
|
324
|
+
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. Times how long the RSC
|
|
325
|
+
// payload ROOT takes to resolve: ~full-stream duration means the root
|
|
326
|
+
// model is not flushed early (server/runtime buffering, e.g. wrangler
|
|
327
|
+
// dev gzip); fast means the block, if any, is downstream in render.
|
|
328
|
+
const vtDebugStart = isBrowserDebugEnabled() ? performance.now() : 0;
|
|
309
329
|
const payload = await payloadPromise;
|
|
330
|
+
if (isBrowserDebugEnabled()) {
|
|
331
|
+
debugLog("[VT-DIAG] payloadResolved", {
|
|
332
|
+
ms: Math.round(performance.now() - vtDebugStart),
|
|
333
|
+
isPartial: payload.metadata?.isPartial,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
310
336
|
|
|
311
337
|
if (tx) {
|
|
312
338
|
browserDebugLog(tx, "response received", {
|
|
@@ -315,7 +341,11 @@ export function createNavigationClient(
|
|
|
315
341
|
diffCount: payload.metadata?.diff?.length ?? 0,
|
|
316
342
|
});
|
|
317
343
|
}
|
|
318
|
-
return {
|
|
344
|
+
return {
|
|
345
|
+
payload,
|
|
346
|
+
streamComplete: streamCompletePromise,
|
|
347
|
+
fullyPrefetched,
|
|
348
|
+
};
|
|
319
349
|
} catch (error) {
|
|
320
350
|
// Convert network-level errors to NetworkError for proper handling
|
|
321
351
|
if (isNetworkError(error)) {
|
|
@@ -28,29 +28,49 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
28
28
|
// Maximum number of history entries to cache (URLs visited)
|
|
29
29
|
const HISTORY_CACHE_SIZE = 20;
|
|
30
30
|
|
|
31
|
-
// Cache entry:
|
|
32
|
-
// stale
|
|
31
|
+
// Cache entry:
|
|
32
|
+
// [url-key, segments, stale, handleData?, routerId?, navInstance?, handlesPending?]
|
|
33
|
+
// stale=true means the data may be outdated and should be revalidated on access.
|
|
34
|
+
// navInstance is the monotonic nav-instance token (see navInstance below): it
|
|
35
|
+
// identifies the per-commit visit that owns this entry. generateHistoryKey is
|
|
36
|
+
// URL-only, so A->B->A reuses the same key; the token lets a late async
|
|
37
|
+
// resolution tell its own visit's entry apart from a newer same-URL visit's, so
|
|
38
|
+
// a stale nav can never clobber a fresher one.
|
|
39
|
+
// handlesPending=true means the entry's handle data is INCOMPLETE (a deferred
|
|
40
|
+
// Meta was still pending when the user navigated away, so it never streamed). A
|
|
41
|
+
// popstate return must REVALIDATE WITH A FULL RE-RENDER (no client segment IDs)
|
|
42
|
+
// to re-stream the handles — a diff-only revalidation omits unchanged segments'
|
|
43
|
+
// handles, so the deferred Meta would never land. Cleared once the deferred Meta
|
|
44
|
+
// resolves while the entry is still owned.
|
|
33
45
|
type HistoryCacheEntry = [
|
|
34
46
|
string,
|
|
35
47
|
ResolvedSegment[],
|
|
36
48
|
boolean,
|
|
37
49
|
HandleData?,
|
|
38
50
|
string?,
|
|
51
|
+
number?,
|
|
52
|
+
boolean?,
|
|
39
53
|
];
|
|
40
54
|
|
|
41
55
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
56
|
+
* Clone the handleData CONTAINERS (the handle-name map and each segment map) so
|
|
57
|
+
* a cache entry is decoupled from the live map that eventController mutates — it
|
|
58
|
+
* adds/deletes segment keys and REPLACES bucket arrays in place. The bucket
|
|
59
|
+
* arrays themselves are shared by reference, NOT copied: a bucket array is only
|
|
60
|
+
* ever replaced wholesale (eventController.setHandleData reassigns it,
|
|
61
|
+
* resolveDeferredHandleValues builds a fresh one) and collect functions read it
|
|
62
|
+
* without mutating, so sharing is safe and skips an O(elements) copy on every
|
|
63
|
+
* cache write — the per-yield streaming hot path. This also preserves any
|
|
64
|
+
* non-serializable bucket contents (React elements, functions, etc.).
|
|
46
65
|
*/
|
|
47
|
-
function cloneHandleData(handleData: HandleData): HandleData {
|
|
66
|
+
export function cloneHandleData(handleData: HandleData): HandleData {
|
|
48
67
|
const cloned: HandleData = {};
|
|
49
68
|
for (const [handleKey, segmentMap] of Object.entries(handleData)) {
|
|
50
|
-
|
|
69
|
+
const clonedMap: Record<string, unknown[]> = {};
|
|
51
70
|
for (const [segmentId, dataArray] of Object.entries(segmentMap)) {
|
|
52
|
-
|
|
71
|
+
clonedMap[segmentId] = dataArray;
|
|
53
72
|
}
|
|
73
|
+
cloned[handleKey] = clonedMap;
|
|
54
74
|
}
|
|
55
75
|
return cloned;
|
|
56
76
|
}
|
|
@@ -239,6 +259,14 @@ export function createNavigationStore(
|
|
|
239
259
|
// Oldest entries (at front) are removed when over cacheSize limit
|
|
240
260
|
const historyCache: HistoryCacheEntry[] = [];
|
|
241
261
|
|
|
262
|
+
// Monotonic nav-instance token. Bumped each time a cache entry is created or
|
|
263
|
+
// replaced in cacheSegmentsForHistory (i.e. once per commit). Because
|
|
264
|
+
// generateHistoryKey is URL-only, two visits to the same URL share a key; this
|
|
265
|
+
// token gives each visit a distinct identity so a late async handle resolution
|
|
266
|
+
// can tell whether it still owns the live page / the target cache entry, and
|
|
267
|
+
// never overwrite a newer same-URL visit's state.
|
|
268
|
+
let navInstance = 0;
|
|
269
|
+
|
|
242
270
|
// Current history key (set on navigation, stored in history.state)
|
|
243
271
|
let currentHistoryKey = config?.initialHistoryKey || generateHistoryKey();
|
|
244
272
|
|
|
@@ -248,6 +276,10 @@ export function createNavigationStore(
|
|
|
248
276
|
config.initialHistoryKey,
|
|
249
277
|
config.initialSegments,
|
|
250
278
|
false,
|
|
279
|
+
undefined,
|
|
280
|
+
undefined,
|
|
281
|
+
++navInstance,
|
|
282
|
+
false,
|
|
251
283
|
]);
|
|
252
284
|
}
|
|
253
285
|
|
|
@@ -565,6 +597,29 @@ export function createNavigationStore(
|
|
|
565
597
|
currentHistoryKey = key;
|
|
566
598
|
},
|
|
567
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Current nav-instance token: the instance of the most recently committed
|
|
602
|
+
* navigation (the value last written by cacheSegmentsForHistory). A late
|
|
603
|
+
* async handle resolution captures this at the start of its own nav and
|
|
604
|
+
* compares it back here to detect whether a NEWER navigation has since
|
|
605
|
+
* committed (token advanced), guarding against a stale nav writing a fresher
|
|
606
|
+
* nav's live state.
|
|
607
|
+
*/
|
|
608
|
+
getNavInstance(): number {
|
|
609
|
+
return navInstance;
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* The nav-instance token recorded on a specific cache entry, or undefined if
|
|
614
|
+
* no entry exists for that key. Because the history key is URL-only, this is
|
|
615
|
+
* how a late resolution tells "the entry I seeded is still mine" from "a
|
|
616
|
+
* newer same-URL visit replaced my entry".
|
|
617
|
+
*/
|
|
618
|
+
getCacheEntryInstance(historyKey: string): number | undefined {
|
|
619
|
+
const entry = historyCache.find(([key]) => key === historyKey);
|
|
620
|
+
return entry ? entry[5] : undefined;
|
|
621
|
+
},
|
|
622
|
+
|
|
568
623
|
/**
|
|
569
624
|
* Store segments for a history entry
|
|
570
625
|
* Updates existing entry if key exists, otherwise adds new entry
|
|
@@ -583,6 +638,11 @@ export function createNavigationStore(
|
|
|
583
638
|
? cloneHandleData(handleData)
|
|
584
639
|
: undefined;
|
|
585
640
|
|
|
641
|
+
// Each commit (create or replace) is a new nav instance. The bump happens
|
|
642
|
+
// here, exactly once per cacheSegmentsForHistory call, so getNavInstance()
|
|
643
|
+
// reflects the visit whose entry this is.
|
|
644
|
+
const instance = ++navInstance;
|
|
645
|
+
|
|
586
646
|
// Check if entry already exists and update it
|
|
587
647
|
const existingIndex = historyCache.findIndex(
|
|
588
648
|
([key]) => key === historyKey,
|
|
@@ -594,6 +654,8 @@ export function createNavigationStore(
|
|
|
594
654
|
false,
|
|
595
655
|
clonedHandleData,
|
|
596
656
|
currentRouterId,
|
|
657
|
+
instance,
|
|
658
|
+
false, // fresh commit: handles complete unless a deferred apply marks it
|
|
597
659
|
];
|
|
598
660
|
} else {
|
|
599
661
|
// Add new entry at the end (not stale)
|
|
@@ -603,6 +665,8 @@ export function createNavigationStore(
|
|
|
603
665
|
false,
|
|
604
666
|
clonedHandleData,
|
|
605
667
|
currentRouterId,
|
|
668
|
+
instance,
|
|
669
|
+
false,
|
|
606
670
|
]);
|
|
607
671
|
// Remove oldest entries if over limit
|
|
608
672
|
while (historyCache.length > cacheSize) {
|
|
@@ -621,6 +685,7 @@ export function createNavigationStore(
|
|
|
621
685
|
stale: boolean;
|
|
622
686
|
handleData?: HandleData;
|
|
623
687
|
routerId?: string;
|
|
688
|
+
handlesPending?: boolean;
|
|
624
689
|
}
|
|
625
690
|
| undefined {
|
|
626
691
|
const entry = historyCache.find(([key]) => key === historyKey);
|
|
@@ -630,6 +695,7 @@ export function createNavigationStore(
|
|
|
630
695
|
stale: entry[2],
|
|
631
696
|
handleData: entry[3],
|
|
632
697
|
routerId: entry[4],
|
|
698
|
+
handlesPending: entry[6],
|
|
633
699
|
};
|
|
634
700
|
},
|
|
635
701
|
|
|
@@ -641,11 +707,23 @@ export function createNavigationStore(
|
|
|
641
707
|
},
|
|
642
708
|
|
|
643
709
|
/**
|
|
644
|
-
* Update only the handleData for an existing
|
|
645
|
-
* Does nothing if the cache entry doesn't exist
|
|
646
|
-
*
|
|
710
|
+
* Update only the handleData (and optionally the stale flag) for an existing
|
|
711
|
+
* cache entry. Does nothing if the cache entry doesn't exist.
|
|
712
|
+
*
|
|
713
|
+
* Used to fix stale handleData after async handles processing AND to flip an
|
|
714
|
+
* entry's stale / handlesPending bits for the deferred-Meta
|
|
715
|
+
* invalidate+revalidate path: while a nav's Meta is deferred-pending its
|
|
716
|
+
* entry is marked stale + handlesPending (a popstate return then revalidates
|
|
717
|
+
* with a full re-render instead of serving the carry/seed as fresh), and once
|
|
718
|
+
* the deferred Meta resolves both are cleared. When a flag is omitted the
|
|
719
|
+
* entry's current value is preserved.
|
|
647
720
|
*/
|
|
648
|
-
updateCacheHandleData(
|
|
721
|
+
updateCacheHandleData(
|
|
722
|
+
historyKey: string,
|
|
723
|
+
handleData: HandleData,
|
|
724
|
+
stale?: boolean,
|
|
725
|
+
handlesPending?: boolean,
|
|
726
|
+
): void {
|
|
649
727
|
const existingIndex = historyCache.findIndex(
|
|
650
728
|
([key]) => key === historyKey,
|
|
651
729
|
);
|
|
@@ -656,13 +734,49 @@ export function createNavigationStore(
|
|
|
656
734
|
historyCache[existingIndex] = [
|
|
657
735
|
entry[0],
|
|
658
736
|
entry[1],
|
|
659
|
-
entry[2],
|
|
737
|
+
stale ?? entry[2], // set stale when provided, else preserve current
|
|
660
738
|
clonedHandleData,
|
|
661
739
|
entry[4], // preserve routerId
|
|
740
|
+
entry[5], // preserve navInstance (entry ownership identity)
|
|
741
|
+
handlesPending ?? entry[6], // set when provided, else preserve current
|
|
662
742
|
];
|
|
663
743
|
}
|
|
664
744
|
},
|
|
665
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Owner-guarded handle-data write: locate the entry, and write ONLY when it
|
|
748
|
+
* is still owned by `ownerInstance` (the nav-instance token that seeded it).
|
|
749
|
+
* Folds the streaming hot path's separate getCacheEntryInstance() ownership
|
|
750
|
+
* probe and updateCacheHandleData() write into a SINGLE historyCache scan
|
|
751
|
+
* (processHandles calls this per yield). Semantics otherwise match
|
|
752
|
+
* updateCacheHandleData: no-op on a missing entry, clone the handleData
|
|
753
|
+
* containers, and preserve stale / handlesPending when the flag is omitted.
|
|
754
|
+
*/
|
|
755
|
+
updateCacheHandleDataIfOwned(
|
|
756
|
+
historyKey: string,
|
|
757
|
+
handleData: HandleData,
|
|
758
|
+
ownerInstance: number,
|
|
759
|
+
stale?: boolean,
|
|
760
|
+
handlesPending?: boolean,
|
|
761
|
+
): void {
|
|
762
|
+
const existingIndex = historyCache.findIndex(
|
|
763
|
+
([key]) => key === historyKey,
|
|
764
|
+
);
|
|
765
|
+
if (existingIndex === -1) return;
|
|
766
|
+
const entry = historyCache[existingIndex];
|
|
767
|
+
if (entry[5] !== ownerInstance) return;
|
|
768
|
+
const clonedHandleData = cloneHandleData(handleData);
|
|
769
|
+
historyCache[existingIndex] = [
|
|
770
|
+
entry[0],
|
|
771
|
+
entry[1],
|
|
772
|
+
stale ?? entry[2],
|
|
773
|
+
clonedHandleData,
|
|
774
|
+
entry[4],
|
|
775
|
+
entry[5],
|
|
776
|
+
handlesPending ?? entry[6],
|
|
777
|
+
];
|
|
778
|
+
},
|
|
779
|
+
|
|
666
780
|
/**
|
|
667
781
|
* Mark all cache entries as stale
|
|
668
782
|
* Called after server actions to indicate data may be outdated
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NetworkError, isNetworkError } from "../errors.js";
|
|
2
|
-
import {
|
|
2
|
+
import { RenderErrorThrower } from "../render-error-thrower.js";
|
|
3
3
|
import type { UpdateSubscriber } from "./types.js";
|
|
4
4
|
import { createElement, startTransition } from "react";
|
|
5
5
|
|
|
@@ -24,18 +24,18 @@ export function toNetworkError(
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Render an error into the segment tree via the onUpdate subscriber so the
|
|
28
|
+
* nearest error boundary catches it. Wrapped in startTransition; RenderErrorThrower
|
|
29
|
+
* throws during render (async rejections do not reach boundaries on their own).
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
function emitErrorToBoundary(
|
|
32
32
|
onUpdate: UpdateSubscriber,
|
|
33
|
-
error:
|
|
33
|
+
error: unknown,
|
|
34
34
|
pathname: string,
|
|
35
35
|
): void {
|
|
36
36
|
startTransition(() => {
|
|
37
37
|
onUpdate({
|
|
38
|
-
root: createElement(
|
|
38
|
+
root: createElement(RenderErrorThrower, { error }),
|
|
39
39
|
metadata: {
|
|
40
40
|
pathname,
|
|
41
41
|
segments: [],
|
|
@@ -45,6 +45,33 @@ export function emitNetworkError(
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Emit a NetworkError to the nearest error boundary (offline, failed fetch).
|
|
50
|
+
*/
|
|
51
|
+
export function emitNetworkError(
|
|
52
|
+
onUpdate: UpdateSubscriber,
|
|
53
|
+
error: NetworkError,
|
|
54
|
+
pathname: string,
|
|
55
|
+
): void {
|
|
56
|
+
emitErrorToBoundary(onUpdate, error, pathname);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Emit a navigation processing error to the nearest error boundary. Used when a
|
|
61
|
+
* navigation response cannot be processed (an undecodable Flight body, or any
|
|
62
|
+
* unanticipated failure while building the response) -- for both fresh and
|
|
63
|
+
* prefetched responses, since both funnel through the navigation catch. Without
|
|
64
|
+
* this, such a failure becomes an uncaught rejection that silently aborts the
|
|
65
|
+
* navigation instead of surfacing the route's error boundary.
|
|
66
|
+
*/
|
|
67
|
+
export function emitNavigationError(
|
|
68
|
+
onUpdate: UpdateSubscriber,
|
|
69
|
+
error: unknown,
|
|
70
|
+
pathname: string,
|
|
71
|
+
): void {
|
|
72
|
+
emitErrorToBoundary(onUpdate, error, pathname);
|
|
73
|
+
}
|
|
74
|
+
|
|
48
75
|
/**
|
|
49
76
|
* Check if an error is safe to suppress in background operations.
|
|
50
77
|
*
|