history.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import { createStorage } from '../base/base';
  2. import { StorageEnum } from '../base/enums';
  3. import type { ChatSession, ChatMessage, ChatHistoryStorage, Message, ChatSessionMetadata } from './types';
  4. // Key for storing chat session metadata
  5. const CHAT_SESSIONS_META_KEY = 'chat_sessions_meta';
  6. // Create storage for session metadata
  7. const chatSessionsMetaStorage = createStorage<ChatSessionMetadata[]>(CHAT_SESSIONS_META_KEY, [], {
  8. storageEnum: StorageEnum.Local,
  9. liveUpdate: true,
  10. });
  11. // Helper function to get storage key for a specific session's messages
  12. const getSessionMessagesKey = (sessionId: string) => `chat_messages_${sessionId}`;
  13. // Helper function to create storage for a specific session's messages
  14. const getSessionMessagesStorage = (sessionId: string) => {
  15. return createStorage<ChatMessage[]>(getSessionMessagesKey(sessionId), [], {
  16. storageEnum: StorageEnum.Local,
  17. liveUpdate: true,
  18. });
  19. };
  20. // Helper function to get current timestamp in milliseconds
  21. const getCurrentTimestamp = (): number => Date.now();
  22. /**
  23. * Creates a chat history storage instance with optimized operations
  24. */
  25. export function createChatHistoryStorage(): ChatHistoryStorage {
  26. return {
  27. getAllSessions: async (): Promise<ChatSession[]> => {
  28. const sessionsMeta = await chatSessionsMetaStorage.get();
  29. // For listing purposes, we can return sessions without loading messages
  30. // This makes the list view very fast
  31. return sessionsMeta.map(meta => ({
  32. ...meta,
  33. messages: [], // Empty array as we don't load messages for listing
  34. }));
  35. },
  36. clearAllSessions: async (): Promise<void> => {
  37. const sessionsMeta = await chatSessionsMetaStorage.get();
  38. for (const sessionMeta of sessionsMeta) {
  39. const messagesStorage = getSessionMessagesStorage(sessionMeta.id);
  40. await messagesStorage.set([]);
  41. }
  42. await chatSessionsMetaStorage.set([]);
  43. },
  44. // Get session metadata without messages (for UI listing)
  45. getSessionsMetadata: async (): Promise<ChatSessionMetadata[]> => {
  46. return await chatSessionsMetaStorage.get();
  47. },
  48. getSession: async (sessionId: string): Promise<ChatSession | null> => {
  49. const sessionsMeta = await chatSessionsMetaStorage.get();
  50. const sessionMeta = sessionsMeta.find(session => session.id === sessionId);
  51. if (!sessionMeta) return null;
  52. // Load messages only when a specific session is requested
  53. const messagesStorage = getSessionMessagesStorage(sessionId);
  54. const messages = await messagesStorage.get();
  55. return {
  56. ...sessionMeta,
  57. messages,
  58. };
  59. },
  60. createSession: async (title: string): Promise<ChatSession> => {
  61. const newSessionId = crypto.randomUUID();
  62. const currentTime = getCurrentTimestamp();
  63. const newSessionMeta: ChatSessionMetadata = {
  64. id: newSessionId,
  65. title,
  66. createdAt: currentTime,
  67. updatedAt: currentTime,
  68. messageCount: 0,
  69. };
  70. // Create empty messages array for the new session
  71. const messagesStorage = getSessionMessagesStorage(newSessionId);
  72. await messagesStorage.set([]);
  73. // Add session metadata to the index
  74. await chatSessionsMetaStorage.set(prevSessions => [...prevSessions, newSessionMeta]);
  75. return {
  76. ...newSessionMeta,
  77. messages: [],
  78. };
  79. },
  80. updateTitle: async (sessionId: string, title: string): Promise<ChatSessionMetadata> => {
  81. let updatedSessionMeta: ChatSessionMetadata | undefined;
  82. // Update the title and capture the updated session in a single pass
  83. await chatSessionsMetaStorage.set(prevSessions => {
  84. return prevSessions.map(session => {
  85. if (session.id === sessionId) {
  86. // Create the updated session
  87. const updated = {
  88. ...session,
  89. title,
  90. updatedAt: getCurrentTimestamp(),
  91. };
  92. // Capture it for return value
  93. updatedSessionMeta = updated;
  94. return updated;
  95. }
  96. return session;
  97. });
  98. });
  99. // Check if we found and updated the session
  100. if (!updatedSessionMeta) {
  101. throw new Error('Session not found');
  102. }
  103. // Return the already captured metadata
  104. return updatedSessionMeta;
  105. },
  106. deleteSession: async (sessionId: string): Promise<void> => {
  107. // Remove session from metadata
  108. await chatSessionsMetaStorage.set(prevSessions => prevSessions.filter(session => session.id !== sessionId));
  109. // Remove the session's messages
  110. const messagesStorage = getSessionMessagesStorage(sessionId);
  111. await messagesStorage.set([]);
  112. },
  113. addMessage: async (sessionId: string, message: Message): Promise<ChatMessage> => {
  114. const newMessage: ChatMessage = {
  115. ...message,
  116. id: crypto.randomUUID(),
  117. };
  118. // First check if session exists and update metadata in a single operation
  119. let sessionFound = false;
  120. await chatSessionsMetaStorage.set(prevSessions => {
  121. return prevSessions.map(session => {
  122. if (session.id === sessionId) {
  123. sessionFound = true;
  124. return {
  125. ...session,
  126. updatedAt: getCurrentTimestamp(),
  127. messageCount: session.messageCount + 1,
  128. };
  129. }
  130. return session;
  131. });
  132. });
  133. // Throw error if session wasn't found
  134. if (!sessionFound) {
  135. throw new Error(`Session with ID ${sessionId} not found`);
  136. }
  137. // Only add the message if the session exists
  138. const messagesStorage = getSessionMessagesStorage(sessionId);
  139. await messagesStorage.set(prevMessages => [...prevMessages, newMessage]);
  140. return newMessage;
  141. },
  142. deleteMessage: async (sessionId: string, messageId: string): Promise<void> => {
  143. // Get the messages storage for this session
  144. const messagesStorage = getSessionMessagesStorage(sessionId);
  145. // Get current messages to calculate the new count
  146. const currentMessages = await messagesStorage.get();
  147. const messageToDelete = currentMessages.find(msg => msg.id === messageId);
  148. if (!messageToDelete) return; // Message not found
  149. // Remove the message directly from the messages storage
  150. await messagesStorage.set(prevMessages => prevMessages.filter(msg => msg.id !== messageId));
  151. // Update the session's metadata (updatedAt timestamp and messageCount)
  152. await chatSessionsMetaStorage.set(prevSessions => {
  153. return prevSessions.map(session => {
  154. if (session.id === sessionId) {
  155. return {
  156. ...session,
  157. updatedAt: getCurrentTimestamp(),
  158. messageCount: Math.max(0, session.messageCount - 1),
  159. };
  160. }
  161. return session;
  162. });
  163. });
  164. },
  165. };
  166. }
  167. // Export the storage instance for direct use
  168. export const chatHistoryStore = createChatHistoryStorage();