@timber-js/app 0.2.0-alpha.124 → 0.2.0-alpha.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/_chunks/{actions-418ygYKC.js → actions-DX2gkoOZ.js} +2 -2
  2. package/dist/_chunks/{actions-418ygYKC.js.map → actions-DX2gkoOZ.js.map} +1 -1
  3. package/dist/_chunks/{build-output-helper-DYOWsPjZ.js → build-output-helper-K7oUCRqU.js} +1 -1
  4. package/dist/_chunks/{build-output-helper-DYOWsPjZ.js.map → build-output-helper-K7oUCRqU.js.map} +1 -1
  5. package/dist/_chunks/{codec-registry-G9hD5LOq.js → codec-registry-DqkeAydt.js} +1 -1
  6. package/dist/_chunks/{codec-registry-G9hD5LOq.js.map → codec-registry-DqkeAydt.js.map} +1 -1
  7. package/dist/_chunks/{define-BJMwm0c4.js → define-DMq-Yjp7.js} +2 -2
  8. package/dist/_chunks/{define-BJMwm0c4.js.map → define-DMq-Yjp7.js.map} +1 -1
  9. package/dist/_chunks/{navigation-root-DaERBEGV.js → navigation-root-B5-RmsPi.js} +35 -28
  10. package/dist/_chunks/navigation-root-B5-RmsPi.js.map +1 -0
  11. package/dist/_chunks/{stale-reload-BN2kcE06.js → stale-reload-CCqb62Z3.js} +22 -2
  12. package/dist/_chunks/{stale-reload-BN2kcE06.js.map → stale-reload-CCqb62Z3.js.map} +1 -1
  13. package/dist/_chunks/{use-segment-params-ByGqwUuO.js → use-segment-params-CIeinIx2.js} +2 -32
  14. package/dist/_chunks/{use-segment-params-ByGqwUuO.js.map → use-segment-params-CIeinIx2.js.map} +1 -1
  15. package/dist/adapters/cloudflare.js +1 -1
  16. package/dist/adapters/nitro.js +1 -1
  17. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  18. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  19. package/dist/client/browser-entry/router-init.d.ts +4 -1
  20. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  21. package/dist/client/index.js +35 -79
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/client/internal.js +128 -270
  24. package/dist/client/internal.js.map +1 -1
  25. package/dist/client/link-pending-store.d.ts +19 -17
  26. package/dist/client/link-pending-store.d.ts.map +1 -1
  27. package/dist/client/link.d.ts.map +1 -1
  28. package/dist/client/nav-link-store.d.ts +3 -12
  29. package/dist/client/nav-link-store.d.ts.map +1 -1
  30. package/dist/client/navigation-root.d.ts +4 -1
  31. package/dist/client/navigation-root.d.ts.map +1 -1
  32. package/dist/client/router.d.ts +10 -7
  33. package/dist/client/router.d.ts.map +1 -1
  34. package/dist/client/rsc-fetch.d.ts.map +1 -1
  35. package/dist/client/segment-cache.d.ts +2 -0
  36. package/dist/client/segment-cache.d.ts.map +1 -1
  37. package/dist/client/segment-outlet.d.ts +23 -37
  38. package/dist/client/segment-outlet.d.ts.map +1 -1
  39. package/dist/client/segment-update-context.d.ts +21 -0
  40. package/dist/client/segment-update-context.d.ts.map +1 -0
  41. package/dist/client/top-loader.d.ts.map +1 -1
  42. package/dist/client/use-pending-navigation.d.ts +3 -6
  43. package/dist/client/use-pending-navigation.d.ts.map +1 -1
  44. package/dist/index.js +1 -1
  45. package/dist/params/index.js +1 -1
  46. package/dist/segment-params/index.js +1 -1
  47. package/dist/server/index.js +1 -1
  48. package/dist/server/internal.js +75 -4
  49. package/dist/server/internal.js.map +1 -1
  50. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  51. package/dist/server/route-element-builder.d.ts.map +1 -1
  52. package/dist/server/rsc-entry/helpers.d.ts +1 -0
  53. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  54. package/dist/server/state-tree-diff.d.ts +12 -5
  55. package/dist/server/state-tree-diff.d.ts.map +1 -1
  56. package/package.json +1 -1
  57. package/src/client/browser-entry/hydrate.ts +7 -1
  58. package/src/client/browser-entry/index.ts +3 -1
  59. package/src/client/browser-entry/post-hydration.ts +0 -13
  60. package/src/client/browser-entry/router-init.ts +50 -24
  61. package/src/client/link-pending-store.ts +22 -17
  62. package/src/client/link.tsx +30 -79
  63. package/src/client/nav-link-store.ts +3 -13
  64. package/src/client/navigation-root.tsx +28 -23
  65. package/src/client/router.ts +109 -97
  66. package/src/client/rsc-fetch.ts +85 -6
  67. package/src/client/segment-cache.ts +5 -2
  68. package/src/client/segment-outlet.tsx +40 -49
  69. package/src/client/segment-update-context.ts +42 -0
  70. package/src/client/top-loader.tsx +5 -15
  71. package/src/client/use-pending-navigation.ts +18 -16
  72. package/src/server/pipeline-outcome.ts +23 -2
  73. package/src/server/route-element-builder.ts +69 -2
  74. package/src/server/rsc-entry/helpers.ts +18 -14
  75. package/src/server/state-tree-diff.ts +26 -22
  76. package/dist/_chunks/navigation-root-DaERBEGV.js.map +0 -1
  77. package/dist/client/segment-merger.d.ts +0 -119
  78. package/dist/client/segment-merger.d.ts.map +0 -1
  79. package/src/client/segment-merger.ts +0 -339
