@timber-js/app 0.2.0-alpha.123 → 0.2.0-alpha.125
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/{actions-ZlcKx0H9.js → actions-418ygYKC.js} +2 -2
- package/dist/_chunks/{actions-ZlcKx0H9.js.map → actions-418ygYKC.js.map} +1 -1
- package/dist/_chunks/{build-output-helper-DYOWsPjZ.js → build-output-helper-K7oUCRqU.js} +1 -1
- package/dist/_chunks/{build-output-helper-DYOWsPjZ.js.map → build-output-helper-K7oUCRqU.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-VcCf1TFY.js → cli-schema-sync-DFHBj6yZ.js} +59 -12
- package/dist/_chunks/{cli-schema-sync-VcCf1TFY.js.map → cli-schema-sync-DFHBj6yZ.js.map} +1 -1
- package/dist/_chunks/cli-schema-sync-vdobDWwX.js +2 -0
- package/dist/_chunks/{define-C0V_eKiW.js → define-BJMwm0c4.js} +2 -2
- package/dist/_chunks/{define-C0V_eKiW.js.map → define-BJMwm0c4.js.map} +1 -1
- package/dist/_chunks/{navigation-root-Ds_hDtRW.js → navigation-root-CeNTCxzS.js} +6 -6
- package/dist/_chunks/navigation-root-CeNTCxzS.js.map +1 -0
- package/dist/_chunks/{stale-reload-BN2kcE06.js → stale-reload-CCqb62Z3.js} +22 -2
- package/dist/_chunks/{stale-reload-BN2kcE06.js.map → stale-reload-CCqb62Z3.js.map} +1 -1
- package/dist/_chunks/{use-segment-params-Du7u25cK.js → use-segment-params-ByGqwUuO.js} +3 -2
- package/dist/_chunks/use-segment-params-ByGqwUuO.js.map +1 -0
- package/dist/_chunks/{walkers-tkw7sDlj.js → walkers-DMSGZeCk.js} +2 -2
- package/dist/_chunks/{walkers-tkw7sDlj.js.map → walkers-DMSGZeCk.js.map} +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/cli-schema-sync.d.ts +6 -4
- package/dist/cli-schema-sync.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -6
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +4 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/index.js +4 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +147 -301
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link-pending-store.d.ts +19 -17
- package/dist/client/link-pending-store.d.ts.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +1 -0
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +4 -1
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts +10 -7
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +2 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-outlet.d.ts +23 -37
- package/dist/client/segment-outlet.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +21 -0
- package/dist/client/segment-update-context.d.ts.map +1 -0
- package/dist/client/use-search-params.d.ts +7 -5
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/routing/index.js +2 -2
- package/dist/segment-params/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +58 -2
- 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 +1 -0
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +12 -5
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli-schema-sync.ts +80 -13
- package/src/cli.ts +18 -7
- package/src/client/browser-entry/hydrate.ts +9 -1
- package/src/client/browser-entry/index.ts +3 -1
- package/src/client/browser-entry/post-hydration.ts +1 -13
- package/src/client/browser-entry/router-init.ts +50 -24
- package/src/client/link-pending-store.ts +22 -17
- package/src/client/link.tsx +12 -6
- package/src/client/navigation-context.ts +2 -1
- package/src/client/navigation-root.tsx +40 -18
- package/src/client/router.ts +113 -99
- package/src/client/rsc-fetch.ts +85 -6
- package/src/client/segment-cache.ts +5 -2
- package/src/client/segment-outlet.tsx +40 -49
- package/src/client/segment-update-context.ts +42 -0
- package/src/client/use-search-params.ts +25 -44
- package/src/server/route-element-builder.ts +69 -2
- package/src/server/rsc-entry/helpers.ts +18 -14
- package/src/server/state-tree-diff.ts +26 -22
- package/dist/_chunks/cli-schema-sync-4HcQIziG.js +0 -2
- package/dist/_chunks/navigation-root-Ds_hDtRW.js.map +0 -1
- package/dist/_chunks/use-segment-params-Du7u25cK.js.map +0 -1
- package/dist/client/segment-merger.d.ts +0 -119
- package/dist/client/segment-merger.d.ts.map +0 -1
- package/src/client/segment-merger.ts +0 -339
package/dist/client/internal.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { t as bindUseQueryStates } from "../_chunks/use-query-states-C9G6LP0F.js";
|
|
2
2
|
import { c as cachedSearchParams, i as _setCachedSearch, n as getSsrData, r as setSsrData, s as cachedSearch, t as clearSsrData } from "../_chunks/ssr-data-DVX8S7vj.js";
|
|
3
|
-
import { a as setNavigationState, i as getNavigationState, r as NavigationProvider, t as setCurrentParams } from "../_chunks/use-segment-params-
|
|
3
|
+
import { a as setNavigationState, i as getNavigationState, o as useNavigationContext, r as NavigationProvider, t as setCurrentParams } from "../_chunks/use-segment-params-ByGqwUuO.js";
|
|
4
4
|
import { n as getRouterOrNull, r as setGlobalRouter, t as getRouter } from "../_chunks/router-ref-B2-2kIIC.js";
|
|
5
5
|
import { TimberErrorBoundary } from "./error-boundary.js";
|
|
6
6
|
import { n as useSegmentContext, t as SegmentProvider } from "../_chunks/segment-context-Bg0pFbLT.js";
|
|
7
|
-
import { t as setHardNavigating } from "../_chunks/navigation-root-
|
|
8
|
-
import {
|
|
7
|
+
import { t as setHardNavigating } from "../_chunks/navigation-root-CeNTCxzS.js";
|
|
8
|
+
import { r as triggerStaleReload, t as isStaleClientReference } from "../_chunks/stale-reload-CCqb62Z3.js";
|
|
9
9
|
//#region src/client/segment-cache.ts
|
|
10
10
|
/**
|
|
11
11
|
* Maintains the client-side segment tree representing currently mounted
|
|
@@ -64,14 +64,15 @@ function buildSegmentTree(segments) {
|
|
|
64
64
|
let root;
|
|
65
65
|
let parent;
|
|
66
66
|
for (const info of layouts) {
|
|
67
|
+
const id = info.segmentId ?? info.path;
|
|
67
68
|
const node = {
|
|
68
|
-
segment:
|
|
69
|
+
segment: id,
|
|
69
70
|
payload: null,
|
|
70
71
|
isAsync: info.isAsync,
|
|
71
72
|
children: /* @__PURE__ */ new Map()
|
|
72
73
|
};
|
|
73
74
|
if (!root) root = node;
|
|
74
|
-
if (parent) parent.children.set(
|
|
75
|
+
if (parent) parent.children.set(id, node);
|
|
75
76
|
parent = node;
|
|
76
77
|
}
|
|
77
78
|
return root;
|
|
@@ -157,217 +158,6 @@ var HistoryStack = class {
|
|
|
157
158
|
}
|
|
158
159
|
};
|
|
159
160
|
//#endregion
|
|
160
|
-
//#region src/client/segment-merger.ts
|
|
161
|
-
/**
|
|
162
|
-
* Segment Merger — client-side tree merging for partial RSC payloads.
|
|
163
|
-
*
|
|
164
|
-
* When the server skips rendering sync layouts (because the client already
|
|
165
|
-
* has them cached), the RSC payload is missing outer segment wrappers.
|
|
166
|
-
* This module reconstructs the full element tree by splicing the partial
|
|
167
|
-
* payload into cached segment subtrees.
|
|
168
|
-
*
|
|
169
|
-
* The approach:
|
|
170
|
-
* 1. After each full RSC payload render, walk the decoded element tree
|
|
171
|
-
* and cache each segment's subtree (identified by SegmentProvider boundaries)
|
|
172
|
-
* 2. When a partial payload arrives, wrap it with cached segment elements
|
|
173
|
-
* using React.cloneElement to preserve component identity
|
|
174
|
-
*
|
|
175
|
-
* React.cloneElement preserves the element's `type` — React sees the same
|
|
176
|
-
* component at the same tree position and reconciles (preserving state)
|
|
177
|
-
* rather than remounting. This is how layout state survives navigations.
|
|
178
|
-
*
|
|
179
|
-
* Design docs: 19-client-navigation.md §"Navigation Reconciliation"
|
|
180
|
-
* Security: access.ts runs on the server regardless of skipping — this
|
|
181
|
-
* is a performance optimization only. See 13-security.md.
|
|
182
|
-
*/
|
|
183
|
-
/**
|
|
184
|
-
* Cache of React element subtrees per segment path.
|
|
185
|
-
* Updated after each navigation with the full decoded RSC element tree.
|
|
186
|
-
*/
|
|
187
|
-
var SegmentElementCache = class {
|
|
188
|
-
entries = /* @__PURE__ */ new Map();
|
|
189
|
-
get(segmentPath) {
|
|
190
|
-
return this.entries.get(segmentPath);
|
|
191
|
-
}
|
|
192
|
-
set(segmentPath, entry) {
|
|
193
|
-
this.entries.set(segmentPath, entry);
|
|
194
|
-
}
|
|
195
|
-
has(segmentPath) {
|
|
196
|
-
return this.entries.has(segmentPath);
|
|
197
|
-
}
|
|
198
|
-
clear() {
|
|
199
|
-
this.entries.clear();
|
|
200
|
-
}
|
|
201
|
-
get size() {
|
|
202
|
-
return this.entries.size;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Get the set of segment paths that are safe for the server to skip.
|
|
206
|
-
* Only segments with an inner SegmentProvider (hasMergeableChild) are
|
|
207
|
-
* included — the merger can only replace inner SegmentProviders, not
|
|
208
|
-
* pages embedded in layout output. Used to filter the state tree.
|
|
209
|
-
*
|
|
210
|
-
* Returns an empty set if the element cache is empty (no elements
|
|
211
|
-
* cached yet). This is the safe default — an empty set means no
|
|
212
|
-
* segments pass the filter, so the state tree is empty and the server
|
|
213
|
-
* does a full render. The element cache is populated lazily after the
|
|
214
|
-
* first SPA navigation (RSC-decoded elements from hydration are
|
|
215
|
-
* thenables that can't be walked until React resolves them).
|
|
216
|
-
*/
|
|
217
|
-
getMergeablePaths() {
|
|
218
|
-
const paths = /* @__PURE__ */ new Set();
|
|
219
|
-
for (const [, entry] of this.entries) if (entry.hasMergeableChild) paths.add(entry.segmentPath);
|
|
220
|
-
return paths;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
/**
|
|
224
|
-
* Check if a React element is a SegmentProvider by looking for the
|
|
225
|
-
* `segments` prop (an array of path segments). This is the only
|
|
226
|
-
* component that receives this prop shape.
|
|
227
|
-
*/
|
|
228
|
-
function isSegmentProvider(element) {
|
|
229
|
-
if (!isValidElement(element)) return false;
|
|
230
|
-
const props = element.props;
|
|
231
|
-
return Array.isArray(props.segments);
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Extract the segment path from a SegmentProvider element.
|
|
235
|
-
*
|
|
236
|
-
* Uses the `segmentId` prop if available (set by the server for route groups
|
|
237
|
-
* to distinguish siblings that share the same urlPath). Falls back to
|
|
238
|
-
* reconstructing from the `segments` array prop.
|
|
239
|
-
*/
|
|
240
|
-
function getSegmentPath(element) {
|
|
241
|
-
const props = element.props;
|
|
242
|
-
if (props.segmentId) return props.segmentId;
|
|
243
|
-
const filtered = props.segments.filter(Boolean);
|
|
244
|
-
return filtered.length === 0 ? "/" : "/" + filtered.join("/");
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Walk a React element tree and extract all SegmentProvider boundaries.
|
|
248
|
-
* Returns an ordered list of segment entries from outermost to innermost.
|
|
249
|
-
*
|
|
250
|
-
* This only finds SegmentProviders along the main children path — it does
|
|
251
|
-
* not descend into parallel routes/slots (those are separate subtrees).
|
|
252
|
-
*/
|
|
253
|
-
function extractSegments(element) {
|
|
254
|
-
const segments = [];
|
|
255
|
-
walkForSegments(element, segments);
|
|
256
|
-
for (let i = 0; i < segments.length; i++) segments[i].hasMergeableChild = i < segments.length - 1;
|
|
257
|
-
return segments;
|
|
258
|
-
}
|
|
259
|
-
function walkForSegments(node, out) {
|
|
260
|
-
if (!isValidElement(node)) return;
|
|
261
|
-
const el = node;
|
|
262
|
-
const props = el.props;
|
|
263
|
-
if (isSegmentProvider(node)) {
|
|
264
|
-
out.push({
|
|
265
|
-
segmentPath: getSegmentPath(el),
|
|
266
|
-
element: el,
|
|
267
|
-
hasMergeableChild: false
|
|
268
|
-
});
|
|
269
|
-
walkChildren(props.children, out);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
walkChildren(props.children, out);
|
|
273
|
-
}
|
|
274
|
-
function walkChildren(children, out) {
|
|
275
|
-
if (children == null) return;
|
|
276
|
-
if (Array.isArray(children)) for (const child of children) walkForSegments(child, out);
|
|
277
|
-
else walkForSegments(children, out);
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Cache all segment subtrees from a fully-rendered RSC element tree.
|
|
281
|
-
* Call this after every full RSC payload render (navigate, refresh, hydration).
|
|
282
|
-
*/
|
|
283
|
-
function cacheSegmentElements(element, cache) {
|
|
284
|
-
const segments = extractSegments(element);
|
|
285
|
-
for (const entry of segments) cache.set(entry.segmentPath, entry);
|
|
286
|
-
}
|
|
287
|
-
function findSegmentProviderPath(node, targetPath) {
|
|
288
|
-
const children = node.props.children;
|
|
289
|
-
if (children == null) return null;
|
|
290
|
-
if (Array.isArray(children)) for (let i = 0; i < children.length; i++) {
|
|
291
|
-
const child = children[i];
|
|
292
|
-
if (!isValidElement(child)) continue;
|
|
293
|
-
if (isSegmentProvider(child)) {
|
|
294
|
-
if (!targetPath || getSegmentPath(child) === targetPath) return [{
|
|
295
|
-
element: node,
|
|
296
|
-
childIndex: i
|
|
297
|
-
}];
|
|
298
|
-
}
|
|
299
|
-
const deeper = findSegmentProviderPath(child, targetPath);
|
|
300
|
-
if (deeper) return [{
|
|
301
|
-
element: node,
|
|
302
|
-
childIndex: i
|
|
303
|
-
}, ...deeper];
|
|
304
|
-
}
|
|
305
|
-
else if (isValidElement(children)) {
|
|
306
|
-
if (isSegmentProvider(children)) {
|
|
307
|
-
if (!targetPath || getSegmentPath(children) === targetPath) return [{
|
|
308
|
-
element: node,
|
|
309
|
-
childIndex: -1
|
|
310
|
-
}];
|
|
311
|
-
}
|
|
312
|
-
const deeper = findSegmentProviderPath(children, targetPath);
|
|
313
|
-
if (deeper) return [{
|
|
314
|
-
element: node,
|
|
315
|
-
childIndex: -1
|
|
316
|
-
}, ...deeper];
|
|
317
|
-
}
|
|
318
|
-
return null;
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Replace a nested SegmentProvider within a cached element tree with
|
|
322
|
-
* new content. Uses cloneElement along the path to produce a new tree
|
|
323
|
-
* with preserved component identity at every level except the replaced node.
|
|
324
|
-
*
|
|
325
|
-
* @param cachedElement The cached SegmentProvider element for this segment
|
|
326
|
-
* @param newInnerContent The new React element to splice in at the inner segment position
|
|
327
|
-
* @param innerSegmentPath The path of the inner segment to replace (optional — replaces first found)
|
|
328
|
-
* @returns New element tree with the inner segment replaced
|
|
329
|
-
*/
|
|
330
|
-
function replaceInnerSegment(cachedElement, newInnerContent, innerSegmentPath) {
|
|
331
|
-
const path = findSegmentProviderPath(cachedElement, innerSegmentPath);
|
|
332
|
-
if (!path || path.length === 0) return cachedElement;
|
|
333
|
-
let replacement = newInnerContent;
|
|
334
|
-
for (let i = path.length - 1; i >= 0; i--) {
|
|
335
|
-
const { element, childIndex } = path[i];
|
|
336
|
-
if (childIndex === -1) replacement = cloneElement(element, {}, replacement);
|
|
337
|
-
else {
|
|
338
|
-
const newChildren = [...element.props.children];
|
|
339
|
-
newChildren[childIndex] = replacement;
|
|
340
|
-
replacement = cloneElement(element, {}, ...newChildren);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return replacement;
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Merge a partial RSC payload with cached segment elements.
|
|
347
|
-
*
|
|
348
|
-
* When the server skips segments, the partial payload starts from the
|
|
349
|
-
* first non-skipped segment. This function wraps it with cached elements
|
|
350
|
-
* for the skipped segments, producing a full tree that React can
|
|
351
|
-
* reconcile with the mounted tree (preserving layout state).
|
|
352
|
-
*
|
|
353
|
-
* @param partialPayload The RSC payload element (may be partial)
|
|
354
|
-
* @param skippedSegments Ordered list of segment paths that were skipped (outermost first)
|
|
355
|
-
* @param cache The segment element cache
|
|
356
|
-
* @returns The merged full element tree, or the partial payload if merging isn't possible
|
|
357
|
-
*/
|
|
358
|
-
function mergeSegmentTree(partialPayload, skippedSegments, cache) {
|
|
359
|
-
if (!isValidElement(partialPayload)) return partialPayload;
|
|
360
|
-
if (skippedSegments.length === 0) return partialPayload;
|
|
361
|
-
let result = partialPayload;
|
|
362
|
-
for (let i = skippedSegments.length - 1; i >= 0; i--) {
|
|
363
|
-
const segmentPath = skippedSegments[i];
|
|
364
|
-
const cached = cache.get(segmentPath);
|
|
365
|
-
if (!cached) return partialPayload;
|
|
366
|
-
result = replaceInnerSegment(cached.element, result);
|
|
367
|
-
}
|
|
368
|
-
return result;
|
|
369
|
-
}
|
|
370
|
-
//#endregion
|
|
371
161
|
//#region src/client/rsc-fetch.ts
|
|
372
162
|
var RSC_CONTENT_TYPE = "text/x-component";
|
|
373
163
|
/**
|
|
@@ -526,6 +316,46 @@ var ServerErrorResponse = class extends Error {
|
|
|
526
316
|
}
|
|
527
317
|
};
|
|
528
318
|
/**
|
|
319
|
+
* Wrap a response body stream to track when it's fully consumed.
|
|
320
|
+
* Returns a new body that passes all chunks through unchanged, plus
|
|
321
|
+
* a `done` promise that resolves when the last chunk is read (or
|
|
322
|
+
* rejects if the stream errors).
|
|
323
|
+
*
|
|
324
|
+
* Used to keep React transitions open for the full RSC stream
|
|
325
|
+
* duration — createFromFetch's thenable resolves on shell arrival,
|
|
326
|
+
* but we need stream completion for useOptimistic pending state.
|
|
327
|
+
*/
|
|
328
|
+
function trackStreamCompletion(body) {
|
|
329
|
+
let resolveDone;
|
|
330
|
+
let rejectDone;
|
|
331
|
+
const done = new Promise((res, rej) => {
|
|
332
|
+
resolveDone = res;
|
|
333
|
+
rejectDone = rej;
|
|
334
|
+
});
|
|
335
|
+
const reader = body.getReader();
|
|
336
|
+
return {
|
|
337
|
+
body: new ReadableStream({
|
|
338
|
+
async pull(controller) {
|
|
339
|
+
try {
|
|
340
|
+
const result = await reader.read();
|
|
341
|
+
if (result.done) {
|
|
342
|
+
controller.close();
|
|
343
|
+
resolveDone();
|
|
344
|
+
} else controller.enqueue(result.value);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
controller.error(error);
|
|
347
|
+
rejectDone(error);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
cancel(reason) {
|
|
351
|
+
reader.cancel(reason);
|
|
352
|
+
resolveDone();
|
|
353
|
+
}
|
|
354
|
+
}),
|
|
355
|
+
done
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
529
359
|
* Fetch an RSC payload from the server. If a decodeRsc function is provided,
|
|
530
360
|
* the response is decoded into a React element tree via createFromFetch.
|
|
531
361
|
* Otherwise, the raw response text is returned (test mode).
|
|
@@ -546,6 +376,7 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
|
546
376
|
let segmentInfo = null;
|
|
547
377
|
let params = null;
|
|
548
378
|
let skippedSegments = null;
|
|
379
|
+
let streamDone = Promise.resolve();
|
|
549
380
|
const wrappedPromise = fetchPromise.then((response) => {
|
|
550
381
|
if (checkReloadSignal(response)) throw new VersionSkewError();
|
|
551
382
|
const redirectLocation = response.headers.get("X-Timber-Redirect") || (response.status >= 300 && response.status < 400 ? response.headers.get("Location") : null);
|
|
@@ -555,15 +386,26 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
|
555
386
|
segmentInfo = extractSegmentInfo(response);
|
|
556
387
|
params = extractParams(response);
|
|
557
388
|
skippedSegments = extractSkippedSegments(response);
|
|
389
|
+
if (response.body) {
|
|
390
|
+
const tracked = trackStreamCompletion(response.body);
|
|
391
|
+
streamDone = tracked.done;
|
|
392
|
+
streamDone.catch(() => {});
|
|
393
|
+
return new Response(tracked.body, {
|
|
394
|
+
headers: response.headers,
|
|
395
|
+
status: response.status
|
|
396
|
+
});
|
|
397
|
+
}
|
|
558
398
|
return response;
|
|
559
399
|
});
|
|
560
400
|
await wrappedPromise;
|
|
561
401
|
const payload = deps.decodeRsc(wrappedPromise);
|
|
562
|
-
const
|
|
563
|
-
|
|
402
|
+
const payloadError = new Promise((_, reject) => {
|
|
403
|
+
Promise.resolve(payload).then(() => {}, reject);
|
|
404
|
+
});
|
|
405
|
+
payloadError.catch(() => {});
|
|
564
406
|
return {
|
|
565
407
|
payload,
|
|
566
|
-
decodePromise,
|
|
408
|
+
decodePromise: Promise.race([streamDone, payloadError]),
|
|
567
409
|
headElements,
|
|
568
410
|
segmentInfo,
|
|
569
411
|
params,
|
|
@@ -603,9 +445,7 @@ function createRouter(deps) {
|
|
|
603
445
|
const segmentCache = new SegmentCache();
|
|
604
446
|
const prefetchCache = new PrefetchCache();
|
|
605
447
|
const historyStack = new HistoryStack();
|
|
606
|
-
const segmentElementCache = new SegmentElementCache();
|
|
607
448
|
let routerPhase = { phase: "idle" };
|
|
608
|
-
let navigationId = 0;
|
|
609
449
|
const pendingListeners = /* @__PURE__ */ new Set();
|
|
610
450
|
let currentNavAbort = null;
|
|
611
451
|
/**
|
|
@@ -640,31 +480,24 @@ function createRouter(deps) {
|
|
|
640
480
|
function renderPayload(payload, navState) {
|
|
641
481
|
if (deps.renderRoot) deps.renderRoot(payload, navState);
|
|
642
482
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
* were skipped. Returns the merged payload ready for rendering.
|
|
646
|
-
*
|
|
647
|
-
* Segment element caching is deferred to after paint by the caller
|
|
648
|
-
* so it doesn't block the render path.
|
|
649
|
-
*/
|
|
650
|
-
async function mergePayload(payload, skippedSegments) {
|
|
651
|
-
if (skippedSegments && skippedSegments.length > 0) return mergeSegmentTree(payload != null && typeof payload === "object" && "then" in payload ? await payload : payload, skippedSegments, segmentElementCache);
|
|
652
|
-
return payload;
|
|
483
|
+
function isPartialNavigation(skippedSegments) {
|
|
484
|
+
return skippedSegments != null && skippedSegments.length > 0;
|
|
653
485
|
}
|
|
654
486
|
/**
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
* Guards against stale writes: if a newer navigation started before
|
|
658
|
-
* this callback fires, the cache write is skipped.
|
|
487
|
+
* Build a segment updates map for partial navigation. Identifies the
|
|
488
|
+
* first non-skipped segment and maps it to the payload content.
|
|
659
489
|
*/
|
|
660
|
-
function
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
490
|
+
function buildSegmentUpdates(result) {
|
|
491
|
+
const skipped = new Set(result.skippedSegments);
|
|
492
|
+
const segmentInfo = result.segmentInfo;
|
|
493
|
+
const updates = /* @__PURE__ */ new Map();
|
|
494
|
+
if (segmentInfo) {
|
|
495
|
+
for (const info of segmentInfo) if (!skipped.has(info.segmentId ?? info.path)) {
|
|
496
|
+
updates.set(info.segmentId ?? info.path, result.payload);
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return updates;
|
|
668
501
|
}
|
|
669
502
|
/**
|
|
670
503
|
* Update navigation state (params + pathname) for the next render.
|
|
@@ -677,9 +510,11 @@ function createRouter(deps) {
|
|
|
677
510
|
function updateNavigationState(params, url) {
|
|
678
511
|
const resolvedParams = params ?? {};
|
|
679
512
|
setCurrentParams(resolvedParams);
|
|
513
|
+
const parsed = new URL(url, "http://localhost");
|
|
680
514
|
const navState = {
|
|
681
515
|
params: resolvedParams,
|
|
682
|
-
pathname:
|
|
516
|
+
pathname: parsed.pathname || "/",
|
|
517
|
+
search: parsed.search
|
|
683
518
|
};
|
|
684
519
|
setNavigationState(navState);
|
|
685
520
|
return navState;
|
|
@@ -687,34 +522,51 @@ function createRouter(deps) {
|
|
|
687
522
|
async function renderViaTransition(url, perform) {
|
|
688
523
|
if (deps.navigateTransition) {
|
|
689
524
|
let headElements = null;
|
|
690
|
-
let decodePromise = null;
|
|
691
525
|
await deps.navigateTransition(url, async (wrapPayload) => {
|
|
692
526
|
const result = await perform();
|
|
693
527
|
headElements = result.headElements;
|
|
694
|
-
|
|
695
|
-
|
|
528
|
+
if (isPartialNavigation(result.skippedSegments)) {
|
|
529
|
+
const segmentUpdates = buildSegmentUpdates(result);
|
|
530
|
+
historyStack.push(url, {
|
|
531
|
+
payload: null,
|
|
532
|
+
headElements: result.headElements,
|
|
533
|
+
params: result.params
|
|
534
|
+
});
|
|
535
|
+
return {
|
|
536
|
+
element: wrapPayload(deps._getCurrentPayload?.() ?? result.payload, result.navState, segmentUpdates),
|
|
537
|
+
decodePromise: result.decodePromise
|
|
538
|
+
};
|
|
539
|
+
}
|
|
696
540
|
historyStack.push(url, {
|
|
697
|
-
payload:
|
|
541
|
+
payload: result.payload,
|
|
698
542
|
headElements: result.headElements,
|
|
699
543
|
params: result.params
|
|
700
544
|
});
|
|
701
|
-
|
|
702
|
-
|
|
545
|
+
return {
|
|
546
|
+
element: wrapPayload(result.payload, result.navState),
|
|
547
|
+
decodePromise: result.decodePromise
|
|
548
|
+
};
|
|
703
549
|
});
|
|
704
550
|
return {
|
|
705
551
|
headElements,
|
|
706
|
-
decodePromise
|
|
552
|
+
decodePromise: null
|
|
707
553
|
};
|
|
708
554
|
}
|
|
709
555
|
const result = await perform();
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
payload: merged,
|
|
556
|
+
if (isPartialNavigation(result.skippedSegments)) historyStack.push(url, {
|
|
557
|
+
payload: null,
|
|
713
558
|
headElements: result.headElements,
|
|
714
559
|
params: result.params
|
|
715
560
|
});
|
|
716
|
-
|
|
717
|
-
|
|
561
|
+
else {
|
|
562
|
+
const payload = result.payload != null && typeof result.payload === "object" && "then" in result.payload ? await result.payload : result.payload;
|
|
563
|
+
historyStack.push(url, {
|
|
564
|
+
payload,
|
|
565
|
+
headElements: result.headElements,
|
|
566
|
+
params: result.params
|
|
567
|
+
});
|
|
568
|
+
renderPayload(payload, result.navState);
|
|
569
|
+
}
|
|
718
570
|
return {
|
|
719
571
|
headElements: result.headElements,
|
|
720
572
|
decodePromise: result.decodePromise
|
|
@@ -754,7 +606,7 @@ function createRouter(deps) {
|
|
|
754
606
|
skippedSegments: prefetched.skippedSegments ?? null
|
|
755
607
|
} : void 0;
|
|
756
608
|
if (result === void 0) {
|
|
757
|
-
const stateTree = segmentCache.serializeStateTree(
|
|
609
|
+
const stateTree = segmentCache.serializeStateTree();
|
|
758
610
|
const rawCurrentUrl = deps.getCurrentUrl();
|
|
759
611
|
result = await fetchRscPayload(url, deps, stateTree, rawCurrentUrl.startsWith("http") ? new URL(rawCurrentUrl).pathname : new URL(rawCurrentUrl, "http://localhost").pathname, options.signal);
|
|
760
612
|
}
|
|
@@ -782,7 +634,6 @@ function createRouter(deps) {
|
|
|
782
634
|
const replace = options.replace === true;
|
|
783
635
|
const externalSignal = options._signal;
|
|
784
636
|
const skipHistory = options._skipHistory === true;
|
|
785
|
-
navigationId++;
|
|
786
637
|
const navAbort = createNavAbort(externalSignal);
|
|
787
638
|
const currentScrollY = deps.getScrollY();
|
|
788
639
|
if (deps.saveNavigationEntryScroll) deps.saveNavigationEntryScroll(currentScrollY);
|
|
@@ -799,19 +650,18 @@ function createRouter(deps) {
|
|
|
799
650
|
}
|
|
800
651
|
setPending(true, url);
|
|
801
652
|
try {
|
|
802
|
-
const { headElements
|
|
653
|
+
const { headElements } = await renderViaTransition(url, () => performNavigationFetch(url, {
|
|
803
654
|
replace,
|
|
804
655
|
signal: navAbort.signal,
|
|
805
656
|
skipHistory: effectiveSkipHistory
|
|
806
657
|
}));
|
|
807
|
-
if (decodePromise) await decodePromise;
|
|
808
658
|
applyHead(headElements);
|
|
809
659
|
window.dispatchEvent(new Event("timber:navigation-end"));
|
|
810
660
|
restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
|
|
811
661
|
} catch (error) {
|
|
812
662
|
if (error instanceof VersionSkewError) {
|
|
813
663
|
setHardNavigating(true);
|
|
814
|
-
const { triggerStaleReload } = await import("../_chunks/stale-reload-
|
|
664
|
+
const { triggerStaleReload } = await import("../_chunks/stale-reload-CCqb62Z3.js").then((n) => n.n);
|
|
815
665
|
triggerStaleReload();
|
|
816
666
|
return new Promise(() => {});
|
|
817
667
|
}
|
|
@@ -826,6 +676,11 @@ function createRouter(deps) {
|
|
|
826
676
|
window.location.href = error.url;
|
|
827
677
|
return new Promise(() => {});
|
|
828
678
|
}
|
|
679
|
+
if (isStaleClientReference(error)) {
|
|
680
|
+
setHardNavigating(true);
|
|
681
|
+
triggerStaleReload();
|
|
682
|
+
return new Promise(() => {});
|
|
683
|
+
}
|
|
829
684
|
if (isAbortError(error)) return;
|
|
830
685
|
throw error;
|
|
831
686
|
} finally {
|
|
@@ -836,11 +691,10 @@ function createRouter(deps) {
|
|
|
836
691
|
}
|
|
837
692
|
async function refresh() {
|
|
838
693
|
const currentUrl = deps.getCurrentUrl();
|
|
839
|
-
navigationId++;
|
|
840
694
|
const navAbort = createNavAbort();
|
|
841
695
|
setPending(true, currentUrl);
|
|
842
696
|
try {
|
|
843
|
-
const { headElements
|
|
697
|
+
const { headElements } = await renderViaTransition(currentUrl, async () => {
|
|
844
698
|
const result = await fetchRscPayload(currentUrl, deps, void 0, void 0, navAbort.signal);
|
|
845
699
|
updateSegmentCache(result.segmentInfo);
|
|
846
700
|
const navState = updateNavigationState(result.params, currentUrl);
|
|
@@ -849,9 +703,13 @@ function createRouter(deps) {
|
|
|
849
703
|
navState
|
|
850
704
|
};
|
|
851
705
|
});
|
|
852
|
-
if (decodePromise) await decodePromise;
|
|
853
706
|
applyHead(headElements);
|
|
854
707
|
} catch (error) {
|
|
708
|
+
if (isStaleClientReference(error)) {
|
|
709
|
+
setHardNavigating(true);
|
|
710
|
+
triggerStaleReload();
|
|
711
|
+
return new Promise(() => {});
|
|
712
|
+
}
|
|
855
713
|
if (isAbortError(error)) return;
|
|
856
714
|
throw error;
|
|
857
715
|
} finally {
|
|
@@ -861,7 +719,6 @@ function createRouter(deps) {
|
|
|
861
719
|
}
|
|
862
720
|
}
|
|
863
721
|
async function handlePopState(url, scrollY = 0, externalSignal) {
|
|
864
|
-
navigationId++;
|
|
865
722
|
const entry = historyStack.get(url);
|
|
866
723
|
if (entry && entry.payload !== null) {
|
|
867
724
|
const navState = updateNavigationState(entry.params, url);
|
|
@@ -872,8 +729,8 @@ function createRouter(deps) {
|
|
|
872
729
|
const navAbort = createNavAbort(externalSignal);
|
|
873
730
|
setPending(true, url);
|
|
874
731
|
try {
|
|
875
|
-
const { headElements
|
|
876
|
-
const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree(
|
|
732
|
+
const { headElements } = await renderViaTransition(url, async () => {
|
|
733
|
+
const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree(), void 0, navAbort.signal);
|
|
877
734
|
updateSegmentCache(result.segmentInfo);
|
|
878
735
|
const navState = updateNavigationState(result.params, url);
|
|
879
736
|
return {
|
|
@@ -881,10 +738,14 @@ function createRouter(deps) {
|
|
|
881
738
|
navState
|
|
882
739
|
};
|
|
883
740
|
});
|
|
884
|
-
if (decodePromise) await decodePromise;
|
|
885
741
|
applyHead(headElements);
|
|
886
742
|
restoreScrollAfterPaint(scrollY);
|
|
887
743
|
} catch (error) {
|
|
744
|
+
if (isStaleClientReference(error)) {
|
|
745
|
+
setHardNavigating(true);
|
|
746
|
+
triggerStaleReload();
|
|
747
|
+
return new Promise(() => {});
|
|
748
|
+
}
|
|
888
749
|
if (isAbortError(error)) return;
|
|
889
750
|
throw error;
|
|
890
751
|
} finally {
|
|
@@ -900,7 +761,8 @@ function createRouter(deps) {
|
|
|
900
761
|
function prefetch(url) {
|
|
901
762
|
if (prefetchCache.get(url) !== void 0) return;
|
|
902
763
|
if (historyStack.has(url)) return;
|
|
903
|
-
fetchRscPayload(url, deps, segmentCache.serializeStateTree(
|
|
764
|
+
fetchRscPayload(url, deps, segmentCache.serializeStateTree()).then((result) => {
|
|
765
|
+
result.decodePromise?.catch(() => {});
|
|
904
766
|
prefetchCache.set(url, result);
|
|
905
767
|
}, () => {});
|
|
906
768
|
}
|
|
@@ -921,12 +783,10 @@ function createRouter(deps) {
|
|
|
921
783
|
payload: element,
|
|
922
784
|
headElements
|
|
923
785
|
});
|
|
924
|
-
deferSegmentCaching(element);
|
|
925
786
|
renderPayload(element, getNavigationState());
|
|
926
787
|
applyHead(headElements);
|
|
927
788
|
},
|
|
928
789
|
initSegmentCache: (segments) => updateSegmentCache(segments),
|
|
929
|
-
cacheElementTree: (element) => cacheSegmentElements(element, segmentElementCache),
|
|
930
790
|
segmentCache,
|
|
931
791
|
prefetchCache,
|
|
932
792
|
historyStack
|
|
@@ -940,9 +800,12 @@ function createRouter(deps) {
|
|
|
940
800
|
* Returns a read-only URLSearchParams instance reflecting the current
|
|
941
801
|
* URL's query string. Updates when client-side navigation changes the URL.
|
|
942
802
|
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
803
|
+
* On the client, reads from NavigationContext which is updated atomically
|
|
804
|
+
* with the RSC tree render. This replaces the previous useSyncExternalStore
|
|
805
|
+
* approach which read window.location.search directly — causing React to
|
|
806
|
+
* detect external store tearing during transitions and fall back to
|
|
807
|
+
* synchronous rendering (renderRootSync instead of renderRootConcurrent),
|
|
808
|
+
* blocking the main thread and freezing animations.
|
|
946
809
|
*
|
|
947
810
|
* Unlike Next.js's ReadonlyURLSearchParams, this returns a standard
|
|
948
811
|
* URLSearchParams. Mutation methods (set, delete, append) work on the
|
|
@@ -951,28 +814,9 @@ function createRouter(deps) {
|
|
|
951
814
|
* During SSR, reads the request search params from the SSR ALS context
|
|
952
815
|
* (populated by ssr-entry.ts) instead of window.location.
|
|
953
816
|
*
|
|
954
|
-
*
|
|
955
|
-
* See design/18-build-system.md §"Singleton State Registry"
|
|
817
|
+
* Compatible with Next.js's `useSearchParams()` from `next/navigation`.
|
|
956
818
|
*/
|
|
957
|
-
function
|
|
958
|
-
if (typeof window !== "undefined") return window.location.search;
|
|
959
|
-
const data = getSsrData();
|
|
960
|
-
if (!data) return "";
|
|
961
|
-
const str = new URLSearchParams(data.searchParams).toString();
|
|
962
|
-
return str ? `?${str}` : "";
|
|
963
|
-
}
|
|
964
|
-
function getServerSearch() {
|
|
965
|
-
const data = getSsrData();
|
|
966
|
-
if (!data) return "";
|
|
967
|
-
const str = new URLSearchParams(data.searchParams).toString();
|
|
968
|
-
return str ? `?${str}` : "";
|
|
969
|
-
}
|
|
970
|
-
function subscribe(callback) {
|
|
971
|
-
window.addEventListener("popstate", callback);
|
|
972
|
-
return () => window.removeEventListener("popstate", callback);
|
|
973
|
-
}
|
|
974
|
-
function getSearchParams() {
|
|
975
|
-
const search = getSearch();
|
|
819
|
+
function getSearchParams(search) {
|
|
976
820
|
if (search !== cachedSearch) {
|
|
977
821
|
const params = new URLSearchParams(search);
|
|
978
822
|
_setCachedSearch(search, params);
|
|
@@ -980,18 +824,20 @@ function getSearchParams() {
|
|
|
980
824
|
}
|
|
981
825
|
return cachedSearchParams;
|
|
982
826
|
}
|
|
983
|
-
function getServerSearchParams() {
|
|
984
|
-
const data = getSsrData();
|
|
985
|
-
return data ? new URLSearchParams(data.searchParams) : new URLSearchParams();
|
|
986
|
-
}
|
|
987
827
|
/**
|
|
988
828
|
* Read the current URL search params.
|
|
989
829
|
*
|
|
990
830
|
* Compatible with Next.js's `useSearchParams()` from `next/navigation`.
|
|
991
831
|
*/
|
|
992
832
|
function useSearchParams() {
|
|
993
|
-
|
|
994
|
-
|
|
833
|
+
try {
|
|
834
|
+
const navContext = useNavigationContext();
|
|
835
|
+
if (navContext !== null) return getSearchParams(navContext.search);
|
|
836
|
+
} catch {}
|
|
837
|
+
const ssrData = getSsrData();
|
|
838
|
+
if (ssrData) return new URLSearchParams(ssrData.searchParams);
|
|
839
|
+
if (typeof window !== "undefined") return getSearchParams(window.location.search);
|
|
840
|
+
return new URLSearchParams();
|
|
995
841
|
}
|
|
996
842
|
//#endregion
|
|
997
843
|
export { HistoryStack, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, clearSsrData, createRouter, getNavigationState, getRouter, getRouterOrNull, getSsrData, setCurrentParams, setGlobalRouter, setNavigationState, setSsrData, useSearchParams, useSegmentContext };
|