fix(persistence): serialize autosave generations before queue effects

Track settings input generations so an older asynchronous secret save cannot mark a newer value durable. Commit queue snapshots before cancellation, cleanup, or pause effects so SQLite failures leave runtime processes and files unchanged.
This commit is contained in:
Sucukdeluxe
2026-08-12 00:38:49 +02:00
parent 6f02e9aa3a
commit 421da7be77
5 changed files with 211 additions and 26 deletions
+35 -22
View File
@@ -52,7 +52,7 @@ import { registerTrustedIpcHandler } from './main/domain/privileged-ipc';
import { createRendererQueueItem, getMergeGroupCleanupPaths } from './main/domain/renderer-queue-input';
import { createAppStateStore, type AppStateStore } from './main/domain/app-state-store';
import { createExportableConfig } from './main/domain/config-export';
import { persistStateChange } from './main/domain/persistence-commit';
import { commitQueueMutation, persistStateChange } from './main/domain/persistence-commit';
import { resolveSecretInputUpdate } from './main/domain/secret-input';
import { createSecretStore, type SecretStore } from './main/domain/secret-store';
import { createElectronSecureStorage } from './main/infra/secure-storage';
@@ -7424,25 +7424,29 @@ registerTrustedIpcHandler(ipcMain, 'add-to-queue', isTrustedRendererEvent, () =>
registerTrustedIpcHandler(ipcMain, 'remove-from-queue', isTrustedRendererEvent, () => Promise.resolve(downloadQueue), async (_, id: string) => {
if (typeof id !== 'string' || !id) return downloadQueue;
const wasActiveItem = activeQueueItemId === id || activeDownloads.has(id) || queueProcessRegistry.activeItemIds().includes(id);
const removedItem = downloadQueue.find((item) => item.id === id);
if (wasActiveItem) {
cancelledItemIds.add(id);
await queueProcessRegistry.cancelItem(id);
activeDownloads.delete(id);
const nextActiveId = queueProcessRegistry.activeItemIds()[0] || null;
activeQueueItemId = nextActiveId;
runtimeMetrics.activeItemId = nextActiveId;
runtimeMetrics.activeItemTitle = nextActiveId ? downloadQueue.find((item) => item.id === nextActiveId)?.title || null : null;
appendDebugLog('queue-item-removed-active-cancelled', { id });
}
// Clean up merge-group temp files (must run for any merge group, not just active)
const removedItem = downloadQueue.find(item => item.id === id);
for (const cleanupPath of getMergeGroupCleanupPaths(removedItem)) {
try { if (fs.existsSync(cleanupPath)) fs.unlinkSync(cleanupPath); } catch { }
}
downloadQueue = persistStateChange(downloadQueue, (current) => current.filter((item) => item.id !== id), saveQueue);
await commitQueueMutation(
downloadQueue,
(current) => current.filter((item) => item.id !== id),
saveQueue,
(nextQueue) => { downloadQueue = nextQueue; },
async () => {
if (wasActiveItem) {
cancelledItemIds.add(id);
await queueProcessRegistry.cancelItem(id);
activeDownloads.delete(id);
const nextActiveId = queueProcessRegistry.activeItemIds()[0] || null;
activeQueueItemId = nextActiveId;
runtimeMetrics.activeItemId = nextActiveId;
runtimeMetrics.activeItemTitle = nextActiveId ? downloadQueue.find((item) => item.id === nextActiveId)?.title || null : null;
appendDebugLog('queue-item-removed-active-cancelled', { id });
}
for (const cleanupPath of getMergeGroupCleanupPaths(removedItem)) {
try { if (fs.existsSync(cleanupPath)) fs.unlinkSync(cleanupPath); } catch { }
}
},
);
emitQueueUpdated();
return downloadQueue;
});
@@ -7631,7 +7635,6 @@ ipcMain.handle('pause-download', async (event) => {
if (!isTrustedRendererEvent(event)) return false;
if (!isDownloading || queuePaused) return false;
await Promise.all(queueProcessRegistry.activeItemIds().map((id) => queueProcessRegistry.pauseItem(id)));
const nextQueue = downloadQueue.map((item) => item.status === 'downloading' ? {
...item,
status: 'paused' as const,
@@ -7639,8 +7642,18 @@ ipcMain.handle('pause-download', async (event) => {
eta: '',
progressStatus: tBackend('downloadPaused')
} : item);
downloadQueue = persistStateChange(downloadQueue, () => nextQueue, saveQueue);
queuePaused = true;
await commitQueueMutation(
downloadQueue,
() => nextQueue,
saveQueue,
(candidate) => {
downloadQueue = candidate;
queuePaused = true;
},
async () => {
await Promise.all(queueProcessRegistry.activeItemIds().map((id) => queueProcessRegistry.pauseItem(id)));
},
);
emitQueueUpdated(true);
mainWindow?.webContents.send('download-paused');
return true;