fix(quick-save): Pending-Queue-Redesign (Blocker 2b) — Worker schreibt eigenen 'quicksave_pending'-Key statt boards, Seite drained in die Inbox; getrennte Schreib-Domaenen, kein boards-Clobber

This commit is contained in:
2026-06-14 14:27:31 +02:00
parent 4897781848
commit 43403bc755
3 changed files with 51 additions and 24 deletions
+37 -11
View File
@@ -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();
});
}