@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19

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 (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
@@ -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 {}
@@ -28,8 +28,12 @@ function startExecution(
28
28
  executing.add(key);
29
29
  abortController ??= new AbortController();
30
30
  execute(abortController.signal).finally(() => {
31
- active--;
32
- executing.delete(key);
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
+ }
33
37
  drain();
34
38
  });
35
39
  }
@@ -76,6 +80,9 @@ export function cancelAllPrefetches(): void {
76
80
 
77
81
  queue.length = 0;
78
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.
79
86
  executing.clear();
80
- // active count resets naturally as aborted fetches settle in .finally()
87
+ active = 0;
81
88
  }
@@ -31,11 +31,11 @@ export type LinkState =
31
31
  | LocationStateEntry[]
32
32
  | StateOrGetter<Record<string, unknown>>;
33
33
 
34
- import { prefetchDirect, prefetchQueued } from "../prefetch-fetch.js";
34
+ import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
35
35
  import {
36
36
  observeForPrefetch,
37
37
  unobserveForPrefetch,
38
- } from "../prefetch-observer.js";
38
+ } from "../prefetch/observer.js";
39
39
 
40
40
  // Touch device detection for hybrid strategy.
41
41
  // Checked once at module load (Link.tsx is "use client", runs only in browser).
@@ -235,31 +235,34 @@ export const Link: ForwardRefExoticComponent<
235
235
  return;
236
236
  }
237
237
 
238
+ // No navigation context (outside provider): fall back to native navigation.
239
+ if (!ctx?.navigate) {
240
+ return;
241
+ }
242
+
238
243
  // Prevent default and use SPA navigation
239
244
  e.preventDefault();
240
245
  // Stop propagation to prevent link-interceptor from also handling this
241
246
  e.stopPropagation();
242
247
 
243
- if (ctx?.navigate) {
244
- const currentState = stateRef.current;
245
- let resolvedState: unknown;
246
-
247
- if (
248
- Array.isArray(currentState) &&
249
- currentState.length > 0 &&
250
- isLocationStateEntry(currentState[0])
251
- ) {
252
- resolvedState = resolveLocationStateEntries(
253
- currentState as LocationStateEntry[],
254
- );
255
- } else if (typeof currentState === "function") {
256
- resolvedState = currentState();
257
- } else if (currentState != null) {
258
- resolvedState = currentState;
259
- }
260
-
261
- ctx.navigate(to, { replace, scroll, state: resolvedState });
248
+ const currentState = stateRef.current;
249
+ let resolvedState: unknown;
250
+
251
+ if (
252
+ Array.isArray(currentState) &&
253
+ currentState.length > 0 &&
254
+ isLocationStateEntry(currentState[0])
255
+ ) {
256
+ resolvedState = resolveLocationStateEntries(
257
+ currentState as LocationStateEntry[],
258
+ );
259
+ } else if (typeof currentState === "function") {
260
+ resolvedState = currentState();
261
+ } else if (currentState != null) {
262
+ resolvedState = currentState;
262
263
  }
264
+
265
+ ctx.navigate(to, { replace, scroll, state: resolvedState });
263
266
  },
264
267
  [to, isExternal, reloadDocument, replace, scroll, ctx, onClick],
265
268
  );
@@ -281,6 +284,7 @@ export const Link: ForwardRefExoticComponent<
281
284
 
282
285
  let cancelled = false;
283
286
  let unsubIdle: (() => void) | undefined;
287
+ let observedElement: Element | null = null;
284
288
 
285
289
  const triggerPrefetch = () => {
286
290
  if (cancelled) return;
@@ -311,6 +315,7 @@ export const Link: ForwardRefExoticComponent<
311
315
  } else if (isViewport) {
312
316
  const element = internalRef.current;
313
317
  if (!element) return;
318
+ observedElement = element;
314
319
  observeForPrefetch(element, () => {
315
320
  scheduleWhenIdle(triggerPrefetch);
316
321
  });
@@ -319,8 +324,8 @@ export const Link: ForwardRefExoticComponent<
319
324
  return () => {
320
325
  cancelled = true;
321
326
  unsubIdle?.();
322
- if (isViewport && internalRef.current) {
323
- unobserveForPrefetch(internalRef.current);
327
+ if (isViewport && observedElement) {
328
+ unobserveForPrefetch(observedElement);
324
329
  }
325
330
  };
326
331
  }, [resolvedStrategy, to, isExternal, ctx]);
