@timber-js/app 0.2.0-alpha.166 → 0.2.0-alpha.167

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 (69) hide show
  1. package/dist/_chunks/{actions-CDPfMp_I.js → actions-TSxpXLHJ.js} +2 -2
  2. package/dist/_chunks/{actions-CDPfMp_I.js.map → actions-TSxpXLHJ.js.map} +1 -1
  3. package/dist/_chunks/{cache-api-DygSeKCB.js → cache-api-DzpQQOEx.js} +52 -48
  4. package/dist/_chunks/{cache-api-DygSeKCB.js.map → cache-api-DzpQQOEx.js.map} +1 -1
  5. package/dist/_chunks/{cli-schema-sync-EXGYPhI2.js → cli-schema-sync-wX-i90Og.js} +4 -2
  6. package/dist/_chunks/cli-schema-sync-wX-i90Og.js.map +1 -0
  7. package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
  8. package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
  9. package/dist/_chunks/logger-t3uxAmbX.js.map +1 -1
  10. package/dist/_chunks/{walkers-DBVzXuWc.js → walkers-Cfwvl-UC.js} +2 -2
  11. package/dist/_chunks/{walkers-DBVzXuWc.js.map → walkers-Cfwvl-UC.js.map} +1 -1
  12. package/dist/adapters/cloudflare-dev.js +1 -1
  13. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  14. package/dist/adapters/cloudflare.d.ts +12 -1
  15. package/dist/adapters/cloudflare.d.ts.map +1 -1
  16. package/dist/adapters/cloudflare.js +2 -461
  17. package/dist/adapters/types.d.ts +2 -0
  18. package/dist/adapters/types.d.ts.map +1 -1
  19. package/dist/cache/cache-api.d.ts +33 -11
  20. package/dist/cache/cache-api.d.ts.map +1 -1
  21. package/dist/cache/index.js +1 -1
  22. package/dist/cli.js +1 -1
  23. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  24. package/dist/client/history.d.ts +10 -0
  25. package/dist/client/history.d.ts.map +1 -1
  26. package/dist/client/internal.js +138 -114
  27. package/dist/client/internal.js.map +1 -1
  28. package/dist/client/router.d.ts.map +1 -1
  29. package/dist/index.js +36 -27
  30. package/dist/index.js.map +1 -1
  31. package/dist/plugins/adapter-build.d.ts.map +1 -1
  32. package/dist/plugins/cache.d.ts +8 -8
  33. package/dist/plugins/cache.d.ts.map +1 -1
  34. package/dist/routing/index.js +2 -2
  35. package/dist/server/als-registry.d.ts +7 -0
  36. package/dist/server/als-registry.d.ts.map +1 -1
  37. package/dist/server/deny-boundary.d.ts +3 -1
  38. package/dist/server/deny-boundary.d.ts.map +1 -1
  39. package/dist/server/index.js +1 -1
  40. package/dist/server/internal.js +6 -3
  41. package/dist/server/internal.js.map +1 -1
  42. package/dist/server/route-element-builder.d.ts +9 -0
  43. package/dist/server/route-element-builder.d.ts.map +1 -1
  44. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  45. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  46. package/dist/server/rsc-entry/rsc-stream.d.ts +8 -0
  47. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  48. package/docs/api/32-api-cache.mdx +4 -4
  49. package/docs/learn/09-caching.mdx +5 -5
  50. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  51. package/docs/more/50-ai-agent-instructions.mdx +5 -5
  52. package/package.json +3 -2
  53. package/src/adapters/cloudflare.ts +63 -25
  54. package/src/adapters/types.ts +2 -0
  55. package/src/cache/cache-api.ts +84 -84
  56. package/src/client/browser-entry/post-hydration.ts +16 -9
  57. package/src/client/history.ts +11 -0
  58. package/src/client/router.ts +212 -206
  59. package/src/plugins/adapter-build.ts +1 -0
  60. package/src/plugins/cache.ts +45 -30
  61. package/src/routing/scanner.ts +7 -0
  62. package/src/server/als-registry.ts +7 -0
  63. package/src/server/deny-boundary.ts +6 -3
  64. package/src/server/route-element-builder.ts +49 -8
  65. package/src/server/rsc-entry/render-route.ts +3 -1
  66. package/src/server/rsc-entry/rsc-payload.ts +21 -4
  67. package/src/server/rsc-entry/rsc-stream.ts +8 -0
  68. package/dist/_chunks/cli-schema-sync-EXGYPhI2.js.map +0 -1
  69. package/dist/adapters/cloudflare.js.map +0 -1
