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

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 (69) hide show
  1. package/dist/_chunks/{resolve-schema-5ma5pp1b.js → resolve-schema-CBR6Lm4i.js} +2 -2
  2. package/dist/_chunks/{resolve-schema-5ma5pp1b.js.map → resolve-schema-CBR6Lm4i.js.map} +1 -1
  3. package/dist/_chunks/{schema-bridge-Cc2Gngu1.js → schema-bridge-C83xa9lT.js} +2 -2
  4. package/dist/_chunks/{schema-bridge-Cc2Gngu1.js.map → schema-bridge-C83xa9lT.js.map} +1 -1
  5. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -1
  6. package/dist/_chunks/{use-query-states-BbU5Ge1V.js → use-query-states-I3JMng6J.js} +29 -5
  7. package/dist/_chunks/use-query-states-I3JMng6J.js.map +1 -0
  8. package/dist/client/index.js +1 -1
  9. package/dist/client/internal.js +13 -4
  10. package/dist/client/internal.js.map +1 -1
  11. package/dist/client/segment-cache.d.ts +6 -0
  12. package/dist/client/segment-cache.d.ts.map +1 -1
  13. package/dist/client/use-query-states.d.ts.map +1 -1
  14. package/dist/codec.js +1 -1
  15. package/dist/cookies/index.js +1 -1
  16. package/dist/params/index.js +1 -1
  17. package/dist/routing/segment-keys.d.ts +22 -0
  18. package/dist/routing/segment-keys.d.ts.map +1 -1
  19. package/dist/schema-bridge.d.ts +4 -1
  20. package/dist/schema-bridge.d.ts.map +1 -1
  21. package/dist/search-params/define.d.ts +30 -4
  22. package/dist/search-params/define.d.ts.map +1 -1
  23. package/dist/search-params/index.d.ts +1 -1
  24. package/dist/search-params/index.d.ts.map +1 -1
  25. package/dist/search-params/index.js +20 -5
  26. package/dist/search-params/index.js.map +1 -1
  27. package/dist/search-params/parse-total.d.ts +14 -3
  28. package/dist/search-params/parse-total.d.ts.map +1 -1
  29. package/dist/search-params/serialize-equal.d.ts +9 -0
  30. package/dist/search-params/serialize-equal.d.ts.map +1 -0
  31. package/dist/server/access-gate.d.ts.map +1 -1
  32. package/dist/server/chain-url-parts.d.ts +2 -3
  33. package/dist/server/chain-url-parts.d.ts.map +1 -1
  34. package/dist/server/internal.js +2 -8
  35. package/dist/server/internal.js.map +1 -1
  36. package/dist/server/route-element-builder.d.ts.map +1 -1
  37. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  38. package/dist/server/skippable-prefix.d.ts +3 -0
  39. package/dist/server/skippable-prefix.d.ts.map +1 -1
  40. package/dist/server/slot-resolver.d.ts +30 -19
  41. package/dist/server/slot-resolver.d.ts.map +1 -1
  42. package/dist/server/state-tree-diff.d.ts +9 -16
  43. package/dist/server/state-tree-diff.d.ts.map +1 -1
  44. package/dist/server/tree-builder.d.ts +0 -7
  45. package/dist/server/tree-builder.d.ts.map +1 -1
  46. package/dist/shared/segment-info.d.ts +7 -0
  47. package/dist/shared/segment-info.d.ts.map +1 -1
  48. package/docs/api/33-api-search-params.mdx +3 -3
  49. package/docs/learn/05-typed-params.mdx +1 -1
  50. package/package.json +1 -1
  51. package/src/client/segment-cache.ts +18 -5
  52. package/src/client/use-query-states.ts +23 -14
  53. package/src/routing/segment-keys.ts +37 -0
  54. package/src/schema-bridge.ts +8 -3
  55. package/src/search-params/define.ts +73 -14
  56. package/src/search-params/index.ts +1 -0
  57. package/src/search-params/parse-total.ts +17 -4
  58. package/src/search-params/serialize-equal.ts +14 -0
  59. package/src/search-params/wrappers.ts +1 -1
  60. package/src/server/access-gate.tsx +1 -28
  61. package/src/server/chain-url-parts.ts +2 -3
  62. package/src/server/route-element-builder.ts +14 -26
  63. package/src/server/rsc-entry/helpers.ts +1 -0
  64. package/src/server/skippable-prefix.ts +21 -39
  65. package/src/server/slot-resolver.ts +141 -187
  66. package/src/server/state-tree-diff.ts +11 -62
  67. package/src/server/tree-builder.ts +0 -10
  68. package/src/shared/segment-info.ts +7 -0
  69. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +0 -1
