From b0194e10a87e16797a568d8a30d53ebecd27d8a4 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 18 Oct 2025 15:04:51 +0000
Subject: [PATCH] =DataStore.js and UploaderManager.js overhaul

---
 assets/js/concise/DataStore.js | 1265 ++++++++++++++++++++++++++-------------------------------
 1 files changed, 581 insertions(+), 684 deletions(-)

diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 4f4ba59..946bf0e 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -1,94 +1,121 @@
 /**
- * Handles GET Requests, storing responses by a key of filters, set with setFilter method
- * Stores:
- * 		- Items: the individual item data, mapped by postID/termID
- * 		- Cache: the cacheKey generated by filters, and the results in the value
- * 		- httpHeaders: If Modified Since tracking
- * 		- domCache: rendered DOM elements, to reduce on-page rendering
+ * ExtendedDataStore - A flexible IndexedDB wrapper with HTTP caching
+ *
+ * Configuration-based approach for different storage needs:
+ * - Configurable endpoint, keyPath, and indexes
+ * - Built-in ETag and If-Modified-Since support
+ * - Automatic DOM reference stripping
+ * - TTL-based cache invalidation
  */
 class DataStore {
 	constructor(config = {}) {
+		// Core configuration with sensible defaults
 		this.config = {
+			// Storage configuration
 			name: 'default',
-			endpoint: false,
+			version: 1,
+			storeName: 'items',
+			keyPath: 'id',
+			indexes: [], // Array of {name, keyPath, unique}
+
+			// API configuration
+			endpoint: null,
 			apiBase: jvbSettings.api,
-			TTL: 3600000, // 1 hour default
-			showLoading: true,
 			headers: {},
 			filters: {},
+
+			// Cache configuration
+			TTL: 3600000, // 1 hour default
+			useHttpCaching: true, // ETag and If-Modified-Since
+			cacheKeyStrategy: 'filters', // How to generate cache keys
+
+			// UI configuration
+			showLoading: true,
+
+			// Features
+			stripDOMReferences: true,
+			storeBlobs: false,
+
 			...config
 		};
-		if (!this.config.endpoint) {
-			console.warn('No endpoint set. Only saving locally');
-		}
+
+		// Initialize base properties
+		this.db = null;
+		this.data = new Map();
+		this.cache = new Map();
+		this.httpHeaders = new Map();
+		this.subscribers = new Set();
+		this.currentRequest = null;
+		this.filters = this.config.filters??{};
+
+		// Set up headers
+		this.headers = {
+			'X-WP-Nonce': jvbSettings?.nonce,
+			...this.config.headers
+		};
 
 		this.body = document.body;
 		this.loading = document.querySelector('dialog.loading');
 
-		this.headers = {
-			'X-WP-Nonce': jvbSettings.nonce,
-			...this.config.headers
-		};
-
-		// Data stores
-		this.items = new Map();
-		this.cache = new Map(); //TODO: call this resultsCache
-		this.httpHeaders = new Map();
-		this.domCache = new Map();
-		this.forms = new Map();
-
-		// State management
-		this.filters = config.filters ?? {};
-		this.subscribers = new Set();
-		this.db = null;
-		this.currentRequest = null;
-
-		// Server Timestamps - needed?
-		this.cachedContent = JSON.parse(cacheJVB.cache) || {};
-		this.lastTimestampUpdate = Date.now();
-
+		// Auto-initialize
 		this.initDB();
-		document.addEventListener('beforeUnload', () =>this.destroy());
+
+		// Cleanup on page unload
+		window.addEventListener('beforeunload', () => this.destroy());
 	}
 
+	/**
+	 * Initialize IndexedDB with configurable schema
+	 */
 	async initDB() {
-		if (!('indexedDB' in window)) return;
+		if (!('indexedDB' in window)) {
+			console.warn('IndexedDB not supported');
+			return;
+		}
 
-		const request = indexedDB.open(`jvb_${this.config.name}_db`, 1);
+		const dbName = `jvb_${this.config.name}_db`;
+		const request = indexedDB.open(dbName, this.config.version);
 
 		request.onupgradeneeded = (e) => {
 			const db = e.target.result;
-			// Items store
-			if (!db.objectStoreNames.contains('items')) {
-				db.createObjectStore('items', { keyPath: 'id' });
-			}
 
-			// DOM cache for rendered elements
-			if (!db.objectStoreNames.contains('dom')) {
-				db.createObjectStore('dom', { keyPath: 'id' });
-			}
-
-			if (!db.objectStoreNames.contains('forms')) {
-				let forms = db.createObjectStore('forms', {
-					keyPath: 'formId',
+			// Create main store with configurable keyPath
+			if (!db.objectStoreNames.contains(this.config.storeName)) {
+				const store = db.createObjectStore(this.config.storeName, {
+					keyPath: this.config.keyPath
 				});
-				forms.createIndex('status', 'status', {unique:false});
-				forms.createIndex('operationId', 'operationId', {unique:false});
-				forms.createIndex('timestamp', 'timestamp', {unique:false});
+
+				// Add configured indexes
+				this.config.indexes.forEach(index => {
+					store.createIndex(
+						index.name,
+						index.keyPath || index.name,
+						{ unique: index.unique || false }
+					);
+				});
 			}
 
-			// Cache store for GET requests with endpoint index
-			if (!db.objectStoreNames.contains('cache')) {
+			// Cache store for HTTP responses
+			if (this.config.endpoint && !db.objectStoreNames.contains('cache')) {
 				const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
 				cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
 				cacheStore.createIndex('endpoint', 'endpoint', { unique: false });
 				cacheStore.createIndex('filters', 'filters', { unique: false });
 			}
 
-			// HTTP headers store
-			if (!db.objectStoreNames.contains('headers')) {
+			// HTTP headers store for ETag/If-Modified-Since
+			if (this.config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
 				db.createObjectStore('headers', { keyPath: 'key' });
 			}
+
+			if (this.config.storeBlobs && !db.objectStoreNames.contains('blobs')) {
+				db.createObjectStore('blobs', { keyPath: 'uploadId' });
+			}
+
+			// Call optional schema extension
+			if (this.config.onUpgrade) {
+				this.config.onUpgrade(db, e.oldVersion, e.newVersion);
+			}
 		};
 
 		request.onsuccess = (e) => {
@@ -97,135 +124,293 @@
 		};
 
 		request.onerror = (e) => {
-			console.error('IndexedDB error:', e);
+			console.error(`IndexedDB error for ${dbName}:`, e);
+			if (this.config.onError) {
+				this.config.onError(e);
+			}
 		};
 	}
 
+	/**
+	 * Load all data from IndexedDB
+	 */
 	async loadFromDB() {
 		if (!this.db) return;
 
+		const loadPromises = [
+			this.loadData()
+		];
+
+		if (this.config.endpoint) {
+			loadPromises.push(this.loadCache());
+		}
+
+		if (this.config.useHttpCaching) {
+			loadPromises.push(this.loadHeaders());
+		}
+
 		try {
-			await Promise.all([
-				this.loadItems(),
-				this.loadCache(),
-				this.loadHeaders(),
-				this.loadDOMCache(),
-				this.loadForms()
-			]);
+			await Promise.all(loadPromises);
+			this.notify('data-loaded', {
+				count: this.data.size,
+				store: this.config.storeName
+			});
 		} catch (error) {
 			console.error('Error loading from DB:', error);
 		}
 	}
 
-	async loadItems() {
+	/**
+	 * Load main data from IndexedDB
+	 */
+	async loadData() {
 		if (!this.db) return;
 
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['items'], 'readonly');
-			const store = tx.objectStore('items');
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
 			const request = store.getAll();
 
 			request.onsuccess = (e) => {
 				e.target.result.forEach(item => {
-					this.items.set(item.id, item);
-				});
-				this.notify('items-loaded', { items: Array.from(this.items.values()) });
-				resolve();
-			};
-		});
-	}
+					// Strip DOM references if needed
+					const cleaned = this.config.stripDOMReferences
+						? this.stripDOMReferences(item)
+						: item;
 
-	async loadCache() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['cache'], 'readonly');
-			const store = tx.objectStore('cache');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					if (this.isCacheValid(item)) {
-						this.cache.set(item.key, item);
-					}
+					const key = this.getItemKey(cleaned);
+					this.data.set(key, cleaned);
 				});
 				resolve();
 			};
