// Lokaler Speicher auf Basis von IndexedDB. // // Warum nicht localStorage: Der ist synchron (blockiert die Oberfläche), // auf wenige Megabyte begrenzt und kennt keine Transaktionen. Für eine // Warteschlange, die auch einen Absturz überleben muss, ist er ungeeignet. "use strict"; const DB_NAME = "einkaufsapp"; const DB_VERSION = 1; const STORE_CACHE = "cache"; // Schlüssel -> zuletzt gesehener Serverstand const STORE_OUTBOX = "outbox"; // noch nicht bestätigte Operationen let dbPromise = null; function open() { if (dbPromise) return dbPromise; dbPromise = new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains(STORE_CACHE)) { db.createObjectStore(STORE_CACHE); } if (!db.objectStoreNames.contains(STORE_OUTBOX)) { const store = db.createObjectStore(STORE_OUTBOX, { keyPath: "op_id" }); // Reihenfolge je Liste: Operationen müssen in der Folge ankommen, // in der sie entstanden sind. store.createIndex("by_list_seq", ["list_id", "seq"]); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); request.onblocked = () => reject(new Error("Datenbank blockiert – bitte andere Tabs schließen.")); }); return dbPromise; } function run(storeName, mode, fn) { return open().then((db) => new Promise((resolve, reject) => { const tx = db.transaction(storeName, mode); const store = tx.objectStore(storeName); let result; try { result = fn(store); } catch (err) { reject(err); return; } tx.oncomplete = () => resolve(result && result.__req ? result.__req.result : result); tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error || new Error("Transaktion abgebrochen")); })); } function wrap(request) { return { __req: request }; } // --------------------------------------------------------------------------- // Zwischenspeicher // --------------------------------------------------------------------------- export function cacheGet(key) { return run(STORE_CACHE, "readonly", (store) => wrap(store.get(key))); } export function cacheSet(key, value) { return run(STORE_CACHE, "readwrite", (store) => wrap(store.put(value, key))); } export function cacheDelete(key) { return run(STORE_CACHE, "readwrite", (store) => wrap(store.delete(key))); } // --------------------------------------------------------------------------- // Outbox // --------------------------------------------------------------------------- /** Zufällige, praktisch eindeutige Kennung. crypto.randomUUID gibt es * nur in sicheren Kontexten (HTTPS oder localhost) - deshalb der * Rückfallweg. */ export function newOpId() { if (globalThis.crypto?.randomUUID) return crypto.randomUUID(); const bytes = new Uint8Array(16); (globalThis.crypto || {}).getRandomValues?.(bytes); return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("") || `${Date.now()}-${Math.random().toString(16).slice(2)}`; } let seqCounter = Date.now(); export function enqueue(listId, kind, payload) { const op = { op_id: newOpId(), list_id: listId, kind, payload, seq: seqCounter++, created_at: new Date().toISOString(), attempts: 0, }; return run(STORE_OUTBOX, "readwrite", (store) => { store.add(op); return op; }); } export function pending(listId) { return run(STORE_OUTBOX, "readonly", (store) => { const index = store.index("by_list_seq"); const range = IDBKeyRange.bound([listId, -Infinity], [listId, Infinity]); return wrap(index.getAll(range)); }); } export function pendingAll() { return run(STORE_OUTBOX, "readonly", (store) => wrap(store.getAll())); } export function remove(opIds) { return run(STORE_OUTBOX, "readwrite", (store) => { for (const id of opIds) store.delete(id); }); } export function bumpAttempts(opIds) { return run(STORE_OUTBOX, "readwrite", (store) => { for (const id of opIds) { const request = store.get(id); request.onsuccess = () => { const op = request.result; if (op) { op.attempts = (op.attempts || 0) + 1; op.last_error_at = new Date().toISOString(); store.put(op); } }; } }); } export async function clearList(listId) { const ops = await pending(listId); await remove(ops.map((o) => o.op_id)); }