@rangojs/router 0.2.0 → 0.4.1

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.
Files changed (105) hide show
  1. package/README.md +19 -1
  2. package/dist/types/browser/event-controller.d.ts +4 -0
  3. package/dist/types/browser/link-interceptor.d.ts +17 -7
  4. package/dist/types/browser/navigation-bridge.d.ts +13 -1
  5. package/dist/types/browser/notify-listeners.d.ts +2 -0
  6. package/dist/types/browser/prefetch/cache.d.ts +1 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +31 -0
  8. package/dist/types/browser/prefetch/invalidation.d.ts +6 -0
  9. package/dist/types/browser/prefetch/loader.d.ts +3 -1
  10. package/dist/types/browser/prefetch/observer.d.ts +3 -6
  11. package/dist/types/browser/prefetch/runtime.d.ts +0 -1
  12. package/dist/types/browser/rango-state.d.ts +4 -0
  13. package/dist/types/browser/react/Link.d.ts +11 -19
  14. package/dist/types/browser/react/context.d.ts +3 -0
  15. package/dist/types/browser/rsc-router.d.ts +7 -2
  16. package/dist/types/browser/types.d.ts +6 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +11 -3
  18. package/dist/types/cache/cache-scope.d.ts +82 -3
  19. package/dist/types/cache/cf/cf-cache-store.d.ts +2 -2
  20. package/dist/types/cache/index.d.ts +3 -1
  21. package/dist/types/cache/search-params-filter.d.ts +64 -0
  22. package/dist/types/cache/shell-snapshot.d.ts +4 -4
  23. package/dist/types/cache/types.d.ts +36 -2
  24. package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -2
  25. package/dist/types/index.d.ts +1 -0
  26. package/dist/types/index.rsc.d.ts +1 -0
  27. package/dist/types/router/match-middleware/cache-lookup.d.ts +13 -0
  28. package/dist/types/router/navigation-snapshot.d.ts +29 -0
  29. package/dist/types/router/prefetch-default.d.ts +28 -0
  30. package/dist/types/router/router-interfaces.d.ts +7 -0
  31. package/dist/types/router/router-options.d.ts +54 -0
  32. package/dist/types/rsc/capture-queue.d.ts +6 -0
  33. package/dist/types/rsc/shell-build-manifest.d.ts +7 -1
  34. package/dist/types/rsc/shell-capture.d.ts +15 -7
  35. package/dist/types/rsc/shell-serve.d.ts +11 -4
  36. package/dist/types/rsc/types.d.ts +19 -0
  37. package/dist/types/server/request-context.d.ts +57 -4
  38. package/dist/types/testing/e2e/index.d.ts +2 -2
  39. package/dist/types/testing/e2e/page-helpers.d.ts +24 -0
  40. package/dist/types/testing/render-route.d.ts +10 -1
  41. package/dist/types/testing/shell-status.d.ts +6 -3
  42. package/dist/types/vite/plugin-types.d.ts +1 -1
  43. package/dist/vite/index.js +9 -6
  44. package/package.json +23 -22
  45. package/skills/caching/SKILL.md +39 -0
  46. package/skills/comparison/references/framework-comparison.md +9 -2
  47. package/skills/links/SKILL.md +24 -0
  48. package/skills/ppr/SKILL.md +80 -16
  49. package/skills/router-setup/SKILL.md +9 -0
  50. package/skills/testing/client-components.md +15 -14
  51. package/skills/vercel/SKILL.md +1 -1
  52. package/src/browser/event-controller.ts +110 -11
  53. package/src/browser/link-interceptor.ts +469 -20
  54. package/src/browser/navigation-bridge.ts +71 -2
  55. package/src/browser/navigation-store.ts +24 -1
  56. package/src/browser/notify-listeners.ts +22 -0
  57. package/src/browser/prefetch/cache.ts +4 -2
  58. package/src/browser/prefetch/default-strategy.ts +74 -0
  59. package/src/browser/prefetch/invalidation.ts +30 -0
  60. package/src/browser/prefetch/loader.ts +18 -17
  61. package/src/browser/prefetch/observer.ts +50 -22
  62. package/src/browser/prefetch/runtime.ts +0 -1
  63. package/src/browser/rango-state.ts +21 -0
  64. package/src/browser/react/Link.tsx +111 -101
  65. package/src/browser/react/NavigationProvider.tsx +1 -0
  66. package/src/browser/react/context.ts +4 -0
  67. package/src/browser/rsc-router.tsx +30 -9
  68. package/src/browser/types.ts +6 -0
  69. package/src/cache/cache-key-utils.ts +18 -4
  70. package/src/cache/cache-runtime.ts +7 -2
  71. package/src/cache/cache-scope.ts +152 -23
  72. package/src/cache/cf/cf-cache-store.ts +55 -16
  73. package/src/cache/document-cache.ts +7 -1
  74. package/src/cache/index.ts +7 -0
  75. package/src/cache/search-params-filter.ts +118 -0
  76. package/src/cache/shell-snapshot.ts +8 -4
  77. package/src/cache/types.ts +39 -2
  78. package/src/cache/vercel/vercel-cache-store.ts +22 -3
  79. package/src/index.rsc.ts +4 -0
  80. package/src/index.ts +6 -0
  81. package/src/router/match-middleware/cache-lookup.ts +146 -33
  82. package/src/router/match-middleware/cache-store.ts +70 -0
  83. package/src/router/navigation-snapshot.ts +46 -6
  84. package/src/router/prefetch-default.ts +59 -0
  85. package/src/router/router-interfaces.ts +8 -0
  86. package/src/router/router-options.ts +59 -1
  87. package/src/router.ts +7 -0
  88. package/src/rsc/capture-queue.ts +24 -1
  89. package/src/rsc/full-payload.ts +1 -0
  90. package/src/rsc/handler.ts +10 -0
  91. package/src/rsc/response-cache-serve.ts +1 -1
  92. package/src/rsc/rsc-rendering.ts +367 -40
  93. package/src/rsc/shell-build-manifest.ts +8 -1
  94. package/src/rsc/shell-capture.ts +99 -50
  95. package/src/rsc/shell-serve.ts +14 -6
  96. package/src/rsc/types.ts +19 -0
  97. package/src/server/request-context.ts +62 -3
  98. package/src/testing/dispatch.ts +21 -3
  99. package/src/testing/e2e/index.ts +6 -0
  100. package/src/testing/e2e/page-helpers.ts +47 -0
  101. package/src/testing/e2e/parity.ts +13 -0
  102. package/src/testing/render-route.tsx +86 -17
  103. package/src/testing/shell-status.ts +20 -3
  104. package/src/vite/plugin-types.ts +1 -1
  105. package/src/vite/plugins/vercel-output.ts +2 -2
