diff --git a/src/js/app.js b/src/js/app.js index 0a7b4b5..510547f 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -233,39 +233,43 @@ function bindGlobalEvents() { }); } -// ---- LIVE-SYNC (Quick-Save aus dem Background) ---- -// Ein Quick-Save schreibt boards im Background. Ein offener Tab muss das sehen, -// sonst ueberschreibt er den Eintrag beim naechsten eigenen Save (QS-03). -// Drained die Quick-Save-Queue in die Inbox. Die SEITE ist die einzige Schreiberin von 'boards'; -// der Background-Worker haengt nur an 'quicksave_pending' an. Dadurch koennen sich Worker und Seite -// nicht im boards-Array gegenseitig ueberschreiben (Datensicherheit, Phase-4-Review-Blocker 2b). +// ---- QUICK-SAVE PENDING-QUEUE ---- +// Der Background-Worker haengt Quick-Saves an den eigenen Store-Key 'quicksave_pending' an (er +// schreibt NIE boards). Diese Seite ist die einzige boards-Schreiberin und drained die Queue in die +// Inbox. Getrennte Schreib-Domaenen -> Worker und Seite koennen sich nicht im boards-Array +// gegenseitig ueberschreiben (Datensicherheit, Phase-4-Review-Blocker 2b). let _drainBusy = false; +let _drainQueued = false; // ein waehrend eines laufenden Drains angefragter Drain wird nachgeholt async function drainQuickSavePending() { - if (_drainBusy) return; // Re-Entry-Schutz (init + onChanged koennten ueberlappen) + if (_drainBusy) { _drainQueued = true; return; } _drainBusy = true; try { const pending = await Store.get('quicksave_pending'); - if (!Array.isArray(pending) || pending.length === 0) return; - const drained = pending.slice(); - const drainedIds = new Set(drained.map(e => e && e.id).filter(Boolean)); - const inbox = await ensureInboxBoard(); // legt die Inbox an, falls noetig; gibt das Board zurueck - for (const e of drained) { - if (e && typeof e.url === 'string' && e.url) { - inbox.bookmarks.push(normalizeBookmark({ title: e.title, url: e.url })); + if (Array.isArray(pending) && pending.length > 0) { + const drained = pending.slice(); + const drainedIds = new Set(drained.map(e => e && e.id).filter(Boolean)); + const inbox = await ensureInboxBoard(); // legt die Inbox an, falls noetig; gibt das Board zurueck + for (const e of drained) { + if (e && typeof e.url === 'string' && e.url) { + inbox.bookmarks.push(normalizeBookmark({ title: e.title, url: e.url })); + } } + await saveBoards(); + // NUR die verarbeiteten Eintraege entfernen — ein gleichzeitiger Worker-Append bleibt erhalten. + const still = await Store.get('quicksave_pending'); + const remaining = Array.isArray(still) ? still.filter(e => e && !drainedIds.has(e.id)) : []; + await Store.set('quicksave_pending', remaining); + // Render nur, wenn gerade KEIN Drag laeuft (renderBoards->replaceChildren wuerde ihn abreissen). + if (!document.querySelector('.board.dragging, .bm-item.dragging-source')) renderBoards(); } - await saveBoards(); - // NUR die verarbeiteten Eintraege entfernen — ein gleichzeitiger Worker-Append bleibt erhalten. - const still = await Store.get('quicksave_pending'); - const remaining = Array.isArray(still) ? still.filter(e => e && !drainedIds.has(e.id)) : []; - await Store.set('quicksave_pending', remaining); - // Render nur, wenn gerade KEIN Drag laeuft (renderBoards->replaceChildren wuerde ihn abreissen). - if (!document.querySelector('.board.dragging, .bm-item.dragging-source')) renderBoards(); } catch (e) { console.error('Quick-Save-Drain fehlgeschlagen:', e && e.message); } finally { _drainBusy = false; } + // Kam waehrend des Drains ein weiterer Quick-Save an (onChanged wurde durch _drainBusy verworfen), + // jetzt nachholen. Der Eintrag war sicher in der Queue, nur noch nicht eingelesen. + if (_drainQueued) { _drainQueued = false; drainQuickSavePending(); } } // Live-Sync (QS-03): ein offener NewTab drained die Queue, sobald der Worker etwas anhaengt.