@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +12 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +441 -134
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3453 -1240
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +76 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +318 -15
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +279 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +153 -109
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +101 -5
- package/skills/router-setup/SKILL.md +116 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +332 -29
- package/skills/use-cache/SKILL.md +46 -14
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +197 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +111 -31
- package/src/browser/navigation-client.ts +201 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +230 -35
- package/src/browser/prefetch/fetch.ts +338 -39
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +111 -16
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +80 -5
- package/src/browser/rsc-router.tsx +218 -76
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -61
- package/src/browser/types.ts +91 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +272 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +214 -48
- package/src/cache/cache-scope.ts +236 -89
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2224 -171
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +119 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +56 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +93 -12
- package/src/index.ts +133 -15
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +469 -276
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +270 -172
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +264 -77
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +474 -385
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +8 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +163 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +163 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +658 -511
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +77 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +105 -72
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +263 -112
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +32 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +26 -46
- package/src/server/request-context.ts +398 -172
- package/src/ssr/index.tsx +25 -16
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +343 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +57 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +155 -65
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +238 -295
- package/src/vite/router-discovery.ts +940 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -4,13 +4,16 @@ import type {
|
|
|
4
4
|
NavigateOptionsInternal,
|
|
5
5
|
ResolvedSegment,
|
|
6
6
|
} from "./types.js";
|
|
7
|
+
import { setAppVersion } from "./app-version.js";
|
|
8
|
+
import { isActionFenceActive } from "./action-fence.js";
|
|
9
|
+
import { getRangoState } from "./rango-state.js";
|
|
7
10
|
import * as React from "react";
|
|
8
11
|
import { startTransition } from "react";
|
|
9
12
|
import {
|
|
10
13
|
createNavigationTransaction,
|
|
11
14
|
resolveNavigationState,
|
|
12
15
|
} from "./navigation-transaction.js";
|
|
13
|
-
import { buildHistoryState } from "./history-state.js";
|
|
16
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
14
17
|
import {
|
|
15
18
|
handleNavigationStart,
|
|
16
19
|
handleNavigationEnd,
|
|
@@ -29,6 +32,7 @@ import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
|
29
32
|
import {
|
|
30
33
|
toNetworkError,
|
|
31
34
|
emitNetworkError,
|
|
35
|
+
emitNavigationError,
|
|
32
36
|
isBackgroundSuppressible,
|
|
33
37
|
} from "./network-error-handler.js";
|
|
34
38
|
import { debugLog } from "./logging.js";
|
|
@@ -40,11 +44,6 @@ if (typeof Symbol.dispose === "undefined") {
|
|
|
40
44
|
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
/** Get IDs of non-loader segments (layouts, routes, parallels). */
|
|
44
|
-
function getNonLoaderSegmentIds(segments: ResolvedSegment[]): string[] {
|
|
45
|
-
return segments.filter((s) => s.type !== "loader").map((s) => s.id);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
47
|
export { createNavigationTransaction };
|
|
49
48
|
|
|
50
49
|
/**
|
|
@@ -52,7 +51,7 @@ export { createNavigationTransaction };
|
|
|
52
51
|
*/
|
|
53
52
|
export interface NavigationBridgeConfigWithController extends NavigationBridgeConfig {
|
|
54
53
|
eventController: EventController;
|
|
55
|
-
/** RSC version from initial payload metadata */
|
|
54
|
+
/** RSC version from initial payload metadata. */
|
|
56
55
|
version?: string;
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -72,8 +71,8 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
|
|
|
72
71
|
export function createNavigationBridge(
|
|
73
72
|
config: NavigationBridgeConfigWithController,
|
|
74
73
|
): NavigationBridge {
|
|
75
|
-
const { store, client, eventController, onUpdate, renderSegments
|
|
76
|
-
|
|
74
|
+
const { store, client, eventController, onUpdate, renderSegments } = config;
|
|
75
|
+
let version = config.version;
|
|
77
76
|
|
|
78
77
|
// Create shared partial updater
|
|
79
78
|
const fetchPartialUpdate = createPartialUpdater({
|
|
@@ -81,7 +80,7 @@ export function createNavigationBridge(
|
|
|
81
80
|
client,
|
|
82
81
|
onUpdate,
|
|
83
82
|
renderSegments,
|
|
84
|
-
version,
|
|
83
|
+
getVersion: () => version,
|
|
85
84
|
});
|
|
86
85
|
|
|
87
86
|
return {
|
|
@@ -163,11 +162,7 @@ export function createNavigationBridge(
|
|
|
163
162
|
},
|
|
164
163
|
{},
|
|
165
164
|
);
|
|
166
|
-
|
|
167
|
-
window.history.replaceState(historyState, "", url);
|
|
168
|
-
} else {
|
|
169
|
-
window.history.pushState(historyState, "", url);
|
|
170
|
-
}
|
|
165
|
+
pushHistoryWithIdx(historyState, url, options?.replace ?? false);
|
|
171
166
|
|
|
172
167
|
// Ensure new history entry has a scroll restoration key
|
|
173
168
|
ensureHistoryKey();
|
|
@@ -265,18 +260,24 @@ export function createNavigationBridge(
|
|
|
265
260
|
// 2. routes that CAN be intercepted - we don't know if this navigation will intercept
|
|
266
261
|
// 3. when leaving intercept - we need fresh non-intercept segments from server
|
|
267
262
|
// 4. redirect-with-state - force re-render so hooks read fresh state
|
|
263
|
+
// 5. stale cache - server action invalidated it, need fresh data with loading state
|
|
268
264
|
const hasUsableCache =
|
|
269
265
|
cachedSegments &&
|
|
270
266
|
cachedSegments.length > 0 &&
|
|
271
267
|
!isInterceptOnlyCache(cachedSegments) &&
|
|
272
268
|
!hasInterceptCache &&
|
|
273
269
|
!isLeavingIntercept &&
|
|
270
|
+
!cached?.stale &&
|
|
274
271
|
!options?._skipCache;
|
|
275
272
|
|
|
273
|
+
// Forward navigations always await fetchPartialUpdate before rendering,
|
|
274
|
+
// so useNavigation should always report "loading". skipLoadingState is
|
|
275
|
+
// only used for popstate background revalidation (line ~526) where
|
|
276
|
+
// cached content renders instantly without a network wait.
|
|
276
277
|
const tx = createNavigationTransaction(store, eventController, url, {
|
|
277
278
|
...options,
|
|
278
279
|
state: resolvedState,
|
|
279
|
-
skipLoadingState:
|
|
280
|
+
skipLoadingState: false,
|
|
280
281
|
});
|
|
281
282
|
|
|
282
283
|
// REVALIDATE: Fetch fresh data from server
|
|
@@ -284,7 +285,7 @@ export function createNavigationBridge(
|
|
|
284
285
|
await fetchPartialUpdate(
|
|
285
286
|
url,
|
|
286
287
|
hasUsableCache
|
|
287
|
-
?
|
|
288
|
+
? cachedSegments!.map((s) => s.id)
|
|
288
289
|
: options?._skipCache
|
|
289
290
|
? [] // Action redirect: send no segments so server renders everything fresh
|
|
290
291
|
: undefined,
|
|
@@ -325,8 +326,15 @@ export function createNavigationBridge(
|
|
|
325
326
|
} as NavigateOptionsInternal);
|
|
326
327
|
}
|
|
327
328
|
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
// Aborted, or superseded by a newer navigation. A superseded nav may
|
|
330
|
+
// reject with a non-AbortError (e.g. a Flight decode that fails after its
|
|
331
|
+
// signal was aborted), so check the signal too -- otherwise we would
|
|
332
|
+
// render a boundary that clobbers the newer navigation's content.
|
|
333
|
+
if (
|
|
334
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
335
|
+
tx.handle.signal.aborted
|
|
336
|
+
) {
|
|
337
|
+
debugLog("[Browser] Navigation aborted or superseded");
|
|
330
338
|
return;
|
|
331
339
|
}
|
|
332
340
|
|
|
@@ -343,7 +351,13 @@ export function createNavigationBridge(
|
|
|
343
351
|
return;
|
|
344
352
|
}
|
|
345
353
|
|
|
346
|
-
|
|
354
|
+
// A response we could not process (undecodable Flight body, or an
|
|
355
|
+
// unanticipated failure building the response). Surface the route's
|
|
356
|
+
// error boundary rather than let the rejection abort the navigation
|
|
357
|
+
// silently. Prefetched responses funnel here too: a failed warm-prefetch
|
|
358
|
+
// payload rejects on consumption and propagates to this catch.
|
|
359
|
+
console.error("[Browser] Unprocessable navigation response:", error);
|
|
360
|
+
emitNavigationError(onUpdate, error, url);
|
|
347
361
|
} finally {
|
|
348
362
|
tx[Symbol.dispose]();
|
|
349
363
|
}
|
|
@@ -372,6 +386,14 @@ export function createNavigationBridge(
|
|
|
372
386
|
tx.with({ url: window.location.href, replace: true, scroll: false }),
|
|
373
387
|
);
|
|
374
388
|
} catch (error) {
|
|
389
|
+
// Aborted or superseded: bail without rendering a boundary (see navigate()).
|
|
390
|
+
if (
|
|
391
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
392
|
+
tx.handle.signal.aborted
|
|
393
|
+
) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
375
397
|
const networkError = toNetworkError(error, {
|
|
376
398
|
url: window.location.href,
|
|
377
399
|
operation: "revalidation",
|
|
@@ -384,7 +406,12 @@ export function createNavigationBridge(
|
|
|
384
406
|
emitNetworkError(onUpdate, networkError, window.location.href);
|
|
385
407
|
return;
|
|
386
408
|
}
|
|
387
|
-
|
|
409
|
+
|
|
410
|
+
// refresh() shares the fetchPartialUpdate chokepoint with navigate()/
|
|
411
|
+
// popstate, so an unprocessable response must surface the error boundary
|
|
412
|
+
// here too rather than become an uncaught rejection.
|
|
413
|
+
console.error("[Browser] Unprocessable refresh response:", error);
|
|
414
|
+
emitNavigationError(onUpdate, error, window.location.href);
|
|
388
415
|
} finally {
|
|
389
416
|
tx[Symbol.dispose]();
|
|
390
417
|
}
|
|
@@ -416,6 +443,15 @@ export function createNavigationBridge(
|
|
|
416
443
|
eventController.abortAllActions();
|
|
417
444
|
}
|
|
418
445
|
|
|
446
|
+
// Popstate that exits an intercept to a non-intercept destination. The
|
|
447
|
+
// fallback fetch path below needs `leave-intercept` mode so it filters
|
|
448
|
+
// the cached @modal segment from the request and forces a re-render —
|
|
449
|
+
// otherwise a cache-miss popstate whose server response has an empty
|
|
450
|
+
// diff hits the "no changes" branch in partial-update and the modal
|
|
451
|
+
// stays on screen.
|
|
452
|
+
const isLeavingIntercept =
|
|
453
|
+
!isIntercept && currentInterceptSource !== null;
|
|
454
|
+
|
|
419
455
|
// Compute history key from URL (with intercept suffix if applicable)
|
|
420
456
|
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
421
457
|
|
|
@@ -439,11 +475,22 @@ export function createNavigationBridge(
|
|
|
439
475
|
// Helper to check if streaming is in progress
|
|
440
476
|
const isStreaming = () => eventController.getState().isStreaming;
|
|
441
477
|
|
|
478
|
+
// Surface any external rotation of the rango state cookie (a server
|
|
479
|
+
// Set-Cookie, a sibling tab, a cookie clear) BEFORE reading the stale bit.
|
|
480
|
+
// The divergence observer only runs inside getRangoState() — fetch-time —
|
|
481
|
+
// so a popstate-first interaction would otherwise serve a pre-mutation
|
|
482
|
+
// page as fresh and never fetch to trigger the observer. Reading here lets
|
|
483
|
+
// the observer mark the history cache stale so getCachedSegments sees it.
|
|
484
|
+
getRangoState();
|
|
485
|
+
|
|
442
486
|
// Check if we can restore from history cache
|
|
443
487
|
const cached = store.getCachedSegments(historyKey);
|
|
444
488
|
const cachedSegments = cached?.segments;
|
|
445
489
|
const cachedHandleData = cached?.handleData;
|
|
446
|
-
|
|
490
|
+
// While an action is in flight the fence persists no stale flag, so OR it
|
|
491
|
+
// in here: a popstate during the flight serves the cached entry AND
|
|
492
|
+
// revalidates (SWR) instead of serving it as fresh.
|
|
493
|
+
const isStale = (cached?.stale ?? false) || isActionFenceActive();
|
|
447
494
|
|
|
448
495
|
if (cachedSegments && cachedSegments.length > 0) {
|
|
449
496
|
// Update store to point to this history entry
|
|
@@ -452,6 +499,12 @@ export function createNavigationBridge(
|
|
|
452
499
|
store.setCurrentUrl(url);
|
|
453
500
|
store.setPath(new URL(url).pathname);
|
|
454
501
|
|
|
502
|
+
// Restore router identity from cache so subsequent navigations
|
|
503
|
+
// don't falsely detect an app switch.
|
|
504
|
+
if (cached?.routerId) {
|
|
505
|
+
store.setRouterId?.(cached.routerId);
|
|
506
|
+
}
|
|
507
|
+
|
|
455
508
|
// Render from cache - force await to skip loading fallbacks
|
|
456
509
|
try {
|
|
457
510
|
const root = await renderSegments(cachedSegments, {
|
|
@@ -477,8 +530,16 @@ export function createNavigationBridge(
|
|
|
477
530
|
cachedHandleData,
|
|
478
531
|
params: cachedParams,
|
|
479
532
|
},
|
|
533
|
+
scroll: { restore: true, isStreaming },
|
|
480
534
|
};
|
|
481
|
-
|
|
535
|
+
// Intercept-driven popstate (entering OR leaving an intercept) only
|
|
536
|
+
// mutates the parallel slot; the main outlet shows the same content.
|
|
537
|
+
// Skip startViewTransition in those cases — same rationale as the
|
|
538
|
+
// intercept guard in partial-update.ts's hasTransition computation.
|
|
539
|
+
const hasTransition =
|
|
540
|
+
!isIntercept &&
|
|
541
|
+
!isLeavingIntercept &&
|
|
542
|
+
cachedSegments.some((s) => s.transition);
|
|
482
543
|
if (hasTransition) {
|
|
483
544
|
startTransition(() => {
|
|
484
545
|
if (addTransitionType) {
|
|
@@ -490,14 +551,11 @@ export function createNavigationBridge(
|
|
|
490
551
|
onUpdate(popstateUpdate);
|
|
491
552
|
}
|
|
492
553
|
|
|
493
|
-
// Restore scroll position for back/forward navigation
|
|
494
|
-
handleNavigationEnd({ restore: true, isStreaming });
|
|
495
|
-
|
|
496
554
|
// SWR: If stale, trigger background revalidation
|
|
497
555
|
if (isStale) {
|
|
498
556
|
debugLog("[Browser] Cache is stale, background revalidating...");
|
|
499
557
|
// Background revalidation - don't await, just fire and forget
|
|
500
|
-
const segmentIds =
|
|
558
|
+
const segmentIds = cachedSegments.map((s) => s.id);
|
|
501
559
|
|
|
502
560
|
const tx = createNavigationTransaction(
|
|
503
561
|
store,
|
|
@@ -562,13 +620,22 @@ export function createNavigationBridge(
|
|
|
562
620
|
intercept: isIntercept,
|
|
563
621
|
interceptSourceUrl,
|
|
564
622
|
}),
|
|
565
|
-
isIntercept
|
|
623
|
+
isIntercept
|
|
624
|
+
? { type: "navigate", interceptSourceUrl }
|
|
625
|
+
: isLeavingIntercept
|
|
626
|
+
? { type: "leave-intercept" }
|
|
627
|
+
: undefined,
|
|
566
628
|
);
|
|
567
629
|
// Restore scroll position after fetch completes
|
|
568
630
|
handleNavigationEnd({ restore: true, isStreaming });
|
|
569
631
|
} catch (error) {
|
|
570
|
-
|
|
571
|
-
|
|
632
|
+
// Aborted or superseded by a newer navigation: bail without clobbering
|
|
633
|
+
// content with a boundary (see navigate()).
|
|
634
|
+
if (
|
|
635
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
636
|
+
tx.handle.signal.aborted
|
|
637
|
+
) {
|
|
638
|
+
debugLog("[Browser] Popstate navigation aborted or superseded");
|
|
572
639
|
return;
|
|
573
640
|
}
|
|
574
641
|
|
|
@@ -585,7 +652,10 @@ export function createNavigationBridge(
|
|
|
585
652
|
return;
|
|
586
653
|
}
|
|
587
654
|
|
|
588
|
-
|
|
655
|
+
// Unprocessable response on a back/forward navigation: surface the
|
|
656
|
+
// error boundary instead of an uncaught rejection (see navigate()).
|
|
657
|
+
console.error("[Browser] Unprocessable popstate response:", error);
|
|
658
|
+
emitNavigationError(onUpdate, error, url);
|
|
589
659
|
} finally {
|
|
590
660
|
tx[Symbol.dispose]();
|
|
591
661
|
}
|
|
@@ -639,6 +709,16 @@ export function createNavigationBridge(
|
|
|
639
709
|
window.removeEventListener("pageshow", handlePageShow);
|
|
640
710
|
};
|
|
641
711
|
},
|
|
712
|
+
|
|
713
|
+
getVersion(): string | undefined {
|
|
714
|
+
return version;
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
updateVersion(newVersion: string): void {
|
|
718
|
+
version = newVersion;
|
|
719
|
+
setAppVersion(newVersion);
|
|
720
|
+
store.clearHistoryCache();
|
|
721
|
+
},
|
|
642
722
|
};
|
|
643
723
|
}
|
|
644
724
|
|
|
@@ -12,12 +12,21 @@ import {
|
|
|
12
12
|
startBrowserTransaction,
|
|
13
13
|
} from "./logging.js";
|
|
14
14
|
import { getRangoState } from "./rango-state.js";
|
|
15
|
+
import { isActionFenceActive } from "./action-fence.js";
|
|
15
16
|
import {
|
|
16
17
|
extractRscHeaderUrl,
|
|
17
18
|
emptyResponse,
|
|
19
|
+
handleReloadHeader,
|
|
18
20
|
teeWithCompletion,
|
|
21
|
+
isForeignRouterId,
|
|
19
22
|
} from "./response-adapter.js";
|
|
20
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
buildPrefetchKey,
|
|
25
|
+
buildSourceKey,
|
|
26
|
+
consumeInflightPrefetch,
|
|
27
|
+
consumePrefetch,
|
|
28
|
+
type DecodedPrefetch,
|
|
29
|
+
} from "./prefetch/cache.js";
|
|
21
30
|
|
|
22
31
|
/**
|
|
23
32
|
* Create a navigation client for fetching RSC payloads
|
|
@@ -26,8 +35,10 @@ import { buildPrefetchKey, consumePrefetch } from "./prefetch/cache.js";
|
|
|
26
35
|
* deserializing the response using the RSC runtime.
|
|
27
36
|
*
|
|
28
37
|
* Checks the in-memory prefetch cache before making a network request.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
38
|
+
* Tries the source-scoped key first (populated when the server tagged
|
|
39
|
+
* the response as source-sensitive via `X-RSC-Prefetch-Scope: source`)
|
|
40
|
+
* and falls back to the Rango-state-keyed wildcard slot used for the
|
|
41
|
+
* common source-agnostic case.
|
|
31
42
|
*
|
|
32
43
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
33
44
|
* @returns NavigationClient instance
|
|
@@ -57,6 +68,7 @@ export function createNavigationClient(
|
|
|
57
68
|
staleRevalidation,
|
|
58
69
|
interceptSourceUrl,
|
|
59
70
|
version,
|
|
71
|
+
routerId,
|
|
60
72
|
hmr,
|
|
61
73
|
} = options;
|
|
62
74
|
|
|
@@ -84,50 +96,130 @@ export function createNavigationClient(
|
|
|
84
96
|
if (version) {
|
|
85
97
|
fetchUrl.searchParams.set("_rsc_v", version);
|
|
86
98
|
}
|
|
99
|
+
if (routerId) {
|
|
100
|
+
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
101
|
+
}
|
|
87
102
|
|
|
88
|
-
// Check in-memory prefetch cache before making a network
|
|
89
|
-
//
|
|
90
|
-
//
|
|
103
|
+
// Check completed in-memory prefetch cache before making a network
|
|
104
|
+
// request. Try the source-scoped key first (populated when the server
|
|
105
|
+
// tagged the prefetch response as source-sensitive, e.g. intercepts,
|
|
106
|
+
// or when a Link opted in with `prefetchKey=":source"`), then fall
|
|
107
|
+
// back to the wildcard slot shared across source pages.
|
|
108
|
+
// Both keys embed the Rango state, so state rotation (deploy or
|
|
109
|
+
// server-action invalidation) auto-invalidates both scopes.
|
|
91
110
|
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
92
111
|
// fresh modules), and intercept contexts (source-dependent responses).
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
// Suspend prefetch consumption while an action is in flight: a queued
|
|
113
|
+
// prefetch holds pre-mutation data and must not be served until the
|
|
114
|
+
// action's response decides whether anything changed.
|
|
115
|
+
const canUsePrefetch =
|
|
116
|
+
!staleRevalidation &&
|
|
117
|
+
!hmr &&
|
|
118
|
+
!interceptSourceUrl &&
|
|
119
|
+
!isActionFenceActive();
|
|
120
|
+
const rangoState = getRangoState();
|
|
121
|
+
const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
|
|
122
|
+
const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
|
|
98
123
|
|
|
124
|
+
let cachedEntry: DecodedPrefetch | null = null;
|
|
125
|
+
let hitKey: string | null = null;
|
|
126
|
+
if (canUsePrefetch) {
|
|
127
|
+
cachedEntry = consumePrefetch(cacheKey);
|
|
128
|
+
if (cachedEntry) {
|
|
129
|
+
hitKey = cacheKey;
|
|
130
|
+
} else {
|
|
131
|
+
cachedEntry = consumePrefetch(wildcardKey);
|
|
132
|
+
if (cachedEntry) hitKey = wildcardKey;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let inflightEntryPromise: Promise<DecodedPrefetch | null> | null = null;
|
|
137
|
+
if (canUsePrefetch && !cachedEntry) {
|
|
138
|
+
inflightEntryPromise = consumeInflightPrefetch(cacheKey);
|
|
139
|
+
if (inflightEntryPromise) {
|
|
140
|
+
hitKey = cacheKey;
|
|
141
|
+
} else {
|
|
142
|
+
inflightEntryPromise = consumeInflightPrefetch(wildcardKey);
|
|
143
|
+
if (inflightEntryPromise) hitKey = wildcardKey;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
99
146
|
// Track when the stream completes
|
|
100
147
|
let resolveStreamComplete: () => void;
|
|
101
148
|
const streamComplete = new Promise<void>((resolve) => {
|
|
102
149
|
resolveStreamComplete = resolve;
|
|
103
150
|
});
|
|
104
151
|
|
|
105
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Validate RSC control headers on any response (fresh, cached, or
|
|
154
|
+
* in-flight). Handles version-mismatch reloads and server redirects.
|
|
155
|
+
* Returns the response unchanged when no control header is present.
|
|
156
|
+
*/
|
|
157
|
+
const validateRscHeaders = (
|
|
158
|
+
response: Response,
|
|
159
|
+
source: string,
|
|
160
|
+
): Response | Promise<Response> => {
|
|
161
|
+
// Version mismatch — server wants a full page reload
|
|
162
|
+
const reloadResult = handleReloadHeader(response, {
|
|
163
|
+
onBlocked: resolveStreamComplete,
|
|
164
|
+
onReload: (url) => {
|
|
165
|
+
if (tx) {
|
|
166
|
+
browserDebugLog(tx, `version mismatch, reloading (${source})`, {
|
|
167
|
+
reloadUrl: url,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (reloadResult) return reloadResult;
|
|
106
173
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
174
|
+
// Server-side redirect without state: the server returned 204 with
|
|
175
|
+
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
176
|
+
// to a URL rendering full HTML). Throw ServerRedirect so the
|
|
177
|
+
// navigation bridge catches it and re-navigates with _skipCache.
|
|
178
|
+
const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
|
|
179
|
+
if (redirect === "blocked") {
|
|
180
|
+
resolveStreamComplete();
|
|
181
|
+
return emptyResponse();
|
|
110
182
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
183
|
+
if (redirect) {
|
|
184
|
+
if (tx) {
|
|
185
|
+
browserDebugLog(tx, `server redirect (${source})`, {
|
|
186
|
+
redirectUrl: redirect.url,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
resolveStreamComplete();
|
|
190
|
+
throw new ServerRedirect(redirect.url, undefined);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Integrity check (pre-decode): refuse a foreign app's content response
|
|
194
|
+
// before createFromFetch imports its chunks. Ordered AFTER the reload
|
|
195
|
+
// and redirect handlers — control responses are never stamped with
|
|
196
|
+
// X-RSC-Router-Id, so they are steered first and never reach here.
|
|
197
|
+
if (isForeignRouterId(response, routerId)) {
|
|
198
|
+
if (tx) {
|
|
199
|
+
browserDebugLog(tx, `router id mismatch, reloading (${source})`);
|
|
200
|
+
}
|
|
201
|
+
resolveStreamComplete();
|
|
202
|
+
window.location.href = targetUrl;
|
|
203
|
+
return new Promise<Response>(() => {});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return response;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** Start a fresh navigation fetch (no cache / inflight hit). */
|
|
210
|
+
const doFreshFetch = (): Promise<Response> => {
|
|
124
211
|
if (tx) {
|
|
125
212
|
browserDebugLog(tx, "fetching", {
|
|
126
213
|
path: `${fetchUrl.pathname}${fetchUrl.search}`,
|
|
127
214
|
});
|
|
128
215
|
}
|
|
129
216
|
|
|
130
|
-
|
|
217
|
+
return fetch(fetchUrl, {
|
|
218
|
+
// During an action's flight the state is not rotated, so the old
|
|
219
|
+
// X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass
|
|
220
|
+
// it so a genuine mid-action navigation fetches fresh instead of being
|
|
221
|
+
// served the stale prefetched bytes.
|
|
222
|
+
...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
|
|
131
223
|
headers: {
|
|
132
224
|
"X-RSC-Router-Client-Path": previousUrl,
|
|
133
225
|
"X-Rango-State": getRangoState(),
|
|
@@ -139,43 +231,11 @@ export function createNavigationClient(
|
|
|
139
231
|
},
|
|
140
232
|
signal,
|
|
141
233
|
}).then((response) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (reload === "blocked") {
|
|
145
|
-
resolveStreamComplete();
|
|
146
|
-
return emptyResponse();
|
|
147
|
-
}
|
|
148
|
-
if (reload) {
|
|
149
|
-
if (tx) {
|
|
150
|
-
browserDebugLog(tx, "version mismatch, reloading", {
|
|
151
|
-
reloadUrl: reload.url,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
window.location.href = reload.url;
|
|
155
|
-
return new Promise<Response>(() => {});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Server-side redirect without state: the server returned 204 with
|
|
159
|
-
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
160
|
-
// to a URL rendering full HTML). Throw ServerRedirect so the
|
|
161
|
-
// navigation bridge catches it and re-navigates with _skipCache.
|
|
162
|
-
const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
|
|
163
|
-
if (redirect === "blocked") {
|
|
164
|
-
resolveStreamComplete();
|
|
165
|
-
return emptyResponse();
|
|
166
|
-
}
|
|
167
|
-
if (redirect) {
|
|
168
|
-
if (tx) {
|
|
169
|
-
browserDebugLog(tx, "server redirect", {
|
|
170
|
-
redirectUrl: redirect.url,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
resolveStreamComplete();
|
|
174
|
-
throw new ServerRedirect(redirect.url, undefined);
|
|
175
|
-
}
|
|
234
|
+
const validated = validateRscHeaders(response, "fetch");
|
|
235
|
+
if (validated instanceof Promise) return validated;
|
|
176
236
|
|
|
177
237
|
return teeWithCompletion(
|
|
178
|
-
|
|
238
|
+
validated,
|
|
179
239
|
() => {
|
|
180
240
|
if (tx) browserDebugLog(tx, "stream complete");
|
|
181
241
|
resolveStreamComplete();
|
|
@@ -183,11 +243,81 @@ export function createNavigationClient(
|
|
|
183
243
|
signal,
|
|
184
244
|
);
|
|
185
245
|
});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// A warm prefetch hit returns its eagerly-decoded payload directly: the
|
|
249
|
+
// route's chunks were imported during the prefetch, so this click runs
|
|
250
|
+
// no decode and no network. Only the fresh path runs createFromFetch and
|
|
251
|
+
// resolves the local streamComplete (via doFreshFetch's teeWithCompletion
|
|
252
|
+
// and the control-header short-circuits in validateRscHeaders).
|
|
253
|
+
const freshResult = (): {
|
|
254
|
+
payload: Promise<RscPayload>;
|
|
255
|
+
streamComplete: Promise<void>;
|
|
256
|
+
} => ({
|
|
257
|
+
payload: deps.createFromFetch<RscPayload>(doFreshFetch()),
|
|
258
|
+
streamComplete,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
let payloadPromise: Promise<RscPayload>;
|
|
262
|
+
let streamCompletePromise: Promise<void>;
|
|
263
|
+
// True only for a prefetch-cache hit whose stream had already fully drained
|
|
264
|
+
// (complete === true). A still-streaming hit and the fresh path stay false,
|
|
265
|
+
// so only a fully-warmed prefetch commits in a transition (no fallback flash).
|
|
266
|
+
let fullyPrefetched = false;
|
|
267
|
+
|
|
268
|
+
if (cachedEntry) {
|
|
269
|
+
if (tx) {
|
|
270
|
+
browserDebugLog(tx, "prefetch cache hit (warm)", {
|
|
271
|
+
key: hitKey,
|
|
272
|
+
wildcard: hitKey === wildcardKey,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
payloadPromise = cachedEntry.payload;
|
|
276
|
+
streamCompletePromise = cachedEntry.streamComplete;
|
|
277
|
+
// Only a hit whose stream already fully drained is "fully prefetched";
|
|
278
|
+
// a still-streaming hit must keep streaming its fallbacks like a cold load.
|
|
279
|
+
fullyPrefetched = cachedEntry.complete;
|
|
280
|
+
} else if (inflightEntryPromise) {
|
|
281
|
+
if (tx) {
|
|
282
|
+
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
283
|
+
key: hitKey,
|
|
284
|
+
wildcard: hitKey === wildcardKey,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const adoptedViaWildcard = hitKey === wildcardKey;
|
|
288
|
+
const entry = await inflightEntryPromise;
|
|
289
|
+
if (!entry) {
|
|
290
|
+
if (tx) {
|
|
291
|
+
browserDebugLog(tx, "inflight prefetch unavailable, refetching");
|
|
292
|
+
}
|
|
293
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
294
|
+
freshResult());
|
|
295
|
+
} else if (adoptedViaWildcard && entry.scope === "source") {
|
|
296
|
+
// A wildcard-adopted inflight that turned out source-scoped was
|
|
297
|
+
// built for a different source page. Discard and refetch.
|
|
298
|
+
if (tx) {
|
|
299
|
+
browserDebugLog(
|
|
300
|
+
tx,
|
|
301
|
+
"wildcard inflight turned out source-scoped, refetching",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
305
|
+
freshResult());
|
|
306
|
+
} else {
|
|
307
|
+
payloadPromise = entry.payload;
|
|
308
|
+
streamCompletePromise = entry.streamComplete;
|
|
309
|
+
// Adopted inflight is normally still streaming (false), but read the
|
|
310
|
+
// flag in case it completed between publish and adoption.
|
|
311
|
+
fullyPrefetched = entry.complete;
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
315
|
+
freshResult());
|
|
186
316
|
}
|
|
187
317
|
|
|
188
318
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
319
|
+
const payload = await payloadPromise;
|
|
320
|
+
|
|
191
321
|
if (tx) {
|
|
192
322
|
browserDebugLog(tx, "response received", {
|
|
193
323
|
isPartial: payload.metadata?.isPartial,
|
|
@@ -195,7 +325,11 @@ export function createNavigationClient(
|
|
|
195
325
|
diffCount: payload.metadata?.diff?.length ?? 0,
|
|
196
326
|
});
|
|
197
327
|
}
|
|
198
|
-
return {
|
|
328
|
+
return {
|
|
329
|
+
payload,
|
|
330
|
+
streamComplete: streamCompletePromise,
|
|
331
|
+
fullyPrefetched,
|
|
332
|
+
};
|
|
199
333
|
} catch (error) {
|
|
200
334
|
// Convert network-level errors to NetworkError for proper handling
|
|
201
335
|
if (isNetworkError(error)) {
|