Files
ddidderr 71dbf27d8b 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
2026-08-10 13:59:40 +02:00

131 lines
4.2 KiB
TypeScript

import {
createThumbnailRequestKey,
startOwnedThumbnailGeneration,
ThumbnailRequestGeneration,
thumbnailIdsFromRequestKey,
} from '../src/lib/thumbnailRequests.ts';
import { AsyncAdoptionScope } from '../src/lib/asyncOwnership.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('thumbnail request keys are stable and deduplicate ids', () => {
const first = createThumbnailRequestKey(['bravo', 'alpha', 'bravo']);
const second = createThumbnailRequestKey(['alpha', 'bravo']);
assertEquals(first, second, 'equivalent id sets should have one effect key');
assertEquals(
JSON.stringify(thumbnailIdsFromRequestKey(first)),
JSON.stringify(['alpha', 'bravo']),
'the effect should receive each id once in stable order',
);
});
Deno.test('constructing a thumbnail generation does not start a request', async () => {
let starts = 0;
const generation = new ThumbnailRequestGeneration(
() => {
starts += 1;
return Promise.resolve('url');
},
() => {},
);
assertEquals(starts, 0, 'request work must wait for the owning effect to start it');
await generation.dispose();
await generation.start(['game']);
assertEquals(starts, 0, 'a disposed generation must not start later work');
});
Deno.test('a thumbnail generation cannot start after window admission closes', async () => {
const scope = new AsyncAdoptionScope();
await scope.disposeOwned();
let starts = 0;
const generation = new ThumbnailRequestGeneration(
() => {
starts += 1;
return Promise.resolve('url');
},
() => {},
);
const cleanup = startOwnedThumbnailGeneration(scope, generation, ['game']);
cleanup();
await scope.drain();
assertEquals(starts, 0, 'closed window admission must prevent the first invoke');
});
Deno.test('an unmounted thumbnail generation suppresses its in-flight result', async () => {
const result = deferred<string>();
const published: string[] = [];
const generation = new ThumbnailRequestGeneration(
() => result.promise,
(id, url) => published.push(`${id}:${url}`),
);
const loading = generation.start(['game']);
const disposal = generation.dispose();
result.resolve('thumbnail-url');
await loading;
await disposal;
assertEquals(published.length, 0, 'an in-flight result must not publish after unmount');
});
Deno.test('a replaced thumbnail generation cannot overwrite the current generation', async () => {
const abandonedResult = deferred<string>();
const published: string[] = [];
const abandoned = new ThumbnailRequestGeneration(
() => abandonedResult.promise,
(_id, url) => published.push(url),
);
const abandonedLoading = abandoned.start(['game']);
const abandonedDisposal = abandoned.dispose();
const current = new ThumbnailRequestGeneration(
() => Promise.resolve('current-url'),
(_id, url) => published.push(url),
);
await current.start(['game']);
abandonedResult.resolve('stale-url');
await abandonedLoading;
await abandonedDisposal;
assertEquals(
JSON.stringify(published),
JSON.stringify(['current-url']),
'only the current effect generation may publish',
);
});
Deno.test('an active failed thumbnail request publishes the empty fallback', async () => {
const failure = new Error('thumbnail unavailable');
const published: string[] = [];
const generation = new ThumbnailRequestGeneration(
() => Promise.reject(failure),
(id, url) => published.push(`${id}:${url}`),
);
await generation.start(['game']);
assertEquals(
JSON.stringify(published),
JSON.stringify(['game:']),
'active failures should retain the placeholder-cover behavior',
);
});