package/README.md CHANGED
@@ -13,7 +13,25 @@ the end. For the design rationale behind these APIs, read
13
13
  [Why Rango](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/why-rango.md); this page shows how it feels, that page
14
14
  argues why it's right.
15
15
 
16
- ## Install
16
+ ## Start a new app
17
+
18
+ The recommended path is to scaffold a complete app with
19
+ [`create-rango`](https://github.com/rangojs/templates):
20
+
21
+ ```bash
22
+ pnpm create rango my-app
23
+ ```
24
+
25
+ Select a deployment target with `--template basic`, `--template cloudflare`, or
26
+ `--template vercel`. The templates include streaming RSC, typed routes, Server
27
+ Actions, Tailwind CSS, and production deployment configuration. For a plain
28
+ JavaScript Node app, add `--template basic --js`. With npm, run
29
+ `npm create rango@latest my-app`. The scaffolder currently requires Node.js 24
30
+ or newer.
31
+
32
+ ## Install manually
33
+
34
+ If you are adding Rango to an existing Vite RSC project:
17
35
 
18
36
  ```bash
19
37
  npm install @rangojs/router@experimental react @vitejs/plugin-rsc
@@ -205,6 +205,9 @@ export interface EventController {
205
205
  /** Whether any concurrent actions have occurred (shared across all handles) */
206
206
  hadAnyConcurrentActions(): boolean;
207
207
  }
208
+ type LocationChangeController = Pick<EventController, "getState" | "subscribe">;
209
+ /** Share one controller subscription across all location-change consumers. */
210
+ export declare function subscribeToLocationChange(eventController: LocationChangeController, listener: (href: string) => void): () => void;
208
211
  export interface EventControllerConfig {
209
212
  initialLocation?: NavigationLocation;
210
213
  }
@@ -219,3 +222,4 @@ export interface EventControllerConfig {
219
222
  * Actions use mergeMap semantics (all run concurrently, consolidate at end).
220
223
  */
221
224
  export declare function createEventController(config?: EventControllerConfig): EventController;
225
+ export {};
@@ -1,9 +1,8 @@
1
1
  import type { LinkInterceptorOptions, NavigateOptions } from "./types.js";
2
- /**
3
- * Check if an anchor points to the same page with only a hash change.
4
- * Used by both Link component and link-interceptor to let the browser
5
- * handle anchor scrolling natively.
6
- */
2
+ import { type EventController } from "./event-controller.js";
3
+ import type { PrefetchStrategy } from "../router/prefetch-default.js";
4
+ type PrefetchScopeChangeListener = () => void;
5
+ export declare function subscribeToPrefetchScopeChange(element: HTMLAnchorElement, listener: PrefetchScopeChangeListener): () => void;
7
6
  export declare function isHashOnlyNavigation(anchor: HTMLAnchorElement): boolean;
8
7
  /**
9
8
  * Default link interception predicate
@@ -19,11 +18,12 @@ export declare function isHashOnlyNavigation(anchor: HTMLAnchorElement): boolean
19
18
  * @returns true if the link should be intercepted
20
19
  */
21
20
  export declare function defaultShouldIntercept(link: HTMLAnchorElement): boolean;
21
+ export declare function isPrefetchScopeDisabled(element: Element): boolean;
22
22
  /**
23
23
  * Set up link interception for SPA navigation
24
24
  *
25
- * Attaches a global click handler to intercept clicks on anchor elements
26
- * and call the onNavigate callback instead of performing a full page load.
25
+ * Attaches a global click handler to intercept clicks on anchor elements and
26
+ * call the onNavigate callback instead of performing a full page load.
27
27
  *
28
28
  * @param onNavigate - Callback when a link should navigate via SPA
29
29
  * @param options - Configuration options
@@ -41,3 +41,13 @@ export declare function defaultShouldIntercept(link: HTMLAnchorElement): boolean
41
41
  * ```
42
42
  */
43
43
  export declare function setupLinkInterception(onNavigate: (url: string, options?: NavigateOptions) => void, options?: LinkInterceptorOptions): () => void;
44
+ export type DelegatedPrefetchCallback = (url: string, priority: "direct" | "queued") => void | (() => void);
45
+ export interface DelegatedPrefetchOptions {
46
+ eventController: Pick<EventController, "getState" | "subscribe">;
47
+ shouldPrefetch?: (link: HTMLAnchorElement) => boolean;
48
+ defaultPrefetch?: PrefetchStrategy;
49
+ root?: HTMLElement;
50
+ basename?: string;
51
+ }
52
+ export declare function setupDelegatedLinkPrefetch(onPrefetch: DelegatedPrefetchCallback, options: DelegatedPrefetchOptions): () => void;
53
+ export {};
@@ -1,6 +1,7 @@
1
- import type { NavigationBridge, NavigationBridgeConfig } from "./types.js";
1
+ import type { NavigationBridge, NavigationBridgeConfig, NavigationStore } from "./types.js";
2
2
  import { createNavigationTransaction } from "./navigation-transaction.js";
3
3
  import type { EventController } from "./event-controller.js";
4
+ import type { PrefetchStrategy } from "../router/prefetch-default.js";
4
5
  export { createNavigationTransaction };
5
6
  /**
6
7
  * Extended configuration for navigation bridge with event controller
@@ -9,7 +10,18 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
9
10
  eventController: EventController;
10
11
  /** RSC version from initial payload metadata. */
11
12
  version?: string;
13
+ /** Server-resolved default used by both Links and delegated anchors. */
14
+ defaultPrefetch?: PrefetchStrategy;
15
+ /** Canonical router basename used to scope delegated anchor prefetch. */
16
+ basename?: string;
12
17
  }
