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:
2026-08-10 13:59:40 +02:00
parent 60fd7ba0c2
commit 71dbf27d8b
42 changed files with 7457 additions and 2101 deletions
@@ -0,0 +1,257 @@
import {
bootstrapFrontend,
type FrontendCloseRequest,
} from '../src/lib/frontendBootstrap.ts';
const assertEquals = <T>(actual: T, expected: T, message: string) => {
if (actual !== expected) {
throw new Error(`${message}: expected ${expected}, got ${actual}`);
}
};
const deferred = <T>() => {
let resolve!: (value: T) => void;
let reject!: (reason: unknown) => void;
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
};
Deno.test('close-listener registration failure destroys the empty window without mounting hooks', async () => {
const registrationFailure = new Error('registration failed');
const destructionStarted = deferred<void>();
const destruction = deferred<void>();
const order: string[] = [];
const reported: unknown[] = [];
let renders = 0;
let settled = false;
const bootstrap = bootstrapFrontend({
registerCloseRequested: () => Promise.reject(registrationFailure),
disposeOwners: () => {
order.push('dispose owners');
return Promise.resolve();
},
drainPersistence: () => {
order.push('drain persistence');
return Promise.resolve();
},
destroyWindow: () => {
order.push('destroy window');
destructionStarted.resolve();
return destruction.promise;
},
render: () => {
renders += 1;
},
reportFailure: (_message, error) => reported.push(error),
}).then(result => {
settled = true;
return result;
});
await destructionStarted.promise;
assertEquals(renders, 0, 'React and its hooks must remain unmounted');
assertEquals(settled, false, 'bootstrap must explicitly await window destruction');
assertEquals(
JSON.stringify(order),
JSON.stringify(['dispose owners', 'drain persistence', 'destroy window']),
'admissions must close and drain before destroying the empty window',
);
destruction.resolve();
assertEquals(await bootstrap, false, 'registration failure must report no render');
assertEquals(renders, 0, 'hooks must never mount after failure cleanup');
assertEquals(reported.length, 1, 'the registration failure must be handled once');
assertEquals(reported[0], registrationFailure, 'the original failure must be reported');
});
Deno.test('bootstrap handles failure of the fail-closed destroy action', async () => {
const registrationFailure = new Error('registration failed');
const destructionFailure = new Error('destroy failed');
const reported: unknown[] = [];
let renders = 0;
const rendered = await bootstrapFrontend({
registerCloseRequested: () => Promise.reject(registrationFailure),
disposeOwners: () => Promise.resolve(),
drainPersistence: () => Promise.resolve(),
destroyWindow: () => Promise.reject(destructionFailure),
render: () => {
renders += 1;
},
reportFailure: (_message, error) => reported.push(error),
});
assertEquals(rendered, false, 'a failed registration must never render');
assertEquals(renders, 0, 'destroy failure must not fall back to mounting hooks');
assertEquals(reported.length, 2, 'both terminal failures must be handled');
assertEquals(reported[0], registrationFailure, 'registration failure must be preserved');
assertEquals(reported[1], destructionFailure, 'destroy failure must be observed');
});
Deno.test('successful bootstrap mounts only after close registration resolves', async () => {
const registered = deferred<(event: FrontendCloseRequest) => void | Promise<void>>();
const registrationAcknowledged = deferred<() => void>();
let renders = 0;
const bootstrap = bootstrapFrontend({
registerCloseRequested: handler => {
registered.resolve(handler);
return registrationAcknowledged.promise;
},
disposeOwners: () => Promise.resolve(),
drainPersistence: () => Promise.resolve(),
destroyWindow: () => Promise.resolve(),
render: () => {
renders += 1;
},
reportFailure: () => {},
});
await registered.promise;
assertEquals(renders, 0, 'render must wait for registration acknowledgement');
registrationAcknowledged.resolve(() => {});
assertEquals(await bootstrap, true, 'successful registration should render the app');
assertEquals(renders, 1, 'the app should mount once');
});
Deno.test('close before registration acknowledgement owns late cleanup and suppresses render', async () => {
const registered = deferred<(event: FrontendCloseRequest) => void | Promise<void>>();
const registrationAcknowledged = deferred<() => void>();
const cleanupDone = deferred<void>();
const order: string[] = [];
let renders = 0;
let prevented = 0;
const bootstrap = bootstrapFrontend({
registerCloseRequested: handler => {
registered.resolve(handler);
return registrationAcknowledged.promise;
},
disposeOwners: () => {
order.push('dispose owners');
return Promise.resolve();
},
drainPersistence: () => {
order.push('drain persistence');
return Promise.resolve();
},
destroyWindow: () => {
order.push('destroy window');
return Promise.resolve();
},
render: () => {
renders += 1;
},
reportFailure: () => {},
});
const close = (await registered.promise)({
preventDefault: () => {
prevented += 1;
},
});
assertEquals(prevented, 1, 'the pre-acknowledgement close must be intercepted');
assertEquals(renders, 0, 'React must remain unmounted while registration is unresolved');
registrationAcknowledged.resolve(async () => {
order.push('unlisten');
await cleanupDone.promise;
});
await Promise.resolve();
assertEquals(renders, 0, 'registration acknowledgement must not render after close began');
assertEquals(
order.includes('destroy window'),
false,
'the late listener cleanup must drain before destruction',
);
cleanupDone.resolve();
await close;
assertEquals(await bootstrap, false, 'a pre-render close must report no mounted app');
assertEquals(renders, 0, 'a closing realm must never mount React');
assertEquals(
JSON.stringify(order),
JSON.stringify(['dispose owners', 'drain persistence', 'unlisten', 'destroy window']),
'late listener ownership must join the same close finalizer',
);
});
Deno.test('render failure drains and unregisters before handled window destruction', async () => {
const renderFailure = new Error('render failed');
const unlistenFailure = new Error('unlisten failed');
const destroyFailure = new Error('destroy failed');
const ownerDrained = deferred<void>();
const persistenceDrained = deferred<void>();
const finalizerStarted = deferred<void>();
const order: string[] = [];
const reported: unknown[] = [];
let cleanups = 0;
const bootstrap = bootstrapFrontend({
registerCloseRequested: () => {
order.push('register');
return Promise.resolve(async () => {
order.push('unlisten');
cleanups += 1;
finalizerStarted.resolve();
throw unlistenFailure;
});
},
disposeOwners: async () => {
order.push('dispose owners');
await ownerDrained.promise;
},
drainPersistence: async () => {
order.push('drain persistence');
await persistenceDrained.promise;
},
destroyWindow: () => {
order.push('destroy window');
return Promise.reject(destroyFailure);
},
render: () => {
order.push('render');
throw renderFailure;
},
reportFailure: (_message, error) => reported.push(error),
});
await finalizerStarted.promise;
assertEquals(cleanups, 1, 'the installed close listener must unregister exactly once');
assertEquals(
JSON.stringify(order),
JSON.stringify(['register', 'render', 'dispose owners', 'drain persistence', 'unlisten']),
'render failure must synchronously start every frontend finalizer',
);
ownerDrained.resolve();
await Promise.resolve();
assertEquals(
order.includes('destroy window'),
false,
'window destruction must wait for persistence as well as owner cleanup',
);
persistenceDrained.resolve();
assertEquals(await bootstrap, false, 'a render failure must be fully handled');
assertEquals(
JSON.stringify(order),
JSON.stringify([
'register',
'render',
'dispose owners',
'drain persistence',
'unlisten',
'destroy window',
]),
'destruction must be the terminal finalizer',
);
assertEquals(reported.length, 3, 'render, cleanup, and destroy failures must be observed');
assertEquals(reported[0], renderFailure, 'the render failure must be reported first');
assertEquals(reported[1], unlistenFailure, 'listener cleanup rejection must be reported');
assertEquals(reported[2], destroyFailure, 'window destroy rejection must be reported');
});