You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b37d887
Browse filesBrowse the repository at this point in the historyBrowse files
fix(pages): keep the committed page when a history write fails (#598)
Fixes#562.
A Pages navigation wrote history last. When `pushState` or
`replaceState` threw (Safari's rate limit, a `SecurityError`), the new
page had already taken over, but the address bar still showed the old
URL and the visit never went idle.
```
before after
────── ─────
render B render B
commit B: commit B:
dispose A, hand over Deferred values write history ── throws ─┐
head, title, scroll key → B (nothing else changed) │
write history ── throws dispose B only │
show A again ◄────────────┘
result: page B, URL A, head B, result: page A, URL A, head A,
A's Deferred values gone, visit rejects with the error,
stuck on "loading" navigation goes idle
```
## What changed
```
navigation-engine.ts write history first, right after the #507 guard
on throw: dispose only the new page, rethrow
scroll-restoration.ts take the new scroll key only once the entry exists
client.ts (Vue, Svelte, Solid) put the committed page back
when the last visit to settle finds a failed one on screen
react pages-client.ts on a failed commit: end the visit, dispatch 'fail'
(shows the committed page), reject router.visit()
hash visits a failed write rejects the visit (loading → idle)
instead of throwing out of router.visit()
```
The engine has to write first. Disposing the old page, handing over its
Deferred values and updating the head can't be undone, so a rollback
after the fact isn't possible.
Overlapping visits:
```
B renders ── B's write fails C still loading → B stays until C settles
C commits → C shown (normal)
C fails too → A shown again
hash visit settles while B renders, then B's write fails → A shown again
```
Kept as before: the #507 guard runs first, #506 (history for every visit
that isn't back/forward), and #527/#566 (a same-URL form submission
replaces its entry).
## Tests
- `pages-client.test.ts`, new `failed history writes (#562)` block. It
covers a failed push, a failed replace, a same-URL POST (form state
kept), a working visit after a failure, an older visit failing while a
newer one loads (the newer one commits, or fails too), a hash visit
settling first, a failed hash visit, and the engine called the way React
calls it (a second `commit()` is a no-op, and the old page still gets
its streamed Deferred values).
- `pages-scroll.test.ts`: after a failed write, positions are saved
under the current entry's key.
- `react-pages-client.test.ts` (browser): a Link push and a `replace`
Link whose write fails. The page stays, the URL stays, state goes idle,
and the next click works.
- Mutation checks: each source change was put back to `main` one at a
time (engine, core client, the hash-visit-first branch, hash shell,
scroll key, React adapter). Each one made a test fail.
- `vp run check`, `vp lint`, `vp fmt`, `vp run test` and one full `vp
test run` pass.
## Choices to check
- **React `useForm` has already reported success** when the shell
arrives, before the commit. If the write then fails, the form keeps its
result, the page rolls back, and the error goes to `reportError` so it
isn't dropped silently. Making `useForm` wait for the commit would
change its timing for every submission.
- **The head and title now change after the history entry is written**,
not before. Browsers ignore the `title` argument, so this shouldn't
show.
- **Not in this PR:** Vue, Svelte and Solid `<Form>`/Link don't catch a
rejected visit, so a failed write there is an unhandled rejection (as
with any other visit error today).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
0 commit comments