fix: multi-level account rotation + clear failed-accounts per batch + size-sort staleness
Three state bugs found during audit:
1. _failedAccounts / _accountOverrides survived across batches. A
rate-limited account from batch 1 stayed permanently blacklisted
for the rest of the app session, so batch 2 skipped straight to
the fallback even after the original recovered. Now cleared in
startBatch so each run evaluates accounts fresh.
2. Account rotation was one level deep. With three accounts [A,B,C]
on the same hoster and A + B both failing, the job errored out
— C was never tried. The fallback-retry was a single if-block.
Replaced with a while-loop that keeps asking main for the next
override and rotating until every account is exhausted.
3. Queue sort cache included 'size' as a static key, but bytesTotal
goes 0 → actual when previews resolve. A queue sorted by size
during preview would cache the all-zeros order and never update.
Removed size from _STATIC_SORT_KEYS — it now re-sorts per render
like status/speed/progress.
This commit is contained in:
parent
5265bcd77a
commit
880537dcfb
@ -132,6 +132,12 @@ class UploadManager extends EventEmitter {
|
||||
this.globalSemaphore = null;
|
||||
this.globalThrottle = null;
|
||||
this.lastStartTime = {};
|
||||
// Reset account-rotation state each batch. Otherwise a previously failed
|
||||
// account (e.g. rate-limited during the last batch) stays permanently
|
||||
// blacklisted until the app restarts — every upload would silently skip
|
||||
// straight to the fallback even after the original recovered.
|
||||
this._failedAccounts.clear();
|
||||
this._accountOverrides.clear();
|
||||
|
||||
const { signal } = this.abortController;
|
||||
const batchId = `batch-${Date.now()}`;
|
||||
@ -461,14 +467,17 @@ class UploadManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Account fallback: if this account hasn't failed before, try switching
|
||||
if (task.accountId && !this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
|
||||
// Account rotation: mark the current account failed, wait for main to
|
||||
// resolve the next fallback, then retry. Loop so A → B → C → ... works
|
||||
// for hosters with 3+ accounts (the old code only did one level: A → B
|
||||
// and stopped, even if C would have worked).
|
||||
while (task.accountId && !this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
|
||||
if (signal.aborted || this.stopAfterActive) break;
|
||||
this._failedAccounts.set(task.hoster + ':' + task.accountId, true);
|
||||
this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId });
|
||||
// Wait briefly for switchAccount() to be called from main process
|
||||
await this._sleep(800, signal);
|
||||
const override = this._accountOverrides.get(task.hoster);
|
||||
if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) {
|
||||
if (!override || this._failedAccounts.has(task.hoster + ':' + override.id)) break;
|
||||
// Switch to fallback account and retry this file
|
||||
task.accountId = override.id;
|
||||
task.username = override.username;
|
||||
@ -479,7 +488,9 @@ class UploadManager extends EventEmitter {
|
||||
speedKbs: 0, elapsed: 0, remaining: 0,
|
||||
error: 'Account-Wechsel zu Fallback', result: null, attempt: 1, maxAttempts
|
||||
});
|
||||
// Re-run retry loop with new account
|
||||
// Retry loop with the new account. On exhausted failure, the while
|
||||
// loop iterates: marks this account failed too, asks main for the next
|
||||
// fallback, and so on.
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
if (signal.aborted || this.stopAfterActive) break;
|
||||
if (attempt > 1) {
|
||||
@ -495,7 +506,8 @@ class UploadManager extends EventEmitter {
|
||||
let lastBytes = 0;
|
||||
let lastSpeedTime = jobStart;
|
||||
let currentSpeedKbs = 0;
|
||||
this.activeJobs.set(uploadId, { jobId, speedKbs: 0, bytesUploaded: 0 });
|
||||
const activeEntry = { jobId, speedKbs: 0, bytesUploaded: 0 };
|
||||
this.activeJobs.set(uploadId, activeEntry);
|
||||
|
||||
const progressCb = (bytesUploaded, bytesTotal) => {
|
||||
const now = Date.now();
|
||||
@ -505,7 +517,8 @@ class UploadManager extends EventEmitter {
|
||||
lastBytes = bytesUploaded;
|
||||
lastSpeedTime = now;
|
||||
}
|
||||
this.activeJobs.set(uploadId, { jobId, speedKbs: currentSpeedKbs, bytesUploaded });
|
||||
activeEntry.speedKbs = currentSpeedKbs;
|
||||
activeEntry.bytesUploaded = bytesUploaded;
|
||||
const elapsed = Math.round((now - jobStart) / 1000);
|
||||
const remaining = currentSpeedKbs > 0 ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) : 0;
|
||||
this._emitProgress(uploadId, fileName, task.hoster, {
|
||||
@ -536,7 +549,6 @@ class UploadManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler';
|
||||
emitFinalStatus('error', { error });
|
||||
|
||||
@ -1062,13 +1062,13 @@ function _onQueueScroll() {
|
||||
const _collatorDE = new Intl.Collator('de', { sensitivity: 'base', numeric: true });
|
||||
const _collatorSimple = new Intl.Collator('de');
|
||||
|
||||
// Queue sort memoization. Keys that don't change during upload (filename, host,
|
||||
// size) reuse the cached result across progress-driven re-renders. Dynamic keys
|
||||
// (status/speed/progress) are recomputed each call since the sort order itself
|
||||
// moves every tick. For a queue of 1000+ jobs sorted by filename, this skips
|
||||
// the Collator-based O(n log n) sort on every 200ms progress render.
|
||||
// Queue sort memoization. Keys that don't change after a job enters the queue
|
||||
// (filename, host) reuse the cached result across progress-driven re-renders.
|
||||
// Dynamic keys (status/speed/progress) AND size (which goes 0 → actual when
|
||||
// previews resolve / upload starts) are recomputed each call — otherwise a
|
||||
// queue sorted by size during previews would be stuck in all-zeros order.
|
||||
let _queueSortCache = { sig: '', result: [] };
|
||||
const _STATIC_SORT_KEYS = new Set(['filename', 'host', 'size']);
|
||||
const _STATIC_SORT_KEYS = new Set(['filename', 'host']);
|
||||
|
||||
function sortQueueJobs(jobs) {
|
||||
const { key, direction } = queueSortState;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user