@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +120 -204
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +190 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +63 -24
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +338 -126
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -1,139 +1,327 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useContext, useEffect, useRef, useState } from "react";
3
+ import {
4
+ isValidElement,
5
+ startTransition,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ type ReactNode,
13
+ } from "react";
4
14
  import { OutletContext, type OutletContextValue } from "./outlet-context.js";
15
+ import { loaderStore, type LoaderEntry } from "./loader-store.js";
5
16
  import type { LoaderDefinition, LoadOptions } from "./types.js";
6
17
 
7
- /**
8
- * Payload returned by loader RSC requests
9
- */
18
+ function isShareableGet(options: LoadOptions | undefined): boolean {
19
+ if (!options) return true;
20
+ if (options.method && options.method !== "GET") return false;
21
+ if ("body" in options && (options as { body?: unknown }).body !== undefined) {
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+
27
+ function isPlainRefetch(options: LoadOptions | undefined): boolean {
28
+ if (!isShareableGet(options)) return false;
29
+ if (options?.params && Object.keys(options.params).length > 0) return false;
30
+ return true;
31
+ }
32
+
33
+ let privateGroupBucketSeq = 0;
34
+
35
+ const NOT_FOUND = Symbol("not-found");
36
+
37
+ function extractContentLoaderData(
38
+ node: ReactNode,
39
+ loaderId: string,
40
+ ): unknown | typeof NOT_FOUND {
41
+ if (!isValidElement(node)) return NOT_FOUND;
42
+ const props = node.props as Record<string, any> | undefined;
43
+ if (!props) return NOT_FOUND;
44
+
45
+ // Direct OutletProvider with loaderData
46
+ if (props.loaderData && loaderId in props.loaderData) {
47
+ return props.loaderData[loaderId];
48
+ }
49
+
50
+ if (
51
+ props.loaderIds &&
52
+ Array.isArray(props.loaderIds) &&
53
+ props.loaderDataPromise &&
54
+ !(props.loaderDataPromise instanceof Promise)
55
+ ) {
56
+ const idx = (props.loaderIds as string[]).indexOf(loaderId);
57
+ if (idx !== -1) {
58
+ const data = (props.loaderDataPromise as any[])[idx];
59
+ if (data && typeof data === "object" && "ok" in data) {
60
+ return data.ok ? data.data : NOT_FOUND;
61
+ }
62
+ return data;
63
+ }
64
+ }
65
+
66
+ if (props.children) return extractContentLoaderData(props.children, loaderId);
67
+ return NOT_FOUND;
68
+ }
69
+
10
70
  interface LoaderRscPayload<T = unknown> {
11
71
  loaderResult: T;
12
72
  loaderError?: { message: string; name: string };
13
73
  }
14
74
 
15
- /**
16
- * Load function type for fetching loader data from the client
17
- */
18
75
  export type LoadFunction<T> = (options?: LoadOptions) => Promise<T>;
19
76
 
20
- /**
21
- * Result type for useLoader hook (strict - data is required)
22
- */
23
77
  export interface UseLoaderResult<T> {
24
- /** The loaded data - guaranteed to exist when loader is registered on route */
25
78
  data: T;
26
- /** True while a load() is in progress */
27
79
  isLoading: boolean;
28
- /** Error from the most recent load attempt, null if successful */
29
80
  error: Error | null;
30
- /** Function to trigger a fetch (only works if loader is fetchable) */
31
81
  load: LoadFunction<T>;
32
- /** Alias for load */
33
82
  refetch: LoadFunction<T>;
34
83
  }
35
84
 
36
- /**
37
- * Result type for useFetchLoader hook (flexible - data is optional)
38
- */
39
85
  export interface UseFetchLoaderResult<T> {
40
- /** The loaded data - may be undefined if not yet fetched or not in context */
41
86
  data: T | undefined;
42
- /** True while a load() is in progress */
43
87
  isLoading: boolean;
44
- /** Error from the most recent load attempt, null if successful */
45
88
  error: Error | null;
46
- /** Function to trigger a fetch (only works if loader is fetchable) */
47
89
  load: LoadFunction<T>;
48
- /** Alias for load */
49
90
  refetch: LoadFunction<T>;
50
91
  }
51
92
 
52
- /**
53
- * Options for useLoader hook
54
- */
55
93
  export interface UseLoaderOptions {
56
- /**
57
- * If true (default), errors from load() will be thrown to the nearest error boundary.
58
- * If false, errors are only captured in the `error` state.
59
- * @default true
60
- */
61
94
  throwOnError?: boolean;
95
+ key?: string;
96
+ refreshGroup?: string | string[];
62
97
  }
63
98
 
64
- /**
65
- * Internal hook implementation shared by useLoader and useFetchLoader
66
- */
67
99
  function useLoaderInternal<T>(
68
100
  loader: LoaderDefinition<T>,
69
101
  options?: UseLoaderOptions,
70
102
  ): UseFetchLoaderResult<T> {
71
103
  const context = useContext(OutletContext);
72
104
 
73
- // Get data from context (SSR/navigation)
74
- const getContextData = useCallback((): T | undefined => {
105
+ const { contextData, hasContextData } = useMemo((): {
106
+ contextData: T | undefined;
107
+ hasContextData: boolean;
108
+ } => {
75
109
  let current: OutletContextValue | null | undefined = context;
76
110
  while (current) {
77
111
  if (current.loaderData && loader.$$id in current.loaderData) {
78
- return current.loaderData[loader.$$id] as T;
112
+ return {
113
+ contextData: current.loaderData[loader.$$id] as T,
114
+ hasContextData: true,
115
+ };
116
+ }
117
+ const contentData = extractContentLoaderData(
118
+ current.content,
119
+ loader.$$id,
120
+ );
121
+ if (contentData !== NOT_FOUND) {
122
+ return { contextData: contentData as T, hasContextData: true };
79
123
  }
80
124
  current = current.parent;
81
125
  }
82
- return undefined;
126
+ return { contextData: undefined, hasContextData: false };
83
127
  }, [context, loader.$$id]);
84
128
 
85
- const contextData = getContextData();
86
-
87
- // Local state for fetched data (from load() calls)
88
- const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
89
- const [isLoading, setIsLoading] = useState(false);
90
- const [error, setError] = useState<Error | null>(null);
91
- const requestIdRef = useRef(0);
92
-
93
- // Track context data changes to reset fetched data on navigation
129
+ const loaderId = loader.$$id;
130
+ const key = options?.key;
131
+ const refreshGroupOption = options?.refreshGroup;
132
+ const groupKey =
133
+ refreshGroupOption === undefined
134
+ ? ""
135
+ : JSON.stringify(
136
+ typeof refreshGroupOption === "string"
137
+ ? [refreshGroupOption]
138
+ : [...new Set(refreshGroupOption)].sort(),
139
+ );
140
+ const groupList = useMemo<string[]>(
141
+ () => (groupKey === "" ? [] : (JSON.parse(groupKey) as string[])),
142
+ [groupKey],
143
+ );
144
+ const hasGroups = groupList.length > 0;
145
+ const privateBucketIdRef = useRef<string | null>(null);
146
+ if (hasGroups && key === undefined && privateBucketIdRef.current === null) {
147
+ privateBucketIdRef.current = `__rg${privateGroupBucketSeq++}`;
148
+ }
149
+ const effectiveKey =
150
+ key ?? (hasGroups ? privateBucketIdRef.current! : undefined);
151
+ const bucketKey =
152
+ effectiveKey === undefined ? loaderId : `${loaderId}::${effectiveKey}`;
153
+
154
+ const groupRefetch = useCallback(async (): Promise<void> => {
155
+ if (!loaderId) return;
156
+ const requestId = loaderStore.reserveRequestId(bucketKey);
157
+ loaderStore.beginRequest(bucketKey, requestId);
158
+ try {
159
+ const url = new URL(window.location.href);
160
+ url.searchParams.set("_rsc_loader", loaderId);
161
+ const response = fetch(url.toString(), {
162
+ method: "GET",
163
+ headers: { Accept: "text/x-component" },
164
+ });
165
+ const { createFromFetch } = await import("./deps/browser.js");
166
+ const payload = await createFromFetch<LoaderRscPayload<T>>(response);
167
+ if (payload.loaderError) {
168
+ throw new Error(payload.loaderError.message);
169
+ }
170
+ loaderStore.finishData(bucketKey, requestId, payload.loaderResult);
171
+ } catch (e) {
172
+ const err = e instanceof Error ? e : new Error(String(e));
173
+ loaderStore.finishError(bucketKey, requestId, err);
174
+ throw err;
175
+ } finally {
176
+ loaderStore.setLoading(bucketKey, requestId, false);
177
+ }
178
+ }, [loaderId, bucketKey]);
179
+
180
+ const [sharedState, setSharedState] = useState<{
181
+ bucketKey: string;
182
+ snapshot: LoaderEntry;
183
+ }>(() => ({
184
+ bucketKey,
185
+ snapshot: loaderStore.getSnapshot(bucketKey),
186
+ }));
187
+ const sharedSnapshot =
188
+ sharedState.bucketKey === bucketKey
189
+ ? sharedState.snapshot
190
+ : loaderStore.getSnapshot(bucketKey);
191
+ useEffect(() => {
192
+ const initial = loaderStore.getSnapshot(bucketKey);
193
+ if (initial !== sharedSnapshot) {
194
+ startTransition(() => {
195
+ setSharedState({ bucketKey, snapshot: initial });
196
+ });
197
+ }
198
+ // ephemeral: a reader with no route context has no route-context reset
199
+ // trigger, so its keyed bucket is reference-counted by the store. A
200
+ // route-registered reader makes the bucket sticky (reset via clearFamily).
201
+ return loaderStore.subscribe(
202
+ bucketKey,
203
+ () => {
204
+ const next = loaderStore.getSnapshot(bucketKey);
205
+ startTransition(() => {
206
+ setSharedState({ bucketKey, snapshot: next });
207
+ });
208
+ },
209
+ {
210
+ loaderId,
211
+ ephemeral: !hasContextData,
212
+ group: hasGroups ? groupList : undefined,
213
+ refetch: hasGroups ? groupRefetch : undefined,
214
+ },
215
+ );
216
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional:
217
+ // sharedSnapshot is captured for the one-shot init sync; we don't want
218
+ // to re-subscribe on every snapshot change. bucketKey, hasContextData,
219
+ // groupKey, and groupRefetch are the only inputs that require a fresh
220
+ // subscription (groupList is memoized on groupKey; groupRefetch is stable
221
+ // per bucketKey).
222
+ }, [bucketKey, hasContextData, groupKey, groupRefetch]);
223
+
224
+ // Local state holds the result of:
225
+ // - parameterized / mutation `load()` calls (load({ params }), POST,
226
+ // etc.) — stay scoped so concurrent same-loader different-params
227
+ // fetches don't clobber each other through the shared store;
228
+ // - any `load()` made by hooks that are NOT in route context (i.e.
229
+ // useFetchLoader of an unregistered loader) — keeping those local
230
+ // prevents two unrelated components from accidentally sharing data
231
+ // through the global store just because they reference the same
232
+ // loader id.
233
+ // `has` distinguishes a committed local result (including `null`/`undefined`)
234
+ // from "no local load yet", so a load() that resolves to a falsy value is not
235
+ // discarded in favor of the shared snapshot or the seeded context.
236
+ const [localFetchedData, setLocalFetchedData] = useState<{
237
+ has: boolean;
238
+ value: T | undefined;
239
+ }>({ has: false, value: undefined });
240
+ const [localIsLoading, setLocalIsLoading] = useState(false);
241
+ const [localError, setLocalError] = useState<Error | null>(null);
242
+
243
+ // Local request id, mirrors the per-hook gating the previous
244
+ // implementation provided. Two quick parameterized loads from the same
245
+ // hook (e.g. load({ params: { q: "a" } }) then load({ params: { q: "b" } }))
246
+ // can resolve out of order — only the latest must commit.
247
+ const localRequestIdRef = useRef(0);
248
+
249
+ // Tracks the request id of the most recent SHARED load() this hook
250
+ // initiated. The render-throw rule below uses it to scope the throw
251
+ // to the originating hook only — sibling readers see the error in
252
+ // `error` but don't blow up their own boundaries.
253
+ const lastSharedRequestIdRef = useRef<number | null>(null);
254
+
255
+ // Reset on navigation. clear() bumps the entry's latest request id so
256
+ // any pre-navigation load() promise that resolves later fails its gate
257
+ // and is dropped — fixes the race where a stale fetch overwrites the
258
+ // new route's context.
94
259
  const prevContextDataRef = useRef(contextData);
95
260
  useEffect(() => {
96
261
  if (prevContextDataRef.current !== contextData) {
97
- // Navigation happened, clear fetched data so context takes precedence
98
- setFetchedData(undefined);
99
- setError(null);
262
+ setLocalFetchedData({ has: false, value: undefined });
263
+ setLocalIsLoading(false);
264
+ setLocalError(null);
265
+ lastSharedRequestIdRef.current = null;
266
+ // Reset every sticky bucket of this loader (keyed or not). Ephemeral
267
+ // (unregistered keyed) buckets are left to their refcount lifecycle.
268
+ loaderStore.clearFamily(loaderId);
100
269
  prevContextDataRef.current = contextData;
101
270
  }
102
- }, [contextData]);
103
-
104
- // Data priority: fetched data (if any) > context data
105
- const data = fetchedData ?? contextData;
271
+ }, [contextData, loaderId]);
272
+
273
+ // Read priority: a committed parameterized load() result overrides the shared
274
+ // snapshot; a committed shared snapshot overrides the server-seeded context.
275
+ // `has`/`hasValue` gate each level so a committed falsy value is not skipped.
276
+ const data = localFetchedData.has
277
+ ? localFetchedData.value
278
+ : sharedSnapshot.hasValue
279
+ ? (sharedSnapshot.value as T | undefined)
280
+ : contextData;
281
+ const isLoading = localIsLoading || sharedSnapshot.isLoading;
282
+ const error = localError ?? sharedSnapshot.error;
106
283
 
107
284
  const throwOnError = options?.throwOnError ?? true;
108
285
 
109
- // Refs for values used inside load() that should NOT cause callback identity
110
- // churn. loader.$$id can change if a reusable component receives a different
111
- // loader without remounting; data changes on every navigation. Refs keep the
112
- // callback stable while always reading the latest values.
113
- const loaderIdRef = useRef(loader.$$id);
114
- loaderIdRef.current = loader.$$id;
286
+ const loaderIdRef = useRef(loaderId);
287
+ loaderIdRef.current = loaderId;
288
+ const bucketKeyRef = useRef(bucketKey);
289
+ bucketKeyRef.current = bucketKey;
115
290
  const dataRef = useRef(data);
116
291
  dataRef.current = data;
292
+ const hasContextDataRef = useRef(hasContextData);
293
+ hasContextDataRef.current = hasContextData;
117
294
 
118
- // Load function for fetching data via the ?_rsc_loader endpoint.
119
- // Supports GET (data fetching) and POST/PUT/PATCH/DELETE (mutations).
120
295
  const load = useCallback(
121
296
  async (loadOptions?: LoadOptions): Promise<T> => {
122
- const requestId = ++requestIdRef.current;
123
- const loaderId = loaderIdRef.current;
124
- // Verify the loader has $$id
125
- if (!loaderId) {
297
+ const id = loaderIdRef.current;
298
+ if (!id) {
126
299
  throw new Error(
127
300
  `Loader is missing $$id. Make sure the exposeLoaderId Vite plugin is enabled.`,
128
301
  );
129
302
  }
130
303
 
131
- setIsLoading(true);
132
- setError(null);
304
+ const bucket = bucketKeyRef.current;
305
+ const hasDedicatedBucket = bucket !== id;
306
+
307
+ const shared = hasDedicatedBucket
308
+ ? isShareableGet(loadOptions)
309
+ : isPlainRefetch(loadOptions) && hasContextDataRef.current;
310
+ let sharedRequestId = -1;
311
+ let localRequestId = -1;
312
+ if (shared) {
313
+ sharedRequestId = loaderStore.reserveRequestId(bucket);
314
+ lastSharedRequestIdRef.current = sharedRequestId;
315
+ loaderStore.beginRequest(bucket, sharedRequestId);
316
+ } else {
317
+ localRequestId = ++localRequestIdRef.current;
318
+ setLocalIsLoading(true);
319
+ setLocalError(null);
320
+ }
133
321
 
134
322
  try {
135
323
  const url = new URL(window.location.href);
136
- url.searchParams.set("_rsc_loader", loaderId);
324
+ url.searchParams.set("_rsc_loader", id);
137
325
 
138
326
  const method = loadOptions?.method ?? "GET";
139
327
  const isBodyMethod = method !== "GET";
@@ -149,8 +337,6 @@ function useLoaderInternal<T>(
149
337
  loadOptions?.params && Object.keys(loadOptions.params).length > 0;
150
338
 
151
339
  if (bodyValue instanceof FormData) {
152
- // FormData body — send as multipart/form-data (preserves File objects).
153
- // Params are appended as a JSON string in a special field.
154
340
  if (hasParams) {
155
341
  bodyValue.set(
156
342
  "_rsc_loader_params",
@@ -163,7 +349,6 @@ function useLoaderInternal<T>(
163
349
  body: bodyValue,
164
350
  };
165
351
  } else {
166
- // JSON body — send params and body as JSON
167
352
  const bodyPayload: {
168
353
  params?: Record<string, string>;
169
354
  body?: unknown;
@@ -185,7 +370,6 @@ function useLoaderInternal<T>(
185
370
  };
186
371
  }
187
372
  } else {
188
- // GET - send params in query string
189
373
  if (
190
374
  loadOptions?.params &&
191
375
  Object.keys(loadOptions.params).length > 0
@@ -214,34 +398,45 @@ function useLoaderInternal<T>(
214
398
  }
215
399
 
216
400
  const result = payload.loaderResult;
217
- if (requestId === requestIdRef.current) {
218
- setFetchedData(result);
401
+ if (shared) {
402
+ loaderStore.finishData(bucket, sharedRequestId, result);
403
+ } else if (localRequestId === localRequestIdRef.current) {
404
+ startTransition(() => {
405
+ setLocalFetchedData({ has: true, value: result });
406
+ setLocalIsLoading(false);
407
+ });
219
408
  }
220
409
  return result;
221
410
  } catch (e) {
222
411
  const err = e instanceof Error ? e : new Error(String(e));
223
- if (requestId === requestIdRef.current) {
224
- setError(err);
412
+ if (shared) {
413
+ loaderStore.finishError(bucket, sharedRequestId, err);
414
+ } else if (localRequestId === localRequestIdRef.current) {
415
+ setLocalError(err);
416
+ setLocalIsLoading(false);
225
417
  }
226
418
  if (throwOnError) {
227
419
  throw err;
228
420
  }
229
- // When throwOnError is false, return the latest data snapshot (previous
230
- // successful value or undefined). Caller should check error state.
231
421
  return dataRef.current as T;
232
422
  } finally {
233
- if (requestId === requestIdRef.current) {
234
- setIsLoading(false);
423
+ if (shared) {
424
+ loaderStore.setLoading(bucket, sharedRequestId, false);
235
425
  }
236
426
  }
237
427
  },
238
428
  [throwOnError],
239
429
  );
240
430
 
241
- // Throw during render if there's an error and throwOnError is true
242
- // This allows ErrorBoundaries to catch async errors from load()
243
- if (error && throwOnError) {
244
- throw error;
431
+ if (throwOnError) {
432
+ if (localError) throw localError;
433
+ if (
434
+ sharedSnapshot.error &&
435
+ lastSharedRequestIdRef.current !== null &&
436
+ sharedSnapshot.requestId === lastSharedRequestIdRef.current
437
+ ) {
438
+ throw sharedSnapshot.error;
439
+ }
245
440
  }
246
441
 
247
442
  return {
@@ -285,7 +480,7 @@ function useLoaderInternal<T>(
285
480
  export function useLoader<T>(
286
481
  loader: LoaderDefinition<T>,
287
482
  options?: UseLoaderOptions,
288
- ): UseLoaderResult<T> {
483
+ ): UseLoaderResult<Rango.FlightSerialize<T>> {
289
484
  const result = useLoaderInternal(loader, options);
290
485
 
291
486
  // Strict mode: throw if data is not in context
@@ -297,7 +492,7 @@ export function useLoader<T>(
297
492
  );
298
493
  }
299
494
 
300
- return result as UseLoaderResult<T>;
495
+ return result as UseLoaderResult<Rango.FlightSerialize<T>>;
301
496
  }
302
497
 
303
498
  /**
@@ -349,6 +544,68 @@ export function useLoader<T>(
349
544
  export function useFetchLoader<T>(
350
545
  loader: LoaderDefinition<T>,
351
546
  options?: UseLoaderOptions,
352
- ): UseFetchLoaderResult<T> {
353
- return useLoaderInternal(loader, options);
547
+ ): UseFetchLoaderResult<Rango.FlightSerialize<T>> {
548
+ return useLoaderInternal(loader, options) as UseFetchLoaderResult<
549
+ Rango.FlightSerialize<T>
550
+ >;
551
+ }
552
+
553
+ /**
554
+ * Get a stable function that refreshes loaders by cross-loader group tag.
555
+ *
556
+ * The returned `refresh(groups)` takes one group name or an array of names and
557
+ * re-runs every currently-mounted read tagged with ANY of them, with a plain GET
558
+ * against the current route URL. This is the cross-loader counterpart to the
559
+ * single-loader `key`: use it to refresh a set of DIFFERENT loaders together
560
+ * (e.g. profile + orders after an account switch). Members are tagged via
561
+ * `useLoader(Loader, { refreshGroup })` / `useFetchLoader(Loader, { refreshGroup })`,
562
+ * where `refreshGroup` is one name or several.
563
+ *
564
+ * Passing the group(s) to the returned function rather than to the hook lets a
565
+ * single `useRefreshLoaders()` instance refresh different groups depending on
566
+ * context, and lets one call refresh several groups at once — their members are
567
+ * unioned and deduped, so a loader tagged into two of the named groups is fetched
568
+ * exactly once.
569
+ *
570
+ * Group refresh never render-throws: a failing member surfaces its error via
571
+ * that read's `error` state, and the returned promise rejects with an
572
+ * `AggregateError` of the failures so the caller can handle them at the await
573
+ * site. Each loader is refreshed in place — no params, no body, no mutations.
574
+ *
575
+ * @example
576
+ * ```tsx
577
+ * "use client";
578
+ * import { useLoader, useRefreshLoaders } from "rsc-router/client";
579
+ *
580
+ * function Profile() {
581
+ * const { data } = useLoader(ProfileLoader, { key: userId, refreshGroup: "account" });
582
+ * return <span>{data.name}</span>;
583
+ * }
584
+ * function Orders() {
585
+ * // Tagged into two groups: refreshed by "account" (the whole set) or "orders".
586
+ * const { data } = useLoader(OrdersLoader, {
587
+ * key: userId,
588
+ * refreshGroup: ["account", "orders"],
589
+ * });
590
+ * return <span>{data.count} orders</span>;
591
+ * }
592
+ * function RefreshButtons() {
593
+ * const refresh = useRefreshLoaders();
594
+ * return (
595
+ * <>
596
+ * <button onClick={() => refresh("account")}>Refresh account</button>
597
+ * <button onClick={() => refresh("orders")}>Refresh orders</button>
598
+ * <button onClick={() => refresh(["account", "orders"])}>Refresh both</button>
599
+ * </>
600
+ * );
601
+ * }
602
+ * ```
603
+ */
604
+ export function useRefreshLoaders(): (
605
+ groups: string | string[],
606
+ ) => Promise<void> {
607
+ return useCallback(
608
+ (groups: string | string[]) => loaderStore.refreshGroups(groups),
609
+ [],
610
+ );
354
611
  }