@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80

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 (312) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4960 -935
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/handler-use/SKILL.md +362 -0
  14. package/skills/hooks/SKILL.md +334 -72
  15. package/skills/host-router/SKILL.md +218 -0
  16. package/skills/intercept/SKILL.md +151 -8
  17. package/skills/layout/SKILL.md +122 -3
  18. package/skills/links/SKILL.md +92 -31
  19. package/skills/loader/SKILL.md +404 -44
  20. package/skills/middleware/SKILL.md +205 -37
  21. package/skills/migrate-nextjs/SKILL.md +560 -0
  22. package/skills/migrate-react-router/SKILL.md +764 -0
  23. package/skills/mime-routes/SKILL.md +128 -0
  24. package/skills/parallel/SKILL.md +263 -1
  25. package/skills/prerender/SKILL.md +685 -0
  26. package/skills/rango/SKILL.md +87 -16
  27. package/skills/response-routes/SKILL.md +411 -0
  28. package/skills/route/SKILL.md +281 -14
  29. package/skills/router-setup/SKILL.md +210 -32
  30. package/skills/tailwind/SKILL.md +129 -0
  31. package/skills/theme/SKILL.md +9 -8
  32. package/skills/typesafety/SKILL.md +328 -89
  33. package/skills/use-cache/SKILL.md +324 -0
  34. package/src/__internal.ts +102 -4
  35. package/src/bin/rango.ts +321 -0
  36. package/src/browser/action-coordinator.ts +97 -0
  37. package/src/browser/action-response-classifier.ts +99 -0
  38. package/src/browser/app-version.ts +14 -0
  39. package/src/browser/event-controller.ts +92 -64
  40. package/src/browser/history-state.ts +80 -0
  41. package/src/browser/intercept-utils.ts +52 -0
  42. package/src/browser/link-interceptor.ts +24 -4
  43. package/src/browser/logging.ts +55 -0
  44. package/src/browser/merge-segment-loaders.ts +20 -12
  45. package/src/browser/navigation-bridge.ts +317 -560
  46. package/src/browser/navigation-client.ts +206 -68
  47. package/src/browser/navigation-store.ts +73 -55
  48. package/src/browser/navigation-transaction.ts +297 -0
  49. package/src/browser/network-error-handler.ts +61 -0
  50. package/src/browser/partial-update.ts +343 -316
  51. package/src/browser/prefetch/cache.ts +216 -0
  52. package/src/browser/prefetch/fetch.ts +206 -0
  53. package/src/browser/prefetch/observer.ts +65 -0
  54. package/src/browser/prefetch/policy.ts +48 -0
  55. package/src/browser/prefetch/queue.ts +160 -0
  56. package/src/browser/prefetch/resource-ready.ts +77 -0
  57. package/src/browser/rango-state.ts +112 -0
  58. package/src/browser/react/Link.tsx +253 -74
  59. package/src/browser/react/NavigationProvider.tsx +87 -11
  60. package/src/browser/react/context.ts +11 -0
  61. package/src/browser/react/filter-segment-order.ts +11 -0
  62. package/src/browser/react/index.ts +12 -12
  63. package/src/browser/react/location-state-shared.ts +95 -53
  64. package/src/browser/react/location-state.ts +60 -15
  65. package/src/browser/react/mount-context.ts +6 -1
  66. package/src/browser/react/nonce-context.ts +23 -0
  67. package/src/browser/react/shallow-equal.ts +27 -0
  68. package/src/browser/react/use-action.ts +29 -51
  69. package/src/browser/react/use-client-cache.ts +5 -3
  70. package/src/browser/react/use-handle.ts +30 -126
  71. package/src/browser/react/use-href.tsx +2 -2
  72. package/src/browser/react/use-link-status.ts +6 -5
  73. package/src/browser/react/use-navigation.ts +44 -65
  74. package/src/browser/react/use-params.ts +65 -0
  75. package/src/browser/react/use-pathname.ts +47 -0
  76. package/src/browser/react/use-router.ts +76 -0
  77. package/src/browser/react/use-search-params.ts +56 -0
  78. package/src/browser/react/use-segments.ts +80 -97
  79. package/src/browser/response-adapter.ts +73 -0
  80. package/src/browser/rsc-router.tsx +214 -58
  81. package/src/browser/scroll-restoration.ts +127 -52
  82. package/src/browser/segment-reconciler.ts +243 -0
  83. package/src/browser/segment-structure-assert.ts +16 -0
  84. package/src/browser/server-action-bridge.ts +510 -603
  85. package/src/browser/shallow.ts +6 -1
  86. package/src/browser/types.ts +141 -48
  87. package/src/browser/validate-redirect-origin.ts +29 -0
  88. package/src/build/generate-manifest.ts +235 -24
  89. package/src/build/generate-route-types.ts +39 -0
  90. package/src/build/index.ts +13 -0
  91. package/src/build/route-trie.ts +291 -0
  92. package/src/build/route-types/ast-helpers.ts +25 -0
  93. package/src/build/route-types/ast-route-extraction.ts +98 -0
  94. package/src/build/route-types/codegen.ts +102 -0
  95. package/src/build/route-types/include-resolution.ts +418 -0
  96. package/src/build/route-types/param-extraction.ts +48 -0
  97. package/src/build/route-types/per-module-writer.ts +128 -0
  98. package/src/build/route-types/router-processing.ts +618 -0
  99. package/src/build/route-types/scan-filter.ts +85 -0
  100. package/src/build/runtime-discovery.ts +231 -0
  101. package/src/cache/background-task.ts +34 -0
  102. package/src/cache/cache-key-utils.ts +44 -0
  103. package/src/cache/cache-policy.ts +125 -0
  104. package/src/cache/cache-runtime.ts +342 -0
  105. package/src/cache/cache-scope.ts +167 -309
  106. package/src/cache/cf/cf-cache-store.ts +571 -17
  107. package/src/cache/cf/index.ts +13 -3
  108. package/src/cache/document-cache.ts +116 -77
  109. package/src/cache/handle-capture.ts +81 -0
  110. package/src/cache/handle-snapshot.ts +41 -0
  111. package/src/cache/index.ts +1 -15
  112. package/src/cache/memory-segment-store.ts +191 -13
  113. package/src/cache/profile-registry.ts +73 -0
  114. package/src/cache/read-through-swr.ts +134 -0
  115. package/src/cache/segment-codec.ts +256 -0
  116. package/src/cache/taint.ts +153 -0
  117. package/src/cache/types.ts +72 -122
  118. package/src/client.rsc.tsx +3 -1
  119. package/src/client.tsx +135 -301
  120. package/src/component-utils.ts +4 -4
  121. package/src/components/DefaultDocument.tsx +5 -1
  122. package/src/context-var.ts +156 -0
  123. package/src/debug.ts +19 -9
  124. package/src/errors.ts +108 -2
  125. package/src/handle.ts +55 -29
  126. package/src/handles/MetaTags.tsx +73 -20
  127. package/src/handles/breadcrumbs.ts +66 -0
  128. package/src/handles/index.ts +1 -0
  129. package/src/handles/meta.ts +30 -13
  130. package/src/host/cookie-handler.ts +21 -15
  131. package/src/host/errors.ts +8 -8
  132. package/src/host/index.ts +4 -7
  133. package/src/host/pattern-matcher.ts +27 -27
  134. package/src/host/router.ts +61 -39
  135. package/src/host/testing.ts +8 -8
  136. package/src/host/types.ts +15 -7
  137. package/src/host/utils.ts +1 -1
  138. package/src/href-client.ts +119 -29
  139. package/src/index.rsc.ts +155 -19
  140. package/src/index.ts +251 -30
  141. package/src/internal-debug.ts +11 -0
  142. package/src/loader.rsc.ts +26 -157
  143. package/src/loader.ts +27 -10
  144. package/src/network-error-thrower.tsx +3 -1
  145. package/src/outlet-provider.tsx +45 -0
  146. package/src/prerender/param-hash.ts +37 -0
  147. package/src/prerender/store.ts +186 -0
  148. package/src/prerender.ts +524 -0
  149. package/src/reverse.ts +354 -0
  150. package/src/root-error-boundary.tsx +41 -29
  151. package/src/route-content-wrapper.tsx +7 -4
  152. package/src/route-definition/dsl-helpers.ts +1121 -0
  153. package/src/route-definition/helper-factories.ts +200 -0
  154. package/src/route-definition/helpers-types.ts +478 -0
  155. package/src/route-definition/index.ts +55 -0
  156. package/src/route-definition/redirect.ts +101 -0
  157. package/src/route-definition/resolve-handler-use.ts +149 -0
  158. package/src/route-definition.ts +1 -1428
  159. package/src/route-map-builder.ts +217 -123
  160. package/src/route-name.ts +53 -0
  161. package/src/route-types.ts +77 -8
  162. package/src/router/content-negotiation.ts +215 -0
  163. package/src/router/debug-manifest.ts +72 -0
  164. package/src/router/error-handling.ts +9 -9
  165. package/src/router/find-match.ts +160 -0
  166. package/src/router/handler-context.ts +438 -86
  167. package/src/router/intercept-resolution.ts +402 -0
  168. package/src/router/lazy-includes.ts +237 -0
  169. package/src/router/loader-resolution.ts +356 -128
  170. package/src/router/logging.ts +251 -0
  171. package/src/router/manifest.ts +163 -35
  172. package/src/router/match-api.ts +555 -0
  173. package/src/router/match-context.ts +5 -3
  174. package/src/router/match-handlers.ts +440 -0
  175. package/src/router/match-middleware/background-revalidation.ts +108 -93
  176. package/src/router/match-middleware/cache-lookup.ts +460 -10
  177. package/src/router/match-middleware/cache-store.ts +98 -26
  178. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  179. package/src/router/match-middleware/segment-resolution.ts +80 -6
  180. package/src/router/match-pipelines.ts +10 -45
  181. package/src/router/match-result.ts +135 -35
  182. package/src/router/metrics.ts +240 -15
  183. package/src/router/middleware-cookies.ts +55 -0
  184. package/src/router/middleware-types.ts +220 -0
  185. package/src/router/middleware.ts +324 -369
  186. package/src/router/navigation-snapshot.ts +182 -0
  187. package/src/router/pattern-matching.ts +211 -43
  188. package/src/router/prerender-match.ts +502 -0
  189. package/src/router/preview-match.ts +98 -0
  190. package/src/router/request-classification.ts +310 -0
  191. package/src/router/revalidation.ts +137 -38
  192. package/src/router/route-snapshot.ts +245 -0
  193. package/src/router/router-context.ts +41 -21
  194. package/src/router/router-interfaces.ts +484 -0
  195. package/src/router/router-options.ts +618 -0
  196. package/src/router/router-registry.ts +24 -0
  197. package/src/router/segment-resolution/fresh.ts +748 -0
  198. package/src/router/segment-resolution/helpers.ts +268 -0
  199. package/src/router/segment-resolution/loader-cache.ts +199 -0
  200. package/src/router/segment-resolution/revalidation.ts +1379 -0
  201. package/src/router/segment-resolution/static-store.ts +67 -0
  202. package/src/router/segment-resolution.ts +21 -0
  203. package/src/router/segment-wrappers.ts +291 -0
  204. package/src/router/telemetry-otel.ts +299 -0
  205. package/src/router/telemetry.ts +300 -0
  206. package/src/router/timeout.ts +148 -0
  207. package/src/router/trie-matching.ts +239 -0
  208. package/src/router/types.ts +78 -3
  209. package/src/router.ts +740 -4252
  210. package/src/rsc/handler-context.ts +45 -0
  211. package/src/rsc/handler.ts +907 -797
  212. package/src/rsc/helpers.ts +140 -6
  213. package/src/rsc/index.ts +0 -20
  214. package/src/rsc/loader-fetch.ts +229 -0
  215. package/src/rsc/manifest-init.ts +90 -0
  216. package/src/rsc/nonce.ts +14 -0
  217. package/src/rsc/origin-guard.ts +141 -0
  218. package/src/rsc/progressive-enhancement.ts +391 -0
  219. package/src/rsc/response-error.ts +37 -0
  220. package/src/rsc/response-route-handler.ts +347 -0
  221. package/src/rsc/rsc-rendering.ts +246 -0
  222. package/src/rsc/runtime-warnings.ts +42 -0
  223. package/src/rsc/server-action.ts +356 -0
  224. package/src/rsc/ssr-setup.ts +128 -0
  225. package/src/rsc/types.ts +46 -11
  226. package/src/search-params.ts +230 -0
  227. package/src/segment-content-promise.ts +67 -0
  228. package/src/segment-loader-promise.ts +122 -0
  229. package/src/segment-system.tsx +134 -36
  230. package/src/server/context.ts +341 -61
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +113 -15
  234. package/src/server/loader-registry.ts +24 -64
  235. package/src/server/request-context.ts +607 -81
  236. package/src/server.ts +35 -130
  237. package/src/ssr/index.tsx +103 -30
  238. package/src/static-handler.ts +126 -0
  239. package/src/theme/ThemeProvider.tsx +21 -15
  240. package/src/theme/ThemeScript.tsx +5 -5
  241. package/src/theme/constants.ts +5 -2
  242. package/src/theme/index.ts +4 -14
  243. package/src/theme/theme-context.ts +4 -30
  244. package/src/theme/theme-script.ts +21 -18
  245. package/src/types/boundaries.ts +158 -0
  246. package/src/types/cache-types.ts +198 -0
  247. package/src/types/error-types.ts +192 -0
  248. package/src/types/global-namespace.ts +100 -0
  249. package/src/types/handler-context.ts +791 -0
  250. package/src/types/index.ts +88 -0
  251. package/src/types/loader-types.ts +210 -0
  252. package/src/types/route-config.ts +170 -0
  253. package/src/types/route-entry.ts +120 -0
  254. package/src/types/segments.ts +150 -0
  255. package/src/types.ts +1 -1623
  256. package/src/urls/include-helper.ts +207 -0
  257. package/src/urls/index.ts +53 -0
  258. package/src/urls/path-helper-types.ts +372 -0
  259. package/src/urls/path-helper.ts +364 -0
  260. package/src/urls/pattern-types.ts +107 -0
  261. package/src/urls/response-types.ts +116 -0
  262. package/src/urls/type-extraction.ts +372 -0
  263. package/src/urls/urls-function.ts +98 -0
  264. package/src/urls.ts +1 -802
  265. package/src/use-loader.tsx +161 -81
  266. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  267. package/src/vite/discovery/discover-routers.ts +348 -0
  268. package/src/vite/discovery/prerender-collection.ts +439 -0
  269. package/src/vite/discovery/route-types-writer.ts +258 -0
  270. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  271. package/src/vite/discovery/state.ts +117 -0
  272. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  273. package/src/vite/index.ts +15 -1133
  274. package/src/vite/plugin-types.ts +103 -0
  275. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  276. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  277. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  278. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  279. package/src/vite/plugins/expose-id-utils.ts +299 -0
  280. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  281. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  282. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  283. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  284. package/src/vite/plugins/expose-ids/types.ts +45 -0
  285. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  286. package/src/vite/plugins/performance-tracks.ts +88 -0
  287. package/src/vite/plugins/refresh-cmd.ts +127 -0
  288. package/src/vite/plugins/use-cache-transform.ts +323 -0
  289. package/src/vite/plugins/version-injector.ts +83 -0
  290. package/src/vite/plugins/version-plugin.ts +266 -0
  291. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  292. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  293. package/src/vite/rango.ts +462 -0
  294. package/src/vite/router-discovery.ts +918 -0
  295. package/src/vite/utils/ast-handler-extract.ts +517 -0
  296. package/src/vite/utils/banner.ts +36 -0
  297. package/src/vite/utils/bundle-analysis.ts +137 -0
  298. package/src/vite/utils/manifest-utils.ts +70 -0
  299. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  300. package/src/vite/utils/prerender-utils.ts +221 -0
  301. package/src/vite/utils/shared-utils.ts +170 -0
  302. package/CLAUDE.md +0 -43
  303. package/src/browser/lru-cache.ts +0 -69
  304. package/src/browser/request-controller.ts +0 -164
  305. package/src/cache/memory-store.ts +0 -253
  306. package/src/href-context.ts +0 -33
  307. package/src/href.ts +0 -255
  308. package/src/server/route-manifest-cache.ts +0 -173
  309. package/src/vite/expose-handle-id.ts +0 -209
  310. package/src/vite/expose-loader-id.ts +0 -426
  311. package/src/vite/expose-location-state-id.ts +0 -177
  312. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -104,6 +104,8 @@ import type { ResolvedSegment } from "../../types.js";
104
104
  import { getRequestContext } from "../../server/request-context.js";
105
105
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
106
106
  import { getRouterContext } from "../router-context.js";
107
+ import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
108
+ import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
107
109
  import type { GeneratorMiddleware } from "./cache-lookup.js";
108
110
 
109
111
  /**
@@ -119,6 +121,8 @@ export function withCacheStore<TEnv>(
119
121
  return async function* (
120
122
  source: AsyncGenerator<ResolvedSegment>,
121
123
  ): AsyncGenerator<ResolvedSegment> {
124
+ const ms = ctx.metricsStore;
125
+
122
126
  // Collect all segments while passing them through
123
127
  const allSegments: ResolvedSegment[] = [];
124
128
  for await (const segment of source) {
@@ -126,6 +130,9 @@ export function withCacheStore<TEnv>(
126
130
  yield segment;
127
131
  }
128
132
 
133
+ // Measure own work only (after source iteration completes)
134
+ const ownStart = performance.now();
135
+
129
136
  // Skip caching if:
130
137
  // 1. Cache miss but cache scope is disabled
131
138
  // 2. This is an action (actions don't cache)
@@ -137,38 +144,54 @@ export function withCacheStore<TEnv>(
137
144
  state.cacheHit ||
138
145
  ctx.request.method !== "GET"
139
146
  ) {
147
+ if (ms) {
148
+ ms.metrics.push({
149
+ label: "pipeline:cache-store",
150
+ duration: performance.now() - ownStart,
151
+ startTime: ownStart - ms.requestStart,
152
+ });
153
+ }
140
154
  return;
141
155
  }
142
156
 
143
157
  const {
144
158
  createHandlerContext,
145
- setupLoaderAccessSilent,
159
+ setupLoaderAccess,
146
160
  resolveAllSegments,
147
161
  resolveInterceptEntry,
162
+ createHandleStore,
148
163
  } = getRouterContext<TEnv>();
149
164
 
150
165
  // Combine main segments with intercept segments
151
166
  const allSegmentsToCache = [...allSegments, ...state.interceptSegments];
152
167
 
153
- // Check if any non-loader segments have null components
154
- // This happens when client already had those segments (partial navigation)
168
+ // Check if any non-loader segments have null components from revalidation
169
+ // skip (client already had them). Segments where the handler intentionally
170
+ // returned null are not revalidation skips — re-rendering them will still
171
+ // produce null, so proactive caching would be wasted work.
172
+ const clientIdSet = new Set(ctx.clientSegmentIds);
155
173
  const hasNullComponents = allSegmentsToCache.some(
156
- (s) => s.component === null && s.type !== "loader",
174
+ (s) =>
175
+ s.component === null && s.type !== "loader" && clientIdSet.has(s.id),
157
176
  );
158
177
 
159
178
  const requestCtx = getRequestContext();
160
179
  if (!requestCtx) return;
161
180
 
162
181
  const cacheScope = ctx.cacheScope;
182
+ const reqId = INTERNAL_RANGO_DEBUG
183
+ ? getOrCreateRequestId(ctx.request)
184
+ : undefined;
163
185
 
164
186
  // Register onResponse callback to skip caching for non-200 responses
165
187
  // Note: error/notFound status codes are set elsewhere (not caching-specific)
166
188
  requestCtx.onResponse((response) => {
167
189
  // Only cache successful responses
168
190
  if (response.status !== 200) {
169
- console.log(
170
- `[CacheStore] Skipping cache: non-200 status ${response.status} for ${ctx.pathname}`,
171
- );
191
+ debugLog("cacheStore", "skipping cache for non-200 response", {
192
+ status: response.status,
193
+ pathname: ctx.pathname,
194
+ });
172
195
  return response;
173
196
  }
174
197
 
@@ -176,31 +199,43 @@ export function withCacheStore<TEnv>(
176
199
  // Proactive caching: render all segments fresh in background
177
200
  // This ensures cache has complete components for future requests
178
201
  requestCtx.waitUntil(async () => {
179
- console.log(
180
- `[Router.matchPartial] Proactive caching: ${ctx.pathname} (rendering null-component segments)`,
181
- );
202
+ // Prevent background metrics from polluting foreground timeline.
203
+ const savedMetrics = ctx.Store.metrics;
204
+ ctx.Store.metrics = undefined;
205
+
206
+ const start = performance.now();
207
+ debugLog("cacheStore", "proactive caching started", {
208
+ pathname: ctx.pathname,
209
+ });
210
+ // Swap to a fresh HandleStore so handle.push() calls from
211
+ // proactive resolution are captured (not silenced). The original
212
+ // store's stream is already sent by waitUntil time.
213
+ // cacheRoute reads from requestCtx._handleStore, so this ensures
214
+ // complete handle data (e.g. breadcrumbs) is cached.
215
+ const originalHandleStore = requestCtx._handleStore;
216
+ requestCtx._handleStore = createHandleStore();
182
217
  try {
183
218
  // Create fresh context for proactive caching
184
- // This prevents handle data from polluting the response stream
185
219
  const proactiveHandlerContext = createHandlerContext(
186
220
  ctx.matched.params,
187
221
  ctx.request,
188
222
  ctx.url.searchParams,
189
223
  ctx.pathname,
190
224
  ctx.url,
191
- ctx.bindings,
225
+ ctx.env,
192
226
  ctx.routeMap,
193
- ctx.matched.routeKey
227
+ ctx.matched.routeKey,
228
+ ctx.matched.responseType,
229
+ ctx.matched.pt === true,
194
230
  );
195
231
  const proactiveLoaderPromises = new Map<string, Promise<any>>();
196
232
 
197
- // Set up loader access that ignores handle pushes
198
- setupLoaderAccessSilent(
199
- proactiveHandlerContext,
200
- proactiveLoaderPromises,
201
- );
233
+ // Use normal loader access so handle data is captured
234
+ setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
202
235
 
203
- // Re-resolve ALL segments without revalidation
236
+ // Re-resolve ALL segments without revalidation.
237
+ // Skip DSL loaders — they are never cached (cacheRoute filters them)
238
+ // and are always resolved fresh on each request.
204
239
  const Store = ctx.Store;
205
240
  const freshSegments = await Store.run(() =>
206
241
  resolveAllSegments(
@@ -209,6 +244,7 @@ export function withCacheStore<TEnv>(
209
244
  ctx.matched.params,
210
245
  proactiveHandlerContext,
211
246
  proactiveLoaderPromises,
247
+ { skipLoaders: true },
212
248
  ),
213
249
  );
214
250
 
@@ -231,36 +267,72 @@ export function withCacheStore<TEnv>(
231
267
  ...freshSegments,
232
268
  ...freshInterceptSegments,
233
269
  ];
270
+ requestCtx._handleStore.seal();
234
271
  await cacheScope.cacheRoute(
235
272
  ctx.pathname,
236
273
  ctx.matched.params,
237
274
  completeSegments,
238
275
  ctx.isIntercept,
239
276
  );
240
- console.log(
241
- `[Router.matchPartial] Proactive caching complete: ${ctx.pathname}`,
242
- );
277
+ if (INTERNAL_RANGO_DEBUG) {
278
+ const dur = performance.now() - start;
279
+ console.log(
280
+ `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
281
+ );
282
+ }
283
+ debugLog("cacheStore", "proactive caching complete", {
284
+ pathname: ctx.pathname,
285
+ });
243
286
  } catch (error) {
244
- console.error(
245
- `[Router.matchPartial] Proactive caching failed:`,
246
- error,
247
- );
287
+ if (INTERNAL_RANGO_DEBUG) {
288
+ const dur = performance.now() - start;
289
+ console.log(
290
+ `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} FAILED (${dur.toFixed(2)}ms) error=${String(error)}`,
291
+ );
292
+ }
293
+ debugWarn("cacheStore", "proactive caching failed", {
294
+ pathname: ctx.pathname,
295
+ error: String(error),
296
+ });
297
+ } finally {
298
+ requestCtx._handleStore = originalHandleStore;
299
+ ctx.Store.metrics = savedMetrics;
248
300
  }
249
301
  });
250
302
  } else {
251
303
  // All segments have components - cache directly
252
304
  // Schedule caching in waitUntil since cacheRoute is now async (key resolution)
305
+ if (INTERNAL_RANGO_DEBUG) {
306
+ console.log(
307
+ `[RSC CacheStore][req:${reqId}] Direct cache path: scheduling cacheRoute for ${ctx.pathname} (${allSegmentsToCache.length} segments, hasNullComponents=${hasNullComponents})`,
308
+ );
309
+ }
253
310
  requestCtx.waitUntil(async () => {
311
+ const start = performance.now();
254
312
  await cacheScope.cacheRoute(
255
313
  ctx.pathname,
256
314
  ctx.matched.params,
257
315
  allSegmentsToCache,
258
316
  ctx.isIntercept,
259
317
  );
318
+ if (INTERNAL_RANGO_DEBUG) {
319
+ const dur = performance.now() - start;
320
+ console.log(
321
+ `[RSC Background][req:${reqId}] Cache store ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${allSegmentsToCache.length}`,
322
+ );
323
+ }
260
324
  });
261
325
  }
262
326
 
263
327
  return response;
264
328
  });
329
+
330
+ if (ms) {
331
+ ms.metrics.push({
332
+ label: "pipeline:cache-store",
333
+ duration: performance.now() - ownStart,
334
+ startTime: ownStart - ms.requestStart,
335
+ });
336
+ }
265
337
  };
266
338
  }
@@ -105,6 +105,7 @@ import type { ResolvedSegment } from "../../types.js";
105
105
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
106
106
  import { getRouterContext } from "../router-context.js";
107
107
  import type { GeneratorMiddleware } from "./cache-lookup.js";
108
+ import { debugLog } from "../logging.js";
108
109
 
109
110
  /**
110
111
  * Creates intercept resolution middleware
@@ -117,11 +118,13 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
117
118
  */
