// SYNC.JS VERSION 8 - LOADING console.log("[SYNC] Loading sync.js v8..."); // NextSnap - Sync Engine with Upload Queue and Retry Logic 'use strict'; const Sync = { currentUsername: null, isOnline: navigator.onLine, isSyncing: false, currentUpload: null, retryTimeouts: {}, // Exponential backoff delays (in milliseconds) retryDelays: [5000, 15000, 45000, 120000, 300000], // 5s, 15s, 45s, 2m, 5m maxRetryDelay: 300000, // Cap at 5 minutes init(username) { this.currentUsername = username; this.setupEventListeners(); // Check for pending uploads on init if (this.isOnline) { this.triggerSync(); } }, setupEventListeners() { // Listen for online/offline events window.addEventListener('online', () => { console.log('Network online - triggering sync'); this.isOnline = true; this.updateConnectivityUI(); this.triggerSync(); }); window.addEventListener('offline', () => { console.log('Network offline'); this.isOnline = false; this.isSyncing = false; this.updateConnectivityUI(); }); }, async triggerSync() { if (!this.isOnline || this.isSyncing) { return; } console.log('Starting sync...'); this.isSyncing = true; this.updateConnectivityUI(); try { await this.processQueue(); } catch (error) { console.error('Sync error:', error); } finally { this.isSyncing = false; this.updateConnectivityUI(); } }, async processQueue() { // Get pending and uploading photos (retry stalled uploads) const pendingPhotos = await Storage.db.photos .where('username').equals(this.currentUsername) .and(photo => photo.status === 'pending' || photo.status === 'uploading') .sortBy('timestamp'); if (pendingPhotos.length === 0) { console.log('No pending photos to upload'); return; } console.log(`Found ${pendingPhotos.length} photos to upload`); // Process uploads sequentially (one at a time) for (const photo of pendingPhotos) { if (!this.isOnline) { console.log('Lost connection - stopping sync'); break; } await this.uploadPhoto(photo); } // Update UI this.updatePendingCount(); this.updateRecentPhotos(); }, async uploadPhoto(photo) { this.currentUpload = photo; try { console.log(`Uploading ${photo.filename}...`); // Update status to uploading await Storage.updatePhoto(photo.id, { status: 'uploading' }); this.updatePendingCount(); // Upload file const formData = new FormData(); formData.append('file', photo.blob, photo.filename); const uploadUrl = `/api/files/upload?path=${encodeURIComponent(photo.targetPath)}`; const uploadResponse = await fetch(uploadUrl, { method: 'POST', body: formData }); if (!uploadResponse.ok) { const error = await uploadResponse.json(); throw new Error(error.error || 'Upload failed'); } const uploadResult = await uploadResponse.json(); console.log(`Upload successful: ${uploadResult.path}`); // Update status to uploaded await Storage.updatePhoto(photo.id, { status: 'uploaded' }); // Verify file exists on server const verifyUrl = `/api/files/verify?path=${encodeURIComponent(uploadResult.path)}`; const verifyResponse = await fetch(verifyUrl); if (!verifyResponse.ok) { throw new Error('Verification failed - file not found on server'); } const verifyResult = await verifyResponse.json(); if (!verifyResult.exists) { throw new Error('Verification failed - file does not exist'); } console.log(`Verification successful: ${uploadResult.path}`); // Update status to verified await Storage.updatePhoto(photo.id, { status: 'verified' }); // Delete from IndexedDB (only after verification!) await Storage.deletePhoto(photo.id); console.log(`Deleted photo ${photo.id} from IndexedDB`); // Clear any pending retry if (this.retryTimeouts[photo.id]) { clearTimeout(this.retryTimeouts[photo.id]); delete this.retryTimeouts[photo.id]; } } catch (error) { console.error(`Error uploading ${photo.filename}:`, error); // Handle upload failure await this.handleUploadFailure(photo, error.message); } finally { this.currentUpload = null; } }, async handleUploadFailure(photo, errorMessage) { const retryCount = (photo.retryCount || 0) + 1; // Update photo with error info await Storage.updatePhoto(photo.id, { status: 'pending', retryCount: retryCount, lastError: errorMessage }); // Calculate retry delay using exponential backoff const delayIndex = Math.min(retryCount - 1, this.retryDelays.length - 1); const delay = this.retryDelays[delayIndex]; console.log(`Scheduling retry #${retryCount} in ${delay / 1000}s for ${photo.filename}`); // Schedule retry if (this.retryTimeouts[photo.id]) { clearTimeout(this.retryTimeouts[photo.id]); } this.retryTimeouts[photo.id] = setTimeout(() => { delete this.retryTimeouts[photo.id]; if (this.isOnline) { console.log(`Retrying upload for ${photo.filename}`); this.uploadPhoto(photo); } }, delay); }, async updateRecentPhotos() { if (typeof Camera !== 'undefined' && Camera.updateRecentPhotos) { await Camera.updateRecentPhotos(); } }, updateConnectivityUI() { const indicator = document.querySelector(".connectivity-indicator"); if (!indicator) return; indicator.classList.remove("online", "offline", "syncing"); if (!this.isOnline) { indicator.classList.add("offline"); indicator.title = "Offline"; } else if (this.isSyncing) { indicator.classList.add("syncing"); indicator.title = "Syncing..."; } else { indicator.classList.add("online"); indicator.title = "Online"; } }, async updatePendingCount() { if (!this.currentUsername) return; const countElement = document.getElementById("pendingCount"); const countValueElement = document.getElementById("pendingCountValue"); if (!countElement || !countValueElement) return; const count = await Storage.getPhotoCount(this.currentUsername, "pending"); if (count > 0) { countValueElement.textContent = count; countElement.style.display = "block"; } else { countElement.style.display = "none"; } }, }, getState() { return { isOnline: this.isOnline, isSyncing: this.isSyncing, }; } // Make Sync globally available as SyncEngine window.SyncEngine = Sync; console.log("[SYNC] SyncEngine exported:", typeof window.SyncEngine);