@timber-js/app 0.2.0-alpha.187 → 0.2.0-alpha.188

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 (36) hide show
  1. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -1
  2. package/dist/client/internal.js +12 -3
  3. package/dist/client/internal.js.map +1 -1
  4. package/dist/client/segment-cache.d.ts +6 -0
  5. package/dist/client/segment-cache.d.ts.map +1 -1
  6. package/dist/routing/segment-keys.d.ts +22 -0
  7. package/dist/routing/segment-keys.d.ts.map +1 -1
  8. package/dist/server/access-gate.d.ts.map +1 -1
  9. package/dist/server/chain-url-parts.d.ts +2 -3
  10. package/dist/server/chain-url-parts.d.ts.map +1 -1
  11. package/dist/server/internal.js +1 -7
  12. package/dist/server/internal.js.map +1 -1
  13. package/dist/server/route-element-builder.d.ts.map +1 -1
  14. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  15. package/dist/server/skippable-prefix.d.ts +3 -0
  16. package/dist/server/skippable-prefix.d.ts.map +1 -1
  17. package/dist/server/slot-resolver.d.ts +18 -19
  18. package/dist/server/slot-resolver.d.ts.map +1 -1
  19. package/dist/server/state-tree-diff.d.ts +9 -16
  20. package/dist/server/state-tree-diff.d.ts.map +1 -1
  21. package/dist/server/tree-builder.d.ts +0 -7
  22. package/dist/server/tree-builder.d.ts.map +1 -1
  23. package/dist/shared/segment-info.d.ts +7 -0
  24. package/dist/shared/segment-info.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/client/segment-cache.ts +18 -5
  27. package/src/routing/segment-keys.ts +37 -0
  28. package/src/server/access-gate.tsx +1 -28
  29. package/src/server/chain-url-parts.ts +2 -3
  30. package/src/server/route-element-builder.ts +3 -25
  31. package/src/server/rsc-entry/helpers.ts +1 -0
  32. package/src/server/skippable-prefix.ts +21 -39
  33. package/src/server/slot-resolver.ts +59 -187
  34. package/src/server/state-tree-diff.ts +11 -62
  35. package/src/server/tree-builder.ts +0 -10
  36. package/src/shared/segment-info.ts +7 -0
@@ -1 +1 @@
1
- {"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAS9D,OAAO,EAA8C,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGpG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAQhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA+C/E;;;;;;;;GAQG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,UAAU,GAChB,MAAM,EAAE,CAGV;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAID,+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;;;;OAIG;IACH,OAAO,EAAE,cAAc,CAAC;IACxB,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;IAC1B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B;;;;OAIG;IACH,YAAY,EAAE,aAAa,EAAE,CAAC;CAC/B;AA8DD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,EACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,kBAAkB,CAAC,CA6jB7B"}
1
+ {"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAS9D,OAAO,EAA8C,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGpG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAQhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA+C/E;;;;;;;;GAQG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,UAAU,GAChB,MAAM,EAAE,CAGV;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAID,+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;;;;OAIG;IACH,OAAO,EAAE,cAAc,CAAC;IACxB,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;IAC1B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B;;;;OAIG;IACH,YAAY,EAAE,aAAa,EAAE,CAAC;CAC/B;AA8DD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,EACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,kBAAkB,CAAC,CAuiB7B"}
@@ -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;AAG/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;;;;;;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;;;;;;;;;;;;GAYG;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,EACF,YAAY,CAAC,EAAE,aAAa,EAAE,EAC9B,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAC7B,gBAAgB,SAAW,GAC1B,WAAW,EAAE,CAqEf;AAMD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,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;AAG/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;;;;;;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;;;;;;;;;;;;GAYG;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,EACF,YAAY,CAAC,EAAE,aAAa,EAAE,EAC9B,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAC7B,gBAAgB,SAAW,GAC1B,WAAW,EAAE,CAsEf;AAMD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC"}
@@ -18,6 +18,9 @@ import { type ClientStateTree } from './state-tree-diff.js';
18
18
  * Removing the segment from the skippable set forces it to render with
