@timber-js/app 0.1.38 → 0.1.40

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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * State Tree Diffing — Server-side parsing and diffing of X-Timber-State-Tree.
3
+ *
4
+ * The client sends X-Timber-State-Tree on navigation requests, listing
5
+ * the sync segments it has cached. The server diffs this against the
6
+ * target route's segments to skip re-rendering unchanged sync layouts.
7
+ *
8
+ * This is a performance optimization only — NOT a security boundary.
9
+ * All access.ts files run regardless of the state tree content.
10
+ * A fabricated state tree can only cause extra rendering work or stale
11
+ * layouts — never auth bypass.
12
+ *
13
+ * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
14
+ * See design/13-security.md §"State tree manipulation"
15
+ */
16
+
17
+ /**
18
+ * Parse the X-Timber-State-Tree header from a request.
19
+ *
20
+ * Returns a Set of segment paths the client has cached, or null if
21
+ * the header is missing, malformed, or empty. Parsing happens before
22
+ * renderToReadableStream — not inside the React render pass.
23
+ *
24
+ * @returns Set of sync segment paths, or null if no valid state tree
25
+ */
26
+ export function parseClientStateTree(req: Request): Set<string> | null {
27
+ const header = req.headers.get('X-Timber-State-Tree');
28
+ if (!header) return null;
29
+
30
+ try {
31
+ const parsed = JSON.parse(header) as { segments?: unknown };
32
+ if (!Array.isArray(parsed.segments) || parsed.segments.length === 0) {
33
+ return null;
34
+ }
35
+ return new Set(parsed.segments as string[]);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Determine whether a segment's layout rendering can be skipped.
43
+ *
44
+ * A segment is skipped when ALL of the following are true:
45
+ * 1. The client has the segment in its state tree (clientSegments contains urlPath)
46
+ * 2. The layout is sync (not an async function — async layouts always re-render)
47
+ * 3. The segment is NOT the leaf (pages are never cached across navigations)
48
+ *
49
+ * Access.ts still runs for skipped segments — this is enforced by the caller
50
+ * (buildRouteElement) which runs all access checks before building the tree.
51
+ *
52
+ * @param urlPath - The segment's URL path (e.g., "/", "/dashboard")
53
+ * @param layoutComponent - The loaded layout component function
54
+ * @param isLeaf - Whether this is the leaf segment (page segment)
55
+ * @param clientSegments - Set of paths from X-Timber-State-Tree, or null
56
+ */
57
+ export function shouldSkipSegment(
58
+ urlPath: string,
59
+ layoutComponent: ((...args: unknown[]) => unknown) | undefined,
60
+ isLeaf: boolean,
61
+ clientSegments: Set<string> | null
62
+ ): boolean {
63
+ // No state tree → full render (initial load, refresh, etc.)
64
+ if (!clientSegments) return false;
65
+
66
+ // Leaf segments (pages) are never skipped
67
+ if (isLeaf) return false;
68
+
69
+ // No layout → nothing to skip
70
+ if (!layoutComponent) return false;
71
+
72
+ // Async layouts always re-render (they may depend on request context)
73
+ if (layoutComponent.constructor?.name === 'AsyncFunction') return false;
74
+
75
+ // Skip if the client already has this segment cached
76
+ return clientSegments.has(urlPath);
77
+ }