mirror of
https://github.com/stackblitz-labs/bolt.diy.git
synced 2025-06-18 01:49:07 +01:00
refactor: migrate snapshot storage from localStorage to IndexedDB
To improve data consistency and reliability, snapshot storage has been migrated from localStorage to IndexedDB. This change includes adding a new 'snapshots' object store, updating database version to 2, and modifying related functions to use IndexedDB for snapshot operations. The migration ensures better handling of snapshots alongside chat data and removes dependency on localStorage preventing UI lag.
This commit is contained in:
parent
5c44cb4e00
commit
fe37f5ceea
@ -1,6 +1,7 @@
|
||||
import type { Message } from 'ai';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import type { ChatHistoryItem } from './useChatHistory';
|
||||
import type { Snapshot } from './types'; // Import Snapshot type
|
||||
|
||||
export interface IChatMetadata {
|
||||
gitUrl: string;
|
||||
@ -18,15 +19,24 @@ export async function openDatabase(): Promise<IDBDatabase | undefined> {
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const request = indexedDB.open('boltHistory', 1);
|
||||
const request = indexedDB.open('boltHistory', 2);
|
||||
|
||||
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
const oldVersion = event.oldVersion;
|
||||
|
||||
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 < 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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -113,12 +123,46 @@ export async function getMessagesById(db: IDBDatabase, id: string): Promise<Chat
|
||||
|
||||
export async function deleteById(db: IDBDatabase, id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction('chats', 'readwrite');
|
||||
const store = transaction.objectStore('chats');
|
||||
const request = store.delete(id);
|
||||
const transaction = db.transaction(['chats', 'snapshots'], 'readwrite'); // Add snapshots store to transaction
|
||||
const chatStore = transaction.objectStore('chats');
|
||||
const snapshotStore = transaction.objectStore('snapshots');
|
||||
|
||||
request.onsuccess = () => resolve(undefined);
|
||||
request.onerror = () => reject(request.error);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -257,3 +301,43 @@ export async function updateChatMetadata(
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ import {
|
||||
setMessages,
|
||||
duplicateChat,
|
||||
createChatFromMessages,
|
||||
getSnapshot,
|
||||
setSnapshot,
|
||||
type IChatMetadata,
|
||||
} from './db';
|
||||
import type { FileMap } from '~/lib/stores/files';
|
||||
@ -61,19 +63,25 @@ export function useChatHistory() {
|
||||
}
|
||||
|
||||
if (mixedId) {
|
||||
getMessages(db, mixedId)
|
||||
.then(async (storedMessages) => {
|
||||
Promise.all([
|
||||
getMessages(db, mixedId),
|
||||
getSnapshot(db, mixedId), // Fetch snapshot from DB
|
||||
])
|
||||
.then(async ([storedMessages, snapshot]) => {
|
||||
if (storedMessages && storedMessages.messages.length > 0) {
|
||||
const snapshotStr = localStorage.getItem(`snapshot:${mixedId}`);
|
||||
const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} };
|
||||
const summary = snapshot.summary;
|
||||
/*
|
||||
* const snapshotStr = localStorage.getItem(`snapshot:${mixedId}`); // Remove localStorage usage
|
||||
* const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} }; // Use snapshot from DB
|
||||
*/
|
||||
const validSnapshot = snapshot || { chatIndex: '', files: {} }; // Ensure snapshot is not undefined
|
||||
const summary = validSnapshot.summary;
|
||||
|
||||
const rewindId = searchParams.get('rewindTo');
|
||||
let startingIdx = -1;
|
||||
const endingIdx = rewindId
|
||||
? storedMessages.messages.findIndex((m) => m.id === rewindId) + 1
|
||||
: storedMessages.messages.length;
|
||||
const snapshotIndex = storedMessages.messages.findIndex((m) => m.id === snapshot.chatIndex);
|
||||
const snapshotIndex = storedMessages.messages.findIndex((m) => m.id === validSnapshot.chatIndex);
|
||||
|
||||
if (snapshotIndex >= 0 && snapshotIndex < endingIdx) {
|
||||
startingIdx = snapshotIndex;
|
||||
@ -93,7 +101,7 @@ export function useChatHistory() {
|
||||
setArchivedMessages(archivedMessages);
|
||||
|
||||
if (startingIdx > 0) {
|
||||
const files = Object.entries(snapshot?.files || {})
|
||||
const files = Object.entries(validSnapshot?.files || {})
|
||||
.map(([key, value]) => {
|
||||
if (value?.type !== 'file') {
|
||||
return null;
|
||||
@ -197,17 +205,20 @@ ${value.content}
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
logStore.logError('Failed to load chat messages', error);
|
||||
toast.error(error.message);
|
||||
logStore.logError('Failed to load chat messages or snapshot', error); // Updated error message
|
||||
toast.error('Failed to load chat: ' + error.message); // More specific error
|
||||
});
|
||||
} else {
|
||||
// Handle case where there is no mixedId (e.g., new chat)
|
||||
setReady(true);
|
||||
}
|
||||
}, [mixedId]);
|
||||
}, [mixedId, db, navigate, searchParams]); // Added db, navigate, searchParams dependencies
|
||||
|
||||
const takeSnapshot = useCallback(
|
||||
async (chatIdx: string, files: FileMap, _chatId?: string | undefined, chatSummary?: string) => {
|
||||
const id = _chatId || chatId;
|
||||
const id = _chatId || chatId.get();
|
||||
|
||||
if (!id) {
|
||||
if (!id || !db) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -216,23 +227,29 @@ ${value.content}
|
||||
files,
|
||||
summary: chatSummary,
|
||||
};
|
||||
localStorage.setItem(`snapshot:${id}`, JSON.stringify(snapshot));
|
||||
|
||||
// localStorage.setItem(`snapshot:${id}`, JSON.stringify(snapshot)); // Remove localStorage usage
|
||||
try {
|
||||
await setSnapshot(db, id, snapshot);
|
||||
} catch (error) {
|
||||
console.error('Failed to save snapshot:', error);
|
||||
toast.error('Failed to save chat snapshot.');
|
||||
}
|
||||
},
|
||||
[chatId],
|
||||
[db],
|
||||
);
|
||||
|
||||
const restoreSnapshot = useCallback(async (id: string) => {
|
||||
const snapshotStr = localStorage.getItem(`snapshot:${id}`);
|
||||
const restoreSnapshot = useCallback(async (id: string, snapshot?: Snapshot) => {
|
||||
// const snapshotStr = localStorage.getItem(`snapshot:${id}`); // Remove localStorage usage
|
||||
const container = await webcontainer;
|
||||
|
||||
// if (snapshotStr)setSnapshot(JSON.parse(snapshotStr));
|
||||
const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} };
|
||||
const validSnapshot = snapshot || { chatIndex: '', files: {} };
|
||||
|
||||
if (!snapshot?.files) {
|
||||
if (!validSnapshot?.files) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(snapshot.files).forEach(async ([key, value]) => {
|
||||
Object.entries(validSnapshot.files).forEach(async ([key, value]) => {
|
||||
if (key.startsWith(container.workdir)) {
|
||||
key = key.replace(container.workdir, '');
|
||||
}
|
||||
@ -241,7 +258,7 @@ ${value.content}
|
||||
await container.fs.mkdir(key, { recursive: true });
|
||||
}
|
||||
});
|
||||
Object.entries(snapshot.files).forEach(async ([key, value]) => {
|
||||
Object.entries(validSnapshot.files).forEach(async ([key, value]) => {
|
||||
if (value?.type === 'file') {
|
||||
if (key.startsWith(container.workdir)) {
|
||||
key = key.replace(container.workdir, '');
|
||||
@ -311,6 +328,7 @@ ${value.content}
|
||||
description.set(firstArtifact?.title);
|
||||
}
|
||||
|
||||
// Ensure chatId.get() is used here as well
|
||||
if (initialMessages.length === 0 && !chatId.get()) {
|
||||
const nextId = await getNextId(db);
|
||||
|
||||
@ -321,9 +339,19 @@ ${value.content}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure chatId.get() is used for the final setMessages call
|
||||
const finalChatId = chatId.get();
|
||||
|
||||
if (!finalChatId) {
|
||||
console.error('Cannot save messages, chat ID is not set.');
|
||||
toast.error('Failed to save chat messages: Chat ID missing.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await setMessages(
|
||||
db,
|
||||
chatId.get() as string,
|
||||
finalChatId, // Use the potentially updated chatId
|
||||
[...archivedMessages, ...messages],
|
||||
urlId,
|
||||
description.get(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user