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

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 (81) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{actions-Dy8vOjFt.js → actions-418ygYKC.js} +5 -5
  3. package/dist/_chunks/{actions-Dy8vOjFt.js.map → actions-418ygYKC.js.map} +1 -1
  4. package/dist/_chunks/{cli-schema-sync-VcCf1TFY.js → cli-schema-sync-DFHBj6yZ.js} +59 -12
  5. package/dist/_chunks/{cli-schema-sync-VcCf1TFY.js.map → cli-schema-sync-DFHBj6yZ.js.map} +1 -1
  6. package/dist/_chunks/cli-schema-sync-vdobDWwX.js +2 -0
  7. package/dist/_chunks/{define-CPVQaWbx.js → define-BJMwm0c4.js} +6 -18
  8. package/dist/_chunks/define-BJMwm0c4.js.map +1 -0
  9. package/dist/_chunks/{handler-store-CcwNX-gk.js → handler-store-bIAstrxQ.js} +1 -1
  10. package/dist/_chunks/{handler-store-CcwNX-gk.js.map → handler-store-bIAstrxQ.js.map} +1 -1
  11. package/dist/_chunks/{merge-search-params-az6ZiBeU.js → merge-search-params-Cm1j99HC.js} +1 -1
  12. package/dist/_chunks/{merge-search-params-az6ZiBeU.js.map → merge-search-params-Cm1j99HC.js.map} +1 -1
  13. package/dist/_chunks/{navigation-root-DoRTeBm5.js → navigation-root-DaERBEGV.js} +2 -2
  14. package/dist/_chunks/{navigation-root-DoRTeBm5.js.map → navigation-root-DaERBEGV.js.map} +1 -1
  15. package/dist/_chunks/{resolve-schema-cURaYvaD.js → resolve-schema-DU2nbVOi.js} +1 -1
  16. package/dist/_chunks/{resolve-schema-cURaYvaD.js.map → resolve-schema-DU2nbVOi.js.map} +1 -1
  17. package/dist/_chunks/{router-ref-CpvjBqdu.js → router-ref-B2-2kIIC.js} +2 -2
  18. package/dist/_chunks/{router-ref-CpvjBqdu.js.map → router-ref-B2-2kIIC.js.map} +1 -1
  19. package/dist/_chunks/{segment-context-EsfQpS--.js → segment-context-Bg0pFbLT.js} +1 -1
  20. package/dist/_chunks/{segment-context-EsfQpS--.js.map → segment-context-Bg0pFbLT.js.map} +1 -1
  21. package/dist/_chunks/{ssr-data-m9-cNEIw.js → ssr-data-DVX8S7vj.js} +1 -1
  22. package/dist/_chunks/{ssr-data-m9-cNEIw.js.map → ssr-data-DVX8S7vj.js.map} +1 -1
  23. package/dist/_chunks/{use-segment-params-CtIAjqK0.js → use-segment-params-ByGqwUuO.js} +5 -9
  24. package/dist/_chunks/use-segment-params-ByGqwUuO.js.map +1 -0
  25. package/dist/_chunks/{validation-BMU5MPzP.js → validation-AfEFM5ze.js} +1 -1
  26. package/dist/_chunks/{validation-BMU5MPzP.js.map → validation-AfEFM5ze.js.map} +1 -1
  27. package/dist/_chunks/{walkers-tkw7sDlj.js → walkers-DMSGZeCk.js} +2 -2
  28. package/dist/_chunks/{walkers-tkw7sDlj.js.map → walkers-DMSGZeCk.js.map} +1 -1
  29. package/dist/cache/index.js +1 -1
  30. package/dist/cli-schema-sync.d.ts +6 -4
  31. package/dist/cli-schema-sync.d.ts.map +1 -1
  32. package/dist/cli.d.ts +1 -1
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +12 -6
  35. package/dist/cli.js.map +1 -1
  36. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  37. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  38. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  39. package/dist/client/error-boundary.js +2 -2
  40. package/dist/client/index.d.ts +1 -0
  41. package/dist/client/index.d.ts.map +1 -1
  42. package/dist/client/index.js +47 -14
  43. package/dist/client/index.js.map +1 -1
  44. package/dist/client/internal.js +87 -64
  45. package/dist/client/internal.js.map +1 -1
  46. package/dist/client/link.d.ts.map +1 -1
  47. package/dist/client/navigation-context.d.ts +1 -0
  48. package/dist/client/navigation-context.d.ts.map +1 -1
  49. package/dist/client/router.d.ts.map +1 -1
  50. package/dist/client/rsc-fetch.d.ts +8 -0
  51. package/dist/client/rsc-fetch.d.ts.map +1 -1
  52. package/dist/client/shallow-url.d.ts +10 -0
  53. package/dist/client/shallow-url.d.ts.map +1 -0
  54. package/dist/client/use-search-params.d.ts +7 -5
  55. package/dist/client/use-search-params.d.ts.map +1 -1
  56. package/dist/cookies/index.js +2 -2
  57. package/dist/index.js +3 -3
  58. package/dist/params/index.js +1 -1
  59. package/dist/routing/index.js +2 -2
  60. package/dist/segment-params/define.d.ts +0 -6
  61. package/dist/segment-params/define.d.ts.map +1 -1
  62. package/dist/segment-params/index.js +1 -1
  63. package/dist/server/index.js +1 -1
  64. package/dist/server/internal.js +3 -3
  65. package/package.json +6 -7
  66. package/src/cli-schema-sync.ts +80 -13
  67. package/src/cli.ts +18 -7
  68. package/src/client/browser-entry/action-dispatch.ts +9 -10
  69. package/src/client/browser-entry/hydrate.ts +2 -0
  70. package/src/client/browser-entry/post-hydration.ts +13 -6
  71. package/src/client/index.ts +1 -5
  72. package/src/client/link.tsx +25 -12
  73. package/src/client/navigation-context.ts +2 -1
  74. package/src/client/router.ts +78 -27
  75. package/src/client/rsc-fetch.ts +20 -5
  76. package/src/client/shallow-url.ts +22 -0
  77. package/src/client/use-search-params.ts +25 -44
  78. package/src/segment-params/define.ts +2 -29
  79. package/dist/_chunks/cli-schema-sync-4HcQIziG.js +0 -2
  80. package/dist/_chunks/define-CPVQaWbx.js.map +0 -1
  81. package/dist/_chunks/use-segment-params-CtIAjqK0.js.map +0 -1
