mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 04:18:44 +00:00
fix(ui): add LRU eviction to chat messages store
Keep at most 25 cached sessions in memory. When the limit is exceeded, evict the oldest idle sessions (never evict sessions with an active run).
This commit is contained in:
1 parent
6d83a6597b
commit
6a0b4769d1
1 file changed
+66
-84
@@ -1,119 +1,101 @@
|
||||
import { create } from "zustand";
|
||||
import type { ChatMessage } from "@/types/chat";
|
||||
|
||||
const MAX_CACHED_SESSIONS = 25;
|
||||
|
||||
interface SessionMessages {
|
||||
messages: ChatMessage[];
|
||||
streamText: string | null;
|
||||
thinkingText: string | null;
|
||||
isRunning: boolean;
|
||||
lastAccessedAt: number;
|
||||
}
|
||||
|
||||
interface ChatMessagesState {
|
||||
// Map of sessionKey -> session message state
|
||||
sessions: Record<string, SessionMessages>;
|
||||
|
||||
// Set messages for a specific session
|
||||
setSessionMessages: (sessionKey: string, messages: ChatMessage[]) => void;
|
||||
|
||||
// Update session messages incrementally
|
||||
updateSessionMessages: (sessionKey: string, updater: (prev: ChatMessage[]) => ChatMessage[]) => void;
|
||||
|
||||
// Set streaming text for a session
|
||||
setSessionStream: (sessionKey: string, streamText: string | null) => void;
|
||||
|
||||
// Set thinking text for a session
|
||||
setSessionThinking: (sessionKey: string, thinkingText: string | null) => void;
|
||||
|
||||
// Set running state for a session
|
||||
setSessionRunning: (sessionKey: string, isRunning: boolean) => void;
|
||||
}
|
||||
|
||||
// Evict oldest idle sessions when cache exceeds limit.
|
||||
// Never evict sessions with an active run (isRunning).
|
||||
function evictStale(sessions: Record<string, SessionMessages>): Record<string, SessionMessages> {
|
||||
const keys = Object.keys(sessions);
|
||||
if (keys.length <= MAX_CACHED_SESSIONS) return sessions;
|
||||
|
||||
const evictable = keys
|
||||
.filter((k) => !sessions[k]?.isRunning)
|
||||
.sort((a, b) => (sessions[a]?.lastAccessedAt ?? 0) - (sessions[b]?.lastAccessedAt ?? 0));
|
||||
|
||||
const toRemove = evictable.slice(0, keys.length - MAX_CACHED_SESSIONS);
|
||||
if (toRemove.length === 0) return sessions;
|
||||
|
||||
const next = { ...sessions };
|
||||
for (const k of toRemove) delete next[k];
|
||||
return next;
|
||||
}
|
||||
|
||||
function touchSession(existing: SessionMessages | undefined, patch: Partial<SessionMessages>): SessionMessages {
|
||||
return {
|
||||
messages: patch.messages ?? existing?.messages ?? [],
|
||||
streamText: patch.streamText !== undefined ? patch.streamText : (existing?.streamText ?? null),
|
||||
thinkingText: patch.thinkingText !== undefined ? patch.thinkingText : (existing?.thinkingText ?? null),
|
||||
isRunning: patch.isRunning !== undefined ? patch.isRunning : (existing?.isRunning ?? false),
|
||||
lastAccessedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
export const useChatMessagesStore = create<ChatMessagesState>((set) => ({
|
||||
sessions: {},
|
||||
|
||||
setSessionMessages: (sessionKey: string, messages: ChatMessage[]) => {
|
||||
setSessionMessages: (sessionKey, messages) => {
|
||||
set((state) => ({
|
||||
sessions: evictStale({
|
||||
...state.sessions,
|
||||
[sessionKey]: touchSession(state.sessions[sessionKey], { messages }),
|
||||
}),
|
||||
}));
|
||||
},
|
||||
|
||||
updateSessionMessages: (sessionKey, updater) => {
|
||||
set((state) => {
|
||||
const existing = state.sessions[sessionKey];
|
||||
const current = state.sessions[sessionKey]?.messages ?? [];
|
||||
return {
|
||||
sessions: {
|
||||
sessions: evictStale({
|
||||
...state.sessions,
|
||||
[sessionKey]: {
|
||||
messages,
|
||||
streamText: existing?.streamText ?? null,
|
||||
thinkingText: existing?.thinkingText ?? null,
|
||||
isRunning: existing?.isRunning ?? false,
|
||||
},
|
||||
},
|
||||
[sessionKey]: touchSession(state.sessions[sessionKey], { messages: updater(current) }),
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
updateSessionMessages: (sessionKey: string, updater: (prev: ChatMessage[]) => ChatMessage[]) => {
|
||||
set((state) => {
|
||||
const existing = state.sessions[sessionKey];
|
||||
const current = existing?.messages ?? [];
|
||||
return {
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[sessionKey]: {
|
||||
messages: updater(current),
|
||||
streamText: existing?.streamText ?? null,
|
||||
thinkingText: existing?.thinkingText ?? null,
|
||||
isRunning: existing?.isRunning ?? false,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
setSessionStream: (sessionKey, streamText) => {
|
||||
set((state) => ({
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[sessionKey]: touchSession(state.sessions[sessionKey], { streamText }),
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
setSessionStream: (sessionKey: string, streamText: string | null) => {
|
||||
set((state) => {
|
||||
const existing = state.sessions[sessionKey];
|
||||
return {
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[sessionKey]: {
|
||||
messages: existing?.messages ?? [],
|
||||
streamText,
|
||||
thinkingText: existing?.thinkingText ?? null,
|
||||
isRunning: existing?.isRunning ?? false,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
setSessionThinking: (sessionKey, thinkingText) => {
|
||||
set((state) => ({
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[sessionKey]: touchSession(state.sessions[sessionKey], { thinkingText }),
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
setSessionThinking: (sessionKey: string, thinkingText: string | null) => {
|
||||
set((state) => {
|
||||
const existing = state.sessions[sessionKey];
|
||||
return {
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[sessionKey]: {
|
||||
messages: existing?.messages ?? [],
|
||||
streamText: existing?.streamText ?? null,
|
||||
thinkingText,
|
||||
isRunning: existing?.isRunning ?? false,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setSessionRunning: (sessionKey: string, isRunning: boolean) => {
|
||||
set((state) => {
|
||||
const existing = state.sessions[sessionKey];
|
||||
return {
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[sessionKey]: {
|
||||
messages: existing?.messages ?? [],
|
||||
streamText: existing?.streamText ?? null,
|
||||
thinkingText: existing?.thinkingText ?? null,
|
||||
isRunning,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
setSessionRunning: (sessionKey, isRunning) => {
|
||||
set((state) => ({
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[sessionKey]: touchSession(state.sessions[sessionKey], { isRunning }),
|
||||
},
|
||||
}));
|
||||
},
|
||||
}));
|
||||
Reference in new issue
Block a user