118
119
  export function withInterceptResolution<TEnv>(
119
120
  ctx: MatchContext<TEnv>,
120
- state: MatchPipelineState
121
+ state: MatchPipelineState,
121
122
  ): GeneratorMiddleware<ResolvedSegment> {
122
123
  return async function* (
123
- source: AsyncGenerator<ResolvedSegment>
124
+ source: AsyncGenerator<ResolvedSegment>,
124
125
  ): AsyncGenerator<ResolvedSegment> {
126
+ const ms = ctx.metricsStore;
127
+
125
128
  // First, yield all segments from the source (main segment resolution or cache)
126
129
  const segments: ResolvedSegment[] = [];
127
130
  for await (const segment of source) {
@@ -129,8 +132,18 @@ export function withInterceptResolution<TEnv>(
129
132
  yield segment;
130
133
  }
131
134
 
135
+ // Measure own work only (after source iteration completes)
136
+ const ownStart = performance.now();
137
+
132
138
  // Skip intercept resolution for full match (document requests don't have intercepts)
133
139
  if (ctx.isFullMatch) {
140
+ if (ms) {
141
+ ms.metrics.push({
142
+ label: "pipeline:intercept",
143
+ duration: performance.now() - ownStart,
144
+ startTime: ownStart - ms.requestStart,
145
+ });
146
+ }
134
147
  return;
135
148
  }
136
149
 
@@ -149,6 +162,13 @@ export function withInterceptResolution<TEnv>(
149
162
  if (ctx.interceptResult && state.cacheHit && ctx.isIntercept) {
150
163
  await handleCacheHitIntercept(ctx, state, segments);
151
164
  }
165
+ if (ms) {
166
+ ms.metrics.push({
167
+ label: "pipeline:intercept",
168
+ duration: performance.now() - ownStart,
169
+ startTime: ownStart - ms.requestStart,
170
+ });
171
+ }
152
172
  return;
153
173
  }
154
174
 
@@ -156,9 +176,10 @@ export function withInterceptResolution<TEnv>(
156
176
  const { resolveInterceptEntry } = getRouterContext<TEnv>();
157
177
 
158
178
  const slotName = ctx.interceptResult!.intercept.slotName;
159
- console.log(
160
- `[Router.matchPartial] Found intercept for "${ctx.localRouteName}" -> slot "${slotName}"`
161
- );
179
+ debugLog("matchPartial.intercept", "intercept resolved", {
180
+ routeName: ctx.localRouteName,
181
+ slotName,
182
+ });
162
183
 
163
184
  // Resolve intercept entry (middleware, loaders, handler)
164
185
  const Store = ctx.Store;
@@ -178,8 +199,8 @@ export function withInterceptResolution<TEnv>(
178
199
  routeKey: ctx.routeKey,
179
200
  actionContext: ctx.actionContext,
180
201
  stale: ctx.stale,
181
- }
182
- )
202
+ },
203
+ ),
183
204
  );
