| | |
| | | ...config |
| | | }; |
| | | this.user = jvbSettings.currentUser; |
| | | console.log(this.user); |
| | | |
| | | |
| | | this.headers = { |
| | |
| | | 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', |
| | |
| | | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | 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() { |
| | |
| | | //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); |
| | |
| | | // 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, |
| | |
| | | operation.headers['Content-Type'] = 'application/json'; |
| | | } |
| | | |
| | | |
| | | |
| | | const response = await fetch(url, { |
| | | method: operation.method, |
| | | headers: operation.headers, |
| | |
| | | |
| | | 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() { |
| | |
| | | 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) { |
| | |
| | | *********************************************/ |
| | | 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 = { |
| | |
| | | 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; |
| | | } |
| | |
| | | } |
| | | |
| | | 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; |
| | |
| | | **************************************************************************/ |
| | | 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); |