fix(sections-editor): don't mistake a stale read of your own saves for someone else's change
What is this contribution about?
Since #7918, editors on a large site report that the CMS shows "Changed while you were editing: countdowns. The latest version of those fields was kept, and the rest of your changes were saved." Their last edits then disappear and have to be redone. Nobody else was editing the block.
What happened
I traced it in one draft's push history, a project with about 2,000 blocks. The editor deleted two items from an array, then renamed an item. Two saves later, each of those edits was undone. Each of the reverting commits is byte-for-byte identical to an earlier save of the same editor:
| Commit | Content |
|---|---|
| save 1 | 9 items |
| save 2 | 8 items (deleted one) |
| save 3 | 7 items (deleted another) |
| save 4 | 9 items, identical to save 1 |
The editor's local copy of the block went back to one of its own earlier versions. Right after a commit, a re-read can still answer with the branch's previous head for a few seconds (provider read-after-write lag, or a response that lands out of order). From there:
- The next autosave compares the form with that older copy, decides someone else changed the block, and merges with the stored version preferred (#7918). The result is the old version. The form shows it, along with the warning.
- The server refuses the write, because the base it names no longer matches the head.
- The retry sends what the form now shows, the old version, against the correct base. The server accepts it, and the edit is reverted on the branch.
Nobody else touched the block in that window. The "conflict" is the editor disagreeing with its own past.
The fix
- The section form knows its own past (
form-base.ts,sections-editor.tsx). It records every version it started from or saved, up to 50, as canonical JSON. A read that returns one of them is treated as an echo, not as someone else's change: no merge, no warning. The save goes ahead with the base the form actually built on. The server's version check (#7909) still refuses it if the branch really moved on. useSaveBlockdoes the same for its own saves (use-save-block.ts). When the re-read after a refused save returns a version this tab saved, it waits 1.5 s and sends the same edit again instead of merging. This covers page name, path, SEO and matcher saves.- Back off before giving up (
sections-editor.tsx). A save refused again is retried up to 3 times (1 s, 2 s, 4 s) before the form reloads. Before, the form reloaded after the second refusal, and while the provider was still catching up that reload could show the older copy.
Real conflicts behave as before. A version that is not one of the form's own still goes through the merge and the warning from #7918.
How did you verify your code works?
bun run verify(fmt, oxlint, knip, 11,002 unit tests) andbun run checkpass.- Unit (
form-base.test.ts): own versions include the starting version and every saved one, and key order is ignored. - New browser test in
site-editor-stale-form.spec.ts, reproducing the reported sequence:- Open a block and save "First edit".
- The next save is refused once, and the re-read that follows returns the block as it was when the form opened (served through
page.route). - Type "Second edit".
- On
main: the stored headline ends at "First edit". The second edit is lost. - With this change: "Second edit" is stored, the form shows it, and no warning appears.
- Related specs (
site-editor-stale-form,decofile-api,fast-preview-git-sync,site-editor-breadcrumbs,cms-publish-surface): 34 passed locally. - Not run: the full e2e suite (CI will run it).
Screenshots/Demonstration
No visual change. The bug was a warning that should not have appeared, followed by lost edits. With the fix, no warning appears in that sequence and the edits stay.
How to Test
Hard to trigger by hand, because it needs the provider to lag right after a commit. The e2e test above stubs exactly that. On a large site:
- Open a block with an array field and delete two items, a few seconds apart.
- Rename another item.
- Expected: every edit stays, with no "Changed while you were editing" warning.
Migration Notes
None.
Review Checklist
- PR title is clear and descriptive
- Changes are tested and working
- Documentation is updated (if needed)
- No breaking changes
Summary by cubic
Fixes a false conflict in the CMS where a stale re-read of your own saves reverted your edits. Right after a save, the provider could answer with the branch's previous head for a few seconds; the editor treated that older copy as someone else's change, merged it in, and the retry wrote it back, losing your last edits. Now the form and useSaveBlock keep a history of versions they started from or saved, treat a read matching one of those as an echo, and retry refused saves with backoff (up to 3 times) before reloading. Real conflicts still go through the existing merge and warning.
- The section form records up to 50 of its own versions; a read matching one is an echo, not a conflict, and the save proceeds with its original base while the server's version check still guards it.
useSaveBlockwaits 1.5 s and resends the same edit when the re-read after a refused save returns a version this tab saved.- New e2e test reproduces the reported sequence and verifies both edits persist with no warning.