+
+			request.onerror = (e) => reject(e);
 		});
 	}
 
-	async loadHeaders() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['headers'], 'readonly');
-			const store = tx.objectStore('headers');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(header => {
-					this.httpHeaders.set(header.key, header);
-				});
-				resolve();
-			};
-		});
-	}
-
-	async loadDOMCache() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['dom'], 'readonly');
-			const store = tx.objectStore('dom');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(domEntry => {
-					// Convert stored HTML back to DOM elements
-					const reconstructed = {};
-					Object.entries(domEntry.views).forEach(([viewName, html]) => {
-						const temp = document.createElement('div');
-						temp.innerHTML = html;
-						reconstructed[viewName] = temp.firstElementChild;
-					});
-					this.domCache.set(domEntry.id, reconstructed);
-				});
-				resolve();
-			};
-		});
-	}
-
-	async loadForms() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['forms'], 'readonly');
-			const store = tx.objectStore('forms');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(form => {
-					this.forms.set(form.key, form);
-				});
-				resolve();
-			};
-		});
-	}
-
-	setLoading(on) {
-		this.body.classList.toggle('loading', on);
-		if (on) {
-			this.loading.showModal();
-		} else {
-			this.loading.close();
-		}
-
-	}
-
 	/**
-	 * Main fetch method with caching and conditional requests
+	 * Strip DOM references from an object (recursive)
 	 */
-	async fetch(endpoint = null, options = {}) {
+	stripDOMReferences(obj) {
+		if (!obj || typeof obj !== 'object') return obj;
+
+		// Handle arrays
+		if (Array.isArray(obj)) {
+			return obj.map(item => this.stripDOMReferences(item));
+		}
+
+		// Handle objects
+		const cleaned = {};
+		for (const [key, value] of Object.entries(obj)) {
+			// Skip DOM-related properties
+			if (this.isDOMReference(key, value)) {
+				continue;
+			}
+
+			// Handle Set/Map collections
+			if (value instanceof Set) {
+				cleaned[key] = Array.from(value);
+			} else if (value instanceof Map) {
+				cleaned[key] = Object.fromEntries(value);
+			} else if (typeof value === 'object' && value !== null) {
+				cleaned[key] = this.stripDOMReferences(value);
+			} else {
+				cleaned[key] = value;
+			}
+		}
+
+		return cleaned;
+	}
+
+	/**
+	 * Check if a property is a DOM reference
+	 */
+	isDOMReference(key, value) {
+		// Check value types
+		if (value instanceof HTMLElement ||
+			value instanceof NodeList ||
+			value instanceof HTMLCollection ||
+			(value && value.nodeType !== undefined)) {
+			return true;
+		}
+
+		// Check key names
+		const domKeys = ['element', 'el', 'dom', 'node', 'ui', 'container', 'wrapper'];
+		if (domKeys.some(k => key.toLowerCase().includes(k))) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Get the key for an item based on configured keyPath
+	 */
+	getItemKey(item) {
+		if (typeof this.config.keyPath === 'function') {
+			return this.config.keyPath(item);
+		}
+
+		// Support nested keypaths like 'meta.id'
+		const keys = this.config.keyPath.split('.');
+		let value = item;
+
+		for (const key of keys) {
+			value = value?.[key];
+		}
+
+		return value;
+	}
+
+	/**
+	 * Save a single item
+	 */
+	async save(item) {
+		const key = this.getItemKey(item);
+
+		// Strip DOM references if configured
+		const cleaned = this.config.stripDOMReferences
+			? this.stripDOMReferences(item)
+			: item;
+
+		// Store in memory
+		this.data.set(key, cleaned);
+
+		// Persist to IndexedDB
+		await this.saveToDB(cleaned);
+
+		// Notify subscribers
+		this.notify('item-saved', { item: cleaned, key });
+
+		return cleaned;
+	}
+
+	/**
+	 * Save item to IndexedDB
+	 */
+	async saveToDB(item) {
+		if (!this.db) return;
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readwrite');
+			const store = tx.objectStore(this.config.storeName);
+			const request = store.put(item);
+
+			request.onsuccess = () => resolve();
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+	/**
+	 * Batch save multiple items
+	 */
+	async saveMany(items) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction([this.config.storeName], 'readwrite');
+		const store = tx.objectStore(this.config.storeName);
+
+		const promises = items.map(item => {
+			const cleaned = this.config.stripDOMReferences
+				? this.stripDOMReferences(item)
+				: item;
+
+			const key = this.getItemKey(cleaned);
+			this.data.set(key, cleaned);
+
+			return store.put(cleaned);
+		});
+
+		await Promise.all(promises);
+		this.notify('items-saved', { count: items.length });
+	}
+
+	/**
+	 * Get a single item
+	 */
+	get(key) {
+		return this.data.get(key);
+	}
+
+	/**
+	 * Get all items
+	 */
+	getAll() {
+		return Array.from(this.data.values());
+	}
+
+	/**
+	 * Delete an item
+	 */
+	async delete(key, storeName = null) {
+		this.data.delete(key);
+
+		if (!storeName) {
+			storeName = this.config.storeName;
+		}
+		if (this.db) {
+			const tx = this.db.transaction([storeName], 'readwrite');
+			const store = tx.objectStore(storeName);
+			await store.delete(key);
+		}
+
+		this.notify('item-deleted', { key });
+	}
+
+	async saveBlob(key, blob) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['blobs'], 'readwrite');
+		const store = tx.objectStore('blobs');
+		await store.put({ key, data: blob, type: blob.type, name: blob.name });
+	}
+
+	async getBlob(key) {
+		if (!this.db) return null;
+
+		return new Promise(resolve => {
+			const tx = this.db.transaction(['blobs'], 'readonly');
+			const request = tx.objectStore('blobs').get(key);
+			request.onsuccess = () => resolve(request.result);
+			request.onerror = () => resolve(null);
+		});
+	}
+
+	/**
+	 * Clear all data
+	 */
+	async clear() {
+		this.data.clear();
+		this.cache.clear();
+		this.httpHeaders.clear();
+
+		if (this.domCache) {
+			this.domCache.clear();
+		}
+
+		if (this.db) {
+			const stores = [this.config.storeName];
+			if (this.config.endpoint) stores.push('cache');
+			if (this.config.useHttpCaching) stores.push('headers');
+
+			const tx = this.db.transaction(stores, 'readwrite');
+			stores.forEach(storeName => {
+				if (this.db.objectStoreNames.contains(storeName)) {
+					tx.objectStore(storeName).clear();
+				}
+			});
+		}
+
+		this.notify('data-cleared');
+	}
+
+	/**
+	 * Fetch data from server with HTTP caching
+	 */
+	async fetch(options = {}) {
+		if (!this.config.endpoint) {
+			throw new Error('No endpoint configured for fetch');
+		}
 		const {
 			filters = this.filters,
 			headers = {},
@@ -235,136 +420,93 @@
 			this.setLoading(true);
 		}
 
+		const cacheKey = this.generateCacheKey(filters);
 
-		// Use provided endpoint or config endpoint
-		const apiEndpoint = endpoint || this.config.endpoint;
-		if (!apiEndpoint) {
-			throw new Error('No endpoint specified');
+		//Check Cached data
+		const cachedData = this.cache.get(cacheKey);
+		if (cachedData && this.isCacheValid(cachedData)) {
+			return cachedData.data;
 		}
 
-		// Generate cache key from endpoint and filters
-		const cacheKey = this.generateCacheKey(apiEndpoint, filters);
-		const cleanedFilters = this.cleanFilters(filters);
-
-		// Build request URL
-		const params = new URLSearchParams(cleanedFilters);
-		const url = `${this.config.apiBase}${apiEndpoint}${params.toString() ? '?' + params : ''}`;
-
-		// Prepare headers with conditional requests
+		// Build request headers with HTTP caching
 		const requestHeaders = {
 			...this.headers,
 			...headers
 		};
 
-		// Add conditional headers from stored data
-		const headerKey = this.generateHeaderKey(url);
-		const storedHeaders = this.httpHeaders.get(headerKey);
-		const cachedData = this.cache.get(cacheKey);
-
-		if (storedHeaders && cachedData) {
-			if (storedHeaders.etag) {
-				requestHeaders['If-None-Match'] = storedHeaders.etag;
-			}
-			if (storedHeaders.lastModified) {
-				requestHeaders['If-Modified-Since'] = storedHeaders.lastModified;
+		if (this.config.useHttpCaching) {
+			const httpCache = this.httpHeaders.get(cacheKey);
+			if (httpCache) {
+				if (httpCache.etag) {
+					requestHeaders['If-None-Match'] = httpCache.etag;
+				}
+				if (httpCache.lastModified) {
+					requestHeaders['If-Modified-Since'] = httpCache.lastModified;
+				}
 			}
 		}
 
+		// Build URL with filters
+
+		const cleanedFilters = this.cleanFilters(filters);
+		const params = new URLSearchParams(cleanedFilters);
+		const url = `${this.config.apiBase}${this.config.endpoint}${params.toString() ? '?' + params : ''}`;
+
 		try {
 			const response = await fetch(url, {
 				method: 'GET',
 				headers: requestHeaders
 			});
 
-			console.log('DataStore response status: ',response.status);
-			// Handle 304 Not Modified - return cached data
-			if (response.status === 304) {
-				console.debug(`304 Not Modified for ${url}`);
-				if (cachedData) {
-					// Update timestamp but keep data
-					cachedData.timestamp = Date.now();
-					this.cache.set(cacheKey, cachedData);
-					await this.saveCacheToDB(cacheKey, cachedData);
-
-					// Store current request info
-					this.currentRequest = {
-						filters: cleanedFilters,
-						data: cachedData.data,
-						cached: true
-					};
-
-					//TODO: should this be items-loaded?
-					this.notify('data-cached', {
-						data: cachedData.data,
-						filters: cleanedFilters,
-						cached: true
-					});
-					return cachedData.data;
-				}
+			// Handle 304 Not Modified
+			if (response.status === 304 && cachedData) {
+				// Update timestamp but keep existing data
+				cachedData.timestamp = Date.now();
+				this.saveCache(cacheKey, cachedData);
+				return cachedData.data;
 			}
 
 			if (!response.ok) {
 				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 			}
 
-			// Store response headers for future conditional requests
-			this.storeResponseHeaders(headerKey, response);
-
 			const data = await response.json();
 
-			console.log('Fetched data: ', data);
+			// Store HTTP caching headers
+			if (this.config.useHttpCaching) {
+				this.storeResponseHeaders(cacheKey, response);
+			}
 
 			// Cache the response
 			const cacheEntry = {
 				key: cacheKey,
-				endpoint: apiEndpoint,
 				data: data,
 				timestamp: Date.now(),
-				filters: cleanedFilters
+				endpoint: this.config.endpoint,
+				filters: filters
 			};
 
 			this.cache.set(cacheKey, cacheEntry);
-			await this.saveCacheToDB(cacheKey, cacheEntry);
+			this.saveCache(cacheKey, cacheEntry);
 
-			// Update items if data contains them
-			if (data.items && this.config.endpoint === apiEndpoint) {
-				this.updateItems(data.items);
+			// Process and store items
+			if (Array.isArray(data)) {
+				await this.saveMany(data);
+			} else if (data.items) {
+				await this.saveMany(data.items);
 			}
 
-			// Store current request info
-			this.currentRequest = {
-				filters: cleanedFilters,
-				data: data,
-				cached: false
-			};
-
-			this.notify('data-fetched', {
-				endpoint: apiEndpoint,
-				data: data,
-				filters: cleanedFilters
-			});
 			return data;
 
 		} catch (error) {
 			console.error('Fetch error:', error);
 
-			// Try to return stale cache on error
+			// Return cached data if available, even if expired
 			if (cachedData) {
-				console.warn('Returning stale cache due to fetch error');
-				this.currentRequest = {
-					filters: cleanedFilters,
-					data: cachedData.data,
-					cached: true,
-					stale: true
-				};
-				this.notify('stale-cache-used', {
-					data: cachedData.data,
-					filters: cleanedFilters
-				});
+				console.warn('Using stale cache due to fetch error');
 				return cachedData.data;
 			}
 
-			this.notify('fetch-error', { error, filters: cleanedFilters });
 			throw error;
 		} finally {
 			if (this.config.showLoading) {
@@ -373,209 +515,6 @@
 		}
 	}
 
-	/**
-	 * Update items in local store
-	 */
-	updateItems(items) {
-		this.items.clear();
-		items.forEach(item => {
-			this.items.set(item.id, item);
-		});
-		this.saveItemsToDB();
-		this.notify('items-updated', { items });
-	}
-
-	/**
-	 * Get current request data and state
-	 */
-	getCurrentRequest() {
-		return this.currentRequest;
-	}
-
-	/**
-	 * Get a specific item by ID
-	 */
-	getItem(id) {
-		let check = parseInt(id);
-		id = isNaN(check) ? id : check;
-		const item = this.items.get(id);
-		return item ? this.unserializeData(item) : null;
-	}
-
-	setItem(id, data, mergeExisting = true) {
-		if (mergeExisting && this.items.has(id)) {
-			let existing = this.getItem(id); // Get unserialized version
-			data = window.deepMerge(existing, data);
-		}
-
-		const serialized = this.serializeData(data);
-		this.items.set(id, serialized); // Store serialized version
-		this.saveItemsToDB();
-		this.notify('item-stored', data); // Notify with original data
-		return data;
-	}
-
-	hasUnrecoverableFiles(data) {
-		if (!data || typeof data !== 'object') return false;
-
-		if (data._wasFile || data._wasBlob) return true;
-
-		if (Array.isArray(data)) {
-			return data.some(item => this.hasUnrecoverableFiles(item));
-		}
-
-		if (data instanceof FormData) {
-			for (const [key, value] of data.entries()) {
-				if (value instanceof File || value instanceof Blob) return true;
-			}
-			return false;
-		}
-
-		return Object.values(data).some(value => this.hasUnrecoverableFiles(value));
-	}
-
-	serializeFormData(formData) {
-		const obj = {};
-
-		for (const [key, value] of formData.entries()) {
-			// Handle file metadata (can't store actual file)
-			if (value instanceof File) {
-				continue;
-			}
-			// Check if key already exists (for multiple values)
-			if (key in obj) {
-				// Convert to array if not already
-				if (!Array.isArray(obj[key])) {
-					obj[key] = [obj[key]];
-				}
-				obj[key].push(value);
-			} else {
-				obj[key] = value;
-			}
-		}
-		return obj;
-	}
-
-	serializeData(data) {
-		if (!data) return null;
-
-		if (data instanceof HTMLElement) {
-			return null;
-		}
-		if (typeof data !== 'object') return data;
-
-		if (data === null) return null;
-
-		if (data instanceof FormData) {
-			return {
-				_type: 'FormData',
-				... this.serializeFormData(data)
-			};
-		}
-
-		// Handle Arrays
-		if (Array.isArray(data)) {
-			return data.map(item => this.serializeData(item));
-		}
-
-		// Handle Date objects
-		if (data instanceof Date) {
-			return {
-				_type: 'Date',
-				value: data.toISOString()
-			};
-		}
-
-		// Handle plain objects
-		const output = {};
-		for (const [key, value] of Object.entries(data)) {
-			output[key] = this.serializeData(value);
-		}
-		return output;
-	}
-
-	unserializeData(data) {
-		if (!data || typeof data !== 'object') return data;
-		if (data === null) return null;
-
-		// Check for special types
-		if (data._type) {
-			switch (data._type) {
-				case 'FormData':
-					return this.unserializeFormData(data);
-				case 'File':
-					// Can't reconstruct File, return metadata with warning flag
-					return {
-						_wasFile: true,
-						_fileMetadata: data,
-						name: data.name,
-						type: data.type,
-						size: data.size
-					};
-				case 'Blob':
-					// Can't reconstruct Blob
-					return {
-						_wasBlob: true,
-						_blobMetadata: data,
-						type: data.type,
-						size: data.size
-					};
-				case 'Date':
-					return new Date(data.value);
-			}
-		}
-
-		// Handle Arrays
-		if (Array.isArray(data)) {
-			return data.map(item => this.unserializeData(item));
-		}
-
-		// Handle plain objects
-		const output = {};
-		for (const [key, value] of Object.entries(data)) { // Fixed: 'of' not 'in'
-			output[key] = this.unserializeData(value);
-		}
-		return output;
-	}
-	unserializeFormData(data) {
-		const formData = new FormData();
-
-		for (const [key, value] of Object.entries(data)) {
-			if (Array.isArray(value)) {
-				value.forEach(item => {
-					if (item?._isFile) {
-						console.warn(`Cannot restore file "${item.name}" from stored data`);
-						// Optionally append metadata as JSON string for reference
-						formData.append(key + '_was_file', JSON.stringify(item));
-					} else {
-						formData.append(key, item);
-					}
-				});
-			} else if (value?._isFile) {
-				console.warn(`Cannot restore file "${value.name}" from stored data`);
-				// Optionally append metadata as JSON string for reference
-				formData.append(key + '_was_file', JSON.stringify(value));
-			} else if (value !== null && value !== undefined) {
-				formData.append(key, value);
-			}
-		}
-
-		return formData;
-	}
-
-
-	clearItem(key) {
-		this.items.delete(key);
-		if (this.db) {
-			const tx = this.db.transaction(['items'], 'readwrite');
-			const store = tx.objectStore('items');
-			store.delete(key);
-		}
-	}
-
-	/**
-	 * Filter helpers
-	 */
 	cleanFilters(filters) {
 		const cleaned = {};
 		Object.entries(filters).forEach(([key, value]) => {
@@ -600,7 +539,29 @@
 		return cleaned;
 	}
 
+	/**
+	 * Generate cache key from filters
+	 */
+	generateCacheKey(filters) {
+		if (this.config.cacheKeyStrategy === 'custom' && this.config.generateCacheKey) {
+			return this.config.generateCacheKey(filters);
+		}
+
+		// Default strategy: sort keys and create string
+		const sorted = Object.keys(filters)
+			.sort()
+			.reduce((acc, key) => {
+				acc[key] = filters[key];
+				return acc;
+			}, {});
+
+		return JSON.stringify(sorted);
+	}
+
 	setFilter(key, value) {
+		if (!this.filters) {
+			this.filters = {};
+		}
 		const oldValue = this.filters[key];
 
 		if (value === '' || value === null || value === undefined) {
@@ -660,25 +621,28 @@
 	}
 
 	/**
-	 * Cache management
+	 * Set multiple filters at once
 	 */
-	generateCacheKey(endpoint, filters) {
-		const sorted = Object.keys(filters).sort().reduce((obj, key) => {
-			obj[key] = filters[key];
-			return obj;
-		}, {});
-		return `${endpoint}_${JSON.stringify(sorted)}`;
+	setFilters(filters) {
+		this.filters = { ...this.filters, ...filters };
+		if (this.config.autoFetch !== false) {
+			return this.fetch(this.filters);
+		}
 	}
 
-	generateHeaderKey(url) {
-		return `headers_${url}`;
-	}
-
-	isCacheValid(cacheEntry, maxAge = this.config.TTL) {
+	/**
+	 * Check if cache entry is still valid
+	 */
+	isCacheValid(cacheEntry) {
 		if (!cacheEntry || !cacheEntry.timestamp) return false;
-		return (Date.now() - cacheEntry.timestamp) < maxAge;
+
+		const age = Date.now() - cacheEntry.timestamp;
+		return age < this.config.TTL;
 	}
 
+	/**
+	 * Store HTTP response headers for caching
+	 */
 	storeResponseHeaders(key, response) {
 		const headers = {
 			key,
@@ -688,10 +652,167 @@
 		};
 
 		this.httpHeaders.set(key, headers);
-		this.saveHeadersToDB(key, headers);
+
+		if (this.db) {
+			const tx = this.db.transaction(['headers'], 'readwrite');
+			const store = tx.objectStore('headers');
+			store.put(headers);
+		}
+	}
+
+	/**
+	 * Save cache entry to IndexedDB
+	 */
+	async saveCache(key, data) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['cache'], 'readwrite');
+		const store = tx.objectStore('cache');
+		await store.put(data);
+	}
+
+	/**
+	 * Load cache from IndexedDB
+	 */
+	async loadCache() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['cache'], 'readonly');
+			const store = tx.objectStore('cache');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(item => {
+					if (this.isCacheValid(item)) {
+						this.cache.set(item.key, item);
+					}
+				});
+				resolve();
+			};
+		});
+	}
+
+	/**
+	 * Load HTTP headers from IndexedDB
+	 */
+	async loadHeaders() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['headers'], 'readonly');
+			const store = tx.objectStore('headers');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(header => {
+					this.httpHeaders.set(header.key, header);
+				});
+				resolve();
+			};
+		});
 	}
 
 
