Jake Vanderwerf
2026-01-01 5b5f37de365ff84fc231e414a719d1b2ff4ceff6
assets/js/concise/DataStoreOld.js
New file
@@ -0,0 +1,1099 @@
/**
 * DataStore - Singleton pattern managing multiple store namespaces
 *
 * Usage:
 *   window.jvbStore = new DataStore();
 *   this.store = window.jvbStore.register('feed', { config });
 */
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.init();
      // window.addEventListener('beforeunload', () => this.destroy());
   }
   async init() {
      if (this._initialized) return;
      this._initialized = true;
      if (!('indexedDB' in window)) {
         console.warn('IndexedDB not supported');
      }
   }
   /**
    * 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
    */
   register(name, configs = [], version = 1.1) {
      if (!Array.isArray(configs)) configs = [configs];
      if (configs.length === 0) return;
      if (!this.dbConfig.has(name)) {
         this.dbConfig.set(name, {
            dbName: `jvb_${name}`,
            version: version,
            stores: {},
            _initialized: false
         });
      }
      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());
         }
      });
      // 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 the API object for a registered store
    */
   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;
   }
   /**
    * Convert FormData to plain object for storage
    */
   formDataToObject(formData) {
      const obj = {
         _isFormData: true,
         entries: {}
      };
      for (const [key, value] of formData.entries()) {
         // Skip File/Blob objects - they're stored separately in UploadManager
         if (value instanceof File || value instanceof Blob) {
            continue;
         }
         // Handle multiple values for same key
         if (obj.entries[key]) {
            if (!Array.isArray(obj.entries[key])) {
               obj.entries[key] = [obj.entries[key]];
            }
            obj.entries[key].push(value);
         } else {
            obj.entries[key] = value;
         }
      }
      return obj;
   }
   /**
    * Convert stored object back to FormData
    */
   async objectToFormData(obj) {
      if (!obj._isFormData) return obj;
      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));
         } else {
            formData.append(key, value);
         }
      }
      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);
            }
         }
      }
      return formData;
   }
   /**
    * Initialize database for a specific store
    */
   async initDB(name) {
      const db = this.dbConfig.get(name);
      if (!db || db._initialized) return;
      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);
      }
   }
   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');
            }
         })
      } 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 {
               params.set(key, value);
            }
         }
      });
      const baseUrl = store.config.apiBase + store.config.endpoint;
      return params.toString() ? `${baseUrl}?${params}` : baseUrl;
   }
   /**
    * Process fetched data (batch from server)
    */
   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 store.filters[key];
         this.notify(name, 'filters-changed', {
            filters: store.filters,
            removed: { key, oldValue }
         });
         if (store.config.endpoint) {
            this.fetch(name);
         }
      }
   }
   clearFilters(name) {
      const store = this.stores.get(name);
      const oldFilters = { ...store.filters };
      store.filters = { ...store.config.filters };
      this.notify(name, 'filters-cleared', {
         oldFilters,
         filters: store.filters
      });
      if (store.config.endpoint) {
         this.fetch(name);
      }
   }
   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');
   }
   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();
         }
      }
   }
   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);
   }
   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'),
         lastModified: response.headers.get('Last-Modified'),
         timestamp: Date.now()
      };
      store.httpHeaders.set(key, headers);
      if (store.db && store.db.objectStoreNames.contains('headers')) {
         const tx = store.db.transaction(['headers'], 'readwrite');
         const objectStore = tx.objectStore('headers');
         objectStore.put(headers);
      }
   }
   async saveToCache(name, key, data) {
      const store = this.stores.get(name);
      if (!store.db || !store.db.objectStoreNames.contains('cache')) return;
      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();
      }
   }
   destroy() {
      this.stores.forEach(store => {
         if (store.currentRequest) {
            store.currentRequest.abort();
         }
      });
      this.databases.forEach(db => db.close());
      this.stores.clear();
      this.subscribers.clear();
      this.databases.clear();
      this.pendingInits.clear();
   }
}
// Initialize singleton on DOMContentLoaded
document.addEventListener('DOMContentLoaded', async function() {
   window.auth.subscribe((event) => {
      if (event === 'auth-loaded') {
         window.jvbStore = new DataStore();
      }
   });
});