fix: memory leak hunting and reconnecting on data error

This commit is contained in:
2026-05-18 19:37:30 +02:00
parent dea114aed0
commit 0152ed9dd2
5 changed files with 148 additions and 60 deletions

View File

@@ -41,7 +41,14 @@ const RETRYABLE_SAVE_ERROR_CODES = new Set([
'EBUSY'
]);
let saveQueue: Promise<void> = Promise.resolve();
interface PendingSaveWaiter {
reject: (error: unknown) => void;
resolve: () => void;
}
let pendingSaveSnapshot: Buffer | null = null;
let pendingSaveWaiters: PendingSaveWaiter[] = [];
let saveInProgress = false;
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -146,16 +153,51 @@ async function writeDatabaseSnapshot(snapshot: Buffer): Promise<void> {
}
}
function settleSaveWaiters(waiters: PendingSaveWaiter[], error?: unknown): void {
for (const waiter of waiters) {
if (error === undefined) {
waiter.resolve();
} else {
waiter.reject(error);
}
}
}
async function drainDatabaseSaveQueue(): Promise<void> {
if (saveInProgress) {
return;
}
saveInProgress = true;
try {
while (pendingSaveSnapshot) {
const snapshot = pendingSaveSnapshot;
const waiters = pendingSaveWaiters;
pendingSaveSnapshot = null;
pendingSaveWaiters = [];
try {
await writeDatabaseSnapshot(snapshot);
settleSaveWaiters(waiters);
} catch (error) {
settleSaveWaiters(waiters, error);
}
}
} finally {
saveInProgress = false;
}
}
async function atomicSave(data: Uint8Array): Promise<void> {
const snapshot = Buffer.from(data);
const saveTask = saveQueue.then(
() => writeDatabaseSnapshot(snapshot),
() => writeDatabaseSnapshot(snapshot)
);
saveQueue = saveTask.catch(() => {});
return saveTask;
return new Promise<void>((resolve, reject) => {
pendingSaveSnapshot = snapshot;
pendingSaveWaiters.push({ resolve, reject });
void drainDatabaseSaveQueue();
});
}
export async function initializeDatabase(): Promise<void> {

View File

@@ -62,6 +62,9 @@ import { listRunningProcessNames } from '../process-list';
import { detectActiveGame } from '../game-detection';
const DEFAULT_MIME_TYPE = 'application/octet-stream';
const MAX_ACTIVE_DESKTOP_NOTIFICATIONS = 20;
const activeDesktopNotifications = new Set<Notification>();
const desktopNotificationCleanups = new Map<Notification, () => void>();
const FILE_CLIPBOARD_FORMATS = [
'x-special/gnome-copied-files',
'text/uri-list',
@@ -399,9 +402,16 @@ export function setupSystemHandlers(): void {
icon: getWindowIconPath(),
silent: true
});
notification.on('click', () => {
const cleanup = () => {
notification.removeListener('click', handleClick);
notification.removeListener('close', cleanup);
notification.removeListener('failed', cleanup);
activeDesktopNotifications.delete(notification);
desktopNotificationCleanups.delete(notification);
};
const handleClick = () => {
if (!mainWindow) {
cleanup();
return;
}
@@ -414,7 +424,26 @@ export function setupSystemHandlers(): void {
}
mainWindow.focus();
});
cleanup();
notification.close();
};
notification.on('click', handleClick);
notification.once('close', cleanup);
notification.once('failed', cleanup);
activeDesktopNotifications.add(notification);
desktopNotificationCleanups.set(notification, cleanup);
while (activeDesktopNotifications.size > MAX_ACTIVE_DESKTOP_NOTIFICATIONS) {
const oldestNotification = activeDesktopNotifications.values().next().value;
if (!oldestNotification) {
break;
}
desktopNotificationCleanups.get(oldestNotification)?.();
oldestNotification.close();
}
notification.show();
} catch {