@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.
- package/dist/_chunks/{actions-418ygYKC.js → actions-DX2gkoOZ.js} +2 -2
- package/dist/_chunks/{actions-418ygYKC.js.map → actions-DX2gkoOZ.js.map} +1 -1
- package/dist/_chunks/{build-output-helper-DYOWsPjZ.js → build-output-helper-K7oUCRqU.js} +1 -1
- package/dist/_chunks/{build-output-helper-DYOWsPjZ.js.map → build-output-helper-K7oUCRqU.js.map} +1 -1
- package/dist/_chunks/{codec-registry-G9hD5LOq.js → codec-registry-DqkeAydt.js} +1 -1
- package/dist/_chunks/{codec-registry-G9hD5LOq.js.map → codec-registry-DqkeAydt.js.map} +1 -1
- package/dist/_chunks/{define-BJMwm0c4.js → define-DMq-Yjp7.js} +2 -2
- package/dist/_chunks/{define-BJMwm0c4.js.map → define-DMq-Yjp7.js.map} +1 -1
- package/dist/_chunks/{navigation-root-DaERBEGV.js → navigation-root-B5-RmsPi.js} +35 -28
- package/dist/_chunks/navigation-root-B5-RmsPi.js.map +1 -0
- package/dist/_chunks/{stale-reload-BN2kcE06.js → stale-reload-CCqb62Z3.js} +22 -2
- package/dist/_chunks/{stale-reload-BN2kcE06.js.map → stale-reload-CCqb62Z3.js.map} +1 -1
- package/dist/_chunks/{use-segment-params-ByGqwUuO.js → use-segment-params-CIeinIx2.js} +2 -32
- package/dist/_chunks/{use-segment-params-ByGqwUuO.js.map → use-segment-params-CIeinIx2.js.map} +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +4 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/index.js +35 -79
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +128 -270
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link-pending-store.d.ts +19 -17
- package/dist/client/link-pending-store.d.ts.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/nav-link-store.d.ts +3 -12
- package/dist/client/nav-link-store.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +4 -1
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts +10 -7
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +2 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-outlet.d.ts +23 -37
- package/dist/client/segment-outlet.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +21 -0
- package/dist/client/segment-update-context.d.ts.map +1 -0
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/use-pending-navigation.d.ts +3 -6
- package/dist/client/use-pending-navigation.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/params/index.js +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +75 -4
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +1 -0
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +12 -5
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry/hydrate.ts +7 -1
- package/src/client/browser-entry/index.ts +3 -1
- package/src/client/browser-entry/post-hydration.ts +0 -13
- package/src/client/browser-entry/router-init.ts +50 -24
- package/src/client/link-pending-store.ts +22 -17
- package/src/client/link.tsx +30 -79
- package/src/client/nav-link-store.ts +3 -13
- package/src/client/navigation-root.tsx +28 -23
- package/src/client/router.ts +109 -97
- package/src/client/rsc-fetch.ts +85 -6
- package/src/client/segment-cache.ts +5 -2
- package/src/client/segment-outlet.tsx +40 -49
- package/src/client/segment-update-context.ts +42 -0
- package/src/client/top-loader.tsx +5 -15
- package/src/client/use-pending-navigation.ts +18 -16
- package/src/server/pipeline-outcome.ts +23 -2
- package/src/server/route-element-builder.ts +69 -2
- package/src/server/rsc-entry/helpers.ts +18 -14
- package/src/server/state-tree-diff.ts +26 -22
- package/dist/_chunks/navigation-root-DaERBEGV.js.map +0 -1
- package/dist/client/segment-merger.d.ts +0 -119
- package/dist/client/segment-merger.d.ts.map +0 -1
- package/src/client/segment-merger.ts +0 -339
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline-outcome.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-outcome.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgBH,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline-outcome.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-outcome.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgBH,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAchE,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,YAAY,GAAG,QAAQ,CAAC;AAE1D,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GAC9D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAID;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,QAAQ,CAAC,CA2HnB"}
|
|
@@ -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;
|
|
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"}
|
|
@@ -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,
|
|
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
|
|
31
|
-
* 2. The
|
|
32
|
-
* 3. The segment
|
|
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
|
-
*
|
|
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(
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.2.0-alpha.126",
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
13
|
-
* instance.setIsPending(LINK_PENDING)
|
|
14
|
-
* 3.
|
|
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
|
|
56
|
-
* activateLinkPending().
|
|
55
|
+
* that happens via activateLinkPending() called from NavigationRoot.
|
|
57
56
|
*
|
|
58
|
-
* Resets the previous link to idle immediately
|
|
59
|
-
*
|
|
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
|
-
*
|
|
69
|
-
*
|
|
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
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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();
|
package/src/client/link.tsx
CHANGED
|
@@ -19,9 +19,7 @@
|
|
|
19
19
|
// - searchParams and inline query string are mutually exclusive
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
useEffect,
|
|
24
|
-
useRef,
|
|
22
|
+
useTransition,
|
|
25
23
|
type AnchorHTMLAttributes,
|
|
26
24
|
type ReactNode,
|
|
27
25
|
type MouseEvent as ReactMouseEvent,
|
|
@@ -34,15 +32,11 @@ import { LinkStatusContext } from './use-link-status.js';
|
|
|
34
32
|
import { getRouterOrNull } from './router-ref.js';
|
|
35
33
|
import { getSsrData } from './ssr-data.js';
|
|
36
34
|
import { mergePreservedSearchParams } from '../shared/merge-search-params.js';
|
|
37
|
-
import {
|
|
38
|
-
setLinkForCurrentNavigation,
|
|
39
|
-
unmountLinkForCurrentNavigation,
|
|
40
|
-
LINK_IDLE,
|
|
41
|
-
type LinkPendingInstance,
|
|
42
|
-
} from './link-pending-store.js';
|
|
43
|
-
import { setNavLinkMetadata } from './nav-link-store.js';
|
|
44
|
-
import { hasNavigationApi } from './navigation-api.js';
|
|
45
35
|
import { getLinkCodec } from '../params/codec-registry.js';
|
|
36
|
+
import type { LinkStatus } from './use-link-status.js';
|
|
37
|
+
|
|
38
|
+
const LINK_PENDING: LinkStatus = { isPending: true };
|
|
39
|
+
const LINK_IDLE: LinkStatus = { isPending: false };
|
|
46
40
|
|
|
47
41
|
// ─── Current Search Params ────────────────────────────────────────
|
|
48
42
|
|
|
@@ -501,37 +495,13 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
501
495
|
} = props as LinkRuntimeProps;
|
|
502
496
|
const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });
|
|
503
497
|
|
|
504
|
-
// ─── Per-link pending state
|
|
505
|
-
// Each Link
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
//
|
|
512
|
-
// See design/19-client-navigation.md §"Per-Link Pending State"
|
|
513
|
-
const [linkStatus, setIsPending] = useOptimistic(LINK_IDLE);
|
|
514
|
-
|
|
515
|
-
// Build the link instance ref for the pending store.
|
|
516
|
-
// The ref is stable across renders — we update the setter on each
|
|
517
|
-
// render to keep it current.
|
|
518
|
-
const linkInstanceRef = useRef<LinkPendingInstance | null>(null);
|
|
519
|
-
if (!linkInstanceRef.current) {
|
|
520
|
-
linkInstanceRef.current = { setIsPending };
|
|
521
|
-
} else {
|
|
522
|
-
linkInstanceRef.current.setIsPending = setIsPending;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Clean up if this link unmounts while it's the current navigation link.
|
|
526
|
-
// Prevents calling setOptimistic on an unmounted component.
|
|
527
|
-
useEffect(() => {
|
|
528
|
-
const instance = linkInstanceRef.current;
|
|
529
|
-
return () => {
|
|
530
|
-
if (instance) {
|
|
531
|
-
unmountLinkForCurrentNavigation(instance);
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
}, []);
|
|
498
|
+
// ─── Per-link pending state ─────────────────────────────────────────
|
|
499
|
+
// Each Link owns a useTransition. On click, it wraps
|
|
500
|
+
// router.navigate() in startTransition — only this Link re-renders
|
|
501
|
+
// for isPending, zero siblings touched. The transition tracks the
|
|
502
|
+
// full navigation lifecycle (fetch + render + commit).
|
|
503
|
+
const [isPending, startTransition] = useTransition();
|
|
504
|
+
const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;
|
|
535
505
|
|
|
536
506
|
// Preserve search params from the current URL when requested.
|
|
537
507
|
// useSearchParams() works during both SSR (reads from request context)
|
|
@@ -571,49 +541,30 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
571
541
|
}
|
|
572
542
|
|
|
573
543
|
const router = getRouterOrNull();
|
|
574
|
-
if (!router) return;
|
|
544
|
+
if (!router) return;
|
|
575
545
|
|
|
576
|
-
const
|
|
546
|
+
const navHref = preserveSearchParams
|
|
547
|
+
? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
|
|
548
|
+
: resolvedHref;
|
|
577
549
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
// naturally — do NOT call preventDefault(). The navigate event
|
|
586
|
-
// handler intercepts it and runs the RSC pipeline. This is a
|
|
587
|
-
// user-initiated navigation, so Chrome shows the native loading
|
|
588
|
-
// indicator (tab spinner). Metadata (scroll, link instance) is
|
|
589
|
-
// passed via nav-link-store so the handler can configure the nav.
|
|
590
|
-
//
|
|
591
|
-
// Without Navigation API (fallback), preventDefault and drive
|
|
592
|
-
// navigation through the router as before.
|
|
593
|
-
if (hasNavigationApi()) {
|
|
594
|
-
setNavLinkMetadata({
|
|
595
|
-
scroll: shouldScroll,
|
|
596
|
-
linkInstance: linkInstanceRef.current,
|
|
597
|
-
});
|
|
598
|
-
// Don't preventDefault — let the <a> click fire the navigate event
|
|
550
|
+
const resolved = new URL(navHref, window.location.href);
|
|
551
|
+
|
|
552
|
+
if (
|
|
553
|
+
resolved.pathname === window.location.pathname &&
|
|
554
|
+
resolved.search === window.location.search &&
|
|
555
|
+
resolved.hash
|
|
556
|
+
) {
|
|
599
557
|
return;
|
|
600
558
|
}
|
|
601
559
|
|
|
602
|
-
// Hash-only hrefs — let the browser handle scroll-to-anchor natively.
|
|
603
|
-
// The Navigation API path already skips these via event.hashChange;
|
|
604
|
-
// this covers the fallback (Safari/Firefox) where we drive navigation manually.
|
|
605
|
-
if (resolvedHref.startsWith('#')) return;
|
|
606
|
-
|
|
607
|
-
// History API fallback — prevent default and navigate via router
|
|
608
560
|
event.preventDefault();
|
|
609
561
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const navHref = preserveSearchParams
|
|
613
|
-
? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
|
|
614
|
-
: resolvedHref;
|
|
562
|
+
const shouldScroll = scroll !== false;
|
|
563
|
+
const absoluteHref = resolved.pathname + resolved.search;
|
|
615
564
|
|
|
616
|
-
|
|
565
|
+
startTransition(async () => {
|
|
566
|
+
await router.navigate(absoluteHref, { scroll: shouldScroll });
|
|
567
|
+
});
|
|
617
568
|
}
|
|
618
569
|
: userOnClick; // External links — just pass through user's onClick
|
|
619
570
|
|
|
@@ -624,11 +575,11 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
624
575
|
userOnMouseEnter?.(event);
|
|
625
576
|
const router = getRouterOrNull();
|
|
626
577
|
if (router) {
|
|
627
|
-
// Re-merge preserved search params at hover time for fresh prefetch URL
|
|
628
578
|
const prefetchHref = preserveSearchParams
|
|
629
579
|
? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
|
|
630
580
|
: resolvedHref;
|
|
631
|
-
|
|
581
|
+
const resolved = new URL(prefetchHref, window.location.href);
|
|
582
|
+
router.prefetch(resolved.pathname + resolved.search);
|
|
632
583
|
}
|
|
633
584
|
}
|
|
634
585
|
: userOnMouseEnter;
|
|
@@ -2,26 +2,16 @@
|
|
|
2
2
|
* Navigation Link Store — passes per-link metadata from Link's onClick
|
|
3
3
|
* to the Navigation API's navigate event handler.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* The navigate event handler reads this metadata to configure the RSC
|
|
9
|
-
* navigation with the correct options.
|
|
10
|
-
*
|
|
11
|
-
* This store is consumed once per navigation — after reading, the metadata
|
|
12
|
-
* is cleared. If no metadata is present (e.g., a plain <a> tag without
|
|
13
|
-
* our Link component), the navigate handler uses default options.
|
|
5
|
+
* Used for external navigations (plain <a> tags intercepted by the
|
|
6
|
+
* Navigation API). Link components drive navigation directly via
|
|
7
|
+
* router.navigate() and don't use this store.
|
|
14
8
|
*
|
|
15
9
|
* See design/19-client-navigation.md §"Navigation API Integration"
|
|
16
10
|
*/
|
|
17
11
|
|
|
18
|
-
import type { LinkPendingInstance } from './link-pending-store.js';
|
|
19
|
-
|
|
20
12
|
export interface NavLinkMetadata {
|
|
21
13
|
/** Whether to scroll to top after navigation. Default: true. */
|
|
22
14
|
scroll: boolean;
|
|
23
|
-
/** The Link's pending state instance for per-link status tracking. */
|
|
24
|
-
linkInstance: LinkPendingInstance | null;
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
let pendingMetadata: NavLinkMetadata | null = null;
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
import { createElement, Fragment, startTransition, useState, type ReactNode } from 'react';
|
|
35
|
-
import { activateLinkPending, resetLinkPending } from './link-pending-store.js';
|
|
36
35
|
import { PendingNavigationProvider } from './navigation-context.js';
|
|
37
36
|
import { TopLoader, type TopLoaderConfig } from './top-loader.js';
|
|
38
37
|
|
|
@@ -132,7 +131,10 @@ let _transitionRender: ((element: ReactNode) => void) | null = null;
|
|
|
132
131
|
* with the pending URL.
|
|
133
132
|
*/
|
|
134
133
|
let _navigateTransition:
|
|
135
|
-
| ((
|
|
134
|
+
| ((
|
|
135
|
+
pendingUrl: string,
|
|
136
|
+
perform: () => Promise<{ element: ReactNode; decodePromise: Promise<void> | null }>
|
|
137
|
+
) => Promise<void>)
|
|
136
138
|
| null = null;
|
|
137
139
|
|
|
138
140
|
// ─── Component ───────────────────────────────────────────────────
|
|
@@ -194,42 +196,42 @@ export function NavigationRoot({
|
|
|
194
196
|
// are deferred. When the transition commits, the new tree and pendingUrl=null
|
|
195
197
|
// both apply in the same React commit — making the pending→active transition
|
|
196
198
|
// atomic (no frame where pending is false but the old tree is still visible).
|
|
197
|
-
_navigateTransition = (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
_navigateTransition = (
|
|
200
|
+
url: string,
|
|
201
|
+
perform: () => Promise<{ element: ReactNode; decodePromise: Promise<void> | null }>
|
|
202
|
+
) => {
|
|
201
203
|
// Increment the transition counter SYNCHRONOUSLY (before startTransition
|
|
202
204
|
// schedules the async work). Each call gets a unique transId; the counter
|
|
203
205
|
// is the same globalThis singleton, so a newer call always has a higher id.
|
|
204
206
|
const counter = getTransitionCounter();
|
|
205
207
|
const transId = ++counter.id;
|
|
206
208
|
|
|
209
|
+
// NO urgent setPendingUrl here. Per-link pending state is driven by
|
|
210
|
+
// Link's own useTransition — only the clicked Link re-renders for
|
|
211
|
+
// isPending. setPendingUrl is set INSIDE startTransition so the
|
|
212
|
+
// TopLoader activates as a transition update (no full-tree re-render).
|
|
213
|
+
|
|
207
214
|
return new Promise<void>((resolve, reject) => {
|
|
208
215
|
startTransition(async () => {
|
|
209
|
-
|
|
210
|
-
// useOptimistic persists the isPending=true value for the duration
|
|
211
|
-
// of this transition, then auto-reverts when it settles.
|
|
212
|
-
activateLinkPending();
|
|
216
|
+
setPendingUrl(url);
|
|
213
217
|
try {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
const { element, decodePromise } = await perform();
|
|
219
|
+
if (counter.id !== transId) {
|
|
220
|
+
decodePromise?.catch(() => {});
|
|
221
|
+
reject(new DOMException('Navigation superseded', 'AbortError'));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
setElement(element);
|
|
225
|
+
if (decodePromise) await decodePromise;
|
|
217
226
|
if (counter.id === transId) {
|
|
218
|
-
setElement(newElement);
|
|
219
227
|
setPendingUrl(null);
|
|
220
228
|
resolve();
|
|
221
229
|
} 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
230
|
reject(new DOMException('Navigation superseded', 'AbortError'));
|
|
227
231
|
}
|
|
228
232
|
} catch (err) {
|
|
229
|
-
// Only clear pending if this is still the active navigation.
|
|
230
233
|
if (counter.id === transId) {
|
|
231
234
|
setPendingUrl(null);
|
|
232
|
-
resetLinkPending();
|
|
233
235
|
}
|
|
234
236
|
reject(err);
|
|
235
237
|
}
|
|
@@ -297,7 +299,7 @@ export function transitionRender(element: ReactNode): void {
|
|
|
297
299
|
*/
|
|
298
300
|
export function navigateTransition(
|
|
299
301
|
pendingUrl: string,
|
|
300
|
-
perform: () => Promise<ReactNode>
|
|
302
|
+
perform: () => Promise<{ element: ReactNode; decodePromise: Promise<void> | null }>
|
|
301
303
|
): Promise<void> {
|
|
302
304
|
if (_navigateTransition) {
|
|
303
305
|
return _navigateTransition(pendingUrl, perform);
|
|
@@ -335,8 +337,11 @@ export function installDeferredNavigation(createAndMount: (initial: ReactNode) =
|
|
|
335
337
|
_transitionRender = (element: ReactNode) => {
|
|
336
338
|
mountOnce(element);
|
|
337
339
|
};
|
|
338
|
-
_navigateTransition = async (
|
|
339
|
-
|
|
340
|
+
_navigateTransition = async (
|
|
341
|
+
_pendingUrl: string,
|
|
342
|
+
perform: () => Promise<{ element: ReactNode; decodePromise: Promise<void> | null }>
|
|
343
|
+
) => {
|
|
344
|
+
const { element } = await perform();
|
|
340
345
|
mountOnce(element);
|
|
341
346
|
};
|
|
342
347
|
}
|