@timber-js/app 0.2.0-alpha.183 → 0.2.0-alpha.185
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.
- package/dist/_chunks/{build-output-helper-BC-Zg0_w.js → build-output-helper-DOGYFb_X.js} +11 -3
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-D3naS3eH.js → cli-schema-sync-BTWEKJXo.js} +1 -21
- package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +1 -0
- package/dist/_chunks/{cloudflare-nlD9KhDj.js → cloudflare-CnT5Lr7U.js} +2 -2
- package/dist/_chunks/{cloudflare-nlD9KhDj.js.map → cloudflare-CnT5Lr7U.js.map} +1 -1
- package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +1 -1
- package/dist/_chunks/segment-classify-Byy425ng.js.map +1 -1
- package/dist/_chunks/{walkers-BtTlKviE.js → walkers-_6zKFlch.js} +2 -2
- package/dist/_chunks/{walkers-BtTlKviE.js.map → walkers-_6zKFlch.js.map} +1 -1
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/internal.js +70 -22
- package/dist/client/internal.js.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +17 -0
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/index.js +128 -9
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +7 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +42 -0
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/types.d.ts +1 -3
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +4 -7
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/internal.js +135 -161
- package/dist/server/internal.js.map +1 -1
- package/dist/server/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +0 -12
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +3 -10
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +8 -19
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +0 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/docs/learn/11-error-handling.mdx +2 -3
- package/package.json +1 -1
- package/src/adapters/build-output-helper.ts +6 -1
- package/src/adapters/shared.ts +5 -2
- package/src/client/router.ts +24 -7
- package/src/client/rsc-fetch.ts +85 -39
- package/src/plugin-context.ts +7 -0
- package/src/plugins/routing.ts +1 -1
- package/src/plugins/static-build.ts +161 -6
- package/src/routing/manifest-codegen.ts +1 -6
- package/src/routing/scanner.ts +0 -24
- package/src/routing/types.ts +1 -3
- package/src/server/deny-boundary.ts +5 -31
- package/src/server/deny-renderer.ts +2 -2
- package/src/server/pipeline-outcome.ts +1 -1
- package/src/server/prebuilt-builder.ts +1 -7
- package/src/server/primitives.ts +0 -18
- package/src/server/rsc-entry/render-route.ts +3 -3
- package/src/server/sitemap-generator.ts +1 -7
- package/src/server/static-generator.ts +2 -18
- package/src/server/status-code-resolver.ts +4 -47
- package/src/shims/navigation-client.ts +8 -26
- package/src/shims/navigation.ts +0 -4
- package/dist/_chunks/build-output-helper-BC-Zg0_w.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-D3naS3eH.js.map +0 -1
package/dist/client/internal.js
CHANGED
|
@@ -222,14 +222,36 @@ var clientDeploymentId = null;
|
|
|
222
222
|
*/
|
|
223
223
|
var staticMode = false;
|
|
224
224
|
/**
|
|
225
|
+
* RSC manifest mapping unhashed → hashed URLs + params. Populated from
|
|
226
|
+
* `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during
|
|
227
|
+
* static generation). See TIM-1254, TIM-1255.
|
|
228
|
+
*/
|
|
229
|
+
var rscManifest = null;
|
|
230
|
+
function getRscManifest() {
|
|
231
|
+
if (rscManifest) return rscManifest;
|
|
232
|
+
if (typeof window !== "undefined" && window.__TIMBER_RSC_MANIFEST__) rscManifest = window.__TIMBER_RSC_MANIFEST__;
|
|
233
|
+
return rscManifest;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
225
236
|
* Convert a route URL to the corresponding _rsc/*.rsc file path.
|
|
226
237
|
* Mirrors the naming in plugins/static-build.ts staticOutputPath.
|
|
227
238
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
239
|
+
* When an RSC manifest is available (hashed filenames from TIM-1254),
|
|
240
|
+
* the manifest is consulted to resolve to the hashed path.
|
|
241
|
+
*
|
|
242
|
+
* / → /_rsc/index.rsc (or /_rsc/index-B7YxEKdN.rsc with manifest)
|
|
243
|
+
* /about → /_rsc/about.rsc (or /_rsc/about-C8ZzFLfO.rsc with manifest)
|
|
230
244
|
* /blog/hello → /_rsc/blog/hello.rsc
|
|
231
245
|
*/
|
|
232
246
|
function toStaticRscUrl(url) {
|
|
247
|
+
const unhashed = toUnhashedRscUrl(url);
|
|
248
|
+
return manifestLookup(unhashed) ?? unhashed;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Compute the unhashed _rsc/*.rsc URL for a route path.
|
|
252
|
+
* @internal Exported for testing.
|
|
253
|
+
*/
|
|
254
|
+
function toUnhashedRscUrl(url) {
|
|
233
255
|
const hashIndex = url.indexOf("#");
|
|
234
256
|
const queryIndex = url.indexOf("?");
|
|
235
257
|
const hashEnd = hashIndex === -1 ? url.length : hashIndex;
|
|
@@ -240,12 +262,30 @@ function toStaticRscUrl(url) {
|
|
|
240
262
|
return `/_rsc${pathname === "/" ? "/index" : pathname}.rsc`;
|
|
241
263
|
}
|
|
242
264
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
|
|
265
|
+
* Look up a key in the RSC manifest, falling back to percent-decoded
|
|
266
|
+
* lookup for encoded browser URLs. Returns the entry or null.
|
|
267
|
+
*/
|
|
268
|
+
function manifestEntry(key) {
|
|
269
|
+
const manifest = getRscManifest();
|
|
270
|
+
if (!manifest) return null;
|
|
271
|
+
if (manifest[key]) return manifest[key];
|
|
272
|
+
try {
|
|
273
|
+
const decoded = decodeURIComponent(key);
|
|
274
|
+
if (decoded !== key && manifest[decoded]) return manifest[decoded];
|
|
275
|
+
} catch {}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Look up the hashed URL for an unhashed RSC path.
|
|
280
|
+
*/
|
|
281
|
+
function manifestLookup(key) {
|
|
282
|
+
return manifestEntry(key)?.url ?? null;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Look up inlined route params for a route from the manifest. TIM-1255.
|
|
246
286
|
*/
|
|
247
|
-
function
|
|
248
|
-
return
|
|
287
|
+
function manifestParams(url) {
|
|
288
|
+
return manifestEntry(toUnhashedRscUrl(url))?.params ?? null;
|
|
249
289
|
}
|
|
250
290
|
/** Header name used by the server to signal a version skew reload. */
|
|
251
291
|
var RELOAD_HEADER = "X-Timber-Reload";
|
|
@@ -427,7 +467,8 @@ function trackStreamCompletion(body) {
|
|
|
427
467
|
* Otherwise, the raw response text is returned (test mode).
|
|
428
468
|
*/
|
|
429
469
|
async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
430
|
-
const
|
|
470
|
+
const fetchTarget = staticMode ? toStaticRscUrl(url) : url;
|
|
471
|
+
const rscUrl = staticMode && fetchTarget !== toUnhashedRscUrl(url) ? fetchTarget : appendRscParam(fetchTarget);
|
|
431
472
|
const headers = buildRscHeaders(staticMode ? void 0 : stateTree, currentUrl);
|
|
432
473
|
if (deps.decodeRsc) {
|
|
433
474
|
const fetchPromise = deps.fetch(rscUrl, {
|
|
@@ -435,11 +476,6 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
|
435
476
|
redirect: "manual",
|
|
436
477
|
signal
|
|
437
478
|
});
|
|
438
|
-
const staticParamsFetch = staticMode ? deps.fetch(toStaticParamsUrl(url), { signal }).then((r) => r.ok ? r.json() : null).catch((e) => {
|
|
439
|
-
if (e instanceof DOMException && e.name === "AbortError") throw e;
|
|
440
|
-
return null;
|
|
441
|
-
}) : null;
|
|
442
|
-
staticParamsFetch?.catch(() => {});
|
|
443
479
|
let segmentInfo = null;
|
|
444
480
|
let params = null;
|
|
445
481
|
let skippedSegments = null;
|
|
@@ -477,7 +513,7 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
|
477
513
|
return response;
|
|
478
514
|
});
|
|
479
515
|
await wrappedPromise;
|
|
480
|
-
if (
|
|
516
|
+
if (staticMode && !params) params = manifestParams(url);
|
|
481
517
|
const payload = deps.decodeRsc(wrappedPromise);
|
|
482
518
|
const payloadError = new Promise((_, reject) => {
|
|
483
519
|
Promise.resolve(payload).then(() => {}, reject);
|
|
@@ -515,12 +551,7 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
|
515
551
|
}
|
|
516
552
|
}
|
|
517
553
|
let fallbackParams = extractParams(response);
|
|
518
|
-
if (staticMode && !fallbackParams)
|
|
519
|
-
const paramsResponse = await deps.fetch(toStaticParamsUrl(url), { signal });
|
|
520
|
-
if (paramsResponse.ok) fallbackParams = await paramsResponse.json();
|
|
521
|
-
} catch (e) {
|
|
522
|
-
if (e instanceof DOMException && e.name === "AbortError") throw e;
|
|
523
|
-
}
|
|
554
|
+
if (staticMode && !fallbackParams) fallbackParams = manifestParams(url);
|
|
524
555
|
return {
|
|
525
556
|
payload: await response.text(),
|
|
526
557
|
decodePromise: null,
|
|
@@ -714,6 +745,23 @@ function createRouter(deps) {
|
|
|
714
745
|
const result = await perform();
|
|
715
746
|
if (!isPartialNavigation(result.skippedSegments)) renderPayload(result.payload, result.navState);
|
|
716
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* Perform a hard navigation to a URL. When the target is same-document
|
|
750
|
+
* (same pathname+search, different hash), `location.href = url` is a
|
|
751
|
+
* hash change — no reload. Use `location.reload()` instead (TIM-1235).
|
|
752
|
+
*
|
|
753
|
+
* `fromUrl` is the departing URL captured before any pushState or
|
|
754
|
+
* Navigation API commit — deps.getCurrentUrl() is unreliable here
|
|
755
|
+
* because the URL may have already been updated.
|
|
756
|
+
*/
|
|
757
|
+
function hardNavigate(url, fromUrl) {
|
|
758
|
+
const current = new URL(fromUrl, window.location.origin);
|
|
759
|
+
const target = new URL(url, window.location.origin);
|
|
760
|
+
if (target.pathname === current.pathname && target.search === current.search) {
|
|
761
|
+
window.location.href = url;
|
|
762
|
+
window.location.reload();
|
|
763
|
+
} else window.location.href = url;
|
|
764
|
+
}
|
|
717
765
|
/** Run a callback after the next paint (after React commit). */
|
|
718
766
|
function afterPaint(callback) {
|
|
719
767
|
if (deps.afterPaint) deps.afterPaint(callback);
|
|
@@ -837,12 +885,12 @@ function createRouter(deps) {
|
|
|
837
885
|
}
|
|
838
886
|
if (error instanceof ServerErrorResponse) {
|
|
839
887
|
setHardNavigating(true);
|
|
840
|
-
|
|
888
|
+
hardNavigate(url, departingUrl);
|
|
841
889
|
await new Promise(() => {});
|
|
842
890
|
}
|
|
843
891
|
if (error instanceof NonRscResponse) {
|
|
844
892
|
setHardNavigating(true);
|
|
845
|
-
|
|
893
|
+
hardNavigate(url, departingUrl);
|
|
846
894
|
await new Promise(() => {});
|
|
847
895
|
}
|
|
848
896
|
throw error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal.js","names":[],"sources":["../../src/client/segment-cache.ts","../../src/client/history.ts","../../src/client/rsc-fetch.ts","../../src/client/router.ts","../../src/client/use-search-params.ts"],"sourcesContent":["// Segment Cache — stores the mounted segment tree and prefetched payloads\n// See design/19-client-navigation.md for architecture details.\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** A prefetched RSC result with optional segment metadata. */\nexport interface PrefetchResult {\n payload: unknown;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo?: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useSegmentParams(). */\n params?: Record<string, string | string[]> | null;\n /** Segment paths skipped by the server (for client-side merging). */\n skippedSegments?: string[] | null;\n}\n\n/**\n * A node in the client-side segment tree. Each node represents a mounted\n * layout or page segment with its RSC flight payload.\n */\nexport interface SegmentNode {\n /** The segment's URL pattern (e.g., \"/\", \"/dashboard\", \"/projects/[id]\") */\n segment: string;\n /** The RSC flight payload for this segment (opaque to the cache) */\n payload: unknown;\n /**\n * Whether the segment/slot is request-dependent (calls getHeaders,\n * getSearchParams, cookies, etc.). Request-dependent segments always\n * re-render on navigation. For segments, this is still based on the\n * AsyncFunction heuristic (to be replaced separately). For slots,\n * this is taint-tracked via ALS.\n */\n isRequestDependent: boolean;\n /** Child segments keyed by segment path */\n children: Map<string, SegmentNode>;\n /** Parallel route slots keyed by slot path (e.g., \"/@sidebar\") */\n slots?: Map<string, SegmentNode>;\n /** Whether this slot's access.ts denied on its last render. */\n denied?: boolean;\n}\n\n/**\n * Serialized state tree sent via X-Timber-State-Tree header.\n * Only sync segments are included — async segments always re-render.\n */\nexport interface StateTree {\n segments: string[];\n slots?: string[];\n}\n\n// ─── Segment Cache ───────────────────────────────────────────────\n\n/**\n * Maintains the client-side segment tree representing currently mounted\n * layouts and pages. Used for navigation reconciliation — the router diffs\n * new routes against this tree to determine which segments to re-fetch.\n */\nexport class SegmentCache {\n private root: SegmentNode | undefined;\n\n get(segment: string): SegmentNode | undefined {\n if (segment === '/' || segment === this.root?.segment) {\n return this.root;\n }\n return undefined;\n }\n\n set(segment: string, node: SegmentNode): void {\n if (segment === '/' || !this.root) {\n this.root = node;\n }\n }\n\n clear(): void {\n this.root = undefined;\n }\n\n /**\n * Serialize the mounted segment tree for the X-Timber-State-Tree header.\n * Only includes sync segments — async segments are excluded because the\n * server must always re-render them (they may depend on request context).\n *\n * When mergeableFilter is provided, only segments whose paths are in the\n * set are included. This ensures the server only skips segments that the\n * client can actually merge (i.e., segments whose cached element tree\n * contains an inner SegmentProvider the merger can splice into).\n *\n * This is a performance optimization only, NOT a security boundary.\n * The server always runs all access.ts files regardless of the state tree.\n */\n serializeStateTree(mergeableFilter?: Set<string>): StateTree {\n const segments: string[] = [];\n const slots: string[] = [];\n if (this.root) {\n collectSyncSegments(this.root, segments, mergeableFilter);\n collectSyncSlots(this.root, slots);\n }\n const tree: StateTree = { segments };\n if (slots.length > 0) {\n tree.slots = slots;\n }\n return tree;\n }\n}\n\n/** Recursively collect sync segment paths from the tree */\nfunction collectSyncSegments(\n node: SegmentNode,\n out: string[],\n mergeableFilter?: Set<string>\n): void {\n if (!node.isRequestDependent && (!mergeableFilter || mergeableFilter.has(node.segment))) {\n out.push(node.segment);\n }\n for (const child of node.children.values()) {\n collectSyncSegments(child, out, mergeableFilter);\n }\n}\n\n/** Recursively collect cacheable slot paths from the tree */\nfunction collectSyncSlots(node: SegmentNode, out: string[]): void {\n if (node.slots) {\n for (const slot of node.slots.values()) {\n // Exclude request-dependent slots (they must re-render every nav)\n // and denied slots (their cached content is denial fallback, not real content)\n if (!slot.isRequestDependent && !slot.denied) {\n out.push(slot.segment);\n }\n }\n }\n for (const child of node.children.values()) {\n collectSyncSlots(child, out);\n }\n}\n\n// ─── Segment Tree Builder ────────────────────────────────────────\n\n/**\n * Segment metadata from the server, sent via X-Timber-Segments header.\n * Describes a rendered segment's path and whether it's async.\n */\nexport interface SegmentInfo {\n path: string;\n /** Outlet key — includes route group name when applicable (e.g., \"/(marketing)\"). */\n segmentId?: string;\n isRequestDependent: boolean;\n /** True for parallel route slot entries. Slots are keyed by their slot path (e.g., \"/@sidebar\"). */\n slot?: boolean;\n /** Parent segment path for slot entries. Used to attach the slot to the correct SegmentNode. */\n parentSegment?: string;\n /** True when the slot's access.ts denied on this render. Denied slots are excluded from the state tree. */\n denied?: boolean;\n /** True when the slot was skipped (cached content reused). Payloads with skipped slots are not replayable. */\n skipped?: boolean;\n}\n\n/**\n * Build a SegmentNode tree from flat segment metadata.\n *\n * Takes an ordered list of segment descriptors (root → leaf) from the\n * server's X-Timber-Segments header and constructs the hierarchical\n * tree structure that SegmentCache expects.\n *\n * Each segment is nested as a child of the previous one, forming a\n * linear chain from root to leaf. The leaf segment (page) is excluded\n * from the tree — pages are never cached across navigations.\n */\nexport function buildSegmentTree(segments: SegmentInfo[]): SegmentNode | undefined {\n // Need at least a root segment to build a tree\n if (segments.length === 0) return undefined;\n\n // Separate slot entries from segment entries. Slots are attached to\n // their parent segment node after the main chain is built.\n const segmentEntries: SegmentInfo[] = [];\n const slotEntries: SegmentInfo[] = [];\n for (const info of segments) {\n if (info.slot) {\n slotEntries.push(info);\n } else {\n segmentEntries.push(info);\n }\n }\n\n // Build the main segment chain.\n let root: SegmentNode | undefined;\n let parent: SegmentNode | undefined;\n const nodeById = new Map<string, SegmentNode>();\n\n for (const info of segmentEntries) {\n const id = info.segmentId ?? info.path;\n const node: SegmentNode = {\n segment: id,\n payload: null,\n isRequestDependent: info.isRequestDependent,\n children: new Map(),\n };\n\n nodeById.set(id, node);\n\n if (!root) {\n root = node;\n }\n\n if (parent) {\n parent.children.set(id, node);\n }\n\n parent = node;\n }\n\n // Attach slot entries to their parent segment nodes.\n for (const slotInfo of slotEntries) {\n const parentId = slotInfo.parentSegment;\n const parentNode = parentId ? nodeById.get(parentId) : root;\n if (!parentNode) continue;\n\n const slotId = slotInfo.segmentId ?? slotInfo.path;\n const slotNode: SegmentNode = {\n segment: slotId,\n payload: null,\n isRequestDependent: slotInfo.isRequestDependent,\n children: new Map(),\n denied: slotInfo.denied,\n };\n\n if (!parentNode.slots) {\n parentNode.slots = new Map();\n }\n parentNode.slots.set(slotId, slotNode);\n }\n\n return root;\n}\n\n// ─── Prefetch Cache ──────────────────────────────────────────────\n\ninterface PrefetchEntry {\n result: PrefetchResult;\n expiresAt: number;\n}\n\n/** Sentinel value for negative cache entries (URL is not a route). */\nconst NEGATIVE_ENTRY: PrefetchResult = Object.freeze({ payload: null });\n\n/**\n * Short-lived cache for hover-triggered prefetches. Entries expire after\n * 30 seconds. When a link is clicked, the prefetched payload is consumed\n * (moved to the history stack) and removed from this cache.\n *\n * timber.js does NOT prefetch on viewport intersection — only explicit\n * hover on <Link prefetch> triggers a prefetch.\n */\nexport class PrefetchCache {\n private static readonly TTL_MS = 30_000;\n private entries = new Map<string, PrefetchEntry>();\n\n set(url: string, result: PrefetchResult): void {\n this.entries.set(url, {\n result,\n expiresAt: Date.now() + PrefetchCache.TTL_MS,\n });\n }\n\n get(url: string): PrefetchResult | undefined {\n const entry = this.entries.get(url);\n if (!entry) return undefined;\n if (Date.now() >= entry.expiresAt) {\n this.entries.delete(url);\n return undefined;\n }\n return entry.result;\n }\n\n /** Get and remove the entry (used when navigation consumes a prefetch) */\n consume(url: string): PrefetchResult | undefined {\n const result = this.get(url);\n if (result !== undefined) {\n this.entries.delete(url);\n }\n return result;\n }\n\n /** Store a negative entry — the URL is not a route (non-RSC Content-Type). */\n setNegative(url: string): void {\n this.set(url, NEGATIVE_ENTRY);\n }\n\n /** Check if the entry is a negative cache entry (URL is not a route). */\n isNegative(url: string): boolean {\n const entry = this.get(url);\n return entry === NEGATIVE_ENTRY;\n }\n}\n","// History Stack — stores RSC payloads by URL for instant back/forward navigation\n// See design/19-client-navigation.md § History Stack\n\nimport type { SegmentInfo } from './segment-cache';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface HistoryEntry {\n /** The complete segment tree payload at the time of navigation */\n payload: unknown;\n /**\n * Route params for this page (for useSegmentParams). Every entry that can\n * be replayed must carry them — the initial SSR entry gets them from the\n * server-embedded __timber_params, navigation entries from X-Timber-Params,\n * and revalidation overwrites preserve the current state (TIM-1037).\n */\n params?: Record<string, string | string[]> | null;\n /**\n * Segment metadata for this page's route. Restored into the segment cache\n * on popstate cached replay so the next forward navigation computes a\n * correct state tree. Without this, the segment cache retains the\n * *previous* page's segments after back-button, causing the server to\n * skip segments that aren't mounted — and the partial payload targets\n * a non-existent outlet.\n */\n segmentInfo?: SegmentInfo[] | null;\n}\n\n// ─── History Stack ───────────────────────────────────────────────\n\n/**\n * Session-lived history stack keyed by URL. Enables instant back/forward\n * navigation without a server roundtrip.\n *\n * On forward navigation, the new page's payload is pushed onto the stack.\n * On popstate, the cached payload is replayed instantly.\n *\n * Entries are keyed by pathname + search. Used with the History API\n * fallback and the Navigation API.\n *\n * Scroll positions are stored in history.state or Navigation API entry\n * state, not in this stack — see design/19-client-navigation.md §Scroll Restoration.\n *\n * Entries persist for the session duration (no expiry) and are cleared\n * when the tab is closed — matching browser back-button behavior.\n */\nexport class HistoryStack {\n private entries = new Map<string, HistoryEntry>();\n\n push(url: string, entry: HistoryEntry): void {\n this.entries.set(url, entry);\n }\n\n get(url: string): HistoryEntry | undefined {\n return this.entries.get(url);\n }\n\n has(url: string): boolean {\n return this.entries.has(url);\n }\n}\n","/**\n * RSC Fetch — handles fetching and parsing RSC Flight payloads.\n *\n * Extracted from router.ts to keep both files under the 500-line limit.\n * This module handles:\n * - Cache-busting URL generation for RSC requests\n * - Building RSC request headers (Accept, X-Timber-State-Tree)\n * - Extracting metadata from RSC response headers\n * - Fetching and decoding RSC payloads\n *\n * See design/19-client-navigation.md §\"RSC Payload Handling\"\n */\n\nimport type { SegmentInfo } from './segment-cache';\nimport type { RouterDeps } from './router';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** Result of fetching an RSC payload — includes segment metadata. */\nexport interface FetchResult {\n payload: unknown;\n /**\n * Promise that settles when the RSC decode completes or fails.\n * The payload thenable is NOT awaited before returning (for streaming),\n * so callers must monitor this to catch async decode errors\n * (truncated streams, Flight parse failures) that would otherwise\n * become unhandled rejections.\n */\n decodePromise: Promise<void> | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useSegmentParams(). */\n params: Record<string, string | string[]> | null;\n /** Segment paths that were skipped by the server (for client-side merging). */\n skippedSegments: string[] | null;\n}\n\n// ─── Constants ───────────────────────────────────────────────────\n\nexport const RSC_CONTENT_TYPE = 'text/x-component';\n\n// ─── URL Helpers ─────────────────────────────────────────────────\n\n/**\n * Generate a short random cache-busting ID (5 chars, a-z0-9).\n * Matches the format Next.js uses for _rsc params.\n */\nfunction generateCacheBustId(): string {\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n let id = '';\n for (let i = 0; i < 5; i++) {\n id += chars[(Math.random() * 36) | 0];\n }\n return id;\n}\n\n/**\n * Append a `_rsc=<id>` query parameter to the URL.\n * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML\n * for RSC navigation requests and signals that this is an RSC fetch.\n *\n * Strips any #fragment before appending — fragments are client-only and\n * fetch() discards them, so _rsc would land inside the hash and be lost.\n */\nfunction appendRscParam(url: string): string {\n const hashIndex = url.indexOf('#');\n const urlWithoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);\n const separator = urlWithoutHash.includes('?') ? '&' : '?';\n return `${urlWithoutHash}${separator}_rsc=${generateCacheBustId()}`;\n}\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The client's deployment ID, set at bootstrap from the runtime config.\n * Sent with every RSC/action request for version skew detection.\n * Null in dev mode. See TIM-446.\n */\nlet clientDeploymentId: string | null = null;\n\n/** Set the client deployment ID. Called once at bootstrap. */\nexport function setClientDeploymentId(id: string | null): void {\n clientDeploymentId = id;\n}\n\n/** Get the client deployment ID. */\nexport function getClientDeploymentId(): string | null {\n return clientDeploymentId;\n}\n\n// ─── Static Mode ────────────────────────────────────────────────\n\n/**\n * When true, RSC fetches use _rsc/*.rsc file URLs instead of\n * the route URL with Accept headers. Static hosts ignore Accept\n * headers, so the client must fetch the pre-generated .rsc files\n * directly. Set at bootstrap from virtual:timber-config output mode.\n */\nlet staticMode = false;\n\nexport function setStaticMode(enabled: boolean): void {\n staticMode = enabled;\n}\n\nexport function isStaticMode(): boolean {\n return staticMode;\n}\n\n/**\n * Convert a route URL to the corresponding _rsc/*.rsc file path.\n * Mirrors the naming in plugins/static-build.ts staticOutputPath.\n *\n * / → /_rsc/index.rsc\n * /about → /_rsc/about.rsc\n * /blog/hello → /_rsc/blog/hello.rsc\n */\nfunction toStaticRscUrl(url: string): string {\n const hashIndex = url.indexOf('#');\n const queryIndex = url.indexOf('?');\n const hashEnd = hashIndex === -1 ? url.length : hashIndex;\n const queryEnd = queryIndex === -1 ? url.length : queryIndex;\n const end = Math.min(hashEnd, queryEnd);\n let pathname = url.slice(0, end);\n // Strip trailing slash (unless root) to match static build output naming\n if (pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n const rscPath = pathname === '/' ? '/index' : pathname;\n return `/_rsc${rscPath}.rsc`;\n}\n\n/**\n * Convert a route URL to the corresponding _rsc/*.params.json sidecar path.\n * Used in static mode to fetch route params that are normally carried\n * in the X-Timber-Params response header. See TIM-1246.\n */\nfunction toStaticParamsUrl(url: string): string {\n return toStaticRscUrl(url).replace(/\\.rsc$/, '.params.json');\n}\n\n// ─── Reload Signal ───────────────────────────────────────────────\n\n/** Header name used by the server to signal a version skew reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n/** Header name for the client's deployment ID. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/**\n * Check if a response signals a version skew reload.\n * Triggers a full page reload if the server indicates the client is stale.\n */\nexport function checkReloadSignal(response: Response): boolean {\n return response.headers.get(RELOAD_HEADER) === '1';\n}\n\n// ─── Header Builder ──────────────────────────────────────────────\n\nexport function buildRscHeaders(\n stateTree: { segments: string[] } | undefined,\n currentUrl?: string\n): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: RSC_CONTENT_TYPE,\n };\n if (stateTree) {\n headers['X-Timber-State-Tree'] = JSON.stringify(stateTree);\n }\n // Send current URL for intercepting route resolution.\n // The server uses this to determine if an intercepting route should\n // render instead of the actual target route (modal pattern).\n // See design/07-routing.md §\"Intercepting Routes\"\n if (currentUrl) {\n headers['X-Timber-URL'] = currentUrl;\n }\n // Send deployment ID for version skew detection (TIM-446).\n // The server compares this against the current build's ID.\n // On mismatch, the server signals a reload instead of returning\n // an RSC payload with mismatched module references.\n if (clientDeploymentId) {\n headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;\n }\n return headers;\n}\n\n// ─── Response Header Extraction ──────────────────────────────────\n\n/** Dev-only warning for malformed framework headers. Tree-shaken in production. */\nfunction warnMalformedHeader(headerName: string, raw: string): void {\n if (process.env.NODE_ENV !== 'production') {\n const preview = raw.length > 200 ? raw.slice(0, 200) + '…' : raw;\n console.warn(\n `[timber] Malformed ${headerName} header \\u2014 JSON.parse failed. ` +\n `This indicates a framework bug or header corruption. Raw (first 200 chars): ${preview}`\n );\n }\n}\n\n/**\n * Extract segment metadata from the X-Timber-Segments response header.\n * Returns null if the header is missing or malformed.\n *\n * Format: JSON array of {path, isRequestDependent} objects describing the rendered\n * segment chain from root to leaf. Used to populate the client-side\n * segment cache for state tree diffing on subsequent navigations.\n */\nexport function extractSegmentInfo(response: Response): SegmentInfo[] | null {\n const header = response.headers.get('X-Timber-Segments');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n warnMalformedHeader('X-Timber-Segments', header);\n return null;\n }\n}\n\n/**\n * Extract skipped segment paths from the X-Timber-Skipped-Segments header.\n * Returns null if the header is missing or malformed.\n *\n * When the server skips sync layouts the client already has cached,\n * it sends this header listing the skipped segment paths (outermost first).\n * The client uses this to merge the partial payload with cached segments.\n */\nexport function extractSkippedSegments(response: Response): string[] | null {\n const header = response.headers.get('X-Timber-Skipped-Segments');\n if (!header) return null;\n try {\n const parsed = JSON.parse(header);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n warnMalformedHeader('X-Timber-Skipped-Segments', header);\n return null;\n }\n}\n\n/**\n * Extract route params from the X-Timber-Params response header.\n * Returns null if the header is missing or malformed.\n *\n * Used to populate useSegmentParams() after client-side navigation.\n */\nexport function extractParams(response: Response): Record<string, string | string[]> | null {\n const header = response.headers.get('X-Timber-Params');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n warnMalformedHeader('X-Timber-Params', header);\n return null;\n }\n}\n\n// ─── Redirect Error ──────────────────────────────────────────────\n\n/**\n * Thrown when an RSC payload response contains X-Timber-Redirect header.\n * Caught in navigate() to trigger a soft router navigation to the redirect target.\n */\nexport class RedirectError extends Error {\n readonly redirectUrl: string;\n constructor(url: string) {\n super(`Server redirect to ${url}`);\n this.redirectUrl = url;\n }\n}\n\n/**\n * Thrown when the server signals a version skew (X-Timber-Reload header).\n * Caught in navigate() to trigger a full page reload.\n * See TIM-446.\n */\nexport class VersionSkewError extends Error {\n constructor() {\n super('Version skew detected — server has been redeployed');\n }\n}\n\n/**\n * Thrown when the server returns an error for an RSC payload request.\n * The server sends X-Timber-Error header and a JSON body instead of a\n * broken RSC stream for any RenderError (4xx or 5xx). Caught in\n * navigate() to trigger a hard navigation so the server can render\n * the error page as HTML.\n *\n * See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n */\nexport class ServerErrorResponse extends Error {\n readonly status: number;\n readonly url: string;\n constructor(status: number, url: string) {\n super(`Server error ${status} during navigation to ${url}`);\n this.status = status;\n this.url = url;\n }\n}\n\n/**\n * Thrown when the RSC fetch response has a Content-Type that is not\n * text/x-component — e.g., a static asset (image, CSS, JS) served\n * for a same-origin URL that isn't a route. The response body is\n * cancelled immediately (headers-only cost). Caught in navigate()\n * to trigger a hard navigation; caught in prefetch() to store a\n * negative cache entry so click hard-navigates without a second fetch.\n *\n * See TIM-1231.\n */\nexport class NonRscResponse extends Error {\n readonly url: string;\n constructor(url: string) {\n super(`Non-RSC response for ${url}`);\n this.url = url;\n }\n}\n\n// ─── Stream Completion Tracking ───────────────────────────────────\n\n/**\n * Wrap a response body stream to track when it's fully consumed.\n * Returns a new body that passes all chunks through unchanged, plus\n * a `done` promise that resolves when the last chunk is read (or\n * rejects if the stream errors).\n *\n * Used to keep React transitions open for the full RSC stream\n * duration — createFromFetch's thenable resolves on shell arrival,\n * but we need stream completion for useOptimistic pending state.\n */\nfunction trackStreamCompletion(body: ReadableStream<Uint8Array>): {\n body: ReadableStream<Uint8Array>;\n done: Promise<void>;\n} {\n let resolveDone!: () => void;\n let rejectDone!: (e: unknown) => void;\n const done = new Promise<void>((res, rej) => {\n resolveDone = res;\n rejectDone = rej;\n });\n\n const reader = body.getReader();\n const tracked = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const result = await reader.read();\n if (result.done) {\n controller.close();\n resolveDone();\n } else {\n controller.enqueue(result.value);\n }\n } catch (error) {\n controller.error(error);\n rejectDone(error);\n }\n },\n cancel(reason) {\n reader.cancel(reason);\n resolveDone();\n },\n });\n\n return { body: tracked, done };\n}\n\n// ─── Fetch ───────────────────────────────────────────────────────\n\n/**\n * Fetch an RSC payload from the server. If a decodeRsc function is provided,\n * the response is decoded into a React element tree via createFromFetch.\n * Otherwise, the raw response text is returned (test mode).\n */\nexport async function fetchRscPayload(\n url: string,\n deps: RouterDeps,\n stateTree?: { segments: string[] },\n currentUrl?: string,\n signal?: AbortSignal\n): Promise<FetchResult> {\n // In static mode, fetch the pre-generated _rsc/*.rsc file directly\n // instead of the route URL with Accept headers. Static hosts ignore\n // Accept headers, so the route URL would return HTML. The _rsc param\n // is still appended for cache busting.\n const fetchTarget = staticMode ? toStaticRscUrl(url) : url;\n const rscUrl = appendRscParam(fetchTarget);\n const headers = buildRscHeaders(staticMode ? undefined : stateTree, currentUrl);\n if (deps.decodeRsc) {\n // Production path: use createFromFetch for streaming RSC decoding.\n // createFromFetch takes a Promise<Response> and progressively parses\n // the RSC Flight stream as chunks arrive.\n //\n // Intercept the response to read segment metadata before createFromFetch\n // consumes the body. Reading headers does NOT consume the body stream.\n const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n // In static mode, fetch the params sidecar in parallel. The .rsc\n // file has no HTTP headers, so params come from a .params.json\n // sidecar written at build time. See TIM-1246.\n const staticParamsFetch = staticMode\n ? deps\n .fetch(toStaticParamsUrl(url), { signal })\n .then((r) => (r.ok ? (r.json() as Promise<Record<string, string | string[]>>) : null))\n .catch((e) => {\n if (e instanceof DOMException && e.name === 'AbortError') throw e;\n return null;\n })\n : null;\n // Observe the sidecar promise so its rejection doesn't become\n // unhandled if wrappedPromise rejects first (TIM-1248).\n staticParamsFetch?.catch(() => {});\n let segmentInfo: SegmentInfo[] | null = null;\n let params: Record<string, string | string[]> | null = null;\n let skippedSegments: string[] | null = null;\n // Track when the full RSC body stream is consumed (not just shell).\n // Initialized to resolved for bodyless responses; overwritten when\n // the response has a body.\n let streamDone: Promise<void> = Promise.resolve();\n\n const wrappedPromise = fetchPromise.then((response) => {\n // Version skew detection (TIM-446): if the server signals a reload,\n // throw VersionSkewError so the caller (router navigate) can trigger\n // a full page reload.\n if (checkReloadSignal(response)) {\n throw new VersionSkewError();\n }\n // Detect server-side redirects. The server returns 204 + X-Timber-Redirect\n // for RSC payload requests instead of a raw 302, because fetch with\n // redirect: \"manual\" turns 302s into opaque redirects (status 0, null body)\n // which crashes createFromFetch when it tries to read the body stream.\n const redirectLocation =\n response.headers.get('X-Timber-Redirect') ||\n (response.status >= 300 && response.status < 400 ? response.headers.get('Location') : null);\n if (redirectLocation) {\n throw new RedirectError(redirectLocation);\n }\n // Detect server error responses. The server returns X-Timber-Error header\n // with a JSON body instead of a broken RSC stream for any RenderError\n // (4xx or 5xx). Hard-navigate so the server renders the error page as HTML.\n // See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n if (response.headers.get('X-Timber-Error') === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard: reject non-RSC responses before createFromFetch\n // tries to parse the body as Flight data.\n // In static mode, accept octet-stream/text/plain/absent content-type\n // (static hosts serve .rsc files with these), but still reject 404s\n // and text/html (missing .rsc file → host returns 404 page or SPA\n // HTML fallback). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const contentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (contentType && contentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n const contentType = response.headers.get('content-type');\n if (!contentType || !contentType.split(';')[0].trim().includes(RSC_CONTENT_TYPE)) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n // Metadata (<title>/<meta>/<link>) now rides the RSC Flight payload\n // as React elements — React 19 Float handles them. See TIM-1151.\n segmentInfo = extractSegmentInfo(response);\n params = extractParams(response);\n skippedSegments = extractSkippedSegments(response);\n\n // Wrap the body to track full stream consumption. createFromFetch's\n // thenable resolves when the root model (shell) arrives, but we need\n // to know when ALL chunks are read so the React transition stays\n // open for the full streaming duration (keeps useOptimistic alive).\n if (response.body) {\n const tracked = trackStreamCompletion(response.body);\n streamDone = tracked.done;\n streamDone.catch(() => {}); // prevent unhandled rejection\n return new Response(tracked.body, {\n headers: response.headers,\n status: response.status,\n });\n }\n return response;\n });\n // Await headers so segmentInfo/params are populated.\n await wrappedPromise;\n // In static mode, params come from the sidecar (header extraction\n // returns null because .rsc files have no HTTP headers). TIM-1246.\n if (staticParamsFetch && !params) {\n params = await staticParamsFetch;\n }\n // Start decoding but do NOT await — return the in-progress thenable.\n // React can render a Flight thenable directly: it suspends on unresolved\n // parts and progressively renders as chunks arrive, spreading work across\n // frames instead of blocking the main thread in one burst.\n const payload = deps.decodeRsc(wrappedPromise);\n // Combine stream completion with payload error propagation.\n // streamDone keeps the transition open for the full RSC stream\n // duration (useOptimistic pending state). payloadError propagates\n // decode failures (stale client references, Flight parse errors)\n // so the router's catch block can trigger recovery (stale reload).\n const payloadError = new Promise<void>((_, reject) => {\n Promise.resolve(payload).then(() => {}, reject);\n });\n payloadError.catch(() => {});\n const decodePromise = Promise.race([streamDone, payloadError]);\n return {\n payload,\n decodePromise,\n segmentInfo,\n params,\n skippedSegments,\n };\n }\n // Test/fallback path: return raw text\n const response = await deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n // Check for redirect in test path too\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('Location');\n if (location) {\n throw new RedirectError(location);\n }\n }\n // Server error guard (same as production path above).\n if (response.headers.get('X-Timber-Error') === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard (same as production path above). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const fallbackContentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (fallbackContentType &&\n fallbackContentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n const fallbackContentType = response.headers.get('content-type');\n if (\n !fallbackContentType ||\n !fallbackContentType.split(';')[0].trim().includes(RSC_CONTENT_TYPE)\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n let fallbackParams = extractParams(response);\n // In static mode, params come from the sidecar. TIM-1246.\n if (staticMode && !fallbackParams) {\n try {\n const paramsResponse = await deps.fetch(toStaticParamsUrl(url), { signal });\n if (paramsResponse.ok) {\n fallbackParams = (await paramsResponse.json()) as Record<string, string | string[]>;\n }\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') throw e;\n // No params sidecar — non-dynamic route, params stay null\n }\n }\n return {\n payload: await response.text(),\n decodePromise: null,\n segmentInfo: extractSegmentInfo(response),\n params: fallbackParams,\n skippedSegments: extractSkippedSegments(response),\n };\n}\n","// Segment Router — manages client-side navigation and RSC payload fetching\n// See design/19-client-navigation.md for the full architecture.\n\nimport { SegmentCache, PrefetchCache, buildSegmentTree } from './segment-cache';\nimport type { SegmentInfo } from './segment-cache';\nimport { HistoryStack } from './history';\nimport { setCurrentParams } from './use-segment-params.js';\nimport {\n setNavigationState,\n getNavigationState,\n type NavigationState,\n} from './navigation-context.js';\n\nimport {\n fetchRscPayload,\n RedirectError,\n ServerErrorResponse,\n VersionSkewError,\n NonRscResponse,\n} from './rsc-fetch.js';\nimport { setHardNavigating, supersedeNavigationTransitions } from './navigation-root.js';\nimport type { FetchResult } from './rsc-fetch.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface NavigationOptions {\n /** Set to false to prevent scroll-to-top on forward navigation */\n scroll?: boolean;\n /** Use replaceState instead of pushState (replaces current history entry) */\n replace?: boolean;\n /**\n * @internal AbortSignal from the Navigation API's NavigateEvent.\n * When provided, the signal is linked to the router's per-navigation\n * AbortController so in-flight RSC fetches are cancelled when a new\n * navigation starts.\n */\n _signal?: AbortSignal;\n /**\n * @internal Skip pushState/replaceState — the Navigation API has already\n * updated the URL via event.intercept(). Used for external navigations\n * intercepted by the navigate event handler.\n */\n _skipHistory?: boolean;\n /**\n * @internal The URL the user is navigating FROM, captured before the\n * Navigation API commits the destination. Sent as X-Timber-URL for\n * slot skip comparison on the server (TIM-1232).\n */\n _departingUrl?: string;\n}\n\n/**\n * Function that decodes an RSC Flight stream into a React element tree.\n * In production: createFromFetch from @vitejs/plugin-rsc/browser.\n * In tests: a mock that returns the raw payload.\n */\nexport type RscDecoder = (fetchPromise: Promise<Response>) => unknown;\n\n/**\n * Function that renders a decoded RSC element tree into the DOM.\n * In production: reactRoot.render(element).\n * In tests: a no-op or mock.\n *\n * Receives the current NavigationState explicitly — no temporal\n * coupling with setNavigationState/getNavigationState. The renderer\n * wraps the element in NavigationProvider with this state.\n */\nexport type RootRenderer = (element: unknown, navState: NavigationState) => void;\n\n/**\n * Platform dependencies injected for testability. In production these\n * map to browser APIs; in tests they're replaced with mocks.\n */\nexport interface RouterDeps {\n fetch: (url: string, init: RequestInit) => Promise<Response>;\n pushState: (data: unknown, unused: string, url: string) => void;\n replaceState: (data: unknown, unused: string, url: string) => void;\n scrollTo: (x: number, y: number) => void;\n getCurrentUrl: () => string;\n getScrollY: () => number;\n /** Decode RSC Flight stream into React elements. If not provided, raw response text is stored. */\n decodeRsc?: RscDecoder;\n /** Render decoded RSC tree into the DOM. If not provided, rendering is a no-op. */\n renderRoot?: RootRenderer;\n /**\n * Schedule a callback after the next paint. In the browser, this is\n * requestAnimationFrame + setTimeout(0) to run after React commits.\n * In tests, this runs the callback synchronously.\n */\n afterPaint?: (callback: () => void) => void;\n /**\n * Run a navigation inside a React transition with optimistic pending URL.\n * The pending URL shows immediately (useOptimistic urgent update) and\n * reverts when the transition commits (atomic with the new tree).\n *\n * The `perform` callback receives a `wrapPayload` function to wrap the\n * decoded RSC payload with NavigationProvider + NuqsAdapter before\n * NavigationRoot sets it as the new element. The `wrapPayload` function\n * receives the NavigationState explicitly — no temporal coupling with\n * getNavigationState().\n *\n * If not provided (tests), the router falls back to renderRoot.\n */\n navigateTransition?: (\n pendingUrl: string,\n perform: (\n wrapPayload: (\n payload: unknown,\n navState: NavigationState,\n segmentUpdates?: Map<string, unknown>\n ) => unknown\n ) => Promise<{ element: unknown; decodePromise: Promise<void> | null }>\n ) => Promise<void>;\n\n /**\n * Whether the Navigation API is active and handling traversals.\n * When true, the popstate handler is a no-op — the Navigation API's\n * navigate event covers back/forward button presses.\n */\n navigationApiActive?: boolean;\n\n /**\n * Called around pushState/replaceState to set a flag that prevents\n * the Navigation API's navigate listener from double-handling\n * router-initiated navigations.\n */\n setRouterNavigating?: (value: boolean) => void;\n\n /**\n * Save scroll position via the Navigation API's per-entry state.\n * When provided, used instead of history.replaceState for scroll storage.\n */\n saveNavigationEntryScroll?: (scrollY: number) => void;\n\n /**\n * Signal that a router-initiated navigation has completed. Resolves the\n * deferred promise that ties the browser's native loading state to the\n * navigation lifecycle. Called in the finally block of navigate/refresh,\n * aligned with when the TopLoader's pendingUrl clears.\n */\n completeRouterNavigation?: () => void;\n\n /**\n * Get the current unwrapped RSC payload element. Used for partial\n * navigation: the router re-wraps the same element with new\n * NavigationProvider context so mounted SegmentOutlets are preserved.\n */\n _getCurrentPayload?: () => unknown;\n\n /**\n * Initiate a navigation via the Navigation API (`navigation.navigate()`).\n * Fires the navigate event BEFORE committing the URL, allowing Chrome\n * to show its native loading indicator. Falls back to pushState when\n * unavailable.\n */\n navigationNavigate?: (url: string, replace: boolean) => void;\n\n /**\n * Scroll the element matching a URL #fragment into view. Returns true\n * when a matching element was found and scrolled. When absent or false,\n * the router falls back to scroll-to-top on forward navigation — same\n * as a full page load with an unknown fragment landing at the top.\n */\n scrollToHash?: (hash: string) => boolean;\n\n /**\n * Whether the client segment cache is enabled. When false (the default),\n * the router does not send X-Timber-State-Tree headers and does not\n * populate the segment cache. Every navigation gets a full RSC payload.\n */\n clientSegmentCache?: boolean;\n}\n\nexport interface RouterInstance {\n /** Navigate to a new URL (forward navigation) */\n navigate(url: string, options?: NavigationOptions): Promise<void>;\n /** Full re-render of the current URL — no state tree sent */\n refresh(): Promise<void>;\n /** Handle a popstate event (back/forward button). scrollY is read from history.state. */\n handlePopState(url: string, scrollY?: number, externalSignal?: AbortSignal): Promise<void>;\n /** Whether a navigation is currently in flight */\n isPending(): boolean;\n /** The URL currently being navigated to, or null if idle */\n getPendingUrl(): string | null;\n /** Subscribe to pending state changes */\n onPendingChange(listener: (pending: boolean) => void): () => void;\n /** Prefetch an RSC payload for a URL (used by Link hover) */\n prefetch(url: string): void;\n /**\n * Apply a piggybacked revalidation payload from a server action response.\n * Renders the element tree and updates head elements without a server fetch.\n * See design/08-forms-and-actions.md §\"Single-Roundtrip Revalidation\".\n */\n applyRevalidation(element: unknown): void;\n /**\n * Populate the segment cache from server-provided segment metadata.\n * Called on initial hydration with segment info embedded in the HTML.\n */\n initSegmentCache(segments: SegmentInfo[]): void;\n\n /** The segment cache (exposed for tests and <Link> prefetch) */\n segmentCache: SegmentCache;\n /** The prefetch cache (exposed for tests and <Link> prefetch) */\n prefetchCache: PrefetchCache;\n /** The history stack (exposed for tests) */\n historyStack: HistoryStack;\n}\n\n/**\n * Check if an error is an abort error (connection closed / fetch aborted).\n * Browsers throw DOMException with name 'AbortError' when a fetch is aborted.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof DOMException && error.name === 'AbortError') return true;\n if (error instanceof Error && error.name === 'AbortError') return true;\n return false;\n}\n\n// ─── Router Factory ──────────────────────────────────────────────\n\n/**\n * Create a router instance. In production, called once at app hydration\n * with real browser APIs. In tests, called with mock dependencies.\n */\n/**\n * Router navigation phase — discriminated union replacing scattered\n * `pending` + `pendingUrl` boolean flags.\n *\n * - `idle`: No navigation in flight. The committed params/pathname\n * are current.\n * - `navigating`: A fetch or render is in progress. `targetUrl` is\n * the destination being navigated to.\n */\nexport type RouterPhase = { phase: 'idle' } | { phase: 'navigating'; targetUrl: string };\n\nexport function createRouter(deps: RouterDeps): RouterInstance {\n const segmentCache = new SegmentCache();\n const prefetchCache = new PrefetchCache();\n const historyStack = new HistoryStack();\n let routerPhase: RouterPhase = { phase: 'idle' };\n const pendingListeners = new Set<(pending: boolean) => void>();\n\n // AbortController for the current in-flight navigation.\n // When a new navigation starts, the previous controller is aborted,\n // cancelling any in-progress RSC fetch. This provides automatic\n // cancellation of stale fetches regardless of Navigation API support.\n let currentNavAbort: AbortController | null = null;\n\n /**\n * Create a new AbortController for a navigation, superseding any\n * previous in-flight navigation. Optionally links to an external\n * signal (e.g., from the Navigation API's NavigateEvent.signal).\n *\n * Superseding is one operation with three parts:\n * 1. Abort the previous navigation's fetch.\n * 2. Invalidate its render transition so a response that already\n * arrived can't commit a stale tree (NavigationRoot's transId guard).\n * 3. Resolve its Navigation API deferred — the superseded navigation's\n * finally block is staleness-guarded (see TIM-1034) and no longer\n * cleans up after itself, so the browser's native loading state for\n * the dead navigation is cleared here.\n */\n function createNavAbort(externalSignal?: AbortSignal): AbortController {\n if (currentNavAbort) {\n currentNavAbort.abort();\n supersedeNavigationTransitions();\n deps.completeRouterNavigation?.();\n }\n const controller = new AbortController();\n currentNavAbort = controller;\n\n // If an external signal is provided (e.g., Navigation API),\n // forward its abort to our controller.\n if (externalSignal) {\n if (externalSignal.aborted) {\n controller.abort();\n } else {\n externalSignal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n }\n\n return controller;\n }\n\n function setPending(value: boolean, url?: string): void {\n const next: RouterPhase =\n value && url ? { phase: 'navigating', targetUrl: url } : { phase: 'idle' };\n // Skip no-op updates\n if (\n routerPhase.phase === next.phase &&\n (routerPhase.phase === 'idle' ||\n (routerPhase.phase === 'navigating' &&\n next.phase === 'navigating' &&\n routerPhase.targetUrl === next.targetUrl))\n ) {\n return;\n }\n routerPhase = next;\n // Notify external store listeners (non-React consumers).\n // React-facing pending state is handled by useOptimistic in\n // NavigationRoot via navigateTransition — not this function.\n for (const listener of pendingListeners) {\n listener(value);\n }\n }\n\n /** Update the segment cache from server-provided segment metadata. */\n function updateSegmentCache(segmentInfo: SegmentInfo[] | null | undefined): void {\n if (!deps.clientSegmentCache) return;\n if (!segmentInfo || segmentInfo.length === 0) return;\n const tree = buildSegmentTree(segmentInfo);\n if (tree) {\n segmentCache.set('/', tree);\n }\n }\n\n /** Render a decoded RSC payload into the DOM if a renderer is available. */\n function renderPayload(payload: unknown, navState: NavigationState): void {\n if (deps.renderRoot) {\n deps.renderRoot(payload, navState);\n }\n }\n\n /**\n * Atomically update all navigation-owned state for a new page. Every\n * code path that changes the \"current page\" must go through this\n * function — making \"forgot a field\" impossible by construction.\n *\n * The three operations:\n * 1. Segment cache — update from server-provided segment metadata\n * 2. Navigation state — params + pathname for useSegmentParams/usePathname\n * 3. History stack — store the payload for instant back/forward replay\n */\n function commitNavigation(\n url: string,\n opts: {\n payload: unknown;\n params?: Record<string, string | string[]> | null;\n segmentInfo?: SegmentInfo[] | null;\n /** When true, clear the segment cache if segmentInfo is empty\n * (popstate replay for entries without layout metadata). */\n clearSegmentCacheOnEmpty?: boolean;\n }\n ): NavigationState {\n if (opts.segmentInfo && opts.segmentInfo.length > 0) {\n updateSegmentCache(opts.segmentInfo);\n } else if (opts.clearSegmentCacheOnEmpty) {\n segmentCache.clear();\n }\n\n const navState = updateNavigationState(opts.params, url);\n\n historyStack.push(url, {\n payload: opts.payload,\n params: navState.params,\n segmentInfo: opts.segmentInfo,\n });\n\n return navState;\n }\n\n /**\n * Wrap a navigation in the standard abort/pending/cleanup lifecycle.\n * Consolidates the createNavAbort + setPending + staleness-guarded\n * finally that was duplicated across navigate, refresh, and both\n * handlePopState paths. AbortErrors are swallowed (not application\n * errors); all other errors propagate to the caller.\n */\n async function runNavigation(\n url: string,\n fn: (navAbort: AbortController) => Promise<void>,\n externalSignal?: AbortSignal\n ): Promise<void> {\n const navAbort = createNavAbort(externalSignal);\n setPending(true, url);\n try {\n await fn(navAbort);\n } catch (error) {\n if (isAbortError(error)) return;\n throw error;\n } finally {\n if (currentNavAbort === navAbort) {\n currentNavAbort = null;\n setPending(false);\n deps.completeRouterNavigation?.();\n }\n }\n }\n\n /**\n * Resolve thenable payloads in the test/fallback path (no navigateTransition).\n * In production, React handles thenables from createFromFetch directly via\n * Suspense. In tests, renderRoot is a plain mock that expects resolved values.\n */\n async function resolveForFallback(payload: unknown): Promise<unknown> {\n if (\n !deps.navigateTransition &&\n payload != null &&\n typeof payload === 'object' &&\n 'then' in payload\n ) {\n return await (payload as PromiseLike<unknown>);\n }\n return payload;\n }\n\n function isPartialNavigation(skippedSegments: string[] | null | undefined): boolean {\n return skippedSegments != null && skippedSegments.length > 0;\n }\n\n /**\n * Build a segment updates map for partial navigation. Identifies the\n * first non-skipped segment and maps it to the payload content.\n */\n function buildSegmentUpdates(result: FetchResult): Map<string, unknown> {\n const skipped = new Set(result.skippedSegments!);\n const segmentInfo = result.segmentInfo;\n const updates = new Map<string, unknown>();\n if (segmentInfo) {\n for (const info of segmentInfo) {\n if (!skipped.has(info.segmentId ?? info.path)) {\n updates.set(info.segmentId ?? info.path, result.payload);\n break;\n }\n }\n }\n return updates;\n }\n\n /**\n * Update navigation state (params + pathname) for the next render.\n *\n * Sets the module-level fallback (for tests and SSR) and the\n * globalThis bridge, then returns the NavigationState so callers\n * can pass it explicitly to renderRoot/wrapPayload — eliminating\n * temporal coupling with getNavigationState().\n */\n function updateNavigationState(\n params: Record<string, string | string[]> | null | undefined,\n url: string\n ): NavigationState {\n const resolvedParams = params ?? {};\n // Module-level fallback for tests (no NavigationProvider) and SSR\n setCurrentParams(resolvedParams);\n // globalThis bridge — kept for backward compat\n const parsed = new URL(url, 'http://localhost');\n const pathname = parsed.pathname || '/';\n const search = parsed.search;\n const navState: NavigationState = { params: resolvedParams, pathname, search };\n setNavigationState(navState);\n return navState;\n }\n\n /**\n * Render a payload via navigateTransition (production) or renderRoot (tests).\n * The perform callback should fetch data, call commitNavigation, and return\n * the FetchResult plus the NavigationState.\n *\n * State management (segmentCache, navState, historyStack) is handled by\n * commitNavigation inside perform — this function only handles rendering.\n */\n async function renderViaTransition(\n url: string,\n perform: () => Promise<FetchResult & { navState: NavigationState }>\n ): Promise<void> {\n if (deps.navigateTransition) {\n await deps.navigateTransition(url, async (wrapPayload) => {\n const result = await perform();\n\n if (isPartialNavigation(result.skippedSegments)) {\n const segmentUpdates = buildSegmentUpdates(result);\n\n // Re-wrap the CURRENT element with new context values.\n // SegmentOutlets read updates from SegmentUpdateContext;\n // NavigationProvider gets new params/pathname.\n const element = wrapPayload(\n deps._getCurrentPayload?.() ?? result.payload,\n result.navState,\n segmentUpdates\n );\n return { element, decodePromise: result.decodePromise };\n }\n\n // Full navigation — empty updates, render the new tree.\n const element = wrapPayload(result.payload, result.navState);\n return { element, decodePromise: result.decodePromise };\n });\n return;\n }\n // Fallback: no transition (tests, no React tree)\n const result = await perform();\n if (!isPartialNavigation(result.skippedSegments)) {\n renderPayload(result.payload, result.navState);\n }\n }\n\n /** Run a callback after the next paint (after React commit). */\n function afterPaint(callback: () => void): void {\n if (deps.afterPaint) {\n deps.afterPaint(callback);\n } else {\n callback();\n }\n }\n\n /**\n * Schedule scroll restoration after the next paint and fire the\n * scroll-restored event. Used by navigate, popstate, and refresh.\n */\n function restoreScrollAfterPaint(scrollY: number): void {\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n /**\n * Scroll to the element matching the URL #fragment after paint, falling\n * back to scroll-to-top when no element matches (matching full-page-load\n * behavior for an unknown fragment). Used by forward navigation to a\n * hash-bearing URL (TIM-1035).\n */\n function scrollToHashAfterPaint(hash: string): void {\n afterPaint(() => {\n if (deps.scrollToHash?.(hash) !== true) {\n deps.scrollTo(0, 0);\n }\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n /**\n * Core navigation logic shared between the transition and fallback paths.\n * Fetches the RSC payload, updates all state, and returns the result.\n */\n async function performNavigationFetch(\n url: string,\n options: {\n replace: boolean;\n commitUrl?: string;\n signal?: AbortSignal;\n skipHistory?: boolean;\n departingUrl?: string;\n }\n ): Promise<FetchResult & { navState: NavigationState }> {\n // Check prefetch cache first. A negative entry means a prior prefetch\n // determined this URL is not a route (non-RSC Content-Type). Hard-navigate\n // immediately without a second fetch. See TIM-1231.\n if (prefetchCache.isNegative(url)) {\n prefetchCache.consume(url);\n throw new NonRscResponse(url);\n }\n\n // PrefetchResult has optional segmentInfo/params fields — normalize\n // to null for FetchResult compatibility.\n const prefetched = prefetchCache.consume(url);\n let result: FetchResult | undefined = prefetched\n ? {\n payload: prefetched.payload,\n decodePromise: null,\n segmentInfo: prefetched.segmentInfo ?? null,\n params: prefetched.params ?? null,\n skippedSegments: prefetched.skippedSegments ?? null,\n }\n : undefined;\n\n if (result === undefined) {\n // Fetch RSC payload with state tree for partial rendering.\n // Send departing URL (pre-navigation) for slot skip comparison.\n // getCurrentUrl() can't be used here because the Navigation API\n // may have already committed the destination URL (TIM-1232).\n const stateTree = deps.clientSegmentCache ? segmentCache.serializeStateTree() : undefined;\n const rawDepartingUrl = options.departingUrl ?? deps.getCurrentUrl();\n const currentUrl = rawDepartingUrl.startsWith('http')\n ? new URL(rawDepartingUrl).pathname\n : new URL(rawDepartingUrl, 'http://localhost').pathname;\n result = await fetchRscPayload(url, deps, stateTree, currentUrl, options.signal);\n }\n\n // Update the browser history — skip when the Navigation API has already\n // updated the URL via event.intercept() (external navigations).\n // The committed URL keeps the #fragment (commitUrl) even though the\n // fetch/history-stack URL is hash-less (TIM-1035).\n if (!options.skipHistory) {\n const commitUrl = options.commitUrl ?? url;\n // Set the router-navigating flag so the Navigation API's navigate\n // listener doesn't double-intercept this pushState/replaceState.\n deps.setRouterNavigating?.(true);\n if (options.replace) {\n deps.replaceState({ timber: true, scrollY: 0 }, '', commitUrl);\n } else {\n deps.pushState({ timber: true, scrollY: 0 }, '', commitUrl);\n }\n deps.setRouterNavigating?.(false);\n }\n\n // Resolve thenable payloads in the test path so popstate replay\n // and renderPayload receive plain values (see resolveForFallback).\n const payload = await resolveForFallback(result.payload);\n\n // Atomically update all navigation state via commitNavigation.\n // Partial navigations and slot-skip navigations store null payload —\n // the RSC tree contains skip holes that can't be replayed standalone;\n // popstate will fetch fresh.\n const isPartial = isPartialNavigation(result.skippedSegments);\n const hasSkippedSlots = result.segmentInfo?.some((s) => s.slot && s.skipped) ?? false;\n const navState = commitNavigation(url, {\n payload: isPartial || hasSkippedSlots ? null : payload,\n params: result.params,\n segmentInfo: result.segmentInfo,\n });\n\n return { ...result, payload, navState };\n }\n\n async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {\n const scroll = options.scroll !== false;\n const replace = options.replace === true;\n const externalSignal = options._signal as AbortSignal | undefined;\n const skipHistory = options._skipHistory === true;\n\n // Split the #fragment off the navigation URL (TIM-1035). The full URL\n // (with hash) is committed to the address bar; the hash-less URL is used\n // for the RSC fetch (fragments are client-only — keeping it would also\n // swallow the ?_rsc cache-bust param into the fragment) and for\n // history-stack/prefetch keys (popstate lookups use pathname + search).\n const hashIndex = url.indexOf('#');\n const hash = hashIndex === -1 ? '' : url.slice(hashIndex);\n const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);\n\n // Use the pre-intercept departing URL when the Navigation API has already\n // committed the destination (_departingUrl from navigation-api.ts). Otherwise\n // capture it now — getCurrentUrl() is still the departing URL at this point\n // for non-Navigation-API navigations (TIM-1232).\n const departingUrl = options._departingUrl ?? deps.getCurrentUrl();\n\n // Capture the departing page's scroll position for scroll={false} preservation.\n const currentScrollY = deps.getScrollY();\n\n // Save the departing page's scroll position — use Navigation API entry\n // state when available, otherwise fall back to history.state.\n if (deps.saveNavigationEntryScroll) {\n deps.saveNavigationEntryScroll(currentScrollY);\n } else {\n deps.replaceState({ timber: true, scrollY: currentScrollY }, '', deps.getCurrentUrl());\n }\n\n let effectiveSkipHistory = skipHistory;\n\n await runNavigation(\n url,\n async (navAbort) => {\n // When Navigation API is active, initiate the navigation via\n // navigation.navigate() BEFORE the fetch. Must happen after\n // createNavAbort supersedes the previous navigation (done by\n // runNavigation) so the old deferred is resolved first.\n if (!effectiveSkipHistory && deps.navigationNavigate) {\n deps.setRouterNavigating?.(true);\n deps.navigationNavigate(url, replace);\n deps.setRouterNavigating?.(false);\n effectiveSkipHistory = true;\n }\n\n try {\n await renderViaTransition(fetchUrl, () =>\n performNavigationFetch(fetchUrl, {\n replace,\n commitUrl: url,\n signal: navAbort.signal,\n skipHistory: effectiveSkipHistory,\n departingUrl,\n })\n );\n\n // Notify nuqs adapter (and any other listeners) that navigation completed.\n window.dispatchEvent(new Event('timber:navigation-end'));\n\n // Scroll-to-top on forward navigation, scroll to the #fragment target\n // when the URL has one, or restore captured position for scroll={false}.\n if (scroll && hash) {\n scrollToHashAfterPaint(hash);\n } else {\n restoreScrollAfterPaint(scroll ? 0 : currentScrollY);\n }\n } catch (error) {\n if (error instanceof VersionSkewError) {\n setHardNavigating(true);\n window.location.reload();\n await new Promise(() => {});\n }\n if (error instanceof RedirectError) {\n if (currentNavAbort !== navAbort) return;\n await navigate(error.redirectUrl, { replace: true });\n return;\n }\n if (error instanceof ServerErrorResponse) {\n setHardNavigating(true);\n // Use the original `url` (with #fragment), not error.url which\n // is hash-stripped. Preserves fragment on hard navigation.\n window.location.href = url;\n await new Promise(() => {});\n }\n if (error instanceof NonRscResponse) {\n setHardNavigating(true);\n // Use the original `url` (with #fragment), not error.url which\n // is hash-stripped. Preserves fragment on hard navigation (e.g.,\n // /manual.pdf#page=5). Matches no-JS <a> behavior.\n window.location.href = url;\n await new Promise(() => {});\n }\n throw error;\n }\n },\n externalSignal\n );\n }\n\n async function refresh(): Promise<void> {\n const currentUrl = deps.getCurrentUrl();\n\n await runNavigation(currentUrl, async (navAbort) => {\n await renderViaTransition(currentUrl, async () => {\n // No state tree sent — server renders the complete RSC payload\n const result = await fetchRscPayload(\n currentUrl,\n deps,\n undefined,\n undefined,\n navAbort.signal\n );\n const payload = await resolveForFallback(result.payload);\n const navState = commitNavigation(currentUrl, {\n payload,\n params: result.params,\n segmentInfo: result.segmentInfo,\n });\n return { ...result, payload, navState };\n });\n });\n }\n\n async function handlePopState(\n url: string,\n scrollY: number = 0,\n externalSignal?: AbortSignal\n ): Promise<void> {\n // Scroll position is read from history.state by the caller (browser-entry.ts)\n // and passed in. This is more reliable than tracking scroll per-URL in memory\n // because the browser maintains per-entry state even with duplicate URLs.\n const entry = historyStack.get(url);\n\n if (entry && entry.payload !== null) {\n // Replay cached payload — no server roundtrip.\n //\n // runNavigation supersedes any in-flight forward navigation (TIM-1022):\n // aborts its fetch and invalidates its render transition so the stale\n // forward payload can't commit over this replay. The replay itself is\n // synchronous — the fn resolves immediately.\n await runNavigation(\n url,\n async () => {\n // clearSegmentCacheOnEmpty: popstate to an entry without layout\n // metadata (e.g., initial SSR page) clears the cache so the next\n // forward navigation gets a full render.\n const navState = commitNavigation(url, {\n payload: entry.payload,\n params: entry.params,\n segmentInfo: entry.segmentInfo,\n clearSegmentCacheOnEmpty: true,\n });\n renderPayload(entry.payload, navState);\n restoreScrollAfterPaint(scrollY);\n },\n externalSignal\n );\n } else {\n // No cached payload — fetch from server.\n // This happens when navigating back to the initial SSR'd page\n // (its payload is null since it was rendered via SSR, not RSC fetch)\n // or when the entry doesn't exist at all.\n await runNavigation(\n url,\n async (navAbort) => {\n await renderViaTransition(url, async () => {\n const stateTree = deps.clientSegmentCache\n ? segmentCache.serializeStateTree()\n : undefined;\n const result = await fetchRscPayload(url, deps, stateTree, undefined, navAbort.signal);\n const payload = await resolveForFallback(result.payload);\n const navState = commitNavigation(url, {\n payload,\n params: result.params,\n segmentInfo: result.segmentInfo,\n });\n return { ...result, payload, navState };\n });\n\n restoreScrollAfterPaint(scrollY);\n },\n externalSignal\n );\n }\n }\n\n /**\n * Prefetch an RSC payload for a URL and store it in the prefetch cache.\n * Called on hover of <Link prefetch> elements.\n */\n function prefetch(url: string): void {\n // Strip fragment — it's client-only and would swallow the _rsc cache-bust\n // param into the hash. The hash-less key also matches navigate()'s fetchUrl.\n const hashIndex = url.indexOf('#');\n const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);\n\n // Don't prefetch if already cached\n if (prefetchCache.get(fetchUrl) !== undefined) return;\n if (historyStack.has(fetchUrl)) return;\n\n // Fire-and-forget fetch\n const stateTree = deps.clientSegmentCache ? segmentCache.serializeStateTree() : undefined;\n void fetchRscPayload(fetchUrl, deps, stateTree).then(\n (result) => {\n result.decodePromise?.catch(() => {});\n prefetchCache.set(fetchUrl, result);\n },\n (error) => {\n if (error instanceof NonRscResponse) {\n prefetchCache.setNegative(fetchUrl);\n return;\n }\n // Prefetch failure is non-fatal — navigation will fetch fresh\n }\n );\n }\n\n return {\n navigate,\n refresh,\n handlePopState,\n isPending: () => routerPhase.phase === 'navigating',\n getPendingUrl: () => (routerPhase.phase === 'navigating' ? routerPhase.targetUrl : null),\n onPendingChange(listener) {\n pendingListeners.add(listener);\n return () => pendingListeners.delete(listener);\n },\n prefetch,\n applyRevalidation(element: unknown): void {\n // Render the piggybacked element tree from a server action response.\n // Updates the current history entry with the fresh payload —\n // same as refresh() but without a server fetch.\n const currentUrl = deps.getCurrentUrl();\n\n // Preserve existing segmentInfo so away-and-back navigation replays\n // with a correct segment cache (TIM-1037). Preserve current params\n // so dynamic route params aren't cleared to {}.\n const existingEntry = historyStack.get(currentUrl);\n const navState = commitNavigation(currentUrl, {\n payload: element,\n params: getNavigationState().params,\n segmentInfo: existingEntry?.segmentInfo,\n });\n renderPayload(element, navState);\n },\n initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),\n segmentCache,\n prefetchCache,\n historyStack,\n };\n}\n","/**\n * useSearchParams() — client-side hook for reading URL search params.\n *\n * Returns a read-only URLSearchParams instance reflecting the current\n * URL's query string. Updates when client-side navigation changes the URL.\n *\n * On the client, reads from NavigationContext which is updated atomically\n * with the RSC tree render during full navigations, AND by\n * syncShallowSearch() for shallow URL updates (nuqs shallow: true,\n * replaceUrl, or any external pushState/replaceState that changes the\n * query string). See router-init.ts.\n *\n * This replaces the previous useSyncExternalStore approach which read\n * window.location.search directly — causing React to detect external\n * store tearing during transitions and fall back to synchronous rendering\n * (renderRootSync instead of renderRootConcurrent), blocking the main\n * thread and freezing animations.\n *\n * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard\n * URLSearchParams. Mutation methods (set, delete, append) work on the\n * local copy but do NOT affect the URL — use the router or nuqs for that.\n *\n * During SSR, reads the request search params from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * Compatible with Next.js's `useSearchParams()` from `next/navigation`.\n */\n\nimport { getSsrData } from './ssr-data.js';\nimport { useNavigationContext } from './navigation-context.js';\nimport { cachedSearch, cachedSearchParams, _setCachedSearch } from './state.js';\n\nfunction getSearchParams(search: string): URLSearchParams {\n if (search !== cachedSearch) {\n const params = new URLSearchParams(search);\n _setCachedSearch(search, params);\n return params;\n }\n return cachedSearchParams;\n}\n\n/**\n * Read the current URL search params.\n *\n * Compatible with Next.js's `useSearchParams()` from `next/navigation`.\n */\nexport function useSearchParams(): URLSearchParams {\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return getSearchParams(navContext.search);\n }\n } catch {\n // No React dispatcher available (called outside a component).\n }\n\n // SSR path: read from ALS-backed SSR data context.\n const ssrData = getSsrData();\n if (ssrData) return new URLSearchParams(ssrData.searchParams);\n\n // Final fallback: window.location (tests, edge cases).\n if (typeof window !== 'undefined') return getSearchParams(window.location.search);\n return new URLSearchParams();\n}\n"],"mappings":";;;;;;;;;;;;;AAyDA,IAAa,eAAb,MAA0B;CACxB;CAEA,IAAI,SAA0C;EAC5C,IAAI,YAAY,OAAO,YAAY,KAAK,MAAM,SAC5C,OAAO,KAAK;CAGhB;CAEA,IAAI,SAAiB,MAAyB;EAC5C,IAAI,YAAY,OAAO,CAAC,KAAK,MAC3B,KAAK,OAAO;CAEhB;CAEA,QAAc;EACZ,KAAK,OAAO,KAAA;CACd;;;;;;;;;;;;;;CAeA,mBAAmB,iBAA0C;EAC3D,MAAM,WAAqB,CAAC;EAC5B,MAAM,QAAkB,CAAC;EACzB,IAAI,KAAK,MAAM;GACb,oBAAoB,KAAK,MAAM,UAAU,eAAe;GACxD,iBAAiB,KAAK,MAAM,KAAK;EACnC;EACA,MAAM,OAAkB,EAAE,SAAS;EACnC,IAAI,MAAM,SAAS,GACjB,KAAK,QAAQ;EAEf,OAAO;CACT;AACF;;AAGA,SAAS,oBACP,MACA,KACA,iBACM;CACN,IAAI,CAAC,KAAK,uBAAuB,CAAC,mBAAmB,gBAAgB,IAAI,KAAK,OAAO,IACnF,IAAI,KAAK,KAAK,OAAO;CAEvB,KAAK,MAAM,SAAS,KAAK,SAAS,OAAO,GACvC,oBAAoB,OAAO,KAAK,eAAe;AAEnD;;AAGA,SAAS,iBAAiB,MAAmB,KAAqB;CAChE,IAAI,KAAK;OACF,MAAM,QAAQ,KAAK,MAAM,OAAO,GAGnC,IAAI,CAAC,KAAK,sBAAsB,CAAC,KAAK,QACpC,IAAI,KAAK,KAAK,OAAO;CAAA;CAI3B,KAAK,MAAM,SAAS,KAAK,SAAS,OAAO,GACvC,iBAAiB,OAAO,GAAG;AAE/B;;;;;;;;;;;;AAkCA,SAAgB,iBAAiB,UAAkD;CAEjF,IAAI,SAAS,WAAW,GAAG,OAAO,KAAA;CAIlC,MAAM,iBAAgC,CAAC;CACvC,MAAM,cAA6B,CAAC;CACpC,KAAK,MAAM,QAAQ,UACjB,IAAI,KAAK,MACP,YAAY,KAAK,IAAI;MAErB,eAAe,KAAK,IAAI;CAK5B,IAAI;CACJ,IAAI;CACJ,MAAM,2BAAW,IAAI,IAAyB;CAE9C,KAAK,MAAM,QAAQ,gBAAgB;EACjC,MAAM,KAAK,KAAK,aAAa,KAAK;EAClC,MAAM,OAAoB;GACxB,SAAS;GACT,SAAS;GACT,oBAAoB,KAAK;GACzB,0BAAU,IAAI,IAAI;EACpB;EAEA,SAAS,IAAI,IAAI,IAAI;EAErB,IAAI,CAAC,MACH,OAAO;EAGT,IAAI,QACF,OAAO,SAAS,IAAI,IAAI,IAAI;EAG9B,SAAS;CACX;CAGA,KAAK,MAAM,YAAY,aAAa;EAClC,MAAM,WAAW,SAAS;EAC1B,MAAM,aAAa,WAAW,SAAS,IAAI,QAAQ,IAAI;EACvD,IAAI,CAAC,YAAY;EAEjB,MAAM,SAAS,SAAS,aAAa,SAAS;EAC9C,MAAM,WAAwB;GAC5B,SAAS;GACT,SAAS;GACT,oBAAoB,SAAS;GAC7B,0BAAU,IAAI,IAAI;GAClB,QAAQ,SAAS;EACnB;EAEA,IAAI,CAAC,WAAW,OACd,WAAW,wBAAQ,IAAI,IAAI;EAE7B,WAAW,MAAM,IAAI,QAAQ,QAAQ;CACvC;CAEA,OAAO;AACT;;AAUA,IAAM,iBAAiC,OAAO,OAAO,EAAE,SAAS,KAAK,CAAC;;;;;;;;;AAUtE,IAAa,gBAAb,MAAa,cAAc;CACzB,OAAwB,SAAS;CACjC,0BAAkB,IAAI,IAA2B;CAEjD,IAAI,KAAa,QAA8B;EAC7C,KAAK,QAAQ,IAAI,KAAK;GACpB;GACA,WAAW,KAAK,IAAI,IAAI,cAAc;EACxC,CAAC;CACH;CAEA,IAAI,KAAyC;EAC3C,MAAM,QAAQ,KAAK,QAAQ,IAAI,GAAG;EAClC,IAAI,CAAC,OAAO,OAAO,KAAA;EACnB,IAAI,KAAK,IAAI,KAAK,MAAM,WAAW;GACjC,KAAK,QAAQ,OAAO,GAAG;GACvB;EACF;EACA,OAAO,MAAM;CACf;;CAGA,QAAQ,KAAyC;EAC/C,MAAM,SAAS,KAAK,IAAI,GAAG;EAC3B,IAAI,WAAW,KAAA,GACb,KAAK,QAAQ,OAAO,GAAG;EAEzB,OAAO;CACT;;CAGA,YAAY,KAAmB;EAC7B,KAAK,IAAI,KAAK,cAAc;CAC9B;;CAGA,WAAW,KAAsB;EAE/B,OADc,KAAK,IAAI,GAChB,MAAU;CACnB;AACF;;;;;;;;;;;;;;;;;;;ACtPA,IAAa,eAAb,MAA0B;CACxB,0BAAkB,IAAI,IAA0B;CAEhD,KAAK,KAAa,OAA2B;EAC3C,KAAK,QAAQ,IAAI,KAAK,KAAK;CAC7B;CAEA,IAAI,KAAuC;EACzC,OAAO,KAAK,QAAQ,IAAI,GAAG;CAC7B;CAEA,IAAI,KAAsB;EACxB,OAAO,KAAK,QAAQ,IAAI,GAAG;CAC7B;AACF;;;ACrBA,IAAa,mBAAmB;;;;;AAQhC,SAAS,sBAA8B;CACrC,MAAM,QAAQ;CACd,IAAI,KAAK;CACT,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,MAAM,MAAO,KAAK,OAAO,IAAI,KAAM;CAErC,OAAO;AACT;;;;;;;;;AAUA,SAAS,eAAe,KAAqB;CAC3C,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,MAAM,iBAAiB,cAAc,KAAK,MAAM,IAAI,MAAM,GAAG,SAAS;CAEtE,OAAO,GAAG,iBADQ,eAAe,SAAS,GAAG,IAAI,MAAM,IAClB,OAAO,oBAAoB;AAClE;;;;;;AASA,IAAI,qBAAoC;;;;;;;AAoBxC,IAAI,aAAa;;;;;;;;;AAkBjB,SAAS,eAAe,KAAqB;CAC3C,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,MAAM,aAAa,IAAI,QAAQ,GAAG;CAClC,MAAM,UAAU,cAAc,KAAK,IAAI,SAAS;CAChD,MAAM,WAAW,eAAe,KAAK,IAAI,SAAS;CAClD,MAAM,MAAM,KAAK,IAAI,SAAS,QAAQ;CACtC,IAAI,WAAW,IAAI,MAAM,GAAG,GAAG;CAE/B,IAAI,SAAS,SAAS,KAAK,SAAS,SAAS,GAAG,GAC9C,WAAW,SAAS,MAAM,GAAG,EAAE;CAGjC,OAAO,QADS,aAAa,MAAM,WAAW,SACvB;AACzB;;;;;;AAOA,SAAS,kBAAkB,KAAqB;CAC9C,OAAO,eAAe,GAAG,CAAC,CAAC,QAAQ,UAAU,cAAc;AAC7D;;AAKA,IAAa,gBAAgB;;AAG7B,IAAa,uBAAuB;;;;;AAMpC,SAAgB,kBAAkB,UAA6B;CAC7D,OAAO,SAAS,QAAQ,IAAI,aAAa,MAAM;AACjD;AAIA,SAAgB,gBACd,WACA,YACwB;CACxB,MAAM,UAAkC,EACtC,QAAQ,iBACV;CACA,IAAI,WACF,QAAQ,yBAAyB,KAAK,UAAU,SAAS;CAM3D,IAAI,YACF,QAAQ,kBAAkB;CAM5B,IAAI,oBACF,QAAQ,wBAAwB;CAElC,OAAO;AACT;;AAKA,SAAS,oBAAoB,YAAoB,KAAmB;CAClE,IAAA,QAAA,IAAA,aAA6B,cAAc;EACzC,MAAM,UAAU,IAAI,SAAS,MAAM,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM;EAC7D,QAAQ,KACN,sBAAsB,WAAW,gHACgD,SACnF;CACF;AACF;;;;;;;;;AAUA,SAAgB,mBAAmB,UAA0C;CAC3E,MAAM,SAAS,SAAS,QAAQ,IAAI,mBAAmB;CACvD,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI;EACF,OAAO,KAAK,MAAM,MAAM;CAC1B,QAAQ;EACN,oBAAoB,qBAAqB,MAAM;EAC/C,OAAO;CACT;AACF;;;;;;;;;AAUA,SAAgB,uBAAuB,UAAqC;CAC1E,MAAM,SAAS,SAAS,QAAQ,IAAI,2BAA2B;CAC/D,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,MAAM;EAChC,OAAO,MAAM,QAAQ,MAAM,IAAI,SAAS;CAC1C,QAAQ;EACN,oBAAoB,6BAA6B,MAAM;EACvD,OAAO;CACT;AACF;;;;;;;AAQA,SAAgB,cAAc,UAA8D;CAC1F,MAAM,SAAS,SAAS,QAAQ,IAAI,iBAAiB;CACrD,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI;EACF,OAAO,KAAK,MAAM,MAAM;CAC1B,QAAQ;EACN,oBAAoB,mBAAmB,MAAM;EAC7C,OAAO;CACT;AACF;;;;;AAQA,IAAa,gBAAb,cAAmC,MAAM;CACvC;CACA,YAAY,KAAa;EACvB,MAAM,sBAAsB,KAAK;EACjC,KAAK,cAAc;CACrB;AACF;;;;;;AAOA,IAAa,mBAAb,cAAsC,MAAM;CAC1C,cAAc;EACZ,MAAM,oDAAoD;CAC5D;AACF;;;;;;;;;;AAWA,IAAa,sBAAb,cAAyC,MAAM;CAC7C;CACA;CACA,YAAY,QAAgB,KAAa;EACvC,MAAM,gBAAgB,OAAO,wBAAwB,KAAK;EAC1D,KAAK,SAAS;EACd,KAAK,MAAM;CACb;AACF;;;;;;;;;;;AAYA,IAAa,iBAAb,cAAoC,MAAM;CACxC;CACA,YAAY,KAAa;EACvB,MAAM,wBAAwB,KAAK;EACnC,KAAK,MAAM;CACb;AACF;;;;;;;;;;;AAcA,SAAS,sBAAsB,MAG7B;CACA,IAAI;CACJ,IAAI;CACJ,MAAM,OAAO,IAAI,SAAe,KAAK,QAAQ;EAC3C,cAAc;EACd,aAAa;CACf,CAAC;CAED,MAAM,SAAS,KAAK,UAAU;CAsB9B,OAAO;EAAE,MAAM,IArBK,eAA2B;GAC7C,MAAM,KAAK,YAAY;IACrB,IAAI;KACF,MAAM,SAAS,MAAM,OAAO,KAAK;KACjC,IAAI,OAAO,MAAM;MACf,WAAW,MAAM;MACjB,YAAY;KACd,OACE,WAAW,QAAQ,OAAO,KAAK;IAEnC,SAAS,OAAO;KACd,WAAW,MAAM,KAAK;KACtB,WAAW,KAAK;IAClB;GACF;GACA,OAAO,QAAQ;IACb,OAAO,OAAO,MAAM;IACpB,YAAY;GACd;EACF,CAEe;EAAS;CAAK;AAC/B;;;;;;AASA,eAAsB,gBACpB,KACA,MACA,WACA,YACA,QACsB;CAMtB,MAAM,SAAS,eADK,aAAa,eAAe,GAAG,IAAI,GACd;CACzC,MAAM,UAAU,gBAAgB,aAAa,KAAA,IAAY,WAAW,UAAU;CAC9E,IAAI,KAAK,WAAW;EAOlB,MAAM,eAAe,KAAK,MAAM,QAAQ;GAAE;GAAS,UAAU;GAAU;EAAO,CAAC;EAI/E,MAAM,oBAAoB,aACtB,KACG,MAAM,kBAAkB,GAAG,GAAG,EAAE,OAAO,CAAC,CAAC,CACzC,MAAM,MAAO,EAAE,KAAM,EAAE,KAAK,IAAmD,IAAK,CAAC,CACrF,OAAO,MAAM;GACZ,IAAI,aAAa,gBAAgB,EAAE,SAAS,cAAc,MAAM;GAChE,OAAO;EACT,CAAC,IACH;EAGJ,mBAAmB,YAAY,CAAC,CAAC;EACjC,IAAI,cAAoC;EACxC,IAAI,SAAmD;EACvD,IAAI,kBAAmC;EAIvC,IAAI,aAA4B,QAAQ,QAAQ;EAEhD,MAAM,iBAAiB,aAAa,MAAM,aAAa;GAIrD,IAAI,kBAAkB,QAAQ,GAC5B,MAAM,IAAI,iBAAiB;GAM7B,MAAM,mBACJ,SAAS,QAAQ,IAAI,mBAAmB,MACvC,SAAS,UAAU,OAAO,SAAS,SAAS,MAAM,SAAS,QAAQ,IAAI,UAAU,IAAI;GACxF,IAAI,kBACF,MAAM,IAAI,cAAc,gBAAgB;GAM1C,IAAI,SAAS,QAAQ,IAAI,gBAAgB,MAAM,KAC7C,MAAM,IAAI,oBAAoB,SAAS,QAAQ,GAAG;GAQpD,IAAI,YAAY;IACd,MAAM,cAAc,SAAS,QAAQ,IAAI,cAAc;IACvD,IACE,CAAC,SAAS,MACT,eAAe,YAAY,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,MAAM,aACnE;KACA,SAAS,MAAM,OAAO;KACtB,MAAM,IAAI,eAAe,GAAG;IAC9B;GACF,OAAO;IACL,MAAM,cAAc,SAAS,QAAQ,IAAI,cAAc;IACvD,IAAI,CAAC,eAAe,CAAC,YAAY,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,SAAA,kBAAyB,GAAG;KAChF,SAAS,MAAM,OAAO;KACtB,MAAM,IAAI,eAAe,GAAG;IAC9B;GACF;GAGA,cAAc,mBAAmB,QAAQ;GACzC,SAAS,cAAc,QAAQ;GAC/B,kBAAkB,uBAAuB,QAAQ;GAMjD,IAAI,SAAS,MAAM;IACjB,MAAM,UAAU,sBAAsB,SAAS,IAAI;IACnD,aAAa,QAAQ;IACrB,WAAW,YAAY,CAAC,CAAC;IACzB,OAAO,IAAI,SAAS,QAAQ,MAAM;KAChC,SAAS,SAAS;KAClB,QAAQ,SAAS;IACnB,CAAC;GACH;GACA,OAAO;EACT,CAAC;EAED,MAAM;EAGN,IAAI,qBAAqB,CAAC,QACxB,SAAS,MAAM;EAMjB,MAAM,UAAU,KAAK,UAAU,cAAc;EAM7C,MAAM,eAAe,IAAI,SAAe,GAAG,WAAW;GACpD,QAAQ,QAAQ,OAAO,CAAC,CAAC,WAAW,CAAC,GAAG,MAAM;EAChD,CAAC;EACD,aAAa,YAAY,CAAC,CAAC;EAE3B,OAAO;GACL;GACA,eAHoB,QAAQ,KAAK,CAAC,YAAY,YAAY,CAG1D;GACA;GACA;GACA;EACF;CACF;CAEA,MAAM,WAAW,MAAM,KAAK,MAAM,QAAQ;EAAE;EAAS,UAAU;EAAU;CAAO,CAAC;CAEjF,IAAI,SAAS,UAAU,OAAO,SAAS,SAAS,KAAK;EACnD,MAAM,WAAW,SAAS,QAAQ,IAAI,UAAU;EAChD,IAAI,UACF,MAAM,IAAI,cAAc,QAAQ;CAEpC;CAEA,IAAI,SAAS,QAAQ,IAAI,gBAAgB,MAAM,KAC7C,MAAM,IAAI,oBAAoB,SAAS,QAAQ,GAAG;CAGpD,IAAI,YAAY;EACd,MAAM,sBAAsB,SAAS,QAAQ,IAAI,cAAc;EAC/D,IACE,CAAC,SAAS,MACT,uBACC,oBAAoB,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,MAAM,aAC7D;GACA,SAAS,MAAM,OAAO;GACtB,MAAM,IAAI,eAAe,GAAG;EAC9B;CACF,OAAO;EACL,MAAM,sBAAsB,SAAS,QAAQ,IAAI,cAAc;EAC/D,IACE,CAAC,uBACD,CAAC,oBAAoB,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,SAAA,kBAAyB,GACnE;GACA,SAAS,MAAM,OAAO;GACtB,MAAM,IAAI,eAAe,GAAG;EAC9B;CACF;CACA,IAAI,iBAAiB,cAAc,QAAQ;CAE3C,IAAI,cAAc,CAAC,gBACjB,IAAI;EACF,MAAM,iBAAiB,MAAM,KAAK,MAAM,kBAAkB,GAAG,GAAG,EAAE,OAAO,CAAC;EAC1E,IAAI,eAAe,IACjB,iBAAkB,MAAM,eAAe,KAAK;CAEhD,SAAS,GAAG;EACV,IAAI,aAAa,gBAAgB,EAAE,SAAS,cAAc,MAAM;CAElE;CAEF,OAAO;EACL,SAAS,MAAM,SAAS,KAAK;EAC7B,eAAe;EACf,aAAa,mBAAmB,QAAQ;EACxC,QAAQ;EACR,iBAAiB,uBAAuB,QAAQ;CAClD;AACF;;;;;;;ACnWA,SAAS,aAAa,OAAyB;CAC7C,IAAI,iBAAiB,gBAAgB,MAAM,SAAS,cAAc,OAAO;CACzE,IAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc,OAAO;CAClE,OAAO;AACT;AAmBA,SAAgB,aAAa,MAAkC;CAC7D,MAAM,eAAe,IAAI,aAAa;CACtC,MAAM,gBAAgB,IAAI,cAAc;CACxC,MAAM,eAAe,IAAI,aAAa;CACtC,IAAI,cAA2B,EAAE,OAAO,OAAO;CAC/C,MAAM,mCAAmB,IAAI,IAAgC;CAM7D,IAAI,kBAA0C;;;;;;;;;;;;;;;CAgB9C,SAAS,eAAe,gBAA+C;EACrE,IAAI,iBAAiB;GACnB,gBAAgB,MAAM;GACtB,+BAA+B;GAC/B,KAAK,2BAA2B;EAClC;EACA,MAAM,aAAa,IAAI,gBAAgB;EACvC,kBAAkB;EAIlB,IAAI,gBACF,IAAI,eAAe,SACjB,WAAW,MAAM;OAEjB,eAAe,iBAAiB,eAAe,WAAW,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;EAIrF,OAAO;CACT;CAEA,SAAS,WAAW,OAAgB,KAAoB;EACtD,MAAM,OACJ,SAAS,MAAM;GAAE,OAAO;GAAc,WAAW;EAAI,IAAI,EAAE,OAAO,OAAO;EAE3E,IACE,YAAY,UAAU,KAAK,UAC1B,YAAY,UAAU,UACpB,YAAY,UAAU,gBACrB,KAAK,UAAU,gBACf,YAAY,cAAc,KAAK,YAEnC;EAEF,cAAc;EAId,KAAK,MAAM,YAAY,kBACrB,SAAS,KAAK;CAElB;;CAGA,SAAS,mBAAmB,aAAqD;EAC/E,IAAI,CAAC,KAAK,oBAAoB;EAC9B,IAAI,CAAC,eAAe,YAAY,WAAW,GAAG;EAC9C,MAAM,OAAO,iBAAiB,WAAW;EACzC,IAAI,MACF,aAAa,IAAI,KAAK,IAAI;CAE9B;;CAGA,SAAS,cAAc,SAAkB,UAAiC;EACxE,IAAI,KAAK,YACP,KAAK,WAAW,SAAS,QAAQ;CAErC;;;;;;;;;;;CAYA,SAAS,iBACP,KACA,MAQiB;EACjB,IAAI,KAAK,eAAe,KAAK,YAAY,SAAS,GAChD,mBAAmB,KAAK,WAAW;OAC9B,IAAI,KAAK,0BACd,aAAa,MAAM;EAGrB,MAAM,WAAW,sBAAsB,KAAK,QAAQ,GAAG;EAEvD,aAAa,KAAK,KAAK;GACrB,SAAS,KAAK;GACd,QAAQ,SAAS;GACjB,aAAa,KAAK;EACpB,CAAC;EAED,OAAO;CACT;;;;;;;;CASA,eAAe,cACb,KACA,IACA,gBACe;EACf,MAAM,WAAW,eAAe,cAAc;EAC9C,WAAW,MAAM,GAAG;EACpB,IAAI;GACF,MAAM,GAAG,QAAQ;EACnB,SAAS,OAAO;GACd,IAAI,aAAa,KAAK,GAAG;GACzB,MAAM;EACR,UAAU;GACR,IAAI,oBAAoB,UAAU;IAChC,kBAAkB;IAClB,WAAW,KAAK;IAChB,KAAK,2BAA2B;GAClC;EACF;CACF;;;;;;CAOA,eAAe,mBAAmB,SAAoC;EACpE,IACE,CAAC,KAAK,sBACN,WAAW,QACX,OAAO,YAAY,YACnB,UAAU,SAEV,OAAO,MAAO;EAEhB,OAAO;CACT;CAEA,SAAS,oBAAoB,iBAAuD;EAClF,OAAO,mBAAmB,QAAQ,gBAAgB,SAAS;CAC7D;;;;;CAMA,SAAS,oBAAoB,QAA2C;EACtE,MAAM,UAAU,IAAI,IAAI,OAAO,eAAgB;EAC/C,MAAM,cAAc,OAAO;EAC3B,MAAM,0BAAU,IAAI,IAAqB;EACzC,IAAI;QACG,MAAM,QAAQ,aACjB,IAAI,CAAC,QAAQ,IAAI,KAAK,aAAa,KAAK,IAAI,GAAG;IAC7C,QAAQ,IAAI,KAAK,aAAa,KAAK,MAAM,OAAO,OAAO;IACvD;GACF;;EAGJ,OAAO;CACT;;;;;;;;;CAUA,SAAS,sBACP,QACA,KACiB;EACjB,MAAM,iBAAiB,UAAU,CAAC;EAElC,iBAAiB,cAAc;EAE/B,MAAM,SAAS,IAAI,IAAI,KAAK,kBAAkB;EAG9C,MAAM,WAA4B;GAAE,QAAQ;GAAgB,UAF3C,OAAO,YAAY;GAEkC,QADvD,OAAO;EACuD;EAC7E,mBAAmB,QAAQ;EAC3B,OAAO;CACT;;;;;;;;;CAUA,eAAe,oBACb,KACA,SACe;EACf,IAAI,KAAK,oBAAoB;GAC3B,MAAM,KAAK,mBAAmB,KAAK,OAAO,gBAAgB;IACxD,MAAM,SAAS,MAAM,QAAQ;IAE7B,IAAI,oBAAoB,OAAO,eAAe,GAAG;KAC/C,MAAM,iBAAiB,oBAAoB,MAAM;KAUjD,OAAO;MAAE,SALO,YACd,KAAK,qBAAqB,KAAK,OAAO,SACtC,OAAO,UACP,cAEO;MAAS,eAAe,OAAO;KAAc;IACxD;IAIA,OAAO;KAAE,SADO,YAAY,OAAO,SAAS,OAAO,QAC1C;KAAS,eAAe,OAAO;IAAc;GACxD,CAAC;GACD;EACF;EAEA,MAAM,SAAS,MAAM,QAAQ;EAC7B,IAAI,CAAC,oBAAoB,OAAO,eAAe,GAC7C,cAAc,OAAO,SAAS,OAAO,QAAQ;CAEjD;;CAGA,SAAS,WAAW,UAA4B;EAC9C,IAAI,KAAK,YACP,KAAK,WAAW,QAAQ;OAExB,SAAS;CAEb;;;;;CAMA,SAAS,wBAAwB,SAAuB;EACtD,iBAAiB;GACf,KAAK,SAAS,GAAG,OAAO;GACxB,OAAO,cAAc,IAAI,MAAM,wBAAwB,CAAC;EAC1D,CAAC;CACH;;;;;;;CAQA,SAAS,uBAAuB,MAAoB;EAClD,iBAAiB;GACf,IAAI,KAAK,eAAe,IAAI,MAAM,MAChC,KAAK,SAAS,GAAG,CAAC;GAEpB,OAAO,cAAc,IAAI,MAAM,wBAAwB,CAAC;EAC1D,CAAC;CACH;;;;;CAMA,eAAe,uBACb,KACA,SAOsD;EAItD,IAAI,cAAc,WAAW,GAAG,GAAG;GACjC,cAAc,QAAQ,GAAG;GACzB,MAAM,IAAI,eAAe,GAAG;EAC9B;EAIA,MAAM,aAAa,cAAc,QAAQ,GAAG;EAC5C,IAAI,SAAkC,aAClC;GACE,SAAS,WAAW;GACpB,eAAe;GACf,aAAa,WAAW,eAAe;GACvC,QAAQ,WAAW,UAAU;GAC7B,iBAAiB,WAAW,mBAAmB;EACjD,IACA,KAAA;EAEJ,IAAI,WAAW,KAAA,GAAW;GAKxB,MAAM,YAAY,KAAK,qBAAqB,aAAa,mBAAmB,IAAI,KAAA;GAChF,MAAM,kBAAkB,QAAQ,gBAAgB,KAAK,cAAc;GAInE,SAAS,MAAM,gBAAgB,KAAK,MAAM,WAHvB,gBAAgB,WAAW,MAAM,IAChD,IAAI,IAAI,eAAe,CAAC,CAAC,WACzB,IAAI,IAAI,iBAAiB,kBAAkB,CAAC,CAAC,UACgB,QAAQ,MAAM;EACjF;EAMA,IAAI,CAAC,QAAQ,aAAa;GACxB,MAAM,YAAY,QAAQ,aAAa;GAGvC,KAAK,sBAAsB,IAAI;GAC/B,IAAI,QAAQ,SACV,KAAK,aAAa;IAAE,QAAQ;IAAM,SAAS;GAAE,GAAG,IAAI,SAAS;QAE7D,KAAK,UAAU;IAAE,QAAQ;IAAM,SAAS;GAAE,GAAG,IAAI,SAAS;GAE5D,KAAK,sBAAsB,KAAK;EAClC;EAIA,MAAM,UAAU,MAAM,mBAAmB,OAAO,OAAO;EAMvD,MAAM,YAAY,oBAAoB,OAAO,eAAe;EAC5D,MAAM,kBAAkB,OAAO,aAAa,MAAM,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK;EAChF,MAAM,WAAW,iBAAiB,KAAK;GACrC,SAAS,aAAa,kBAAkB,OAAO;GAC/C,QAAQ,OAAO;GACf,aAAa,OAAO;EACtB,CAAC;EAED,OAAO;GAAE,GAAG;GAAQ;GAAS;EAAS;CACxC;CAEA,eAAe,SAAS,KAAa,UAA6B,CAAC,GAAkB;EACnF,MAAM,SAAS,QAAQ,WAAW;EAClC,MAAM,UAAU,QAAQ,YAAY;EACpC,MAAM,iBAAiB,QAAQ;EAC/B,MAAM,cAAc,QAAQ,iBAAiB;EAO7C,MAAM,YAAY,IAAI,QAAQ,GAAG;EACjC,MAAM,OAAO,cAAc,KAAK,KAAK,IAAI,MAAM,SAAS;EACxD,MAAM,WAAW,cAAc,KAAK,MAAM,IAAI,MAAM,GAAG,SAAS;EAMhE,MAAM,eAAe,QAAQ,iBAAiB,KAAK,cAAc;EAGjE,MAAM,iBAAiB,KAAK,WAAW;EAIvC,IAAI,KAAK,2BACP,KAAK,0BAA0B,cAAc;OAE7C,KAAK,aAAa;GAAE,QAAQ;GAAM,SAAS;EAAe,GAAG,IAAI,KAAK,cAAc,CAAC;EAGvF,IAAI,uBAAuB;EAE3B,MAAM,cACJ,KACA,OAAO,aAAa;GAKlB,IAAI,CAAC,wBAAwB,KAAK,oBAAoB;IACpD,KAAK,sBAAsB,IAAI;IAC/B,KAAK,mBAAmB,KAAK,OAAO;IACpC,KAAK,sBAAsB,KAAK;IAChC,uBAAuB;GACzB;GAEA,IAAI;IACF,MAAM,oBAAoB,gBACxB,uBAAuB,UAAU;KAC/B;KACA,WAAW;KACX,QAAQ,SAAS;KACjB,aAAa;KACb;IACF,CAAC,CACH;IAGA,OAAO,cAAc,IAAI,MAAM,uBAAuB,CAAC;IAIvD,IAAI,UAAU,MACZ,uBAAuB,IAAI;SAE3B,wBAAwB,SAAS,IAAI,cAAc;GAEvD,SAAS,OAAO;IACd,IAAI,iBAAiB,kBAAkB;KACrC,kBAAkB,IAAI;KACtB,OAAO,SAAS,OAAO;KACvB,MAAM,IAAI,cAAc,CAAC,CAAC;IAC5B;IACA,IAAI,iBAAiB,eAAe;KAClC,IAAI,oBAAoB,UAAU;KAClC,MAAM,SAAS,MAAM,aAAa,EAAE,SAAS,KAAK,CAAC;KACnD;IACF;IACA,IAAI,iBAAiB,qBAAqB;KACxC,kBAAkB,IAAI;KAGtB,OAAO,SAAS,OAAO;KACvB,MAAM,IAAI,cAAc,CAAC,CAAC;IAC5B;IACA,IAAI,iBAAiB,gBAAgB;KACnC,kBAAkB,IAAI;KAItB,OAAO,SAAS,OAAO;KACvB,MAAM,IAAI,cAAc,CAAC,CAAC;IAC5B;IACA,MAAM;GACR;EACF,GACA,cACF;CACF;CAEA,eAAe,UAAyB;EACtC,MAAM,aAAa,KAAK,cAAc;EAEtC,MAAM,cAAc,YAAY,OAAO,aAAa;GAClD,MAAM,oBAAoB,YAAY,YAAY;IAEhD,MAAM,SAAS,MAAM,gBACnB,YACA,MACA,KAAA,GACA,KAAA,GACA,SAAS,MACX;IACA,MAAM,UAAU,MAAM,mBAAmB,OAAO,OAAO;IACvD,MAAM,WAAW,iBAAiB,YAAY;KAC5C;KACA,QAAQ,OAAO;KACf,aAAa,OAAO;IACtB,CAAC;IACD,OAAO;KAAE,GAAG;KAAQ;KAAS;IAAS;GACxC,CAAC;EACH,CAAC;CACH;CAEA,eAAe,eACb,KACA,UAAkB,GAClB,gBACe;EAIf,MAAM,QAAQ,aAAa,IAAI,GAAG;EAElC,IAAI,SAAS,MAAM,YAAY,MAO7B,MAAM,cACJ,KACA,YAAY;GAIV,MAAM,WAAW,iBAAiB,KAAK;IACrC,SAAS,MAAM;IACf,QAAQ,MAAM;IACd,aAAa,MAAM;IACnB,0BAA0B;GAC5B,CAAC;GACD,cAAc,MAAM,SAAS,QAAQ;GACrC,wBAAwB,OAAO;EACjC,GACA,cACF;OAMA,MAAM,cACJ,KACA,OAAO,aAAa;GAClB,MAAM,oBAAoB,KAAK,YAAY;IAIzC,MAAM,SAAS,MAAM,gBAAgB,KAAK,MAHxB,KAAK,qBACnB,aAAa,mBAAmB,IAChC,KAAA,GACuD,KAAA,GAAW,SAAS,MAAM;IACrF,MAAM,UAAU,MAAM,mBAAmB,OAAO,OAAO;IACvD,MAAM,WAAW,iBAAiB,KAAK;KACrC;KACA,QAAQ,OAAO;KACf,aAAa,OAAO;IACtB,CAAC;IACD,OAAO;KAAE,GAAG;KAAQ;KAAS;IAAS;GACxC,CAAC;GAED,wBAAwB,OAAO;EACjC,GACA,cACF;CAEJ;;;;;CAMA,SAAS,SAAS,KAAmB;EAGnC,MAAM,YAAY,IAAI,QAAQ,GAAG;EACjC,MAAM,WAAW,cAAc,KAAK,MAAM,IAAI,MAAM,GAAG,SAAS;EAGhE,IAAI,cAAc,IAAI,QAAQ,MAAM,KAAA,GAAW;EAC/C,IAAI,aAAa,IAAI,QAAQ,GAAG;EAIhC,gBAAqB,UAAU,MADb,KAAK,qBAAqB,aAAa,mBAAmB,IAAI,KAAA,CAClC,CAAC,CAAC,MAC7C,WAAW;GACV,OAAO,eAAe,YAAY,CAAC,CAAC;GACpC,cAAc,IAAI,UAAU,MAAM;EACpC,IACC,UAAU;GACT,IAAI,iBAAiB,gBAAgB;IACnC,cAAc,YAAY,QAAQ;IAClC;GACF;EAEF,CACF;CACF;CAEA,OAAO;EACL;EACA;EACA;EACA,iBAAiB,YAAY,UAAU;EACvC,qBAAsB,YAAY,UAAU,eAAe,YAAY,YAAY;EACnF,gBAAgB,UAAU;GACxB,iBAAiB,IAAI,QAAQ;GAC7B,aAAa,iBAAiB,OAAO,QAAQ;EAC/C;EACA;EACA,kBAAkB,SAAwB;GAIxC,MAAM,aAAa,KAAK,cAAc;GAKtC,MAAM,gBAAgB,aAAa,IAAI,UAAU;GAMjD,cAAc,SALG,iBAAiB,YAAY;IAC5C,SAAS;IACT,QAAQ,mBAAmB,CAAC,CAAC;IAC7B,aAAa,eAAe;GAC9B,CACuB,CAAQ;EACjC;EACA,mBAAmB,aAA4B,mBAAmB,QAAQ;EAC1E;EACA;EACA;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACp0BA,SAAS,gBAAgB,QAAiC;CACxD,IAAI,WAAW,cAAc;EAC3B,MAAM,SAAS,IAAI,gBAAgB,MAAM;EACzC,iBAAiB,QAAQ,MAAM;EAC/B,OAAO;CACT;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAmC;CACjD,IAAI;EAEF,MAAM,aAAa,qBAAqB;EACxC,IAAI,eAAe,MACjB,OAAO,gBAAgB,WAAW,MAAM;CAE5C,QAAQ,CAER;CAGA,MAAM,UAAU,WAAW;CAC3B,IAAI,SAAS,OAAO,IAAI,gBAAgB,QAAQ,YAAY;CAG5D,IAAI,OAAO,WAAW,aAAa,OAAO,gBAAgB,OAAO,SAAS,MAAM;CAChF,OAAO,IAAI,gBAAgB;AAC7B"}
|
|
1
|
+
{"version":3,"file":"internal.js","names":[],"sources":["../../src/client/segment-cache.ts","../../src/client/history.ts","../../src/client/rsc-fetch.ts","../../src/client/router.ts","../../src/client/use-search-params.ts"],"sourcesContent":["// Segment Cache — stores the mounted segment tree and prefetched payloads\n// See design/19-client-navigation.md for architecture details.\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** A prefetched RSC result with optional segment metadata. */\nexport interface PrefetchResult {\n payload: unknown;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo?: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useSegmentParams(). */\n params?: Record<string, string | string[]> | null;\n /** Segment paths skipped by the server (for client-side merging). */\n skippedSegments?: string[] | null;\n}\n\n/**\n * A node in the client-side segment tree. Each node represents a mounted\n * layout or page segment with its RSC flight payload.\n */\nexport interface SegmentNode {\n /** The segment's URL pattern (e.g., \"/\", \"/dashboard\", \"/projects/[id]\") */\n segment: string;\n /** The RSC flight payload for this segment (opaque to the cache) */\n payload: unknown;\n /**\n * Whether the segment/slot is request-dependent (calls getHeaders,\n * getSearchParams, cookies, etc.). Request-dependent segments always\n * re-render on navigation. For segments, this is still based on the\n * AsyncFunction heuristic (to be replaced separately). For slots,\n * this is taint-tracked via ALS.\n */\n isRequestDependent: boolean;\n /** Child segments keyed by segment path */\n children: Map<string, SegmentNode>;\n /** Parallel route slots keyed by slot path (e.g., \"/@sidebar\") */\n slots?: Map<string, SegmentNode>;\n /** Whether this slot's access.ts denied on its last render. */\n denied?: boolean;\n}\n\n/**\n * Serialized state tree sent via X-Timber-State-Tree header.\n * Only sync segments are included — async segments always re-render.\n */\nexport interface StateTree {\n segments: string[];\n slots?: string[];\n}\n\n// ─── Segment Cache ───────────────────────────────────────────────\n\n/**\n * Maintains the client-side segment tree representing currently mounted\n * layouts and pages. Used for navigation reconciliation — the router diffs\n * new routes against this tree to determine which segments to re-fetch.\n */\nexport class SegmentCache {\n private root: SegmentNode | undefined;\n\n get(segment: string): SegmentNode | undefined {\n if (segment === '/' || segment === this.root?.segment) {\n return this.root;\n }\n return undefined;\n }\n\n set(segment: string, node: SegmentNode): void {\n if (segment === '/' || !this.root) {\n this.root = node;\n }\n }\n\n clear(): void {\n this.root = undefined;\n }\n\n /**\n * Serialize the mounted segment tree for the X-Timber-State-Tree header.\n * Only includes sync segments — async segments are excluded because the\n * server must always re-render them (they may depend on request context).\n *\n * When mergeableFilter is provided, only segments whose paths are in the\n * set are included. This ensures the server only skips segments that the\n * client can actually merge (i.e., segments whose cached element tree\n * contains an inner SegmentProvider the merger can splice into).\n *\n * This is a performance optimization only, NOT a security boundary.\n * The server always runs all access.ts files regardless of the state tree.\n */\n serializeStateTree(mergeableFilter?: Set<string>): StateTree {\n const segments: string[] = [];\n const slots: string[] = [];\n if (this.root) {\n collectSyncSegments(this.root, segments, mergeableFilter);\n collectSyncSlots(this.root, slots);\n }\n const tree: StateTree = { segments };\n if (slots.length > 0) {\n tree.slots = slots;\n }\n return tree;\n }\n}\n\n/** Recursively collect sync segment paths from the tree */\nfunction collectSyncSegments(\n node: SegmentNode,\n out: string[],\n mergeableFilter?: Set<string>\n): void {\n if (!node.isRequestDependent && (!mergeableFilter || mergeableFilter.has(node.segment))) {\n out.push(node.segment);\n }\n for (const child of node.children.values()) {\n collectSyncSegments(child, out, mergeableFilter);\n }\n}\n\n/** Recursively collect cacheable slot paths from the tree */\nfunction collectSyncSlots(node: SegmentNode, out: string[]): void {\n if (node.slots) {\n for (const slot of node.slots.values()) {\n // Exclude request-dependent slots (they must re-render every nav)\n // and denied slots (their cached content is denial fallback, not real content)\n if (!slot.isRequestDependent && !slot.denied) {\n out.push(slot.segment);\n }\n }\n }\n for (const child of node.children.values()) {\n collectSyncSlots(child, out);\n }\n}\n\n// ─── Segment Tree Builder ────────────────────────────────────────\n\n/**\n * Segment metadata from the server, sent via X-Timber-Segments header.\n * Describes a rendered segment's path and whether it's async.\n */\nexport interface SegmentInfo {\n path: string;\n /** Outlet key — includes route group name when applicable (e.g., \"/(marketing)\"). */\n segmentId?: string;\n isRequestDependent: boolean;\n /** True for parallel route slot entries. Slots are keyed by their slot path (e.g., \"/@sidebar\"). */\n slot?: boolean;\n /** Parent segment path for slot entries. Used to attach the slot to the correct SegmentNode. */\n parentSegment?: string;\n /** True when the slot's access.ts denied on this render. Denied slots are excluded from the state tree. */\n denied?: boolean;\n /** True when the slot was skipped (cached content reused). Payloads with skipped slots are not replayable. */\n skipped?: boolean;\n}\n\n/**\n * Build a SegmentNode tree from flat segment metadata.\n *\n * Takes an ordered list of segment descriptors (root → leaf) from the\n * server's X-Timber-Segments header and constructs the hierarchical\n * tree structure that SegmentCache expects.\n *\n * Each segment is nested as a child of the previous one, forming a\n * linear chain from root to leaf. The leaf segment (page) is excluded\n * from the tree — pages are never cached across navigations.\n */\nexport function buildSegmentTree(segments: SegmentInfo[]): SegmentNode | undefined {\n // Need at least a root segment to build a tree\n if (segments.length === 0) return undefined;\n\n // Separate slot entries from segment entries. Slots are attached to\n // their parent segment node after the main chain is built.\n const segmentEntries: SegmentInfo[] = [];\n const slotEntries: SegmentInfo[] = [];\n for (const info of segments) {\n if (info.slot) {\n slotEntries.push(info);\n } else {\n segmentEntries.push(info);\n }\n }\n\n // Build the main segment chain.\n let root: SegmentNode | undefined;\n let parent: SegmentNode | undefined;\n const nodeById = new Map<string, SegmentNode>();\n\n for (const info of segmentEntries) {\n const id = info.segmentId ?? info.path;\n const node: SegmentNode = {\n segment: id,\n payload: null,\n isRequestDependent: info.isRequestDependent,\n children: new Map(),\n };\n\n nodeById.set(id, node);\n\n if (!root) {\n root = node;\n }\n\n if (parent) {\n parent.children.set(id, node);\n }\n\n parent = node;\n }\n\n // Attach slot entries to their parent segment nodes.\n for (const slotInfo of slotEntries) {\n const parentId = slotInfo.parentSegment;\n const parentNode = parentId ? nodeById.get(parentId) : root;\n if (!parentNode) continue;\n\n const slotId = slotInfo.segmentId ?? slotInfo.path;\n const slotNode: SegmentNode = {\n segment: slotId,\n payload: null,\n isRequestDependent: slotInfo.isRequestDependent,\n children: new Map(),\n denied: slotInfo.denied,\n };\n\n if (!parentNode.slots) {\n parentNode.slots = new Map();\n }\n parentNode.slots.set(slotId, slotNode);\n }\n\n return root;\n}\n\n// ─── Prefetch Cache ──────────────────────────────────────────────\n\ninterface PrefetchEntry {\n result: PrefetchResult;\n expiresAt: number;\n}\n\n/** Sentinel value for negative cache entries (URL is not a route). */\nconst NEGATIVE_ENTRY: PrefetchResult = Object.freeze({ payload: null });\n\n/**\n * Short-lived cache for hover-triggered prefetches. Entries expire after\n * 30 seconds. When a link is clicked, the prefetched payload is consumed\n * (moved to the history stack) and removed from this cache.\n *\n * timber.js does NOT prefetch on viewport intersection — only explicit\n * hover on <Link prefetch> triggers a prefetch.\n */\nexport class PrefetchCache {\n private static readonly TTL_MS = 30_000;\n private entries = new Map<string, PrefetchEntry>();\n\n set(url: string, result: PrefetchResult): void {\n this.entries.set(url, {\n result,\n expiresAt: Date.now() + PrefetchCache.TTL_MS,\n });\n }\n\n get(url: string): PrefetchResult | undefined {\n const entry = this.entries.get(url);\n if (!entry) return undefined;\n if (Date.now() >= entry.expiresAt) {\n this.entries.delete(url);\n return undefined;\n }\n return entry.result;\n }\n\n /** Get and remove the entry (used when navigation consumes a prefetch) */\n consume(url: string): PrefetchResult | undefined {\n const result = this.get(url);\n if (result !== undefined) {\n this.entries.delete(url);\n }\n return result;\n }\n\n /** Store a negative entry — the URL is not a route (non-RSC Content-Type). */\n setNegative(url: string): void {\n this.set(url, NEGATIVE_ENTRY);\n }\n\n /** Check if the entry is a negative cache entry (URL is not a route). */\n isNegative(url: string): boolean {\n const entry = this.get(url);\n return entry === NEGATIVE_ENTRY;\n }\n}\n","// History Stack — stores RSC payloads by URL for instant back/forward navigation\n// See design/19-client-navigation.md § History Stack\n\nimport type { SegmentInfo } from './segment-cache';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface HistoryEntry {\n /** The complete segment tree payload at the time of navigation */\n payload: unknown;\n /**\n * Route params for this page (for useSegmentParams). Every entry that can\n * be replayed must carry them — the initial SSR entry gets them from the\n * server-embedded __timber_params, navigation entries from X-Timber-Params,\n * and revalidation overwrites preserve the current state (TIM-1037).\n */\n params?: Record<string, string | string[]> | null;\n /**\n * Segment metadata for this page's route. Restored into the segment cache\n * on popstate cached replay so the next forward navigation computes a\n * correct state tree. Without this, the segment cache retains the\n * *previous* page's segments after back-button, causing the server to\n * skip segments that aren't mounted — and the partial payload targets\n * a non-existent outlet.\n */\n segmentInfo?: SegmentInfo[] | null;\n}\n\n// ─── History Stack ───────────────────────────────────────────────\n\n/**\n * Session-lived history stack keyed by URL. Enables instant back/forward\n * navigation without a server roundtrip.\n *\n * On forward navigation, the new page's payload is pushed onto the stack.\n * On popstate, the cached payload is replayed instantly.\n *\n * Entries are keyed by pathname + search. Used with the History API\n * fallback and the Navigation API.\n *\n * Scroll positions are stored in history.state or Navigation API entry\n * state, not in this stack — see design/19-client-navigation.md §Scroll Restoration.\n *\n * Entries persist for the session duration (no expiry) and are cleared\n * when the tab is closed — matching browser back-button behavior.\n */\nexport class HistoryStack {\n private entries = new Map<string, HistoryEntry>();\n\n push(url: string, entry: HistoryEntry): void {\n this.entries.set(url, entry);\n }\n\n get(url: string): HistoryEntry | undefined {\n return this.entries.get(url);\n }\n\n has(url: string): boolean {\n return this.entries.has(url);\n }\n}\n","/**\n * RSC Fetch — handles fetching and parsing RSC Flight payloads.\n *\n * Extracted from router.ts to keep both files under the 500-line limit.\n * This module handles:\n * - Cache-busting URL generation for RSC requests\n * - Building RSC request headers (Accept, X-Timber-State-Tree)\n * - Extracting metadata from RSC response headers\n * - Fetching and decoding RSC payloads\n *\n * See design/19-client-navigation.md §\"RSC Payload Handling\"\n */\n\nimport type { SegmentInfo } from './segment-cache';\nimport type { RouterDeps } from './router';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** Result of fetching an RSC payload — includes segment metadata. */\nexport interface FetchResult {\n payload: unknown;\n /**\n * Promise that settles when the RSC decode completes or fails.\n * The payload thenable is NOT awaited before returning (for streaming),\n * so callers must monitor this to catch async decode errors\n * (truncated streams, Flight parse failures) that would otherwise\n * become unhandled rejections.\n */\n decodePromise: Promise<void> | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useSegmentParams(). */\n params: Record<string, string | string[]> | null;\n /** Segment paths that were skipped by the server (for client-side merging). */\n skippedSegments: string[] | null;\n}\n\n// ─── Constants ───────────────────────────────────────────────────\n\nexport const RSC_CONTENT_TYPE = 'text/x-component';\n\n// ─── URL Helpers ─────────────────────────────────────────────────\n\n/**\n * Generate a short random cache-busting ID (5 chars, a-z0-9).\n * Matches the format Next.js uses for _rsc params.\n */\nfunction generateCacheBustId(): string {\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n let id = '';\n for (let i = 0; i < 5; i++) {\n id += chars[(Math.random() * 36) | 0];\n }\n return id;\n}\n\n/**\n * Append a `_rsc=<id>` query parameter to the URL.\n * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML\n * for RSC navigation requests and signals that this is an RSC fetch.\n *\n * Strips any #fragment before appending — fragments are client-only and\n * fetch() discards them, so _rsc would land inside the hash and be lost.\n */\nfunction appendRscParam(url: string): string {\n const hashIndex = url.indexOf('#');\n const urlWithoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);\n const separator = urlWithoutHash.includes('?') ? '&' : '?';\n return `${urlWithoutHash}${separator}_rsc=${generateCacheBustId()}`;\n}\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The client's deployment ID, set at bootstrap from the runtime config.\n * Sent with every RSC/action request for version skew detection.\n * Null in dev mode. See TIM-446.\n */\nlet clientDeploymentId: string | null = null;\n\n/** Set the client deployment ID. Called once at bootstrap. */\nexport function setClientDeploymentId(id: string | null): void {\n clientDeploymentId = id;\n}\n\n/** Get the client deployment ID. */\nexport function getClientDeploymentId(): string | null {\n return clientDeploymentId;\n}\n\n// ─── Static Mode ────────────────────────────────────────────────\n\n/**\n * When true, RSC fetches use _rsc/*.rsc file URLs instead of\n * the route URL with Accept headers. Static hosts ignore Accept\n * headers, so the client must fetch the pre-generated .rsc files\n * directly. Set at bootstrap from virtual:timber-config output mode.\n */\nlet staticMode = false;\n\nexport function setStaticMode(enabled: boolean): void {\n staticMode = enabled;\n}\n\nexport function isStaticMode(): boolean {\n return staticMode;\n}\n\n/**\n * RSC manifest entry: hashed URL with optional inlined route params.\n * Params are embedded at build time (TIM-1255) so no sidecar fetch needed.\n */\ninterface RscManifestEntry {\n url: string;\n params?: Record<string, string | string[]>;\n}\n\ntype RscManifest = Record<string, RscManifestEntry>;\n\n/**\n * RSC manifest mapping unhashed → hashed URLs + params. Populated from\n * `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during\n * static generation). See TIM-1254, TIM-1255.\n */\nlet rscManifest: RscManifest | null = null;\n\nfunction getRscManifest(): RscManifest | null {\n if (rscManifest) return rscManifest;\n if (typeof window !== 'undefined' && (window as any).__TIMBER_RSC_MANIFEST__) {\n rscManifest = (window as any).__TIMBER_RSC_MANIFEST__;\n }\n return rscManifest;\n}\n\n/** @internal Exported for testing. */\nexport function setRscManifest(manifest: RscManifest | null): void {\n rscManifest = manifest;\n}\n\n/**\n * Convert a route URL to the corresponding _rsc/*.rsc file path.\n * Mirrors the naming in plugins/static-build.ts staticOutputPath.\n *\n * When an RSC manifest is available (hashed filenames from TIM-1254),\n * the manifest is consulted to resolve to the hashed path.\n *\n * / → /_rsc/index.rsc (or /_rsc/index-B7YxEKdN.rsc with manifest)\n * /about → /_rsc/about.rsc (or /_rsc/about-C8ZzFLfO.rsc with manifest)\n * /blog/hello → /_rsc/blog/hello.rsc\n */\nfunction toStaticRscUrl(url: string): string {\n const unhashed = toUnhashedRscUrl(url);\n return manifestLookup(unhashed) ?? unhashed;\n}\n\n/**\n * Compute the unhashed _rsc/*.rsc URL for a route path.\n * @internal Exported for testing.\n */\nexport function toUnhashedRscUrl(url: string): string {\n const hashIndex = url.indexOf('#');\n const queryIndex = url.indexOf('?');\n const hashEnd = hashIndex === -1 ? url.length : hashIndex;\n const queryEnd = queryIndex === -1 ? url.length : queryIndex;\n const end = Math.min(hashEnd, queryEnd);\n let pathname = url.slice(0, end);\n // Strip trailing slash (unless root) to match static build output naming\n if (pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n const rscPath = pathname === '/' ? '/index' : pathname;\n return `/_rsc${rscPath}.rsc`;\n}\n\n/**\n * Look up a key in the RSC manifest, falling back to percent-decoded\n * lookup for encoded browser URLs. Returns the entry or null.\n */\nfunction manifestEntry(key: string): RscManifestEntry | null {\n const manifest = getRscManifest();\n if (!manifest) return null;\n if (manifest[key]) return manifest[key];\n try {\n const decoded = decodeURIComponent(key);\n if (decoded !== key && manifest[decoded]) return manifest[decoded];\n } catch {\n // Malformed percent sequence (e.g. /100%.rsc) — skip decoded lookup\n }\n return null;\n}\n\n/**\n * Look up the hashed URL for an unhashed RSC path.\n */\nfunction manifestLookup(key: string): string | null {\n return manifestEntry(key)?.url ?? null;\n}\n\n/**\n * Look up inlined route params for a route from the manifest. TIM-1255.\n */\nfunction manifestParams(url: string): Record<string, string | string[]> | null {\n const key = toUnhashedRscUrl(url);\n return manifestEntry(key)?.params ?? null;\n}\n\n// ─── Reload Signal ───────────────────────────────────────────────\n\n/** Header name used by the server to signal a version skew reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n/** Header name for the client's deployment ID. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/**\n * Check if a response signals a version skew reload.\n * Triggers a full page reload if the server indicates the client is stale.\n */\nexport function checkReloadSignal(response: Response): boolean {\n return response.headers.get(RELOAD_HEADER) === '1';\n}\n\n// ─── Header Builder ──────────────────────────────────────────────\n\nexport function buildRscHeaders(\n stateTree: { segments: string[] } | undefined,\n currentUrl?: string\n): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: RSC_CONTENT_TYPE,\n };\n if (stateTree) {\n headers['X-Timber-State-Tree'] = JSON.stringify(stateTree);\n }\n // Send current URL for intercepting route resolution.\n // The server uses this to determine if an intercepting route should\n // render instead of the actual target route (modal pattern).\n // See design/07-routing.md §\"Intercepting Routes\"\n if (currentUrl) {\n headers['X-Timber-URL'] = currentUrl;\n }\n // Send deployment ID for version skew detection (TIM-446).\n // The server compares this against the current build's ID.\n // On mismatch, the server signals a reload instead of returning\n // an RSC payload with mismatched module references.\n if (clientDeploymentId) {\n headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;\n }\n return headers;\n}\n\n// ─── Response Header Extraction ──────────────────────────────────\n\n/** Dev-only warning for malformed framework headers. Tree-shaken in production. */\nfunction warnMalformedHeader(headerName: string, raw: string): void {\n if (process.env.NODE_ENV !== 'production') {\n const preview = raw.length > 200 ? raw.slice(0, 200) + '…' : raw;\n console.warn(\n `[timber] Malformed ${headerName} header \\u2014 JSON.parse failed. ` +\n `This indicates a framework bug or header corruption. Raw (first 200 chars): ${preview}`\n );\n }\n}\n\n/**\n * Extract segment metadata from the X-Timber-Segments response header.\n * Returns null if the header is missing or malformed.\n *\n * Format: JSON array of {path, isRequestDependent} objects describing the rendered\n * segment chain from root to leaf. Used to populate the client-side\n * segment cache for state tree diffing on subsequent navigations.\n */\nexport function extractSegmentInfo(response: Response): SegmentInfo[] | null {\n const header = response.headers.get('X-Timber-Segments');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n warnMalformedHeader('X-Timber-Segments', header);\n return null;\n }\n}\n\n/**\n * Extract skipped segment paths from the X-Timber-Skipped-Segments header.\n * Returns null if the header is missing or malformed.\n *\n * When the server skips sync layouts the client already has cached,\n * it sends this header listing the skipped segment paths (outermost first).\n * The client uses this to merge the partial payload with cached segments.\n */\nexport function extractSkippedSegments(response: Response): string[] | null {\n const header = response.headers.get('X-Timber-Skipped-Segments');\n if (!header) return null;\n try {\n const parsed = JSON.parse(header);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n warnMalformedHeader('X-Timber-Skipped-Segments', header);\n return null;\n }\n}\n\n/**\n * Extract route params from the X-Timber-Params response header.\n * Returns null if the header is missing or malformed.\n *\n * Used to populate useSegmentParams() after client-side navigation.\n */\nexport function extractParams(response: Response): Record<string, string | string[]> | null {\n const header = response.headers.get('X-Timber-Params');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n warnMalformedHeader('X-Timber-Params', header);\n return null;\n }\n}\n\n// ─── Redirect Error ──────────────────────────────────────────────\n\n/**\n * Thrown when an RSC payload response contains X-Timber-Redirect header.\n * Caught in navigate() to trigger a soft router navigation to the redirect target.\n */\nexport class RedirectError extends Error {\n readonly redirectUrl: string;\n constructor(url: string) {\n super(`Server redirect to ${url}`);\n this.redirectUrl = url;\n }\n}\n\n/**\n * Thrown when the server signals a version skew (X-Timber-Reload header).\n * Caught in navigate() to trigger a full page reload.\n * See TIM-446.\n */\nexport class VersionSkewError extends Error {\n constructor() {\n super('Version skew detected — server has been redeployed');\n }\n}\n\n/**\n * Thrown when the server returns an error for an RSC payload request.\n * The server sends X-Timber-Error header and a JSON body instead of a\n * broken RSC stream for any RenderError (4xx or 5xx). Caught in\n * navigate() to trigger a hard navigation so the server can render\n * the error page as HTML.\n *\n * See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n */\nexport class ServerErrorResponse extends Error {\n readonly status: number;\n readonly url: string;\n constructor(status: number, url: string) {\n super(`Server error ${status} during navigation to ${url}`);\n this.status = status;\n this.url = url;\n }\n}\n\n/**\n * Thrown when the RSC fetch response has a Content-Type that is not\n * text/x-component — e.g., a static asset (image, CSS, JS) served\n * for a same-origin URL that isn't a route. The response body is\n * cancelled immediately (headers-only cost). Caught in navigate()\n * to trigger a hard navigation; caught in prefetch() to store a\n * negative cache entry so click hard-navigates without a second fetch.\n *\n * See TIM-1231.\n */\nexport class NonRscResponse extends Error {\n readonly url: string;\n constructor(url: string) {\n super(`Non-RSC response for ${url}`);\n this.url = url;\n }\n}\n\n// ─── Stream Completion Tracking ───────────────────────────────────\n\n/**\n * Wrap a response body stream to track when it's fully consumed.\n * Returns a new body that passes all chunks through unchanged, plus\n * a `done` promise that resolves when the last chunk is read (or\n * rejects if the stream errors).\n *\n * Used to keep React transitions open for the full RSC stream\n * duration — createFromFetch's thenable resolves on shell arrival,\n * but we need stream completion for useOptimistic pending state.\n */\nfunction trackStreamCompletion(body: ReadableStream<Uint8Array>): {\n body: ReadableStream<Uint8Array>;\n done: Promise<void>;\n} {\n let resolveDone!: () => void;\n let rejectDone!: (e: unknown) => void;\n const done = new Promise<void>((res, rej) => {\n resolveDone = res;\n rejectDone = rej;\n });\n\n const reader = body.getReader();\n const tracked = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const result = await reader.read();\n if (result.done) {\n controller.close();\n resolveDone();\n } else {\n controller.enqueue(result.value);\n }\n } catch (error) {\n controller.error(error);\n rejectDone(error);\n }\n },\n cancel(reason) {\n reader.cancel(reason);\n resolveDone();\n },\n });\n\n return { body: tracked, done };\n}\n\n// ─── Fetch ───────────────────────────────────────────────────────\n\n/**\n * Fetch an RSC payload from the server. If a decodeRsc function is provided,\n * the response is decoded into a React element tree via createFromFetch.\n * Otherwise, the raw response text is returned (test mode).\n */\nexport async function fetchRscPayload(\n url: string,\n deps: RouterDeps,\n stateTree?: { segments: string[] },\n currentUrl?: string,\n signal?: AbortSignal\n): Promise<FetchResult> {\n // In static mode, fetch the pre-generated _rsc/*.rsc file directly\n // instead of the route URL with Accept headers. Static hosts ignore\n // Accept headers, so the route URL would return HTML.\n const fetchTarget = staticMode ? toStaticRscUrl(url) : url;\n // Skip the cache-bust param when the manifest supplies a content-hashed\n // URL — the hash itself guarantees freshness, and the param would defeat\n // the immutable cache headers on /_rsc/* (TIM-1254).\n const isHashedFromManifest = staticMode && fetchTarget !== toUnhashedRscUrl(url);\n const rscUrl = isHashedFromManifest ? fetchTarget : appendRscParam(fetchTarget);\n const headers = buildRscHeaders(staticMode ? undefined : stateTree, currentUrl);\n if (deps.decodeRsc) {\n // Production path: use createFromFetch for streaming RSC decoding.\n // createFromFetch takes a Promise<Response> and progressively parses\n // the RSC Flight stream as chunks arrive.\n //\n // Intercept the response to read segment metadata before createFromFetch\n // consumes the body. Reading headers does NOT consume the body stream.\n const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n let segmentInfo: SegmentInfo[] | null = null;\n let params: Record<string, string | string[]> | null = null;\n let skippedSegments: string[] | null = null;\n // Track when the full RSC body stream is consumed (not just shell).\n // Initialized to resolved for bodyless responses; overwritten when\n // the response has a body.\n let streamDone: Promise<void> = Promise.resolve();\n\n const wrappedPromise = fetchPromise.then((response) => {\n // Version skew detection (TIM-446): if the server signals a reload,\n // throw VersionSkewError so the caller (router navigate) can trigger\n // a full page reload.\n if (checkReloadSignal(response)) {\n throw new VersionSkewError();\n }\n // Detect server-side redirects. The server returns 204 + X-Timber-Redirect\n // for RSC payload requests instead of a raw 302, because fetch with\n // redirect: \"manual\" turns 302s into opaque redirects (status 0, null body)\n // which crashes createFromFetch when it tries to read the body stream.\n const redirectLocation =\n response.headers.get('X-Timber-Redirect') ||\n (response.status >= 300 && response.status < 400 ? response.headers.get('Location') : null);\n if (redirectLocation) {\n throw new RedirectError(redirectLocation);\n }\n // Detect server error responses. The server returns X-Timber-Error header\n // with a JSON body instead of a broken RSC stream for any RenderError\n // (4xx or 5xx). Hard-navigate so the server renders the error page as HTML.\n // See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n if (response.headers.get('X-Timber-Error') === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard: reject non-RSC responses before createFromFetch\n // tries to parse the body as Flight data.\n // In static mode, accept octet-stream/text/plain/absent content-type\n // (static hosts serve .rsc files with these), but still reject 404s\n // and text/html (missing .rsc file → host returns 404 page or SPA\n // HTML fallback). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const contentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (contentType && contentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n const contentType = response.headers.get('content-type');\n if (!contentType || !contentType.split(';')[0].trim().includes(RSC_CONTENT_TYPE)) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n // Metadata (<title>/<meta>/<link>) now rides the RSC Flight payload\n // as React elements — React 19 Float handles them. See TIM-1151.\n segmentInfo = extractSegmentInfo(response);\n params = extractParams(response);\n skippedSegments = extractSkippedSegments(response);\n\n // Wrap the body to track full stream consumption. createFromFetch's\n // thenable resolves when the root model (shell) arrives, but we need\n // to know when ALL chunks are read so the React transition stays\n // open for the full streaming duration (keeps useOptimistic alive).\n if (response.body) {\n const tracked = trackStreamCompletion(response.body);\n streamDone = tracked.done;\n streamDone.catch(() => {}); // prevent unhandled rejection\n return new Response(tracked.body, {\n headers: response.headers,\n status: response.status,\n });\n }\n return response;\n });\n // Await headers so segmentInfo/params are populated.\n await wrappedPromise;\n // In static mode, params come from the manifest (inlined at build\n // time) since .rsc files have no HTTP headers. TIM-1255.\n if (staticMode && !params) {\n params = manifestParams(url);\n }\n // Start decoding but do NOT await — return the in-progress thenable.\n // React can render a Flight thenable directly: it suspends on unresolved\n // parts and progressively renders as chunks arrive, spreading work across\n // frames instead of blocking the main thread in one burst.\n const payload = deps.decodeRsc(wrappedPromise);\n // Combine stream completion with payload error propagation.\n // streamDone keeps the transition open for the full RSC stream\n // duration (useOptimistic pending state). payloadError propagates\n // decode failures (stale client references, Flight parse errors)\n // so the router's catch block can trigger recovery (stale reload).\n const payloadError = new Promise<void>((_, reject) => {\n Promise.resolve(payload).then(() => {}, reject);\n });\n payloadError.catch(() => {});\n const decodePromise = Promise.race([streamDone, payloadError]);\n return {\n payload,\n decodePromise,\n segmentInfo,\n params,\n skippedSegments,\n };\n }\n // Test/fallback path: return raw text\n const response = await deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n // Check for redirect in test path too\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('Location');\n if (location) {\n throw new RedirectError(location);\n }\n }\n // Server error guard (same as production path above).\n if (response.headers.get('X-Timber-Error') === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard (same as production path above). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const fallbackContentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (fallbackContentType &&\n fallbackContentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n const fallbackContentType = response.headers.get('content-type');\n if (\n !fallbackContentType ||\n !fallbackContentType.split(';')[0].trim().includes(RSC_CONTENT_TYPE)\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n let fallbackParams = extractParams(response);\n // In static mode, params come from the manifest. TIM-1255.\n if (staticMode && !fallbackParams) {\n fallbackParams = manifestParams(url);\n }\n return {\n payload: await response.text(),\n decodePromise: null,\n segmentInfo: extractSegmentInfo(response),\n params: fallbackParams,\n skippedSegments: extractSkippedSegments(response),\n };\n}\n","// Segment Router — manages client-side navigation and RSC payload fetching\n// See design/19-client-navigation.md for the full architecture.\n\nimport { SegmentCache, PrefetchCache, buildSegmentTree } from './segment-cache';\nimport type { SegmentInfo } from './segment-cache';\nimport { HistoryStack } from './history';\nimport { setCurrentParams } from './use-segment-params.js';\nimport {\n setNavigationState,\n getNavigationState,\n type NavigationState,\n} from './navigation-context.js';\n\nimport {\n fetchRscPayload,\n RedirectError,\n ServerErrorResponse,\n VersionSkewError,\n NonRscResponse,\n} from './rsc-fetch.js';\nimport { setHardNavigating, supersedeNavigationTransitions } from './navigation-root.js';\nimport type { FetchResult } from './rsc-fetch.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface NavigationOptions {\n /** Set to false to prevent scroll-to-top on forward navigation */\n scroll?: boolean;\n /** Use replaceState instead of pushState (replaces current history entry) */\n replace?: boolean;\n /**\n * @internal AbortSignal from the Navigation API's NavigateEvent.\n * When provided, the signal is linked to the router's per-navigation\n * AbortController so in-flight RSC fetches are cancelled when a new\n * navigation starts.\n */\n _signal?: AbortSignal;\n /**\n * @internal Skip pushState/replaceState — the Navigation API has already\n * updated the URL via event.intercept(). Used for external navigations\n * intercepted by the navigate event handler.\n */\n _skipHistory?: boolean;\n /**\n * @internal The URL the user is navigating FROM, captured before the\n * Navigation API commits the destination. Sent as X-Timber-URL for\n * slot skip comparison on the server (TIM-1232).\n */\n _departingUrl?: string;\n}\n\n/**\n * Function that decodes an RSC Flight stream into a React element tree.\n * In production: createFromFetch from @vitejs/plugin-rsc/browser.\n * In tests: a mock that returns the raw payload.\n */\nexport type RscDecoder = (fetchPromise: Promise<Response>) => unknown;\n\n/**\n * Function that renders a decoded RSC element tree into the DOM.\n * In production: reactRoot.render(element).\n * In tests: a no-op or mock.\n *\n * Receives the current NavigationState explicitly — no temporal\n * coupling with setNavigationState/getNavigationState. The renderer\n * wraps the element in NavigationProvider with this state.\n */\nexport type RootRenderer = (element: unknown, navState: NavigationState) => void;\n\n/**\n * Platform dependencies injected for testability. In production these\n * map to browser APIs; in tests they're replaced with mocks.\n */\nexport interface RouterDeps {\n fetch: (url: string, init: RequestInit) => Promise<Response>;\n pushState: (data: unknown, unused: string, url: string) => void;\n replaceState: (data: unknown, unused: string, url: string) => void;\n scrollTo: (x: number, y: number) => void;\n getCurrentUrl: () => string;\n getScrollY: () => number;\n /** Decode RSC Flight stream into React elements. If not provided, raw response text is stored. */\n decodeRsc?: RscDecoder;\n /** Render decoded RSC tree into the DOM. If not provided, rendering is a no-op. */\n renderRoot?: RootRenderer;\n /**\n * Schedule a callback after the next paint. In the browser, this is\n * requestAnimationFrame + setTimeout(0) to run after React commits.\n * In tests, this runs the callback synchronously.\n */\n afterPaint?: (callback: () => void) => void;\n /**\n * Run a navigation inside a React transition with optimistic pending URL.\n * The pending URL shows immediately (useOptimistic urgent update) and\n * reverts when the transition commits (atomic with the new tree).\n *\n * The `perform` callback receives a `wrapPayload` function to wrap the\n * decoded RSC payload with NavigationProvider + NuqsAdapter before\n * NavigationRoot sets it as the new element. The `wrapPayload` function\n * receives the NavigationState explicitly — no temporal coupling with\n * getNavigationState().\n *\n * If not provided (tests), the router falls back to renderRoot.\n */\n navigateTransition?: (\n pendingUrl: string,\n perform: (\n wrapPayload: (\n payload: unknown,\n navState: NavigationState,\n segmentUpdates?: Map<string, unknown>\n ) => unknown\n ) => Promise<{ element: unknown; decodePromise: Promise<void> | null }>\n ) => Promise<void>;\n\n /**\n * Whether the Navigation API is active and handling traversals.\n * When true, the popstate handler is a no-op — the Navigation API's\n * navigate event covers back/forward button presses.\n */\n navigationApiActive?: boolean;\n\n /**\n * Called around pushState/replaceState to set a flag that prevents\n * the Navigation API's navigate listener from double-handling\n * router-initiated navigations.\n */\n setRouterNavigating?: (value: boolean) => void;\n\n /**\n * Save scroll position via the Navigation API's per-entry state.\n * When provided, used instead of history.replaceState for scroll storage.\n */\n saveNavigationEntryScroll?: (scrollY: number) => void;\n\n /**\n * Signal that a router-initiated navigation has completed. Resolves the\n * deferred promise that ties the browser's native loading state to the\n * navigation lifecycle. Called in the finally block of navigate/refresh,\n * aligned with when the TopLoader's pendingUrl clears.\n */\n completeRouterNavigation?: () => void;\n\n /**\n * Get the current unwrapped RSC payload element. Used for partial\n * navigation: the router re-wraps the same element with new\n * NavigationProvider context so mounted SegmentOutlets are preserved.\n */\n _getCurrentPayload?: () => unknown;\n\n /**\n * Initiate a navigation via the Navigation API (`navigation.navigate()`).\n * Fires the navigate event BEFORE committing the URL, allowing Chrome\n * to show its native loading indicator. Falls back to pushState when\n * unavailable.\n */\n navigationNavigate?: (url: string, replace: boolean) => void;\n\n /**\n * Scroll the element matching a URL #fragment into view. Returns true\n * when a matching element was found and scrolled. When absent or false,\n * the router falls back to scroll-to-top on forward navigation — same\n * as a full page load with an unknown fragment landing at the top.\n */\n scrollToHash?: (hash: string) => boolean;\n\n /**\n * Whether the client segment cache is enabled. When false (the default),\n * the router does not send X-Timber-State-Tree headers and does not\n * populate the segment cache. Every navigation gets a full RSC payload.\n */\n clientSegmentCache?: boolean;\n}\n\nexport interface RouterInstance {\n /** Navigate to a new URL (forward navigation) */\n navigate(url: string, options?: NavigationOptions): Promise<void>;\n /** Full re-render of the current URL — no state tree sent */\n refresh(): Promise<void>;\n /** Handle a popstate event (back/forward button). scrollY is read from history.state. */\n handlePopState(url: string, scrollY?: number, externalSignal?: AbortSignal): Promise<void>;\n /** Whether a navigation is currently in flight */\n isPending(): boolean;\n /** The URL currently being navigated to, or null if idle */\n getPendingUrl(): string | null;\n /** Subscribe to pending state changes */\n onPendingChange(listener: (pending: boolean) => void): () => void;\n /** Prefetch an RSC payload for a URL (used by Link hover) */\n prefetch(url: string): void;\n /**\n * Apply a piggybacked revalidation payload from a server action response.\n * Renders the element tree and updates head elements without a server fetch.\n * See design/08-forms-and-actions.md §\"Single-Roundtrip Revalidation\".\n */\n applyRevalidation(element: unknown): void;\n /**\n * Populate the segment cache from server-provided segment metadata.\n * Called on initial hydration with segment info embedded in the HTML.\n */\n initSegmentCache(segments: SegmentInfo[]): void;\n\n /** The segment cache (exposed for tests and <Link> prefetch) */\n segmentCache: SegmentCache;\n /** The prefetch cache (exposed for tests and <Link> prefetch) */\n prefetchCache: PrefetchCache;\n /** The history stack (exposed for tests) */\n historyStack: HistoryStack;\n}\n\n/**\n * Check if an error is an abort error (connection closed / fetch aborted).\n * Browsers throw DOMException with name 'AbortError' when a fetch is aborted.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof DOMException && error.name === 'AbortError') return true;\n if (error instanceof Error && error.name === 'AbortError') return true;\n return false;\n}\n\n// ─── Router Factory ──────────────────────────────────────────────\n\n/**\n * Create a router instance. In production, called once at app hydration\n * with real browser APIs. In tests, called with mock dependencies.\n */\n/**\n * Router navigation phase — discriminated union replacing scattered\n * `pending` + `pendingUrl` boolean flags.\n *\n * - `idle`: No navigation in flight. The committed params/pathname\n * are current.\n * - `navigating`: A fetch or render is in progress. `targetUrl` is\n * the destination being navigated to.\n */\nexport type RouterPhase = { phase: 'idle' } | { phase: 'navigating'; targetUrl: string };\n\nexport function createRouter(deps: RouterDeps): RouterInstance {\n const segmentCache = new SegmentCache();\n const prefetchCache = new PrefetchCache();\n const historyStack = new HistoryStack();\n let routerPhase: RouterPhase = { phase: 'idle' };\n const pendingListeners = new Set<(pending: boolean) => void>();\n\n // AbortController for the current in-flight navigation.\n // When a new navigation starts, the previous controller is aborted,\n // cancelling any in-progress RSC fetch. This provides automatic\n // cancellation of stale fetches regardless of Navigation API support.\n let currentNavAbort: AbortController | null = null;\n\n /**\n * Create a new AbortController for a navigation, superseding any\n * previous in-flight navigation. Optionally links to an external\n * signal (e.g., from the Navigation API's NavigateEvent.signal).\n *\n * Superseding is one operation with three parts:\n * 1. Abort the previous navigation's fetch.\n * 2. Invalidate its render transition so a response that already\n * arrived can't commit a stale tree (NavigationRoot's transId guard).\n * 3. Resolve its Navigation API deferred — the superseded navigation's\n * finally block is staleness-guarded (see TIM-1034) and no longer\n * cleans up after itself, so the browser's native loading state for\n * the dead navigation is cleared here.\n */\n function createNavAbort(externalSignal?: AbortSignal): AbortController {\n if (currentNavAbort) {\n currentNavAbort.abort();\n supersedeNavigationTransitions();\n deps.completeRouterNavigation?.();\n }\n const controller = new AbortController();\n currentNavAbort = controller;\n\n // If an external signal is provided (e.g., Navigation API),\n // forward its abort to our controller.\n if (externalSignal) {\n if (externalSignal.aborted) {\n controller.abort();\n } else {\n externalSignal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n }\n\n return controller;\n }\n\n function setPending(value: boolean, url?: string): void {\n const next: RouterPhase =\n value && url ? { phase: 'navigating', targetUrl: url } : { phase: 'idle' };\n // Skip no-op updates\n if (\n routerPhase.phase === next.phase &&\n (routerPhase.phase === 'idle' ||\n (routerPhase.phase === 'navigating' &&\n next.phase === 'navigating' &&\n routerPhase.targetUrl === next.targetUrl))\n ) {\n return;\n }\n routerPhase = next;\n // Notify external store listeners (non-React consumers).\n // React-facing pending state is handled by useOptimistic in\n // NavigationRoot via navigateTransition — not this function.\n for (const listener of pendingListeners) {\n listener(value);\n }\n }\n\n /** Update the segment cache from server-provided segment metadata. */\n function updateSegmentCache(segmentInfo: SegmentInfo[] | null | undefined): void {\n if (!deps.clientSegmentCache) return;\n if (!segmentInfo || segmentInfo.length === 0) return;\n const tree = buildSegmentTree(segmentInfo);\n if (tree) {\n segmentCache.set('/', tree);\n }\n }\n\n /** Render a decoded RSC payload into the DOM if a renderer is available. */\n function renderPayload(payload: unknown, navState: NavigationState): void {\n if (deps.renderRoot) {\n deps.renderRoot(payload, navState);\n }\n }\n\n /**\n * Atomically update all navigation-owned state for a new page. Every\n * code path that changes the \"current page\" must go through this\n * function — making \"forgot a field\" impossible by construction.\n *\n * The three operations:\n * 1. Segment cache — update from server-provided segment metadata\n * 2. Navigation state — params + pathname for useSegmentParams/usePathname\n * 3. History stack — store the payload for instant back/forward replay\n */\n function commitNavigation(\n url: string,\n opts: {\n payload: unknown;\n params?: Record<string, string | string[]> | null;\n segmentInfo?: SegmentInfo[] | null;\n /** When true, clear the segment cache if segmentInfo is empty\n * (popstate replay for entries without layout metadata). */\n clearSegmentCacheOnEmpty?: boolean;\n }\n ): NavigationState {\n if (opts.segmentInfo && opts.segmentInfo.length > 0) {\n updateSegmentCache(opts.segmentInfo);\n } else if (opts.clearSegmentCacheOnEmpty) {\n segmentCache.clear();\n }\n\n const navState = updateNavigationState(opts.params, url);\n\n historyStack.push(url, {\n payload: opts.payload,\n params: navState.params,\n segmentInfo: opts.segmentInfo,\n });\n\n return navState;\n }\n\n /**\n * Wrap a navigation in the standard abort/pending/cleanup lifecycle.\n * Consolidates the createNavAbort + setPending + staleness-guarded\n * finally that was duplicated across navigate, refresh, and both\n * handlePopState paths. AbortErrors are swallowed (not application\n * errors); all other errors propagate to the caller.\n */\n async function runNavigation(\n url: string,\n fn: (navAbort: AbortController) => Promise<void>,\n externalSignal?: AbortSignal\n ): Promise<void> {\n const navAbort = createNavAbort(externalSignal);\n setPending(true, url);\n try {\n await fn(navAbort);\n } catch (error) {\n if (isAbortError(error)) return;\n throw error;\n } finally {\n if (currentNavAbort === navAbort) {\n currentNavAbort = null;\n setPending(false);\n deps.completeRouterNavigation?.();\n }\n }\n }\n\n /**\n * Resolve thenable payloads in the test/fallback path (no navigateTransition).\n * In production, React handles thenables from createFromFetch directly via\n * Suspense. In tests, renderRoot is a plain mock that expects resolved values.\n */\n async function resolveForFallback(payload: unknown): Promise<unknown> {\n if (\n !deps.navigateTransition &&\n payload != null &&\n typeof payload === 'object' &&\n 'then' in payload\n ) {\n return await (payload as PromiseLike<unknown>);\n }\n return payload;\n }\n\n function isPartialNavigation(skippedSegments: string[] | null | undefined): boolean {\n return skippedSegments != null && skippedSegments.length > 0;\n }\n\n /**\n * Build a segment updates map for partial navigation. Identifies the\n * first non-skipped segment and maps it to the payload content.\n */\n function buildSegmentUpdates(result: FetchResult): Map<string, unknown> {\n const skipped = new Set(result.skippedSegments!);\n const segmentInfo = result.segmentInfo;\n const updates = new Map<string, unknown>();\n if (segmentInfo) {\n for (const info of segmentInfo) {\n if (!skipped.has(info.segmentId ?? info.path)) {\n updates.set(info.segmentId ?? info.path, result.payload);\n break;\n }\n }\n }\n return updates;\n }\n\n /**\n * Update navigation state (params + pathname) for the next render.\n *\n * Sets the module-level fallback (for tests and SSR) and the\n * globalThis bridge, then returns the NavigationState so callers\n * can pass it explicitly to renderRoot/wrapPayload — eliminating\n * temporal coupling with getNavigationState().\n */\n function updateNavigationState(\n params: Record<string, string | string[]> | null | undefined,\n url: string\n ): NavigationState {\n const resolvedParams = params ?? {};\n // Module-level fallback for tests (no NavigationProvider) and SSR\n setCurrentParams(resolvedParams);\n // globalThis bridge — kept for backward compat\n const parsed = new URL(url, 'http://localhost');\n const pathname = parsed.pathname || '/';\n const search = parsed.search;\n const navState: NavigationState = { params: resolvedParams, pathname, search };\n setNavigationState(navState);\n return navState;\n }\n\n /**\n * Render a payload via navigateTransition (production) or renderRoot (tests).\n * The perform callback should fetch data, call commitNavigation, and return\n * the FetchResult plus the NavigationState.\n *\n * State management (segmentCache, navState, historyStack) is handled by\n * commitNavigation inside perform — this function only handles rendering.\n */\n async function renderViaTransition(\n url: string,\n perform: () => Promise<FetchResult & { navState: NavigationState }>\n ): Promise<void> {\n if (deps.navigateTransition) {\n await deps.navigateTransition(url, async (wrapPayload) => {\n const result = await perform();\n\n if (isPartialNavigation(result.skippedSegments)) {\n const segmentUpdates = buildSegmentUpdates(result);\n\n // Re-wrap the CURRENT element with new context values.\n // SegmentOutlets read updates from SegmentUpdateContext;\n // NavigationProvider gets new params/pathname.\n const element = wrapPayload(\n deps._getCurrentPayload?.() ?? result.payload,\n result.navState,\n segmentUpdates\n );\n return { element, decodePromise: result.decodePromise };\n }\n\n // Full navigation — empty updates, render the new tree.\n const element = wrapPayload(result.payload, result.navState);\n return { element, decodePromise: result.decodePromise };\n });\n return;\n }\n // Fallback: no transition (tests, no React tree)\n const result = await perform();\n if (!isPartialNavigation(result.skippedSegments)) {\n renderPayload(result.payload, result.navState);\n }\n }\n\n /**\n * Perform a hard navigation to a URL. When the target is same-document\n * (same pathname+search, different hash), `location.href = url` is a\n * hash change — no reload. Use `location.reload()` instead (TIM-1235).\n *\n * `fromUrl` is the departing URL captured before any pushState or\n * Navigation API commit — deps.getCurrentUrl() is unreliable here\n * because the URL may have already been updated.\n */\n function hardNavigate(url: string, fromUrl: string): void {\n const current = new URL(fromUrl, window.location.origin);\n const target = new URL(url, window.location.origin);\n if (target.pathname === current.pathname && target.search === current.search) {\n // Assign first to update the hash in the address bar, then reload\n // to force a full page load. href alone is a no-op hash change.\n window.location.href = url;\n window.location.reload();\n } else {\n window.location.href = url;\n }\n }\n\n /** Run a callback after the next paint (after React commit). */\n function afterPaint(callback: () => void): void {\n if (deps.afterPaint) {\n deps.afterPaint(callback);\n } else {\n callback();\n }\n }\n\n /**\n * Schedule scroll restoration after the next paint and fire the\n * scroll-restored event. Used by navigate, popstate, and refresh.\n */\n function restoreScrollAfterPaint(scrollY: number): void {\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n /**\n * Scroll to the element matching the URL #fragment after paint, falling\n * back to scroll-to-top when no element matches (matching full-page-load\n * behavior for an unknown fragment). Used by forward navigation to a\n * hash-bearing URL (TIM-1035).\n */\n function scrollToHashAfterPaint(hash: string): void {\n afterPaint(() => {\n if (deps.scrollToHash?.(hash) !== true) {\n deps.scrollTo(0, 0);\n }\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n /**\n * Core navigation logic shared between the transition and fallback paths.\n * Fetches the RSC payload, updates all state, and returns the result.\n */\n async function performNavigationFetch(\n url: string,\n options: {\n replace: boolean;\n commitUrl?: string;\n signal?: AbortSignal;\n skipHistory?: boolean;\n departingUrl?: string;\n }\n ): Promise<FetchResult & { navState: NavigationState }> {\n // Check prefetch cache first. A negative entry means a prior prefetch\n // determined this URL is not a route (non-RSC Content-Type). Hard-navigate\n // immediately without a second fetch. See TIM-1231.\n if (prefetchCache.isNegative(url)) {\n prefetchCache.consume(url);\n throw new NonRscResponse(url);\n }\n\n // PrefetchResult has optional segmentInfo/params fields — normalize\n // to null for FetchResult compatibility.\n const prefetched = prefetchCache.consume(url);\n let result: FetchResult | undefined = prefetched\n ? {\n payload: prefetched.payload,\n decodePromise: null,\n segmentInfo: prefetched.segmentInfo ?? null,\n params: prefetched.params ?? null,\n skippedSegments: prefetched.skippedSegments ?? null,\n }\n : undefined;\n\n if (result === undefined) {\n // Fetch RSC payload with state tree for partial rendering.\n // Send departing URL (pre-navigation) for slot skip comparison.\n // getCurrentUrl() can't be used here because the Navigation API\n // may have already committed the destination URL (TIM-1232).\n const stateTree = deps.clientSegmentCache ? segmentCache.serializeStateTree() : undefined;\n const rawDepartingUrl = options.departingUrl ?? deps.getCurrentUrl();\n const currentUrl = rawDepartingUrl.startsWith('http')\n ? new URL(rawDepartingUrl).pathname\n : new URL(rawDepartingUrl, 'http://localhost').pathname;\n result = await fetchRscPayload(url, deps, stateTree, currentUrl, options.signal);\n }\n\n // Update the browser history — skip when the Navigation API has already\n // updated the URL via event.intercept() (external navigations).\n // The committed URL keeps the #fragment (commitUrl) even though the\n // fetch/history-stack URL is hash-less (TIM-1035).\n if (!options.skipHistory) {\n const commitUrl = options.commitUrl ?? url;\n // Set the router-navigating flag so the Navigation API's navigate\n // listener doesn't double-intercept this pushState/replaceState.\n deps.setRouterNavigating?.(true);\n if (options.replace) {\n deps.replaceState({ timber: true, scrollY: 0 }, '', commitUrl);\n } else {\n deps.pushState({ timber: true, scrollY: 0 }, '', commitUrl);\n }\n deps.setRouterNavigating?.(false);\n }\n\n // Resolve thenable payloads in the test path so popstate replay\n // and renderPayload receive plain values (see resolveForFallback).\n const payload = await resolveForFallback(result.payload);\n\n // Atomically update all navigation state via commitNavigation.\n // Partial navigations and slot-skip navigations store null payload —\n // the RSC tree contains skip holes that can't be replayed standalone;\n // popstate will fetch fresh.\n const isPartial = isPartialNavigation(result.skippedSegments);\n const hasSkippedSlots = result.segmentInfo?.some((s) => s.slot && s.skipped) ?? false;\n const navState = commitNavigation(url, {\n payload: isPartial || hasSkippedSlots ? null : payload,\n params: result.params,\n segmentInfo: result.segmentInfo,\n });\n\n return { ...result, payload, navState };\n }\n\n async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {\n const scroll = options.scroll !== false;\n const replace = options.replace === true;\n const externalSignal = options._signal as AbortSignal | undefined;\n const skipHistory = options._skipHistory === true;\n\n // Split the #fragment off the navigation URL (TIM-1035). The full URL\n // (with hash) is committed to the address bar; the hash-less URL is used\n // for the RSC fetch (fragments are client-only — keeping it would also\n // swallow the ?_rsc cache-bust param into the fragment) and for\n // history-stack/prefetch keys (popstate lookups use pathname + search).\n const hashIndex = url.indexOf('#');\n const hash = hashIndex === -1 ? '' : url.slice(hashIndex);\n const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);\n\n // Use the pre-intercept departing URL when the Navigation API has already\n // committed the destination (_departingUrl from navigation-api.ts). Otherwise\n // capture it now — getCurrentUrl() is still the departing URL at this point\n // for non-Navigation-API navigations (TIM-1232).\n const departingUrl = options._departingUrl ?? deps.getCurrentUrl();\n\n // Capture the departing page's scroll position for scroll={false} preservation.\n const currentScrollY = deps.getScrollY();\n\n // Save the departing page's scroll position — use Navigation API entry\n // state when available, otherwise fall back to history.state.\n if (deps.saveNavigationEntryScroll) {\n deps.saveNavigationEntryScroll(currentScrollY);\n } else {\n deps.replaceState({ timber: true, scrollY: currentScrollY }, '', deps.getCurrentUrl());\n }\n\n let effectiveSkipHistory = skipHistory;\n\n await runNavigation(\n url,\n async (navAbort) => {\n // When Navigation API is active, initiate the navigation via\n // navigation.navigate() BEFORE the fetch. Must happen after\n // createNavAbort supersedes the previous navigation (done by\n // runNavigation) so the old deferred is resolved first.\n if (!effectiveSkipHistory && deps.navigationNavigate) {\n deps.setRouterNavigating?.(true);\n deps.navigationNavigate(url, replace);\n deps.setRouterNavigating?.(false);\n effectiveSkipHistory = true;\n }\n\n try {\n await renderViaTransition(fetchUrl, () =>\n performNavigationFetch(fetchUrl, {\n replace,\n commitUrl: url,\n signal: navAbort.signal,\n skipHistory: effectiveSkipHistory,\n departingUrl,\n })\n );\n\n // Notify nuqs adapter (and any other listeners) that navigation completed.\n window.dispatchEvent(new Event('timber:navigation-end'));\n\n // Scroll-to-top on forward navigation, scroll to the #fragment target\n // when the URL has one, or restore captured position for scroll={false}.\n if (scroll && hash) {\n scrollToHashAfterPaint(hash);\n } else {\n restoreScrollAfterPaint(scroll ? 0 : currentScrollY);\n }\n } catch (error) {\n if (error instanceof VersionSkewError) {\n setHardNavigating(true);\n window.location.reload();\n await new Promise(() => {});\n }\n if (error instanceof RedirectError) {\n if (currentNavAbort !== navAbort) return;\n await navigate(error.redirectUrl, { replace: true });\n return;\n }\n if (error instanceof ServerErrorResponse) {\n setHardNavigating(true);\n hardNavigate(url, departingUrl);\n await new Promise(() => {});\n }\n if (error instanceof NonRscResponse) {\n setHardNavigating(true);\n hardNavigate(url, departingUrl);\n await new Promise(() => {});\n }\n throw error;\n }\n },\n externalSignal\n );\n }\n\n async function refresh(): Promise<void> {\n const currentUrl = deps.getCurrentUrl();\n\n await runNavigation(currentUrl, async (navAbort) => {\n await renderViaTransition(currentUrl, async () => {\n // No state tree sent — server renders the complete RSC payload\n const result = await fetchRscPayload(\n currentUrl,\n deps,\n undefined,\n undefined,\n navAbort.signal\n );\n const payload = await resolveForFallback(result.payload);\n const navState = commitNavigation(currentUrl, {\n payload,\n params: result.params,\n segmentInfo: result.segmentInfo,\n });\n return { ...result, payload, navState };\n });\n });\n }\n\n async function handlePopState(\n url: string,\n scrollY: number = 0,\n externalSignal?: AbortSignal\n ): Promise<void> {\n // Scroll position is read from history.state by the caller (browser-entry.ts)\n // and passed in. This is more reliable than tracking scroll per-URL in memory\n // because the browser maintains per-entry state even with duplicate URLs.\n const entry = historyStack.get(url);\n\n if (entry && entry.payload !== null) {\n // Replay cached payload — no server roundtrip.\n //\n // runNavigation supersedes any in-flight forward navigation (TIM-1022):\n // aborts its fetch and invalidates its render transition so the stale\n // forward payload can't commit over this replay. The replay itself is\n // synchronous — the fn resolves immediately.\n await runNavigation(\n url,\n async () => {\n // clearSegmentCacheOnEmpty: popstate to an entry without layout\n // metadata (e.g., initial SSR page) clears the cache so the next\n // forward navigation gets a full render.\n const navState = commitNavigation(url, {\n payload: entry.payload,\n params: entry.params,\n segmentInfo: entry.segmentInfo,\n clearSegmentCacheOnEmpty: true,\n });\n renderPayload(entry.payload, navState);\n restoreScrollAfterPaint(scrollY);\n },\n externalSignal\n );\n } else {\n // No cached payload — fetch from server.\n // This happens when navigating back to the initial SSR'd page\n // (its payload is null since it was rendered via SSR, not RSC fetch)\n // or when the entry doesn't exist at all.\n await runNavigation(\n url,\n async (navAbort) => {\n await renderViaTransition(url, async () => {\n const stateTree = deps.clientSegmentCache\n ? segmentCache.serializeStateTree()\n : undefined;\n const result = await fetchRscPayload(url, deps, stateTree, undefined, navAbort.signal);\n const payload = await resolveForFallback(result.payload);\n const navState = commitNavigation(url, {\n payload,\n params: result.params,\n segmentInfo: result.segmentInfo,\n });\n return { ...result, payload, navState };\n });\n\n restoreScrollAfterPaint(scrollY);\n },\n externalSignal\n );\n }\n }\n\n /**\n * Prefetch an RSC payload for a URL and store it in the prefetch cache.\n * Called on hover of <Link prefetch> elements.\n */\n function prefetch(url: string): void {\n // Strip fragment — it's client-only and would swallow the _rsc cache-bust\n // param into the hash. The hash-less key also matches navigate()'s fetchUrl.\n const hashIndex = url.indexOf('#');\n const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);\n\n // Don't prefetch if already cached\n if (prefetchCache.get(fetchUrl) !== undefined) return;\n if (historyStack.has(fetchUrl)) return;\n\n // Fire-and-forget fetch\n const stateTree = deps.clientSegmentCache ? segmentCache.serializeStateTree() : undefined;\n void fetchRscPayload(fetchUrl, deps, stateTree).then(\n (result) => {\n result.decodePromise?.catch(() => {});\n prefetchCache.set(fetchUrl, result);\n },\n (error) => {\n if (error instanceof NonRscResponse) {\n prefetchCache.setNegative(fetchUrl);\n return;\n }\n // Prefetch failure is non-fatal — navigation will fetch fresh\n }\n );\n }\n\n return {\n navigate,\n refresh,\n handlePopState,\n isPending: () => routerPhase.phase === 'navigating',\n getPendingUrl: () => (routerPhase.phase === 'navigating' ? routerPhase.targetUrl : null),\n onPendingChange(listener) {\n pendingListeners.add(listener);\n return () => pendingListeners.delete(listener);\n },\n prefetch,\n applyRevalidation(element: unknown): void {\n // Render the piggybacked element tree from a server action response.\n // Updates the current history entry with the fresh payload —\n // same as refresh() but without a server fetch.\n const currentUrl = deps.getCurrentUrl();\n\n // Preserve existing segmentInfo so away-and-back navigation replays\n // with a correct segment cache (TIM-1037). Preserve current params\n // so dynamic route params aren't cleared to {}.\n const existingEntry = historyStack.get(currentUrl);\n const navState = commitNavigation(currentUrl, {\n payload: element,\n params: getNavigationState().params,\n segmentInfo: existingEntry?.segmentInfo,\n });\n renderPayload(element, navState);\n },\n initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),\n segmentCache,\n prefetchCache,\n historyStack,\n };\n}\n","/**\n * useSearchParams() — client-side hook for reading URL search params.\n *\n * Returns a read-only URLSearchParams instance reflecting the current\n * URL's query string. Updates when client-side navigation changes the URL.\n *\n * On the client, reads from NavigationContext which is updated atomically\n * with the RSC tree render during full navigations, AND by\n * syncShallowSearch() for shallow URL updates (nuqs shallow: true,\n * replaceUrl, or any external pushState/replaceState that changes the\n * query string). See router-init.ts.\n *\n * This replaces the previous useSyncExternalStore approach which read\n * window.location.search directly — causing React to detect external\n * store tearing during transitions and fall back to synchronous rendering\n * (renderRootSync instead of renderRootConcurrent), blocking the main\n * thread and freezing animations.\n *\n * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard\n * URLSearchParams. Mutation methods (set, delete, append) work on the\n * local copy but do NOT affect the URL — use the router or nuqs for that.\n *\n * During SSR, reads the request search params from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * Compatible with Next.js's `useSearchParams()` from `next/navigation`.\n */\n\nimport { getSsrData } from './ssr-data.js';\nimport { useNavigationContext } from './navigation-context.js';\nimport { cachedSearch, cachedSearchParams, _setCachedSearch } from './state.js';\n\nfunction getSearchParams(search: string): URLSearchParams {\n if (search !== cachedSearch) {\n const params = new URLSearchParams(search);\n _setCachedSearch(search, params);\n return params;\n }\n return cachedSearchParams;\n}\n\n/**\n * Read the current URL search params.\n *\n * Compatible with Next.js's `useSearchParams()` from `next/navigation`.\n */\nexport function useSearchParams(): URLSearchParams {\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return getSearchParams(navContext.search);\n }\n } catch {\n // No React dispatcher available (called outside a component).\n }\n\n // SSR path: read from ALS-backed SSR data context.\n const ssrData = getSsrData();\n if (ssrData) return new URLSearchParams(ssrData.searchParams);\n\n // Final fallback: window.location (tests, edge cases).\n if (typeof window !== 'undefined') return getSearchParams(window.location.search);\n return new URLSearchParams();\n}\n"],"mappings":"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"}
|