diff --git a/src/js/app.js b/src/js/app.js index c9a7f8f..0a7b4b5 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -27,6 +27,7 @@ async function init() { bindGlobalEvents(); bindSettingsEvents(); bindStorageSync(); + await drainQuickSavePending(); // beim Start angesammelte Quick-Saves (kein Tab war offen) einlesen initSearch(); initPalette(); await migrateSticky(); @@ -235,20 +236,45 @@ 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). +let _drainBusy = false; +async function drainQuickSavePending() { + if (_drainBusy) return; // Re-Entry-Schutz (init + onChanged koennten ueberlappen) + _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 })); + } + } + 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; + } +} + +// Live-Sync (QS-03): ein offener NewTab drained die Queue, sobald der Worker etwas anhaengt. function bindStorageSync() { if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.onChanged) return; chrome.storage.onChanged.addListener((changes, area) => { - if (area !== 'local' || !changes.boards) return; - const next = changes.boards.newValue; - if (!Array.isArray(next)) return; - // Guard (W-c, nach Phase-4-Review auf REALE Klassen korrigiert): nicht mitten in einer offenen - // Interaktion das boards-Array ersetzen und neu rendern, sonst verwaist eine per-Closure gehaltene - // board-Referenz oder ein laufender Drag/Render wird abgerissen. Abgedeckt: Settings (.panel-overlay), - // Modals Add-Board/Add-Bookmark/Rename (.modal-overlay), HellionDialog/Onboarding (.dialog-overlay), - // Board-Drag (.board.dragging), Bookmark-Drag (.bm-item.dragging-source). - if (document.querySelector('.panel-overlay.active, .modal-overlay.active, .dialog-overlay.active, .board.dragging, .bm-item.dragging-source')) return; - boards = next; - renderBoards(); + // Nur auf die Quick-Save-Queue reagieren — 'boards' schreibt ausschliesslich diese Seite. + if (area !== 'local' || !changes.quicksave_pending) return; + drainQuickSavePending(); }); } diff --git a/src/js/background.js b/src/js/background.js index 8ac2673..d66236a 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -46,7 +46,10 @@ function flashBadge(text, color) { // Interne/nicht speicherbare Seiten (Browser-UI, Extension-Seiten) — kein sinnvolles Bookmark. const UNSAVEABLE_URL = /^(chrome|chrome-extension|about|edge|opera|moz-extension|brave|vivaldi|view-source|devtools):/i; -// Quick-Save: aktiven Tab lesen, in die Inbox haengen (read-modify-write). +// Quick-Save: aktiven Tab in die Pending-Queue haengen — NICHT boards schreiben. +// Datensicherheit (Phase-4-Review 2b): boards schreibt ausschliesslich die NewTab-Seite. Der Worker +// haengt nur an 'quicksave_pending' an; die Seite drained die Queue in die Inbox. So koennen Worker +// und Seite sich nicht im boards-Array gegenseitig ueberschreiben (kein Lost-Update bestehender Daten). async function quickSaveActiveTab() { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tab = tabs && tabs[0]; @@ -56,13 +59,11 @@ async function quickSaveActiveTab() { return; } - // read-modify-write: aktuellen Stand frisch aus Storage holen, NICHT blind setzen. - const boards = (await bgGet('boards')) ?? []; - const inbox = ensureInbox(boards); - inbox.bookmarks.push(normalizeBookmark({ title: tab.title || tab.url, url: tab.url })); - try { - await bgSet('boards', boards); + // read-modify-write nur auf der EIGENEN Queue (bgGet/bgSet sind via quickSaveChain serialisiert). + const pending = (await bgGet('quicksave_pending')) ?? []; + pending.push({ id: uid(), title: tab.title || tab.url, url: tab.url }); + await bgSet('quicksave_pending', pending); flashBadge(chrome.i18n.getMessage('quickSaveBadge')); } catch (e) { // Quota o.ae.: Badge zeigt nichts Gruenes, Fehler in die Worker-Konsole. diff --git a/src/js/opera/background.js b/src/js/opera/background.js index 8498d56..bcae82e 100644 --- a/src/js/opera/background.js +++ b/src/js/opera/background.js @@ -60,12 +60,12 @@ function quickSaveActiveTab() { qsBusy = false; return; } - // read-modify-write: aktuellen boards-Stand frisch holen, anhaengen, zurueckschreiben. - chrome.storage.local.get(['boards'], (r) => { - const boards = r.boards ?? []; - const inbox = ensureInbox(boards); - inbox.bookmarks.push(normalizeBookmark({ title: tab.title || tab.url, url: tab.url })); - chrome.storage.local.set({ boards }, () => { + // Datensicher: NICHT boards schreiben — nur an die Pending-Queue anhaengen (die Seite + // drained sie in die Inbox). So kann der Worker das boards-Array nicht clobbern (Review 2b). + chrome.storage.local.get(['quicksave_pending'], (r) => { + const pending = Array.isArray(r.quicksave_pending) ? r.quicksave_pending : []; + pending.push({ id: uid(), title: tab.title || tab.url, url: tab.url }); + chrome.storage.local.set({ quicksave_pending: pending }, () => { if (chrome.runtime.lastError) { console.error('Quick-Save fehlgeschlagen:', chrome.runtime.lastError.message); qsBusy = false;