/** * InkNote Offline Store — IndexedDB local-first storage * * All reads/writes go here first (instant, works offline). * Sync engine flushes changes to server when online. */ const DB_NAME = 'inknote'; const DB_VERSION = 2; let db = null; function open() { return new Promise((resolve, reject) => { if (db) return resolve(db); const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = (e) => { const d = e.target.result; // Notebooks store if (!d.objectStoreNames.contains('notebooks')) { const nb = d.createObjectStore('notebooks', { keyPath: 'id' }); nb.createIndex('sortOrder', 'sortOrder'); } // Notes metadata store if (!d.objectStoreNames.contains('notes')) { const n = d.createObjectStore('notes', { keyPath: 'id' }); n.createIndex('folderId', 'folderId'); n.createIndex('trashed', 'trashed'); n.createIndex('updatedAt', 'updatedAt'); } // Note content store (separate for lazy loading) if (!d.objectStoreNames.contains('noteContents')) { d.createObjectStore('noteContents', { keyPath: 'id' }); } // Sync queue — pending changes to push to server if (!d.objectStoreNames.contains('syncQueue')) { const sq = d.createObjectStore('syncQueue', { keyPath: 'queueId', autoIncrement: true }); sq.createIndex('entityId', 'entityId'); sq.createIndex('timestamp', 'timestamp'); } // Key-value meta store (token, user, last sync time, etc) if (!d.objectStoreNames.contains('meta')) { d.createObjectStore('meta', { keyPath: 'key' }); } }; req.onsuccess = (e) => { db = e.target.result; resolve(db); }; req.onerror = (e) => reject(e.target.error); }); } // ─── Generic helpers ─── async function tx(stores, mode, fn) { const d = await open(); return new Promise((resolve, reject) => { const t = d.transaction(stores, mode); const result = fn(t); t.oncomplete = () => resolve(result._value); t.onerror = (e) => reject(e.target.error); }); } function reqToPromise(req) { return new Promise((res, rej) => { req.onsuccess = () => res(req.result); req.onerror = (e) => rej(e.target.error); }); } // ─── Meta (key-value) ─── export async function getMeta(key) { const d = await open(); const r = await reqToPromise(d.transaction('meta', 'readonly').objectStore('meta').get(key)); return r?.value ?? null; } export async function setMeta(key, value) { const d = await open(); await reqToPromise(d.transaction('meta', 'readwrite').objectStore('meta').put({ key, value })); } export async function deleteMeta(key) { const d = await open(); await reqToPromise(d.transaction('meta', 'readwrite').objectStore('meta').delete(key)); } // ─── Notebooks ─── export async function getAllNotebooks() { const d = await open(); const all = await reqToPromise(d.transaction('notebooks', 'readonly').objectStore('notebooks').getAll()); return all.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); } export async function putNotebook(nb) { const d = await open(); await reqToPromise(d.transaction('notebooks', 'readwrite').objectStore('notebooks').put(nb)); } export async function putNotebooks(list) { const d = await open(); const t = d.transaction('notebooks', 'readwrite'); const s = t.objectStore('notebooks'); for (const nb of list) s.put(nb); return new Promise((res, rej) => { t.oncomplete = res; t.onerror = rej; }); } export async function deleteNotebook(id) { const d = await open(); await reqToPromise(d.transaction('notebooks', 'readwrite').objectStore('notebooks').delete(id)); } export async function clearNotebooks() { const d = await open(); await reqToPromise(d.transaction('notebooks', 'readwrite').objectStore('notebooks').clear()); } // ─── Notes (metadata) ─── export async function getAllNotes() { const d = await open(); return reqToPromise(d.transaction('notes', 'readonly').objectStore('notes').getAll()); } export async function getNote(id) { const d = await open(); return reqToPromise(d.transaction('notes', 'readonly').objectStore('notes').get(id)); } export async function putNote(note) { const d = await open(); await reqToPromise(d.transaction('notes', 'readwrite').objectStore('notes').put(note)); } export async function putNotes(list) { const d = await open(); const t = d.transaction('notes', 'readwrite'); const s = t.objectStore('notes'); for (const n of list) s.put(n); return new Promise((res, rej) => { t.oncomplete = res; t.onerror = rej; }); } export async function deleteNote(id) { const d = await open(); const t = d.transaction(['notes', 'noteContents'], 'readwrite'); t.objectStore('notes').delete(id); t.objectStore('noteContents').delete(id); return new Promise((res, rej) => { t.oncomplete = res; t.onerror = rej; }); } export async function clearNotes() { const d = await open(); const t = d.transaction(['notes'], 'readwrite'); t.objectStore('notes').clear(); return new Promise((res, rej) => { t.oncomplete = res; t.onerror = rej; }); } // ─── Note content (lazy loaded) ─── export async function getNoteContent(id) { const d = await open(); return reqToPromise(d.transaction('noteContents', 'readonly').objectStore('noteContents').get(id)); } export async function putNoteContent(id, content, drawingData) { const d = await open(); await reqToPromise(d.transaction('noteContents', 'readwrite').objectStore('noteContents').put({ id, content, drawingData })); } // ─── Sync Queue ─── export async function enqueue(action) { const d = await open(); const entry = { ...action, timestamp: Date.now() }; await reqToPromise(d.transaction('syncQueue', 'readwrite').objectStore('syncQueue').add(entry)); } export async function peekQueue(limit = 50) { const d = await open(); return reqToPromise(d.transaction('syncQueue', 'readonly').objectStore('syncQueue').getAll(null, limit)); } export async function dequeue(queueId) { const d = await open(); await reqToPromise(d.transaction('syncQueue', 'readwrite').objectStore('syncQueue').delete(queueId)); } export async function clearQueue() { const d = await open(); await reqToPromise(d.transaction('syncQueue', 'readwrite').objectStore('syncQueue').clear()); } export async function getQueueSize() { const d = await open(); return reqToPromise(d.transaction('syncQueue', 'readonly').objectStore('syncQueue').count()); } // ─── Full wipe (for logout) ─── export async function clearAll() { const d = await open(); const stores = ['notebooks', 'notes', 'noteContents', 'syncQueue', 'meta']; const t = d.transaction(stores, 'readwrite'); stores.forEach((s) => t.objectStore(s).clear()); return new Promise((res, rej) => { t.oncomplete = res; t.onerror = rej; }); }