Compare commits

..

4 Commits

Author SHA1 Message Date
NimBold 086567897a test(startup): isolate properties bridge host (#37) 2026-08-27 23:25:21 +03:30
NimBold 639f5bf091 fix(startup): defer renderer IPC past load callback (#37)
- Issue #37: Windows still overflowed when startup IPC ran from the WebView2 load callback.
- Release the renderer startup gate on the next event-loop task after load returns.
- Keep logger and console forwarding behind the same post-load boundary.

Fixes #37.
2026-08-27 22:45:55 +03:30
NimBold 97f15dee37 fix(startup): gate renderer IPC on document load (#37)
- Issue #37: Windows 1.4.0 exited with STATUS_STACK_OVERFLOW before showing the main window.
- Gate lazy entrypoint imports, Zustand persistence, logger initialization, and console forwarding until WebView2 document load.
- Preserve the native Ready reveal and pending-window restore lifecycle across platforms.
- Show a closeable startup error if the main entrypoint cannot load.

Fixes #37.
2026-08-27 22:26:53 +03:30
NimBold 8d1cde8d2c fix(startup): restore Windows renderer startup (#37)
- Issue #37: Windows 1.4.0 exited before showing its main window.\n- Keep the logger state read independent of WebViewWindow extraction during WebView2 bootstrap.\n- Preserve main-window authorization for logging mutations and keep the process-wide pause state consistent across renderers.\n\nFixes #37.
2026-08-27 21:23:00 +03:30
3 changed files with 127 additions and 17 deletions
+6 -4
View File
@@ -18200,9 +18200,12 @@ fn toggle_log_pause(caller: tauri::WebviewWindow, pause: bool) -> Result<(), Str
}
#[tauri::command]
fn is_log_paused(caller: tauri::WebviewWindow) -> bool {
properties_window::ensure_main_window(&caller).is_ok()
&& LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed)
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)
}
#[tauri::command]
@@ -19900,7 +19903,6 @@ 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")]
-2
View File
@@ -20,7 +20,6 @@ import {
} from "./store/useSettingsStore";
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
import { WindowControls } from "./components/WindowControls";
import { PropertiesWindowBridgeHost } from "./components/PropertiesWindowBridgeHost";
import { useToast } from "./contexts/ToastContext";
import { setLogStreamActive } from './utils/logger';
import { updateDockBadge } from './utils/dockBadge';
@@ -1287,7 +1286,6 @@ function App() {
{isAddModalOpen && <AddDownloadsModal />}
<PropertiesWindowBridgeHost />
{isDeleteModalOpen && (
<Suspense fallback={null}>
<DeleteConfirmationModal />
+121 -11
View File
@@ -1,4 +1,4 @@
import { StrictMode } from "react";
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";
@@ -7,12 +7,38 @@ import "@fontsource-variable/outfit/wght.css";
import "@fontsource-variable/roboto/wght.css";
import "@fontsource-variable/vazirmatn/wght.css";
import "./index.css";
import App from "./App";
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";
void initLogger();
import { getCurrentWindow } from '@tauri-apps/api/window';
import { invokeCommand as invoke } from './ipc';
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
// WebView2 can overflow its native call stack when the renderer sends IPC
// during document bootstrapping. Keep all renderer-to-native startup work
// behind the document load boundary, not just the first logger query. This is
// also needed for Zustand persistence, whose module initialization reads the
// native database before React mounts.
const documentLoaded = new Promise<void>((resolve) => {
const releaseAfterNativeLoad = () => {
// The load event is dispatched from WebView2's navigation callback. Move
// renderer startup to the next task so its first IPC cannot re-enter that
// native callback stack.
window.setTimeout(resolve, 0);
};
if (document.readyState === 'complete') {
releaseAfterNativeLoad();
return;
}
window.addEventListener('load', releaseAfterNativeLoad, { once: true });
});
void documentLoaded.then(() => {
void initLogger();
});
const serializeConsoleArguments = (values: unknown[]) => values.map(value => {
if (value instanceof Error) return `${value.name}: ${value.message}\n${value.stack || ''}`;
@@ -32,32 +58,116 @@ 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);
const message = redactConsoleMessage(serializeConsoleArguments(values));
void documentLoaded.then(() => logError(message)).catch(() => undefined);
};
console.warn = (...values: unknown[]) => {
originalConsoleWarn(...values);
void logWarn(redactConsoleMessage(serializeConsoleArguments(values))).catch(() => undefined);
const message = redactConsoleMessage(serializeConsoleArguments(values));
void documentLoaded.then(() => logWarn(message)).catch(() => undefined);
};
const rootElement = document.getElementById("root");
const renderApp = () => {
const renderRoot = (RootComponent: ComponentType) => {
if (!rootElement) return;
createRoot(rootElement).render(
<StrictMode>
<ErrorBoundary>
<ToastProvider>
<App />
<RootComponent />
</ToastProvider>
</ErrorBoundary>
</StrictMode>,
);
};
void i18nReady.then(renderApp).catch(error => {
console.error('Failed to initialize localization:', error);
renderApp();
});
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 MainStartupFailure = () => (
<main className="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">Firelink 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('[MainStartupFailure] close failed', error);
});
}}
>
Close
</button>
</main>
);
const renderMainApp = async () => {
if (!rootElement) return;
await documentLoaded;
try {
// 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);
} catch (error) {
console.error('Failed to initialize Firelink:', error);
renderRoot(MainStartupFailure);
}
};
const renderPropertiesApp = async () => {
if (!rootElement) return;
await documentLoaded;
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()