@timber-js/app 0.2.0-alpha.124 → 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/{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/{navigation-root-DaERBEGV.js → navigation-root-CeNTCxzS.js} +5 -5
- 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/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.js +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 +3 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +127 -269
- 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-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/index.js +1 -1
- package/dist/server/internal.js +57 -1
- 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/client/browser-entry/hydrate.ts +7 -1
- package/src/client/browser-entry/index.ts +3 -1
- package/src/client/browser-entry/post-hydration.ts +0 -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-root.tsx +40 -18
- package/src/client/router.ts +109 -97
- 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/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/navigation-root-DaERBEGV.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
|
@@ -4,8 +4,8 @@ import { a as setNavigationState, i as getNavigationState, o as useNavigationCon
|
|
|
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.
|
|
@@ -689,34 +522,51 @@ function createRouter(deps) {
|
|
|
689
522
|
async function renderViaTransition(url, perform) {
|
|
690
523
|
if (deps.navigateTransition) {
|
|
691
524
|
let headElements = null;
|
|
692
|
-
let decodePromise = null;
|
|
693
525
|
await deps.navigateTransition(url, async (wrapPayload) => {
|
|
694
526
|
const result = await perform();
|
|
695
527
|
headElements = result.headElements;
|
|
696
|
-
|
|
697
|
-
|
|
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
|
+
}
|
|
698
540
|
historyStack.push(url, {
|
|
699
|
-
payload:
|
|
541
|
+
payload: result.payload,
|
|
700
542
|
headElements: result.headElements,
|
|
701
543
|
params: result.params
|
|
702
544
|
});
|
|
703
|
-
|
|
704
|
-
|
|
545
|
+
return {
|
|
546
|
+
element: wrapPayload(result.payload, result.navState),
|
|
547
|
+
decodePromise: result.decodePromise
|
|
548
|
+
};
|
|
705
549
|
});
|
|
706
550
|
return {
|
|
707
551
|
headElements,
|
|
708
|
-
decodePromise
|
|
552
|
+
decodePromise: null
|
|
709
553
|
};
|
|
710
554
|
}
|
|
711
555
|
const result = await perform();
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
payload: merged,
|
|
556
|
+
if (isPartialNavigation(result.skippedSegments)) historyStack.push(url, {
|
|
557
|
+
payload: null,
|
|
715
558
|
headElements: result.headElements,
|
|
716
559
|
params: result.params
|
|
717
560
|
});
|
|
718
|
-
|
|
719
|
-
|
|
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
|
+
}
|
|
720
570
|
return {
|
|
721
571
|
headElements: result.headElements,
|
|
722
572
|
decodePromise: result.decodePromise
|
|
@@ -756,7 +606,7 @@ function createRouter(deps) {
|
|
|
756
606
|
skippedSegments: prefetched.skippedSegments ?? null
|
|
757
607
|
} : void 0;
|
|
758
608
|
if (result === void 0) {
|
|
759
|
-
const stateTree = segmentCache.serializeStateTree(
|
|
609
|
+
const stateTree = segmentCache.serializeStateTree();
|
|
760
610
|
const rawCurrentUrl = deps.getCurrentUrl();
|
|
761
611
|
result = await fetchRscPayload(url, deps, stateTree, rawCurrentUrl.startsWith("http") ? new URL(rawCurrentUrl).pathname : new URL(rawCurrentUrl, "http://localhost").pathname, options.signal);
|
|
762
612
|
}
|
|
@@ -784,7 +634,6 @@ function createRouter(deps) {
|
|
|
784
634
|
const replace = options.replace === true;
|
|
785
635
|
const externalSignal = options._signal;
|
|
786
636
|
const skipHistory = options._skipHistory === true;
|
|
787
|
-
navigationId++;
|
|
788
637
|
const navAbort = createNavAbort(externalSignal);
|
|
789
638
|
const currentScrollY = deps.getScrollY();
|
|
790
639
|
if (deps.saveNavigationEntryScroll) deps.saveNavigationEntryScroll(currentScrollY);
|
|
@@ -801,19 +650,18 @@ function createRouter(deps) {
|
|
|
801
650
|
}
|
|
802
651
|
setPending(true, url);
|
|
803
652
|
try {
|
|
804
|
-
const { headElements
|
|
653
|
+
const { headElements } = await renderViaTransition(url, () => performNavigationFetch(url, {
|
|
805
654
|
replace,
|
|
806
655
|
signal: navAbort.signal,
|
|
807
656
|
skipHistory: effectiveSkipHistory
|
|
808
657
|
}));
|
|
809
|
-
if (decodePromise) await decodePromise;
|
|
810
658
|
applyHead(headElements);
|
|
811
659
|
window.dispatchEvent(new Event("timber:navigation-end"));
|
|
812
660
|
restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
|
|
813
661
|
} catch (error) {
|
|
814
662
|
if (error instanceof VersionSkewError) {
|
|
815
663
|
setHardNavigating(true);
|
|
816
|
-
const { triggerStaleReload } = await import("../_chunks/stale-reload-
|
|
664
|
+
const { triggerStaleReload } = await import("../_chunks/stale-reload-CCqb62Z3.js").then((n) => n.n);
|
|
817
665
|
triggerStaleReload();
|
|
818
666
|
return new Promise(() => {});
|
|
819
667
|
}
|
|
@@ -828,6 +676,11 @@ function createRouter(deps) {
|
|
|
828
676
|
window.location.href = error.url;
|
|
829
677
|
return new Promise(() => {});
|
|
830
678
|
}
|
|
679
|
+
if (isStaleClientReference(error)) {
|
|
680
|
+
setHardNavigating(true);
|
|
681
|
+
triggerStaleReload();
|
|
682
|
+
return new Promise(() => {});
|
|
683
|
+
}
|
|
831
684
|
if (isAbortError(error)) return;
|
|
832
685
|
throw error;
|
|
833
686
|
} finally {
|
|
@@ -838,11 +691,10 @@ function createRouter(deps) {
|
|
|
838
691
|
}
|
|
839
692
|
async function refresh() {
|
|
840
693
|
const currentUrl = deps.getCurrentUrl();
|
|
841
|
-
navigationId++;
|
|
842
694
|
const navAbort = createNavAbort();
|
|
843
695
|
setPending(true, currentUrl);
|
|
844
696
|
try {
|
|
845
|
-
const { headElements
|
|
697
|
+
const { headElements } = await renderViaTransition(currentUrl, async () => {
|
|
846
698
|
const result = await fetchRscPayload(currentUrl, deps, void 0, void 0, navAbort.signal);
|
|
847
699
|
updateSegmentCache(result.segmentInfo);
|
|
848
700
|
const navState = updateNavigationState(result.params, currentUrl);
|
|
@@ -851,9 +703,13 @@ function createRouter(deps) {
|
|
|
851
703
|
navState
|
|
852
704
|
};
|
|
853
705
|
});
|
|
854
|
-
if (decodePromise) await decodePromise;
|
|
855
706
|
applyHead(headElements);
|
|
856
707
|
} catch (error) {
|
|
708
|
+
if (isStaleClientReference(error)) {
|
|
709
|
+
setHardNavigating(true);
|
|
710
|
+
triggerStaleReload();
|
|
711
|
+
return new Promise(() => {});
|
|
712
|
+
}
|
|
857
713
|
if (isAbortError(error)) return;
|
|
858
714
|
throw error;
|
|
859
715
|
} finally {
|
|
@@ -863,7 +719,6 @@ function createRouter(deps) {
|
|
|
863
719
|
}
|
|
864
720
|
}
|
|
865
721
|
async function handlePopState(url, scrollY = 0, externalSignal) {
|
|
866
|
-
navigationId++;
|
|
867
722
|
const entry = historyStack.get(url);
|
|
868
723
|
if (entry && entry.payload !== null) {
|
|
869
724
|
const navState = updateNavigationState(entry.params, url);
|
|
@@ -874,8 +729,8 @@ function createRouter(deps) {
|
|
|
874
729
|
const navAbort = createNavAbort(externalSignal);
|
|
875
730
|
setPending(true, url);
|
|
876
731
|
try {
|
|
877
|
-
const { headElements
|
|
878
|
-
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);
|
|
879
734
|
updateSegmentCache(result.segmentInfo);
|
|
880
735
|
const navState = updateNavigationState(result.params, url);
|
|
881
736
|
return {
|
|
@@ -883,10 +738,14 @@ function createRouter(deps) {
|
|
|
883
738
|
navState
|
|
884
739
|
};
|
|
885
740
|
});
|
|
886
|
-
if (decodePromise) await decodePromise;
|
|
887
741
|
applyHead(headElements);
|
|
888
742
|
restoreScrollAfterPaint(scrollY);
|
|
889
743
|
} catch (error) {
|
|
744
|
+
if (isStaleClientReference(error)) {
|
|
745
|
+
setHardNavigating(true);
|
|
746
|
+
triggerStaleReload();
|
|
747
|
+
return new Promise(() => {});
|
|
748
|
+
}
|
|
890
749
|
if (isAbortError(error)) return;
|
|
891
750
|
throw error;
|
|
892
751
|
} finally {
|
|
@@ -902,7 +761,8 @@ function createRouter(deps) {
|
|
|
902
761
|
function prefetch(url) {
|
|
903
762
|
if (prefetchCache.get(url) !== void 0) return;
|
|
904
763
|
if (historyStack.has(url)) return;
|
|
905
|
-
fetchRscPayload(url, deps, segmentCache.serializeStateTree(
|
|
764
|
+
fetchRscPayload(url, deps, segmentCache.serializeStateTree()).then((result) => {
|
|
765
|
+
result.decodePromise?.catch(() => {});
|
|
906
766
|
prefetchCache.set(url, result);
|
|
907
767
|
}, () => {});
|
|
908
768
|
}
|
|
@@ -923,12 +783,10 @@ function createRouter(deps) {
|
|
|
923
783
|
payload: element,
|
|
924
784
|
headElements
|
|
925
785
|
});
|
|
926
|
-
deferSegmentCaching(element);
|
|
927
786
|
renderPayload(element, getNavigationState());
|
|
928
787
|
applyHead(headElements);
|
|
929
788
|
},
|
|
930
789
|
initSegmentCache: (segments) => updateSegmentCache(segments),
|
|
931
|
-
cacheElementTree: (element) => cacheSegmentElements(element, segmentElementCache),
|
|
932
790
|
segmentCache,
|
|
933
791
|
prefetchCache,
|
|
934
792
|
historyStack
|