@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa
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 -4
- package/README.md +296 -887
- package/dist/bin/rango.js +459 -91
- package/dist/testing/vitest.js +36 -2
- package/dist/vite/index.js +1708 -414
- package/package.json +35 -10
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +82 -5
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +14 -9
- package/skills/caching/SKILL.md +221 -12
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +11 -3
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +9 -9
- package/skills/hooks/SKILL.md +73 -900
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +84 -7
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +51 -17
- package/skills/layout/SKILL.md +38 -16
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +48 -20
- package/skills/middleware/SKILL.md +11 -5
- package/skills/migrate-nextjs/SKILL.md +203 -20
- package/skills/migrate-react-router/SKILL.md +59 -675
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +3 -3
- package/skills/observability/SKILL.md +70 -5
- package/skills/parallel/SKILL.md +32 -8
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +59 -28
- package/skills/rango/SKILL.md +124 -50
- package/skills/response-routes/SKILL.md +78 -46
- package/skills/route/SKILL.md +85 -6
- package/skills/router-setup/SKILL.md +41 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +28 -3
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +68 -654
- 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 +131 -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 +85 -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/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -918
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +47 -17
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +178 -100
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +81 -68
- package/src/browser/navigation-client.ts +115 -70
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +153 -88
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +157 -144
- package/src/browser/prefetch/cache.ts +148 -81
- package/src/browser/prefetch/fetch.ts +231 -51
- package/src/browser/prefetch/queue.ts +25 -7
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +40 -7
- package/src/browser/react/NavigationProvider.tsx +140 -99
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +14 -15
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +6 -4
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +74 -8
- package/src/browser/rsc-router.tsx +97 -22
- package/src/browser/scroll-restoration.ts +15 -8
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +216 -38
- package/src/browser/types.ts +94 -22
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +11 -5
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -22
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +12 -1
- package/src/build/route-types/include-resolution.ts +455 -61
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +159 -54
- package/src/cache/cache-tag.ts +149 -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 +2170 -377
- 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 +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +39 -22
- package/src/client.tsx +28 -58
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +57 -40
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +7 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +88 -8
- package/src/index.ts +90 -16
- package/src/internal-debug.ts +11 -10
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +75 -7
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +13 -49
- package/src/route-definition/dsl-helpers.ts +60 -53
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +46 -46
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +0 -5
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +35 -2
- package/src/router/lazy-includes.ts +79 -56
- package/src/router/loader-resolution.ts +151 -73
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +76 -52
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +181 -178
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +115 -194
- package/src/router/match-middleware/cache-store.ts +61 -50
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +36 -67
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +231 -120
- package/src/router/navigation-snapshot.ts +7 -56
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +99 -152
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +111 -66
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +47 -42
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +70 -8
- package/src/router/router-options.ts +126 -4
- package/src/router/segment-resolution/fresh.ts +104 -80
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +215 -304
- 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 +35 -15
- 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 +54 -35
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +9 -23
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -64
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +13 -5
- package/src/router.ts +119 -48
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +267 -152
- package/src/rsc/helpers.ts +78 -4
- package/src/rsc/index.ts +1 -4
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +11 -15
- package/src/rsc/progressive-enhancement.ts +120 -13
- package/src/rsc/redirect-guard.ts +100 -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 +58 -141
- package/src/rsc/rsc-rendering.ts +492 -49
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +268 -82
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +45 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +260 -95
- package/src/server/context.ts +99 -9
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +125 -2
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +379 -138
- package/src/ssr/index.tsx +491 -182
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +14 -31
- package/src/testing/dispatch.ts +533 -160
- package/src/testing/e2e/fixture.ts +45 -11
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/e2e/parity.ts +85 -4
- package/src/testing/e2e/server.ts +12 -0
- package/src/testing/flight-matchers.ts +7 -14
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +36 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +30 -0
- package/src/testing/flight.ts +145 -70
- package/src/testing/generated-routes.ts +26 -50
- package/src/testing/index.ts +18 -19
- package/src/testing/internal/context.ts +184 -68
- 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 +134 -115
- package/src/testing/run-loader.ts +140 -51
- package/src/testing/run-middleware.ts +59 -33
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
- package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
- package/src/testing/vitest.ts +138 -16
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +45 -15
- package/src/types/index.ts +2 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +100 -13
- package/src/urls/include-helper.ts +10 -12
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -8
- package/src/urls/path-helper-types.ts +52 -14
- package/src/urls/path-helper.ts +5 -54
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +76 -42
- package/src/urls/urls-function.ts +0 -14
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +34 -43
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +9 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +22 -27
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +137 -26
- package/src/vite/rango.ts +146 -135
- package/src/vite/router-discovery.ts +189 -48
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +71 -44
- package/src/vite/utils/shared-utils.ts +55 -37
- 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
|
@@ -20,24 +20,23 @@ import {
|
|
|
20
20
|
} from "./intercept-utils.js";
|
|
21
21
|
import type { BoundTransaction } from "./navigation-transaction.js";
|
|
22
22
|
import { ServerRedirect } from "../errors.js";
|
|
23
|
-
import {
|
|
24
|
-
|
|
23
|
+
import {
|
|
24
|
+
debugLog,
|
|
25
|
+
isBrowserDebugEnabled,
|
|
26
|
+
IS_BROWSER_DEBUG,
|
|
27
|
+
} from "./logging.js";
|
|
28
|
+
import {
|
|
29
|
+
validateRedirectOrigin,
|
|
30
|
+
validateExternalRedirect,
|
|
31
|
+
} from "./validate-redirect-origin.js";
|
|
25
32
|
import type { NavigationUpdate } from "./types.js";
|
|
26
33
|
|
|
27
|
-
/** Build a scroll payload from the commit's scroll option */
|
|
28
34
|
function toScrollPayload(
|
|
29
35
|
scroll: boolean | undefined,
|
|
30
36
|
): NonNullable<NavigationUpdate["scroll"]> {
|
|
31
37
|
return { enabled: scroll !== false ? scroll : false };
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
/**
|
|
35
|
-
* Whether to wrap an update in startViewTransition.
|
|
36
|
-
*
|
|
37
|
-
* Intercept-driven updates only mutate the parallel slot — the main outlet
|
|
38
|
-
* shows the same content — so transitions on the underlying main segments
|
|
39
|
-
* shouldn't fire (otherwise their elements get hoisted above the modal).
|
|
40
|
-
*/
|
|
41
40
|
function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
|
|
42
41
|
let hasIntercept = false;
|
|
43
42
|
let hasTransition = false;
|
|
@@ -61,13 +60,6 @@ export interface PartialUpdateConfig {
|
|
|
61
60
|
) => Promise<ReactNode> | ReactNode;
|
|
62
61
|
/** RSC version getter — returns the current version (may change after HMR) */
|
|
63
62
|
getVersion?: () => string | undefined;
|
|
64
|
-
/**
|
|
65
|
-
* Replace the active app-shell when a cross-app navigation is detected.
|
|
66
|
-
* Called before the full-update tree replacement renders, so the new
|
|
67
|
-
* payload's rootLayout, basename, and version are picked up. Theme,
|
|
68
|
-
* warmup, and prefetch TTL are not part of the shell — see AppShell.
|
|
69
|
-
*/
|
|
70
|
-
applyAppShell?: (next: import("./app-shell.js").AppShell) => void;
|
|
71
63
|
}
|
|
72
64
|
|
|
73
65
|
/**
|
|
@@ -119,15 +111,6 @@ export type PartialUpdater = (
|
|
|
119
111
|
mode?: UpdateMode,
|
|
120
112
|
) => Promise<void>;
|
|
121
113
|
|
|
122
|
-
/**
|
|
123
|
-
* Create a partial updater for fetching and applying RSC partial updates
|
|
124
|
-
*
|
|
125
|
-
* This function is shared between navigation-bridge and server-action-bridge
|
|
126
|
-
* to handle partial RSC updates with HMR resilience.
|
|
127
|
-
*
|
|
128
|
-
* @param config - Partial update configuration
|
|
129
|
-
* @returns fetchPartialUpdate function
|
|
130
|
-
*/
|
|
131
114
|
export function createPartialUpdater(
|
|
132
115
|
config: PartialUpdateConfig,
|
|
133
116
|
): PartialUpdater {
|
|
@@ -137,24 +120,14 @@ export function createPartialUpdater(
|
|
|
137
120
|
onUpdate,
|
|
138
121
|
renderSegments,
|
|
139
122
|
getVersion = () => undefined,
|
|
140
|
-
applyAppShell,
|
|
141
123
|
} = config;
|
|
142
124
|
|
|
143
|
-
/**
|
|
144
|
-
* Get current page's cached segments as an array
|
|
145
|
-
*/
|
|
146
125
|
function getCurrentCachedSegments(): ResolvedSegment[] {
|
|
147
126
|
const currentKey = store.getHistoryKey();
|
|
148
127
|
const cached = store.getCachedSegments(currentKey);
|
|
149
128
|
return cached?.segments || [];
|
|
150
129
|
}
|
|
151
130
|
|
|
152
|
-
/**
|
|
153
|
-
* Fetch partial update and trigger UI update
|
|
154
|
-
*
|
|
155
|
-
* @param tx - Transaction for committing segment state (required)
|
|
156
|
-
* @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
|
|
157
|
-
*/
|
|
158
131
|
async function fetchPartialUpdate(
|
|
159
132
|
targetUrl: string,
|
|
160
133
|
segmentIds: string[] | undefined,
|
|
@@ -166,117 +139,114 @@ export function createPartialUpdater(
|
|
|
166
139
|
const segmentState = store.getSegmentState();
|
|
167
140
|
const url = targetUrl || window.location.href;
|
|
168
141
|
|
|
169
|
-
// Capture history key at start for stale revalidation consistency check
|
|
170
142
|
const historyKeyAtStart = store.getHistoryKey();
|
|
171
143
|
|
|
172
144
|
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
173
145
|
|
|
174
|
-
// When leaving intercept, filter out intercept-specific segments
|
|
175
146
|
let segments: string[];
|
|
176
147
|
if (mode.type === "leave-intercept") {
|
|
177
148
|
const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
|
|
178
149
|
const currentCached = getCurrentCachedSegments();
|
|
179
150
|
const interceptIds = new Set(
|
|
180
|
-
currentCached
|
|
181
|
-
.filter((s) => s.namespace?.startsWith("intercept:"))
|
|
182
|
-
.map((s) => s.id),
|
|
151
|
+
currentCached.filter(isInterceptSegment).map((s) => s.id),
|
|
183
152
|
);
|
|
184
153
|
segments = currentSegments.filter((id) => !interceptIds.has(id));
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
154
|
+
if (IS_BROWSER_DEBUG) {
|
|
155
|
+
debugLog(
|
|
156
|
+
`[Browser] Leaving intercept - filtered segments: ${segments.join(", ")}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
188
159
|
} else {
|
|
189
160
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
190
161
|
}
|
|
191
162
|
|
|
192
|
-
// For intercept revalidation, use the intercept source URL as previousUrl.
|
|
193
|
-
// For leave-intercept, tx.currentUrl captures window.location.href at tx
|
|
194
|
-
// creation, which on popstate is already the destination URL and would
|
|
195
|
-
// tell the server "from == to". segmentState.currentUrl still points at
|
|
196
|
-
// the URL the cached segments render (the intercept URL), which is the
|
|
197
|
-
// correct "from" for the server's diff computation.
|
|
198
163
|
const previousUrl =
|
|
199
164
|
mode.type === "leave-intercept"
|
|
200
165
|
? segmentState.currentUrl || tx.currentUrl
|
|
201
166
|
: interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
|
|
202
167
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
168
|
+
if (IS_BROWSER_DEBUG) {
|
|
169
|
+
debugLog(`\n[Browser] >>> NAVIGATION`);
|
|
170
|
+
debugLog(`[Browser] From: ${previousUrl}`);
|
|
171
|
+
debugLog(`[Browser] To: ${url}`);
|
|
172
|
+
debugLog(`[Browser] Segments to send: ${segments.join(", ")}`);
|
|
173
|
+
if (interceptSourceUrl) {
|
|
174
|
+
debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
|
|
175
|
+
}
|
|
209
176
|
}
|
|
210
177
|
|
|
211
|
-
// Get cached segments for merging with server diff.
|
|
212
|
-
// When navigating with targetCacheSegments, use those for consistency.
|
|
213
|
-
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
214
178
|
const targetCache =
|
|
215
179
|
mode.type === "navigate" && mode.targetCacheSegments?.length
|
|
216
180
|
? mode.targetCacheSegments
|
|
217
181
|
: undefined;
|
|
218
182
|
const cachedSegs = targetCache ?? getCurrentCachedSegments();
|
|
219
183
|
const cachedSegsSource = targetCache ? "history-cache" : "current-page";
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
184
|
+
if (IS_BROWSER_DEBUG) {
|
|
185
|
+
debugLog(
|
|
186
|
+
`[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
223
189
|
|
|
224
|
-
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
225
190
|
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
226
191
|
fetchResult = await client.fetchPartial({
|
|
227
192
|
targetUrl: url,
|
|
228
193
|
segmentIds: segments,
|
|
229
194
|
previousUrl,
|
|
230
|
-
// Mark stale when explicitly requested OR when no segments are sent
|
|
231
|
-
// (action redirect sends empty segments for a fresh render).
|
|
232
195
|
staleRevalidation:
|
|
233
196
|
mode.type === "stale-revalidation" || segments.length === 0,
|
|
234
197
|
version: getVersion(),
|
|
235
198
|
routerId: store.getRouterId?.(),
|
|
236
199
|
});
|
|
237
|
-
// Mark navigation as streaming (response received, now parsing RSC).
|
|
238
|
-
// Called after fetchPartial so pendingUrl stays set during the network wait,
|
|
239
|
-
// allowing useLinkStatus to show per-link pending indicators.
|
|
240
200
|
const streamingToken = tx.startStreaming();
|
|
241
|
-
const {
|
|
201
|
+
const {
|
|
202
|
+
payload,
|
|
203
|
+
streamComplete: rawStreamComplete,
|
|
204
|
+
fullyPrefetched,
|
|
205
|
+
} = fetchResult;
|
|
242
206
|
debugLog("payload.metadata", payload.metadata);
|
|
243
207
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
`[Browser] App switch detected (${prevRouterId} → ${payload.metadata.routerId}), forcing full update`,
|
|
262
|
-
);
|
|
263
|
-
payload.metadata.isPartial = false;
|
|
264
|
-
applyAppShell?.({
|
|
265
|
-
routerId: payload.metadata.routerId,
|
|
266
|
-
rootLayout: payload.metadata.rootLayout,
|
|
267
|
-
basename: payload.metadata.basename,
|
|
268
|
-
version: payload.metadata.version,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
store.setRouterId?.(payload.metadata.routerId);
|
|
208
|
+
// Side effect only: end the streaming token once the stream settles.
|
|
209
|
+
// The wrapped promise was never read as a value; only the .end() matters.
|
|
210
|
+
// The .catch keeps an unhandled rejection from leaking if the stream errors.
|
|
211
|
+
rawStreamComplete.then(() => streamingToken.end()).catch(() => {});
|
|
212
|
+
|
|
213
|
+
const currentRouterId = store.getRouterId?.();
|
|
214
|
+
if (
|
|
215
|
+
payload.metadata?.routerId &&
|
|
216
|
+
currentRouterId &&
|
|
217
|
+
payload.metadata.routerId !== currentRouterId
|
|
218
|
+
) {
|
|
219
|
+
console.error(
|
|
220
|
+
`[rango] Partial response router id "${payload.metadata.routerId}" does not ` +
|
|
221
|
+
`match this client ("${currentRouterId}"); discarding it and reloading to re-sync.`,
|
|
222
|
+
);
|
|
223
|
+
window.location.href = url;
|
|
224
|
+
return;
|
|
272
225
|
}
|
|
273
226
|
|
|
274
|
-
// Handle server-side redirect with state
|
|
275
227
|
if (payload.metadata?.redirect) {
|
|
276
228
|
if (signal?.aborted) {
|
|
277
229
|
debugLog("[Browser] Ignoring stale redirect (aborted)");
|
|
278
230
|
return;
|
|
279
231
|
}
|
|
232
|
+
// Explicit off-host redirect (redirect(url, { external: true })):
|
|
233
|
+
// hard-navigate, but still scheme-validate (http/https only). external
|
|
234
|
+
// waives the same-origin check the app opted out of, NOT scheme safety, so
|
|
235
|
+
// a forged payload carrying a javascript:/data: URL cannot script via
|
|
236
|
+
// location.assign.
|
|
237
|
+
if (payload.metadata.redirect.external) {
|
|
238
|
+
const externalUrl = validateExternalRedirect(
|
|
239
|
+
payload.metadata.redirect.url,
|
|
240
|
+
window.location.origin,
|
|
241
|
+
);
|
|
242
|
+
if (!externalUrl) {
|
|
243
|
+
debugLog("[Browser] Ignoring blocked external redirect payload");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
debugLog("[Browser] External redirect (hard navigation)");
|
|
247
|
+
window.location.assign(externalUrl);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
280
250
|
const redirectUrl = validateRedirectOrigin(
|
|
281
251
|
payload.metadata.redirect.url,
|
|
282
252
|
window.location.origin,
|
|
@@ -298,10 +268,11 @@ export function createPartialUpdater(
|
|
|
298
268
|
return;
|
|
299
269
|
}
|
|
300
270
|
|
|
301
|
-
|
|
302
|
-
|
|
271
|
+
if (IS_BROWSER_DEBUG) {
|
|
272
|
+
debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
|
|
273
|
+
debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
|
|
274
|
+
}
|
|
303
275
|
|
|
304
|
-
// If diff is empty, nothing changed on server side.
|
|
305
276
|
if (!diff || diff.length === 0) {
|
|
306
277
|
const matchedIds = matched || [];
|
|
307
278
|
const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
|
|
@@ -309,7 +280,6 @@ export function createPartialUpdater(
|
|
|
309
280
|
.map((id: string) => cacheMap.get(id))
|
|
310
281
|
.filter(Boolean) as ResolvedSegment[];
|
|
311
282
|
|
|
312
|
-
// When navigating with cached segments to a different route, render them.
|
|
313
283
|
if (mode.type === "navigate" && targetCache) {
|
|
314
284
|
debugLog(
|
|
315
285
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
@@ -324,10 +294,6 @@ export function createPartialUpdater(
|
|
|
324
294
|
existingSegments,
|
|
325
295
|
);
|
|
326
296
|
|
|
327
|
-
// tx.commit() cached the source page's handleData because
|
|
328
|
-
// eventController hasn't been updated yet. Overwrite with the
|
|
329
|
-
// correct cached handleData to prevent cache corruption on
|
|
330
|
-
// subsequent navigations to this same URL.
|
|
331
297
|
if (mode.targetCacheHandleData) {
|
|
332
298
|
store.updateCacheHandleData(
|
|
333
299
|
store.getHistoryKey(),
|
|
@@ -335,10 +301,6 @@ export function createPartialUpdater(
|
|
|
335
301
|
);
|
|
336
302
|
}
|
|
337
303
|
|
|
338
|
-
// Include cachedHandleData in metadata so NavigationProvider can restore
|
|
339
|
-
// breadcrumbs and other handle data from cache.
|
|
340
|
-
// Remove `handles` from metadata to prevent NavigationProvider from
|
|
341
|
-
// processing an empty handles stream, which would clear the cached breadcrumbs.
|
|
342
304
|
const { handles: _unusedHandles, ...metadataWithoutHandles } =
|
|
343
305
|
payload.metadata!;
|
|
344
306
|
const cachedUpdate = {
|
|
@@ -365,7 +327,6 @@ export function createPartialUpdater(
|
|
|
365
327
|
return;
|
|
366
328
|
}
|
|
367
329
|
|
|
368
|
-
// When leaving intercept, force re-render even with empty diff
|
|
369
330
|
if (mode.type === "leave-intercept") {
|
|
370
331
|
debugLog(
|
|
371
332
|
"[Browser] Leaving intercept - forcing re-render to remove modal",
|
|
@@ -390,7 +351,6 @@ export function createPartialUpdater(
|
|
|
390
351
|
return;
|
|
391
352
|
}
|
|
392
353
|
|
|
393
|
-
// Same route revalidation with no changes - skip UI update
|
|
394
354
|
debugLog(
|
|
395
355
|
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
396
356
|
);
|
|
@@ -399,7 +359,6 @@ export function createPartialUpdater(
|
|
|
399
359
|
return;
|
|
400
360
|
}
|
|
401
361
|
|
|
402
|
-
// Reconcile server segments with cached segments (single source of truth)
|
|
403
362
|
const matchedIds = matched || [];
|
|
404
363
|
const actor: ReconcileActor =
|
|
405
364
|
mode.type === "stale-revalidation" || mode.type === "action"
|
|
@@ -415,7 +374,6 @@ export function createPartialUpdater(
|
|
|
415
374
|
insertMissingDiff: true,
|
|
416
375
|
});
|
|
417
376
|
|
|
418
|
-
// HMR RESILIENCE: Check if we're missing any matched segments
|
|
419
377
|
const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
|
|
420
378
|
const missingIds = matchedIds.filter(
|
|
421
379
|
(id: string) => !reconciledIdSet.has(id),
|
|
@@ -437,13 +395,19 @@ export function createPartialUpdater(
|
|
|
437
395
|
return;
|
|
438
396
|
}
|
|
439
397
|
if (mode.type === "action") {
|
|
398
|
+
// An action refetch that lands on missing segments (navigated away /
|
|
399
|
+
// consolidation / HMR) drops rather than refetch-all: the action flow
|
|
400
|
+
// is storeOnly / skipLoadingState, so a full refetch here would fight
|
|
401
|
+
// it. Keep the stale-but-consistent tree; log so the drop is visible.
|
|
402
|
+
debugLog(
|
|
403
|
+
`[Browser] Action refetch: ${missingCount} segments missing; dropping (stale-but-consistent tree kept).`,
|
|
404
|
+
);
|
|
440
405
|
return;
|
|
441
406
|
}
|
|
442
407
|
console.warn(
|
|
443
408
|
`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
|
|
444
409
|
);
|
|
445
410
|
|
|
446
|
-
// Refetch with empty segments = server sends everything
|
|
447
411
|
return fetchPartialUpdate(url, [], true, signal, tx, mode);
|
|
448
412
|
}
|
|
449
413
|
|
|
@@ -452,44 +416,59 @@ export function createPartialUpdater(
|
|
|
452
416
|
return;
|
|
453
417
|
}
|
|
454
418
|
|
|
455
|
-
// Rebuild tree on client (await for loader data resolution)
|
|
456
419
|
const renderOptions = {
|
|
457
420
|
isAction: mode.type === "action",
|
|
458
|
-
forceAwait
|
|
421
|
+
// forceAwait unwraps the ROUTER loader promises during render so they
|
|
422
|
+
// land without a loading()/fallback frame. A fully-prefetched nav has
|
|
423
|
+
// its router data already resolved (the prefetch stream drained), so
|
|
424
|
+
// awaiting it here is free and lets us commit NORMALLY (not in a
|
|
425
|
+
// transition) below — a normal commit still shows fallbacks for any
|
|
426
|
+
// CLIENT component that suspends on mount, which a transition would
|
|
427
|
+
// wrongly suppress by holding the old UI until that suspense settles.
|
|
428
|
+
forceAwait: mode.type === "stale-revalidation" || fullyPrefetched,
|
|
459
429
|
interceptSegments:
|
|
460
430
|
reconciled.interceptSegments.length > 0
|
|
461
431
|
? reconciled.interceptSegments
|
|
462
432
|
: undefined,
|
|
463
433
|
};
|
|
464
|
-
|
|
465
|
-
|
|
434
|
+
let newTree: Awaited<ReturnType<typeof renderSegments>>;
|
|
435
|
+
if (signal) {
|
|
436
|
+
// Race render against abort. Store the abort handler and register it
|
|
437
|
+
// { once:true } so a non-aborted render (which wins the race) can
|
|
438
|
+
// remove it in finally — otherwise the listener stays attached and the
|
|
439
|
+
// rejecting promise never settles. Mirrors teeWithCompletion in
|
|
440
|
+
// browser/response-adapter.ts.
|
|
441
|
+
let onAbort: (() => void) | undefined;
|
|
442
|
+
const abortPromise = new Promise<never>((_, reject) => {
|
|
443
|
+
if (signal.aborted) {
|
|
444
|
+
reject(new DOMException("Navigation aborted", "AbortError"));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
onAbort = () =>
|
|
448
|
+
reject(new DOMException("Navigation aborted", "AbortError"));
|
|
449
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
450
|
+
});
|
|
451
|
+
try {
|
|
452
|
+
newTree = await Promise.race([
|
|
466
453
|
renderSegments(reconciled.mainSegments, renderOptions),
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
: renderSegments(reconciled.mainSegments, renderOptions));
|
|
477
|
-
|
|
478
|
-
// Final abort check before committing - another navigation may have started
|
|
454
|
+
abortPromise,
|
|
455
|
+
]);
|
|
456
|
+
} finally {
|
|
457
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
newTree = await renderSegments(reconciled.mainSegments, renderOptions);
|
|
461
|
+
}
|
|
462
|
+
|
|
479
463
|
if (signal?.aborted) {
|
|
480
464
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
481
465
|
return;
|
|
482
466
|
}
|
|
483
467
|
|
|
484
|
-
// Check if this is an intercept response (any slot is active)
|
|
485
468
|
const isInterceptResponse = hasActiveInterceptSlots(
|
|
486
469
|
payload.metadata?.slots,
|
|
487
470
|
);
|
|
488
471
|
|
|
489
|
-
// Track intercept context (only on navigation, not actions or stale revalidation)
|
|
490
|
-
// Use the authoritative source from mode/history state when restoring an
|
|
491
|
-
// intercept via popstate cache miss; fall back to the current URL for fresh
|
|
492
|
-
// intercept navigations.
|
|
493
472
|
const effectiveInterceptSource =
|
|
494
473
|
interceptSourceUrl || segmentState.currentUrl;
|
|
495
474
|
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
@@ -500,9 +479,6 @@ export function createPartialUpdater(
|
|
|
500
479
|
}
|
|
501
480
|
}
|
|
502
481
|
|
|
503
|
-
// Commit navigation - use server's matched as the authoritative segment ID list.
|
|
504
|
-
// reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
|
|
505
|
-
// but the server's matched always includes all expected segment IDs.
|
|
506
482
|
const allSegmentIds = matchedIds;
|
|
507
483
|
const serverLocationState = payload.metadata?.locationState;
|
|
508
484
|
const overrides: CommitOverrides | undefined = isInterceptResponse
|
|
@@ -521,7 +497,6 @@ export function createPartialUpdater(
|
|
|
521
497
|
overrides,
|
|
522
498
|
);
|
|
523
499
|
|
|
524
|
-
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
525
500
|
if (mode.type === "stale-revalidation") {
|
|
526
501
|
const historyKeyNow = store.getHistoryKey();
|
|
527
502
|
if (historyKeyNow !== historyKeyAtStart) {
|
|
@@ -534,9 +509,22 @@ export function createPartialUpdater(
|
|
|
534
509
|
|
|
535
510
|
debugLog("[partial-update] updating document");
|
|
536
511
|
|
|
537
|
-
// Emit update to trigger React render.
|
|
538
|
-
// Scroll info is included so NavigationProvider applies it after React commits.
|
|
539
512
|
const hasTransition = shouldStartViewTransition(reconciled.segments);
|
|
513
|
+
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. Reports which reconciled
|
|
514
|
+
// segment still carries a transition after the server-side when-gate, and
|
|
515
|
+
// whether the commit will be held in a startTransition. If `withTransition`
|
|
516
|
+
// lists an ancestor (layout/root) id rather than the gated leaf, an ungated
|
|
517
|
+
// ancestor transition is holding the subtree (missing loading() fallback).
|
|
518
|
+
if (isBrowserDebugEnabled()) {
|
|
519
|
+
debugLog("[VT-DIAG] commit", {
|
|
520
|
+
mode: mode.type,
|
|
521
|
+
hasTransition,
|
|
522
|
+
withTransition: reconciled.segments
|
|
523
|
+
.filter((s) => s.transition)
|
|
524
|
+
.map((s) => s.id),
|
|
525
|
+
all: reconciled.segments.map((s) => s.id),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
540
528
|
const scrollPayload = toScrollPayload(navScroll);
|
|
541
529
|
|
|
542
530
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
@@ -562,6 +550,20 @@ export function createPartialUpdater(
|
|
|
562
550
|
});
|
|
563
551
|
});
|
|
564
552
|
} else {
|
|
553
|
+
// Normal commit (cold/partial nav AND fully-prefetched nav). For a
|
|
554
|
+
// fully-prefetched nav, renderOptions.forceAwait (above) unwrapped the
|
|
555
|
+
// already-resolved ROUTER loader data AND route content during render, so
|
|
556
|
+
// the new tree carries it inline with no loading()/fallback frame — yet we
|
|
557
|
+
// still commit NORMALLY here rather than in a transition. A transition
|
|
558
|
+
// holds the OLD UI until ALL suspense in the new tree settles, including a
|
|
559
|
+
// CLIENT component that starts its own data request only when mounted
|
|
560
|
+
// (post-commit) under a persistent boundary; that would retain the
|
|
561
|
+
// previous page indefinitely with no feedback. A normal commit lets such
|
|
562
|
+
// client-initiated suspense reveal a fallback (correct) while the router
|
|
563
|
+
// data — genuinely ready — never flashes. Cold/partial navs
|
|
564
|
+
// (fullyPrefetched=false) do not forceAwait, so they stream their
|
|
565
|
+
// fallbacks. Explicit transition() routes keep the broader content-hold
|
|
566
|
+
// via the hasTransition branch above (the documented opt-in).
|
|
565
567
|
onUpdate({
|
|
566
568
|
root: newTree,
|
|
567
569
|
metadata: payload.metadata!,
|
|
@@ -572,7 +574,6 @@ export function createPartialUpdater(
|
|
|
572
574
|
debugLog("[Browser] Navigation complete");
|
|
573
575
|
return;
|
|
574
576
|
} else {
|
|
575
|
-
// Full update (fallback)
|
|
576
577
|
console.warn(`[Browser] Full update (fallback)`);
|
|
577
578
|
|
|
578
579
|
const segments = payload.metadata?.segments || [];
|
|
@@ -603,6 +604,18 @@ export function createPartialUpdater(
|
|
|
603
604
|
|
|
604
605
|
if (mode.type === "stale-revalidation") {
|
|
605
606
|
await rawStreamComplete;
|
|
607
|
+
// Mirror the partial branch's history-key staleness guard (above): the
|
|
608
|
+
// await above is a real async suspension, so the user may have navigated
|
|
609
|
+
// away while this background revalidation was draining. Dropping a late
|
|
610
|
+
// full-update here prevents it from clobbering the freshly committed UI
|
|
611
|
+
// of the page the user moved to.
|
|
612
|
+
const historyKeyNow = store.getHistoryKey();
|
|
613
|
+
if (historyKeyNow !== historyKeyAtStart) {
|
|
614
|
+
debugLog(
|
|
615
|
+
`[Browser] Stale revalidation (full update): history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
|
|
616
|
+
);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
606
619
|
startTransition(() => {
|
|
607
620
|
if (fullHasTransition && addTransitionType) {
|
|
608
621
|
addTransitionType("action");
|
|
@@ -614,7 +627,7 @@ export function createPartialUpdater(
|
|
|
614
627
|
});
|
|
615
628
|
});
|
|
616
629
|
} else if (mode.type === "action") {
|
|
617
|
-
startTransition(
|
|
630
|
+
startTransition(() => {
|
|
618
631
|
if (fullHasTransition && addTransitionType) {
|
|
619
632
|
addTransitionType("action");
|
|
620
633
|
}
|