@ivogt/rsc-router 0.0.0-experimental.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 (123) hide show
  1. package/README.md +19 -0
  2. package/package.json +131 -0
  3. package/src/__mocks__/version.ts +6 -0
  4. package/src/__tests__/route-definition.test.ts +63 -0
  5. package/src/browser/event-controller.ts +876 -0
  6. package/src/browser/index.ts +18 -0
  7. package/src/browser/link-interceptor.ts +121 -0
  8. package/src/browser/lru-cache.ts +69 -0
  9. package/src/browser/merge-segment-loaders.ts +126 -0
  10. package/src/browser/navigation-bridge.ts +891 -0
  11. package/src/browser/navigation-client.ts +155 -0
  12. package/src/browser/navigation-store.ts +823 -0
  13. package/src/browser/partial-update.ts +545 -0
  14. package/src/browser/react/Link.tsx +248 -0
  15. package/src/browser/react/NavigationProvider.tsx +228 -0
  16. package/src/browser/react/ScrollRestoration.tsx +94 -0
  17. package/src/browser/react/context.ts +53 -0
  18. package/src/browser/react/index.ts +52 -0
  19. package/src/browser/react/location-state-shared.ts +120 -0
  20. package/src/browser/react/location-state.ts +62 -0
  21. package/src/browser/react/use-action.ts +240 -0
  22. package/src/browser/react/use-client-cache.ts +56 -0
  23. package/src/browser/react/use-handle.ts +178 -0
  24. package/src/browser/react/use-link-status.ts +134 -0
  25. package/src/browser/react/use-navigation.ts +150 -0
  26. package/src/browser/react/use-segments.ts +188 -0
  27. package/src/browser/request-controller.ts +149 -0
  28. package/src/browser/rsc-router.tsx +310 -0
  29. package/src/browser/scroll-restoration.ts +324 -0
  30. package/src/browser/server-action-bridge.ts +747 -0
  31. package/src/browser/shallow.ts +35 -0
  32. package/src/browser/types.ts +443 -0
  33. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  34. package/src/cache/__tests__/memory-store.test.ts +484 -0
  35. package/src/cache/cache-scope.ts +565 -0
  36. package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
  37. package/src/cache/cf/cf-cache-store.ts +274 -0
  38. package/src/cache/cf/index.ts +19 -0
  39. package/src/cache/index.ts +52 -0
  40. package/src/cache/memory-segment-store.ts +150 -0
  41. package/src/cache/memory-store.ts +253 -0
  42. package/src/cache/types.ts +366 -0
  43. package/src/client.rsc.tsx +88 -0
  44. package/src/client.tsx +609 -0
  45. package/src/components/DefaultDocument.tsx +20 -0
  46. package/src/default-error-boundary.tsx +88 -0
  47. package/src/deps/browser.ts +8 -0
  48. package/src/deps/html-stream-client.ts +2 -0
  49. package/src/deps/html-stream-server.ts +2 -0
  50. package/src/deps/rsc.ts +10 -0
  51. package/src/deps/ssr.ts +2 -0
  52. package/src/errors.ts +259 -0
  53. package/src/handle.ts +120 -0
  54. package/src/handles/MetaTags.tsx +178 -0
  55. package/src/handles/index.ts +6 -0
  56. package/src/handles/meta.ts +247 -0
  57. package/src/href-client.ts +128 -0
  58. package/src/href.ts +139 -0
  59. package/src/index.rsc.ts +69 -0
  60. package/src/index.ts +84 -0
  61. package/src/loader.rsc.ts +204 -0
  62. package/src/loader.ts +47 -0
  63. package/src/network-error-thrower.tsx +21 -0
  64. package/src/outlet-context.ts +15 -0
  65. package/src/root-error-boundary.tsx +277 -0
  66. package/src/route-content-wrapper.tsx +198 -0
  67. package/src/route-definition.ts +1333 -0
  68. package/src/route-map-builder.ts +140 -0
  69. package/src/route-types.ts +148 -0
  70. package/src/route-utils.ts +89 -0
  71. package/src/router/__tests__/match-context.test.ts +104 -0
  72. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  73. package/src/router/__tests__/match-result.test.ts +566 -0
  74. package/src/router/__tests__/on-error.test.ts +935 -0
  75. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  76. package/src/router/error-handling.ts +287 -0
  77. package/src/router/handler-context.ts +60 -0
  78. package/src/router/loader-resolution.ts +326 -0
  79. package/src/router/manifest.ts +116 -0
  80. package/src/router/match-context.ts +261 -0
  81. package/src/router/match-middleware/background-revalidation.ts +236 -0
  82. package/src/router/match-middleware/cache-lookup.ts +261 -0
  83. package/src/router/match-middleware/cache-store.ts +250 -0
  84. package/src/router/match-middleware/index.ts +81 -0
  85. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  86. package/src/router/match-middleware/segment-resolution.ts +174 -0
  87. package/src/router/match-pipelines.ts +214 -0
  88. package/src/router/match-result.ts +212 -0
  89. package/src/router/metrics.ts +62 -0
  90. package/src/router/middleware.test.ts +1355 -0
  91. package/src/router/middleware.ts +748 -0
  92. package/src/router/pattern-matching.ts +271 -0
  93. package/src/router/revalidation.ts +190 -0
  94. package/src/router/router-context.ts +299 -0
  95. package/src/router/types.ts +96 -0
  96. package/src/router.ts +3484 -0
  97. package/src/rsc/__tests__/helpers.test.ts +175 -0
  98. package/src/rsc/handler.ts +942 -0
  99. package/src/rsc/helpers.ts +64 -0
  100. package/src/rsc/index.ts +56 -0
  101. package/src/rsc/nonce.ts +18 -0
  102. package/src/rsc/types.ts +225 -0
  103. package/src/segment-system.tsx +405 -0
  104. package/src/server/__tests__/request-context.test.ts +171 -0
  105. package/src/server/context.ts +340 -0
  106. package/src/server/handle-store.ts +230 -0
  107. package/src/server/loader-registry.ts +174 -0
  108. package/src/server/request-context.ts +470 -0
  109. package/src/server/root-layout.tsx +10 -0
  110. package/src/server/tsconfig.json +14 -0
  111. package/src/server.ts +126 -0
  112. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  113. package/src/ssr/index.tsx +215 -0
  114. package/src/types.ts +1473 -0
  115. package/src/use-loader.tsx +346 -0
  116. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  117. package/src/vite/expose-action-id.ts +344 -0
  118. package/src/vite/expose-handle-id.ts +209 -0
  119. package/src/vite/expose-loader-id.ts +357 -0
  120. package/src/vite/expose-location-state-id.ts +177 -0
  121. package/src/vite/index.ts +608 -0
  122. package/src/vite/version.d.ts +12 -0
  123. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Router Manifest Loading
