From a81f7043fc44382775f9afac48e4c7a651e7ac6c Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 04 Jan 2026 18:29:10 +0000
Subject: [PATCH] =PopulateForm.js and ContentRoutes.php minor changes

---
 assets/js/concise/DataStoreOld.js | 1812 +++++++++++++++++++++++++++++++--------------------------
 1 files changed, 990 insertions(+), 822 deletions(-)

diff --git a/assets/js/concise/DataStoreOld.js b/assets/js/concise/DataStoreOld.js
index 3686ac2..4aa4119 100644
--- a/assets/js/concise/DataStoreOld.js
+++ b/assets/js/concise/DataStoreOld.js
@@ -1,682 +1,1014 @@
 /**
- * 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
+ * DataStore - Singleton pattern managing multiple store namespaces
+ *
+ * Usage:
+ *   window.jvbStore = new DataStore();
+ *   this.store = window.jvbStore.register('feed', { config });
  */
-class DataStore {
-	constructor(config = {}) {
-		this.config = {
-			name: 'default',
-			endpoint: false,
-			apiBase: jvbSettings.api,
-			TTL: 3600000, // 1 hour default
-			showLoading: true,
-			headers: {},
-			filters: {},
-			...config
-		};
-		if (!this.config.endpoint) {
-			console.warn('No endpoint set. Only saving locally');
+class DataStoreOld {
+	constructor() {
+		// Singleton pattern
+		if (DataStore.instance) {
+			return DataStore.instance;
 		}
+		DataStore.instance = this;
 
+		// Shared resources
+		this.dbConfig = new Map();		// Definitions for the databases
+		this.databases = new Map();     // Shared IndexedDB connections
+		this.stores = new Map();        // Registered store namespaces
+		this.subscribers = new Map();   // Per-store event subscribers
+		this.pendingInits = new Map();  // Track initialization promises
+		this.fetchQueue = [];
+
+		// Global state
+		this._initialized = false;
 		this.body = document.body;
 		this.loading = document.querySelector('dialog.loading');
 
-		this.headers = {
-			'X-WP-Nonce': jvbSettings.nonce,
-			...this.config.headers
-		};
+		this.init();
 
-		// 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();
-
-		this.initDB();
-		document.addEventListener('beforeUnload', () =>this.destroy());
+		// window.addEventListener('beforeunload', () => this.destroy());
 	}
 
-	async initDB() {
-		if (!('indexedDB' in window)) return;
+	async init() {
+		if (this._initialized) return;
+		this._initialized = true;
 
-		const request = indexedDB.open(`jvb_${this.config.name}_db`, 1);
-
-		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',
-				});
-				forms.createIndex('status', 'status', {unique:false});
-				forms.createIndex('operationId', 'operationId', {unique:false});
-				forms.createIndex('timestamp', 'timestamp', {unique:false});
-			}
-
-			// Cache store for GET requests with endpoint index
-			if (!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')) {
-				db.createObjectStore('headers', { keyPath: 'key' });
-			}
-		};
-
-		request.onsuccess = (e) => {
-			this.db = e.target.result;
-			this.loadFromDB();
-		};
-
-		request.onerror = (e) => {
-			console.error('IndexedDB error:', e);
-		};
-	}
-
-	async loadFromDB() {
-		if (!this.db) return;
-
-		try {
-			await Promise.all([
-				this.loadItems(),
-				this.loadCache(),
-				this.loadHeaders(),
-				this.loadDOMCache(),
-				this.loadForms()
-			]);
-		} catch (error) {
-			console.error('Error loading from DB:', error);
+		if (!('indexedDB' in window)) {
+			console.warn('IndexedDB not supported');
 		}
 	}
 
-	async loadItems() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['items'], 'readonly');
-			const store = tx.objectStore('items');
-			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();
-			};
-		});
-	}
-
-	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();
-			};
-		});
-	}
-
-	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
+	 * Register a new store namespace
+	 * @param {string} name Database Name
+	 * @param {object|array} configs An object defining the store, or an array of objects defining the stores
+	 * @param {number} version the database version
 	 */
