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.
This commit is contained in:
NimBold
2026-08-27 22:26:53 +03:30
parent 8d1cde8d2c
commit 97f15dee37
2 changed files with 52 additions and 10 deletions
-1
View File
@@ -19903,7 +19903,6 @@ pub fn run() {
mark_main_window_startup_complete(app_handle); mark_main_window_startup_complete(app_handle);
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
reveal_main_window(app_handle); reveal_main_window(app_handle);
#[cfg(not(target_os = "windows"))]
restore_pending_main_window(app_handle); restore_pending_main_window(app_handle);
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
+52 -9
View File
@@ -16,7 +16,22 @@ import { invokeCommand as invoke } from './ipc';
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-'); const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
void initLogger(); // 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) => {
if (document.readyState === 'complete') {
resolve();
return;
}
window.addEventListener('load', () => resolve(), { once: true });
});
void documentLoaded.then(() => {
void initLogger();
});
const serializeConsoleArguments = (values: unknown[]) => values.map(value => { const serializeConsoleArguments = (values: unknown[]) => values.map(value => {
if (value instanceof Error) return `${value.name}: ${value.message}\n${value.stack || ''}`; if (value instanceof Error) return `${value.name}: ${value.message}\n${value.stack || ''}`;
@@ -36,11 +51,13 @@ const originalConsoleError = console.error.bind(console);
const originalConsoleWarn = console.warn.bind(console); const originalConsoleWarn = console.warn.bind(console);
console.error = (...values: unknown[]) => { console.error = (...values: unknown[]) => {
originalConsoleError(...values); 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[]) => { console.warn = (...values: unknown[]) => {
originalConsoleWarn(...values); 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 rootElement = document.getElementById("root");
@@ -75,20 +92,46 @@ const PropertiesStartupFailure = () => (
</main> </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 () => { const renderMainApp = async () => {
if (!rootElement) return; if (!rootElement) return;
// Keep the child entrypoint isolated from the main application module. App await documentLoaded;
// imports the persistent Zustand stores, whose module initialization issues
// main-window-only IPC commands. Loading it in a Properties child creates a try {
// second persistence owner and can race the bridge handshake. // Keep the child entrypoint isolated from the main application module. App
const RootComponent = (await import('./App')).default; // imports the persistent Zustand stores, whose module initialization issues
renderRoot(RootComponent); // 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 () => { const renderPropertiesApp = async () => {
if (!rootElement) return; if (!rootElement) return;
await documentLoaded;
try { try {
// Properties starts with the synchronous English catalog and changes locale // Properties starts with the synchronous English catalog and changes locale
// after its first paint. Waiting for a lazy locale chunk here delays the // after its first paint. Waiting for a lazy locale chunk here delays the