Skip to content

fix(router-core): isolate client loading lane ownership#7848

Draft
Sheraff wants to merge 2 commits into
fix-router-core-lane-match-loaderfrom
opencode/match-loading-review
Draft

fix(router-core): isolate client loading lane ownership#7848
Sheraff wants to merge 2 commits into
fix-router-core-lane-match-loaderfrom
opencode/match-loading-review

Conversation

@Sheraff

@Sheraff Sheraff commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

This follow-up makes client loading ownership explicit: foreground transactions, preload lanes, loader flights, background reloads, and HMR no longer borrow authority from one another implicitly. It preserves accepted loader data while preventing stale context, work, and projections from crossing lane boundaries.

Architecture changes and fixed bugs

1. Loader-flight ownership is lane-local

  • Global per-match joining mixed independent lanes. The router-wide _flights registry allowed unrelated navigation, preload, and background lanes to join an individual loader by match id even when their parent generation or complete lane semantics differed. The registry is removed; each loader invocation belongs to one lane, and active sharing happens only through complete-lane adoption.
  • Cancellation and cache cleanup could affect the wrong owner. A lane releasing shared work could abort a loader still represented by an accepted active or cached match. Match-local leases now survive settlement and transfer only when the semantic generation is accepted; losing, trimmed, expired, and cache-rejected matches release exactly their own lease.
  • Adopted controllers had ambiguous lifetime. Superseded claims and fatal post-adoption failures could retain the preload controller, while HMR could release an inherited flight before preload execution acquired it. Controller ownership now transfers directly to the claiming transaction and every unsuccessful path performs one authoritative cleanup.

2. Preloads are shared and claimed as complete lanes

  • Sharing only a descendant loader produced incoherent parent/context generations. Per-loader borrowing could combine cached parent data, active background work, and a child from another lane. _preloads now tracks complete lanes; identical concurrent preloads and navigations adopt the whole serial-plus-loader pipeline or run independently.
  • Match ids alone were not an exact adoption boundary. Lanes with different parsed params or search objects could be treated as compatible despite producing different route semantics. Adoption now requires the same route sequence, match ids/loader deps, exact parsed params, and exact search.
  • Completed preload context leaked into navigation. Cached beforeLoad output could reuse stale authorization or route context even though only loader data is safe to retain. Completed preloads now clear beforeLoad context before cache publication; navigation reruns context while reusing fresh loader data.
  • preload: false could be bypassed by a claim. A navigation could adopt a speculative lane where a required loader had intentionally been skipped. Such lanes are no longer claimable, so navigation performs its blocking load.
  • Failed or terminal preload claims could partially become navigation state. Root global-not-found, failed, redirected, canceled, or otherwise invalid lanes could leave descendants or partial work in the claiming transaction. Rejected adoption discards the donor and reruns the complete navigation lane in blocking mode.
  • Stale cache generations could win a late claim or publication. Cache clearing or replacement after preload planning could still allow old work to publish. Claims validate the captured committed base and cache-array identity, and cache publication uses per-match identity compare-and-swap checks.
  • A superseded navigation could remain blocked on an independent preload. Navigation now races its wait against its own transaction signal, allowing the navigation to settle without canceling unrelated speculative work.

3. Foreground transaction installation preserves the current writer

  • Planning could retire the current transaction before a replacement existed. If matching failed or reentered, the router could lose its authoritative foreground owner. _preflight now guards synchronous planning, while _tx remains current until a successfully planned replacement transaction is installed.
  • Resource handoff was coupled to promise settlement instead of publication authority. Currentness checks now guard commit, view-transition work, and completion publication; a losing lane releases its private matches without mutating committed state.

4. Background reloads publish detached generations

  • Stale projection mutated committed matches before compare-and-swap checks. Async head and scripts hooks ran on objects shared with the committed lane, so a superseded background reload could alter visible metadata even when publication was rejected. Projection now runs on detached, flight-free copies and publishes only while both the owning transaction and exact committed base remain current.
  • Untouched matches gained false fetches during projection-only publication. Detached copies now preserve fetchCount for unchanged matches.
  • Trimmed or rejected background work leaked loader ownership. Failure boundaries clone shared matches before mutation, discarded descendants release private resources, and successful publication transfers loader-flight ownership into the detached projected lane.
  • Successful background reloads redundantly waited for normal route chunks. The committed route chunk is already loaded, so successful background work skips it while error and not-found boundaries still load their required chunks.
  • Projection continued invoking hooks after cancellation. Projection awaits are raced against the lane controller and iteration stops once the lane is aborted.