@@ -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,EAIL,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAG5B,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,CA4iB7B"}
@@ -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.
@@ -134,6 +127,18 @@ export declare function slotUrlParts(segment: ManifestSegmentNode, match: RouteM
134
127
  * @internal
135
128
  */
136
129
  export declare function publishSlotSegmentParams(segment: ManifestSegmentNode, match: RouteMatch, destinationUrl: string, parentTreePath: string | undefined): void;
130
+ /**
131
+ * Emit slot metadata for a layout whose rendering was SKIPPED.
132
+ *
133
+ * Skipped layouts never reach `resolveSlotProps`, but the client rebuilds
134
+ * its segment tree from each navigation's X-Timber-Segments response. If
135
+ * skipped slots are absent, the client loses their contentKeys and can't
136
+ * advertise them on the next navigation — breaking key-based slot skipping.
137
+ *
138
+ * This computes contentKey and isRequestDependent for each slot without
139
+ * rendering anything, using the same derivation as `resolveSlotProps`.
140
+ */
141
+ export declare function emitSlotInfoForSkippedLayout(segment: ManifestSegmentNode, segmentId: string, match: RouteMatch, destinationUrl: string, slotSkipInfo: SlotSkipEntry[]): void;
137
142
  interface ResolveSlotPropsArgs {
138
143
  segment: ManifestSegmentNode;
139
144
  segmentId: string;
@@ -141,17 +146,23 @@ interface ResolveSlotPropsArgs {
141
146
  h: CreateElementFn;
142
147
  interception?: InterceptionContext;
143
148
  parentTreePath: string;
144
- departingUrl: string | null;
145
149
  destinationUrl: string;
146
150
  clientStateTree: ClientStateTree | null;
147
151
  slotSkipInfo: SlotSkipEntry[];
148
152
  }
149
153
  /**
150
154
  * 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.
155
+ * SegmentOutlet for client-side caching. Slots whose content key matches
156
+ * one the client advertised are rendered with `skip=true` so the client
157
+ * keeps its cached content.
158
+ *
159
+ * The skip decision is key-based (TIM-1370): the server computes a content
160
+ * key per slot from the owning segment's URL parts, the slot name, the
161
+ * matched entry file, and the slot's own params. The client advertises
162
+ * the keys it has cached; the server checks membership. This replaces
163
+ * the previous approach of reconstructing the client's state from a
164
+ * departing URL.
154
165
  */
155
- export declare function resolveSlotProps({ segment, segmentId, match, h, interception, parentTreePath, departingUrl, destinationUrl, clientStateTree, slotSkipInfo, }: ResolveSlotPropsArgs): Promise<Record<string, unknown>>;
166
+ export declare function resolveSlotProps({ segment, segmentId, match, h, interception, parentTreePath, destinationUrl, clientStateTree, slotSkipInfo, }: ResolveSlotPropsArgs): Promise<Record<string, unknown>>;
156
167
  export {};
157
168
  //# 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":"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"}
@@ -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"}
@@ -108,7 +108,7 @@ The codec protocol shared across search params, segment params, and cookies. See
108
108
  ```ts
109
109
  interface SearchParamCodec<T> {
110
110
  parse(value: string | string[] | undefined): T;
111
- serialize(value: T): string | null;
111
+ serialize(value: T): string | string[] | null;
112
112
  urlKey?: string;
113
113
  // optional total entry point — preferred over parse when present
114
114
  parseServerSide?(value: string | string[] | undefined): T;
@@ -141,11 +141,11 @@ So `?page=1&page=2` gives `1`, `parseAsBoolean` on a missing `?debug=` gives `nu
141
141
 
142
142
  A present value is not validated for you — each parser decides. `parseAsBoolean` on `?v=maybe` is `false`, and `parseAsInteger` on `?v=12abc` is `12`. Use a schema when you need the value rejected rather than coerced.
143
143
 
144
- The exception is `parseAsNativeArrayOf`, nuqs's *multi*-value parser for repeated keys. It is **not usable in `defineSearchParams`**: its `parse` is declared over `readonly string[]`, so the field is a compile error, and forcing it through leaves the client hook reading it as empty. Use an array schema for repeated keys. Tracked as TIM-1355.
144
+ `parseAsNativeArrayOf` is nuqs's *multi*-value parser for repeated keys (`?tags=a&tags=b`). It works in `defineSearchParams`: timber accepts it via `TotalSearchParamCodec` (its `parseServerSide` covers the full domain) and its `serialize` returns `string[]`, which `buildSearchParams` emits as repeated keys. The codec round-trips correctly.
145
145
 
146
146
  Two consequences worth knowing:
147
147
 
148
- - **`parseAsArrayOf` reads one key, not repeated keys.** It is a comma-separated list inside a single value, so `?tags=a,b` gives `['a','b']` but `?tags=a&tags=b` gives `['a']`. For repeated keys use an array schema — `tags: z.array(z.string()).default([])` reads both `?tags=a` and `?tags=a&tags=b`.
148
+ - **`parseAsArrayOf` reads one key, not repeated keys.** It is a comma-separated list inside a single value, so `?tags=a,b` gives `['a','b']` but `?tags=a&tags=b` gives `['a']`. For repeated keys use `parseAsNativeArrayOf` or an array schema — `tags: z.array(z.string()).default([])`.
149
149
  - **`parseAsJson` requires a validator argument** — `parseAsJson(z.object({ … }).parse)`. Called bare it does not type-check, and at runtime returns `null` for every value.
150
150
 
151
151
  A field typed by a bare parser is nullable (`parseAsString` → `string | null`). Use `withDefault()` — timber's or the parser's own — to make it non-nullable.
@@ -15,7 +15,7 @@ A codec converts between string values and typed values:
15
15
  ```ts
16
16
  interface Codec<T> {
17
17
  parse(value: string | string[] | undefined): T;
18
- serialize(value: T): string | null;
18
+ serialize(value: T): string | string[] | null; // string[] for repeated keys
19
19
  }
20
20
  ```
21
21
 
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.189",
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) {
@@ -19,6 +19,7 @@ import type {
19
19
  QueryStatesOptions,
20
20
  } from '../search-params/define.js';
21
21
  import { parseTotal } from '../search-params/parse-total.js';
22
+ import { serializedEqual } from '../search-params/serialize-equal.js';
22
23
 
23
24
  // ─── Codec Bridge ─────────────────────────────────────────────────
24
25
 
@@ -113,22 +114,28 @@ function bridgeCodec<T>(codec: SearchParamCodec<T>): MultiParser<T> & { defaultV
113
114
  wrapNuqsValue(parseTotal(codec, v.length === 1 ? v[0] : [...v])),
114
115
  serialize: (v: unknown) => {
115
116
  const value = unwrapNuqsValue(v);
116
- // Delegate null to the codec — some codecs encode null as a real
117
- // query value. undefined has no encoding; nuqs requires a string.
118
- //
119
- // ONE element, always. A multi parser may return several — nuqs
120
- // appends one key per entry — but `Codec.serialize` returns a single
121
- // string by construction, so timber cannot emit repeated keys here
122
- // any more than `buildSearchParams` can. Widening that protocol is
123
- // TIM-1353. Wrapping the same string keeps the URL the hook writes
124
- // byte-identical to the one `buildSearchParams` writes.
125
- return [value === undefined ? '' : (codec.serialize(value as T) ?? '')];
117
+ if (value === undefined) return [''];
118
+ // TIM-1354: catch TypeError from codecs whose serialize is not total
119
+ // over null (e.g. parseAsIsoDate.serialize(null) → null.toISOString()).
120
+ // Only TypeError — deliberate signals must not be swallowed.
121
+ let result: string | string[] | null;
122
+ try {
123
+ result = codec.serialize(value as T);
124
+ } catch (error) {
125
+ if (error instanceof TypeError) return [''];
126
+ throw error;
127
+ }
128
+ // TIM-1353: pass through string[] from codecs that emit repeated
129
+ // keys. nuqs multi parsers append one key=value per array element.
130
+ if (Array.isArray(result)) return result;
131
+ return [result ?? ''];
126
132
  },
127
133
  eq: (a: unknown, b: unknown) => {
128
134
  if (a === b) return true;
129
135
  try {
130
- return (
131
- codec.serialize(unwrapNuqsValue(a) as T) === codec.serialize(unwrapNuqsValue(b) as T)
136
+ return serializedEqual(
137
+ codec.serialize(unwrapNuqsValue(a) as T),
138
+ codec.serialize(unwrapNuqsValue(b) as T)
132
139
  );
133
140
  } catch {
134
141
  return false;
@@ -275,9 +282,11 @@ export function useQueryStates<T extends Record<string, unknown>>(
275
282
  if (forwarded === partial) forwarded = { ...partial };
276
283
  forwarded[key] = null;
277
284
  } else if (value === null) {
278
- let encoded: string | null = null;
285
+ let encoded: string | string[] | null = null;
279
286
  try {
280
- encoded = codecs[key as keyof T]?.serialize(null as T[keyof T]) ?? null;
287
+ const raw = codecs[key as keyof T]?.serialize(null as T[keyof T]);
288
+ // string[] is non-null, but an empty array carries no value
289
+ encoded = raw === null ? null : Array.isArray(raw) ? (raw.length > 0 ? raw : null) : raw;
281
290
  } catch {
282
291
  // Codec can't serialize null — treat as a deletion.
283
292
  }
@@ -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
+ }
@@ -358,16 +358,21 @@ export function fromCookieSchema<T>(schema: StandardSchemaV1<T>): Codec<T> {
358
358
  * It also serializes an empty array to `null` (omitting the key) where
359
359
  * `fromSchema` writes `''`.
360
360
  */
361
- export function fromArraySchema<T>(schema: StandardSchemaV1<T>): Codec<T> {
361
+ export function fromArraySchema<T>(schema: StandardSchemaV1<T>): {
362
+ parse(value: string | string[] | undefined): T;
363
+ serialize(value: T): string | string[] | null;
364
+ } {
362
365
  return {
363
366
  parse: (value: string | string[] | undefined): T =>
364
367
  parseThroughSchema(schema, arrayFirst, value),
365
- serialize(value: T): string | null {
368
+ serialize(value: T): string | string[] | null {
366
369
  if (value === null || value === undefined) {
367
370
  return null;
368
371
  }
372
+ // TIM-1353: return string[] so buildSearchParams emits repeated keys
373
+ // (?tag=a&tag=b) instead of comma-joining into one key (?tag=a%2Cb).
369
374
  if (Array.isArray(value)) {
370
- return value.length === 0 ? null : value.join(',');
375
+ return value.length === 0 ? null : value.map(String);
371
376
  }
372
377
  return String(value);
373
378
  },