@@ -1,11 +1,11 @@
1
1
  import { t as bindUseQueryStates } from "../_chunks/use-query-states-C9G6LP0F.js";
2
2
  import { c as cachedSearchParams, i as _setCachedSearch, n as getSsrData, r as setSsrData, s as cachedSearch, t as clearSsrData } from "../_chunks/ssr-data-DVX8S7vj.js";
3
- import { a as setNavigationState, i as getNavigationState, o as useNavigationContext, r as NavigationProvider, t as setCurrentParams } from "../_chunks/use-segment-params-ByGqwUuO.js";
3
+ import { a as setNavigationState, i as getNavigationState, o as useNavigationContext, r as NavigationProvider, t as setCurrentParams } from "../_chunks/use-segment-params-CIeinIx2.js";
4
4
  import { n as getRouterOrNull, r as setGlobalRouter, t as getRouter } from "../_chunks/router-ref-B2-2kIIC.js";
5
5
  import { TimberErrorBoundary } from "./error-boundary.js";
6
6
  import { n as useSegmentContext, t as SegmentProvider } from "../_chunks/segment-context-Bg0pFbLT.js";
7
- import { t as setHardNavigating } from "../_chunks/navigation-root-DaERBEGV.js";
8
- import { cloneElement, isValidElement } from "react";
7
+ import { t as setHardNavigating } from "../_chunks/navigation-root-B5-RmsPi.js";
8
+ import { r as triggerStaleReload, t as isStaleClientReference } from "../_chunks/stale-reload-CCqb62Z3.js";
9
9
  //#region src/client/segment-cache.ts
10
10
  /**
11
11
  * Maintains the client-side segment tree representing currently mounted
@@ -64,14 +64,15 @@ function buildSegmentTree(segments) {
64
64
  let root;
65
65
  let parent;
66
66
  for (const info of layouts) {
67
+ const id = info.segmentId ?? info.path;
67
68
  const node = {
68
- segment: info.path,
69
+ segment: id,
69
70
  payload: null,
70
71
  isAsync: info.isAsync,
71
72
  children: /* @__PURE__ */ new Map()
72
73
  };
73
74
  if (!root) root = node;
74
- if (parent) parent.children.set(info.path, node);
75
+ if (parent) parent.children.set(id, node);
75
76
  parent = node;
76
77
  }
77
78
  return root;
