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,103 @@
import {
PersistenceShutdownScope,
startAdmittedPersistenceEffect,
} from '../src/lib/frontendPersistence.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('window close drains admitted persistence and rejects later registration', async () => {
const first = deferred<void>();
const starts: string[] = [];
const scope = new PersistenceShutdownScope();
scope.register(() => {
starts.push('first');
return first.promise;
});
let drained = false;
const drain = scope.closeAndDrain().then(() => {
drained = true;
});
const lateRegistration = scope.register(() => {
starts.push('late');
return Promise.resolve();
});
assertEquals(lateRegistration, undefined, 'closed persistence admission must stay closed');
assertEquals(JSON.stringify(starts), JSON.stringify(['first']), 'late queues must not start');
assertEquals(drained, false, 'close must remain pending while persistence is active');
first.resolve();
await drain;
assertEquals(drained, true, 'close should settle after every admitted queue');
});
Deno.test('settings hydration cannot start after close wins before its passive effect', async () => {
const scope = new PersistenceShutdownScope();
await scope.closeAndDrain();
let storeLoads = 0;
let publications = 0;
const cleanup = startAdmittedPersistenceEffect(
scope,
() => Promise.resolve(),
() => {
storeLoads += 1;
publications += 1;
},
);
cleanup();
assertEquals(storeLoads, 0, 'closed admission must prevent the settings store load');
assertEquals(publications, 0, 'closed admission must prevent settings publication');
});
Deno.test('game-directory hydration cannot load or invoke after pre-effect close', async () => {
const scope = new PersistenceShutdownScope();
await scope.closeAndDrain();
let storeLoads = 0;
let backendInvokes = 0;
const cleanup = startAdmittedPersistenceEffect(
scope,
() => Promise.resolve(),
() => {
storeLoads += 1;
backendInvokes += 1;
},
);
cleanup();
assertEquals(storeLoads, 0, 'closed admission must prevent the directory store load');
assertEquals(backendInvokes, 0, 'closed admission must prevent the directory invoke');
});
Deno.test('persistence shutdown rejection is observed without blocking other queues', async () => {
const failure = new Error('persistence drain failed');
const reported: unknown[] = [];
const scope = new PersistenceShutdownScope(error => reported.push(error));
let secondFinished = false;
scope.register(() => Promise.reject(failure));
scope.register(async () => {
secondFinished = true;
});
await scope.closeAndDrain();
assertEquals(reported.length, 1, 'the failed shutdown should be reported once');
assertEquals(reported[0], failure, 'the original shutdown failure should be reported');
assertEquals(secondFinished, true, 'one failure must not skip another persistence queue');
});