Jake Vanderwerf
2025-11-10 e9967fa22781d922ba4eb8fb44fe72d200ac4b14
assets/js/concise/SimpleCache.js
@@ -24,15 +24,9 @@
         ...config
      };
      // Check if Cache API is available
      this.cacheAvailable = 'caches' in window;
      if(!this.cacheAvailable){
         console.warn('Browser Cache API unavailable, reverting to LocalStorage');
      }
      // Initialize memory cache
      this._memoryCache = new Map();
      this._cache = new Map();
      this.subscribers = new Set();
   }
@@ -41,8 +35,8 @@
    * @returns {number} Number of items cleared
    */
   clearMemoryCache() {
      const count = this._memoryCache.size;
      this._memoryCache.clear();
      const count = this._cache.size;
      this._cache.clear();
      console.log(`Cleared ${count} items from memory cache`);
      return count;
@@ -51,295 +45,40 @@
   /**
    * Get a setting value
    */
   async get(key) {
   get(key) {
      //Check memory cache first
      if (this._cache.has(key)) {
         return this._cache.get(key);
      }
      let cacheKey = `${this.base}_${key}`;
      const cachedItem = await this.getCacheItem(cacheKey);
      if (!cachedItem) {
      let item;
      try {
         item =  localStorage.getItem(cacheKey);
         if (!item) {
            return null;
         }
         item = JSON.parse(item);
      } catch (error) {
         console.warn('Error getting from localStorage:', error);
         return null;
      }
      // Deserialize data back to original type
      return this.deserializeData(cachedItem.data, cachedItem.dataType);
      if (item) {
         this._cache.set(key, item);
      }
      return item;
   }
   /**
    * Set a setting value
    */
   async set(key, value) {
   set(key, value) {
      this._cache.set(key, value);
      let cacheKey = `${this.base}_${key}`;
      // Serialize data with type preservation
      const serializedData = this.serializeData(value);
      const cacheItem = {
         data: serializedData.data,
         dataType: serializedData.type,
         timestamp: Date.now(),
      };
      await this.setCacheItem(cacheKey, cacheItem);
      // Notify subscribers
      this.notify('cache-saved', { key, value });
   }
   remove(key) {
      let cacheKey = `${this.base}_${key}`;
      //TODO: Actually remove it
   }
   serializeData(data) {
      // Handle null/undefined
      if (data === null || data === undefined) {
         return { data, type: 'primitive' };
      }
      // Handle Maps
      if (data instanceof Map) {
         return {
            data: Array.from(data.entries()),
            type: 'Map'
         };
      }
      // Handle Sets
      if (data instanceof Set) {
         return {
            data: Array.from(data),
            type: 'Set'
         };
      }
      // Handle Dates
      if (data instanceof Date) {
         return {
            data: data.toISOString(),
            type: 'Date'
         };
      }
      // Handle RegExp
      if (data instanceof RegExp) {
         return {
            data: { source: data.source, flags: data.flags },
            type: 'RegExp'
         };
      }
      // Handle Arrays (check before general objects)
      if (Array.isArray(data)) {
         // Recursively serialize array elements that might contain Maps/Sets
         return {
            data: data.map(item => this.serializeData(item)),
            type: 'Array'
         };
      }
      // Handle plain objects
      if (data && typeof data === 'object' && data.constructor === Object) {
         const serializedObj = {};
         for (const [key, value] of Object.entries(data)) {
            serializedObj[key] = this.serializeData(value);
         }
         return {
            data: serializedObj,
            type: 'Object'
         };
      }
      // Handle primitives (string, number, boolean)
      return { data, type: 'primitive' };
   }
   deserializeData(data, type) {
      if (!type || type === 'primitive') {
         return data;
      }
      switch (type) {
         case 'Map':
            return new Map(data);
         case 'Set':
            return new Set(data);
         case 'Date':
            return new Date(data);
         case 'RegExp':
            return new RegExp(data.source, data.flags);
         case 'Array':
            // Recursively deserialize array elements
            return data.map(item =>
               this.deserializeData(item.data, item.type)
            );
         case 'Object':
            const restoredObj = {};
            for (const [key, value] of Object.entries(data)) {
               restoredObj[key] = this.deserializeData(value.data, value.type);
            }
            return restoredObj;
         default:
            console.warn(`Unknown data type: ${type}, returning as-is`);
            return data;
      }
   }
   /**********************************************************
    CACHE
   **********************************************************/
   /**
    * Main method to get an item from cache
    * Tries Browser Cache API first, falls back to localStorage
    *
    * @param {string} key - Cache key
    * @returns {Promise<any>} - Cached item or null
    */
   async getCacheItem(key) {
      //Check memory cache first
      if (this._memoryCache.has(key)) {
         return this._memoryCache.get(key);
      }
      // Then check persistent cache
      const item = this.cacheAvailable ?
         await this.getBrowserCacheItem(key) :
         this.getLocalStorageItem(key);
      // Store in memory cache if found
      if (item) {
         this._memoryCache.set(key, item);
      }
      return item;
   }
   /**
    * Main method to set an item in cache
    * Uses Browser Cache API if available, falls back to localStorage
    *
    * @param {string} key - Cache key
    * @param {any} item - Item to cache
    * @returns {Promise<void>}
    */
   async setCacheItem(key, item) {
      // Always store in memory cache
      this._memoryCache.set(key, item);
      return this.cacheAvailable ?
         await this.setBrowserCacheItem(key, item) :
         this.setLocalStorageItem(key, item);
   }
   /**
    * Remove an item from cache
    *
    * @param {string} key - Cache key
    * @returns {Promise<void>}
    */
   async removeCacheItem(key) {
      // Remove from memory cache
      this._memoryCache.delete(key);
      return this.cacheAvailable ?
         await this.removeBrowserCacheItem(key) :
         this.removeLocalStorageItem(key);
   }
   /*********************************************************************
    BROWSER CACHE
   *********************************************************************/
   /**
    * Get item from Browser Cache API
    *
    * @param {string} key - Cache key
    * @returns {Promise<any>} - Cached item or null
    */
   async getBrowserCacheItem(key) {
      try {
         const cache = await caches.open(this.config.namespace);
         const response = await cache.match(key);
         if (!response) {
            return null;
         }
         return await response.json();
      } catch (error) {
         console.warn('Error getting from Browser Cache API:', error);
         return null;
      }
   }
   /**
    * Set item in Browser Cache API
    *
    * @param {string} key - Cache key
    * @param {any} item - Item to cache
    * @returns {Promise<void>}
    */
   async setBrowserCacheItem(key, item) {
      try {
         const cache = await caches.open(this.config.namespace);
         const response = new Response(JSON.stringify(item), {
            headers: { 'Content-Type': 'application/json' }
         });
         await cache.put(key, response);
      } catch (error) {
         console.warn('Error setting in Browser Cache API:', error);
      }
   }
   /**
    * Remove item from Browser Cache API
    *
    * @param {string} key - Cache key
    * @returns {Promise<void>}
    */
   async removeBrowserCacheItem(key) {
      try {
         const cache = await caches.open(this.config.namespace);
         await cache.delete(key);
      } catch (error) {
         console.warn('Error removing from Browser Cache API:', error);
      }
   }
   /*************************************************************************
    LOCAL STORAGE
   *************************************************************************/
   /**
    * Get item from localStorage
    *
    * @param {string} key - Cache key
    * @returns {object|null} - Cached item or null
    */
   getLocalStorageItem(key) {
      try {
         const stored = localStorage.getItem(key);
         if (!stored) {
            return null;
         }
         return JSON.parse(stored);
      } catch (error) {
         console.warn('Error getting from localStorage:', error);
         return null;
      }
   }
   /**
    * Set item in localStorage
    *
    * @param {string} key - Cache key
    * @param {any} item - Item to cache
    */
   setLocalStorageItem(key, item) {
      try {
         localStorage.setItem(key, JSON.stringify(item));
         localStorage.setItem(cacheKey, JSON.stringify(value));
      } catch (error) {
         // Handle quota exceeded
         if (error instanceof DOMException && error.code === 22) {
@@ -353,16 +92,15 @@
            console.warn('Error setting localStorage item:', error);
         }
      }
      // Notify subscribers
      this.notify('cache-saved', { key, value });
   }
   /**
    * Remove item from localStorage
    *
    * @param {string} key - Cache key
    */
   removeLocalStorageItem(key) {
   remove(key) {
      let cacheKey = `${this.base}_${key}`;
      try {
         localStorage.removeItem(key);
         localStorage.removeItem(cacheKey);
      } catch (error) {
         console.warn('Error removing localStorage item:', error);
      }
@@ -403,65 +141,20 @@
         console.warn('Error cleaning up localStorage:', error);
      }
   }
   /************************************************************************
    CLEANUP
   ************************************************************************/
   /**
    * Clean expired items from cache
    *
    * @returns {Promise<void>}
    */
   async cleanExpired() {
      const now = Date.now();
      const maxAge = this.config.TTL;
      if (this.cacheAvailable) {
         try {
            const cache = await caches.open(this.options.namespace);
            const keys = await cache.keys();
            for (const request of keys) {
               const response = await cache.match(request);
               try {
                  const cacheItem = await response.json();
                  if (now - cacheItem.timestamp > maxAge) {
                     await cache.delete(request);
                  }
               } catch (e) {
                  // If we can't parse it, just leave it alone
               }
   async loadFromCache() {
      for (let i = 0; i < localStorage.length; i++) {
         const key = localStorage.key(i);
         // Check if key starts with this cache's base prefix
         if (key.startsWith(`${this.base}_`)) {
            let cleanKey = key.replace(`${this.base}_`, '');
            try {
               // Parse the JSON value before caching
               const value = JSON.parse(localStorage.getItem(key));
               this._cache.set(cleanKey, value);
            } catch (error) {
               console.warn(`Failed to parse cached value for ${key}:`, error);
            }
         } catch (error) {
            console.warn('Error cleaning browser cache:', error);
         }
      } else {
         // Clean localStorage
         try {
            for (let i = 0; i < localStorage.length; i++) {
               const key = localStorage.key(i);
               if (key && key.startsWith(this.options.namespace)) {
                  try {
                     const item = JSON.parse(localStorage.getItem(key));
                     if (now - item.timestamp > maxAge) {
                        localStorage.removeItem(key);
                     }
                  } catch (e) {
                     // Skip invalid items
                  }
               }
            }
         } catch (error) {
            console.warn('Error cleaning localStorage cache:', error);
         }
      }
      // Clean memory cache
      for (const [key, item] of this._memoryCache.entries()) {
         if (now - item.timestamp > maxAge) {
            this._memoryCache.delete(key);
         }
      }
   }
@@ -472,29 +165,12 @@
    * @returns {Promise<void>}
    */
   async clear() {
      // Clear memory cache
      this._memoryCache.clear();
      this._cache.clear();
      if (this.cacheAvailable) {
         try {
            await caches.delete(this.options.namespace);
         } catch (error) {
            console.warn('Error clearing browser cache:', error);
         }
      }
      // Also clear localStorage in case we've used it as fallback
      this.clearLocalStorage();
   }
   /**
    * Clear just localStorage cache
    */
   clearLocalStorage() {
      try {
         for (let i = localStorage.length - 1; i >= 0; i--) {
            const key = localStorage.key(i);
            if (key && key.startsWith(this.options.namespace)) {
            if (key && key.startsWith(this.config.namespace)) {
               localStorage.removeItem(key);
            }
         }
@@ -502,7 +178,6 @@
         console.warn('Error clearing localStorage cache:', error);
      }
   }
   /************************** OLD **********************************/
   /**
    * Subscribe to setting changes