18
+ export interface NavigationBridgeDelegatedPrefetchOptions {
19
+ defaultPrefetch?: PrefetchStrategy;
20
+ root?: HTMLElement;
21
+ basename?: string;
22
+ }
23
+ /** Register delegated anchor prefetch with the production bridge wiring. */
24
+ export declare function setupNavigationBridgeDelegatedPrefetch(store: NavigationStore, eventController: EventController, getVersion: () => string | undefined, options?: NavigationBridgeDelegatedPrefetchOptions): () => void;
13
25
  /**
14
26
  * Create a navigation bridge for handling client-side navigation
15
27
  *
@@ -0,0 +1,2 @@
1
+ /** Notify every active listener and surface every callback failure. */
2
+ export declare function notifyListeners<T>(entries: Iterable<T>, notify: (entry: T) => void, isActive?: (entry: T) => boolean, shouldContinue?: () => boolean): void;
@@ -180,4 +180,4 @@ export declare function setInflightPromise(key: string, promise: Promise<Decoded
180
180
  */
181
181
  export declare function setInflightPromiseWithAliases(keys: string[], promise: Promise<DecodedPrefetch | null>): void;
182
182
  export declare function clearPrefetchInflight(key: string): void;
183
- export declare function clearPrefetchCache(): void;
183
+ export declare function clearPrefetchCache(rotateRangoState?: boolean): void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Router-wide default prefetch strategy (client seat).
3
+ *
4
+ * The server resolves `createRouter({ defaultPrefetch })` once at router init
5
+ * (router/prefetch-default.ts) and ships it in initial payload metadata; the
6
+ * browser entry applies it here before hydration — same lifecycle as
7
+ * `initPrefetchCache` / `setPrefetchConcurrency`. Every `<Link>` without an
8
+ * explicit `prefetch` prop and every eligible intercepted plain anchor that has
9
+ * not opted out with `data-prefetch="false"` or `data-prefetch="none"` uses it.
10
+ * Containers use the same `false`/`none` vocabulary via
11
+ * `data-prefetch-scope`.
12
+ *
13
+ * The module initial value must equal the server resolver's environment default:
14
+ * `"none"` in development and `"viewport"` in production. During SSR this
15
+ * module is never initialized from metadata, so keeping both seats aligned also
16
+ * gives metadata-less payloads the documented behavior.
17
+ */
18
+ import type { PrefetchStrategy } from "../../router/prefetch-default.js";
19
+ /** Reset module state between tests that replace browser media globals. */
20
+ export declare function resetAdaptiveStrategyForTesting(): void;
21
+ /**
22
+ * Apply the server-resolved default strategy. Called once at browser app init
23
+ * from payload metadata; also used by tests to reset state.
24
+ */
25
+ export declare function setDefaultPrefetchStrategy(strategy: PrefetchStrategy): void;
26
+ /** Current default strategy for Links and delegated plain anchors. */
27
+ export declare function getDefaultPrefetchStrategy(): PrefetchStrategy;
28
+ /** Resolve adaptive to the strategy for the current input capability. */
29
+ export declare function resolveAdaptiveStrategy(strategy: PrefetchStrategy): PrefetchStrategy;
30
+ /** Subscribe to changes in the input capability used by adaptive prefetch. */
31
+ export declare function subscribeToAdaptiveStrategyChange(listener: () => void): () => void;
@@ -0,0 +1,6 @@
1
+ type PrefetchCacheInvalidationListener = () => void;
2
+ /** Subscribe to completed local prefetch-cache invalidations. */
3
+ export declare function subscribeToPrefetchCacheInvalidation(listener: PrefetchCacheInvalidationListener): () => void;
4
+ /** Notify the registrations that existed when the cache was invalidated. */
5
+ export declare function notifyPrefetchCacheInvalidated(): void;
6
+ export {};
@@ -1,10 +1,12 @@
1
1
  import type { RscPayload } from "../types.js";
2
+ import type { EventController } from "../event-controller.js";
2
3
  type PrefetchDecoder = (response: Promise<Response>) => Promise<RscPayload>;
3
4
  export declare function setPrefetchDecoder(fn: PrefetchDecoder): void;
4
5
  export declare function setPrefetchConcurrency(value: number): void;
5
6
  export declare function prefetchDirect(url: string, segmentIds: string[], version?: string, routerId?: string, prefetchKey?: ":source"): void;
6
7
  export declare function prefetchQueued(url: string, segmentIds: string[], version?: string, routerId?: string, prefetchKey?: ":source"): void;
7
- export declare function observeForPrefetch(element: Element, callback: () => void): () => void;
8
+ /** Run speculative work only when no navigation or stream is active. */
9
+ export declare function schedulePrefetchWhenRouterIdle(eventController: Pick<EventController, "getState" | "subscribe">, callback: () => void): (() => void) | undefined;
8
10
  export declare function cancelAllPrefetches(keepUrl?: string | null): void;
9
11
  export declare function abortAllPrefetches(): void;
10
12
  export {};
@@ -12,16 +12,13 @@
12
12
  * scrolls in and out repeatedly.
13
13
  */
14
14
  type PrefetchCallback = () => void;
15
+ /** Reset module state between tests that replace browser observer globals. */
16
+ export declare function resetPrefetchObserverForTesting(): void;
15
17
  /**
16
18
  * Observe an element for viewport intersection.
17
19
  * When the element becomes visible (within 200px margin), the callback fires
18
20
  * and the element is automatically unobserved.
19
21
  * No-op in environments without IntersectionObserver (SSR, some test runners).
20
22
  */
21
- export declare function observeForPrefetch(element: Element, onVisible: PrefetchCallback): void;
22
- /**
23
- * Stop observing an element. Used for cleanup when a Link unmounts
24
- * before entering the viewport.
25
- */
26
- export declare function unobserveForPrefetch(element: Element): void;
23
+ export declare function observeForPrefetch(element: Element, onVisible: PrefetchCallback): () => void;
27
24
  export {};
@@ -1,3 +1,2 @@
1
1
  export { prefetchDirect, prefetchQueued, setPrefetchDecoder } from "./fetch.js";
2
2
  export { abortAllPrefetches, cancelAllPrefetches, setPrefetchConcurrency, } from "./queue.js";
3
- export { observeForPrefetch, unobserveForPrefetch } from "./observer.js";
@@ -29,6 +29,8 @@
29
29
  * (invalidateRangoState) update the mirror synchronously and never fire it.
30
30
  */
31
31
  export declare function setRangoStateObserver(observer: ((value: string) => void) | null): void;
32
+ /** Return the resolved cookie name used to namespace cross-tab messages. */
33
+ export declare function getRangoStateCookieName(): string;
32
34
  /**
33
35
  * Initialize the Rango state cookie at app startup. `version` is the build
34
36
  * version; `stateCookieName` is the server-resolved cookie name from payload
@@ -50,3 +52,5 @@ export declare function getRangoState(): string;
50
52
  * the external-rotation observer is NOT triggered by our own write.
51
53
  */
52
54
  export declare function invalidateRangoState(): void;
55
+ /** Adopt the state carried by a same-origin cache-invalidation broadcast. */
56
+ export declare function adoptRangoState(value: string): boolean;
@@ -8,23 +8,9 @@ import { type LocationStateEntry } from "./location-state.js";
8
8
  */
9
9
  export type StateOrGetter<T = unknown> = T | (() => T);
10
10
  export type LinkState = LocationStateEntry[] | StateOrGetter<Record<string, unknown>>;
11
- /**
12
- * Prefetch strategy for the Link component
13
- * - "hover": Prefetch on mouse enter (direct, no queue)
14
- * - "viewport": Prefetch when link enters viewport (queued, waits for idle)
15
- * - "render": Prefetch on component mount regardless of visibility (queued, waits for idle)
16
- * - "adaptive": Hover on pointer devices, viewport on touch devices
17
- * - "none": No prefetching (default)
18
- */
19
- export type PrefetchStrategy = "hover" | "viewport" | "render" | "adaptive" | "none";
20
- /**
21
- * Resolve a prefetch strategy, expanding "adaptive" to the concrete strategy
22
- * for the CURRENT input capability: "viewport" on touch (no-hover) devices,
23
- * "hover" on pointer devices. Non-adaptive strategies pass through unchanged.
24
- * Reads touch capability live (not a module-load snapshot) so the result
25
- * tracks input-capability changes.
26
- */
27
- export declare function resolveAdaptiveStrategy(prefetch: PrefetchStrategy): PrefetchStrategy;
11
+ import type { PrefetchStrategy } from "../../router/prefetch-default.js";
12
+ export type { PrefetchStrategy } from "../../router/prefetch-default.js";
13
+ export { resolveAdaptiveStrategy } from "../prefetch/default-strategy.js";
28
14
  /**
29
15
  * Link component props
30
16
  */
@@ -56,8 +42,14 @@ export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEle
56
42
  */
57
43
  revalidate?: boolean;
58
44
  /**
59
- * Prefetch strategy for the link destination
60
- * @default "none"
45
+ * Prefetch strategy for the link destination. When omitted, falls back to
46
+ * the router-wide default (`createRouter({ defaultPrefetch })`: `"none"` in
47
+ * development, `"viewport"` in production). An explicit value always wins
48
+ * over the router default, including `"none"` to opt a single Link out. An
49
+ * ancestor with `data-prefetch-scope="false"` or `"none"` remains a hard
50
+ * subtree opt-out.
51
+ *
52
+ * @default the router's environment-aware `defaultPrefetch`
61
53
  */
62
54
  prefetch?: PrefetchStrategy;
63
55
  /**
@@ -1,6 +1,7 @@
1
1
  import { type Context } from "react";
2
2
  import type { NavigationStore, NavigateOptions } from "../types.js";
3
3
  import type { EventController } from "../event-controller.js";
4
+ import type { PrefetchStrategy } from "../../router/prefetch-default.js";
4
5
  /**
5
6
  * Navigation context value provided by NavigationProvider
6
7
  *
@@ -44,6 +45,8 @@ export interface NavigationStoreContextValue {
44
45
  * Used by Link and useRouter() to auto-prefix app-local paths.
45
46
  */
46
47
  basename: string | undefined;
48
+ /** Router default from this instance's initial payload. */
49
+ defaultPrefetch?: PrefetchStrategy;
47
50
  }
