@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
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
startTransition,
|
|
10
10
|
} from "react";
|
|
11
11
|
import { NavigationStoreContext } from "./context.js";
|
|
12
|
-
import
|
|
12
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
13
|
+
import type { TrackedActionState } from "../types.js";
|
|
13
14
|
import { invariant } from "../../errors.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -23,32 +24,24 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
23
24
|
result: null,
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
* Normalize action ID - returns the ID as-is
|
|
28
|
-
*
|
|
29
|
-
* Server actions have IDs like "hash#actionName" or "src/actions.ts#actionName".
|
|
30
|
-
* When using function references, we use the full ID for exact matching.
|
|
31
|
-
* When using strings, the event controller supports suffix matching
|
|
32
|
-
* (e.g., "addToCart" matches "hash#addToCart").
|
|
33
|
-
*/
|
|
34
|
-
function normalizeActionId(actionId: string): string {
|
|
35
|
-
return actionId;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
27
|
/**
|
|
39
28
|
* Extract action ID from a server action function or string.
|
|
40
29
|
*
|
|
41
30
|
* Actions passed as props from server components lose their metadata
|
|
42
31
|
* during RSC serialization - use a string action name instead.
|
|
32
|
+
*
|
|
33
|
+
* The extracted $$id (e.g. "hash#actionName" or "src/actions.ts#actionName")
|
|
34
|
+
* is returned as-is. Suffix-vs-exact matching against this ID happens
|
|
35
|
+
* downstream in the event controller, not here.
|
|
43
36
|
*/
|
|
44
|
-
|
|
37
|
+
function getActionId(action: ServerActionFunction | string): string {
|
|
45
38
|
invariant(
|
|
46
39
|
typeof action === "function" || typeof action === "string",
|
|
47
|
-
`useAction: action must be a function or string, got ${typeof action}
|
|
40
|
+
`useAction: action must be a function or string, got ${typeof action}`,
|
|
48
41
|
);
|
|
49
42
|
const actionId = (action as any)?.$$id;
|
|
50
43
|
if (actionId) {
|
|
51
|
-
return
|
|
44
|
+
return actionId;
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
// If action is a string, use it directly
|
|
@@ -72,7 +65,7 @@ Solutions:
|
|
|
72
65
|
2. Use the action name as a string:
|
|
73
66
|
const state = useAction("myAction");
|
|
74
67
|
|
|
75
|
-
The string must match the exported function name from your "use server" file
|
|
68
|
+
The string must match the exported function name from your "use server" file.`,
|
|
76
69
|
);
|
|
77
70
|
}
|
|
78
71
|
|
|
@@ -126,19 +119,24 @@ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
|
|
|
126
119
|
* const error = useAction(addToCart, state => state.error);
|
|
127
120
|
* ```
|
|
128
121
|
*
|
|
122
|
+
* @note The selector is expected to be stable for a given hook instance.
|
|
123
|
+
* This hook tracks one projection of one action. Changing selector semantics
|
|
124
|
+
* for the same action ID without a new action event is not a supported pattern;
|
|
125
|
+
* use separate useAction() subscriptions if you need different projections.
|
|
126
|
+
*
|
|
129
127
|
* @note Actions passed as props from server components lose their metadata
|
|
130
128
|
* during RSC serialization. Use a string action name or import directly.
|
|
131
129
|
*/
|
|
132
130
|
export function useAction(
|
|
133
|
-
action: ServerActionFunction | string
|
|
131
|
+
action: ServerActionFunction | string,
|
|
134
132
|
): TrackedActionState;
|
|
135
133
|
export function useAction<T>(
|
|
136
134
|
action: ServerActionFunction | string,
|
|
137
|
-
selector: (state: TrackedActionState) => T
|
|
135
|
+
selector: (state: TrackedActionState) => T,
|
|
138
136
|
): T;
|
|
139
137
|
export function useAction<T>(
|
|
140
138
|
action: ServerActionFunction | string,
|
|
141
|
-
selector?: (state: TrackedActionState) => T
|
|
139
|
+
selector?: (state: TrackedActionState) => T,
|
|
142
140
|
): T | TrackedActionState {
|
|
143
141
|
const ctx = useContext(NavigationStoreContext);
|
|
144
142
|
const actionId =
|
|
@@ -156,12 +154,14 @@ export function useAction<T>(
|
|
|
156
154
|
});
|
|
157
155
|
const prevSelected = useRef(baseState);
|
|
158
156
|
prevSelected.current = baseState;
|
|
159
|
-
// useOptimistic allows immediate updates during transitions/actions
|
|
160
157
|
const [optimisticState, setOptimisticState] = useOptimistic<
|
|
161
158
|
T | TrackedActionState
|
|
162
159
|
>(null!);
|
|
163
160
|
|
|
164
|
-
//
|
|
161
|
+
// Ref keeps the latest selector for subscription callbacks without
|
|
162
|
+
// re-subscribing on every render. Selector changes themselves are not
|
|
163
|
+
// treated as a reactive input; this hook expects a stable selector and
|
|
164
|
+
// represents one subscription/projection for one action.
|
|
165
165
|
const selectorRef = useRef(selector);
|
|
166
166
|
selectorRef.current = selector;
|
|
167
167
|
|
|
@@ -169,6 +169,17 @@ export function useAction<T>(
|
|
|
169
169
|
useEffect(() => {
|
|
170
170
|
if (!ctx) return;
|
|
171
171
|
|
|
172
|
+
// Sync current state for the (possibly new) actionId so that switching
|
|
173
|
+
// actions on an idle page doesn't leave stale data from the old action.
|
|
174
|
+
const currentState = ctx.eventController.getActionState(actionId);
|
|
175
|
+
const currentSelected = selectorRef.current
|
|
176
|
+
? selectorRef.current(currentState)
|
|
177
|
+
: currentState;
|
|
178
|
+
if (!shallowEqual(currentSelected, prevSelected.current)) {
|
|
179
|
+
prevSelected.current = currentSelected;
|
|
180
|
+
setBaseState(currentSelected);
|
|
181
|
+
}
|
|
182
|
+
|
|
172
183
|
// Subscribe to action-specific updates
|
|
173
184
|
const unsubscribe = ctx.eventController.subscribeToAction(
|
|
174
185
|
actionId,
|
|
@@ -177,14 +188,14 @@ export function useAction<T>(
|
|
|
177
188
|
? selectorRef.current(state)
|
|
178
189
|
: state;
|
|
179
190
|
|
|
180
|
-
if (!
|
|
191
|
+
if (!shallowEqual(selectedState, prevSelected.current)) {
|
|
181
192
|
prevSelected.current = selectedState;
|
|
182
193
|
setBaseState(selectedState);
|
|
183
194
|
startTransition(() => {
|
|
184
195
|
setOptimisticState(selectedState);
|
|
185
196
|
});
|
|
186
197
|
}
|
|
187
|
-
}
|
|
198
|
+
},
|
|
188
199
|
);
|
|
189
200
|
|
|
190
201
|
return () => {
|
|
@@ -195,46 +206,4 @@ export function useAction<T>(
|
|
|
195
206
|
return (optimisticState ?? baseState) as T | TrackedActionState;
|
|
196
207
|
}
|
|
197
208
|
|
|
198
|
-
function isShallowEqual<T, U>(selectedState: T, baseState: U): boolean {
|
|
199
|
-
// If references are equal, they're shallow equal
|
|
200
|
-
//@ts-expect-error -- TS doesn't like comparing generics
|
|
201
|
-
if (selectedState === baseState) {
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// If either is null/undefined and they're not equal, they're not shallow equal
|
|
206
|
-
if (selectedState == null || baseState == null) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// If types are different, they're not shallow equal
|
|
211
|
-
if (typeof selectedState !== typeof baseState) {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// For primitives, === comparison is sufficient (already checked above)
|
|
216
|
-
if (typeof selectedState !== "object") {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// For objects, compare keys and values shallowly
|
|
221
|
-
const keysA = Object.keys(selectedState as object);
|
|
222
|
-
const keysB = Object.keys(baseState as object);
|
|
223
|
-
|
|
224
|
-
if (keysA.length !== keysB.length) {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
for (const key of keysA) {
|
|
229
|
-
if (
|
|
230
|
-
!Object.prototype.hasOwnProperty.call(baseState, key) ||
|
|
231
|
-
(selectedState as any)[key] !== (baseState as any)[key]
|
|
232
|
-
) {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
209
|
export type { TrackedActionState };
|
|
@@ -9,119 +9,10 @@ import {
|
|
|
9
9
|
startTransition,
|
|
10
10
|
} from "react";
|
|
11
11
|
import type { Handle } from "../../handle.js";
|
|
12
|
-
import {
|
|
12
|
+
import { collectHandleData } from "../../handle.js";
|
|
13
13
|
import type { HandleData } from "../types.js";
|
|
14
14
|
import { NavigationStoreContext } from "./context.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* SSR module-level state.
|
|
18
|
-
* Populated by initHandleDataSync before React renders.
|
|
19
|
-
* Used by useState initializer during SSR.
|
|
20
|
-
*/
|
|
21
|
-
let ssrHandleData: HandleData = {};
|
|
22
|
-
let ssrSegmentOrder: string[] = [];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Filter segment IDs to only include routes and layouts.
|
|
26
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
27
|
-
*/
|
|
28
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
29
|
-
return matched.filter((id) => {
|
|
30
|
-
if (id.includes(".@")) return false;
|
|
31
|
-
if (/D\d+\./.test(id)) return false;
|
|
32
|
-
return true;
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Resolve the collect function for a handle.
|
|
38
|
-
* Handle objects are plain { __brand, $$id } - collect is stored in the registry
|
|
39
|
-
* (populated when createHandle runs on the client).
|
|
40
|
-
*/
|
|
41
|
-
function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
|
|
42
|
-
// Look up collect from the registry (populated when the handle module is imported).
|
|
43
|
-
const registered = getCollectFn(handle.$$id);
|
|
44
|
-
if (registered) {
|
|
45
|
-
return registered as (segments: T[][]) => A;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Fall back to default flat collect with a dev warning.
|
|
49
|
-
if (process.env.NODE_ENV !== "production") {
|
|
50
|
-
console.warn(
|
|
51
|
-
`[rsc-router] Handle "${handle.$$id}" was passed as a prop but its collect ` +
|
|
52
|
-
`function could not be resolved. Falling back to flat array. ` +
|
|
53
|
-
`Import the handle module in a client component to register its collect function.`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
return ((segments: unknown[][]) => segments.flat()) as unknown as (segments: T[][]) => A;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Collect handle data from segments and transform to final value.
|
|
61
|
-
*/
|
|
62
|
-
function collectHandle<T, A>(
|
|
63
|
-
handle: Handle<T, A>,
|
|
64
|
-
data: HandleData,
|
|
65
|
-
segmentOrder: string[]
|
|
66
|
-
): A {
|
|
67
|
-
const collect = resolveCollect(handle);
|
|
68
|
-
const segmentData = data[handle.$$id];
|
|
69
|
-
|
|
70
|
-
if (!segmentData) {
|
|
71
|
-
return collect([]);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Build array of segment arrays in parent -> child order
|
|
75
|
-
const segmentArrays: T[][] = [];
|
|
76
|
-
for (const segmentId of segmentOrder) {
|
|
77
|
-
const entries = segmentData[segmentId];
|
|
78
|
-
if (entries && entries.length > 0) {
|
|
79
|
-
segmentArrays.push(entries as T[]);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Call collect once with all segment data
|
|
84
|
-
return collect(segmentArrays);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Shallow equality check for selector results.
|
|
89
|
-
*/
|
|
90
|
-
function shallowEqual<T>(a: T, b: T): boolean {
|
|
91
|
-
if (Object.is(a, b)) return true;
|
|
92
|
-
if (
|
|
93
|
-
typeof a !== "object" ||
|
|
94
|
-
a === null ||
|
|
95
|
-
typeof b !== "object" ||
|
|
96
|
-
b === null
|
|
97
|
-
) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
const keysA = Object.keys(a);
|
|
101
|
-
const keysB = Object.keys(b);
|
|
102
|
-
if (keysA.length !== keysB.length) return false;
|
|
103
|
-
for (const key of keysA) {
|
|
104
|
-
if (
|
|
105
|
-
!Object.hasOwn(b, key) ||
|
|
106
|
-
!Object.is((a as any)[key], (b as any)[key])
|
|
107
|
-
) {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Initialize handle data synchronously for SSR.
|
|
116
|
-
* Called before rendering to populate state for useState initializer.
|
|
117
|
-
*
|
|
118
|
-
* @param data - Handle data from RSC payload
|
|
119
|
-
* @param matched - Segment order for reduction
|
|
120
|
-
*/
|
|
121
|
-
export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
122
|
-
ssrHandleData = data;
|
|
123
|
-
ssrSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
124
|
-
}
|
|
15
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
125
16
|
|
|
126
17
|
/**
|
|
127
18
|
* Hook to access collected handle data.
|
|
@@ -141,49 +32,71 @@ export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
|
141
32
|
* const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
142
33
|
* ```
|
|
143
34
|
*/
|
|
144
|
-
export function useHandle<T, A>(handle: Handle<T, A>): A
|
|
35
|
+
export function useHandle<T, A>(handle: Handle<T, A>): Rango.FlightSerialize<A>;
|
|
145
36
|
export function useHandle<T, A, S>(
|
|
146
37
|
handle: Handle<T, A>,
|
|
147
|
-
selector: (data: A) => S
|
|
38
|
+
selector: (data: Rango.FlightSerialize<A>) => S,
|
|
148
39
|
): S;
|
|
149
40
|
export function useHandle<T, A, S>(
|
|
150
41
|
handle: Handle<T, A>,
|
|
151
|
-
selector?: (data: A) => S
|
|
152
|
-
): A | S {
|
|
42
|
+
selector?: (data: Rango.FlightSerialize<A>) => S,
|
|
43
|
+
): Rango.FlightSerialize<A> | S {
|
|
153
44
|
const ctx = useContext(NavigationStoreContext);
|
|
154
45
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
46
|
+
const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
|
|
47
|
+
if (!ctx) {
|
|
48
|
+
const collected = collectHandleData(
|
|
49
|
+
handle,
|
|
50
|
+
{},
|
|
51
|
+
[],
|
|
52
|
+
) as Rango.FlightSerialize<A>;
|
|
160
53
|
return selector ? selector(collected) : collected;
|
|
161
54
|
}
|
|
162
55
|
|
|
163
|
-
// On client, use event controller state
|
|
164
56
|
const state = ctx.eventController.getHandleState();
|
|
165
|
-
const collected =
|
|
57
|
+
const collected = collectHandleData(
|
|
58
|
+
handle,
|
|
59
|
+
state.data,
|
|
60
|
+
state.segmentOrder,
|
|
61
|
+
) as Rango.FlightSerialize<A>;
|
|
166
62
|
return selector ? selector(collected) : collected;
|
|
167
63
|
});
|
|
168
64
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
169
65
|
|
|
170
|
-
// Track previous value for shallow comparison
|
|
171
66
|
const prevValueRef = useRef(value);
|
|
172
67
|
prevValueRef.current = value;
|
|
173
68
|
|
|
174
|
-
// Memoize selector ref
|
|
175
69
|
const selectorRef = useRef(selector);
|
|
176
70
|
selectorRef.current = selector;
|
|
177
71
|
|
|
178
|
-
// Subscribe to handle data changes (client only)
|
|
179
72
|
useEffect(() => {
|
|
180
73
|
if (!ctx) return;
|
|
181
74
|
|
|
75
|
+
// Sync current state for the (possibly new) handle so that switching
|
|
76
|
+
// handles on an idle page doesn't leave stale data from the old handle.
|
|
77
|
+
const currentHandleState = ctx.eventController.getHandleState();
|
|
78
|
+
const currentCollected = collectHandleData(
|
|
79
|
+
handle,
|
|
80
|
+
currentHandleState.data,
|
|
81
|
+
currentHandleState.segmentOrder,
|
|
82
|
+
) as Rango.FlightSerialize<A>;
|
|
83
|
+
const currentValue = selectorRef.current
|
|
84
|
+
? selectorRef.current(currentCollected)
|
|
85
|
+
: currentCollected;
|
|
86
|
+
if (!shallowEqual(currentValue, prevValueRef.current)) {
|
|
87
|
+
prevValueRef.current = currentValue;
|
|
88
|
+
setValue(currentValue);
|
|
89
|
+
}
|
|
90
|
+
|
|
182
91
|
return ctx.eventController.subscribeToHandles(() => {
|
|
183
92
|
const state = ctx.eventController.getHandleState();
|
|
184
93
|
const isAction =
|
|
185
94
|
ctx.eventController.getState().inflightActions.length > 0;
|
|
186
|
-
const collected =
|
|
95
|
+
const collected = collectHandleData(
|
|
96
|
+
handle,
|
|
97
|
+
state.data,
|
|
98
|
+
state.segmentOrder,
|
|
99
|
+
) as Rango.FlightSerialize<A>;
|
|
187
100
|
const nextValue = selectorRef.current
|
|
188
101
|
? selectorRef.current(collected)
|
|
189
102
|
: collected;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useCallback } from "react";
|
|
3
4
|
import { href, type ValidPaths } from "../../href-client.js";
|
|
4
5
|
import { useMount } from "./use-mount.js";
|
|
5
6
|
|
|
@@ -36,5 +37,11 @@ import { useMount } from "./use-mount.js";
|
|
|
36
37
|
*/
|
|
37
38
|
export function useHref(): (path: `/${string}`) => string {
|
|
38
39
|
const mount = useMount();
|
|
39
|
-
|
|
40
|
+
// Memoize on `mount` (stable within a route) so the returned function is
|
|
41
|
+
// referentially stable across re-renders — a consumer can safely pass it as a
|
|
42
|
+
// dependency or a prop to a memoized child without forcing re-renders.
|
|
43
|
+
return useCallback(
|
|
44
|
+
(path: `/${string}`) => href(path as ValidPaths, mount),
|
|
45
|
+
[mount],
|
|
46
|
+
);
|
|
40
47
|
}
|
|
@@ -16,7 +16,9 @@ import { NavigationStoreContext } from "./context.js";
|
|
|
16
16
|
* Context for Link component to provide its destination URL
|
|
17
17
|
* Used by useLinkStatus to determine if this specific link is pending
|
|
18
18
|
*/
|
|
19
|
-
export const LinkContext: Context<string | null> = createContext<string | null>(
|
|
19
|
+
export const LinkContext: Context<string | null> = createContext<string | null>(
|
|
20
|
+
null,
|
|
21
|
+
);
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Link status returned by useLinkStatus hook
|
|
@@ -46,7 +48,7 @@ function normalizeUrl(url: string, origin: string): string {
|
|
|
46
48
|
function isPendingFor(
|
|
47
49
|
linkTo: string | null,
|
|
48
50
|
pendingUrl: string | null,
|
|
49
|
-
origin: string
|
|
51
|
+
origin: string,
|
|
50
52
|
): boolean {
|
|
51
53
|
if (linkTo === null || pendingUrl === null) {
|
|
52
54
|
return false;
|
|
@@ -80,12 +82,9 @@ export function useLinkStatus(): LinkStatus {
|
|
|
80
82
|
const linkTo = useContext(LinkContext);
|
|
81
83
|
const ctx = useContext(NavigationStoreContext);
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
? window.location.origin
|
|
86
|
-
: "http://localhost";
|
|
85
|
+
const origin =
|
|
86
|
+
typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
87
87
|
|
|
88
|
-
// Base state for useOptimistic
|
|
89
88
|
const [basePending, setBasePending] = useState<boolean>(() => {
|
|
90
89
|
if (!ctx || linkTo === null) {
|
|
91
90
|
return false;
|
|
@@ -96,7 +95,13 @@ export function useLinkStatus(): LinkStatus {
|
|
|
96
95
|
|
|
97
96
|
const prevPending = useRef(basePending);
|
|
98
97
|
|
|
99
|
-
//
|
|
98
|
+
// Tracks whether the most recent setOptimisticPending call pinned the value
|
|
99
|
+
// to a non-idle (loading) state. Used to decide whether to emit a release
|
|
100
|
+
// update when returning to idle, so the optimistic store doesn't stay pinned
|
|
101
|
+
// to `true` if a parent transition (e.g. the <Link> click / view transition
|
|
102
|
+
// commit) is still pending. Mirrors useNavigation's optimisticPinnedRef.
|
|
103
|
+
const optimisticPinnedRef = useRef(false);
|
|
104
|
+
|
|
100
105
|
const [pending, setOptimisticPending] = useOptimistic(basePending);
|
|
101
106
|
|
|
102
107
|
useEffect(() => {
|
|
@@ -104,25 +109,46 @@ export function useLinkStatus(): LinkStatus {
|
|
|
104
109
|
return;
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
|
|
108
|
-
return ctx.eventController.subscribe(() => {
|
|
112
|
+
const update = () => {
|
|
109
113
|
const state = ctx.eventController.getState();
|
|
110
114
|
const isPending = isPendingFor(linkTo, state.pendingUrl, origin);
|
|
111
115
|
|
|
112
116
|
if (isPending !== prevPending.current) {
|
|
113
117
|
prevPending.current = isPending;
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
const shouldPin = isPending && state.state !== "idle";
|
|
120
|
+
|
|
121
|
+
if (shouldPin) {
|
|
122
|
+
// Pin the optimistic value so the spinner shows immediately even if
|
|
123
|
+
// a parent transition (e.g. <Link> click) defers the urgent
|
|
124
|
+
// setBasePending commit.
|
|
117
125
|
startTransition(() => {
|
|
118
126
|
setOptimisticPending(isPending);
|
|
119
127
|
});
|
|
128
|
+
optimisticPinnedRef.current = true;
|
|
129
|
+
} else if (optimisticPinnedRef.current) {
|
|
130
|
+
// Release a previously-pinned optimistic value. Without this,
|
|
131
|
+
// useOptimistic keeps returning the stale `true` while any parent
|
|
132
|
+
// transition is still pending, even after basePending flipped to
|
|
133
|
+
// false at navigation completion — leaving the link spinner stuck.
|
|
134
|
+
startTransition(() => {
|
|
135
|
+
setOptimisticPending(isPending);
|
|
136
|
+
});
|
|
137
|
+
optimisticPinnedRef.current = false;
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
// Always update base state
|
|
123
141
|
setBasePending(isPending);
|
|
124
142
|
}
|
|
125
|
-
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Catch-up: re-read state synchronously on mount before subscribing, so a
|
|
146
|
+
// navigation that started between the seeding render and this effect commit
|
|
147
|
+
// isn't dropped until the next (debounced) notify. Mirrors usePathname /
|
|
148
|
+
// useSearchParams.
|
|
149
|
+
update();
|
|
150
|
+
|
|
151
|
+
return ctx.eventController.subscribe(update);
|
|
126
152
|
}, [linkTo, origin]);
|
|
127
153
|
|
|
128
154
|
// If not inside a Link, return not pending
|