fix(notifications): preserve stopped package suppression

This commit is contained in:
Sucukdeluxe
2026-08-22 06:06:27 +02:00
parent 96966c3eca
commit 26ebd931b7
2 changed files with 78 additions and 31 deletions
+27 -8
View File
@@ -6346,7 +6346,8 @@ export class DownloadManager extends EventEmitter {
const stoppedRunContext = wasRunning const stoppedRunContext = wasRunning
? this.stopActiveRunContext(this.runPackageIds, this.session.runStartedAt) ? this.stopActiveRunContext(this.runPackageIds, this.session.runStartedAt)
: null; : null;
this.schedulerGeneration += 1; this.suppressStandalonePackageResults();
this.schedulerGeneration += 1;
this.session.running = false; this.session.running = false;
this.session.paused = false; this.session.paused = false;
this.session.reconnectUntil = 0; this.session.reconnectUntil = 0;
@@ -8325,8 +8326,8 @@ export class DownloadManager extends EventEmitter {
completedItems: completedItems.length, completedItems: completedItems.length,
targetedItems: targetItems.length targetedItems: targetItems.length
}); });
const generation = this.beginPackageResultGeneration(packageId, false, true); this.beginPackageResultGeneration(packageId, false, true);
this.standalonePackageResults.add(this.packageResultKey(packageId, generation)); this.reactivateStandalonePackageResult(packageId);
this.persistSoon(); this.persistSoon();
this.emitState(true); this.emitState(true);
void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (retryExtraction): ${compactErrorText(err)}`)); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (retryExtraction): ${compactErrorText(err)}`));
@@ -8355,8 +8356,8 @@ export class DownloadManager extends EventEmitter {
completedItems: completedItems.length, completedItems: completedItems.length,
targetedItems: targetItems.length targetedItems: targetItems.length
}); });
const generation = this.beginPackageResultGeneration(packageId, false, true); this.beginPackageResultGeneration(packageId, false, true);
this.standalonePackageResults.add(this.packageResultKey(packageId, generation)); this.reactivateStandalonePackageResult(packageId);
this.persistSoon(); this.persistSoon();
this.emitState(true); this.emitState(true);
void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (extractNow): ${compactErrorText(err)}`)); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (extractNow): ${compactErrorText(err)}`));
@@ -11629,9 +11630,9 @@ export class DownloadManager extends EventEmitter {
if (!pkg) { if (!pkg) {
continue; continue;
} }
const generation = this.beginPackageResultGeneration(packageId, false, true); this.beginPackageResultGeneration(packageId, false, true);
if (!this.runPackageIds.has(packageId)) { if (!this.runPackageIds.has(packageId)) {
this.standalonePackageResults.add(this.packageResultKey(packageId, generation)); this.reactivateStandalonePackageResult(packageId);
} }
this.refreshPackageStatus(pkg); this.refreshPackageStatus(pkg);
} }
@@ -11827,11 +11828,26 @@ export class DownloadManager extends EventEmitter {
} }
private trackStandalonePackageResult(packageId: string): void { private trackStandalonePackageResult(packageId: string): void {
const key = this.packageResultKey(packageId, this.getPackageResultGeneration(packageId));
if (this.suppressedPackageResults.has(key)) {
return;
}
this.standalonePackageResults.add(key);
}
private reactivateStandalonePackageResult(packageId: string): void {
const key = this.packageResultKey(packageId, this.getPackageResultGeneration(packageId)); const key = this.packageResultKey(packageId, this.getPackageResultGeneration(packageId));
this.suppressedPackageResults.delete(key); this.suppressedPackageResults.delete(key);
this.standalonePackageResults.add(key); this.standalonePackageResults.add(key);
} }
private suppressStandalonePackageResults(): void {
for (const key of this.standalonePackageResults) {
this.suppressedPackageResults.add(key);
}
this.standalonePackageResults.clear();
}
private trackPackagePostProcessResult(packageId: string): void { private trackPackagePostProcessResult(packageId: string): void {
const generation = this.getPackageResultGeneration(packageId); const generation = this.getPackageResultGeneration(packageId);
const key = this.packageResultKey(packageId, generation); const key = this.packageResultKey(packageId, generation);
@@ -11909,6 +11925,10 @@ export class DownloadManager extends EventEmitter {
return null; return null;
} }
const generation = this.getPackageResultGeneration(packageId); const generation = this.getPackageResultGeneration(packageId);
const key = this.packageResultKey(packageId, generation);
if (this.suppressedPackageResults.has(key)) {
return null;
}
if (!this.runPackageIds.has(packageId) && !this.isPackageResultTracked(packageId, generation)) { if (!this.runPackageIds.has(packageId) && !this.isPackageResultTracked(packageId, generation)) {
return null; return null;
} }
@@ -11916,7 +11936,6 @@ export class DownloadManager extends EventEmitter {
if (items.some((item) => !isFinishedStatus(item.status)) || this.hasPackageLifecycleWork(packageId)) { if (items.some((item) => !isFinishedStatus(item.status)) || this.hasPackageLifecycleWork(packageId)) {
return null; return null;
} }
const key = this.packageResultKey(packageId, generation);
const existing = this.finalizedPackageResults.get(key); const existing = this.finalizedPackageResults.get(key);
if (existing) { if (existing) {
return existing; return existing;
+51 -23
View File
@@ -575,16 +575,20 @@ describe("authoritative run completion", () => {
expect(history[0].name).toBe(pkg.name); expect(history[0].name).toBe(pkg.name);
}); });
it("stops and removes the active run context before late package work can emit", async () => { it("keeps stopped package postprocessing suppressed when a later start only runs another package", async () => {
const { manager, session, events, history } = setup({ autoExtractWhenStopped: true }); const { manager, session, events, history } = setup({ autoExtractWhenStopped: true });
const packageA = addPackage(session, ["completed"], "stopped-package"); const packageA = addPackage(session, ["queued"], "stopped-package");
const state = internal(manager); const state = internal(manager);
session.running = true; vi.spyOn(state, "ensureScheduler").mockResolvedValue(undefined);
session.runStartedAt = Date.now() - 20_000;
state.runItemIds = new Set(packageA.itemIds); await manager.start();
state.runPackageIds = new Set([packageA.id]); const packageAItem = session.items[packageA.itemIds[0]];
state.runOutcomes = new Map([[packageA.itemIds[0], "completed"]]); packageAItem.status = "completed";
const stoppedContext = state.beginActiveRunContext(state.runPackageIds, session.runStartedAt); packageAItem.downloadedBytes = 1_000;
packageAItem.totalBytes = 1_000;
packageAItem.progressPercent = 100;
packageAItem.fullStatus = "Fertig";
packageA.status = "completed";
let releasePostProcess = (): void => {}; let releasePostProcess = (): void => {};
const postProcessGate = new Promise<void>((resolve) => { const postProcessGate = new Promise<void>((resolve) => {
releasePostProcess = resolve; releasePostProcess = resolve;
@@ -595,31 +599,55 @@ describe("authoritative run completion", () => {
manager.stop(); manager.stop();
await flushNotifications(); await flushNotifications();
const stoppedEvent = events.find((event) => event.type === "run_stopped");
expect(stoppedEvent?.id).toBe(`run:${stoppedContext.id}:run_stopped`); const packageB = addPackage(session, ["queued"], "follow-up-package");
expect(state.activeRunContextId).toBeNull(); await manager.start();
expect(state.runContexts.has(stoppedContext.id)).toBe(false); expect(session.running).toBe(true);
expect(state.runPackageIds).toEqual(new Set([packageB.id]));
releasePostProcess(); releasePostProcess();
await latePostProcess; await latePostProcess;
await flushNotifications(); await flushNotifications();
expect(events.filter((event) => event.type === "package_completed")).toHaveLength(0); expect(events.filter((event) => event.type === "package_completed")).toHaveLength(0);
expect(history).toHaveLength(0); expect(history).toHaveLength(0);
});
const packageB = addPackage(session, ["completed"], "follow-up-package"); it("suppresses a stopped postprocess-only generation and allows an explicit package retry", async () => {
session.running = true; const { manager, session, events, history } = setup({ autoExtract: true, autoExtractWhenStopped: true });
session.runStartedAt = Date.now() - 5_000; const pkg = addPackage(session, ["completed"], "postprocess-only-package");
state.runItemIds = new Set(packageB.itemIds); const state = internal(manager);
state.runPackageIds = new Set([packageB.id]); vi.spyOn(state, "ensureScheduler").mockResolvedValue(undefined);
state.runOutcomes = new Map([[packageB.itemIds[0], "completed"]]); let releasePostProcess = (): void => {};
state.beginActiveRunContext(state.runPackageIds, session.runStartedAt); let postProcessGate = new Promise<void>((resolve) => {
state.finishRun(); releasePostProcess = resolve;
});
state.handlePackagePostProcessing = vi.fn(async () => postProcessGate);
await manager.start();
const stoppedPostProcess = state.packagePostProcessTasks.get(pkg.id);
expect(stoppedPostProcess).toBeDefined();
expect(state.runItemIds.size).toBe(0);
manager.stop();
releasePostProcess();
await stoppedPostProcess;
await flushNotifications();
expect(events.filter((event) => event.type === "package_completed")).toHaveLength(0);
expect(history).toHaveLength(0);
session.items[pkg.itemIds[0]].fullStatus = "Entpacken - Error";
postProcessGate = new Promise<void>((resolve) => {
releasePostProcess = resolve;
});
manager.retryExtraction(pkg.id);
const retriedPostProcess = state.packagePostProcessTasks.get(pkg.id);
expect(retriedPostProcess).toBeDefined();
releasePostProcess();
await retriedPostProcess;
await flushNotifications(); await flushNotifications();
expect(events.filter((event) => event.type === "run_stopped")).toHaveLength(1);
expect(events.filter((event) => event.type === "run_completed")).toHaveLength(1);
expect(events.filter((event) => event.type === "package_completed")).toHaveLength(1); expect(events.filter((event) => event.type === "package_completed")).toHaveLength(1);
expect(history.map((entry) => entry.name)).toEqual([packageB.name]); expect(history.map((entry) => entry.name)).toEqual([pkg.name]);
}); });
it("finalizes overlapping runs independently when the earlier run finishes deferred work last", async () => { it("finalizes overlapping runs independently when the earlier run finishes deferred work last", async () => {