Add a durable, acknowledged Local network sharing switch with fail-closed hydration, serialized mutation, and redacted ephemeral-identity diagnostics. Keep local Call-to-Play state available while gating every network action on the effective sharing generation. Render revisioned verification, invalid-source retry, and sticky source exhaustion states. Preserve opaque attempt IDs through progress delivery so out-of-order webview events cannot attach stale bytes to a successor transfer, and keep terminal exhaustion visible after the last source departs. Own listeners, native invokes, persistence, dialogs, and companion-window creation through webview close. Late creation is settled and cleaned before the parent realm is destroyed. Test Plan: - `just frontend-test` -- passed (91/91) - `just build` -- passed with TypeScript, Vite, and release Tauri compilation - `just test` -- passed on the completed stack (708 workspace tests) - `just clippy` -- passed on the completed stack - `git diff --cached --check` -- passed
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import { type AsyncCleanup } from './asyncOwnership';
|
|
|
|
export interface FrontendCloseRequest {
|
|
preventDefault: () => void;
|
|
}
|
|
|
|
export interface FrontendBootstrapOptions {
|
|
registerCloseRequested: (
|
|
handler: (event: FrontendCloseRequest) => void | Promise<void>,
|
|
) => Promise<AsyncCleanup>;
|
|
disposeOwners: () => Promise<void>;
|
|
drainPersistence: () => Promise<void>;
|
|
destroyWindow: () => Promise<void>;
|
|
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<void>,
|
|
): Promise<void> => {
|
|
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<boolean> => {
|
|
let closing: Promise<void> | undefined;
|
|
let publishCloseCleanup!: (cleanup: AsyncCleanup) => void;
|
|
const closeCleanupReady = new Promise<AsyncCleanup>(resolve => {
|
|
publishCloseCleanup = resolve;
|
|
});
|
|
|
|
const finalizeWindow = (): Promise<void> => {
|
|
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;
|
|
};
|