@@ -157,217 +158,6 @@ var HistoryStack = class {
157
158
  }
158
159
  };
159
160
  //#endregion
160
- //#region src/client/segment-merger.ts
161
- /**
162
- * Segment Merger — client-side tree merging for partial RSC payloads.
163
- *
164
- * When the server skips rendering sync layouts (because the client already
165
- * has them cached), the RSC payload is missing outer segment wrappers.
166
- * This module reconstructs the full element tree by splicing the partial
167
- * payload into cached segment subtrees.
168
- *
169
- * The approach:
170
- * 1. After each full RSC payload render, walk the decoded element tree
171
- * and cache each segment's subtree (identified by SegmentProvider boundaries)
172
- * 2. When a partial payload arrives, wrap it with cached segment elements
173
- * using React.cloneElement to preserve component identity
174
- *
175
- * React.cloneElement preserves the element's `type` — React sees the same
176
- * component at the same tree position and reconciles (preserving state)
177
- * rather than remounting. This is how layout state survives navigations.
178
- *
179
- * Design docs: 19-client-navigation.md §"Navigation Reconciliation"
180
- * Security: access.ts runs on the server regardless of skipping — this
181
- * is a performance optimization only. See 13-security.md.
182
- */
183
- /**
184
- * Cache of React element subtrees per segment path.
185
- * Updated after each navigation with the full decoded RSC element tree.
186
- */
187
- var SegmentElementCache = class {
188
- entries = /* @__PURE__ */ new Map();
189
- get(segmentPath) {
190
- return this.entries.get(segmentPath);
191
- }
192
- set(segmentPath, entry) {
193
- this.entries.set(segmentPath, entry);
194
- }
195
- has(segmentPath) {
196
- return this.entries.has(segmentPath);
197
- }
198
- clear() {
199
- this.entries.clear();
200
- }
201
- get size() {
202
- return this.entries.size;
203
- }
204
- /**
205
- * Get the set of segment paths that are safe for the server to skip.
206
- * Only segments with an inner SegmentProvider (hasMergeableChild) are
207
- * included — the merger can only replace inner SegmentProviders, not
208
- * pages embedded in layout output. Used to filter the state tree.
209
- *
210
- * Returns an empty set if the element cache is empty (no elements
211
- * cached yet). This is the safe default — an empty set means no
212
- * segments pass the filter, so the state tree is empty and the server
213
- * does a full render. The element cache is populated lazily after the
214
- * first SPA navigation (RSC-decoded elements from hydration are
215
- * thenables that can't be walked until React resolves them).
216
- */
217
- getMergeablePaths() {
218
- const paths = /* @__PURE__ */ new Set();
219
- for (const [, entry] of this.entries) if (entry.hasMergeableChild) paths.add(entry.segmentPath);
220
- return paths;
221
- }
222
- };
223
- /**
224
- * Check if a React element is a SegmentProvider by looking for the
225
- * `segments` prop (an array of path segments). This is the only
226
- * component that receives this prop shape.
227
- */
228
- function isSegmentProvider(element) {
229
- if (!isValidElement(element)) return false;
230
- const props = element.props;
231
- return Array.isArray(props.segments);
232
- }
233
- /**
234
- * Extract the segment path from a SegmentProvider element.
235
- *
236
- * Uses the `segmentId` prop if available (set by the server for route groups
237
- * to distinguish siblings that share the same urlPath). Falls back to
238
- * reconstructing from the `segments` array prop.
239
- */
240
- function getSegmentPath(element) {
241
- const props = element.props;
242
- if (props.segmentId) return props.segmentId;
243
- const filtered = props.segments.filter(Boolean);
244
- return filtered.length === 0 ? "/" : "/" + filtered.join("/");
245
- }
246
- /**
247
- * Walk a React element tree and extract all SegmentProvider boundaries.
248
- * Returns an ordered list of segment entries from outermost to innermost.
249
- *
250
- * This only finds SegmentProviders along the main children path — it does
251
- * not descend into parallel routes/slots (those are separate subtrees).
252
- */
253
- function extractSegments(element) {
254
- const segments = [];
255
- walkForSegments(element, segments);
256
- for (let i = 0; i < segments.length; i++) segments[i].hasMergeableChild = i < segments.length - 1;
257
- return segments;
258
- }
259
- function walkForSegments(node, out) {
260
- if (!isValidElement(node)) return;
261
- const el = node;
262
- const props = el.props;
263
- if (isSegmentProvider(node)) {
264
- out.push({
265
- segmentPath: getSegmentPath(el),
266
- element: el,
267
- hasMergeableChild: false
268
- });
269
- walkChildren(props.children, out);
270
- return;
271
- }
272
- walkChildren(props.children, out);
273
- }
274
- function walkChildren(children, out) {
275
- if (children == null) return;
276
- if (Array.isArray(children)) for (const child of children) walkForSegments(child, out);
277
- else walkForSegments(children, out);
278
- }
279
- /**
280
- * Cache all segment subtrees from a fully-rendered RSC element tree.
281
- * Call this after every full RSC payload render (navigate, refresh, hydration).
282
- */
283
- function cacheSegmentElements(element, cache) {
284
- const segments = extractSegments(element);
285
- for (const entry of segments) cache.set(entry.segmentPath, entry);
286
- }
287
- function findSegmentProviderPath(node, targetPath) {
288
- const children = node.props.children;
289
- if (children == null) return null;
290
- if (Array.isArray(children)) for (let i = 0; i < children.length; i++) {
291
- const child = children[i];
292
- if (!isValidElement(child)) continue;
293
- if (isSegmentProvider(child)) {
294
- if (!targetPath || getSegmentPath(child) === targetPath) return [{
295
- element: node,
296
- childIndex: i
297
- }];
298
- }
299
- const deeper = findSegmentProviderPath(child, targetPath);
300
- if (deeper) return [{
301
- element: node,
302
- childIndex: i
303
- }, ...deeper];
304
- }
305
- else if (isValidElement(children)) {
306
- if (isSegmentProvider(children)) {
307
- if (!targetPath || getSegmentPath(children) === targetPath) return [{
308
- element: node,
309
- childIndex: -1
310
- }];
311
- }
312
- const deeper = findSegmentProviderPath(children, targetPath);
313
- if (deeper) return [{
314
- element: node,
315
- childIndex: -1
316
- }, ...deeper];
317
- }
318
- return null;
319
- }
320
- /**
321
- * Replace a nested SegmentProvider within a cached element tree with
322
- * new content. Uses cloneElement along the path to produce a new tree
323
- * with preserved component identity at every level except the replaced node.
324
- *
325
- * @param cachedElement The cached SegmentProvider element for this segment
326
- * @param newInnerContent The new React element to splice in at the inner segment position
327
- * @param innerSegmentPath The path of the inner segment to replace (optional — replaces first found)
328
- * @returns New element tree with the inner segment replaced
329
- */
330
- function replaceInnerSegment(cachedElement, newInnerContent, innerSegmentPath) {
331
- const path = findSegmentProviderPath(cachedElement, innerSegmentPath);
332
- if (!path || path.length === 0) return cachedElement;
333
- let replacement = newInnerContent;
334
- for (let i = path.length - 1; i >= 0; i--) {
335
- const { element, childIndex } = path[i];
336
- if (childIndex === -1) replacement = cloneElement(element, {}, replacement);
337
- else {
338
- const newChildren = [...element.props.children];
339
- newChildren[childIndex] = replacement;
340
- replacement = cloneElement(element, {}, ...newChildren);
341
- }
342
- }
343
- return replacement;
344
- }
345
- /**
346
- * Merge a partial RSC payload with cached segment elements.
347
- *
348
- * When the server skips segments, the partial payload starts from the
349
- * first non-skipped segment. This function wraps it with cached elements
350
- * for the skipped segments, producing a full tree that React can
351
- * reconcile with the mounted tree (preserving layout state).
352
- *
353
- * @param partialPayload The RSC payload element (may be partial)
354
- * @param skippedSegments Ordered list of segment paths that were skipped (outermost first)
355
- * @param cache The segment element cache
356
- * @returns The merged full element tree, or the partial payload if merging isn't possible
357
- */
358
- function mergeSegmentTree(partialPayload, skippedSegments, cache) {
359
- if (!isValidElement(partialPayload)) return partialPayload;
360
- if (skippedSegments.length === 0) return partialPayload;
361
- let result = partialPayload;
362
- for (let i = skippedSegments.length - 1; i >= 0; i--) {
363
- const segmentPath = skippedSegments[i];
364
- const cached = cache.get(segmentPath);
365
- if (!cached) return partialPayload;
366
- result = replaceInnerSegment(cached.element, result);
367
- }
368
- return result;
369
- }
370
- //#endregion
371
161
  //#region src/client/rsc-fetch.ts
