runSync() captures libraryRef.current at the start (src/App.jsx:351), awaits two network round-trips, then commits the merge with setLibrary(merged) (src/App.jsx:362). Anything typed in between is silently overwritten by the merge.
Narrow, but real:
- It only fires when something actually moved upstream — a quiet sync returns early and commits nothing.
- Both entry points follow a lull. The visibility handler runs when the user has just come back to the tab; the pre-save call runs after the 10s debounce has elapsed.
So it needs a keystroke landing inside a round-trip that was itself triggered by a moment of not typing. Unlikely, not impossible — and the failure is silent, which is what makes it worth fixing rather than documenting.
Shape of a fix
Merge against libraryRef.current at commit time rather than the copy captured before the fetch. doSync() already returns the merged song list separately from the library envelope, so the reconcile could be re-applied per song against whatever state is current when the result lands: songs the sync didn't touch keep the newest local copy, songs it adopted take the server's.
Alternatively, commit through the functional form and re-run the merge inside the updater. Cheaper, but it puts the reconcile in a place React may call twice.
runSync()captureslibraryRef.currentat the start (src/App.jsx:351), awaits two network round-trips, then commits the merge withsetLibrary(merged)(src/App.jsx:362). Anything typed in between is silently overwritten by the merge.Narrow, but real:
So it needs a keystroke landing inside a round-trip that was itself triggered by a moment of not typing. Unlikely, not impossible — and the failure is silent, which is what makes it worth fixing rather than documenting.
Shape of a fix
Merge against
libraryRef.currentat commit time rather than the copy captured before the fetch.doSync()already returns the merged song list separately from the library envelope, so the reconcile could be re-applied per song against whatever state is current when the result lands: songs the sync didn't touch keep the newest local copy, songs it adopted take the server's.Alternatively, commit through the functional form and re-run the merge inside the updater. Cheaper, but it puts the reconcile in a place React may call twice.