@@ -1,11 +1,11 @@
1
1
  import { t as bindUseQueryStates } from "../_chunks/use-query-states-C9G6LP0F.js";
2
- import { c as cachedSearchParams, i as _setCachedSearch, n as getSsrData, r as setSsrData, s as cachedSearch, t as clearSsrData } from "../_chunks/ssr-data-m9-cNEIw.js";
3
- import { n as getRouterOrNull, r as setGlobalRouter, t as getRouter } from "../_chunks/router-ref-CpvjBqdu.js";
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";
4
+ import { n as getRouterOrNull, r as setGlobalRouter, t as getRouter } from "../_chunks/router-ref-B2-2kIIC.js";
4
5
  import { TimberErrorBoundary } from "./error-boundary.js";
5
- import { n as useSegmentContext, t as SegmentProvider } from "../_chunks/segment-context-EsfQpS--.js";
6
- import { t as setHardNavigating } from "../_chunks/navigation-root-DoRTeBm5.js";
7
- import { a as getNavigationState, i as NavigationProvider, o as setNavigationState, t as setCurrentParams } from "../_chunks/use-segment-params-CtIAjqK0.js";
8
- import { cloneElement, isValidElement, useSyncExternalStore } from "react";
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";
9
9
  //#region src/client/segment-cache.ts