48
51
  /**
49
52
  * React context for navigation store
@@ -26,8 +26,11 @@ export interface InitBrowserAppOptions {
26
26
  };
27
27
  /**
28
28
  * Enable global link interception for SPA navigation.
29
- * When enabled, clicks on same-origin anchor elements are intercepted
30
- * and handled via client-side navigation instead of full page loads.
29
+ * When enabled, clicks on eligible same-origin HTML anchor elements are intercepted
30
+ * and handled via client-side navigation instead of full page loads. Plain
31
+ * anchors inside the router basename also follow its default prefetch strategy
32
+ * after hydration. `data-prefetch="false"`/`"none"` opts out; `"true"` allows
33
+ * an application route with a common static-resource suffix.
31
34
  *
32
35
  * Links rendered with the Link component handle their own navigation
33
36
  * regardless of this setting.
@@ -79,6 +82,8 @@ export interface BrowserAppContext {
79
82
  warmupEnabled?: boolean;
80
83
  /** Whether the hydrated tree should be wrapped in React.StrictMode */
81
84
  strictMode?: boolean;
85
+ /** Whether plain-anchor click interception and delegated prefetch are enabled */
86
+ linkInterceptionEnabled?: boolean;
82
87
  /** App version for prefetch version mismatch detection */
83
88
  version?: string;
