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

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 (58) hide show
  1. package/dist/_chunks/{build-output-helper-DYOWsPjZ.js → build-output-helper-K7oUCRqU.js} +1 -1
  2. package/dist/_chunks/{build-output-helper-DYOWsPjZ.js.map → build-output-helper-K7oUCRqU.js.map} +1 -1
  3. package/dist/_chunks/{navigation-root-DaERBEGV.js → navigation-root-CeNTCxzS.js} +5 -5
  4. package/dist/_chunks/navigation-root-CeNTCxzS.js.map +1 -0
  5. package/dist/_chunks/{stale-reload-BN2kcE06.js → stale-reload-CCqb62Z3.js} +22 -2
  6. package/dist/_chunks/{stale-reload-BN2kcE06.js.map → stale-reload-CCqb62Z3.js.map} +1 -1
  7. package/dist/adapters/cloudflare.js +1 -1
  8. package/dist/adapters/nitro.js +1 -1
  9. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  10. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  11. package/dist/client/browser-entry/router-init.d.ts +4 -1
  12. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  13. package/dist/client/index.js +3 -3
  14. package/dist/client/index.js.map +1 -1
  15. package/dist/client/internal.js +127 -269
  16. package/dist/client/internal.js.map +1 -1
  17. package/dist/client/link-pending-store.d.ts +19 -17
  18. package/dist/client/link-pending-store.d.ts.map +1 -1
  19. package/dist/client/link.d.ts.map +1 -1
  20. package/dist/client/navigation-root.d.ts +4 -1
  21. package/dist/client/navigation-root.d.ts.map +1 -1
  22. package/dist/client/router.d.ts +10 -7
  23. package/dist/client/router.d.ts.map +1 -1
  24. package/dist/client/rsc-fetch.d.ts.map +1 -1
  25. package/dist/client/segment-cache.d.ts +2 -0
  26. package/dist/client/segment-cache.d.ts.map +1 -1
  27. package/dist/client/segment-outlet.d.ts +23 -37
  28. package/dist/client/segment-outlet.d.ts.map +1 -1
  29. package/dist/client/segment-update-context.d.ts +21 -0
  30. package/dist/client/segment-update-context.d.ts.map +1 -0
  31. package/dist/index.js +1 -1
  32. package/dist/server/internal.js +57 -1
  33. package/dist/server/internal.js.map +1 -1
  34. package/dist/server/route-element-builder.d.ts.map +1 -1
  35. package/dist/server/rsc-entry/helpers.d.ts +1 -0
  36. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  37. package/dist/server/state-tree-diff.d.ts +12 -5
  38. package/dist/server/state-tree-diff.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/client/browser-entry/hydrate.ts +7 -1
  41. package/src/client/browser-entry/index.ts +3 -1
  42. package/src/client/browser-entry/post-hydration.ts +0 -13
  43. package/src/client/browser-entry/router-init.ts +50 -24
  44. package/src/client/link-pending-store.ts +22 -17
  45. package/src/client/link.tsx +12 -6
  46. package/src/client/navigation-root.tsx +40 -18
  47. package/src/client/router.ts +109 -97
  48. package/src/client/rsc-fetch.ts +85 -6
  49. package/src/client/segment-cache.ts +5 -2
  50. package/src/client/segment-outlet.tsx +40 -49
  51. package/src/client/segment-update-context.ts +42 -0
  52. package/src/server/route-element-builder.ts +69 -2
  53. package/src/server/rsc-entry/helpers.ts +18 -14
  54. package/src/server/state-tree-diff.ts +26 -22
  55. package/dist/_chunks/navigation-root-DaERBEGV.js.map +0 -1
  56. package/dist/client/segment-merger.d.ts +0 -119
  57. package/dist/client/segment-merger.d.ts.map +0 -1
  58. package/src/client/segment-merger.ts +0 -339
