// IndexedDB storage layer for Side Notes. // // Schema: // Object store: "notes" // Key path: id (auto-generated) // Indexes: "byKey" (non-unique, on derived URL key) // "byUpdated" (on updatedAt) // // Retrieval always filters `deletedAt == null`, so at most one live note // per URL key is visible even if soft-deleted records coexist. const DB_NAME = 'side-notes'; const DB_VERSION = 1; const STORE_NAME = 'notes'; /** @returns {Promise} */ function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true, }); store.createIndex('byKey', 'key', { unique: false }); store.createIndex('byUpdated', 'updatedAt'); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } /** * Generate a unique id. crypto.randomUUID is available in service workers * and extension pages in Chromium-based browsers. */ function generateId() { return crypto.randomUUID(); } /** * Retrieve the live (non-deleted) note for a given URL key. * @param {string} key * @returns {Promise} */ export async function getNoteByKey(key) { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const index = tx.objectStore(STORE_NAME).index('byKey'); const req = index.openCursor(IDBKeyRange.only(key), 'prev'); req.onsuccess = () => { const cursor = req.result; if (!cursor) return resolve(null); // Skip soft-deleted records; return the first live one (most recent // because we iterate in reverse insertion order via 'prev'). if (cursor.value.deletedAt == null) { resolve(cursor.value); } else { cursor.continue(); } }; req.onerror = () => reject(req.error); }); } /** * Persist (insert or update) a note for a given key. * * The save strategy: look up the existing live note for this key; if one * exists, overwrite it in-place. Otherwise insert a new record. * * @param {string} key * @param {string} content — markdown body * @param {{ pageTitle?: string, favIconUrl?: string }} pageMeta * @returns {Promise} the saved record */ export async function saveNote(key, content, pageMeta = {}) { const db = await openDB(); const now = new Date().toISOString(); // Find the existing live record for this key. const existing = await getNoteByKey(key); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); let req; if (existing) { // Update in-place. const updated = { ...existing, key, content, pageTitle: pageMeta.pageTitle ?? existing.pageTitle ?? '', favIconUrl: pageMeta.favIconUrl ?? existing.favIconUrl ?? '', updatedAt: now, }; req = store.put(updated); } else { // Insert new. const record = { id: generateId(), key, content, pageTitle: pageMeta.pageTitle ?? '', favIconUrl: pageMeta.favIconUrl ?? '', createdAt: now, updatedAt: now, deletedAt: null, }; req = store.add(record); } req.onsuccess = () => { // Return the saved record. const getReq = store.get(req.result); getReq.onsuccess = () => resolve(getReq.result); getReq.onerror = () => reject(getReq.error); }; req.onerror = () => reject(req.error); }); } /** * List all live notes ordered by updatedAt descending. * @returns {Promise} */ export async function listNotes() { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const index = tx.objectStore(STORE_NAME).index('byUpdated'); const req = index.openCursor(null, 'prev'); const results = []; req.onsuccess = () => { const cursor = req.result; if (cursor) { if (cursor.value.deletedAt == null) { results.push(cursor.value); } cursor.continue(); } else { resolve(results); } }; req.onerror = () => reject(req.error); }); } /** * Soft-delete a note by id (sets deletedAt). No-op if already deleted. * @param {string|number} id * @returns {Promise} */ export async function markDeleted(id) { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); const getReq = store.get(id); getReq.onsuccess = () => { const record = getReq.result; if (!record || record.deletedAt != null) { resolve(); return; } record.deletedAt = new Date().toISOString(); store.put(record).onsuccess = () => resolve(); }; getReq.onerror = () => reject(getReq.error); }); }