84
89
  /**
@@ -77,6 +77,11 @@ export interface RscMetadata {
77
77
  * Sent on initial render so the browser can configure its prefetch queue.
78
78
  */
79
79
  prefetchConcurrency?: number;
80
+ /**
81
+ * Router-wide default prefetch strategy for Links without a `prefetch` prop.
82
+ * Sent on initial render; applied once at init (default-strategy.ts).
83
+ */
84
+ defaultPrefetch?: import("../router/prefetch-default.js").PrefetchStrategy;
80
85
  /**
81
86
  * Server-resolved rango state cookie name (`{prefix}_{routerId}`). The client
82
87
  * reads it verbatim and binds the rango state cookie to it; composition
@@ -494,6 +499,7 @@ export interface NavigationBridge {
494
499
  refresh(): Promise<void>;
495
500
  handlePopstate(): Promise<void>;
496
501
  registerLinkInterception(): () => void;
502
+ registerDelegatedPrefetch(): () => void;
497
503
  /** Current RSC version (live, reflects the latest updateVersion). */
498
504
  getVersion(): string | undefined;
499
505
  /** Update the RSC version (e.g. after HMR). Clears prefetch cache. */
@@ -5,13 +5,21 @@
5
5
  * for cache key generation. Used by cache-runtime, cache-scope,
6
6
  * document-cache, and loader-cache.
7
7
  */
8
+ import type { SearchParamsFilter } from "./search-params-filter.js";
8
9
  /**
9
10
  * Build a sorted, deterministic query string from URLSearchParams,
10
11
  * excluding the router's reserved params (see isReservedSearchParam).
11
12
  *
12
- * Returns empty string when no user-facing params exist.
13
+ * `filter` is the compiled `cache.searchParams` config (search-params-filter.ts),
14
+ * applied AFTER the reserved-param exclusion and BEFORE the sort, so reserved
15
+ * params can never be re-included (`include: ["__no_cache"]` is a no-op) and
16
+ * surviving params stay order-insensitive. `undefined` means no filtering --
17
+ * that path must stay byte-identical to the pre-filter format (cacheKeyBase
18
+ * output is byte-stable by contract).
19
+ *
20
+ * Returns empty string when no user-facing params survive.
13
21
  */
14
- export declare function sortedSearchString(searchParams: URLSearchParams): string;
22
+ export declare function sortedSearchString(searchParams: URLSearchParams, filter?: SearchParamsFilter): string;
15
23
  /**
16
24
  * Build a sorted, deterministic string from route params.
17
25
  *
@@ -32,4 +40,4 @@ export declare function sortedRouteParams(params: Record<string, string> | undef
32
40
  * invalidates every persisted cache entry on upgrade. Callers append their own
33
41
  * tier-specific suffixes (`:rsc`/`:html`, segment hash) after this base.
34
42
  */
35
- export declare function cacheKeyBase(host: string, pathname: string, searchParams?: URLSearchParams, params?: Record<string, string>): string;
43
+ export declare function cacheKeyBase(host: string, pathname: string, searchParams?: URLSearchParams, params?: Record<string, string>, filter?: SearchParamsFilter): string;
@@ -12,6 +12,19 @@ import type { ResolvedSegment } from "../types.js";
12
12
  import type { SegmentCacheStore } from "./types.js";
13
13
  import type { RequestContext } from "../server/request-context.js";