5. Development route refresh uses the normal transaction protocol

  • HMR eagerly aborted the foreground transaction before rematching succeeded. Refresh now retires preload/cache generations first, plans synchronously with scoped reuse disabled for the changed subtree, and lets ordinary replacement installation retire the previous foreground transaction.
  • Aborted HMR preloads could hang forever in projection or route-chunk readiness. Development readiness and projection waits observe the preload signal, so refresh settles even when obsolete head, scripts, or route-chunk promises never resolve.
  • Old route code could republish through speculative cache work. HMR globally aborts active preload lanes and clears cached leases before rematerializing the changed route and descendants.

6. Exact object equality has an explicit mode

  • Exact lane comparison still ignored undefined keys. The default deep-equality behavior treats missing and undefined keys as equivalent, which is unsafe for parsed params and search lane identity. deepEqual(a, b, false) now requests exact object keys recursively, including keys whose value is undefined.

Documentation

  • Updates the public preloading guide to clarify that completed preload context is rerun for navigation while fresh loader data remains reusable.
  • Expands router-core/INTERNALS.md with the transaction, lane, lease, cache, background, route-chunk, and HMR ownership protocols.

Validation

  • @tanstack/router-core:test:unit: 93 files, 1,337 passed, 3 expected failures
  • @tanstack/router-core:test:types: TypeScript 5.5 through 6.0
  • @tanstack/router-core:test:eslint: passed with 20 existing warnings
  • Full 17-scenario bundle-size build
  • react-router.minimal: gzip 89,443 bytes, matching the base ceiling
  • git diff --check

@github-actions github-actions Bot added documentation Everything documentation related package: router-core labels Jul 18, 2026
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cda8640b-b9f4-40ba-a59d-ad0726f46c16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch opencode/match-loading-review

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 24e3e6a

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 13m 13s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 3s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-18 07:59:39 UTC

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: fbe3b9627d80
  • Measured at: 2026-07-18T07:46:37.241Z
  • Baseline source: history:62a191baa068
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 87.35 KiB 0 B (0.00%) 87.21 KiB 271.61 KiB 75.99 KiB ███▁▁▁▁▁▁▁▁
react-router.full 91.05 KiB -11 B (-0.01%) 90.92 KiB 283.51 KiB 79.13 KiB ███▂▂▂▂▂▂▂▂▁
solid-router.minimal 35.34 KiB -188 B (-0.52%) 35.21 KiB 102.87 KiB 31.85 KiB ███▇▇▇▇▇▇▇▇▁
solid-router.full 40.35 KiB -242 B (-0.58%) 40.22 KiB 117.94 KiB 36.34 KiB ███▇▇▇▇▇▇▇▇▁
vue-router.minimal 52.61 KiB -396 B (-0.73%) 52.48 KiB 146.32 KiB 47.35 KiB ███▇▇▇▇▇▇▇▇▁
vue-router.full 58.55 KiB -396 B (-0.66%) 58.42 KiB 165.08 KiB 52.51 KiB ███▇▇▇▇▇▇▇▇▁
react-start.minimal 101.83 KiB -154 B (-0.15%) 101.70 KiB 319.54 KiB 88.11 KiB ███▇▇▇▇▇▇▇▇▁
react-start.deferred-hydration 102.57 KiB -151 B (-0.14%) 101.72 KiB 320.92 KiB 88.78 KiB ███▇▇▇▇▇▇▇▇▁
react-start.full 105.25 KiB -113 B (-0.10%) 105.11 KiB 329.47 KiB 91.03 KiB ███▅▅▅▅▅▅▅▅▁
react-start.rsbuild.minimal 99.63 KiB -48 B (-0.05%) 99.46 KiB 314.09 KiB 85.82 KiB ███▄▄▄▄▄▄▄▄▁
react-start.rsbuild.minimal-iife 100.04 KiB -43 B (-0.04%) 99.87 KiB 315.03 KiB 86.25 KiB ███▄▄▄▄▄▄▄▄▁
react-start.rsbuild.full 102.87 KiB -28 B (-0.03%) 102.70 KiB 324.15 KiB 88.46 KiB ███▃▃▃▃▃▃▃▃▁
solid-start.minimal 49.20 KiB -421 B (-0.83%) 49.07 KiB 148.53 KiB 43.49 KiB ███▇▇▇▇▇▇▇▇▁
solid-start.deferred-hydration 52.46 KiB -414 B (-0.76%) 49.13 KiB 156.57 KiB 46.42 KiB ███▇▇▇▇▇▇▇▇▁
solid-start.full 55.05 KiB -375 B (-0.66%) 54.92 KiB 165.44 KiB 48.61 KiB ███▇▇▇▇▇▇▇▇▁
vue-start.minimal 70.51 KiB -531 B (-0.73%) 70.38 KiB 203.08 KiB 62.45 KiB ███▇▇▇▇▇▇▇▇▁
vue-start.full 74.51 KiB -536 B (-0.70%) 74.38 KiB 215.71 KiB 65.97 KiB ███▇▇▇▇▇▇▇▇▁

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@pkg-pr-new