10
10
  /**
11
11
  * Maintains the client-side segment tree representing currently mounted
@@ -558,8 +558,12 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
558
558
  return response;
559
559
  });
560
560
  await wrappedPromise;
561
+ const payload = deps.decodeRsc(wrappedPromise);
562
+ const decodePromise = Promise.resolve(payload).then(() => void 0);
563
+ decodePromise.catch(() => {});
561
564
  return {
562
- payload: await deps.decodeRsc(wrappedPromise),
565
+ payload,
566
+ decodePromise,
563
567
  headElements,
564
568
  segmentInfo,
565
569
  params,
@@ -577,6 +581,7 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
577
581
  }
578
582
  return {
579
583
  payload: await response.text(),
584
+ decodePromise: null,
580
585
  headElements: extractHeadElements(response),
581
586
  segmentInfo: extractSegmentInfo(response),
582
587
  params: extractParams(response),
@@ -600,6 +605,7 @@ function createRouter(deps) {
600
605
  const historyStack = new HistoryStack();
601
606
  const segmentElementCache = new SegmentElementCache();
602
607
  let routerPhase = { phase: "idle" };
608
+ let navigationId = 0;
603
609
  const pendingListeners = /* @__PURE__ */ new Set();
604
610
  let currentNavAbort = null;
605
611
  /**
@@ -636,14 +642,29 @@ function createRouter(deps) {
636
642
  }
637
643
  /**
638
644
  * Merge a partial RSC payload with cached segment elements if segments
639
- * were skipped, then cache segments from the (merged) payload.
640
- * Returns the merged payload ready for rendering.
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;
653
+ }
654
+ /**
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.
641
659
  */
642
- function mergeAndCachePayload(payload, skippedSegments) {
643
- let merged = payload;
644
- if (skippedSegments && skippedSegments.length > 0) merged = mergeSegmentTree(payload, skippedSegments, segmentElementCache);
645
- cacheSegmentElements(merged, segmentElementCache);
646
- return merged;
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
+ });
647
668
  }
648
669
  /**
649
670
  * Update navigation state (params + pathname) for the next render.
@@ -656,44 +677,50 @@ function createRouter(deps) {
656
677
  function updateNavigationState(params, url) {
657
678
  const resolvedParams = params ?? {};
658
679
  setCurrentParams(resolvedParams);
680
+ const parsed = new URL(url, "http://localhost");
659
681
  const navState = {
660
682
  params: resolvedParams,
661
- pathname: url.startsWith("http") ? new URL(url).pathname : url.split("?")[0] || "/"
683
+ pathname: parsed.pathname || "/",
684
+ search: parsed.search
662
685
  };
663
686
  setNavigationState(navState);
664
687
  return navState;
665
688
  }
666
- /**
667
- * Render a payload via navigateTransition (production) or renderRoot (tests).
668
- * The perform callback should fetch data, update state, and return the
669
- * FetchResult plus the NavigationState (so it can be passed explicitly
670
- * to wrapPayload/renderRoot without temporal coupling).
671
- */
672
689
  async function renderViaTransition(url, perform) {
673
690
  if (deps.navigateTransition) {
674
691
  let headElements = null;
692
+ let decodePromise = null;
675
693
  await deps.navigateTransition(url, async (wrapPayload) => {
676
694
  const result = await perform();
677
695
  headElements = result.headElements;
678
- const merged = mergeAndCachePayload(result.payload, result.skippedSegments);
696
+ decodePromise = result.decodePromise;
697
+ const merged = await mergePayload(result.payload, result.skippedSegments);
679
698
  historyStack.push(url, {
680
699
  payload: merged,
681
700
  headElements: result.headElements,
682
701
  params: result.params
683
702
  });
703
+ deferSegmentCaching(merged);
684
704
  return wrapPayload(merged, result.navState);
685
705
  });
686
- return headElements;
706
+ return {
707
+ headElements,
708
+ decodePromise
709
+ };
687
710
  }
688
711
  const result = await perform();
689
- const merged = mergeAndCachePayload(result.payload, result.skippedSegments);
712
+ const merged = await mergePayload(result.payload, result.skippedSegments);
690
713
  historyStack.push(url, {
691
714
  payload: merged,
692
715
  headElements: result.headElements,
693
716
  params: result.params
694
717
  });
718
+ deferSegmentCaching(merged);
695
719
  renderPayload(merged, result.navState);
696
- return result.headElements;
720
+ return {
721
+ headElements: result.headElements,
722
+ decodePromise: result.decodePromise
723
+ };
697
724
  }
698
725
  /** Apply head elements (title, meta tags) to the DOM if available. */