14
14
  export declare function resolveCacheTags(config: PartialCacheOptions | false, ctx: RequestContext | undefined): string[] | undefined;
15
+ /**
16
+ * Discriminated outcome of a route cache lookup — see
17
+ * {@link CacheScope.lookupRouteDetailed} for what each status licenses.
18
+ */
19
+ export type CacheRouteLookupOutcome = {
20
+ status: "hit";
21
+ result: {
22
+ segments: ResolvedSegment[];
23
+ shouldRevalidate: boolean;
24
+ };
25
+ } | {
26
+ status: "miss" | "bypass" | "error";
27
+ };
15
28
  /**
16
29
  * CacheScope represents a cache boundary in the route tree.
17
30
  *
@@ -68,11 +81,44 @@ export declare class CacheScope {
68
81
  * @internal
69
82
  */
70
83
  private resolveKey;
84
+ /**
85
+ * @internal Whether a cache read/write is allowed for the current request:
86
+ * the scope is enabled AND its `condition` (if any) returns true. "read"
87
+ * is consulted by the PPR navigation-replay gate BEFORE any shell-store
88
+ * read so a cache(false)/condition-false route reports `cache-disabled`
89
+ * without spending getShell I/O; "write" gates the capture's snapshot-only
90
+ * doc record (cache-store middleware) under the same semantics as the
91
+ * scope's own store write. Consumer opt-outs are absolute — the seeded
92
+ * fallback must never serve where the consumer refused cached serves.
93
+ */
94
+ allowsCache(op: "read" | "write"): boolean;
95
+ /**
96
+ * @internal True for scopes minted from the `_shellImplicitCache` marker
97
+ * (createShellImplicitDocScope) — the only construction path that passes
98
+ * the `doc` defaultKeyPrefix; route-derived scopes (createCacheScope) never
99
+ * do. withCacheLookup/withCacheStore use this to tell the implicit doc
100
+ * scope from a route-derived scope: the two compose on the replay serve
101
+ * path and only the route-derived kind gets the seeded fallback /
102
+ * doc-record treatment.
103
+ */
104
+ get isShellImplicitDocScope(): boolean;
71
105
  /**
72
106
  * Evaluate the cache `condition` predicate. Returns false (skip the cache
73
107
  * operation) when the predicate returns false or throws; returns true when
74
108
  * there is no condition or no request context to evaluate it against.
75
109
  */
110
+ /**
111
+ * One WRITE decision per (scope, request), memoized on the request context.
112
+ * A capture render has TWO writers consulting the same predicate — the
113
+ * explicit tier's cacheRoute and the snapshot-only doc record gate
114
+ * (recordShellCaptureDocRecord) — and a true→false flap between the two
115
+ * evaluations recorded a REPLAYABLE canonical snapshot for a render whose
116
+ * real write was refused. The first evaluation pins the answer for the
117
+ * whole render (the capture's derived context during captures). READ
118
+ * decisions stay per-lookup by design: pre-deciding a flappable predicate
119
+ * at the replay gate was the round-2 regression.
120
+ */
121
+ private readonly writeConditionMemo;
76
122
  private conditionAllows;
77
123
  /**
78
124
  * Lookup cached segments for a route (single cache entry per request).
@@ -86,6 +132,25 @@ export declare class CacheScope {
86
132
  segments: ResolvedSegment[];
87
133
  shouldRevalidate: boolean;
88
134
  } | null>;
135
+ /**
136
+ * @internal lookupRoute with a discriminated outcome. The PPR replay
137
+ * composition (withCacheLookup) may substitute the seeded doc record ONLY on
138
+ * a true `miss` — the other non-hit outcomes must not be papered over:
139
+ *
140
+ * - `bypass`: the scope refused the read — cache(false), a false
141
+ * `condition()`, or no resolvable store. Consumer opt-outs are absolute;
142
+ * a fallback here would serve cached segments to a request the consumer
143
+ * said must render fresh.
144
+ * - `error`: a throwing consumer key()/store keyGenerator/store.get. The
145
+ * contract on that failure is "render uncached" (see the catch below) —
146
+ * falling back would serve the canonical doc record across a broken key
147
+ * partition, exactly the collision resolveCacheKey's no-default-fallback
148
+ * rule exists to prevent.
149
+ *
150
+ * A corrupt entry that was evicted reads as `miss`: the store was consulted
151
+ * and holds nothing servable, which is the post-eviction truth.
152
+ */
153
+ lookupRouteDetailed(pathname: string, params: Record<string, string>, isIntercept?: boolean): Promise<CacheRouteLookupOutcome>;
89
154
  /**
90
155
  * Record this scope's segment-DSL cache({ tags }) into the request tag union
91
156
  * synchronously, under the same gate cacheRoute() uses for a write.
@@ -119,6 +184,17 @@ export declare class CacheScope {
119
184
  export declare function createCacheScope(config: {
120
185
  options: PartialCacheOptions | false;
121
186
  } | undefined, parent?: CacheScope | null): CacheScope | null;
187
+ type ShellImplicitCacheMarker = NonNullable<RequestContext["_shellImplicitCache"]>;
188
+ /**
189
+ * Mint the implicit doc-level scope for a `_shellImplicitCache` marker: key
190
+ * resolution under the marker's `doc` namespace against the marker's store,
191
+ * with the marker's onHit wired as the hit observer. Shared by
192
+ * {@link resolveShellImplicitCacheScope} (routes that derived no scope) and
193
+ * the explicit-scope composition sites (capture doc record in the cache-store
194
+ * middleware, seeded replay fallback in withCacheLookup) so both ends of the
195
+ * shell contract resolve the SAME canonical document key.
196
+ */
197
+ export declare function createShellImplicitDocScope(marker: ShellImplicitCacheMarker): CacheScope;
122
198
  /**
123
199
  * Shell fast path: when the route tree derived NO cache scope and the current
124
200
  * request context carries the `_shellImplicitCache` marker (a shell capture,
@@ -129,8 +205,11 @@ export declare function createCacheScope(config: {
129
205
  * (resolveFreshLoadersAndYield).
130
206
  *
131
207
  * An existing scope — including an explicit cache(false) opt-out — always
132
- * wins: the consumer's cache() semantics (their ttl/swr/store/condition) are
133
- * never overridden, and cache(false) keeps the tail on the full handler
134
- * re-run path.
208
+ * wins HERE: the consumer's cache() semantics (their ttl/swr/store/condition)
209
+ * are never overridden, and cache(false) keeps the tail on the full handler
210
+ * re-run path. On the navigation-replay serve path the marker still composes
211
+ * with an explicit scope downstream (withCacheLookup's seeded fallback after
212
+ * an explicit-tier miss) — see `onExplicitHit` on `_shellImplicitCache`.
135
213
  */
