@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.
- package/dist/_chunks/{resolve-schema-5ma5pp1b.js → resolve-schema-CBR6Lm4i.js} +2 -2
- package/dist/_chunks/{resolve-schema-5ma5pp1b.js.map → resolve-schema-CBR6Lm4i.js.map} +1 -1
- package/dist/_chunks/{schema-bridge-Cc2Gngu1.js → schema-bridge-C83xa9lT.js} +2 -2
- package/dist/_chunks/{schema-bridge-Cc2Gngu1.js.map → schema-bridge-C83xa9lT.js.map} +1 -1
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -1
- package/dist/_chunks/{use-query-states-BbU5Ge1V.js → use-query-states-I3JMng6J.js} +29 -5
- package/dist/_chunks/use-query-states-I3JMng6J.js.map +1 -0
- package/dist/client/index.js +1 -1
- package/dist/client/internal.js +13 -4
- package/dist/client/internal.js.map +1 -1
- package/dist/client/segment-cache.d.ts +6 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/params/index.js +1 -1
- package/dist/routing/segment-keys.d.ts +22 -0
- package/dist/routing/segment-keys.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +4 -1
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +30 -4
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.d.ts +1 -1
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +20 -5
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/parse-total.d.ts +14 -3
- package/dist/search-params/parse-total.d.ts.map +1 -1
- package/dist/search-params/serialize-equal.d.ts +9 -0
- package/dist/search-params/serialize-equal.d.ts.map +1 -0
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +2 -3
- package/dist/server/chain-url-parts.d.ts.map +1 -1
- package/dist/server/internal.js +2 -8
- package/dist/server/internal.js.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/skippable-prefix.d.ts +3 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +30 -19
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +9 -16
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +0 -7
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/shared/segment-info.d.ts +7 -0
- package/dist/shared/segment-info.d.ts.map +1 -1
- package/docs/api/33-api-search-params.mdx +3 -3
- package/docs/learn/05-typed-params.mdx +1 -1
- package/package.json +1 -1
- package/src/client/segment-cache.ts +18 -5
- package/src/client/use-query-states.ts +23 -14
- package/src/routing/segment-keys.ts +37 -0
- package/src/schema-bridge.ts +8 -3
- package/src/search-params/define.ts +73 -14
- package/src/search-params/index.ts +1 -0
- package/src/search-params/parse-total.ts +17 -4
- package/src/search-params/serialize-equal.ts +14 -0
- package/src/search-params/wrappers.ts +1 -1
- package/src/server/access-gate.tsx +1 -28
- package/src/server/chain-url-parts.ts +2 -3
- package/src/server/route-element-builder.ts +14 -26
- package/src/server/rsc-entry/helpers.ts +1 -0
- package/src/server/skippable-prefix.ts +21 -39
- package/src/server/slot-resolver.ts +141 -187
- package/src/server/state-tree-diff.ts +11 -62
- package/src/server/tree-builder.ts +0 -10
- package/src/shared/segment-info.ts +7 -0
- 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,
|
|
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,
|
|
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;
|
|
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
|
|
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
|
|
152
|
-
*
|
|
153
|
-
*
|
|
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,
|
|
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":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;
|
|
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
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
|
-
*
|
|
78
|
-
*
|
|
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(
|
|
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;
|
|
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;
|
|
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;
|
|
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
|
-
|
|
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([])
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
+
}
|
package/src/schema-bridge.ts
CHANGED
|
@@ -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>):
|
|
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.
|
|
375
|
+
return value.length === 0 ? null : value.map(String);
|
|
371
376
|
}
|
|
372
377
|
return String(value);
|
|
373
378
|
},
|