@timber-js/app 0.1.29 → 0.1.31

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.
@@ -3,9 +3,16 @@
3
3
  *
4
4
  * Splits client bundles into cache tiers based on update frequency:
5
5
  *
6
- * Tier 1: vendor-react — react, react-dom, scheduler (changes rarely)
7
- * Tier 2: vendor-timber — timber runtime, RSC runtime (changes per framework update)
8
- * Tier 3: [route]-* per-route app code (changes per deploy, handled by Vite defaults)
6
+ * Tier 1: vendor-react — react, react-dom, scheduler (changes rarely)
7
+ * Tier 2: vendor-timber — timber runtime, RSC runtime (changes per framework update)
8
+ * Tier 3: vendor-app user node_modules (changes on dependency updates)
9
+ * Tier 4: shared-app — small shared app utilities/components (< 5KB source)
10
+ * Tier 5: [route]-* — per-route page/layout chunks (default Rollup splitting)
11
+ *
12
+ * The shared-app tier prevents tiny utility modules (constants, helpers,
13
+ * small UI components) from becoming individual chunks when shared across
14
+ * routes. Without this, Rolldown creates per-module chunks for any code
15
+ * shared between two or more entry points, producing many sub-1KB chunks.
9
16
  *
10
17
  * Server environments (RSC, SSR) are left to Vite's default chunking since
11
18
  * Cloudflare Workers load all code from a single deployment bundle with no
@@ -17,8 +24,8 @@ import type { Plugin } from 'vite';
17
24
  /**
18
25
  * Categorize a module ID into a cache tier chunk name.
19
26
  *
20
- * Returns a chunk name for vendor modules, or undefined to let
21
- * Rollup's default splitting handle app/route code.
27
+ * Returns a chunk name for vendor modules and small shared app code,
28
+ * or undefined to let Rollup's default splitting handle route code.
22
29
  */
23
30
  export declare function assignChunk(id: string): string | undefined;
24
31
  /**
@@ -27,7 +34,11 @@ export declare function assignChunk(id: string): string | undefined;
27
34
  * The RSC plugin creates separate entry points for each 'use client' module,
28
35
  * which manualChunks can't merge. This function is passed as the RSC plugin's
29
36
  * `clientChunks` callback to group timber internals into a single chunk.
30
- * User and third-party client components are left to default per-route splitting.
37
+ *
38
+ * User client components that are small (< 5KB) are grouped into shared-client
39
+ * to prevent thin facade wrappers from becoming individual chunks. This handles
40
+ * the RSC client reference facade problem where each 'use client' module gets
41
+ * a ~100-300 byte re-export wrapper chunk.
31
42
  */
