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,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');
|
||||
});
|
||||
Reference in New Issue
Block a user