bolt.diy/app/shared/lib/persistence/db.ts
KevIsDev 4d3222ee96 refactor: reorganize project structure by moving files to a more dev friendly setup
- Move stores/utils/types to their relative directories (i.e chat stores in chat directory)
- Move utility files to shared/utils
- Move component files to shared/components
- Move type definitions to shared/types
- Move stores to shared/stores
- Update import paths across the project
2025-06-16 15:33:59 +01:00

344 lines
10 KiB
TypeScript

import type { Message } from 'ai';
import { createScopedLogger } from '~/shared/utils/logger';
import type { ChatHistoryItem } from './useChatHistory';
import type { Snapshot } from './types'; // Import Snapshot type
export interface IChatMetadata {
gitUrl: string;
gitBranch?: string;
netlifySiteId?: string;
}
const logger = createScopedLogger('ChatHistory');
// this is used at the top level and never rejects
export async function openDatabase(): Promise<IDBDatabase | undefined> {
if (typeof indexedDB === 'undefined') {
console.error('indexedDB is not available in this environment.');
return undefined;
}
return new Promise((resolve) => {
const request = indexedDB.open('boltHistory', 2);
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
if (!db.objectStoreNames.contains('chats')) {
const store = db.createObjectStore('chats', { keyPath: 'id' });
store.createIndex('id', 'id', { unique: true });
store.createIndex('urlId', 'urlId', { unique: true });
}
}
if (oldVersion < 2) {
if (!db.objectStoreNames.contains('snapshots')) {
db.createObjectStore('snapshots', { keyPath: 'chatId' });
}
}
};
request.onsuccess = (event: Event) => {
resolve((event.target as IDBOpenDBRequest).result);
};
request.onerror = (event: Event) => {
resolve(undefined);
logger.error((event.target as IDBOpenDBRequest).error);
};
});
}
export async function getAll(db: IDBDatabase): Promise<ChatHistoryItem[]> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const request = store.getAll();
request.onsuccess = () => resolve(request.result as ChatHistoryItem[]);
request.onerror = () => reject(request.error);
});
}
export async function setMessages(
db: IDBDatabase,
id: string,
messages: Message[],
urlId?: string,
description?: string,
timestamp?: string,
metadata?: IChatMetadata,
): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readwrite');
const store = transaction.objectStore('chats');
if (timestamp && isNaN(Date.parse(timestamp))) {
reject(new Error('Invalid timestamp'));
return;
}
const request = store.put({
id,
messages,
urlId,
description,
timestamp: timestamp ?? new Date().toISOString(),
metadata,
});
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
export async function getMessages(db: IDBDatabase, id: string): Promise<ChatHistoryItem> {
return (await getMessagesById(db, id)) || (await getMessagesByUrlId(db, id));
}
export async function getMessagesByUrlId(db: IDBDatabase, id: string): Promise<ChatHistoryItem> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const index = store.index('urlId');
const request = index.get(id);
request.onsuccess = () => resolve(request.result as ChatHistoryItem);
request.onerror = () => reject(request.error);
});
}
export async function getMessagesById(db: IDBDatabase, id: string): Promise<ChatHistoryItem> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const request = store.get(id);
request.onsuccess = () => resolve(request.result as ChatHistoryItem);
request.onerror = () => reject(request.error);
});
}
export async function deleteById(db: IDBDatabase, id: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['chats', 'snapshots'], 'readwrite'); // Add snapshots store to transaction
const chatStore = transaction.objectStore('chats');
const snapshotStore = transaction.objectStore('snapshots');
const deleteChatRequest = chatStore.delete(id);
const deleteSnapshotRequest = snapshotStore.delete(id); // Also delete snapshot
let chatDeleted = false;
let snapshotDeleted = false;
const checkCompletion = () => {
if (chatDeleted && snapshotDeleted) {
resolve(undefined);
}
};
deleteChatRequest.onsuccess = () => {
chatDeleted = true;
checkCompletion();
};
deleteChatRequest.onerror = () => reject(deleteChatRequest.error);
deleteSnapshotRequest.onsuccess = () => {
snapshotDeleted = true;
checkCompletion();
};
deleteSnapshotRequest.onerror = (event) => {
if ((event.target as IDBRequest).error?.name === 'NotFoundError') {
snapshotDeleted = true;
checkCompletion();
} else {
reject(deleteSnapshotRequest.error);
}
};
transaction.oncomplete = () => {
// This might resolve before checkCompletion if one operation finishes much faster
};
transaction.onerror = () => reject(transaction.error);
});
}
export async function getNextId(db: IDBDatabase): Promise<string> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const request = store.getAllKeys();
request.onsuccess = () => {
const highestId = request.result.reduce((cur, acc) => Math.max(+cur, +acc), 0);
resolve(String(+highestId + 1));
};
request.onerror = () => reject(request.error);
});
}
export async function getUrlId(db: IDBDatabase, id: string): Promise<string> {
const idList = await getUrlIds(db);
if (!idList.includes(id)) {
return id;
} else {
let i = 2;
while (idList.includes(`${id}-${i}`)) {
i++;
}
return `${id}-${i}`;
}
}
async function getUrlIds(db: IDBDatabase): Promise<string[]> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const idList: string[] = [];
const request = store.openCursor();
request.onsuccess = (event: Event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
if (cursor) {
idList.push(cursor.value.urlId);
cursor.continue();
} else {
resolve(idList);
}
};
request.onerror = () => {
reject(request.error);
};
});
}
export async function forkChat(db: IDBDatabase, chatId: string, messageId: string): Promise<string> {
const chat = await getMessages(db, chatId);
if (!chat) {
throw new Error('Chat not found');
}
// Find the index of the message to fork at
const messageIndex = chat.messages.findIndex((msg) => msg.id === messageId);
if (messageIndex === -1) {
throw new Error('Message not found');
}
// Get messages up to and including the selected message
const messages = chat.messages.slice(0, messageIndex + 1);
return createChatFromMessages(db, chat.description ? `${chat.description} (fork)` : 'Forked chat', messages);
}
export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
const chat = await getMessages(db, id);
if (!chat) {
throw new Error('Chat not found');
}
return createChatFromMessages(db, `${chat.description || 'Chat'} (copy)`, chat.messages);
}
export async function createChatFromMessages(
db: IDBDatabase,
description: string,
messages: Message[],
metadata?: IChatMetadata,
): Promise<string> {
const newId = await getNextId(db);
const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
await setMessages(
db,
newId,
messages,
newUrlId, // Use the new urlId
description,
undefined, // Use the current timestamp
metadata,
);
return newUrlId; // Return the urlId instead of id for navigation
}
export async function updateChatDescription(db: IDBDatabase, id: string, description: string): Promise<void> {
const chat = await getMessages(db, id);
if (!chat) {
throw new Error('Chat not found');
}
if (!description.trim()) {
throw new Error('Description cannot be empty');
}
await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp, chat.metadata);
}
export async function updateChatMetadata(
db: IDBDatabase,
id: string,
metadata: IChatMetadata | undefined,
): Promise<void> {
const chat = await getMessages(db, id);
if (!chat) {
throw new Error('Chat not found');
}
await setMessages(db, id, chat.messages, chat.urlId, chat.description, chat.timestamp, metadata);
}
export async function getSnapshot(db: IDBDatabase, chatId: string): Promise<Snapshot | undefined> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('snapshots', 'readonly');
const store = transaction.objectStore('snapshots');
const request = store.get(chatId);
request.onsuccess = () => resolve(request.result?.snapshot as Snapshot | undefined);
request.onerror = () => reject(request.error);
});
}
export async function setSnapshot(db: IDBDatabase, chatId: string, snapshot: Snapshot): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('snapshots', 'readwrite');
const store = transaction.objectStore('snapshots');
const request = store.put({ chatId, snapshot });
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
export async function deleteSnapshot(db: IDBDatabase, chatId: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('snapshots', 'readwrite');
const store = transaction.objectStore('snapshots');
const request = store.delete(chatId);
request.onsuccess = () => resolve();
request.onerror = (event) => {
if ((event.target as IDBRequest).error?.name === 'NotFoundError') {
resolve();
} else {
reject(request.error);
}
};
});
}