fix(settings): harden settings and diagnostic controls

This commit is contained in:
NimBold
2026-07-17 00:39:26 +03:30
parent 5144ecd39e
commit 6ef911919d
10 changed files with 226 additions and 18 deletions
+3
View File
@@ -200,6 +200,9 @@ describe('useDownloadStore', () => {
expect(normalizeCustomProxy(' socks5://127.0.0.1 ', 1080)).toBeNull();
expect(normalizeCustomProxy('https://proxy.local', 8443)).toBeNull();
expect(normalizeCustomProxy('127.0.0.1', NaN)).toBeNull();
expect(normalizeCustomProxy('127.0.0.1:9000', 8080)).toBeNull();
expect(normalizeCustomProxy('127.0.0.1/path', 8080)).toBeNull();
expect(normalizeCustomProxy('[::1]', 8080)).toBe('http://[::1]:8080');
expect(await getProxyArgs({
proxyMode: 'none',
+19 -1
View File
@@ -276,6 +276,7 @@ export const normalizeCustomProxy = (host: string, port: number): string | null
try {
const parsed = new URL(trimmedHost);
if (parsed.protocol !== 'http:') return null;
if (!parsed.hostname) return null;
if (!parsed.port) parsed.port = String(normalizedPort);
return parsed.toString().replace(/\/$/, '');
} catch {
@@ -283,7 +284,24 @@ export const normalizeCustomProxy = (host: string, port: number): string | null
}
}
return `http://${trimmedHost}:${normalizedPort}`;
try {
const parsed = new URL(`http://${trimmedHost}:${normalizedPort}`);
if (
!parsed.hostname
|| parsed.username
|| parsed.password
|| parsed.pathname !== '/'
|| parsed.search
|| parsed.hash
|| (parsed.port && Number(parsed.port) !== normalizedPort)
|| (!parsed.port && normalizedPort !== 80)
) {
return null;
}
return `http://${trimmedHost}:${normalizedPort}`;
} catch {
return null;
}
};
export const getProxyArgs = async (settings: ReturnType<typeof useSettingsStore.getState>) => {
+24 -1
View File
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useSettingsStore } from './useSettingsStore';
import { subscribeToSettingsPersistenceErrors, useSettingsStore } from './useSettingsStore';
import * as ipc from '../ipc';
vi.mock('../ipc', () => ({
@@ -66,3 +66,26 @@ describe('useSettingsStore credential-store startup flow', () => {
expect(useSettingsStore.getState().keychainPromptDismissed).toBe(true);
});
});
describe('useSettingsStore persistence failures', () => {
it('reports a database save failure and retries the next settings update', async () => {
vi.clearAllMocks();
await new Promise(resolve => setTimeout(resolve, 0));
const onPersistenceError = vi.fn();
const unsubscribe = subscribeToSettingsPersistenceErrors(onPersistenceError);
vi.mocked(ipc.invokeCommand).mockRejectedValueOnce(new Error('database unavailable'));
useSettingsStore.setState({ theme: 'dark' });
await new Promise(resolve => setTimeout(resolve, 0));
expect(onPersistenceError).toHaveBeenCalledTimes(1);
vi.mocked(ipc.invokeCommand).mockResolvedValueOnce(undefined);
useSettingsStore.setState({ theme: 'light' });
await new Promise(resolve => setTimeout(resolve, 0));
expect(onPersistenceError).toHaveBeenCalledTimes(1);
unsubscribe();
});
});
+26 -2
View File
@@ -20,9 +20,29 @@ import {
import { normalizeSpeedLimitForBackend } from '../utils/downloads';
let settingsSave = Promise.resolve();
const settingsPersistenceErrorListeners = new Set<() => void>();
let settingsPersistenceFailed = false;
const DEFAULT_SCHEDULER_QUEUE_ID = '00000000-0000-0000-0000-000000000001';
export const DEFAULT_SPEED_LIMIT_PRESET_VALUES = [1, 5, 10];
export const subscribeToSettingsPersistenceErrors = (listener: () => void): (() => void) => {
settingsPersistenceErrorListeners.add(listener);
if (settingsPersistenceFailed) listener();
return () => settingsPersistenceErrorListeners.delete(listener);
};
const notifySettingsPersistenceError = () => {
if (settingsPersistenceFailed) return;
settingsPersistenceFailed = true;
for (const listener of settingsPersistenceErrorListeners) {
try {
listener();
} catch (error) {
console.error('Settings persistence error listener failed', error);
}
}
};
const THEME_VALUES = ['system', 'light', 'dark', 'dracula', 'nord'] as const;
const APP_FONT_SIZE_VALUES = ['small', 'standard', 'large'] as const;
const LIST_ROW_DENSITY_VALUES = ['compact', 'standard', 'relaxed'] as const;
@@ -83,9 +103,13 @@ const tauriStorage: StateStorage = {
setItem: async (name: string, value: string): Promise<void> => {
if (name === 'firelink-settings') {
settingsSave = settingsSave
.catch(() => undefined)
.then(() => invoke('db_save_settings', { data: value }))
.catch(e => {
console.error("Failed to save settings to DB", e);
.then(() => {
settingsPersistenceFailed = false;
}, () => {
console.error('Failed to save settings to DB');
notifySettingsPersistenceError();
});
await settingsSave;
}