refactor: Improves abort signal handling

This commit is contained in:
Aleksander Grygier 2026-01-27 14:52:23 +01:00
parent 38e33f063b
commit b9e08737e1
6 changed files with 68 additions and 69 deletions

View File

@ -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,

View File

@ -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<DatabaseMessage | null> {
): Promise<DatabaseMessage> {
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<DatabaseMessage | null> {
private async createAssistantMessage(parentId?: string): Promise<DatabaseMessage> {
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;

View File

@ -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;
}

View File

@ -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<ToolExecutionResult> {
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;
}

View File

@ -395,7 +395,7 @@ class ChatStore {
type: MessageType = MessageType.TEXT,
parent: string = '-1',
extras?: DatabaseMessageExtra[]
): Promise<DatabaseMessage | null> {
): Promise<DatabaseMessage> {
return chatClient.addMessage(role, content, type, parent, extras);
}

View File

@ -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';