Alpha Version of Syncing and Desktop/Anroid PWA
This commit is contained in:
1 parent
8cd8133f9e
commit
a8779d7be1
9 files changed
+1381
-454
No files matched your search
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 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; });
|
||||
}
|
||||
Reference in new issue
Block a user