Conversation
|
Install the latest version of pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/a83ab3a1c1bf7ef7124da07800468dba0a285a57Open in Note This PR is from a fork. A maintainer must approve approve each commit before it can be built and installed. |
🦋 Changeset detectedLatest commit: a83ab3a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
…unning afterNavigate callbacks
khughitt
marked this pull request as ready for review
October 2, 2026 10:14
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
navigate()checksnavigation_tokenonce its route has loaded, then awaitsonNavigatecallbacks and commits without checking again. When a callback returns a promise (the view transition recipe from the docs holds the navigation until the transition's update callback runs), a newer navigation can start during that wait, and the superseded one still setscurrentand applies its render tree when its callbacks settle. This adds the same token check after theonNavigatecallbacks as the one after load.With
experimental.forkPreloads,navigate()has already taken the preload fork out ofload_cacheat that point, so nothing else can reach it. The new abort path therefore discards the fork itself, the same waydiscard_load_cache()does.Functions returned by
onNavigatemust also not outlive an aborted navigation, or they run when the next navigation completes.run_on_navigate_callbacks()used to register them before the check. It now returns them, and the caller registers them only if the navigation is still current.finish_navigation()also receives them and removes them when it aborts, which covers a navigation superseded while its render settles. A newer navigation can finish before that abort runs (a shallowgoto()doesn't wait for the older render), so a navigation that finishes first removes any other navigation's registrations before it runsafterNavigatecallbacks. Only navigations it superseded can have any, since registering requires the navigation to still be current. They stay registered before the commit, so they still run in the same order relative to otherafterNavigatecallbacks. Each registration wraps the returned functions in entries of its own.after_navigate_callbacksis aSet, so if two navigations'onNavigatecalls returned the same function object, removing the aborted navigation's registration would otherwise remove the newer navigation's too.A shallow
goto()has the same gap inupdate_state(): superseded whileonNavigatewas pending, it still applied its page state and registered its functions. It now gets the same check. As with a full navigation aborted at that point, its history entry has already been pushed and stays; the check only stops the page update and the registration.On 3.x the newer navigation still commits afterwards, so the result is transient: the superseded page mounts, renders and runs its effects, and its
onNavigatereturn value is registered as anafterNavigatecallback, just before the newest navigation replaces it.On 2.x (
version-2) the same gap leaves the wrong page in place. There, a navigation only sends thedata_Nprops whose node data differs fromcurrentat load time. If a superseded navigation commits between a newer navigation's load and its commit, the newer navigation's props are diffed against a stalecurrent: it commits withoutdata_Nfor a page whose data didn't change relative to thatcurrent. The URL then shows the newest route while the page keeps the superseded route's data. We hit this with rapid link clicks and Back under view transitions. I'm happy to open a backport againstversion-2if that's wanted.This replaces #17200, which targeted
version-3and was closed when that branch merged intomain. The change is the same, applied tomain. No existing issue covers this. #12809 involved a pendingonNavigatetoo, but that was a different symptom.Test
navigation-lifecycle/on-navigate-superseded/[id]holds every navigation inonNavigate. The test starts a navigation tob, then a newer one back toa, releases the superseded one first, and asserts thatbnever renders and that only the committed navigation'sonNavigatereturn value runs. It fails onmainwithout the change (Received: "b"), fails without the registration change (the aborted navigation's function runs), and passes 20/20 in dev and build with both. A second case runs the same sequence with a shallowgoto()as the superseded navigation. Without the change, the superseded navigation's state is applied (Received: "active").In
async, which enablesforkPreloads,fork/supersededpreloads a page that subscribes to a store counting its subscribers. The navigation to that page is held inonNavigateand superseded, and the test asserts that releasing it drops the count back to 0, meaning the fork was discarded. Without the discard it fails (Received: 1), and it passes 20/20 in dev and build with it.Also in
async,navigation-settleholds a page's render with a top-levelawait, and a newer navigation starts before it settles. The test asserts that only the newer navigation'sonNavigatereturn value runs. Without removing them on that abort it fails (the held navigation's function runs), and it passes 20/20 in dev and build with it. The page also returns one shared function object from every navigation, and the test asserts that the newer navigation still runs it. Without the per-registration entries, the aborted navigation's cleanup removes it. A second case supersedes the held navigation with a shallowgoto(), which finishes before the held render settles. Without removing other navigations' registrations before dispatch, the held navigation's function runs andsharedruns twice.On
main, all five new tests fail without the change, and each passes 10/10 in dev and build with it (the original four tests also passed 20/20 each onversion-3before).Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
Tests
pnpm testand lint the project withpnpm lintandpnpm checkRan
pnpm format,pnpm lint,pnpm -F @sveltejs/kit test:unit, andpnpm checkinpackages/kit,basicsandasync(all clean), plus thebasics,optionsandasyncPlaywright suites on Chromium in dev and build.options,asyncandbasicsbuild pass. Inbasicsdev, a fewLoad,SPA mode,data-sveltekit-preload-codeandINPtests fail under the full suite's load, and they did so before this change too. They pass when rerun on their own. I didn't run the fullpnpm test.Changesets
pnpm changesetand following the prompts. Changesets that add features should beminorand those that fix bugs should bepatch. Please prefix changeset messages withfeat:,fix:, orchore:.Edits