+	/**
+	 * Subscribe to store events
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	/**
+	 * Notify subscribers of events
+	 */
+	notify(event, data = {}) {
+		this.subscribers.forEach(callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
+			}
+		});
+	}
+
+	/**
+	 * Query items using an index
+	 */
+	async query(indexName, value) {
+		if (!this.db) return [];
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
+
+			if (!store.indexNames.contains(indexName)) {
+				reject(new Error(`Index ${indexName} does not exist`));
+				return;
+			}
+
+			const index = store.index(indexName);
+			const request = value !== undefined
+				? index.getAll(value)
+				: index.getAll();
+
+			request.onsuccess = (e) => {
+				const results = e.target.result.map(item => {
+					return this.config.stripDOMReferences
+						? this.stripDOMReferences(item)
+						: item;
+				});
+				resolve(results);
+			};
+
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+	/**
+	 * Count items in store
+	 */
+	async count() {
+		if (!this.db) return this.data.size;
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
+			const request = store.count();
+
+			request.onsuccess = (e) => resolve(e.target.result);
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+
+	setLoading(on) {
+		this.body.classList.toggle('loading', on);
+		if (on) {
+			this.loading.showModal();
+		} else {
+			this.loading.close();
+		}
+
+	}
+
+	/**
+	 * Cleanup and destroy
+	 */
+	destroy() {
+		if (this.currentRequest) {
+			this.currentRequest.abort();
+		}
+
+		this.subscribers.clear();
+		this.data.clear();
+		this.cache.clear();
+		this.httpHeaders.clear();
+
+		if (this.db) {
+			this.db.close();
+			this.db = null;
+		}
+	}
 
 	clearCache() {
 		this.cache.clear();
@@ -704,231 +825,7 @@
 
 		this.notify('cache-cleared');
 	}
-
-	invalidateCache(pattern) {
-		const keysToDelete = [];
-
-		this.cache.forEach((value, key) => {
-			if (typeof pattern === 'string' && key.includes(pattern)) {
-				keysToDelete.push(key);
-			} else if (pattern instanceof RegExp && pattern.test(key)) {
-				keysToDelete.push(key);
-			}
-		});
-
-		keysToDelete.forEach(key => {
-			this.cache.delete(key);
-			if (this.db) {
-				const tx = this.db.transaction(['cache'], 'readwrite');
-				const store = tx.objectStore('cache');
-				store.delete(key);
-			}
-		});
-
-		this.notify('cache-invalidated', { count: keysToDelete.length });
-	}
-
-	/**
-	 * DOM Cache Management
-	 */
-
-	/**
-	 * Store rendered DOM element for a specific item and view
-	 */
-	storeDOMElement(itemId, viewName, element) {
-		if (!this.domCache.has(itemId)) {
-			this.domCache.set(itemId, {});
-		}
-
-		const itemCache = this.domCache.get(itemId);
-		itemCache[viewName] = element.cloneNode(true);
-		this.domCache.set(itemId, itemCache);
-
-		// Save to IndexedDB
-		this.saveDOMCacheToDB(itemId, itemCache);
-	}
-
-
-	/**
-	 * Retrieve cached DOM element for a specific item and view
-	 */
-	getDOMElement(itemId, viewName) {
-		const itemCache = this.domCache.get(itemId);
-		if (itemCache && itemCache[viewName]) {
-			return itemCache[viewName].cloneNode(true);
-		}
-		return null;
-	}
-
-	/**
-	 * Check if DOM element exists in cache
-	 */
-	hasDOMElement(itemId, viewName) {
-		const itemCache = this.domCache.get(itemId);
-		return itemCache && itemCache[viewName];
-	}
-
-	/**
-	 * Clear DOM cache for a specific item
-	 */
-	clearDOMCache(itemId) {
-		this.domCache.delete(itemId);
-
-		if (this.db) {
-			const tx = this.db.transaction(['dom'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.delete(itemId);
-		}
-	}
-
-	/**
-	 * Clear all DOM cache
-	 */
-	clearAllDOMCache() {
-		this.domCache.clear();
-
-		if (this.db) {
-			const tx = this.db.transaction(['dom'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.clear();
-		}
-	}
-
-	/**
-	 * Helper method to render or retrieve cached DOM elements
-	 */
-	renderOrRetrieve(item, viewName, renderFunction) {
-		// Check cache first
-		const cached = this.getDOMElement(item.id, viewName);
-		if (cached) {
-			return cached;
-		}
-
-		// Render new element
-		const element = renderFunction(item);
-
-		// Cache the rendered element
-		this.storeDOMElement(item.id, viewName, element);
-
-		return element;
-	}
-	/**
-	 * Database operations
-	 */
-	async saveItemsToDB() {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['items'], 'readwrite');
-		const store = tx.objectStore('items');
-
-		store.clear();
-		this.items.forEach(item => {
-			if (!item._deleted) {
-				store.put(item);
-			}
-		});
-	}
-
-	async saveCacheToDB(key, data) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['cache'], 'readwrite');
-		const store = tx.objectStore('cache');
-		store.put(data);
-	}
-
-	async saveHeadersToDB(key, headers) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['headers'], 'readwrite');
-		const store = tx.objectStore('headers');
-		store.put(headers);
-	}
-
-	async saveDOMCacheToDB(itemId, domCache) {
-		if (!this.db) return;
-
-		// Convert DOM elements to HTML strings for storage
-		const serialized = {
-			id: itemId,
-			views: {}
-		};
-
-		Object.entries(domCache).forEach(([viewName, element]) => {
-			if (element && element.outerHTML) {
-				serialized.views[viewName] = element.outerHTML;
-			}
-		});
-
-		const tx = this.db.transaction(['dom'], 'readwrite');
-		const store = tx.objectStore('dom');
-		store.put(serialized);
-	}
-
-	async saveFormsToDB(key, form) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['forms'], 'readwrite');
-		const store = tx.objectStore('forms');
-		store.put(form);
-	}
-
-	storeForm(key, form) {
-		this.forms.set(key, form);
-		this.saveFormsToDB(key, form);
-	}
-
-	getForm(key) {
-		return this.forms.has(key) ? this.forms.get(key) : null;
-	}
-
-	getAllForms() {
-		return this.forms;
-	}
-
-	clearForm(key) {
-		this.forms.delete(key);
-		if (this.db) {
-			const tx = this.db.transaction(['forms'], 'readwrite');
-			const store = tx.objectStore('forms');
-			store.delete(key);
-		}
-	}
-
-	clearAllForms() {
-		this.forms.clear();
-		if (this.db) {
-			const tx = this.db.transaction(['forms'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.clear();
-		}
-	}
-
-	/**
-	 * Event system
-	 */
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-
-	/**
-	 * Cleanup
-	 */
-	destroy() {
-		if (this.db) {
-			this.db.close();
-		}
-		this.subscribers.clear();
-		this.items.clear();
-		this.cache.clear();
-		this.domCache.clear();
-		this.httpHeaders.clear();
-	}
 }
 
+// Export for use
 window.jvbStore = DataStore;

--
Gitblit v1.10.0