Fix uploads stuck pending due to page navigation killing sync

The sync engine was adding per-photo backoff delays (2-60s) inside
the processing loop, during which the user would navigate to another
page (browser -> queue -> browser), killing the sync mid-wait. The
upload POST never fired because the delay ran out the clock.

Changes:
- Remove per-photo backoff delay from processQueue loop — uploads
  start immediately on page load without blocking
- Move retry scheduling to after the full queue pass: if any uploads
  failed, schedule a new triggerSync() after 15s instead of blocking
  inline
- Keep the duplicate check on retries (fast, catches interrupted
  uploads that actually succeeded server-side)
- uploadPhoto now returns true/false so processQueue can track
  failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 15:48:44 -06:00
parent 4491531acb
commit bc748d05ac

View File

@@ -10,8 +10,7 @@ const Sync = {
MAX_RETRIES: 15, MAX_RETRIES: 15,
VERIFY_DELAY_MS: 800, VERIFY_DELAY_MS: 800,
UPLOAD_TIMEOUT_MS: 120000, UPLOAD_TIMEOUT_MS: 120000,
// Backoff delays in ms per retry attempt _retryTimerId: null,
BACKOFF_DELAYS: [0, 2000, 5000, 10000, 15000, 30000, 30000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000],
init(username) { init(username) {
this.currentUsername = username; this.currentUsername = username;
@@ -34,6 +33,10 @@ const Sync = {
console.log('[SYNC] Network offline'); console.log('[SYNC] Network offline');
this.isOnline = false; this.isOnline = false;
this.isSyncing = false; this.isSyncing = false;
if (this._retryTimerId) {
clearTimeout(this._retryTimerId);
this._retryTimerId = null;
}
}); });
}, },
@@ -63,14 +66,26 @@ const Sync = {
console.log('[SYNC] Starting sync...'); console.log('[SYNC] Starting sync...');
this.isSyncing = true; this.isSyncing = true;
let hadFailures = false;
try { try {
await this.processQueue(); hadFailures = await this.processQueue();
} catch (error) { } catch (error) {
console.error('[SYNC] Error:', error); console.error('[SYNC] Error:', error);
hadFailures = true;
} finally { } finally {
this.isSyncing = false; this.isSyncing = false;
this._setUploading(false); this._setUploading(false);
} }
// If there were failures, schedule a retry cycle after a delay
// so it doesn't block the current page load
if (hadFailures && this.isOnline) {
console.log('[SYNC] Scheduling retry in 15s...');
this._retryTimerId = setTimeout(() => {
this._retryTimerId = null;
this.triggerSync();
}, 15000);
}
}, },
async processQueue() { async processQueue() {
@@ -82,9 +97,11 @@ const Sync = {
console.log('[SYNC] Found', pendingPhotos.length, 'photos to process'); console.log('[SYNC] Found', pendingPhotos.length, 'photos to process');
if (pendingPhotos.length === 0) { if (pendingPhotos.length === 0) {
return; return false;
} }
let hadFailures = false;
for (const photo of pendingPhotos) { for (const photo of pendingPhotos) {
if (!this.isOnline) { if (!this.isOnline) {
console.log('[SYNC] Lost connection, stopping'); console.log('[SYNC] Lost connection, stopping');
@@ -99,21 +116,13 @@ const Sync = {
continue; continue;
} }
// Backoff delay before retry attempts // No delay — upload immediately. Retries are handled by
if (retryCount > 0) { // scheduling a new triggerSync() after the full queue pass.
const backoff = this.BACKOFF_DELAYS[Math.min(retryCount, this.BACKOFF_DELAYS.length - 1)]; const success = await this.uploadPhoto(photo);
console.log('[SYNC] Waiting', backoff + 'ms before retry', retryCount + 1); if (!success) hadFailures = true;
await this.delay(backoff);
// Re-check connectivity after waiting
if (!this.isOnline) {
console.log('[SYNC] Lost connection during backoff, stopping');
break;
}
} }
await this.uploadPhoto(photo); return hadFailures;
}
}, },
async uploadPhoto(photo) { async uploadPhoto(photo) {
@@ -134,7 +143,8 @@ const Sync = {
// Prevent page navigation during upload // Prevent page navigation during upload
this._setUploading(true); this._setUploading(true);
// Check for duplicates on retries (skip on first attempt to reduce latency) // On retries, first check if a previous attempt actually succeeded
// (the upload may have completed but the verify/status update was interrupted)
if (retryCount > 0) { if (retryCount > 0) {
const fullPath = photo.targetPath.replace(/\/$/, '') + '/' + photo.filename; const fullPath = photo.targetPath.replace(/\/$/, '') + '/' + photo.filename;
const alreadyExists = await this.checkFileExists(fullPath); const alreadyExists = await this.checkFileExists(fullPath);
@@ -148,7 +158,7 @@ const Sync = {
}); });
await this.pruneHistory(); await this.pruneHistory();
this._setUploading(false); this._setUploading(false);
return; return true;
} }
} }
@@ -227,6 +237,7 @@ const Sync = {
// Clear navigation guard after successful upload // Clear navigation guard after successful upload
this._setUploading(false); this._setUploading(false);
return true;
} catch (error) { } catch (error) {
this._setUploading(false); this._setUploading(false);
@@ -237,6 +248,7 @@ const Sync = {
lastError: error.message, lastError: error.message,
error: error.message error: error.message
}); });
return false;
} }
}, },
@@ -264,7 +276,6 @@ const Sync = {
const result = await response.json(); const result = await response.json();
return result.exists === true; return result.exists === true;
} catch (e) { } catch (e) {
// If we can't check, assume it doesn't exist and proceed with upload
return false; return false;
} }
}, },