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
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
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');
|
|
});
|