Skip to content

fix: Note content corruption on restart#3

Open
rgcr wants to merge 1 commit into
jaesuny:mainfrom
rgcr:fix/debounce-note-id-race
Open

fix: Note content corruption on restart#3
rgcr wants to merge 1 commit into
jaesuny:mainfrom
rgcr:fix/debounce-note-id-race

Conversation

@rgcr
Copy link
Copy Markdown

@rgcr rgcr commented Mar 23, 2026

Problem

After an OS restart (or app relaunch), inactive notes have their content replaced with the active note's content. The first note's content gets replicated to the others.

Root Cause

The contentChanged debounce timer (300ms in JS) captures currentNoteId at fire time, not at schedule time. When a note switch occurs between scheduling and firing:

  1. User edits note A → debounce timer starts (300ms)
  2. User clicks note B → Swift queues: serializeState(), setCurrentNoteId('B'), restoreState(B)
  3. JS executes setCurrentNoteId('B')currentNoteId = B
  4. Debounce fires → sends A's content with noteId = B → overwrites note B with A's content
  5. restoreState(B) executes → clears debounce (too late), loads B's correct content from stateCache

The corruption is invisible during the session because the in-memory stateCache still holds B's correct content. But on next launch (or after OS restart), stateCache is empty and B loads from the corrupted persisted data — showing A's content.

Fix

Clear the debounce timer in setCurrentNoteId(). This is safe because serializeState()cacheSerializedState() already persists the old note's content before setCurrentNoteId is called in every note-switch path.

window.setCurrentNoteId = function (id) {
  clearTimeout(debounceTimer);
  currentNoteId = id;
};

Changed files

  • editor-web/src/editor.js — 1 line added in setCurrentNoteId()

@rgcr rgcr changed the title fix: Clear contentChanged debounce on note switch to prevent content corruption fix: Note content corruption on restart Mar 23, 2026
…corruption

The contentChanged debounce timer (300ms) captures currentNoteId at fire time,
not at schedule time. When a note switch occurs between scheduling and firing:

1. User edits note A → debounce timer starts
2. User clicks note B → Swift queues: serializeState(), setCurrentNoteId('B'), restoreState(B)
3. JS executes setCurrentNoteId('B') → currentNoteId = B
4. Debounce fires → sends A's content with noteId=B → overwrites B with A's content
5. restoreState(B) executes → clears debounce (too late)

The corruption is invisible during the session because the in-memory stateCache
still holds B's correct content. But on next launch (or after OS restart), the
stateCache is empty and B loads from the corrupted persisted data.

Fix: Clear the debounce timer in setCurrentNoteId(). This is safe because
serializeState() → cacheSerializedState() already persists the old note's
content before setCurrentNoteId is called in every note-switch path.
@rgcr rgcr force-pushed the fix/debounce-note-id-race branch 2 times, most recently from f4027ea to 227aeb5 Compare March 23, 2026 16:51
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