128 lines
5.8 KiB
JavaScript
128 lines
5.8 KiB
JavaScript
/**
|
|
* 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,
|
|
};
|
|
})();
|