Fix stale-state when account credentials change at runtime

Reported user bug: "When I add an account, remove it, add a new one, the
new account is only really used after restart." Multiple cache layers were
not invalidated when settings changed, causing the system to keep using
stale state until the app was restarted.

Three layers of caching needed invalidation:

1. DebridService cached client instances (debrid.ts ~3067)
   The cached DebridLinkClient / LinkSnappyClient / DdownloadClient hold
   internal state (session cookies, auth tokens, parsed key lists). Without
   explicit invalidation when credentials change, the OLD client instance
   keeps serving requests until apiKeysRaw / login+password no longer match
   the cache key - which doesn't always trigger because the cache key may
   incidentally match (e.g. user removes and re-adds the same key).
   Fix: setSettings() now compares previous vs next credentials per provider
   and explicitly clears the cache when they differ.

2. MegaDebridClient.cachedApiTokens (debrid.ts ~1533)
   Static module-level token cache keyed by login (lowercase). When a user
   changes the password for an existing login (same login, new password),
   the cached token was kept for up to 20 minutes and would only get cleared
   after the API returned 401/403.
   Fix: Two new static methods - pruneCachedTokensNotIn() removes entries
   whose login is no longer in the active list, and clearCachedApiToken()
   force-clears a specific login. Both called from setSettings() based on
   diff between previous and next account list.

3. Module-level Debrid-Link cooldown maps (debrid.ts ~41-51)
   debridLinkKeyCooldowns / debridLinkKeyCooldownDetails / runtime statuses
   are keyed by API key ID (FNV-1a hash of the key). When a key was put
   into cooldown then removed from settings then re-added later, the old
   cooldown entry would still block it.
   Fix: New pruneDebridLinkRuntimeStateForKeys() function called from
   setSettings() removes cooldown entries for keys no longer in the active
   set.

4. providerFailures circuit-breaker map (download-manager.ts ~1990)
   Per-provider failure tracking with cooldownUntil. When a user removes a
   failing account and adds a new one of the same provider, the cooldown
   would carry over. Now setSettings() compares per-provider credentials
   and clears matching providerFailures entries when they change.

Reproduction (now fixed):
  1. Add Debrid-Link key A
  2. Trigger a failure (e.g. invalid link) so A goes into cooldown
  3. Remove A, add B in settings
  4. Try a download immediately - previously the cooldown for A or the
     cached client with A's state could still prevent B from being used.
     After this fix B is used immediately.

Tests: 201/201 (debrid + download-manager) green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-04-19 21:59:28 +02:00
co-authored by Claude Opus 4.6
parent 04413599d8
commit c3590f08fc
2 changed files with 121 additions and 0 deletions
+31
View File
@@ -1987,6 +1987,37 @@ export class DownloadManager extends EventEmitter {
this.hybridFailedArchives.clear();
}
// When account credentials change, clear the provider-failure circuit-breaker
// for affected providers. Otherwise a freshly added account would inherit
// the cooldown that the previous (now removed) account had triggered, and
// the user would be confused why "their new account doesn't work right away".
const credChanges: Array<{ prev: string; next: string; providers: string[] }> = [
{ prev: previous.token || "", next: next.token || "", providers: ["realdebrid"] },
{ prev: previous.allDebridToken || "", next: next.allDebridToken || "", providers: ["alldebrid"] },
{ prev: previous.bestDebridApiKey || "", next: next.bestDebridApiKey || "", providers: ["bestdebrid"] },
{ prev: previous.debridLinkApiKeys || "", next: next.debridLinkApiKeys || "", providers: ["debridlink"] },
{ prev: previous.linkSnappyLogin + "|" + previous.linkSnappyPassword, next: next.linkSnappyLogin + "|" + next.linkSnappyPassword, providers: ["linksnappy"] },
{ prev: previous.ddownloadLogin + "|" + previous.ddownloadPassword, next: next.ddownloadLogin + "|" + next.ddownloadPassword, providers: ["ddownload"] },
{ prev: previous.megaCredentials + "|" + previous.megaPassword, next: next.megaCredentials + "|" + next.megaPassword, providers: ["megadebrid", "megadebrid-api", "megadebrid-web"] }
];
let clearedProviderFailures = 0;
for (const change of credChanges) {
if (change.prev === change.next) continue;
for (const provider of change.providers) {
// Provider failure keys are sometimes "provider" alone, sometimes "provider:hoster".
// Clear all entries that start with the provider name.
for (const key of [...this.providerFailures.keys()]) {
if (key === provider || key.startsWith(`${provider}:`)) {
this.providerFailures.delete(key);
clearedProviderFailures += 1;
}
}
}
}
if (clearedProviderFailures > 0) {
logger.info(`Settings-Update: ${clearedProviderFailures} Provider-Failure(s) gecleart wegen geaenderter Credentials`);
}
this.resolveExistingQueuedOpaqueFilenames();
void this.cleanupExistingExtractedArchives().catch((err) => logger.warn(`cleanupExistingExtractedArchives Fehler (setSettings): ${compactErrorText(err)}`));
if (next.completedCleanupPolicy !== "never") {