chat-storage.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. /**
  2. * Chat Storage - Persist chat sessions to IndexedDB
  3. *
  4. * Independent from stage/scene storage cycle.
  5. * Handles serialization, truncation, and batch writes.
  6. */
  7. import type { ChatSession, ChatMessageMetadata, SessionStatus } from '@/lib/types/chat';
  8. import type { UIMessage } from 'ai';
  9. import { db, type ChatSessionRecord } from './database';
  10. /** Maximum messages per session to avoid IndexedDB bloat */
  11. const MAX_MESSAGES_PER_SESSION = 200;
  12. /**
  13. * Save chat sessions for a stage to IndexedDB.
  14. * - Active sessions are saved as 'interrupted' (streaming context lost on refresh)
  15. * - pendingToolCalls are cleared (runtime-only state)
  16. * - Messages are truncated to MAX_MESSAGES_PER_SESSION
  17. */
  18. export async function saveChatSessions(stageId: string, sessions: ChatSession[]): Promise<void> {
  19. if (!sessions || sessions.length === 0) {
  20. // Delete all sessions for this stage if empty
  21. await db.chatSessions.where('stageId').equals(stageId).delete();
  22. return;
  23. }
  24. const records: ChatSessionRecord[] = sessions.map((session) => ({
  25. id: session.id,
  26. stageId,
  27. type: session.type,
  28. title: session.title,
  29. // Mark active sessions as interrupted (streaming context lost on refresh)
  30. status: (session.status === 'active' ? 'interrupted' : session.status) as SessionStatus,
  31. // Truncate messages and strip non-serializable data
  32. messages: session.messages.slice(-MAX_MESSAGES_PER_SESSION),
  33. config: session.config,
  34. toolCalls: session.toolCalls,
  35. pendingToolCalls: [], // Clear runtime state
  36. createdAt: session.createdAt,
  37. updatedAt: session.updatedAt,
  38. sceneId: session.sceneId,
  39. lastActionIndex: session.lastActionIndex,
  40. }));
  41. await db.transaction('rw', db.chatSessions, async () => {
  42. // Delete old sessions for this stage, then bulk insert new ones
  43. await db.chatSessions.where('stageId').equals(stageId).delete();
  44. await db.chatSessions.bulkPut(records);
  45. });
  46. }
  47. /**
  48. * Load chat sessions for a stage from IndexedDB.
  49. * Returns sessions sorted by createdAt.
  50. */
  51. export async function loadChatSessions(stageId: string): Promise<ChatSession[]> {
  52. const records = await db.chatSessions.where('stageId').equals(stageId).sortBy('createdAt');
  53. return records.map((record) => ({
  54. id: record.id,
  55. type: record.type,
  56. title: record.title,
  57. status: record.status,
  58. messages: record.messages as UIMessage<ChatMessageMetadata>[],
  59. config: record.config,
  60. toolCalls: record.toolCalls,
  61. pendingToolCalls: record.pendingToolCalls,
  62. createdAt: record.createdAt,
  63. updatedAt: record.updatedAt,
  64. sceneId: record.sceneId,
  65. lastActionIndex: record.lastActionIndex,
  66. }));
  67. }
  68. /**
  69. * Delete all chat sessions for a stage.
  70. */
  71. export async function deleteChatSessions(stageId: string): Promise<void> {
  72. await db.chatSessions.where('stageId').equals(stageId).delete();
  73. }