refactor: Create shared ActiveConversationStore to avoid circular dependency between ChatStore and ConversationsStore
This commit is contained in:
parent
9cce846f32
commit
7ba1b458d5
|
|
@ -33,6 +33,7 @@ import {
|
|||
MAX_INACTIVE_CONVERSATION_STATES,
|
||||
INACTIVE_CONVERSATION_STATE_MAX_AGE_MS
|
||||
} from '$lib/constants/cache';
|
||||
import { isActiveConversation } from '$lib/stores/shared';
|
||||
|
||||
interface ConversationStateEntry {
|
||||
lastAccessed: number;
|
||||
|
|
@ -56,6 +57,14 @@ class ChatStore {
|
|||
private addFilesHandler: ((files: File[]) => void) | null = $state(null);
|
||||
pendingEditMessageId = $state<string | null>(null);
|
||||
|
||||
/**
|
||||
* Callback for updating message content in conversationsStore.
|
||||
* Registered by conversationsStore to avoid circular dependency.
|
||||
*/
|
||||
private messageUpdateCallback:
|
||||
| ((messageId: string, updates: Partial<DatabaseMessage>) => void)
|
||||
| null = null;
|
||||
|
||||
// Draft preservation for navigation (e.g., when adding system prompt from welcome page)
|
||||
private _pendingDraftMessage = $state<string>('');
|
||||
private _pendingDraftFiles = $state<ChatUploadedFile[]>([]);
|
||||
|
|
@ -84,30 +93,24 @@ class ChatStore {
|
|||
|
||||
private setChatLoading(convId: string, loading: boolean): void {
|
||||
this.touchConversationState(convId);
|
||||
import('$lib/stores/conversations.svelte').then(({ conversationsStore }) => {
|
||||
if (loading) {
|
||||
this.chatLoadingStates.set(convId, true);
|
||||
if (conversationsStore.activeConversation?.id === convId) this.isLoading = true;
|
||||
} else {
|
||||
this.chatLoadingStates.delete(convId);
|
||||
if (conversationsStore.activeConversation?.id === convId) this.isLoading = false;
|
||||
}
|
||||
});
|
||||
if (loading) {
|
||||
this.chatLoadingStates.set(convId, true);
|
||||
if (isActiveConversation(convId)) this.isLoading = true;
|
||||
} else {
|
||||
this.chatLoadingStates.delete(convId);
|
||||
if (isActiveConversation(convId)) this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private setChatStreaming(convId: string, response: string, messageId: string): void {
|
||||
this.touchConversationState(convId);
|
||||
this.chatStreamingStates.set(convId, { response, messageId });
|
||||
import('$lib/stores/conversations.svelte').then(({ conversationsStore }) => {
|
||||
if (conversationsStore.activeConversation?.id === convId) this.currentResponse = response;
|
||||
});
|
||||
if (isActiveConversation(convId)) this.currentResponse = response;
|
||||
}
|
||||
|
||||
private clearChatStreaming(convId: string): void {
|
||||
this.chatStreamingStates.delete(convId);
|
||||
import('$lib/stores/conversations.svelte').then(({ conversationsStore }) => {
|
||||
if (conversationsStore.activeConversation?.id === convId) this.currentResponse = '';
|
||||
});
|
||||
if (isActiveConversation(convId)) this.currentResponse = '';
|
||||
}
|
||||
|
||||
private getChatStreaming(convId: string): { response: string; messageId: string } | undefined {
|
||||
|
|
@ -121,16 +124,21 @@ class ChatStore {
|
|||
|
||||
// If there's an active stream for this conversation, update the message content
|
||||
// This ensures streaming content is visible when switching back to a conversation
|
||||
if (streamingState?.response && streamingState?.messageId) {
|
||||
import('$lib/stores/conversations.svelte').then(({ conversationsStore }) => {
|
||||
const idx = conversationsStore.findMessageIndex(streamingState.messageId);
|
||||
if (idx !== -1) {
|
||||
conversationsStore.updateMessageAtIndex(idx, { content: streamingState.response });
|
||||
}
|
||||
});
|
||||
if (streamingState?.response && streamingState?.messageId && this.messageUpdateCallback) {
|
||||
this.messageUpdateCallback(streamingState.messageId, { content: streamingState.response });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for updating message content.
|
||||
* Called by conversationsStore during initialization to avoid circular dependency.
|
||||
*/
|
||||
registerMessageUpdateCallback(
|
||||
callback: (messageId: string, updates: Partial<DatabaseMessage>) => void
|
||||
): void {
|
||||
this.messageUpdateCallback = callback;
|
||||
}
|
||||
|
||||
clearUIState(): void {
|
||||
this.isLoading = false;
|
||||
this.currentResponse = '';
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
import { browser } from '$app/environment';
|
||||
import type { McpServerOverride } from '$lib/types/database';
|
||||
import { setActiveConversationId } from '$lib/stores/shared';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
|
||||
class ConversationsStore {
|
||||
/** List of all conversations */
|
||||
|
|
@ -65,11 +67,23 @@ class ConversationsStore {
|
|||
const { conversationsClient } = await import('$lib/clients/conversations.client');
|
||||
this._client = conversationsClient;
|
||||
|
||||
// Register message update callback with chatStore to avoid circular dependency
|
||||
chatStore.registerMessageUpdateCallback((messageId, updates) => {
|
||||
const idx = this.findMessageIndex(messageId);
|
||||
if (idx !== -1) {
|
||||
this.updateMessageAtIndex(idx, updates);
|
||||
}
|
||||
});
|
||||
|
||||
conversationsClient.setStoreCallbacks({
|
||||
getConversations: () => this.conversations,
|
||||
setConversations: (conversations) => (this.conversations = conversations),
|
||||
getActiveConversation: () => this.activeConversation,
|
||||
setActiveConversation: (conversation) => (this.activeConversation = conversation),
|
||||
setActiveConversation: (conversation) => {
|
||||
this.activeConversation = conversation;
|
||||
// Update shared state for chatStore to use without circular dependency
|
||||
setActiveConversationId(conversation?.id ?? null);
|
||||
},
|
||||
getActiveMessages: () => this.activeMessages,
|
||||
setActiveMessages: (messages) => (this.activeMessages = messages),
|
||||
updateActiveMessages: (updater) => (this.activeMessages = updater(this.activeMessages)),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Shared Active Conversation State
|
||||
*
|
||||
* This module provides a dependency-free shared state for tracking the active conversation.
|
||||
* It eliminates circular dependencies between chatStore and conversationsStore.
|
||||
*
|
||||
* **Why this exists:**
|
||||
* - chatStore needs to know the active conversation ID to sync global loading/streaming state
|
||||
* - conversationsStore manages the active conversation
|
||||
* - Direct imports between stores would create circular dependencies
|
||||
*
|
||||
* **Usage:**
|
||||
* - conversationsStore: calls setId() when switching conversations
|
||||
* - chatStore: calls isActive() to check if state should sync to global
|
||||
*/
|
||||
|
||||
class ActiveConversationStore {
|
||||
private _id = $state<string | null>(null);
|
||||
|
||||
/**
|
||||
* Get the currently active conversation ID.
|
||||
* Returns null if no conversation is active.
|
||||
*/
|
||||
get id(): string | null {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active conversation ID.
|
||||
* Should only be called by conversationsStore when switching conversations.
|
||||
*/
|
||||
setId(id: string | null): void {
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given conversation ID is the currently active one.
|
||||
*/
|
||||
isActive(convId: string): boolean {
|
||||
return this._id === convId;
|
||||
}
|
||||
}
|
||||
|
||||
export const activeConversationStore = new ActiveConversationStore();
|
||||
|
||||
// Convenience exports for backward compatibility
|
||||
export const getActiveConversationId = () => activeConversationStore.id;
|
||||
export const setActiveConversationId = (id: string | null) => activeConversationStore.setId(id);
|
||||
export const isActiveConversation = (convId: string) => activeConversationStore.isActive(convId);
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Shared State Modules
|
||||
*
|
||||
* This directory contains dependency-free state modules that can be safely
|
||||
* imported by any store without creating circular dependencies.
|
||||
*
|
||||
* **Rules for modules in this folder:**
|
||||
* - NO imports from other stores
|
||||
* - NO imports from clients or services
|
||||
* - Only pure reactive state with no business logic
|
||||
*/
|
||||
|
||||
export {
|
||||
activeConversationStore,
|
||||
getActiveConversationId,
|
||||
setActiveConversationId,
|
||||
isActiveConversation
|
||||
} from './active-conversation.svelte';
|
||||
Loading…
Reference in New Issue