Files
InkNote/frontend/src/offlineStore.js
T

204 lines
6.8 KiB
JavaScript

/**
* 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; });
}