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:
42 files changed
+7457
-2101
No files matched your search
@@ -19,6 +19,7 @@ import {
|
||||
rowFromPayload,
|
||||
rowsFromHistory,
|
||||
} from './lib/mainLogs';
|
||||
import { AsyncOwner, windowAsyncScope } from './lib/asyncOwnership';
|
||||
|
||||
import './MainLogsWindow.css';
|
||||
|
||||
@@ -43,6 +44,8 @@ export const MainLogsWindow = () => {
|
||||
const pausedRef = useRef(false);
|
||||
const lastHistorySequenceRef = useRef(0);
|
||||
const historyLineCountsRef = useRef<Map<string, number>>(new Map());
|
||||
const viewOwnerRef = useRef<AsyncOwner | null>(null);
|
||||
const copyStatusTimerRef = useRef<number | undefined>(undefined);
|
||||
|
||||
const appendVisibleRows = useCallback((rows: MainLogRow[]) => {
|
||||
setLogs(current => capLogRows([...current, ...rows]));
|
||||
@@ -54,10 +57,18 @@ export const MainLogsWindow = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let unlisten: (() => void) | undefined;
|
||||
const owner = new AsyncOwner(error => {
|
||||
console.error('Failed to unregister main log listener:', error);
|
||||
});
|
||||
viewOwnerRef.current = owner;
|
||||
owner.own(() => {
|
||||
if (copyStatusTimerRef.current !== undefined) {
|
||||
window.clearTimeout(copyStatusTimerRef.current);
|
||||
copyStatusTimerRef.current = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const handleIncomingRow = (row: MainLogRow) => {
|
||||
const handleIncomingRow = owner.guard((row: MainLogRow) => {
|
||||
if (!historyLoadedRef.current) {
|
||||
initialBufferRef.current = capLogRows([...initialBufferRef.current, row]);
|
||||
return;
|
||||
@@ -76,43 +87,55 @@ export const MainLogsWindow = () => {
|
||||
return;
|
||||
}
|
||||
appendVisibleRows([row]);
|
||||
};
|
||||
});
|
||||
|
||||
const setup = async () => {
|
||||
try {
|
||||
unlisten = await listen<MainLogLinePayload>('main-log-line', event => {
|
||||
handleIncomingRow(rowFromPayload(event.payload));
|
||||
});
|
||||
|
||||
const history = await invoke<MainLogHistoryPayload>('get_main_logs');
|
||||
if (cancelled) return;
|
||||
|
||||
lastHistorySequenceRef.current = history.lastSequence;
|
||||
const historyRows = rowsFromHistory(history.contents);
|
||||
const historyLineCounts = lineCountsFromRows(historyRows);
|
||||
const liveRows = dedupeBufferedRows(
|
||||
historyLineCounts,
|
||||
initialBufferRef.current,
|
||||
lastHistorySequenceRef.current,
|
||||
const registered = await owner.register(() =>
|
||||
listen<MainLogLinePayload>('main-log-line', event => {
|
||||
handleIncomingRow(rowFromPayload(event.payload));
|
||||
})
|
||||
);
|
||||
initialBufferRef.current = [];
|
||||
historyLineCountsRef.current = historyLineCounts;
|
||||
historyLoadedRef.current = true;
|
||||
|
||||
if (pausedRef.current) {
|
||||
setLogs(capLogRows(historyRows));
|
||||
bufferPausedRows(liveRows);
|
||||
} else {
|
||||
setLogs(capLogRows([...historyRows, ...liveRows]));
|
||||
}
|
||||
setLoadError(null);
|
||||
if (!registered) return;
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
if (owner.isActive()) {
|
||||
historyLoadedRef.current = true;
|
||||
setLoadError(err instanceof Error ? err.message : String(err));
|
||||
setLoading(false);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await owner.applyIfActive(
|
||||
() => invoke<MainLogHistoryPayload>('get_main_logs'),
|
||||
history => {
|
||||
lastHistorySequenceRef.current = history.lastSequence;
|
||||
const historyRows = rowsFromHistory(history.contents);
|
||||
const historyLineCounts = lineCountsFromRows(historyRows);
|
||||
const liveRows = dedupeBufferedRows(
|
||||
historyLineCounts,
|
||||
initialBufferRef.current,
|
||||
lastHistorySequenceRef.current,
|
||||
);
|
||||
initialBufferRef.current = [];
|
||||
historyLineCountsRef.current = historyLineCounts;
|
||||
historyLoadedRef.current = true;
|
||||
|
||||
if (pausedRef.current) {
|
||||
setLogs(capLogRows(historyRows));
|
||||
bufferPausedRows(liveRows);
|
||||
} else {
|
||||
setLogs(capLogRows([...historyRows, ...liveRows]));
|
||||
}
|
||||
setLoadError(null);
|
||||
setLoading(false);
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
if (owner.isActive()) {
|
||||
historyLoadedRef.current = true;
|
||||
setLoadError(err instanceof Error ? err.message : String(err));
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -121,12 +144,12 @@ export const MainLogsWindow = () => {
|
||||
void setup();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
windowAsyncScope.adopt(owner.dispose());
|
||||
if (viewOwnerRef.current === owner) viewOwnerRef.current = null;
|
||||
historyLoadedRef.current = false;
|
||||
initialBufferRef.current = [];
|
||||
lastHistorySequenceRef.current = 0;
|
||||
historyLineCountsRef.current = new Map();
|
||||
unlisten?.();
|
||||
};
|
||||
}, [appendVisibleRows, bufferPausedRows]);
|
||||
|
||||
@@ -156,9 +179,10 @@ export const MainLogsWindow = () => {
|
||||
const viewport = viewportRef.current;
|
||||
if (!viewport) return;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const frame = requestAnimationFrame(() => {
|
||||
viewport.scrollTop = viewport.scrollHeight;
|
||||
});
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [autoScroll, filteredRows.length, lastVisibleRow?.id]);
|
||||
|
||||
const flushPausedRows = useCallback(() => {
|
||||
@@ -189,13 +213,24 @@ export const MainLogsWindow = () => {
|
||||
}, []);
|
||||
|
||||
const copyFilteredLogs = useCallback(async () => {
|
||||
const owner = viewOwnerRef.current;
|
||||
if (!owner) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(filteredRows.map(row => row.line).join('\n'));
|
||||
setCopyStatus('Copied');
|
||||
if (!await owner.applyIfActive(
|
||||
() => navigator.clipboard.writeText(filteredRows.map(row => row.line).join('\n')),
|
||||
() => setCopyStatus('Copied'),
|
||||
)) return;
|
||||
} catch {
|
||||
if (!owner.isActive()) return;
|
||||
setCopyStatus('Copy failed');
|
||||
}
|
||||
window.setTimeout(() => setCopyStatus(null), 1600);
|
||||
if (copyStatusTimerRef.current !== undefined) {
|
||||
window.clearTimeout(copyStatusTimerRef.current);
|
||||
}
|
||||
copyStatusTimerRef.current = window.setTimeout(owner.guard(() => {
|
||||
copyStatusTimerRef.current = undefined;
|
||||
setCopyStatus(null);
|
||||
}), 1600);
|
||||
}, [filteredRows]);
|
||||
|
||||
return (
|
||||
@@ -219,7 +254,7 @@ export const MainLogsWindow = () => {
|
||||
</button>
|
||||
<button
|
||||
className="settings-button"
|
||||
onClick={() => void copyFilteredLogs()}
|
||||
onClick={() => windowAsyncScope.adopt(copyFilteredLogs())}
|
||||
disabled={filteredRows.length === 0}
|
||||
>
|
||||
Copy
|
||||
|
||||
Reference in new issue
Block a user