
Save indicators need the written revision
In short: Capture the revision before saving, acknowledge only that snapshot, and control write order so newer edits cannot inherit an older save's success.
javascript ▸ import assert from 'node:assert/strict';
function documentState() {
let text = '', revision = 0, saved = 0, busy = false;
return {
edit(value) { text = value; revision++; },
get dirty() { return revision !== saved; },
async save(write) {
if (busy) throw new Error('save already running');
busy = true;
const snapshot = { text, revision };
try {
await write(snapshot.text);
saved = snapshot.revision;
} finally { busy = false; }
}
};
}
const doc = documentState();
let release, stored;
const gate = new Promise(resolve => { release = resolve; });
doc.edit('first draft');
const saving = doc.save(async text => {
await gate;
stored = text;
});
doc.edit('newer draft');
await assert.rejects(doc.save(async () => {}), /already running/);
release();
await saving;
assert.equal(stored, 'first draft');
assert.equal(doc.dirty, true);
await doc.save(async text => { stored = text; });
assert.equal(stored, 'newer draft');
assert.equal(doc.dirty, false);
console.log('old snapshot saved: still dirty; latest snapshot saved: clean');The save finishes. The editor says everything is saved. But the user typed another sentence while the write was running. In a synthetic Node 22.15.0 check on 21 October 2026, revision 1 completed while revision 2 was still unsaved.
Save indicators need the written revision, not whichever revision happens to exist when the promise resolves. Capture content and revision together before the write; acknowledge that snapshot only after success. Then give overlapping writes an explicit policy. A correct badge cannot compensate for an older write overwriting a newer one.
How Slate tracks the written revision
Jessica Doering's account of building Slate describes this race. Her project is the Rust desktop editor, not the similarly named JavaScript framework. At commit 7ee826d, its document code compares revision with saved_revision; mark_saved receives the revision that was written. Its test includes an intervening edit.
The useful distinction is between finishing an operation and finishing the user's current work. JavaScript has the same opportunity for interleaving: await suspends the surrounding async function, not every caller that can change application state.
Test the save indicator while editing continues
Save this independent adaptation as save-check.mjs and run node save-check.mjs. It needs no packages. A controlled promise holds the writer open; there are no sleeps hoping to trigger the race.
import assert from 'node:assert/strict';
function documentState() {
let text = '', revision = 0, saved = 0, busy = false;
return {
edit(value) { text = value; revision++; },
get dirty() { return revision !== saved; },
async save(write) {
if (busy) throw new Error('save already running');
busy = true;
const snapshot = { text, revision };
try {
await write(snapshot.text);
saved = snapshot.revision;
} finally { busy = false; }
}
};
}
const doc = documentState();
let release, stored;
const gate = new Promise(resolve => { release = resolve; });
doc.edit('first draft');
const saving = doc.save(async text => {
await gate;
stored = text;
});
doc.edit('newer draft');
await assert.rejects(doc.save(async () => {}), /already running/);
release();
await saving;
assert.equal(stored, 'first draft');
assert.equal(doc.dirty, true);
await doc.save(async text => { stored = text; });
assert.equal(stored, 'newer draft');
assert.equal(doc.dirty, false);
console.log('old snapshot saved: still dirty; latest snapshot saved: clean');
This passed locally. The first completed write stored the older string without clearing the newer edit. Saving again stored the current string and cleared the warning. An additional rejection check confirmed that a failed writer leaves the document dirty and permits a subsequent save. These are in-memory tests, not a Rust build or a disk-durability test.
My judgement for an AI-assisted editor
My acceptance rule is to test the promise made by the badge, not just whether the save callback ran. In a hypothetical prompt editor, a user might change an instruction while an earlier version is saving. A success toast for that earlier version must not imply the new instruction will survive closing the tab.
The busy guard deliberately rejects another save until the first settles. A real UI should handle that rejection or disable the save action while retaining editing. Autosave can queue a later snapshot, but it needs an explicit drain policy; this example does not provide one.
Keep the snapshot immutable too. Strings make that straightforward here. A nested mutable object would need a suitable copy or serialisation before dispatch, otherwise the supposedly captured payload could change underneath the writer.
Local revisions do not protect remote writes
The counter belongs to one document instance. It does not coordinate another tab, another client or a server-side transformation. For an HTTP resource, conditional writes using If-Match address a different boundary: rejecting an update when the stored representation no longer matches the expected entity tag. That integration was not tested here.
Also define what resolving write means. An acknowledgement that only queues work is not proof of persistence. This fixture assumes resolution means its snapshot has been stored; production code must obtain that guarantee from the actual storage contract.
What's in it for you
- Add an edit-during-save test before trusting a generated editor's success state.
- Review snapshot acknowledgement and write ordering together, while keeping remote conflict handling separate.
A saved badge is a receipt for a particular snapshot, not a blessing for everything currently on screen.


