mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 20:06:57 +02:00
fix: close note-switching race conditions and restore offline-first note open
Three issues found in review, all around switching to a different note while the editor is already open (via a note-link or backlink chip): - TiptapEditor's onTransaction/onUpdate had no guard against writing state after the component is torn down. Removing a focused editor's DOM node fires a native blur, which ProseMirror turns into one last transaction synchronously during that same teardown pass, crashing with state_unsafe_mutation. Deferred the onTransaction write to a microtask (checked against a destroyed flag) so it lands after Svelte's current pass finishes; guarded onUpdate synchronously. - openEditor/openNoteById in NotesView.svelte awaited a network fetch before ever showing the editor, for every note open (not just link-driven navigation) — a regression for an offline-first app, and the source of a real race: two rapid note-link clicks left two fetches in flight with no ordering guarantee, so whichever resolved last won, not whichever was clicked last. Reverted to opening instantly from local/IndexedDB data (with a request-id guard against the residual race), and instead fetch backlinks lazily in NoteEditor.svelte only if not already present — mirroring the existing attachments-fetch pattern already in that file. Also applied the same post-teardown guard to that pre-existing attachments effect, since remounting on note-switch newly exposes it to the same class of race. - FormattingToolbar's closeDropdowns() cleared search results but never cancelled the pending debounce timer, so a stale query could repopulate results after the dropdown had already closed. Added the missing clearTimeout, plus a try/catch around the search fetch for consistency with the pattern used elsewhere in the app. Verified: pnpm check (0 errors), full unit suite (316/316), and the existing e2e suite including all note-linking scenarios (19/19) all pass against these changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Fred Cabassut
co-authored by
Claude Sonnet 5
parent
534d84224d
commit
3eb8d2eeab
@@ -32,18 +32,8 @@
|
||||
// Use SvelteKit's replaceState (not the raw history API) so the router keeps
|
||||
// ownership of the entry — NoteEditor's close-on-back relies on popstate being
|
||||
// handled by SvelteKit.
|
||||
async function openEditor(note: Note) {
|
||||
try {
|
||||
const res = await fetch(`/api/notes/${note.id}`);
|
||||
if (res.ok) {
|
||||
const full = await res.json();
|
||||
editingNote = { ...note, backlinks: full.backlinks ?? [] };
|
||||
} else {
|
||||
editingNote = note;
|
||||
}
|
||||
} catch {
|
||||
editingNote = note;
|
||||
}
|
||||
function openEditor(note: Note) {
|
||||
editingNote = note;
|
||||
replaceState(`#${note.id}`, {});
|
||||
}
|
||||
|
||||
@@ -54,25 +44,18 @@
|
||||
replaceState(location.pathname, {});
|
||||
}
|
||||
|
||||
async function openNoteById(noteId: string) {
|
||||
showNewNote = false;
|
||||
newNoteChecklist = false;
|
||||
editingNote = null;
|
||||
// Reads from the local IndexedDB cache rather than fetching over the network —
|
||||
// this keeps note-link navigation instant and offline-safe (matching how the
|
||||
// rest of the app opens notes), and backlinks are filled in progressively by
|
||||
// NoteEditor's own fetch-if-not-pre-populated effect once the editor is open,
|
||||
// the same pattern already used there for attachments.
|
||||
let openNoteRequestId = 0;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/notes/${noteId}`);
|
||||
if (res.ok) {
|
||||
const full = await res.json();
|
||||
editingNote = full;
|
||||
replaceState(`#${noteId}`, {});
|
||||
}
|
||||
} catch {
|
||||
const note = await getIdbNote(noteId);
|
||||
if (note) {
|
||||
editingNote = note;
|
||||
replaceState(`#${noteId}`, {});
|
||||
}
|
||||
}
|
||||
async function openNoteById(noteId: string) {
|
||||
const requestId = ++openNoteRequestId;
|
||||
const note = await getIdbNote(noteId);
|
||||
if (requestId !== openNoteRequestId) return; // a newer navigation superseded this one
|
||||
if (note) openEditor(note);
|
||||
}
|
||||
|
||||
function handleReorder(noteIds: string[]) {
|
||||
@@ -173,5 +156,13 @@
|
||||
{/if}
|
||||
|
||||
{#if editingNote}
|
||||
<NoteEditor note={editingNote} onClose={closeEditor} onOpenNote={openNoteById} />
|
||||
<!-- Forces a full destroy+remount on note-switch (e.g. clicking a note-link
|
||||
while the editor is already open) instead of reusing the same instance
|
||||
with a new `note` prop — a persisting instance's own back-navigation
|
||||
effect misreads `openEditor`'s replaceState call as "browser back was
|
||||
pressed" and closes the editor, and its once-only local state never
|
||||
resyncs to the new note otherwise. -->
|
||||
{#key editingNote.id}
|
||||
<NoteEditor note={editingNote} onClose={closeEditor} onOpenNote={openNoteById} />
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user