@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,261 @@
1
+ /**
2
+ * Cache Lookup Middleware
3
+ *
4
+ * First middleware in the pipeline. Checks cache before segment resolution.
5
+ *
6
+ * FLOW DIAGRAM
7
+ * ============
8
+ *
9
+ * source (empty)
10
+ * |
11
+ * v
12
+ * +---------------------+
13
+ * | Is action request? |──yes──> yield* source (pass through)
14
+ * +---------------------+
15
+ * | no
16
+ * v
17
+ * +---------------------+
18
+ * | Cache enabled? |──no───> yield* source (pass through)
19
+ * +---------------------+
20
+ * | yes
21
+ * v
22
+ * +---------------------+
23
+ * | Lookup cache |
24
+ * | (pathname, params) |
25
+ * +---------------------+
26
+ * |
27
+ * +-----+-----+
28
+ * | |
29
+ * miss hit
30
+ * | |
31
+ * v v
32
+ * yield* Set state.cacheHit = true
33
+ * source Set state.shouldRevalidate
34
+ * | |
35
+ * | v
36
+ * | +---------------------------+
37
+ * | | For each cached segment: |
38
+ * | | - Apply revalidation |
39
+ * | | - Set component = null |
40
+ * | | if client has it |
41
+ * | +---------------------------+
42
+ * | |
43
+ * | v
44
+ * | +---------------------------+
45
+ * | | Resolve fresh loaders | <-- Loaders are NEVER cached
46
+ * | | (always fresh data) |
47
+ * | +---------------------------+
48
+ * | |
49
+ * | v
50
+ * | yield cached segments
51
+ * | yield fresh loader segments
52
+ * | |
53
+ * +-----------+
54
+ * |
55
+ * v
56
+ * next middleware
57
+ *
58
+ *
59
+ * CACHE BEHAVIOR
60
+ * ==============
61
+ *
62
+ * Cache HIT:
63
+ * - state.cacheHit = true signals downstream middleware to skip
64
+ * - Cached segments have their components nullified if client already has them
65
+ * - Loaders are always re-resolved for fresh data
66
+ * - state.shouldRevalidate triggers background SWR if cache was stale
67
+ *
68
+ * Cache MISS:
69
+ * - Passes through to segment-resolution middleware
70
+ * - No segments yielded from this middleware
71
+ *
72
+ * Loaders:
73
+ * - NEVER cached by design
74
+ * - Always resolved fresh on every request
75
+ * - Ensures data freshness even with cached UI components
76
+ *
77
+ *
78
+ * REVALIDATION RULES
79
+ * ==================
80
+ *
81
+ * Each cached segment is evaluated against its revalidation rules:
82
+ *
83
+ * 1. No rules defined -> use default (skip if client has segment)
84
+ * 2. Rules return false -> skip re-render (nullify component)
85
+ * 3. Rules return true -> re-render (keep component)
86
+ *
87
+ * Revalidation context includes:
88
+ * - Previous/next URL and params
89
+ * - Request object
90
+ * - Action context (if POST)
91
+ */
92
+ import type { ResolvedSegment } from "../../types.js";
93
+ import type { MatchContext, MatchPipelineState } from "../match-context.js";
94
+ import { getRouterContext } from "../router-context.js";
95
+
96
+ /**
97
+ * Async generator middleware type
98
+ */
99
+ export type GeneratorMiddleware<T> = (
100
+ source: AsyncGenerator<T>
101
+ ) => AsyncGenerator<T>;
102
+
103
+ /**
104
+ * Creates cache lookup middleware
105
+ *
106
+ * Checks cache for segments. If cache hit:
107
+ * - Applies revalidation to determine which segments need re-rendering
108
+ * - Resolves loaders fresh (loaders are NOT cached by design)
109
+ * - Sets state.cacheHit = true
110
+ * - Sets state.shouldRevalidate if SWR needed
111
+ * - Yields cached segments + fresh loader segments
112
+ *
113
+ * If cache miss:
114
+ * - Passes through to next middleware
115
+ */
116
+ export function withCacheLookup<TEnv>(
117
+ ctx: MatchContext<TEnv>,
118
+ state: MatchPipelineState
119
+ ): GeneratorMiddleware<ResolvedSegment> {
120
+ return async function* (
121
+ source: AsyncGenerator<ResolvedSegment>
122
+ ): AsyncGenerator<ResolvedSegment> {
123
+ const {
124
+ evaluateRevalidation,
125
+ buildEntryRevalidateMap,
126
+ resolveLoadersOnlyWithRevalidation,
127
+ resolveLoadersOnly,
128
+ } = getRouterContext<TEnv>();
129
+
130
+ // Skip cache during actions
131
+ if (ctx.isAction || !ctx.cacheScope?.enabled) {
132
+ // Cache miss - pass through to segment resolution
133
+ yield* source;
134
+ return;
135
+ }
136
+
137
+ // Lookup cache
138
+ const cacheResult = await ctx.cacheScope.lookupRoute(
139
+ ctx.pathname,
140
+ ctx.matched.params,
141
+ ctx.isIntercept
142
+ );
143
+
144
+ if (!cacheResult) {
145
+ // Cache miss - pass through to segment resolution
146
+ yield* source;
147
+ return;
148
+ }
149
+
150
+ // Cache HIT
151
+ state.cacheHit = true;
152
+ state.shouldRevalidate = cacheResult.shouldRevalidate;
153
+ state.cachedSegments = cacheResult.segments;
154
+ state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
155
+
156
+ // Apply revalidation to cached segments
157
+ const entryRevalidateMap = buildEntryRevalidateMap?.(ctx.entries);
158
+
159
+ for (const segment of cacheResult.segments) {
160
+ // Skip segments client doesn't have - they need their component
161
+ if (!ctx.clientSegmentSet.has(segment.id)) {
162
+ yield segment;
163
+ continue;
164
+ }
165
+
166
+ // Skip intercept segments - they're handled separately
167
+ if (segment.namespace?.startsWith("intercept:")) {
168
+ yield segment;
169
+ continue;
170
+ }
171
+
172
+ // Look up revalidation rules for this segment
173
+ const entryInfo = entryRevalidateMap?.get(segment.id);
174
+ if (!entryInfo || entryInfo.revalidate.length === 0) {
175
+ // No revalidation rules, use default behavior (skip if client has)
176
+ segment.component = null;
177
+ segment.loading = undefined;
178
+ yield segment;
179
+ continue;
180
+ }
181
+
182
+ // Evaluate revalidation rules
183
+ const shouldRevalidate = await evaluateRevalidation({
184
+ segment,
185
+ prevParams: ctx.prevParams,
186
+ getPrevSegment: null,
187
+ request: ctx.request,
188
+ prevUrl: ctx.prevUrl,
189
+ nextUrl: ctx.url,
190
+ revalidations: entryInfo.revalidate.map((fn, i) => ({
191
+ name: `revalidate${i}`,
192
+ fn,
193
+ })),
194
+ routeKey: ctx.routeKey,
195
+ context: ctx.handlerContext,
196
+ actionContext: ctx.actionContext,
197
+ });
198
+
199
+ if (!shouldRevalidate) {
200
+ // Client has it, no revalidation needed
201
+ segment.component = null;
202
+ segment.loading = undefined;
203
+ }
204
+
205
+ yield segment;
206
+ }
207
+
208
+ // Resolve loaders fresh (loaders are NOT cached by default)
209
+ // This ensures fresh data even on cache hit
210
+ const Store = ctx.Store;
211
+
212
+ if (ctx.isFullMatch) {
213
+ // Full match (document request) - simple loader resolution without revalidation
214
+ if (resolveLoadersOnly) {
215
+ const loaderSegments = await Store.run(() =>
216
+ resolveLoadersOnly(ctx.entries, ctx.handlerContext)
217
+ );
218
+
219
+ // Update state - full match doesn't track matchedIds separately
220
+ state.matchedIds = state.cachedMatchedIds!;
221
+
222
+ // Yield fresh loader segments
223
+ for (const segment of loaderSegments) {
224
+ yield segment;
225
+ }
226
+ } else {
227
+ state.matchedIds = state.cachedMatchedIds!;
228
+ }
229
+ } else {
230
+ // Partial match (navigation) - loader resolution with revalidation
231
+ if (resolveLoadersOnlyWithRevalidation) {
232
+ const loaderResult = await Store.run(() =>
233
+ resolveLoadersOnlyWithRevalidation(
234
+ ctx.entries,
235
+ ctx.handlerContext,
236
+ ctx.clientSegmentSet,
237
+ ctx.prevParams,
238
+ ctx.request,
239
+ ctx.prevUrl,
240
+ ctx.url,
241
+ ctx.routeKey,
242
+ ctx.actionContext
243
+ )
244
+ );
245
+
246
+ // Update state with fresh loader matchedIds
247
+ state.matchedIds = [
248
+ ...state.cachedMatchedIds!,
249
+ ...loaderResult.matchedIds,
250
+ ];
251
+
252
+ // Yield fresh loader segments
253
+ for (const segment of loaderResult.segments) {
254
+ yield segment;
255
+ }
256
+ } else {
257
+ state.matchedIds = state.cachedMatchedIds!;
258
+ }
259
+ }
260
+ };
261
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Cache Store Middleware
3
+ *
4
+ * Stores resolved segments in cache for future requests.
5
+ * Implements proactive caching for partial navigation scenarios.
6
+ *
7
+ * FLOW DIAGRAM
8
+ * ============
9
+ *
10
+ * source (from intercept-resolution)
11
+ * |
12
+ * v
13
+ * +---------------------------+
14
+ * | Collect + yield all | Observer pattern: pass through
15
+ * | allSegments[] |
16
+ * +---------------------------+
17
+ * |
18
+ * v
19
+ * +---------------------+
20
+ * | Should skip cache? |
21
+ * | - !cacheScope |──yes──> return
22
+ * | - isAction |
23
+ * | - cacheHit |
24
+ * | - method !== GET |
25
+ * +---------------------+
26
+ * | no
27
+ * v
28
+ * +-------------------------------+
29
+ * | Any null components? |
30
+ * | (client already has segment) |
31
+ * +-------------------------------+
32
+ * |
33
+ * +-----+-----+
34
+ * | |
35
+ * yes no
36
+ * | |
37
+ * v v
38
+ * PROACTIVE DIRECT
39
+ * CACHE CACHE
40
+ * | |
41
+ * v v
42
+ * waitUntil() cacheRoute()
43
+ * re-render immediately
44
+ * fresh |
45
+ * | |
46
+ * +-----------+
47
+ * |
48
+ * v
49
+ * next middleware
50
+ *
51
+ *
52
+ * CACHING STRATEGIES
53
+ * ==================
54
+ *
55
+ * 1. Direct Cache (all components present):
56
+ * - Immediate cacheRoute() call
57
+ * - All segments have valid components
58
+ * - Used for fresh full-page renders
59
+ *
60
+ * 2. Proactive Cache (null components present):
61
+ * - Background re-render via waitUntil()
62
+ * - Creates fresh context to avoid polluting response
63
+ * - Re-resolves ALL segments without revalidation
64
+ * - Ensures cache has complete components for future requests
65
+ *
66
+ *
67
+ * WHY PROACTIVE CACHING?
68
+ * ======================
69
+ *
70
+ * During partial navigation, some segments have null components:
71
+ *
72
+ * Request: /products/123 -> /products/456
73
+ * Segments: [ProductLayout(null), ProductPage(component)]
74
+ *
75
+ * The null means "client already has this, don't re-send."
76
+ * But if we cache these null components, future document requests
77
+ * would fail (no component to render).
78
+ *
79
+ * Solution: Background re-render all segments fresh, then cache.
80
+ * This ensures the cache always has complete, renderable segments.
81
+ *
82
+ *
83
+ * PROACTIVE CACHE FLOW
84
+ * ====================
85
+ *
86
+ * 1. Current request returns (fast, with nulls)
87
+ * 2. waitUntil() triggers background work
88
+ * 3. Create fresh handler context (silent, no stream pollution)
89
+ * 4. Re-resolve all entries without revalidation logic
90
+ * 5. Also resolve intercept segments if applicable
91
+ * 6. Store complete segments in cache
92
+ *
93
+ *
94
+ * SKIP CONDITIONS
95
+ * ===============
96
+ *
97
+ * Caching is skipped when:
98
+ * - Cache scope disabled (no caching configured)
99
+ * - This is an action request (mutations shouldn't cache)
100
+ * - Cache was already hit (no need to re-cache same data)
101
+ * - Non-GET request (only GET requests are cacheable)
102
+ */
103
+ import type { ResolvedSegment } from "../../types.js";
104
+ import { getRequestContext } from "../../server/request-context.js";
105
+ import type { MatchContext, MatchPipelineState } from "../match-context.js";
106
+ import { getRouterContext } from "../router-context.js";
107
+ import type { GeneratorMiddleware } from "./cache-lookup.js";
108
+
109
+ /**
110
+ * Creates cache store middleware
111
+ *
112
+ * Observes all segments passing through and stores them in cache after pipeline completes.
113
+ * Handles proactive caching for null-component segments.
114
+ */
115
+ export function withCacheStore<TEnv>(
116
+ ctx: MatchContext<TEnv>,
117
+ state: MatchPipelineState
118
+ ): GeneratorMiddleware<ResolvedSegment> {
119
+ return async function* (
120
+ source: AsyncGenerator<ResolvedSegment>
121
+ ): AsyncGenerator<ResolvedSegment> {
122
+ // Collect all segments while passing them through
123
+ const allSegments: ResolvedSegment[] = [];
124
+ for await (const segment of source) {
125
+ allSegments.push(segment);
126
+ yield segment;
127
+ }
128
+
129
+ // Skip caching if:
130
+ // 1. Cache miss but cache scope is disabled
131
+ // 2. This is an action (actions don't cache)
132
+ // 3. Cache was already hit (no need to re-cache)
133
+ // 4. Non-GET request (only cache GET requests)
134
+ if (!ctx.cacheScope?.enabled || ctx.isAction || state.cacheHit || ctx.request.method !== "GET") {
135
+ return;
136
+ }
137
+
138
+ const {
139
+ createHandlerContext,
140
+ setupLoaderAccessSilent,
141
+ resolveAllSegments,
142
+ resolveInterceptEntry,
143
+ } = getRouterContext<TEnv>();
144
+
145
+ // Combine main segments with intercept segments
146
+ const allSegmentsToCache = [...allSegments, ...state.interceptSegments];
147
+
148
+ // Check if any non-loader segments have null components
149
+ // This happens when client already had those segments (partial navigation)
150
+ const hasNullComponents = allSegmentsToCache.some(
151
+ (s) => s.component === null && s.type !== "loader"
152
+ );
153
+
154
+ const requestCtx = getRequestContext();
155
+ if (!requestCtx) return;
156
+
157
+ const cacheScope = ctx.cacheScope;
158
+
159
+ // Register onResponse callback to skip caching for non-200 responses
160
+ // Note: error/notFound status codes are set elsewhere (not caching-specific)
161
+ requestCtx.onResponse((response) => {
162
+ // Only cache successful responses
163
+ if (response.status !== 200) {
164
+ console.log(
165
+ `[CacheStore] Skipping cache: non-200 status ${response.status} for ${ctx.pathname}`
166
+ );
167
+ return response;
168
+ }
169
+
170
+ if (hasNullComponents) {
171
+ // Proactive caching: render all segments fresh in background
172
+ // This ensures cache has complete components for future requests
173
+ requestCtx.waitUntil(async () => {
174
+ console.log(
175
+ `[Router.matchPartial] Proactive caching: ${ctx.pathname} (rendering null-component segments)`
176
+ );
177
+ try {
178
+ // Create fresh context for proactive caching
179
+ // This prevents handle data from polluting the response stream
180
+ const proactiveHandlerContext = createHandlerContext(
181
+ ctx.matched.params,
182
+ ctx.request,
183
+ ctx.url.searchParams,
184
+ ctx.pathname,
185
+ ctx.url,
186
+ ctx.bindings
187
+ );
188
+ const proactiveLoaderPromises = new Map<string, Promise<any>>();
189
+
190
+ // Set up loader access that ignores handle pushes
191
+ setupLoaderAccessSilent(proactiveHandlerContext, proactiveLoaderPromises);
192
+
193
+ // Re-resolve ALL segments without revalidation
194
+ const Store = ctx.Store;
195
+ const freshSegments = await Store.run(() =>
196
+ resolveAllSegments(
197
+ ctx.entries,
198
+ ctx.routeKey,
199
+ ctx.matched.params,
200
+ proactiveHandlerContext,
201
+ proactiveLoaderPromises
202
+ )
203
+ );
204
+
205
+ // Also resolve intercept segments fresh if applicable
206
+ let freshInterceptSegments: ResolvedSegment[] = [];
207
+ if (ctx.interceptResult) {
208
+ freshInterceptSegments = await Store.run(() =>
209
+ resolveInterceptEntry(
210
+ ctx.interceptResult!.intercept,
211
+ ctx.interceptResult!.entry,
212
+ ctx.matched.params,
213
+ proactiveHandlerContext,
214
+ true // belongsToRoute
215
+ // No revalidationContext = render fresh
216
+ )
217
+ );
218
+ }
219
+
220
+ const completeSegments = [...freshSegments, ...freshInterceptSegments];
221
+ await cacheScope.cacheRoute(
222
+ ctx.pathname,
223
+ ctx.matched.params,
224
+ completeSegments,
225
+ ctx.isIntercept
226
+ );
227
+ console.log(
228
+ `[Router.matchPartial] Proactive caching complete: ${ctx.pathname}`
229
+ );
230
+ } catch (error) {
231
+ console.error(`[Router.matchPartial] Proactive caching failed:`, error);
232
+ }
233
+ });
234
+ } else {
235
+ // All segments have components - cache directly
236
+ // Schedule caching in waitUntil since cacheRoute is now async (key resolution)
237
+ requestCtx.waitUntil(async () => {
238
+ await cacheScope.cacheRoute(
239
+ ctx.pathname,
240
+ ctx.matched.params,
241
+ allSegmentsToCache,
242
+ ctx.isIntercept
243
+ );
244
+ });
245
+ }
246
+
247
+ return response;
248
+ });
249
+ };
250
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Match Middleware
3
+ *
4
+ * Async generator middleware for the match pipeline.
5
+ * Each middleware transforms or enriches the segment stream.
6
+ *
7
+ * MIDDLEWARE OVERVIEW
8
+ * ===================
9
+ *
10
+ * The pipeline consists of 5 middleware layers, each with a specific role:
11
+ *
12
+ * +-------------------------------------------------------------------------+
13
+ * | MIDDLEWARE PIPELINE |
14
+ * +-------------------------------------------------------------------------+
15
+ * | |
16
+ * | [1] CACHE LOOKUP (innermost) |
17
+ * | Purpose: Check cache before resolving |
18
+ * | On hit: Yield cached segments + fresh loaders |
19
+ * | On miss: Pass through to segment resolution |
20
+ * | Side effects: Sets state.cacheHit, state.shouldRevalidate |
21
+ * | |
22
+ * | [2] SEGMENT RESOLUTION |
23
+ * | Purpose: Resolve segments when cache misses |
24
+ * | Skips if: state.cacheHit === true |
25
+ * | Produces: All route segments (layouts, routes, loaders) |
26
+ * | Two modes: Full (simple) vs Partial (with revalidation) |
27
+ * | |
28
+ * | [3] INTERCEPT RESOLUTION |
29
+ * | Purpose: Resolve intercept segments (modal slots) |
30
+ * | Triggers: When ctx.interceptResult exists |
31
+ * | Produces: Additional segments for named slots |
32
+ * | Updates: state.slots[slotName] with intercept segments |
33
+ * | |
34
+ * | [4] CACHE STORE |
35
+ * | Purpose: Store segments in cache for future requests |
36
+ * | Skips if: Cache hit, actions, or cache disabled |
37
+ * | Strategy: Direct cache if all components present |
38
+ * | Proactive cache if null components (via waitUntil) |
39
+ * | |
40
+ * | [5] BACKGROUND REVALIDATION (outermost) |
41
+ * | Purpose: SWR - serve stale, revalidate in background |
42
+ * | Triggers: When state.shouldRevalidate === true |
43
+ * | Action: Async resolution via waitUntil(), updates cache |
44
+ * | |
45
+ * +-------------------------------------------------------------------------+
46
+ *
47
+ *
48
+ * MIDDLEWARE INTERACTION PATTERNS
49
+ * ===============================
50
+ *
51
+ * Pattern 1: Producer Middleware (cache-lookup, segment-resolution)
52
+ * - Yields segments into the stream
53
+ * - Creates new data for downstream middleware
54
+ *
55
+ * Pattern 2: Transformer Middleware (intercept-resolution)
56
+ * - Passes through existing segments
57
+ * - Adds additional segments (intercepts)
58
+ *
59
+ * Pattern 3: Observer Middleware (cache-store, background-revalidation)
60
+ * - Passes through all segments unchanged
61
+ * - Triggers side effects based on state
62
+ *
63
+ *
64
+ * STATE FLAGS
65
+ * ===========
66
+ *
67
+ * The middleware communicate through MatchPipelineState:
68
+ *
69
+ * state.cacheHit - Set by cache-lookup, read by others to skip work
70
+ * state.shouldRevalidate - Set by cache-lookup, triggers bg-revalidation
71
+ * state.segments - Accumulated segments from pipeline
72
+ * state.interceptSegments - Segments for intercept slots
73
+ * state.slots - Named slot data for client
74
+ */
75
+
76
+ export { withCacheLookup } from "./cache-lookup.js";
77
+ export { withSegmentResolution } from "./segment-resolution.js";
78
+ export { withInterceptResolution } from "./intercept-resolution.js";
79
+ export { withCacheStore } from "./cache-store.js";
80
+ export { withBackgroundRevalidation } from "./background-revalidation.js";
81
+ export type { GeneratorMiddleware } from "./cache-lookup.js";