pkg-pr-new Bot commented Jul 18, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7848

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7848

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7848

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7848

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7848

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7848

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7848

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7848

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7848

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7848

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7848

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7848

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7848

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7848

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7848

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7848

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7848

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7848

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7848

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7848

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7848

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7848

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7848

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7848

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7848

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7848

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7848

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7848

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7848

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7848

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7848

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7848

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7848

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7848

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7848

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7848

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7848

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7848

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7848

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7848

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7848

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7848

commit: 20906b6

@codspeed-hq

codspeed-hq Bot commented Jul 18, 2026

Copy link
Copy Markdown

Merging this PR will regress 12 benchmarks

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 7 improved benchmarks
❌ 12 regressed benchmarks
✅ 161 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem server error-paths redirect (vue) 436.7 KB 693.3 KB -37.01%
Simulation ssr server-fn POST (solid) 62.8 ms 72.3 ms -13.23%
Simulation ssr server-fn not-found (solid) 60.9 ms 69.5 ms -12.3%
Simulation ssr server-fn send-context (solid) 64.1 ms 70.7 ms -9.28%
Simulation ssr server-fn redirect (solid) 53.5 ms 57.4 ms -6.79%
Simulation ssr server-fn GET (solid) 68.4 ms 73.2 ms -6.54%
Memory mem server streaming-peak chunked (react) 10.9 MB 11.6 MB -5.94%
Memory mem client navigation-churn (react) 714.8 KB 750.3 KB -4.73%
Simulation ssr dehydrate plain control (react) 51.4 ms 53.8 ms -4.51%
Memory mem server error-paths unmatched (react) 298.4 KB 311.8 KB -4.29%
Memory mem server error-paths redirect (solid) 290.6 KB 301.2 KB -3.52%
Memory mem client interrupted-navigations (solid) 837.5 KB 863.6 KB -3.02%
Memory mem server server-fn-churn (vue) 4,319.7 KB 268.3 KB ×16
Memory mem server error-paths unmatched (vue) 2,474.2 KB 490.3 KB ×5
Memory mem server peak-large-page (react) 2,064.6 KB 944.3 KB ×2.2
Memory mem server peak-large-page (solid) 946.7 KB 780 KB +21.37%
Memory mem server error-paths redirect (react) 240.7 KB 224.1 KB +7.4%
Memory mem client preload-churn (solid) 2.2 MB 2.1 MB +5.27%
Memory mem server error-paths not-found (solid) 1.1 MB 1 MB +4.83%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing opencode/match-loading-review (20906b6) with fix-router-core-lane-match-loader (3d68db6)

Open in CodSpeed

@Sheraff Sheraff added the invalid This doesn't seem right label Jul 18, 2026
@Sheraff
Sheraff marked this pull request as draft July 18, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Everything documentation related invalid This doesn't seem right package: router-core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant