From 8acb22d3afdafd573ed790078bf93997d6f5e558 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 10 Jun 2026 00:28:50 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Dedup-Lifecycle=20der=20Benachrichtigung?= =?UTF-8?q?en=20=E2=80=94=20Recovery/Retry/Trailing-Pakete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit-Befunde DEDUP-1 (HIGH), DEDUP-3, DEDUP-4, NOTIFY-DEDUP-BEFORE-CONFIRM: - DEDUP-1: recoverRetryableItems (Auto-Requeue gefailter Items beim Start) loescht jetzt den notifiedPackages-Eintrag der betroffenen Pakete. Vorher blieb der Failed-Marker aus Run 1 kleben — die Erfolgs-Benachrichtigung nach geglueckter Recovery in Run 2 (genau die Nachricht, auf die man wartet) kam nie; in Discord blieb das Paket fuer immer ❌. - DEDUP-3: retryExtraction/extractNow loeschen den Dedup-Marker und treten dem Run UNBEDINGT bei (runPackageIds.add) — der korrigierende ✅ nach manuell wiederholtem Entpacken kam sonst nie an, v.a. wenn der Run schon vorbei war. History-Dedup bewusst unangetastet (sonst doppelte History-Eintraege). - DEDUP-4: start()/startPackages()/startItems() ersetzen runPackageIds; Pakete mit noch LAUFENDEM Post-Processing (Task, Deferred, Hybrid) flogen aus dem Set und ihre Abschluss-Benachrichtigung wurde nach dem naechsten finishRun verworfen (autoExtractWhenStopped beendet den Run ~sofort). Neuer Helper addTrailingPostProcessPackageIds ergaenzt das neue Set an allen 5 Stellen — inkl. der Start-ohne-Queue-Pfade, die sogar die GERADE angestossenen Entpackungen aus dem Set warfen. - Dedup-Marker wird wieder freigegeben, wenn der Versand (nach den Sender- Retries) endgueltig scheiterte — ein transienter Ausfall verbraucht den Einmal-Slot nicht mehr dauerhaft. --- src/main/download-manager.ts | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 3df8a80..2dd9cad 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -4661,6 +4661,29 @@ export class DownloadManager extends EventEmitter { return false; } + // Packages whose post-processing (task, deferred pass or hybrid round) is still + // alive must keep their run-membership when the run set is replaced/cleared — + // otherwise their terminal notification is dropped as soon as session.running + // flips false (the notify guard checks running || runPackageIds). + private addTrailingPostProcessPackageIds(target: Set): void { + for (const id of this.packagePostProcessTasks.keys()) { + target.add(id); + } + for (const [id, controller] of this.packageDeferredPostProcessAbortControllers) { + if (!controller.signal.aborted) { + target.add(id); + } + } + for (const [id, hybridSet] of this.packageHybridPostProcessControllers) { + for (const c of hybridSet) { + if (!c.signal.aborted) { + target.add(id); + break; + } + } + } + } + private buildCollectFolderCandidates(sourcePath: string, sourceRoot: string, pkg: PackageEntry): string[] { const folderCandidates: string[] = []; let currentDir = path.dirname(sourcePath); @@ -5326,6 +5349,7 @@ export class DownloadManager extends EventEmitter { } this.runItemIds = new Set(runItems.map((item) => item.id)); this.runPackageIds = new Set(runItems.map((item) => item.packageId)); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); @@ -5430,6 +5454,7 @@ export class DownloadManager extends EventEmitter { } this.runItemIds = new Set(runItems.map((item) => item.id)); this.runPackageIds = new Set(runItems.map((item) => item.packageId)); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); @@ -5519,6 +5544,7 @@ export class DownloadManager extends EventEmitter { if (this.packagePostProcessTasks.size > 0) { this.runItemIds.clear(); this.runPackageIds.clear(); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.session.running = true; @@ -5537,6 +5563,7 @@ export class DownloadManager extends EventEmitter { } this.runItemIds.clear(); this.runPackageIds.clear(); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); @@ -5567,6 +5594,7 @@ export class DownloadManager extends EventEmitter { } this.runItemIds = new Set(runItems.map((item) => item.id)); this.runPackageIds = new Set(runItems.map((item) => item.packageId)); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); @@ -7499,6 +7527,11 @@ export class DownloadManager extends EventEmitter { completedItems: completedItems.length, targetedItems: targetItems.length }); + // Fresh outcome must notify again (the corrective checkmark after a notified + // extraction failure); unconditional run-membership so the notify guard also + // passes when the retry happens after the run already ended. + this.notifiedPackages.delete(packageId); + this.runPackageIds.add(packageId); this.persistSoon(); this.emitState(true); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (retryExtraction): ${compactErrorText(err)}`)); @@ -7527,6 +7560,8 @@ export class DownloadManager extends EventEmitter { completedItems: completedItems.length, targetedItems: targetItems.length }); + this.notifiedPackages.delete(packageId); + this.runPackageIds.add(packageId); this.persistSoon(); this.emitState(true); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (extractNow): ${compactErrorText(err)}`)); @@ -10419,6 +10454,10 @@ export class DownloadManager extends EventEmitter { if (!pkg) { continue; } + // The package gets a fresh chance — its next outcome must notify again + // (a recovery success after a notified failure is the message the user + // most wants). History dedup stays untouched (separate semantics). + this.notifiedPackages.delete(packageId); this.refreshPackageStatus(pkg); } logger.warn( @@ -10503,10 +10542,17 @@ export class DownloadManager extends EventEmitter { return; } this.notifiedPackages.add(pkg.id); + // Release the dedup marker if the delivery ultimately failed (after the + // sender's own retries), so a later manual re-run can notify again instead + // of a transient outage permanently consuming the once-per-package slot. void sendNotification(url, { title: kind === "completed" ? "✅ Paket fertig" : "❌ Paket fehlgeschlagen", message: `${pkg.name}\n${detail}`, mention: this.settings.notifyMention + }).then((ok) => { + if (!ok) { + this.notifiedPackages.delete(pkg.id); + } }); }