92 lines
3.5 KiB
JavaScript
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;
|
|
},
|
|
};
|
|
})();
|