@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
@@ -10,6 +10,19 @@
10
10
  */
11
11
  import type { ResolvedSegment } from "../types.js";
12
12
  import type { RequestContext } from "../server/request-context.js";
13
+ /**
14
+ * Sentinel a `SegmentCacheStore.get` MAY return instead of `null` when the
15
+ * read FAILED (backend error) rather than genuinely missing. For the render
16
+ * outcome the two are identical — render fresh, re-cache — so hit/miss-only
17
+ * consumers can treat it as a miss. The PPR replay composition needs the
18
+ * distinction: an errored explicit-tier read must render uncached
19
+ * (`lookupRouteDetailed` classifies it `error`), never be substituted by the
20
+ * seeded doc record — the built-in stores swallow backend errors internally,
21
+ * so without this signal their failures read as replayable misses. Third-party
22
+ * stores returning plain `null` on error keep the miss classification.
23
+ */
24
+ export declare const CACHE_READ_ERROR: unique symbol;
25
+ export type CacheReadError = typeof CACHE_READ_ERROR;
13
26
  /**
14
27
  * Result from cache get() including data and revalidation status
15
28
  */
@@ -81,9 +94,10 @@ export interface SegmentCacheStore<TEnv = unknown> {
81
94
  readonly keyGenerator?: (ctx: RequestContext<TEnv>, defaultKey: string) => string | Promise<string>;
82
95
  /**
83
96
  * Get cached entry data by key
84
- * @returns Cache result with data and staleness, or null if not found/expired
97
+ * @returns Cache result with data and staleness, null if not found/expired,
98
+ * or CACHE_READ_ERROR when the read failed (optional — see the sentinel).
85
99
  */
86
- get(key: string): Promise<CacheGetResult | null>;
100
+ get(key: string): Promise<CacheGetResult | null | CacheReadError>;
87
101
  /**
88
102
  * Store entry data with TTL
89
103
  * @param key - Cache key
@@ -265,6 +279,26 @@ export interface ShellCacheEntry {
265
279
  * heals it. See docs/design/ppr-shell-resume.md ("the capture data snapshot").
266
280
  */
267
281
  snapshot?: ShellSnapshotRecord[];
282
+ /**
283
+ * The key of the CANONICAL document segment record inside `snapshot` — the
284
+ * one navigation replay can actually consume (resolved under the implicit
285
+ * doc namespace at capture; see CacheScope.cacheRoute). Replay eligibility
286
+ * requires this exact record: the snapshot also carries incidentally
287
+ * recorded explicit-tier records (RecordingShellStore passthroughs) whose
288
+ * keys a partial lookup can never resolve, and counting those declared
289
+ * entries "replayable" that always missed (`snapshot-miss` flip-flop).
290
+ * Absent on entries captured before the field existed OR when the capture
291
+ * recorded no doc record (cache(false)/condition-false routes, prerender
292
+ * short-circuit) — both read as `no-segment-snapshot`; recapture heals the
293
+ * former.
294
+ */
295
+ docKey?: string;
296
+ /**
297
+ * The entry was captured from a partial request only to produce an eligible
298
+ * segment snapshot. Document serving must treat its HTML prelude as a miss;
299
+ * the partial request's headers and middleware state are not document state.
300
+ */
301
+ navigationOnly?: true;
268
302
  /**
269
303
  * True when the capture's HANDLER layer declared per-request liveness: a
270
304
  * handle pushed OUTSIDE a DSL loader scope carried a nested thenable (the
@@ -30,7 +30,7 @@
30
30
  * `getCache(...)` result, which satisfies the shape structurally. That keeps the
31
31
  * router free of a hard Vercel dependency.
32
32
  */
33
- import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry } from "../types.js";
33
+ import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry, CacheReadError } from "../types.js";
34
34
  import type { RequestContext } from "../../server/request-context.js";
