Fix review perf issues.

This commit is contained in:
Greyson Parrelli
2026-08-27 10:22:32 -04:00
parent 7414c89fe6
commit 5cb9bb117a
2 changed files with 46 additions and 16 deletions
+10 -11
View File
@@ -373,21 +373,20 @@ export default function App() {
}, []);
// Live updates. Every event on this stream is about this review — the stream is
// the tab's own — so anything that isn't the opening handshake means the comment
// list moved and is worth refetching.
// the tab's own — so anything that arrives means the comment list moved and is
// worth refetching.
//
// The handshake refetches too, which it did not used to. The stream is dropped
// while the pane is hidden (see useSSE) and reconnects when it comes back, so
// `connected` is the one moment the rail is known to be behind: every comment
// written while this pane was off screen is a frame nobody was listening for.
// Treating the handshake as "something changed" is what closes that gap — and
// the same gap after a dropped connection, which was always there.
//
// Comments only. The diff is not pushed: the server has no watcher on the work
// tree, and a comment arriving is a change to the rail, which can be applied
// under the reader safely. A changed *diff* cannot — see the poll below.
useSSE(
useCallback(
(e) => {
if (e.type === 'connected') return;
refetchComments();
},
[refetchComments],
),
);
useSSE(useCallback(() => refetchComments(), [refetchComments]));
// Notice that the diff on screen has been overtaken, and say so. Nothing here
// reloads anything: it sets `stale`, the banner offers the refresh, and the
+36 -5
View File
@@ -13,6 +13,22 @@ export interface ServerEvent {
// The stream is scoped by the URL it is opened on — the tab's own `api/events` —
// so unlike the tool this came from there is nothing to filter here: activity in
// another tab's review never arrives in the first place.
//
// ## Why this closes while the pane is hidden
//
// Every pane is a webview onto the same origin, and they all share one WebKit
// network session — so they also share its cap of six HTTP/1.1 connections per
// host. A stream held open for the pane's whole lifetime is one of those six
// spent permanently, and with a review open in seven tabs the panes starve each
// other: the diff, comment and repo fetches queue behind streams that are only
// ever going to sit there, and a small diff that the server answers in
// milliseconds never loads at all.
//
// So a hidden pane gives its connection back, exactly as the diff poll gives up
// its timer, and takes one again when it comes back on screen. What that costs
// is the events that arrived while it was away, which is why reconnecting
// resyncs the comment list rather than assuming the rail is still current — see
// the `connected` handler in App.
export function useSSE(onEvent: (e: ServerEvent) => void): void {
const handler = useRef(onEvent);
handler.current = onEvent;
@@ -22,8 +38,17 @@ export function useSSE(onEvent: (e: ServerEvent) => void): void {
let closed = false;
let retry: ReturnType<typeof setTimeout> | undefined;
const disconnect = () => {
if (retry) {
clearTimeout(retry);
retry = undefined;
}
es?.close();
es = null;
};
const connect = () => {
if (closed) return;
if (closed || es || document.hidden) return;
es = new EventSource(`${apiBase}/events`);
// The server's opening nudge is an SSE comment, so it never reaches
// onmessage; synthesize it, so a caller that wants to know the stream is up
@@ -37,16 +62,22 @@ export function useSSE(onEvent: (e: ServerEvent) => void): void {
}
};
es.onerror = () => {
es?.close();
if (!closed) retry = setTimeout(connect, 2000);
disconnect();
if (!closed && !document.hidden) retry = setTimeout(connect, 2000);
};
};
const onVisibility = () => {
if (document.hidden) disconnect();
else connect();
};
connect();
document.addEventListener('visibilitychange', onVisibility);
return () => {
closed = true;
if (retry) clearTimeout(retry);
es?.close();
document.removeEventListener('visibilitychange', onVisibility);
disconnect();
};
}, []);
}