@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad
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 +12 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +441 -134
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3453 -1240
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +76 -21
- 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 +247 -23
- package/skills/caching/SKILL.md +318 -15
- package/skills/composability/SKILL.md +27 -2
- 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 +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +279 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +153 -109
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +101 -5
- package/skills/router-setup/SKILL.md +116 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -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 +121 -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 +332 -29
- package/skills/use-cache/SKILL.md +46 -14
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- 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/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +197 -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 +111 -31
- package/src/browser/navigation-client.ts +201 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +230 -35
- package/src/browser/prefetch/fetch.ts +338 -39
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +111 -16
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- 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 +23 -69
- 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 +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- 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 +80 -5
- package/src/browser/rsc-router.tsx +218 -76
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -61
- package/src/browser/types.ts +91 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- 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 +125 -24
- 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 +272 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- 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 +214 -48
- package/src/cache/cache-scope.ts +236 -89
- 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 +2224 -171
- 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 +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +119 -290
- 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 +84 -2
- package/src/debug.ts +2 -2
- 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 +70 -22
- package/src/handles/MetaTags.tsx +56 -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 +93 -12
- package/src/index.ts +133 -15
- 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 +37 -41
- package/src/prerender.ts +198 -82
- 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 +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +469 -276
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +270 -172
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +264 -77
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +474 -385
- 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 +36 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +8 -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 +163 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +163 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +658 -511
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +77 -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 +99 -189
- package/src/rsc/rsc-rendering.ts +105 -72
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +263 -112
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +32 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +26 -46
- package/src/server/request-context.ts +398 -172
- package/src/ssr/index.tsx +25 -16
- package/src/static-handler.ts +27 -18
- 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 +99 -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 +343 -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/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 +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -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 +214 -132
- 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 +57 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +155 -65
- 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-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- 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 +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +238 -295
- package/src/vite/router-discovery.ts +940 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- 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 +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- 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,11 +14,34 @@ 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";
|
|
28
|
+
import type { NavigationUpdate } from "./types.js";
|
|
29
|
+
|
|
30
|
+
function toScrollPayload(
|
|
31
|
+
scroll: boolean | undefined,
|
|
32
|
+
): NonNullable<NavigationUpdate["scroll"]> {
|
|
33
|
+
return { enabled: scroll !== false ? scroll : false };
|
|
34
|
+
}
|
|
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
|
+
}
|
|
22
45
|
|
|
23
46
|
/**
|
|
24
47
|
* Configuration for creating a partial updater
|
|
@@ -31,8 +54,8 @@ export interface PartialUpdateConfig {
|
|
|
31
54
|
segments: ResolvedSegment[],
|
|
32
55
|
options?: RenderSegmentsOptions,
|
|
33
56
|
) => Promise<ReactNode> | ReactNode;
|
|
34
|
-
/** RSC version
|
|
35
|
-
|
|
57
|
+
/** RSC version getter — returns the current version (may change after HMR) */
|
|
58
|
+
getVersion?: () => string | undefined;
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
/**
|
|
@@ -68,7 +91,7 @@ export type UpdateMode =
|
|
|
68
91
|
/** Source URL for intercept restore (popstate cache miss) */
|
|
69
92
|
interceptSourceUrl?: string;
|
|
70
93
|
}
|
|
71
|
-
| { type: "leave-intercept" }
|
|
94
|
+
| { type: "leave-intercept"; interceptSourceUrl?: string }
|
|
72
95
|
| { type: "stale-revalidation"; interceptSourceUrl?: string }
|
|
73
96
|
| { type: "action"; interceptSourceUrl?: string };
|
|
74
97
|
|
|
@@ -84,35 +107,23 @@ export type PartialUpdater = (
|
|
|
84
107
|
mode?: UpdateMode,
|
|
85
108
|
) => Promise<void>;
|
|
86
109
|
|
|
87
|
-
/**
|
|
88
|
-
* Create a partial updater for fetching and applying RSC partial updates
|
|
89
|
-
*
|
|
90
|
-
* This function is shared between navigation-bridge and server-action-bridge
|
|
91
|
-
* to handle partial RSC updates with HMR resilience.
|
|
92
|
-
*
|
|
93
|
-
* @param config - Partial update configuration
|
|
94
|
-
* @returns fetchPartialUpdate function
|
|
95
|
-
*/
|
|
96
110
|
export function createPartialUpdater(
|
|
97
111
|
config: PartialUpdateConfig,
|
|
98
112
|
): PartialUpdater {
|
|
99
|
-
const {
|
|
113
|
+
const {
|
|
114
|
+
store,
|
|
115
|
+
client,
|
|
116
|
+
onUpdate,
|
|
117
|
+
renderSegments,
|
|
118
|
+
getVersion = () => undefined,
|
|
119
|
+
} = config;
|
|
100
120
|
|
|
101
|
-
/**
|
|
102
|
-
* Get current page's cached segments as an array
|
|
103
|
-
*/
|
|
104
121
|
function getCurrentCachedSegments(): ResolvedSegment[] {
|
|
105
122
|
const currentKey = store.getHistoryKey();
|
|
106
123
|
const cached = store.getCachedSegments(currentKey);
|
|
107
124
|
return cached?.segments || [];
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
/**
|
|
111
|
-
* Fetch partial update and trigger UI update
|
|
112
|
-
*
|
|
113
|
-
* @param tx - Transaction for committing segment state (required)
|
|
114
|
-
* @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
|
|
115
|
-
*/
|
|
116
127
|
async function fetchPartialUpdate(
|
|
117
128
|
targetUrl: string,
|
|
118
129
|
segmentIds: string[] | undefined,
|
|
@@ -124,26 +135,16 @@ export function createPartialUpdater(
|
|
|
124
135
|
const segmentState = store.getSegmentState();
|
|
125
136
|
const url = targetUrl || window.location.href;
|
|
126
137
|
|
|
127
|
-
// Capture history key at start for stale revalidation consistency check
|
|
128
138
|
const historyKeyAtStart = store.getHistoryKey();
|
|
129
139
|
|
|
130
|
-
|
|
131
|
-
const interceptSourceUrl =
|
|
132
|
-
mode.type === "stale-revalidation" ||
|
|
133
|
-
mode.type === "action" ||
|
|
134
|
-
mode.type === "navigate"
|
|
135
|
-
? mode.interceptSourceUrl
|
|
136
|
-
: undefined;
|
|
140
|
+
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
137
141
|
|
|
138
|
-
// When leaving intercept, filter out intercept-specific segments
|
|
139
142
|
let segments: string[];
|
|
140
143
|
if (mode.type === "leave-intercept") {
|
|
141
144
|
const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
|
|
142
145
|
const currentCached = getCurrentCachedSegments();
|
|
143
146
|
const interceptIds = new Set(
|
|
144
|
-
currentCached
|
|
145
|
-
.filter((s) => s.namespace?.startsWith("intercept:"))
|
|
146
|
-
.map((s) => s.id),
|
|
147
|
+
currentCached.filter(isInterceptSegment).map((s) => s.id),
|
|
147
148
|
);
|
|
148
149
|
segments = currentSegments.filter((id) => !interceptIds.has(id));
|
|
149
150
|
debugLog(
|
|
@@ -153,9 +154,10 @@ export function createPartialUpdater(
|
|
|
153
154
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
// For intercept revalidation, use the intercept source URL as previousUrl
|
|
157
157
|
const previousUrl =
|
|
158
|
-
|
|
158
|
+
mode.type === "leave-intercept"
|
|
159
|
+
? segmentState.currentUrl || tx.currentUrl
|
|
160
|
+
: interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
|
|
159
161
|
|
|
160
162
|
debugLog(`\n[Browser] >>> NAVIGATION`);
|
|
161
163
|
debugLog(`[Browser] From: ${previousUrl}`);
|
|
@@ -165,45 +167,76 @@ export function createPartialUpdater(
|
|
|
165
167
|
debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
|
|
166
168
|
}
|
|
167
169
|
|
|
168
|
-
// Get cached segments for merging with server diff.
|
|
169
|
-
// When navigating with targetCacheSegments, use those for consistency.
|
|
170
|
-
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
171
170
|
const targetCache =
|
|
172
|
-
mode.type === "navigate"
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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";
|
|
176
|
+
debugLog(
|
|
177
|
+
`[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
|
|
178
|
+
);
|
|
177
179
|
|
|
178
|
-
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
179
180
|
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
180
181
|
fetchResult = await client.fetchPartial({
|
|
181
182
|
targetUrl: url,
|
|
182
183
|
segmentIds: segments,
|
|
183
184
|
previousUrl,
|
|
184
|
-
// Mark stale when explicitly requested OR when no segments are sent
|
|
185
|
-
// (action redirect sends empty segments for a fresh render).
|
|
186
185
|
staleRevalidation:
|
|
187
186
|
mode.type === "stale-revalidation" || segments.length === 0,
|
|
188
|
-
version,
|
|
187
|
+
version: getVersion(),
|
|
188
|
+
routerId: store.getRouterId?.(),
|
|
189
189
|
});
|
|
190
|
-
// Mark navigation as streaming (response received, now parsing RSC).
|
|
191
|
-
// Called after fetchPartial so pendingUrl stays set during the network wait,
|
|
192
|
-
// allowing useLinkStatus to show per-link pending indicators.
|
|
193
190
|
const streamingToken = tx.startStreaming();
|
|
194
|
-
const {
|
|
191
|
+
const {
|
|
192
|
+
payload,
|
|
193
|
+
streamComplete: rawStreamComplete,
|
|
194
|
+
fullyPrefetched,
|
|
195
|
+
} = fetchResult;
|
|
195
196
|
debugLog("payload.metadata", payload.metadata);
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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;
|
|
215
|
+
}
|
|
200
216
|
|
|
201
|
-
// Handle server-side redirect with state
|
|
202
217
|
if (payload.metadata?.redirect) {
|
|
203
218
|
if (signal?.aborted) {
|
|
204
219
|
debugLog("[Browser] Ignoring stale redirect (aborted)");
|
|
205
220
|
return;
|
|
206
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
|
+
}
|
|
207
240
|
const redirectUrl = validateRedirectOrigin(
|
|
208
241
|
payload.metadata.redirect.url,
|
|
209
242
|
window.location.origin,
|
|
@@ -228,7 +261,6 @@ export function createPartialUpdater(
|
|
|
228
261
|
debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
|
|
229
262
|
debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
|
|
230
263
|
|
|
231
|
-
// If diff is empty, nothing changed on server side.
|
|
232
264
|
if (!diff || diff.length === 0) {
|
|
233
265
|
const matchedIds = matched || [];
|
|
234
266
|
const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
|
|
@@ -236,8 +268,7 @@ export function createPartialUpdater(
|
|
|
236
268
|
.map((id: string) => cacheMap.get(id))
|
|
237
269
|
.filter(Boolean) as ResolvedSegment[];
|
|
238
270
|
|
|
239
|
-
|
|
240
|
-
if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
|
|
271
|
+
if (mode.type === "navigate" && targetCache) {
|
|
241
272
|
debugLog(
|
|
242
273
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
243
274
|
);
|
|
@@ -246,12 +277,18 @@ export function createPartialUpdater(
|
|
|
246
277
|
forceAwait: true,
|
|
247
278
|
});
|
|
248
279
|
|
|
249
|
-
tx.commit(
|
|
280
|
+
const { scroll: commitScroll } = tx.commit(
|
|
281
|
+
matchedIds,
|
|
282
|
+
existingSegments,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (mode.targetCacheHandleData) {
|
|
286
|
+
store.updateCacheHandleData(
|
|
287
|
+
store.getHistoryKey(),
|
|
288
|
+
mode.targetCacheHandleData,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
250
291
|
|
|
251
|
-
// Include cachedHandleData in metadata so NavigationProvider can restore
|
|
252
|
-
// breadcrumbs and other handle data from cache.
|
|
253
|
-
// Remove `handles` from metadata to prevent NavigationProvider from
|
|
254
|
-
// processing an empty handles stream, which would clear the cached breadcrumbs.
|
|
255
292
|
const { handles: _unusedHandles, ...metadataWithoutHandles } =
|
|
256
293
|
payload.metadata!;
|
|
257
294
|
const cachedUpdate = {
|
|
@@ -260,12 +297,10 @@ export function createPartialUpdater(
|
|
|
260
297
|
...metadataWithoutHandles,
|
|
261
298
|
cachedHandleData: mode.targetCacheHandleData,
|
|
262
299
|
},
|
|
300
|
+
scroll: toScrollPayload(commitScroll),
|
|
263
301
|
};
|
|
264
302
|
|
|
265
|
-
|
|
266
|
-
(s) => s.transition,
|
|
267
|
-
);
|
|
268
|
-
if (cachedHasTransition) {
|
|
303
|
+
if (shouldStartViewTransition(existingSegments)) {
|
|
269
304
|
startTransition(() => {
|
|
270
305
|
if (addTransitionType) {
|
|
271
306
|
addTransitionType("navigation");
|
|
@@ -280,7 +315,6 @@ export function createPartialUpdater(
|
|
|
280
315
|
return;
|
|
281
316
|
}
|
|
282
317
|
|
|
283
|
-
// When leaving intercept, force re-render even with empty diff
|
|
284
318
|
if (mode.type === "leave-intercept") {
|
|
285
319
|
debugLog(
|
|
286
320
|
"[Browser] Leaving intercept - forcing re-render to remove modal",
|
|
@@ -290,18 +324,21 @@ export function createPartialUpdater(
|
|
|
290
324
|
forceAwait: true,
|
|
291
325
|
});
|
|
292
326
|
|
|
293
|
-
tx.commit(
|
|
327
|
+
const { scroll: leaveScroll } = tx.commit(
|
|
328
|
+
matchedIds,
|
|
329
|
+
existingSegments,
|
|
330
|
+
);
|
|
294
331
|
|
|
295
332
|
onUpdate({
|
|
296
333
|
root: newTree,
|
|
297
334
|
metadata: payload.metadata,
|
|
335
|
+
scroll: toScrollPayload(leaveScroll),
|
|
298
336
|
});
|
|
299
337
|
|
|
300
338
|
debugLog("[Browser] Navigation complete (left intercept)");
|
|
301
339
|
return;
|
|
302
340
|
}
|
|
303
341
|
|
|
304
|
-
// Same route revalidation with no changes - skip UI update
|
|
305
342
|
debugLog(
|
|
306
343
|
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
307
344
|
);
|
|
@@ -310,7 +347,6 @@ export function createPartialUpdater(
|
|
|
310
347
|
return;
|
|
311
348
|
}
|
|
312
349
|
|
|
313
|
-
// Reconcile server segments with cached segments (single source of truth)
|
|
314
350
|
const matchedIds = matched || [];
|
|
315
351
|
const actor: ReconcileActor =
|
|
316
352
|
mode.type === "stale-revalidation" || mode.type === "action"
|
|
@@ -326,7 +362,6 @@ export function createPartialUpdater(
|
|
|
326
362
|
insertMissingDiff: true,
|
|
327
363
|
});
|
|
328
364
|
|
|
329
|
-
// HMR RESILIENCE: Check if we're missing any matched segments
|
|
330
365
|
const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
|
|
331
366
|
const missingIds = matchedIds.filter(
|
|
332
367
|
(id: string) => !reconciledIdSet.has(id),
|
|
@@ -354,7 +389,6 @@ export function createPartialUpdater(
|
|
|
354
389
|
`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
|
|
355
390
|
);
|
|
356
391
|
|
|
357
|
-
// Refetch with empty segments = server sends everything
|
|
358
392
|
return fetchPartialUpdate(url, [], true, signal, tx, mode);
|
|
359
393
|
}
|
|
360
394
|
|
|
@@ -363,7 +397,6 @@ export function createPartialUpdater(
|
|
|
363
397
|
return;
|
|
364
398
|
}
|
|
365
399
|
|
|
366
|
-
// Rebuild tree on client (await for loader data resolution)
|
|
367
400
|
const renderOptions = {
|
|
368
401
|
isAction: mode.type === "action",
|
|
369
402
|
forceAwait: mode.type === "stale-revalidation",
|
|
@@ -372,35 +405,44 @@ export function createPartialUpdater(
|
|
|
372
405
|
? reconciled.interceptSegments
|
|
373
406
|
: undefined,
|
|
374
407
|
};
|
|
375
|
-
|
|
376
|
-
|
|
408
|
+
let newTree: Awaited<ReturnType<typeof renderSegments>>;
|
|
409
|
+
if (signal) {
|
|
410
|
+
// Race render against abort. Store the abort handler and register it
|
|
411
|
+
// { once:true } so a non-aborted render (which wins the race) can
|
|
412
|
+
// remove it in finally — otherwise the listener stays attached and the
|
|
413
|
+
// rejecting promise never settles. Mirrors teeWithCompletion in
|
|
414
|
+
// browser/response-adapter.ts.
|
|
415
|
+
let onAbort: (() => void) | undefined;
|
|
416
|
+
const abortPromise = new Promise<never>((_, reject) => {
|
|
417
|
+
if (signal.aborted) {
|
|
418
|
+
reject(new DOMException("Navigation aborted", "AbortError"));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
onAbort = () =>
|
|
422
|
+
reject(new DOMException("Navigation aborted", "AbortError"));
|
|
423
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
424
|
+
});
|
|
425
|
+
try {
|
|
426
|
+
newTree = await Promise.race([
|
|
377
427
|
renderSegments(reconciled.mainSegments, renderOptions),
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
: renderSegments(reconciled.mainSegments, renderOptions));
|
|
388
|
-
|
|
389
|
-
// Final abort check before committing - another navigation may have started
|
|
428
|
+
abortPromise,
|
|
429
|
+
]);
|
|
430
|
+
} finally {
|
|
431
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
newTree = await renderSegments(reconciled.mainSegments, renderOptions);
|
|
435
|
+
}
|
|
436
|
+
|
|
390
437
|
if (signal?.aborted) {
|
|
391
438
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
392
439
|
return;
|
|
393
440
|
}
|
|
394
441
|
|
|
395
|
-
// Check if this is an intercept response (any slot is active)
|
|
396
442
|
const isInterceptResponse = hasActiveInterceptSlots(
|
|
397
443
|
payload.metadata?.slots,
|
|
398
444
|
);
|
|
399
445
|
|
|
400
|
-
// Track intercept context (only on navigation, not actions or stale revalidation)
|
|
401
|
-
// Use the authoritative source from mode/history state when restoring an
|
|
402
|
-
// intercept via popstate cache miss; fall back to the current URL for fresh
|
|
403
|
-
// intercept navigations.
|
|
404
446
|
const effectiveInterceptSource =
|
|
405
447
|
interceptSourceUrl || segmentState.currentUrl;
|
|
406
448
|
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
@@ -411,8 +453,7 @@ export function createPartialUpdater(
|
|
|
411
453
|
}
|
|
412
454
|
}
|
|
413
455
|
|
|
414
|
-
|
|
415
|
-
const allSegmentIds = reconciled.segments.map((s) => s.id);
|
|
456
|
+
const allSegmentIds = matchedIds;
|
|
416
457
|
const serverLocationState = payload.metadata?.locationState;
|
|
417
458
|
const overrides: CommitOverrides | undefined = isInterceptResponse
|
|
418
459
|
? {
|
|
@@ -424,9 +465,12 @@ export function createPartialUpdater(
|
|
|
424
465
|
: serverLocationState
|
|
425
466
|
? { serverState: serverLocationState }
|
|
426
467
|
: undefined;
|
|
427
|
-
tx.commit(
|
|
468
|
+
const { scroll: navScroll } = tx.commit(
|
|
469
|
+
allSegmentIds,
|
|
470
|
+
reconciled.segments,
|
|
471
|
+
overrides,
|
|
472
|
+
);
|
|
428
473
|
|
|
429
|
-
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
430
474
|
if (mode.type === "stale-revalidation") {
|
|
431
475
|
const historyKeyNow = store.getHistoryKey();
|
|
432
476
|
if (historyKeyNow !== historyKeyAtStart) {
|
|
@@ -439,8 +483,8 @@ export function createPartialUpdater(
|
|
|
439
483
|
|
|
440
484
|
debugLog("[partial-update] updating document");
|
|
441
485
|
|
|
442
|
-
|
|
443
|
-
const
|
|
486
|
+
const hasTransition = shouldStartViewTransition(reconciled.segments);
|
|
487
|
+
const scrollPayload = toScrollPayload(navScroll);
|
|
444
488
|
|
|
445
489
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
446
490
|
startTransition(() => {
|
|
@@ -450,6 +494,7 @@ export function createPartialUpdater(
|
|
|
450
494
|
onUpdate({
|
|
451
495
|
root: newTree,
|
|
452
496
|
metadata: payload.metadata!,
|
|
497
|
+
scroll: scrollPayload,
|
|
453
498
|
});
|
|
454
499
|
});
|
|
455
500
|
} else if (hasTransition) {
|
|
@@ -460,19 +505,34 @@ export function createPartialUpdater(
|
|
|
460
505
|
onUpdate({
|
|
461
506
|
root: newTree,
|
|
462
507
|
metadata: payload.metadata!,
|
|
508
|
+
scroll: scrollPayload,
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
} else if (fullyPrefetched) {
|
|
512
|
+
// Fully-prefetched navigation: the payload is already fully resolved, so
|
|
513
|
+
// commit inside a transition to hold the current UI across the synchronous
|
|
514
|
+
// use()/Suspense resolution and avoid a 1-frame loading()/fallback flash.
|
|
515
|
+
// No addTransitionType: this is not a view transition, just the React
|
|
516
|
+
// content-hold. Cold and partially-prefetched navs do NOT reach here, so
|
|
517
|
+
// their fallbacks still stream (see fullyPrefetched in navigation-client).
|
|
518
|
+
startTransition(() => {
|
|
519
|
+
onUpdate({
|
|
520
|
+
root: newTree,
|
|
521
|
+
metadata: payload.metadata!,
|
|
522
|
+
scroll: scrollPayload,
|
|
463
523
|
});
|
|
464
524
|
});
|
|
465
525
|
} else {
|
|
466
526
|
onUpdate({
|
|
467
527
|
root: newTree,
|
|
468
528
|
metadata: payload.metadata!,
|
|
529
|
+
scroll: scrollPayload,
|
|
469
530
|
});
|
|
470
531
|
}
|
|
471
532
|
|
|
472
533
|
debugLog("[Browser] Navigation complete");
|
|
473
534
|
return;
|
|
474
535
|
} else {
|
|
475
|
-
// Full update (fallback)
|
|
476
536
|
console.warn(`[Browser] Full update (fallback)`);
|
|
477
537
|
|
|
478
538
|
const segments = payload.metadata?.segments || [];
|
|
@@ -492,18 +552,29 @@ export function createPartialUpdater(
|
|
|
492
552
|
}
|
|
493
553
|
|
|
494
554
|
const fullUpdateServerState = payload.metadata?.locationState;
|
|
495
|
-
|
|
496
|
-
tx.commit(segmentIds, segments, {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
555
|
+
const { scroll: fullScroll } = fullUpdateServerState
|
|
556
|
+
? tx.commit(segmentIds, segments, {
|
|
557
|
+
serverState: fullUpdateServerState,
|
|
558
|
+
})
|
|
559
|
+
: tx.commit(segmentIds, segments);
|
|
500
560
|
|
|
501
|
-
const fullHasTransition = segments
|
|
502
|
-
|
|
503
|
-
);
|
|
561
|
+
const fullHasTransition = shouldStartViewTransition(segments);
|
|
562
|
+
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
504
563
|
|
|
505
564
|
if (mode.type === "stale-revalidation") {
|
|
506
565
|
await rawStreamComplete;
|
|
566
|
+
// Mirror the partial branch's history-key staleness guard (above): the
|
|
567
|
+
// await above is a real async suspension, so the user may have navigated
|
|
568
|
+
// away while this background revalidation was draining. Dropping a late
|
|
569
|
+
// full-update here prevents it from clobbering the freshly committed UI
|
|
570
|
+
// of the page the user moved to.
|
|
571
|
+
const historyKeyNow = store.getHistoryKey();
|
|
572
|
+
if (historyKeyNow !== historyKeyAtStart) {
|
|
573
|
+
debugLog(
|
|
574
|
+
`[Browser] Stale revalidation (full update): history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
|
|
575
|
+
);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
507
578
|
startTransition(() => {
|
|
508
579
|
if (fullHasTransition && addTransitionType) {
|
|
509
580
|
addTransitionType("action");
|
|
@@ -511,16 +582,18 @@ export function createPartialUpdater(
|
|
|
511
582
|
onUpdate({
|
|
512
583
|
root: newTree,
|
|
513
584
|
metadata: payload.metadata!,
|
|
585
|
+
scroll: fullScrollPayload,
|
|
514
586
|
});
|
|
515
587
|
});
|
|
516
588
|
} else if (mode.type === "action") {
|
|
517
|
-
startTransition(
|
|
589
|
+
startTransition(() => {
|
|
518
590
|
if (fullHasTransition && addTransitionType) {
|
|
519
591
|
addTransitionType("action");
|
|
520
592
|
}
|
|
521
593
|
onUpdate({
|
|
522
594
|
root: newTree,
|
|
523
595
|
metadata: payload.metadata!,
|
|
596
|
+
scroll: fullScrollPayload,
|
|
524
597
|
});
|
|
525
598
|
});
|
|
526
599
|
} else if (fullHasTransition) {
|
|
@@ -531,12 +604,14 @@ export function createPartialUpdater(
|
|
|
531
604
|
onUpdate({
|
|
532
605
|
root: newTree,
|
|
533
606
|
metadata: payload.metadata!,
|
|
607
|
+
scroll: fullScrollPayload,
|
|
534
608
|
});
|
|
535
609
|
});
|
|
536
610
|
} else {
|
|
537
611
|
onUpdate({
|
|
538
612
|
root: newTree,
|
|
539
613
|
metadata: payload.metadata!,
|
|
614
|
+
scroll: fullScrollPayload,
|
|
540
615
|
});
|
|
541
616
|
}
|
|
542
617
|
|