-	async fetch(endpoint = null, options = {}) {
-		const {
-			filters = this.filters,
-			headers = {},
-		} = options;
+	register(name, configs = [], version = 1.1) {
+		if (!Array.isArray(configs)) configs = [configs];
+		if (configs.length === 0) return;
 
-		if (this.config.showLoading) {
-			this.setLoading(true);
-		}
-
-
-		// Use provided endpoint or config endpoint
-		const apiEndpoint = endpoint || this.config.endpoint;
-		if (!apiEndpoint) {
-			throw new Error('No endpoint specified');
-		}
-
-		// 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
-		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;
-			}
-		}
-
-		try {
-			const response = await fetch(url, {
-				method: 'GET',
-				headers: requestHeaders
+		if (!this.dbConfig.has(name)) {
+			this.dbConfig.set(name, {
+				dbName: `jvb_${name}`,
+				version: version,
+				stores: {},
+				_initialized: false
 			});
-
-			// 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;
-				}
-			}
-
-			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();
-
-			// Cache the response
-			const cacheEntry = {
-				key: cacheKey,
-				endpoint: apiEndpoint,
-				data: data,
-				timestamp: Date.now(),
-				filters: cleanedFilters
-			};
-
-			this.cache.set(cacheKey, cacheEntry);
-			await this.saveCacheToDB(cacheKey, cacheEntry);
-
-			// Update items if data contains them
-			if (data.items && this.config.endpoint === apiEndpoint) {
-				this.updateItems(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
-			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
-				});
-				return cachedData.data;
-			}
-
-			this.notify('fetch-error', { error, filters: cleanedFilters });
-			throw error;
-		} finally {
-			if (this.config.showLoading) {
-				this.setLoading(false);
-			}
 		}
-	}
 