35
35
  /**
36
36
  * Minimal structural shape of the Vercel Runtime Cache returned by `getCache()`
@@ -194,7 +194,7 @@ export declare class VercelCacheStore<TEnv = unknown> implements SegmentCacheSto
194
194
  private readonly name?;
195
195
  private readonly debug?;
196
196
  constructor(options: VercelCacheStoreOptions<TEnv>);
197
- get(key: string): Promise<CacheGetResult | null>;
197
+ get(key: string): Promise<CacheGetResult | null | CacheReadError>;
198
198
  set(key: string, data: CachedEntryData, ttl: number, swr?: number): Promise<void>;
199
199
  delete(key: string): Promise<boolean>;
200
200
  getResponse(key: string): Promise<{
@@ -13,6 +13,7 @@ export { RouteNotFoundError, DataNotFoundError, notFound, MiddlewareError, Handl
13
13
  export type { DocumentProps, DefaultEnv, RouteDefinition, RouteConfig, RouteDefinitionOptions, TrailingSlashMode, Handler, // Supports params object, path pattern, or route name
14
14
  HandlerContext, ExtractParams, GenericParams, Middleware, RevalidateParams, Revalidate, ActionRef, RouteKeys, LoaderDefinition, LoaderFn, LoaderContext, FetchableLoaderOptions, LoadOptions, ErrorInfo, ErrorBoundaryFallbackProps, ErrorBoundaryHandler, ClientErrorBoundaryFallbackProps, NotFoundInfo, NotFoundBoundaryFallbackProps, NotFoundBoundaryHandler, ErrorPhase, OnErrorContext, OnErrorCallback, } from "./types.js";
15
15
  export type { SearchSchema, SearchSchemaValue, ResolveSearchSchema, RouteSearchParams, RouteParams, } from "./search-params.js";
16
+ export { TRACKING_SEARCH_PARAMS, type CacheSearchParams, } from "./cache/search-params-filter.js";
16
17
  export { createLoader } from "./loader.js";
17
18
  export type { RouteHelpers, RouteHandlers } from "./route-definition.js";
18
19
  export type { TransitionConfig, TransitionWhenFn, TransitionWhenContext, ViewTransitionClass, } from "./types.js";
@@ -24,6 +24,7 @@ export { Static, type StaticHandlerDefinition, type StaticHandlerOptions, } from
24
24
  export { urls, type PathHelpers, type PathOptions, type PartialPrerenderProps, type UrlPatterns, type IncludeOptions, type IncludeItem, type RouteResponse, type ProblemDetails, type ResponseHandler, type ResponseHandlerContext, type JsonResponseHandler, type TextResponseHandler, type JsonValue, type ResponsePathFn, type JsonResponsePathFn, type TextResponsePathFn, } from "./urls.js";
25
25
  export { createRouter, type Rango, type RootLayoutProps, type RouterRequestInput, } from "./router.js";
26
26
  export type { HandlerCacheConfig } from "./rsc/types.js";
27
+ export { TRACKING_SEARCH_PARAMS, type CacheSearchParams, } from "./cache/search-params-filter.js";
27
28
  export { Meta } from "./handles/meta.js";
28
29
  export { Script, type ScriptConfig, type ScriptAttributes, } from "./handles/script.js";
29
30
  export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
@@ -92,7 +92,20 @@
92
92
  * - Action context (if POST)
93
93
  */
94
94
  import type { ResolvedSegment } from "../../types.js";
95
+ import type { EntryData } from "../../server/context.js";
95
96
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
97
+ /**
98
+ * Whether the prerender store holds a baked entry for this route + params.
99
+ * Consulted by the PPR replay gate (matchPartialWithPprReplay), which must
100
+ * only report `prerender-store` when the short-circuit below will actually
101
+ * serve: a Passthrough(Prerender()) route with an unbaked/passthrough param
102
+ * misses the store and renders live, and replay — including its heal
103
+ * capture — must stay available for it (withCacheStore records the doc
104
+ * record on that path; state.cacheSource is not "prerender"). The store
105
+ * memoizes per routeKey/paramHash, so this probe and tryPrerenderLookup's
106
+ * subsequent get() share one underlying load.
107
+ */
108
+ export declare function prerenderEntryExists(routeKey: string | undefined, params: Record<string, string>, pathname: string, entries: EntryData[]): Promise<boolean>;
96
109
  /**
97
110
  * Async generator middleware type
98
111
  */