372
162
  var RSC_CONTENT_TYPE = "text/x-component";
373
163
  /**
@@ -526,6 +316,46 @@ var ServerErrorResponse = class extends Error {
526
316
  }
527
317
  };
528
318
  /**
319
+ * Wrap a response body stream to track when it's fully consumed.
320
+ * Returns a new body that passes all chunks through unchanged, plus
321
+ * a `done` promise that resolves when the last chunk is read (or
322
+ * rejects if the stream errors).
323
+ *
324
+ * Used to keep React transitions open for the full RSC stream
325
+ * duration — createFromFetch's thenable resolves on shell arrival,
326
+ * but we need stream completion for useOptimistic pending state.
327
+ */
328
+ function trackStreamCompletion(body) {
329
+ let resolveDone;
330
+ let rejectDone;
331
+ const done = new Promise((res, rej) => {
332
+ resolveDone = res;
333
+ rejectDone = rej;
334
+ });
335
+ const reader = body.getReader();
336
+ return {
337
+ body: new ReadableStream({
338
+ async pull(controller) {
339
+ try {
340
+ const result = await reader.read();
341
+ if (result.done) {
342
+ controller.close();
343
+ resolveDone();
344
+ } else controller.enqueue(result.value);
345
+ } catch (error) {
346
+ controller.error(error);
347
+ rejectDone(error);
348
+ }
349
+ },
350
+ cancel(reason) {
351
+ reader.cancel(reason);
352
+ resolveDone();
353
+ }
354
+ }),
355
+ done
356
+ };
357
+ }
358
+ /**
529
359
  * Fetch an RSC payload from the server. If a decodeRsc function is provided,
530
360
  * the response is decoded into a React element tree via createFromFetch.
531
361
  * Otherwise, the raw response text is returned (test mode).
@@ -546,6 +376,7 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
546
376
  let segmentInfo = null;
547
377
  let params = null;
548
378
  let skippedSegments = null;
379
+ let streamDone = Promise.resolve();
549
380
  const wrappedPromise = fetchPromise.then((response) => {
550
381
  if (checkReloadSignal(response)) throw new VersionSkewError();
551
382
  const redirectLocation = response.headers.get("X-Timber-Redirect") || (response.status >= 300 && response.status < 400 ? response.headers.get("Location") : null);
@@ -555,15 +386,26 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
555
386
  segmentInfo = extractSegmentInfo(response);
556
387
  params = extractParams(response);
557
388
  skippedSegments = extractSkippedSegments(response);
389
+ if (response.body) {
390
+ const tracked = trackStreamCompletion(response.body);
391
+ streamDone = tracked.done;
392
+ streamDone.catch(() => {});
393
+ return new Response(tracked.body, {
394
+ headers: response.headers,
395
+ status: response.status
396
+ });
397
+ }
558
398
  return response;
559
399
  });
560
400
  await wrappedPromise;
561
401
  const payload = deps.decodeRsc(wrappedPromise);
562
- const decodePromise = Promise.resolve(payload).then(() => void 0);
563
- decodePromise.catch(() => {});
402
+ const payloadError = new Promise((_, reject) => {
403
+ Promise.resolve(payload).then(() => {}, reject);
404
+ });
405
+ payloadError.catch(() => {});
564
406
  return {
565
407
  payload,
566
- decodePromise,
408
+ decodePromise: Promise.race([streamDone, payloadError]),
567
409
  headElements,
568
410
  segmentInfo,
569
411
  params,
@@ -603,9 +445,7 @@ function createRouter(deps) {
603
445
  const segmentCache = new SegmentCache();
604
446
  const prefetchCache = new PrefetchCache();
605
447
  const historyStack = new HistoryStack();
606
- const segmentElementCache = new SegmentElementCache();
607
448
  let routerPhase = { phase: "idle" };
608
- let navigationId = 0;
609
449
  const pendingListeners = /* @__PURE__ */ new Set();
