feat: improve SSE reconnection and visibility handling (#246)
- Keep SSE connection alive while tab is hidden to avoid disconnects - Trigger a soft resync on visibility restore or window focus after stall - Guard against multiple reconnects during visibility/window transitions
This commit is contained in:
committed by
GitHub
parent
520dd8c900
commit
caa097b42b
@@ -1830,6 +1830,10 @@ export const useEventStream = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nextAttempt = reconnectAttemptsRef.current + 1;
|
const nextAttempt = reconnectAttemptsRef.current + 1;
|
||||||
reconnectAttemptsRef.current = nextAttempt;
|
reconnectAttemptsRef.current = nextAttempt;
|
||||||
const statusHint = hint ?? `Retrying (${nextAttempt})`;
|
const statusHint = hint ?? `Retrying (${nextAttempt})`;
|
||||||
@@ -1881,54 +1885,50 @@ export const useEventStream = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pauseStreamSoon = () => {
|
const handleVisibilityChange = () => {
|
||||||
if (pauseTimeoutRef.current) return;
|
visibilityStateRef.current = resolveVisibilityState();
|
||||||
|
|
||||||
pauseTimeoutRef.current = setTimeout(() => {
|
if (visibilityStateRef.current !== 'visible') {
|
||||||
const pendingVisibility = resolveVisibilityState();
|
// Keep SSE connection alive while hidden; browsers may briefly toggle
|
||||||
visibilityStateRef.current = pendingVisibility;
|
// visibility during tab/window transitions.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pendingVisibility !== 'visible') {
|
|
||||||
stopStream();
|
|
||||||
pendingResumeRef.current = true;
|
|
||||||
publishStatus('paused', 'Paused while hidden');
|
|
||||||
} else {
|
|
||||||
clearPauseTimeout();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVisibilityChange = () => {
|
|
||||||
visibilityStateRef.current = resolveVisibilityState();
|
|
||||||
|
|
||||||
if (visibilityStateRef.current === 'visible') {
|
|
||||||
clearPauseTimeout();
|
clearPauseTimeout();
|
||||||
maybeBootstrapIfStale('visibility_restore');
|
maybeBootstrapIfStale('visibility_restore');
|
||||||
|
|
||||||
|
const isStalled = Date.now() - lastEventTimestampRef.current > 45000;
|
||||||
|
if (isStalled) {
|
||||||
|
console.info('[useEventStream] Visibility restored with stalled stream, reconnecting...');
|
||||||
|
pendingResumeRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (pendingResumeRef.current || !unsubscribeRef.current) {
|
if (pendingResumeRef.current || !unsubscribeRef.current) {
|
||||||
console.info('[useEventStream] Visibility restored, triggering soft refresh...');
|
console.info('[useEventStream] Visibility restored, triggering soft refresh...');
|
||||||
const sessionId = currentSessionIdRef.current;
|
const sessionId = currentSessionIdRef.current;
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
scheduleSoftResync(sessionId, 'visibility_restore', getActiveSessionWindow());
|
scheduleSoftResync(sessionId, 'visibility_restore', getActiveSessionWindow());
|
||||||
requestSessionMetadataRefresh(sessionId);
|
requestSessionMetadataRefresh(sessionId);
|
||||||
}
|
|
||||||
|
|
||||||
void refreshSessionActivityStatus();
|
|
||||||
publishStatus('connecting', 'Resuming stream');
|
|
||||||
startStream({ resetAttempts: true });
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
publishStatus('paused', 'Paused while hidden');
|
void refreshSessionActivityStatus();
|
||||||
pauseStreamSoon();
|
publishStatus('connecting', 'Resuming stream');
|
||||||
|
startStream({ resetAttempts: true });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWindowFocus = () => {
|
const handleWindowFocus = () => {
|
||||||
visibilityStateRef.current = resolveVisibilityState();
|
visibilityStateRef.current = resolveVisibilityState();
|
||||||
|
|
||||||
if (visibilityStateRef.current === 'visible') {
|
if (visibilityStateRef.current === 'visible') {
|
||||||
clearPauseTimeout();
|
clearPauseTimeout();
|
||||||
maybeBootstrapIfStale('window_focus');
|
maybeBootstrapIfStale('window_focus');
|
||||||
|
|
||||||
|
const isStalled = Date.now() - lastEventTimestampRef.current > 45000;
|
||||||
|
if (isStalled) {
|
||||||
|
pendingResumeRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (pendingResumeRef.current || !unsubscribeRef.current) {
|
if (pendingResumeRef.current || !unsubscribeRef.current) {
|
||||||
console.info('[useEventStream] Window focused after pause, triggering soft refresh...');
|
console.info('[useEventStream] Window focused after pause, triggering soft refresh...');
|
||||||
const sessionId = currentSessionIdRef.current;
|
const sessionId = currentSessionIdRef.current;
|
||||||
@@ -2018,9 +2018,12 @@ export const useEventStream = () => {
|
|||||||
const healthy = await opencodeClient.checkHealth();
|
const healthy = await opencodeClient.checkHealth();
|
||||||
if (!healthy) {
|
if (!healthy) {
|
||||||
scheduleReconnect('Refreshing stalled stream');
|
scheduleReconnect('Refreshing stalled stream');
|
||||||
} else {
|
return;
|
||||||
lastEventTimestampRef.current = Date.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If health is ok but SSE has been silent (including heartbeat),
|
||||||
|
// treat it as a stalled connection and reconnect.
|
||||||
|
scheduleReconnect('Refreshing stalled stream');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Health check after stale stream failed:', error);
|
console.warn('Health check after stale stream failed:', error);
|
||||||
scheduleReconnect('Refreshing stalled stream');
|
scheduleReconnect('Refreshing stalled stream');
|
||||||
|
|||||||
Reference in New Issue
Block a user