Jake Vanderwerf
2026-01-01 8d0e2130627497b55b1a61cbe374bfb309ef2f27
assets/js/concise/DataStore.js
@@ -6,6 +6,7 @@
 *   this.store = window.jvbStore.register('feed', { config });
 */
class DataStore {
   constructor() {
      // Singleton pattern
      if (DataStore.instance) {
@@ -140,6 +141,8 @@
         delete: (id) => this.delete(name, id),
         get: (id) => this.get(name, id),
         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),
@@ -747,30 +750,33 @@
      // Reject functions
      if (type === 'function') {
         return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null };
         if (validate) return { valid: false, error: `Function at ${path}` };
         console.debug(`[DataStore] Stripped function at ${path}`);
         return { valid: true, data: undefined };
      }
      // DOM elements
      if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
         return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null };
         if (validate) return { valid: false, error: `DOM element at ${path}` };
         console.debug(`[DataStore] Stripped DOM element at ${path}`);
         return { valid: true, data: undefined };
      }
      // FormData - convert and continue
      if (obj instanceof FormData) {
         return validate
            ? { valid: false, error: `FormData at ${path}` }
            : { valid: true, data: this.formDataToObject(obj) };
         if (validate) return { valid: false, error: `FormData at ${path}` };
         console.debug(`[DataStore] Converted FormData at ${path}`);
         return { valid: true, data: this.formDataToObject(obj) };
      }
      // Preserve safe types
      if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
      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) {
         const arr = Array.from(obj);
         return this.processForStorage(arr, validate, path);
         return this.processForStorage(Array.from(obj), validate, path);
      }
      // Convert Maps to Objects
@@ -784,7 +790,7 @@
         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);
            if (result.data !== undefined) processed.push(result.data);
         }
         return { valid: true, data: processed };
      }
@@ -795,14 +801,14 @@
         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;
            if (result.data !== undefined) processed[key] = result.data;
         }
         return { valid: true, data: processed };
      }
      return validate
         ? { valid: false, error: `Unknown type at ${path}` }
         : { valid: true, data: null };
      if (validate) return { valid: false, error: `Unknown type at ${path}` };
      console.debug(`[DataStore] Stripped unknown type at ${path}`);
      return { valid: true, data: undefined };
   }
   /***********************************************************************
@@ -829,6 +835,64 @@
      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 => {
         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);
@@ -859,12 +923,8 @@
   }
   /***********************************************************************
    * FILTER OPERATIONS (UNIFIED)
    * FILTER OPERATIONS
    ***********************************************************************/
   /**
    * Unified filter update - handles all filter operations
    */
   async updateFilters(name, updates, clearAll = false) {
      const store = this.stores.get(name);
      const oldFilters = { ...store.filters };