32
43
  export declare function assignClientChunk(meta: {
33
44
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"chunks.d.ts","sourceRoot":"","sources":["../../src/plugins/chunks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoB1D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,MAAM,GAAG,SAAS,CAErB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAoBrC"}
1
+ {"version":3,"file":"chunks.d.ts","sourceRoot":"","sources":["../../src/plugins/chunks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAmGnC;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA4B1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,MAAM,GAAG,SAAS,CAarB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAoBrC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -52,7 +52,7 @@ import { isPageUnloading } from './unload-guard.js';
52
52
  import { NavigationProvider, getNavigationState, setNavigationState } from './navigation-context.js';
53
53
  import { setupServerLogReplay, setupClientErrorForwarding } from './browser-dev.js';
54
54
  import { handleLinkClick, handleLinkHover } from './browser-links.js';
55
- import { TransitionRoot, transitionRender } from './transition-root.js';
55
+ import { TransitionRoot, transitionRender, navigateTransition } from './transition-root.js';
56
56
 
57
57
  // ─── Server Action Dispatch ──────────────────────────────────────
58
58
 
@@ -368,22 +368,12 @@ function bootstrap(runtimeConfig: typeof config): void {
368
368
  },
369
369
 
370
370
  // Render decoded RSC tree via TransitionRoot's state-based mechanism.
371
- // Wraps with NavigationProvider (for atomic useParams/usePathname updates)
372
- // and TimberNuqsAdapter (for nuqs context).
371
+ // Used for non-navigation renders (popstate cached replay, applyRevalidation).
372
+ // Wraps with NavigationProvider + TimberNuqsAdapter.
373
373
  //
374
- // The router calls setNavigationState() before renderRoot(), so
375
- // getNavigationState() returns the new params/pathname. By wrapping
376
- // the element in NavigationProvider here, the context value and the
377
- // RSC tree are passed to startTransition(() => setState()) in the same
378
- // call — making the update atomic. Preserved layout components that call
379
- // useParams() or usePathname() re-render in the same pass as the
380
- // new tree, preventing the dual-active-state flash.
381
- //
382
- // Using transitionRender instead of reactRoot.render() enables
383
- // client-side Suspense deferral: React keeps the old committed tree
384
- // visible while new Suspense boundaries in the navigation resolve.
385
- // This is the client-side equivalent of deferSuspenseFor on the server.
386
- // See design/05-streaming.md.
374
+ // For navigation renders (navigate, refresh, popstate-with-fetch),
375
+ // navigateTransition is used instead it wraps the entire navigation
376
+ // in a React transition with useOptimistic for the pending URL.
387
377
  renderRoot: (element: unknown) => {
388
378
  const navState = getNavigationState();
389
379
  const withNav = createElement(NavigationProvider, { value: navState }, element as React.ReactNode);
@@ -391,6 +381,26 @@ function bootstrap(runtimeConfig: typeof config): void {
391
381
  transitionRender(wrapped);
392
382
  },
393
383
 
384
+ // Run a navigation inside a React transition with optimistic pending URL.
385
+ // The entire fetch + state update runs inside startTransition. useOptimistic
386
+ // shows the pending URL immediately and reverts to null when the transition
387
+ // commits (atomic with the new tree + params).
388
+ //
389
+ // The perform callback receives a wrapPayload function that wraps the
390
+ // decoded RSC payload with NavigationProvider + NuqsAdapter — this must
391
+ // happen inside the transition so the NavigationProvider reads the
392
+ // UPDATED navigation state (set by the router inside perform).
393
+ navigateTransition: (pendingUrl: string, perform) => {
394
+ return navigateTransition(pendingUrl, async () => {
395
+ const payload = await perform((rawPayload: unknown) => {
396
+ const navState = getNavigationState();
397
+ const withNav = createElement(NavigationProvider, { value: navState }, rawPayload as React.ReactNode);
398
+ return createElement(TimberNuqsAdapter, null, withNav);
399
+ });
400
+ return payload as React.ReactNode;
401
+ });
402
+ },
403
+
394
404
  // Schedule a callback after the next paint so scroll operations
395
405
  // happen after React commits the new content to the DOM.
396
406
  // Double-rAF ensures the browser has painted the new frame.
@@ -2,39 +2,29 @@
2
2
 
3
3
  // LinkStatusProvider — client component that provides per-link pending status
4
4
  // via React context. Used inside <Link> to power useLinkStatus().
5
+ //
6
+ // Reads pendingUrl from PendingNavigationContext (provided by TransitionRoot).
7
+ // The pending URL is set as an URGENT update at navigation start (shows
8
+ // immediately) and cleared inside startTransition when the new tree commits
9
+ // (atomic with params/pathname). This eliminates both:
10
+ // 1. The delay before showing the spinner (urgent update, not deferred)
11
+ // 2. The gap between spinner disappearing and active state updating (same commit)
5
12
 
6
- import { useSyncExternalStore, type ReactNode } from 'react';
13
+ import type { ReactNode } from 'react';
7
14
  import { LinkStatusContext, type LinkStatus } from './use-link-status.js';
8
- import { getRouter } from './router-ref.js';
15
+ import { usePendingNavigationUrl } from './pending-navigation-context.js';
9
16
 
10
17
  const NOT_PENDING: LinkStatus = { pending: false };
11
18
  const IS_PENDING: LinkStatus = { pending: true };
12
19
 
13
20
  /**
14
- * Client component that subscribes to the router's pending URL and provides
15
- * a scoped LinkStatusContext to children. Renders no extra DOM — just a
16
- * context provider around children.
21
+ * Client component that reads the pending URL from PendingNavigationContext
22
+ * and provides a scoped LinkStatusContext to children. Renders no extra DOM —
23
+ * just a context provider around children.
17
24
  */
18
25
  export function LinkStatusProvider({ href, children }: { href: string; children: ReactNode }) {
19
- const status = useSyncExternalStore(
20
- (callback) => {
21
- try {
22
- return getRouter().onPendingChange(callback);
23
- } catch {
24
- return () => {};
25
- }
26
- },
27
- () => {
28
- try {
29
- const pendingUrl = getRouter().getPendingUrl();
30
- if (pendingUrl === href) return IS_PENDING;
31
- return NOT_PENDING;
32
- } catch {
33
- return NOT_PENDING;
34
- }
35
- },
36
- () => NOT_PENDING
37
- );
26
+ const pendingUrl = usePendingNavigationUrl();
27
+ const status = pendingUrl === href ? IS_PENDING : NOT_PENDING;
38
28
 
39
29
  return <LinkStatusContext.Provider value={status}>{children}</LinkStatusContext.Provider>;
40
30
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * PendingNavigationContext — React context for the in-flight navigation URL.
3
+ *
4
+ * Provided by TransitionRoot. The value is the URL being navigated to,
5
+ * or null when idle. Used by:
6
+ * - LinkStatusProvider to show per-link pending spinners
7
+ * - useNavigationPending to return a global pending boolean
8
+ *
9
+ * The pending URL is set as an URGENT update (shows immediately) and
10
+ * cleared inside startTransition (commits atomically with the new tree).
11
+ * This ensures pending state appears instantly on navigation start and
12
+ * disappears in the same React commit as the new params/tree.
13
+ *
14
+ * Separate from NavigationContext (which holds params + pathname) because
15
+ * the pending URL is managed as React state in TransitionRoot, while
16
+ * params/pathname are set via module-level state read by renderRoot.
17
+ * Both contexts commit together in the same transition.
18
+ *
19
+ * See design/19-client-navigation.md §"NavigationContext"
20
+ */
21
+
22
+ import React, { createElement, type ReactNode } from 'react';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Lazy context initialization (same pattern as NavigationContext)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ let _context: React.Context<string | null> | undefined;
29
+
30
+ function getOrCreateContext(): React.Context<string | null> | undefined {
31
+ if (_context !== undefined) return _context;
32
+ if (typeof React.createContext === 'function') {
33
+ _context = React.createContext<string | null>(null);
34
+ }
35
+ return _context;
36
+ }
37
+
38
+ /**
39
+ * Read the pending navigation URL from context.
40
+ * Returns null during SSR (no provider) or in the RSC environment.
41
+ * Internal — used by LinkStatusProvider and useNavigationPending.
42
+ */
43
+ export function usePendingNavigationUrl(): string | null {
44
+ const ctx = getOrCreateContext();
45
+ if (!ctx) return null;
46
+ if (typeof React.useContext !== 'function') return null;
47
+ return React.useContext(ctx);
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Provider component
52
+ // ---------------------------------------------------------------------------
53
+
54
+ export function PendingNavigationProvider({
55
+ value,
56
+ children,
57
+ }: {
58
+ value: string | null;
59
+ children?: ReactNode;
60
+ }): React.ReactElement {
61
+ const ctx = getOrCreateContext();
62
+ if (!ctx) {
63
+ return children as React.ReactElement;
64
+ }
65
+ return createElement(ctx.Provider, { value }, children);
66
+ }
@@ -54,6 +54,21 @@ export interface RouterDeps {
54
54
  afterPaint?: (callback: () => void) => void;
55
55
  /** Apply resolved head elements (title, meta tags) to the DOM after navigation. */
56
56
  applyHead?: (elements: HeadElement[]) => void;
57
+ /**
58
+ * Run a navigation inside a React transition with optimistic pending URL.
59
+ * The pending URL shows immediately (useOptimistic urgent update) and
60
+ * reverts when the transition commits (atomic with the new tree).
61
+ *
62
+ * The `perform` callback receives a `wrapPayload` function to wrap the
63
+ * decoded RSC payload with NavigationProvider + NuqsAdapter before
64
+ * TransitionRoot sets it as the new element.
65
+ *
66
+ * If not provided (tests), the router falls back to renderRoot.
67
+ */
68
+ navigateTransition?: (
69
+ pendingUrl: string,
70
+ perform: (wrapPayload: (payload: unknown) => unknown) => Promise<unknown>,
71
+ ) => Promise<void>;
57
72
  }
58
73
 
59
74
  /** Result of fetching an RSC payload — includes head elements and segment metadata. */
@@ -304,6 +319,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
304
319
  if (pending === value && pendingUrl === newPendingUrl) return;
305
320
  pending = value;
306
321
  pendingUrl = newPendingUrl;
322
+ // Notify external store listeners (non-React consumers).
323
+ // React-facing pending state is handled by useOptimistic in
324
+ // TransitionRoot via navigateTransition — not this function.
307
325
  for (const listener of pendingListeners) {
308
326
  listener(value);
309
327
  }
@@ -347,6 +365,31 @@ export function createRouter(deps: RouterDeps): RouterInstance {
347
365
  setNavigationState({ params: resolvedParams, pathname });
348
366
  }
349
367
 
368
+ /**
369
+ * Render a payload via navigateTransition (production) or renderRoot (tests).
370
+ * The perform callback should fetch data, update state, and return the payload.
371
+ * In production, the entire callback runs inside a React transition with
372
+ * useOptimistic for the pending URL. In tests, the payload is rendered directly.
373
+ */
374
+ async function renderViaTransition(
375
+ pendingUrl: string,
376
+ perform: () => Promise<FetchResult>,
377
+ ): Promise<HeadElement[] | null> {
378
+ if (deps.navigateTransition) {
379
+ let headElements: HeadElement[] | null = null;
380
+ await deps.navigateTransition(pendingUrl, async (wrapPayload) => {
381
+ const result = await perform();
382
+ headElements = result.headElements;
383
+ return wrapPayload(result.payload);
384
+ });
385
+ return headElements;
386
+ }
387
+ // Fallback: no transition (tests, no React tree)
388
+ const result = await perform();
389
+ renderPayload(result.payload);
390
+ return result.headElements;
391
+ }
392
+
350
393
  /** Apply head elements (title, meta tags) to the DOM if available. */
351
394
  function applyHead(elements: HeadElement[] | null | undefined): void {
352
395
  if (elements && deps.applyHead) {
@@ -363,6 +406,60 @@ export function createRouter(deps: RouterDeps): RouterInstance {
363
406
  }
364
407
  }
365
408
 
409
+ /**
410
+ * Core navigation logic shared between the transition and fallback paths.
411
+ * Fetches the RSC payload, updates all state, and returns the result.
412
+ */
413
+ async function performNavigationFetch(
414
+ url: string,
415
+ options: { replace: boolean },
416
+ ): Promise<FetchResult> {
417
+ // Check prefetch cache first. PrefetchResult has optional segmentInfo/params
418
+ // fields — normalize to null for FetchResult compatibility.
419
+ const prefetched = prefetchCache.consume(url);
420
+ let result: FetchResult | undefined = prefetched
421
+ ? {
422
+ payload: prefetched.payload,
423
+ headElements: prefetched.headElements,
424
+ segmentInfo: prefetched.segmentInfo ?? null,
425
+ params: prefetched.params ?? null,
426
+ }
427
+ : undefined;
428
+
429
+ if (result === undefined) {
430
+ // Fetch RSC payload with state tree for partial rendering.
431
+ // Send current URL for intercepting route resolution (modal pattern).
432
+ const stateTree = segmentCache.serializeStateTree();
433
+ const rawCurrentUrl = deps.getCurrentUrl();
434
+ const currentUrl = rawCurrentUrl.startsWith('http')
435
+ ? new URL(rawCurrentUrl).pathname
436
+ : new URL(rawCurrentUrl, 'http://localhost').pathname;
437
+ result = await fetchRscPayload(url, deps, stateTree, currentUrl);
438
+ }
439
+
440
+ // Update the browser history — replace mode overwrites the current entry
441
+ if (options.replace) {
442
+ deps.replaceState({ timber: true, scrollY: 0 }, '', url);
443
+ } else {
444
+ deps.pushState({ timber: true, scrollY: 0 }, '', url);
445
+ }
446
+
447
+ // Store the payload in the history stack
448
+ historyStack.push(url, {
449
+ payload: result.payload,
450
+ headElements: result.headElements,
451
+ params: result.params,
452
+ });
453
+
454
+ // Update the segment cache with the new route's segment tree.
455
+ updateSegmentCache(result.segmentInfo);
456
+
457
+ // Update navigation state (params + pathname) before rendering.
458
+ updateNavigationState(result.params, url);
459
+
460
+ return result;
461
+ }
462
+
366
463
  async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {
367
464
  const scroll = options.scroll !== false;
368
465
  const replace = options.replace === true;
@@ -378,54 +475,14 @@ export function createRouter(deps: RouterDeps): RouterInstance {
378
475
  setPending(true, url);
379
476
 
380
477
  try {
381
- // Check prefetch cache first
382
- let result = prefetchCache.consume(url);
383
-
384
- if (result === undefined) {
385
- // Fetch RSC payload with state tree for partial rendering.
386
- // Send current URL for intercepting route resolution (modal pattern).
387
- const stateTree = segmentCache.serializeStateTree();
388
- const rawCurrentUrl = deps.getCurrentUrl();
389
- const currentUrl = rawCurrentUrl.startsWith('http')
390
- ? new URL(rawCurrentUrl).pathname
391
- : new URL(rawCurrentUrl, 'http://localhost').pathname;
392
- result = await fetchRscPayload(url, deps, stateTree, currentUrl);
393
- }
394
-
395
- // Update the browser history — replace mode overwrites the current entry
396
- if (replace) {
397
- deps.replaceState({ timber: true, scrollY: 0 }, '', url);
398
- } else {
399
- deps.pushState({ timber: true, scrollY: 0 }, '', url);
400
- }
401
-
402
- // Store the payload in the history stack
403
- historyStack.push(url, {
404
- payload: result.payload,
405
- headElements: result.headElements,
406
- params: result.params,
407
- });
408
-
409
- // Update the segment cache with the new route's segment tree.
410
- // This must happen before the next navigation so the state tree
411
- // header reflects the currently mounted segments.
412
- updateSegmentCache(result.segmentInfo);
413
-
414
- // Update navigation state (params + pathname) before rendering.
415
- // The renderRoot callback reads this state and wraps the RSC element
416
- // in NavigationProvider — so the context value and the element tree
417
- // are passed to reactRoot.render() in the same call, making the
418
- // update atomic. Preserved layouts see new params in the same render
419
- // pass as the new tree, preventing the dual-active-row flash.
420
- updateNavigationState(result.params, url);
421
- renderPayload(result.payload);
478
+ const headElements = await renderViaTransition(url, () =>
479
+ performNavigationFetch(url, { replace }),
480
+ );
422
481
 
423
482
  // Update document.title and <meta> tags with the new page's metadata
424
- applyHead(result.headElements);
483
+ applyHead(headElements);
425
484
 
426
485
  // Notify nuqs adapter (and any other listeners) that navigation completed.
427
- // The nuqs adapter syncs its searchParams state from window.location.search
428
- // on this event so URL-bound inputs reflect the new URL after navigation.
429
486
  window.dispatchEvent(new Event('timber:navigation-end'));
430
487
 
431
488
  // Scroll-to-top on forward navigation, or restore captured position
@@ -441,17 +498,12 @@ export function createRouter(deps: RouterDeps): RouterInstance {
441
498
  });
442
499
  } catch (error) {
443
500
  // Server-side redirect during RSC fetch → soft router navigation.
444
- // access.ts called redirect() — the server returns X-Timber-Redirect
445
- // header, and fetchRscPayload throws RedirectError. We re-navigate
446
- // to the redirect target using the router for a seamless SPA transition.
447
501
  if (error instanceof RedirectError) {
448
502
  setPending(false);
449
503
  await navigate(error.redirectUrl, { replace: true });
450
504
  return;
451
505
  }
452
- // Abort errors from the fetch (user refreshed or navigated away
453
- // while the RSC payload was loading) are not application errors.
454
- // Swallow them silently — the page is being replaced.
506
+ // Abort errors are not application errors swallow silently.
455
507
  if (isAbortError(error)) return;
456
508
  throw error;
457
509
  } finally {
@@ -465,23 +517,20 @@ export function createRouter(deps: RouterDeps): RouterInstance {
465
517
  setPending(true, currentUrl);
466
518
 
467
519
  try {
468
- // No state tree sent server renders the complete RSC payload
469
- const result = await fetchRscPayload(currentUrl, deps);
470
-
471
- // Update the history entry with the fresh payload
472
- historyStack.push(currentUrl, {
473
- payload: result.payload,
474
- headElements: result.headElements,
475
- params: result.params,
520
+ const headElements = await renderViaTransition(currentUrl, async () => {
521
+ // No state tree sent — server renders the complete RSC payload
522
+ const result = await fetchRscPayload(currentUrl, deps);
523
+ historyStack.push(currentUrl, {
524
+ payload: result.payload,
525
+ headElements: result.headElements,
526
+ params: result.params,
527
+ });
528
+ updateSegmentCache(result.segmentInfo);
529
+ updateNavigationState(result.params, currentUrl);
530
+ return result;
476
531
  });
477
532
 
478
- // Update segment cache with fresh segment info from full render
479
- updateSegmentCache(result.segmentInfo);
480
-
481
- // Atomic update — see navigate() for rationale on NavigationProvider.
482
- updateNavigationState(result.params, currentUrl);
483
- renderPayload(result.payload);
484
- applyHead(result.headElements);
533
+ applyHead(headElements);
485
534
  } finally {
486
535
  setPending(false);
487
536
  }
@@ -509,17 +558,20 @@ export function createRouter(deps: RouterDeps): RouterInstance {
509
558
  // or when the entry doesn't exist at all.
510
559
  setPending(true, url);
511
560
  try {
512
- const stateTree = segmentCache.serializeStateTree();
513
- const result = await fetchRscPayload(url, deps, stateTree);
514
- updateSegmentCache(result.segmentInfo);
515
- updateNavigationState(result.params, url);
516
- historyStack.push(url, {
517
- payload: result.payload,
518
- headElements: result.headElements,
519
- params: result.params,
561
+ const headElements = await renderViaTransition(url, async () => {
562
+ const stateTree = segmentCache.serializeStateTree();
563
+ const result = await fetchRscPayload(url, deps, stateTree);
564
+ updateSegmentCache(result.segmentInfo);
565
+ updateNavigationState(result.params, url);
566
+ historyStack.push(url, {
567
+ payload: result.payload,
568
+ headElements: result.headElements,
569
+ params: result.params,
570
+ });
571
+ return result;
520
572
  });
521
- renderPayload(result.payload);
522
- applyHead(result.headElements);
573
+
574
+ applyHead(headElements);
523
575
  afterPaint(() => {
524
576
  deps.scrollTo(0, scrollY);
525
577
  window.dispatchEvent(new Event('timber:scroll-restored'));