Persist collector state and stabilize download controls

Persist collector packages and disclosure state through validated atomic AppData storage with backup recovery and shutdown synchronization. Align download sidebar metrics with the hidden-extracted presentation scope and keep Start available through temporary Real-Debrid cooldowns while preserving hard account blocks.
This commit is contained in:
Sucukdeluxe
2026-08-26 17:24:07 +02:00
parent 5644ba25da
commit bdcf9e3754
20 changed files with 933 additions and 48 deletions
+82
View File
@@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import { validateCollectorPersistenceState } from "../src/shared/collector";
import {
createCollectorPersistenceCoordinator,
restoreCollectorPersistenceState
} from "../src/renderer/views/collector/collector-persistence";
const packageEntry = {
id: "package-one",
name: "Staffel Eins",
nameSource: "explicit" as const,
links: [{
id: "link-one",
url: "https://1fichier.com/?example",
fileName: "episode.part01.rar",
fileSizeBytes: 471_859_200,
hoster: "1fichier",
availability: "online" as const,
status: "ready" as const,
addedAt: 1_000
}],
addedAt: 1_000
};
describe("collector persistence payload", () => {
it("accepts a complete state and prunes unknown collapsed package ids", () => {
const input = { packages: [packageEntry], collapsedPackageIds: ["package-one", "missing", "package-one"] };
expect(validateCollectorPersistenceState(input)).toEqual({
packages: [packageEntry],
collapsedPackageIds: ["package-one"]
});
expect(validateCollectorPersistenceState(input)).not.toBe(input);
});
it("accepts an empty collector without loosening the payload shape", () => {
expect(validateCollectorPersistenceState({ packages: [], collapsedPackageIds: [] })).toEqual({ packages: [], collapsedPackageIds: [] });
expect(() => validateCollectorPersistenceState({ packages: [], collapsedPackageIds: [], selectedLinkIds: [] })).toThrow("Linksammler-Speicherzustand ist ungültig");
});
it("rejects invalid packages and excessive queue sizes", () => {
expect(() => validateCollectorPersistenceState({ packages: [{ ...packageEntry, name: "" }], collapsedPackageIds: [] })).toThrow("Linksammler-Speicherzustand ist ungültig");
expect(() => validateCollectorPersistenceState({ packages: Array.from({ length: 2_001 }, () => packageEntry), collapsedPackageIds: [] })).toThrow("Linksammler-Speicherzustand ist ungültig");
});
});
describe("collector persistence renderer flow", () => {
it("merges a late restore with current imports and keeps collapse state from both sides", () => {
const persisted = { packages: [packageEntry], collapsedPackageIds: ["package-one"] };
const currentPackage = {
...packageEntry,
id: "package-current",
name: "Aktueller Import",
links: [{ ...packageEntry.links[0], id: "link-current", url: "https://example.com/current" }]
};
expect(restoreCollectorPersistenceState(persisted, [currentPackage], new Set(["package-current"]))).toEqual({
packages: [packageEntry, currentPackage],
collapsedPackageIds: ["package-one", "package-current"]
});
});
it("serializes saves and lets the newest queued state win", async () => {
const saved: string[] = [];
let releaseFirst: () => void = () => {};
const coordinator = createCollectorPersistenceCoordinator(async (state) => {
saved.push(state.packages[0]?.id || "empty");
if (saved.length === 1) await new Promise<void>((resolve) => { releaseFirst = resolve; });
return state;
}, 0);
coordinator.schedule({ packages: [{ ...packageEntry, id: "first" }], collapsedPackageIds: [] });
const firstFlush = coordinator.flush();
await Promise.resolve();
coordinator.schedule({ packages: [{ ...packageEntry, id: "latest" }], collapsedPackageIds: [] });
releaseFirst();
await firstFlush;
await coordinator.flush();
expect(saved).toEqual(["first", "latest"]);
});
});