@@ -1 +1 @@
1
- {"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAY9D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAa7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAezD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAID,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CACvC;AAED,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC;IAC5B,2CAA2C;IAC3C,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,wDAAwD;IACxD,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;aAE7B,MAAM,EAAE,UAAU,GAAG,cAAc;aACnC,gBAAgB,EAAE,oBAAoB,EAAE;aACxC,QAAQ,EAAE,mBAAmB,EAAE;gBAF/B,MAAM,EAAE,UAAU,GAAG,cAAc,EACnC,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,QAAQ,EAAE,mBAAmB,EAAE;CAIlD;AA8DD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,kBAAkB,CAAC,CA0Y7B"}
1
+ {"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAY9D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAc7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAuCzD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAID,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CACvC;AAED,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC;IAC5B,2CAA2C;IAC3C,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,wDAAwD;IACxD,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;aAE7B,MAAM,EAAE,UAAU,GAAG,cAAc;aACnC,gBAAgB,EAAE,oBAAoB,EAAE;aACxC,QAAQ,EAAE,mBAAmB,EAAE;gBAF/B,MAAM,EAAE,UAAU,GAAG,cAAc,EACnC,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,QAAQ,EAAE,mBAAmB,EAAE;CAIlD;AA8DD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,kBAAkB,CAAC,CAob7B"}
@@ -83,6 +83,7 @@ export declare function buildSegmentInfo(segments: ManifestSegmentNode[], layout
83
83
  segment: ManifestSegmentNode;
84
84
  }>): Array<{
85
85
  path: string;
86
+ segmentId?: string;
86
87
  isAsync: boolean;
87
88
  }>;
88
89
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGlD,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI;IAAE,QAAQ,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAA;CAAE,CAQ/F;AAID;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;IACzB,yDAAyD;IACzD,gBAAgB,IAAI,MAAM,CAAC;IAC3B,yEAAyE;IACzE,aAAa,IAAI,mBAAmB,EAAE,CAAC;CACxC;AAED,wBAAgB,2BAA2B,IAAI,qBAAqB,CAkCnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,EAAE,CA8ClE;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,KAAK,CAAC;IACtB,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B,CAAC,GACD,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA0B3C;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGzD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,cAAc,EACtB,eAAe,EAAE,OAAO,GACvB,QAAQ,CAOV;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIpD;AAGD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAW1E"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGlD,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI;IAAE,QAAQ,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAA;CAAE,CAQ/F;AAID;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;IACzB,yDAAyD;IACzD,gBAAgB,IAAI,MAAM,CAAC;IAC3B,yEAAyE;IACzE,aAAa,IAAI,mBAAmB,EAAE,CAAC;CACxC;AAED,wBAAgB,2BAA2B,IAAI,qBAAqB,CAkCnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,EAAE,CA8ClE;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,KAAK,CAAC;IACtB,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B,CAAC,GACD,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA8B/D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGzD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,cAAc,EACtB,eAAe,EAAE,OAAO,GACvB,QAAQ,CAOV;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIpD;AAGD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAW1E"}
@@ -27,17 +27,24 @@ export declare function parseClientStateTree(req: Request): Set<string> | null;
27
27
  * Determine whether a segment's layout rendering can be skipped.
28
28
  *
29
29
  * A segment is skipped when ALL of the following are true:
30
- * 1. The client has the segment in its state tree (clientSegments contains urlPath)
31
- * 2. The layout is sync (not an async function — async layouts always re-render)
32
- * 3. The segment is NOT the leaf (pages are never cached across navigations)
30
+ * 1. The client sent a state tree (clientSegments is non-null)
31
+ * 2. The client has this segment cached (clientSegments contains urlPath)
32
+ * 3. The segment has a layout (otherwise nothing to skip)
33
+ * 4. The layout is sync (not an async function — async layouts may depend
34
+ * on per-request data and must always re-render)
35
+ * 5. The segment is NOT the leaf (pages are never cached across navigations)
36
+ *
37
+ * Additional constraints enforced by the caller (buildRouteElement):
38
+ * - Must have a rendered layout below (so SegmentOutlet has content to show)
39
+ * - Route groups are never skipped (siblings share urlPath)
33
40
  *
34
41
  * Access.ts still runs for skipped segments — this is enforced by the caller
35
- * (buildRouteElement) which runs all access checks before building the tree.
42
+ * which wraps with AccessGate regardless of skip status.
36
43
  *