@@ -22,8 +22,9 @@ import type { EventController } from "../event-controller.js";
22
22
  import { RootErrorBoundary } from "../../root-error-boundary.js";
23
23
  import type { HandleData } from "../types.js";
24
24
  import { ThemeProvider } from "../../theme/ThemeProvider.js";
25
+ import { NonceContext } from "./nonce-context.js";
25
26
  import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
26
- import { cancelAllPrefetches } from "../prefetch-queue.js";
27
+ import { cancelAllPrefetches } from "../prefetch/queue.js";
27
28
 
28
29
  /**
29
30
  * Process handles from an async generator, updating the event controller
@@ -370,6 +371,13 @@ export function NavigationProvider({
370
371
  );
371
372
  }
372
373
 
374
+ // Match SSR tree shape: NonceContext.Provider is always present so
375
+ // hydration sees the same component tree. Value is undefined on the
376
+ // client — CSP nonces are a server-side HTML concern.
377
+ content = (
378
+ <NonceContext.Provider value={undefined}>{content}</NonceContext.Provider>
379
+ );
380
+
373
381
  return (
374
382
  <NavigationStoreContext.Provider value={contextValue}>
375
383
  {content}
@@ -15,14 +15,10 @@ export { useParams } from "./use-params.js";
15
15
  export { useAction, type TrackedActionState } from "./use-action.js";
16
16
 
17
17
  // Segments state hook
18
- export {
19
- useSegments,
20
- initSegmentsSync,
21
- type SegmentsState,
22
- } from "./use-segments.js";
18
+ export { useSegments, type SegmentsState } from "./use-segments.js";
23
19
 
24
20
  // Handle data hook
25
- export { useHandle, initHandleDataSync } from "./use-handle.js";
21
+ export { useHandle } from "./use-handle.js";
26
22
 
27
23
  // Client cache controls hook
28
24
  export {
@@ -79,7 +79,7 @@ export function createLocationState<TState>(
79
79
  let _key: string | undefined;
80
80
 
81
81
  function getKey(): string {
82
- if (!_key && process.env.NODE_ENV !== "production") {
82
+ if (!_key && process.env.NODE_ENV === "development") {
83
83
  throw new Error(
84
84
  "[rsc-router] createLocationState key not set. " +
85
85
  "Make sure the exposeInternalIds Vite plugin is enabled and " +
@@ -80,6 +80,8 @@ export function useLocationState<TArgs extends unknown[], TState>(
80
80
  } else {
81
81
  setState(val);
82
82
  }
83
+ } else {
84
+ setState(window.history.state?.state as TState | undefined);
83
85
  }
84
86
  };
85
87
 
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Context for CSP nonce propagation to client components during SSR.
5
+ *
6
+ * The SSR renderer wraps the tree with NonceContext.Provider so that
7
+ * client components (e.g. MetaTags) can apply nonces to inline scripts.
8
+ * On the browser side, no provider is needed — the default undefined
9
+ * is correct since CSP nonces are a server-side HTML concern.
10
+ */
11
+
12
+ import { createContext, useContext, type Context } from "react";
13
+
14
+ export const NonceContext: Context<string | undefined> = createContext<
15
+ string | undefined
16
+ >(undefined);
17
+
18
+ /**
19
+ * Read the CSP nonce during SSR. Returns undefined on the client.
20
+ */
21
+ export function useNonce(): string | undefined {
22
+ return useContext(NonceContext);
23
+ }
@@ -127,6 +127,11 @@ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
127
127
  * const error = useAction(addToCart, state => state.error);