136
214
  export declare function resolveShellImplicitCacheScope(scope: CacheScope | null): CacheScope | null;
215
+ export {};
@@ -22,7 +22,7 @@ declare global {
22
22
  * - Non-blocking writes via waitUntil
23
23
  * - KV L2 for cross-colo cache persistence
24
24
  */
25
- import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry } from "../types.js";
25
+ import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry, CacheReadError } from "../types.js";
26
26
  import { type RequestContext } from "../../server/request-context.js";
27
27
  import { CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER, CACHE_TAGS_HEADER, CACHE_TAGGED_AT_HEADER, TAG_MARKER_PREFIX, CACHE_REVALIDATING_AT_HEADER, MAX_REVALIDATION_INTERVAL, EDGE_LOOKUP_TIMEOUT_MS, EDGE_READ_TIMEOUT_MS, KV_READ_TIMEOUT_MS } from "./cf-cache-constants.js";
28
28
  export { CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER, CACHE_TAGS_HEADER, CACHE_TAGGED_AT_HEADER, TAG_MARKER_PREFIX, CACHE_REVALIDATING_AT_HEADER, MAX_REVALIDATION_INTERVAL, EDGE_LOOKUP_TIMEOUT_MS, EDGE_READ_TIMEOUT_MS, KV_READ_TIMEOUT_MS, };
@@ -209,7 +209,7 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
209
209
  * On L1 miss, falls back to KV (L2) if configured.
210
210
  * KV hits are promoted to L1 in the background.
211
211
  */
