Skip to content

breaking: don't abort navigation when calling invalidate(All) during navigation#16188

Open
dummdidumm wants to merge 4 commits into
version-3from
invalidate-navigation-fix
Open

breaking: don't abort navigation when calling invalidate(All) during navigation#16188
dummdidumm wants to merge 4 commits into
version-3from
invalidate-navigation-fix

Conversation

@dummdidumm

Copy link
Copy Markdown
Member

When a navigation starts, invalidate(All) will no longer abort that navigation - previously it indirectly did that because both invalidation and navigation shared the same navigation token; then navigation thought "oh I'm outdated I abort".

Now there's a separate invalidation token so we can handle each case sensibly:

  • when invalidation finishes after goto finishes, it will not apply its stale data. It will apply redirects though since technically it's the last navigation that happened and so it should take precedence
  • when invalidation finishes before goto finishes, it will apply, but it will not abort the navigation

Closes #9354

…g navigation

When a navigation starts, `invalidate(All)` will no longer abort that navigation - previously it indirectly did that because both invalidation and navigation shared the same navigation token; then navigation thought "oh I'm outdated I abort".

Now there's a separate invalidation token so we can handle each case sensibly:
- when invalidation finishes after goto finishes, it will not apply its stale data. It _will_ apply redirects though since technically it's the last navigation that happened and so it should take precedence
- when invalidation finishes before goto finishes, it will apply, but it will not abort the navigation

Closes #9354
@changeset-bot

changeset-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 42ae46b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Major

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

@svelte-docs-bot

Copy link
Copy Markdown

Comment thread packages/kit/src/runtime/client/client.js
vercel Bot and others added 3 commits June 26, 2026 20:53
…ing a concurrent in-flight `invalidate(All)` to be silently aborted even though the navigation never happened.

This commit fixes the issue reported at packages/kit/src/runtime/client/client.js:1778

## Bug

At the top of `navigate()` in `packages/kit/src/runtime/client/client.js`:

```js
const prev_token = navigation_token;
navigation_token = invalidation_token = nav_token;
```

Both `navigation_token` and `invalidation_token` are overwritten with the new navigation's `nav_token`. However, on the block path (when `beforeNavigate` cancels the navigation and `nav` is `null`), only `navigation_token` is rolled back:

```js
if (!nav) {
    block();
    if (navigation_token === nav_token) navigation_token = prev_token;
    return;
}
```

`invalidation_token` is left stuck at the aborted navigation's `nav_token`, and the previous value was never captured so it couldn't be restored.

### Concrete failure trigger

1. `invalidate(All)` runs `_invalidate()`: sets `token = invalidation_token = A`, captures `nav_token = navigation_token (= N0)`, then `await load_route(intent)`.
2. A navigation is triggered. `navigate()` synchronously runs `navigation_token = invalidation_token = B` (the new `nav_token`).
3. A `beforeNavigate` callback cancels it, so `nav` is `null`. `navigation_token` is restored to `N0`, but `invalidation_token` stays `B`.
4. The invalidation resumes and checks `token !== invalidation_token` → `A !== B` is true → it returns early, silently dropping the invalidation result, even though the navigation never actually happened.

This is a regression from the previous single-shared-`token` design, where the block branch restored the token so an in-flight invalidation was not aborted by a navigation that ended up blocked. The doc comment on `invalidation_token` says it is "Superseeded by both later invalidate(All)s and navigations" — but a *blocked* navigation should not supersede it.

## Fix

Capture the previous `invalidation_token` alongside `prev_token`, and restore it on the block path mirroring the `navigation_token` rollback (guarded by `=== nav_token` so a genuinely later `invalidate(All)` that superseded it is not clobbered):

```js
const prev_token = navigation_token;
const prev_invalidation_token = invalidation_token;
navigation_token = invalidation_token = nav_token;
...
if (!nav) {
    block();
    if (navigation_token === nav_token) navigation_token = prev_token;
    if (invalidation_token === nav_token) invalidation_token = prev_invalidation_token;
    return;
}
```

The `=== nav_token` guard ensures that if a newer invalidation or navigation has already taken over `invalidation_token` in the meantime, we don't roll it back.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: dummdidumm <sholthausen@web.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant