@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140
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 +17 -0
- package/README.md +432 -7
- package/dist/bin/rango.js +2073 -213
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +7258 -2714
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +140 -67
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +329 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +487 -0
- package/skills/caching/SKILL.md +357 -25
- 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 +246 -0
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +16 -10
- package/skills/document-cache/SKILL.md +87 -62
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +557 -79
- package/skills/host-router/SKILL.md +320 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +207 -15
- package/skills/layout/SKILL.md +146 -6
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +616 -54
- package/skills/middleware/SKILL.md +217 -37
- package/skills/migrate-nextjs/SKILL.md +611 -0
- package/skills/migrate-react-router/SKILL.md +927 -0
- package/skills/mime-routes/SKILL.md +42 -11
- package/skills/observability/SKILL.md +194 -0
- package/skills/parallel/SKILL.md +284 -3
- package/skills/ppr/SKILL.md +426 -0
- package/skills/prerender/SKILL.md +437 -52
- package/skills/rango/SKILL.md +369 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +263 -121
- package/skills/route/SKILL.md +350 -21
- package/skills/router-setup/SKILL.md +246 -33
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +126 -222
- 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 +9 -8
- package/skills/typesafety/SKILL.md +532 -103
- package/skills/use-cache/SKILL.md +367 -0
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +77 -44
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +114 -0
- 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 +293 -202
- package/src/browser/history-state.ts +101 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +11 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +385 -576
- package/src/browser/navigation-client.ts +245 -75
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +184 -118
- package/src/browser/navigation-transaction.ts +247 -0
- package/src/browser/network-error-handler.ts +88 -0
- package/src/browser/partial-update.ts +412 -364
- package/src/browser/prefetch/cache.ts +359 -0
- package/src/browser/prefetch/fetch.ts +452 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +209 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +194 -0
- package/src/browser/react/Link.tsx +275 -68
- package/src/browser/react/NavigationProvider.tsx +265 -109
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +70 -0
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +272 -60
- package/src/browser/react/location-state.ts +90 -20
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +35 -66
- package/src/browser/react/use-handle.ts +39 -126
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +39 -13
- package/src/browser/react/use-navigation.ts +53 -69
- package/src/browser/react/use-params.ts +75 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +98 -0
- package/src/browser/react/use-search-params.ts +51 -0
- package/src/browser/react/use-segments.ts +72 -99
- package/src/browser/response-adapter.ts +164 -0
- package/src/browser/rsc-router.tsx +300 -72
- package/src/browser/scroll-restoration.ts +138 -50
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +17 -1
- package/src/browser/server-action-bridge.ts +668 -613
- package/src/browser/types.ts +223 -51
- package/src/browser/validate-redirect-origin.ts +56 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +252 -161
- package/src/build/generate-route-types.ts +41 -1038
- package/src/build/index.ts +12 -7
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +225 -42
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +105 -0
- package/src/build/route-types/codegen.ts +113 -0
- package/src/build/route-types/include-resolution.ts +812 -0
- package/src/build/route-types/param-extraction.ts +51 -0
- package/src/build/route-types/per-module-writer.ts +144 -0
- package/src/build/route-types/router-processing.ts +695 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +223 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +60 -0
- package/src/cache/cache-policy.ts +199 -0
- package/src/cache/cache-runtime.ts +525 -0
- package/src/cache/cache-scope.ts +298 -332
- 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 +2500 -158
- 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 +17 -17
- package/src/cache/document-cache.ts +199 -92
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +111 -0
- package/src/cache/index.ts +29 -35
- package/src/cache/memory-segment-store.ts +363 -30
- package/src/cache/profile-registry.ts +88 -0
- package/src/cache/read-through-swr.ts +178 -0
- package/src/cache/segment-codec.ts +248 -0
- package/src/cache/shell-cache.ts +386 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +156 -211
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1102 -0
- package/src/client.rsc.tsx +43 -21
- package/src/client.tsx +131 -347
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +23 -4
- package/src/components/DefaultDocument.tsx +13 -3
- package/src/context-var.ts +168 -0
- package/src/debug.ts +19 -9
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +106 -10
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +110 -35
- package/src/handles/MetaTags.tsx +83 -59
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +93 -0
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +44 -53
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +20 -65
- package/src/host/errors.ts +21 -30
- package/src/host/index.ts +13 -9
- package/src/host/pattern-matcher.ts +50 -79
- package/src/host/router.ts +151 -121
- package/src/host/testing.ts +45 -32
- package/src/host/types.ts +52 -11
- package/src/host/utils.ts +2 -2
- package/src/href-client.ts +192 -57
- package/src/index.rsc.ts +177 -35
- package/src/index.ts +255 -71
- package/src/internal-debug.ts +9 -2
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +31 -99
- package/src/loader.ts +30 -12
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +41 -0
- package/src/prerender/param-hash.ts +16 -14
- package/src/prerender/store.ts +121 -21
- package/src/prerender.ts +460 -26
- 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 +198 -128
- package/src/root-error-boundary.tsx +42 -48
- package/src/route-content-wrapper.tsx +22 -77
- package/src/route-definition/dsl-helpers.ts +1116 -0
- package/src/route-definition/helper-factories.ts +88 -0
- package/src/route-definition/helpers-types.ts +505 -0
- package/src/route-definition/index.ts +54 -0
- package/src/route-definition/redirect.ts +134 -0
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-definition.ts +1 -1481
- package/src/route-map-builder.ts +82 -144
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +71 -45
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +263 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +54 -27
- package/src/router/find-match.ts +245 -0
- package/src/router/handler-context.ts +377 -125
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +59 -28
- package/src/router/lazy-includes.ts +254 -0
- package/src/router/loader-resolution.ts +421 -157
- package/src/router/logging.ts +106 -6
- package/src/router/manifest.ts +131 -57
- package/src/router/match-api.ts +167 -246
- package/src/router/match-context.ts +4 -24
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +117 -93
- package/src/router/match-middleware/cache-lookup.ts +297 -150
- package/src/router/match-middleware/cache-store.ts +123 -51
- package/src/router/match-middleware/intercept-resolution.ts +44 -43
- package/src/router/match-middleware/segment-resolution.ts +64 -22
- package/src/router/match-pipelines.ts +11 -87
- package/src/router/match-result.ts +121 -50
- package/src/router/metrics.ts +219 -28
- package/src/router/middleware-types.ts +93 -0
- package/src/router/middleware.ts +505 -441
- package/src/router/navigation-snapshot.ts +133 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +311 -142
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +547 -0
- package/src/router/preview-match.ts +102 -0
- package/src/router/request-classification.ts +278 -0
- package/src/router/revalidation.ts +203 -62
- package/src/router/route-snapshot.ts +246 -0
- package/src/router/router-context.ts +45 -48
- package/src/router/router-interfaces.ts +554 -0
- package/src/router/router-options.ts +779 -0
- package/src/router/router-registry.ts +21 -0
- package/src/router/segment-resolution/fresh.ts +772 -0
- package/src/router/segment-resolution/helpers.ts +348 -0
- package/src/router/segment-resolution/loader-cache.ts +250 -0
- package/src/router/segment-resolution/loader-mask.ts +44 -0
- package/src/router/segment-resolution/revalidation.ts +1331 -0
- package/src/router/segment-resolution/static-store.ts +81 -0
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +25 -1354
- package/src/router/segment-wrappers.ts +292 -0
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +261 -0
- package/src/router/telemetry.ts +377 -0
- package/src/router/timeout.ts +128 -0
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +240 -61
- package/src/router/types.ts +23 -70
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +781 -2378
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +46 -0
- package/src/rsc/handler.ts +905 -1142
- package/src/rsc/helpers.ts +275 -19
- package/src/rsc/index.ts +2 -25
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +305 -0
- package/src/rsc/manifest-init.ts +77 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +155 -0
- package/src/rsc/progressive-enhancement.ts +502 -0
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +104 -0
- package/src/rsc/response-route-handler.ts +257 -0
- package/src/rsc/rsc-rendering.ts +337 -0
- package/src/rsc/runtime-warnings.ts +55 -0
- package/src/rsc/server-action.ts +522 -0
- package/src/rsc/shell-capture.ts +439 -0
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +95 -12
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +99 -82
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +349 -134
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +452 -85
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +310 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +123 -42
- package/src/server/live.ts +130 -0
- package/src/server/loader-registry.ts +51 -100
- package/src/server/request-context.ts +842 -157
- package/src/server.ts +15 -8
- package/src/ssr/index.tsx +412 -136
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +45 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -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 +199 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +584 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +40 -72
- package/src/theme/ThemeScript.tsx +12 -14
- package/src/theme/constants.ts +57 -15
- package/src/theme/index.ts +3 -20
- package/src/theme/theme-context.ts +5 -35
- package/src/theme/theme-script.ts +43 -39
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +123 -0
- package/src/types/cache-types.ts +207 -0
- package/src/types/error-types.ts +132 -0
- package/src/types/global-namespace.ts +113 -0
- package/src/types/handler-context.ts +839 -0
- package/src/types/index.ts +81 -0
- package/src/types/loader-types.ts +212 -0
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +138 -0
- package/src/types/route-entry.ts +114 -0
- package/src/types/segments.ts +271 -0
- package/src/types.ts +1 -1795
- package/src/urls/include-helper.ts +162 -0
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +43 -0
- package/src/urls/path-helper-types.ts +413 -0
- package/src/urls/path-helper.ts +275 -0
- package/src/urls/pattern-types.ts +124 -0
- package/src/urls/response-types.ts +109 -0
- package/src/urls/type-extraction.ts +316 -0
- package/src/urls/urls-function.ts +80 -0
- package/src/urls.ts +1 -1341
- package/src/use-loader.tsx +406 -141
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +182 -0
- package/src/vite/discovery/discover-routers.ts +389 -0
- package/src/vite/discovery/discovery-errors.ts +255 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +467 -0
- package/src/vite/discovery/route-types-writer.ts +214 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +161 -0
- package/src/vite/discovery/virtual-module-codegen.ts +183 -0
- package/src/vite/index.ts +23 -2255
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +303 -0
- package/src/vite/plugins/cjs-to-esm.ts +90 -0
- package/src/vite/plugins/client-ref-dedup.ts +120 -0
- package/src/vite/plugins/client-ref-hashing.ts +118 -0
- 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/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
- package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
- package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +805 -0
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +313 -0
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +94 -0
- package/src/vite/plugins/version-plugin.ts +263 -0
- package/src/vite/plugins/virtual-entries.ts +234 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +560 -0
- package/src/vite/router-discovery.ts +1638 -0
- package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +132 -0
- 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 +15 -0
- package/src/vite/utils/package-resolution.ts +89 -0
- package/src/vite/utils/prerender-utils.ts +249 -0
- package/src/vite/utils/shared-utils.ts +269 -0
- package/CLAUDE.md +0 -43
- package/dist/vite/index.named-routes.gen.ts +0 -103
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/react/use-client-cache.ts +0 -56
- package/src/browser/request-controller.ts +0 -164
- package/src/browser/shallow.ts +0 -35
- package/src/cache/memory-store.ts +0 -253
- package/src/handles/index.ts +0 -6
- package/src/href-context.ts +0 -33
- package/src/network-error-thrower.tsx +0 -21
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-internal-ids.ts +0 -1167
- package/src/vite/package-resolution.ts +0 -125
- package/src/vite/virtual-entries.ts +0 -114
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -1,442 +1,49 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
NavigationBridge,
|
|
3
3
|
NavigationBridgeConfig,
|
|
4
|
-
|
|
5
|
-
NavigationStore,
|
|
4
|
+
NavigateOptionsInternal,
|
|
6
5
|
ResolvedSegment,
|
|
7
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";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import { startTransition } from "react";
|
|
8
12
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./
|
|
13
|
+
createNavigationTransaction,
|
|
14
|
+
resolveNavigationState,
|
|
15
|
+
} from "./navigation-transaction.js";
|
|
16
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
17
|
+
import {
|
|
18
|
+
handleNavigationStart,
|
|
19
|
+
handleNavigationEnd,
|
|
20
|
+
ensureHistoryKey,
|
|
21
|
+
} from "./scroll-restoration.js";
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function isTypedLocationState(
|
|
17
|
-
state: unknown
|
|
18
|
-
): state is Record<string, unknown> {
|
|
19
|
-
if (state === null || typeof state !== "object") return false;
|
|
20
|
-
return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
|
|
21
|
-
}
|
|
23
|
+
// addTransitionType is only available in React experimental
|
|
24
|
+
const addTransitionType: ((type: string) => void) | undefined =
|
|
25
|
+
"addTransitionType" in React ? (React as any).addTransitionType : undefined;
|
|
22
26
|
|
|
23
|
-
/**
|
|
24
|
-
* Resolve navigation state - handles both LocationStateEntry[] and legacy formats
|
|
25
|
-
*/
|
|
26
|
-
function resolveNavigationState(state: unknown): unknown {
|
|
27
|
-
// Check if it's an array of LocationStateEntry
|
|
28
|
-
if (
|
|
29
|
-
Array.isArray(state) &&
|
|
30
|
-
state.length > 0 &&
|
|
31
|
-
isLocationStateEntry(state[0])
|
|
32
|
-
) {
|
|
33
|
-
return resolveLocationStateEntries(state);
|
|
34
|
-
}
|
|
35
|
-
// Return as-is for legacy formats
|
|
36
|
-
return state;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Build history state object from user state
|
|
41
|
-
* - Typed state: spread directly into history.state
|
|
42
|
-
* - Legacy state: store in history.state.state
|
|
43
|
-
*/
|
|
44
|
-
function buildHistoryState(
|
|
45
|
-
userState: unknown,
|
|
46
|
-
routerState?: { intercept?: boolean; sourceUrl?: string }
|
|
47
|
-
): Record<string, unknown> | null {
|
|
48
|
-
const result: Record<string, unknown> = {};
|
|
49
|
-
|
|
50
|
-
// Add router internal state
|
|
51
|
-
if (routerState?.intercept) {
|
|
52
|
-
result.intercept = true;
|
|
53
|
-
if (routerState.sourceUrl) {
|
|
54
|
-
result.sourceUrl = routerState.sourceUrl;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Add user state
|
|
59
|
-
if (userState !== undefined) {
|
|
60
|
-
if (isTypedLocationState(userState)) {
|
|
61
|
-
// Typed state: spread directly
|
|
62
|
-
Object.assign(result, userState);
|
|
63
|
-
} else {
|
|
64
|
-
// Legacy state: store in .state
|
|
65
|
-
result.state = userState;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return Object.keys(result).length > 0 ? result : null;
|
|
70
|
-
}
|
|
71
27
|
import { setupLinkInterception } from "./link-interceptor.js";
|
|
72
28
|
import { createPartialUpdater } from "./partial-update.js";
|
|
73
29
|
import { generateHistoryKey } from "./navigation-store.js";
|
|
30
|
+
import type { EventController } from "./event-controller.js";
|
|
31
|
+
import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
74
32
|
import {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
import {
|
|
81
|
-
import {
|
|
82
|
-
import {
|
|
33
|
+
toNetworkError,
|
|
34
|
+
emitNetworkError,
|
|
35
|
+
emitNavigationError,
|
|
36
|
+
isBackgroundSuppressible,
|
|
37
|
+
} from "./network-error-handler.js";
|
|
38
|
+
import { debugLog } from "./logging.js";
|
|
39
|
+
import { ServerRedirect } from "../errors.js";
|
|
40
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
83
41
|
|
|
84
42
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
85
43
|
if (typeof Symbol.dispose === "undefined") {
|
|
86
44
|
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
87
45
|
}
|
|
88
46
|
|
|
89
|
-
/**
|
|
90
|
-
* Check if a segment is an intercept segment
|
|
91
|
-
* Intercept segments have namespace starting with "intercept:" or ID containing .@
|
|
92
|
-
*/
|
|
93
|
-
function isInterceptSegment(s: ResolvedSegment): boolean {
|
|
94
|
-
return (
|
|
95
|
-
s.namespace?.startsWith("intercept:") ||
|
|
96
|
-
(s.type === "parallel" && s.id.includes(".@"))
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Check if cached segments are intercept-only (no main route segments)
|
|
102
|
-
* Intercept responses shouldn't be used for optimistic rendering since
|
|
103
|
-
* whether interception happens depends on the current page context
|
|
104
|
-
*/
|
|
105
|
-
function isInterceptOnlyCache(segments: ResolvedSegment[]): boolean {
|
|
106
|
-
return segments.some(isInterceptSegment);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Options for committing a navigation transaction
|
|
111
|
-
*/
|
|
112
|
-
interface CommitOptions {
|
|
113
|
-
url: string;
|
|
114
|
-
segmentIds: string[];
|
|
115
|
-
segments: ResolvedSegment[];
|
|
116
|
-
replace?: boolean;
|
|
117
|
-
scroll?: boolean;
|
|
118
|
-
/** User-provided state to store in history.state */
|
|
119
|
-
state?: unknown;
|
|
120
|
-
/** If true, only update store without changing URL/history (for server actions) */
|
|
121
|
-
storeOnly?: boolean;
|
|
122
|
-
/** If true, this is an intercept route - store in history.state for popstate handling */
|
|
123
|
-
intercept?: boolean;
|
|
124
|
-
/** Source URL where the intercept was triggered from (stored in history.state) */
|
|
125
|
-
interceptSourceUrl?: string;
|
|
126
|
-
/** If true, only update cache without touching store or history (for background stale revalidation) */
|
|
127
|
-
cacheOnly?: boolean;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Options that can override the pre-configured commit settings
|
|
132
|
-
*/
|
|
133
|
-
interface BoundCommitOverrides {
|
|
134
|
-
/** Override scroll behavior (e.g., disable for intercepts) */
|
|
135
|
-
scroll?: boolean;
|
|
136
|
-
/** Override replace behavior (e.g., force replace for intercepts) */
|
|
137
|
-
replace?: boolean;
|
|
138
|
-
/** Override user-provided state */
|
|
139
|
-
state?: unknown;
|
|
140
|
-
/** Mark this as an intercept route */
|
|
141
|
-
intercept?: boolean;
|
|
142
|
-
/** Source URL where intercept was triggered from */
|
|
143
|
-
interceptSourceUrl?: string;
|
|
144
|
-
/** If true, only update cache (for stale revalidation) */
|
|
145
|
-
cacheOnly?: boolean;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Token for tracking an active stream - call end() when stream completes
|
|
150
|
-
*/
|
|
151
|
-
export interface StreamingToken {
|
|
152
|
-
end(): void;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Bound transaction with pre-configured commit options (without segmentIds/segments)
|
|
157
|
-
*/
|
|
158
|
-
export interface BoundTransaction {
|
|
159
|
-
readonly currentUrl: string;
|
|
160
|
-
/** Start streaming and get a token to end it when the stream completes */
|
|
161
|
-
startStreaming(): StreamingToken;
|
|
162
|
-
commit(
|
|
163
|
-
segmentIds: string[],
|
|
164
|
-
segments: ResolvedSegment[],
|
|
165
|
-
overrides?: BoundCommitOverrides
|
|
166
|
-
): void;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Navigation transaction for managing state during navigation
|
|
171
|
-
* Uses the event controller handle for lifecycle management
|
|
172
|
-
*/
|
|
173
|
-
interface NavigationTransaction extends Disposable {
|
|
174
|
-
/** Optimistically commit from cache - instant render before revalidation */
|
|
175
|
-
optimisticCommit(options: CommitOptions): void;
|
|
176
|
-
/** Final commit with server data (or reconciliation after optimistic) */
|
|
177
|
-
commit(options: CommitOptions): void;
|
|
178
|
-
with(
|
|
179
|
-
options: Omit<CommitOptions, "segmentIds" | "segments">
|
|
180
|
-
): BoundTransaction;
|
|
181
|
-
/** The navigation handle from the event controller */
|
|
182
|
-
handle: NavigationHandle;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Creates a navigation transaction that coordinates with the event controller.
|
|
187
|
-
* Handles loading state transitions and cleanup on completion/abort.
|
|
188
|
-
*
|
|
189
|
-
* Supports optimistic navigation: render from cache immediately,
|
|
190
|
-
* then revalidate in background and reconcile if data changed.
|
|
191
|
-
*/
|
|
192
|
-
function createNavigationTransaction(
|
|
193
|
-
store: NavigationStore,
|
|
194
|
-
eventController: EventController,
|
|
195
|
-
url: string,
|
|
196
|
-
options?: NavigateOptions & { skipLoadingState?: boolean }
|
|
197
|
-
): NavigationTransaction {
|
|
198
|
-
let committed = false;
|
|
199
|
-
let optimisticallyCommitted = false;
|
|
200
|
-
let earlyStatePushed = false;
|
|
201
|
-
const currentUrl = window.location.href;
|
|
202
|
-
|
|
203
|
-
// Start navigation in event controller (this sets loading state)
|
|
204
|
-
const handle = eventController.startNavigation(url, options);
|
|
205
|
-
|
|
206
|
-
// If state is provided, push it to history immediately so loading UI can access it
|
|
207
|
-
// This enables "optimistic state" - showing product names in skeletons etc.
|
|
208
|
-
if (options?.state !== undefined && !options?.replace) {
|
|
209
|
-
const earlyHistoryState = buildHistoryState(options.state);
|
|
210
|
-
window.history.pushState(earlyHistoryState, "", url);
|
|
211
|
-
earlyStatePushed = true;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Optimistically commit from cache - renders immediately before revalidation
|
|
216
|
-
* Sets optimisticallyCommitted flag so final commit() knows to reconcile
|
|
217
|
-
*/
|
|
218
|
-
function optimisticCommit(opts: CommitOptions): void {
|
|
219
|
-
optimisticallyCommitted = true;
|
|
220
|
-
|
|
221
|
-
const { url, segmentIds, segments, replace, scroll } = opts;
|
|
222
|
-
const parsedUrl = new URL(url, window.location.origin);
|
|
223
|
-
|
|
224
|
-
// Save current scroll position before navigating
|
|
225
|
-
handleNavigationStart();
|
|
226
|
-
|
|
227
|
-
// Update segment state
|
|
228
|
-
store.setSegmentIds(segmentIds);
|
|
229
|
-
store.setCurrentUrl(url);
|
|
230
|
-
store.setPath(parsedUrl.pathname);
|
|
231
|
-
|
|
232
|
-
// Generate history key from URL
|
|
233
|
-
const historyKey = generateHistoryKey(url);
|
|
234
|
-
store.setHistoryKey(historyKey);
|
|
235
|
-
|
|
236
|
-
// Cache segments with current handleData (will be overwritten by fresh data on final commit)
|
|
237
|
-
const currentHandleData = eventController.getHandleState().data;
|
|
238
|
-
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
239
|
-
|
|
240
|
-
// Build history state with user state if provided
|
|
241
|
-
const historyState = buildHistoryState(opts.state);
|
|
242
|
-
|
|
243
|
-
// Update browser URL
|
|
244
|
-
// Use replaceState if we already pushed early (for optimistic state access)
|
|
245
|
-
if (replace || earlyStatePushed) {
|
|
246
|
-
window.history.replaceState(historyState, "", url);
|
|
247
|
-
} else {
|
|
248
|
-
window.history.pushState(historyState, "", url);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Ensure new history entry has a scroll restoration key
|
|
252
|
-
ensureHistoryKey();
|
|
253
|
-
|
|
254
|
-
// Complete the navigation in event controller (sets idle state)
|
|
255
|
-
handle.complete(parsedUrl);
|
|
256
|
-
|
|
257
|
-
// Handle scroll after navigation
|
|
258
|
-
handleNavigationEnd({ scroll });
|
|
259
|
-
|
|
260
|
-
console.log(
|
|
261
|
-
"[Browser] Optimistic commit from cache, historyKey:",
|
|
262
|
-
historyKey
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Commit the navigation - updates store and URL atomically
|
|
268
|
-
* If optimisticCommit was called, this becomes a reconciliation
|
|
269
|
-
*/
|
|
270
|
-
function commit(opts: CommitOptions): void {
|
|
271
|
-
committed = true;
|
|
272
|
-
|
|
273
|
-
// If optimistic commit already done, adjust options for reconciliation
|
|
274
|
-
const isReconciliation = optimisticallyCommitted;
|
|
275
|
-
const {
|
|
276
|
-
url,
|
|
277
|
-
segmentIds,
|
|
278
|
-
segments,
|
|
279
|
-
storeOnly,
|
|
280
|
-
intercept,
|
|
281
|
-
interceptSourceUrl,
|
|
282
|
-
cacheOnly,
|
|
283
|
-
} = opts;
|
|
284
|
-
// For reconciliation: always replace (URL already pushed), no scroll
|
|
285
|
-
const replace = isReconciliation ? true : opts.replace;
|
|
286
|
-
const scroll = isReconciliation ? false : opts.scroll;
|
|
287
|
-
|
|
288
|
-
const parsedUrl = new URL(url, window.location.origin);
|
|
289
|
-
|
|
290
|
-
// Generate history key from URL (with intercept suffix for separate caching)
|
|
291
|
-
const historyKey = generateHistoryKey(url, { intercept });
|
|
292
|
-
|
|
293
|
-
// For cache-only commits (stale revalidation), only update cache and return
|
|
294
|
-
// Don't touch store state or history - user may have navigated elsewhere
|
|
295
|
-
if (cacheOnly) {
|
|
296
|
-
const currentHandleData = eventController.getHandleState().data;
|
|
297
|
-
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
298
|
-
console.log("[Browser] Cache-only commit, historyKey:", historyKey);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Save current scroll position before navigating (only for non-reconciliation)
|
|
303
|
-
if (!isReconciliation) {
|
|
304
|
-
handleNavigationStart();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Update segment state atomically
|
|
308
|
-
store.setSegmentIds(segmentIds);
|
|
309
|
-
store.setCurrentUrl(url);
|
|
310
|
-
store.setPath(parsedUrl.pathname);
|
|
311
|
-
|
|
312
|
-
store.setHistoryKey(historyKey);
|
|
313
|
-
|
|
314
|
-
// Cache segments with current handleData for this history entry (fresh data overwrites optimistic)
|
|
315
|
-
const currentHandleData = eventController.getHandleState().data;
|
|
316
|
-
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
317
|
-
|
|
318
|
-
// For server actions, skip URL/history updates but still complete navigation
|
|
319
|
-
if (storeOnly) {
|
|
320
|
-
console.log("[Browser] Store updated (action)");
|
|
321
|
-
// Complete navigation to clear loading state
|
|
322
|
-
handle.complete(parsedUrl);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Build history state - include user state and intercept info for popstate handling
|
|
327
|
-
const historyState = buildHistoryState(opts.state, {
|
|
328
|
-
intercept,
|
|
329
|
-
sourceUrl: interceptSourceUrl,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// Update browser URL (skip if reconciliation - already done in optimisticCommit)
|
|
333
|
-
if (!isReconciliation) {
|
|
334
|
-
// Use replaceState if we already pushed early (for optimistic state access) or replace requested
|
|
335
|
-
if (replace || earlyStatePushed) {
|
|
336
|
-
window.history.replaceState(historyState, "", url);
|
|
337
|
-
} else {
|
|
338
|
-
window.history.pushState(historyState, "", url);
|
|
339
|
-
}
|
|
340
|
-
// Ensure new history entry has a scroll restoration key
|
|
341
|
-
ensureHistoryKey();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Complete the navigation in event controller (sets idle state, updates location)
|
|
345
|
-
handle.complete(parsedUrl);
|
|
346
|
-
|
|
347
|
-
// Handle scroll after navigation (skip if reconciliation)
|
|
348
|
-
if (!isReconciliation) {
|
|
349
|
-
handleNavigationEnd({ scroll });
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (isReconciliation) {
|
|
353
|
-
console.log("[Browser] Reconciliation commit, historyKey:", historyKey);
|
|
354
|
-
} else {
|
|
355
|
-
console.log(
|
|
356
|
-
"[Browser] Navigation committed, historyKey:",
|
|
357
|
-
historyKey,
|
|
358
|
-
intercept ? "(intercept)" : ""
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return {
|
|
364
|
-
handle,
|
|
365
|
-
optimisticCommit,
|
|
366
|
-
commit,
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Create a bound transaction with pre-configured URL options
|
|
370
|
-
* segmentIds and segments provided at commit time (after they're resolved)
|
|
371
|
-
*/
|
|
372
|
-
with(
|
|
373
|
-
opts: Omit<CommitOptions, "segmentIds" | "segments">
|
|
374
|
-
): BoundTransaction {
|
|
375
|
-
return {
|
|
376
|
-
get currentUrl() {
|
|
377
|
-
return currentUrl;
|
|
378
|
-
},
|
|
379
|
-
startStreaming() {
|
|
380
|
-
return handle.startStreaming();
|
|
381
|
-
},
|
|
382
|
-
commit: (
|
|
383
|
-
segmentIds: string[],
|
|
384
|
-
segments: ResolvedSegment[],
|
|
385
|
-
overrides?: BoundCommitOverrides
|
|
386
|
-
) => {
|
|
387
|
-
// Allow overrides to disable scroll (e.g., for intercepts)
|
|
388
|
-
const finalScroll =
|
|
389
|
-
overrides?.scroll !== undefined ? overrides.scroll : opts.scroll;
|
|
390
|
-
// Allow overrides to force replace (e.g., for intercepts)
|
|
391
|
-
const finalReplace =
|
|
392
|
-
overrides?.replace !== undefined ? overrides.replace : opts.replace;
|
|
393
|
-
// Intercept info: overrides take precedence, fallback to opts
|
|
394
|
-
const intercept =
|
|
395
|
-
overrides?.intercept !== undefined
|
|
396
|
-
? overrides.intercept
|
|
397
|
-
: opts.intercept;
|
|
398
|
-
const interceptSourceUrl =
|
|
399
|
-
overrides?.interceptSourceUrl !== undefined
|
|
400
|
-
? overrides.interceptSourceUrl
|
|
401
|
-
: opts.interceptSourceUrl;
|
|
402
|
-
// Cache-only mode: overrides take precedence, fallback to opts
|
|
403
|
-
const cacheOnly =
|
|
404
|
-
overrides?.cacheOnly !== undefined
|
|
405
|
-
? overrides.cacheOnly
|
|
406
|
-
: opts.cacheOnly;
|
|
407
|
-
// User state: overrides take precedence, fallback to opts
|
|
408
|
-
const state =
|
|
409
|
-
overrides?.state !== undefined ? overrides.state : opts.state;
|
|
410
|
-
commit({
|
|
411
|
-
...opts,
|
|
412
|
-
segmentIds,
|
|
413
|
-
segments,
|
|
414
|
-
scroll: finalScroll,
|
|
415
|
-
replace: finalReplace,
|
|
416
|
-
state,
|
|
417
|
-
intercept,
|
|
418
|
-
interceptSourceUrl,
|
|
419
|
-
cacheOnly,
|
|
420
|
-
});
|
|
421
|
-
},
|
|
422
|
-
};
|
|
423
|
-
},
|
|
424
|
-
|
|
425
|
-
[Symbol.dispose]() {
|
|
426
|
-
// If aborted, another navigation took over - don't touch state
|
|
427
|
-
if (handle.signal.aborted) return;
|
|
428
|
-
|
|
429
|
-
// If not committed (and not optimistically committed), the handle's dispose
|
|
430
|
-
// will reset state to idle via the event controller
|
|
431
|
-
if (!committed && !optimisticallyCommitted) {
|
|
432
|
-
handle[Symbol.dispose]();
|
|
433
|
-
// The NavigationHandle's [Symbol.dispose] handles this
|
|
434
|
-
}
|
|
435
|
-
},
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Export for use by server-action-bridge
|
|
440
47
|
export { createNavigationTransaction };
|
|
441
48
|
|
|
442
49
|
/**
|
|
@@ -444,7 +51,7 @@ export { createNavigationTransaction };
|
|
|
444
51
|
*/
|
|
445
52
|
export interface NavigationBridgeConfigWithController extends NavigationBridgeConfig {
|
|
446
53
|
eventController: EventController;
|
|
447
|
-
/** RSC version from initial payload metadata */
|
|
54
|
+
/** RSC version from initial payload metadata. */
|
|
448
55
|
version?: string;
|
|
449
56
|
}
|
|
450
57
|
|
|
@@ -462,9 +69,10 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
|
|
|
462
69
|
* @returns NavigationBridge instance
|
|
463
70
|
*/
|
|
464
71
|
export function createNavigationBridge(
|
|
465
|
-
config: NavigationBridgeConfigWithController
|
|
72
|
+
config: NavigationBridgeConfigWithController,
|
|
466
73
|
): NavigationBridge {
|
|
467
|
-
const { store, client, eventController, onUpdate, renderSegments
|
|
74
|
+
const { store, client, eventController, onUpdate, renderSegments } = config;
|
|
75
|
+
let version = config.version;
|
|
468
76
|
|
|
469
77
|
// Create shared partial updater
|
|
470
78
|
const fetchPartialUpdate = createPartialUpdater({
|
|
@@ -472,25 +80,123 @@ export function createNavigationBridge(
|
|
|
472
80
|
client,
|
|
473
81
|
onUpdate,
|
|
474
82
|
renderSegments,
|
|
475
|
-
version,
|
|
83
|
+
getVersion: () => version,
|
|
476
84
|
});
|
|
477
85
|
|
|
478
86
|
return {
|
|
479
87
|
/**
|
|
480
88
|
* Navigate to a URL
|
|
481
|
-
* Uses
|
|
89
|
+
* Uses cached segments for SWR revalidation when available
|
|
482
90
|
*/
|
|
483
|
-
async navigate(
|
|
91
|
+
async navigate(
|
|
92
|
+
url: string,
|
|
93
|
+
options?: NavigateOptionsInternal,
|
|
94
|
+
): Promise<void> {
|
|
484
95
|
// Resolve LocationStateEntry[] to flat object if needed
|
|
485
96
|
const resolvedState =
|
|
486
97
|
options?.state !== undefined
|
|
487
98
|
? resolveNavigationState(options.state)
|
|
488
99
|
: undefined;
|
|
489
100
|
|
|
101
|
+
// Cross-origin URLs are not handled by SPA navigation.
|
|
102
|
+
// Fall back to a full browser navigation for http/https only.
|
|
103
|
+
let targetUrl: URL;
|
|
104
|
+
try {
|
|
105
|
+
targetUrl = new URL(url, window.location.origin);
|
|
106
|
+
} catch {
|
|
107
|
+
console.warn(`[rango] navigate() ignored: malformed URL "${url}"`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (targetUrl.origin !== window.location.origin) {
|
|
111
|
+
if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
|
|
112
|
+
console.error(
|
|
113
|
+
`[rango] navigate() blocked: unsupported scheme "${targetUrl.protocol}"`,
|
|
114
|
+
);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
window.location.href = targetUrl.href;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Shallow navigation: skip RSC fetch when revalidate is false
|
|
122
|
+
// and the pathname hasn't changed (search param / hash only change).
|
|
123
|
+
if (
|
|
124
|
+
options?.revalidate === false &&
|
|
125
|
+
targetUrl.pathname === new URL(window.location.href).pathname
|
|
126
|
+
) {
|
|
127
|
+
// Preserve intercept context from the current history entry so that
|
|
128
|
+
// popstate uses the correct cache key (:intercept suffix) and restores
|
|
129
|
+
// the right full-page vs modal semantics.
|
|
130
|
+
const currentHistoryState = window.history.state;
|
|
131
|
+
const isIntercept = currentHistoryState?.intercept === true;
|
|
132
|
+
const interceptSourceUrl = isIntercept
|
|
133
|
+
? currentHistoryState?.sourceUrl
|
|
134
|
+
: undefined;
|
|
135
|
+
|
|
136
|
+
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
137
|
+
|
|
138
|
+
// Copy current segments to the new history key so back/forward restores instantly
|
|
139
|
+
const currentKey = store.getHistoryKey();
|
|
140
|
+
const currentCache = store.getCachedSegments(currentKey);
|
|
141
|
+
if (currentCache?.segments) {
|
|
142
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
143
|
+
store.cacheSegmentsForHistory(
|
|
144
|
+
historyKey,
|
|
145
|
+
currentCache.segments,
|
|
146
|
+
currentHandleData,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Save current scroll position before changing URL
|
|
151
|
+
handleNavigationStart();
|
|
152
|
+
|
|
153
|
+
// Snapshot old state before pushState/replaceState overwrites it
|
|
154
|
+
const oldState = window.history.state;
|
|
155
|
+
|
|
156
|
+
// Update browser URL (carry intercept context into history state)
|
|
157
|
+
const historyState = buildHistoryState(
|
|
158
|
+
resolvedState,
|
|
159
|
+
{
|
|
160
|
+
intercept: isIntercept || undefined,
|
|
161
|
+
sourceUrl: interceptSourceUrl,
|
|
162
|
+
},
|
|
163
|
+
{},
|
|
164
|
+
);
|
|
165
|
+
pushHistoryWithIdx(historyState, url, options?.replace ?? false);
|
|
166
|
+
|
|
167
|
+
// Ensure new history entry has a scroll restoration key
|
|
168
|
+
ensureHistoryKey();
|
|
169
|
+
|
|
170
|
+
// Notify useLocationState() hooks when state changes
|
|
171
|
+
const hasOldState =
|
|
172
|
+
oldState &&
|
|
173
|
+
typeof oldState === "object" &&
|
|
174
|
+
("state" in oldState ||
|
|
175
|
+
Object.keys(oldState).some((k) => k.startsWith("__rsc_ls_")));
|
|
176
|
+
const hasNewState =
|
|
177
|
+
historyState &&
|
|
178
|
+
("state" in historyState ||
|
|
179
|
+
Object.keys(historyState).some((k) => k.startsWith("__rsc_ls_")));
|
|
180
|
+
if (hasOldState || hasNewState) {
|
|
181
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Update store history key so future navigations reference the right cache
|
|
185
|
+
store.setHistoryKey(historyKey);
|
|
186
|
+
store.setCurrentUrl(url);
|
|
187
|
+
|
|
188
|
+
// Notify hooks — location updates, state stays idle
|
|
189
|
+
eventController.setLocation(targetUrl);
|
|
190
|
+
|
|
191
|
+
// Handle post-navigation scroll
|
|
192
|
+
handleNavigationEnd({ scroll: options.scroll });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
490
196
|
// Only abort pending requests when navigating to a different route
|
|
491
197
|
// Same-route navigation (e.g., /todos -> /todos) should not cancel in-flight actions
|
|
492
198
|
const currentPath = new URL(window.location.href).pathname;
|
|
493
|
-
const targetPath =
|
|
199
|
+
const targetPath = targetUrl.pathname;
|
|
494
200
|
if (currentPath !== targetPath) {
|
|
495
201
|
eventController.abortNavigation();
|
|
496
202
|
}
|
|
@@ -503,22 +209,27 @@ export function createNavigationBridge(
|
|
|
503
209
|
const isLeavingIntercept = isCurrentlyIntercept && isSamePathNavigation;
|
|
504
210
|
|
|
505
211
|
if (isLeavingIntercept) {
|
|
506
|
-
|
|
212
|
+
debugLog(
|
|
213
|
+
"[Browser] Leaving intercept - same URL navigation from intercept",
|
|
214
|
+
);
|
|
507
215
|
// Clear intercept source URL to ensure server doesn't treat this as intercept
|
|
508
216
|
store.setInterceptSourceUrl(null);
|
|
509
217
|
}
|
|
510
218
|
|
|
511
|
-
// Before navigating away, update the source page's cache with the latest
|
|
512
|
-
// This ensures the cache has correct handleData even if handles
|
|
219
|
+
// Before navigating away, update the source page's cache with the latest
|
|
220
|
+
// handleData. This ensures the cache has correct handleData even if handles
|
|
221
|
+
// were streaming. Use updateCacheHandleData (not cacheSegmentsForHistory):
|
|
222
|
+
// the source page's segments are unchanged, so this is a handleData refresh,
|
|
223
|
+
// not a commit. Critically it PRESERVES the entry's stale flag — when the
|
|
224
|
+
// source page has a deferred Meta still pending, its entry was marked stale
|
|
225
|
+
// (invalidate-on-pending) so a popstate return revalidates; re-committing it
|
|
226
|
+
// here would reset stale to false and serve the carried (pre-resolution)
|
|
227
|
+
// title as fresh. It also leaves the nav-instance token intact.
|
|
513
228
|
const sourceHistoryKey = store.getHistoryKey();
|
|
514
229
|
const sourceCached = store.getCachedSegments(sourceHistoryKey);
|
|
515
230
|
if (sourceCached?.segments && sourceCached.segments.length > 0) {
|
|
516
231
|
const currentHandleData = eventController.getHandleState().data;
|
|
517
|
-
store.
|
|
518
|
-
sourceHistoryKey,
|
|
519
|
-
sourceCached.segments,
|
|
520
|
-
currentHandleData
|
|
521
|
-
);
|
|
232
|
+
store.updateCacheHandleData(sourceHistoryKey, currentHandleData);
|
|
522
233
|
}
|
|
523
234
|
|
|
524
235
|
// Check if we have cached segments for target URL
|
|
@@ -532,7 +243,7 @@ export function createNavigationBridge(
|
|
|
532
243
|
const cachedHandleData = cached?.handleData;
|
|
533
244
|
if (cachedSegments && sourceCached?.segments) {
|
|
534
245
|
const sourceSegmentMap = new Map(
|
|
535
|
-
sourceCached.segments.map((s) => [s.id, s])
|
|
246
|
+
sourceCached.segments.map((s) => [s.id, s]),
|
|
536
247
|
);
|
|
537
248
|
cachedSegments = cachedSegments.map((targetSeg) => {
|
|
538
249
|
const sourceSeg = sourceSegmentMap.get(targetSeg.id);
|
|
@@ -547,28 +258,40 @@ export function createNavigationBridge(
|
|
|
547
258
|
const interceptHistoryKey = generateHistoryKey(url, { intercept: true });
|
|
548
259
|
const hasInterceptCache = store.hasHistoryCache(interceptHistoryKey);
|
|
549
260
|
|
|
550
|
-
// Skip
|
|
261
|
+
// Skip cached SWR for:
|
|
551
262
|
// 1. intercept caches - interception depends on source page context
|
|
552
263
|
// 2. routes that CAN be intercepted - we don't know if this navigation will intercept
|
|
553
264
|
// 3. when leaving intercept - we need fresh non-intercept segments from server
|
|
265
|
+
// 4. redirect-with-state - force re-render so hooks read fresh state
|
|
266
|
+
// 5. stale cache - server action invalidated it, need fresh data with loading state
|
|
554
267
|
const hasUsableCache =
|
|
555
268
|
cachedSegments &&
|
|
556
269
|
cachedSegments.length > 0 &&
|
|
557
270
|
!isInterceptOnlyCache(cachedSegments) &&
|
|
558
271
|
!hasInterceptCache &&
|
|
559
|
-
!isLeavingIntercept
|
|
560
|
-
|
|
561
|
-
|
|
272
|
+
!isLeavingIntercept &&
|
|
273
|
+
!cached?.stale &&
|
|
274
|
+
!options?._skipCache;
|
|
275
|
+
|
|
276
|
+
// Forward navigations always await fetchPartialUpdate before rendering,
|
|
277
|
+
// so useNavigation should always report "loading". skipLoadingState is
|
|
278
|
+
// only used for popstate background revalidation (line ~526) where
|
|
279
|
+
// cached content renders instantly without a network wait.
|
|
280
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
562
281
|
...options,
|
|
563
282
|
state: resolvedState,
|
|
564
|
-
skipLoadingState:
|
|
283
|
+
skipLoadingState: false,
|
|
565
284
|
});
|
|
566
285
|
|
|
567
286
|
// REVALIDATE: Fetch fresh data from server
|
|
568
287
|
try {
|
|
569
288
|
await fetchPartialUpdate(
|
|
570
289
|
url,
|
|
571
|
-
hasUsableCache
|
|
290
|
+
hasUsableCache
|
|
291
|
+
? cachedSegments!.map((s) => s.id)
|
|
292
|
+
: options?._skipCache
|
|
293
|
+
? [] // Action redirect: send no segments so server renders everything fresh
|
|
294
|
+
: undefined,
|
|
572
295
|
false,
|
|
573
296
|
tx.handle.signal,
|
|
574
297
|
tx.with({
|
|
@@ -577,55 +300,69 @@ export function createNavigationBridge(
|
|
|
577
300
|
scroll: options?.scroll,
|
|
578
301
|
state: resolvedState,
|
|
579
302
|
}),
|
|
580
|
-
// Pass cached segments (merged with current page's fresh segments for shared IDs)
|
|
581
|
-
// so the segment map is consistent with what we tell the server we have.
|
|
582
|
-
// Server decides what needs revalidation based on route matching and custom functions.
|
|
583
|
-
// No need for staleRevalidation flag - we're sending the freshest segments we have.
|
|
584
|
-
// Also pass cached handle data for restoring breadcrumbs when server returns empty diff.
|
|
585
|
-
// When leaving intercept, pass the flag so fetchPartialUpdate knows to filter segments.
|
|
586
303
|
hasUsableCache
|
|
587
|
-
? {
|
|
304
|
+
? {
|
|
305
|
+
type: "navigate" as const,
|
|
306
|
+
targetCacheSegments: cachedSegments,
|
|
307
|
+
targetCacheHandleData: cachedHandleData,
|
|
308
|
+
}
|
|
588
309
|
: isLeavingIntercept
|
|
589
|
-
? {
|
|
590
|
-
: undefined
|
|
310
|
+
? { type: "leave-intercept" as const }
|
|
311
|
+
: undefined,
|
|
591
312
|
);
|
|
592
313
|
} catch (error) {
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
314
|
+
// Server-side redirect with location state: the current transaction's
|
|
315
|
+
// cleanup resets loading state. Re-navigate to the redirect
|
|
316
|
+
// target carrying the server-set state into history.pushState.
|
|
317
|
+
if (error instanceof ServerRedirect) {
|
|
318
|
+
const redirectUrl = validateRedirectOrigin(
|
|
319
|
+
error.url,
|
|
320
|
+
window.location.origin,
|
|
321
|
+
);
|
|
322
|
+
if (!redirectUrl) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
return this.navigate(redirectUrl, {
|
|
326
|
+
state: error.state,
|
|
327
|
+
replace: options?.replace,
|
|
328
|
+
_skipCache: true,
|
|
329
|
+
} as NavigateOptionsInternal);
|
|
597
330
|
}
|
|
598
331
|
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
332
|
+
// Aborted, or superseded by a newer navigation. A superseded nav may
|
|
333
|
+
// reject with a non-AbortError (e.g. a Flight decode that fails after its
|
|
334
|
+
// signal was aborted), so check the signal too -- otherwise we would
|
|
335
|
+
// render a boundary that clobbers the newer navigation's content.
|
|
336
|
+
if (
|
|
337
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
338
|
+
tx.handle.signal.aborted
|
|
339
|
+
) {
|
|
340
|
+
debugLog("[Browser] Navigation aborted or superseded");
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
608
343
|
|
|
344
|
+
const networkError = toNetworkError(error, {
|
|
345
|
+
url,
|
|
346
|
+
operation: "navigation",
|
|
347
|
+
});
|
|
348
|
+
if (networkError) {
|
|
609
349
|
console.error(
|
|
610
350
|
"[Browser] Network error during navigation:",
|
|
611
|
-
networkError
|
|
351
|
+
networkError,
|
|
612
352
|
);
|
|
613
|
-
|
|
614
|
-
// Emit update with NetworkErrorThrower to trigger root error boundary
|
|
615
|
-
startTransition(() => {
|
|
616
|
-
onUpdate({
|
|
617
|
-
root: createElement(NetworkErrorThrower, { error: networkError }),
|
|
618
|
-
metadata: {
|
|
619
|
-
pathname: url,
|
|
620
|
-
segments: [],
|
|
621
|
-
isError: true,
|
|
622
|
-
},
|
|
623
|
-
});
|
|
624
|
-
});
|
|
353
|
+
emitNetworkError(onUpdate, networkError, url);
|
|
625
354
|
return;
|
|
626
355
|
}
|
|
627
356
|
|
|
628
|
-
|
|
357
|
+
// A response we could not process (undecodable Flight body, or an
|
|
358
|
+
// unanticipated failure building the response). Surface the route's
|
|
359
|
+
// error boundary rather than let the rejection abort the navigation
|
|
360
|
+
// silently. Prefetched responses funnel here too: a failed warm-prefetch
|
|
361
|
+
// payload rejects on consumption and propagates to this catch.
|
|
362
|
+
console.error("[Browser] Unprocessable navigation response:", error);
|
|
363
|
+
emitNavigationError(onUpdate, error, url);
|
|
364
|
+
} finally {
|
|
365
|
+
tx[Symbol.dispose]();
|
|
629
366
|
}
|
|
630
367
|
},
|
|
631
368
|
|
|
@@ -635,11 +372,11 @@ export function createNavigationBridge(
|
|
|
635
372
|
async refresh(): Promise<void> {
|
|
636
373
|
eventController.abortNavigation();
|
|
637
374
|
|
|
638
|
-
|
|
375
|
+
const tx = createNavigationTransaction(
|
|
639
376
|
store,
|
|
640
377
|
eventController,
|
|
641
378
|
window.location.href,
|
|
642
|
-
{ replace: true }
|
|
379
|
+
{ replace: true },
|
|
643
380
|
);
|
|
644
381
|
|
|
645
382
|
try {
|
|
@@ -649,41 +386,37 @@ export function createNavigationBridge(
|
|
|
649
386
|
[],
|
|
650
387
|
false,
|
|
651
388
|
tx.handle.signal,
|
|
652
|
-
tx.with({ url: window.location.href, replace: true, scroll: false })
|
|
389
|
+
tx.with({ url: window.location.href, replace: true, scroll: false }),
|
|
653
390
|
);
|
|
654
391
|
} catch (error) {
|
|
655
|
-
//
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
{
|
|
663
|
-
cause: error,
|
|
664
|
-
url: window.location.href,
|
|
665
|
-
operation: "revalidation",
|
|
666
|
-
}
|
|
667
|
-
);
|
|
392
|
+
// Aborted or superseded: bail without rendering a boundary (see navigate()).
|
|
393
|
+
if (
|
|
394
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
395
|
+
tx.handle.signal.aborted
|
|
396
|
+
) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
668
399
|
|
|
400
|
+
const networkError = toNetworkError(error, {
|
|
401
|
+
url: window.location.href,
|
|
402
|
+
operation: "revalidation",
|
|
403
|
+
});
|
|
404
|
+
if (networkError) {
|
|
669
405
|
console.error(
|
|
670
406
|
"[Browser] Network error during refresh:",
|
|
671
|
-
networkError
|
|
407
|
+
networkError,
|
|
672
408
|
);
|
|
673
|
-
|
|
674
|
-
startTransition(() => {
|
|
675
|
-
onUpdate({
|
|
676
|
-
root: createElement(NetworkErrorThrower, { error: networkError }),
|
|
677
|
-
metadata: {
|
|
678
|
-
pathname: window.location.href,
|
|
679
|
-
segments: [],
|
|
680
|
-
isError: true,
|
|
681
|
-
},
|
|
682
|
-
});
|
|
683
|
-
});
|
|
409
|
+
emitNetworkError(onUpdate, networkError, window.location.href);
|
|
684
410
|
return;
|
|
685
411
|
}
|
|
686
|
-
|
|
412
|
+
|
|
413
|
+
// refresh() shares the fetchPartialUpdate chokepoint with navigate()/
|
|
414
|
+
// popstate, so an unprocessable response must surface the error boundary
|
|
415
|
+
// here too rather than become an uncaught rejection.
|
|
416
|
+
console.error("[Browser] Unprocessable refresh response:", error);
|
|
417
|
+
emitNavigationError(onUpdate, error, window.location.href);
|
|
418
|
+
} finally {
|
|
419
|
+
tx[Symbol.dispose]();
|
|
687
420
|
}
|
|
688
421
|
},
|
|
689
422
|
|
|
@@ -707,20 +440,29 @@ export function createNavigationBridge(
|
|
|
707
440
|
const currentInterceptSource = store.getInterceptSourceUrl();
|
|
708
441
|
const newInterceptSource = interceptSourceUrl ?? null;
|
|
709
442
|
if (currentInterceptSource !== newInterceptSource) {
|
|
710
|
-
|
|
711
|
-
`[Browser] Intercept context changing (${currentInterceptSource} -> ${newInterceptSource}), aborting in-flight actions
|
|
443
|
+
debugLog(
|
|
444
|
+
`[Browser] Intercept context changing (${currentInterceptSource} -> ${newInterceptSource}), aborting in-flight actions`,
|
|
712
445
|
);
|
|
713
446
|
eventController.abortAllActions();
|
|
714
447
|
}
|
|
715
448
|
|
|
449
|
+
// Popstate that exits an intercept to a non-intercept destination. The
|
|
450
|
+
// fallback fetch path below needs `leave-intercept` mode so it filters
|
|
451
|
+
// the cached @modal segment from the request and forces a re-render —
|
|
452
|
+
// otherwise a cache-miss popstate whose server response has an empty
|
|
453
|
+
// diff hits the "no changes" branch in partial-update and the modal
|
|
454
|
+
// stays on screen.
|
|
455
|
+
const isLeavingIntercept =
|
|
456
|
+
!isIntercept && currentInterceptSource !== null;
|
|
457
|
+
|
|
716
458
|
// Compute history key from URL (with intercept suffix if applicable)
|
|
717
459
|
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
718
460
|
|
|
719
|
-
|
|
461
|
+
debugLog(
|
|
720
462
|
"[Browser] Popstate -",
|
|
721
463
|
isIntercept ? "intercept" : "normal",
|
|
722
464
|
"key:",
|
|
723
|
-
historyKey
|
|
465
|
+
historyKey,
|
|
724
466
|
);
|
|
725
467
|
|
|
726
468
|
// Update location in event controller
|
|
@@ -736,11 +478,22 @@ export function createNavigationBridge(
|
|
|
736
478
|
// Helper to check if streaming is in progress
|
|
737
479
|
const isStreaming = () => eventController.getState().isStreaming;
|
|
738
480
|
|
|
481
|
+
// Surface any external rotation of the rango state cookie (a server
|
|
482
|
+
// Set-Cookie, a sibling tab, a cookie clear) BEFORE reading the stale bit.
|
|
483
|
+
// The divergence observer only runs inside getRangoState() — fetch-time —
|
|
484
|
+
// so a popstate-first interaction would otherwise serve a pre-mutation
|
|
485
|
+
// page as fresh and never fetch to trigger the observer. Reading here lets
|
|
486
|
+
// the observer mark the history cache stale so getCachedSegments sees it.
|
|
487
|
+
getRangoState();
|
|
488
|
+
|
|
739
489
|
// Check if we can restore from history cache
|
|
740
490
|
const cached = store.getCachedSegments(historyKey);
|
|
741
491
|
const cachedSegments = cached?.segments;
|
|
742
492
|
const cachedHandleData = cached?.handleData;
|
|
743
|
-
|
|
493
|
+
// While an action is in flight the fence persists no stale flag, so OR it
|
|
494
|
+
// in here: a popstate during the flight serves the cached entry AND
|
|
495
|
+
// revalidates (SWR) instead of serving it as fresh.
|
|
496
|
+
const isStale = (cached?.stale ?? false) || isActionFenceActive();
|
|
744
497
|
|
|
745
498
|
if (cachedSegments && cachedSegments.length > 0) {
|
|
746
499
|
// Update store to point to this history entry
|
|
@@ -749,12 +502,27 @@ export function createNavigationBridge(
|
|
|
749
502
|
store.setCurrentUrl(url);
|
|
750
503
|
store.setPath(new URL(url).pathname);
|
|
751
504
|
|
|
505
|
+
// Restore router identity from cache so subsequent navigations
|
|
506
|
+
// don't falsely detect an app switch.
|
|
507
|
+
if (cached?.routerId) {
|
|
508
|
+
store.setRouterId?.(cached.routerId);
|
|
509
|
+
}
|
|
510
|
+
|
|
752
511
|
// Render from cache - force await to skip loading fallbacks
|
|
753
512
|
try {
|
|
754
|
-
const root = renderSegments(cachedSegments, {
|
|
513
|
+
const root = await renderSegments(cachedSegments, {
|
|
755
514
|
forceAwait: true,
|
|
756
515
|
});
|
|
757
|
-
|
|
516
|
+
// Merge params from cached segments for useParams restoration.
|
|
517
|
+
// Set params on event controller before onUpdate so both location
|
|
518
|
+
// and params are current when the debounced notify() fires.
|
|
519
|
+
const cachedParams: Record<string, string> = {};
|
|
520
|
+
for (const s of cachedSegments) {
|
|
521
|
+
if (s.params) Object.assign(cachedParams, s.params);
|
|
522
|
+
}
|
|
523
|
+
eventController.setParams(cachedParams);
|
|
524
|
+
|
|
525
|
+
const popstateUpdate = {
|
|
758
526
|
root,
|
|
759
527
|
metadata: {
|
|
760
528
|
pathname: new URL(url).pathname,
|
|
@@ -763,23 +531,51 @@ export function createNavigationBridge(
|
|
|
763
531
|
matched: cachedSegments.map((s) => s.id),
|
|
764
532
|
diff: [],
|
|
765
533
|
cachedHandleData,
|
|
534
|
+
params: cachedParams,
|
|
766
535
|
},
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
//
|
|
770
|
-
|
|
536
|
+
scroll: { restore: true, isStreaming },
|
|
537
|
+
};
|
|
538
|
+
// Intercept-driven popstate (entering OR leaving an intercept) only
|
|
539
|
+
// mutates the parallel slot; the main outlet shows the same content.
|
|
540
|
+
// Skip startViewTransition in those cases — same rationale as the
|
|
541
|
+
// intercept guard in partial-update.ts's hasTransition computation.
|
|
542
|
+
const hasTransition =
|
|
543
|
+
!isIntercept &&
|
|
544
|
+
!isLeavingIntercept &&
|
|
545
|
+
cachedSegments.some((s) => s.transition);
|
|
546
|
+
if (hasTransition) {
|
|
547
|
+
startTransition(() => {
|
|
548
|
+
if (addTransitionType) {
|
|
549
|
+
addTransitionType("navigation-back");
|
|
550
|
+
}
|
|
551
|
+
onUpdate(popstateUpdate);
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
onUpdate(popstateUpdate);
|
|
555
|
+
}
|
|
771
556
|
|
|
772
557
|
// SWR: If stale, trigger background revalidation
|
|
773
558
|
if (isStale) {
|
|
774
|
-
|
|
775
|
-
// Background revalidation - don't await, just fire and forget
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
559
|
+
debugLog("[Browser] Cache is stale, background revalidating...");
|
|
560
|
+
// Background revalidation - don't await, just fire and forget.
|
|
561
|
+
// When the entry's handles are incomplete (a deferred Meta was still
|
|
562
|
+
// pending when the user navigated away — see handlesPending), send NO
|
|
563
|
+
// segment IDs so the server returns a FULL re-render with the handle
|
|
564
|
+
// stream. A normal stale revalidation sends the cached IDs and the
|
|
565
|
+
// server returns a diff-only payload that omits unchanged segments'
|
|
566
|
+
// handles, so a deferred Meta would never re-stream and the title
|
|
567
|
+
// would stay the pre-resolution carry. handlesPending is set only for
|
|
568
|
+
// the deferred-Meta-aborted case, so action/cross-tab SWR keeps the
|
|
569
|
+
// cheap diff path.
|
|
570
|
+
const segmentIds = cached?.handlesPending
|
|
571
|
+
? []
|
|
572
|
+
: cachedSegments.map((s) => s.id);
|
|
573
|
+
|
|
574
|
+
const tx = createNavigationTransaction(
|
|
779
575
|
store,
|
|
780
576
|
eventController,
|
|
781
577
|
url,
|
|
782
|
-
{ skipLoadingState: true, replace: true }
|
|
578
|
+
{ skipLoadingState: true, replace: true },
|
|
783
579
|
);
|
|
784
580
|
|
|
785
581
|
fetchPartialUpdate(
|
|
@@ -795,41 +591,33 @@ export function createNavigationBridge(
|
|
|
795
591
|
interceptSourceUrl,
|
|
796
592
|
cacheOnly: true,
|
|
797
593
|
}),
|
|
798
|
-
{
|
|
799
|
-
)
|
|
800
|
-
|
|
801
|
-
error
|
|
802
|
-
error
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
// For background revalidation, network errors are logged but don't trigger error boundary
|
|
808
|
-
// since the user is already seeing cached content
|
|
809
|
-
if (error instanceof NetworkError || isNetworkError(error)) {
|
|
810
|
-
console.warn(
|
|
811
|
-
"[Browser] Background revalidation network error (cached content preserved):",
|
|
812
|
-
error.message
|
|
594
|
+
{ type: "stale-revalidation", interceptSourceUrl },
|
|
595
|
+
)
|
|
596
|
+
.catch((error) => {
|
|
597
|
+
if (isBackgroundSuppressible(error)) return;
|
|
598
|
+
console.error(
|
|
599
|
+
"[Browser] Background revalidation failed:",
|
|
600
|
+
error,
|
|
813
601
|
);
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
602
|
+
})
|
|
603
|
+
.finally(() => {
|
|
604
|
+
tx[Symbol.dispose]();
|
|
605
|
+
});
|
|
818
606
|
}
|
|
819
607
|
return;
|
|
820
608
|
} catch (error) {
|
|
821
609
|
console.warn(
|
|
822
610
|
"[Browser] Failed to render from cache, fetching:",
|
|
823
|
-
error
|
|
611
|
+
error,
|
|
824
612
|
);
|
|
825
613
|
// Fall through to fetch
|
|
826
614
|
}
|
|
827
615
|
} else {
|
|
828
|
-
|
|
616
|
+
debugLog("[Browser] History cache miss for key:", historyKey);
|
|
829
617
|
}
|
|
830
618
|
|
|
831
619
|
// Fetch if not cached
|
|
832
|
-
|
|
620
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
833
621
|
replace: true,
|
|
834
622
|
});
|
|
835
623
|
|
|
@@ -839,45 +627,51 @@ export function createNavigationBridge(
|
|
|
839
627
|
undefined,
|
|
840
628
|
false,
|
|
841
629
|
tx.handle.signal,
|
|
842
|
-
tx.with({
|
|
630
|
+
tx.with({
|
|
631
|
+
url,
|
|
632
|
+
replace: true,
|
|
633
|
+
scroll: false,
|
|
634
|
+
intercept: isIntercept,
|
|
635
|
+
interceptSourceUrl,
|
|
636
|
+
}),
|
|
637
|
+
isIntercept
|
|
638
|
+
? { type: "navigate", interceptSourceUrl }
|
|
639
|
+
: isLeavingIntercept
|
|
640
|
+
? { type: "leave-intercept" }
|
|
641
|
+
: undefined,
|
|
843
642
|
);
|
|
844
643
|
// Restore scroll position after fetch completes
|
|
845
644
|
handleNavigationEnd({ restore: true, isStreaming });
|
|
846
645
|
} catch (error) {
|
|
847
|
-
|
|
848
|
-
|
|
646
|
+
// Aborted or superseded by a newer navigation: bail without clobbering
|
|
647
|
+
// content with a boundary (see navigate()).
|
|
648
|
+
if (
|
|
649
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
650
|
+
tx.handle.signal.aborted
|
|
651
|
+
) {
|
|
652
|
+
debugLog("[Browser] Popstate navigation aborted or superseded");
|
|
849
653
|
return;
|
|
850
654
|
}
|
|
851
655
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
: new NetworkError(
|
|
858
|
-
"Unable to connect to server. Please check your connection.",
|
|
859
|
-
{ cause: error, url, operation: "navigation" }
|
|
860
|
-
);
|
|
861
|
-
|
|
656
|
+
const networkError = toNetworkError(error, {
|
|
657
|
+
url,
|
|
658
|
+
operation: "navigation",
|
|
659
|
+
});
|
|
660
|
+
if (networkError) {
|
|
862
661
|
console.error(
|
|
863
|
-
"[Browser] Network error during popstate
|
|
864
|
-
networkError
|
|
662
|
+
"[Browser] Network error during popstate:",
|
|
663
|
+
networkError,
|
|
865
664
|
);
|
|
866
|
-
|
|
867
|
-
startTransition(() => {
|
|
868
|
-
onUpdate({
|
|
869
|
-
root: createElement(NetworkErrorThrower, { error: networkError }),
|
|
870
|
-
metadata: {
|
|
871
|
-
pathname: url,
|
|
872
|
-
segments: [],
|
|
873
|
-
isError: true,
|
|
874
|
-
},
|
|
875
|
-
});
|
|
876
|
-
});
|
|
665
|
+
emitNetworkError(onUpdate, networkError, url);
|
|
877
666
|
return;
|
|
878
667
|
}
|
|
879
668
|
|
|
880
|
-
|
|
669
|
+
// Unprocessable response on a back/forward navigation: surface the
|
|
670
|
+
// error boundary instead of an uncaught rejection (see navigate()).
|
|
671
|
+
console.error("[Browser] Unprocessable popstate response:", error);
|
|
672
|
+
emitNavigationError(onUpdate, error, url);
|
|
673
|
+
} finally {
|
|
674
|
+
tx[Symbol.dispose]();
|
|
881
675
|
}
|
|
882
676
|
},
|
|
883
677
|
|
|
@@ -904,8 +698,13 @@ export function createNavigationBridge(
|
|
|
904
698
|
// Abort the stale navigation to reset state to idle.
|
|
905
699
|
const handlePageShow = (event: PageTransitionEvent) => {
|
|
906
700
|
if (event.persisted) {
|
|
907
|
-
|
|
701
|
+
debugLog(
|
|
702
|
+
"[Browser] Page restored from bfcache, resetting navigation state",
|
|
703
|
+
);
|
|
908
704
|
eventController.abortNavigation();
|
|
705
|
+
// pagehide flips scrollRestoration to "auto" for bfcache compat;
|
|
706
|
+
// restore "manual" so the router controls scroll on SPA navigations.
|
|
707
|
+
window.history.scrollRestoration = "manual";
|
|
909
708
|
}
|
|
910
709
|
};
|
|
911
710
|
|
|
@@ -916,7 +715,7 @@ export function createNavigationBridge(
|
|
|
916
715
|
|
|
917
716
|
window.addEventListener("popstate", handlePopstate);
|
|
918
717
|
window.addEventListener("pageshow", handlePageShow);
|
|
919
|
-
|
|
718
|
+
debugLog("[Browser] Navigation bridge ready");
|
|
920
719
|
|
|
921
720
|
return () => {
|
|
922
721
|
cleanupLinks();
|
|
@@ -924,6 +723,16 @@ export function createNavigationBridge(
|
|
|
924
723
|
window.removeEventListener("pageshow", handlePageShow);
|
|
925
724
|
};
|
|
926
725
|
},
|
|
726
|
+
|
|
727
|
+
getVersion(): string | undefined {
|
|
728
|
+
return version;
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
updateVersion(newVersion: string): void {
|
|
732
|
+
version = newVersion;
|
|
733
|
+
setAppVersion(newVersion);
|
|
734
|
+
store.clearHistoryCache();
|
|
735
|
+
},
|
|
927
736
|
};
|
|
928
737
|
}
|
|
929
738
|
|