@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7
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 +3373 -1176
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- 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 +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 +235 -9
- 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 +77 -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 +70 -5
- package/skills/router-setup/SKILL.md +65 -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 +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 +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 +186 -100
- 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 +165 -112
- package/src/browser/prefetch/cache.ts +205 -62
- package/src/browser/prefetch/fetch.ts +347 -39
- package/src/browser/prefetch/queue.ts +42 -8
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +102 -15
- package/src/browser/react/NavigationProvider.tsx +295 -119
- 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 +114 -24
- 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 -72
- package/src/browser/types.ts +102 -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 +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 +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 +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 +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 +143 -69
- 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 +52 -31
- 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 +268 -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 +40 -24
- package/src/router/match-middleware/cache-lookup.ts +170 -276
- 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 +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +87 -39
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -140
- package/src/router/middleware.ts +266 -169
- 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 +3 -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 +162 -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 +351 -321
- 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 +9 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +157 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +655 -529
- 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 +122 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +131 -14
- 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 +109 -74
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -115
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +29 -9
- 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 +236 -202
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +224 -52
- 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 +401 -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 +125 -71
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +40 -11
- 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 +64 -170
- 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 +118 -114
- 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/debug-channel.ts +0 -93
- 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
|
*/
|
|
@@ -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,6 @@ export function createPartialUpdater(
|
|
|
259
282
|
existingSegments,
|
|
260
283
|
);
|
|
261
284
|
|
|
262
|
-
// Fix: tx.commit() cached the source page's handleData because
|
|
263
|
-
// eventController hasn't been updated yet. Overwrite with the
|
|
264
|
-
// correct cached handleData to prevent cache corruption on
|
|
265
|
-
// subsequent navigations to this same URL.
|
|
266
285
|
if (mode.targetCacheHandleData) {
|
|
267
286
|
store.updateCacheHandleData(
|
|
268
287
|
store.getHistoryKey(),
|
|
@@ -270,10 +289,6 @@ export function createPartialUpdater(
|
|
|
270
289
|
);
|
|
271
290
|
}
|
|
272
291
|
|
|
273
|
-
// Include cachedHandleData in metadata so NavigationProvider can restore
|
|
274
|
-
// breadcrumbs and other handle data from cache.
|
|
275
|
-
// Remove `handles` from metadata to prevent NavigationProvider from
|
|
276
|
-
// processing an empty handles stream, which would clear the cached breadcrumbs.
|
|
277
292
|
const { handles: _unusedHandles, ...metadataWithoutHandles } =
|
|
278
293
|
payload.metadata!;
|
|
279
294
|
const cachedUpdate = {
|
|
@@ -285,10 +300,7 @@ export function createPartialUpdater(
|
|
|
285
300
|
scroll: toScrollPayload(commitScroll),
|
|
286
301
|
};
|
|
287
302
|
|
|
288
|
-
|
|
289
|
-
(s) => s.transition,
|
|
290
|
-
);
|
|
291
|
-
if (cachedHasTransition) {
|
|
303
|
+
if (shouldStartViewTransition(existingSegments)) {
|
|
292
304
|
startTransition(() => {
|
|
293
305
|
if (addTransitionType) {
|
|
294
306
|
addTransitionType("navigation");
|
|
@@ -303,7 +315,6 @@ export function createPartialUpdater(
|
|
|
303
315
|
return;
|
|
304
316
|
}
|
|
305
317
|
|
|
306
|
-
// When leaving intercept, force re-render even with empty diff
|
|
307
318
|
if (mode.type === "leave-intercept") {
|
|
308
319
|
debugLog(
|
|
309
320
|
"[Browser] Leaving intercept - forcing re-render to remove modal",
|
|
@@ -328,7 +339,6 @@ export function createPartialUpdater(
|
|
|
328
339
|
return;
|
|
329
340
|
}
|
|
330
341
|
|
|
331
|
-
// Same route revalidation with no changes - skip UI update
|
|
332
342
|
debugLog(
|
|
333
343
|
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
334
344
|
);
|
|
@@ -337,7 +347,6 @@ export function createPartialUpdater(
|
|
|
337
347
|
return;
|
|
338
348
|
}
|
|
339
349
|
|
|
340
|
-
// Reconcile server segments with cached segments (single source of truth)
|
|
341
350
|
const matchedIds = matched || [];
|
|
342
351
|
const actor: ReconcileActor =
|
|
343
352
|
mode.type === "stale-revalidation" || mode.type === "action"
|
|
@@ -353,7 +362,6 @@ export function createPartialUpdater(
|
|
|
353
362
|
insertMissingDiff: true,
|
|
354
363
|
});
|
|
355
364
|
|
|
356
|
-
// HMR RESILIENCE: Check if we're missing any matched segments
|
|
357
365
|
const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
|
|
358
366
|
const missingIds = matchedIds.filter(
|
|
359
367
|
(id: string) => !reconciledIdSet.has(id),
|
|
@@ -381,7 +389,6 @@ export function createPartialUpdater(
|
|
|
381
389
|
`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
|
|
382
390
|
);
|
|
383
391
|
|
|
384
|
-
// Refetch with empty segments = server sends everything
|
|
385
392
|
return fetchPartialUpdate(url, [], true, signal, tx, mode);
|
|
386
393
|
}
|
|
387
394
|
|
|
@@ -390,44 +397,59 @@ export function createPartialUpdater(
|
|
|
390
397
|
return;
|
|
391
398
|
}
|
|
392
399
|
|
|
393
|
-
// Rebuild tree on client (await for loader data resolution)
|
|
394
400
|
const renderOptions = {
|
|
395
401
|
isAction: mode.type === "action",
|
|
396
|
-
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,
|
|
397
410
|
interceptSegments:
|
|
398
411
|
reconciled.interceptSegments.length > 0
|
|
399
412
|
? reconciled.interceptSegments
|
|
400
413
|
: undefined,
|
|
401
414
|
};
|
|
402
|
-
|
|
403
|
-
|
|
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([
|
|
404
434
|
renderSegments(reconciled.mainSegments, renderOptions),
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
: renderSegments(reconciled.mainSegments, renderOptions));
|
|
415
|
-
|
|
416
|
-
// 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
|
+
|
|
417
444
|
if (signal?.aborted) {
|
|
418
445
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
419
446
|
return;
|
|
420
447
|
}
|
|
421
448
|
|
|
422
|
-
// Check if this is an intercept response (any slot is active)
|
|
423
449
|
const isInterceptResponse = hasActiveInterceptSlots(
|
|
424
450
|
payload.metadata?.slots,
|
|
425
451
|
);
|
|
426
452
|
|
|
427
|
-
// Track intercept context (only on navigation, not actions or stale revalidation)
|
|
428
|
-
// Use the authoritative source from mode/history state when restoring an
|
|
429
|
-
// intercept via popstate cache miss; fall back to the current URL for fresh
|
|
430
|
-
// intercept navigations.
|
|
431
453
|
const effectiveInterceptSource =
|
|
432
454
|
interceptSourceUrl || segmentState.currentUrl;
|
|
433
455
|
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
@@ -438,9 +460,6 @@ export function createPartialUpdater(
|
|
|
438
460
|
}
|
|
439
461
|
}
|
|
440
462
|
|
|
441
|
-
// Commit navigation - use server's matched as the authoritative segment ID list.
|
|
442
|
-
// reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
|
|
443
|
-
// but the server's matched always includes all expected segment IDs.
|
|
444
463
|
const allSegmentIds = matchedIds;
|
|
445
464
|
const serverLocationState = payload.metadata?.locationState;
|
|
446
465
|
const overrides: CommitOverrides | undefined = isInterceptResponse
|
|
@@ -459,7 +478,6 @@ export function createPartialUpdater(
|
|
|
459
478
|
overrides,
|
|
460
479
|
);
|
|
461
480
|
|
|
462
|
-
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
463
481
|
if (mode.type === "stale-revalidation") {
|
|
464
482
|
const historyKeyNow = store.getHistoryKey();
|
|
465
483
|
if (historyKeyNow !== historyKeyAtStart) {
|
|
@@ -472,9 +490,21 @@ export function createPartialUpdater(
|
|
|
472
490
|
|
|
473
491
|
debugLog("[partial-update] updating document");
|
|
474
492
|
|
|
475
|
-
|
|
476
|
-
//
|
|
477
|
-
|
|
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
|
+
});
|
|
478
508
|
const scrollPayload = toScrollPayload(navScroll);
|
|
479
509
|
|
|
480
510
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
@@ -500,6 +530,20 @@ export function createPartialUpdater(
|
|
|
500
530
|
});
|
|
501
531
|
});
|
|
502
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).
|
|
503
547
|
onUpdate({
|
|
504
548
|
root: newTree,
|
|
505
549
|
metadata: payload.metadata!,
|
|
@@ -510,7 +554,6 @@ export function createPartialUpdater(
|
|
|
510
554
|
debugLog("[Browser] Navigation complete");
|
|
511
555
|
return;
|
|
512
556
|
} else {
|
|
513
|
-
// Full update (fallback)
|
|
514
557
|
console.warn(`[Browser] Full update (fallback)`);
|
|
515
558
|
|
|
516
559
|
const segments = payload.metadata?.segments || [];
|
|
@@ -536,13 +579,23 @@ export function createPartialUpdater(
|
|
|
536
579
|
})
|
|
537
580
|
: tx.commit(segmentIds, segments);
|
|
538
581
|
|
|
539
|
-
const fullHasTransition = segments
|
|
540
|
-
(s: ResolvedSegment) => s.transition,
|
|
541
|
-
);
|
|
582
|
+
const fullHasTransition = shouldStartViewTransition(segments);
|
|
542
583
|
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
543
584
|
|
|
544
585
|
if (mode.type === "stale-revalidation") {
|
|
545
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
|
+
}
|
|
546
599
|
startTransition(() => {
|
|
547
600
|
if (fullHasTransition && addTransitionType) {
|
|
548
601
|
addTransitionType("action");
|
|
@@ -554,7 +607,7 @@ export function createPartialUpdater(
|
|
|
554
607
|
});
|
|
555
608
|
});
|
|
556
609
|
} else if (mode.type === "action") {
|
|
557
|
-
startTransition(
|
|
610
|
+
startTransition(() => {
|
|
558
611
|
if (fullHasTransition && addTransitionType) {
|
|
559
612
|
addTransitionType("action");
|
|
560
613
|
}
|