From 56a9a1ccf764ff7a6af8f8a2292cb07443cb4aa7 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 28 May 2026 18:19:57 +0000
Subject: [PATCH] =New Gitbit setpu

---
 assets/js/concise/DataStore.js | 2259 ++++++++++++++++++++++++++++++++++------------------------
 1 files changed, 1,325 insertions(+), 934 deletions(-)

diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index fe9b22e..284c913 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -1,355 +1,193 @@
 /**
- * ExtendedDataStore - A flexible IndexedDB wrapper with HTTP caching
+ * DataStore - Singleton pattern managing multiple store namespaces
  *
- * 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
- *
- * All notifications:
- *
-  		this.store.subscribe((event, data) => {
-  			switch (event) {
-  				case 'data-loaded':
-  					break;
-  				case 'item-saved':
-  					break;
-  				case 'items-saved':
-  					break;
-  				case 'item-deleted':
-  					break;
-  				case 'data-cleared':
-  					break;
-  				case 'filters-changed':
-  					break;
-  				case 'filters-cleared':
-  					break;
-  				case 'cache-cleared':
-  					break;
-  			}
-  		});
+ * Usage:
+ *   window.jvbStore = new DataStore();
+ *   this.store = window.jvbStore.register('feed', { config });
  */
 class DataStore {
-	constructor(config = {}) {
-		// Core configuration with sensible defaults
-		this.config = {
-			// Storage configuration
-			name: 'default',
-			version: 1,
-			storeName: 'items',
-			keyPath: 'id',
-			indexes: [], // Array of {name, keyPath, unique}
 
-			// API configuration
-			endpoint: null,
-			saveToServer: false,
-			apiBase: jvbSettings.api,
-			headers: {},
-			filters: {},
-			required:  null, //any required filters before fetching
-			icon: null,
-			getBlobs: null,
+	constructor() {
+		// Singleton pattern
+		if (DataStore.instance) {
+			return DataStore.instance;
+		}
+		DataStore.instance = this;
 
-			// Cache configuration
-			TTL: 3600000, // 1 hour default
-			useHttpCaching: true, // ETag and If-Modified-Since
-			cacheKeyStrategy: 'filters', // How to generate cache keys
+		// 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 = [];
 
-			// UI configuration
-			showLoading: true,
-
-			// Features
-			stripDOMReferences: true,
-			storeBlobs: false,
-
-			...config
-		};
-
-		// Initialize base properties
-		this.db = null;
-		this.data = new Map();
-		this.cache = new Map();
-		this.isFetching = false;
-		this.pendingFetch = null;
-		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
-		};
-
+		// Global state
+		this._initialized = false;
 		this.body = document.body;
 		this.loading = document.querySelector('dialog.loading');
 
-		// Auto-initialize
-		this.initDB();
-
-		// Cleanup on page unload
-		window.addEventListener('beforeunload', () => this.destroy());
+		this.init();
 	}
 
-	/**
-	 * Initialize IndexedDB with configurable schema
-	 */
-	async initDB() {
+	async init() {
+		if (this._initialized) return;
+		this._initialized = true;
+
 		if (!('indexedDB' in window)) {
 			console.warn('IndexedDB not supported');
-			return;
 		}
-
-		const dbName = `jvb_${this.config.name}_db`;
-		const request = indexedDB.open(dbName, this.config.version);
-
-		request.onupgradeneeded = (e) => {
-			const db = e.target.result;
-
-			// Create main store with configurable keyPath
-			if (!db.objectStoreNames.contains(this.config.storeName)) {
-				const store = db.createObjectStore(this.config.storeName, {
-					keyPath: this.config.keyPath
-				});
-
-				// Add configured indexes
-				this.config.indexes.forEach(index => {
-					store.createIndex(
-						index.name,
-						index.keyPath || index.name,
-						{ unique: index.unique || false }
-					);
-				});
-			}
-
-			// 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 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 = async (e) => {
-			this.db = e.target.result;
-
-			// Load cache and headers BEFORE fetching (only if stores exist)
-			const loadTasks = [this.loadFromDB()];
-
-			if (this.db.objectStoreNames.contains('cache')) {
-				loadTasks.push(this.loadCache());
-			}
-
-			if (this.config.useHttpCaching && this.db.objectStoreNames.contains('headers')) {
-				loadTasks.push(this.loadHeaders());
-			}
-
-			await Promise.all(loadTasks);
-
-			this.notify('db-init');
-
-			// Now fetch if needed (cache might already have data)
-			if (this.config.endpoint) {
-				this.fetch();
-			}
-		};
-
-		request.onerror = (e) => {
-			console.error(`IndexedDB error for ${dbName}:`, e);
-			if (this.config.onError) {
-				this.config.onError(e);
-			}
-		};
 	}
 
 	/**
-	 * Load all data from IndexedDB
+	 * 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 loadFromDB() {
-		if (!this.db) return;
+	register(name, configs = [], version = 1.25) {
+		if (!Array.isArray(configs)) configs = [configs];
+		if (configs.length === 0) return;
 
-		return new Promise(async (resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
-			const request = store.getAll();
+		if (!this.dbConfig.has(name)) {
+			this.dbConfig.set(name, {
+				dbName: `${jvbBase.base}${name}`,
+				version: version,
+				stores: {},
+				_initialized: false
+			});
+		}
 
-			request.onsuccess = async (e) => {
-				const items = e.target.result;
+		let dbEntry = this.dbConfig.get(name);
 
-				// Restore FormData for ALL items on startup
-				for (const item of items) {
-					if (item.data?._isFormData && this.config.getBlobs) {
-						item.data = await this.objectToFormData(item.data);
-					}
-					const key = this.getItemKey(item);
-					this.data.set(key, item);
-				}
+		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`);
+			}
 
-				this.notify('data-loaded', { count: items.length });
-				resolve(items);
+			const storeKey = `${name}_${config.storeName}`;
+
+			const store = {
+				config: {
+					// Storage
+					dbName: dbEntry.dbName,
+					storeName: 'items',
+					keyPath: 'id',
+					indexes: [],
+
+					// API
+					endpoint: null,
+					apiBase: jvbSettings.api,
+					filters: {},
+					ignore: [],			//any filters to ignore when filtering store locally
+					required: null,
+
+					isAuth: false,
+
+					// Cache
+					TTL: 3600000, // 1 hour
+					useHttpCaching: true,
+
+					// Behavior
+					showLoading: false,
+					delayFetch: true,
+					validateData: true,
+					...config
+				},
+				dbKey: name,
+				storeKey: storeKey,
+				data: new Map(),
+				cache: new Map(),
+				filters: {...(config.filters || {})},
+				isFetching: false,
+				currentRequest: null,
+				lastResponse: null,
+				_initialized: false
 			};
 
-			request.onerror = (e) => reject(e);
-		});
-	}
+			store.ignoreFilters = new Set([
+				... ['search', 'page', 'per_page', 'orderby', 'order'],
+				... ['context', 'source'],
+				... store.config.ignore
+			]);
 
-
-
-	/**
-	 * Load main data from IndexedDB
-	 */
-	async loadData() {
-		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);
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					// Strip DOM references if needed
-					const cleaned = this.config.stripDOMReferences
-						? this.stripDOMReferences(item)
-						: item;
-
-					const key = this.getItemKey(cleaned);
-					this.data.set(key, cleaned);
-				});
-				resolve();
+			store.config.headers = {
+				'X-WP-Nonce': window.auth.getNonce(),
+				...store.config.headers
 			};
 
