import { ChatSession, useAccessStore, useAppConfig, useChatStore, } from "../store"; import { useMaskStore } from "../store/mask"; import { usePromptStore } from "../store/prompt"; import { StoreKey } from "../constant"; import { merge } from "./merge"; type NonFunctionKeys = { [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K; }[keyof T]; type NonFunctionFields = Pick>; export function getNonFunctionFileds(obj: T) { const ret: any = {}; Object.entries(obj).map(([k, v]) => { if (typeof v !== "function") { ret[k] = v; } }); return ret as NonFunctionFields; } export type GetStoreState = T extends { getState: () => infer U } ? NonFunctionFields : never; const LocalStateSetters = { [StoreKey.Chat]: useChatStore.setState, [StoreKey.Access]: useAccessStore.setState, [StoreKey.Config]: useAppConfig.setState, [StoreKey.Mask]: useMaskStore.setState, [StoreKey.Prompt]: usePromptStore.setState, } as const; const LocalStateGetters = { [StoreKey.Chat]: () => getNonFunctionFileds(useChatStore.getState()), [StoreKey.Access]: () => getNonFunctionFileds(useAccessStore.getState()), [StoreKey.Config]: () => getNonFunctionFileds(useAppConfig.getState()), [StoreKey.Mask]: () => getNonFunctionFileds(useMaskStore.getState()), [StoreKey.Prompt]: () => getNonFunctionFileds(usePromptStore.getState()), } as const; export type AppState = { [k in keyof typeof LocalStateGetters]: ReturnType< (typeof LocalStateGetters)[k] >; }; type Merger = ( localState: U, remoteState: U, ) => U; type StateMerger = { [K in keyof AppState]: Merger; }; // we merge remote state to local state const MergeStates: StateMerger = { [StoreKey.Chat]: (localState, remoteState) => { // merge sessions const localSessions: Record = {}; localState.sessions.forEach((s) => (localSessions[s.id] = s)); remoteState.sessions.forEach((remoteSession) => { // skip empty chats if (remoteSession.messages.length === 0) return; const localSession = localSessions[remoteSession.id]; if (!localSession) { // if remote session is new, just merge it localState.sessions.push(remoteSession); } else { // if both have the same session id, merge the messages const localMessageIds = new Set(localSession.messages.map((v) => v.id)); remoteSession.messages.forEach((m) => { if (!localMessageIds.has(m.id)) { localSession.messages.push(m); } }); // sort local messages with date field in asc order localSession.messages.sort( (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), ); } }); // sort local sessions with date field in desc order localState.sessions.sort( (a, b) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(), ); return localState; }, [StoreKey.Prompt]: (localState, remoteState) => { localState.prompts = { ...remoteState.prompts, ...localState.prompts, }; return localState; }, [StoreKey.Mask]: (localState, remoteState) => { localState.masks = { ...remoteState.masks, ...localState.masks, }; return localState; }, [StoreKey.Config]: mergeWithUpdate, [StoreKey.Access]: mergeWithUpdate, }; export function getLocalAppState() { const appState = Object.fromEntries( Object.entries(LocalStateGetters).map(([key, getter]) => { return [key, getter()]; }), ) as AppState; return appState; } export function setLocalAppState(appState: AppState) { Object.entries(LocalStateSetters).forEach(([key, setter]) => { setter(appState[key as keyof AppState]); }); } export function mergeAppState(localState: AppState, remoteState: AppState) { Object.keys(localState).forEach((k: string) => { const key = k as T; const localStoreState = localState[key]; const remoteStoreState = remoteState[key]; MergeStates[key](localStoreState, remoteStoreState); }); return localState; } /** * Merge state with `lastUpdateTime`, older state will be override */ export function mergeWithUpdate( localState: T, remoteState: T, ) { const localUpdateTime = localState.lastUpdateTime ?? 0; const remoteUpdateTime = localState.lastUpdateTime ?? 1; if (localUpdateTime < remoteUpdateTime) { merge(remoteState, localState); return { ...remoteState }; } else { merge(localState, remoteState); return { ...localState }; } }