@rangojs/router 0.0.0-experimental.0f44aca1

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 (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  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 +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Prefetch Fetch
3
+ *
4
+ * Fetch-based prefetch logic used by Link (hover/viewport/render strategies)
5
+ * and useRouter().prefetch(). Sends the same headers and segment IDs as a
6
+ * real navigation so the server returns a proper diff. The Response is fully
7
+ * buffered and stored in an in-memory cache for instant consumption on
8
+ * subsequent navigation.
9
+ */
10
+
11
+ import {
12
+ buildPrefetchKey,
13
+ hasPrefetch,
14
+ markPrefetchInflight,
15
+ storePrefetch,
16
+ clearPrefetchInflight,
17
+ currentGeneration,
18
+ } from "./cache.js";
19
+ import { getRangoState } from "../rango-state.js";
20
+ import { enqueuePrefetch } from "./queue.js";
21
+ import { shouldPrefetch } from "./policy.js";
22
+
23
+ /**
24
+ * Build an RSC partial URL for prefetching.
25
+ * Includes _rsc_segments so the server can diff against currently mounted
26
+ * segments, and _rsc_v for version mismatch detection.
27
+ * Returns null for malformed or cross-origin URLs.
28
+ */
29
+ function buildPrefetchUrl(
30
+ url: string,
31
+ segmentIds: string[],
32
+ version?: string,
33
+ ): URL | null {
34
+ let targetUrl: URL;
35
+ try {
36
+ targetUrl = new URL(url, window.location.origin);
37
+ } catch {
38
+ return null;
39
+ }
40
+ if (targetUrl.origin !== window.location.origin) {
41
+ return null;
42
+ }
43
+ targetUrl.searchParams.set("_rsc_partial", "true");
44
+ if (segmentIds.length > 0) {
45
+ targetUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
46
+ }
47
+ if (version) {
48
+ targetUrl.searchParams.set("_rsc_v", version);
49
+ }
50
+ return targetUrl;
51
+ }
52
+
53
+ /**
54
+ * Core prefetch fetch logic. Fetches the response, fully buffers the body,
55
+ * and stores it in the in-memory cache. Returns a Promise and accepts an
56
+ * optional AbortSignal for cancellation by the prefetch queue.
57
+ */
58
+ function executePrefetchFetch(
59
+ key: string,
60
+ fetchUrl: string,
61
+ signal?: AbortSignal,
62
+ ): Promise<void> {
63
+ const gen = currentGeneration();
64
+ markPrefetchInflight(key);
65
+
66
+ return fetch(fetchUrl, {
67
+ priority: "low" as RequestPriority,
68
+ signal,
69
+ headers: {
70
+ "X-Rango-State": getRangoState(),
71
+ "X-RSC-Router-Client-Path": window.location.href,
72
+ "X-Rango-Prefetch": "1",
73
+ },
74
+ })
75
+ .then(async (response) => {
76
+ if (!response.ok) return;
77
+ // Fully buffer the response body so the cached Response is
78
+ // self-contained and doesn't depend on the network connection.
79
+ // This eliminates the race condition where the user clicks before
80
+ // the response body has been fully downloaded.
81
+ const buffer = await response.arrayBuffer();
82
+ const cachedResponse = new Response(buffer, {
83
+ headers: response.headers,
84
+ status: response.status,
85
+ statusText: response.statusText,
86
+ });
87
+ storePrefetch(key, cachedResponse, gen);
88
+ })
89
+ .catch(() => {
90
+ // Silently ignore prefetch failures (including abort)
91
+ })
92
+ .finally(() => {
93
+ clearPrefetchInflight(key);
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Prefetch (direct): fetch with low priority and store in in-memory cache.
99
+ * Used by hover strategy -- fires immediately without queueing.
100
+ */
101
+ export function prefetchDirect(
102
+ url: string,
103
+ segmentIds: string[],
104
+ version?: string,
105
+ ): void {
106
+ if (!shouldPrefetch()) return;
107
+
108
+ const targetUrl = buildPrefetchUrl(url, segmentIds, version);
109
+ if (!targetUrl) return;
110
+ const key = buildPrefetchKey(window.location.href, targetUrl);
111
+ if (hasPrefetch(key)) return;
112
+ executePrefetchFetch(key, targetUrl.toString());
113
+ }
114
+
115
+ /**
116
+ * Prefetch (queued): goes through the concurrency-limited queue.
117
+ * Used by viewport/render strategies to avoid flooding the server.
118
+ * Returns the cache key for use in cleanup.
119
+ */
120
+ export function prefetchQueued(
121
+ url: string,
122
+ segmentIds: string[],
123
+ version?: string,
124
+ ): string {
125
+ if (!shouldPrefetch()) return "";
126
+ const targetUrl = buildPrefetchUrl(url, segmentIds, version);
127
+ if (!targetUrl) return "";
128
+ const key = buildPrefetchKey(window.location.href, targetUrl);
129
+ if (hasPrefetch(key)) return key;
130
+ const fetchUrlStr = targetUrl.toString();
131
+ enqueuePrefetch(key, (signal) =>
132
+ executePrefetchFetch(key, fetchUrlStr, signal),
133
+ );
134
+ return key;
135
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Prefetch Observer
3
+ *
4
+ * Shared singleton IntersectionObserver for viewport-based prefetching.
5
+ * One observer handles all Link components with prefetch="viewport".
6
+ *
7
+ * Lazy-created on first call to avoid issues in SSR or test environments
8
+ * where IntersectionObserver may not exist.
9
+ *
10
+ * Observation is one-shot: once a link enters the viewport and the callback
11
+ * fires, the element is unobserved. This prevents re-prefetching when a link
12
+ * scrolls in and out repeatedly.
13
+ */
14
+
15
+ type PrefetchCallback = () => void;
16
+
17
+ const callbacks = new Map<Element, PrefetchCallback>();
18
+ let observer: IntersectionObserver | null = null;
19
+
20
+ function getObserver(): IntersectionObserver {
21
+ if (!observer) {
22
+ observer = new IntersectionObserver(
23
+ (entries) => {
24
+ for (const entry of entries) {
25
+ if (entry.isIntersecting) {
26
+ const callback = callbacks.get(entry.target);
27
+ if (callback) {
28
+ observer!.unobserve(entry.target);
29
+ callbacks.delete(entry.target);
30
+ callback();
31
+ }
32
+ }
33
+ }
34
+ },
35
+ { rootMargin: "200px" },
36
+ );
37
+ }
38
+ return observer;
39
+ }
40
+
41
+ /**
42
+ * Observe an element for viewport intersection.
43
+ * When the element becomes visible (within 200px margin), the callback fires
44
+ * and the element is automatically unobserved.
45
+ * No-op in environments without IntersectionObserver (SSR, some test runners).
46
+ */
47
+ export function observeForPrefetch(
48
+ element: Element,
49
+ onVisible: PrefetchCallback,
50
+ ): void {
51
+ if (typeof IntersectionObserver === "undefined") return;
52
+ callbacks.set(element, onVisible);
53
+ getObserver().observe(element);
54
+ }
55
+
56
+ /**
57
+ * Stop observing an element. Used for cleanup when a Link unmounts
58
+ * before entering the viewport.
59
+ */
60
+ export function unobserveForPrefetch(element: Element): void {
61
+ callbacks.delete(element);
62
+ if (observer) {
63
+ observer.unobserve(element);
64
+ }
65
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Prefetch Policy
3
+ *
4
+ * Determines whether speculative prefetching should run for the current user.
5
+ * Honors browser reduced-data preferences when available.
6
+ */
7
+
8
+ type NavigatorWithConnection = Navigator & {
9
+ connection?: {
10
+ saveData?: boolean;
11
+ };
12
+ };
13
+
14
+ /**
15
+ * Evaluate on every call so runtime changes to Save-Data or
16
+ * prefers-reduced-data are respected immediately.
17
+ */
18
+ export function shouldPrefetch(): boolean {
19
+ if (typeof window === "undefined") return false;
20
+
21
+ const nav =
22
+ typeof navigator !== "undefined"
23
+ ? (navigator as NavigatorWithConnection)
24
+ : undefined;
25
+
26
+ if (nav?.connection?.saveData) return false;
27
+
28
+ if (typeof window.matchMedia === "function") {
29
+ try {
30
+ if (window.matchMedia("(prefers-reduced-data: reduce)").matches) {
31
+ return false;
32
+ }
33
+ } catch {
34
+ // Ignore unsupported query errors and allow prefetch.
35
+ }
36
+ }
37
+
38
+ return true;
39
+ }
40
+
41
+ /** No-op, kept for test compatibility. */
42
+ export function resetPrefetchPolicy(): void {}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Prefetch Queue
3
+ *
4
+ * Concurrency-limited FIFO queue for speculative prefetches (viewport/render).
5
+ * Hover prefetches bypass this queue — they fire directly for immediate response
6
+ * to user intent.
7
+ *
8
+ * All queued/executing prefetches share a single AbortController so they can
9
+ * be cancelled in bulk when a navigation starts.
10
+ */
11
+
12
+ const MAX_CONCURRENT = 2;
13
+
14
+ let active = 0;
15
+ const queue: Array<{
16
+ key: string;
17
+ execute: (signal: AbortSignal) => Promise<void>;
18
+ }> = [];
19
+ const queued = new Set<string>();
20
+ const executing = new Set<string>();
21
+ let abortController: AbortController | null = null;
22
+
23
+ function startExecution(
24
+ key: string,
25
+ execute: (signal: AbortSignal) => Promise<void>,
26
+ ): void {
27
+ active++;
28
+ executing.add(key);
29
+ abortController ??= new AbortController();
30
+ execute(abortController.signal).finally(() => {
31
+ // Only decrement if this key wasn't already cleared by cancelAllPrefetches.
32
+ // Without this guard, cancelled tasks' .finally() would underflow active
33
+ // below zero, breaking the MAX_CONCURRENT guarantee.
34
+ if (executing.delete(key)) {
35
+ active--;
36
+ }
37
+ drain();
38
+ });
39
+ }
40
+
41
+ function drain(): void {
42
+ while (active < MAX_CONCURRENT && queue.length > 0) {
43
+ const item = queue.shift()!;
44
+ queued.delete(item.key);
45
+ startExecution(item.key, item.execute);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Enqueue a prefetch for concurrency-limited execution.
51
+ * If below the concurrency limit, executes immediately.
52
+ * Otherwise queues for later execution.
53
+ * Deduplicates by key — items already queued or executing are skipped.
54
+ *
55
+ * The executor receives an AbortSignal that is aborted when
56
+ * cancelAllPrefetches() is called (e.g. on navigation start).
57
+ */
58
+ export function enqueuePrefetch(
59
+ key: string,
60
+ execute: (signal: AbortSignal) => Promise<void>,
61
+ ): void {
62
+ if (queued.has(key) || executing.has(key)) return;
63
+
64
+ if (active < MAX_CONCURRENT) {
65
+ startExecution(key, execute);
66
+ } else {
67
+ queued.add(key);
68
+ queue.push({ key, execute });
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Cancel all in-flight and queued prefetches.
74
+ * Called when a navigation starts — speculative prefetches should not
75
+ * compete with navigation fetches for connection slots.
76
+ */
77
+ export function cancelAllPrefetches(): void {
78
+ abortController?.abort();
79
+ abortController = null;
80
+
81
+ queue.length = 0;
82
+ queued.clear();
83
+ // Clear executing before resetting active. In-flight .finally() callbacks
84
+ // check executing.delete(key) — if the key is gone, they skip decrementing,
85
+ // so active settles at 0 without underflow.
86
+ executing.clear();
87
+ active = 0;
88
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Rango State
3
+ *
4
+ * Manages a localStorage-based state key for HTTP cache invalidation.
5
+ * The key is sent as the `X-Rango-State` header on both prefetch and
6
+ * navigation requests. The server responds with `Vary: X-Rango-State`,
7
+ * so the browser HTTP cache keys responses by (URL, X-Rango-State value).
8
+ *
9
+ * Format: `{buildVersion}:{invalidationTimestamp}`
10
+ * - Build version changes on deploy, busting all cached prefetches.
11
+ * - Timestamp changes on server action invalidation.
12
+ *
13
+ * localStorage is cross-tab and survives page refresh, so:
14
+ * - One tab's prefetch warms the cache for all tabs.
15
+ * - Invalidation in one tab is picked up by other tabs on next fetch.
16
+ */
17
+
18
+ const STORAGE_KEY = "rango-state";
19
+
20
+ // Module-level cache avoids hitting localStorage on every getRangoState() call.
21
+ // Initialized from localStorage on first access or by initRangoState().
22
+ let cachedState: string | null = null;
23
+
24
+ // Cross-tab sync: the `storage` event fires in OTHER tabs when one tab writes
25
+ // to localStorage, keeping cachedState fresh without polling.
26
+ let storageListenerAttached = false;
27
+
28
+ function attachStorageListener(): void {
29
+ if (storageListenerAttached || typeof window === "undefined") return;
30
+ window.addEventListener("storage", (e) => {
31
+ if (e.key !== STORAGE_KEY) return;
32
+ cachedState = e.newValue;
33
+ });
34
+ storageListenerAttached = true;
35
+ }
36
+
37
+ /**
38
+ * Initialize the Rango state key in localStorage.
39
+ * Called once at app startup with the build version from the server.
40
+ * If localStorage already has a key with matching version prefix, keeps it
41
+ * (preserves invalidation state across refresh). Otherwise writes a new key.
42
+ */
43
+ export function initRangoState(version: string): void {
44
+ if (typeof window === "undefined") return;
45
+
46
+ attachStorageListener();
47
+
48
+ try {
49
+ const existing = localStorage.getItem(STORAGE_KEY);
50
+ if (existing) {
51
+ const colonIdx = existing.indexOf(":");
52
+ if (colonIdx > 0) {
53
+ const existingVersion = existing.slice(0, colonIdx);
54
+ if (existingVersion === version) {
55
+ cachedState = existing;
56
+ return;
57
+ }
58
+ }
59
+ }
60
+ // New version or first load
61
+ const newState = `${version}:${Date.now()}`;
62
+ localStorage.setItem(STORAGE_KEY, newState);
63
+ cachedState = newState;
64
+ } catch {
65
+ // localStorage may be unavailable (private browsing in some browsers)
66
+ cachedState = `${version}:${Date.now()}`;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Get the current Rango state key value.
72
+ * Used as the `X-Rango-State` header value for prefetch and navigation requests.
73
+ */
74
+ export function getRangoState(): string {
75
+ if (cachedState) return cachedState;
76
+
77
+ if (typeof window === "undefined") return "0:0";
78
+
79
+ try {
80
+ const stored = localStorage.getItem(STORAGE_KEY);
81
+ if (stored) {
82
+ cachedState = stored;
83
+ return stored;
84
+ }
85
+ } catch {
86
+ // Fallback for unavailable localStorage
87
+ }
88
+
89
+ return "0:0";
90
+ }
91
+
92
+ /**
93
+ * Invalidate the Rango state key. Called when server actions mutate data.
94
+ * Updates the timestamp portion while keeping the version prefix.
95
+ * The new value takes effect immediately for all subsequent fetches,
96
+ * causing Vary mismatches with previously cached responses.
97
+ */
98
+ export function invalidateRangoState(): void {
99
+ const current = getRangoState();
100
+ const colonIdx = current.indexOf(":");
101
+ const version = colonIdx > 0 ? current.slice(0, colonIdx) : "0";
102
+ const newState = `${version}:${Date.now()}`;
103
+ cachedState = newState;
104
+
105
+ if (typeof window === "undefined") return;
106
+
107
+ try {
108
+ localStorage.setItem(STORAGE_KEY, newState);
109
+ } catch {
110
+ // Silently handle localStorage errors
111
+ }
112
+ }