fix(history): remove selections atomically

This commit is contained in:
Sucukdeluxe
2026-08-21 00:59:45 +02:00
parent 3a95136ef1
commit 869dbc25be
11 changed files with 107 additions and 20 deletions
+9 -5
View File
@@ -53,7 +53,7 @@ import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log";
import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log";
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
import { MegaWebFallback } from "./mega-web-fallback";
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadHistoryForRetention, loadSessionWithStatus, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage";
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadHistoryForRetention, loadSessionWithStatus, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntries, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage";
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
import { runInstallWithResume } from "./update-install-flow";
import { rotateDebugToken, startDebugServer, stopDebugServer, restartDebugServer, getDebugServerRuntimeStatus, getActiveDebugToken, getDebugAllowlist, writeDebugServerConfig, clearDebugToken } from "./debug-server";
@@ -1381,10 +1381,14 @@ export class AppController {
this.manager.resetItems(itemIds);
}
public removeHistoryEntry(entryId: string): void {
this.audit("INFO", "Verlaufseintrag entfernt", { entryId });
removeHistoryEntry(this.storagePaths, entryId);
}
public removeHistoryEntry(entryId: string): void {
this.removeHistoryEntries([entryId]);
}
public removeHistoryEntries(entryIds: string[]): void {
this.audit("INFO", "Verlaufseinträge entfernt", { count: entryIds.length });
removeHistoryEntries(this.storagePaths, entryIds, this.historyLimits());
}
public addToHistory(entry: HistoryEntry): void {
this.audit("INFO", "Verlaufseintrag hinzugefügt", {
+11
View File
@@ -97,6 +97,14 @@ function isDevMode(): boolean {
return process.env.NODE_ENV === "development";
}
function validateHistoryEntryIds(value: unknown): string[] {
const entryIds = validateStringArray(value, "entryIds");
if (entryIds.length > 100_000 || entryIds.some((entryId) => entryId.length === 0 || entryId.length > 4_096)) {
throw new Error("entryIds ist ungültig");
}
return [...new Set(entryIds)];
}
function getRendererFileUrl(): string {
return pathToFileURL(path.join(app.getAppPath(), "build", "renderer", "index.html")).toString();
}
@@ -588,6 +596,9 @@ function registerIpcHandlers(): void {
validateString(entryId, "entryId");
return controller.removeHistoryEntry(entryId);
});
handleTrusted(IPC_CHANNELS.REMOVE_HISTORY_ENTRIES, (_event: IpcMainInvokeEvent, entryIds: unknown) => {
return controller.removeHistoryEntries(validateHistoryEntryIds(entryIds));
});
handleTrusted(IPC_CHANNELS.REVEAL_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: unknown) => {
return revealHistoryEntry({ entryId }, {
loadHistory: () => controller.getHistory(),
+16 -6
View File
@@ -1546,12 +1546,22 @@ export function resetHistoryForRetention(paths: StoragePaths, retentionMode: His
clearHistory(paths);
}
export function removeHistoryEntry(paths: StoragePaths, entryId: string): HistoryEntry[] {
const existing = loadHistory(paths);
const updated = existing.filter(e => e.id !== entryId);
saveHistory(paths, updated);
return updated;
}
export function removeHistoryEntries(paths: StoragePaths, entryIds: readonly string[], limits?: HistoryLimits): HistoryEntry[] {
const existing = loadHistory(paths, limits);
const removedIds = new Set(entryIds);
if (removedIds.size === 0) {
return existing;
}
const updated = existing.filter((entry) => !removedIds.has(entry.id));
if (updated.length !== existing.length) {
saveHistory(paths, updated, limits);
}
return updated;
}
export function removeHistoryEntry(paths: StoragePaths, entryId: string, limits?: HistoryLimits): HistoryEntry[] {
return removeHistoryEntries(paths, [entryId], limits);
}
export function clearHistory(paths: StoragePaths): void {
ensureBaseDir(paths.baseDir);
+1
View File
@@ -124,6 +124,7 @@ const api: ElectronApi = {
},
clearHistory: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY),
removeHistoryEntry: (entryId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId),
removeHistoryEntries: (entryIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRIES, entryIds),
revealHistoryEntry: (entryId: string): Promise<HistoryRevealResult> => ipcRenderer.invoke(IPC_CHANNELS.REVEAL_HISTORY_ENTRY, entryId),
setPackagePriority: (packageId: string, priority: PackagePriority): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.SET_PACKAGE_PRIORITY, packageId, priority),
skipItems: (itemIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.SKIP_ITEMS, itemIds),
+4 -3
View File
@@ -3285,9 +3285,10 @@ export function App(): ReactElement {
if (!confirmed) {
return;
}
const results = await Promise.allSettled(ids.map((id) => window.rd.removeHistoryEntry(id)));
if (results.some((result) => result.status === "rejected")) {
showToast("Einige Verlaufseinträge konnten nicht entfernt werden");
try {
await window.rd.removeHistoryEntries(ids);
} catch {
showToast(ids.length === 1 ? "Verlaufseintrag konnte nicht entfernt werden" : "Verlaufseinträge konnten nicht entfernt werden");
await loadHistoryEntries();
return;
}
+1
View File
@@ -82,6 +82,7 @@ export const IPC_CHANNELS = {
HISTORY_ENTRY_ADDED: "history:entry-added",
CLEAR_HISTORY: "history:clear",
REMOVE_HISTORY_ENTRY: "history:remove-entry",
REMOVE_HISTORY_ENTRIES: "history:remove-entries",
REVEAL_HISTORY_ENTRY: "history:reveal-entry",
SET_PACKAGE_PRIORITY: "queue:set-package-priority",
SKIP_ITEMS: "queue:skip-items",
+1
View File
@@ -145,6 +145,7 @@ export interface ElectronApi {
onHistoryEntryAdded: (callback: (entry: HistoryEntry) => void) => () => void;
clearHistory: () => Promise<void>;
removeHistoryEntry: (entryId: string) => Promise<void>;
removeHistoryEntries: (entryIds: string[]) => Promise<void>;
revealHistoryEntry: (entryId: string) => Promise<HistoryRevealResult>;
setPackagePriority: (packageId: string, priority: PackagePriority) => Promise<void>;
skipItems: (itemIds: string[]) => Promise<void>;