Compare commits

..

1 Commits

Author SHA1 Message Date
NimBold 8931f14f72 test(startup): isolate window persistence (#37)
- Issue #37: remove only the renderer's main-window resize persistence from the Windows package comparison.\n- Keep native startup geometry, the application code, and the delayed stability smoke unchanged so the result isolates the frontend persistence owner.\n\nRefs #37.
2026-08-27 19:03:29 +03:30
3 changed files with 166 additions and 56 deletions
+40 -7
View File
@@ -18200,12 +18200,9 @@ fn toggle_log_pause(caller: tauri::WebviewWindow, pause: bool) -> Result<(), Str
}
#[tauri::command]
fn is_log_paused() -> bool {
// This read is needed during renderer bootstrap, including by standalone
// Properties windows. Avoid extracting a WebviewWindow here: on Windows,
// WebView2 can invoke the command while its host is still initializing.
// Mutating logging commands remain restricted to the main window below.
LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed)
fn is_log_paused(caller: tauri::WebviewWindow) -> bool {
properties_window::ensure_main_window(&caller).is_ok()
&& LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed)
}
#[tauri::command]
@@ -19860,7 +19857,42 @@ pub fn run() {
}
}
})
.invoke_handler(tauri::generate_handler![begin_dock_badge_session, is_log_paused])
.invoke_handler(tauri::generate_handler![
get_engine_status, get_aria2_engine_status, get_ytdlp_engine_status, get_ffmpeg_engine_status,
get_deno_engine_status, test_ytdlp, test_aria2c, test_ffmpeg, test_deno,
pause_download, resume_download, fetch_metadata, inspect_torrent, rekey_torrent_metadata, remove_torrent_metadata, fetch_media_metadata, fetch_media_playlist_metadata,
begin_dock_badge_session, update_dock_badge, get_platform_info, approve_download_root, set_prevent_sleep, set_power_preferences, get_free_space, perform_system_action,
ack_schedule_trigger,
check_automation_permission, request_automation_permission, open_automation_settings,
set_keychain_password, get_keychain_password, delete_keychain_password,
save_site_login, delete_site_login,
hydrate_extension_pairing_token, get_session_pairing_token, regenerate_pairing_token, grant_keychain_access,
get_keychain_grant_status, accept_keychain_grant, abandon_keychain_grant,
authorize_keychain_access,
acknowledge_pairing_token_change,
inspect_download_target, toggle_tray_icon, set_extension_pairing_token,
get_extension_server_port, set_extension_frontend_ready, ack_frontend_exit, ack_extension_download, set_concurrent_limit, set_queue_concurrency_limits, set_download_speed_limit, set_torrent_upload_limit, set_torrent_peer_options, get_torrent_peers, get_torrent_availability, get_torrent_file_progress, get_torrent_piece_progress, get_torrent_file_selection, set_torrent_file_selection, get_torrent_details, get_torrent_magnet_link, export_torrent_metadata, move_torrent_data, cancel_torrent_move_data, verify_torrent_data, get_torrent_web_seeds, set_torrent_web_seeds, set_torrent_max_open_files, set_torrent_overall_upload_limit, set_global_speed_limit, remove_download, get_download_primary_path,
detach_download_for_reconfigure,
enqueue_download, enqueue_many, cancel_enqueue_generation, move_in_queue, move_many_in_queue, remove_from_queue, get_pending_order,
commands::reveal_in_file_manager, commands::open_downloaded_file,
properties_window::open_download_properties_window,
properties_window::get_properties_window_download_id,
properties_window::properties_window_send_ready,
properties_window::properties_window_reveal,
properties_window::properties_window_send_action,
properties_window::validate_properties_window_request,
properties_window::close_download_properties_window,
properties_window::properties_window_registry_remove_for_download,
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media, parity::get_supported_media_domains,
parity::create_category_directories,
db_save_settings, db_load_settings, canonicalize_torrent_network_setting,
db_get_all_downloads, db_replace_downloads,
db_commit_download_state,
clear_torrent_removal_paths, reconcile_torrent_removal_reservations,
db_get_all_queues, db_replace_queues,
read_logs, export_logs, toggle_log_pause, is_log_paused, clear_logs,
set_log_stream_active
])
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app_handle, event| match event {
@@ -19868,6 +19900,7 @@ pub fn run() {
mark_main_window_startup_complete(app_handle);
#[cfg(target_os = "windows")]
reveal_main_window(app_handle);
#[cfg(not(target_os = "windows"))]
restore_pending_main_window(app_handle);
}
#[cfg(target_os = "macos")]
+1 -37
View File
@@ -15,8 +15,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
import { initDownloadListener } from './store/downloadStore';
import {
subscribeToSettingsPersistenceErrors,
useSettingsStore,
waitForSettingsPersistence
useSettingsStore
} from "./store/useSettingsStore";
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
import { WindowControls } from "./components/WindowControls";
@@ -40,9 +39,7 @@ import { changeAppLocale, localeDirection, resolveAppLocale, syncDocumentLocale
import { useTranslation } from 'react-i18next';
import { formatDownloadBytes } from './utils/downloadProgress';
import { synchronizeDocumentAppearance } from './utils/documentAppearance';
import { createMainWindowSizePersistence } from './utils/mainWindowState';
import { createSidebarResizeSession } from './utils/sidebarResize';
import type { MainWindowSize } from './bindings/MainWindowSize';
import {
beginSchedulerControl,
consumeSchedulerHandoffIds,
@@ -494,44 +491,14 @@ function App() {
useEffect(() => {
const disposePersistence = initializeDownloadPersistence(getCurrentWindow().label);
let active = true;
let exitRequested = false;
let exiting = false;
let settingsHydrated = useSettingsStore.persist.hasHydrated();
let latestSizeBeforeHydration: MainWindowSize | null = null;
const unlistenSettingsHydration = settingsHydrated
? null
: useSettingsStore.persist.onFinishHydration(() => {
settingsHydrated = true;
const size = latestSizeBeforeHydration;
latestSizeBeforeHydration = null;
if (size && active && !exitRequested && !exiting) {
useSettingsStore.getState().setMainWindowSize(size);
}
});
const mainWindowSizePersistence = createMainWindowSizePersistence({
appWindow: getCurrentWindow(),
onSize: size => {
if (!active || exiting) return;
if (!settingsHydrated) {
latestSizeBeforeHydration = size;
return;
}
useSettingsStore.getState().setMainWindowSize(size);
}
});
let cleanupListeners: (() => void) | null = null;
let unlistenExit: (() => void) | null = null;
const exitListener = listen('app-exit-requested', async () => {
exitRequested = true;
try {
await mainWindowSizePersistence.flush();
await waitForSettingsPersistence();
await flushDownloadPersistence();
} catch (error) {
console.error('Failed to flush download state before exit:', error);
} finally {
exiting = true;
latestSizeBeforeHydration = null;
await invoke('ack_frontend_exit').catch(error => {
console.error('Failed to acknowledge frontend exit flush:', error);
});
@@ -550,7 +517,6 @@ function App() {
let unlistenDeepLink: (() => void) | null = null;
const disposeListeners = () => {
void queueFrontendReadyUpdate(false).catch(() => {});
mainWindowSizePersistence.dispose();
unlistenExit?.();
unlistenExit = null;
unlistenTerminalState?.();
@@ -756,8 +722,6 @@ function App() {
cleanupListeners = null;
unlistenExit?.();
unlistenExit = null;
unlistenSettingsHydration?.();
mainWindowSizePersistence.dispose();
disposePersistence();
};
}, [addToast, enqueueAddInput, processExtensionDownload, queueFrontendReadyUpdate]);
+125 -12
View File
@@ -1,15 +1,128 @@
import { invoke } from '@tauri-apps/api/core';
import { StrictMode, type ComponentType } from "react";
import { createRoot } from "react-dom/client";
import "@fontsource-variable/inter/wght.css";
import "@fontsource-variable/noto-sans-hebrew/wght.css";
import "@fontsource-variable/noto-sans-sc/wght.css";
import "@fontsource-variable/outfit/wght.css";
import "@fontsource-variable/roboto/wght.css";
import "@fontsource-variable/vazirmatn/wght.css";
import "./index.css";
import { i18nReady } from "./i18n";
import { ErrorBoundary } from "./components/ErrorBoundary";
import { ToastProvider } from "./contexts/ToastContext";
import { error as logError, warn as logWarn, initLogger } from "./utils/logger";
import { getCurrentWindow } from '@tauri-apps/api/window';
import { invokeCommand as invoke } from './ipc';
const rootElement = document.getElementById('root');
if (rootElement) rootElement.textContent = 'Firelink startup control';
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
const documentLoaded = new Promise<void>((resolve) => {
const releaseAfterNativeLoad = () => window.setTimeout(resolve, 0);
if (document.readyState === 'complete') releaseAfterNativeLoad();
else window.addEventListener('load', releaseAfterNativeLoad, { once: true });
});
void documentLoaded.then(async () => {
await invoke<boolean>('is_log_paused');
if (rootElement) rootElement.textContent = 'Firelink post-load log read';
void initLogger();
const serializeConsoleArguments = (values: unknown[]) => values.map(value => {
if (value instanceof Error) return `${value.name}: ${value.message}\n${value.stack || ''}`;
if (typeof value === 'string') return value;
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}).join(' ');
const redactConsoleMessage = (message: string) => message
.replace(/(authorization|cookie|password|token|secret)\s*[:=]\s*([^\s,;]+)/gi, '$1=[redacted]')
.replace(/(https?:\/\/[^\s?]+)\?[^\s]+/g, '$1?[redacted]');
const originalConsoleError = console.error.bind(console);
const originalConsoleWarn = console.warn.bind(console);
console.error = (...values: unknown[]) => {
originalConsoleError(...values);
void logError(redactConsoleMessage(serializeConsoleArguments(values))).catch(() => undefined);
};
console.warn = (...values: unknown[]) => {
originalConsoleWarn(...values);
void logWarn(redactConsoleMessage(serializeConsoleArguments(values))).catch(() => undefined);
};
const rootElement = document.getElementById("root");
const renderRoot = (RootComponent: ComponentType) => {
if (!rootElement) return;
createRoot(rootElement).render(
<StrictMode>
<ErrorBoundary>
<ToastProvider>
<RootComponent />
</ToastProvider>
</ErrorBoundary>
</StrictMode>,
);
};
const PropertiesStartupFailure = () => (
<main className="properties-window-shell flex h-screen min-h-0 flex-col items-center justify-center gap-4 bg-main-bg p-6 text-text-primary">
<p role="alert">Download Properties could not be loaded.</p>
<button
type="button"
className="app-button app-button-primary px-3 text-xs"
onClick={() => {
void getCurrentWindow().close().catch(error => {
console.error('[PropertiesStartupFailure] close failed', error);
});
}}
>
Close
</button>
</main>
);
const renderMainApp = async () => {
if (!rootElement) return;
// Keep the child entrypoint isolated from the main application module. App
// imports the persistent Zustand stores, whose module initialization issues
// main-window-only IPC commands. Loading it in a Properties child creates a
// second persistence owner and can race the bridge handshake.
const RootComponent = (await import('./App')).default;
renderRoot(RootComponent);
};
const renderPropertiesApp = async () => {
if (!rootElement) return;
try {
// Properties starts with the synchronous English catalog and changes locale
// after its first paint. Waiting for a lazy locale chunk here delays the
// loading shell and makes native window startup visible to the user.
const RootComponent = (await import('./components/PropertiesWindowApp')).PropertiesWindowApp;
renderRoot(RootComponent);
} catch (error) {
// A failed lazy chunk must not leave the native window hidden forever. Show
// a styled, closable failure state and use the same caller-validated native
// reveal command as the normal child path.
console.error('Failed to initialize the Properties window:', error);
renderRoot(PropertiesStartupFailure);
const fallbackSessionId = crypto.randomUUID();
void invoke('properties_window_send_ready', { sessionId: fallbackSessionId })
.then(() => invoke('properties_window_reveal', { sessionId: fallbackSessionId }))
.catch(revealError => {
console.error('Failed to reveal the Properties startup error:', revealError);
});
}
};
if (isPropertiesWindow) {
void renderPropertiesApp();
} else {
void i18nReady.then(renderMainApp).catch(error => {
console.error('Failed to initialize localization:', error);
void renderMainApp();
});
}
// Prevent the webview's default context menu ("Reload", etc.) on right-click.
// Individual components that provide custom context menus call preventDefault()
// in their own onContextMenu handlers, which fires before this document-level
// listener and is unaffected.
document.addEventListener('contextmenu', (e) => {
e.preventDefault();
});