-			request.onerror = (e) => reject(e);
+			dbEntry.stores[config.storeName] = storeKey;
+
+			this.stores.set(storeKey, store);
+			if (!this.subscribers.has(storeKey)) {
+				this.subscribers.set(storeKey, new Set());
+			}
 		});
+
+		// 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;
 	}
 
 	/**
-	 * Strip DOM references from an object (recursive)
+	 * Get the API object for a registered store
 	 */
-	stripDOMReferences(obj) {
-		if (!obj || typeof obj !== 'object') return obj;
+	getStoreAPI(name) {
+		const api = {
+			// Data methods
+			fetch: () => this.fetch(name),
+			save: (item) => this.save(name, item),
+			saveMany: (items) => this.saveMany(name, items),
+			delete: (id) => this.delete(name, id),
+			deleteMany: (items) => this.deleteMany(name, items),
+			get: (id) => this.get(name, id),
+			getMany: (ids) => this.getMany(name, ids),
+			getAll: () => this.getAll(name),
+			getAllByIndex: (indexName, value) => this.getAllByIndex(name, indexName, value),
+			filterByIndex: (criteria) => this.filterByIndex(name, criteria),
+			getFiltered: () => this.getFiltered(name),
+			clear: () => this.clear(name),
 
-		// Handle arrays
-		if (Array.isArray(obj)) {
-			return obj.map(item => this.stripDOMReferences(item));
-		}
+			// 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),
 
-		// Handle objects
-		const cleaned = {};
-		for (const [key, value] of Object.entries(obj)) {
-			// Skip DOM-related properties
-			if (this.isDOMReference(key, value)) {
-				continue;
-			}
+			// Cache methods
+			clearCache: () => this.clearCache(name),
 
-			// 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;
-			}
-		}
+			// Event methods
+			subscribe: (callback) => this.subscribe(name, callback),
 
-		return cleaned;
-	}
+			// Utility
+			ensureInitialized: () => this.ensureStoreInitialized(name),
 
-	/**
-	 * 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;
-		}
+			// Exposed properties (read-only)
+			get filters() {
+				return { ...api.getStore().filters };
+			},
+			get lastResponse() {
+				return api.getStore().lastResponse;
+			},
+			get data() {
+				return api.getStore().data;
+			},
 
-		// Check key names - use exact match or word boundaries
-		const domKeys = ['element', 'el', 'dom', 'node', 'ui', 'container', 'wrapper'];
-		const lowerKey = key.toLowerCase();
+			getStore: () => this.stores.get(name)
+		};
 
-		// Only match if it's the exact key OR starts/ends with the pattern
-		if (domKeys.includes(lowerKey) ||
-			domKeys.some(k => lowerKey === k || lowerKey.startsWith(k + '_') || lowerKey.endsWith('_' + 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
-	 */
-	/**
-	 * Save a single item
-	 */
-	async save(item) {
-		const key = this.getItemKey(item);
-
-		// Keep ORIGINAL item in memory (with FormData intact)
-		this.data.set(key, item);  // ← Store original
-
-		// Create cleaned version ONLY for IndexedDB
-		let cleaned = { ...item };
-		if (cleaned.data instanceof FormData) {
-			cleaned.data = this.formDataToObject(cleaned.data);
-		}
-
-		if (this.config.stripDOMReferences) {
-			cleaned = this.stripDOMReferences(cleaned);
-		}
-
-		// Persist cleaned version to IndexedDB
-		await this.saveToDB(cleaned);
-
-		if(this.config.endpoint){
-			this.saveToServer(item);
-		}
-
-		this.notify('item-saved', { item: cleaned, key });
-
-		return cleaned;
+		return api;
 	}
 
 	/**
@@ -357,12 +195,12 @@
 	 */
 	formDataToObject(formData) {
 		const obj = {
-			_isFormData: true, // Flag to reconstruct later
+			_isFormData: true,
 			entries: {}
 		};
 
 		for (const [key, value] of formData.entries()) {
-			// Skip File/Blob objects - they're stored separately
+			// Skip File/Blob objects - they're stored separately in UploadManager
 			if (value instanceof File || value instanceof Blob) {
 				continue;
 			}
@@ -389,6 +227,7 @@
 
 		const formData = new FormData();
 
+		// Restore text entries
 		for (const [key, value] of Object.entries(obj.entries)) {
 			if (Array.isArray(value)) {
 				value.forEach(v => formData.append(key, v));
@@ -396,18 +235,13 @@
 				formData.append(key, value);
 			}
 		}
-		// Restore files from external blob store (UploadManager)
-		if (this.config.getBlobs && obj.entries.upload_ids) {
-			const uploadIds = JSON.parse(obj.entries.upload_ids);
-			const blobs = await this.config.getBlobs(uploadIds);  // ← Await here
 
-			for (const blobData of blobs) {
-				if (blobData) {
-					const file = new File(
-						[blobData.data],
-						blobData.name,
-						{ type: blobData.type, lastModified: blobData.lastModified }
-					);
+		if (window.jvbUploads && obj.entries.upload_ids) {
+			const uploadIds = JSON.parse(obj.entries.upload_ids);
+
+			for (const uploadId of uploadIds) {
+				const file = await window.jvbUploads.getBlobData(uploadId);
+				if (file) {
 					formData.append('files[]', file);
 				}
 			}
@@ -416,691 +250,1248 @@
 		return formData;
 	}
 
-	/**
-	 * Save item to IndexedDB
-	 */
-	async saveToDB(item) {
-		if (!this.db) return;
+	/***********************************************************************
+	 * DATABASE INITIALIZATION
+	 ***********************************************************************/
 
-		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);
+	async initDB(name) {
+		const db = this.dbConfig.get(name);
+		if (!db || db._initialized) return;
 
-			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);  // ← Returns original with FormData
-	}
-
-	/**
-	 * 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);
+		if (this.pendingInits.has(name)) {
+			return this.pendingInits.get(name);
 		}
 
-		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({
-			uploadId: key,  // Match keyPath
-			data: blob,
-			type: blob.type,
-			name: blob.name,
-			lastModified: blob.lastModified || Date.now()
-		});
-	}
-
-	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 = {},
-		} = options;
-
-		if (this.config.required && this.filters[this.config.required] === ''){
-			console.log(this.config.storeName+ ': Not fetch as we don\'t have the required items');
-			return;
-		}
-
-		// PREVENT CONCURRENT FETCHES FOR SAME DATA
-		const cacheKey = this.generateCacheKey(filters);
-		console.log('CacheKey: ', cacheKey);
-
-		// If already fetching this exact query, return a promise that resolves when done
-		if (this.isFetching && this.currentCacheKey === cacheKey) {
-			return new Promise((resolve) => {
-				// Store multiple waiting promises if needed
-				if (!this.pendingFetches) {
-					this.pendingFetches = [];
-				}
-				this.pendingFetches.push(resolve);
-			});
-		}
-
-		this.isFetching = true;
-		this.currentCacheKey = cacheKey;
-		let fetchResult = null; // Capture result for pending fetches
-
-		if (this.config.showLoading) {
-			this.setLoading(true);
-		}
-
-		//Check Cached data
-		const cachedData = this.cache.get(cacheKey);
-		console.log('Cached Data: ', cachedData);
-		if (cachedData && this.isCacheValid(cachedData)) {
-			console.log('Returning cached data: ');
-			this.isFetching = false;
-			this.currentCacheKey = null;
-			if (this.config.showLoading) {
-				this.setLoading(false);
-			}
-			return cachedData.data;
-		}
-
-		// Build request headers with HTTP caching
-		const requestHeaders = {
-			...this.headers,
-			...headers
-		};
-
-		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 : ''}`;
+		const initPromise = this._performDBInit(name);
+		this.pendingInits.set(name, initPromise);
 
 		try {
-			const response = await fetch(url, {
-				method: 'GET',
-				headers: requestHeaders
+			await initPromise;
+			db._initialized = true;
+		} finally {
+			this.pendingInits.delete(name);
+		}
+	}
+
+	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);
+						}
+					});
+				});
+				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');
+				}
 			});
 
-			// Handle 304 Not Modified
-			if (response.status === 304 && cachedData) {
-				// Update timestamp but keep existing data
-				cachedData.timestamp = Date.now();
-				cachedData.fromCache = true;
-				cachedData.isError = false;
-				this.saveCache(cacheKey, cachedData);
-				console.log(this.config.storeName+' Data loaded from cache');
-				this.notify('data-loaded', cachedData);
-				fetchResult = cachedData.data;
-				return cachedData.data;
+		} 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 (now includes HTTP headers)
+		if (config.endpoint && !db.objectStoreNames.contains('cache')) {
+			const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
+			cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
+		}
+	}
+
+	/**
+	 * Generic loader for any object store
+	 */
+	async loadFromObjectStore(name, storeName, processItem) {
+		const store = this.stores.get(name);
+		if (!store?.db || !store.db.objectStoreNames.contains(storeName)) {
+			return [];
+		}
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction([storeName], 'readonly');
+			const objectStore = tx.objectStore(storeName);
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				const items = e.target.result || [];
+				items.forEach(processItem);
+				resolve(items);
+			};
+
+			request.onerror = () => resolve([]);
+		});
+	}
+
+	loadStoreDataInBackground(name) {
+		const store = this.stores.get(name);
+		if (!store?.db) return;
+
+		Promise.all([
+			// Load main data
+			this.loadFromObjectStore(name, store.config.storeName, (item) => {
+				const key = this.getItemKey(item, store.config.keyPath);
+				store.data.set(key, item);
+			}),
+
+			// Load cache (includes HTTP headers now!)
+			this.loadFromObjectStore(name, 'cache', (item) => {
+				if (this.isCacheValid(item, store.config.TTL)) {
+					store.cache.set(item.key, item);
+				}
+			})
+		])
+			.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 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);
+		}
+	}
+
+	/***********************************************************************
+	 * TRANSACTION HELPER
+	 ***********************************************************************/
+
+	/**
+	 * Create transaction helper - reduces boilerplate
+	 */
+	async withTransaction(name, storeNames, mode, callback) {
+		const store = this.stores.get(name);
+		if (!store?.db) return null;
+
+		// Ensure storeNames is an array
+		if (typeof storeNames === 'string') storeNames = [storeNames];
+
+		return new Promise((resolve, reject) => {
+			const tx = store.db.transaction(storeNames, mode);
+			const stores = storeNames.map(name => tx.objectStore(name));
+			const objectStore = stores.length === 1 ? stores[0] : stores;
+
+			let result;
+			tx.oncomplete = () => resolve(result);
+			tx.onerror = () => {
+				const error = tx.error || new Error('Transaction failed with unknown error');
+				reject(error);
+			};
+
+			// Call callback immediately to queue operations
+			try {
+				result = callback(objectStore, tx);
+			} catch (error) {
+				reject(error || new Error('Callback failed with unknown error'));
+			}
+		});
+	}
+
+	/***********************************************************************
+	 * FETCH & DATA PROCESSING
+	 ***********************************************************************/
+
+	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)) {
+				let items = cached.items.map(itemId => this.get(name, itemId));
+				this.notify(name, 'data-loaded', {
+					cached: true,
+					items: items??[]
+				});
+				return cached;
+			}
+
+			if (store.config.showLoading) {
+				this.setLoading(true);
+			}
+
+			const url = this.buildFetchUrl(name);
+			const headers = { ...store.config.headers };
+
+			// Use HTTP cache headers from cache entry
+			if (store.config.useHttpCaching && cached) {
+				if (cached.etag) headers['If-None-Match'] = cached.etag;
+				if (cached.lastModified) headers['If-Modified-Since'] = cached.lastModified;
+			}
+
+			const controller = new AbortController();
+			store.currentRequest = controller;
+
+			let response;
+			if (store.isAuth) {
+				response = await window.auth.fetch(url, {
+					method: 'GET',
+					headers,
+					signal: controller.signal
+				});
+			} else {
+				response = await fetch(url, {
+					method: 'GET',
+					headers,
+					signal: controller.signal
+				});
 			}
 
 			if (!response.ok) {
+				// Access the error details from the response body
+				const errorBody = await response.text();
+				// Throw a new error with a descriptive message
+				throw new Error(`HTTP error! status: ${response.status}, message: ${errorBody}`);
+			}
+
+			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.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();
 
