204 lines
6.8 KiB
JavaScript
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; });
|
|
} |