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
@@ -1,286 +1,418 @@
import {
actionLabel,
activeStatusById,
applyFilterAndSort,
canStreamInstall,
countByFilter,
deriveState,
downloadProgressPercent,
formatDownloadBytes,
formatBytesPerSecond,
formatDownloadEta,
formatDownloadSpeed,
formatDownloadSpeedShort,
gameStatusLabel,
mergeGameUpdate,
stateChipLabel,
} from '../src/lib/gameState.ts';
actionLabel,
activeStatusById,
applyFilterAndSort,
applyGameTransferStatusSnapshot,
canStreamInstall,
countByFilter,
deriveState,
downloadProgressPercent,
formatBytesPerSecond,
formatDownloadBytes,
formatDownloadEta,
formatDownloadSpeed,
formatDownloadSpeedShort,
gameStatusLabel,
mergeGameUpdate,
stateChipLabel,
} from "../src/lib/gameState.ts";
import {
ActiveOperationKind,
GameAvailability,
InstallStatus,
type Game,
} from '../src/lib/types.ts';
ActiveOperationKind,
type Game,
GameAvailability,
GameTransferStatus,
InstallStatus,
} from "../src/lib/types.ts";
const assertEquals = <T>(actual: T, expected: T, message: string) => {
if (actual !== expected) {
throw new Error(`${message}: expected ${expected}, got ${actual}`);
}
if (actual !== expected) {
throw new Error(`${message}: expected ${expected}, got ${actual}`);
}
};
const game = (overrides: Partial<Game> = {}): Game => ({
id: 'game',
name: 'Game',
description: '',
size: 0,
downloaded: false,
installed: false,
availability: GameAvailability.LocalOnly,
install_status: InstallStatus.NotInstalled,
id: "game",
name: "Game",
description: "",
size: 0,
downloaded: false,
installed: false,
availability: GameAvailability.LocalOnly,
install_status: InstallStatus.NotInstalled,
peer_count: 1,
...overrides,
});
Deno.test("snapshot keeps installing visible until installed state settles", () => {
const fromDownloading = game({
install_status: InstallStatus.Downloading,
});
const installing = mergeGameUpdate(
game({ downloaded: true }),
fromDownloading,
InstallStatus.Installing,
);
const installedWhileActive = mergeGameUpdate(
game({ downloaded: true, installed: true }),
installing,
InstallStatus.Installing,
);
const settled = mergeGameUpdate(
game({ downloaded: true, installed: true }),
installedWhileActive,
);
assertEquals(
installing.install_status,
InstallStatus.Installing,
"active install snapshot should render Installing",
);
assertEquals(
installedWhileActive.install_status,
InstallStatus.Installing,
"installed local state should not override an active install snapshot",
);
assertEquals(
settled.install_status,
InstallStatus.Installed,
"cleared active snapshot with installed local state should render Installed",
);
});
Deno.test("active operation snapshot is the source of busy status", () => {
const statuses = activeStatusById([
{ id: "game", operation: ActiveOperationKind.Downloading },
{ id: "other", operation: ActiveOperationKind.Updating },
]);
assertEquals(
statuses.get("game"),
InstallStatus.Downloading,
"download operation should render Downloading",
);
assertEquals(
statuses.get("other"),
InstallStatus.Installing,
"update operation should render Installing",
);
});
Deno.test("download progress is preserved only while actively downloading", () => {
const downloading = game({
install_status: InstallStatus.Downloading,
download_progress: {
attemptId: "1",
downloaded_bytes: 50,
total_bytes: 100,
bytes_per_second: 12_500_000,
active_peer_count: 2,
},
});
const stillDownloading = mergeGameUpdate(
game(),
downloading,
InstallStatus.Downloading,
);
const settled = mergeGameUpdate(game({ downloaded: true }), stillDownloading);
assertEquals(
stillDownloading.download_progress?.downloaded_bytes,
50,
"active download snapshot should keep progress",
);
assertEquals(
stillDownloading.download_progress?.active_peer_count,
2,
"active download snapshot should keep live peer count",
);
assertEquals(
settled.download_progress,
undefined,
"settled snapshot should clear progress",
);
});
Deno.test("downloading action label includes current speed", () => {
const downloading = game({
install_status: InstallStatus.Downloading,
download_progress: {
attemptId: "1",
downloaded_bytes: 50,
total_bytes: 100,
bytes_per_second: 12_500_000,
active_peer_count: 2,
},
});
assertEquals(
formatBytesPerSecond(12_500_000),
"12.5 MB/s",
"speed formatter should use compact decimal units",
);
assertEquals(
actionLabel(downloading),
"Downloading… 12.5 MB/s",
"download label should include speed",
);
});
Deno.test("downloading state is distinct and stays on the local filter", () => {
const downloading = game({
id: "downloading",
name: "Downloading",
install_status: InstallStatus.Downloading,
});
const local = game({
id: "local",
name: "Local",
downloaded: true,
});
const remote = game({
id: "remote",
name: "Remote",
peer_count: 1,
...overrides,
});
assertEquals(
deriveState(downloading),
"downloading",
"download operation should render the dedicated downloading state",
);
assertEquals(
countByFilter([downloading, local, remote]).local,
2,
"local filter count should include in-flight downloads",
);
assertEquals(
applyFilterAndSort([downloading, local, remote], "local", "status", "")
.length,
2,
"local filter should include in-flight downloads",
);
});
Deno.test('snapshot keeps installing visible until installed state settles', () => {
const fromDownloading = game({
install_status: InstallStatus.Downloading,
});
const installing = mergeGameUpdate(
game({ downloaded: true }),
fromDownloading,
InstallStatus.Installing,
);
const installedWhileActive = mergeGameUpdate(
game({ downloaded: true, installed: true }),
installing,
InstallStatus.Installing,
);
const settled = mergeGameUpdate(
game({ downloaded: true, installed: true }),
installedWhileActive,
);
Deno.test("sticky transfer exhaustion keeps a departed remote game visible until cleared", () => {
const remoteOnly = game({
id: "remote-only",
name: "Remote only",
peer_count: 1,
});
assertEquals(
applyFilterAndSort([remoteOnly], "all", "az", "").length,
1,
"a remote-only game should initially be visible",
);
assertEquals(
installing.install_status,
InstallStatus.Installing,
'active install snapshot should render Installing',
);
assertEquals(
installedWhileActive.install_status,
InstallStatus.Installing,
'installed local state should not override an active install snapshot',
);
assertEquals(
settled.install_status,
InstallStatus.Installed,
'cleared active snapshot with installed local state should render Installed',
);
const peerDeparted = mergeGameUpdate(
game({ id: "remote-only", name: "Remote only", peer_count: 0 }),
remoteOnly,
);
assertEquals(
peerDeparted.install_status,
InstallStatus.NotInstalled,
"an empty active-operation snapshot should leave the game idle",
);
assertEquals(
peerDeparted.peer_count,
0,
"the sole remote source should be absent from the next catalog snapshot",
);
assertEquals(
applyFilterAndSort([peerDeparted], "all", "az", "").length,
0,
"an idle remote-only game should be invisible before exhaustion arrives",
);
const [exhausted] = applyGameTransferStatusSnapshot([peerDeparted], {
revision: 1,
statuses: { "remote-only": GameTransferStatus.Exhausted },
openAttempts: {},
});
assertEquals(
exhausted.transfer_status,
GameTransferStatus.Exhausted,
"the sticky snapshot should preserve the exact terminal status",
);
assertEquals(
applyFilterAndSort([exhausted], "all", "az", "").length,
1,
"terminal exhaustion should keep the departed remote game visible",
);
assertEquals(
countByFilter([exhausted]).all,
1,
"the All-filter count should match terminal exhaustion visibility",
);
const [cleared] = applyGameTransferStatusSnapshot([exhausted], {
revision: 2,
statuses: {},
openAttempts: {},
});
assertEquals(
cleared.transfer_status,
undefined,
"a full replacement that omits the game should clear exhaustion",
);
assertEquals(
applyFilterAndSort([cleared], "all", "az", "").length,
0,
"clearing exhaustion should remove the otherwise-invisible game",
);
});
Deno.test('active operation snapshot is the source of busy status', () => {
const statuses = activeStatusById([
{ id: 'game', operation: ActiveOperationKind.Downloading },
{ id: 'other', operation: ActiveOperationKind.Updating },
]);
Deno.test("transient transfer activity does not resurrect an invisible game", () => {
const departed = game({ peer_count: 0 });
for (
const transferStatus of [
GameTransferStatus.Verifying,
GameTransferStatus.Retrying,
]
) {
assertEquals(
statuses.get('game'),
InstallStatus.Downloading,
'download operation should render Downloading',
);
assertEquals(
statuses.get('other'),
InstallStatus.Installing,
'update operation should render Installing',
applyFilterAndSort(
[{ ...departed, transfer_status: transferStatus }],
"all",
"az",
"",
).length,
0,
`${transferStatus} should not make an otherwise-invisible game visible`,
);
}
});
Deno.test('download progress is preserved only while actively downloading', () => {
const downloading = game({
install_status: InstallStatus.Downloading,
download_progress: {
downloaded_bytes: 50,
total_bytes: 100,
bytes_per_second: 12_500_000,
active_peer_count: 2,
},
});
Deno.test("download progress formatting matches the progress-bar layouts", () => {
const downloading = game({
install_status: InstallStatus.Downloading,
download_progress: {
attemptId: "1",
downloaded_bytes: 12 * 1024 * 1024 * 1024,
total_bytes: 35 * 1024 * 1024 * 1024,
bytes_per_second: 49_400_000,
active_peer_count: 3,
},
});
const stillDownloading = mergeGameUpdate(
game(),
downloading,
InstallStatus.Downloading,
);
const settled = mergeGameUpdate(game({ downloaded: true }), stillDownloading);
assertEquals(
stillDownloading.download_progress?.downloaded_bytes,
50,
'active download snapshot should keep progress',
);
assertEquals(
stillDownloading.download_progress?.active_peer_count,
2,
'active download snapshot should keep live peer count',
);
assertEquals(
settled.download_progress,
undefined,
'settled snapshot should clear progress',
);
assertEquals(
Math.round(downloadProgressPercent(downloading)),
34,
"progress percent should come from backend byte counters",
);
assertEquals(
formatDownloadSpeed(49_400_000),
"49.4 MB/s",
"large bar speed format",
);
assertEquals(
formatDownloadSpeedShort(49_400_000),
"49 MB/s",
"card speed format",
);
assertEquals(
formatDownloadBytes(12 * 1024 * 1024 * 1024),
"12 GB",
"downloaded byte format should avoid noisy trailing decimals",
);
assertEquals(
formatDownloadEta(485),
"8 min",
"eta format should stay compact",
);
});
Deno.test('downloading action label includes current speed', () => {
const downloading = game({
install_status: InstallStatus.Downloading,
download_progress: {
downloaded_bytes: 50,
total_bytes: 100,
bytes_per_second: 12_500_000,
active_peer_count: 2,
},
});
assertEquals(
formatBytesPerSecond(12_500_000),
'12.5 MB/s',
'speed formatter should use compact decimal units',
);
assertEquals(
actionLabel(downloading),
'Downloading… 12.5 MB/s',
'download label should include speed',
);
});
Deno.test('downloading state is distinct and stays on the local filter', () => {
const downloading = game({
id: 'downloading',
name: 'Downloading',
install_status: InstallStatus.Downloading,
});
const local = game({
id: 'local',
name: 'Local',
downloaded: true,
});
const remote = game({
id: 'remote',
name: 'Remote',
peer_count: 1,
});
assertEquals(
deriveState(downloading),
'downloading',
'download operation should render the dedicated downloading state',
);
assertEquals(
countByFilter([downloading, local, remote]).local,
2,
'local filter count should include in-flight downloads',
);
assertEquals(
applyFilterAndSort([downloading, local, remote], 'local', 'status', '').length,
2,
'local filter should include in-flight downloads',
);
});
Deno.test('download progress formatting matches the progress-bar layouts', () => {
const downloading = game({
install_status: InstallStatus.Downloading,
download_progress: {
downloaded_bytes: 12 * 1024 * 1024 * 1024,
total_bytes: 35 * 1024 * 1024 * 1024,
bytes_per_second: 49_400_000,
active_peer_count: 3,
},
});
assertEquals(
Math.round(downloadProgressPercent(downloading)),
34,
'progress percent should come from backend byte counters',
);
assertEquals(formatDownloadSpeed(49_400_000), '49.4 MB/s', 'large bar speed format');
assertEquals(formatDownloadSpeedShort(49_400_000), '49 MB/s', 'card speed format');
assertEquals(
formatDownloadBytes(12 * 1024 * 1024 * 1024),
'12 GB',
'downloaded byte format should avoid noisy trailing decimals',
);
assertEquals(formatDownloadEta(485), '8 min', 'eta format should stay compact');
});
Deno.test('stream install is available only for idle remote games', () => {
assertEquals(
canStreamInstall(game({ downloaded: false, installed: false, peer_count: 1 })),
true,
'remote-only idle games should allow streamed install',
);
assertEquals(
canStreamInstall(game({ downloaded: true, installed: false, peer_count: 1 })),
false,
'downloaded games should install from local archives',
);
assertEquals(
canStreamInstall(game({ downloaded: false, installed: true, peer_count: 1 })),
false,
'installed games should not expose streamed install',
);
assertEquals(
canStreamInstall(game({ downloaded: false, installed: false, peer_count: 0 })),
false,
'games without peers should not expose streamed install',
);
assertEquals(
canStreamInstall(game({
downloaded: false,
installed: false,
peer_count: 1,
install_status: InstallStatus.CheckingPeers,
})),
false,
'busy games should not expose streamed install',
);
});
Deno.test('streamed local installs are labeled installed but not shareable', () => {
const streamed = game({
Deno.test("stream install is available only for idle remote games", () => {
assertEquals(
canStreamInstall(
game({ downloaded: false, installed: false, peer_count: 1 }),
true,
),
true,
"catalog-supported remote-only idle games should allow streamed install",
);
assertEquals(
canStreamInstall(
game({ downloaded: false, installed: false, peer_count: 1 }),
false,
),
false,
"catalog-unsupported games should not expose streamed install",
);
assertEquals(
canStreamInstall(
game({ downloaded: true, installed: false, peer_count: 1 }),
true,
),
false,
"downloaded games should install from local archives",
);
assertEquals(
canStreamInstall(
game({ downloaded: false, installed: true, peer_count: 1 }),
true,
),
false,
"installed games should not expose streamed install",
);
assertEquals(
canStreamInstall(
game({ downloaded: false, installed: false, peer_count: 0 }),
true,
),
false,
"games without peers should not expose streamed install",
);
assertEquals(
canStreamInstall(
game({
downloaded: false,
installed: true,
install_status: InstallStatus.Installed,
});
const downloadedInstall = game({
downloaded: true,
installed: true,
install_status: InstallStatus.Installed,
});
installed: false,
peer_count: 1,
install_status: InstallStatus.Installing,
}),
true,
),
false,
"busy games should not expose streamed install",
);
});
assertEquals(
deriveState(streamed),
'installed',
'streamed local installs should keep installed visual state',
);
assertEquals(
stateChipLabel(streamed),
'Not shareable',
'card chip should make the non-shareable state visible',
);
assertEquals(
gameStatusLabel(streamed),
'Installed, not shareable',
'detail status should spell out installed plus non-shareable',
);
assertEquals(
stateChipLabel(downloadedInstall),
'Installed',
'normal downloaded installs should keep the installed chip label',
);
assertEquals(
gameStatusLabel(downloadedInstall),
'Installed',
'normal downloaded installs should keep the installed detail label',
);
Deno.test("streamed local installs are labeled installed but not shareable", () => {
const streamed = game({
downloaded: false,
installed: true,
install_status: InstallStatus.Installed,
});
const downloadedInstall = game({
downloaded: true,
installed: true,
install_status: InstallStatus.Installed,
});
assertEquals(
deriveState(streamed),
"installed",
"streamed local installs should keep installed visual state",
);
assertEquals(
stateChipLabel(streamed),
"Not shareable",
"card chip should make the non-shareable state visible",
);
assertEquals(
gameStatusLabel(streamed),
"Installed, not shareable",
"detail status should spell out installed plus non-shareable",
);
assertEquals(
stateChipLabel(downloadedInstall),
"Installed",
"normal downloaded installs should keep the installed chip label",
);
assertEquals(
gameStatusLabel(downloadedInstall),
"Installed",
"normal downloaded installs should keep the installed detail label",
);
});