@@ -458,6 +458,59 @@ function createRouter(deps) {
458
458
  function renderPayload(payload, navState) {
459
459
  if (deps.renderRoot) deps.renderRoot(payload, navState);
460
460
  }
461
+ /**
462
+ * Atomically update all navigation-owned state for a new page. Every
463
+ * code path that changes the "current page" must go through this
464
+ * function — making "forgot a field" impossible by construction.
465
+ *
466
+ * The three operations:
467
+ * 1. Segment cache — update from server-provided segment metadata
468
+ * 2. Navigation state — params + pathname for useSegmentParams/usePathname
469
+ * 3. History stack — store the payload for instant back/forward replay
470
+ */
471
+ function commitNavigation(url, opts) {
472
+ if (opts.segmentInfo && opts.segmentInfo.length > 0) updateSegmentCache(opts.segmentInfo);
473
+ else if (opts.clearSegmentCacheOnEmpty) segmentCache.clear();
474
+ const navState = updateNavigationState(opts.params, url);
475
+ historyStack.push(url, {
476
+ payload: opts.payload,
477
+ params: navState.params,
478
+ segmentInfo: opts.segmentInfo
479
+ });
480
+ return navState;
481
+ }
482
+ /**
483
+ * Wrap a navigation in the standard abort/pending/cleanup lifecycle.
484
+ * Consolidates the createNavAbort + setPending + staleness-guarded
485
+ * finally that was duplicated across navigate, refresh, and both
486
+ * handlePopState paths. AbortErrors are swallowed (not application
487
+ * errors); all other errors propagate to the caller.
488
+ */
489
+ async function runNavigation(url, fn, externalSignal) {
490
+ const navAbort = createNavAbort(externalSignal);
491
+ setPending(true, url);
492
+ try {
493
+ await fn(navAbort);
494
+ } catch (error) {
495
+ if (isAbortError(error)) return;
496
+ throw error;
497
+ } finally {
498
+ if (currentNavAbort === navAbort) {
499
+ currentNavAbort = null;
500
+ setPending(false);
501
+ deps.completeRouterNavigation?.();
502
+ }
503
+ }
504
+ }
505
+ /**
506
+ * Resolve thenable payloads in the test/fallback path (no navigateTransition).
507
+ * In production, React handles thenables from createFromFetch directly via
508
+ * Suspense. In tests, renderRoot is a plain mock that expects resolved values.
509
+ */
510
+ async function resolveForFallback(payload) {
511
+ if (!deps.navigateTransition && payload != null && typeof payload === "object" && "then" in payload) return await payload;
512
+ return payload;
513
+ }
461
514
  function isPartialNavigation(skippedSegments) {
462
515
  return skippedSegments != null && skippedSegments.length > 0;
463
516
  }
@@ -499,9 +552,11 @@ function createRouter(deps) {
499
552
  }
500
553
  /**
501
554
  * Render a payload via navigateTransition (production) or renderRoot (tests).
502
- * The perform callback should fetch data, update state, and return the
503
- * FetchResult plus the NavigationState (so it can be passed explicitly
504
- * to wrapPayload/renderRoot without temporal coupling).
555
+ * The perform callback should fetch data, call commitNavigation, and return
556
+ * the FetchResult plus the NavigationState.
557
+ *
558
+ * State management (segmentCache, navState, historyStack) is handled by
559
+ * commitNavigation inside perform — this function only handles rendering.
505
560
  */
506
561
  async function renderViaTransition(url, perform) {
507
562
  if (deps.navigateTransition) {
@@ -509,19 +564,11 @@ function createRouter(deps) {
509
564
  const result = await perform();
510
565
  if (isPartialNavigation(result.skippedSegments)) {
511
566
  const segmentUpdates = buildSegmentUpdates(result);
512
- historyStack.push(url, {
513
- payload: null,
514
- params: result.params
515
- });
516
567
  return {
517
568
  element: wrapPayload(deps._getCurrentPayload?.() ?? result.payload, result.navState, segmentUpdates),
518
569
  decodePromise: result.decodePromise
519
570
  };
520
571
  }
521
- historyStack.push(url, {
522
- payload: result.payload,
523
- params: result.params
524
- });
525
572
  return {
526
573
  element: wrapPayload(result.payload, result.navState),
527
574
  decodePromise: result.decodePromise
@@ -530,18 +577,7 @@ function createRouter(deps) {
530
577
  return;
531
578
  }
532
579
  const result = await perform();
533
- if (isPartialNavigation(result.skippedSegments)) historyStack.push(url, {
534
- payload: null,
535
- params: result.params
536
- });
537
- else {
538
- const payload = result.payload != null && typeof result.payload === "object" && "then" in result.payload ? await result.payload : result.payload;
539
- historyStack.push(url, {
540
- payload,
541
- params: result.params
542
- });
543
- renderPayload(payload, result.navState);
544
- }
580
+ if (!isPartialNavigation(result.skippedSegments)) renderPayload(result.payload, result.navState);
545
581
  }
546
582
  /** Run a callback after the next paint (after React commit). */
547
583
  function afterPaint(callback) {
@@ -601,10 +637,15 @@ function createRouter(deps) {
601
637
  }, "", commitUrl);
602
638
  deps.setRouterNavigating?.(false);
603
639
  }
604
- updateSegmentCache(result.segmentInfo);
605
- const navState = updateNavigationState(result.params, url);
640
+ const payload = await resolveForFallback(result.payload);
641
+ const navState = commitNavigation(url, {
642
+ payload: isPartialNavigation(result.skippedSegments) ? null : payload,
643
+ params: result.params,
644
+ segmentInfo: result.segmentInfo
645
+ });
606
646
  return {
607
647
  ...result,
648
+ payload,
608
649
  navState
609
650
  };
610
651
  }
@@ -616,7 +657,6 @@ function createRouter(deps) {
616
657
  const hashIndex = url.indexOf("#");
617
658
  const hash = hashIndex === -1 ? "" : url.slice(hashIndex);
618
659
  const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);
619
- const navAbort = createNavAbort(externalSignal);
620
660
  const currentScrollY = deps.getScrollY();
621
661
  if (deps.saveNavigationEntryScroll) deps.saveNavigationEntryScroll(currentScrollY);
622
662
  else deps.replaceState({
@@ -624,107 +664,91 @@ function createRouter(deps) {
624
664
  scrollY: currentScrollY
625
665
  }, "", deps.getCurrentUrl());
626
666
  let effectiveSkipHistory = skipHistory;
627
- if (!skipHistory && deps.navigationNavigate) {
628
- deps.setRouterNavigating?.(true);
629
- deps.navigationNavigate(url, replace);
630
- deps.setRouterNavigating?.(false);
631
- effectiveSkipHistory = true;
632
- }
633
- setPending(true, url);
634
- try {
635
- await renderViaTransition(fetchUrl, () => performNavigationFetch(fetchUrl, {
636
- replace,
637
- commitUrl: url,
638
- signal: navAbort.signal,
639
- skipHistory: effectiveSkipHistory
640
- }));
641
- window.dispatchEvent(new Event("timber:navigation-end"));
642
- if (scroll && hash) scrollToHashAfterPaint(hash);
643
- else restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
644
- } catch (error) {
645
- if (error instanceof VersionSkewError) {
646
- setHardNavigating(true);
647
- window.location.reload();
648
- return new Promise(() => {});
649
- }
650
- if (error instanceof RedirectError) {
651
- if (currentNavAbort !== navAbort) return;
652
- await navigate(error.redirectUrl, { replace: true });
653
- return;
667
+ await runNavigation(url, async (navAbort) => {
668
+ if (!effectiveSkipHistory && deps.navigationNavigate) {
669
+ deps.setRouterNavigating?.(true);
670
+ deps.navigationNavigate(url, replace);
671
+ deps.setRouterNavigating?.(false);
672
+ effectiveSkipHistory = true;
654
673
  }
655
- if (error instanceof ServerErrorResponse) {
656
- setHardNavigating(true);
657
- window.location.href = error.url;
658
- return new Promise(() => {});
659
- }
660
- if (isAbortError(error)) return;
661
- throw error;
662
- } finally {
663
- if (currentNavAbort === navAbort) {
664
- currentNavAbort = null;
665
- setPending(false);
666
- deps.completeRouterNavigation?.();
674
+ try {
675
+ await renderViaTransition(fetchUrl, () => performNavigationFetch(fetchUrl, {
676
+ replace,
677
+ commitUrl: url,
678
+ signal: navAbort.signal,
679
+ skipHistory: effectiveSkipHistory
680
+ }));
681
+ window.dispatchEvent(new Event("timber:navigation-end"));
682
+ if (scroll && hash) scrollToHashAfterPaint(hash);
683
+ else restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
684
+ } catch (error) {
685
+ if (error instanceof VersionSkewError) {
686
+ setHardNavigating(true);
687
+ window.location.reload();
688
+ await new Promise(() => {});
689
+ }
690
+ if (error instanceof RedirectError) {
691
+ if (currentNavAbort !== navAbort) return;
692
+ await navigate(error.redirectUrl, { replace: true });
693
+ return;
694
+ }
695
+ if (error instanceof ServerErrorResponse) {
696
+ setHardNavigating(true);
697
+ window.location.href = error.url;
698
+ await new Promise(() => {});
699
+ }
700
+ throw error;
667
701
  }
668
- }
702
+ }, externalSignal);
669
703
  }
670
704
  async function refresh() {
671
705
  const currentUrl = deps.getCurrentUrl();
672
- const navAbort = createNavAbort();
673
- setPending(true, currentUrl);
674
- try {
706
+ await runNavigation(currentUrl, async (navAbort) => {
675
707
  await renderViaTransition(currentUrl, async () => {
676
708
  const result = await fetchRscPayload(currentUrl, deps, void 0, void 0, navAbort.signal);
677
- updateSegmentCache(result.segmentInfo);
678
- const navState = updateNavigationState(result.params, currentUrl);
709
+ const payload = await resolveForFallback(result.payload);
710
+ const navState = commitNavigation(currentUrl, {
711
+ payload,
712
+ params: result.params,
713
+ segmentInfo: result.segmentInfo
714
+ });
679
715
  return {
680
716
  ...result,
717
+ payload,
681
718
  navState
682
719
  };
683
720
  });
684
- } catch (error) {
685
- if (isAbortError(error)) return;
686
- throw error;
687
- } finally {
688
- if (currentNavAbort === navAbort) {
689
- currentNavAbort = null;
690
- setPending(false);
691
- deps.completeRouterNavigation?.();
692
- }
693
- }
721
+ });
694
722
  }
695
723
  async function handlePopState(url, scrollY = 0, externalSignal) {
696
724
  const entry = historyStack.get(url);
697
- if (entry && entry.payload !== null) {
698
- createNavAbort(externalSignal);
699
- currentNavAbort = null;
700
- setPending(false);
701
- const navState = updateNavigationState(entry.params, url);
725
+ if (entry && entry.payload !== null) await runNavigation(url, async () => {
726
+ const navState = commitNavigation(url, {
727
+ payload: entry.payload,
728
+ params: entry.params,
729
+ segmentInfo: entry.segmentInfo,
730
+ clearSegmentCacheOnEmpty: true
731
+ });
702
732
  renderPayload(entry.payload, navState);
703
733
  restoreScrollAfterPaint(scrollY);
704
- } else {
705
- const navAbort = createNavAbort(externalSignal);
706
- setPending(true, url);
707
- try {
708
- await renderViaTransition(url, async () => {
709
- const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree(), void 0, navAbort.signal);
710
- updateSegmentCache(result.segmentInfo);
711
- const navState = updateNavigationState(result.params, url);
712
- return {
713
- ...result,
714
- navState
715
- };
734
+ }, externalSignal);
735
+ else await runNavigation(url, async (navAbort) => {
736
+ await renderViaTransition(url, async () => {
737
+ const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree(), void 0, navAbort.signal);
738
+ const payload = await resolveForFallback(result.payload);
739
+ const navState = commitNavigation(url, {
740
+ payload,
741
+ params: result.params,
742
+ segmentInfo: result.segmentInfo
716
743
  });
717
- restoreScrollAfterPaint(scrollY);
718
- } catch (error) {
719
- if (isAbortError(error)) return;
720
- throw error;
721
- } finally {
722
- if (currentNavAbort === navAbort) {
723
- currentNavAbort = null;
724
- setPending(false);
725
- }
726
- }
727
- }
744
+ return {
745
+ ...result,
746
+ payload,
747
+ navState
748
+ };
749
+ });
750
+ restoreScrollAfterPaint(scrollY);
751
+ }, externalSignal);
728
752
  }
729
753
  /**
730
754
  * Prefetch an RSC payload for a URL and store it in the prefetch cache.
@@ -753,12 +777,12 @@ function createRouter(deps) {
753
777
  prefetch,
754
778
  applyRevalidation(element) {
755
779
  const currentUrl = deps.getCurrentUrl();
756
- const navState = getNavigationState();
757
- historyStack.push(currentUrl, {
780
+ const existingEntry = historyStack.get(currentUrl);
781
+ renderPayload(element, commitNavigation(currentUrl, {
758
782
  payload: element,
759
- params: navState.params
760
- });
761
- renderPayload(element, navState);
783
+ params: getNavigationState().params,
784
+ segmentInfo: existingEntry?.segmentInfo
785
+ }));
762
786
  },
763
787
  initSegmentCache: (segments) => updateSegmentCache(segments),
764
788
  segmentCache,