feat(app): expose local sharing and verified transfers
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
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user