@@ -18,5 +18,34 @@ export interface NavigationSnapshot {
18
18
  export interface ResolveNavigationDeps {
19
19
  findMatch: (pathname: string) => RouteMatchResult | null | Promise<RouteMatchResult | null>;
20
20
  }
21
+ /**
22
+ * The raw navigation-context header for a partial request, or null when the
23
+ * request carries none. Single source of truth shared by resolveNavigation and
24
+ * the PPR replay gate (matchPartialWithPprReplay): a context-less partial can
25
+ * never produce a partial match, so the replay path must decide
26
+ * `no-navigation-context` BEFORE spending shell-store reads — the two
27
+ * predicates drifting apart would misattribute the bypass again.
28
+ */
29
+ export declare function getNavigationContextHeader(request: Request): string | null;
30
+ /**
31
+ * Whether withCacheLookup's prerender short-circuit will serve this partial
32
+ * from the build-time prerender store: a Prerender() match (`matched.pr`)
33
+ * outside dev HMR — the short-circuit is disabled on X-RSC-HMR so HMR
34
+ * navigations pick up edited modules. Shared by withCacheLookup and the PPR
35
+ * replay gate (matchPartialWithPprReplay) for the same drift reason as
36
+ * getNavigationContextHeader: the gate's `prerender-store` bypass must decide
37
+ * exactly what the middleware will actually do. (Both callers additionally
38
+ * exclude actions on their own paths — the gate via its GET-only check.)
39
+ */
40
+ export declare function prerenderStoreShortCircuits(pr: true | undefined, request: Request): boolean;
41
+ /**
42
+ * The intercept-source header for a partial request, or null when the request
43
+ * carries none. Shared by resolveNavigation and the PPR replay gate's
44
+ * prerender probe: intercept navigations consult the intercept-specific
45
+ * prerender artifact (`paramHash + "/i"`), so the probe must check the same
46
+ * variant the middleware will — probing the bare hash for an intercept
47
+ * request would report `prerender-store` for an artifact that cannot serve.
48
+ */
49
+ export declare function getInterceptSourceHeader(request: Request): string | null;
21
50
  export declare function resolveNavigation(request: Request, url: URL, currentRouteKey: string, deps: ResolveNavigationDeps): Promise<NavigationSnapshot | null>;
22
51
  export declare function createNavigationSnapshot(overrides?: Partial<NavigationSnapshot>): NavigationSnapshot;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolve the router-wide default Link prefetch strategy once, at router init.
3
+ * The resolved value ships to the client in payload metadata; the browser
4
+ * entry applies it via `setDefaultPrefetchStrategy` (browser/prefetch/
5
+ * default-strategy.ts) and every `<Link>` without an explicit `prefetch` prop
6
+ * falls back to it.
7
+ *
8
+ * The type lives HERE (not in browser/react/Link.tsx) because both seats need
9
+ * it: this server-side resolver and the client Link/default-strategy modules.
10
+ * Link.tsx re-exports it so the public `PrefetchStrategy` export path is
11
+ * unchanged.
12
+ *
13
+ * Policy note: automatic prefetch stays quiet in development, matching the
14
+ * production-only default used by Next.js. Explicit router and per-Link
15
+ * strategies still work in every mode. Invalid untyped inputs fall back to the
16
+ * current environment's default.
17
+ */
18
+ /**
19
+ * Prefetch strategy for the Link component
20
+ * - "hover": Prefetch on mouse enter (direct, no queue)
21
+ * - "viewport": Prefetch when link enters viewport (queued, waits for idle)
22
+ * - "render": Prefetch on component mount regardless of visibility (queued, waits for idle)
23
+ * - "adaptive": Hover on pointer devices, viewport on touch devices
24
+ * - "none": No prefetching
25
+ */
26
+ export type PrefetchStrategy = "hover" | "viewport" | "render" | "adaptive" | "none";
27
+ export declare const DEFAULT_PREFETCH_STRATEGY: PrefetchStrategy;
28
+ export declare function resolveDefaultPrefetch(raw: PrefetchStrategy | undefined, nodeEnv?: string | undefined): PrefetchStrategy;
@@ -249,6 +249,13 @@ export interface RangoInternal<TEnv = any, TRoutes extends Record<string, unknow
249
249
  * prefetchConcurrency.
250
250
  */
251
251
  readonly prefetchConcurrency: number;
252
+ /**
253
+ * Router-wide default Link prefetch strategy for Links without an explicit
254
+ * `prefetch` prop. Shipped to the client in payload metadata. Derived from
255
+ * the `defaultPrefetch` option (default "none" in development and
256
+ * "viewport" in production).
257
+ */
258
+ readonly defaultPrefetch: import("./prefetch-default.js").PrefetchStrategy;
252
259
  /**
253
260
  * Resolved rango state cookie name (`{prefix}_{routerId}`), composed once at
254
261
  * router init and shipped to the client in payload metadata. The server-side
@@ -1,5 +1,6 @@
1
1
  import type { ComponentType, ReactNode } from "react";
2
2
  import type { SegmentCacheStore } from "../cache/types.js";
3
+ import type { CacheSearchParams } from "../cache/search-params-filter.js";
3
4
  import type { ErrorBoundaryHandler, NotFoundBoundaryHandler, OnErrorCallback } from "../types";
4
5
  import type { NonceProvider } from "../rsc/types.js";
5
6
  import type { ShellCaptureDebug } from "../rsc/shell-capture.js";
@@ -7,6 +8,7 @@ import type { ExecutionContext } from "../server/request-context.js";
7
8
  import type { UrlPatterns } from "../urls.js";
8
9
  import type { UrlBuilder } from "../urls/pattern-types.js";
9
10
  import type { NamedRouteEntry } from "./content-negotiation.js";
11
+ import type { PrefetchStrategy } from "./prefetch-default.js";
10
12
  import type { TelemetrySink } from "./telemetry.js";
11
13
  import type { RouterTracingConfig } from "./tracing.js";
12
14
  import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
@@ -282,13 +284,32 @@ export interface RangoOptions<TEnv = any> {
282
284
  * }),
283
285
  * });
284
286
  * ```
287
+ *
288
+ * `searchParams` controls which query params key the cache (default:
289
+ * `"all"`). Cache keys only -- handlers still see the full query string.
290
+ * Excluding a param is a promise that rendered output does not depend on
291
+ * it; if it does, the first variant is cached and served to everyone.
292
+ *
293
+ * @example Ignore tracking params for cache keys
294
+ * ```typescript
295
+ * import { TRACKING_SEARCH_PARAMS } from "@rangojs/router";
296
+ *
297
+ * const router = createRouter({
298
+ * cache: {
299
+ * store: cacheStore,
300
+ * searchParams: { exclude: TRACKING_SEARCH_PARAMS },
301
+ * },
302
+ * });
303
+ * ```
285
304
  */
286
305
  cache?: {
287
306
  store: SegmentCacheStore;
288
307
  enabled?: boolean;
308
+ searchParams?: CacheSearchParams;
289
309
  } | ((env: TEnv, ctx?: ExecutionContext) => {
290
310
  store: SegmentCacheStore;
291
311
  enabled?: boolean;
312
+ searchParams?: CacheSearchParams;
292
313
  });
293
314
  /**
294
315
  * Named cache profiles for "use cache" directive.
@@ -495,6 +516,39 @@ export interface RangoOptions<TEnv = any> {
495
516
  * @default 2
496
517
  */
497
518
  prefetchConcurrency?: number;
519
+ /**
520
+ * Default prefetch strategy for every `<Link>` that does not set its own
521
+ * `prefetch` prop and every eligible intercepted plain anchor. Plain anchors
522
+ * can opt out with `data-prefetch="false"` or `data-prefetch="none"`. Common
523
+ * static-resource suffixes are excluded unless `data-prefetch="true"` marks
524
+ * the URL as an application route. Mark side-effectful GET links such as
525
+ * `/logout` with an opt-out because the router cannot infer endpoint safety.
526
+ * A per-Link `prefetch` prop always wins in both directions (a Link can opt
527
+ * out with `prefetch="none"`, or opt in under `defaultPrefetch: "none"`).
528
+ *
529
+ * - `"viewport"` (production default): prefetch when the link enters the viewport —
530
+ * idle-gated and queued (see `prefetchConcurrency`), so prefetches never
531
+ * compete with hydration or an active navigation.
532
+ * - `"hover"`: prefetch on mouse enter only. Much lighter on the server —
533
+ * only links the pointer approaches are fetched — at the cost of the
534
+ * ~100-300ms head start viewport prefetch gives.
535
+ * - `"adaptive"`: `"hover"` on pointer devices, `"viewport"` on touch
536
+ * devices (no hover to wait for).
537
+ * - `"render"`: prefetch on mount regardless of visibility.
538
+ * - `"none"`: manual mode — nothing prefetches unless a Link opts in.
539
+ *
540
+ * Automatic prefetch defaults to `"none"` in development and `"viewport"`
541
+ * in production. With the production default, every visible Link triggers an
542
+ * RSC render (loaders included) on the server. Routes served from cache/PPR
543
+ * absorb this cheaply; per-user dynamic routes pay full price per viewport
544
+ * entry. Set `"hover"`, `"adaptive"`, or `"none"` when that cost matters.
545
+ *
546
+ * To disable the prefetch subsystem entirely (including per-Link opt-ins
547
+ * and `useRouter().prefetch()`), set `prefetchCacheTTL: false` instead.
548
+ *
549
+ * @default "none" in development; "viewport" in production
550
+ */
551
+ defaultPrefetch?: PrefetchStrategy;
498
552
  /**
499
553
  * Prefix for the rango state cookie name. The resolved name is
500
554
  * `{prefix}_{routerId}`; the prefix is sanitized to cookie-name-safe
@@ -1,3 +1,9 @@
1
+ /** Bound queued/running capture closures retained by one isolate. */
2
+ export declare const MAX_ADMITTED_CAPTURES: number;
3
+ /** The caller may drop this best-effort capture and retry on a later request. */
4
+ export declare class CaptureQueueFullError extends Error {
5
+ constructor();
6
+ }
1
7
  /**
2
8
  * Run `task` after every previously enqueued capture has settled. Returns a
3
9
  * promise for THIS task's completion (rejections propagate to the caller —
@@ -25,6 +25,7 @@
25
25
  * markers say whether it is still current.
26
26
  */
27
27
  import type { SegmentCacheStore, ShellCacheEntry } from "../cache/types.js";
28
+ import type { SearchParamsFilter } from "../cache/search-params-filter.js";
28
29
  /** One baked manifest record (the __ps asset module's default export). */
29
30
  export interface BuildShellEntry {
30
31
  entry: ShellCacheEntry;
@@ -79,6 +80,11 @@ export interface DevShellLookup {
79
80
  * version validity, payload integrity, and tag-invalidation markers. Returns
80
81
  * null on any gate failure — the caller degrades to the ordinary MISS path
81
82
  * (axis 1 + runtime capture), never a broken serve.
83
+ *
84
+ * "Search-less" is evaluated AFTER the request's `cache.searchParams` filter:
85
+ * a URL whose only params are excluded ones (`?fbclid=…` under a tracking
86
+ * exclusion) matches the baked shell — ad-click traffic is exactly the
87
+ * traffic the shell was prerendered for.
82
88
  */
83
- export declare function lookupBuildShell(url: URL, buildVersion: string, store: SegmentCacheStore, dev?: DevShellLookup): Promise<BuildShellHit | null>;
89
+ export declare function lookupBuildShell(url: URL, buildVersion: string, store: SegmentCacheStore, dev?: DevShellLookup, filter?: SearchParamsFilter): Promise<BuildShellHit | null>;
84
90
  export {};
@@ -124,10 +124,11 @@ export interface ShellCaptureDebugEvent {
124
124
  * the key (stampede guard) and scheduled nothing
125
125
  * - skip-backoff: the key is inside its refused-capture backoff window and
126
126
  * the capture was not attempted
127
+ * - skip-capacity: the isolate capture queue is full; a later request may retry
127
128
  * - backoff: the key entered (or escalated) backoff after a terminal
128
129
  * no-shell — carries the new backoff state
129
130
  */
130
- outcome: "stored" | "redirect" | "no-shell" | "refused" | "error" | "skip-in-flight" | "skip-backoff" | "backoff";
131
+ outcome: "stored" | "redirect" | "no-shell" | "refused" | "error" | "skip-in-flight" | "skip-backoff" | "skip-capacity" | "backoff";
131
132
  /** Attempt number (1 = first, 2 = in-place retry). Absent on skips. */
132
133
  attempt?: number;
133
134
  /** Wall-clock ms of the whole attempt (barrier + render + drain + put). */
@@ -148,6 +149,8 @@ export interface ShellCaptureDebugEvent {
148
149
  snapshotBytes?: number;
149
150
  /** True when the snapshot exceeded maxSnapshotBytes and was dropped. */
150
151
  snapshotSkipped?: boolean;
152
+ /** A bake-lane loader settled into a shell that uses TTL/SWR-only invalidation. */
153
+ untaggedBake?: true;
151
154
  /** Outcome reported by a store that supports shell-write acknowledgements. */
152
155
  storeWrite?: "stored" | "invalidated";
153
156
  /** Consecutive failure count in the key's backoff entry, when one exists. */
@@ -280,6 +283,8 @@ export interface ShellCaptureDescriptor {
280
283
  * {@link ShellCaptureDebugEvent} per attempt/skip.
281
284
  */
282
285
  debugSink?: (event: ShellCaptureDebugEvent) => void;
286
+ /** Store the snapshot for navigation replay, never the captured HTML prelude. */
287
+ navigationOnly?: true;
283
288
  }
284
289
  /**
285
290
  * Schedule the background shell capture for a served document. Stampede-guarded:
@@ -288,11 +293,11 @@ export interface ShellCaptureDescriptor {
288
293
  * error is routed through reportCacheError — capture is best-effort; a failure just
289
294
  * means the next request recaptures.
290
295
  *
291
- * Eligibility (nonce/allReady/partial/status/strategy) is decided by the caller
292
- * (rsc-rendering.ts maybeScheduleShellCapture); this function only owns the
293
- * stampede guard and the background dispatch.
296
+ * Eligibility (nonce/partial/status/strategy) is decided by the caller. An SSR
297
+ * module loader may be passed for cold partial requests; it runs only after the
298
+ * capture enters the guarded background queue, never on response latency.
294
299
  */
295
- export declare function scheduleShellCapture(ctx: HandlerContext<any>, request: Request, env: any, url: URL, reqCtx: RequestContext<any>, ssrModule: SSRModule, descriptor: ShellCaptureDescriptor): void;
300
+ export declare function scheduleShellCapture(ctx: HandlerContext<any>, request: Request, env: any, url: URL, reqCtx: RequestContext<any>, ssrModule: SSRModule | ((request: Request, url: URL) => Promise<SSRModule | null>), descriptor: ShellCaptureDescriptor): void;
296
301
  /**
297
302
  * The outcome of one capture attempt.
298
303
  * - `stored`: a usable shell was captured (and a putShell was attempted; a store
@@ -310,7 +315,7 @@ type CaptureAttemptOutcome = "stored" | "redirect" | "no-shell" | "refused";
310
315
  * bag, not a return value: captureAndStoreShell's outcome type stays a string
311
316
  * union its existing callers (producer B, tests) consume unchanged.
312
317
  */
313
- type CaptureAttemptStats = Pick<ShellCaptureDebugEvent, "barrierWaitMs" | "writeSettleMs" | "preludeBytes" | "snapshotBytes" | "snapshotSkipped" | "storeWrite">;
318
+ type CaptureAttemptStats = Pick<ShellCaptureDebugEvent, "barrierWaitMs" | "writeSettleMs" | "preludeBytes" | "snapshotBytes" | "snapshotSkipped" | "untaggedBake" | "storeWrite">;
314
319
  /**
315
320
  * Run the shell capture with a single in-place retry, then store the result.
316
321
  *
@@ -351,7 +356,10 @@ export interface CaptureContextDerivation {
351
356
  * createRequestContext over the build env, with a fresh MemorySegmentCacheStore
352
357
  * as `_cacheStore` so the recording/snapshot machinery arms identically.
353
358
  */
354
- export declare function deriveShellCaptureContext(reqCtx: RequestContext<any>, descriptor: Pick<ShellCaptureDescriptor, "ttl" | "swr">): CaptureContextDerivation;
359
+ export declare function deriveShellCaptureContext(reqCtx: RequestContext<any>, descriptor: Pick<ShellCaptureDescriptor, "ttl" | "swr">, identity?: {
360
+ request: Request;
361
+ url: URL;
362
+ }): CaptureContextDerivation;
355
363
  /**
356
364
  * Seal handles, derive the quiesce signal, prerender + abort via the SSR module's
357
365
  * captureShellHTML, and store the result. Returns the attempt outcome (the caller
@@ -16,6 +16,7 @@
16
16
  * intent cannot be honored — unlike an undeclared route, which is silent).
17
17
  */
18
18
  import { type EntryData } from "../server/context.js";
19
+ import type { SearchParamsFilter } from "../cache/search-params-filter.js";
19
20
  import type { ShellCacheEntry, SegmentCacheStore } from "../cache/types.js";
20
21
  /** Debug/status header the browser (and e2e assertions) can read: HIT | MISS. */
21
22
  export declare const SHELL_STATUS_HEADER = "x-rango-shell";
@@ -53,9 +54,10 @@ export interface ResolvedPprConfig {
53
54
  captureTimeout?: number;
54
55
  }
55
56
  /**
56
- * Validate the raw `ppr.captureTimeout` option: a finite number >= 1ms passes
57
- * through; anything else (including 0/negative/NaN/Infinity/non-number)
58
- * resolves to undefined, which means "use the capture default" downstream.
57
+ * Validate the raw `ppr.captureTimeout` option: a finite number >= 1ms is
58
+ * clamped to the default ceiling; anything else (including
59
+ * 0/negative/NaN/Infinity/non-number) resolves to undefined, which means "use
60
+ * the capture default" downstream.
59
61
  * Mirrors the prefetch-limit option policy: invalid values silently fall back
60
62
  * to the default rather than throwing at request time. Also the boundary
61
63
  * re-normalizer for the dev /__rsc_shell endpoint (vite/router-discovery.ts),
@@ -85,8 +87,13 @@ export declare function resolvePprConfig(entry: EntryData | undefined | null): R
85
87
  * The key includes the request HOST: in a multi-tenant host-router deployment
86
88
  * (one worker, one shared KV/runtime-cache store) a host-less key would serve
87
89
  * tenant A's captured shell to tenant B's users.
90
+ *
91
+ * `filter` is the request's compiled `cache.searchParams` config
92
+ * (ctx._searchParamsFilter): excluded params collapse onto one shell slot.
93
+ * Callers on the serve/capture path MUST pass it -- key drift between capture
94
+ * and lookup makes every shell request a permanent miss.
88
95
  */
89
- export declare function buildShellKey(url: URL): string;
96
+ export declare function buildShellKey(url: URL, filter?: SearchParamsFilter): string;
90
97
  /**
91
98
  * Version gates for a stored shell: reactVersion AND buildVersion must both
92
99
  * match the running server. The postponed blob encodes hole positions against
@@ -49,6 +49,8 @@ export interface RscPayload {
49
49
  prefetchCacheSize?: number;
50
50
  /** Max concurrent speculative prefetch requests on the client */
51
51
  prefetchConcurrency?: number;
52
+ /** Router-wide default prefetch strategy for Links without a `prefetch` prop */
53
+ defaultPrefetch?: import("../router/prefetch-default.js").PrefetchStrategy;
52
54
  /** Server-resolved rango state cookie name; the client reads it verbatim. */
53
55
  stateCookieName?: string;
54
56
  /** Theme configuration for FOUC prevention */
@@ -192,6 +194,23 @@ export interface HandlerCacheConfig {
192
194
  store: import("../cache/types.js").SegmentCacheStore;
193
195
  /** Enable/disable caching (default: true) */
194
196
  enabled?: boolean;
197
+ /**
198
+ * Which query params key the cache (default: `"all"`). Affects cache keys
199
+ * ONLY -- handlers and loaders still see the full query string. Global by
200
+ * design: the per-route case is already reachable through `cache({ key })`.
201
+ *
202
+ * Excluding a param is a promise that rendered output does not depend on
203
+ * it; if it does, the first variant is cached and served to everyone.
204
+ *
205
+ * @example
206
+ * ```typescript
207
+ * cache: {
208
+ * store: cacheStore,
209
+ * searchParams: { exclude: TRACKING_SEARCH_PARAMS },
210
+ * }
211
+ * ```
212
+ */
213
+ searchParams?: import("../cache/search-params-filter.js").CacheSearchParams;
195
214
  }
196
215
  /**
197
216
  * Nonce provider function type.
@@ -143,8 +143,24 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
143
143
  }>;
144
144
  /** @internal PPR transition decisions evaluated before cache lookup/handlers. */
145
145
  _pprTransitionDecisions?: Map<string, boolean>;
146
+ /**
147
+ * @internal Post-match serve-source truth for the PPR replay reporter.
148
+ * Stamped from the match context as `intercept`, then overwritten as
149
+ * `prerender-store` if that lookup actually serves. The latter wins because
150
+ * it identifies the response source. matchPartialWithPprReplay reads this
151
+ * AFTER matching to reclassify its pre-match guess and suppress pointless
152
+ * heal captures.
153
+ */
154
+ _pprReplayPostMatchReason?: "prerender-store" | "intercept";
146
155
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
147
156
  _cacheStore?: SegmentCacheStore;
157
+ /**
158
+ * @internal Compiled `cache.searchParams` filter for default cache-key
159
+ * generation (undefined = "all", the byte-stable unfiltered format). Set
160
+ * once per request by handler.ts; every URL-keyed tier (segment, document,
161
+ * response, shell, "use cache") reads it so the tiers cannot drift.
162
+ */
163
+ _searchParamsFilter?: import("../cache/search-params-filter.js").SearchParamsFilter;
148
164
  /**
149
165
  * @internal PPR shell-capture ACTIVE marker. True ONLY inside the background
150
166
  * capture task's derived request context (built by shell-capture.ts). This is
@@ -190,9 +206,13 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
190
206
  * (b) a HIT tail's seeded context, and (c) an eligible normal-route partial
191
207
  * replay, where `store` is a request-local segment overlay. Handler-live holes
192
208
  * and conditional transitions decline replay.
193
- * Routes with their own cache() config (including cache(false)) are never
194
- * overridden: the marker only applies when the route tree derived NO cache
195
- * scope.
209
+ * Routes with their own cache() config are never overridden — their scope's
210
+ * store/key/ttl/swr/condition semantics stay authoritative. On the
211
+ * navigation-replay serve path (`onExplicitHit` set) the marker COMPOSES with
212
+ * such a scope instead of being ignored: the seeded doc record supplies the match
213
+ * only when the explicit tier misses (withCacheLookup). cache(false) and a
214
+ * false condition() stay absolute opt-outs — replay bypasses before any
215
+ * shell read (`cache-disabled`).
196
216
  */
197
217
  _shellImplicitCache?: {
198
218
  ttl?: number;
@@ -206,6 +226,34 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
206
226
  keyPrefix?: "doc";
207
227
  /** @internal Called only after the implicit cache hit decodes successfully. */
208
228
  onHit?: () => void;
229
+ /**
230
+ * @internal The resolved key of the canonical document segment record.
231
+ * Written during a CAPTURE render by CacheScope.cacheRoute when a
232
+ * doc-namespaced scope records the matched segments; captureAndStoreShell
233
+ * stamps it onto the ShellCacheEntry (`docKey`) so replay eligibility can
234
+ * require the exact consumable record instead of "any segment record".
235
+ */
236
+ docKey?: string;
237
+ /**
238
+ * @internal Set ONLY by matchPartialWithPprReplay on the navigation-replay
239
+ * serve path. Its presence arms the explicit-scope composition in
240
+ * withCacheLookup: a route-derived cache() scope stays authoritative, and
241
+ * only when its lookup MISSes does the seeded doc record supply the match.
242
+ * A capture render must never set this (its match must re-run handlers so
243
+ * SWR recapture stays fresh), which is why the fallback keys off this
244
+ * field and not off the marker itself. Fired when the route-derived
245
+ * scope's own lookup supplied the match — the header then reports
246
+ * `BYPASS; reason=explicit-cache-hit`, never a false replay HIT.
247
+ */
248
+ onExplicitHit?: () => void;
249
+ /**
250
+ * @internal Companion to onExplicitHit, same lifecycle: fired when the
251
+ * route-derived scope's lookup REFUSED the read (`bypass` outcome — a
252
+ * false condition() or no store). The gate only pre-decides the static
253
+ * cache(false) case; a predicate refusal is known only at lookup time and
254
+ * reports `BYPASS; reason=cache-disabled` post-match.
255
+ */
256
+ onExplicitBypass?: () => void;
209
257
  };
210
258
  /**
211
259
  * @internal Shell-HIT tail marker: cache/prerender-store hits during THIS
@@ -518,7 +566,7 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
518
566
  * This is the type exported to library consumers. Internal code should
519
567
  * use the full RequestContext interface directly.
520
568
  */
521
- export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhen" | "_pprTransitionDecisions" | "_cacheStore" | "_shellCaptureRun" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureHandleLiveness" | "_shellCaptureLoaderHandleValues" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_locationState" | "_routeName" | "_prevRouteKey" | "_gateCurrentUrl" | "_gateCurrentParams" | "_gateActionId" | "_gateActionUrl" | "_gateActionResult" | "_gateFormData" | "_inActionRevalidation" | "_reportedErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_variables" | "_classifiedRoute" | "_cacheSignal" | "_dynamic" | "res">;
569
+ export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhen" | "_pprTransitionDecisions" | "_pprReplayPostMatchReason" | "_cacheStore" | "_searchParamsFilter" | "_shellCaptureRun" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureHandleLiveness" | "_shellCaptureLoaderHandleValues" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_locationState" | "_routeName" | "_prevRouteKey" | "_gateCurrentUrl" | "_gateCurrentParams" | "_gateActionId" | "_gateActionUrl" | "_gateActionResult" | "_gateFormData" | "_inActionRevalidation" | "_reportedErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_variables" | "_classifiedRoute" | "_cacheSignal" | "_dynamic" | "res">;
522
570
  /**
523
571
  * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
524
572
  * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
@@ -574,6 +622,11 @@ export interface CreateRequestContextOptions<TEnv> {
574
622
  initialResponse?: Response;
575
623
  /** Optional cache store for segment caching (used by CacheScope) */
576
624
  cacheStore?: SegmentCacheStore;
625
+ /**
626
+ * Compiled `cache.searchParams` filter from the resolved handler cache
627
+ * config (handler.ts). Stored as _searchParamsFilter.
628
+ */
629
+ searchParamsFilter?: import("../cache/search-params-filter.js").SearchParamsFilter;
577
630
  /**
578
631
  * Handler-owned registry of explicit per-scope stores for cross-store tag
579
632
  * invalidation. Created once per handler, reused across requests.
@@ -1,11 +1,11 @@
1
1
  import type { Expect, TestType } from "@playwright/test";
2
2
  import { createUseFixture, type Fixture, type FixtureOptions } from "./fixture.js";
3
- import { createPageHelpers, createStopwatch, getHistoryState, getNumericContent, goBack, goForward, isVisibleInViewport, measureTime, type PageHelpers, parseNumber, type Stopwatch, testId, waitForElement, waitForHydration, waitForNavigation } from "./page-helpers.js";
3
+ import { blockPrefetch, unblockPrefetch, createPageHelpers, createStopwatch, getHistoryState, getNumericContent, goBack, goForward, isPrefetchRequest, isVisibleInViewport, measureTime, type PageHelpers, parseNumber, type Stopwatch, testId, waitForElement, waitForHydration, waitForNavigation } from "./page-helpers.js";
4
4
  import { createParity, type ExpectParityOptions, type Parity, type ParityDescribeOptions, type ParityIntent } from "./parity.js";
5
5
  import { createRangoMatchers, type RangoMatchers } from "./matchers.js";
6
6
  export { assertCacheStatus, assertCacheDecision, parseCacheHeader, createCacheSink, filterCacheDecisions, type CacheSink, type ExpectedCacheStatus, type CacheStatusTarget, } from "../cache-status.js";
7
7
  export { assertPprReplayStatus, assertShellStatus, parsePprReplayStatus, parseShellStatus, PPR_REPLAY_STATUS_HEADER, shellCacheKey, SHELL_STATUS_HEADER, type PprReplayBypassReason, type PprReplayStatus, type ShellStatus, type ShellStatusTarget, } from "../shell-status.js";
8
- export { testId, waitForHydration, waitForNavigation, goBack, goForward, getHistoryState, waitForElement, isVisibleInViewport, parseNumber, getNumericContent, createStopwatch, measureTime, createPageHelpers, createUseFixture, createParity, createRangoMatchers, };
8
+ export { testId, waitForHydration, waitForNavigation, goBack, goForward, getHistoryState, waitForElement, isVisibleInViewport, isPrefetchRequest, blockPrefetch, unblockPrefetch, parseNumber, getNumericContent, createStopwatch, measureTime, createPageHelpers, createUseFixture, createParity, createRangoMatchers, };
9
9
  export type { Fixture, FixtureOptions, PageHelpers, Stopwatch, Parity, ParityIntent, ParityDescribeOptions, ExpectParityOptions, RangoMatchers, };
10
10
  export interface RangoE2E extends PageHelpers, Parity {
11
11
  useFixture: (options: FixtureOptions) => Fixture;
@@ -43,6 +43,30 @@ export declare function measureTime<T>(fn: () => Promise<T>): Promise<{
43
43
  elapsed: number;
44
44
  result: T;
45
45
  }>;
46
+ /**
47
+ * True when the request is a speculative prefetch (Link viewport/hover/render
48
+ * strategies or `useRouter().prefetch()`). Every prefetch fetch carries the
49
+ * `X-Rango-Prefetch` header. Links prefetch by default in production unless
50
+ * the router opts out, so tests that track `_rsc_partial` requests to pin
51
+ * NAVIGATION behavior must skip these — a request-count or first-request
52
+ * assertion otherwise races background prefetch traffic.
53
+ */
54
+ export declare function isPrefetchRequest(req: {
55
+ headers: () => Record<string, string>;
56
+ }): boolean;
57
+ /**
58
+ * Abort every speculative prefetch request on this page. Install BEFORE
59
+ * `page.goto` so no prefetch can complete first. For tests whose semantics
60
+ * need a virgin prefetch cache: a click on a Link whose target was already
61
+ * prefetched ADOPTS the warmed entry and issues no navigation fetch at all —
62
+ * a `page.route` override, a request waiter, or a Set-Cookie assertion aimed
63
+ * at the navigation request then sees nothing (or sees the prefetch's side
64
+ * effects instead). Aborted prefetches are benign by design (evicted, never
65
+ * adopted, no page error), so the click's real fetch is guaranteed live.
66
+ */
67
+ export declare function blockPrefetch(page: Page): Promise<void>;
68
+ /** Remove a `blockPrefetch` guard, restoring normal prefetch traffic. */
69
+ export declare function unblockPrefetch(page: Page): Promise<void>;
46
70
  export interface PageHelpers {
47
71
  /** Assert that no full page reload occurred between scope entry and exit. */
48
72
  expectNoReload: (page: Page) => AsyncDisposable;