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,
VERIFY_DELAY_MS: 800,
UPLOAD_TIMEOUT_MS: 120000,
// Backoff delays in ms per retry attempt
BACKOFF_DELAYS: [0, 2000, 5000, 10000, 15000, 30000, 30000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000],
_retryTimerId: null,
init(username) {
this.currentUsername = username;
@@ -34,6 +33,10 @@ const Sync = {
console.log('[SYNC] Network offline');
this.isOnline = false;
this.isSyncing = false;
if (this._retryTimerId) {
clearTimeout(this._retryTimerId);
this._retryTimerId = null;
}
});
},
@@ -63,14 +66,26 @@ const Sync = {
console.log('[SYNC] Starting sync...');
this.isSyncing = true;
let hadFailures = false;
try {
await this.processQueue();
hadFailures = await this.processQueue();
} catch (error) {
console.error('[SYNC] Error:', error);
hadFailures = true;
} finally {
this.isSyncing = 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() {
@@ -82,9 +97,11 @@ const Sync = {
console.log('[SYNC] Found', pendingPhotos.length, 'photos to process');
if (pendingPhotos.length === 0) {
return;
return false;
}
let hadFailures = false;
for (const photo of pendingPhotos) {
if (!this.isOnline) {
console.log('[SYNC] Lost connection, stopping');
@@ -99,21 +116,13 @@ const Sync = {
continue;
}
// Backoff delay before retry attempts
if (retryCount > 0) {
const backoff = this.BACKOFF_DELAYS[Math.min(retryCount, this.BACKOFF_DELAYS.length - 1)];
console.log('[SYNC] Waiting', backoff + 'ms before retry', retryCount + 1);
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);
// No delay — upload immediately. Retries are handled by
// scheduling a new triggerSync() after the full queue pass.
const success = await this.uploadPhoto(photo);
if (!success) hadFailures = true;
}
return hadFailures;
},
async uploadPhoto(photo) {
@@ -134,7 +143,8 @@ const Sync = {
// Prevent page navigation during upload
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) {
const fullPath = photo.targetPath.replace(/\/$/, '') + '/' + photo.filename;
const alreadyExists = await this.checkFileExists(fullPath);
@@ -148,7 +158,7 @@ const Sync = {
});
await this.pruneHistory();
this._setUploading(false);
return;
return true;
}
}
@@ -227,6 +237,7 @@ const Sync = {
// Clear navigation guard after successful upload
this._setUploading(false);
return true;
} catch (error) {
this._setUploading(false);
@@ -237,6 +248,7 @@ const Sync = {
lastError: error.message,
error: error.message
});
return false;
}
},
@@ -264,7 +276,6 @@ const Sync = {
const result = await response.json();
return result.exists === true;
} catch (e) {
// If we can't check, assume it doesn't exist and proceed with upload
return false;
}
},