@timber-js/app 0.1.53 → 0.1.54
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/client/index.js +59 -33
- package/dist/client/index.js.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +6 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts +8 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.d.ts +25 -2
- package/dist/client/segment-merger.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry.ts +6 -6
- package/src/client/router.ts +24 -21
- package/src/client/segment-cache.ts +14 -5
- package/src/client/segment-context.ts +8 -1
- package/src/client/segment-merger.ts +55 -7
- package/src/server/route-element-builder.ts +42 -6
package/dist/client/index.js
CHANGED
|
@@ -349,19 +349,24 @@ var SegmentCache = class {
|
|
|
349
349
|
* Only includes sync segments — async segments are excluded because the
|
|
350
350
|
* server must always re-render them (they may depend on request context).
|
|
351
351
|
*
|
|
352
|
+
* When mergeableFilter is provided, only segments whose paths are in the
|
|
353
|
+
* set are included. This ensures the server only skips segments that the
|
|
354
|
+
* client can actually merge (i.e., segments whose cached element tree
|
|
355
|
+
* contains an inner SegmentProvider the merger can splice into).
|
|
356
|
+
*
|
|
352
357
|
* This is a performance optimization only, NOT a security boundary.
|
|
353
358
|
* The server always runs all access.ts files regardless of the state tree.
|
|
354
359
|
*/
|
|
355
|
-
serializeStateTree() {
|
|
360
|
+
serializeStateTree(mergeableFilter) {
|
|
356
361
|
const segments = [];
|
|
357
|
-
if (this.root) collectSyncSegments(this.root, segments);
|
|
362
|
+
if (this.root) collectSyncSegments(this.root, segments, mergeableFilter);
|
|
358
363
|
return { segments };
|
|
359
364
|
}
|
|
360
365
|
};
|
|
361
366
|
/** Recursively collect sync segment paths from the tree */
|
|
362
|
-
function collectSyncSegments(node, out) {
|
|
363
|
-
if (!node.isAsync) out.push(node.segment);
|
|
364
|
-
for (const child of node.children.values()) collectSyncSegments(child, out);
|
|
367
|
+
function collectSyncSegments(node, out, mergeableFilter) {
|
|
368
|
+
if (!node.isAsync && (!mergeableFilter || mergeableFilter.has(node.segment))) out.push(node.segment);
|
|
369
|
+
for (const child of node.children.values()) collectSyncSegments(child, out, mergeableFilter);
|
|
365
370
|
}
|
|
366
371
|
/**
|
|
367
372
|
* Build a SegmentNode tree from flat segment metadata.
|
|
@@ -524,6 +529,24 @@ var SegmentElementCache = class {
|
|
|
524
529
|
get size() {
|
|
525
530
|
return this.entries.size;
|
|
526
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* Get the set of segment paths that are safe for the server to skip.
|
|
534
|
+
* Only segments with an inner SegmentProvider (hasMergeableChild) are
|
|
535
|
+
* included — the merger can only replace inner SegmentProviders, not
|
|
536
|
+
* pages embedded in layout output. Used to filter the state tree.
|
|
537
|
+
*
|
|
538
|
+
* Returns an empty set if the element cache is empty (no elements
|
|
539
|
+
* cached yet). This is the safe default — an empty set means no
|
|
540
|
+
* segments pass the filter, so the state tree is empty and the server
|
|
541
|
+
* does a full render. The element cache is populated lazily after the
|
|
542
|
+
* first SPA navigation (RSC-decoded elements from hydration are
|
|
543
|
+
* thenables that can't be walked until React resolves them).
|
|
544
|
+
*/
|
|
545
|
+
getMergeablePaths() {
|
|
546
|
+
const paths = /* @__PURE__ */ new Set();
|
|
547
|
+
for (const [, entry] of this.entries) if (entry.hasMergeableChild) paths.add(entry.segmentPath);
|
|
548
|
+
return paths;
|
|
549
|
+
}
|
|
527
550
|
};
|
|
528
551
|
/**
|
|
529
552
|
* Check if a React element is a SegmentProvider by looking for the
|
|
@@ -537,11 +560,15 @@ function isSegmentProvider(element) {
|
|
|
537
560
|
}
|
|
538
561
|
/**
|
|
539
562
|
* Extract the segment path from a SegmentProvider element.
|
|
540
|
-
*
|
|
541
|
-
*
|
|
563
|
+
*
|
|
564
|
+
* Uses the `segmentId` prop if available (set by the server for route groups
|
|
565
|
+
* to distinguish siblings that share the same urlPath). Falls back to
|
|
566
|
+
* reconstructing from the `segments` array prop.
|
|
542
567
|
*/
|
|
543
568
|
function getSegmentPath(element) {
|
|
544
|
-
const
|
|
569
|
+
const props = element.props;
|
|
570
|
+
if (props.segmentId) return props.segmentId;
|
|
571
|
+
const filtered = props.segments.filter(Boolean);
|
|
545
572
|
return filtered.length === 0 ? "/" : "/" + filtered.join("/");
|
|
546
573
|
}
|
|
547
574
|
/**
|
|
@@ -554,6 +581,7 @@ function getSegmentPath(element) {
|
|
|
554
581
|
function extractSegments(element) {
|
|
555
582
|
const segments = [];
|
|
556
583
|
walkForSegments(element, segments);
|
|
584
|
+
for (let i = 0; i < segments.length; i++) segments[i].hasMergeableChild = i < segments.length - 1;
|
|
557
585
|
return segments;
|
|
558
586
|
}
|
|
559
587
|
function walkForSegments(node, out) {
|
|
@@ -563,7 +591,8 @@ function walkForSegments(node, out) {
|
|
|
563
591
|
if (isSegmentProvider(node)) {
|
|
564
592
|
out.push({
|
|
565
593
|
segmentPath: getSegmentPath(el),
|
|
566
|
-
element: el
|
|
594
|
+
element: el,
|
|
595
|
+
hasMergeableChild: false
|
|
567
596
|
});
|
|
568
597
|
walkChildren(props.children, out);
|
|
569
598
|
return;
|
|
@@ -628,7 +657,7 @@ function findSegmentProviderPath(node, targetPath) {
|
|
|
628
657
|
*/
|
|
629
658
|
function replaceInnerSegment(cachedElement, newInnerContent, innerSegmentPath) {
|
|
630
659
|
const path = findSegmentProviderPath(cachedElement, innerSegmentPath);
|
|
631
|
-
if (!path || path.length === 0) return
|
|
660
|
+
if (!path || path.length === 0) return cachedElement;
|
|
632
661
|
let replacement = newInnerContent;
|
|
633
662
|
for (let i = path.length - 1; i >= 0; i--) {
|
|
634
663
|
const { element, childIndex } = path[i];
|
|
@@ -894,18 +923,30 @@ function createRouter(deps) {
|
|
|
894
923
|
* In production, the entire callback runs inside a React transition with
|
|
895
924
|
* useOptimistic for the pending URL. In tests, the payload is rendered directly.
|
|
896
925
|
*/
|
|
897
|
-
async function renderViaTransition(
|
|
926
|
+
async function renderViaTransition(url, perform) {
|
|
898
927
|
if (deps.navigateTransition) {
|
|
899
928
|
let headElements = null;
|
|
900
|
-
await deps.navigateTransition(
|
|
929
|
+
await deps.navigateTransition(url, async (wrapPayload) => {
|
|
901
930
|
const result = await perform();
|
|
902
931
|
headElements = result.headElements;
|
|
903
|
-
|
|
932
|
+
const merged = mergeAndCachePayload(result.payload, result.skippedSegments);
|
|
933
|
+
historyStack.push(url, {
|
|
934
|
+
payload: merged,
|
|
935
|
+
headElements: result.headElements,
|
|
936
|
+
params: result.params
|
|
937
|
+
});
|
|
938
|
+
return wrapPayload(merged);
|
|
904
939
|
});
|
|
905
940
|
return headElements;
|
|
906
941
|
}
|
|
907
942
|
const result = await perform();
|
|
908
|
-
|
|
943
|
+
const merged = mergeAndCachePayload(result.payload, result.skippedSegments);
|
|
944
|
+
historyStack.push(url, {
|
|
945
|
+
payload: merged,
|
|
946
|
+
headElements: result.headElements,
|
|
947
|
+
params: result.params
|
|
948
|
+
});
|
|
949
|
+
renderPayload(merged);
|
|
909
950
|
return result.headElements;
|
|
910
951
|
}
|
|
911
952
|
/** Apply head elements (title, meta tags) to the DOM if available. */
|
|
@@ -931,7 +972,7 @@ function createRouter(deps) {
|
|
|
931
972
|
skippedSegments: prefetched.skippedSegments ?? null
|
|
932
973
|
} : void 0;
|
|
933
974
|
if (result === void 0) {
|
|
934
|
-
const stateTree = segmentCache.serializeStateTree();
|
|
975
|
+
const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());
|
|
935
976
|
const rawCurrentUrl = deps.getCurrentUrl();
|
|
936
977
|
result = await fetchRscPayload(url, deps, stateTree, rawCurrentUrl.startsWith("http") ? new URL(rawCurrentUrl).pathname : new URL(rawCurrentUrl, "http://localhost").pathname);
|
|
937
978
|
}
|
|
@@ -943,11 +984,6 @@ function createRouter(deps) {
|
|
|
943
984
|
timber: true,
|
|
944
985
|
scrollY: 0
|
|
945
986
|
}, "", url);
|
|
946
|
-
historyStack.push(url, {
|
|
947
|
-
payload: result.payload,
|
|
948
|
-
headElements: result.headElements,
|
|
949
|
-
params: result.params
|
|
950
|
-
});
|
|
951
987
|
updateSegmentCache(result.segmentInfo);
|
|
952
988
|
updateNavigationState(result.params, url);
|
|
953
989
|
return result;
|
|
@@ -987,11 +1023,6 @@ function createRouter(deps) {
|
|
|
987
1023
|
try {
|
|
988
1024
|
applyHead(await renderViaTransition(currentUrl, async () => {
|
|
989
1025
|
const result = await fetchRscPayload(currentUrl, deps);
|
|
990
|
-
historyStack.push(currentUrl, {
|
|
991
|
-
payload: result.payload,
|
|
992
|
-
headElements: result.headElements,
|
|
993
|
-
params: result.params
|
|
994
|
-
});
|
|
995
1026
|
updateSegmentCache(result.segmentInfo);
|
|
996
1027
|
updateNavigationState(result.params, currentUrl);
|
|
997
1028
|
return result;
|
|
@@ -1014,14 +1045,9 @@ function createRouter(deps) {
|
|
|
1014
1045
|
setPending(true, url);
|
|
1015
1046
|
try {
|
|
1016
1047
|
applyHead(await renderViaTransition(url, async () => {
|
|
1017
|
-
const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree());
|
|
1048
|
+
const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths()));
|
|
1018
1049
|
updateSegmentCache(result.segmentInfo);
|
|
1019
1050
|
updateNavigationState(result.params, url);
|
|
1020
|
-
historyStack.push(url, {
|
|
1021
|
-
payload: result.payload,
|
|
1022
|
-
headElements: result.headElements,
|
|
1023
|
-
params: result.params
|
|
1024
|
-
});
|
|
1025
1051
|
return result;
|
|
1026
1052
|
}));
|
|
1027
1053
|
afterPaint(() => {
|
|
@@ -1040,7 +1066,7 @@ function createRouter(deps) {
|
|
|
1040
1066
|
function prefetch(url) {
|
|
1041
1067
|
if (prefetchCache.get(url) !== void 0) return;
|
|
1042
1068
|
if (historyStack.has(url)) return;
|
|
1043
|
-
fetchRscPayload(url, deps, segmentCache.serializeStateTree()).then((result) => {
|
|
1069
|
+
fetchRscPayload(url, deps, segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths())).then((result) => {
|
|
1044
1070
|
prefetchCache.set(url, result);
|
|
1045
1071
|
}, () => {});
|
|
1046
1072
|
}
|
|
@@ -1331,7 +1357,7 @@ function useSegmentContext() {
|
|
|
1331
1357
|
* Wraps each layout to provide segment position context.
|
|
1332
1358
|
* Injected by rsc-entry.ts during element tree construction.
|
|
1333
1359
|
*/
|
|
1334
|
-
function SegmentProvider({ segments, parallelRouteKeys, children }) {
|
|
1360
|
+
function SegmentProvider({ segments, segmentId: _segmentId, parallelRouteKeys, children }) {
|
|
1335
1361
|
const value = useMemo(() => ({
|
|
1336
1362
|
segments,
|
|
1337
1363
|
parallelRouteKeys
|