Skip to content

Commit ce3d5c3

Browse files
authored
fix: enhance chat streaming logic with abort handling (#140)
This PR fixes the issue where you start a new conversation and if one happens to already be streaming in, it continues to stream in in the new chat. The fix introduces an AbortController to manage streaming cancellations effectively. It prevents unnecessary processing of stream chunks when the stream is aborted or cancelled, improving performance and error handling. The chat component now properly handles stream cancellation, ensuring that no errors are logged if the stream is intentionally stopped. We can use this in future for a stop streaming button feature in a current chat, a common pattern in Chat apps. Fixes #107 **Before** ![CleanShot 2025-07-13 at 10 52 44](https://github.com/user-attachments/assets/8e47e40c-69c5-4c7a-b4dc-84261e1b81c0) **After** ![CleanShot 2025-07-13 at 10 51 33](https://github.com/user-attachments/assets/806c7fb7-23bd-46d2-8761-7b1a1cf5c89a)
1 parent f5a82dd commit ce3d5c3

File tree

1 file changed

+42
-3
lines changed

1 file changed

+42
-3
lines changed

src/components/Chat.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ export function Chat() {
200200
const textBufferRef = useRef<string>('')
201201
const lastAssistantIdRef = useRef<string | null>(null)
202202
const pendingStreamEventsRef = useRef<StreamEvent[]>([])
203+
const abortControllerRef = useRef<AbortController | null>(null)
204+
const streamCancelledRef = useRef<boolean>(false)
203205

204206
const flushTextBuffer = useCallback(() => {
205207
if (textBufferRef.current && lastAssistantIdRef.current) {
@@ -357,6 +359,9 @@ export function Chat() {
357359
setStreaming(true)
358360
setTimedOut(false)
359361

362+
abortControllerRef.current = new AbortController()
363+
streamCancelledRef.current = false
364+
360365
// Clone the response to handle our custom streaming while letting useChat handle its own
361366
const reader = response.clone().body?.getReader()
362367

@@ -382,6 +387,13 @@ export function Chat() {
382387
let receivedCompletion = false
383388

384389
const processChunk = (line: string) => {
390+
if (
391+
abortControllerRef.current?.signal.aborted ||
392+
streamCancelledRef.current
393+
) {
394+
return
395+
}
396+
385397
if (line.startsWith('e:')) {
386398
// Handle error messages
387399
try {
@@ -781,6 +793,13 @@ export function Chat() {
781793

782794
const readChunk = async () => {
783795
try {
796+
if (
797+
abortControllerRef.current?.signal.aborted ||
798+
streamCancelledRef.current
799+
) {
800+
return
801+
}
802+
784803
const { done, value } = await reader.read()
785804
if (done) {
786805
// Flush any remaining text buffer
@@ -801,8 +820,19 @@ export function Chat() {
801820
if (line.trim()) processChunk(line)
802821
}
803822

804-
readChunk()
823+
// Only continue reading if not cancelled
824+
if (!streamCancelledRef.current) {
825+
readChunk()
826+
}
805827
} catch (error) {
828+
// Don't show error if stream was aborted or cancelled
829+
if (
830+
abortControllerRef.current?.signal.aborted ||
831+
streamCancelledRef.current
832+
) {
833+
return
834+
}
835+
806836
console.error('Error reading stream chunk:', error)
807837
setStreamBuffer((prev: StreamEvent[]) => [
808838
...prev,
@@ -823,7 +853,7 @@ export function Chat() {
823853
[updateAssistantText, flushTextBuffer],
824854
)
825855

826-
const { messages, isLoading, setMessages, append } = useChat({
856+
const { messages, isLoading, setMessages, append, stop } = useChat({
827857
body: chatBody,
828858
onError: handleError,
829859
onResponse: handleResponse,
@@ -895,6 +925,15 @@ export function Chat() {
895925
clearTimeout(streamUpdateTimeoutRef.current)
896926
}
897927

928+
streamCancelledRef.current = true
929+
930+
if (abortControllerRef.current) {
931+
abortControllerRef.current.abort()
932+
abortControllerRef.current = null
933+
}
934+
935+
stop()
936+
898937
setHasStartedChat(false)
899938
setStreamBuffer([])
900939
setStreaming(false)
@@ -907,7 +946,7 @@ export function Chat() {
907946
textBufferRef.current = ''
908947
lastAssistantIdRef.current = null
909948
pendingStreamEventsRef.current = []
910-
}, [setMessages])
949+
}, [setMessages, stop])
911950

912951
const handleScrollToBottom = useCallback(() => {
913952
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })

0 commit comments

Comments
 (0)