334 lines
11 KiB
TypeScript
334 lines
11 KiB
TypeScript
import { PersistentAssetCache } from "../../features/grid/PersistentAssetCache.js";
|
|
|
|
const GRID_SNAPSHOT_CACHE = new Map();
|
|
const GRID_SNAPSHOT_CACHE_ENABLED = true;
|
|
const GRID_SNAPSHOT_CACHE_MAX = 8;
|
|
const GRID_SNAPSHOT_STORAGE_ASSET_LIMIT = 200;
|
|
const GRID_SNAPSHOT_STORAGE_KEY = "mjr_grid_snapshot_cache_v2";
|
|
const GRID_SNAPSHOT_TTL_MS = 30 * 60 * 1000;
|
|
const GRID_SNAPSHOT_PERSIST_DEBOUNCE_MS = 1500;
|
|
|
|
let gridSnapshotStorageLoaded = false;
|
|
let gridSnapshotPersistTimer: any = null;
|
|
let gridSnapshotPersistPending = false;
|
|
|
|
function isGridSnapshotCacheEnabled() {
|
|
return GRID_SNAPSHOT_CACHE_ENABLED;
|
|
}
|
|
|
|
function clearStoredGridSnapshots() {
|
|
try {
|
|
GRID_SNAPSHOT_CACHE.clear();
|
|
const storage = gridSnapshotStorage();
|
|
storage?.removeItem?.(GRID_SNAPSHOT_STORAGE_KEY);
|
|
globalThis?.sessionStorage?.removeItem?.(GRID_SNAPSHOT_STORAGE_KEY);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
const GRID_SNAPSHOT_ASSET_FIELDS = [
|
|
"id",
|
|
"filename",
|
|
"name",
|
|
"display_name",
|
|
"filepath",
|
|
"path",
|
|
"fullpath",
|
|
"full_path",
|
|
"file_info",
|
|
"subfolder",
|
|
"source",
|
|
"type",
|
|
"root_id",
|
|
"custom_root_id",
|
|
"kind",
|
|
"ext",
|
|
"size",
|
|
"mtime",
|
|
"generation_time",
|
|
"file_creation_time",
|
|
"created_at",
|
|
"updated_at",
|
|
"indexed_at",
|
|
"width",
|
|
"height",
|
|
"duration",
|
|
"thumbnail_url",
|
|
"thumb_url",
|
|
"preview_url",
|
|
"poster",
|
|
"url",
|
|
"rating",
|
|
"tags",
|
|
"has_workflow",
|
|
"hasWorkflow",
|
|
"has_generation_data",
|
|
"hasGenerationData",
|
|
"workflow_type",
|
|
"workflowType",
|
|
"workflow_id",
|
|
"workflowId",
|
|
"generation_time_ms",
|
|
"positive_prompt",
|
|
"enhanced_caption",
|
|
"auto_tags",
|
|
"has_ai_info",
|
|
"has_ai_vector",
|
|
"has_ai_auto_tags",
|
|
"has_ai_enhanced_caption",
|
|
"job_id",
|
|
"stack_id",
|
|
"source_node_id",
|
|
"source_node_type",
|
|
"date",
|
|
"date_exact",
|
|
];
|
|
|
|
function normalizeSnapshotPart(value: any, fallback = "") {
|
|
try {
|
|
return String(value ?? fallback).trim();
|
|
} catch {
|
|
return String(fallback);
|
|
}
|
|
}
|
|
|
|
export function buildGridSnapshotKey(parts: Record<string, any> = {}): string {
|
|
return JSON.stringify({
|
|
scope: normalizeSnapshotPart(parts.scope || "output", "output"),
|
|
query: normalizeSnapshotPart(parts.query || parts.q || "*", "*"),
|
|
customRootId: normalizeSnapshotPart(parts.customRootId || ""),
|
|
subfolder: normalizeSnapshotPart(parts.subfolder || ""),
|
|
collectionId: normalizeSnapshotPart(parts.collectionId || ""),
|
|
viewScope: normalizeSnapshotPart(parts.viewScope || ""),
|
|
kind: normalizeSnapshotPart(parts.kind || ""),
|
|
workflowOnly: normalizeSnapshotPart(parts.workflowOnly ? "1" : ""),
|
|
minRating: normalizeSnapshotPart(parts.minRating || ""),
|
|
minSizeMB: normalizeSnapshotPart(parts.minSizeMB || ""),
|
|
maxSizeMB: normalizeSnapshotPart(parts.maxSizeMB || ""),
|
|
resolutionCompare: normalizeSnapshotPart(parts.resolutionCompare || ""),
|
|
minWidth: normalizeSnapshotPart(parts.minWidth || ""),
|
|
minHeight: normalizeSnapshotPart(parts.minHeight || ""),
|
|
maxWidth: normalizeSnapshotPart(parts.maxWidth || ""),
|
|
maxHeight: normalizeSnapshotPart(parts.maxHeight || ""),
|
|
workflowType: normalizeSnapshotPart(parts.workflowType || "").toUpperCase(),
|
|
workflowId: normalizeSnapshotPart(parts.workflowId || ""),
|
|
workflowModel: normalizeSnapshotPart(parts.workflowModel || ""),
|
|
workflowRunsOn: normalizeSnapshotPart(parts.workflowRunsOn || "").toLowerCase(),
|
|
dateRange: normalizeSnapshotPart(parts.dateRange || ""),
|
|
dateExact: normalizeSnapshotPart(parts.dateExact || ""),
|
|
sort: normalizeSnapshotPart(parts.sort || "mtime_desc", "mtime_desc"),
|
|
semanticMode: normalizeSnapshotPart(parts.semanticMode ? "1" : ""),
|
|
});
|
|
}
|
|
|
|
export function compactSnapshotAsset(asset: Record<string, any> | null | undefined): Record<string, any> | null {
|
|
if (!asset || typeof asset !== "object") return null;
|
|
const out: Record<string, any> = {};
|
|
for (const field of GRID_SNAPSHOT_ASSET_FIELDS) {
|
|
if (asset[field] !== undefined) out[field] = asset[field];
|
|
}
|
|
if (!out.type && out.source) out.type = out.source;
|
|
if (!out.source && out.type) out.source = out.type;
|
|
if (!out.kind) out.kind = asset.kind || "image";
|
|
if (Array.isArray(out.tags)) out.tags = out.tags.slice(0, 80);
|
|
if (Array.isArray(out.auto_tags)) out.auto_tags = out.auto_tags.slice(0, 80);
|
|
return out;
|
|
}
|
|
|
|
export function normalizeGridSnapshot(snapshot: any) {
|
|
if (!snapshot || typeof snapshot !== "object") return null;
|
|
const at = Number(snapshot.at || 0) || 0;
|
|
if (!at || Date.now() - at > GRID_SNAPSHOT_TTL_MS) return null;
|
|
const assets = Array.isArray(snapshot.assets)
|
|
? snapshot.assets.map(compactSnapshotAsset).filter(Boolean)
|
|
: [];
|
|
if (!assets.length) return null;
|
|
const totalRaw = Number(snapshot.total ?? assets.length);
|
|
const offsetRaw = Number(snapshot.offset ?? assets.length);
|
|
return {
|
|
assets,
|
|
title: normalizeSnapshotPart(snapshot.title || "Cached", "Cached"),
|
|
at,
|
|
total: Number.isFinite(totalRaw) ? Math.max(0, totalRaw) : assets.length,
|
|
offset: Number.isFinite(offsetRaw) ? Math.max(assets.length, offsetRaw) : assets.length,
|
|
done: !!snapshot.done,
|
|
query: normalizeSnapshotPart(snapshot.query || "*", "*"),
|
|
};
|
|
}
|
|
|
|
function gridSnapshotStorage() {
|
|
try {
|
|
return globalThis?.localStorage || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!isGridSnapshotCacheEnabled()) {
|
|
clearStoredGridSnapshots();
|
|
}
|
|
|
|
function migrateLegacySessionSnapshots(storage: any) {
|
|
try {
|
|
const legacy = globalThis?.sessionStorage;
|
|
if (!legacy || !storage) return;
|
|
const raw = legacy.getItem?.(GRID_SNAPSHOT_STORAGE_KEY);
|
|
if (!raw) return;
|
|
if (!storage.getItem?.(GRID_SNAPSHOT_STORAGE_KEY)) {
|
|
storage.setItem(GRID_SNAPSHOT_STORAGE_KEY, raw);
|
|
}
|
|
legacy.removeItem(GRID_SNAPSHOT_STORAGE_KEY);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
function pruneGridSnapshotCache() {
|
|
const now = Date.now();
|
|
for (const [key, snapshot] of GRID_SNAPSHOT_CACHE.entries()) {
|
|
const at = Number(snapshot?.at || 0) || 0;
|
|
if (!at || now - at > GRID_SNAPSHOT_TTL_MS) {
|
|
GRID_SNAPSHOT_CACHE.delete(key);
|
|
}
|
|
}
|
|
while (GRID_SNAPSHOT_CACHE.size > GRID_SNAPSHOT_CACHE_MAX) {
|
|
const oldestKey = GRID_SNAPSHOT_CACHE.keys().next().value;
|
|
if (!oldestKey) break;
|
|
GRID_SNAPSHOT_CACHE.delete(oldestKey);
|
|
}
|
|
}
|
|
|
|
function loadGridSnapshotsFromStorage() {
|
|
if (!isGridSnapshotCacheEnabled()) {
|
|
gridSnapshotStorageLoaded = true;
|
|
clearStoredGridSnapshots();
|
|
return;
|
|
}
|
|
if (gridSnapshotStorageLoaded) return;
|
|
gridSnapshotStorageLoaded = true;
|
|
try {
|
|
const storage = gridSnapshotStorage();
|
|
migrateLegacySessionSnapshots(storage);
|
|
const raw = storage?.getItem?.(GRID_SNAPSHOT_STORAGE_KEY);
|
|
if (!raw) return;
|
|
const parsed = JSON.parse(raw);
|
|
const entries = Array.isArray(parsed?.entries) ? parsed.entries : [];
|
|
for (const entry of entries) {
|
|
if (!Array.isArray(entry) || entry.length < 2) continue;
|
|
const key = String(entry[0] || "");
|
|
const snapshot = normalizeGridSnapshot(entry[1]);
|
|
if (key && snapshot) GRID_SNAPSHOT_CACHE.set(key, snapshot);
|
|
}
|
|
pruneGridSnapshotCache();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
function persistGridSnapshotsToStorageNow() {
|
|
if (!isGridSnapshotCacheEnabled()) {
|
|
clearStoredGridSnapshots();
|
|
return;
|
|
}
|
|
try {
|
|
pruneGridSnapshotCache();
|
|
const storage = gridSnapshotStorage();
|
|
if (!storage) return;
|
|
const entries = Array.from(GRID_SNAPSHOT_CACHE.entries()).map(([key, snapshot]) => [
|
|
key,
|
|
{
|
|
...snapshot,
|
|
assets: Array.isArray(snapshot?.assets)
|
|
? snapshot.assets.slice(0, GRID_SNAPSHOT_STORAGE_ASSET_LIMIT)
|
|
: [],
|
|
},
|
|
]);
|
|
storage.setItem(GRID_SNAPSHOT_STORAGE_KEY, JSON.stringify({ entries }));
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
function persistGridSnapshotsToStorage() {
|
|
if (!isGridSnapshotCacheEnabled()) {
|
|
clearStoredGridSnapshots();
|
|
gridSnapshotPersistPending = false;
|
|
return;
|
|
}
|
|
gridSnapshotPersistPending = true;
|
|
if (gridSnapshotPersistTimer) return;
|
|
gridSnapshotPersistTimer = setTimeout(() => {
|
|
gridSnapshotPersistTimer = null;
|
|
if (!gridSnapshotPersistPending) return;
|
|
gridSnapshotPersistPending = false;
|
|
persistGridSnapshotsToStorageNow();
|
|
}, GRID_SNAPSHOT_PERSIST_DEBOUNCE_MS);
|
|
}
|
|
|
|
export function flushGridSnapshotsPersist() {
|
|
if (gridSnapshotPersistTimer) {
|
|
clearTimeout(gridSnapshotPersistTimer);
|
|
gridSnapshotPersistTimer = null;
|
|
}
|
|
if (!gridSnapshotPersistPending) return;
|
|
gridSnapshotPersistPending = false;
|
|
persistGridSnapshotsToStorageNow();
|
|
}
|
|
|
|
export function getGridSnapshot(key: any) {
|
|
if (!isGridSnapshotCacheEnabled()) return null;
|
|
loadGridSnapshotsFromStorage();
|
|
const snapshot = normalizeGridSnapshot(GRID_SNAPSHOT_CACHE.get(key));
|
|
if (!snapshot) {
|
|
GRID_SNAPSHOT_CACHE.delete(key);
|
|
persistGridSnapshotsToStorage();
|
|
return null;
|
|
}
|
|
GRID_SNAPSHOT_CACHE.delete(key);
|
|
GRID_SNAPSHOT_CACHE.set(key, snapshot);
|
|
persistGridSnapshotsToStorage();
|
|
return snapshot;
|
|
}
|
|
|
|
export async function getPersistentGridSnapshot(key: any) {
|
|
const memory = getGridSnapshot(key);
|
|
if (memory) return memory;
|
|
const persisted = await PersistentAssetCache.get(String(key || ""));
|
|
const normalized = normalizeGridSnapshot(persisted);
|
|
if (!normalized) return null;
|
|
GRID_SNAPSHOT_CACHE.set(String(key || ""), normalized);
|
|
return normalized;
|
|
}
|
|
|
|
export function hasGridSnapshot(key: string): boolean {
|
|
if (!isGridSnapshotCacheEnabled()) return false;
|
|
loadGridSnapshotsFromStorage();
|
|
return !!normalizeGridSnapshot(GRID_SNAPSHOT_CACHE.get(key));
|
|
}
|
|
|
|
export function rememberGridSnapshot(key: any, snapshot: any) {
|
|
if (!isGridSnapshotCacheEnabled()) return false;
|
|
if (!key || !snapshot || typeof snapshot !== "object") return false;
|
|
const normalized = normalizeGridSnapshot({
|
|
...snapshot,
|
|
at: snapshot.at || Date.now(),
|
|
});
|
|
if (!normalized) return false;
|
|
GRID_SNAPSHOT_CACHE.delete(key);
|
|
GRID_SNAPSHOT_CACHE.set(key, normalized);
|
|
persistGridSnapshotsToStorage();
|
|
PersistentAssetCache.put(String(key || ""), normalized).catch((e: any) => {
|
|
console.debug?.(e);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
export function resetGridSnapshotCacheForTests() {
|
|
GRID_SNAPSHOT_CACHE.clear();
|
|
gridSnapshotStorageLoaded = false;
|
|
gridSnapshotPersistPending = false;
|
|
if (gridSnapshotPersistTimer) clearTimeout(gridSnapshotPersistTimer);
|
|
gridSnapshotPersistTimer = null;
|
|
}
|