import { type AsyncCleanup } from './asyncOwnership'; export interface FrontendCloseRequest { preventDefault: () => void; } export interface FrontendBootstrapOptions { registerCloseRequested: ( handler: (event: FrontendCloseRequest) => void | Promise, ) => Promise; disposeOwners: () => Promise; drainPersistence: () => Promise; destroyWindow: () => Promise; render: () => void; reportFailure: (message: string, error: unknown) => void; } const report = ( options: FrontendBootstrapOptions, message: string, error: unknown, ): void => { try { options.reportFailure(message, error); } catch { // Reporting must not turn a handled bootstrap failure into a rejection. } }; const runHandled = async ( options: FrontendBootstrapOptions, message: string, operation: () => void | Promise, ): Promise => { try { await operation(); } catch (error) { report(options, message, error); } }; /** * Installs the webview's close boundary before mounting any React hooks. * Registration failure closes the empty realm instead of running without an * ownership boundary. */ export const bootstrapFrontend = async ( options: FrontendBootstrapOptions, ): Promise => { let closing: Promise | undefined; let publishCloseCleanup!: (cleanup: AsyncCleanup) => void; const closeCleanupReady = new Promise(resolve => { publishCloseCleanup = resolve; }); const finalizeWindow = (): Promise => { if (closing !== undefined) return closing; // Calling every operation closes its admission or starts its cleanup // synchronously, before any asynchronous drain is awaited. const ownerDrain = runHandled( options, 'Failed to dispose frontend window owners:', options.disposeOwners, ); const persistenceDrain = runHandled( options, 'Failed to drain frontend persistence:', options.drainPersistence, ); const listenerDrain = runHandled( options, 'Failed to unregister the window close listener:', async () => { const cleanup = await closeCleanupReady; await cleanup(); }, ); closing = Promise.all([ ownerDrain, persistenceDrain, listenerDrain, ]) .then(() => runHandled( options, 'Failed to destroy the frontend window:', options.destroyWindow, )); return closing; }; try { const cleanupCloseListener = await options.registerCloseRequested(event => { event.preventDefault(); return finalizeWindow(); }); publishCloseCleanup(cleanupCloseListener); } catch (error) { publishCloseCleanup(() => {}); report(options, 'Failed to install the frontend close-drain boundary:', error); // Nothing has rendered, but close both admissions in case a module-level // owner was registered while loading the bundle, then explicitly await // destruction of the empty webview. await finalizeWindow(); return false; } if (closing !== undefined) { // A close request may arrive after the native listener is callable but // before its registration promise acknowledges ownership. The active // finalizer now owns the late cleanup; never mount React into a realm // that has already begun closing. await closing; return false; } try { options.render(); } catch (error) { report(options, 'Failed to render the frontend:', error); await finalizeWindow(); return false; } return true; };