@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
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 +8 -0
- package/README.md +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +117 -23
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1989 -378
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +96 -12
- package/src/index.ts +94 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +96 -68
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +109 -118
- package/src/vite/router-discovery.ts +718 -119
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -14,20 +14,35 @@ const addTransitionType: ((type: string) => void) | undefined =
|
|
|
14
14
|
import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
15
15
|
import { reconcileSegments } from "./segment-reconciler.js";
|
|
16
16
|
import type { ReconcileActor } from "./segment-reconciler.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
hasActiveIntercept as hasActiveInterceptSlots,
|
|
19
|
+
isInterceptSegment,
|
|
20
|
+
} from "./intercept-utils.js";
|
|
18
21
|
import type { BoundTransaction } from "./navigation-transaction.js";
|
|
19
22
|
import { ServerRedirect } from "../errors.js";
|
|
20
23
|
import { debugLog } from "./logging.js";
|
|
21
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
validateRedirectOrigin,
|
|
26
|
+
validateExternalRedirect,
|
|
27
|
+
} from "./validate-redirect-origin.js";
|
|
22
28
|
import type { NavigationUpdate } from "./types.js";
|
|
23
29
|
|
|
24
|
-
/** Build a scroll payload from the commit's scroll option */
|
|
25
30
|
function toScrollPayload(
|
|
26
31
|
scroll: boolean | undefined,
|
|
27
32
|
): NonNullable<NavigationUpdate["scroll"]> {
|
|
28
33
|
return { enabled: scroll !== false ? scroll : false };
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
|
|
37
|
+
let hasIntercept = false;
|
|
38
|
+
let hasTransition = false;
|
|
39
|
+
for (const s of segments) {
|
|
40
|
+
if (isInterceptSegment(s)) hasIntercept = true;
|
|
41
|
+
else if (s.transition) hasTransition = true;
|
|
42
|
+
}
|
|
43
|
+
return !hasIntercept && hasTransition;
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/**
|
|
32
47
|
* Configuration for creating a partial updater
|
|
33
48
|
*/
|
|
@@ -76,7 +91,7 @@ export type UpdateMode =
|
|
|
76
91
|
/** Source URL for intercept restore (popstate cache miss) */
|
|
77
92
|
interceptSourceUrl?: string;
|
|
78
93
|
}
|
|
79
|
-
| { type: "leave-intercept" }
|
|
94
|
+
| { type: "leave-intercept"; interceptSourceUrl?: string }
|
|
80
95
|
| { type: "stale-revalidation"; interceptSourceUrl?: string }
|
|
81
96
|
| { type: "action"; interceptSourceUrl?: string };
|
|
82
97
|
|
|
@@ -92,15 +107,6 @@ export type PartialUpdater = (
|
|
|
92
107
|
mode?: UpdateMode,
|
|
93
108
|
) => Promise<void>;
|
|
94
109
|
|
|
95
|
-
/**
|
|
96
|
-
* Create a partial updater for fetching and applying RSC partial updates
|
|
97
|
-
*
|
|
98
|
-
* This function is shared between navigation-bridge and server-action-bridge
|
|
99
|
-
* to handle partial RSC updates with HMR resilience.
|
|
100
|
-
*
|
|
101
|
-
* @param config - Partial update configuration
|
|
102
|
-
* @returns fetchPartialUpdate function
|
|
103
|
-
*/
|
|
104
110
|
export function createPartialUpdater(
|
|
105
111
|
config: PartialUpdateConfig,
|
|
106
112
|
): PartialUpdater {
|
|
@@ -112,21 +118,12 @@ export function createPartialUpdater(
|
|
|
112
118
|
getVersion = () => undefined,
|
|
113
119
|
} = config;
|
|
114
120
|
|
|
115
|
-
/**
|
|
116
|
-
* Get current page's cached segments as an array
|
|
117
|
-
*/
|
|
118
121
|
function getCurrentCachedSegments(): ResolvedSegment[] {
|
|
119
122
|
const currentKey = store.getHistoryKey();
|
|
120
123
|
const cached = store.getCachedSegments(currentKey);
|
|
121
124
|
return cached?.segments || [];
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
/**
|
|
125
|
-
* Fetch partial update and trigger UI update
|
|
126
|
-
*
|
|
127
|
-
* @param tx - Transaction for committing segment state (required)
|
|
128
|
-
* @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
|
|
129
|
-
*/
|
|
130
127
|
async function fetchPartialUpdate(
|
|
131
128
|
targetUrl: string,
|
|
132
129
|
segmentIds: string[] | undefined,
|
|
@@ -138,26 +135,16 @@ export function createPartialUpdater(
|
|
|
138
135
|
const segmentState = store.getSegmentState();
|
|
139
136
|
const url = targetUrl || window.location.href;
|
|
140
137
|
|
|
141
|
-
// Capture history key at start for stale revalidation consistency check
|
|
142
138
|
const historyKeyAtStart = store.getHistoryKey();
|
|
143
139
|
|
|
144
|
-
|
|
145
|
-
const interceptSourceUrl =
|
|
146
|
-
mode.type === "stale-revalidation" ||
|
|
147
|
-
mode.type === "action" ||
|
|
148
|
-
mode.type === "navigate"
|
|
149
|
-
? mode.interceptSourceUrl
|
|
150
|
-
: undefined;
|
|
140
|
+
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
151
141
|
|
|
152
|
-
// When leaving intercept, filter out intercept-specific segments
|
|
153
142
|
let segments: string[];
|
|
154
143
|
if (mode.type === "leave-intercept") {
|
|
155
144
|
const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
|
|
156
145
|
const currentCached = getCurrentCachedSegments();
|
|
157
146
|
const interceptIds = new Set(
|
|
158
|
-
currentCached
|
|
159
|
-
.filter((s) => s.namespace?.startsWith("intercept:"))
|
|
160
|
-
.map((s) => s.id),
|
|
147
|
+
currentCached.filter(isInterceptSegment).map((s) => s.id),
|
|
161
148
|
);
|
|
162
149
|
segments = currentSegments.filter((id) => !interceptIds.has(id));
|
|
163
150
|
debugLog(
|
|
@@ -167,12 +154,6 @@ export function createPartialUpdater(
|
|
|
167
154
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
168
155
|
}
|
|
169
156
|
|
|
170
|
-
// For intercept revalidation, use the intercept source URL as previousUrl.
|
|
171
|
-
// For leave-intercept, tx.currentUrl captures window.location.href at tx
|
|
172
|
-
// creation, which on popstate is already the destination URL and would
|
|
173
|
-
// tell the server "from == to". segmentState.currentUrl still points at
|
|
174
|
-
// the URL the cached segments render (the intercept URL), which is the
|
|
175
|
-
// correct "from" for the server's diff computation.
|
|
176
157
|
const previousUrl =
|
|
177
158
|
mode.type === "leave-intercept"
|
|
178
159
|
? segmentState.currentUrl || tx.currentUrl
|
|
@@ -186,66 +167,76 @@ export function createPartialUpdater(
|
|
|
186
167
|
debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
|
|
187
168
|
}
|
|
188
169
|
|
|
189
|
-
// Get cached segments for merging with server diff.
|
|
190
|
-
// When navigating with targetCacheSegments, use those for consistency.
|
|
191
|
-
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
192
170
|
const targetCache =
|
|
193
|
-
mode.type === "navigate"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const cachedSegsSource =
|
|
199
|
-
targetCache && targetCache.length > 0 ? "history-cache" : "current-page";
|
|
171
|
+
mode.type === "navigate" && mode.targetCacheSegments?.length
|
|
172
|
+
? mode.targetCacheSegments
|
|
173
|
+
: undefined;
|
|
174
|
+
const cachedSegs = targetCache ?? getCurrentCachedSegments();
|
|
175
|
+
const cachedSegsSource = targetCache ? "history-cache" : "current-page";
|
|
200
176
|
debugLog(
|
|
201
177
|
`[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
|
|
202
178
|
);
|
|
203
179
|
|
|
204
|
-
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
205
180
|
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
206
181
|
fetchResult = await client.fetchPartial({
|
|
207
182
|
targetUrl: url,
|
|
208
183
|
segmentIds: segments,
|
|
209
184
|
previousUrl,
|
|
210
|
-
// Mark stale when explicitly requested OR when no segments are sent
|
|
211
|
-
// (action redirect sends empty segments for a fresh render).
|
|
212
185
|
staleRevalidation:
|
|
213
186
|
mode.type === "stale-revalidation" || segments.length === 0,
|
|
214
187
|
version: getVersion(),
|
|
215
188
|
routerId: store.getRouterId?.(),
|
|
216
189
|
});
|
|
217
|
-
// Mark navigation as streaming (response received, now parsing RSC).
|
|
218
|
-
// Called after fetchPartial so pendingUrl stays set during the network wait,
|
|
219
|
-
// allowing useLinkStatus to show per-link pending indicators.
|
|
220
190
|
const streamingToken = tx.startStreaming();
|
|
221
|
-
const {
|
|
191
|
+
const {
|
|
192
|
+
payload,
|
|
193
|
+
streamComplete: rawStreamComplete,
|
|
194
|
+
fullyPrefetched,
|
|
195
|
+
} = fetchResult;
|
|
222
196
|
debugLog("payload.metadata", payload.metadata);
|
|
223
197
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
198
|
+
// Side effect only: end the streaming token once the stream settles.
|
|
199
|
+
// The wrapped promise was never read as a value; only the .end() matters.
|
|
200
|
+
// The .catch keeps an unhandled rejection from leaking if the stream errors.
|
|
201
|
+
rawStreamComplete.then(() => streamingToken.end()).catch(() => {});
|
|
202
|
+
|
|
203
|
+
const currentRouterId = store.getRouterId?.();
|
|
204
|
+
if (
|
|
205
|
+
payload.metadata?.routerId &&
|
|
206
|
+
currentRouterId &&
|
|
207
|
+
payload.metadata.routerId !== currentRouterId
|
|
208
|
+
) {
|
|
209
|
+
console.error(
|
|
210
|
+
`[rango] Partial response router id "${payload.metadata.routerId}" does not ` +
|
|
211
|
+
`match this client ("${currentRouterId}"); discarding it and reloading to re-sync.`,
|
|
212
|
+
);
|
|
213
|
+
window.location.href = url;
|
|
214
|
+
return;
|
|
241
215
|
}
|
|
242
216
|
|
|
243
|
-
// Handle server-side redirect with state
|
|
244
217
|
if (payload.metadata?.redirect) {
|
|
245
218
|
if (signal?.aborted) {
|
|
246
219
|
debugLog("[Browser] Ignoring stale redirect (aborted)");
|
|
247
220
|
return;
|
|
248
221
|
}
|
|
222
|
+
// Explicit off-host redirect (redirect(url, { external: true })):
|
|
223
|
+
// hard-navigate, but still scheme-validate (http/https only). external
|
|
224
|
+
// waives the same-origin check the app opted out of, NOT scheme safety, so
|
|
225
|
+
// a forged payload carrying a javascript:/data: URL cannot script via
|
|
226
|
+
// location.assign.
|
|
227
|
+
if (payload.metadata.redirect.external) {
|
|
228
|
+
const externalUrl = validateExternalRedirect(
|
|
229
|
+
payload.metadata.redirect.url,
|
|
230
|
+
window.location.origin,
|
|
231
|
+
);
|
|
232
|
+
if (!externalUrl) {
|
|
233
|
+
debugLog("[Browser] Ignoring blocked external redirect payload");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
debugLog("[Browser] External redirect (hard navigation)");
|
|
237
|
+
window.location.assign(externalUrl);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
249
240
|
const redirectUrl = validateRedirectOrigin(
|
|
250
241
|
payload.metadata.redirect.url,
|
|
251
242
|
window.location.origin,
|
|
@@ -270,7 +261,6 @@ export function createPartialUpdater(
|
|
|
270
261
|
debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
|
|
271
262
|
debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
|
|
272
263
|
|
|
273
|
-
// If diff is empty, nothing changed on server side.
|
|
274
264
|
if (!diff || diff.length === 0) {
|
|
275
265
|
const matchedIds = matched || [];
|
|
276
266
|
const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
|
|
@@ -278,8 +268,7 @@ export function createPartialUpdater(
|
|
|
278
268
|
.map((id: string) => cacheMap.get(id))
|
|
279
269
|
.filter(Boolean) as ResolvedSegment[];
|
|
280
270
|
|
|
281
|
-
|
|
282
|
-
if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
|
|
271
|
+
if (mode.type === "navigate" && targetCache) {
|
|
283
272
|
debugLog(
|
|
284
273
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
285
274
|
);
|
|
@@ -293,10 +282,6 @@ export function createPartialUpdater(
|
|
|
293
282
|
existingSegments,
|
|
294
283
|
);
|
|
295
284
|
|
|
296
|
-
// tx.commit() cached the source page's handleData because
|
|
297
|
-
// eventController hasn't been updated yet. Overwrite with the
|
|
298
|
-
// correct cached handleData to prevent cache corruption on
|
|
299
|
-
// subsequent navigations to this same URL.
|
|
300
285
|
if (mode.targetCacheHandleData) {
|
|
301
286
|
store.updateCacheHandleData(
|
|
302
287
|
store.getHistoryKey(),
|
|
@@ -304,10 +289,6 @@ export function createPartialUpdater(
|
|
|
304
289
|
);
|
|
305
290
|
}
|
|
306
291
|
|
|
307
|
-
// Include cachedHandleData in metadata so NavigationProvider can restore
|
|
308
|
-
// breadcrumbs and other handle data from cache.
|
|
309
|
-
// Remove `handles` from metadata to prevent NavigationProvider from
|
|
310
|
-
// processing an empty handles stream, which would clear the cached breadcrumbs.
|
|
311
292
|
const { handles: _unusedHandles, ...metadataWithoutHandles } =
|
|
312
293
|
payload.metadata!;
|
|
313
294
|
const cachedUpdate = {
|
|
@@ -319,10 +300,7 @@ export function createPartialUpdater(
|
|
|
319
300
|
scroll: toScrollPayload(commitScroll),
|
|
320
301
|
};
|
|
321
302
|
|
|
322
|
-
|
|
323
|
-
(s) => s.transition,
|
|
324
|
-
);
|
|
325
|
-
if (cachedHasTransition) {
|
|
303
|
+
if (shouldStartViewTransition(existingSegments)) {
|
|
326
304
|
startTransition(() => {
|
|
327
305
|
if (addTransitionType) {
|
|
328
306
|
addTransitionType("navigation");
|
|
@@ -337,7 +315,6 @@ export function createPartialUpdater(
|
|
|
337
315
|
return;
|
|
338
316
|
}
|
|
339
317
|
|
|
340
|
-
// When leaving intercept, force re-render even with empty diff
|
|
341
318
|
if (mode.type === "leave-intercept") {
|
|
342
319
|
debugLog(
|
|
343
320
|
"[Browser] Leaving intercept - forcing re-render to remove modal",
|
|
@@ -362,7 +339,6 @@ export function createPartialUpdater(
|
|
|
362
339
|
return;
|
|
363
340
|
}
|
|
364
341
|
|
|
365
|
-
// Same route revalidation with no changes - skip UI update
|
|
366
342
|
debugLog(
|
|
367
343
|
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
368
344
|
);
|
|
@@ -371,7 +347,6 @@ export function createPartialUpdater(
|
|
|
371
347
|
return;
|
|
372
348
|
}
|
|
373
349
|
|
|
374
|
-
// Reconcile server segments with cached segments (single source of truth)
|
|
375
350
|
const matchedIds = matched || [];
|
|
376
351
|
const actor: ReconcileActor =
|
|
377
352
|
mode.type === "stale-revalidation" || mode.type === "action"
|
|
@@ -387,7 +362,6 @@ export function createPartialUpdater(
|
|
|
387
362
|
insertMissingDiff: true,
|
|
388
363
|
});
|
|
389
364
|
|
|
390
|
-
// HMR RESILIENCE: Check if we're missing any matched segments
|
|
391
365
|
const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
|
|
392
366
|
const missingIds = matchedIds.filter(
|
|
393
367
|
(id: string) => !reconciledIdSet.has(id),
|
|
@@ -415,7 +389,6 @@ export function createPartialUpdater(
|
|
|
415
389
|
`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
|
|
416
390
|
);
|
|
417
391
|
|
|
418
|
-
// Refetch with empty segments = server sends everything
|
|
419
392
|
return fetchPartialUpdate(url, [], true, signal, tx, mode);
|
|
420
393
|
}
|
|
421
394
|
|
|
@@ -424,44 +397,59 @@ export function createPartialUpdater(
|
|
|
424
397
|
return;
|
|
425
398
|
}
|
|
426
399
|
|
|
427
|
-
// Rebuild tree on client (await for loader data resolution)
|
|
428
400
|
const renderOptions = {
|
|
429
401
|
isAction: mode.type === "action",
|
|
430
|
-
forceAwait
|
|
402
|
+
// forceAwait unwraps the ROUTER loader promises during render so they
|
|
403
|
+
// land without a loading()/fallback frame. A fully-prefetched nav has
|
|
404
|
+
// its router data already resolved (the prefetch stream drained), so
|
|
405
|
+
// awaiting it here is free and lets us commit NORMALLY (not in a
|
|
406
|
+
// transition) below — a normal commit still shows fallbacks for any
|
|
407
|
+
// CLIENT component that suspends on mount, which a transition would
|
|
408
|
+
// wrongly suppress by holding the old UI until that suspense settles.
|
|
409
|
+
forceAwait: mode.type === "stale-revalidation" || fullyPrefetched,
|
|
431
410
|
interceptSegments:
|
|
432
411
|
reconciled.interceptSegments.length > 0
|
|
433
412
|
? reconciled.interceptSegments
|
|
434
413
|
: undefined,
|
|
435
414
|
};
|
|
436
|
-
|
|
437
|
-
|
|
415
|
+
let newTree: Awaited<ReturnType<typeof renderSegments>>;
|
|
416
|
+
if (signal) {
|
|
417
|
+
// Race render against abort. Store the abort handler and register it
|
|
418
|
+
// { once:true } so a non-aborted render (which wins the race) can
|
|
419
|
+
// remove it in finally — otherwise the listener stays attached and the
|
|
420
|
+
// rejecting promise never settles. Mirrors teeWithCompletion in
|
|
421
|
+
// browser/response-adapter.ts.
|
|
422
|
+
let onAbort: (() => void) | undefined;
|
|
423
|
+
const abortPromise = new Promise<never>((_, reject) => {
|
|
424
|
+
if (signal.aborted) {
|
|
425
|
+
reject(new DOMException("Navigation aborted", "AbortError"));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
onAbort = () =>
|
|
429
|
+
reject(new DOMException("Navigation aborted", "AbortError"));
|
|
430
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
431
|
+
});
|
|
432
|
+
try {
|
|
433
|
+
newTree = await Promise.race([
|
|
438
434
|
renderSegments(reconciled.mainSegments, renderOptions),
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
: renderSegments(reconciled.mainSegments, renderOptions));
|
|
449
|
-
|
|
450
|
-
// Final abort check before committing - another navigation may have started
|
|
435
|
+
abortPromise,
|
|
436
|
+
]);
|
|
437
|
+
} finally {
|
|
438
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
newTree = await renderSegments(reconciled.mainSegments, renderOptions);
|
|
442
|
+
}
|
|
443
|
+
|
|
451
444
|
if (signal?.aborted) {
|
|
452
445
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
453
446
|
return;
|
|
454
447
|
}
|
|
455
448
|
|
|
456
|
-
// Check if this is an intercept response (any slot is active)
|
|
457
449
|
const isInterceptResponse = hasActiveInterceptSlots(
|
|
458
450
|
payload.metadata?.slots,
|
|
459
451
|
);
|
|
460
452
|
|
|
461
|
-
// Track intercept context (only on navigation, not actions or stale revalidation)
|
|
462
|
-
// Use the authoritative source from mode/history state when restoring an
|
|
463
|
-
// intercept via popstate cache miss; fall back to the current URL for fresh
|
|
464
|
-
// intercept navigations.
|
|
465
453
|
const effectiveInterceptSource =
|
|
466
454
|
interceptSourceUrl || segmentState.currentUrl;
|
|
467
455
|
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
@@ -472,9 +460,6 @@ export function createPartialUpdater(
|
|
|
472
460
|
}
|
|
473
461
|
}
|
|
474
462
|
|
|
475
|
-
// Commit navigation - use server's matched as the authoritative segment ID list.
|
|
476
|
-
// reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
|
|
477
|
-
// but the server's matched always includes all expected segment IDs.
|
|
478
463
|
const allSegmentIds = matchedIds;
|
|
479
464
|
const serverLocationState = payload.metadata?.locationState;
|
|
480
465
|
const overrides: CommitOverrides | undefined = isInterceptResponse
|
|
@@ -493,7 +478,6 @@ export function createPartialUpdater(
|
|
|
493
478
|
overrides,
|
|
494
479
|
);
|
|
495
480
|
|
|
496
|
-
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
497
481
|
if (mode.type === "stale-revalidation") {
|
|
498
482
|
const historyKeyNow = store.getHistoryKey();
|
|
499
483
|
if (historyKeyNow !== historyKeyAtStart) {
|
|
@@ -506,9 +490,21 @@ export function createPartialUpdater(
|
|
|
506
490
|
|
|
507
491
|
debugLog("[partial-update] updating document");
|
|
508
492
|
|
|
509
|
-
|
|
510
|
-
//
|
|
511
|
-
|
|
493
|
+
const hasTransition = shouldStartViewTransition(reconciled.segments);
|
|
494
|
+
// TEMP DIAGNOSTIC (transition.when hold investigation) - remove before merge.
|
|
495
|
+
// Prints which reconciled segment still carries a transition after the
|
|
496
|
+
// server-side when-gate, and whether the commit will be held in a
|
|
497
|
+
// startTransition. If `withTransition` lists an ancestor (layout/root)
|
|
498
|
+
// id rather than the gated leaf, an ungated ancestor transition is the
|
|
499
|
+
// cause of the missing loading() fallback.
|
|
500
|
+
console.log("[VT-DIAG]", {
|
|
501
|
+
mode: mode.type,
|
|
502
|
+
hasTransition,
|
|
503
|
+
withTransition: reconciled.segments
|
|
504
|
+
.filter((s) => s.transition)
|
|
505
|
+
.map((s) => s.id),
|
|
506
|
+
all: reconciled.segments.map((s) => s.id),
|
|
507
|
+
});
|
|
512
508
|
const scrollPayload = toScrollPayload(navScroll);
|
|
513
509
|
|
|
514
510
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
@@ -534,6 +530,20 @@ export function createPartialUpdater(
|
|
|
534
530
|
});
|
|
535
531
|
});
|
|
536
532
|
} else {
|
|
533
|
+
// Normal commit (cold/partial nav AND fully-prefetched nav). For a
|
|
534
|
+
// fully-prefetched nav, renderOptions.forceAwait (above) unwrapped the
|
|
535
|
+
// already-resolved ROUTER loader data AND route content during render, so
|
|
536
|
+
// the new tree carries it inline with no loading()/fallback frame — yet we
|
|
537
|
+
// still commit NORMALLY here rather than in a transition. A transition
|
|
538
|
+
// holds the OLD UI until ALL suspense in the new tree settles, including a
|
|
539
|
+
// CLIENT component that starts its own data request only when mounted
|
|
540
|
+
// (post-commit) under a persistent boundary; that would retain the
|
|
541
|
+
// previous page indefinitely with no feedback. A normal commit lets such
|
|
542
|
+
// client-initiated suspense reveal a fallback (correct) while the router
|
|
543
|
+
// data — genuinely ready — never flashes. Cold/partial navs
|
|
544
|
+
// (fullyPrefetched=false) do not forceAwait, so they stream their
|
|
545
|
+
// fallbacks. Explicit transition() routes keep the broader content-hold
|
|
546
|
+
// via the hasTransition branch above (the documented opt-in).
|
|
537
547
|
onUpdate({
|
|
538
548
|
root: newTree,
|
|
539
549
|
metadata: payload.metadata!,
|
|
@@ -544,7 +554,6 @@ export function createPartialUpdater(
|
|
|
544
554
|
debugLog("[Browser] Navigation complete");
|
|
545
555
|
return;
|
|
546
556
|
} else {
|
|
547
|
-
// Full update (fallback)
|
|
548
557
|
console.warn(`[Browser] Full update (fallback)`);
|
|
549
558
|
|
|
550
559
|
const segments = payload.metadata?.segments || [];
|
|
@@ -570,13 +579,23 @@ export function createPartialUpdater(
|
|
|
570
579
|
})
|
|
571
580
|
: tx.commit(segmentIds, segments);
|
|
572
581
|
|
|
573
|
-
const fullHasTransition = segments
|
|
574
|
-
(s: ResolvedSegment) => s.transition,
|
|
575
|
-
);
|
|
582
|
+
const fullHasTransition = shouldStartViewTransition(segments);
|
|
576
583
|
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
577
584
|
|
|
578
585
|
if (mode.type === "stale-revalidation") {
|
|
579
586
|
await rawStreamComplete;
|
|
587
|
+
// Mirror the partial branch's history-key staleness guard (above): the
|
|
588
|
+
// await above is a real async suspension, so the user may have navigated
|
|
589
|
+
// away while this background revalidation was draining. Dropping a late
|
|
590
|
+
// full-update here prevents it from clobbering the freshly committed UI
|
|
591
|
+
// of the page the user moved to.
|
|
592
|
+
const historyKeyNow = store.getHistoryKey();
|
|
593
|
+
if (historyKeyNow !== historyKeyAtStart) {
|
|
594
|
+
debugLog(
|
|
595
|
+
`[Browser] Stale revalidation (full update): history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
|
|
596
|
+
);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
580
599
|
startTransition(() => {
|
|
581
600
|
if (fullHasTransition && addTransitionType) {
|
|
582
601
|
addTransitionType("action");
|
|
@@ -588,7 +607,7 @@ export function createPartialUpdater(
|
|
|
588
607
|
});
|
|
589
608
|
});
|
|
590
609
|
} else if (mode.type === "action") {
|
|
591
|
-
startTransition(
|
|
610
|
+
startTransition(() => {
|
|
592
611
|
if (fullHasTransition && addTransitionType) {
|
|
593
612
|
addTransitionType("action");
|
|
594
613
|
}
|