3
+ *
4
+ * Handles lazy loading and validation of route manifests.
5
+ */
6
+
7
+ import { invariant, RouteNotFoundError } from "../errors";
8
+ import { getContext, type EntryData, type MetricsStore } from "../server/context";
9
+ import type { RouteEntry } from "../types";
10
+
11
+ /**
12
+ * Module-level cache for manifests per mount index.
13
+ * Only used in production - dev mode skips caching for HMR support.
14
+ */
15
+ const manifestCache = new Map<number, Map<string, EntryData>>();
16
+
17
+ /**
18
+ * Load manifest from route entry with AsyncLocalStorage context
19
+ * Handles lazy imports, unwrapping, and validation
20
+ */
21
+ export async function loadManifest(
22
+ entry: RouteEntry<any>,
23
+ routeKey: string,
24
+ path: string,
25
+ metricsStore?: MetricsStore,
26
+ isSSR?: boolean
27
+ ): Promise<EntryData> {
28
+ const mountIndex = entry.mountIndex;
29
+ const isDev = process.env.NODE_ENV !== "production";
30
+
31
+ // In production, check cache first
32
+ if (!isDev) {
33
+ const cachedManifest = manifestCache.get(mountIndex);
34
+ if (cachedManifest && cachedManifest.has(routeKey)) {
35
+ return cachedManifest.get(routeKey)!;
36
+ }
37
+ }
38
+
39
+ const Store = getContext().getOrCreateStore(routeKey);
40
+
41
+ // Set mount index in store for unique shortCode prefixes
42
+ Store.mountIndex = mountIndex;
43
+
44
+ // Set isSSR flag so loading() can check if we're in SSR
45
+ Store.isSSR = isSSR;
46
+
47
+ // Attach metrics store to context if provided
48
+ if (metricsStore) {
49
+ Store.metrics = metricsStore;
50
+ }
51
+
52
+ // Clear manifest before rebuilding to prevent stale entry mutations
53
+ Store.manifest.clear();
54
+
55
+ try {
56
+ // Include mountIndex in namespace to ensure unique cache keys per mount
57
+ const namespaceWithMount = mountIndex !== undefined
58
+ ? `#router.M${mountIndex}`
59
+ : "#router";
60
+
61
+ const useItems = await getContext().runWithStore(
62
+ Store,
63
+ Store.namespace || namespaceWithMount,
64
+ Store.parent,
65
+ async () => {
66
+ const load = await entry.handler();
67
+ if (
68
+ load &&
69
+ load !== null &&
70
+ typeof load === "object" &&
71
+ "default" in load
72
+ ) {
73
+ return load.default();
74
+ }
75
+ if (typeof load === "function") {
76
+ return load();
77
+ }
78
+ return load;
79
+ }
80
+ );
81
+
82
+ invariant(
83
+ useItems && useItems.length > 0,
84
+ "Did not receive any handler from router.map()"
85
+ );
86
+ invariant(
87
+ useItems.some((item) => item.type === "layout"),
88
+ "Top-level handler must be a layout"
89
+ );
90
+
91
+ invariant(
92
+ Store.manifest.has(routeKey),
93
+ `Route must be registered for ${routeKey}`
94
+ );
95
+
96
+ // Cache manifest in production after successful build
97
+ if (!isDev) {
98
+ manifestCache.set(mountIndex, new Map(Store.manifest));
99
+ }
100
+
101
+ return Store.manifest.get(routeKey)!;
102
+ } catch (e) {
103
+ throw new RouteNotFoundError(
104
+ `Failed to load route handlers for ${path}: ${(e as Error).message}`,
105
+ {
106
+ cause: {
107
+ error: e,
108
+ state: {
109
+ path,
110
+ routeKey,
111
+ },
112
+ },
113
+ }
114
+ );
115
+ }
116
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Match Context for Router Pipeline
3
+ *
4
+ * Encapsulates all state needed by the match pipeline middleware.
5
+ * Created once at the start of match()/matchPartial() and passed through the pipeline.
6
+ *
7
+ * DATA FLOW ARCHITECTURE
8
+ * ======================
9
+ *
10
+ * The router uses two complementary data structures:
11
+ *
12
+ * MatchContext (ctx) - Immutable request state
13
+ * MatchPipelineState (state) - Mutable pipeline state
14
+ *
15
+ *
16
+ * Request
17
+ * |
18
+ * v
19
+ * +-------------------+
20
+ * | Create Context | ctx = immutable snapshot of request
21
+ * +-------------------+
22
+ * |
23
+ * v
24
+ * +-------------------+
25
+ * | Create State | state = mutable accumulator
26
+ * +-------------------+
27
+ * |
28
+ * +---> [Pipeline Middleware]
29
+ * | |
30
+ * | ctx: read-only
31
+ * | state: read/write
32
+ * | |
33
+ * +<----------+
34
+ * |
35
+ * v
36
+ * +-------------------+
37
+ * | Build Result | Merge ctx + state into MatchResult
38
+ * +-------------------+
39
+ *
40
+ *
41
+ * MATCHCONTEXT FIELDS
42
+ * ===================
43
+ *
44
+ * Request Info:
45
+ * - request, url, pathname: The incoming HTTP request
46
+ *
47
+ * Environment:
48
+ * - env, bindings: Server environment (Cloudflare bindings, etc.)
49
+ *
50
+ * Client State (from RSC request headers):
51
+ * - clientSegmentIds: Segments the client currently has
52
+ * - clientSegmentSet: Set version for O(1) lookup
53
+ * - stale: Whether client considers its cache stale
54
+ *
55
+ * Navigation State:
56
+ * - prevUrl, prevParams, prevMatch: Previous navigation for comparison
57
+ *
58
+ * Current Match:
59
+ * - matched: Route match result (params, route key)
60
+ * - manifestEntry: Resolved manifest data
61
+ * - entries: All route entries (layouts, loaders, etc.)
62
+ * - routeKey, localRouteName: Route identifiers
63
+ *
64
+ * Handler Context:
65
+ * - handlerContext: Context passed to loaders
66
+ * - loaderPromises: Memoized loader promises
67
+ *
68
+ * Intercepts:
69
+ * - interceptResult: Detected intercept (if soft navigation)
70
+ * - interceptSelectorContext: Context for intercept matching
71
+ *
72
+ * Cache:
73
+ * - cacheScope: Cache configuration and methods
74
+ * - isIntercept: Whether this is an intercept request
75
+ *
76
+ * Flags:
77
+ * - isAction: POST/mutation request
78
+ * - isFullMatch: Document request vs navigation
79
+ *
80
+ *
81
+ * MATCHPIPELINESTATE FIELDS
82
+ * =========================
83
+ *
84
+ * State flags (set by middleware, read by others):
85
+ * - cacheHit: Cache lookup succeeded
86
+ * - shouldRevalidate: SWR revalidation needed
87
+ *
88
+ * Segment accumulation:
89
+ * - segments: Resolved segments from pipeline
90
+ * - matchedIds: All segment IDs in match order
91
+ * - cachedSegments: Segments from cache (if hit)
92
+ *
93
+ * Intercept data:
94
+ * - interceptSegments: Segments for modal slots
95
+ * - slots: Named slot data for client
96
+ *
97
+ *
98
+ * IMMUTABILITY CONTRACT
99
+ * =====================
100
+ *
101
+ * MatchContext is treated as immutable after creation.
102
+ * Middleware should NEVER modify ctx properties.
103
+ *
104
+ * MatchPipelineState is explicitly mutable.
105
+ * Middleware communicate by setting state flags.
106
+ *
107
+ * This separation ensures:
108
+ * - Request data is consistent across all middleware
109
+ * - Pipeline state changes are explicit and trackable
110
+ * - No hidden side effects in request handling
111
+ */
112
+ import type { CacheScope } from "../cache/cache-scope.js";
113
+ import type {
114
+ EntryData,
115
+ InterceptSelectorContext,
116
+ MetricsStore,
117
+ } from "../server/context.js";
118
+ import type { HandlerContext, ResolvedSegment } from "../types.js";
119
+ import type { RouteMatchResult } from "./pattern-matching.js";
120
+ import type { InterceptResult } from "./router-context.js";
121
+
122
+ /**
123
+ * Action context passed to matchPartial
124
+ */
125
+ export interface ActionContext {
126
+ actionId?: string;
127
+ actionUrl?: URL;
128
+ actionResult?: any;
129
+ formData?: FormData;
130
+ }
131
+
132
+ /**
133
+ * Match context containing all state for the match pipeline
134
+ */
135
+ export interface MatchContext<TEnv = any> {
136
+ // Request info
137
+ request: Request;
138
+ url: URL;
139
+ pathname: string;
140
+
141
+ // Environment
142
+ env: TEnv;
143
+ bindings: TEnv;
144
+
145
+ // Client state
146
+ clientSegmentIds: string[];
147
+ clientSegmentSet: Set<string>;
148
+ stale: boolean;
149
+
150
+ // Previous navigation state
151
+ prevUrl: URL;
152
+ prevParams: Record<string, string>;
153
+ prevMatch: RouteMatchResult | null;
154
+
155
+ // Current route match
156
+ matched: RouteMatchResult;
157
+ manifestEntry: EntryData;
158
+ entries: EntryData[];
159
+ routeKey: string;
160
+ localRouteName: string;
161
+
162
+ // Handler context (for loaders)
163
+ handlerContext: HandlerContext<any, TEnv>;
164
+ loaderPromises: Map<string, Promise<any>>;
165
+
166
+ // Metrics
167
+ metricsStore: MetricsStore | undefined;
168
+
169
+ // Store for running within context
170
+ Store: any;
171
+
172
+ // Intercept detection
173
+ interceptContextMatch: RouteMatchResult | null;
174
+ interceptSelectorContext: InterceptSelectorContext;
175
+ isSameRouteNavigation: boolean;
176
+ interceptResult: InterceptResult | null;
177
+
178
+ // Cache
179
+ cacheScope: CacheScope | null;
180
+ isIntercept: boolean;
181
+
182
+ // Action context (if this is an action)
183
+ actionContext?: ActionContext;
184
+ isAction: boolean;
185
+
186
+ // Route middleware
187
+ routeMiddleware: Array<{
188
+ handler: any;
189
+ params: Record<string, string>;
190
+ }>;
191
+
192
+ // Full match flag (document requests vs partial/navigation requests)
193
+ // When true, uses simpler resolution without revalidation logic
194
+ isFullMatch: boolean;
195
+ }
196
+
197
+ /**
198
+ * Mutable state that flows through the pipeline
199
+ */
200
+ export interface MatchPipelineState {
201
+ // Whether cache was hit
202
+ cacheHit: boolean;
203
+
204
+ // Cached segments (if cache hit)
205
+ cachedSegments?: ResolvedSegment[];
206
+ cachedMatchedIds?: string[];
207
+
208
+ // Whether cache should be revalidated (SWR)
209
+ shouldRevalidate?: boolean;
210
+
211
+ // Resolved segments from pipeline
212
+ segments: ResolvedSegment[];
213
+ matchedIds: string[];
214
+
215
+ // Intercept segments
216
+ interceptSegments: ResolvedSegment[];
217
+
218
+ // Slots state
219
+ slots: Record<
220
+ string,
221
+ {
222
+ active: boolean;
223
+ segments: ResolvedSegment[];
224
+ }
225
+ >;
226
+ }
227
+
228
+ /**
229
+ * Create initial pipeline state
230
+ */
231
+ export function createPipelineState(): MatchPipelineState {
232
+ return {
233
+ cacheHit: false,
234
+ segments: [],
235
+ matchedIds: [],
236
+ interceptSegments: [],
237
+ slots: {},
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Input parameters for createMatchContext
243
+ */
244
+ export interface CreateMatchContextInput<TEnv = any> {
245
+ request: Request;
246
+ env: TEnv;
247
+ actionContext?: ActionContext;
248
+ }
249
+
250
+ /**
251
+ * Result from createMatchContext - either a context or null (fall back to full match)
252
+ */
253
+ export type CreateMatchContextResult<TEnv = any> =
254
+ | { type: "context"; ctx: MatchContext<TEnv> }
255
+ | { type: "fallback"; reason: string }
256
+ | { type: "error"; error: Error };
257
+
258
+ // Note: createMatchContext() will be implemented in Step J10 when we wire everything together.
259
+ // It requires access to RouterContext (findMatch, loadManifest, etc.) which are closure
260
+ // functions from createRSCRouter(). The implementation will live in router.ts initially
261
+ // and call getRouterContext() to access these dependencies.
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Background Revalidation Middleware
3
+ *
4
+ * Implements SWR (stale-while-revalidate) pattern.
5
+ * Triggers background refresh when cached data is stale.
6
+ *
7
+ * FLOW DIAGRAM
8
+ * ============
9
+ *
10
+ * source (from cache-store)
11
+ * |
12
+ * v
13
+ * +---------------------------+
14
+ * | yield* source | Pure pass-through
15
+ * | (no modifications) |
16
+ * +---------------------------+
17
+ * |
18
+ * v
19
+ * +---------------------+
20
+ * | Should revalidate? |
21
+ * | - shouldRevalidate |──no───> return
22
+ * | - cacheHit |
23
+ * | - cacheScope |
24
+ * +---------------------+
25
+ * | yes
26
+ * v
27
+ * +---------------------------+
28
+ * | requestCtx.waitUntil() | Non-blocking background task
29
+ * +---------------------------+
30
+ * |
31
+ * v (async, doesn't block response)
32
+ * +---------------------------+
33
+ * | Create fresh handleStore | Isolate from response stream
34
+ * +---------------------------+
35
+ * |
36
+ * v
37
+ * +---------------------+
38
+ * | isFullMatch? |
39
+ * +---------------------+
40
+ * |
41
+ * +-----+-----+
42
+ * | |
43
+ * yes no
44
+ * | |
45
+ * v v
46
+ * resolveAll resolveWithRevalidation
47
+ * Segments + resolveIntercepts
48
+ * | |
49
+ * +-----------+
50
+ * |
51
+ * v
52
+ * +---------------------------+
53
+ * | cacheScope.cacheRoute() | Update cache with fresh data
54
+ * +---------------------------+
55
+ *
56
+ *
57
+ * SWR PATTERN
58
+ * ===========
59
+ *
60
+ * Stale-While-Revalidate provides fast responses with eventual consistency:
61
+ *
62
+ * Timeline:
63
+ * ---------
64
+ * T0: Request arrives
65
+ * T1: Cache lookup finds stale entry
66
+ * T2: Return stale data immediately (fast!)
67
+ * T3: Response sent to client
68
+ * T4: waitUntil() triggers background revalidation
69
+ * T5: Fresh data resolved
70
+ * T6: Cache updated with fresh data
71
+ * T7: Next request gets fresh data from cache
72
+ *
73
+ * Benefits:
74
+ * - Fast initial response (cached data)
75
+ * - Eventually consistent (background refresh)
76
+ * - No blocking on revalidation
77
+ *
78
+ *
79
+ * WHEN IS CACHE STALE?
80
+ * ====================
81
+ *
82
+ * The cache-lookup middleware sets state.shouldRevalidate based on:
83
+ * - TTL (time-to-live) expiration
84
+ * - Cache entry metadata
85
+ * - Configured staleness rules
86
+ *
87
+ * This middleware only acts on the flag, it doesn't determine staleness.
88
+ *
89
+ *
90
+ * ISOLATION FROM RESPONSE
91
+ * =======================
92
+ *
93
+ * The background revalidation creates a fresh handleStore:
94
+ *
95
+ * requestCtx._handleStore = createHandleStore();
96
+ *
97
+ * This prevents background handle.push() calls from:
98
+ * - Polluting the current response stream
99
+ * - Causing duplicate data in the client
100
+ * - Creating race conditions
101
+ *
102
+ *
103
+ * FULL VS PARTIAL REVALIDATION
104
+ * ============================
105
+ *
106
+ * Full Match (document request):
107
+ * - Simple resolveAllSegments()
108
+ * - No need to compare with previous state
109
+ *
110
+ * Partial Match (navigation):
111
+ * - resolveAllSegmentsWithRevalidation()
112
+ * - Also resolves intercept segments if applicable
113
+ * - More complex but handles all scenarios
114
+ */
115
+ import type { ResolvedSegment } from "../../types.js";
116
+ import type { MatchContext, MatchPipelineState } from "../match-context.js";
117
+ import { getRouterContext } from "../router-context.js";
118
+ import type { GeneratorMiddleware } from "./cache-lookup.js";
119
+
120
+ /**
121
+ * Creates background revalidation middleware
122
+ *
123
+ * If cache was stale (state.shouldRevalidate === true):
124
+ * - Triggers background resolution via waitUntil
125
+ * - Observes segments but doesn't modify them
126
+ * - Updates cache with fresh segments after revalidation completes
127
+ */
128
+ export function withBackgroundRevalidation<TEnv>(
129
+ ctx: MatchContext<TEnv>,
130
+ state: MatchPipelineState
131
+ ): GeneratorMiddleware<ResolvedSegment> {
132
+ return async function* (
133
+ source: AsyncGenerator<ResolvedSegment>
134
+ ): AsyncGenerator<ResolvedSegment> {
135
+ // Pass through all segments unchanged
136
+ for await (const segment of source) {
137
+ yield segment;
138
+ }
139
+
140
+ // Only trigger background revalidation if:
141
+ // 1. Cache was hit and stale
142
+ // 2. Cache scope exists
143
+ if (!state.shouldRevalidate || !state.cacheHit || !ctx.cacheScope) {
144
+ return;
145
+ }
146
+
147
+ const {
148
+ getRequestContext,
149
+ createHandleStore,
150
+ resolveAllSegmentsWithRevalidation,
151
+ resolveAllSegments,
152
+ resolveInterceptEntry,
153
+ } = getRouterContext<TEnv>();
154
+
155
+ const requestCtx = getRequestContext();
156
+ const cacheScope = ctx.cacheScope;
157
+
158
+ const logPrefix = ctx.isFullMatch ? "[Router.match]" : "[Router.matchPartial]";
159
+
160
+ requestCtx?.waitUntil(async () => {
161
+ console.log(`${logPrefix} Revalidating stale route: ${ctx.pathname}`);
162
+ try {
163
+ // Create a fresh handleStore for background revalidation
164
+ // to avoid polluting the current response's handle stream
165
+ if (requestCtx) {
166
+ requestCtx._handleStore = createHandleStore();
167
+ }
168
+
169
+ let freshSegments: ResolvedSegment[];
170
+
171
+ if (ctx.isFullMatch) {
172
+ // Full match (document request) - simple resolution
173
+ freshSegments = await resolveAllSegments(
174
+ ctx.entries,
175
+ ctx.routeKey,
176
+ ctx.matched.params,
177
+ ctx.handlerContext,
178
+ ctx.loaderPromises
179
+ );
180
+ } else {
181
+ // Partial match (navigation) - resolution with revalidation
182
+ const freshResult = await resolveAllSegmentsWithRevalidation(
183
+ ctx.entries,
184
+ ctx.routeKey,
185
+ ctx.matched.params,
186
+ ctx.handlerContext,
187
+ ctx.clientSegmentSet,
188
+ ctx.prevParams,
189
+ ctx.request,
190
+ ctx.prevUrl,
191
+ ctx.url,
192
+ ctx.loaderPromises,
193
+ ctx.actionContext,
194
+ ctx.interceptResult,
195
+ ctx.localRouteName,
196
+ ctx.pathname
197
+ );
198
+
199
+ freshSegments = freshResult.segments;
200
+
201
+ // For intercept revalidation, also resolve fresh intercept segments
202
+ if (ctx.interceptResult) {
203
+ const freshInterceptSegments = await resolveInterceptEntry(
204
+ ctx.interceptResult.intercept,
205
+ ctx.interceptResult.entry,
206
+ ctx.matched.params,
207
+ ctx.handlerContext,
208
+ true,
209
+ {
210
+ clientSegmentIds: ctx.clientSegmentSet,
211
+ prevParams: ctx.prevParams,
212
+ request: ctx.request,
213
+ prevUrl: ctx.prevUrl,
214
+ nextUrl: ctx.url,
215
+ routeKey: ctx.routeKey,
216
+ actionContext: ctx.actionContext,
217
+ stale: false,
218
+ }
219
+ );
220
+ freshSegments = [...freshSegments, ...freshInterceptSegments];
221
+ }
222
+ }
223
+
224
+ await cacheScope.cacheRoute(
225
+ ctx.pathname,
226
+ ctx.matched.params,
227
+ freshSegments,
228
+ ctx.isIntercept
229
+ );
230
+ console.log(`${logPrefix} Revalidation complete: ${ctx.pathname}`);
231
+ } catch (error) {
232
+ console.error(`${logPrefix} Revalidation failed:`, error);
233
+ }
234
+ });
235
+ };
236
+ }