184
205
 
185
206
  // Update state
@@ -193,6 +214,14 @@ export function withInterceptResolution<TEnv>(
193
214
  for (const segment of interceptSegments) {
194
215
  yield segment;
195
216
  }
217
+
218
+ if (ms) {
219
+ ms.metrics.push({
220
+ label: "pipeline:intercept",
221
+ duration: performance.now() - ownStart,
222
+ startTime: ownStart - ms.requestStart,
223
+ });
224
+ }
196
225
  };
197
226
  }
198
227
 
@@ -204,7 +233,7 @@ export function withInterceptResolution<TEnv>(
204
233
  async function handleCacheHitIntercept<TEnv>(
205
234
  ctx: MatchContext<TEnv>,
206
235
  state: MatchPipelineState,
207
- segments: ResolvedSegment[]
236
+ segments: ResolvedSegment[],
208
237
  ): Promise<void> {
209
238
  if (!ctx.interceptResult) return;
210
239
 
@@ -214,7 +243,7 @@ async function handleCacheHitIntercept<TEnv>(
214
243
 
215
244
  // Find intercept segments from cached segments (namespace starts with "intercept:")
216
245
  const interceptSegments = segments.filter((s) =>
217
- s.namespace?.startsWith("intercept:")
246
+ s.namespace?.startsWith("intercept:"),
218
247
  );
219
248
  state.interceptSegments = interceptSegments;
220
249
 
@@ -238,25 +267,36 @@ async function handleCacheHitIntercept<TEnv>(
238
267
  routeKey: ctx.routeKey,
239
268
  actionContext: ctx.actionContext,
240
269
  stale: ctx.stale,
241
- }
242
- )
270
+ },
271
+ ),
243
272
  );
