125 lines
4.3 KiB
TypeScript
125 lines
4.3 KiB
TypeScript
const DB_NAME = "mjr-asset-cache";
|
|
const DB_VERSION = 1;
|
|
const STORE = "snapshots";
|
|
const MAX_ENTRIES = 24;
|
|
const MAX_ASSETS = 500;
|
|
const TTL_MS = 45 * 60 * 1000;
|
|
|
|
let dbPromise: Promise<IDBDatabase | null> | null = null;
|
|
|
|
function openDb(): Promise<IDBDatabase | null> {
|
|
if (dbPromise) return dbPromise;
|
|
dbPromise = new Promise((resolve) => {
|
|
try {
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE, { keyPath: "key" });
|
|
};
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => resolve(null);
|
|
} catch {
|
|
resolve(null);
|
|
}
|
|
});
|
|
return dbPromise;
|
|
}
|
|
|
|
function compactAsset(asset: any) {
|
|
if (!asset || typeof asset !== "object") return null;
|
|
return {
|
|
id: asset.id,
|
|
filename: asset.filename,
|
|
filepath: asset.filepath || asset.path,
|
|
subfolder: asset.subfolder || "",
|
|
source: asset.source || asset.type,
|
|
type: asset.type || asset.source,
|
|
root_id: asset.root_id || asset.custom_root_id,
|
|
kind: asset.kind,
|
|
ext: asset.ext,
|
|
size: asset.size,
|
|
mtime: asset.mtime,
|
|
width: asset.width,
|
|
height: asset.height,
|
|
duration: asset.duration,
|
|
rating: asset.rating,
|
|
tags: Array.isArray(asset.tags) ? asset.tags.slice(0, 60) : undefined,
|
|
thumbnail_url: asset.thumbnail_url || asset.thumb_url,
|
|
preview_url: asset.preview_url,
|
|
has_workflow: asset.has_workflow || asset.hasWorkflow,
|
|
workflow_type: asset.workflow_type || asset.workflowType,
|
|
positive_prompt: asset.positive_prompt,
|
|
};
|
|
}
|
|
|
|
function normalizeSnapshot(value: any) {
|
|
if (!value || typeof value !== "object") return null;
|
|
const at = Number(value.at || 0);
|
|
if (!at || Date.now() - at > TTL_MS) return null;
|
|
const assets = Array.isArray(value.assets) ? value.assets.map(compactAsset).filter(Boolean) : [];
|
|
if (!assets.length) return null;
|
|
return { ...value, assets };
|
|
}
|
|
|
|
async function transaction(mode: IDBTransactionMode) {
|
|
const db = await openDb();
|
|
if (!db) return null;
|
|
try {
|
|
return db.transaction(STORE, mode).objectStore(STORE);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export const PersistentAssetCache = {
|
|
async get(key: string) {
|
|
const store = await transaction("readonly");
|
|
if (!store || !key) return null;
|
|
return await new Promise((resolve) => {
|
|
const req = store.get(key);
|
|
req.onsuccess = () => resolve(normalizeSnapshot(req.result));
|
|
req.onerror = () => resolve(null);
|
|
});
|
|
},
|
|
async put(key: string, snapshot: any) {
|
|
const store = await transaction("readwrite");
|
|
const normalized = normalizeSnapshot({
|
|
...snapshot,
|
|
key,
|
|
at: snapshot?.at || Date.now(),
|
|
assets: Array.isArray(snapshot?.assets) ? snapshot.assets.slice(0, MAX_ASSETS) : [],
|
|
});
|
|
if (!store || !key || !normalized) return false;
|
|
await new Promise((resolve) => {
|
|
const req = store.put(normalized);
|
|
req.onsuccess = req.onerror = () => resolve(null);
|
|
});
|
|
this.prune();
|
|
return true;
|
|
},
|
|
async prune() {
|
|
const store = await transaction("readwrite");
|
|
if (!store) return;
|
|
const entries: any[] = await new Promise((resolve) => {
|
|
const out: any[] = [];
|
|
const req = store.openCursor();
|
|
req.onsuccess = () => {
|
|
const cursor = req.result;
|
|
if (!cursor) return resolve(out);
|
|
out.push(cursor.value);
|
|
cursor.continue();
|
|
};
|
|
req.onerror = () => resolve(out);
|
|
});
|
|
const stale = entries
|
|
.filter((entry) => !normalizeSnapshot(entry))
|
|
.map((entry) => entry.key);
|
|
const overflow = entries
|
|
.filter((entry) => !stale.includes(entry.key))
|
|
.sort((a, b) => Number(a.at || 0) - Number(b.at || 0))
|
|
.slice(0, Math.max(0, entries.length - MAX_ENTRIES))
|
|
.map((entry) => entry.key);
|
|
for (const key of [...stale, ...overflow]) store.delete(key);
|
|
},
|
|
};
|