// offline-store.js — the tablet's local data store (IndexedDB), so the app keeps // working with zero connectivity once the directory/an animal has been opened while // still on the practice WiFi. Two jobs: (1) cache what's been read, so it can be // re-displayed offline; (2) queue what's been written while offline, so it can be // replayed against the practice PC once reachable again. "use strict"; (function () { const DB_NAME = "fellakte-offline"; const DB_VERSION = 1; let dbPromise = null; function openDb() { if (dbPromise) return dbPromise; dbPromise = new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains("bundles")) db.createObjectStore("bundles", { keyPath: "id" }); if (!db.objectStoreNames.contains("meta")) db.createObjectStore("meta", { keyPath: "key" }); if (!db.objectStoreNames.contains("queue")) db.createObjectStore("queue", { keyPath: "id", autoIncrement: true }); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); return dbPromise; } // Runs a write (put/add/delete) against `name` and resolves once the transaction // commits. The return value of `fn` (an IDBRequest) is not needed by any caller. async function runWrite(name, fn) { const db = await openDb(); return new Promise((resolve, reject) => { const t = db.transaction(name, "readwrite"); fn(t.objectStore(name)); t.oncomplete = () => resolve(); t.onerror = () => reject(t.error); }); } // Runs a single read request against `name` and resolves with its result // (undefined on error, so callers can treat a miss the same as "not cached"). async function runRead(name, fn) { const db = await openDb(); return new Promise((resolve) => { const t = db.transaction(name, "readonly"); const req = fn(t.objectStore(name)); req.onsuccess = () => resolve(req.result); req.onerror = () => resolve(undefined); }); } window.offlineStore = { async saveDirectory(list) { await runWrite("meta", (s) => s.put({ key: "directory", value: list, at: Date.now() })); }, async getDirectory() { const row = await runRead("meta", (s) => s.get("directory")); return row ? row.value : null; }, async saveBundle(bundle) { if (!bundle || !bundle.animal) return; await runWrite("bundles", (s) => s.put({ id: bundle.animal.id, bundle, at: Date.now() })); }, async getBundle(id) { const row = await runRead("bundles", (s) => s.get(id)); return row ? row.bundle : null; }, // Scans cached bundles for one containing a finding with this id — used for // update/delete offline, where the caller only has the finding id, not the // animal it belongs to (same shape as the desktop IPC calls). async findAnimalIdForFinding(findingId) { const rows = (await runRead("bundles", (s) => s.getAll())) || []; const hit = rows.find((r) => r.bundle.findings.some((f) => f.id === findingId)); return hit ? hit.id : null; }, async enqueue(op) { await runWrite("queue", (s) => s.add({ ...op, createdAt: Date.now() })); }, async listQueue() { return (await runRead("queue", (s) => s.getAll())) || []; }, async removeFromQueue(id) { await runWrite("queue", (s) => s.delete(id)); }, async queueCount() { return (await this.listQueue()).length; }, }; })();