diff --git a/src/client/editor/autosave.ts b/src/client/editor/autosave.ts index 6a6b08c8..d568a3c8 100644 --- a/src/client/editor/autosave.ts +++ b/src/client/editor/autosave.ts @@ -72,7 +72,7 @@ export class PageAutosave { this.publish({ remote: page }); return; } - if (this.changed || this.state.status === 'conflict') { + if (this.dirty) { clearTimeout(this.timer); this.publish({ remote: page, diff --git a/tests/autosave.test.ts b/tests/autosave.test.ts index 3ce66931..7bc8d78f 100644 --- a/tests/autosave.test.ts +++ b/tests/autosave.test.ts @@ -154,3 +154,38 @@ it('keeps navigation guarded when typing reverts to the old content during a sav expect(autosave.getSnapshot().status).toBe('saved'); autosave.dispose(); }); +it('preserves a reverted draft when polling finds a save whose response was lost', async () => { + vi.useFakeTimers(); + const pending = deferred(); + const save = vi.fn(() => pending.promise); + const autosave = new PageAutosave(save); + autosave.receive(page); + autosave.edit({ content: 'Temporary' }); + await vi.advanceTimersByTimeAsync(800); + autosave.edit({ content: 'Original' }); + pending.reject(new Error('Connection lost after the server committed')); + await vi.advanceTimersByTimeAsync(0); + expect(autosave.getSnapshot()).toMatchObject({ + status: 'error', + draft: { content: 'Original' }, + }); + + autosave.receive({ ...page, content: 'Temporary', revision: 2 }); + expect(autosave.getSnapshot()).toMatchObject({ + status: 'conflict', + draft: { content: 'Original' }, + remote: { content: 'Temporary', revision: 2 }, + }); + expect(autosave.dirty).toBe(true); + await vi.advanceTimersByTimeAsync(2000); + expect(save).toHaveBeenCalledTimes(1); + + autosave.useLatest(); + expect(autosave.getSnapshot()).toMatchObject({ + status: 'saved', + draft: { content: 'Temporary' }, + page: { revision: 2 }, + }); + expect(autosave.dirty).toBe(false); + autosave.dispose(); +});