refactor: repartition server-side and client-side code

This commit is contained in:
LIlGG
2025-10-11 18:26:07 +08:00
parent 7acc4949fb
commit e9b573a276
309 changed files with 631 additions and 962 deletions

View File

@@ -0,0 +1,514 @@
import { useSearchParams } from '@remix-run/react';
import { getChatId } from '~/.client/stores/ai-state';
import type { Page, Section } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger';
/**
* 序列化标记常量
*/
export const SERIALIZATION_MARKERS = {
FUNCTION_PREFIX: '__FUNCTION__:',
ABORT_SIGNAL_PREFIX: '__ABORT_SIGNAL__',
ABORT_CONTROLLER_PREFIX: '__ABORT_CONTROLLER__',
};
/**
* 将对象序列化为可存储在 IndexedDB 中的格式
* 将函数转换为特殊格式的字符串
* @param data 需要序列化的数据
* @returns 序列化后的数据(可存储在 IndexedDB 中)
*/
function serializeForIndexedDB<T>(data: T): any {
if (data === null || data === undefined) {
return data;
}
if (typeof data === 'function') {
const funcStr = data.toString();
if (funcStr.includes('abortController.abort()')) {
return `${SERIALIZATION_MARKERS.FUNCTION_PREFIX}abort`;
}
return `${SERIALIZATION_MARKERS.FUNCTION_PREFIX}${funcStr}`;
}
if (data && typeof data === 'object' && 'aborted' in data && 'onabort' in data) {
return SERIALIZATION_MARKERS.ABORT_SIGNAL_PREFIX;
}
if (data && typeof data === 'object' && 'signal' in data && 'abort' in data) {
return SERIALIZATION_MARKERS.ABORT_CONTROLLER_PREFIX;
}
if (data instanceof Date) {
return {
__type: 'Date',
value: data.toISOString(),
};
}
if (Array.isArray(data)) {
return data.map((item) => serializeForIndexedDB(item));
}
if (typeof data === 'object') {
const serializedObject: Record<string, any> = {};
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
serializedObject[key] = serializeForIndexedDB((data as Record<string, any>)[key]);
}
}
return serializedObject;
}
return data;
}
/**
* 将从 IndexedDB 中读取的数据反序列化
* 将特殊格式的字符串转换回函数
* @param data 需要反序列化的数据
* @returns 反序列化后的数据
*/
function deserializeFromIndexedDB<T>(data: any): T {
if (data === null || data === undefined) {
return data as T;
}
if (typeof data === 'string') {
if (data.startsWith(SERIALIZATION_MARKERS.FUNCTION_PREFIX)) {
const funcBody = data.substring(SERIALIZATION_MARKERS.FUNCTION_PREFIX.length);
if (funcBody === 'abort') {
const abortController = new AbortController();
return function () {
abortController.abort();
} as unknown as T;
}
try {
return new Function(`return ${funcBody}`)() as T;
} catch (error) {
console.error('Failed to deserialize function:', error);
return (() => {
// ignore error
return undefined;
}) as unknown as T;
}
}
if (data === SERIALIZATION_MARKERS.ABORT_SIGNAL_PREFIX) {
return new AbortController().signal as unknown as T;
}
if (data === SERIALIZATION_MARKERS.ABORT_CONTROLLER_PREFIX) {
return new AbortController() as unknown as T;
}
}
if (data && typeof data === 'object') {
if (data.__type === 'Date' && data.value) {
return new Date(data.value) as unknown as T;
}
if (Array.isArray(data)) {
return data.map((item) => deserializeFromIndexedDB(item)) as unknown as T;
}
const deserializedObject: Record<string, any> = {};
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
deserializedObject[key] = deserializeFromIndexedDB(data[key]);
}
}
return deserializedObject as T;
}
return data as T;
}
export interface IEditorMessageProject {
messageId: string;
pages: Page[];
sections: Section[];
}
export interface IProject {
id: string;
messageProjects: IEditorMessageProject[];
timestamp: string;
}
const logger = createScopedLogger('EditorProjects');
/**
* 打开 editor 本地数据库。
* @returns editor 本地数据库。
*/
export async function openEditorDatabase(): Promise<IDBDatabase | undefined> {
if (typeof indexedDB === 'undefined') {
logger.debug('indexedDB 在当前环境中不可用');
return undefined;
}
return new Promise((resolve) => {
const request = indexedDB.open('editorProjects', 1);
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
if (!db.objectStoreNames.contains('projects')) {
const store = db.createObjectStore('projects', { keyPath: 'id' });
store.createIndex('id', 'id', { unique: true });
}
}
};
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 saveProject(
db: IDBDatabase,
messageId: string,
pages: Page[],
sections: Section[],
): Promise<void> {
return new Promise((resolve, reject) => {
// 序列化数据,处理不可序列化的内容
const serializedPages = serializeForIndexedDB(pages);
const serializedSections = serializeForIndexedDB(sections);
const transaction = db.transaction('projects', 'readwrite');
const store = transaction.objectStore('projects');
// 首先尝试获取现有记录
const getRequest = store.get(getChatId()!);
getRequest.onsuccess = () => {
const existingData = getRequest.result as IProject | undefined;
const timestamp = new Date().toISOString();
if (existingData) {
/*
* 如果记录存在
* 检查是否已存在相同 messageId 的项目
*/
const existingIndex = existingData.messageProjects.findIndex((p) => p.messageId === messageId);
let messageProjects;
if (existingIndex !== -1) {
// 如果找到了相同 messageId 的项目,则更新它
messageProjects = existingData.messageProjects.map((p, index) =>
index === existingIndex ? { ...p, pages: serializedPages, sections: serializedSections } : p,
);
} else {
// 如果没有找到相同 messageId 的项目,则添加新项目
messageProjects = [
...existingData.messageProjects,
{ messageId, pages: serializedPages, sections: serializedSections },
];
}
const updatedData = {
...existingData,
messageProjects,
timestamp,
};
const putRequest = store.put(updatedData);
putRequest.onsuccess = () => resolve();
putRequest.onerror = () => reject(putRequest.error);
} else {
// 创建新记录
const newData: IProject = {
id: getChatId()!,
messageProjects: [{ messageId, pages: serializedPages, sections: serializedSections }],
timestamp,
};
const putRequest = store.put(newData);
putRequest.onsuccess = () => resolve();
putRequest.onerror = () => reject(putRequest.error);
}
};
getRequest.onerror = () => reject(getRequest.error);
});
}
export async function setEditorProjects(db: IDBDatabase, id: string, projects: IEditorMessageProject[]): Promise<void> {
return new Promise((resolve, reject) => {
// 序列化项目数据,处理不可序列化的内容
const serializedProjects = serializeForIndexedDB(projects);
const transaction = db.transaction('projects', 'readwrite');
const store = transaction.objectStore('projects');
const request = store.put({
id,
messageProjects: serializedProjects,
timestamp: new Date().toISOString(),
});
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
export async function getEditorProjects(db: IDBDatabase, chatId: string): Promise<IProject> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('projects', 'readonly');
const store = transaction.objectStore('projects');
const request = store.get(chatId);
request.onsuccess = () => {
const project = request.result as IProject;
if (project && project.messageProjects) {
const deserializedProject = {
...project,
messageProjects: project.messageProjects.map((mp) => ({
...mp,
pages: mp.pages ? deserializeFromIndexedDB<Page[]>(mp.pages) : [],
sections: mp.sections ? deserializeFromIndexedDB<Section[]>(mp.sections) : [],
})),
};
resolve(deserializedProject);
} else {
resolve(project);
}
};
request.onerror = () => reject(request.error);
});
}
// 获取项目数据
export async function getEditorProject(
db: IDBDatabase,
messageId?: string,
): Promise<{ pages: Page[]; sections: Section[] | undefined; project?: IProject } | undefined> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('projects', 'readonly');
const store = transaction.objectStore('projects');
const request = store.get(getChatId()!);
request.onsuccess = () => {
const project = request.result as IProject;
if (!project) {
resolve(undefined);
return;
}
if (messageId) {
// 返回特定消息 ID 的项目数据
const data = project.messageProjects?.find((p) => p.messageId === messageId);
const deserializedPages = data?.pages ? deserializeFromIndexedDB<Page[]>(data.pages) : [];
const deserializedSections = data?.sections ? deserializeFromIndexedDB<Section[]>(data.sections) : undefined;
resolve({
pages: deserializedPages,
sections: deserializedSections,
project,
});
} else {
// 没有指定消息 ID返回最新的项目数据
const messageIds = project.messageProjects.map((p) => p.messageId);
if (messageIds.length === 0) {
resolve({ pages: [], sections: undefined, project });
} else {
// 按时间戳排序(如果有时间戳),或者取最后一个
const lastMessageId = messageIds[messageIds.length - 1];
const lastMessageProject = project.messageProjects.find((p) => p.messageId === lastMessageId);
const deserializedPages = lastMessageProject?.pages
? deserializeFromIndexedDB<Page[]>(lastMessageProject.pages)
: [];
const deserializedSections = lastMessageProject?.sections
? deserializeFromIndexedDB<Section[]>(lastMessageProject.sections)
: undefined;
resolve({
pages: deserializedPages,
sections: deserializedSections,
project,
});
}
}
};
request.onerror = () => reject(request.error);
});
}
// 删除项目数据
export async function deleteEditorProject(db: IDBDatabase, chatId: string, messageId?: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('projects', 'readwrite');
const store = transaction.objectStore('projects');
if (messageId) {
// 只删除特定消息 ID 的项目数据
const getRequest = store.get(chatId);
getRequest.onsuccess = () => {
const project = getRequest.result as IProject;
if (project && project.messageProjects && project.messageProjects.find((p) => p.messageId === messageId)) {
// 删除特定消息的项目数据
project.messageProjects = project.messageProjects.filter((p) => p.messageId !== messageId);
if (project.messageProjects.length === 0) {
// 如果没有剩余项目,删除整个记录
const deleteRequest = store.delete(chatId);
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = () => reject(deleteRequest.error);
} else {
// 更新记录
project.timestamp = new Date().toISOString();
const putRequest = store.put(project);
putRequest.onsuccess = () => resolve();
putRequest.onerror = () => reject(putRequest.error);
}
} else {
resolve(); // 项目不存在或消息 ID 不存在,视为删除成功
}
};
getRequest.onerror = () => reject(getRequest.error);
} else {
// 删除整个聊天 ID 的所有项目数据
const request = store.delete(chatId);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
}
});
}
// 获取所有项目数据
export async function getAllEditorProjects(db: IDBDatabase): Promise<IProject[]> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('projects', 'readonly');
const store = transaction.objectStore('projects');
const request = store.getAll();
request.onsuccess = () => {
const projects = request.result as IProject[];
resolve(projects);
};
request.onerror = () => reject(request.error);
});
}
export async function createEditorProjectFromMessages(
db: IDBDatabase,
chatId: string,
messageProjects: IEditorMessageProject[],
): Promise<void> {
await setEditorProjects(db, chatId, messageProjects);
}
export async function forkEditorProject(
db: IDBDatabase,
chatId: string,
messageId: string,
newChatId: string,
): Promise<void> {
const project = await getEditorProjects(db, chatId);
if (!project) {
console.warn('editor project not found, It may be old project data.');
return;
}
const messageIndex = project.messageProjects.findIndex((msg) => msg.messageId === messageId);
if (messageIndex === -1) {
throw new Error('Message not found');
}
const messages = project.messageProjects.slice(0, messageIndex + 1);
await createEditorProjectFromMessages(db, newChatId, messages);
}
export async function duplicateEditorProject(db: IDBDatabase, id: string, newChatId: string): Promise<void> {
const project = await getEditorProjects(db, id);
if (!project) {
console.warn('editor project not found, It may be old project data.');
return;
}
createEditorProjectFromMessages(db, newChatId, project.messageProjects);
}
// 在 GrapesEditor 中使用的Hook
export function useEditorStorage() {
const [searchParams] = useSearchParams();
const currentMessageId = searchParams.get('rewindTo');
// 保存项目至本地数据库
const saveEditorProject = async (messageId: string | undefined, pages: Page[], sections: Section[]) => {
const db = await openEditorDatabase();
if (!db || !messageId) {
return false;
}
try {
await saveProject(db, messageId, pages, sections);
return true;
} catch (error) {
logger.error('保存 editor 项目失败', error);
return false;
} finally {
db.close();
}
};
/**
* 加载 editor 项目。
* @returns editor 项目数据。
*/
const loadEditorProject = async (): Promise<Page[] | undefined> => {
const db = await openEditorDatabase();
if (!db) {
return undefined;
}
const messageId = currentMessageId || undefined;
try {
const result = await getEditorProject(db, messageId);
return result?.pages;
} catch (error) {
logger.error('加载 editor 项目失败', error);
return undefined;
} finally {
db.close();
}
};
return {
saveEditorProject,
loadEditorProject,
};
}

View File

@@ -0,0 +1,2 @@
export * from '../hooks/useChatHistory';
export * from './local-storage';

View File

@@ -0,0 +1,28 @@
// Client-side storage utilities
const isClient = typeof window !== 'undefined' && typeof localStorage !== 'undefined';
export function getLocalStorage(key: string): any | null {
if (!isClient) {
return null;
}
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error(`Error reading from localStorage key "${key}":`, error);
return null;
}
}
export function setLocalStorage(key: string, value: any): void {
if (!isClient) {
return;
}
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error writing to localStorage key "${key}":`, error);
}
}