Skip to content

Commit 7ddea00

Browse files
Add SSE reconnection before critical actions and handle Cloudflare 524 errors
1 parent 8955573 commit 7ddea00

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

frontend/src/contexts/EventContext.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { OpenCodeClient } from '@/api/opencode'
66
import { listRepos } from '@/api/repos'
77
import type { PermissionRequest, PermissionResponse, QuestionRequest, SSEEvent } from '@/api/types'
88
import { showToast } from '@/lib/toast'
9-
import { subscribeToSSE, addSSEDirectory } from '@/lib/sseManager'
9+
import { subscribeToSSE, addSSEDirectory, ensureSSEConnected } from '@/lib/sseManager'
1010
import { OPENCODE_API_ENDPOINT } from '@/config'
1111
import { addToSessionKeyedState, removeFromSessionKeyedState } from '@/lib/sessionKeyedState'
1212

@@ -156,13 +156,23 @@ export function EventProvider({ children }: { children: React.ReactNode }) {
156156
}, [allPermissions.length, showPermissionDialog])
157157

158158
const respondToPermission = useCallback(async (permissionID: string, sessionID: string, response: PermissionResponse) => {
159+
const connected = await ensureSSEConnected()
160+
if (!connected) {
161+
showToast.error('Unable to connect. Please try again.')
162+
throw new Error('SSE connection failed')
163+
}
159164
const client = getClient(sessionID)
160165
if (!client) throw new Error('No client found for session')
161166
await client.respondToPermission(sessionID, permissionID, response)
162167
removePermission(permissionID, sessionID)
163168
}, [getClient, removePermission])
164169

165170
const replyToQuestion = useCallback(async (requestID: string, answers: string[][]) => {
171+
const connected = await ensureSSEConnected()
172+
if (!connected) {
173+
showToast.error('Unable to connect. Please try again.')
174+
throw new Error('SSE connection failed')
175+
}
166176
const question = Object.values(questionsBySession).flat().find(q => q.id === requestID)
167177
if (!question) throw new Error('Question not found')
168178
const client = getClient(question.sessionID)
@@ -172,6 +182,11 @@ export function EventProvider({ children }: { children: React.ReactNode }) {
172182
}, [getClient, removeQuestion, questionsBySession])
173183

174184
const rejectQuestion = useCallback(async (requestID: string) => {
185+
const connected = await ensureSSEConnected()
186+
if (!connected) {
187+
showToast.error('Unable to connect. Please try again.')
188+
throw new Error('SSE connection failed')
189+
}
175190
const question = Object.values(questionsBySession).flat().find(q => q.id === requestID)
176191
if (!question) throw new Error('Question not found')
177192
const client = getClient(question.sessionID)

frontend/src/hooks/useOpenCode.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { paths, components } from "../api/opencode-types";
1111
import { parseNetworkError } from "../lib/opencode-errors";
1212
import { showToast } from "../lib/toast";
1313
import { useSessionStatus } from "../stores/sessionStatusStore";
14+
import { ensureSSEConnected, reconnectSSE } from "../lib/sseManager";
1415

1516
const titleGeneratingSessionsState = new Set<string>();
1617
const titleGeneratingListeners = new Set<() => void>();
@@ -267,6 +268,12 @@ export const useSendPrompt = (opcodeUrl: string | null | undefined, directory?:
267268
}) => {
268269
if (!client) throw new Error("No client available");
269270

271+
const connected = await ensureSSEConnected();
272+
if (!connected) {
273+
showToast.error("Unable to connect. Please try again.");
274+
throw new Error("SSE connection failed");
275+
}
276+
270277
setSessionStatus(sessionID, { type: "busy" });
271278

272279
const optimisticUserID = `optimistic_user_${Date.now()}_${Math.random()}`;
@@ -344,6 +351,13 @@ export const useSendPrompt = (opcodeUrl: string | null | undefined, directory?:
344351
if (isNetworkError) {
345352
return;
346353
}
354+
355+
const fetchError = error as { statusCode?: number };
356+
const isCloudflareTimeout = fetchError.statusCode === 524;
357+
if (isCloudflareTimeout) {
358+
reconnectSSE();
359+
return;
360+
}
347361

348362
setSessionStatus(sessionID, { type: "idle" });
349363
queryClient.setQueryData<MessageListResponse>(

frontend/src/lib/sseManager.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,51 @@ class SSEManager {
265265
getConnectionStatus(): boolean {
266266
return this.isConnected
267267
}
268+
269+
async ensureConnected(timeoutMs: number = 5000): Promise<boolean> {
270+
if (this.isConnected && this.clientId) {
271+
return true
272+
}
273+
274+
if (this.subscribers.size === 0) {
275+
return false
276+
}
277+
278+
this.reconnectDelay = RECONNECT_DELAY_MS
279+
if (this.reconnectTimeout) {
280+
clearTimeout(this.reconnectTimeout)
281+
this.reconnectTimeout = null
282+
}
283+
284+
if (this.eventSource) {
285+
this.eventSource.close()
286+
this.eventSource = null
287+
}
288+
289+
return new Promise<boolean>((resolve) => {
290+
const timeout = setTimeout(() => {
291+
resolve(false)
292+
}, timeoutMs)
293+
294+
const checkConnection = () => {
295+
if (this.isConnected && this.clientId) {
296+
clearTimeout(timeout)
297+
resolve(true)
298+
}
299+
}
300+
301+
const originalNotify = this.notifyStatusChange.bind(this)
302+
this.notifyStatusChange = (connected: boolean) => {
303+
originalNotify(connected)
304+
if (connected) {
305+
this.notifyStatusChange = originalNotify
306+
checkConnection()
307+
}
308+
}
309+
310+
this.connect()
311+
})
312+
}
268313
}
269314

270315
export const sseManager = SSEManager.getInstance()
@@ -291,3 +336,7 @@ export function reconnectSSE(): void {
291336
export function isSSEConnected(): boolean {
292337
return sseManager.getConnectionStatus()
293338
}
339+
340+
export async function ensureSSEConnected(timeoutMs?: number): Promise<boolean> {
341+
return sseManager.ensureConnected(timeoutMs)
342+
}

0 commit comments

Comments
 (0)