fix(notifications): preserve run and package result identity
This commit is contained in:
+161
-156
@@ -466,7 +466,7 @@ type DownloadManagerOptions = {
|
|||||||
protectEmptyClobber?: boolean;
|
protectEmptyClobber?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PendingRunResult = {
|
type RunLifecycleContext = {
|
||||||
id: string;
|
id: string;
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
packageGenerations: Map<string, number>;
|
packageGenerations: Map<string, number>;
|
||||||
@@ -1936,13 +1936,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private historyRecordedPackages = new Set<string>();
|
private historyRecordedPackages = new Set<string>();
|
||||||
|
|
||||||
private packageResultGenerations = new Map<string, number>();
|
|
||||||
|
|
||||||
private finalizedPackageResults = new Map<string, PackageResult>();
|
private finalizedPackageResults = new Map<string, PackageResult>();
|
||||||
|
|
||||||
private runPackageGenerations = new Map<string, number>();
|
private runContexts = new Map<string, RunLifecycleContext>();
|
||||||
|
|
||||||
private pendingRunResult: PendingRunResult | null = null;
|
private standalonePackageResults = new Set<string>();
|
||||||
|
|
||||||
private successDigestResults = new Map<string, PackageResultEnvelope>();
|
private successDigestResults = new Map<string, PackageResultEnvelope>();
|
||||||
|
|
||||||
@@ -3122,10 +3120,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.runOutcomes.clear();
|
this.runOutcomes.clear();
|
||||||
this.runCompletedPackages.clear();
|
this.runCompletedPackages.clear();
|
||||||
this.historyRecordedPackages.clear();
|
this.historyRecordedPackages.clear();
|
||||||
this.packageResultGenerations.clear();
|
|
||||||
this.finalizedPackageResults.clear();
|
this.finalizedPackageResults.clear();
|
||||||
this.runPackageGenerations.clear();
|
this.runContexts.clear();
|
||||||
this.pendingRunResult = null;
|
this.standalonePackageResults.clear();
|
||||||
this.successDigestResults.clear();
|
this.successDigestResults.clear();
|
||||||
if (this.successDigestTimer) {
|
if (this.successDigestTimer) {
|
||||||
clearTimeout(this.successDigestTimer);
|
clearTimeout(this.successDigestTimer);
|
||||||
@@ -3194,6 +3191,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
cleanedTotalBytes: 0,
|
cleanedTotalBytes: 0,
|
||||||
cleanedUrls: [],
|
cleanedUrls: [],
|
||||||
cleanedProviders: [],
|
cleanedProviders: [],
|
||||||
|
resultGeneration: 1,
|
||||||
downloadStartedAt: 0,
|
downloadStartedAt: 0,
|
||||||
downloadCompletedAt: 0,
|
downloadCompletedAt: 0,
|
||||||
createdAt: nowMs(),
|
createdAt: nowMs(),
|
||||||
@@ -3240,7 +3238,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (this.session.running) {
|
if (this.session.running) {
|
||||||
this.runItemIds.add(itemId);
|
this.runItemIds.add(itemId);
|
||||||
this.runPackageIds.add(packageId);
|
this.runPackageIds.add(packageId);
|
||||||
this.runPackageGenerations.set(packageId, this.beginPackageResultGeneration(packageId));
|
this.beginPackageResultGeneration(packageId);
|
||||||
}
|
}
|
||||||
if (looksLikeOpaqueFilename(fileName)) {
|
if (looksLikeOpaqueFilename(fileName)) {
|
||||||
const existing = unresolvedByLink.get(link) ?? [];
|
const existing = unresolvedByLink.get(link) ?? [];
|
||||||
@@ -3971,7 +3969,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async directoryHasAnyFiles(rootDir: string): Promise<boolean> {
|
private async directoryHasAnyFiles(rootDir: string): Promise<boolean> {
|
||||||
if (!rootDir) {
|
if (!rootDir) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -4005,10 +4003,39 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeEmptyDirectoryTree(rootDir: string): Promise<number> {
|
private async countPackageOutputFiles(rootDir: string): Promise<number> {
|
||||||
|
if (!rootDir) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const stack = [rootDir];
|
||||||
|
let count = 0;
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const current = stack.pop() as string;
|
||||||
|
let entries: fs.Dirent[] = [];
|
||||||
|
try {
|
||||||
|
entries = await fs.promises.readdir(current, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(current, entry.name);
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
stack.push(fullPath);
|
||||||
|
} else if (entry.isFile() && !isArchiveLikePath(fullPath) && !isIgnorableEmptyDirFileName(entry.name)) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeEmptyDirectoryTree(rootDir: string): Promise<number> {
|
||||||
if (!rootDir) {
|
if (!rootDir) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -5288,32 +5315,6 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return false;
|
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, tasks] of this.packageHybridPostProcessTasks) {
|
|
||||||
if (tasks.size > 0) 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[] {
|
private buildCollectFolderCandidates(sourcePath: string, sourceRoot: string, pkg: PackageEntry): string[] {
|
||||||
const folderCandidates: string[] = [];
|
const folderCandidates: string[] = [];
|
||||||
let currentDir = path.dirname(sourcePath);
|
let currentDir = path.dirname(sourcePath);
|
||||||
@@ -5784,7 +5785,6 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.runItemIds.add(itemId);
|
this.runItemIds.add(itemId);
|
||||||
}
|
}
|
||||||
this.runPackageIds.add(packageId);
|
this.runPackageIds.add(packageId);
|
||||||
this.runPackageGenerations.set(packageId, this.getPackageResultGeneration(packageId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.allSettled(postProcessTasks);
|
await Promise.allSettled(postProcessTasks);
|
||||||
@@ -5867,7 +5867,6 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
if (this.session.running) {
|
if (this.session.running) {
|
||||||
this.runPackageIds.add(pkgId);
|
this.runPackageIds.add(pkgId);
|
||||||
this.runPackageGenerations.set(pkgId, this.getPackageResultGeneration(pkgId));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5990,7 +5989,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (item.status === "queued" || item.status === "reconnect_wait") {
|
if (item.status === "queued" || item.status === "reconnect_wait") {
|
||||||
this.runItemIds.add(item.id);
|
this.runItemIds.add(item.id);
|
||||||
this.runPackageIds.add(item.packageId);
|
this.runPackageIds.add(item.packageId);
|
||||||
this.runPackageGenerations.set(item.packageId, this.beginPackageResultGeneration(item.packageId));
|
this.beginPackageResultGeneration(item.packageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
@@ -6011,11 +6010,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.runItemIds = new Set(runItems.map((item) => item.id));
|
this.runItemIds = new Set(runItems.map((item) => item.id));
|
||||||
this.runPackageIds = new Set(runItems.map((item) => item.packageId));
|
this.runPackageIds = new Set(runItems.map((item) => item.packageId));
|
||||||
this.addTrailingPostProcessPackageIds(this.runPackageIds);
|
|
||||||
this.runPackageGenerations.clear();
|
|
||||||
this.ensureRunPackageGenerations(this.runPackageIds);
|
|
||||||
this.runOutcomes.clear();
|
this.runOutcomes.clear();
|
||||||
this.runCompletedPackages.clear();
|
this.runCompletedPackages.clear();
|
||||||
this.retryAfterByItem.clear();
|
this.retryAfterByItem.clear();
|
||||||
@@ -6098,7 +6094,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (item.status === "queued" || item.status === "reconnect_wait") {
|
if (item.status === "queued" || item.status === "reconnect_wait") {
|
||||||
this.runItemIds.add(item.id);
|
this.runItemIds.add(item.id);
|
||||||
this.runPackageIds.add(item.packageId);
|
this.runPackageIds.add(item.packageId);
|
||||||
this.runPackageGenerations.set(item.packageId, this.beginPackageResultGeneration(item.packageId));
|
this.beginPackageResultGeneration(item.packageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
@@ -6120,11 +6116,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.runItemIds = new Set(runItems.map((item) => item.id));
|
this.runItemIds = new Set(runItems.map((item) => item.id));
|
||||||
this.runPackageIds = new Set(runItems.map((item) => item.packageId));
|
this.runPackageIds = new Set(runItems.map((item) => item.packageId));
|
||||||
this.addTrailingPostProcessPackageIds(this.runPackageIds);
|
|
||||||
this.runPackageGenerations.clear();
|
|
||||||
this.ensureRunPackageGenerations(this.runPackageIds);
|
|
||||||
this.runOutcomes.clear();
|
this.runOutcomes.clear();
|
||||||
this.runCompletedPackages.clear();
|
this.runCompletedPackages.clear();
|
||||||
this.retryAfterByItem.clear();
|
this.retryAfterByItem.clear();
|
||||||
@@ -6216,11 +6209,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
if (runItems.length === 0) {
|
if (runItems.length === 0) {
|
||||||
if (this.packagePostProcessTasks.size > 0) {
|
if (this.packagePostProcessTasks.size > 0) {
|
||||||
this.runItemIds.clear();
|
this.runItemIds.clear();
|
||||||
this.runPackageIds.clear();
|
this.runPackageIds.clear();
|
||||||
this.addTrailingPostProcessPackageIds(this.runPackageIds);
|
|
||||||
this.runPackageGenerations.clear();
|
|
||||||
this.ensureRunPackageGenerations(this.runPackageIds);
|
|
||||||
this.runOutcomes.clear();
|
this.runOutcomes.clear();
|
||||||
this.runCompletedPackages.clear();
|
this.runCompletedPackages.clear();
|
||||||
this.session.running = true;
|
this.session.running = true;
|
||||||
@@ -6237,11 +6227,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.runItemIds.clear();
|
this.runItemIds.clear();
|
||||||
this.runPackageIds.clear();
|
this.runPackageIds.clear();
|
||||||
this.addTrailingPostProcessPackageIds(this.runPackageIds);
|
|
||||||
this.runPackageGenerations.clear();
|
|
||||||
this.ensureRunPackageGenerations(this.runPackageIds);
|
|
||||||
this.runOutcomes.clear();
|
this.runOutcomes.clear();
|
||||||
this.runCompletedPackages.clear();
|
this.runCompletedPackages.clear();
|
||||||
this.retryAfterByItem.clear();
|
this.retryAfterByItem.clear();
|
||||||
@@ -6270,11 +6257,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.runItemIds = new Set(runItems.map((item) => item.id));
|
this.runItemIds = new Set(runItems.map((item) => item.id));
|
||||||
this.runPackageIds = new Set(runItems.map((item) => item.packageId));
|
this.runPackageIds = new Set(runItems.map((item) => item.packageId));
|
||||||
this.addTrailingPostProcessPackageIds(this.runPackageIds);
|
|
||||||
this.runPackageGenerations.clear();
|
|
||||||
this.ensureRunPackageGenerations(this.runPackageIds);
|
|
||||||
this.runOutcomes.clear();
|
this.runOutcomes.clear();
|
||||||
this.runCompletedPackages.clear();
|
this.runCompletedPackages.clear();
|
||||||
this.retryAfterByItem.clear();
|
this.retryAfterByItem.clear();
|
||||||
@@ -6371,16 +6355,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
pkg.status = "queued";
|
pkg.status = "queued";
|
||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (wasRunning && !parkForRestart && this.settings.notifyOnRunFinished && this.runItemIds.size > 0) {
|
if (wasRunning && !parkForRestart && this.settings.notifyOnRunFinished && this.runItemIds.size > 0) {
|
||||||
const outcomes = Array.from(this.runOutcomes.values());
|
const packageResults = [...this.runPackageIds]
|
||||||
const success = outcomes.filter((s) => s === "completed").length;
|
.flatMap((packageId) => {
|
||||||
const failed = outcomes.filter((s) => s === "failed").length;
|
const result = this.finalizedPackageResults.get(this.packageResultKey(packageId, this.getPackageResultGeneration(packageId)));
|
||||||
const cancelled = outcomes.filter((s) => s === "cancelled").length;
|
|
||||||
this.ensureRunPackageGenerations(this.runPackageIds);
|
|
||||||
const packageResults = [...this.runPackageGenerations]
|
|
||||||
.flatMap(([packageId, generation]) => {
|
|
||||||
const result = this.finalizedPackageResults.get(this.packageResultKey(packageId, generation));
|
|
||||||
return result ? [result] : [];
|
return result ? [result] : [];
|
||||||
});
|
});
|
||||||
this.flushPackageSuccessDigest();
|
this.flushPackageSuccessDigest();
|
||||||
@@ -6390,13 +6369,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
startedAt: this.session.runStartedAt,
|
startedAt: this.session.runStartedAt,
|
||||||
completedAt: nowMs(),
|
completedAt: nowMs(),
|
||||||
packages: packageResults,
|
packages: packageResults,
|
||||||
totalPackages: this.runPackageIds.size,
|
totalPackages: this.runPackageIds.size
|
||||||
successfulFiles: success,
|
|
||||||
failedFiles: failed,
|
|
||||||
cancelledFiles: cancelled
|
|
||||||
})));
|
})));
|
||||||
this.pendingRunResult = null;
|
|
||||||
}
|
}
|
||||||
|
this.runItemIds.clear();
|
||||||
|
this.runPackageIds.clear();
|
||||||
|
this.runOutcomes.clear();
|
||||||
|
this.runCompletedPackages.clear();
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
}
|
}
|
||||||
@@ -8307,10 +8286,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.logPackageForPackage(pkg, "INFO", "Extraktion manuell wiederholt", {
|
this.logPackageForPackage(pkg, "INFO", "Extraktion manuell wiederholt", {
|
||||||
completedItems: completedItems.length,
|
completedItems: completedItems.length,
|
||||||
targetedItems: targetItems.length
|
targetedItems: targetItems.length
|
||||||
});
|
});
|
||||||
const generation = this.beginPackageResultGeneration(packageId, false, true);
|
const generation = this.beginPackageResultGeneration(packageId, false, true);
|
||||||
this.runPackageIds.add(packageId);
|
this.standalonePackageResults.add(this.packageResultKey(packageId, generation));
|
||||||
this.runPackageGenerations.set(packageId, generation);
|
|
||||||
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)}`));
|
||||||
@@ -8338,10 +8316,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.logPackageForPackage(pkg, "INFO", "Jetzt entpacken ausgelöst", {
|
this.logPackageForPackage(pkg, "INFO", "Jetzt entpacken ausgelöst", {
|
||||||
completedItems: completedItems.length,
|
completedItems: completedItems.length,
|
||||||
targetedItems: targetItems.length
|
targetedItems: targetItems.length
|
||||||
});
|
});
|
||||||
const generation = this.beginPackageResultGeneration(packageId, false, true);
|
const generation = this.beginPackageResultGeneration(packageId, false, true);
|
||||||
this.runPackageIds.add(packageId);
|
this.standalonePackageResults.add(this.packageResultKey(packageId, generation));
|
||||||
this.runPackageGenerations.set(packageId, generation);
|
|
||||||
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)}`));
|
||||||
@@ -8353,9 +8330,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private notePackageDownloadCompleted(pkg: PackageEntry, completedAt = nowMs()): void {
|
private notePackageDownloadCompleted(pkg: PackageEntry, completedAt = nowMs()): void {
|
||||||
this.notePackageDownloadStarted(pkg, completedAt);
|
this.notePackageDownloadStarted(pkg, completedAt);
|
||||||
pkg.downloadCompletedAt = Math.max(pkg.downloadCompletedAt || 0, completedAt);
|
pkg.downloadCompletedAt = Math.max(pkg.downloadCompletedAt || 0, completedAt);
|
||||||
|
if (pkg.itemIds.every((itemId) => {
|
||||||
|
const item = this.session.items[itemId];
|
||||||
|
return !item || isFinishedStatus(item.status);
|
||||||
|
})) {
|
||||||
|
pkg.downloadEndedAt = Math.max(pkg.downloadEndedAt || 0, completedAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPackageHistoryDurationSeconds(pkg: PackageEntry): number {
|
private getPackageHistoryDurationSeconds(pkg: PackageEntry): number {
|
||||||
@@ -11609,8 +11592,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const generation = this.beginPackageResultGeneration(packageId, false, true);
|
const generation = this.beginPackageResultGeneration(packageId, false, true);
|
||||||
if (this.runPackageIds.has(packageId)) {
|
if (!this.runPackageIds.has(packageId)) {
|
||||||
this.runPackageGenerations.set(packageId, generation);
|
this.standalonePackageResults.add(this.packageResultKey(packageId, generation));
|
||||||
}
|
}
|
||||||
this.refreshPackageStatus(pkg);
|
this.refreshPackageStatus(pkg);
|
||||||
}
|
}
|
||||||
@@ -11681,21 +11664,26 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getPackageResultGeneration(packageId: string): number {
|
private getPackageResultGeneration(packageId: string): number {
|
||||||
const existing = this.packageResultGenerations.get(packageId);
|
const pkg = this.session.packages[packageId];
|
||||||
if (existing && existing > 0) {
|
if (!pkg) {
|
||||||
return existing;
|
return 1;
|
||||||
}
|
}
|
||||||
this.packageResultGenerations.set(packageId, 1);
|
const generation = Number.isFinite(pkg.resultGeneration)
|
||||||
return 1;
|
? Math.max(1, Math.floor(pkg.resultGeneration || 1))
|
||||||
|
: 1;
|
||||||
|
pkg.resultGeneration = generation;
|
||||||
|
return generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private beginPackageResultGeneration(packageId: string, resetDownloadTelemetry = false, forceReset = false): number {
|
private beginPackageResultGeneration(packageId: string, resetDownloadTelemetry = false, forceReset = false): number {
|
||||||
const current = this.getPackageResultGeneration(packageId);
|
const current = this.getPackageResultGeneration(packageId);
|
||||||
const currentKey = this.packageResultKey(packageId, current);
|
const currentKey = this.packageResultKey(packageId, current);
|
||||||
const wasFinalized = this.finalizedPackageResults.has(currentKey);
|
|
||||||
const next = wasFinalized ? current + 1 : current;
|
|
||||||
this.packageResultGenerations.set(packageId, next);
|
|
||||||
const pkg = this.session.packages[packageId];
|
const pkg = this.session.packages[packageId];
|
||||||
|
const wasFinalized = this.finalizedPackageResults.has(currentKey) || (pkg?.terminalAt || 0) > 0;
|
||||||
|
const next = wasFinalized ? current + 1 : current;
|
||||||
|
if (pkg) {
|
||||||
|
pkg.resultGeneration = next;
|
||||||
|
}
|
||||||
if (pkg && (wasFinalized || resetDownloadTelemetry || forceReset)) {
|
if (pkg && (wasFinalized || resetDownloadTelemetry || forceReset)) {
|
||||||
if (resetDownloadTelemetry) {
|
if (resetDownloadTelemetry) {
|
||||||
pkg.downloadStartedAt = 0;
|
pkg.downloadStartedAt = 0;
|
||||||
@@ -11714,14 +11702,27 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureRunPackageGenerations(packageIds: Iterable<string>): void {
|
private captureRunContext(packageIds: Iterable<string>, startedAt: number): RunLifecycleContext {
|
||||||
|
const packageGenerations = new Map<string, number>();
|
||||||
for (const packageId of packageIds) {
|
for (const packageId of packageIds) {
|
||||||
if (this.runPackageGenerations.has(packageId)) {
|
packageGenerations.set(packageId, this.getPackageResultGeneration(packageId));
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const generation = this.getPackageResultGeneration(packageId);
|
|
||||||
this.runPackageGenerations.set(packageId, generation);
|
|
||||||
}
|
}
|
||||||
|
const context: RunLifecycleContext = { id: uuidv4(), startedAt, packageGenerations };
|
||||||
|
this.runContexts.set(context.id, context);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPackageResultTracked(packageId: string, generation: number): boolean {
|
||||||
|
const key = this.packageResultKey(packageId, generation);
|
||||||
|
if (this.standalonePackageResults.has(key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (const context of this.runContexts.values()) {
|
||||||
|
if (context.packageGenerations.get(packageId) === generation) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private queueNotificationEvent(notification: NotificationEvent): void {
|
private queueNotificationEvent(notification: NotificationEvent): void {
|
||||||
@@ -11784,14 +11785,17 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private tryFinalizePackageResult(packageId: string): PackageResult | null {
|
private tryFinalizePackageResult(packageId: string): PackageResult | null {
|
||||||
const pkg = this.session.packages[packageId];
|
const pkg = this.session.packages[packageId];
|
||||||
if (!pkg || (!this.runPackageIds.has(packageId) && !this.runPackageGenerations.has(packageId))) {
|
if (!pkg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const generation = this.getPackageResultGeneration(packageId);
|
||||||
|
if (!this.runPackageIds.has(packageId) && !this.isPackageResultTracked(packageId, generation)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const items = pkg.itemIds.map((itemId) => this.session.items[itemId]).filter(Boolean) as DownloadItem[];
|
const items = pkg.itemIds.map((itemId) => this.session.items[itemId]).filter(Boolean) as DownloadItem[];
|
||||||
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 generation = this.runPackageGenerations.get(packageId) || this.getPackageResultGeneration(packageId);
|
|
||||||
const key = this.packageResultKey(packageId, generation);
|
const key = this.packageResultKey(packageId, generation);
|
||||||
const existing = this.finalizedPackageResults.get(key);
|
const existing = this.finalizedPackageResults.get(key);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -11839,45 +11843,45 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
this.tryFinalizeRunResult();
|
this.standalonePackageResults.delete(key);
|
||||||
|
this.tryFinalizeRunResults();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryFinalizeRunResult(): void {
|
private tryFinalizeRunResults(): void {
|
||||||
const pending = this.pendingRunResult;
|
for (const context of [...this.runContexts.values()]) {
|
||||||
if (!pending) {
|
const packageResults: PackageResult[] = [];
|
||||||
return;
|
let complete = true;
|
||||||
}
|
for (const [packageId, generation] of context.packageGenerations) {
|
||||||
const packageResults: PackageResult[] = [];
|
const result = this.finalizedPackageResults.get(this.packageResultKey(packageId, generation));
|
||||||
for (const [packageId, generation] of pending.packageGenerations) {
|
if (!result) {
|
||||||
const result = this.finalizedPackageResults.get(this.packageResultKey(packageId, generation));
|
complete = false;
|
||||||
if (!result) {
|
break;
|
||||||
return;
|
}
|
||||||
|
packageResults.push(result);
|
||||||
}
|
}
|
||||||
packageResults.push(result);
|
if (!complete) {
|
||||||
}
|
continue;
|
||||||
const result = buildRunResult({
|
}
|
||||||
id: pending.id,
|
const result = buildRunResult({
|
||||||
stopped: false,
|
id: context.id,
|
||||||
startedAt: pending.startedAt,
|
stopped: false,
|
||||||
completedAt: nowMs(),
|
startedAt: context.startedAt,
|
||||||
packages: packageResults,
|
completedAt: nowMs(),
|
||||||
totalPackages: pending.packageGenerations.size
|
packages: packageResults,
|
||||||
});
|
totalPackages: context.packageGenerations.size
|
||||||
this.flushPackageSuccessDigest(result.completedAt);
|
});
|
||||||
if (this.settings.notifyOnRunFinished) {
|
this.flushPackageSuccessDigest(result.completedAt);
|
||||||
this.queueNotificationEvent(buildRunNotificationEvent(result));
|
if (this.settings.notifyOnRunFinished) {
|
||||||
}
|
this.queueNotificationEvent(buildRunNotificationEvent(result));
|
||||||
this.pendingRunResult = null;
|
}
|
||||||
for (const [packageId, generation] of pending.packageGenerations) {
|
this.runContexts.delete(context.id);
|
||||||
if (!this.session.packages[packageId]) {
|
for (const [packageId, generation] of context.packageGenerations) {
|
||||||
this.finalizedPackageResults.delete(this.packageResultKey(packageId, generation));
|
if (!this.session.packages[packageId] && !this.isPackageResultTracked(packageId, generation)) {
|
||||||
this.packageResultGenerations.delete(packageId);
|
this.finalizedPackageResults.delete(this.packageResultKey(packageId, generation));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.runPackageIds.clear();
|
|
||||||
this.runPackageGenerations.clear();
|
|
||||||
this.runCompletedPackages.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshPackageStatus(pkg: PackageEntry): void {
|
private refreshPackageStatus(pkg: PackageEntry): void {
|
||||||
@@ -11917,7 +11921,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pkg.downloadEndedAt = Math.max(pkg.downloadEndedAt || 0, nowMs());
|
if (!(pkg.downloadEndedAt || 0)) {
|
||||||
|
pkg.downloadEndedAt = nowMs();
|
||||||
|
}
|
||||||
if (failed > 0 || extractFailed > 0) {
|
if (failed > 0 || extractFailed > 0) {
|
||||||
pkg.status = "failed";
|
pkg.status = "failed";
|
||||||
} else if (cancelled > 0) {
|
} else if (cancelled > 0) {
|
||||||
@@ -12570,9 +12576,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.emitState();
|
this.emitState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
pkg.outputCount = Math.max(pkg.outputCount || 0, await this.countPackageOutputFiles(pkg.extractDir));
|
||||||
logger.info(`Hybrid-Extract Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}`);
|
|
||||||
|
logger.info(`Hybrid-Extract Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}`);
|
||||||
this.logPackageForPackage(pkg, "INFO", "Hybrid-Extract abgeschlossen", {
|
this.logPackageForPackage(pkg, "INFO", "Hybrid-Extract abgeschlossen", {
|
||||||
extracted: result.extracted,
|
extracted: result.extracted,
|
||||||
failed: result.failed
|
failed: result.failed
|
||||||
@@ -13125,8 +13132,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
emitExtractStatus(overallLabel);
|
emitExtractStatus(overallLabel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
logger.info(`Post-Processing Entpacken Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}, lastError=${result.lastError || ""}`);
|
pkg.outputCount = Math.max(pkg.outputCount || 0, await this.countPackageOutputFiles(pkg.extractDir));
|
||||||
|
logger.info(`Post-Processing Entpacken Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}, lastError=${result.lastError || ""}`);
|
||||||
this.logPackageForPackage(pkg, "INFO", "Post-Processing Entpacken Ende", {
|
this.logPackageForPackage(pkg, "INFO", "Post-Processing Entpacken Ende", {
|
||||||
extracted: result.extracted,
|
extracted: result.extracted,
|
||||||
failed: result.failed,
|
failed: result.failed,
|
||||||
@@ -13355,8 +13363,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
throwIfAborted();
|
throwIfAborted();
|
||||||
extractedCount += nestedResult.extracted;
|
pkg.outputCount = Math.max(pkg.outputCount || 0, await this.countPackageOutputFiles(pkg.extractDir));
|
||||||
|
extractedCount += nestedResult.extracted;
|
||||||
logger.info(`Deferred Nested-Extraction Ende: extracted=${nestedResult.extracted}, failed=${nestedResult.failed}`);
|
logger.info(`Deferred Nested-Extraction Ende: extracted=${nestedResult.extracted}, failed=${nestedResult.failed}`);
|
||||||
this.logPackageForPackage(pkg, "INFO", "Deferred Nested-Extraction Ende", {
|
this.logPackageForPackage(pkg, "INFO", "Deferred Nested-Extraction Ende", {
|
||||||
extracted: nestedResult.extracted,
|
extracted: nestedResult.extracted,
|
||||||
@@ -13629,15 +13638,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
extracted,
|
extracted,
|
||||||
durationSeconds: duration,
|
durationSeconds: duration,
|
||||||
averageSpeedBps: avgSpeed
|
averageSpeedBps: avgSpeed
|
||||||
};
|
};
|
||||||
this.session.summaryText = `Summary: Dauer ${duration}s, Ø Speed ${humanSize(avgSpeed)}/s, Erfolg ${success}/${total}`;
|
this.session.summaryText = `Summary: Dauer ${duration}s, Ø Speed ${humanSize(avgSpeed)}/s, Erfolg ${success}/${total}`;
|
||||||
this.ensureRunPackageGenerations(this.runPackageIds);
|
const runContext = total > 0 ? this.captureRunContext(this.runPackageIds, runStartedAt) : null;
|
||||||
this.pendingRunResult = total > 0 ? {
|
|
||||||
id: uuidv4(),
|
|
||||||
startedAt: runStartedAt,
|
|
||||||
packageGenerations: new Map(this.runPackageGenerations)
|
|
||||||
} : null;
|
|
||||||
this.runItemIds.clear();
|
this.runItemIds.clear();
|
||||||
|
this.runPackageIds.clear();
|
||||||
this.runOutcomes.clear();
|
this.runOutcomes.clear();
|
||||||
this.retryAfterByItem.clear();
|
this.retryAfterByItem.clear();
|
||||||
this.providerStartReservations.clear();
|
this.providerStartReservations.clear();
|
||||||
@@ -13658,10 +13663,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.lastSettingsPersistAt = 0;
|
this.lastSettingsPersistAt = 0;
|
||||||
this.persistNow();
|
this.persistNow();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
for (const packageId of this.runPackageIds) {
|
for (const packageId of runContext?.packageGenerations.keys() || []) {
|
||||||
this.tryFinalizePackageResult(packageId);
|
this.tryFinalizePackageResult(packageId);
|
||||||
}
|
}
|
||||||
this.tryFinalizeRunResult();
|
this.tryFinalizeRunResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSessionStats(): import("../shared/types").SessionStats {
|
public getSessionStats(): import("../shared/types").SessionStats {
|
||||||
|
|||||||
@@ -998,6 +998,7 @@ export function normalizeLoadedSession(raw: unknown): SessionState {
|
|||||||
remuxOperations: normalizeRemuxOperations(pkg.remuxOperations),
|
remuxOperations: normalizeRemuxOperations(pkg.remuxOperations),
|
||||||
outputCount: clampNumber(pkg.outputCount, 0, 0, 1_000_000),
|
outputCount: clampNumber(pkg.outputCount, 0, 0, 1_000_000),
|
||||||
cleanupErrorCategory: asText(pkg.cleanupErrorCategory),
|
cleanupErrorCategory: asText(pkg.cleanupErrorCategory),
|
||||||
|
resultGeneration: clampNumber(pkg.resultGeneration, 1, 1, Number.MAX_SAFE_INTEGER),
|
||||||
createdAt: clampNumber(pkg.createdAt, now, 0, Number.MAX_SAFE_INTEGER),
|
createdAt: clampNumber(pkg.createdAt, now, 0, Number.MAX_SAFE_INTEGER),
|
||||||
updatedAt: clampNumber(pkg.updatedAt, now, 0, Number.MAX_SAFE_INTEGER)
|
updatedAt: clampNumber(pkg.updatedAt, now, 0, Number.MAX_SAFE_INTEGER)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -572,6 +572,7 @@ export interface PackageEntry {
|
|||||||
remuxOperations?: RemuxOperationMetric[];
|
remuxOperations?: RemuxOperationMetric[];
|
||||||
outputCount?: number;
|
outputCount?: number;
|
||||||
cleanupErrorCategory?: string;
|
cleanupErrorCategory?: string;
|
||||||
|
resultGeneration?: number;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4455,9 +4455,10 @@ describe("download manager", () => {
|
|||||||
return originalFetch(input, init);
|
return originalFetch(input, init);
|
||||||
};
|
};
|
||||||
|
|
||||||
let releaseBlockedPostProcess: ((value?: void | PromiseLike<void>) => void) | undefined;
|
let releaseBlockedPostProcess: ((value?: void | PromiseLike<void>) => void) | undefined;
|
||||||
try {
|
try {
|
||||||
const manager = new DownloadManager(
|
const history: HistoryEntry[] = [];
|
||||||
|
const manager = new DownloadManager(
|
||||||
{
|
{
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "rd-token",
|
token: "rd-token",
|
||||||
@@ -4466,14 +4467,15 @@ describe("download manager", () => {
|
|||||||
autoExtract: false,
|
autoExtract: false,
|
||||||
maxParallel: 1
|
maxParallel: 1
|
||||||
},
|
},
|
||||||
emptySession(),
|
emptySession(),
|
||||||
createStoragePaths(path.join(root, "state"))
|
createStoragePaths(path.join(root, "state")),
|
||||||
|
{ onHistoryEntry: (entry) => history.push(entry) }
|
||||||
);
|
);
|
||||||
|
|
||||||
const blocker = new Promise<void>((resolve) => {
|
const blocker = new Promise<void>((resolve) => {
|
||||||
releaseBlockedPostProcess = resolve;
|
releaseBlockedPostProcess = resolve;
|
||||||
});
|
});
|
||||||
(manager as unknown as { packagePostProcessQueue: Promise<void> }).packagePostProcessQueue = blocker;
|
(manager as any).handlePackagePostProcessing = vi.fn(async () => blocker);
|
||||||
|
|
||||||
manager.addPackages([
|
manager.addPackages([
|
||||||
{ name: "first", links: ["https://dummy/first"] },
|
{ name: "first", links: ["https://dummy/first"] },
|
||||||
@@ -4489,19 +4491,31 @@ describe("download manager", () => {
|
|||||||
manager.start();
|
manager.start();
|
||||||
|
|
||||||
await waitFor(() => manager.getSnapshot().session.items[firstItem]?.status === "completed", 12000);
|
await waitFor(() => manager.getSnapshot().session.items[firstItem]?.status === "completed", 12000);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const state = manager.getSnapshot().session.items[secondItem]?.status;
|
const state = manager.getSnapshot().session.items[secondItem]?.status;
|
||||||
return state === "validating" || state === "downloading" || state === "integrity_check" || state === "completed";
|
return state === "validating" || state === "downloading" || state === "integrity_check" || state === "completed";
|
||||||
}, 6000);
|
}, 6000);
|
||||||
|
|
||||||
|
const pendingPackage = manager.getSnapshot().session.packages[firstPackage];
|
||||||
|
expect(pendingPackage?.downloadStartedAt).toBeGreaterThan(0);
|
||||||
|
expect(pendingPackage?.downloadEndedAt).toBeGreaterThanOrEqual(pendingPackage?.downloadStartedAt || 0);
|
||||||
|
expect(pendingPackage?.downloadEndedAt).toBe(pendingPackage?.downloadCompletedAt);
|
||||||
|
const downloadEndedAt = pendingPackage?.downloadEndedAt || 0;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1_200));
|
||||||
|
expect(manager.getSnapshot().session.packages[firstPackage]?.downloadEndedAt).toBe(downloadEndedAt);
|
||||||
|
|
||||||
if (releaseBlockedPostProcess) {
|
if (releaseBlockedPostProcess) {
|
||||||
releaseBlockedPostProcess();
|
releaseBlockedPostProcess();
|
||||||
}
|
}
|
||||||
await waitFor(() => !manager.getSnapshot().session.running, 25000);
|
await waitFor(() => !manager.getSnapshot().session.running, 25000);
|
||||||
|
await waitFor(() => history.some((entry) => entry.name === "first"), 6000);
|
||||||
const done = manager.getSnapshot();
|
|
||||||
expect(done.session.items[firstItem]?.status).toBe("completed");
|
const done = manager.getSnapshot();
|
||||||
expect(done.session.items[secondItem]?.status).toBe("completed");
|
expect(done.session.items[firstItem]?.status).toBe("completed");
|
||||||
|
expect(done.session.items[secondItem]?.status).toBe("completed");
|
||||||
|
const firstHistory = history.find((entry) => entry.name === "first");
|
||||||
|
expect(firstHistory?.downloadEndedAt).toBe(downloadEndedAt);
|
||||||
|
expect(firstHistory?.totalDurationSeconds).toBeGreaterThan(firstHistory?.downloadDurationSeconds || 0);
|
||||||
} finally {
|
} finally {
|
||||||
if (releaseBlockedPostProcess) {
|
if (releaseBlockedPostProcess) {
|
||||||
releaseBlockedPostProcess();
|
releaseBlockedPostProcess();
|
||||||
@@ -11055,6 +11069,7 @@ describe("download manager", () => {
|
|||||||
errorCategory: ""
|
errorCategory: ""
|
||||||
});
|
});
|
||||||
expect(snapshot.session.packages[packageId]?.archiveOperations?.[0].durationMs).toBeGreaterThanOrEqual(0);
|
expect(snapshot.session.packages[packageId]?.archiveOperations?.[0].durationMs).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(snapshot.session.packages[packageId]?.outputCount).toBe(1);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
it("does not fail startup post-processing when source package dir is missing but extract output exists", async () => {
|
it("does not fail startup post-processing when source package dir is missing but extract output exists", async () => {
|
||||||
|
|||||||
+100
-2
@@ -243,6 +243,51 @@ describe("authoritative package completion", () => {
|
|||||||
expect(history).toHaveLength(2);
|
expect(history).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("continues the persisted result generation after an extraction retry following restart", async () => {
|
||||||
|
const { manager, session, events, history } = setup();
|
||||||
|
const pkg = addPackage(session);
|
||||||
|
const state = internal(manager);
|
||||||
|
pkg.resultGeneration = 7;
|
||||||
|
pkg.terminalAt = Date.now() - 1_000;
|
||||||
|
pkg.archiveOperations = [{
|
||||||
|
id: "archive-restart-failed",
|
||||||
|
name: "episode.rar",
|
||||||
|
itemIds: [...pkg.itemIds],
|
||||||
|
partCount: 1,
|
||||||
|
startedAt: 10_000,
|
||||||
|
completedAt: 12_000,
|
||||||
|
durationMs: 2_000,
|
||||||
|
status: "failed",
|
||||||
|
errorCategory: "crc_error"
|
||||||
|
}];
|
||||||
|
session.items[pkg.itemIds[0]].fullStatus = "Entpacken - Error";
|
||||||
|
vi.spyOn(state, "runPackagePostProcessing").mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
manager.retryExtraction(pkg.id);
|
||||||
|
expect(pkg.resultGeneration).toBe(8);
|
||||||
|
|
||||||
|
pkg.archiveOperations = [{
|
||||||
|
id: "archive-restart-completed",
|
||||||
|
name: "episode.rar",
|
||||||
|
itemIds: [...pkg.itemIds],
|
||||||
|
partCount: 1,
|
||||||
|
startedAt: 20_000,
|
||||||
|
completedAt: 23_000,
|
||||||
|
durationMs: 3_000,
|
||||||
|
status: "completed",
|
||||||
|
errorCategory: ""
|
||||||
|
}];
|
||||||
|
session.items[pkg.itemIds[0]].fullStatus = "Entpackt - Done (3.0s)";
|
||||||
|
pkg.status = "completed";
|
||||||
|
state.tryFinalizePackageResult(pkg.id);
|
||||||
|
await flushNotifications();
|
||||||
|
|
||||||
|
expect(events).toHaveLength(1);
|
||||||
|
expect(events[0].id).toContain(":8:");
|
||||||
|
expect(history).toHaveLength(1);
|
||||||
|
expect(history[0].id).toBe(`hist-${pkg.id}-8`);
|
||||||
|
});
|
||||||
|
|
||||||
it("moves a pending success digest into the outbox before shutdown", async () => {
|
it("moves a pending success digest into the outbox before shutdown", async () => {
|
||||||
const { manager, session, events } = setup({ notifyPackageSuccessMode: "digest" });
|
const { manager, session, events } = setup({ notifyPackageSuccessMode: "digest" });
|
||||||
const pkg = addPackage(session);
|
const pkg = addPackage(session);
|
||||||
@@ -279,6 +324,29 @@ describe("authoritative run completion", () => {
|
|||||||
expect(events.some((event) => event.type === "run_completed")).toBe(false);
|
expect(events.some((event) => event.type === "run_completed")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds run_stopped file counters only from finalized package results", async () => {
|
||||||
|
const { manager, session, events } = setup();
|
||||||
|
const pkg = addPackage(session);
|
||||||
|
const state = internal(manager);
|
||||||
|
state.runPackageIds = new Set([pkg.id]);
|
||||||
|
state.tryFinalizePackageResult(pkg.id);
|
||||||
|
await flushNotifications();
|
||||||
|
events.length = 0;
|
||||||
|
|
||||||
|
session.running = true;
|
||||||
|
session.runStartedAt = Date.now() - 10_000;
|
||||||
|
state.runItemIds = new Set(pkg.itemIds);
|
||||||
|
state.runPackageIds = new Set([pkg.id]);
|
||||||
|
state.runOutcomes = new Map([[pkg.itemIds[0], "failed"]]);
|
||||||
|
|
||||||
|
manager.stop();
|
||||||
|
await flushNotifications();
|
||||||
|
|
||||||
|
const stopped = events.find((event) => event.type === "run_stopped");
|
||||||
|
expect(stopped).toBeDefined();
|
||||||
|
expect(stopped?.payload.fields.some((field) => field.name === "Dateien" && field.value === "1 erfolgreich · 0 fehlgeschlagen · 0 abgebrochen")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("waits for failed extraction package results before emitting the final run summary", async () => {
|
it("waits for failed extraction package results before emitting the final run summary", async () => {
|
||||||
const { manager, session, events } = setup({ notifyPackageSuccessMode: "digest" });
|
const { manager, session, events } = setup({ notifyPackageSuccessMode: "digest" });
|
||||||
const pkg = addPackage(session);
|
const pkg = addPackage(session);
|
||||||
@@ -344,8 +412,6 @@ describe("authoritative run completion", () => {
|
|||||||
session.runStartedAt = Date.now() - 20_000;
|
session.runStartedAt = Date.now() - 20_000;
|
||||||
state.runItemIds = new Set(pkg.itemIds);
|
state.runItemIds = new Set(pkg.itemIds);
|
||||||
state.runPackageIds = new Set([pkg.id]);
|
state.runPackageIds = new Set([pkg.id]);
|
||||||
state.packageResultGenerations = new Map([[pkg.id, 1]]);
|
|
||||||
state.runPackageGenerations = new Map([[pkg.id, 1]]);
|
|
||||||
state.runOutcomes = new Map([[pkg.itemIds[0], "completed"]]);
|
state.runOutcomes = new Map([[pkg.itemIds[0], "completed"]]);
|
||||||
|
|
||||||
state.tryFinalizePackageResult(pkg.id);
|
state.tryFinalizePackageResult(pkg.id);
|
||||||
@@ -357,4 +423,36 @@ describe("authoritative run completion", () => {
|
|||||||
expect(events.map((event) => event.type)).toEqual(["package_completed", "run_completed"]);
|
expect(events.map((event) => event.type)).toEqual(["package_completed", "run_completed"]);
|
||||||
expect(events[0].payload.title).toContain("Paket-Digest");
|
expect(events[0].payload.title).toContain("Paket-Digest");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("finalizes overlapping runs independently when the earlier run finishes deferred work last", async () => {
|
||||||
|
const { manager, session, events, history } = setup();
|
||||||
|
const packageA = addPackage(session, ["completed"], "package-a");
|
||||||
|
const packageB = addPackage(session, ["completed"], "package-b");
|
||||||
|
const state = internal(manager);
|
||||||
|
|
||||||
|
session.running = true;
|
||||||
|
session.runStartedAt = Date.now() - 20_000;
|
||||||
|
state.runItemIds = new Set(packageA.itemIds);
|
||||||
|
state.runPackageIds = new Set([packageA.id]);
|
||||||
|
state.runOutcomes = new Map([[packageA.itemIds[0], "completed"]]);
|
||||||
|
state.packageDeferredPostProcessTasks.set(packageA.id, new Set([Promise.resolve()]));
|
||||||
|
state.finishRun();
|
||||||
|
|
||||||
|
session.running = true;
|
||||||
|
session.runStartedAt = Date.now() - 5_000;
|
||||||
|
state.runItemIds = new Set(packageB.itemIds);
|
||||||
|
state.runPackageIds = new Set([packageB.id]);
|
||||||
|
state.runOutcomes = new Map([[packageB.itemIds[0], "completed"]]);
|
||||||
|
state.finishRun();
|
||||||
|
|
||||||
|
state.packageDeferredPostProcessTasks.delete(packageA.id);
|
||||||
|
state.tryFinalizePackageResult(packageA.id);
|
||||||
|
await flushNotifications();
|
||||||
|
|
||||||
|
expect(events.filter((event) => event.type === "package_completed")).toHaveLength(2);
|
||||||
|
expect(events.filter((event) => event.type === "run_completed")).toHaveLength(2);
|
||||||
|
expect(new Set(events.filter((event) => event.type === "package_completed").map((event) => event.id)).size).toBe(2);
|
||||||
|
expect(new Set(events.filter((event) => event.type === "run_completed").map((event) => event.id)).size).toBe(2);
|
||||||
|
expect(history.map((entry) => entry.name).sort()).toEqual([packageA.name, packageB.name].sort());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1137,6 +1137,7 @@ describe("settings storage", () => {
|
|||||||
itemIds: [],
|
itemIds: [],
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
resultGeneration: 7,
|
||||||
downloadStartedAt: 1_000,
|
downloadStartedAt: 1_000,
|
||||||
downloadCompletedAt: 10_000,
|
downloadCompletedAt: 10_000,
|
||||||
downloadEndedAt: 12_000,
|
downloadEndedAt: 12_000,
|
||||||
@@ -1174,6 +1175,7 @@ describe("settings storage", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(normalized.packages["pkg-1"]).toEqual(expect.objectContaining({
|
expect(normalized.packages["pkg-1"]).toEqual(expect.objectContaining({
|
||||||
|
resultGeneration: 7,
|
||||||
downloadEndedAt: 12_000,
|
downloadEndedAt: 12_000,
|
||||||
postProcessQueuedAt: 13_000,
|
postProcessQueuedAt: 13_000,
|
||||||
postProcessStartedAt: 14_000,
|
postProcessStartedAt: 14_000,
|
||||||
|
|||||||
Reference in New Issue
Block a user