refactor: Chat requests abort handling

This commit is contained in:
Aleksander Grygier 2025-11-26 16:48:13 +01:00
parent 42483f463d
commit 456828b365
2 changed files with 47 additions and 41 deletions

View File

@ -54,11 +54,9 @@ import type { SettingsChatServiceOptions } from '$lib/types/settings';
* - Streaming response handling with real-time callbacks
* - Reasoning content extraction and processing
* - File attachment processing (images, PDFs, audio, text)
* - Request lifecycle management (abort, cleanup)
* - Request lifecycle management (abort via AbortSignal)
*/
export class ChatService {
private abortControllers: Map<string, AbortController> = new Map();
/**
* Sends a chat completion request to the llama.cpp server.
* Supports both streaming and non-streaming responses with comprehensive parameter configuration.
@ -72,7 +70,8 @@ export class ChatService {
async sendMessage(
messages: ApiChatMessageData[] | (DatabaseMessage & { extra?: DatabaseMessageExtra[] })[],
options: SettingsChatServiceOptions = {},
conversationId?: string
conversationId?: string,
signal?: AbortSignal
): Promise<string | void> {
const {
stream,
@ -112,15 +111,6 @@ export class ChatService {
const currentConfig = config();
const requestId = conversationId || 'default';
if (this.abortControllers.has(requestId)) {
this.abortControllers.get(requestId)?.abort();
}
const abortController = new AbortController();
this.abortControllers.set(requestId, abortController);
const normalizedMessages: ApiChatMessageData[] = messages
.map((msg) => {
if ('id' in msg && 'convId' in msg && 'timestamp' in msg) {
@ -206,7 +196,7 @@ export class ChatService {
method: 'POST',
headers: getJsonHeaders(),
body: JSON.stringify(requestBody),
signal: abortController.signal
signal
});
if (!response.ok) {
@ -228,7 +218,7 @@ export class ChatService {
onModel,
onTimings,
conversationId,
abortController.signal
signal
);
return;
} else {
@ -272,8 +262,6 @@ export class ChatService {
onError(userFriendlyError);
}
throw userFriendlyError;
} finally {
this.abortControllers.delete(requestId);
}
}
@ -739,27 +727,6 @@ export class ChatService {
}
}
/**
* Aborts any ongoing chat completion request.
* Cancels the current request and cleans up the abort controller.
*
* @public
*/
public abortChatCompletionRequest(conversationId?: string): void {
if (conversationId) {
const abortController = this.abortControllers.get(conversationId);
if (abortController) {
abortController.abort();
this.abortControllers.delete(conversationId);
}
} else {
for (const controller of this.abortControllers.values()) {
controller.abort();
}
this.abortControllers.clear();
}
}
/**
* Injects a system message at the beginning of the conversation if configured in settings.
* Checks for existing system messages to avoid duplication and retrieves the system message

View File

@ -61,6 +61,9 @@ class ChatStore {
chatLoadingStates = new SvelteMap<string, boolean>();
chatStreamingStates = new SvelteMap<string, { response: string; messageId: string }>();
// Abort controllers for per-conversation request cancellation
private abortControllers = new SvelteMap<string, AbortController>();
// Processing state tracking - per-conversation timing/context info
private processingStates = new SvelteMap<string, ApiProcessingState | null>();
private processingCallbacks = new SvelteSet<(state: ApiProcessingState | null) => void>();
@ -517,6 +520,8 @@ class ChatStore {
this.startStreaming();
this.setActiveProcessingConversation(assistantMessage.convId);
const abortController = this.getOrCreateAbortController(assistantMessage.convId);
await chatService.sendMessage(
allMessages,
{
@ -615,7 +620,8 @@ class ChatStore {
if (onError) onError(error);
}
},
assistantMessage.convId
assistantMessage.convId,
abortController.signal
);
}
@ -673,12 +679,42 @@ class ChatStore {
if (!activeConv) return;
await this.savePartialResponseIfNeeded(activeConv.id);
this.stopStreaming();
chatService.abortChatCompletionRequest(activeConv.id);
this.abortRequest(activeConv.id);
this.setChatLoading(activeConv.id, false);
this.clearChatStreaming(activeConv.id);
this.clearProcessingState(activeConv.id);
}
/**
* Gets or creates an AbortController for a conversation
*/
private getOrCreateAbortController(convId: string): AbortController {
let controller = this.abortControllers.get(convId);
if (!controller || controller.signal.aborted) {
controller = new AbortController();
this.abortControllers.set(convId, controller);
}
return controller;
}
/**
* Aborts any ongoing request for a conversation
*/
private abortRequest(convId?: string): void {
if (convId) {
const controller = this.abortControllers.get(convId);
if (controller) {
controller.abort();
this.abortControllers.delete(convId);
}
} else {
for (const controller of this.abortControllers.values()) {
controller.abort();
}
this.abortControllers.clear();
}
}
private async savePartialResponseIfNeeded(convId?: string): Promise<void> {
const conversationId = convId || conversationsStore.activeConversation?.id;
if (!conversationId) return;
@ -1123,6 +1159,8 @@ class ChatStore {
appendedThinking = '',
hasReceivedContent = false;
const abortController = this.getOrCreateAbortController(msg.convId);
await chatService.sendMessage(
contextWithContinue,
{
@ -1218,7 +1256,8 @@ class ChatStore {
);
}
},
msg.convId
msg.convId,
abortController.signal
);
} catch (error) {
if (!this.isAbortError(error)) console.error('Failed to continue message:', error);