Files
fellakte/web-client/offline-store.js
T

92 lines
3.5 KiB
JavaScript

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