212
- get(key: string): Promise<CacheGetResult | null>;
212
+ get(key: string): Promise<CacheGetResult | null | CacheReadError>;
213
213
  /**
214
214
  * Store entry data with TTL and optional SWR window.
215
215
  * Uses waitUntil for non-blocking write when available.
@@ -1,7 +1,9 @@
1
- export type { SegmentCacheStore, CachedEntryData, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry, SerializedSegmentData, SegmentHandleData, } from "./types.js";
1
+ export type { SegmentCacheStore, CachedEntryData, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry, SerializedSegmentData, SegmentHandleData, CacheReadError, } from "./types.js";
2
+ export { CACHE_READ_ERROR } from "./types.js";
2
3
  export { MemorySegmentCacheStore } from "./memory-segment-store.js";
3
4
  export { CFCacheStore, type CFCacheStoreOptions, type CFCacheDebug, type CFCacheReadDebugEvent, type KVNamespace, CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER, CACHE_REVALIDATING_AT_HEADER, EDGE_LOOKUP_TIMEOUT_MS, EDGE_READ_TIMEOUT_MS, KV_READ_TIMEOUT_MS, createCloudflareZonePurge, type CloudflareZonePurgeOptions, } from "./cf/index.js";
4
5
  export { VercelCacheStore, type VercelCacheStoreOptions, type VercelRuntimeCache, type VercelCacheDebug, type VercelCacheReadDebugEvent, type VercelCacheReadOutcome, VERCEL_MAX_ITEM_BYTES, VERCEL_MAX_TAGS_PER_ITEM, VERCEL_MAX_TAG_BYTES, } from "./vercel/index.js";
6
+ export { TRACKING_SEARCH_PARAMS, type CacheSearchParams, } from "./search-params-filter.js";
5
7
  export { CacheScope, createCacheScope } from "./cache-scope.js";
6
8
  export { createDocumentCacheMiddleware, type DocumentCacheOptions, } from "./document-cache.js";
7
9
  export type { CacheErrorCategory } from "./cache-error.js";
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Global search-param cache-key filtering (`cache.searchParams` on the
3
+ * handler/createRouter cache config).
4
+ *
5
+ * Controls WHICH query params participate in default cache-key generation
6
+ * across every tier that keys by URL (segment, document, response, PPR shell,
7
+ * "use cache" ctx normalization, prerendered-shell manifest matching). The
8
+ * filter affects cache keys ONLY -- `ctx.searchParams` and the request URL are
9
+ * untouched, handlers and loaders still see the full query string.
10
+ *
11
+ * Design doc: docs/design/caching.md ("Search param filtering").
12
+ *
13
+ * The footgun this must not soften: excluding a param is a promise that
14
+ * rendered output does not depend on it. If it does, the first variant is
15
+ * cached and served to everyone (the classic CDN cache-key mistake). The
16
+ * default therefore stays "all" -- correct by default, opt into collapsing.
17
+ */
18
+ /**
19
+ * `cache.searchParams` config value.
20
+ *
21
+ * - `"all"` (default) -- every non-reserved param keys the cache (today's
22
+ * behavior; reserved = the router's own `_rsc*` / `__` allowlist params,
23
+ * see cache-key-utils.ts).
24
+ * - `"none"` -- query params never key the cache.
25
+ * - `{ include }` -- allowlist: only the named params key the cache.
26
+ * - `{ exclude }` -- denylist: every param except the named ones keys the cache.
27
+ *
28
+ * Names match exactly, plus a `*` SUFFIX wildcard (`"utm_*"` matches every
29
+ * param starting with `utm_`). No RegExp: keeps the config serializable and
30
+ * deterministic. `include` + `exclude` together is unrepresentable -- the
31
+ * union forces exactly one mode.
32
+ */
33
+ export type CacheSearchParams = "all" | "none" | {
34
+ include: readonly string[];
35
+ exclude?: never;
36
+ } | {
37
+ exclude: readonly string[];
38
+ include?: never;
39
+ };
40
+ /**
41
+ * Compiled form of a `CacheSearchParams` config: returns true when the param
42
+ * name should participate in the cache key. `undefined` means "no filter"
43
+ * ("all") so the unfiltered path stays byte-identical to the pre-feature key
44
+ * format (cacheKeyBase output is byte-stable by contract).
45
+ */
46
+ export type SearchParamsFilter = (name: string) => boolean;
47
+ /**
48
+ * Well-known tracking/click-id params that fragment caches without changing
49
+ * rendered output for (almost) every app. Exported so the common case is one
50
+ * line: `searchParams: { exclude: TRACKING_SEARCH_PARAMS }`.
51
+ *
52
+ * Sources: Google (gclid/gclsrc/dclid/gbraid/wbraid + utm_*), Meta (fbclid),
53
+ * Microsoft (msclkid), TikTok (ttclid), Twitter/X (twclid), LinkedIn
54
+ * (li_fat_id), Mailchimp (mc_cid/mc_eid), Instagram (igshid), Yandex (yclid),
55
+ * HubSpot (_hsenc/_hsmi).
56
+ */
57
+ export declare const TRACKING_SEARCH_PARAMS: readonly string[];
58
+ /**
59
+ * Compile a `cache.searchParams` config into a predicate, once per resolved
60
+ * cache config (handler.ts). Returns `undefined` for the default ("all") so
61
+ * every call site can cheaply skip filtering and keep the shipped key format
62
+ * byte-stable.
63
+ */
64
+ export declare function compileSearchParamsFilter(config: CacheSearchParams | undefined): SearchParamsFilter | undefined;
@@ -22,7 +22,7 @@
22
22
  * the capture render performed; replaying them on a HIT reproduces the shell
23
23
  * content byte-identically; everything not recorded stays live.
24
24
  */
25
- import type { SegmentCacheStore, CacheGetResult, CacheItemResult, CacheItemOptions, CachedEntryData, ShellCacheEntry, ShellSnapshotRecord } from "./types.js";
25
+ import type { SegmentCacheStore, CacheGetResult, CacheItemResult, CacheItemOptions, CachedEntryData, ShellCacheEntry, ShellSnapshotRecord, CacheReadError } from "./types.js";
26
26
  /**
27
27
  * A store wrapper the CAPTURE render reads through. Every call passes through to
28
28
  * the underlying store unchanged; for the item/segment/response families it also
@@ -73,7 +73,7 @@ export declare class RecordingShellStore<TEnv = unknown> implements SegmentCache
73
73
  settleWrites(timeoutMs: number): Promise<void>;
74
74
  /** The recorded snapshot (last-write-wins per family+key), or undefined if empty. */
75
75
  drainSnapshot(): ShellSnapshotRecord[] | undefined;
76
- get(key: string): Promise<CacheGetResult | null>;
76
+ get(key: string): Promise<CacheGetResult | null | CacheReadError>;
77
77
  set(key: string, data: CachedEntryData, ttl: number, swr?: number): Promise<void>;
78
78
  delete(key: string): Promise<boolean>;
79
79
  clear(): Promise<void>;
@@ -109,7 +109,7 @@ export declare class SnapshotOnlySegmentStore<TEnv = unknown> implements Segment
109
109
  constructor(recording: RecordingShellStore<TEnv>);
110
110
  get defaults(): SegmentCacheStore<TEnv>["defaults"];
111
111
  get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"];
112
- get(key: string): Promise<CacheGetResult | null>;
112
+ get(key: string): Promise<CacheGetResult | null | CacheReadError>;
113
113
  set(key: string, data: CachedEntryData): Promise<void>;
114
114
  delete(key: string): Promise<boolean>;
115
115
  }
@@ -164,7 +164,7 @@ export declare class SeededShellStore<TEnv = unknown> implements SegmentCacheSto
164
164
  get defaults(): SegmentCacheStore<TEnv>["defaults"];
165
165
  get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"];
166
166
  get supportsPassiveShellReads(): true | undefined;
167
- get(key: string): Promise<CacheGetResult | null>;
167
+ get(key: string): Promise<CacheGetResult | null | CacheReadError>;
168
168
  set(key: string, data: CachedEntryData, ttl: number, swr?: number): Promise<void>;
169
169
  delete(key: string): Promise<boolean>;
170
170
  clear(): Promise<void>;