| | |
| | | } |
| | | |
| | | async initQueue() { |
| | | const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false) |
| | | |
| | | if (incomplete.length > 0) { |
| | | this.startPolling(); |
| | | } else { |
| | | let polling = this.maybeStartPolling(); |
| | | if (!polling) { |
| | | this.updateStatusPanel('synced'); |
| | | } |
| | | |
| | | |
| | | this.store.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'data-loaded': |
| | | case 'items-saved': |
| | | // Initial load from IndexedDB |
| | | const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false); |
| | | if (incomplete.length > 0) { |
| | | this.startPolling(); |
| | | } |
| | | this.maybeStartPolling(); |
| | | this.updateUI(); |
| | | break; |
| | | case 'item-saved': |
| | | // Check for status changes |
| | | if (data.item) { |
| | | const oldItem = this.store.data.get(data.item.id); |
| | | if (oldItem && oldItem.status !== data.item.status) { |
| | | this.handleOperationStatusChange(data.item, oldItem.status); |
| | | } |
| | | console.log(data,'Item saved data'); |
| | | if (data.previousItem && data.previousItem.status !== data.item.status) { |
| | | this.handleOperationStatusChange(data.item, data.previousItem.status); |
| | | } |
| | | if (this.hasQueuedOperations()) { |
| | | this.startPolling(); |
| | | } |
| | | this.maybeStartPolling(); |
| | | break; |
| | | default: |
| | | this.updateUI(); |
| | |
| | | } |
| | | |
| | | }); |
| | | this.notify('queue-initialized', {operations: incomplete}); |
| | | } |
| | | |
| | | maybeStartPolling() |
| | | { |
| | | const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false); |
| | | if (incomplete.length > 0) { |
| | | this.startPolling(); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Handle operation status changes and notify subscribers |
| | | */ |
| | | handleOperationStatusChange(operation, oldStatus) { |
| | | if (!operation || oldStatus === operation.status) return; |
| | | handleOperationStatusChange(operation) { |
| | | |
| | | // Notify based on new status |
| | | switch(operation.status) { |
| | | case 'completed': |
| | | console.log(operation); |
| | | this.notify('operation-completed', operation); |
| | | break; |
| | | case 'failed': |
| | |
| | | method: 'POST', |
| | | headers: {}, |
| | | data: {}, |
| | | sendNow: false, // true = process immediately |
| | | canMerge: true, |
| | | popup: 'Saving changes...', |
| | | title: 'Operation', |
| | |
| | | return null; |
| | | } |
| | | |
| | | if (item.sendNow) { |
| | | this.processOperation(item).then(()=> {}); |
| | | this.store.clearCache(); |
| | | window.debouncer.schedule('fastQueue', this.startPolling.bind(this), 200); |
| | | this.showQueue(); |
| | | return item.id; |
| | | } |
| | | |
| | | const existingOps = Array.from(this.store.data.values()).filter(op=> |
| | | op.status === 'queued' && |
| | | op.endpoint === item.endpoint && |
| | |
| | | return existing.id; |
| | | } |
| | | |
| | | console.log('Added to Queue: ', item); |
| | | this.store.clearCache(); |
| | | |
| | | //Add new operation to DataStore |
| | |
| | | } |
| | | |
| | | clearQueue(itemID) { |
| | | const item = this.store.get(itemID); |
| | | this.store.delete(itemID); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | resetActivityTimer() { |
| | | this.lastActivity = Date.now(); |
| | | |
| | | if (this.activityTimer) { |
| | | clearTimeout(this.activityTimer); |
| | | } |
| | |
| | | this.setProcessing(false); |
| | | this.stopActivityTracking(); |
| | | |
| | | const pending = this.getOperationsByStatus(['queued', 'completed', 'failed_permanent'], false); |
| | | if (pending.length > 0) { |
| | | this.startPolling(); |
| | | this.showQueue(); |
| | | } else { |
| | | this.hideQueue(); |
| | | } |
| | | this.maybeStartPolling() ? this.showQueue() : this.hideQueue(); |
| | | } |
| | | |
| | | async processOperation(operation) { |
| | | async processOperation(operation, skip = false) { |
| | | try { |
| | | this.updateOperationStatus(operation.id, 'uploading'); |
| | | if (!skip) { |
| | | this.updateOperationStatus(operation.id, 'uploading'); |
| | | |
| | | if (operation.data?._isFormData) { |
| | | operation.data = await this.store.objectToFormData(operation.data); |
| | | if (operation.data?._isFormData) { |
| | | operation.data = await this.store.objectToFormData(operation.data); |
| | | } |
| | | } |
| | | |
| | | const url = `${this.config.apiBase}${operation.endpoint}`; |
| | |
| | | }); |
| | | operation.headers['Content-Type'] = 'application/json'; |
| | | } |
| | | |
| | | const response = await fetch(url, { |
| | | method: operation.method, |
| | | headers: operation.headers, |
| | |
| | | }); |
| | | |
| | | const result = await response.json(); |
| | | |
| | | if (skip) { |
| | | operation.data = {}; |
| | | } |
| | | if (response.ok && result.success !== false) { |
| | | // Handle server-side merge |
| | | if (result.id && operation.id !== result.id) { |
| | | |
| | | // Check if the returned ID exists locally |
| | | const existingOp = this.getQueue(result.id); |
| | | |
| | | if (existingOp) { |
| | | // Merge data from both operations |
| | | existingOp.data = window.deepMerge(existingOp.data, operation.data); |
| | | existingOp.status = result.status || 'pending'; |
| | | existingOp.serverData = result; |
| | | this.updateOperationStatus(existingOp.id, existingOp.status); |
| | | // Update the existing operation |
| | | this.setQueue(existingOp); |
| | | |
| | | this.removeOperationFromUI(operation.id); |
| | | |
| | | // Switch reference to the merged operation |
| | | operation = existingOp; |
| | | } else { |
| | | // Server merged with an operation we don't have locally |
| | | // Update the ID and continue |
| | | this.clearQueue(operation.id); |
| | | operation.id = result.id; |
| | | operation.status = result.status || 'pending'; |
| | | operation.serverData = result; |
| | | this.updateOperationStatus(operation.id, operation.status); |
| | | this.setQueue(operation); |
| | | } |
| | | operation = await this.handleServerMerge(operation, result); |
| | | } else { |
| | | // Normal processing - no merge |
| | | operation.status = result.status || 'pending'; |
| | | operation.serverData = result; |
| | | this.updateOperationStatus(operation.id, operation.status); |
| | | this.setQueue(operation); |
| | | } |
| | | |
| | | this.a11y.announce(`${operation.title} sent to server for processing.`); |
| | |
| | | } |
| | | } |
| | | |
| | | async handleServerMerge(operation, result) { |
| | | const existingOp = this.getQueue(result.id); |
| | | |
| | | if (existingOp) { |
| | | // Merge with existing local operation |
| | | existingOp.data = window.deepMerge(existingOp.data, operation.data); |
| | | existingOp.status = result.status || 'pending'; |
| | | existingOp.serverData = result; |
| | | this.updateOperationStatus(existingOp.id, existingOp.status); |
| | | this.removeOperationFromUI(operation.id); |
| | | this.clearQueue(operation.id); |
| | | return existingOp; |
| | | } else { |
| | | // Server merged with unknown operation |
| | | this.clearQueue(operation.id); |
| | | operation.id = result.id; |
| | | operation.status = result.status || 'pending'; |
| | | operation.serverData = result; |
| | | this.updateOperationStatus(operation.id, operation.status); |
| | | return operation; |
| | | } |
| | | } |
| | | |
| | | startPolling() { |
| | | if (this.isPolling) return; |
| | | |
| | |
| | | this.store.clearCache(); |
| | | await this.store.fetch(); // Fetches from server, updates store.data |
| | | |
| | | const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false); |
| | | if (incomplete.length === 0) { |
| | | if (!this.maybeStartPolling()) { |
| | | this.stopPolling(); |
| | | this.updateStatusPanel('synced'); |
| | | } |
| | |
| | | this.countdownTimer = null; |
| | | } |
| | | } |
| | | |
| | | getOperationIds(operations) { |
| | | return operations.map(op => op.id); |
| | | } |
| | | /*********************************************************** |
| | | USER ACTIONS |
| | | ***********************************************************/ |
| | |
| | | }; |
| | | this.handleOffline = () => this.updateStatusPanel('offline'); |
| | | this.handleBeforeUnload = (e) => { |
| | | const hasPending = this.getOperationsByStatus(['queued', 'uploading']); |
| | | if (hasPending.length > 0) { |
| | | if (this.isPolling || this.isProcessing) { |
| | | e.preventDefault(); |
| | | return 'You have unsaved changes in the queue.'; |
| | | return 'You have unsaved changes in the queue. Proceed?'; |
| | | } |
| | | }; |
| | | |
| | |
| | | this.store.clearHttpHeaders(); // Clear cached headers first |
| | | this.store.fetch(); |
| | | } else if (e.target.closest(this.selectors.clearButton)) { |
| | | const completedOps = this.getOperationsByStatus('completed'); |
| | | const completedOps = this.getOperationIds(this.getOperationsByStatus('completed')); |
| | | if (completedOps.length > 0) { |
| | | const ids = completedOps.map(op => op.id); |
| | | this.updateServerOperations(ids, 'dismiss'); |
| | | this.updateServerOperations(completedOps, 'dismiss'); |
| | | } |
| | | } else if (e.target.closest(this.selectors.retryButton)) { |
| | | const failedOps = this.getOperationsByStatus('failed'); |
| | | const failedOps = this.getOperationIds(this.getOperationsByStatus('failed')); |
| | | if (failedOps.length > 0) { |
| | | const ids = failedOps.map(op => op.id); |
| | | this.updateServerOperations(ids, 'retry'); |
| | | this.updateServerOperations(failedOps, 'retry'); |
| | | } |
| | | } else if (e.target.closest('[data-action]')) { |
| | | const button = e.target.closest('[data-action]'); |