@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +12 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +441 -134
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3453 -1240
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +76 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +318 -15
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +279 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +153 -109
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +101 -5
- package/skills/router-setup/SKILL.md +116 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +332 -29
- package/skills/use-cache/SKILL.md +46 -14
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +197 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +111 -31
- package/src/browser/navigation-client.ts +201 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +230 -35
- package/src/browser/prefetch/fetch.ts +338 -39
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +111 -16
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +80 -5
- package/src/browser/rsc-router.tsx +218 -76
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -61
- package/src/browser/types.ts +91 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +272 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +214 -48
- package/src/cache/cache-scope.ts +236 -89
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2224 -171
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +119 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +56 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +93 -12
- package/src/index.ts +133 -15
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +469 -276
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +270 -172
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +264 -77
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +474 -385
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +8 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +163 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +163 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +658 -511
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +77 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +105 -72
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +263 -112
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +32 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +26 -46
- package/src/server/request-context.ts +398 -172
- package/src/ssr/index.tsx +25 -16
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +343 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +57 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +155 -65
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +238 -295
- package/src/vite/router-discovery.ts +940 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -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,11 +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";
|
|
25
27
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
28
|
+
import { setPrefetchDecoder } from "./prefetch/fetch.js";
|
|
29
|
+
import { setAppVersion } from "./app-version.js";
|
|
26
30
|
import {
|
|
27
31
|
isInterceptSegment,
|
|
28
32
|
splitInterceptSegments,
|
|
29
33
|
} from "./intercept-utils.js";
|
|
34
|
+
import { createAppShellRef } from "./app-shell.js";
|
|
30
35
|
|
|
31
36
|
// Vite HMR types are provided by vite/client
|
|
32
37
|
|
|
@@ -111,15 +116,26 @@ export interface BrowserAppContext {
|
|
|
111
116
|
initialTheme?: Theme;
|
|
112
117
|
/** Whether connection warmup is enabled */
|
|
113
118
|
warmupEnabled?: boolean;
|
|
119
|
+
/** Whether the hydrated tree should be wrapped in React.StrictMode */
|
|
120
|
+
strictMode?: boolean;
|
|
114
121
|
/** App version for prefetch version mismatch detection */
|
|
115
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;
|
|
116
132
|
}
|
|
117
133
|
|
|
118
134
|
// Module-level state for the initialized app
|
|
119
135
|
let browserAppContext: BrowserAppContext | null = null;
|
|
120
136
|
|
|
121
137
|
/**
|
|
122
|
-
* Initialize the browser app. Must be called before rendering
|
|
138
|
+
* Initialize the browser app. Must be called before rendering Rango.
|
|
123
139
|
*
|
|
124
140
|
* This function:
|
|
125
141
|
* - Loads the initial RSC payload from the stream
|
|
@@ -139,7 +155,6 @@ export async function initBrowserApp(
|
|
|
139
155
|
initialTheme,
|
|
140
156
|
} = options;
|
|
141
157
|
|
|
142
|
-
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
143
158
|
const initialPayload =
|
|
144
159
|
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
145
160
|
|
|
@@ -164,6 +179,18 @@ export async function initBrowserApp(
|
|
|
164
179
|
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
165
180
|
});
|
|
166
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
|
+
|
|
167
194
|
// Create event controller for reactive state management
|
|
168
195
|
const eventController = createEventController({
|
|
169
196
|
initialLocation: new URL(window.location.href),
|
|
@@ -198,13 +225,25 @@ export async function initBrowserApp(
|
|
|
198
225
|
// Create composable utilities
|
|
199
226
|
const client = createNavigationClient(deps);
|
|
200
227
|
|
|
201
|
-
//
|
|
202
|
-
|
|
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.
|
|
203
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
|
+
});
|
|
204
240
|
|
|
205
|
-
// Initialize the
|
|
206
|
-
//
|
|
207
|
-
|
|
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);
|
|
208
247
|
|
|
209
248
|
// Initialize the in-memory prefetch cache TTL from server config.
|
|
210
249
|
// A value of 0 disables the cache; undefined falls back to the module default.
|
|
@@ -213,11 +252,22 @@ export async function initBrowserApp(
|
|
|
213
252
|
initPrefetchCache(prefetchCacheTTL);
|
|
214
253
|
}
|
|
215
254
|
|
|
216
|
-
//
|
|
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.
|
|
217
263
|
const renderSegments = (
|
|
218
264
|
segments: ResolvedSegment[],
|
|
219
265
|
options?: RenderSegmentsOptions,
|
|
220
|
-
) =>
|
|
266
|
+
) =>
|
|
267
|
+
baseRenderSegments(segments, {
|
|
268
|
+
...options,
|
|
269
|
+
rootLayout: appShellRef.get().rootLayout,
|
|
270
|
+
});
|
|
221
271
|
|
|
222
272
|
// Lazy reference for navigation bridge — the action bridge is created first
|
|
223
273
|
// but may need to trigger SPA navigation for action redirects.
|
|
@@ -231,10 +281,15 @@ export async function initBrowserApp(
|
|
|
231
281
|
deps,
|
|
232
282
|
onUpdate: (update) => store.emitUpdate(update),
|
|
233
283
|
renderSegments,
|
|
234
|
-
version,
|
|
235
284
|
onNavigate: (url, options) => {
|
|
236
285
|
if (!navigateFn) {
|
|
237
|
-
|
|
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
|
+
}
|
|
238
293
|
return Promise.resolve();
|
|
239
294
|
}
|
|
240
295
|
return navigateFn(url, options);
|
|
@@ -249,7 +304,7 @@ export async function initBrowserApp(
|
|
|
249
304
|
client,
|
|
250
305
|
onUpdate: (update) => store.emitUpdate(update),
|
|
251
306
|
renderSegments,
|
|
252
|
-
version,
|
|
307
|
+
version: version,
|
|
253
308
|
});
|
|
254
309
|
|
|
255
310
|
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
@@ -263,75 +318,157 @@ export async function initBrowserApp(
|
|
|
263
318
|
// Build initial tree with rootLayout
|
|
264
319
|
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
265
320
|
|
|
266
|
-
// 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).
|
|
267
325
|
if (import.meta.hot) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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,
|
|
285
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
|
+
});
|
|
286
378
|
|
|
287
|
-
|
|
288
|
-
const segments = payload.metadata.segments || [];
|
|
289
|
-
const matched = payload.metadata.matched || [];
|
|
379
|
+
if (abort.signal.aborted) return;
|
|
290
380
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
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
|
+
}
|
|
295
387
|
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
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);
|
|
299
408
|
}
|
|
300
409
|
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
}
|
|
303
453
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
store.emitUpdate({
|
|
317
|
-
root: renderSegments(main, {
|
|
318
|
-
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
319
|
-
}),
|
|
320
|
-
metadata: payload.metadata,
|
|
321
|
-
});
|
|
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]();
|
|
322
466
|
}
|
|
323
|
-
|
|
324
|
-
await streamComplete;
|
|
325
|
-
handle.complete(new URL(window.location.href));
|
|
326
|
-
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
327
|
-
} finally {
|
|
328
|
-
streamingToken.end();
|
|
329
|
-
handle[Symbol.dispose]();
|
|
330
|
-
}
|
|
467
|
+
}, 200);
|
|
331
468
|
});
|
|
332
469
|
}
|
|
333
470
|
|
|
334
|
-
// Store context for
|
|
471
|
+
// Store context for Rango component
|
|
335
472
|
const context: BrowserAppContext = {
|
|
336
473
|
store,
|
|
337
474
|
eventController,
|
|
@@ -341,7 +478,9 @@ export async function initBrowserApp(
|
|
|
341
478
|
themeConfig: effectiveThemeConfig,
|
|
342
479
|
initialTheme: effectiveInitialTheme,
|
|
343
480
|
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
481
|
+
strictMode: initialPayload.metadata?.strictMode ?? true,
|
|
344
482
|
version,
|
|
483
|
+
appShellRef,
|
|
345
484
|
};
|
|
346
485
|
browserAppContext = context;
|
|
347
486
|
|
|
@@ -354,7 +493,7 @@ export async function initBrowserApp(
|
|
|
354
493
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
355
494
|
if (!browserAppContext) {
|
|
356
495
|
throw new Error(
|
|
357
|
-
"
|
|
496
|
+
"Rango: initBrowserApp() must be called before rendering Rango",
|
|
358
497
|
);
|
|
359
498
|
}
|
|
360
499
|
return browserAppContext;
|
|
@@ -368,18 +507,18 @@ export function resetBrowserAppContext(): void {
|
|
|
368
507
|
}
|
|
369
508
|
|
|
370
509
|
/**
|
|
371
|
-
* Props for the
|
|
510
|
+
* Props for the Rango component
|
|
372
511
|
*/
|
|
373
|
-
export interface
|
|
512
|
+
export interface RangoProps {}
|
|
374
513
|
|
|
375
514
|
/**
|
|
376
|
-
*
|
|
515
|
+
* Rango component - renders the RSC router with all internal wiring.
|
|
377
516
|
*
|
|
378
517
|
* Must be called after initBrowserApp() has completed.
|
|
379
518
|
*
|
|
380
519
|
* @example
|
|
381
520
|
* ```tsx
|
|
382
|
-
* import { initBrowserApp,
|
|
521
|
+
* import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
383
522
|
* import { rscStream } from "rsc-html-stream/client";
|
|
384
523
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
385
524
|
*
|
|
@@ -389,14 +528,14 @@ export interface RSCRouterProps {}
|
|
|
389
528
|
* hydrateRoot(
|
|
390
529
|
* document,
|
|
391
530
|
* <React.StrictMode>
|
|
392
|
-
* <
|
|
531
|
+
* <Rango />
|
|
393
532
|
* </React.StrictMode>
|
|
394
533
|
* );
|
|
395
534
|
* }
|
|
396
535
|
* main();
|
|
397
536
|
* ```
|
|
398
537
|
*/
|
|
399
|
-
export function
|
|
538
|
+
export function Rango(_props: RangoProps): React.ReactElement {
|
|
400
539
|
const {
|
|
401
540
|
store,
|
|
402
541
|
eventController,
|
|
@@ -407,6 +546,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
407
546
|
initialTheme,
|
|
408
547
|
warmupEnabled,
|
|
409
548
|
version,
|
|
549
|
+
appShellRef,
|
|
410
550
|
} = getBrowserAppContext();
|
|
411
551
|
|
|
412
552
|
// Signal that the React tree has hydrated. useEffect only fires after
|
|
@@ -426,6 +566,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
426
566
|
initialTheme={initialTheme}
|
|
427
567
|
warmupEnabled={warmupEnabled}
|
|
428
568
|
version={version}
|
|
569
|
+
basename={initialPayload.metadata?.basename}
|
|
570
|
+
appShellRef={appShellRef}
|
|
429
571
|
/>
|
|
430
572
|
);
|
|
431
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
|
|