From b9e08737e1ce98358c4ebfd0c769a52d1ebca13c Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 27 Jan 2026 14:52:23 +0100 Subject: [PATCH] refactor: Improves abort signal handling --- .../webui/src/lib/clients/agentic.client.ts | 3 +- .../webui/src/lib/clients/chat.client.ts | 112 ++++++++---------- .../webui/src/lib/services/chat.service.ts | 4 +- .../webui/src/lib/services/mcp.service.ts | 7 +- .../webui/src/lib/stores/chat.svelte.ts | 2 +- tools/server/webui/src/lib/utils/index.ts | 9 ++ 6 files changed, 68 insertions(+), 69 deletions(-) diff --git a/tools/server/webui/src/lib/clients/agentic.client.ts b/tools/server/webui/src/lib/clients/agentic.client.ts index 0f8db8818a..9de0be19f8 100644 --- a/tools/server/webui/src/lib/clients/agentic.client.ts +++ b/tools/server/webui/src/lib/clients/agentic.client.ts @@ -55,6 +55,7 @@ import type { DatabaseMessageExtraImageFile } from '$lib/types/database'; import { AttachmentType, MessageRole } from '$lib/enums'; +import { isAbortError } from '$lib/utils'; /** * Converts API messages to agentic format. @@ -507,7 +508,7 @@ export class AgenticClient { const executionResult = await mcpClient.executeTool(mcpCall, signal); result = executionResult.content; } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { + if (isAbortError(error)) { onComplete?.( '', undefined, diff --git a/tools/server/webui/src/lib/clients/chat.client.ts b/tools/server/webui/src/lib/clients/chat.client.ts index 09e8ee7663..3113b847f0 100644 --- a/tools/server/webui/src/lib/clients/chat.client.ts +++ b/tools/server/webui/src/lib/clients/chat.client.ts @@ -12,7 +12,8 @@ import { normalizeModelName, filterByLeafNodeId, findDescendantMessages, - findLeafNode + findLeafNode, + isAbortError } from '$lib/utils'; import { agenticStore } from '$lib/stores/agentic.svelte'; import { DEFAULT_CONTEXT } from '$lib/constants/default-context'; @@ -132,7 +133,8 @@ export class ChatClient { * @param type - Message type (text or root) * @param parent - Parent message ID, or '-1' to append to conversation end * @param extras - Optional attachments (images, files, etc.) - * @returns The created message or null if failed + * @returns The created message + * @throws Error if no active conversation or database operation fails */ async addMessage( role: MessageRole, @@ -140,56 +142,50 @@ export class ChatClient { type: MessageType = MessageType.TEXT, parent: string = '-1', extras?: DatabaseMessageExtra[] - ): Promise { + ): Promise { const activeConv = conversationsStore.activeConversation; if (!activeConv) { - console.error('No active conversation when trying to add message'); - return null; + throw new Error('No active conversation when trying to add message'); } - try { - let parentId: string | null = null; + let parentId: string | null = null; - if (parent === '-1') { - const activeMessages = conversationsStore.activeMessages; - if (activeMessages.length > 0) { - parentId = activeMessages[activeMessages.length - 1].id; - } else { - const allMessages = await conversationsStore.getConversationMessages(activeConv.id); - const rootMessage = allMessages.find((m) => m.parent === null && m.type === 'root'); - if (!rootMessage) { - parentId = await DatabaseService.createRootMessage(activeConv.id); - } else { - parentId = rootMessage.id; - } - } + if (parent === '-1') { + const activeMessages = conversationsStore.activeMessages; + if (activeMessages.length > 0) { + parentId = activeMessages[activeMessages.length - 1].id; } else { - parentId = parent; + const allMessages = await conversationsStore.getConversationMessages(activeConv.id); + const rootMessage = allMessages.find((m) => m.parent === null && m.type === 'root'); + if (!rootMessage) { + parentId = await DatabaseService.createRootMessage(activeConv.id); + } else { + parentId = rootMessage.id; + } } - - const message = await DatabaseService.createMessageBranch( - { - convId: activeConv.id, - role, - content, - type, - timestamp: Date.now(), - toolCalls: '', - children: [], - extra: extras - }, - parentId - ); - - conversationsStore.addMessageToActive(message); - await conversationsStore.updateCurrentNode(message.id); - conversationsStore.updateConversationTimestamp(); - - return message; - } catch (error) { - console.error('Failed to add message:', error); - return null; + } else { + parentId = parent; } + + const message = await DatabaseService.createMessageBranch( + { + convId: activeConv.id, + role, + content, + type, + timestamp: Date.now(), + toolCalls: '', + children: [], + extra: extras + }, + parentId + ); + + conversationsStore.addMessageToActive(message); + await conversationsStore.updateCurrentNode(message.id); + conversationsStore.updateConversationTimestamp(); + + return message; } /** @@ -334,9 +330,11 @@ export class ChatClient { * */ - private async createAssistantMessage(parentId?: string): Promise { + private async createAssistantMessage(parentId?: string): Promise { const activeConv = conversationsStore.activeConversation; - if (!activeConv) return null; + if (!activeConv) { + throw new Error('No active conversation when creating assistant message'); + } return await DatabaseService.createMessageBranch( { @@ -406,12 +404,10 @@ export class ChatClient { parentIdForUserMessage ?? '-1', extras ); - if (!userMessage) throw new Error('Failed to add user message'); if (isNewConversation && content) await conversationsStore.updateConversationName(currentConv.id, content.trim()); const assistantMessage = await this.createAssistantMessage(userMessage.id); - if (!assistantMessage) throw new Error('Failed to create assistant message'); conversationsStore.addMessageToActive(assistantMessage); await this.streamChatCompletion( @@ -419,7 +415,7 @@ export class ChatClient { assistantMessage ); } catch (error) { - if (this.isAbortError(error)) { + if (isAbortError(error)) { this.store.setChatLoading(currentConv.id, false); return; } @@ -611,7 +607,7 @@ export class ChatClient { onError: (error: Error) => { this.store.setStreamingActive(false); - if (this.isAbortError(error)) { + if (isAbortError(error)) { this.store.setChatLoading(assistantMessage.convId, false); this.store.clearChatStreaming(assistantMessage.convId); this.store.setProcessingState(assistantMessage.convId, null); @@ -806,7 +802,6 @@ export class ChatClient { this.store.clearChatStreaming(activeConv.id); const assistantMessage = await this.createAssistantMessage(); - if (!assistantMessage) throw new Error('Failed to create assistant message'); conversationsStore.addMessageToActive(assistantMessage); @@ -822,7 +817,7 @@ export class ChatClient { } ); } catch (error) { - if (!this.isAbortError(error)) console.error('Failed to update message:', error); + if (!isAbortError(error)) console.error('Failed to update message:', error); } } @@ -861,14 +856,13 @@ export class ChatClient { ? conversationsStore.activeMessages[conversationsStore.activeMessages.length - 1].id : undefined; const assistantMessage = await this.createAssistantMessage(parentMessageId); - if (!assistantMessage) throw new Error('Failed to create assistant message'); conversationsStore.addMessageToActive(assistantMessage); await this.streamChatCompletion( conversationsStore.activeMessages.slice(0, -1), assistantMessage ); } catch (error) { - if (!this.isAbortError(error)) console.error('Failed to regenerate message:', error); + if (!isAbortError(error)) console.error('Failed to regenerate message:', error); this.store.setChatLoading(activeConv?.id || '', false); } } @@ -926,7 +920,7 @@ export class ChatClient { modelToUse ); } catch (error) { - if (!this.isAbortError(error)) + if (!isAbortError(error)) console.error('Failed to regenerate message with branching:', error); this.store.setChatLoading(activeConv?.id || '', false); } @@ -1153,7 +1147,7 @@ export class ChatClient { }, onError: async (error: Error) => { - if (this.isAbortError(error)) { + if (isAbortError(error)) { if (hasReceivedContent && appendedContent) { await DatabaseService.updateMessage(msg.id, { content: originalContent + appendedContent, @@ -1189,7 +1183,7 @@ export class ChatClient { abortController.signal ); } catch (error) { - if (!this.isAbortError(error)) console.error('Failed to continue message:', error); + if (!isAbortError(error)) console.error('Failed to continue message:', error); if (activeConv) this.store.setChatLoading(activeConv.id, false); } } @@ -1564,10 +1558,6 @@ export class ChatClient { * */ - private isAbortError(error: unknown): boolean { - return error instanceof Error && (error.name === 'AbortError' || error instanceof DOMException); - } - private isChatLoading(convId: string): boolean { const streamingState = this.store.getChatStreaming(convId); return streamingState !== undefined; diff --git a/tools/server/webui/src/lib/services/chat.service.ts b/tools/server/webui/src/lib/services/chat.service.ts index 9696beffb4..ee575c42cf 100644 --- a/tools/server/webui/src/lib/services/chat.service.ts +++ b/tools/server/webui/src/lib/services/chat.service.ts @@ -1,4 +1,4 @@ -import { getJsonHeaders, formatAttachmentText } from '$lib/utils'; +import { getJsonHeaders, formatAttachmentText, isAbortError } from '$lib/utils'; import { AGENTIC_REGEX } from '$lib/constants/agentic'; import { AttachmentType, MessageRole, ReasoningFormat } from '$lib/enums'; import type { ApiChatMessageContentPart } from '$lib/types/api'; @@ -257,7 +257,7 @@ export class ChatService { ); } } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { + if (isAbortError(error)) { console.log('Chat completion request was aborted'); return; } diff --git a/tools/server/webui/src/lib/services/mcp.service.ts b/tools/server/webui/src/lib/services/mcp.service.ts index 73ae01772d..2769364ad0 100644 --- a/tools/server/webui/src/lib/services/mcp.service.ts +++ b/tools/server/webui/src/lib/services/mcp.service.ts @@ -37,6 +37,7 @@ import type { } from '$lib/types'; import { MCPConnectionPhase, MCPLogLevel, MCPTransportType } from '$lib/enums'; import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp'; +import { throwIfAborted, isAbortError } from '$lib/utils'; interface ToolResultContentItem { type: string; @@ -341,9 +342,7 @@ export class MCPService { params: ToolCallParams, signal?: AbortSignal ): Promise { - if (signal?.aborted) { - throw new DOMException('Aborted', 'AbortError'); - } + throwIfAborted(signal); try { const result = await connection.client.callTool( @@ -357,7 +356,7 @@ export class MCPService { isError: (result as ToolCallResult).isError ?? false }; } catch (error) { - if (error instanceof DOMException && error.name === 'AbortError') { + if (isAbortError(error)) { throw error; } diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts index 09852e5f60..08d133030b 100644 --- a/tools/server/webui/src/lib/stores/chat.svelte.ts +++ b/tools/server/webui/src/lib/stores/chat.svelte.ts @@ -395,7 +395,7 @@ class ChatStore { type: MessageType = MessageType.TEXT, parent: string = '-1', extras?: DatabaseMessageExtra[] - ): Promise { + ): Promise { return chatClient.addMessage(role, content, type, parent, extras); } diff --git a/tools/server/webui/src/lib/utils/index.ts b/tools/server/webui/src/lib/utils/index.ts index 1398df777f..33c330396c 100644 --- a/tools/server/webui/src/lib/utils/index.ts +++ b/tools/server/webui/src/lib/utils/index.ts @@ -122,3 +122,12 @@ export { parseAgenticContent, type AgenticSection } from './agentic'; // Cache utilities export { TTLCache, ReactiveTTLMap, type TTLCacheOptions } from './cache-ttl'; + +// Abort signal utilities +export { + throwIfAborted, + isAbortError, + createLinkedController, + createTimeoutSignal, + withAbortSignal +} from './abort';