/** * ZDDC Classifier — workspace persistence (IndexedDB). * * A "workspace" is one classification project: the picked source directory * HANDLE, a SNAPSHOT of its completed scan (folder/file structure — names and * paths only, no contents), and the Classify & Copy map (assignments + target * trees). Scan once, resume instantly across sessions without re-walking the * (often cloud-backed, high-latency) source. * * Two object stores so the welcome list stays cheap: * - 'index' (tiny): { id, name, rootName, createdAt, updatedAt, summary } * - 'data' (large): { id, rootHandle, tree, classify } * * A FileSystemDirectoryHandle is structured-cloneable, so IndexedDB can hold * it; on reuse we re-request permission (one click). It's only needed at COPY * time — opening a workspace runs entirely from the snapshot. */ (function () { 'use strict'; var DB_NAME = 'zddc-classifier'; var DB_VERSION = 2; var IDX = 'index'; var DATA = 'data'; var available = typeof indexedDB !== 'undefined'; function openDB() { return new Promise(function (resolve, reject) { if (!available) { reject(new Error('IndexedDB unavailable')); return; } var req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = function () { var db = req.result; // 'kv' (v1, single implicit map) is intentionally left behind. if (!db.objectStoreNames.contains(IDX)) db.createObjectStore(IDX, { keyPath: 'id' }); if (!db.objectStoreNames.contains(DATA)) db.createObjectStore(DATA, { keyPath: 'id' }); }; req.onsuccess = function () { resolve(req.result); }; req.onerror = function () { reject(req.error); }; }); } function reqP(req) { return new Promise(function (resolve, reject) { req.onsuccess = function () { resolve(req.result); }; req.onerror = function () { reject(req.error); }; }); } // ── public API ───────────────────────────────────────────────────────── // Light metadata for every workspace (for the welcome list). Sorted newest // first. Never loads the big snapshot. function listWorkspaces() { return openDB().then(function (db) { return reqP(db.transaction(IDX, 'readonly').objectStore(IDX).getAll()); }).then(function (rows) { (rows || []).sort(function (a, b) { return (b.updatedAt || 0) - (a.updatedAt || 0); }); return rows || []; }).catch(function (e) { console.warn('persist.list', e); return []; }); } // Full data record for one workspace: { id, rootHandle, tree, classify }. function getWorkspace(id) { return openDB().then(function (db) { return reqP(db.transaction(DATA, 'readonly').objectStore(DATA).get(id)); }).catch(function (e) { console.warn('persist.get', e); return null; }); } // Save (create or update). meta = {id,name,rootName,createdAt,updatedAt,summary}; // data = {id, rootHandle, tree, classify}. tree may be omitted on a classify- // only autosave (the snapshot rarely changes) — then we preserve the stored one. function putWorkspace(meta, data) { return openDB().then(function (db) { return new Promise(function (resolve, reject) { var t = db.transaction([IDX, DATA], 'readwrite'); t.oncomplete = function () { resolve(); }; t.onerror = function () { reject(t.error); }; t.objectStore(IDX).put(meta); var ds = t.objectStore(DATA); if (data && typeof data.tree !== 'undefined') { ds.put(data); } else if (data) { // Merge classify/rootHandle without clobbering the snapshot. var g = ds.get(meta.id); g.onsuccess = function () { var existing = g.result || { id: meta.id }; if (typeof data.rootHandle !== 'undefined') existing.rootHandle = data.rootHandle; if (typeof data.classify !== 'undefined') existing.classify = data.classify; existing.id = meta.id; ds.put(existing); }; } }); }).catch(function (e) { console.warn('persist.put', e); }); } function deleteWorkspace(id) { return openDB().then(function (db) { return new Promise(function (resolve, reject) { var t = db.transaction([IDX, DATA], 'readwrite'); t.oncomplete = function () { resolve(); }; t.onerror = function () { reject(t.error); }; t.objectStore(IDX).delete(id); t.objectStore(DATA).delete(id); }); }).catch(function (e) { console.warn('persist.delete', e); }); } // Re-acquire read permission on a stored handle (one click). true if usable. function verifyPermission(handle, write) { if (!handle || typeof handle.queryPermission !== 'function') return Promise.resolve(false); var opts = { mode: write ? 'readwrite' : 'read' }; return handle.queryPermission(opts).then(function (p) { if (p === 'granted') return true; return handle.requestPermission(opts).then(function (p2) { return p2 === 'granted'; }); }).catch(function () { return false; }); } window.app.modules.persist = { available: available, listWorkspaces: listWorkspaces, getWorkspace: getWorkspace, putWorkspace: putWorkspace, deleteWorkspace: deleteWorkspace, verifyPermission: verifyPermission, }; })();