-			// Store HTTP caching headers
-			if (this.config.useHttpCaching) {
-				this.storeResponseHeaders(cacheKey, response);
-			}
+			await this.processFetchedData(name, data, cacheKey, response);
 
-			// Cache the response
-			const cacheEntry = {
-				key: cacheKey,
-				data: data,
-				timestamp: Date.now(),
-				endpoint: this.config.endpoint,
-				filters: filters
-			};
-			console.log(this.config.storeName + 'Fetched fresh from server');
-
-			this.cache.set(cacheKey, cacheEntry);
-			this.saveCache(cacheKey, cacheEntry);
-
-			let items = (Array.isArray(data)) ? data : data.items;
-			await this.saveMany(items);
-
-			this.notify('data-loaded', {
-				data: {
-					items: items,
-					...data
-				},
-				count: items.length,
-				filters: filters,
-				fromCache: false,
-				isError: false
+			this.notify(name, 'data-loaded', {
+				cached: false,
+				items: data.items || []
 			});
 
-			fetchResult = data;
 			return data;
 
 		} catch (error) {
-			console.error('Fetch error:', error);
+			const isAbortError = error?.name === 'AbortError';
 
-			// Return cached data if available, even if expired
-			if (cachedData) {
-				console.warn('Using stale cache due to fetch error');
-				cachedData.isError = true;
-				this.notify('data-loaded', cachedData);
-				fetchResult = cachedData.data;
-				return cachedData.data;
+			if (!isAbortError) {
+				console.error(`Fetch error for store "${name}":`, error.message);
+				console.dir(error);
+				this.notify(name, 'fetch-error', { error });
+				throw error;
 			}
 
-			throw error;
 		} finally {
-			if (this.config.showLoading) {
+			store.isFetching = false;
+			store.currentRequest = null;
+
+			if (store.config.showLoading) {
 				this.setLoading(false);
 			}
-
-			this.isFetching = false;
-			this.currentCacheKey = null;
-
-			// Resolve any pending fetches that were waiting
-			if (this.pendingFetches && this.pendingFetches.length > 0) {
-				this.pendingFetches.forEach(resolve => resolve(fetchResult));
-				this.pendingFetches = [];
-			}
 		}
 	}
 
-	/**
-	 * Fetch data from server with HTTP caching
-	 */
-	async saveToServer(item) {
-		if (!this.config.saveToServer || !jvbSettings.currentUser) {
-			return;
-		}
-		if (!this.config.endpoint && this.config.saveToServer) {
-			throw new Error('No endpoint configured for saving to server');
-		}
+	buildFetchUrl(name) {
+		const store = this.stores.get(name);
+		const params = new URLSearchParams();
 
-		let requestBody;
-		let headers = this.config.headers;
-		headers['X-WP-Nonce'] = jvbSettings.nonce;
-		if (item instanceof FormData) {
-			item.append('user', jvbSettings.currentUser);
-			requestBody = item;
-
-			// console.log('Sending formData: ');
-			// for (const pair of requestBody.entries()) {
-			// 	console.log(pair[0], pair[1]);
-			// }
-		} else {
-			requestBody = JSON.stringify({
-				...item,
-				user: jvbSettings.currentUser
-			});
-			// console.log('Sending data: ', {
-			// 	...operation.data,
-			// 	id: operation.id,
-			// 	user: this.user
-			// });
-
-			headers['Content-Type'] = 'application/json';
-		}
-
-		const response = await fetch(
-			`${this.config.apiBase}${this.config.endpoint}`,
-			{
-				method: 'POST',
-				headers: headers,
-				body: requestBody
-			}
-		);
-
-		const result = await response.json();
-		this.notify(
-			'saved-to-server',
-			{
-				success: result.ok && result.success
-			}
-		);
-	}
-
-	cleanFilters(filters) {
-		const cleaned = {};
-		Object.entries(filters).forEach(([key, value]) => {
+		Object.entries(store.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;
-						}
-					});
-				} else if (key === 'date' && typeof value === 'object') {
-					if (value.after) cleaned.after = value.after;
-					if (value.before) cleaned.before = value.before;
+				if (typeof value === 'object') {
+					params.set(key, JSON.stringify(value));
 				} else {
-					cleaned[key] = value;
+					params.set(key, value);
 				}
 			}
 		});
-		return cleaned;
+
+		const baseUrl = store.config.apiBase + store.config.endpoint;
+		return params.toString() ? `${baseUrl}?${params}` : baseUrl;
 	}
 
 	/**
-	 * Generate cache key from filters
+	 * Process fetched data (batch from server)
 	 */
