@rangojs/router 0.0.0-experimental.10

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 (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
@@ -0,0 +1,382 @@
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
+ import type { PrerenderStore } from "../../prerender/store.js";
96
+
97
+ // Lazily initialized prerender store singleton and dynamically imported deps.
98
+ // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
99
+ // top-level, which breaks vitest (only URLs with file:, data:, node: schemes).
100
+ let prerenderStoreInstance: PrerenderStore | null | undefined;
101
+ let _deserializeSegments: typeof import("../../cache/cache-scope.js").deserializeSegments | undefined;
102
+ let _hashParams: typeof import("../../prerender/param-hash.js").hashParams | undefined;
103
+ let _getRequestContext: typeof import("../../server/request-context.js").getRequestContext | undefined;
104
+
105
+ async function ensurePrerenderDeps() {
106
+ if (!_deserializeSegments) {
107
+ const [cache, paramHash, reqCtx, store] = await Promise.all([
108
+ import("../../cache/cache-scope.js"),
109
+ import("../../prerender/param-hash.js"),
110
+ import("../../server/request-context.js"),
111
+ import("../../prerender/store.js"),
112
+ ]);
113
+ _deserializeSegments = cache.deserializeSegments;
114
+ _hashParams = paramHash.hashParams;
115
+ _getRequestContext = reqCtx.getRequestContext;
116
+ if (prerenderStoreInstance === undefined) {
117
+ prerenderStoreInstance = store.createPrerenderStore();
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Async generator middleware type
124
+ */
125
+ export type GeneratorMiddleware<T> = (
126
+ source: AsyncGenerator<T>
127
+ ) => AsyncGenerator<T>;
128
+
129
+ /**
130
+ * Creates cache lookup middleware
131
+ *
132
+ * Checks cache for segments. If cache hit:
133
+ * - Applies revalidation to determine which segments need re-rendering
134
+ * - Resolves loaders fresh (loaders are NOT cached by design)
135
+ * - Sets state.cacheHit = true
136
+ * - Sets state.shouldRevalidate if SWR needed
137
+ * - Yields cached segments + fresh loader segments
138
+ *
139
+ * If cache miss:
140
+ * - Passes through to next middleware
141
+ */
142
+ export function withCacheLookup<TEnv>(
143
+ ctx: MatchContext<TEnv>,
144
+ state: MatchPipelineState
145
+ ): GeneratorMiddleware<ResolvedSegment> {
146
+ return async function* (
147
+ source: AsyncGenerator<ResolvedSegment>
148
+ ): AsyncGenerator<ResolvedSegment> {
149
+ const pipelineStart = performance.now();
150
+ const ms = ctx.metricsStore;
151
+
152
+ const {
153
+ evaluateRevalidation,
154
+ buildEntryRevalidateMap,
155
+ resolveLoadersOnlyWithRevalidation,
156
+ resolveLoadersOnly,
157
+ } = getRouterContext<TEnv>();
158
+
159
+ // Prerender lookup: check build-time cached data before runtime cache.
160
+ // Prerender data is available regardless of runtime cache configuration.
161
+ if (!ctx.isAction && ctx.matched.pr) {
162
+ await ensurePrerenderDeps();
163
+ if (prerenderStoreInstance) {
164
+ const paramHash = _hashParams!(ctx.matched.params);
165
+ const entry = prerenderStoreInstance.get(ctx.matched.routeKey, paramHash);
166
+ if (entry) {
167
+ const segments = await _deserializeSegments!(entry.segments);
168
+
169
+ // Replay handle data (same as runtime cache hit path)
170
+ const handleStore = _getRequestContext!()?._handleStore;
171
+ if (handleStore) {
172
+ for (const [segId, segHandles] of Object.entries(entry.handles)) {
173
+ if (Object.keys(segHandles).length > 0) {
174
+ handleStore.replaySegmentData(segId, segHandles);
175
+ }
176
+ }
177
+ }
178
+
179
+ state.cacheHit = true;
180
+ state.cachedSegments = segments;
181
+ state.cachedMatchedIds = segments.map((s) => s.id);
182
+
183
+ // Yield prerendered segments (same flow as cache hit)
184
+ // For partial navigation, nullify components the client already has
185
+ // so parent layouts stay live (client keeps its existing versions).
186
+ for (const segment of segments) {
187
+ if (!ctx.isFullMatch && ctx.clientSegmentSet.has(segment.id)) {
188
+ segment.component = null;
189
+ segment.loading = undefined;
190
+ }
191
+ yield segment;
192
+ }
193
+
194
+ // Resolve loaders fresh (loaders are never pre-rendered)
195
+ if (ctx.isFullMatch) {
196
+ if (resolveLoadersOnly) {
197
+ const loaderSegments = await ctx.Store.run(() =>
198
+ resolveLoadersOnly(ctx.entries, ctx.handlerContext),
199
+ );
200
+ state.matchedIds = state.cachedMatchedIds!;
201
+ for (const segment of loaderSegments) {
202
+ yield segment;
203
+ }
204
+ } else {
205
+ state.matchedIds = state.cachedMatchedIds!;
206
+ }
207
+ } else {
208
+ if (resolveLoadersOnlyWithRevalidation) {
209
+ const loaderResult = await ctx.Store.run(() =>
210
+ resolveLoadersOnlyWithRevalidation(
211
+ ctx.entries,
212
+ ctx.handlerContext,
213
+ ctx.clientSegmentSet,
214
+ ctx.prevParams,
215
+ ctx.request,
216
+ ctx.prevUrl,
217
+ ctx.url,
218
+ ctx.routeKey,
219
+ ctx.actionContext,
220
+ ),
221
+ );
222
+ state.matchedIds = [
223
+ ...state.cachedMatchedIds!,
224
+ ...loaderResult.matchedIds,
225
+ ];
226
+ for (const segment of loaderResult.segments) {
227
+ yield segment;
228
+ }
229
+ } else {
230
+ state.matchedIds = state.cachedMatchedIds!;
231
+ }
232
+ }
233
+
234
+ if (ms) {
235
+ ms.metrics.push({ label: "pipeline:cache-lookup", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
236
+ }
237
+ return;
238
+ }
239
+ }
240
+ }
241
+
242
+ // Skip cache during actions
243
+ if (ctx.isAction || !ctx.cacheScope?.enabled) {
244
+ // Cache miss - pass through to segment resolution
245
+ yield* source;
246
+ if (ms) {
247
+ ms.metrics.push({ label: "pipeline:cache-lookup", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
248
+ }
249
+ return;
250
+ }
251
+
252
+ // Lookup cache
253
+ const cacheResult = await ctx.cacheScope.lookupRoute(
254
+ ctx.pathname,
255
+ ctx.matched.params,
256
+ ctx.isIntercept
257
+ );
258
+
259
+ if (!cacheResult) {
260
+ // Cache miss - pass through to segment resolution
261
+ yield* source;
262
+ if (ms) {
263
+ ms.metrics.push({ label: "pipeline:cache-lookup", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
264
+ }
265
+ return;
266
+ }
267
+
268
+ // Cache HIT
269
+ state.cacheHit = true;
270
+ state.shouldRevalidate = cacheResult.shouldRevalidate;
271
+ state.cachedSegments = cacheResult.segments;
272
+ state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
273
+
274
+ // Apply revalidation to cached segments
275
+ const entryRevalidateMap = buildEntryRevalidateMap?.(ctx.entries);
276
+
277
+ for (const segment of cacheResult.segments) {
278
+ // Skip segments client doesn't have - they need their component
279
+ if (!ctx.clientSegmentSet.has(segment.id)) {
280
+ yield segment;
281
+ continue;
282
+ }
283
+
284
+ // Skip intercept segments - they're handled separately
285
+ if (segment.namespace?.startsWith("intercept:")) {
286
+ yield segment;
287
+ continue;
288
+ }
289
+
290
+ // Look up revalidation rules for this segment
291
+ const entryInfo = entryRevalidateMap?.get(segment.id);
292
+ if (!entryInfo || entryInfo.revalidate.length === 0) {
293
+ // No revalidation rules, use default behavior (skip if client has)
294
+ segment.component = null;
295
+ segment.loading = undefined;
296
+ yield segment;
297
+ continue;
298
+ }
299
+
300
+ // Evaluate revalidation rules
301
+ const shouldRevalidate = await evaluateRevalidation({
302
+ segment,
303
+ prevParams: ctx.prevParams,
304
+ getPrevSegment: null,
305
+ request: ctx.request,
306
+ prevUrl: ctx.prevUrl,
307
+ nextUrl: ctx.url,
308
+ revalidations: entryInfo.revalidate.map((fn, i) => ({
309
+ name: `revalidate${i}`,
310
+ fn,
311
+ })),
312
+ routeKey: ctx.routeKey,
313
+ context: ctx.handlerContext,
314
+ actionContext: ctx.actionContext,
315
+ });
316
+
317
+ if (!shouldRevalidate) {
318
+ // Client has it, no revalidation needed
319
+ segment.component = null;
320
+ segment.loading = undefined;
321
+ }
322
+
323
+ yield segment;
324
+ }
325
+
326
+ // Resolve loaders fresh (loaders are NOT cached by default)
327
+ // This ensures fresh data even on cache hit
328
+ const Store = ctx.Store;
329
+
330
+ if (ctx.isFullMatch) {
331
+ // Full match (document request) - simple loader resolution without revalidation
332
+ if (resolveLoadersOnly) {
333
+ const loaderSegments = await Store.run(() =>
334
+ resolveLoadersOnly(ctx.entries, ctx.handlerContext)
335
+ );
336
+
337
+ // Update state - full match doesn't track matchedIds separately
338
+ state.matchedIds = state.cachedMatchedIds!;
339
+
340
+ // Yield fresh loader segments
341
+ for (const segment of loaderSegments) {
342
+ yield segment;
343
+ }
344
+ } else {
345
+ state.matchedIds = state.cachedMatchedIds!;
346
+ }
347
+ } else {
348
+ // Partial match (navigation) - loader resolution with revalidation
349
+ if (resolveLoadersOnlyWithRevalidation) {
350
+ const loaderResult = await Store.run(() =>
351
+ resolveLoadersOnlyWithRevalidation(
352
+ ctx.entries,
353
+ ctx.handlerContext,
354
+ ctx.clientSegmentSet,
355
+ ctx.prevParams,
356
+ ctx.request,
357
+ ctx.prevUrl,
358
+ ctx.url,
359
+ ctx.routeKey,
360
+ ctx.actionContext
361
+ )
362
+ );
363
+
364
+ // Update state with fresh loader matchedIds
365
+ state.matchedIds = [
366
+ ...state.cachedMatchedIds!,
367
+ ...loaderResult.matchedIds,
368
+ ];
369
+
370
+ // Yield fresh loader segments
371
+ for (const segment of loaderResult.segments) {
372
+ yield segment;
373
+ }
374
+ } else {
375
+ state.matchedIds = state.cachedMatchedIds!;
376
+ }
377
+ }
378
+ if (ms) {
379
+ ms.metrics.push({ label: "pipeline:cache-lookup", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
380
+ }
381
+ };
382
+ }
@@ -0,0 +1,276 @@
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
+ const pipelineStart = performance.now();
123
+ const ms = ctx.metricsStore;
124
+
125
+ // Collect all segments while passing them through
126
+ const allSegments: ResolvedSegment[] = [];
127
+ for await (const segment of source) {
128
+ allSegments.push(segment);
129
+ yield segment;
130
+ }
131
+
132
+ // Skip caching if:
133
+ // 1. Cache miss but cache scope is disabled
134
+ // 2. This is an action (actions don't cache)
135
+ // 3. Cache was already hit (no need to re-cache)
136
+ // 4. Non-GET request (only cache GET requests)
137
+ if (
138
+ !ctx.cacheScope?.enabled ||
139
+ ctx.isAction ||
140
+ state.cacheHit ||
141
+ ctx.request.method !== "GET"
142
+ ) {
143
+ if (ms) {
144
+ ms.metrics.push({ label: "pipeline:cache-store", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
145
+ }
146
+ return;
147
+ }
148
+
149
+ const {
150
+ createHandlerContext,
151
+ setupLoaderAccessSilent,
152
+ resolveAllSegments,
153
+ resolveInterceptEntry,
154
+ } = getRouterContext<TEnv>();
155
+
156
+ // Combine main segments with intercept segments
157
+ const allSegmentsToCache = [...allSegments, ...state.interceptSegments];
158
+
159
+ // Check if any non-loader segments have null components
160
+ // This happens when client already had those segments (partial navigation)
161
+ const hasNullComponents = allSegmentsToCache.some(
162
+ (s) => s.component === null && s.type !== "loader",
163
+ );
164
+
165
+ const requestCtx = getRequestContext();
166
+ if (!requestCtx) return;
167
+
168
+ const cacheScope = ctx.cacheScope;
169
+
170
+ // Register onResponse callback to skip caching for non-200 responses
171
+ // Note: error/notFound status codes are set elsewhere (not caching-specific)
172
+ requestCtx.onResponse((response) => {
173
+ // Only cache successful responses
174
+ if (response.status !== 200) {
175
+ console.log(
176
+ `[CacheStore] Skipping cache: non-200 status ${response.status} for ${ctx.pathname}`,
177
+ );
178
+ return response;
179
+ }
180
+
181
+ if (hasNullComponents) {
182
+ // Proactive caching: render all segments fresh in background
183
+ // This ensures cache has complete components for future requests
184
+ requestCtx.waitUntil(async () => {
185
+ console.log(
186
+ `[Router.matchPartial] Proactive caching: ${ctx.pathname} (rendering null-component segments)`,
187
+ );
188
+ try {
189
+ // Create fresh context for proactive caching
190
+ // This prevents handle data from polluting the response stream
191
+ const proactiveHandlerContext = createHandlerContext(
192
+ ctx.matched.params,
193
+ ctx.request,
194
+ ctx.url.searchParams,
195
+ ctx.pathname,
196
+ ctx.url,
197
+ ctx.bindings,
198
+ ctx.routeMap,
199
+ ctx.matched.routeKey
200
+ );
201
+ const proactiveLoaderPromises = new Map<string, Promise<any>>();
202
+
203
+ // Set up loader access that ignores handle pushes
204
+ setupLoaderAccessSilent(
205
+ proactiveHandlerContext,
206
+ proactiveLoaderPromises,
207
+ );
208
+
209
+ // Re-resolve ALL segments without revalidation
210
+ const Store = ctx.Store;
211
+ const freshSegments = await Store.run(() =>
212
+ resolveAllSegments(
213
+ ctx.entries,
214
+ ctx.routeKey,
215
+ ctx.matched.params,
216
+ proactiveHandlerContext,
217
+ proactiveLoaderPromises,
218
+ ),
219
+ );
220
+
221
+ // Also resolve intercept segments fresh if applicable
222
+ let freshInterceptSegments: ResolvedSegment[] = [];
223
+ if (ctx.interceptResult) {
224
+ freshInterceptSegments = await Store.run(() =>
225
+ resolveInterceptEntry(
226
+ ctx.interceptResult!.intercept,
227
+ ctx.interceptResult!.entry,
228
+ ctx.matched.params,
229
+ proactiveHandlerContext,
230
+ true, // belongsToRoute
231
+ // No revalidationContext = render fresh
232
+ ),
233
+ );
234
+ }
235
+
236
+ const completeSegments = [
237
+ ...freshSegments,
238
+ ...freshInterceptSegments,
239
+ ];
240
+ await cacheScope.cacheRoute(
241
+ ctx.pathname,
242
+ ctx.matched.params,
243
+ completeSegments,
244
+ ctx.isIntercept,
245
+ );
246
+ console.log(
247
+ `[Router.matchPartial] Proactive caching complete: ${ctx.pathname}`,
248
+ );
249
+ } catch (error) {
250
+ console.error(
251
+ `[Router.matchPartial] Proactive caching failed:`,
252
+ error,
253
+ );
254
+ }
255
+ });
256
+ } else {
257
+ // All segments have components - cache directly
258
+ // Schedule caching in waitUntil since cacheRoute is now async (key resolution)
259
+ requestCtx.waitUntil(async () => {
260
+ await cacheScope.cacheRoute(
261
+ ctx.pathname,
262
+ ctx.matched.params,
263
+ allSegmentsToCache,
264
+ ctx.isIntercept,
265
+ );
266
+ });
267
+ }
268
+
269
+ return response;
270
+ });
271
+
272
+ if (ms) {
273
+ ms.metrics.push({ label: "pipeline:cache-store", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
274
+ }
275
+ };
276
+ }