Jake Vanderwerf
2025-11-10 e9967fa22781d922ba4eb8fb44fe72d200ac4b14
assets/js/concise/Queue.js
@@ -16,6 +16,7 @@
         ...config
      };
      this.user = jvbSettings.currentUser;
      console.log(this.user);
      this.headers = {
@@ -32,16 +33,27 @@
         storeName: 'operations',
         keyPath: 'id',
         endpoint: this.config.endpoint,
         TTL: Infinity, //Queue data doesn't expire,
         TTL: Infinity,
         indexes: [
            {name: 'status', keyPath: 'status'},
            {name: 'type', keyPath: 'type'},
         ],
         showLoading: false,
         getBlobs: async (ids) => {
            if (window.jvbUploadBlobs) {
               if (!Array.isArray(ids) && typeof ids === 'string') {
                  ids = [ids];
               }
               // Get individual blobs (not all items)
               const blobs = await Promise.all(
                  ids.map(id => window.jvbUploadBlobs.getBlob(id))
               );
               return blobs.filter(Boolean); // Remove nulls
            }
            return null;
         }
      });
      this.queue = new Map();
      this.classes = [
         'offline',
         'synced',
@@ -96,27 +108,30 @@
      this.store.subscribe((event, data) => {
         switch (event) {
            case 'data-fetched':
            case 'data-cached':
               this.updateOperationsFromServer(data.data.items);
            case 'data-loaded':
               // Initial load from IndexedDB
               const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false);
               if (incomplete.length > 0) {
                  this.startPolling();
               }
               this.updateUI();
               break;
            case 'items-updated':
               this.updateOperationsFromServer(data.items);
               break;
            case 'item-stored':
               this.updateOperationsFromServer([data])
            case 'item-saved':
               if (this.hasQueuedOperations()) {
                  this.startPolling();
               }
            default:
               this.updateUI();
               break;
         }
      });
      this.store.fetch();
      this.notify('queue-initialized', {operations: incomplete});
   }
   /**
    *
    * @param {object} operation
    * @param {string} operation.endpoint The endpoint, excluding the apiBase
    * @param {string} operatio   n.endpoint The endpoint, excluding the apiBase
    * @param {object} operation.data The data to save
    * @param {boolean} operation.canMerge Whether data can merge
    * @param {string} operation.title The title of the operation for the Queue Panel
@@ -152,7 +167,7 @@
         return null;
      }
      const existingOps = Array.from(this.queue.values()).filter(op=>
      const existingOps = Array.from(this.store.data.values()).filter(op=>
         op.status === 'queued' &&
         op.endpoint === item.endpoint &&
         op.canMerge
@@ -185,32 +200,26 @@
   }
   setQueue(item) {
      this.queue.set(item.id, item);
      this.store.save(item.id, item);
      this.store.save(item);  // Remove first parameter
   }
   updateOperationStatus(itemID, status) {
      let item = this.queue.get(itemID);
      let item = this.store.get(itemID);
      if (!item){
         return;
      }
      item.status = status;
      this.notify('operation-status', item);
      this.updateOperationUI(item);
   }
   getQueue(itemID) {
      if (this.queue.has(itemID)) {
         return this.queue.get(itemID);
      }
      return this.store.getItem(itemID);
      return this.store.get(itemID);
   }
   clearQueue(itemID) {
      if (this.queue.has(itemID)) {
         this.queue.delete(itemID);
      }
      this.store.clearItem(itemID);
      this.store.delete(itemID);
   }
   startActivityTracking() {
@@ -287,10 +296,13 @@
         //update to uploading
         this.updateOperationStatus(operation.id, 'uploading');
         // Get fresh copy from store to restore FormData
         operation = this.getQueue(operation.id);
         //build request
         const url = `${this.config.apiBase}${operation.endpoint}`;
         let requestBody;
         console.log(operation.data);
         if (operation.data instanceof FormData) {
            operation.data.append('id', operation.id);
            operation.data.append('user', this.user);
@@ -299,6 +311,11 @@
            // for (const pair of requestBody.entries()) {
            //    console.log(pair[0], pair[1]);
            // }
            console.log('Sending to server:');
            for (var [key, value] of requestBody.entries()) {
               console.log(key, value);
            }
         } else {
            requestBody = JSON.stringify({
               ...operation.data,
@@ -313,6 +330,8 @@
            operation.headers['Content-Type'] = 'application/json';
         }
         const response = await fetch(url, {
            method: operation.method,
            headers: operation.headers,
@@ -384,74 +403,23 @@
   startPolling() {
      if (this.isPolling) return;
      this.isPolling = true;
      this.pollServer();
      this.pollTimer = setInterval(() => {
         this.pollServer();
      }, this.config.pollInterval);
      this.updateCountdown();
   }
   pollServer(force = false) {
      const operations = this.getOperationsByStatus(['pending', 'processing', 'uploading']);
      if (operations.length === 0 && !force) {
         this.stopPolling();
         return;
      }
      this.updateStatusPanel('pending');
      try {
         // const operationIds = operations.map(op => op.id);
         // this.store.setFilter('operation_ids', operationIds.join(','));
         this.store.fetch();
      } catch (error) {
         console.error('Polling error:', error);
      } finally {
         this.updateStatusPanel();
      }
   }
      this.pollTimer = setInterval(async () => {
         try {
            await this.store.fetch(); // Fetches from server, updates store.data
   async updateOperationsFromServer(serverOperations) {
      let hasChanges = false;
      const processedIds = new Set();
      for (const serverOp of serverOperations) {
         let operation = (this.queue.has(serverOp.id)) ? this.queue.get(serverOp.id) : {};
         processedIds.add(serverOp.id);
         if (serverOp.status !== operation.status) {
            operation = {
               ... operation,
               ... serverOp
            };
            // Update in DataStore
            this.queue.set(operation.id, operation);
            // Update UI for this operation
            this.updateOperationStatus(operation.id, operation.status);
            const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false);
            if (incomplete.length === 0) {
               this.stopPolling();
               this.updateStatusPanel('synced');
            }
         } catch (error) {
            console.error('Polling error:', error);
         }
      }
      // Clean up operations that were completed/dismissed on server
      const localOps = this.getOperationsByStatus(['pending', 'processing', 'uploading']);
      for (const localOp of localOps) {
         if (!processedIds.has(localOp.id)) {
            localOp.status = 'completed';
            localOp.completedAt = Date.now();
            this.setQueue(localOp);
            hasChanges = true;
            this.updateOperationStatus(localOp.id, localOp.status);
         }
      }
      // Check if all operations are completed
      const pendingOps = this.getOperationsByStatus(['pending', 'processing', 'uploading']);
      if (pendingOps.length === 0) {
         this.stopPolling();
      }
      this.updateUI();
      }, this.config.pollInterval);
   }
   stopPolling() {
@@ -602,8 +570,11 @@
      window.addEventListener('beforeunload', this.handleBeforeUnload);
   }
   handleClick(e) {
      if (!e.target.closest(this.selectors.panel, this.selectors.toggle)) {
         return;
      }
      if (e.target.closest(this.selectors.refreshButton)) {
         this.pollServer(true);
         this.store.fetch();
      } else if (e.target.closest(this.selectors.clearButton)) {
         const completedOps = this.getOperationsByStatus('completed');
         if (completedOps.length > 0) {
@@ -637,9 +608,9 @@
    *********************************************/
   initUI() {
      this.icons = {
         queued: 'refresh', localProcessing: 'refresh', uploading: 'syncing',
         pending: 'cloud', processing: 'syncing', completed: 'synced',
         failed: 'error', failed_permanent: 'error'
         queued: 'arrows-clockwise', localProcessing: 'arrows-clockwise', uploading: 'syncing',
         pending: 'cloud', processing: 'syncing', completed: 'cloud-check',
         failed: 'cloud-warning', failed_permanent: 'cloud-warning'
      };
      this.selectors = {
@@ -790,14 +761,14 @@
         stats[status] = 0;
      });
      Array.from(this.store.items.values())
      Array.from(this.store.data.values())  // Change items to data
         .forEach(op => {
            if (stats.hasOwnProperty(op.status)) {
               stats[op.status]++;
            }
         });
      stats.total = Array.from(this.store.items.values()).length;
      stats.total = Array.from(this.store.data.values()).length;  // Change items to data
      return stats;
   }
@@ -986,7 +957,7 @@
   }
   getFilteredOperations(filter) {
      const operations = Array.from(this.store.items.values());
      const operations = Array.from(this.store.data.values());  // Change items to data
      if (filter === 'all') {
         return operations;
@@ -1017,20 +988,16 @@
   **************************************************************************/
   getOperationsByStatus(status, include = true) {
      status = Array.isArray(status) ? status : ((status.includes(',')) ? status.split(',') : [status]);
      if (include) {
         return Array.from(this.queue.values()).filter(op =>
            status.includes(op.status)
         );
      if (!Array.isArray(status) && typeof status === 'string') {
         status = [status];
      }
      return Array.from(this.queue.values()).filter(op =>
         !status.includes(op.status)
      );
      return (include)
         ? Array.from(this.store.data.values()).filter((item) => status.includes(item.status))
         : Array.from(this.store.data.values()).filter((item) => !status.includes(item.status));
   }
   hasQueuedOperations() {
      return this.queue.some(op =>
         op.status === 'queued'
      );
   async hasQueuedOperations() {
      const queued = await this.store.query('status', 'queued');
      return queued.length > 0;
   }
   subscribe(callback) {
      this.subscribers.add(callback);