699
726
  function applyHead(elements) {
@@ -722,6 +749,7 @@ function createRouter(deps) {
722
749
  const prefetched = prefetchCache.consume(url);
723
750
  let result = prefetched ? {
724
751
  payload: prefetched.payload,
752
+ decodePromise: null,
725
753
  headElements: prefetched.headElements,
726
754
  segmentInfo: prefetched.segmentInfo ?? null,
727
755
  params: prefetched.params ?? null,
@@ -756,6 +784,7 @@ function createRouter(deps) {
756
784
  const replace = options.replace === true;
757
785
  const externalSignal = options._signal;
758
786
  const skipHistory = options._skipHistory === true;
787
+ navigationId++;
759
788
  const navAbort = createNavAbort(externalSignal);
760
789
  const currentScrollY = deps.getScrollY();
761
790
  if (deps.saveNavigationEntryScroll) deps.saveNavigationEntryScroll(currentScrollY);
@@ -772,11 +801,13 @@ function createRouter(deps) {
772
801
  }
773
802
  setPending(true, url);
774
803
  try {
775
- applyHead(await renderViaTransition(url, () => performNavigationFetch(url, {
804
+ const { headElements, decodePromise } = await renderViaTransition(url, () => performNavigationFetch(url, {
776
805
  replace,
777
806
  signal: navAbort.signal,
778
807
  skipHistory: effectiveSkipHistory
779
- })));
808
+ }));
809
+ if (decodePromise) await decodePromise;
810
+ applyHead(headElements);
780
811
  window.dispatchEvent(new Event("timber:navigation-end"));
781
812
  restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
782
813
  } catch (error) {
@@ -807,10 +838,11 @@ function createRouter(deps) {
807
838
  }
808
839
  async function refresh() {
809
840
  const currentUrl = deps.getCurrentUrl();
841
+ navigationId++;
810
842
  const navAbort = createNavAbort();
811
843
  setPending(true, currentUrl);
812
844
  try {
813
- applyHead(await renderViaTransition(currentUrl, async () => {
845
+ const { headElements, decodePromise } = await renderViaTransition(currentUrl, async () => {
814
846
  const result = await fetchRscPayload(currentUrl, deps, void 0, void 0, navAbort.signal);
815
847
  updateSegmentCache(result.segmentInfo);
816
848
  const navState = updateNavigationState(result.params, currentUrl);
@@ -818,7 +850,9 @@ function createRouter(deps) {
818
850
  ...result,
819
851
  navState
820
852
  };
821
- }));
853
+ });
854
+ if (decodePromise) await decodePromise;
855
+ applyHead(headElements);
822
856
  } catch (error) {
823
857
  if (isAbortError(error)) return;
824
858
  throw error;
@@ -829,6 +863,7 @@ function createRouter(deps) {
829
863
  }
830
864
  }
831
865
  async function handlePopState(url, scrollY = 0, externalSignal) {
866
+ navigationId++;
832
867
  const entry = historyStack.get(url);
833
868
  if (entry && entry.payload !== null) {
834
869
  const navState = updateNavigationState(entry.params, url);
@@ -839,7 +874,7 @@ function createRouter(deps) {
839
874
  const navAbort = createNavAbort(externalSignal);
840
875
  setPending(true, url);
841
876
  try {
842
- applyHead(await renderViaTransition(url, async () => {
877
+ const { headElements, decodePromise } = await renderViaTransition(url, async () => {
843
878
  const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths()), void 0, navAbort.signal);
844
879
  updateSegmentCache(result.segmentInfo);
845
880
  const navState = updateNavigationState(result.params, url);
@@ -847,7 +882,9 @@ function createRouter(deps) {
847
882
  ...result,
848
883
  navState
849
884
  };
850
- }));
885
+ });
886
+ if (decodePromise) await decodePromise;
887
+ applyHead(headElements);
851
888
  restoreScrollAfterPaint(scrollY);
852
889
  } catch (error) {
853
890
  if (isAbortError(error)) return;
@@ -882,12 +919,12 @@ function createRouter(deps) {
882
919
  prefetch,
883
920
  applyRevalidation(element, headElements) {
884
921
  const currentUrl = deps.getCurrentUrl();
885
- const merged = mergeAndCachePayload(element, null);
886
922
  historyStack.push(currentUrl, {
887
- payload: merged,
923
+ payload: element,
888
924
  headElements
889
925
  });
890
- renderPayload(merged, getNavigationState());
926
+ deferSegmentCaching(element);
927
+ renderPayload(element, getNavigationState());
891
928
  applyHead(headElements);
892
929
  },
893
930
  initSegmentCache: (segments) => updateSegmentCache(segments),
@@ -905,9 +942,12 @@ function createRouter(deps) {
905
942
  * Returns a read-only URLSearchParams instance reflecting the current
906
943
  * URL's query string. Updates when client-side navigation changes the URL.
907
944
  *
908
- * This is a thin wrapper over window.location.search, provided for
909
- * Next.js API compatibility (libraries like nuqs import useSearchParams
910
- * from next/navigation).
945
+ * On the client, reads from NavigationContext which is updated atomically
946
+ * with the RSC tree render. This replaces the previous useSyncExternalStore
947
+ * approach which read window.location.search directly — causing React to
948
+ * detect external store tearing during transitions and fall back to
949
+ * synchronous rendering (renderRootSync instead of renderRootConcurrent),
950
+ * blocking the main thread and freezing animations.
911
951
  *
912
952
  * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard
913
953
  * URLSearchParams. Mutation methods (set, delete, append) work on the
@@ -916,28 +956,9 @@ function createRouter(deps) {
916
956
  * During SSR, reads the request search params from the SSR ALS context
917
957
  * (populated by ssr-entry.ts) instead of window.location.
918
958
  *
919
- * All mutable state is delegated to client/state.ts for singleton guarantees.
920
- * See design/18-build-system.md §"Singleton State Registry"
959
+ * Compatible with Next.js's `useSearchParams()` from `next/navigation`.
921
960
  */
922
- function getSearch() {
923
- if (typeof window !== "undefined") return window.location.search;
924
- const data = getSsrData();
925
- if (!data) return "";
926
- const str = new URLSearchParams(data.searchParams).toString();
927
- return str ? `?${str}` : "";
928
- }
929
- function getServerSearch() {
930
- const data = getSsrData();
931
- if (!data) return "";
932
- const str = new URLSearchParams(data.searchParams).toString();
933
- return str ? `?${str}` : "";
934
- }
935
- function subscribe(callback) {
936
- window.addEventListener("popstate", callback);
937
- return () => window.removeEventListener("popstate", callback);
938
- }
939
- function getSearchParams() {
940
- const search = getSearch();
961
+ function getSearchParams(search) {
941
962
  if (search !== cachedSearch) {
942
963
  const params = new URLSearchParams(search);
943
964
  _setCachedSearch(search, params);
@@ -945,18 +966,20 @@ function getSearchParams() {
945
966
  }
946
967
  return cachedSearchParams;
947
968
  }
948
- function getServerSearchParams() {
949
- const data = getSsrData();
950
- return data ? new URLSearchParams(data.searchParams) : new URLSearchParams();
951
- }
952
969
  /**
953
970
  * Read the current URL search params.
954
971
  *
955
972
  * Compatible with Next.js's `useSearchParams()` from `next/navigation`.
956
973
  */
957
974
  function useSearchParams() {
958
- useSyncExternalStore(subscribe, getSearch, getServerSearch);
959
- return typeof window !== "undefined" ? getSearchParams() : getServerSearchParams();
975
+ try {
976
+ const navContext = useNavigationContext();
977
+ if (navContext !== null) return getSearchParams(navContext.search);
978
+ } catch {}
979
+ const ssrData = getSsrData();
980
+ if (ssrData) return new URLSearchParams(ssrData.searchParams);
981
+ if (typeof window !== "undefined") return getSearchParams(window.location.search);
982
+ return new URLSearchParams();
960
983
  }
961
984
  //#endregion
962
985
  export { HistoryStack, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, clearSsrData, createRouter, getNavigationState, getRouter, getRouterOrNull, getSsrData, setCurrentParams, setGlobalRouter, setNavigationState, setSsrData, useSearchParams, useSegmentContext };