610
450
  let currentNavAbort = null;
611
451
  /**
@@ -640,31 +480,24 @@ function createRouter(deps) {
640
480
  function renderPayload(payload, navState) {
641
481
  if (deps.renderRoot) deps.renderRoot(payload, navState);
642
482
  }
643
- /**
644
- * Merge a partial RSC payload with cached segment elements if segments
645
- * were skipped. Returns the merged payload ready for rendering.
646
- *
647
- * Segment element caching is deferred to after paint by the caller
648
- * so it doesn't block the render path.
649
- */
650
- async function mergePayload(payload, skippedSegments) {
651
- if (skippedSegments && skippedSegments.length > 0) return mergeSegmentTree(payload != null && typeof payload === "object" && "then" in payload ? await payload : payload, skippedSegments, segmentElementCache);
652
- return payload;
483
+ function isPartialNavigation(skippedSegments) {
484
+ return skippedSegments != null && skippedSegments.length > 0;
653
485
  }
654
486
  /**
655
- * Schedule segment element caching after paint. Resolves thenable
656
- * payloads first since tree walking requires resolved React elements.
657
- * Guards against stale writes: if a newer navigation started before
658
- * this callback fires, the cache write is skipped.
487
+ * Build a segment updates map for partial navigation. Identifies the
488
+ * first non-skipped segment and maps it to the payload content.
659
489
  */
660
- function deferSegmentCaching(payload) {
661
- const capturedNavId = navigationId;
662
- afterPaint(async () => {
663
- if (capturedNavId !== navigationId) return;
664
- const resolved = payload != null && typeof payload === "object" && "then" in payload ? await payload : payload;
665
- if (capturedNavId !== navigationId) return;
666
- cacheSegmentElements(resolved, segmentElementCache);
667
- });
490
+ function buildSegmentUpdates(result) {
491
+ const skipped = new Set(result.skippedSegments);
492
+ const segmentInfo = result.segmentInfo;
493
+ const updates = /* @__PURE__ */ new Map();
494
+ if (segmentInfo) {
495
+ for (const info of segmentInfo) if (!skipped.has(info.segmentId ?? info.path)) {
496
+ updates.set(info.segmentId ?? info.path, result.payload);
497
+ break;
498
+ }
499
+ }
500
+ return updates;
668
501
  }
669
502
  /**
670
503
  * Update navigation state (params + pathname) for the next render.
@@ -689,34 +522,51 @@ function createRouter(deps) {
689
522
  async function renderViaTransition(url, perform) {
690
523
  if (deps.navigateTransition) {
691
524
  let headElements = null;
692
- let decodePromise = null;
693
525
  await deps.navigateTransition(url, async (wrapPayload) => {
694
526
  const result = await perform();
695
527
  headElements = result.headElements;
696
- decodePromise = result.decodePromise;
697
- const merged = await mergePayload(result.payload, result.skippedSegments);
528
+ if (isPartialNavigation(result.skippedSegments)) {
529
+ const segmentUpdates = buildSegmentUpdates(result);
530
+ historyStack.push(url, {
531
+ payload: null,
532
+ headElements: result.headElements,
533
+ params: result.params
534
+ });
535
+ return {
536
+ element: wrapPayload(deps._getCurrentPayload?.() ?? result.payload, result.navState, segmentUpdates),
537
+ decodePromise: result.decodePromise
538
+ };
539
+ }
698
540
  historyStack.push(url, {
699
- payload: merged,
541
+ payload: result.payload,
700
542
  headElements: result.headElements,
701
543
  params: result.params
702
544
  });
703
- deferSegmentCaching(merged);
704
- return wrapPayload(merged, result.navState);
545
+ return {
546
+ element: wrapPayload(result.payload, result.navState),
547
+ decodePromise: result.decodePromise
548
+ };
705
549
  });
706
550
  return {
707
551
  headElements,
708
- decodePromise
552
+ decodePromise: null
709
553
  };
710
554
  }
711
555
  const result = await perform();
712
- const merged = await mergePayload(result.payload, result.skippedSegments);
713
- historyStack.push(url, {
714
- payload: merged,
556
+ if (isPartialNavigation(result.skippedSegments)) historyStack.push(url, {
557
+ payload: null,
715
558
  headElements: result.headElements,
716
559
  params: result.params
717
560
  });
718
- deferSegmentCaching(merged);
719
- renderPayload(merged, result.navState);
561
+ else {
562
+ const payload = result.payload != null && typeof result.payload === "object" && "then" in result.payload ? await result.payload : result.payload;
563
+ historyStack.push(url, {
564
+ payload,
565
+ headElements: result.headElements,
566
+ params: result.params
567
+ });
568
+ renderPayload(payload, result.navState);
569
+ }
720
570
  return {
721
571
  headElements: result.headElements,
722
572
  decodePromise: result.decodePromise
@@ -756,7 +606,7 @@ function createRouter(deps) {
756
606
  skippedSegments: prefetched.skippedSegments ?? null
757
607
  } : void 0;
758
608
  if (result === void 0) {
759
- const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());
609
+ const stateTree = segmentCache.serializeStateTree();
760
610
  const rawCurrentUrl = deps.getCurrentUrl();
761
611
  result = await fetchRscPayload(url, deps, stateTree, rawCurrentUrl.startsWith("http") ? new URL(rawCurrentUrl).pathname : new URL(rawCurrentUrl, "http://localhost").pathname, options.signal);
762
612
  }
@@ -784,7 +634,6 @@ function createRouter(deps) {
784
634
  const replace = options.replace === true;
785
635
  const externalSignal = options._signal;
786
636
  const skipHistory = options._skipHistory === true;
787
- navigationId++;
788
637
  const navAbort = createNavAbort(externalSignal);
789
638
  const currentScrollY = deps.getScrollY();
790
639
  if (deps.saveNavigationEntryScroll) deps.saveNavigationEntryScroll(currentScrollY);
@@ -801,19 +650,18 @@ function createRouter(deps) {
801
650
  }
802
651
  setPending(true, url);
803
652
  try {
804
- const { headElements, decodePromise } = await renderViaTransition(url, () => performNavigationFetch(url, {
653
+ const { headElements } = await renderViaTransition(url, () => performNavigationFetch(url, {
805
654
  replace,
806
655
  signal: navAbort.signal,
807
656
  skipHistory: effectiveSkipHistory
808
657
  }));
809
- if (decodePromise) await decodePromise;
810
658
  applyHead(headElements);
811
659
  window.dispatchEvent(new Event("timber:navigation-end"));
812
660
  restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
813
661
  } catch (error) {
814
662
  if (error instanceof VersionSkewError) {
815
663
  setHardNavigating(true);
816
- const { triggerStaleReload } = await import("../_chunks/stale-reload-BN2kcE06.js");
664
+ const { triggerStaleReload } = await import("../_chunks/stale-reload-CCqb62Z3.js").then((n) => n.n);
817
665
  triggerStaleReload();
818
666
  return new Promise(() => {});
819
667
  }
@@ -828,6 +676,11 @@ function createRouter(deps) {
828
676
  window.location.href = error.url;
829
677
  return new Promise(() => {});
830
678
  }
679
+ if (isStaleClientReference(error)) {
680
+ setHardNavigating(true);
681
+ triggerStaleReload();
682
+ return new Promise(() => {});
683
+ }
831
684
  if (isAbortError(error)) return;
832
685
  throw error;
833
686
  } finally {
@@ -838,11 +691,10 @@ function createRouter(deps) {
838
691
  }
839
692
  async function refresh() {
840
693
  const currentUrl = deps.getCurrentUrl();
841
- navigationId++;
842
694
  const navAbort = createNavAbort();
843
695
  setPending(true, currentUrl);
844
696
  try {
845
- const { headElements, decodePromise } = await renderViaTransition(currentUrl, async () => {
697
+ const { headElements } = await renderViaTransition(currentUrl, async () => {
846
698
  const result = await fetchRscPayload(currentUrl, deps, void 0, void 0, navAbort.signal);
847
699
  updateSegmentCache(result.segmentInfo);
848
700
  const navState = updateNavigationState(result.params, currentUrl);
@@ -851,9 +703,13 @@ function createRouter(deps) {
851
703
  navState
852
704
  };
853
705
  });
854
- if (decodePromise) await decodePromise;
855
706
  applyHead(headElements);
856
707
  } catch (error) {
708
+ if (isStaleClientReference(error)) {
709
+ setHardNavigating(true);
710
+ triggerStaleReload();
711
+ return new Promise(() => {});
712
+ }
857
713
  if (isAbortError(error)) return;
858
714
  throw error;
859
715
  } finally {
@@ -863,7 +719,6 @@ function createRouter(deps) {
863
719
  }
864
720
  }
865
721
  async function handlePopState(url, scrollY = 0, externalSignal) {
866
- navigationId++;
867
722
  const entry = historyStack.get(url);
868
723
  if (entry && entry.payload !== null) {
869
724
  const navState = updateNavigationState(entry.params, url);
@@ -874,8 +729,8 @@ function createRouter(deps) {
874
729
  const navAbort = createNavAbort(externalSignal);
875
730
  setPending(true, url);
876
731
  try {
877
- const { headElements, decodePromise } = await renderViaTransition(url, async () => {
878
- const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths()), void 0, navAbort.signal);
732
+ const { headElements } = await renderViaTransition(url, async () => {
733
+ const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree(), void 0, navAbort.signal);
879
734
  updateSegmentCache(result.segmentInfo);
880
735
  const navState = updateNavigationState(result.params, url);
881
736
  return {
@@ -883,10 +738,14 @@ function createRouter(deps) {
883
738
  navState
884
739
  };
885
740
  });
886
- if (decodePromise) await decodePromise;
887
741
  applyHead(headElements);
888
742
  restoreScrollAfterPaint(scrollY);
889
743
  } catch (error) {
744
+ if (isStaleClientReference(error)) {
745
+ setHardNavigating(true);
746
+ triggerStaleReload();
747
+ return new Promise(() => {});
748
+ }
890
749
  if (isAbortError(error)) return;
891
750
  throw error;
892
751
  } finally {
@@ -902,7 +761,8 @@ function createRouter(deps) {
902
761
  function prefetch(url) {
903
762
  if (prefetchCache.get(url) !== void 0) return;
904
763
  if (historyStack.has(url)) return;
905
- fetchRscPayload(url, deps, segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths())).then((result) => {
764
+ fetchRscPayload(url, deps, segmentCache.serializeStateTree()).then((result) => {
765
+ result.decodePromise?.catch(() => {});
906
766
  prefetchCache.set(url, result);
907
767
  }, () => {});
908
768
  }
@@ -923,12 +783,10 @@ function createRouter(deps) {
923
783
  payload: element,
924
784
  headElements
925
785
  });
926
- deferSegmentCaching(element);
927
786
  renderPayload(element, getNavigationState());
928
787
  applyHead(headElements);
929
788
  },
930
789
  initSegmentCache: (segments) => updateSegmentCache(segments),
931
- cacheElementTree: (element) => cacheSegmentElements(element, segmentElementCache),
932
790
  segmentCache,
933
791
  prefetchCache,
934
792
  historyStack