fix(notifications): preserve stopped package suppression
This commit is contained in:
@@ -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
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user