@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136
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 +245 -49
- package/dist/bin/rango.js +440 -133
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3386 -1111
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +78 -25
- 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 +281 -11
- 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 +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 +87 -18
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +289 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +914 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +144 -7
- package/skills/prerender/SKILL.md +172 -125
- 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 +117 -10
- 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 +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 +332 -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 +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 +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 +131 -30
- package/src/browser/navigation-client.ts +199 -86
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +157 -74
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +174 -109
- package/src/browser/prefetch/cache.ts +222 -69
- package/src/browser/prefetch/fetch.ts +347 -39
- package/src/browser/prefetch/queue.ts +113 -32
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +102 -15
- package/src/browser/react/NavigationProvider.tsx +300 -122
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- 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 +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 +99 -8
- package/src/browser/rsc-router.tsx +125 -28
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -61
- package/src/browser/types.ts +115 -12
- 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 +260 -94
- 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 +239 -52
- package/src/cache/cache-scope.ts +234 -87
- 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/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/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 +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 +99 -13
- package/src/index.ts +139 -19
- 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 +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +149 -74
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -46
- 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 +44 -23
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +124 -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 +54 -25
- package/src/router/match-middleware/cache-lookup.ts +205 -271
- package/src/router/match-middleware/cache-store.ts +81 -50
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +93 -39
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +268 -171
- 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/prefetch-limits.ts +37 -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 +3 -27
- package/src/router/router-interfaces.ts +129 -35
- package/src/router/router-options.ts +202 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +197 -92
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +392 -338
- 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 +163 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +174 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +660 -518
- 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 +133 -13
- 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 +115 -73
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -113
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +36 -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 +269 -202
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +235 -51
- 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 +38 -46
- package/src/server/request-context.ts +442 -173
- package/src/ssr/index.tsx +24 -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 +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 +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +234 -82
- package/src/types/index.ts +3 -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 +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +68 -18
- 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 -19
- 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 -4
- 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 +178 -5
- 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 +48 -95
- 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 +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 +119 -111
- package/src/vite/router-discovery.ts +941 -142
- 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 +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,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
|
-
import { debugLog } from "./logging.js";
|
|
21
|
-
import {
|
|
23
|
+
import { debugLog, isBrowserDebugEnabled } from "./logging.js";
|
|
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
|
*/
|
|
@@ -39,8 +54,8 @@ export interface PartialUpdateConfig {
|
|
|
39
54
|
segments: ResolvedSegment[],
|
|
40
55
|
options?: RenderSegmentsOptions,
|
|
41
56
|
) => Promise<ReactNode> | ReactNode;
|
|
42
|
-
/** RSC version
|
|
43
|
-
|
|
57
|
+
/** RSC version getter — returns the current version (may change after HMR) */
|
|
58
|
+
getVersion?: () => string | undefined;
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
/**
|
|
@@ -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,35 +107,23 @@ 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 {
|
|
107
|
-
const {
|
|
113
|
+
const {
|
|
114
|
+
store,
|
|
115
|
+
client,
|
|
116
|
+
onUpdate,
|
|
117
|
+
renderSegments,
|
|
118
|
+
getVersion = () => undefined,
|
|
119
|
+
} = config;
|
|
108
120
|
|
|
109
|
-
/**
|
|
110
|
-
* Get current page's cached segments as an array
|
|
111
|
-
*/
|
|
112
121
|
function getCurrentCachedSegments(): ResolvedSegment[] {
|
|
113
122
|
const currentKey = store.getHistoryKey();
|
|
114
123
|
const cached = store.getCachedSegments(currentKey);
|
|
115
124
|
return cached?.segments || [];
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
/**
|
|
119
|
-
* Fetch partial update and trigger UI update
|
|
120
|
-
*
|
|
121
|
-
* @param tx - Transaction for committing segment state (required)
|
|
122
|
-
* @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
|
|
123
|
-
*/
|
|
124
127
|
async function fetchPartialUpdate(
|
|
125
128
|
targetUrl: string,
|
|
126
129
|
segmentIds: string[] | undefined,
|
|
@@ -132,26 +135,16 @@ export function createPartialUpdater(
|
|
|
132
135
|
const segmentState = store.getSegmentState();
|
|
133
136
|
const url = targetUrl || window.location.href;
|
|
134
137
|
|
|
135
|
-
// Capture history key at start for stale revalidation consistency check
|
|
136
138
|
const historyKeyAtStart = store.getHistoryKey();
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
const interceptSourceUrl =
|
|
140
|
-
mode.type === "stale-revalidation" ||
|
|
141
|
-
mode.type === "action" ||
|
|
142
|
-
mode.type === "navigate"
|
|
143
|
-
? mode.interceptSourceUrl
|
|
144
|
-
: undefined;
|
|
140
|
+
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
145
141
|
|
|
146
|
-
// When leaving intercept, filter out intercept-specific segments
|
|
147
142
|
let segments: string[];
|
|
148
143
|
if (mode.type === "leave-intercept") {
|
|
149
144
|
const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
|
|
150
145
|
const currentCached = getCurrentCachedSegments();
|
|
151
146
|
const interceptIds = new Set(
|
|
152
|
-
currentCached
|
|
153
|
-
.filter((s) => s.namespace?.startsWith("intercept:"))
|
|
154
|
-
.map((s) => s.id),
|
|
147
|
+
currentCached.filter(isInterceptSegment).map((s) => s.id),
|
|
155
148
|
);
|
|
156
149
|
segments = currentSegments.filter((id) => !interceptIds.has(id));
|
|
157
150
|
debugLog(
|
|
@@ -161,9 +154,10 @@ export function createPartialUpdater(
|
|
|
161
154
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
162
155
|
}
|
|
163
156
|
|
|
164
|
-
// For intercept revalidation, use the intercept source URL as previousUrl
|
|
165
157
|
const previousUrl =
|
|
166
|
-
|
|
158
|
+
mode.type === "leave-intercept"
|
|
159
|
+
? segmentState.currentUrl || tx.currentUrl
|
|
160
|
+
: interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
|
|
167
161
|
|
|
168
162
|
debugLog(`\n[Browser] >>> NAVIGATION`);
|
|
169
163
|
debugLog(`[Browser] From: ${previousUrl}`);
|
|
@@ -173,45 +167,76 @@ export function createPartialUpdater(
|
|
|
173
167
|
debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
|
|
174
168
|
}
|
|
175
169
|
|
|
176
|
-
// Get cached segments for merging with server diff.
|
|
177
|
-
// When navigating with targetCacheSegments, use those for consistency.
|
|
178
|
-
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
179
170
|
const targetCache =
|
|
180
|
-
mode.type === "navigate"
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
);
|
|
185
179
|
|
|
186
|
-
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
187
180
|
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
188
181
|
fetchResult = await client.fetchPartial({
|
|
189
182
|
targetUrl: url,
|
|
190
183
|
segmentIds: segments,
|
|
191
184
|
previousUrl,
|
|
192
|
-
// Mark stale when explicitly requested OR when no segments are sent
|
|
193
|
-
// (action redirect sends empty segments for a fresh render).
|
|
194
185
|
staleRevalidation:
|
|
195
186
|
mode.type === "stale-revalidation" || segments.length === 0,
|
|
196
|
-
version,
|
|
187
|
+
version: getVersion(),
|
|
188
|
+
routerId: store.getRouterId?.(),
|
|
197
189
|
});
|
|
198
|
-
// Mark navigation as streaming (response received, now parsing RSC).
|
|
199
|
-
// Called after fetchPartial so pendingUrl stays set during the network wait,
|
|
200
|
-
// allowing useLinkStatus to show per-link pending indicators.
|
|
201
190
|
const streamingToken = tx.startStreaming();
|
|
202
|
-
const {
|
|
191
|
+
const {
|
|
192
|
+
payload,
|
|
193
|
+
streamComplete: rawStreamComplete,
|
|
194
|
+
fullyPrefetched,
|
|
195
|
+
} = fetchResult;
|
|
203
196
|
debugLog("payload.metadata", payload.metadata);
|
|
204
197
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
+
}
|
|
208
216
|
|
|
209
|
-
// Handle server-side redirect with state
|
|
210
217
|
if (payload.metadata?.redirect) {
|
|
211
218
|
if (signal?.aborted) {
|
|
212
219
|
debugLog("[Browser] Ignoring stale redirect (aborted)");
|
|
213
220
|
return;
|
|
214
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
|
+
}
|
|
215
240
|
const redirectUrl = validateRedirectOrigin(
|
|
216
241
|
payload.metadata.redirect.url,
|
|
217
242
|
window.location.origin,
|
|
@@ -236,7 +261,6 @@ export function createPartialUpdater(
|
|
|
236
261
|
debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
|
|
237
262
|
debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
|
|
238
263
|
|
|
239
|
-
// If diff is empty, nothing changed on server side.
|
|
240
264
|
if (!diff || diff.length === 0) {
|
|
241
265
|
const matchedIds = matched || [];
|
|
242
266
|
const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
|
|
@@ -244,8 +268,7 @@ export function createPartialUpdater(
|
|
|
244
268
|
.map((id: string) => cacheMap.get(id))
|
|
245
269
|
.filter(Boolean) as ResolvedSegment[];
|
|
246
270
|
|
|
247
|
-
|
|
248
|
-
if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
|
|
271
|
+
if (mode.type === "navigate" && targetCache) {
|
|
249
272
|
debugLog(
|
|
250
273
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
251
274
|
);
|
|
@@ -259,10 +282,13 @@ export function createPartialUpdater(
|
|
|
259
282
|
existingSegments,
|
|
260
283
|
);
|
|
261
284
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
285
|
+
if (mode.targetCacheHandleData) {
|
|
286
|
+
store.updateCacheHandleData(
|
|
287
|
+
store.getHistoryKey(),
|
|
288
|
+
mode.targetCacheHandleData,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
266
292
|
const { handles: _unusedHandles, ...metadataWithoutHandles } =
|
|
267
293
|
payload.metadata!;
|
|
268
294
|
const cachedUpdate = {
|
|
@@ -274,10 +300,7 @@ export function createPartialUpdater(
|
|
|
274
300
|
scroll: toScrollPayload(commitScroll),
|
|
275
301
|
};
|
|
276
302
|
|
|
277
|
-
|
|
278
|
-
(s) => s.transition,
|
|
279
|
-
);
|
|
280
|
-
if (cachedHasTransition) {
|
|
303
|
+
if (shouldStartViewTransition(existingSegments)) {
|
|
281
304
|
startTransition(() => {
|
|
282
305
|
if (addTransitionType) {
|
|
283
306
|
addTransitionType("navigation");
|
|
@@ -292,7 +315,6 @@ export function createPartialUpdater(
|
|
|
292
315
|
return;
|
|
293
316
|
}
|
|
294
317
|
|
|
295
|
-
// When leaving intercept, force re-render even with empty diff
|
|
296
318
|
if (mode.type === "leave-intercept") {
|
|
297
319
|
debugLog(
|
|
298
320
|
"[Browser] Leaving intercept - forcing re-render to remove modal",
|
|
@@ -317,7 +339,6 @@ export function createPartialUpdater(
|
|
|
317
339
|
return;
|
|
318
340
|
}
|
|
319
341
|
|
|
320
|
-
// Same route revalidation with no changes - skip UI update
|
|
321
342
|
debugLog(
|
|
322
343
|
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
323
344
|
);
|
|
@@ -326,7 +347,6 @@ export function createPartialUpdater(
|
|
|
326
347
|
return;
|
|
327
348
|
}
|
|
328
349
|
|
|
329
|
-
// Reconcile server segments with cached segments (single source of truth)
|
|
330
350
|
const matchedIds = matched || [];
|
|
331
351
|
const actor: ReconcileActor =
|
|
332
352
|
mode.type === "stale-revalidation" || mode.type === "action"
|
|
@@ -342,7 +362,6 @@ export function createPartialUpdater(
|
|
|
342
362
|
insertMissingDiff: true,
|
|
343
363
|
});
|
|
344
364
|
|
|
345
|
-
// HMR RESILIENCE: Check if we're missing any matched segments
|
|
346
365
|
const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
|
|
347
366
|
const missingIds = matchedIds.filter(
|
|
348
367
|
(id: string) => !reconciledIdSet.has(id),
|
|
@@ -370,7 +389,6 @@ export function createPartialUpdater(
|
|
|
370
389
|
`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
|
|
371
390
|
);
|
|
372
391
|
|
|
373
|
-
// Refetch with empty segments = server sends everything
|
|
374
392
|
return fetchPartialUpdate(url, [], true, signal, tx, mode);
|
|
375
393
|
}
|
|
376
394
|
|
|
@@ -379,44 +397,59 @@ export function createPartialUpdater(
|
|
|
379
397
|
return;
|
|
380
398
|
}
|
|
381
399
|
|
|
382
|
-
// Rebuild tree on client (await for loader data resolution)
|
|
383
400
|
const renderOptions = {
|
|
384
401
|
isAction: mode.type === "action",
|
|
385
|
-
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,
|
|
386
410
|
interceptSegments:
|
|
387
411
|
reconciled.interceptSegments.length > 0
|
|
388
412
|
? reconciled.interceptSegments
|
|
389
413
|
: undefined,
|
|
390
414
|
};
|
|
391
|
-
|
|
392
|
-
|
|
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([
|
|
393
434
|
renderSegments(reconciled.mainSegments, renderOptions),
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
: renderSegments(reconciled.mainSegments, renderOptions));
|
|
404
|
-
|
|
405
|
-
// 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
|
+
|
|
406
444
|
if (signal?.aborted) {
|
|
407
445
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
408
446
|
return;
|
|
409
447
|
}
|
|
410
448
|
|
|
411
|
-
// Check if this is an intercept response (any slot is active)
|
|
412
449
|
const isInterceptResponse = hasActiveInterceptSlots(
|
|
413
450
|
payload.metadata?.slots,
|
|
414
451
|
);
|
|
415
452
|
|
|
416
|
-
// Track intercept context (only on navigation, not actions or stale revalidation)
|
|
417
|
-
// Use the authoritative source from mode/history state when restoring an
|
|
418
|
-
// intercept via popstate cache miss; fall back to the current URL for fresh
|
|
419
|
-
// intercept navigations.
|
|
420
453
|
const effectiveInterceptSource =
|
|
421
454
|
interceptSourceUrl || segmentState.currentUrl;
|
|
422
455
|
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
@@ -427,9 +460,6 @@ export function createPartialUpdater(
|
|
|
427
460
|
}
|
|
428
461
|
}
|
|
429
462
|
|
|
430
|
-
// Commit navigation - use server's matched as the authoritative segment ID list.
|
|
431
|
-
// reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
|
|
432
|
-
// but the server's matched always includes all expected segment IDs.
|
|
433
463
|
const allSegmentIds = matchedIds;
|
|
434
464
|
const serverLocationState = payload.metadata?.locationState;
|
|
435
465
|
const overrides: CommitOverrides | undefined = isInterceptResponse
|
|
@@ -448,7 +478,6 @@ export function createPartialUpdater(
|
|
|
448
478
|
overrides,
|
|
449
479
|
);
|
|
450
480
|
|
|
451
|
-
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
452
481
|
if (mode.type === "stale-revalidation") {
|
|
453
482
|
const historyKeyNow = store.getHistoryKey();
|
|
454
483
|
if (historyKeyNow !== historyKeyAtStart) {
|
|
@@ -461,9 +490,22 @@ export function createPartialUpdater(
|
|
|
461
490
|
|
|
462
491
|
debugLog("[partial-update] updating document");
|
|
463
492
|
|
|
464
|
-
|
|
465
|
-
//
|
|
466
|
-
|
|
493
|
+
const hasTransition = shouldStartViewTransition(reconciled.segments);
|
|
494
|
+
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. Reports which reconciled
|
|
495
|
+
// segment still carries a transition after the server-side when-gate, and
|
|
496
|
+
// whether the commit will be held in a startTransition. If `withTransition`
|
|
497
|
+
// lists an ancestor (layout/root) id rather than the gated leaf, an ungated
|
|
498
|
+
// ancestor transition is holding the subtree (missing loading() fallback).
|
|
499
|
+
if (isBrowserDebugEnabled()) {
|
|
500
|
+
debugLog("[VT-DIAG] commit", {
|
|
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
|
+
});
|
|
508
|
+
}
|
|
467
509
|
const scrollPayload = toScrollPayload(navScroll);
|
|
468
510
|
|
|
469
511
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
@@ -489,6 +531,20 @@ export function createPartialUpdater(
|
|
|
489
531
|
});
|
|
490
532
|
});
|
|
491
533
|
} else {
|
|
534
|
+
// Normal commit (cold/partial nav AND fully-prefetched nav). For a
|
|
535
|
+
// fully-prefetched nav, renderOptions.forceAwait (above) unwrapped the
|
|
536
|
+
// already-resolved ROUTER loader data AND route content during render, so
|
|
537
|
+
// the new tree carries it inline with no loading()/fallback frame — yet we
|
|
538
|
+
// still commit NORMALLY here rather than in a transition. A transition
|
|
539
|
+
// holds the OLD UI until ALL suspense in the new tree settles, including a
|
|
540
|
+
// CLIENT component that starts its own data request only when mounted
|
|
541
|
+
// (post-commit) under a persistent boundary; that would retain the
|
|
542
|
+
// previous page indefinitely with no feedback. A normal commit lets such
|
|
543
|
+
// client-initiated suspense reveal a fallback (correct) while the router
|
|
544
|
+
// data — genuinely ready — never flashes. Cold/partial navs
|
|
545
|
+
// (fullyPrefetched=false) do not forceAwait, so they stream their
|
|
546
|
+
// fallbacks. Explicit transition() routes keep the broader content-hold
|
|
547
|
+
// via the hasTransition branch above (the documented opt-in).
|
|
492
548
|
onUpdate({
|
|
493
549
|
root: newTree,
|
|
494
550
|
metadata: payload.metadata!,
|
|
@@ -499,7 +555,6 @@ export function createPartialUpdater(
|
|
|
499
555
|
debugLog("[Browser] Navigation complete");
|
|
500
556
|
return;
|
|
501
557
|
} else {
|
|
502
|
-
// Full update (fallback)
|
|
503
558
|
console.warn(`[Browser] Full update (fallback)`);
|
|
504
559
|
|
|
505
560
|
const segments = payload.metadata?.segments || [];
|
|
@@ -525,13 +580,23 @@ export function createPartialUpdater(
|
|
|
525
580
|
})
|
|
526
581
|
: tx.commit(segmentIds, segments);
|
|
527
582
|
|
|
528
|
-
const fullHasTransition = segments
|
|
529
|
-
(s: ResolvedSegment) => s.transition,
|
|
530
|
-
);
|
|
583
|
+
const fullHasTransition = shouldStartViewTransition(segments);
|
|
531
584
|
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
532
585
|
|
|
533
586
|
if (mode.type === "stale-revalidation") {
|
|
534
587
|
await rawStreamComplete;
|
|
588
|
+
// Mirror the partial branch's history-key staleness guard (above): the
|
|
589
|
+
// await above is a real async suspension, so the user may have navigated
|
|
590
|
+
// away while this background revalidation was draining. Dropping a late
|
|
591
|
+
// full-update here prevents it from clobbering the freshly committed UI
|
|
592
|
+
// of the page the user moved to.
|
|
593
|
+
const historyKeyNow = store.getHistoryKey();
|
|
594
|
+
if (historyKeyNow !== historyKeyAtStart) {
|
|
595
|
+
debugLog(
|
|
596
|
+
`[Browser] Stale revalidation (full update): history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
|
|
597
|
+
);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
535
600
|
startTransition(() => {
|
|
536
601
|
if (fullHasTransition && addTransitionType) {
|
|
537
602
|
addTransitionType("action");
|
|
@@ -543,7 +608,7 @@ export function createPartialUpdater(
|
|
|
543
608
|
});
|
|
544
609
|
});
|
|
545
610
|
} else if (mode.type === "action") {
|
|
546
|
-
startTransition(
|
|
611
|
+
startTransition(() => {
|
|
547
612
|
if (fullHasTransition && addTransitionType) {
|
|
548
613
|
addTransitionType("action");
|
|
549
614
|
}
|