Fix: Dedup-Lifecycle der Benachrichtigungen — Recovery/Retry/Trailing-Pakete

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.
This commit is contained in:
Sucukdeluxe 2026-06-10 00:28:50 +02:00
parent 99a24592c5
commit 8acb22d3af

View File

@ -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<string>): 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);
}
});
}