37
44
  * @param urlPath - The segment's URL path (e.g., "/", "/dashboard")
38
45
  * @param layoutComponent - The loaded layout component function
39
46
  * @param isLeaf - Whether this is the leaf segment (page segment)
40
47
  * @param clientSegments - Set of paths from X-Timber-State-Tree, or null
41
48
  */
42
- export declare function shouldSkipSegment(_urlPath: string, _layoutComponent: ((...args: unknown[]) => unknown) | undefined, _isLeaf: boolean, _clientSegments: Set<string> | null): boolean;
49
+ export declare function shouldSkipSegment(urlPath: string, layoutComponent: ((...args: unknown[]) => unknown) | undefined, isLeaf: boolean, clientSegments: Set<string> | null): boolean;
43
50
  //# sourceMappingURL=state-tree-diff.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"state-tree-diff.d.ts","sourceRoot":"","sources":["../../src/server/state-tree-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAcrE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS,EAC/D,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAClC,OAAO,CAeT"}
1
+ {"version":3,"file":"state-tree-diff.d.ts","sourceRoot":"","sources":["../../src/server/state-tree-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAcrE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS,EAC9D,MAAM,EAAE,OAAO,EACf,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GACjC,OAAO,CAYT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.124",
3
+ "version": "0.2.0-alpha.125",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -25,6 +25,7 @@ import {
25
25
  getNavigationState,
26
26
  setNavigationState,
27
27
  } from '../navigation-context.js';
28
+ import { SegmentUpdateContext, EMPTY_SEGMENT_UPDATES } from '../segment-update-context.js';
28
29
  import { TimberNuqsAdapter } from '../nuqs-adapter.js';
29
30
  import { isPageUnloading } from '../unload-guard.js';
30
31
  import { NavigationRoot, installDeferredNavigation } from '../navigation-root.js';
@@ -89,7 +90,12 @@ export function hydrateApp({ rscResult, config }: HydrateOptions): void {
89
90
  { value: navState },
90
91
  element as React.ReactNode
91
92
  );
92
- const wrapped = createElement(TimberNuqsAdapter, null, withNav);
93
+ const withUpdates = createElement(
94
+ SegmentUpdateContext.Provider,
95
+ { value: EMPTY_SEGMENT_UPDATES },
96
+ withNav
97
+ );
98
+ const wrapped = createElement(TimberNuqsAdapter, null, withUpdates);
93
99
  const rootElement = createElement(NavigationRoot, {
94
100
  initial: wrapped,
95
101
  topLoaderConfig: config.topLoader,
@@ -81,7 +81,9 @@ function bootstrap(runtimeConfig: typeof config): void {
81
81
  const rscResult = createRscPayloadStream();
82
82
 
83
83
  // Step 3: Create router + Navigation API integration
84
- const { router, navApiController } = createTimberRouter();
84
+ const { router, navApiController } = createTimberRouter({
85
+ initialElement: rscResult?.element ?? undefined,
86
+ });
85
87
 
86
88
  // Step 4: Pre-hydration — set params + navigation state (MUST run after router init)
87
89
  runPreHydration();
@@ -58,19 +58,6 @@ export function setupPostHydration({
58
58
  delete (self as unknown as Record<string, unknown>).__timber_segments;
59
59
  }
60
60
 
61
- // Cache segment elements from the initial RSC payload after hydration.
62
- // The element from createFromReadableStream is a Flight thenable that
63
- // React resolves during hydration. Once resolved (status='fulfilled'),
64
- // its .value contains the React element tree we can walk.
65
- // Use requestAnimationFrame to run after hydration commits.
66
- if (initialElement != null) {
67
- requestAnimationFrame(() => {
68
- const el = initialElement as { status?: string; value?: unknown };
69
- const resolved = el.status === 'fulfilled' && el.value != null ? el.value : initialElement;
70
- router.cacheElementTree(resolved);
71
- });
72
- }
73
-
74
61
  // If the hydration path was skipped (no RSC payload), populate the
75
62
  // fallback params from server embed here.
76
63
  const lateTimberParams = (self as unknown as Record<string, unknown>).__timber_params;
@@ -16,6 +16,7 @@ import { applyHeadElements } from '../head.js';
16
16
  import { TimberNuqsAdapter } from '../nuqs-adapter.js';
17
17
  import { NavigationProvider } from '../navigation-context.js';
18
18
  import { transitionRender, navigateTransition } from '../navigation-root.js';
19
+ import { SegmentUpdateContext, EMPTY_SEGMENT_UPDATES } from '../segment-update-context.js';
19
20
  import { isStaleClientReference, triggerStaleReload } from '../stale-reload.js';
20
21
  import {
21
22
  hasNavigationApi,
@@ -24,6 +25,10 @@ import {
24
25
  } from '../navigation-api.js';
25
26
  import { getScrollY } from './scroll.js';
26
27
 
28
+ export interface RouterInitOptions {
29
+ initialElement?: unknown;
30
+ }
31
+
27
32
  export interface RouterInitResult {
28
33
  router: RouterInstance;
29
34
  navApiController: NavigationApiController | null;
@@ -36,12 +41,20 @@ export interface RouterInitResult {
36
41
  * the initial render (methods lazily resolve the router at invocation,
37
42
  * not render time, but initRouter must still run first).
38
43
  */
39
- export function createTimberRouter(): RouterInitResult {
44
+ export function createTimberRouter(options?: RouterInitOptions): RouterInitResult {
40
45
  // Feature-detect Navigation API. When available, the navigate event
41
46
  // replaces popstate for back/forward and catches external navigations.
42
47
  // See design/19-client-navigation.md §"Navigation API Integration"
43
48
  const useNavApi = hasNavigationApi();
44
49
 
50
+ // Track the current unwrapped RSC payload. On partial navigation, the
51
+ // router re-wraps this same element with new NavigationProvider context
52
+ // so mounted SegmentOutlets are preserved (they pick up changes via
53
+ // SegmentUpdateContext, not from new children props).
54
+ // Initialized from the hydration element so partial nav works on the
55
+ // very first client navigation (before any wrapPayload call).
56
+ let currentPayload: unknown = options?.initialElement ?? null;
57
+
45
58
  const deps: RouterDeps = {
46
59
  fetch: (url, init) => window.fetch(url, init),
47
60
  pushState: (data, unused, url) => window.history.pushState(data, unused, url),
@@ -75,20 +88,14 @@ export function createTimberRouter(): RouterInitResult {
75
88
  // reference module IDs that don't exist in the old client bundle.
76
89
  // We catch "Could not find the module" errors and trigger a full
77
90
  // page reload so the browser fetches the new bundle.
78
- decodeRsc: async (fetchPromise: Promise<Response>) => {
91
+ // Pass the fetch promise directly to createFromFetch for streaming.
92
+ // React progressively renders chunks via Suspense as they arrive.
93
+ // The transition stays open via decodePromise (awaited inside
94
+ // startTransition) so useOptimistic pending state persists for
95
+ // the full stream duration.
96
+ decodeRsc: (fetchPromise: Promise<Response>) => {
79
97
  try {
80
- // Wait for the full RSC stream to arrive before decoding.
81
- // createFromFetch resolves on the first Flight chunk (the shell),
82
- // but we need the transition to stay pending for the full response
83
- // so useOptimistic (per-link pending) persists during slow navigations.
84
- // Buffer the complete response, then decode from the buffered stream.
85
- const response = await fetchPromise;
86
- const body = await response.arrayBuffer();
87
- const buffered = new Response(body, {
88
- headers: response.headers,
89
- status: response.status,
90
- });
91
- return await createFromFetch(Promise.resolve(buffered));
98
+ return createFromFetch(fetchPromise);
92
99
  } catch (error) {
93
100
  if (isStaleClientReference(error)) {
94
101
  triggerStaleReload();
@@ -114,7 +121,12 @@ export function createTimberRouter(): RouterInitResult {
114
121
  { value: navState },
115
122
  element as React.ReactNode
116
123
  );
117
- const wrapped = createElement(TimberNuqsAdapter, null, withNav);
124
+ const withUpdates = createElement(
125
+ SegmentUpdateContext.Provider,
126
+ { value: EMPTY_SEGMENT_UPDATES },
127
+ withNav
128
+ );
129
+ const wrapped = createElement(TimberNuqsAdapter, null, withUpdates);
118
130
  transitionRender(wrapped);
119
131
  },
120
132
 
@@ -128,18 +140,32 @@ export function createTimberRouter(): RouterInitResult {
128
140
  // is passed explicitly by the router — no getNavigationState() needed.
129
141
  navigateTransition: (pendingUrl: string, perform) => {
130
142
  return navigateTransition(pendingUrl, async () => {
131
- const payload = await perform((rawPayload: unknown, navState: NavigationState) => {
132
- const withNav = createElement(
133
- NavigationProvider,
134
- { value: navState },
135
- rawPayload as React.ReactNode
136
- );
137
- return createElement(TimberNuqsAdapter, null, withNav);
138
- });
139
- return payload as React.ReactNode;
143
+ const { element, decodePromise } = await perform(
144
+ (
145
+ rawPayload: unknown,
146
+ navState: NavigationState,
147
+ segmentUpdates?: Map<string, unknown>
148
+ ) => {
149
+ currentPayload = rawPayload;
150
+ const withNav = createElement(
151
+ NavigationProvider,
152
+ { value: navState },
153
+ rawPayload as React.ReactNode
154
+ );
155
+ const withUpdates = createElement(
156
+ SegmentUpdateContext.Provider,
157
+ { value: (segmentUpdates ?? EMPTY_SEGMENT_UPDATES) as Map<string, React.ReactNode> },
158
+ withNav
159
+ );
160
+ return createElement(TimberNuqsAdapter, null, withUpdates);
161
+ }
162
+ );
163
+ return { element: element as React.ReactNode, decodePromise };
140
164
  });
141
165
  },
142
166
 
167
+ _getCurrentPayload: () => currentPayload,
168
+
143
169
  // Schedule a callback after the next paint so scroll operations
144
170
  // happen after React commits the new content to the DOM.
145
171
  // Double-rAF ensures the browser has painted the new frame.
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Link Pending Store — per-link optimistic pending state.
2
+ * Link Pending Store — per-link pending state.
3
3
  *
4
4
  * Tracks which link instance is currently navigating so that only the
5
- * clicked link shows pending. Uses `useOptimistic` from React 19 —
6
- * the optimistic value (isPending=true) is set inside NavigationRoot's
7
- * async startTransition so it persists for the duration of the RSC
8
- * fetch, then auto-reverts to idle when the transition settles.
5
+ * clicked link shows pending. Uses useState in Link — the pending
6
+ * state is set as an urgent update (outside startTransition) so it
7
+ * shows immediately, then reset inside the transition so it commits
8
+ * atomically with the new tree.
9
9
  *
10
10
  * Flow:
11
11
  * 1. Link click → setLinkForCurrentNavigation(instance) stores the ref
12
- * 2. NavigationRoot startTransition → activateLinkPending() calls
13
- * instance.setIsPending(LINK_PENDING) inside the async transition
14
- * 3. Transition settles → useOptimistic auto-reverts to LINK_IDLE
12
+ * 2. NavigationRoot → activateLinkPending() calls
13
+ * instance.setIsPending(LINK_PENDING) as an urgent update
14
+ * 3. Inside startTransition → resetLinkPending() commits with new tree
15
15
  *
16
16
  * SINGLETON GUARANTEE: Uses `globalThis` via `Symbol.for` keys because
17
17
  * the RSC client bundler can duplicate this module across chunks.
@@ -52,21 +52,25 @@ function getStore(): { current: LinkPendingInstance | null } {
52
52
  /**
53
53
  * Register the link instance that initiated the current navigation.
54
54
  * Called from Link's click handler. Does NOT call setIsPending —
55
- * that happens inside NavigationRoot's startTransition via
56
- * activateLinkPending().
55
+ * that happens via activateLinkPending() called from NavigationRoot.
57
56
  *
58
- * Resets the previous link to idle immediately (the old link's
59
- * useOptimistic reverts since its transition already settled).
57
+ * Resets the previous link to idle immediately if a different link
58
+ * is being registered (handles overlapping link clicks).
60
59
  */
61
60
  export function setLinkForCurrentNavigation(link: LinkPendingInstance | null): void {
62
61
  const store = getStore();
62
+ if (store.current && store.current !== link) {
63
+ store.current.setIsPending(LINK_IDLE);
64
+ }
63
65
  store.current = link;
64
66
  }
65
67
 
66
68
  /**
67
69
  * Activate pending state on the current link instance.
68
- * MUST be called inside NavigationRoot's async startTransition —
69
- * this is what makes useOptimistic persist for the navigation duration.
70
+ * Called as an urgent update (outside startTransition) so the link
71
+ * shows pending immediately. The corresponding resetLinkPending()
72
+ * is called inside the transition so it commits atomically with
73
+ * the new tree.
70
74
  */
71
75
  export function activateLinkPending(): void {
72
76
  const store = getStore();
@@ -74,9 +78,10 @@ export function activateLinkPending(): void {
74
78
  }
75
79
 
76
80
  /**
77
- * Reset the current link's pending state. With useOptimistic this is
78
- * handled automatically when the transition settles. Kept for callers
79
- * that explicitly need to clear on error paths.
81
+ * Reset the current link's pending state and clear the store.
82
+ * Call inside startTransition so the reset commits atomically
83
+ * with the new tree (no frame where pending=false but old content
84
+ * is still visible).
80
85
  */
81
86
  export function resetLinkPending(): void {
82
87
  const store = getStore();
@@ -19,7 +19,7 @@
19
19
  // - searchParams and inline query string are mutually exclusive
20
20
 
21
21
  import {
22
- useOptimistic,
22
+ useState,
23
23
  useEffect,
24
24
  useRef,
25
25
  type AnchorHTMLAttributes,
@@ -501,16 +501,22 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
501
501
  } = props as LinkRuntimeProps;
502
502
  const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });
503
503
 
504
- // ─── Per-link pending state (useOptimistic) ────────────────────────
504
+ // ─── Per-link pending state ─────────────────────────────────────────
505
505
  // Each Link has its own pending state. Only the clicked link's
506
506
  // setter is invoked during navigation — zero other links re-render.
507
507
  //
508
- // Link click stores the instance; NavigationRoot activates inside startTransition.
509
- // useOptimistic auto-reverts to LINK_IDLE when the navigation
510
- // startTransition — batched with the new tree commit.
508
+ // Link click stores the instance; NavigationRoot activates as an
509
+ // urgent update (outside startTransition) so the pending state
510
+ // shows immediately. resetLinkPending() inside the transition
511
+ // commits atomically with the new tree.
512
+ //
513
+ // Uses useState (not useOptimistic) to avoid TIM-964: useOptimistic
514
+ // setters inside async startTransition + await decodePromise
515
+ // trigger "Rendered more hooks" when two navigations overlap
516
+ // under the Navigation API.
511
517
  //
512
518
  // See design/19-client-navigation.md §"Per-Link Pending State"
513
- const [linkStatus, setIsPending] = useOptimistic(LINK_IDLE);
519
+ const [linkStatus, setIsPending] = useState(LINK_IDLE);
514
520
 
515
521
  // Build the link instance ref for the pending store.
516
522
  // The ref is stable across renders — we update the setter on each
@@ -132,7 +132,10 @@ let _transitionRender: ((element: ReactNode) => void) | null = null;
132
132
  * with the pending URL.
133
133
  */
134
134
  let _navigateTransition:
135
- | ((pendingUrl: string, perform: () => Promise<ReactNode>) => Promise<void>)
135
+ | ((
136
+ pendingUrl: string,
137
+ perform: () => Promise<{ element: ReactNode; decodePromise: Promise<void> | null }>
138
+ ) => Promise<void>)
136
139
  | null = null;
137
140
 
138
141
  // ─── Component ───────────────────────────────────────────────────
@@ -194,7 +197,10 @@ export function NavigationRoot({
194
197
  // are deferred. When the transition commits, the new tree and pendingUrl=null
195
198
  // both apply in the same React commit — making the pending→active transition
196
199
  // atomic (no frame where pending is false but the old tree is still visible).
197
- _navigateTransition = (url: string, perform: () => Promise<ReactNode>) => {
200
+ _navigateTransition = (
201
+ url: string,
202
+ perform: () => Promise<{ element: ReactNode; decodePromise: Promise<void> | null }>
203
+ ) => {
198
204
  // Urgent: show pending state immediately (for TopLoader / usePendingNavigation)
199
205
  setPendingUrl(url);
200
206
 
@@ -204,29 +210,42 @@ export function NavigationRoot({
204
210
  const counter = getTransitionCounter();
205
211
  const transId = ++counter.id;
206
212
 
213
+ // Activate per-link pending state as an URGENT update (outside
214
+ // startTransition). With useState (not useOptimistic), this commits
215
+ // immediately so the clicked link shows pending before the next paint.
216
+ // The corresponding resetLinkPending() is called INSIDE the transition
217
+ // so it commits atomically with the new tree.
218
+ //
219
+ // CRITICAL: activateLinkPending must NOT be called inside the
220
+ // startTransition that awaits decodePromise. With useOptimistic,
221
+ // that combination triggers "Rendered more hooks" when two
222
+ // navigations overlap under the Navigation API (TIM-964).
223
+ activateLinkPending();
224
+
207
225
  return new Promise<void>((resolve, reject) => {
208
226
  startTransition(async () => {
209
- // Activate per-link pending state inside this async transition.
210
- // useOptimistic persists the isPending=true value for the duration
211
- // of this transition, then auto-reverts when it settles.
212
- activateLinkPending();
213
227
  try {
214
- const newElement = await perform();
215
- // Only commit state if this is still the active navigation.
216
- // A superseded transition's updates must be dropped entirely.
228
+ const { element, decodePromise } = await perform();
229
+ if (counter.id !== transId) {
230
+ decodePromise?.catch(() => {});
231
+ reject(new DOMException('Navigation superseded', 'AbortError'));
232
+ return;
233
+ }
234
+ setElement(element);
235
+ // Await RSC stream completion inside the transition so the
236
+ // link's pending state (set urgently above) persists until
237
+ // the full payload is consumed. resetLinkPending() below
238
+ // commits atomically with the new tree when the transition
239
+ // settles — no frame where pending=false but old content shows.
240
+ if (decodePromise) await decodePromise;
217
241
  if (counter.id === transId) {
218
- setElement(newElement);
242
+ resetLinkPending();
219
243
  setPendingUrl(null);
220
244
  resolve();
221
245
  } else {
222
- // Stale transition — a newer navigation has superseded this one.
223
- // Reject so the caller (navigate/refresh/handlePopState) doesn't
224
- // run post-transition side effects (applyHead, scroll, event
225
- // dispatch) with stale data. All callers catch AbortError.
226
246
  reject(new DOMException('Navigation superseded', 'AbortError'));
227
247
  }
228
248
  } catch (err) {
229
- // Only clear pending if this is still the active navigation.
230
249
  if (counter.id === transId) {
231
250
  setPendingUrl(null);
232
251
  resetLinkPending();
@@ -297,7 +316,7 @@ export function transitionRender(element: ReactNode): void {
297
316
  */
298
317
  export function navigateTransition(
299
318
  pendingUrl: string,
300
- perform: () => Promise<ReactNode>
319
+ perform: () => Promise<{ element: ReactNode; decodePromise: Promise<void> | null }>
301
320
  ): Promise<void> {
302
321
  if (_navigateTransition) {
303
322
  return _navigateTransition(pendingUrl, perform);
@@ -335,8 +354,11 @@ export function installDeferredNavigation(createAndMount: (initial: ReactNode) =
335
354
  _transitionRender = (element: ReactNode) => {
336
355
  mountOnce(element);
337
356
  };
338
- _navigateTransition = async (_pendingUrl: string, perform: () => Promise<ReactNode>) => {
339
- const element = await perform();
357
+ _navigateTransition = async (
358
+ _pendingUrl: string,
359
+ perform: () => Promise<{ element: ReactNode; decodePromise: Promise<void> | null }>
360
+ ) => {
361
+ const { element } = await perform();
340
362
  mountOnce(element);
341
363
  };
342
364
  }