-	/**
-	 * Update items in local store
-	 */
-	updateItems(items) {
-		this.items.clear();
-		items.forEach(item => {
-			this.items.set(item.id, item);
+		let dbEntry = this.dbConfig.get(name);
+
+		configs.forEach(config => {
+			if (!config.storeName) {
+				throw new Error(`Store config for "${name}" missing storeName`);
+			}
+			if (!config.keyPath) {
+				throw new Error(`Store "${config.storeName}" requires keyPath`);
+			}
+
+
+			const storeKey = `${name}_${config.storeName}`;
+
+			const store = {
+				config: {
+					// Storage
+					dbName: dbEntry.dbName,
+					storeName: 'items',
+					keyPath: 'id',
+					indexes: [],
+
+					// API
+					endpoint: null,
+					apiBase: jvbSettings.api,
+					filters: {},
+					required: null,
+
+					// Cache
+					TTL: 3600000, // 1 hour
+					useHttpCaching: true,
+
+					// Behavior
+					showLoading: false,
+					delayFetch: true,
+					validateData: true, // Validate data is serializable
+					...config
+				},
+				dbKey: name,
+				storeKey: storeKey,
+				data: new Map(),
+				cache: new Map(),
+				httpHeaders: new Map(),
+				subscribers: new Map(),
+				filters: {...(config.filters || {}) },
+				isFetching: false,
+				currentRequest: null,
+				lastResponse: null,
+				_initialized: false
+			};
+
+			store.config.headers = {
+				'X-WP-Nonce': window.auth.getNonce(),
+				...store.config.headers
+			};
+
+			dbEntry.stores[config.storeName] = storeKey;
+
+			this.stores.set(storeKey, store);
+			if (!this.subscribers.has(storeKey)) {
+				this.subscribers.set(storeKey, new Set());
+			}
 		});
-		this.saveItemsToDB();
-		this.notify('items-updated', { items });
+
+
+		// Initialize database asynchronously
+		this.initDB(name).catch(error => {
+			console.error(`Failed to initialize store "${name}":`, error);
+		});
+
+		const apis = {};
+		for (const [storeName, storeKey] of Object.entries(dbEntry.stores)) {
+			apis[storeName] = this.getStoreAPI(storeKey);
+		}
+		return apis;
 	}
 
 	/**
-	 * Get current request data and state
+	 * Get the API object for a registered store
 	 */
-	getCurrentRequest() {
-		return this.currentRequest;
+	getStoreAPI(name) {
+		const api = {
+			// Data methods
+			fetch: () => this.fetch(name),
+			save: (item) => this.save(name, item),
+			delete: (id) => this.delete(name, id),
+			get: (id) => this.get(name, id),
+			getAll: () => this.getAll(name),
+			getFiltered: () => this.getFiltered(name),
+			clear: () => this.clear(name),
+
+			// Filter methods
+			setFilter: (key, value) => this.setFilter(name, key, value),
+			setFilters: (filters) => this.setFilters(name, filters),
+			removeFilter: (key) => this.removeFilter(name, key),
+			clearFilters: () => this.clearFilters(name),
+
+			// Cache methods
+			clearCache: () => this.clearCache(name),
+			clearHttpHeaders: (key) => this.clearHttpHeaders(name, key),
+
+			// Event methods
+			subscribe: (callback) => this.subscribe(name, callback),
+
+			// Utility
+			ensureInitialized: () => this.ensureStoreInitialized(name),
+
+			// Exposed properties (read-only)
+			get filters() {
+				return { ...api.getStore().filters };
+			},
+			get lastResponse() {
+				return api.getStore().lastResponse;
+			},
+			get data() {
+				return api.getStore().data;
+			},
+
+			getStore: () => this.stores.get(name)
+		};
+
+		return api;
 	}
 
 	/**
-	 * Get a specific item by ID
+	 * Convert FormData to plain object for storage
 	 */
-	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 = {};
+	formDataToObject(formData) {
+		const obj = {
+			_isFormData: true,
+			entries: {}
+		};
 
 		for (const [key, value] of formData.entries()) {
-			// Handle file metadata (can't store actual file)
-			if (value instanceof File) {
+			// Skip File/Blob objects - they're stored separately in UploadManager
+			if (value instanceof File || value instanceof Blob) {
 				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]];
+
+			// Handle multiple values for same key
+			if (obj.entries[key]) {
+				if (!Array.isArray(obj.entries[key])) {
+					obj.entries[key] = [obj.entries[key]];
 				}
-				obj[key].push(value);
+				obj.entries[key].push(value);
 			} else {
-				obj[key] = value;
+				obj.entries[key] = value;
 			}
 		}
+
 		return obj;
 	}
 
-	serializeData(data) {
-		if (!data) return null;
+	/**
+	 * Convert stored object back to FormData
+	 */
+	async objectToFormData(obj) {
+		if (!obj._isFormData) return obj;
 
-		if (data instanceof HTMLElement) {
-			return null;
-		}
-		if (typeof data !== 'object') return data;
+		const formData = new FormData();
 
-		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);
+		// Restore text entries
+		for (const [key, value] of Object.entries(obj.entries)) {
+			if (Array.isArray(value)) {
+				value.forEach(v => formData.append(key, v));
+			} else {
+				formData.append(key, value);
 			}
 		}
 
-		// Handle Arrays
-		if (Array.isArray(data)) {
-			return data.map(item => this.unserializeData(item));
-		}
+		if (window.jvbUploads && obj.entries.upload_ids) {
+			const uploadIds = JSON.parse(obj.entries.upload_ids);
 
-		// 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);
+			for (const uploadId of uploadIds) {
+				const file = await window.jvbUploads.getBlobData(uploadId);
+				if (file) {
+					formData.append('files[]', file);
+				}
 			}
 		}
 
 		return formData;
 	}
 
+	/**
+	 * Initialize database for a specific store
+	 */
+	async initDB(name) {
+		const db = this.dbConfig.get(name);
+		if (!db || db._initialized) return;
 
-	clearItem(key) {
-		this.items.delete(key);
-		if (this.db) {
-			const tx = this.db.transaction(['items'], 'readwrite');
-			const store = tx.objectStore('items');
-			store.delete(key);
+		if (this.pendingInits.has(name)) {
+			return this.pendingInits.get(name);
+		}
+
+		const initPromise = this._performDBInit(name);
+		this.pendingInits.set(name, initPromise);
+
+		try {
+			await initPromise;
+			db._initialized = true;
+		} finally {
+			this.pendingInits.delete(name);
 		}
 	}
 
-	/**
-	 * Filter helpers
-	 */
-	cleanFilters(filters) {
-		const cleaned = {};
-		Object.entries(filters).forEach(([key, value]) => {
-			if (value !== null && value !== undefined && value !== '') {
-				// Handle special cases based on existing patterns
-				if (key === 'taxonomies' && typeof value === 'object') {
-					Object.entries(value).forEach(([taxName, terms]) => {
-						if (Array.isArray(terms) && terms.length > 0) {
-							cleaned[`tax_${taxName}`] = terms.join(',');
-						} else if (terms) {
-							cleaned[`tax_${taxName}`] = terms;
+	async _performDBInit(name) {
+		const database = this.dbConfig.get(name);
+		const { dbName, version } = database;
+		const stores = Object.values(database.stores);
+
+		try {
+			if (!this.databases.has(dbName)) {
+				const db = await this.openDatabase(dbName, version, (db) => {
+					stores.forEach(store => {
+						let storeObj = this.stores.get(store);
+						if (storeObj) {
+							this.setupStores(db, storeObj.config);
 						}
 					});
-				} else if (key === 'date' && typeof value === 'object') {
-					if (value.after) cleaned.after = value.after;
-					if (value.before) cleaned.before = value.before;
+				});
+				this.databases.set(dbName, db);
+			}
+
+			stores.forEach(storeName => {
+				let store = this.stores.get(storeName);
+				if (store) {
+					store.db = this.databases.get(dbName);
+					store._initialized = true;
+					this.loadStoreDataInBackground(storeName);
+					this.notify(storeName, 'db-init');
+				}
+			})
+
+		} catch (error) {
+			console.error(`Failed to initialize database for store "${name}":`, error);
+			throw error;
+		}
+	}
+
+	openDatabase(dbName, version, onUpgrade) {
+		return new Promise((resolve, reject) => {
+			const request = indexedDB.open(dbName, version);
+
+			request.onupgradeneeded = (e) => {
+				if (onUpgrade) {
+					onUpgrade(e.target.result, e.oldVersion, e.newVersion);
+				}
+			};
+
+			request.onsuccess = (e) => resolve(e.target.result);
+			request.onerror = (e) => reject(e.target.error);
+			request.onblocked = () => {
+				console.warn(`Database ${dbName} blocked. Close other tabs.`);
+			};
+		});
+	}
+
+	setupStores(db, config) {
+		// Main store
+		if (!db.objectStoreNames.contains(config.storeName)) {
+			const store = db.createObjectStore(config.storeName, {
+				keyPath: config.keyPath
+			});
+
+			config.indexes.forEach(index => {
+				store.createIndex(
+					index.name,
+					index.keyPath || index.name,
+					{ unique: index.unique || false }
+				);
+			});
+		}
+
+		// Cache store
+		if (config.endpoint && !db.objectStoreNames.contains('cache')) {
+			const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
+			cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
+		}
+
+		// HTTP headers store
+		if (config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
+			db.createObjectStore('headers', { keyPath: 'key' });
+		}
+	}
+
+	loadStoreDataInBackground(name) {
+		const store = this.stores.get(name);
+		if (!store?.db) return;
+
+		const tasks = [
+			this.loadStoreData(name),
+			this.loadStoreCache(name),
+			this.loadStoreHeaders(name)
+		];
+
+		Promise.all(tasks)
+			.then(() => {
+				this.notify(name, 'data-ready');
+
+				// Add to fetch queue instead of immediate fetch
+				if (store.config.endpoint && store.config.delayFetch) {
+					this.fetchQueue.push(name);
+
+					// Start processing queue if not already running
+					if (this.fetchQueue.length === 1) {
+						this.processFetchQueue();
+					}
+				} else if (store.config.endpoint && !store.config.delayFetch) {
+					// Immediate fetch
+					if ('requestIdleCallback' in window) {
+						requestIdleCallback(() => this.fetch(name), { timeout: 2000 });
+					} else {
+						setTimeout(() => this.fetch(name), 100);
+					}
+				}
+			})
+			.catch(error => {
+				console.error(`Background load error for store "${name}":`, error);
+			});
+	}
+
+	async processFetchQueue() {
+		if (this.fetchQueue.length === 0) return;
+
+		const name = this.fetchQueue.shift();
+		const store = this.stores.get(name);
+
+		if (!store) {
+			// Store was removed, continue with next
+			return this.processFetchQueue();
+		}
+
+		try {
+			await this.fetch(name);
+		} catch (error) {
+			console.error(`Queue fetch error for "${name}":`, error);
+		}
+
+		// Process next item with idle callback
+		if (this.fetchQueue.length > 0) {
+			if ('requestIdleCallback' in window) {
+				requestIdleCallback(() => this.processFetchQueue(), { timeout: 2000 });
+			} else {
+				setTimeout(() => this.processFetchQueue(), 50);
+			}
+		}
+	}
+
+	async loadStoreData(name) {
+		const store = this.stores.get(name);
+		if (!store?.db) return;
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction([store.config.storeName], 'readonly');
+			const objectStore = tx.objectStore(store.config.storeName);
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				const items = e.target.result || [];
+				items.forEach(item => {
+					const key = this.getItemKey(item, store.config.keyPath);
+					store.data.set(key, item);
+				});
+				this.notify(name, 'data-loaded', { count: items.length });
+				resolve(items);
+			};
+
+			request.onerror = () => resolve([]);
+		});
+	}
+
+	async loadStoreCache(name) {
+		const store = this.stores.get(name);
+		if (!store?.db || !store.db.objectStoreNames.contains('cache')) return;
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction(['cache'], 'readonly');
+			const objectStore = tx.objectStore('cache');
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				(e.target.result || []).forEach(item => {
+					if (this.isCacheValid(item, store.config.TTL)) {
+						store.cache.set(item.key, item);
+					}
+				});
+				resolve();
+			};
+
+			request.onerror = () => resolve();
+		});
+	}
+
+	async loadStoreHeaders(name) {
+		const store = this.stores.get(name);
+		if (!store?.db || !store.db.objectStoreNames.contains('headers')) return;
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction(['headers'], 'readonly');
+			const objectStore = tx.objectStore('headers');
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				(e.target.result || []).forEach(header => {
+					store.httpHeaders.set(header.key, header);
+				});
+				resolve();
+			};
+
+			request.onerror = () => resolve();
+		});
+	}
+
+	async ensureStoreInitialized(name) {
+		const store = this.stores.get(name);
+		if (!store) {
+			throw new Error(`Store "${name}" not registered`);
+		}
+
+		if (!store._initialized) {
+			await this.initDB(store.dbKey);
+		}
+	}
+
+	async fetch(name) {
+		await this.ensureStoreInitialized(name);
+
+		const store = this.stores.get(name);
+
+		if (store.isFetching) return;
+
+		// Check required filters
+		if (store.config.required) {
+			const required = Array.isArray(store.config.required)
+				? store.config.required
+				: [store.config.required];
+
+			const missing = required.some(key =>
+				!store.filters[key] || store.filters[key] === ''
+			);
+
+			if (missing) return;
+		}
+
+		store.isFetching = true;
+
+		try {
+			// Check cache
+			const cacheKey = this.generateCacheKey(store.filters);
+			const cached = store.cache.get(cacheKey);
+
+			if (cached && this.isCacheValid(cached, store.config.TTL)) {
+				this.notify(name, 'data-loaded', {
+					cached: true,
+					items: cached.items || []
+				});
+				return cached;
+			}
+
+			if (store.config.showLoading) {
+				this.setLoading(true);
+			}
+
+			const url = this.buildFetchUrl(name);
+			const headers = { ...store.config.headers };
+			const cachedHeaders = store.httpHeaders.get(cacheKey);
+
+			if (store.config.useHttpCaching && cachedHeaders) {
+				if (cachedHeaders.etag) {
+					headers['If-None-Match'] = cachedHeaders.etag;
+				}
+				if (cachedHeaders.lastModified) {
+					headers['If-Modified-Since'] = cachedHeaders.lastModified;
+				}
+			}
+
+			const controller = new AbortController();
+			store.currentRequest = controller;
+
+			const response = await fetch(url, {
+				method: 'GET',
+				headers,
+				signal: controller.signal
+			});
+
+			if (response.status === 304) {
+				// 304 means "Not Modified" - use cached data if available
+				if (cached) {
+					this.notify(name, 'data-loaded', {
+						cached: true,
+						notModified: true,
+						items: cached.items || []
+					});
+					return cached;
+				}
+
+				// No cached data but server says not modified - return empty result
+				// This can happen on first load when cache headers exist but data doesn't
+				this.notify(name, 'data-loaded', {
+					cached: false,
+					notModified: true,
+					items: []
+				});
+
+				// Initialize empty lastResponse
+				store.lastResponse = {
+					has_more: false,
+					total: 0,
+					pages: 1,
+					queue_stats: {}
+				};
+
+				return { items: [] };
+			}
+
+			// Now check for other non-OK responses
+			if (!response.ok) {
+				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+			}
+
+			const data = await response.json();
+
+			if (store.config.useHttpCaching) {
+				this.storeResponseHeaders(name, cacheKey, response);
+			}
+			await this.processFetchedData(name, data, cacheKey);
+
+			this.notify(name, 'data-loaded', {
+				cached: false,
+				items: data.items || []
+			});
+
+			return data;
+
+		} catch (error) {
+			if (error.name !== 'AbortError') {
+				console.error(`Fetch error for store "${name}":`, error);
+				this.notify(name, 'fetch-error', { error });
+			}
+			throw error;
+
+		} finally {
+			store.isFetching = false;
+			store.currentRequest = null;
+
+			if (store.config.showLoading) {
+				this.setLoading(false);
+			}
+		}
+	}
+
+	buildFetchUrl(name) {
+		const store = this.stores.get(name);
+		const params = new URLSearchParams();
+
+		Object.entries(store.filters).forEach(([key, value]) => {
+			if (value !== null && value !== undefined && value !== '') {
+				if (typeof value === 'object') {
+					params.set(key, JSON.stringify(value));
 				} else {
-					cleaned[key] = value;
+					params.set(key, value);
 				}
 			}
 		});
-		return cleaned;
-	}
 
-	setFilter(key, value) {
-		const oldValue = this.filters[key];
-
-		if (value === '' || value === null || value === undefined) {
-			delete this.filters[key];
-		} else {
-			this.filters[key] = value;
-		}
-
-		this.notify('filters-changed', {
-			filters: this.filters,
-			changed: { key, oldValue, newValue: value }
-		});
-
-		// Auto-fetch if endpoint is configured
-		if (this.config.endpoint) {
-			this.fetch();
-		}
+		const baseUrl = store.config.apiBase + store.config.endpoint;
+		return params.toString() ? `${baseUrl}?${params}` : baseUrl;
 	}
 
 	/**
-	 * Remove a filter
+	 * Process fetched data (batch from server)
 	 */
-	removeFilter(key) {
-		const oldValue = this.filters[key];
+	async processFetchedData(name, data, cacheKey) {
+		const store = this.stores.get(name);
+		const items = data.items || [];
+		const changes = []; // Track all changes
+
+		// Batch process with single transaction
+		if (store.db && items.length > 0) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+
+			for (const item of items) {
+				try {
+					// Use shared save logic
+					const changeInfo = await this._saveItem(name, item, false);
+					changes.push(changeInfo);
+
+					// Queue for batch write
+					await objectStore.put(changeInfo.processed);
+				} catch (error) {
+					console.error(`Error processing item:`, error);
+				}
+			}
+
+			// Wait for transaction to complete
+			await new Promise((resolve, reject) => {
+				tx.oncomplete = () => resolve();
+				tx.onerror = () => reject(tx.error);
+			});
+		}
+
+		// Update cache
+		const cacheEntry = {
+			key: cacheKey,
+			items: items.map(item => this.getItemKey(item, store.config.keyPath)),
+			timestamp: Date.now(),
+			endpoint: store.config.endpoint,
+			filters: { ...store.filters }
+		};
+
+		store.cache.set(cacheKey, cacheEntry);
+		await this.saveToCache(name, cacheKey, cacheEntry);
+
+		// Update lastResponse metadata
+		store.lastResponse = {
+			...data,
+			has_more: data.has_more || false,
+			total: data.total || items.length,
+			pages: data.pages || 1,
+			queue_stats: data.queue_stats || {}
+		};
+
+		// Emit events for items with status changes
+		changes.forEach(changeInfo => {
+			if (changeInfo.statusChanged) {
+				this.notify(name, 'item-saved', {
+					item: changeInfo.item,
+					key: changeInfo.key,
+					previousItem: changeInfo.previousItem
+				});
+			}
+		});
+	}
+
+
+	/**
+	 * Internal method: Save a single item with full tracking
+	 */
+	async _saveItem(name, item, emitEvent = true) {
+		const store = this.stores.get(name);
+
+		const result = this.processForStorage(item, store.config.validateData);
+		if (!result.valid) {
+			throw new Error(`Non-serializable data: ${result.error}`);
+		}
+		const processed = result.data;
+
+		const key = this.getItemKey(processed, store.config.keyPath);
+
+		// Capture previous state
+		const previousItem = store.data.get(key);
+
+		// Update in-memory store (with original data intact)
+		store.data.set(key, item);
+
+		// Write to IndexedDB happens in batch transaction (if provided)
+		// or individual transaction (if not)
+
+		// Return change info for event emission
+		return {
+			item,
+			previousItem,
+			key,
+			processed,
+			statusChanged: previousItem && previousItem.status !== item.status
+		};
+	}
+
+	/**
+	 * Save single item (public API)
+	 */
+	async save(name, item) {
+		const store = this.stores.get(name);
+		const changeInfo = await this._saveItem(name, item);
+
+		// Write to IndexedDB immediately for single saves
+		if (store.db) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+			await objectStore.put(changeInfo.processed);
+		}
+
+		// Always emit for explicit saves
+		this.notify(name, 'item-saved', {
+			item: changeInfo.item,
+			key: changeInfo.key,
+			previousItem: changeInfo.previousItem
+		});
+
+		return changeInfo.key;
+	}
+
+	processForStorage(obj, validate = true, path = 'root') {
+		if (obj === null || obj === undefined) return { valid: true, data: obj };
+
+		const type = typeof obj;
+
+		// Handle primitives
+		if (['string', 'number', 'boolean'].includes(type)) {
+			return { valid: true, data: obj };
+		}
+
+		// Reject functions
+		if (type === 'function') {
+			return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null };
+		}
+
+		// DOM elements
+		if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
+			return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null };
+		}
+
+		// FormData - convert and continue
+		if (obj instanceof FormData) {
+			return validate
+				? { valid: false, error: `FormData at ${path}` }
+				: { valid: true, data: this.formDataToObject(obj) };
+		}
+
+		// Preserve safe types
+		if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+			return { valid: true, data: obj };
+		}
+
+		// Convert Sets to Arrays
+		if (obj instanceof Set) {
+			const arr = Array.from(obj);
+			return this.processForStorage(arr, validate, path);
+		}
+
+		// Convert Maps to Objects
+		if (obj instanceof Map) {
+			obj = Object.fromEntries(obj);
+		}
+
+		// Arrays
+		if (Array.isArray(obj)) {
+			const processed = [];
+			for (let i = 0; i < obj.length; i++) {
+				const result = this.processForStorage(obj[i], validate, `${path}[${i}]`);
+				if (!result.valid) return result;
+				if (result.data !== null) processed.push(result.data);
+			}
+			return { valid: true, data: processed };
+		}
+
+		// Objects
+		if (type === 'object') {
+			const processed = {};
+			for (const [key, value] of Object.entries(obj)) {
+				const result = this.processForStorage(value, validate, `${path}.${key}`);
+				if (!result.valid) return result;
+				if (result.data !== null) processed[key] = result.data;
+			}
+			return { valid: true, data: processed };
+		}
+
+		return validate
+			? { valid: false, error: `Unknown type at ${path}` }
+			: { valid: true, data: null };
+	}
+
+	async delete(name, id) {
+		const store = this.stores.get(name);
+		store.data.delete(id);
+
+		if (store.db) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+			await objectStore.delete(id);
+		}
+
+		this.notify(name, 'item-deleted', { id });
+	}
+
+	get(name, id) {
+		const store = this.stores.get(name);
+		return store.data.get(id);
+	}
+
+	getAll(name) {
+		const store = this.stores.get(name);
+		return Array.from(store.data.values());
+	}
+
+	getFiltered(name) {
+		const store = this.stores.get(name);
+		const cacheKey = this.generateCacheKey(store.filters);
+		const cacheEntry = store.cache.get(cacheKey);
+
+		if (cacheEntry && cacheEntry.items) {
+			return cacheEntry.items.reduce((acc, id) => {
+				const item = store.data.get(id);
+				if (item) acc.push(item);
+				return acc;
+			}, []);
+		}
+
+		return this.getAll(name);
+	}
+
+	async clear(name) {
+		const store = this.stores.get(name);
+		store.data.clear();
+		store.cache.clear();
+
+		if (store.db) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+			await objectStore.clear();
+		}
+
+		this.notify(name, 'data-cleared');
+	}
+
+	setFilter(name, key, value) {
+		const store = this.stores.get(name);
+		const oldValue = store.filters[key];
+
+		if (value === null || value === undefined || value === '') {
+			delete store.filters[key];
+		} else {
+			store.filters[key] = value;
+		}
+		this.notify(name, 'filters-changed', {
+			filters: store.filters,
+			changed: { key, oldValue, newValue: value }
+		});
+
+		if (store.config.endpoint) {
+			this.fetch(name);
+		}
+	}
+
+	async setFilters(name, filters) {
+		const store = this.stores.get(name);
+
+		const hasChanges = Object.keys(filters).some(
+			key => store.filters[key] !== filters[key]
+		);
+
+		if (!hasChanges) return;
+
+		store.filters = { ...store.filters, ...filters };
+
+		this.notify(name, 'filters-changed', {
+			filters: store.filters,
+			changed: filters
+		});
+
+		if (store.config.endpoint) {
+			await this.fetch(name);
+		}
+	}
+
+	removeFilter(name, key) {
+		const store = this.stores.get(name);
+		const oldValue = store.filters[key];
 
 		if (oldValue !== undefined) {
-			delete this.filters[key];
-			this.notify('filters-changed', {
-				filters: this.filters,
+			delete store.filters[key];
+
+			this.notify(name, 'filters-changed', {
+				filters: store.filters,
 				removed: { key, oldValue }
 			});
 
-			// Auto-fetch if endpoint is configured
-			if (this.config.endpoint) {
-				this.fetch();
+			if (store.config.endpoint) {
+				this.fetch(name);
 			}
 		}
 	}
 
-	/**
-	 * Clear all filters
-	 */
-	clearFilters() {
-		const oldFilters = { ...this.filters };
-		//Restore baseline filters
-		this.filters = this.config.filters;
+	clearFilters(name) {
+		const store = this.stores.get(name);
+		const oldFilters = { ...store.filters };
 
-		this.notify('filters-cleared', {
+		store.filters = { ...store.config.filters };
+
+		this.notify(name, 'filters-cleared', {
 			oldFilters,
-			filters: this.filters
+			filters: store.filters
 		});
 
-		// Auto-fetch if endpoint is configured
-		if (this.config.endpoint) {
-			this.fetch();
+		if (store.config.endpoint) {
+			this.fetch(name);
 		}
 	}
 
-	/**
-	 * Cache management
-	 */
-	generateCacheKey(endpoint, filters) {
-		const sorted = Object.keys(filters).sort().reduce((obj, key) => {
-			obj[key] = filters[key];
-			return obj;
-		}, {});
-		return `${endpoint}_${JSON.stringify(sorted)}`;
+	clearCache(name) {
+		const store = this.stores.get(name);
+		store.cache.clear();
+
+		if (store.db && store.db.objectStoreNames.contains('cache')) {
+			const tx = store.db.transaction(['cache'], 'readwrite');
+			const objectStore = tx.objectStore('cache');
+			objectStore.clear();
+		}
+
+		this.notify(name, 'cache-cleared');
 	}
 
-	generateHeaderKey(url) {
-		return `headers_${url}`;
+	clearHttpHeaders(name, cacheKey = null) {
+		const store = this.stores.get(name);
+
+		if (cacheKey) {
+			store.httpHeaders.delete(cacheKey);
+
+			if (store.db && store.db.objectStoreNames.contains('headers')) {
+				const tx = store.db.transaction(['headers'], 'readwrite');
+				const objectStore = tx.objectStore('headers');
+				objectStore.delete(cacheKey);
+			}
+		} else {
+			store.httpHeaders.clear();
+
+			if (store.db && store.db.objectStoreNames.contains('headers')) {
+				const tx = store.db.transaction(['headers'], 'readwrite');
+				const objectStore = tx.objectStore('headers');
+				objectStore.clear();
+			}
+		}
 	}
 
-	isCacheValid(cacheEntry, maxAge = this.config.TTL) {
-		if (!cacheEntry || !cacheEntry.timestamp) return false;
-		return (Date.now() - cacheEntry.timestamp) < maxAge;
+	subscribe(name, callback) {
+		if (!this.subscribers.has(name)) {
+			this.subscribers.set(name, new Set());
+		}
+		const subscribers = this.subscribers.get(name);
+		subscribers.add(callback);
+		return () => subscribers.delete(callback);
 	}
 
-	storeResponseHeaders(key, response) {
+	notify(name, event, data = {}) {
+		const subscribers = this.subscribers.get(name);
+		if (!subscribers) return;
+
+		subscribers.forEach(callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error(`Subscriber error for store "${name}":`, error);
+			}
+		});
+	}
+
+	storeResponseHeaders(name, key, response) {
+		const store = this.stores.get(name);
+
 		const headers = {
 			key,
 			etag: response.headers.get('ETag'),
@@ -684,248 +1016,84 @@
 			timestamp: Date.now()
 		};
 
-		this.httpHeaders.set(key, headers);
-		this.saveHeadersToDB(key, headers);
-	}
+		store.httpHeaders.set(key, headers);
 
-
-
-	clearCache() {
-		this.cache.clear();
-
-		if (this.db) {
-			const tx = this.db.transaction(['cache'], 'readwrite');
-			const store = tx.objectStore('cache');
-			store.clear();
-		}
-
-		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);
+		if (store.db && store.db.objectStoreNames.contains('headers')) {
+			const tx = store.db.transaction(['headers'], 'readwrite');
+			const objectStore = tx.objectStore('headers');
+			objectStore.put(headers);
 		}
 	}
 
-	/**
-	 * Clear all DOM cache
-	 */
-	clearAllDOMCache() {
-		this.domCache.clear();
+	async saveToCache(name, key, data) {
+		const store = this.stores.get(name);
+		if (!store.db || !store.db.objectStoreNames.contains('cache')) return;
 
-		if (this.db) {
-			const tx = this.db.transaction(['dom'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.clear();
+		const tx = store.db.transaction(['cache'], 'readwrite');
+		const objectStore = tx.objectStore('cache');
+		await objectStore.put(data);
+	}
+
+	generateCacheKey(filters) {
+		const normalized = Object.keys(filters)
+			.sort()
+			.reduce((acc, key) => {
+				acc[key] = filters[key];
+				return acc;
+			}, {});
+		return JSON.stringify(normalized);
+	}
+
+	isCacheValid(entry, ttl) {
+		if (!entry || !entry.timestamp) return false;
+		const age = Date.now() - entry.timestamp;
+		return age < ttl;
+	}
+
+	getItemKey(item, keyPath) {
+		if (typeof keyPath === 'function') {
+			return keyPath(item);
+		}
+
+		const keys = keyPath.split('.');
+		let value = item;
+
+		for (const key of keys) {
+			value = value?.[key];
+		}
+
+		return value;
+	}
+
+	setLoading(on) {
+		this.body.classList.toggle('loading', on);
+		if (on) {
+			this.loading?.showModal();
+		} else {
+			this.loading?.close();
 		}
 	}
 
-	/**
-	 * 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.stores.forEach(store => {
+			if (store.currentRequest) {
+				store.currentRequest.abort();
+			}
+		});
+
+		this.databases.forEach(db => db.close());
+		this.stores.clear();
 		this.subscribers.clear();
-		this.items.clear();
-		this.cache.clear();
-		this.domCache.clear();
-		this.httpHeaders.clear();
+		this.databases.clear();
+		this.pendingInits.clear();
 	}
 }
 
-window.jvbStore = DataStore;
+// Initialize singleton on DOMContentLoaded
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbStore = new DataStore();
+		}
+	});
+});

--
Gitblit v1.10.0