244
273
 
245
274
  // Update intercept segment's loaderDataPromise with fresh data
246
275
  if (freshLoaderResult) {
247
276
  const interceptMainSegment = interceptSegments.find(
248
- (s) => s.type === "parallel" && s.slot
277
+ (s) => s.type === "parallel" && s.slot,
249
278
  );
250
279
  if (interceptMainSegment) {
251
- interceptMainSegment.loaderDataPromise = freshLoaderResult.loaderDataPromise;
280
+ interceptMainSegment.loaderDataPromise =
281
+ freshLoaderResult.loaderDataPromise;
252
282
  interceptMainSegment.loaderIds = freshLoaderResult.loaderIds;
253
- console.log(
254
- `[Router.matchPartial] Cache HIT + fresh loaders for intercept "${ctx.localRouteName}" -> slot "${slotName}"`
283
+ debugLog(
284
+ "matchPartial.intercept",
285
+ "cache hit with fresh intercept loaders",
286
+ {
287
+ routeName: ctx.localRouteName,
288
+ slotName,
289
+ },
255
290
  );
256
291
  }
257
292
  } else {
258
- console.log(
259
- `[Router.matchPartial] Cache HIT for intercept "${ctx.localRouteName}" -> slot "${slotName}" (no loader revalidation)`
293
+ debugLog(
294
+ "matchPartial.intercept",
295
+ "cache hit without intercept loader revalidation",
296
+ {
297
+ routeName: ctx.localRouteName,
298
+ slotName,
299
+ },
260
300
  );
261
301
  }
262
302
  }
@@ -87,10 +87,49 @@
87
87
  * if (state.cacheHit) return; // Now we can check
88
88
  */
89
89
  import type { ResolvedSegment } from "../../types.js";
90
+ import type { EntryData } from "../../server/context.js";
91
+ import { _getRequestContext } from "../../server/request-context.js";
90
92
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
91
93
  import { getRouterContext } from "../router-context.js";
92
94
  import type { GeneratorMiddleware } from "./cache-lookup.js";
93
95
 
96
+ /**
97
+ * Check whether any entry in the tree uses loading() (streaming).
98
+ * Matches the router's streaming semantics in fresh.ts: streaming is
99
+ * enabled when `loading` is defined AND not `false`. `loading: false`
100
+ * explicitly disables streaming; `undefined` means no loading at all.
101
+ */
102
+ export function treeHasStreaming(entries: EntryData[]): boolean {
103
+ for (const entry of entries) {
104
+ if (
105
+ "loading" in entry &&
106
+ entry.loading !== undefined &&
107
+ entry.loading !== false
108
+ )
109
+ return true;
110
+ if (entry.layout) {
111
+ if (treeHasStreaming(entry.layout)) return true;
112
+ }
113
+ if (entry.parallel) {
114
+ for (const key in entry.parallel) {
115
+ const parallelEntry = entry.parallel[key as `@${string}`];
116
+ if (parallelEntry) {
117
+ if (
118
+ "loading" in parallelEntry &&
119
+ parallelEntry.loading !== undefined &&
120
+ parallelEntry.loading !== false
121
+ )
122
+ return true;
123
+ if (parallelEntry.layout) {
124
+ if (treeHasStreaming(parallelEntry.layout)) return true;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return false;
131
+ }
132
+
94
133
  /**
95
134
  * Creates segment resolution middleware
96
135
  *
@@ -99,22 +138,40 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
99
138
  */
100
139
  export function withSegmentResolution<TEnv>(
101
140
  ctx: MatchContext<TEnv>,
102
- state: MatchPipelineState
141
+ state: MatchPipelineState,
103
142
  ): GeneratorMiddleware<ResolvedSegment> {
104
143
  return async function* (
105
- source: AsyncGenerator<ResolvedSegment>
144
+ source: AsyncGenerator<ResolvedSegment>,
106
145
  ): AsyncGenerator<ResolvedSegment> {
146
+ const ms = ctx.metricsStore;
147
+
107
148
  // IMPORTANT: Always iterate source first to give cache-lookup a chance
108
149
  // to run and set state.cacheHit. Without this, cache-lookup never executes!
109
150
  for await (const segment of source) {
110
151
  yield segment;
111
152
  }
112
153
 
154
+ // Measure own work only (after source iteration completes)
155
+ const ownStart = performance.now();
156
+
113
157
  // If cache hit, segments were already yielded by cache lookup
158
+ // (render barrier is resolved on the cache-hit path)
114
159
  if (state.cacheHit) {
160
+ if (ms) {
161
+ ms.metrics.push({
162
+ label: "pipeline:segment-resolve",
163
+ duration: performance.now() - ownStart,
164
+ startTime: ownStart - ms.requestStart,
165
+ });
166
+ }
115
167
  return;
116
168
  }
117
169
 
170
+ const reqCtx = _getRequestContext();
171
+ if (reqCtx && reqCtx._treeHasStreaming === undefined) {
172
+ reqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
173
+ }
174
+
118
175
  const { resolveAllSegmentsWithRevalidation, resolveAllSegments } =
119
176
  getRouterContext<TEnv>();
120
177
 
@@ -128,14 +185,18 @@ export function withSegmentResolution<TEnv>(
128
185
  ctx.routeKey,
129
186
  ctx.matched.params,
130
187
  ctx.handlerContext,
131
- ctx.loaderPromises
132
- )
188
+ ctx.loaderPromises,
189
+ ),
133
190
  );
134
191
 
135
192
  // Update state with resolved segments
136
193
  state.segments = segments;
137
194
  state.matchedIds = segments.map((s: { id: string }) => s.id);
138
195
 
196
+ if (reqCtx) {
197
+ reqCtx._resolveRenderBarrier(segments);
198
+ }
199
+
139
200
  // Yield all resolved segments
140
201
  for (const segment of segments) {
141
202
  yield segment;
@@ -157,18 +218,31 @@ export function withSegmentResolution<TEnv>(
157
218
  ctx.actionContext,
158
219
  ctx.interceptResult,
159
220
  ctx.localRouteName,
160
- ctx.pathname
161
- )
221
+ ctx.pathname,
222
+ ctx.stale,
223
+ ),
162
224
  );
163
225
 
164
226
  // Update state with resolved segments
165
227
  state.segments = result.segments;
166
228
  state.matchedIds = result.matchedIds;
167
229
 
230
+ if (reqCtx) {
231
+ reqCtx._resolveRenderBarrier(result.segments);
232
+ }
233
+
168
234
  // Yield all resolved segments
169
235
  for (const segment of result.segments) {
170
236
  yield segment;
171
237
  }
172
238
  }
239
+
240
+ if (ms) {
241
+ ms.metrics.push({
242
+ label: "pipeline:segment-resolve",
243
+ duration: performance.now() - ownStart,
244
+ startTime: ownStart - ms.requestStart,
245
+ });
246
+ }
173
247
  };
174
248
  }
@@ -86,19 +86,14 @@
86
86
  * -> output: cached segments + fresh loader data
87
87
  *
88
88
  *
89
- * TWO PIPELINE VARIANTS
90
- * =====================
91
- *
92
- * 1. createMatchPipeline (Full Match)
93
- * - Used for document requests (initial page load)
94
- * - No revalidation logic (no previous state to compare)
95
- * - Simpler segment resolution
96
- *
97
- * 2. createMatchPartialPipeline (Partial Match)
98
- * - Used for client-side navigation
99
- * - Includes revalidation for SWR
100
- * - Compares with previous params/URL
101
- * - Supports intercepts (soft navigation modals)
89
+ * PIPELINE VARIANT
90
+ * ================
91
+ *
92
+ * createMatchPartialPipeline handles both full (document) and partial
93
+ * (navigation) requests. The middleware steps adapt based on ctx.isFullMatch:
94
+ * - cache-lookup/store work for both
95
+ * - background-revalidation is a no-op for full matches (no stale state)
96
+ * - intercept-resolution is a no-op for full matches (no previous navigation)
102
97
  */
103
98
  import type { ResolvedSegment } from "../types.js";
104
99
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
@@ -163,7 +158,7 @@ export async function* empty<T>(): AsyncGenerator<T> {
163
158
  */
164
159
  export function createMatchPartialPipeline<TEnv>(
165
160
  ctx: MatchContext<TEnv>,
166
- state: MatchPipelineState
161
+ state: MatchPipelineState,
167
162
  ): AsyncGenerator<ResolvedSegment> {
168
163
  // Build the middleware chain
169
164
  const pipeline = compose<ResolvedSegment>(
@@ -176,39 +171,9 @@ export function createMatchPartialPipeline<TEnv>(
176
171
  // Resolves segments on cache miss
177
172
  withSegmentResolution(ctx, state),
178
173
  // Innermost - checks cache first
179
- withCacheLookup(ctx, state)
174
+ withCacheLookup(ctx, state),
180
175
  );
181
176
 
182
177
  // Start with empty source - cache lookup or segment resolution will produce segments
183
178
  return pipeline(empty());
184
179
  }
185
-
186
- /**
187
- * Create the full match pipeline (simpler, no revalidation)
188
- *
189
- * Used for document requests (initial page load) where we don't need
190
- * revalidation logic since there's no previous state to compare against.
191
- */
192
- export function createMatchPipeline<TEnv>(
193
- ctx: MatchContext<TEnv>,
194
- state: MatchPipelineState
195
- ): AsyncGenerator<ResolvedSegment> {
196
- // For full match, we only need:
197
- // 1. Cache lookup
198
- // 2. Segment resolution (without revalidation)
199
- // 3. Intercept resolution
200
- // 4. Cache store
201
-
202
- // Note: Full match uses different resolution logic (resolveAllSegments instead of
203
- // resolveAllSegmentsWithRevalidation). This will be handled by the segment resolution
204
- // middleware checking ctx.isFullMatch or similar flag.
205
-
206
- const pipeline = compose<ResolvedSegment>(
207
- withCacheStore(ctx, state),
208
- withInterceptResolution(ctx, state),
209
- withSegmentResolution(ctx, state),
210
- withCacheLookup(ctx, state)
211
- );
212
-
213
- return pipeline(empty());
214
- }