@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17
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 +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- 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 +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -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 +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- 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/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/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- 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 +148 -19
- package/src/browser/navigation-client.ts +187 -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/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- 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 +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -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-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- 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 +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -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 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- 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 +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- 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/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -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 +437 -274
- 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 +52 -10
- 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 +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- 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 +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- 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 +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +192 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +64 -25
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- 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 +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +348 -128
- package/src/ssr/index.tsx +23 -15
- 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 +618 -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 +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -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 +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -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 +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- 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 +107 -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 +192 -99
- 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 +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- 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 +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -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/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- 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/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 +27 -29
- 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/router/middleware-cookies.ts +0 -55
|
@@ -3,22 +3,73 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fetch-based prefetch logic used by Link (hover/viewport/render strategies)
|
|
5
5
|
* and useRouter().prefetch(). Sends the same headers and segment IDs as a
|
|
6
|
-
* real navigation so the server returns a proper diff. The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* real navigation so the server returns a proper diff. The response is fetched
|
|
7
|
+
* AND eagerly decoded (createFromFetch) up front: decoding the Flight stream
|
|
8
|
+
* resolves the route's client references, so the route's JS chunks are imported
|
|
9
|
+
* during prefetch rather than on click. The decoded payload is stored in an
|
|
10
|
+
* in-memory cache and reused verbatim by navigation, so a prefetched click
|
|
11
|
+
* loads no new code.
|
|
12
|
+
*
|
|
13
|
+
* In-flight promises are tracked in the cache so that navigation can reuse
|
|
14
|
+
* a prefetch that is still downloading/decoding instead of starting a
|
|
15
|
+
* duplicate request.
|
|
9
16
|
*/
|
|
10
17
|
|
|
11
18
|
import {
|
|
12
19
|
buildPrefetchKey,
|
|
20
|
+
buildSourceKey,
|
|
13
21
|
hasPrefetch,
|
|
14
22
|
markPrefetchInflight,
|
|
23
|
+
setInflightPromiseWithAliases,
|
|
15
24
|
storePrefetch,
|
|
16
25
|
clearPrefetchInflight,
|
|
17
26
|
currentGeneration,
|
|
27
|
+
type DecodedPrefetch,
|
|
18
28
|
} from "./cache.js";
|
|
19
29
|
import { getRangoState } from "../rango-state.js";
|
|
30
|
+
import { isActionFenceActive } from "../action-fence.js";
|
|
20
31
|
import { enqueuePrefetch } from "./queue.js";
|
|
21
32
|
import { shouldPrefetch } from "./policy.js";
|
|
33
|
+
import { debugLog } from "../logging.js";
|
|
34
|
+
import { teeWithCompletion, isForeignRouterId } from "../response-adapter.js";
|
|
35
|
+
import type { RscPayload } from "../types.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decoder injected at app startup (see setPrefetchDecoder). This is
|
|
39
|
+
* `deps.createFromFetch` — decoupled from the RSC runtime exactly like the
|
|
40
|
+
* navigation client. Prefetch decodes through it so the route's client chunks
|
|
41
|
+
* are pulled during the prefetch, not on click.
|
|
42
|
+
*/
|
|
43
|
+
type PrefetchDecoder = (response: Promise<Response>) => Promise<RscPayload>;
|
|
44
|
+
|
|
45
|
+
let decoder: PrefetchDecoder | null = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Wire the RSC decoder used to eagerly decode prefetched responses. Called
|
|
49
|
+
* once from initBrowserApp with the same createFromFetch the navigation client
|
|
50
|
+
* uses. Until set, prefetch warming is inert (prefetches are skipped) — the
|
|
51
|
+
* browser app always sets it before any Link can fire.
|
|
52
|
+
*/
|
|
53
|
+
export function setPrefetchDecoder(fn: PrefetchDecoder): void {
|
|
54
|
+
decoder = fn;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a URL resolves to the current page (same pathname + search).
|
|
59
|
+
* Used to prevent same-page prefetching, which produces a trivial diff
|
|
60
|
+
* that would corrupt the (default wildcard) prefetch cache entry.
|
|
61
|
+
*/
|
|
62
|
+
function isSamePage(url: string): boolean {
|
|
63
|
+
try {
|
|
64
|
+
const target = new URL(url, window.location.origin);
|
|
65
|
+
return (
|
|
66
|
+
target.pathname + target.search ===
|
|
67
|
+
window.location.pathname + window.location.search
|
|
68
|
+
);
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
22
73
|
|
|
23
74
|
/**
|
|
24
75
|
* Build an RSC partial URL for prefetching.
|
|
@@ -30,6 +81,7 @@ function buildPrefetchUrl(
|
|
|
30
81
|
url: string,
|
|
31
82
|
segmentIds: string[],
|
|
32
83
|
version?: string,
|
|
84
|
+
routerId?: string,
|
|
33
85
|
): URL | null {
|
|
34
86
|
let targetUrl: URL;
|
|
35
87
|
try {
|
|
@@ -47,24 +99,64 @@ function buildPrefetchUrl(
|
|
|
47
99
|
if (version) {
|
|
48
100
|
targetUrl.searchParams.set("_rsc_v", version);
|
|
49
101
|
}
|
|
102
|
+
if (routerId) {
|
|
103
|
+
targetUrl.searchParams.set("_rsc_rid", routerId);
|
|
104
|
+
}
|
|
50
105
|
return targetUrl;
|
|
51
106
|
}
|
|
52
107
|
|
|
53
108
|
/**
|
|
54
|
-
* Core prefetch fetch logic. Fetches the response,
|
|
55
|
-
*
|
|
56
|
-
*
|
|
109
|
+
* Core prefetch fetch logic. Fetches the response, eagerly decodes it, and
|
|
110
|
+
* stores the decoded payload in the in-memory cache. The returned Promise
|
|
111
|
+
* resolves to the decoded entry (or null on failure / control header) so
|
|
112
|
+
* navigation can safely reuse an in-flight prefetch via
|
|
113
|
+
* consumeInflightPrefetch().
|
|
114
|
+
*
|
|
115
|
+
* Eager decode is the warming step: createFromFetch parses the Flight stream,
|
|
116
|
+
* which resolves the route's client references and imports its JS chunks. The
|
|
117
|
+
* stored payload is reused as-is by navigation, so the click loads no new code.
|
|
118
|
+
*
|
|
119
|
+
* Control headers are NOT acted on here. A speculative prefetch must never
|
|
120
|
+
* reload the page or throw a redirect — if the response carries X-RSC-Reload
|
|
121
|
+
* or X-RSC-Redirect, we drop it (resolve null) and let the real navigation
|
|
122
|
+
* re-fetch and honor it.
|
|
123
|
+
*
|
|
124
|
+
* Inflight + storage key selection:
|
|
125
|
+
*
|
|
126
|
+
* - `forceSourceScope` (Link opted in with `prefetchKey=":source"`): single
|
|
127
|
+
* inflight registration under `sourceKey`; entry stored under `sourceKey`.
|
|
128
|
+
* No wildcard leak is possible.
|
|
129
|
+
*
|
|
130
|
+
* - Otherwise: dual inflight registration under both `wildcardKey` and
|
|
131
|
+
* `sourceKey` so same-source navigations adopt directly via their own
|
|
132
|
+
* source key. Storage key is chosen at response time from the
|
|
133
|
+
* `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (intercept
|
|
134
|
+
* modals etc.), anything else → `wildcardKey`. The entry records its scope
|
|
135
|
+
* so cross-source navigations that adopted via `wildcardKey` can bail out
|
|
136
|
+
* in `navigation-client.ts` when the adopted entry turns out source-scoped.
|
|
57
137
|
*/
|
|
58
138
|
function executePrefetchFetch(
|
|
59
|
-
|
|
139
|
+
wildcardKey: string,
|
|
140
|
+
sourceKey: string,
|
|
60
141
|
fetchUrl: string,
|
|
142
|
+
forceSourceScope: boolean,
|
|
143
|
+
expectedRouterId?: string,
|
|
61
144
|
signal?: AbortSignal,
|
|
62
|
-
): Promise<
|
|
145
|
+
): Promise<DecodedPrefetch | null> {
|
|
63
146
|
const gen = currentGeneration();
|
|
64
|
-
|
|
147
|
+
const inflightKeys = forceSourceScope
|
|
148
|
+
? [sourceKey]
|
|
149
|
+
: [wildcardKey, sourceKey];
|
|
150
|
+
for (const k of inflightKeys) markPrefetchInflight(k);
|
|
65
151
|
|
|
66
|
-
|
|
152
|
+
const promise: Promise<DecodedPrefetch | null> = fetch(fetchUrl, {
|
|
67
153
|
priority: "low" as RequestPriority,
|
|
154
|
+
// During an action's flight the state is not rotated, so the old
|
|
155
|
+
// X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass it so
|
|
156
|
+
// a prefetch fetches fresh rather than warming the map with stale bytes (the
|
|
157
|
+
// fence's HTTP-cache-bypass requirement applies to prefetch as well as
|
|
158
|
+
// navigation fetches).
|
|
159
|
+
...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
|
|
68
160
|
signal,
|
|
69
161
|
headers: {
|
|
70
162
|
"X-Rango-State": getRangoState(),
|
|
@@ -72,64 +164,194 @@ function executePrefetchFetch(
|
|
|
72
164
|
"X-Rango-Prefetch": "1",
|
|
73
165
|
},
|
|
74
166
|
})
|
|
75
|
-
.then(
|
|
76
|
-
if (!response.ok) return;
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
167
|
+
.then((response) => {
|
|
168
|
+
if (!response.ok || !decoder) return null;
|
|
169
|
+
// Control headers mean this response is stale (reload) or redirecting.
|
|
170
|
+
// Don't warm it — drop so navigation re-fetches and acts on the header.
|
|
171
|
+
if (
|
|
172
|
+
response.headers.has("X-RSC-Reload") ||
|
|
173
|
+
response.headers.has("X-RSC-Redirect")
|
|
174
|
+
) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
// Integrity check: never warm (or decode/import the chunks of) a foreign
|
|
178
|
+
// app's payload. A speculative prefetch must never reload — just drop it;
|
|
179
|
+
// navigation re-fetches and the server steers it.
|
|
180
|
+
if (isForeignRouterId(response, expectedRouterId)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const scope: "source" | "wildcard" =
|
|
185
|
+
forceSourceScope ||
|
|
186
|
+
response.headers.get("x-rsc-prefetch-scope") === "source"
|
|
187
|
+
? "source"
|
|
188
|
+
: "wildcard";
|
|
189
|
+
const storageKey = scope === "source" ? sourceKey : wildcardKey;
|
|
190
|
+
|
|
191
|
+
// Track stream completion off a tee so navigation's scroll/revalidation
|
|
192
|
+
// gating matches the fresh-fetch path; decode the other branch.
|
|
193
|
+
let resolveStreamComplete!: () => void;
|
|
194
|
+
const streamComplete = new Promise<void>((resolve) => {
|
|
195
|
+
resolveStreamComplete = resolve;
|
|
86
196
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
197
|
+
const tracked = teeWithCompletion(
|
|
198
|
+
response,
|
|
199
|
+
() => resolveStreamComplete(),
|
|
200
|
+
signal,
|
|
201
|
+
// Speculative prefetch: a never-consumed/aborted stream error is benign.
|
|
202
|
+
true,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Eager decode: parsing the Flight stream imports the route's client
|
|
206
|
+
// chunks now, not on click.
|
|
207
|
+
const payload = decoder(Promise.resolve(tracked));
|
|
208
|
+
// Mark handled so an unconsumed prefetch decode error stays quiet; the
|
|
209
|
+
// error is still surfaced to navigation if it consumes the entry.
|
|
210
|
+
payload.catch(() => {});
|
|
211
|
+
|
|
212
|
+
const entry: DecodedPrefetch = { payload, streamComplete, scope };
|
|
213
|
+
storePrefetch(storageKey, entry, gen);
|
|
214
|
+
return entry;
|
|
91
215
|
})
|
|
216
|
+
.catch(() => null)
|
|
92
217
|
.finally(() => {
|
|
93
|
-
clearPrefetchInflight(
|
|
218
|
+
clearPrefetchInflight(inflightKeys[0]!);
|
|
94
219
|
});
|
|
220
|
+
|
|
221
|
+
setInflightPromiseWithAliases(inflightKeys, promise);
|
|
222
|
+
return promise;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Dedup check for prefetch entry presence.
|
|
227
|
+
*
|
|
228
|
+
* Forced `:source` must NOT dedupe against a pre-existing wildcard entry —
|
|
229
|
+
* otherwise the source slot would stay unpopulated and navigation from
|
|
230
|
+
* this source would fall through to the (potentially wrong) wildcard
|
|
231
|
+
* response, defeating the opt-out.
|
|
232
|
+
*/
|
|
233
|
+
function hasPrefetchHit(
|
|
234
|
+
forceSourceScope: boolean,
|
|
235
|
+
wildcardKey: string,
|
|
236
|
+
sourceKey: string,
|
|
237
|
+
): boolean {
|
|
238
|
+
return forceSourceScope
|
|
239
|
+
? hasPrefetch(sourceKey)
|
|
240
|
+
: hasPrefetch(wildcardKey) || hasPrefetch(sourceKey);
|
|
95
241
|
}
|
|
96
242
|
|
|
97
243
|
/**
|
|
98
244
|
* Prefetch (direct): fetch with low priority and store in in-memory cache.
|
|
99
245
|
* Used by hover strategy -- fires immediately without queueing.
|
|
246
|
+
*
|
|
247
|
+
* By default the wildcard key (Rango-state-keyed) is used for inflight
|
|
248
|
+
* dedup and for responses that are not source-sensitive; source-scoped
|
|
249
|
+
* storage is automatic when the server emits `X-RSC-Prefetch-Scope: source`.
|
|
250
|
+
*
|
|
251
|
+
* Pass `prefetchKey=":source"` to force source-scoped inflight + storage
|
|
252
|
+
* (e.g. when the target uses a custom `revalidate()` that reads
|
|
253
|
+
* `currentUrl` and the wildcard slot would serve the wrong diff).
|
|
100
254
|
*/
|
|
101
255
|
export function prefetchDirect(
|
|
102
256
|
url: string,
|
|
103
257
|
segmentIds: string[],
|
|
104
258
|
version?: string,
|
|
259
|
+
routerId?: string,
|
|
260
|
+
prefetchKey?: ":source",
|
|
105
261
|
): void {
|
|
106
262
|
if (!shouldPrefetch()) return;
|
|
107
263
|
|
|
108
|
-
const targetUrl = buildPrefetchUrl(url, segmentIds, version);
|
|
264
|
+
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
109
265
|
if (!targetUrl) return;
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
266
|
+
const forceSourceScope = prefetchKey === ":source";
|
|
267
|
+
// Skip same-page prefetch — a same-page diff is trivial and would corrupt
|
|
268
|
+
// the wildcard cache entry used for cross-page navigation.
|
|
269
|
+
// When `:source` is forced the entry is source-scoped (single-aliased to
|
|
270
|
+
// itself), so it cannot poison any shared slot — allow it.
|
|
271
|
+
if (!forceSourceScope && isSamePage(url)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const sourceHref = window.location.href;
|
|
275
|
+
const rangoState = getRangoState();
|
|
276
|
+
const wildcardKey = buildPrefetchKey(rangoState, targetUrl);
|
|
277
|
+
const sourceKey = buildSourceKey(rangoState, sourceHref, targetUrl);
|
|
278
|
+
if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
|
|
279
|
+
debugLog("[prefetch] direct dedup (key already exists)", {
|
|
280
|
+
url,
|
|
281
|
+
wildcardKey,
|
|
282
|
+
sourceKey,
|
|
283
|
+
forceSourceScope,
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
debugLog("[prefetch] direct fetch", {
|
|
288
|
+
url,
|
|
289
|
+
wildcardKey,
|
|
290
|
+
sourceKey,
|
|
291
|
+
source: sourceHref,
|
|
292
|
+
forceSourceScope,
|
|
293
|
+
});
|
|
294
|
+
executePrefetchFetch(
|
|
295
|
+
wildcardKey,
|
|
296
|
+
sourceKey,
|
|
297
|
+
targetUrl.toString(),
|
|
298
|
+
forceSourceScope,
|
|
299
|
+
routerId,
|
|
300
|
+
);
|
|
113
301
|
}
|
|
114
302
|
|
|
115
303
|
/**
|
|
116
304
|
* Prefetch (queued): goes through the concurrency-limited queue.
|
|
117
305
|
* Used by viewport/render strategies to avoid flooding the server.
|
|
118
|
-
* Returns the
|
|
306
|
+
* Returns the inflight key (wildcard by default, source-scoped when
|
|
307
|
+
* `prefetchKey=":source"` is passed).
|
|
119
308
|
*/
|
|
120
309
|
export function prefetchQueued(
|
|
121
310
|
url: string,
|
|
122
311
|
segmentIds: string[],
|
|
123
312
|
version?: string,
|
|
313
|
+
routerId?: string,
|
|
314
|
+
prefetchKey?: ":source",
|
|
124
315
|
): string {
|
|
125
316
|
if (!shouldPrefetch()) return "";
|
|
126
|
-
const targetUrl = buildPrefetchUrl(url, segmentIds, version);
|
|
317
|
+
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
127
318
|
if (!targetUrl) return "";
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
319
|
+
const forceSourceScope = prefetchKey === ":source";
|
|
320
|
+
if (!forceSourceScope && isSamePage(url)) {
|
|
321
|
+
return "";
|
|
322
|
+
}
|
|
323
|
+
const sourceHref = window.location.href;
|
|
324
|
+
const rangoState = getRangoState();
|
|
325
|
+
const wildcardKey = buildPrefetchKey(rangoState, targetUrl);
|
|
326
|
+
const sourceKey = buildSourceKey(rangoState, sourceHref, targetUrl);
|
|
327
|
+
const queueKey = forceSourceScope ? sourceKey : wildcardKey;
|
|
328
|
+
if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
|
|
329
|
+
debugLog("[prefetch] queued dedup (key already exists)", {
|
|
330
|
+
url,
|
|
331
|
+
wildcardKey,
|
|
332
|
+
sourceKey,
|
|
333
|
+
forceSourceScope,
|
|
334
|
+
});
|
|
335
|
+
return queueKey;
|
|
336
|
+
}
|
|
130
337
|
const fetchUrlStr = targetUrl.toString();
|
|
131
|
-
enqueuePrefetch(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
338
|
+
enqueuePrefetch(queueKey, (signal) => {
|
|
339
|
+
// Re-check at execution time: a hover-triggered prefetchDirect may
|
|
340
|
+
// have started or completed this key while the item sat in the queue.
|
|
341
|
+
if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
|
|
342
|
+
return Promise.resolve();
|
|
343
|
+
}
|
|
344
|
+
if (!forceSourceScope && isSamePage(url)) {
|
|
345
|
+
return Promise.resolve();
|
|
346
|
+
}
|
|
347
|
+
return executePrefetchFetch(
|
|
348
|
+
wildcardKey,
|
|
349
|
+
sourceKey,
|
|
350
|
+
fetchUrlStr,
|
|
351
|
+
forceSourceScope,
|
|
352
|
+
routerId,
|
|
353
|
+
signal,
|
|
354
|
+
).then(() => {});
|
|
355
|
+
});
|
|
356
|
+
return queueKey;
|
|
135
357
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Honors browser reduced-data preferences when available.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isPrefetchCacheDisabled } from "./cache.js";
|
|
9
|
+
|
|
8
10
|
type NavigatorWithConnection = Navigator & {
|
|
9
11
|
connection?: {
|
|
10
12
|
saveData?: boolean;
|
|
@@ -18,6 +20,10 @@ type NavigatorWithConnection = Navigator & {
|
|
|
18
20
|
export function shouldPrefetch(): boolean {
|
|
19
21
|
if (typeof window === "undefined") return false;
|
|
20
22
|
|
|
23
|
+
// When prefetchCacheTTL is false/0, prefetching is fully disabled —
|
|
24
|
+
// no point issuing requests whose responses will be discarded.
|
|
25
|
+
if (isPrefetchCacheDisabled()) return false;
|
|
26
|
+
|
|
21
27
|
const nav =
|
|
22
28
|
typeof navigator !== "undefined"
|
|
23
29
|
? (navigator as NavigatorWithConnection)
|
|
@@ -5,11 +5,19 @@
|
|
|
5
5
|
* Hover prefetches bypass this queue — they fire directly for immediate response
|
|
6
6
|
* to user intent.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Draining waits for an idle main-thread moment and for viewport images to
|
|
9
|
+
* finish loading, so prefetch fetch() calls never compete with critical
|
|
10
|
+
* resources for the browser's connection pool.
|
|
11
|
+
*
|
|
12
|
+
* When a navigation starts, queued prefetches are cancelled but executing ones
|
|
13
|
+
* are left running. Navigation can reuse their in-flight responses via the
|
|
14
|
+
* prefetch cache's inflight promise map, avoiding duplicate requests.
|
|
10
15
|
*/
|
|
11
16
|
|
|
17
|
+
import { wait, waitForIdle, waitForViewportImages } from "./resource-ready.js";
|
|
18
|
+
|
|
12
19
|
const MAX_CONCURRENT = 2;
|
|
20
|
+
const IMAGE_WAIT_TIMEOUT = 2000;
|
|
13
21
|
|
|
14
22
|
let active = 0;
|
|
15
23
|
const queue: Array<{
|
|
@@ -18,7 +26,9 @@ const queue: Array<{
|
|
|
18
26
|
}> = [];
|
|
19
27
|
const queued = new Set<string>();
|
|
20
28
|
const executing = new Set<string>();
|
|
21
|
-
|
|
29
|
+
const abortControllers = new Map<string, AbortController>();
|
|
30
|
+
let drainScheduled = false;
|
|
31
|
+
let drainGeneration = 0;
|
|
22
32
|
|
|
23
33
|
function startExecution(
|
|
24
34
|
key: string,
|
|
@@ -26,18 +36,52 @@ function startExecution(
|
|
|
26
36
|
): void {
|
|
27
37
|
active++;
|
|
28
38
|
executing.add(key);
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
const ac = new AbortController();
|
|
40
|
+
abortControllers.set(key, ac);
|
|
41
|
+
execute(ac.signal).finally(() => {
|
|
42
|
+
abortControllers.delete(key);
|
|
31
43
|
// Only decrement if this key wasn't already cleared by cancelAllPrefetches.
|
|
32
44
|
// Without this guard, cancelled tasks' .finally() would underflow active
|
|
33
45
|
// below zero, breaking the MAX_CONCURRENT guarantee.
|
|
34
46
|
if (executing.delete(key)) {
|
|
35
47
|
active--;
|
|
36
48
|
}
|
|
37
|
-
|
|
49
|
+
scheduleDrain();
|
|
38
50
|
});
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Schedule a drain after the browser is idle and viewport images are loaded.
|
|
55
|
+
* Coalesces multiple drain requests into a single deferred callback so
|
|
56
|
+
* batch completion doesn't schedule redundant waits.
|
|
57
|
+
*
|
|
58
|
+
* The two-step wait ensures prefetch fetch() calls don't compete with
|
|
59
|
+
* images for the browser's connection pool:
|
|
60
|
+
* 1. waitForIdle — yield until the main thread has a quiet moment
|
|
61
|
+
* 2. waitForViewportImages OR 2s timeout — yield until visible images
|
|
62
|
+
* finish loading, but don't let slow/broken images block indefinitely
|
|
63
|
+
*/
|
|
64
|
+
function scheduleDrain(): void {
|
|
65
|
+
if (drainScheduled) return;
|
|
66
|
+
if (active >= MAX_CONCURRENT || queue.length === 0) return;
|
|
67
|
+
drainScheduled = true;
|
|
68
|
+
const gen = drainGeneration;
|
|
69
|
+
waitForIdle()
|
|
70
|
+
.then(() =>
|
|
71
|
+
Promise.race([waitForViewportImages(), wait(IMAGE_WAIT_TIMEOUT)]),
|
|
72
|
+
)
|
|
73
|
+
.then(() => {
|
|
74
|
+
// Stale drain: a cancel/abort happened while we were waiting, and a fresh
|
|
75
|
+
// scheduleDrain may already own drainScheduled for the new generation.
|
|
76
|
+
// Bail WITHOUT clearing the flag so we don't clobber the live wait's
|
|
77
|
+
// single-in-flight-drain coalescing (clearing it here would let the next
|
|
78
|
+
// enqueue start a third overlapping wait).
|
|
79
|
+
if (gen !== drainGeneration) return;
|
|
80
|
+
drainScheduled = false;
|
|
81
|
+
if (queue.length > 0) drain();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
41
85
|
function drain(): void {
|
|
42
86
|
while (active < MAX_CONCURRENT && queue.length > 0) {
|
|
43
87
|
const item = queue.shift()!;
|
|
@@ -48,9 +92,10 @@ function drain(): void {
|
|
|
48
92
|
|
|
49
93
|
/**
|
|
50
94
|
* Enqueue a prefetch for concurrency-limited execution.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* Deduplicates by key — items already queued or executing
|
|
95
|
+
* Execution is deferred until the browser is idle and viewport images
|
|
96
|
+
* have finished loading, so prefetches never compete with critical
|
|
97
|
+
* resources. Deduplicates by key — items already queued or executing
|
|
98
|
+
* are skipped.
|
|
54
99
|
*
|
|
55
100
|
* The executor receives an AbortSignal that is aborted when
|
|
56
101
|
* cancelAllPrefetches() is called (e.g. on navigation start).
|
|
@@ -61,22 +106,81 @@ export function enqueuePrefetch(
|
|
|
61
106
|
): void {
|
|
62
107
|
if (queued.has(key) || executing.has(key)) return;
|
|
63
108
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
109
|
+
queued.add(key);
|
|
110
|
+
queue.push({ key, execute });
|
|
111
|
+
scheduleDrain();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Normalize a URL-like string for keep-alive matching: parse against a
|
|
116
|
+
* placeholder origin and strip internal `_rsc_*` query params. Returns
|
|
117
|
+
* `pathname + search` so comparisons ignore hash and the internal params
|
|
118
|
+
* that prefetch appends to targets (`_rsc_partial`, `_rsc_segments`,
|
|
119
|
+
* `_rsc_v`, `_rsc_rid`, `_rsc_stale`).
|
|
120
|
+
*/
|
|
121
|
+
function normalizeForMatch(urlish: string): string {
|
|
122
|
+
try {
|
|
123
|
+
const u = new URL(urlish, "http://placeholder");
|
|
124
|
+
for (const k of [...u.searchParams.keys()]) {
|
|
125
|
+
if (k.startsWith("_rsc_")) u.searchParams.delete(k);
|
|
126
|
+
}
|
|
127
|
+
return u.pathname + u.search;
|
|
128
|
+
} catch {
|
|
129
|
+
return urlish;
|
|
69
130
|
}
|
|
70
131
|
}
|
|
71
132
|
|
|
72
133
|
/**
|
|
73
|
-
* Cancel
|
|
74
|
-
*
|
|
75
|
-
*
|
|
134
|
+
* Cancel queued prefetches and abort in-flight ones that don't match
|
|
135
|
+
* the current navigation target. If `keepUrl` is provided, the
|
|
136
|
+
* executing prefetch whose key targets that URL is kept alive so
|
|
137
|
+
* navigation can reuse its response via consumeInflightPrefetch.
|
|
138
|
+
*
|
|
139
|
+
* Called when a navigation starts via the NavigationProvider's
|
|
140
|
+
* event controller subscription.
|
|
76
141
|
*/
|
|
77
|
-
export function cancelAllPrefetches(): void {
|
|
78
|
-
|
|
79
|
-
|
|
142
|
+
export function cancelAllPrefetches(keepUrl?: string | null): void {
|
|
143
|
+
queue.length = 0;
|
|
144
|
+
queued.clear();
|
|
145
|
+
drainScheduled = false;
|
|
146
|
+
drainGeneration++;
|
|
147
|
+
|
|
148
|
+
// Abort in-flight prefetches that aren't for the navigation target.
|
|
149
|
+
// Key shapes (see prefetch/cache.ts buildPrefetchKey):
|
|
150
|
+
// wildcard: "rangoState\0/target?..."
|
|
151
|
+
// source-scoped: "rangoState\0sourceHref\0/target?..."
|
|
152
|
+
// The target portion is always the final \0-delimited segment and
|
|
153
|
+
// includes internal `_rsc_*` params (from buildPrefetchUrl); keepUrl
|
|
154
|
+
// comes from NavigationProvider's pendingUrl which is the bare
|
|
155
|
+
// navigation target. Normalize both sides before comparing.
|
|
156
|
+
const normalizedKeep = keepUrl ? normalizeForMatch(keepUrl) : null;
|
|
157
|
+
for (const [key, ac] of abortControllers) {
|
|
158
|
+
const lastNul = key.lastIndexOf("\0");
|
|
159
|
+
const target = lastNul >= 0 ? key.substring(lastNul + 1) : "";
|
|
160
|
+
if (
|
|
161
|
+
normalizedKeep &&
|
|
162
|
+
target &&
|
|
163
|
+
normalizeForMatch(target) === normalizedKeep
|
|
164
|
+
)
|
|
165
|
+
continue;
|
|
166
|
+
ac.abort();
|
|
167
|
+
abortControllers.delete(key);
|
|
168
|
+
if (executing.delete(key)) {
|
|
169
|
+
active--;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Hard-cancel everything including in-flight prefetches.
|
|
176
|
+
* Used by clearPrefetchCache (server action invalidation) where
|
|
177
|
+
* in-flight responses would be stale.
|
|
178
|
+
*/
|
|
179
|
+
export function abortAllPrefetches(): void {
|
|
180
|
+
for (const ac of abortControllers.values()) {
|
|
181
|
+
ac.abort();
|
|
182
|
+
}
|
|
183
|
+
abortControllers.clear();
|
|
80
184
|
|
|
81
185
|
queue.length = 0;
|
|
82
186
|
queued.clear();
|
|
@@ -85,4 +189,6 @@ export function cancelAllPrefetches(): void {
|
|
|
85
189
|
// so active settles at 0 without underflow.
|
|
86
190
|
executing.clear();
|
|
87
191
|
active = 0;
|
|
192
|
+
drainScheduled = false;
|
|
193
|
+
drainGeneration++;
|
|
88
194
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Readiness
|
|
3
|
+
*
|
|
4
|
+
* Utilities to defer speculative prefetches until critical resources
|
|
5
|
+
* (viewport images) have finished loading. Prevents prefetch fetch()
|
|
6
|
+
* calls from competing with images for the browser's connection pool.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve when all in-viewport images have finished loading.
|
|
11
|
+
* Returns immediately if no images are pending.
|
|
12
|
+
*
|
|
13
|
+
* Only checks images that exist at call time — does not observe
|
|
14
|
+
* dynamically added images. For SPA navigations where new images
|
|
15
|
+
* appear after render, call this after the navigation settles.
|
|
16
|
+
*/
|
|
17
|
+
export function waitForViewportImages(): Promise<void> {
|
|
18
|
+
if (typeof document === "undefined") return Promise.resolve();
|
|
19
|
+
|
|
20
|
+
const pending = Array.from(document.querySelectorAll("img")).filter((img) => {
|
|
21
|
+
if (img.complete) return false;
|
|
22
|
+
const rect = img.getBoundingClientRect();
|
|
23
|
+
return (
|
|
24
|
+
rect.bottom > 0 &&
|
|
25
|
+
rect.right > 0 &&
|
|
26
|
+
rect.top < window.innerHeight &&
|
|
27
|
+
rect.left < window.innerWidth
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (pending.length === 0) return Promise.resolve();
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const settled = new Set<HTMLImageElement>();
|
|
35
|
+
|
|
36
|
+
const settle = (img: HTMLImageElement) => {
|
|
37
|
+
if (settled.has(img)) return;
|
|
38
|
+
settled.add(img);
|
|
39
|
+
if (settled.size >= pending.length) resolve();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (const img of pending) {
|
|
43
|
+
img.addEventListener("load", () => settle(img), { once: true });
|
|
44
|
+
img.addEventListener("error", () => settle(img), { once: true });
|
|
45
|
+
// Re-check: image may have completed between the initial filter
|
|
46
|
+
// and listener attachment. settle() is idempotent per image, so
|
|
47
|
+
// a queued load event firing afterward is harmless.
|
|
48
|
+
if (img.complete) settle(img);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve after the given number of milliseconds.
|
|
55
|
+
*/
|
|
56
|
+
export function wait(ms: number): Promise<void> {
|
|
57
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve when the browser has an idle main-thread moment.
|
|
62
|
+
* Uses requestIdleCallback where available, falls back to setTimeout.
|
|
63
|
+
*
|
|
64
|
+
* This is a scheduling hint, not an asset-loaded detector — combine
|
|
65
|
+
* with waitForViewportImages() for full resource readiness.
|
|
66
|
+
*/
|
|
67
|
+
export function waitForIdle(timeout = 200): Promise<void> {
|
|
68
|
+
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
window.requestIdleCallback(() => resolve(), { timeout });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
setTimeout(resolve, 0);
|
|
76
|
+
});
|
|
77
|
+
}
|