Jake Vanderwerf
5 hours ago 56a9a1ccf764ff7a6af8f8a2292cb07443cb4aa7
assets/js/concise/DataStore.js
@@ -45,13 +45,13 @@
    * @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) {
   register(name, configs = [], version = 1.25) {
      if (!Array.isArray(configs)) configs = [configs];
      if (configs.length === 0) return;
      if (!this.dbConfig.has(name)) {
         this.dbConfig.set(name, {
            dbName: `jvb_${name}`,
            dbName: `${jvbBase.base}${name}`,
            version: version,
            stores: {},
            _initialized: false
@@ -85,6 +85,8 @@
               ignore: [],       //any filters to ignore when filtering store locally
               required: null,
               isAuth: false,
               // Cache
               TTL: 3600000, // 1 hour
               useHttpCaching: true,
@@ -108,6 +110,7 @@
         store.ignoreFilters = new Set([
            ... ['search', 'page', 'per_page', 'orderby', 'order'],
            ... ['context', 'source'],
            ... store.config.ignore
         ]);
@@ -471,13 +474,16 @@
         let result;
         tx.oncomplete = () => resolve(result);
         tx.onerror = () => reject(tx.error);
         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);
            reject(error || new Error('Callback failed with unknown error'));
         }
      });
   }
@@ -514,9 +520,10 @@
         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: cached.items || []
               items: items??[]
            });
            return cached;
         }
@@ -537,11 +544,27 @@
         const controller = new AbortController();
         store.currentRequest = controller;
         const response = await fetch(url, {
            method: 'GET',
            headers,
            signal: controller.signal
         });
         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
@@ -576,7 +599,6 @@
         if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
         }
         const data = await response.json();
         await this.processFetchedData(name, data, cacheKey, response);
@@ -589,11 +611,14 @@
         return data;
      } catch (error) {
         if (error.name !== 'AbortError') {
            console.error(`Fetch error for store "${name}":`, error);
         const isAbortError = error?.name === 'AbortError';
         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 {
         store.isFetching = false;
@@ -628,8 +653,8 @@
    */
   async processFetchedData(name, data, cacheKey, response) {
      const store = this.stores.get(name);
      const items = data.items || [];
      const changes = []; // Track all changes
      const items = (data.items || []).filter(item => item && typeof item === 'object');
      const changes = [];
      // Batch process with single transaction
      if (store.db && items.length > 0) {
@@ -829,7 +854,15 @@
   }
   processForStorage(obj, validate = true, path = 'root') {
      if (obj === null || obj === undefined) return { valid: true, data: obj };
      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;
@@ -888,9 +921,13 @@
      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;
            if (result.data !== undefined) processed[key] = result.data;
            // Include null values, skip undefined
            if (result.data !== undefined || value === null) {
               processed[key] = result.data;
            }
         }
         return { valid: true, data: processed };
      }
@@ -1005,6 +1042,7 @@
      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]);
@@ -1057,42 +1095,62 @@
      // First check if we have cached results for exact filters
      if (cacheEntry?.items) {
         return this.applyOrdering(
            cacheEntry.items.reduce((acc, id) => {
               const item = store.data.get(id);
               if (item) acc.push(item);
               return acc;
            }, []),
            store
         );
         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 (store.ignoreFilters.has(key)) continue;
         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;
         // Comma-separated values
         if (typeof value === 'string' && value.includes(',')) {
            const accepted = value.split(',').map(v => v.trim());
            filterPredicates.push(item => accepted.includes(String(item[key])));
            continue;
         } else {
            filterPredicates.push(item => String(item[key]) === String(value));
         }
         filterPredicates.push(item => String(item[key]) === String(value));
      }
      const filtered = allItems.filter(item => {
         // Apply all non-search filters
         for (const predicate of filterPredicates) {
            if (!predicate(item)) return false;
         }
         // Apply search if present
         return !(searchQuery && !this.searchObject(item, searchQuery));
      });
@@ -1103,37 +1161,50 @@
      if (!Array.isArray(items)) items = Array.from(items);
      if (items.length === 0) return items;
      if (store.filters.orderby || store.filters.order) {
         const orderby = store.filters.orderby || 'date';
         const order = (store.filters.order || 'desc').toLowerCase();
      const orderby = store.filters.orderby || 'date';
      const order = (store.filters.order || 'desc').toLowerCase();
         items.sort((a, b) => {
            let aVal, bVal;
            switch (orderby) {
               case 'alphabetical':
               case 'title':
                  aVal = (a.fields?.post_title || a.title || a.name || '').toLowerCase();
                  bVal = (b.fields?.post_title || b.title || b.name || '').toLowerCase();
                  break;
               case 'modified':
                  aVal = new Date(a.modified || 0);
                  bVal = new Date(b.modified || 0);
                  break;
               case 'date':
               default:
                  aVal = new Date(a.date || 0);
                  bVal = new Date(b.date || 0);
            }
            if (aVal < bVal) return order === 'asc' ? -1 : 1;
            if (aVal > bVal) return order === 'asc' ? 1 : -1;
            return 0;
         });
      // 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);
@@ -1184,23 +1255,22 @@
            store.filters[key] = value;
         }
      });
      this.notify(name, 'filters-changed', {
         oldFilters,
         filters: store.filters,
         updates
      });
      this.notify(name, 'data-loaded', {
         cached: true,
         items: this.getFiltered(name)
      });
      const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
      if (store.config.endpoint && shouldFetch) {
         await this.fetch(name);
      } else if (store.config.endpoint) {
         this.notify(name, 'data-loaded');
      } else {
         const filtered = this.getFiltered(name);
         this.notify(name, 'data-loaded', {
            cached: true,
            items: filtered
         });
      }
   }
@@ -1220,14 +1290,9 @@
      }
      if (store.lastResponse.has_more === false) {
         // Check if new filters are a subset of what we have
         const isSubsetFilter = Object.entries(updates).every(([key, value]) => {
            if (store.ignoreFilters.has(key)) return true;
            if (key === 'page') return true; // Handle pagination locally
            return true; // We have all data, can filter locally
         });
         if (isSubsetFilter) return false;
         if (this.hasCompleteData(store, store.filters)) {
            return false;
         }
      }
      if ('page' in updates) {