19
19
  * its slots. The prefix must stay contiguous, so all segments after the
20
20
  * first changed-slot segment are also removed.
21
+ *
22
+ * Uses key-based diffing (TIM-1370): computes the destination content key
23
+ * per slot and checks if the client's advertised set contains it.
21
24
  */
22
25
  export declare function truncateSkippableForChangedSlots(skippableSet: Set<number>, segments: ManifestSegmentNode[], segmentKeys: string[], match: RouteMatch, clientStateTree: ClientStateTree, req: Request): void;
23
26
  //# sourceMappingURL=skippable-prefix.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"skippable-prefix.d.ts","sourceRoot":"","sources":["../../src/server/skippable-prefix.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAI5E;;;;;;;;;GASG;AACH,wBAAgB,gCAAgC,CAC9C,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,EACzB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,eAAe,EAChC,GAAG,EAAE,OAAO,GACX,IAAI,CA2DN"}
1
+ {"version":3,"file":"skippable-prefix.d.ts","sourceRoot":"","sources":["../../src/server/skippable-prefix.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAM5E;;;;;;;;;;;;GAYG;AACH,wBAAgB,gCAAgC,CAC9C,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,EACzB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,eAAe,EAChC,GAAG,EAAE,OAAO,GACX,IAAI,CAoCN"}
@@ -16,7 +16,6 @@
16
16
  * See design/02-rendering-pipeline.md §"Parallel Slots"
17
17
  */
18
18
  import type { InterceptionContext, RouteMatch } from './pipeline.js';
19
- import { DenySignal, RedirectSignal } from './primitives.js';
20
19
  import type { ManifestSegmentNode } from './route-matcher.js';
21
20
  import { type ClientStateTree } from './state-tree-diff.js';
22
21
  type CreateElementFn = (...args: unknown[]) => React.ReactElement;
@@ -33,7 +32,7 @@ type CreateElementFn = (...args: unknown[]) => React.ReactElement;
33
32
  * 3. SlotAccessGate if the segment has access.ts (gate OUTSIDE the layout,
34
33
  * so a denied layout never executes — see TIM-1074)
35
34
  */
36
- export declare function resolveSlotElement(slotNode: ManifestSegmentNode, match: RouteMatch, h: CreateElementFn, interception?: InterceptionContext, parentTreePath?: string, accessVerdicts?: SlotAccessVerdict[]): Promise<React.ReactElement | null>;
35
+ export declare function resolveSlotElement(slotNode: ManifestSegmentNode, match: RouteMatch, h: CreateElementFn, interception?: InterceptionContext, parentTreePath?: string): Promise<React.ReactElement | null>;
37
36
  /** Result of matching a slot's sub-tree against the current route. */
38
37
  export interface SlotMatchResult {
39
38
  /** The page file at the matched leaf. */
@@ -81,18 +80,6 @@ export interface SlotMatchResult {
81
80
  * node standalone, and for those the old behaviour is still the right answer.
82
81
  */
83
82
  export declare function findSlotOwnerIndex(slotNode: ManifestSegmentNode, segments: ManifestSegmentNode[]): number;
84
- /** Result of eagerly evaluating one segment's access.ts. */
85
- export interface SlotAccessVerdict {
86
- node: ManifestSegmentNode;
87
- /**
88
- * 'pass' — access allowed.
89
- * DenySignal/RedirectSignal — access denied, replayed in SlotAccessGate.
90
- * 'error' — unknown error. Forces full render. No verdict passed to
91
- * SlotAccessGate, so it calls accessFn during render and the error
92
- * reaches the slot's error boundary.
93
- */
94
- verdict: 'pass' | 'error' | DenySignal | RedirectSignal;
95
- }
96
83
  export interface SlotSkipEntry {
97
84
  slotKey: string;
98
85
  parentSegmentId: string;
@@ -106,6 +93,12 @@ export interface SlotSkipEntry {
106
93
  denied: boolean;
107
94
  /** Whether the slot was skipped (content not rendered, client keeps cached). */
108
95
  skipped?: boolean;
96
+ /**
97
+ * Content key encoding owner parts + slot name + entry file + slot params.
98
+ * Sent to the client via X-Timber-Segments; the client advertises it back
99
+ * so the server can decide skips by key membership (TIM-1370).
100
+ */
101
+ contentKey: string;
109
102
  }
110
103
  /**
111
104
  * The destination URL parts a segment's slots match against.
@@ -141,17 +134,23 @@ interface ResolveSlotPropsArgs {
141
134
  h: CreateElementFn;
142
135
  interception?: InterceptionContext;
143
136
  parentTreePath: string;
144
- departingUrl: string | null;
145
137
  destinationUrl: string;
146
138
  clientStateTree: ClientStateTree | null;
147
139
  slotSkipInfo: SlotSkipEntry[];
148
140
  }
149
141
  /**
150
142
  * Resolve all parallel route slots for a layout, wrapping each in a
151
- * SegmentOutlet for client-side caching. Slots whose matched content
152
- * hasn't changed between the departing and destination URLs are rendered
153
- * with `skip=true` so the client keeps its cached content.
143
+ * SegmentOutlet for client-side caching. Slots whose content key matches
144
+ * one the client advertised are rendered with `skip=true` so the client
145
+ * keeps its cached content.
146
+ *
147
+ * The skip decision is key-based (TIM-1370): the server computes a content
148
+ * key per slot from the owning segment's URL parts, the slot name, the
149
+ * matched entry file, and the slot's own params. The client advertises
150
+ * the keys it has cached; the server checks membership. This replaces
151
+ * the previous approach of reconstructing the client's state from a
152
+ * departing URL.
154
153
  */
155
- export declare function resolveSlotProps({ segment, segmentId, match, h, interception, parentTreePath, departingUrl, destinationUrl, clientStateTree, slotSkipInfo, }: ResolveSlotPropsArgs): Promise<Record<string, unknown>>;
154
+ export declare function resolveSlotProps({ segment, segmentId, match, h, interception, parentTreePath, destinationUrl, clientStateTree, slotSkipInfo, }: ResolveSlotPropsArgs): Promise<Record<string, unknown>>;
156
155
  export {};
157
156
  //# sourceMappingURL=slot-resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAO9D,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAI5E,KAAK,eAAe,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC;AAgClE;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,EACjB,CAAC,EAAE,eAAe,EAClB,YAAY,CAAC,EAAE,mBAAmB,EAClC,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAoIpC;AAsKD,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,yCAAyC;IACzC,IAAI,EAAE,WAAW,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,+EAA+E;IAC/E,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CAC/C;AAED;;;;;;;;;;;;;;GAcG;AACH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,MAAM,CAYR;AA+BD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAC5B,0DAA0D;IAC1D,MAAM,EAAE,OAAO,CAAC;IAChB,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAUV;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,UAAU,EACjB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,IAAI,CAcN;AAED,UAAU,oBAAoB;IAC5B,OAAO,EAAE,mBAAmB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;IAClB,CAAC,EAAE,eAAe,CAAC;IACnB,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,YAAY,EAAE,aAAa,EAAE,CAAC;CAC/B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,OAAO,EACP,SAAS,EACT,KAAK,EACL,CAAC,EACD,YAAY,EACZ,cAAc,EACd,cAAc,EACd,eAAe,EACf,YAAY,GACb,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAwIzD"}
@@ -13,7 +13,6 @@
13
13
  * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
14
14
  * See design/13-security.md §"State tree manipulation"
15
15
  */
16
- import type { ManifestSegmentNode } from './route-matcher.js';
17
16
  /** Parsed client state tree with segment paths and optional slot paths. */
18
17
  export interface ClientStateTree extends Set<string> {
19
18
  slots?: Set<string> | null;
@@ -59,27 +58,21 @@ export declare function parseClientStateTree(req: Request): ClientStateTree | nu
59
58
  * @param clientSegments - Set of paths from X-Timber-State-Tree, or null
60
59
  */
61
60
  export declare function shouldSkipSegment(segmentKey: string, layoutComponent: ((...args: unknown[]) => unknown) | undefined, isLeaf: boolean, clientSegments: Set<string> | null): boolean;
62
- interface ShouldSkipSlotArgs {
63
- slotKey: string;
64
- clientSlots: Set<string> | null;
65
- slotNode: ManifestSegmentNode;
66
- departingUrlParts: string[];
67
- destinationUrlParts: string[];
68
- }
69
61
  /**
70
62
  * Determine whether a parallel route slot can be skipped.
71
63
  *
72
- * A slot is skipped when ALL of:
73
- * 1. The client has this slot cached (slotKey is in clientSlots)
74
- * 2. The slot's matched page is the SAME for both the departing and
75
- * destination URLs (same page file + same extracted params)
64
+ * A slot is skipped when the destination content key — computed from the
65
+ * owning segment's URL parts, the slot name, the matched entry file, and
66
+ * the slot's extracted params — is in the set of keys the client advertised.
76
67
  *
77
- * The comparison uses matchUrlParts to find what each URL would match
78
- * in the slot's sub-tree, then compares the matched page file and params.
68
+ * This replaced URL-based departing/destination diffing in TIM-1370.
69
+ * The previous approach reconstructed the client's state by re-matching a
70
+ * departing URL against each slot's sub-tree, which was a recurring bug
71
+ * source because the reconstruction could drift from the client's actual
72
+ * mounted state. The key-based approach is a single set-membership check.
79
73
  *
80
74
  * This is a performance optimization only, NOT a security boundary.
81
75
  * Slot access.ts always runs via SlotAccessGate regardless.
82
76
  */
83
- export declare function shouldSkipSlot({ slotKey, clientSlots, slotNode, departingUrlParts, destinationUrlParts, }: ShouldSkipSlotArgs): boolean;
84
- export {};
77
+ export declare function shouldSkipSlot(contentKey: string, clientSlotKeys: Set<string> | null): boolean;
85
78
  //# 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,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAI9D,2EAA2E;AAC3E,MAAM,WAAW,eAAgB,SAAQ,GAAG,CAAC,MAAM,CAAC;IAClD,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAwBzE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,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,CAaT;AAID,UAAU,kBAAkB;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,mBAAmB,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,WAAW,EACX,QAAQ,EACR,iBAAiB,EACjB,mBAAmB,GACpB,EAAE,kBAAkB,GAAG,OAAO,CAuB9B"}
1
+ {"version":3,"file":"state-tree-diff.d.ts","sourceRoot":"","sources":["../../src/server/state-tree-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,2EAA2E;AAC3E,MAAM,WAAW,eAAgB,SAAQ,GAAG,CAAC,MAAM,CAAC;IAClD,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAwBzE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,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,CAaT;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,OAAO,CAG9F"}
@@ -121,13 +121,6 @@ export interface SlotAccessGateProps {
121
121
  createElement: CreateElement;
122
122
  defaultFallback: ReactNode;
123
123
  children: ReactNode;
124
- /**
125
- * Pre-computed verdict from eager access evaluation. When provided,
126
- * SlotAccessGate replays it synchronously instead of re-calling accessFn.
127
- * 'pass' → render children. DenySignal → graceful degradation.
128
- * undefined → call accessFn during render (backward compat, error re-run).
129
- */
130
- verdict?: 'pass' | import('./primitives.js').DenySignal | import('./primitives.js').RedirectSignal;
131
124
  }
132
125
  /**
133
126
  * Result of building the element tree.
@@ -1 +1 @@
1
- {"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAI/D,mDAAmD;AACnD,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mEAAmE;IACnE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAErF;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,QAAQ,EAAE,OAAO,EAAE,KACnB,SAAS,CAAC;AAEf;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAElD,0CAA0C;AAC1C,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,mCAAmC;IACnC,UAAU,EAAE,YAAY,CAAC;IACzB,yCAAyC;IACzC,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAID;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,oBAAoB,EAAE,aAAa,EAAE,CAAC;IACzD;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,wFAAwF;IACxF,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,aAAa,EAAE,aAAa,CAAC;IAC7B,eAAe,EAAE,SAAS,CAAC;IAC3B,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;;OAKG;IACH,OAAO,CAAC,EACJ,MAAM,GACN,OAAO,iBAAiB,EAAE,UAAU,GACpC,OAAO,iBAAiB,EAAE,cAAc,CAAC;CAC9C;AAID;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkF1F"}
1
+ {"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAI/D,mDAAmD;AACnD,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mEAAmE;IACnE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAErF;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,QAAQ,EAAE,OAAO,EAAE,KACnB,SAAS,CAAC;AAEf;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAElD,0CAA0C;AAC1C,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,mCAAmC;IACnC,UAAU,EAAE,YAAY,CAAC;IACzB,yCAAyC;IACzC,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAID;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,oBAAoB,EAAE,aAAa,EAAE,CAAC;IACzD;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,wFAAwF;IACxF,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,aAAa,EAAE,aAAa,CAAC;IAC7B,eAAe,EAAE,SAAS,CAAC;IAC3B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAID;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkF1F"}
@@ -38,6 +38,13 @@ export interface SegmentInfo {
38
38
  denied?: boolean;
39
39
  /** True when the slot was skipped (cached content reused). Payloads with skipped slots are not replayable. */
40
40
  skipped?: boolean;
41
+ /**
42
+ * Content key for slot entries — encodes owner parts, slot name, matched
43
+ * entry file, and slot params. The client stores and advertises this key;
44
+ * the server computes the destination key and checks membership.
45
+ * Replaces URL-based departing/destination diffing (TIM-1370).
46
+ */
47
+ contentKey?: string;
41
48
  }
42
49
  /**
43
50
  * The client's `X-Timber-State-Tree` request header.
@@ -1 +1 @@
1
- {"version":3,"file":"segment-info.d.ts","sourceRoot":"","sources":["../../src/shared/segment-info.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mFAAmF;IACnF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2GAA2G;IAC3G,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8GAA8G;IAC9G,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB"}
1
+ {"version":3,"file":"segment-info.d.ts","sourceRoot":"","sources":["../../src/shared/segment-info.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mFAAmF;IACnF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2GAA2G;IAC3G,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8GAA8G;IAC9G,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.187",
3
+ "version": "0.2.0-alpha.188",
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",
@@ -64,6 +64,12 @@ export interface SegmentNode {
64
64
  slots?: Map<string, SegmentNode>;
65
65
  /** Whether this slot's access.ts denied on its last render. */
66
66
  denied?: boolean;
67
+ /**
68
+ * Content key for slot nodes — opaque string the server computed from
69
+ * owner parts, slot name, entry file, and slot params. Advertised back
70
+ * to the server so it can decide skips by key membership (TIM-1370).
71
+ */
72
+ contentKey?: string;
67
73
  }
68
74
 
69
75
  // ─── Segment Cache ───────────────────────────────────────────────
@@ -161,14 +167,20 @@ function collectTreePaths(node: SegmentNode, out: string[]): void {
161
167
  }
162
168
  }
163
169
 
164
- /** Recursively collect cacheable slot paths from the tree */
170
+ /**
171
+ * Recursively collect content keys from cacheable slots.
172
+ *
173
+ * The server advertises a content key per slot on every render (TIM-1370),
174
+ * encoding the owner's URL parts, the slot name, the matched entry file,
175
+ * and the slot's params. The client stores this key and sends it back on
176
+ * the next navigation so the server can decide skips by key membership —
177
+ * no departing URL reconstruction needed.
178
+ */
165
179
  function collectSyncSlots(node: SegmentNode, out: string[]): void {
166
180
  if (node.slots) {
167
181
  for (const slot of node.slots.values()) {
168
- // Exclude request-dependent slots (they must re-render every nav)
169
- // and denied slots (their cached content is denial fallback, not real content)
170
- if (!slot.isRequestDependent && !slot.denied) {
171
- out.push(slot.segment);
182
+ if (!slot.isRequestDependent && !slot.denied && slot.contentKey) {
183
+ out.push(slot.contentKey);
172
184
  }
173
185
  }
174
186
  }
@@ -247,6 +259,7 @@ export function buildSegmentTree(segments: SegmentInfo[]): SegmentNode | undefin
247
259
  isRequestDependent: slotInfo.isRequestDependent,
248
260
  children: new Map(),
249
261
  denied: slotInfo.denied,
262
+ contentKey: slotInfo.contentKey,
250
263
  };
251
264
 
252
265
  if (!parentNode.slots) {
@@ -144,3 +144,40 @@ export function computeSlotKey(parentSegmentId: string, slotName: string): strin
144
144
  const prefix = parentSegmentId === '/' ? '' : parentSegmentId;
145
145
  return `${prefix}/${name}`;
146
146
  }
147
+
148
+ /**
149
+ * Compute a content key for a parallel route slot.
150
+ *
151
+ * The content key encodes everything that determines the slot's rendered
152
+ * output for a given navigation: the slot key (which includes the owning
153
+ * segment's identity and the slot name), the URL parts consumed by the
154
+ * owning segment (which determine parent params the slot page might
155
+ * read), the matched page file (or a sentinel for `default.tsx`), and
156
+ * the slot's own extracted params.
157
+ *
158
+ * The slot key prevents cross-group collisions: two route groups at the
159
+ * same URL level each owning a `@sidebar` slot produce different keys
160
+ * even when both fall back to `default.tsx`.
161
+ *
162
+ * The key is opaque to the client — it stores whatever the server sent
163
+ * and advertises it back. The server computes the destination key and
164
+ * checks membership. This eliminates the need for the server to
165
+ * reconstruct the client's state from a departing URL.
166
+ *
167
+ * See design/07-routing.md §"Segment Tree Diffing on Navigation"
168
+ */
169
+ export function computeSlotContentKey(
170
+ slotKey: string,
171
+ ownerParts: string[],
172
+ entryFile: string | null,
173
+ slotParams: Record<string, string | string[]>
174
+ ): string {
175
+ const owner = ownerParts.join('/');
176
+ const entry = entryFile ?? '\x01';
177
+ const paramKeys = Object.keys(slotParams).sort();
178
+ const paramParts = paramKeys.map((k) => {
179
+ const v = slotParams[k];
180
+ return Array.isArray(v) ? `${k}=${v.join('\x02')}` : `${k}=${v}`;
181
+ });
182
+ return [slotKey, owner, entry, ...paramParts].join('\0');
183
+ }
@@ -144,35 +144,8 @@ async function accessGateFallback(
144
144
  * slot doesn't make architectural sense.
145
145
  */
146
146
  export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactNode> {
147
- const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children, verdict } =
148
- props;
147
+ const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children } = props;
149
148
 
150
- // Fast path: replay pre-computed verdict from eager evaluation in resolveSlotProps.
151
- if (verdict !== undefined) {
152
- if (verdict === 'pass') return children;
153
- if (verdict instanceof DenySignal) {
154
- return (
155
- buildDeniedFallback(DeniedComponent, slotName, verdict.data, createElement) ??
156
- defaultFallback ??
157
- null
158
- );
159
- }
160
- // RedirectSignal: treat as deny in production (same as existing behavior)
161
- if (isDebug()) {
162
- console.error(
163
- '[timber] redirect() is not allowed in slot access.ts. ' +
164
- 'Slots use deny() for graceful degradation — denied.tsx → default.tsx → null. ' +
165
- "If you need to redirect, move the logic to the parent segment's access.ts."
166
- );
167
- }
168
- return (
169
- buildDeniedFallback(DeniedComponent, slotName, undefined, createElement) ??
170
- defaultFallback ??
171
- null
172
- );
173
- }
174
-
175
- // Fallback path: call accessFn during render (no pre-computed verdict).
176
149
  try {
177
150
  await accessFn();
178
151
  } catch (error: unknown) {
@@ -7,9 +7,8 @@
7
7
  * - the layout depth handed to `SegmentProvider` (`route-element-builder.ts`),
8
8
  * which `useSelectedLayoutSegment(s)` indexes the live pathname with;
9
9
  * - `slotUrlParts`, which slices the destination pathname for slot matching;
10
- * - `resolveSlotProps`, which slices the *departing* pathname the same way to
11
- * decide whether a slot can be skipped;
12
- * - `skippable-prefix.ts`, which does the same for the contiguous skip prefix.
10
+ * - `resolveSlotProps` and `skippable-prefix.ts`, which derive content keys
11
+ * for slot skip decisions (TIM-1370).
13
12
  *
14
13
  * They were four separate `extractUrlParts(...).length` calls. That was fine
15
14
  * while the answer was a plain walk, and stopped being fine when interception
@@ -547,30 +547,9 @@ export async function buildRouteElement(
547
547
 
548
548
  let outermostSegmentProvider: React.ReactElement | null = null;
549
549
 
550
- // Slot caching: the departing URL (X-Timber-URL header) and the
551
- // destination URL (request URL) are compared to determine which
552
- // slots' content has changed.
553
- //
554
- // Pathnames only. The client sends `X-Timber-URL` as a pathname
555
- // (`client/router.ts` takes `.pathname` on both branches) and
556
- // `slot-resolver.ts` reads `.pathname` from both sides, so search
557
- // params take no part in the skip decision.
558
- //
559
- // Nothing here can tell whether a slot's content depends on the query.
560
- // What is supposed to make that safe lives elsewhere: a slot whose server
561
- // render varies with the query must reach it through a request accessor
562
- // (page components are built with no props), which the request-dep analysis
563
- // is meant to catch — and that analysis is incomplete for accessors reached
564
- // through indirection, so this is currently a real staleness hole. See
565
- // TIM-1291 and design/19-client-navigation.md. A slot whose *client* subtree
566
- // varies with the query reads useSearchParams(), which re-renders from
567
- // NavigationContext whether or not the slot was skipped; that half is
568
- // unconditional.
569
- //
570
- // Do not add search to the comparison by sending the full departing URL:
571
- // the header is hashed into the public `_rsc` cache key (TIM-1268),
572
- // and a digest does not anonymize a guessable query string.
573
- const departingUrl = req.headers.get('X-Timber-URL');
550
+ // Slot caching uses key-based diffing (TIM-1370). The server computes a
551
+ // content key per slot and the client advertises keys for cached slots.
552
+ // The skip decision is a set-membership check — no departing URL needed.
574
553
  const destinationUrl = req.url;
575
554
  const slotSkipInfo: SlotSkipEntry[] = [];
576
555
 
@@ -705,7 +684,6 @@ export async function buildRouteElement(
705
684
  h,
706
685
  interception,
707
686
  parentTreePath,
708
- departingUrl,
709
687
  destinationUrl,
710
688
  clientStateTree: clientStateTree as ClientStateTree | null,
711
689
  slotSkipInfo,
@@ -248,6 +248,7 @@ export function buildSegmentInfo(
248
248
  isRequestDependent: slot.isRequestDependent,
249
249
  slot: true,
250
250
  parentSegment: slot.parentSegmentId,
251
+ contentKey: slot.contentKey,
251
252
  };
252
253
  if (slot.skipped) entry.skipped = true;
253
254
  if (slot.denied) entry.denied = true;
@@ -11,7 +11,9 @@ import type { ManifestSegmentNode } from './route-matcher.js';
11
11
  import type { RouteMatch } from './pipeline.js';
12
12
  import { shouldSkipSlot, type ClientStateTree } from './state-tree-diff.js';
13
13
  import { consumedUrlParts } from './chain-url-parts.js';
14
- import { computeSlotKey } from '../routing/segment-keys.js';
14
+ import { computeSlotContentKey, computeSlotKey } from '../routing/segment-keys.js';
15
+ import { matchUrlParts } from './tree-match.js';
16
+ import { slotUrlParts } from './slot-resolver.js';
15
17
 
16
18
  /**
17
19
  * Truncate the skippable prefix when a skippable layout has slots whose
@@ -22,6 +24,9 @@ import { computeSlotKey } from '../routing/segment-keys.js';
22
24
  * Removing the segment from the skippable set forces it to render with
23
25
  * its slots. The prefix must stay contiguous, so all segments after the
24
26
  * first changed-slot segment are also removed.
27
+ *
28
+ * Uses key-based diffing (TIM-1370): computes the destination content key
29
+ * per slot and checks if the client's advertised set contains it.
25
30
  */
26
31
  export function truncateSkippableForChangedSlots(
27
32
  skippableSet: Set<number>,
@@ -31,23 +36,8 @@ export function truncateSkippableForChangedSlots(
31
36
  clientStateTree: ClientStateTree,
32
37
  req: Request
33
38
  ): void {
34
- const departingUrl = req.headers.get('X-Timber-URL');
35
- let depParsed: URL | null = null;
36
- if (departingUrl) {
37
- try {
38
- depParsed = new URL(departingUrl, 'http://localhost');
39
- } catch {
40
- skippableSet.clear();
41
- return;
42
- }
43
- }
44
- const destPathname = new URL(req.url, 'http://localhost').pathname;
45
- const depPathname = depParsed?.pathname ?? null;
46
- const split = (p: string) => (p === '/' ? [] : p.slice(1).split('/'));
47
- const destAll = split(destPathname);
48
- const depAll = depPathname ? split(depPathname) : null;
49
- const clientSlots = clientStateTree.slots ?? null;
50
- const rawParams = match.rawSegmentParams ?? match.segmentParams ?? {};
39
+ const clientSlotKeys = clientStateTree.slots ?? null;
40
+ const destUrl = req.url;
51
41
 
52
42
  for (let i = 0; i < segments.length; i++) {
53
43
  if (!skippableSet.has(i)) continue;
@@ -56,33 +46,25 @@ export function truncateSkippableForChangedSlots(
56
46
  if (slotEntries.length === 0) continue;
57
47
 
58
48
  const segIdx = match.segments.indexOf(segment);
59
- // The root consumes no URL part, and a climbing interception marker
60
- // removes owner segments from the target — `consumedUrlParts` handles both,
61
- // and is the same derivation slot matching and layout depth use.
62
- const sliceAt = segIdx >= 0 ? consumedUrlParts(match.segments, segIdx, match).length : 0;
63
- const destParts = destAll.slice(sliceAt);
64
- const depParts = depAll ? depAll.slice(sliceAt) : null;
65
- let parentChanged = false;
66
- if (depAll) {
67
- const dp = depAll.slice(0, sliceAt),
68
- dd = destAll.slice(0, sliceAt);
69
- parentChanged = dp.length !== dd.length || dp.some((p, j) => p !== dd[j]);
70
- }
49
+ const ownerParts = segIdx >= 0 ? consumedUrlParts(match.segments, segIdx, match) : [];
50
+ const destParts = slotUrlParts(segment, match, destUrl);
71
51
 
72
52
  for (const [slotName, slotNode] of slotEntries) {
73
53
  const m = slotNode as ManifestSegmentNode;
74
54
  const k = computeSlotKey(segmentKeys[i], `@${slotName}`);
55
+
56
+ // Slots with access.ts are never skip candidates (TIM-1364).
57
+ const destMatch = matchUrlParts(m, destParts);
58
+ const hasAccessInChain = !!m.access || (destMatch?.chain.some((seg) => seg.access) ?? false);
59
+
60
+ const destLeaf = destMatch?.chain[destMatch.chain.length - 1];
61
+ const entryFile = destLeaf?.page?.filePath ?? null;
62
+ const contentKey = computeSlotContentKey(k, ownerParts, entryFile, destMatch?.params ?? {});
63
+
75
64
  if (
65
+ hasAccessInChain ||
76
66
  m.children.some((c) => c.segmentType === 'intercepting') ||
77
- parentChanged ||
78
- depParts === null ||
79
- !shouldSkipSlot({
80
- slotKey: k,
81
- clientSlots,
82
- slotNode: m,
83
- departingUrlParts: depParts,
84
- destinationUrlParts: destParts,
85
- })
67
+ !shouldSkipSlot(contentKey, clientSlotKeys)
86
68
  ) {
87
69
  for (let j = i; j < segments.length; j++) skippableSet.delete(j);
88
70
  return;