@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Prefetch Fetch
3
+ *
4
+ * Fetch-based prefetch logic used by Link (hover/viewport/render strategies)
5
+ * and useRouter().prefetch(). Sends low-priority fetch requests with
6
+ * X-Rango-State and X-Rango-Prefetch headers so the browser HTTP cache
7
+ * can serve the response on subsequent navigation.
8
+ */
9
+
10
+ import {
11
+ hasPrefetch,
12
+ markPrefetchInflight,
13
+ markPrefetched,
14
+ clearPrefetchInflight,
15
+ currentGeneration,
16
+ } from "./cache.js";
17
+ import { getRangoState } from "../rango-state.js";
18
+ import { enqueuePrefetch } from "./queue.js";
19
+ import { shouldPrefetch } from "./policy.js";
20
+
21
+ /**
22
+ * Build an RSC partial URL for prefetching.
23
+ * Includes _rsc_v for version mismatch detection when available.
24
+ * Returns null for malformed or cross-origin URLs to prevent
25
+ * leaking router headers to external origins.
26
+ */
27
+ function buildPrefetchUrl(
28
+ url: string,
29
+ segmentIds: string[],
30
+ version?: string,
31
+ ): URL | null {
32
+ let targetUrl: URL;
33
+ try {
34
+ targetUrl = new URL(url, window.location.origin);
35
+ } catch {
36
+ return null;
37
+ }
38
+ if (targetUrl.origin !== window.location.origin) {
39
+ return null;
40
+ }
41
+ targetUrl.searchParams.set("_rsc_partial", "true");
42
+ if (segmentIds.length > 0) {
43
+ targetUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
44
+ }
45
+ if (version) {
46
+ targetUrl.searchParams.set("_rsc_v", version);
47
+ }
48
+ return targetUrl;
49
+ }
50
+
51
+ /**
52
+ * Build the dedup key for prefetch tracking.
53
+ * Includes the source page pathname so the same target prefetched from
54
+ * different pages gets separate entries — the server response varies on
55
+ * X-RSC-Router-Client-Path (source page context).
56
+ */
57
+ function buildPrefetchKey(targetUrl: URL): string {
58
+ return window.location.href + "\0" + targetUrl.pathname + targetUrl.search;
59
+ }
60
+
61
+ /**
62
+ * Core prefetch fetch logic. Returns a Promise and accepts an optional
63
+ * AbortSignal for cancellation by the prefetch queue.
64
+ */
65
+ function executePrefetchFetch(
66
+ key: string,
67
+ fetchUrl: string,
68
+ signal?: AbortSignal,
69
+ ): Promise<void> {
70
+ const gen = currentGeneration();
71
+ markPrefetchInflight(key);
72
+
73
+ return fetch(fetchUrl, {
74
+ priority: "low" as RequestPriority,
75
+ signal,
76
+ headers: {
77
+ "X-Rango-State": getRangoState(),
78
+ "X-RSC-Router-Client-Path": window.location.href,
79
+ "X-Rango-Prefetch": "1",
80
+ },
81
+ })
82
+ .then((response) => {
83
+ // Drain body to ensure full download for browser HTTP cache.
84
+ // pipeTo avoids decoding the stream into a JS string (unlike .text()).
85
+ if (response.ok && response.body) {
86
+ return response.body
87
+ .pipeTo(new WritableStream())
88
+ .then(() => markPrefetched(key, gen));
89
+ }
90
+ })
91
+ .catch(() => {
92
+ // Silently ignore prefetch failures (including abort)
93
+ })
94
+ .finally(() => {
95
+ clearPrefetchInflight(key);
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Prefetch (direct): fetch with low priority and store in browser HTTP cache.
101
+ * Used by hover strategy -- fires immediately without queueing.
102
+ */
103
+ export function prefetchDirect(
104
+ url: string,
105
+ segmentIds: string[],
106
+ version?: string,
107
+ ): void {
108
+ if (!shouldPrefetch()) return;
109
+
110
+ const targetUrl = buildPrefetchUrl(url, segmentIds, version);
111
+ if (!targetUrl) return;
112
+ const key = buildPrefetchKey(targetUrl);
113
+ if (hasPrefetch(key)) return;
114
+ executePrefetchFetch(key, targetUrl.toString());
115
+ }
116
+
117
+ /**
118
+ * Prefetch (queued): goes through the concurrency-limited queue.
119
+ * Used by viewport/render strategies to avoid flooding the server.
120
+ * Returns the cache key for use in cleanup.
121
+ */
122
+ export function prefetchQueued(
123
+ url: string,
124
+ segmentIds: string[],
125
+ version?: string,
126
+ ): string {
127
+ if (!shouldPrefetch()) return "";
128
+ const targetUrl = buildPrefetchUrl(url, segmentIds, version);
129
+ if (!targetUrl) return "";
130
+ const key = buildPrefetchKey(targetUrl);
131
+ if (hasPrefetch(key)) return key;
132
+ const fetchUrlStr = targetUrl.toString();
133
+ enqueuePrefetch(key, (signal) =>
134
+ executePrefetchFetch(key, fetchUrlStr, signal),
135
+ );
136
+ return key;
137
+ }
@@ -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
+ }