128
128
  * ```
129
129
  *
130
+ * @note The selector is expected to be stable for a given hook instance.
131
+ * This hook tracks one projection of one action. Changing selector semantics
132
+ * for the same action ID without a new action event is not a supported pattern;
133
+ * use separate useAction() subscriptions if you need different projections.
134
+ *
130
135
  * @note Actions passed as props from server components lose their metadata
131
136
  * during RSC serialization. Use a string action name or import directly.
132
137
  */
@@ -162,7 +167,10 @@ export function useAction<T>(
162
167
  T | TrackedActionState
163
168
  >(null!);
164
169
 
165
- // Ref keeps the latest selector without re-subscribing on every render.
170
+ // Ref keeps the latest selector for subscription callbacks without
171
+ // re-subscribing on every render. Selector changes themselves are not
172
+ // treated as a reactive input; this hook expects a stable selector and
173
+ // represents one subscription/projection for one action.
166
174
  const selectorRef = useRef(selector);
167
175
  selectorRef.current = selector;
168
176
 
@@ -12,17 +12,8 @@ import type { Handle } from "../../handle.js";
12
12
  import { getCollectFn } from "../../handle.js";
13
13
  import type { HandleData } from "../types.js";
14
14
  import { NavigationStoreContext } from "./context.js";
15
- import { filterSegmentOrder } from "./filter-segment-order.js";
16
15
  import { shallowEqual } from "./shallow-equal.js";
17
16
 
18
- /**
19
- * SSR module-level state.
20
- * Populated by initHandleDataSync before React renders.
21
- * Used by useState initializer during SSR.
22
- */
23
- let ssrHandleData: HandleData = {};
24
- let ssrSegmentOrder: string[] = [];
25
-
26
17
  /**
27
18
  * Resolve the collect function for a handle.
28
19
  * Handle objects are plain { __brand, $$id } - collect is stored in the registry
@@ -76,18 +67,6 @@ function collectHandle<T, A>(
76
67
  return collect(segmentArrays);
77
68
  }
78
69
 
79
- /**
80
- * Initialize handle data synchronously for SSR.
81
- * Called before rendering to populate state for useState initializer.
82
- *
83
- * @param data - Handle data from RSC payload
84
- * @param matched - Segment order for reduction
85
- */
86
- export function initHandleDataSync(data: HandleData, matched?: string[]): void {
87
- ssrHandleData = data;
88
- ssrSegmentOrder = filterSegmentOrder(matched ?? []);
89
- }
90
-
91
70
  /**
92
71
  * Hook to access collected handle data.
93
72
  *
@@ -117,11 +96,10 @@ export function useHandle<T, A, S>(
117
96
  ): A | S {
118
97
  const ctx = useContext(NavigationStoreContext);
119
98
 
120
- // Initial state from SSR module state or event controller
99
+ // Initial state from context event controller, or empty fallback without provider.
121
100
  const [value, setValue] = useState<A | S>(() => {
122
- // During SSR, use module-level state
123
- if (typeof document === "undefined" || !ctx) {
124
- const collected = collectHandle(handle, ssrHandleData, ssrSegmentOrder);
101
+ if (!ctx) {
102
+ const collected = collectHandle(handle, {}, []);
125
103
  return selector ? selector(collected) : collected;
126
104
  }
127
105
 
@@ -3,7 +3,6 @@
3
3
  import { useContext, useState, useEffect, useRef } from "react";
4
4
  import { NavigationStoreContext } from "./context.js";
5
5
  import { shallowEqual } from "./shallow-equal.js";
6
- import { getSsrParams } from "./use-segments.js";
7
6
 
8
7
  /**
9
8
  * Hook to access the current route params.
@@ -31,9 +30,8 @@ export function useParams<T>(
31
30
  const ctx = useContext(NavigationStoreContext);
32
31
 
33
32
  const [value, setValue] = useState<T | Record<string, string>>(() => {
34
- if (typeof document === "undefined" || !ctx) {
35
- const ssrParams = getSsrParams();
36
- return selector ? selector(ssrParams) : ssrParams;
33
+ if (!ctx) {
34
+ return selector ? selector({}) : {};
37
35
  }
38
36
  const params = ctx.eventController.getParams();
39
37
  return selector ? selector(params) : params;
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { useContext, useState, useEffect, useRef } from "react";
4
4
  import { NavigationStoreContext } from "./context.js";
5
- import { getSsrPathname } from "./use-segments.js";
6
5
 
7
6
  /**
8
7
  * Hook to access the current pathname.
@@ -20,8 +19,8 @@ export function usePathname(): string {
20
19
  const ctx = useContext(NavigationStoreContext);
21
20
 
22
21
  const [pathname, setPathname] = useState<string>(() => {
23
- if (typeof document === "undefined" || !ctx) {
24
- return getSsrPathname();
22
+ if (!ctx) {
23
+ return "/";
25
24
  }
26
25
  return (ctx.eventController.getState().location as URL).pathname;
27
26
  });
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useContext, useMemo } from "react";
4
4
  import { NavigationStoreContext } from "./context.js";
5
- import { prefetchDirect } from "../prefetch-fetch.js";
5
+ import { prefetchDirect } from "../prefetch/fetch.js";
6
6
  import type { RouterInstance, RouterNavigateOptions } from "../types.js";
7
7
 
8
8
  /**
@@ -41,7 +41,8 @@ export function useSearchParams(): ReadonlyURLSearchParams {
41
41
  const nextSearch = location.searchParams.toString();
42
42
  if (nextSearch !== prevSearch.current) {
43
43
  prevSearch.current = nextSearch;
44
- setSearchParams(location.searchParams);
44
+ // Create a snapshot so callers cannot mutate the source URLSearchParams
45
+ setSearchParams(new URLSearchParams(nextSearch));
45
46
  }
46
47
  };
47
48
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { useContext, useState, useEffect, useRef } from "react";
4
4
  import { NavigationStoreContext } from "./context.js";
5
- import { filterSegmentOrder } from "./filter-segment-order.js";
6
5
  import { shallowEqual } from "./shallow-equal.js";
7
6
 
8
7
  /**
@@ -17,47 +16,6 @@ export interface SegmentsState {
17
16
  location: URL;
18
17
  }
19
18
 
20
- /**
21
- * SSR module-level state.
22
- * Populated by initSegmentsSync before React renders.
23
- * Used by useState initializer during SSR.
24
- */
25
- let ssrSegmentOrder: string[] = [];
26
- let ssrPathname: string = "/";
27
- let ssrParams: Record<string, string> = {};
28
-
29
- /**
30
- * Initialize segments data synchronously for SSR.
31
- * Called before rendering to populate state for useState initializer.
32
- *
33
- * @param matched - Segment order from RSC metadata
34
- * @param pathname - Current pathname
35
- * @param params - Merged route params
36
- */
37
- export function initSegmentsSync(
38
- matched?: string[],
39
- pathname?: string,
40
- params?: Record<string, string>,
41
- ): void {
42
- ssrSegmentOrder = filterSegmentOrder(matched ?? []);
43
- ssrPathname = pathname ?? "/";
44
- ssrParams = params ?? {};
45
- }
46
-
47
- /**
48
- * Get SSR params for use-params hook initialization.
49
- */
50
- export function getSsrParams(): Record<string, string> {
51
- return ssrParams;
52
- }
53
-
54
- /**
55
- * Get SSR pathname for use-pathname hook initialization.
56
- */
57
- export function getSsrPathname(): string {
58
- return ssrPathname;
59
- }
60
-
61
19
  /**
62
20
  * Parse pathname into path segments
63
21
  * /shop/products/123 → ["shop", "products", "123"]
@@ -80,18 +38,6 @@ function buildSegmentsState(
80
38
  };
81
39
  }
82
40
 
83
- /**
84
- * Build SSR state from module-level variables
85
- */
86
- function buildSsrState(): SegmentsState {
87
- const location = new URL(ssrPathname, "http://localhost");
88
- return {
89
- path: parsePathname(ssrPathname),
90
- segmentIds: ssrSegmentOrder,
91
- location,
92
- };
93
- }
94
-
95
41
  /**
96
42
  * Hook to access current route segments with optional selector for performance
97
43
  *
@@ -115,13 +61,14 @@ export function useSegments<T>(
115
61
  ): T | SegmentsState {
116
62
  const ctx = useContext(NavigationStoreContext);
117
63
 
118
- // Build initial state from SSR module state or event controller.
119
- // Inlined rather than calling recompute() because the segmentsCache
120
- // ref is not yet initialized during the useState initializer.
64
+ // Build initial state from event controller when context exists.
65
+ // Inlined rather than calling recompute() because the segmentsCache ref
66
+ // is not yet initialized during the useState initializer.
121
67
  const [state, setState] = useState<T | SegmentsState>(() => {
122
- if (typeof document === "undefined" || !ctx) {
123
- const ssrState = buildSsrState();
124
- return selector ? selector(ssrState) : ssrState;
68
+ if (!ctx) {
69
+ const fallbackLocation = new URL("/", "http://localhost");
70
+ const fallbackState = buildSegmentsState(fallbackLocation, []);
71
+ return selector ? selector(fallbackState) : fallbackState;
125
72
  }
126
73
  const location = ctx.eventController.getLocation();
127
74
  const handleState = ctx.eventController.getHandleState();
@@ -0,0 +1,73 @@
1
+ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
2
+
3
+ type HeaderResult = { url: string } | "blocked" | null;
4
+
5
+ /**
6
+ * Extract and validate an RSC response header URL (X-RSC-Reload, X-RSC-Redirect).
7
+ * Returns { url } if valid, "blocked" if present but invalid origin, null if absent.
8
+ */
9
+ export function extractRscHeaderUrl(
10
+ response: Response,
11
+ header: string,
12
+ ): HeaderResult {
13
+ const raw = response.headers.get(header);
14
+ if (!raw) return null;
15
+ const url = validateRedirectOrigin(raw, window.location.origin);
16
+ return url ? { url } : "blocked";
17
+ }
18
+
19
+ /**
20
+ * Empty 200 response that won't choke Flight parsing.
21
+ * Used when a header URL is blocked by origin validation.
22
+ */
23
+ export function emptyResponse(): Response {
24
+ return new Response(null, { status: 200 });
25
+ }
26
+
27
+ /**
28
+ * Tee a response body for RSC parsing and stream completion tracking.
29
+ * Returns a new Response with one branch; the other is consumed to detect
30
+ * end-of-stream, calling onComplete when done.
31
+ *
32
+ * If the response has no body, onComplete fires synchronously.
33
+ * If signal is provided, an abort cancels the tracking reader.
34
+ */
35
+ export function teeWithCompletion(
36
+ response: Response,
37
+ onComplete: () => void,
38
+ signal?: AbortSignal,
39
+ ): Response {
40
+ if (!response.body) {
41
+ onComplete();
42
+ return response;
43
+ }
44
+
45
+ const [rscStream, trackingStream] = response.body.tee();
46
+
47
+ (async () => {
48
+ const reader = trackingStream.getReader();
49
+ const onAbort = signal ? reader.cancel.bind(reader) : undefined;
50
+ if (onAbort) signal!.addEventListener("abort", onAbort, { once: true });
51
+ try {
52
+ while (true) {
53
+ const { done } = await reader.read();
54
+ if (done) break;
55
+ }
56
+ } finally {
57
+ if (onAbort) signal!.removeEventListener("abort", onAbort);
58
+ reader.releaseLock();
59
+ onComplete();
60
+ }
61
+ })().catch((error) => {
62
+ if (!signal?.aborted) {
63
+ console.error("[Browser] Error reading tracking stream:", error);
64
+ }
65
+ onComplete();
66
+ });
67
+
68
+ return new Response(rscStream, {
69
+ headers: response.headers,
70
+ status: response.status,
71
+ statusText: response.statusText,
72
+ });
73
+ }