Jake Vanderwerf
2025-11-25 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850
assets/js/concise/DataStore.js
@@ -14,6 +14,7 @@
      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
@@ -27,7 +28,7 @@
      this.init();
      window.addEventListener('beforeunload', () => this.destroy());
      // window.addEventListener('beforeunload', () => this.destroy());
   }
   async init() {
@@ -41,23 +42,40 @@
   /**
    * 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, config = {}) {
      if (this.stores.has(name)) {
         console.warn(`Store "${name}" already registered`);
         return this.getStoreAPI(name);
   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
         });
      }
      if (!config.keyPath) {
         throw new Error(`Store "${name}" requires a keyPath`);
      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 = {
         name,
         config: {
            // Storage
            dbName: `jvb_${name}_db`,
            version: 1,
               dbName: dbEntry.dbName,
            storeName: 'items',
            keyPath: 'id',
            indexes: [],
@@ -76,16 +94,15 @@
            showLoading: false,
            delayFetch: true,
            validateData: true, // Validate data is serializable
            ...config
         },
         // State
         db: null,
            dbKey: name,
            storeKey: storeKey,
         data: new Map(),
         cache: new Map(),
         httpHeaders: new Map(),
         filters: { ...config.filters },
            subscribers: new Map(),
            filters: {...(config.filters || {}) },
         isFetching: false,
         currentRequest: null,
         lastResponse: null,
@@ -97,15 +114,25 @@
         ...store.config.headers
      };
      this.stores.set(name, store);
      this.subscribers.set(name, new Set());
         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.initStoreDB(name).catch(error => {
      this.initDB(name).catch(error => {
         console.error(`Failed to initialize store "${name}":`, error);
      });
      return this.getStoreAPI(name);
      const apis = {};
      for (const [storeName, storeKey] of Object.entries(dbEntry.stores)) {
         apis[storeName] = this.getStoreAPI(storeKey);
      }
      return apis;
   }
   /**
@@ -171,6 +198,16 @@
         return Object.fromEntries(obj);
      }
      // Preserve ArrayBuffer and TypedArrays (needed for blob storage)
      if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
         return obj;
      }
      // Preserve Date objects
      if (obj instanceof Date) {
         return obj;
      }
      // Handle Arrays
      if (Array.isArray(obj)) {
         return obj.map(item => this.normalizeForStorage(item));
@@ -189,6 +226,66 @@
   }
   /**
    * 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;
   }
   /**
    * Strip DOM references from object
    */
   stripDOMReferences(obj, visited = new WeakSet()) {
@@ -212,6 +309,12 @@
         return null;
      }
      // ✅ PRESERVE ArrayBuffer and TypedArrays (needed for blob storage)
      if (obj instanceof ArrayBuffer ||
         ArrayBuffer.isView(obj)) {
         return obj;
      }
      // Handle Date
      if (obj instanceof Date) {
         return obj;
@@ -242,40 +345,52 @@
   /**
    * Initialize database for a specific store
    */
   async initStoreDB(name) {
      const store = this.stores.get(name);
      if (!store || store._initialized) return;
   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._performStoreInit(name);
      const initPromise = this._performDBInit(name);
      this.pendingInits.set(name, initPromise);
      try {
         await initPromise;
         store._initialized = true;
         db._initialized = true;
      } finally {
         this.pendingInits.delete(name);
      }
   }
   async _performStoreInit(name) {
      const store = this.stores.get(name);
      const { dbName, version } = store.config;
   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) => {
               this.setupStores(db, store.config);
               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);
         this.loadStoreDataInBackground(name);
         this.notify(name, 'db-init');
               store._initialized = true;
               this.loadStoreDataInBackground(storeName);
               this.notify(storeName, 'db-init');
            }
         })
      } catch (error) {
         console.error(`Failed to initialize database for store "${name}":`, error);
@@ -464,7 +579,7 @@
      }
      if (!store._initialized) {
         await this.initStoreDB(name);
         await this.initDB(store.dbKey);
      }
   }
@@ -627,6 +742,14 @@
      // Auto-normalize Sets/Maps
      let processed = this.normalizeForStorage(item);
      if (processed.data instanceof FormData) {
         processed = {
            ...processed,
            data: this.formDataToObject(processed.data)
         };
      }
      processed = this.stripDOMReferences(processed);
      // Validate data is serializable
@@ -640,10 +763,10 @@
      const key = this.getItemKey(processed, store.config.keyPath);
      // Store in memory
      // Store the original in memory (with original data intact)
      store.data.set(key, item);
      // Store in IndexedDB
      // Store processed in IndexedDB
      if (store.db) {
         const tx = store.db.transaction([store.config.storeName], 'readwrite');
         const objectStore = tx.objectStore(store.config.storeName);
@@ -682,6 +805,10 @@
         return { valid: true };
      }
      if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
         return { valid: true };
      }
      // Reject DOM elements
      if (obj instanceof HTMLElement ||
         obj instanceof NodeList ||
@@ -810,7 +937,6 @@
      } else {
         store.filters[key] = value;
      }
      this.notify(name, 'filters-changed', {
         filters: store.filters,
         changed: { key, oldValue, newValue: value }
@@ -912,6 +1038,9 @@
   }
   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);