@rangojs/router 0.0.0-experimental.19 → 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 +17 -0
- package/README.md +291 -61
- package/dist/bin/rango.js +544 -143
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3744 -1329
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +67 -13
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +312 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +322 -19
- 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 +282 -60
- package/skills/host-router/SKILL.md +278 -0
- 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 +297 -31
- 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 +155 -111
- package/skills/rango/SKILL.md +251 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +104 -9
- package/skills/router-setup/SKILL.md +124 -11
- 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 +125 -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 +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 +357 -52
- 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 +67 -40
- package/src/bin/rango.ts +18 -0
- 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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +200 -30
- package/src/browser/navigation-client.ts +217 -58
- 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 +312 -30
- package/src/browser/prefetch/fetch.ts +344 -47
- package/src/browser/prefetch/policy.ts +6 -0
- 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 +125 -18
- 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 +226 -75
- 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 +619 -442
- package/src/browser/types.ts +115 -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 +7 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +182 -37
- 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 +392 -106
- 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 +45 -21
- package/src/client.tsx +120 -336
- 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 +95 -0
- 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 -5
- 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 +79 -29
- package/src/index.ts +149 -65
- 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 +63 -26
- 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 -3
- 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 +110 -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 +121 -205
- 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 -49
- package/src/router/metrics.ts +217 -26
- package/src/router/middleware-types.ts +24 -110
- package/src/router/middleware.ts +384 -197
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +148 -91
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +199 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +144 -74
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +129 -36
- package/src/router/router-options.ts +185 -23
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +281 -76
- package/src/router/segment-resolution/helpers.ts +116 -31
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +493 -391
- 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 +180 -58
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +182 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +702 -460
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -25
- 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 +98 -19
- 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 +126 -106
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +269 -114
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +34 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +49 -41
- 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 +317 -63
- 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 +425 -177
- package/src/server.ts +6 -0
- 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 +3 -19
- 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 +236 -88
- 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 +10 -45
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -19
- 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 +64 -91
- package/src/vite/discovery/discover-routers.ts +147 -88
- 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 +247 -145
- 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 +61 -13
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +10 -3
- 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 +120 -0
- 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 +127 -0
- 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 +241 -287
- package/src/vite/router-discovery.ts +956 -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 +141 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/CLAUDE.md +0 -5
- 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 -6
- package/src/network-error-thrower.tsx +0 -23
- package/src/route-definition/route-function.ts +0 -119
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
generateHistoryKey,
|
|
9
9
|
} from "./navigation-store.js";
|
|
10
10
|
import { createEventController } from "./event-controller.js";
|
|
11
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
11
12
|
import { createNavigationClient } from "./navigation-client.js";
|
|
12
13
|
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
14
|
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
@@ -22,10 +23,15 @@ import type {
|
|
|
22
23
|
import type { EventController } from "./event-controller.js";
|
|
23
24
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
24
25
|
import { initRangoState } from "./rango-state.js";
|
|
26
|
+
import { registerNavigationStore } from "./navigation-store-handle.js";
|
|
27
|
+
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
28
|
+
import { setPrefetchDecoder } from "./prefetch/fetch.js";
|
|
29
|
+
import { setAppVersion } from "./app-version.js";
|
|
25
30
|
import {
|
|
26
31
|
isInterceptSegment,
|
|
27
32
|
splitInterceptSegments,
|
|
28
33
|
} from "./intercept-utils.js";
|
|
34
|
+
import { createAppShellRef } from "./app-shell.js";
|
|
29
35
|
|
|
30
36
|
// Vite HMR types are provided by vite/client
|
|
31
37
|
|
|
@@ -110,15 +116,26 @@ export interface BrowserAppContext {
|
|
|
110
116
|
initialTheme?: Theme;
|
|
111
117
|
/** Whether connection warmup is enabled */
|
|
112
118
|
warmupEnabled?: boolean;
|
|
119
|
+
/** Whether the hydrated tree should be wrapped in React.StrictMode */
|
|
120
|
+
strictMode?: boolean;
|
|
113
121
|
/** App version for prefetch version mismatch detection */
|
|
114
122
|
version?: string;
|
|
123
|
+
/**
|
|
124
|
+
* App-shell ref, read through on each render so renderSegments and the
|
|
125
|
+
* NavigationProvider see rootLayout/basename/version without closing over a
|
|
126
|
+
* stale snapshot. Set once from the initial payload and not swapped within a
|
|
127
|
+
* session: a cross-app navigation is a full document load (X-RSC-Reload), so
|
|
128
|
+
* the target app establishes its own shell on load. Theme, warmup, and
|
|
129
|
+
* prefetch TTL are document-lifetime too (see AppShell).
|
|
130
|
+
*/
|
|
131
|
+
appShellRef?: import("./app-shell.js").AppShellRef;
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
// Module-level state for the initialized app
|
|
118
135
|
let browserAppContext: BrowserAppContext | null = null;
|
|
119
136
|
|
|
120
137
|
/**
|
|
121
|
-
* Initialize the browser app. Must be called before rendering
|
|
138
|
+
* Initialize the browser app. Must be called before rendering Rango.
|
|
122
139
|
*
|
|
123
140
|
* This function:
|
|
124
141
|
* - Loads the initial RSC payload from the stream
|
|
@@ -138,7 +155,6 @@ export async function initBrowserApp(
|
|
|
138
155
|
initialTheme,
|
|
139
156
|
} = options;
|
|
140
157
|
|
|
141
|
-
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
142
158
|
const initialPayload =
|
|
143
159
|
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
144
160
|
|
|
@@ -163,6 +179,18 @@ export async function initBrowserApp(
|
|
|
163
179
|
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
164
180
|
});
|
|
165
181
|
|
|
182
|
+
// Register the active store on the module-level handle and wire the
|
|
183
|
+
// jar-divergence observer before any getRangoState() read can detect a
|
|
184
|
+
// cross-tab/server rotation. There is no global store singleton, so this
|
|
185
|
+
// handle is the live reference.
|
|
186
|
+
registerNavigationStore(store);
|
|
187
|
+
|
|
188
|
+
// Seed router identity from the initial SSR payload so the first
|
|
189
|
+
// cross-app SPA navigation can detect the app switch.
|
|
190
|
+
if (initialPayload.metadata?.routerId) {
|
|
191
|
+
store.setRouterId?.(initialPayload.metadata.routerId);
|
|
192
|
+
}
|
|
193
|
+
|
|
166
194
|
// Create event controller for reactive state management
|
|
167
195
|
const eventController = createEventController({
|
|
168
196
|
initialLocation: new URL(window.location.href),
|
|
@@ -197,19 +225,49 @@ export async function initBrowserApp(
|
|
|
197
225
|
// Create composable utilities
|
|
198
226
|
const client = createNavigationClient(deps);
|
|
199
227
|
|
|
200
|
-
//
|
|
201
|
-
|
|
228
|
+
// Capture the per-router app-shell. rootLayout, basename, and version live
|
|
229
|
+
// here and are read through the ref at call time rather than closed over.
|
|
230
|
+
// It is set once from the initial payload and not swapped within a session:
|
|
231
|
+
// a cross-app navigation is a full document load (X-RSC-Reload), so the
|
|
232
|
+
// target app establishes its own shell on load.
|
|
202
233
|
const version = initialPayload.metadata?.version;
|
|
234
|
+
const appShellRef = createAppShellRef({
|
|
235
|
+
routerId: initialPayload.metadata?.routerId,
|
|
236
|
+
rootLayout: initialPayload.metadata?.rootLayout,
|
|
237
|
+
basename: initialPayload.metadata?.basename,
|
|
238
|
+
version,
|
|
239
|
+
});
|
|
203
240
|
|
|
204
|
-
// Initialize the
|
|
205
|
-
//
|
|
206
|
-
|
|
241
|
+
// Initialize the rango state cookie for cache invalidation. The build version
|
|
242
|
+
// busts cached prefetches on deploy; the server-resolved cookie name
|
|
243
|
+
// namespaces the cookie so sibling apps on the same origin don't collide
|
|
244
|
+
// (falls back to the bare default prefix if metadata lacks the name).
|
|
245
|
+
initRangoState(version ?? "0", initialPayload.metadata?.stateCookieName);
|
|
246
|
+
setAppVersion(version);
|
|
247
|
+
|
|
248
|
+
// Initialize the in-memory prefetch cache TTL from server config.
|
|
249
|
+
// A value of 0 disables the cache; undefined falls back to the module default.
|
|
250
|
+
const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
|
|
251
|
+
if (prefetchCacheTTL !== undefined) {
|
|
252
|
+
initPrefetchCache(prefetchCacheTTL);
|
|
253
|
+
}
|
|
207
254
|
|
|
208
|
-
//
|
|
255
|
+
// Wire the RSC decoder so prefetches decode eagerly and warm the route's
|
|
256
|
+
// client chunks (same createFromFetch the navigation client uses).
|
|
257
|
+
setPrefetchDecoder((response) => deps.createFromFetch<RscPayload>(response));
|
|
258
|
+
|
|
259
|
+
// Create a bound renderSegments that reads rootLayout through the shell ref.
|
|
260
|
+
// The shell is set once at init and not swapped within a session (a cross-app
|
|
261
|
+
// navigation is a full document load), so this always renders this app's
|
|
262
|
+
// Document; reading through the ref just avoids closing over a stale value.
|
|
209
263
|
const renderSegments = (
|
|
210
264
|
segments: ResolvedSegment[],
|
|
211
265
|
options?: RenderSegmentsOptions,
|
|
212
|
-
) =>
|
|
266
|
+
) =>
|
|
267
|
+
baseRenderSegments(segments, {
|
|
268
|
+
...options,
|
|
269
|
+
rootLayout: appShellRef.get().rootLayout,
|
|
270
|
+
});
|
|
213
271
|
|
|
214
272
|
// Lazy reference for navigation bridge — the action bridge is created first
|
|
215
273
|
// but may need to trigger SPA navigation for action redirects.
|
|
@@ -223,10 +281,15 @@ export async function initBrowserApp(
|
|
|
223
281
|
deps,
|
|
224
282
|
onUpdate: (update) => store.emitUpdate(update),
|
|
225
283
|
renderSegments,
|
|
226
|
-
version,
|
|
227
284
|
onNavigate: (url, options) => {
|
|
228
285
|
if (!navigateFn) {
|
|
229
|
-
|
|
286
|
+
// Navigation bridge not wired yet: hard-navigate, but re-validate
|
|
287
|
+
// same-origin defensively so this init-window fallback cannot become an
|
|
288
|
+
// open redirect (the normal path validates inside the navigation bridge).
|
|
289
|
+
const safe = validateRedirectOrigin(url, window.location.origin);
|
|
290
|
+
if (safe) {
|
|
291
|
+
window.location.href = safe;
|
|
292
|
+
}
|
|
230
293
|
return Promise.resolve();
|
|
231
294
|
}
|
|
232
295
|
return navigateFn(url, options);
|
|
@@ -241,7 +304,7 @@ export async function initBrowserApp(
|
|
|
241
304
|
client,
|
|
242
305
|
onUpdate: (update) => store.emitUpdate(update),
|
|
243
306
|
renderSegments,
|
|
244
|
-
version,
|
|
307
|
+
version: version,
|
|
245
308
|
});
|
|
246
309
|
|
|
247
310
|
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
@@ -255,74 +318,157 @@ export async function initBrowserApp(
|
|
|
255
318
|
// Build initial tree with rootLayout
|
|
256
319
|
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
257
320
|
|
|
258
|
-
// Setup HMR
|
|
321
|
+
// Setup HMR with debounce — burst saves (format-on-save, rapid edits)
|
|
322
|
+
// fire many rsc:update events in quick succession. Without debouncing,
|
|
323
|
+
// each event triggers a fetchPartial() which on slow routes can pile up
|
|
324
|
+
// and overwhelm the worker (cross-request promise issues, 500s).
|
|
259
325
|
if (import.meta.hot) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
326
|
+
let hmrTimer: ReturnType<typeof setTimeout> | null = null;
|
|
327
|
+
let hmrAbort: AbortController | null = null;
|
|
328
|
+
|
|
329
|
+
import.meta.hot.on("rsc:update", () => {
|
|
330
|
+
// Cancel any pending debounce timer
|
|
331
|
+
if (hmrTimer !== null) {
|
|
332
|
+
clearTimeout(hmrTimer);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Abort any in-flight HMR fetch so it doesn't race with the next one
|
|
336
|
+
if (hmrAbort) {
|
|
337
|
+
hmrAbort.abort();
|
|
338
|
+
hmrAbort = null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Debounce: wait 200ms of quiet before fetching
|
|
342
|
+
hmrTimer = setTimeout(async () => {
|
|
343
|
+
hmrTimer = null;
|
|
344
|
+
|
|
345
|
+
// Don't interrupt an active user navigation — startNavigation()
|
|
346
|
+
// would abort it and refetch the old URL (window.location.href
|
|
347
|
+
// hasn't updated yet). The user's navigation will pick up the
|
|
348
|
+
// new server code when it completes. isNavigating covers the
|
|
349
|
+
// full lifecycle (fetching + streaming, before commit) without
|
|
350
|
+
// blocking on server actions.
|
|
351
|
+
if (eventController.getState().isNavigating) {
|
|
352
|
+
console.log("[Rango] HMR: Skipping — navigation in progress");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
console.log("[Rango] HMR: Server update, refetching RSC");
|
|
357
|
+
|
|
358
|
+
const abort = new AbortController();
|
|
359
|
+
hmrAbort = abort;
|
|
360
|
+
|
|
361
|
+
const handle = eventController.startNavigation(window.location.href, {
|
|
362
|
+
replace: true,
|
|
277
363
|
});
|
|
364
|
+
const streamingToken = handle.startStreaming();
|
|
365
|
+
|
|
366
|
+
const interceptSourceUrl = store.getInterceptSourceUrl();
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const { payload, streamComplete } = await client.fetchPartial({
|
|
370
|
+
targetUrl: window.location.href,
|
|
371
|
+
segmentIds: [],
|
|
372
|
+
previousUrl: store.getSegmentState().currentUrl,
|
|
373
|
+
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
374
|
+
routerId: store.getRouterId?.(),
|
|
375
|
+
hmr: true,
|
|
376
|
+
signal: abort.signal,
|
|
377
|
+
});
|
|
278
378
|
|
|
279
|
-
|
|
280
|
-
const segments = payload.metadata.segments || [];
|
|
281
|
-
const matched = payload.metadata.matched || [];
|
|
379
|
+
if (abort.signal.aborted) return;
|
|
282
380
|
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
381
|
+
// If the server returned a non-RSC response (404, 500 without
|
|
382
|
+
// error boundary), the payload won't have valid metadata.
|
|
383
|
+
// Reload to recover rather than leaving the page stale.
|
|
384
|
+
if (!payload.metadata) {
|
|
385
|
+
throw new Error("HMR refetch returned invalid payload");
|
|
386
|
+
}
|
|
287
387
|
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
388
|
+
// Update version BEFORE rebuilding state so that
|
|
389
|
+
// clearHistoryCache() runs first, then the fresh segment
|
|
390
|
+
// cache entry we create below survives.
|
|
391
|
+
//
|
|
392
|
+
// Compare against the bridge's live version, not the init-time
|
|
393
|
+
// `version` const: after the first HMR bump the const is stale, so a
|
|
394
|
+
// later update with an unchanged version would otherwise re-clear the
|
|
395
|
+
// cache and re-broadcast across tabs/apps. The live read fires only
|
|
396
|
+
// on a genuine version change.
|
|
397
|
+
const newVersion = payload.metadata.version;
|
|
398
|
+
const currentVersion = navigationBridge.getVersion();
|
|
399
|
+
if (newVersion && newVersion !== currentVersion) {
|
|
400
|
+
console.log(
|
|
401
|
+
"[Rango] HMR: version changed",
|
|
402
|
+
currentVersion,
|
|
403
|
+
"→",
|
|
404
|
+
newVersion,
|
|
405
|
+
"clearing caches",
|
|
406
|
+
);
|
|
407
|
+
navigationBridge.updateVersion(newVersion);
|
|
291
408
|
}
|
|
292
409
|
|
|
293
|
-
|
|
294
|
-
|
|
410
|
+
// Apply only partial segment updates. A non-partial payload during
|
|
411
|
+
// HMR is transient: the worker route table is still rebuilding after
|
|
412
|
+
// the edit, so the URL momentarily resolves to not-found/catch-all.
|
|
413
|
+
// Skip it -- the debounced follow-up refetch returns the settled
|
|
414
|
+
// route's partial payload and renders it below. We never reload here:
|
|
415
|
+
// a paramless document GET would run the SSR path and surface the
|
|
416
|
+
// not-found page during that same transient.
|
|
417
|
+
if (payload.metadata?.isPartial) {
|
|
418
|
+
const segments = payload.metadata.segments || [];
|
|
419
|
+
const matched = payload.metadata.matched || [];
|
|
420
|
+
|
|
421
|
+
// Derive intercept state from the returned payload, not the
|
|
422
|
+
// pre-fetch store snapshot. If the HMR edit removed intercept
|
|
423
|
+
// behavior, the response won't contain intercept segments.
|
|
424
|
+
const responseIsIntercept = segments.some(isInterceptSegment);
|
|
425
|
+
|
|
426
|
+
// Sync store intercept state with what the server returned
|
|
427
|
+
if (!responseIsIntercept && interceptSourceUrl) {
|
|
428
|
+
store.setInterceptSourceUrl(null);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
store.setSegmentIds(matched);
|
|
432
|
+
store.setCurrentUrl(window.location.href);
|
|
433
|
+
|
|
434
|
+
const historyKey = generateHistoryKey(window.location.href, {
|
|
435
|
+
intercept: responseIsIntercept,
|
|
436
|
+
});
|
|
437
|
+
store.setHistoryKey(historyKey);
|
|
438
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
439
|
+
store.cacheSegmentsForHistory(
|
|
440
|
+
historyKey,
|
|
441
|
+
segments,
|
|
442
|
+
currentHandleData,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const { main, intercept } = splitInterceptSegments(segments);
|
|
446
|
+
store.emitUpdate({
|
|
447
|
+
root: renderSegments(main, {
|
|
448
|
+
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
449
|
+
}),
|
|
450
|
+
metadata: payload.metadata,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
295
453
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
store.emitUpdate({
|
|
309
|
-
root: renderSegments(main, {
|
|
310
|
-
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
311
|
-
}),
|
|
312
|
-
metadata: payload.metadata,
|
|
313
|
-
});
|
|
454
|
+
await streamComplete;
|
|
455
|
+
handle.complete(new URL(window.location.href));
|
|
456
|
+
console.log("[Rango] HMR: RSC stream complete");
|
|
457
|
+
} catch (err) {
|
|
458
|
+
if (abort.signal.aborted) return;
|
|
459
|
+
console.warn("[Rango] HMR: Refetch failed, reloading page", err);
|
|
460
|
+
window.location.reload();
|
|
461
|
+
return;
|
|
462
|
+
} finally {
|
|
463
|
+
if (hmrAbort === abort) hmrAbort = null;
|
|
464
|
+
streamingToken.end();
|
|
465
|
+
handle[Symbol.dispose]();
|
|
314
466
|
}
|
|
315
|
-
|
|
316
|
-
await streamComplete;
|
|
317
|
-
handle.complete(new URL(window.location.href));
|
|
318
|
-
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
319
|
-
} finally {
|
|
320
|
-
streamingToken.end();
|
|
321
|
-
}
|
|
467
|
+
}, 200);
|
|
322
468
|
});
|
|
323
469
|
}
|
|
324
470
|
|
|
325
|
-
// Store context for
|
|
471
|
+
// Store context for Rango component
|
|
326
472
|
const context: BrowserAppContext = {
|
|
327
473
|
store,
|
|
328
474
|
eventController,
|
|
@@ -332,7 +478,9 @@ export async function initBrowserApp(
|
|
|
332
478
|
themeConfig: effectiveThemeConfig,
|
|
333
479
|
initialTheme: effectiveInitialTheme,
|
|
334
480
|
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
481
|
+
strictMode: initialPayload.metadata?.strictMode ?? true,
|
|
335
482
|
version,
|
|
483
|
+
appShellRef,
|
|
336
484
|
};
|
|
337
485
|
browserAppContext = context;
|
|
338
486
|
|
|
@@ -345,7 +493,7 @@ export async function initBrowserApp(
|
|
|
345
493
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
346
494
|
if (!browserAppContext) {
|
|
347
495
|
throw new Error(
|
|
348
|
-
"
|
|
496
|
+
"Rango: initBrowserApp() must be called before rendering Rango",
|
|
349
497
|
);
|
|
350
498
|
}
|
|
351
499
|
return browserAppContext;
|
|
@@ -359,18 +507,18 @@ export function resetBrowserAppContext(): void {
|
|
|
359
507
|
}
|
|
360
508
|
|
|
361
509
|
/**
|
|
362
|
-
* Props for the
|
|
510
|
+
* Props for the Rango component
|
|
363
511
|
*/
|
|
364
|
-
export interface
|
|
512
|
+
export interface RangoProps {}
|
|
365
513
|
|
|
366
514
|
/**
|
|
367
|
-
*
|
|
515
|
+
* Rango component - renders the RSC router with all internal wiring.
|
|
368
516
|
*
|
|
369
517
|
* Must be called after initBrowserApp() has completed.
|
|
370
518
|
*
|
|
371
519
|
* @example
|
|
372
520
|
* ```tsx
|
|
373
|
-
* import { initBrowserApp,
|
|
521
|
+
* import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
374
522
|
* import { rscStream } from "rsc-html-stream/client";
|
|
375
523
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
376
524
|
*
|
|
@@ -380,14 +528,14 @@ export interface RSCRouterProps {}
|
|
|
380
528
|
* hydrateRoot(
|
|
381
529
|
* document,
|
|
382
530
|
* <React.StrictMode>
|
|
383
|
-
* <
|
|
531
|
+
* <Rango />
|
|
384
532
|
* </React.StrictMode>
|
|
385
533
|
* );
|
|
386
534
|
* }
|
|
387
535
|
* main();
|
|
388
536
|
* ```
|
|
389
537
|
*/
|
|
390
|
-
export function
|
|
538
|
+
export function Rango(_props: RangoProps): React.ReactElement {
|
|
391
539
|
const {
|
|
392
540
|
store,
|
|
393
541
|
eventController,
|
|
@@ -398,6 +546,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
398
546
|
initialTheme,
|
|
399
547
|
warmupEnabled,
|
|
400
548
|
version,
|
|
549
|
+
appShellRef,
|
|
401
550
|
} = getBrowserAppContext();
|
|
402
551
|
|
|
403
552
|
// Signal that the React tree has hydrated. useEffect only fires after
|
|
@@ -417,6 +566,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
417
566
|
initialTheme={initialTheme}
|
|
418
567
|
warmupEnabled={warmupEnabled}
|
|
419
568
|
version={version}
|
|
569
|
+
basename={initialPayload.metadata?.basename}
|
|
570
|
+
appShellRef={appShellRef}
|
|
420
571
|
/>
|
|
421
572
|
);
|
|
422
573
|
}
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
import { debugLog } from "./logging.js";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Defers a callback to the next animation frame.
|
|
15
|
+
* Falls back to setTimeout(0) in environments without requestAnimationFrame.
|
|
16
|
+
*/
|
|
17
|
+
const deferToNextPaint: (fn: () => void) => void =
|
|
18
|
+
typeof requestAnimationFrame === "function"
|
|
19
|
+
? requestAnimationFrame
|
|
20
|
+
: (fn) => setTimeout(fn, 0);
|
|
21
|
+
|
|
13
22
|
const SCROLL_STORAGE_KEY = "rsc-router-scroll-positions";
|
|
14
23
|
|
|
15
24
|
/**
|
|
@@ -182,10 +191,15 @@ export function saveCurrentScrollPosition(): void {
|
|
|
182
191
|
|
|
183
192
|
/**
|
|
184
193
|
* Persist scroll positions to sessionStorage.
|
|
185
|
-
* If the write fails
|
|
186
|
-
* entries and retry
|
|
194
|
+
* If the write fails (typically QuotaExceededError), evict the oldest ~1/4 of
|
|
195
|
+
* entries ONCE and retry the write a single time; if that still fails, remove
|
|
196
|
+
* our storage key entirely so we don't block other sessionStorage consumers.
|
|
197
|
+
* This is a single evict-then-retry-then-clear ladder, not a loop.
|
|
198
|
+
*
|
|
199
|
+
* Exported so that single eviction/retry/clear ladder is unit-testable directly.
|
|
200
|
+
* The browser drives it from the `pagehide` handler.
|
|
187
201
|
*/
|
|
188
|
-
function persistToSessionStorage(): void {
|
|
202
|
+
export function persistToSessionStorage(): void {
|
|
189
203
|
try {
|
|
190
204
|
sessionStorage.setItem(
|
|
191
205
|
SCROLL_STORAGE_KEY,
|
|
@@ -264,51 +278,35 @@ export function restoreScrollPosition(options?: {
|
|
|
264
278
|
return false;
|
|
265
279
|
}
|
|
266
280
|
|
|
267
|
-
//
|
|
268
|
-
const maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
|
|
269
|
-
const canScrollToPosition = savedY <= maxScrollY;
|
|
270
|
-
|
|
271
|
-
if (canScrollToPosition) {
|
|
272
|
-
window.scrollTo(0, savedY);
|
|
273
|
-
debugLog("[Scroll] Restored position:", savedY, "for key:", key);
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Scroll as far as we can for now
|
|
278
|
-
window.scrollTo(0, maxScrollY);
|
|
279
|
-
debugLog("[Scroll] Partial restore to:", maxScrollY, "target:", savedY);
|
|
280
|
-
|
|
281
|
-
// Poll while streaming until we can scroll to target position
|
|
281
|
+
// If streaming, poll until streaming ends then scroll to saved position
|
|
282
282
|
if (options?.retryIfStreaming && options?.isStreaming?.()) {
|
|
283
283
|
const startTime = Date.now();
|
|
284
284
|
|
|
285
285
|
pendingPollInterval = setInterval(() => {
|
|
286
|
-
// Stop if we've exceeded the timeout
|
|
287
286
|
if (Date.now() - startTime > SCROLL_POLL_TIMEOUT_MS) {
|
|
288
287
|
debugLog("[Scroll] Polling timeout, giving up");
|
|
289
288
|
cancelScrollRestorationPolling();
|
|
290
289
|
return;
|
|
291
290
|
}
|
|
292
291
|
|
|
293
|
-
// Stop if streaming ended
|
|
294
292
|
if (!options.isStreaming?.()) {
|
|
295
|
-
debugLog("[Scroll] Streaming ended, stopping poll");
|
|
296
|
-
cancelScrollRestorationPolling();
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Check if we can now scroll to the target position
|
|
301
|
-
const currentMaxScrollY =
|
|
302
|
-
document.documentElement.scrollHeight - window.innerHeight;
|
|
303
|
-
if (savedY <= currentMaxScrollY) {
|
|
304
293
|
window.scrollTo(0, savedY);
|
|
305
|
-
debugLog("[Scroll]
|
|
294
|
+
debugLog("[Scroll] Restored after streaming:", savedY);
|
|
306
295
|
cancelScrollRestorationPolling();
|
|
307
296
|
}
|
|
308
297
|
}, SCROLL_POLL_INTERVAL_MS);
|
|
298
|
+
|
|
299
|
+
return true;
|
|
309
300
|
}
|
|
310
301
|
|
|
311
|
-
|
|
302
|
+
// Not streaming — scroll after React commits and browser paints.
|
|
303
|
+
// startTransition defers the DOM commit, so scrolling synchronously
|
|
304
|
+
// would be overwritten when React replaces the content.
|
|
305
|
+
deferToNextPaint(() => {
|
|
306
|
+
window.scrollTo(0, savedY);
|
|
307
|
+
debugLog("[Scroll] Restored position:", savedY, "for key:", key);
|
|
308
|
+
});
|
|
309
|
+
return true;
|
|
312
310
|
}
|
|
313
311
|
|
|
314
312
|
/**
|
|
@@ -339,6 +337,8 @@ export function scrollToHash(): boolean {
|
|
|
339
337
|
* Scroll to top of page
|
|
340
338
|
*/
|
|
341
339
|
export function scrollToTop(): void {
|
|
340
|
+
if (typeof window === "undefined") return;
|
|
341
|
+
if (typeof window.scrollTo !== "function") return;
|
|
342
342
|
window.scrollTo(0, 0);
|
|
343
343
|
}
|
|
344
344
|
|
|
@@ -363,31 +363,43 @@ export function handleNavigationEnd(options: {
|
|
|
363
363
|
scroll?: boolean;
|
|
364
364
|
isStreaming?: () => boolean;
|
|
365
365
|
}): void {
|
|
366
|
-
if (!initialized) {
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
366
|
const { restore = false, scroll = true, isStreaming } = options;
|
|
371
367
|
|
|
372
|
-
// Don't scroll if explicitly disabled
|
|
373
|
-
if (scroll === false) {
|
|
368
|
+
// Don't scroll if explicitly disabled or not in a browser
|
|
369
|
+
if (scroll === false || typeof window === "undefined") {
|
|
374
370
|
return;
|
|
375
371
|
}
|
|
376
372
|
|
|
377
|
-
//
|
|
378
|
-
|
|
373
|
+
// Save/restore requires initialization (sessionStorage, history state).
|
|
374
|
+
// But basic scroll-to-top and hash scrolling work without it — this
|
|
375
|
+
// matters during cross-app navigation where ScrollRestoration unmounts
|
|
376
|
+
// and remounts, creating a brief window where initialized is false.
|
|
377
|
+
if (restore && initialized) {
|
|
379
378
|
if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
|
|
380
379
|
return;
|
|
381
380
|
}
|
|
382
381
|
// Fall through to hash or top if no saved position
|
|
383
382
|
}
|
|
384
383
|
|
|
385
|
-
//
|
|
384
|
+
// scrollToHash / scrollToTop run synchronously here.
|
|
385
|
+
// handleNavigationEnd is invoked from NavigationProvider's
|
|
386
|
+
// useLayoutEffect (post-commit, pre-paint), so a sync scrollTo is
|
|
387
|
+
// captured by the upcoming paint AND by startViewTransition's snapshot.
|
|
388
|
+
// Deferring via rAF here pushed the call past the snapshot capture,
|
|
389
|
+
// making forward navigations wrapped in a layout/route view transition
|
|
390
|
+
// skip scroll-to-top — the live DOM scrolled but the captured snapshot
|
|
391
|
+
// was at the previous scroll position, so the user-facing page stayed
|
|
392
|
+
// visually clamped at the source page's scrollY (often the new tree's
|
|
393
|
+
// max scroll for tall→short navs). Y=0 / a hash element are robust
|
|
394
|
+
// against unmeasured layout, so sync scroll is correct here even
|
|
395
|
+
// before the new tree's scrollHeight settles.
|
|
396
|
+
//
|
|
397
|
+
// (The restore branch above keeps deferToNextPaint because savedY
|
|
398
|
+
// depends on the new tree's max scroll; sync scrollTo against an
|
|
399
|
+
// unmeasured DOM would clamp savedY to whatever the old/zero max was.)
|
|
386
400
|
if (scrollToHash()) {
|
|
387
401
|
return;
|
|
388
402
|
}
|
|
389
|
-
|
|
390
|
-
// Default: scroll to top
|
|
391
403
|
scrollToTop();
|
|
392
404
|
}
|
|
393
405
|
|