-	generateCacheKey(filters) {
-		if (this.config.cacheKeyStrategy === 'custom' && this.config.generateCacheKey) {
-			return this.config.generateCacheKey(filters);
+	async processFetchedData(name, data, cacheKey, response) {
+		const store = this.stores.get(name);
+		const items = (data.items || []).filter(item => item && typeof item === 'object');
+		const changes = [];
+
+		// Batch process with single transaction
+		if (store.db && items.length > 0) {
+			await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+				items.forEach(item => {
+					try {
+						// Use shared save logic
+						const changeInfo = this._saveItem(name, item);
+						changes.push(changeInfo);
+
+						// Queue for batch write
+						objectStore.put(changeInfo.processed);
+					} catch (error) {
+						console.error(`Error processing item:`, error);
+					}
+				});
+			});
 		}
 
-		// Default strategy: sort keys and create string
-		const sorted = Object.keys(filters)
+		// Update cache (now includes HTTP headers!)
+		const cacheEntry = {
+			key: cacheKey,
+			items: items.map(item => this.getItemKey(item, store.config.keyPath)),
+			timestamp: Date.now(),
+			endpoint: store.config.endpoint,
+			filters: { ...store.filters },
+			etag: response.headers.get('ETag'),
+			lastModified: response.headers.get('Last-Modified'),
+			has_more: data.has_more || false
+		};
+
+		store.cache.set(cacheKey, cacheEntry);
+
+		// Save cache to IndexedDB
+		if (store.db?.objectStoreNames.contains('cache')) {
+			await this.withTransaction(name, 'cache', 'readwrite', (objectStore) => {
+				objectStore.put(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 || {}
+		};
+
+		for (let [key, value] of Object.entries(store.filters)) {
+			if (typeof value === 'string' && value.includes(',')) {
+				this.createSplitCacheEntries(name, items, key, store.filters, response);
+			}
+		}
+
+		// 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
+				});
+			}
+		});
+	}
+
+	createSplitCacheEntries(name, items, key, filters, response) {
+		const store = this.stores.get(name);
+		const keys = filters[key].split(',').map(v => v.trim());
+
+		keys.forEach(value => {
+			let temp = {};
+			temp[key] = value;
+			const newFilters = {
+				... filters,
+				[key]: value
+			};
+			const cacheKey = this.generateCacheKey(newFilters);
+			if(store.cache.has(cacheKey)) return;
+			let filteredItems = this.filterByIndex(name,temp).map(item => this.getItemKey(item, store.config.keyPath));
+
+			const entry = {
+				key: cacheKey,
+				items: filteredItems,
+				timestamp: Date.now(),
+				endpoint: store.config.endpoint,
+				filters: newFilters,
+				etag: response.headers.get('Etag'),
+				lastModified: response.headers.get('Last-Modified'),
+				has_more: filteredItems.length === 20,
+			}
+			store.cache.set(cacheKey, entry);
+			if (store.db?.objectStoreNames.contains('cache')) {
+				this.withTransaction(name, 'cache', 'readwrite', (objectStore) =>{
+					objectStore.put(entry);
+				});
+			}
+		})
+	}
+	/***********************************************************************
+	 * SAVE OPERATIONS
+	 ***********************************************************************/
+
+	/**
+	 * Internal method: Save a single item with full tracking
+	 * Returns change info without writing to IndexedDB (caller handles that)
+	 */
+	_saveItem(name, item) {
+		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);
+
+		// 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 = this._saveItem(name, item);
+
+		// Write to IndexedDB immediately for single saves
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			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;
+	}
+
+	/**
+	 * Save multiple items in a single transaction (batch write)
+	 * @param {string} name - Store name
+	 * @param {Array|Map} items - Array of items or Map of items to save
+	 * @returns {Promise<Array>} - Array of saved keys
+	 */
+	async saveMany(name, items) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		// Convert Map to array if needed
+		const itemArray = items instanceof Map
+			? Array.from(items.values())
+			: Array.isArray(items) ? items : Object.values(items);
+
+		if (itemArray.length === 0) return [];
+
+		const changes = [];
+
+		// Process all items and update in-memory store
+		itemArray.forEach(item => {
+			const changeInfo = this._saveItem(name, item);
+			changes.push(changeInfo);
+		});
+
+		// Single transaction for all writes
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			changes.forEach(changeInfo => {
+				objectStore.put(changeInfo.processed);
+			});
+		});
+
+		// Notify once for batch
+		this.notify(name, 'items-saved', {
+			count: changes.length,
+			keys: changes.map(c => c.key)
+		});
+
+		return changes.map(c => c.key);
+	}
+
+	processForStorage(obj, validate = true, path = 'root') {
+		if (obj === null) {
+			return { valid: true, data: null };
+		}
+		if (obj === undefined) {
+			if (validate) {
+				return { valid: false, error: `Undefined value at ${path}` };
+			}
+			return { valid: true, data: undefined };
+		}
+
+		const type = typeof obj;
+
+		// Handle primitives
+		if (['string', 'number', 'boolean'].includes(type)) {
+			return { valid: true, data: obj };
+		}
+
+		// Reject functions
+		if (type === 'function') {
+			if (validate) return { valid: false, error: `Function at ${path}` };
+
+			return { valid: true, data: undefined };
+		}
+
+		// DOM elements
+		if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
+			if (validate) return { valid: false, error: `DOM element at ${path}` };
+
+			return { valid: true, data: undefined };
+		}
+
+		// FormData - convert and continue
+		if (obj instanceof FormData) {
+
+			return { valid: true, data: this.formDataToObject(obj) };
+		}
+
+		// Preserve safe types
+		if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj) || obj instanceof Blob) {
+			return { valid: true, data: obj };
+		}
+
+		// Convert Sets to Arrays
+		if (obj instanceof Set) {
+			return this.processForStorage(Array.from(obj), 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 !== undefined) processed.push(result.data);
+			}
+			return { valid: true, data: processed };
+		}
+
+		// Objects
+		if (type === 'object') {
+			const processed = {};
+			for (const [key, value] of Object.entries(obj)) {
+				if (value === undefined) continue;
+				const result = this.processForStorage(value, validate, `${path}.${key}`);
+				if (!result.valid) return result;
+				// Include null values, skip undefined
+				if (result.data !== undefined || value === null) {
+					processed[key] = result.data;
+				}
+			}
+			return { valid: true, data: processed };
+		}
+
+		if (validate) return { valid: false, error: `Unknown type at ${path}` };
+
+		return { valid: true, data: undefined };
+	}
+
+	/***********************************************************************
+	 * DATA ACCESS
+	 ***********************************************************************/
+
+	async delete(name, id) {
+		const store = this.stores.get(name);
+		store.data.delete(id);
+
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			objectStore.delete(id);
+		});
+
+		this.notify(name, 'item-deleted', { id });
+	}
+
+	/**
+	 * Delete multiple items in a single transaction (batch delete)
+	 * @param {string} name - Store name
+	 * @param {Array|Set} ids - Array or Set of IDs to delete
+	 * @returns {Promise<Array>} - Array of deleted IDs
+	 */
+	async deleteMany(name, ids) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		// Convert Set to array if needed
+		const idArray = ids instanceof Set
+			? Array.from(ids)
+			: Array.isArray(ids) ? ids : Object.keys(ids);
+
+		if (idArray.length === 0) return [];
+
+		// Update in-memory store
+		idArray.forEach(id => {
+			store.data.delete(id);
+		});
+
+		// Single transaction for all deletes
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			idArray.forEach(id => {
+				objectStore.delete(id);
+			});
+		});
+
+		// Notify once for batch
+		this.notify(name, 'items-deleted', {
+			count: idArray.length,
+			ids: idArray
+		});
+
+		return idArray;
+	}
+
+	get(name, id) {
+		const store = this.stores.get(name);
+		return store.data.get(id);
+	}
+
+	/**
+	 * Get multiple items by IDs in a single call
+	 * @param {string} name - Store name
+	 * @param {Array|Set} ids - Array or Set of IDs to retrieve
+	 * @param {boolean} skipMissing - If true, omit missing items; if false, include null for missing
+	 * @returns {Array} - Array of items (in same order as IDs)
+	 */
+	getMany(name, ids, skipMissing = true) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		const idArray = ids instanceof Set
+			? Array.from(ids)
+			: Array.isArray(ids) ? ids : Object.keys(ids);
+
+		if (idArray.length === 0) return [];
+
+		if (skipMissing) {
+			return idArray.reduce((acc, id) => {
+				const item = store.data.get(id);
+				if (item) acc.push(item);
+				return acc;
+			}, []);
+		}
+
+		// Preserve order, include null for missing
+		return idArray.map(id => store.data.get(id) ?? null);
+	}
+
+	getAll(name) {
+		const store = this.stores.get(name);
+		return Array.from(store.data.values());
+	}
+	/**
+	 * Filter in-memory data by multiple index/value pairs
+	 * @param {string} name - Store name
+	 * @param {Object} criteria - Object of { indexName: acceptedValue(s) }
+	 * @returns {Array} - Items matching ALL criteria
+	 *
+	 * @example
+	 * filterByIndex(name, { field: 'upload_123', status: ['queued', 'uploading'] })
+	 */
+	filterByIndex(name, criteria) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		return Array.from(store.data.values()).filter(item => {
+			if (!item || typeof item !== 'object') return false;
+			return Object.entries(criteria).every(([key, value]) => {
+				const accepted = Array.isArray(value) ? value : [value];
+				return accepted.includes(item[key]);
+			});
+		});
+	}
+	/**
+	 * Get all items matching an index value
+	 * @param {string} name - Store name
+	 * @param {string} indexName - Name of the index to query
+	 * @param {*} value - Value to match
+	 * @returns {Promise<Array>} - Matching items
+	 */
+	async getAllByIndex(name, indexName, value) {
+		const store = this.stores.get(name);
+		const values = Array.isArray(value) ? value : [value];
+
+		// Try IndexedDB index query first (more efficient for large datasets)
+		if (store.db && store.db.objectStoreNames.contains(store.config.storeName)) {
+			try {
+				const tx = store.db.transaction([store.config.storeName], 'readonly');
+				const objectStore = tx.objectStore(store.config.storeName);
+
+				if (objectStore.indexNames.contains(indexName)) {
+					const index = objectStore.index(indexName);
+
+					const results = await Promise.all(
+						values.map(v => new Promise((resolve, reject) => {
+							const request = index.getAll(v);
+							request.onsuccess = () => resolve(request.result || []);
+							request.onerror = () => reject(request.error);
+						}))
+					);
+
+					return results.flat();
+				}
+			} catch (error) {
+				console.warn(`Index query failed for "${indexName}", falling back to filter:`, error);
+			}
+		}
+
+		// Fallback: filter in-memory data
+		return Array.from(store.data.values()).filter(item => values.includes(item[indexName]));
+	}
+
+	getFiltered(name) {
+		const store = this.stores.get(name);
+		const cacheKey = this.generateCacheKey(store.filters);
+		const cacheEntry = store.cache.get(cacheKey);
+
+		// First check if we have cached results for exact filters
+		if (cacheEntry?.items) {
+			const items = cacheEntry.items.reduce((acc, id) => {
+				const item = store.data.get(id);
+				if (item) acc.push(item);
+				return acc;
+			}, []);
+			return this.applyOrdering(items, store);
+		}
+
+		const allItems = Array.from(store.data.values());
+
+		const searchQuery = store.filters.search?.toLowerCase().trim() || '';
+
+		const filterPredicates = [];
+
+		// Handle taxonomy filters separately
+		if (store.filters.taxonomy && typeof store.filters.taxonomy === 'object') {
+			Object.entries(store.filters.taxonomy).forEach(([taxonomy, termIds]) => {
+				const acceptedTermIds = Array.isArray(termIds) ? termIds : [termIds];
+
+				filterPredicates.push(item => {
+					if (!item.taxonomies || !item.taxonomies[taxonomy]) {
+						return false;
+					}
+					const itemTermIds = Object.keys(item.taxonomies[taxonomy]).map(id => parseInt(id));
+					const matches = acceptedTermIds.some(termId => itemTermIds.includes(parseInt(termId)));
+					return matches;
+				});
+			});
+		}
+
+		// Handle other filters
+		for (const [key, value] of Object.entries(store.filters)) {
+			if (key === 'taxonomy') {
+				if (typeof value === 'string' && !value.includes(',')) {
+					filterPredicates.push(item => item.taxonomy === value);
+				}
+				continue;
+			}
+			if (store.ignoreFilters.has(key)) {
+				continue;
+			}
+			if (value === null || value === undefined || value === '') continue;
+			if (value === 'all') continue;
+
+			if (typeof value === 'string' && value.includes(',')) {
+				const accepted = value.split(',').map(v => v.trim());
+				filterPredicates.push(item => accepted.includes(String(item[key])));
+			} else {
+				filterPredicates.push(item => String(item[key]) === String(value));
+			}
+		}
+
+		const filtered = allItems.filter(item => {
+			for (const predicate of filterPredicates) {
+				if (!predicate(item)) return false;
+			}
+			return !(searchQuery && !this.searchObject(item, searchQuery));
+		});
+
+		return this.applyOrdering(filtered, store);
+	}
+
+	applyOrdering(items, store) {
+		if (!Array.isArray(items)) items = Array.from(items);
+		if (items.length === 0) return items;
+
+		const orderby = store.filters.orderby || 'date';
+		const order = (store.filters.order || 'desc').toLowerCase();
+
+		// Handle random ordering
+		if (['random', 'rand'].includes(orderby) || ['random', 'rand'].includes(order)) {
+			return this.shuffle(items);
+		}
+
+		items.sort((a, b) => {
+			let aVal, bVal;
+
+			switch (orderby) {
+				case 'alphabetical':
+				case 'title':
+					aVal = (a.title || a.name || '').toLowerCase();
+					bVal = (b.title || b.name || '').toLowerCase();
+					break;
+				case 'modified':
+					aVal = new Date(a.modified || a.date || 0);
+					bVal = new Date(b.modified || b.date || 0);
+					break;
+				case 'date':
+				default:
+					aVal = new Date(a.date || a.modified || 0);
+					bVal = new Date(b.date || b.modified || 0);
+			}
+
+			if (aVal < bVal) return order === 'asc' ? -1 : 1;
+			if (aVal > bVal) return order === 'asc' ? 1 : -1;
+			return 0;
+		});
+
+		return items;
+	}
+
+	shuffle(items) {
+		const array = items.slice();
+		for (let i = array.length - 1; i > 0; i--) {
+			const j = Math.floor(Math.random() * (i + 1));
+			[array[i], array[j]] = [array[j], array[i]];
+		}
+		return array;
+	}
+
+	searchObject(obj, search) {
+		if (!obj || typeof obj !== 'object') {
+			return typeof obj === 'string' && obj.toLowerCase().includes(search);
+		}
+
+		for (const value of Object.values(obj)) {
+			if (value === null || value === undefined) continue;
+
+			if (typeof value === 'object') {
+				if (this.searchObject(value, search)) return true;
+				continue;
+			}
+
+			if (typeof value === 'string' && value.toLowerCase().includes(search)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	async clear(name) {
+		const store = this.stores.get(name);
+		store.data.clear();
+		store.cache.clear();
+
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			objectStore.clear();
+		});
+
+		this.notify(name, 'data-cleared');
+	}
+
+	/***********************************************************************
+	 * FILTER OPERATIONS
+	 ***********************************************************************/
+	async updateFilters(name, updates, clearAll = false) {
+		const store = this.stores.get(name);
+		const oldFilters = { ...store.filters };
+
+		if (clearAll) {
+			store.filters = { ...store.config.filters };
+		}
+
+		Object.entries(updates).forEach(([key, value]) => {
+			if (value === null || value === undefined || value === '') {
+				delete store.filters[key];
+			} else {
+				store.filters[key] = value;
+			}
+		});
+		this.notify(name, 'filters-changed', {
+			oldFilters,
+			filters: store.filters,
+			updates
+		});
+
+		const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
+
+		if (store.config.endpoint && shouldFetch) {
+			await this.fetch(name);
+		} else {
+			const filtered = this.getFiltered(name);
+			this.notify(name, 'data-loaded', {
+				cached: true,
+				items: filtered
+			});
+		}
+	}
+
+	/**
+	 * Determine if we need to fetch or can use local data
+	 * @param {string} name - Store name
+	 * @param {object} updates - Filter updates being applied
+	 * @param {object} oldFilters - Previous filter state
+	 * @returns {Promise<boolean>} - True if fetch is needed, false if local filtering suffices
+	 */
+	async shouldFetchWithFilters(name, updates, oldFilters) {
+		const store = this.stores.get(name);
+
+		// If no endpoint or no lastResponse, always fetch
+		if (!store.config.endpoint || !store.lastResponse) {
+			return true;
+		}
+
+		if (store.lastResponse.has_more === false) {
+			if (this.hasCompleteData(store, store.filters)) {
+				return false;
+			}
+		}
+
+		if ('page' in updates) {
+			const newPage = updates.page;
+			const oldPage = oldFilters.page || 1;
+
+			// If trying to go to a higher page but no more data available
+			if (newPage > oldPage && !store.lastResponse.has_more) {
+				// Reset page to last valid page
+				store.filters.page = oldPage;
+				return false;
+			}
+		}
+
+		// SEARCH OPTIMIZATION: Check if we need to fetch for search
+		if ('search' in updates) {
+			const searchQuery = updates.search?.trim() || '';
+			const oldSearch = oldFilters.search?.trim() || '';
+
+			// If search is being cleared, we might already have the data
+			if (!searchQuery && oldSearch) {
+				// Check if we have all base data (without search)
+				const baseFilters = { ...store.filters };
+				delete baseFilters.search;
+				baseFilters.page = 1;
+
+				// If we have complete base data, no need to fetch
+				if (this.hasCompleteData(store, baseFilters)) {
+					return false;
+				}
+			}
+
+			// If search is new or changed, check if we have all data to filter locally
+			if (searchQuery && searchQuery !== oldSearch) {
+				// Check: do we have all data for base filters (no search, page 1)?
+				const baseFilters = { ...store.filters };
+				delete baseFilters.search;
+				baseFilters.page = 1;
+
+				// If we have complete base data, we can filter locally
+				if (this.hasCompleteData(store, baseFilters)) {
+					return false;
+				}
+			}
+		}
+
+		// Default: fetch is needed
+		return true;
+	}
+
+	/**
+	 * Check if we have complete data for given filters
+	 * @param {object} store - Store instance
+	 * @param {object} filters - Filters to check
+	 * @returns {boolean} - True if we have all data
+	 */
+	hasCompleteData(store, filters) {
+		const cacheKey = this.generateCacheKey(filters);
+		const cached = store.cache.get(cacheKey);
+
+		if (!cached) return false;
+
+		// Check if cache indicates no more data
+		return cached.has_more === false || store.lastResponse?.has_more === false;
+	}
+
+	setFilter(name, key, value) {
+		return this.updateFilters(name, { [key]: value });
+	}
+
+	async setFilters(name, filters) {
+		const store = this.stores.get(name);
+
+		const hasChanges = Object.keys(filters).some(
+			key => store.filters[key] !== filters[key]
+		) || Object.keys(store.filters).some(
+			key => !(key in filters) && filters !== store.config.filters
+		);
+
+		if (!hasChanges) return;
+
+		return this.updateFilters(name, filters);
+	}
+
+	removeFilter(name, key) {
+		return this.updateFilters(name, { [key]: null });
+	}
+
+	clearFilters(name) {
+		return this.updateFilters(name, {}, true);
+	}
+
+	/***********************************************************************
+	 * CACHE OPERATIONS
+	 ***********************************************************************/
+
+	clearCache(name) {
+		const store = this.stores.get(name);
+		store.cache.clear();
+
+		if (store.db?.objectStoreNames.contains('cache')) {
+			this.withTransaction(name, 'cache', 'readwrite', (objectStore) => {
+				objectStore.clear();
+			});
+		}
+
+		this.notify(name, 'cache-cleared');
+	}
+
+	generateCacheKey(filters) {
+		const normalized = Object.keys(filters)
 			.sort()
 			.reduce((acc, key) => {
 				acc[key] = filters[key];
 				return acc;
 			}, {});
-
-		return JSON.stringify(sorted);
+		return JSON.stringify(normalized);
 	}
 
-	setFilter(key, value) {
-		if (!this.filters) {
-			this.filters = {};
+	isCacheValid(entry, ttl) {
+		if (!entry || !entry.timestamp) return false;
+		const age = Date.now() - entry.timestamp;
+		return age < ttl;
+	}
+
+	/***********************************************************************
+	 * EVENT SYSTEM
+	 ***********************************************************************/
+
+	subscribe(name, callback) {
+		if (!this.subscribers.has(name)) {
+			this.subscribers.set(name, new Set());
 		}
-		const oldValue = this.filters[key];
-		if (oldValue === value) {
-			return;
-		}else 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) {
-			window.debouncer.schedule(
-				this.config.endpoint,
-				this.fetch.bind(this),
-				100
-			);
-		}
+		const subscribers = this.subscribers.get(name);
+		subscribers.add(callback);
+		return () => subscribers.delete(callback);
 	}
 
+	notify(name, event, data = {}) {
+		const subscribers = this.subscribers.get(name);
+		if (!subscribers) return;
 
-	/**
-	 * Remove a filter
-	 */
-	removeFilter(key) {
-		const oldValue = this.filters[key];
-
-		if (oldValue !== undefined) {
-			delete this.filters[key];
-			this.notify('filters-changed', {
-				filters: this.filters,
-				removed: { key, oldValue }
-			});
-
-			// Auto-fetch if endpoint is configured
-			if (this.config.endpoint) {
-				window.debouncer.schedule(
-					this.config.endpoint,
-					this.fetch.bind(this),
-					100
-				);
-			}
-		}
-	}
-
-	/**
-	 * Clear all filters
-	 */
-	clearFilters() {
-		const oldFilters = { ...this.filters };
-		//Restore baseline filters
-		this.filters = this.config.filters;
-
-		this.notify('filters-cleared', {
-			oldFilters,
-			filters: this.filters
-		});
-
-		// Auto-fetch if endpoint is configured
-		if (this.config.endpoint) {
-			this.fetch();
-		}
-	}
-
-	/**
-	 * Set multiple filters at once
-	 */
-	async setFilters(filters) {
-		const hasChanges = Object.keys(filters).some(
-			key => this.filters[key] !== filters[key]
-		);
-
-		if (!hasChanges) {
-			return;
-		}
-
-		this.filters = { ...this.filters, ...filters };
-
-		this.notify('filters-changed', {
-			filters: this.filters,
-			changed: filters,
-		});
-
-		// Only fetch if endpoint configured
-		if (this.config.endpoint) {
-			window.debouncer.schedule(
-				this.config.endpoint,
-				this.fetch.bind(this),
-				100
-			);
-		}
-	}
-
-	/**
-	 * Check if cache entry is still valid
-	 */
-	isCacheValid(cacheEntry) {
-		if (!cacheEntry || !cacheEntry.timestamp) return false;
-
-		const age = Date.now() - cacheEntry.timestamp;
-		return age < this.config.TTL;
-	}
-
-	/**
-	 * Store HTTP response headers for caching
-	 */
-	storeResponseHeaders(key, response) {
-		const headers = {
-			key,
-			etag: response.headers.get('ETag'),
-			lastModified: response.headers.get('Last-Modified'),
-			timestamp: Date.now()
-		};
-
-		this.httpHeaders.set(key, headers);
-
-		if (this.db && this.db.objectStoreNames.contains('headers')) {
-			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 || !this.db.objectStoreNames.contains('cache')) 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 => {
+		subscribers.forEach(callback => {
 			try {
 				callback(event, data);
 			} catch (error) {
-				console.error('Subscriber error:', error);
+				console.error(`Subscriber error for store "${name}":`, error);
 			}
 		});
 	}
 
-	/**
-	 * Query items using an index
-	 */
-	async query(indexName, value) {
-		if (!this.db) return [];
+	/***********************************************************************
+	 * UTILITIES
+	 ***********************************************************************/
 
-		return new Promise((resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
+	getItemKey(item, keyPath) {
+		if (typeof keyPath === 'function') {
+			return keyPath(item);
+		}
 
-			if (!store.indexNames.contains(indexName)) {
-				reject(new Error(`Index ${indexName} does not exist`));
-				return;
-			}
+		const keys = keyPath.split('.');
+		let value = item;
 
-			const index = store.index(indexName);
-			const request = value !== undefined
-				? index.getAll(value)
-				: index.getAll();
+		for (const key of keys) {
+			value = value?.[key];
+		}
 
-			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);
-		});
+		return value;
 	}
 
-	/**
-	 * 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) {
-		console.log('Setting Loading ' + (on) ? 'on' : 'off' + ' from '.this.config.storeName);
 		this.body.classList.toggle('loading', on);
 		if (on) {
-			this.loading.showModal();
+			this.loading?.showModal();
 		} else {
-			this.loading.close();
+			this.loading?.close();
 		}
-
 	}
 
-	/**
-	 * Cleanup and destroy
-	 */
 	destroy() {
-		if (this.currentRequest) {
-			this.currentRequest.abort();
-		}
+		this.stores.forEach(store => {
+			if (store.currentRequest) {
+				store.currentRequest.abort();
+			}
+		});
 
+		this.databases.forEach(db => db.close());
+		this.stores.clear();
 		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();
-
-		if (this.db) {
-			const tx = this.db.transaction(['cache'], 'readwrite');
-			const store = tx.objectStore('cache');
-			store.clear();
-		}
-
-		this.notify('cache-cleared');
+		this.databases.clear();
+		this.pendingInits.clear();
 	}
 }
 
-// Export for use
-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