Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 1 addition & 165 deletions app/utils/sync.ts
Original file line number Diff line number Diff line change
@@ -1,165 +1 @@
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<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
}[keyof T];
type NonFunctionFields<T> = Pick<T, NonFunctionKeys<T>>;

export function getNonFunctionFileds<T extends object>(obj: T) {
const ret: any = {};

Object.entries(obj).map(([k, v]) => {
if (typeof v !== "function") {
ret[k] = v;
}
});

return ret as NonFunctionFields<T>;
}

export type GetStoreState<T> = T extends { getState: () => infer U }
? NonFunctionFields<U>
: 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<T extends keyof AppState, U = AppState[T]> = (
localState: U,
remoteState: U,
) => U;

type StateMerger = {
[K in keyof AppState]: Merger<K>;
};

// we merge remote state to local state
const MergeStates: StateMerger = {
[StoreKey.Chat]: (localState, remoteState) => {
// merge sessions
const localSessions: Record<string, ChatSession> = {};
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<AppState[StoreKey.Config]>,
[StoreKey.Access]: mergeWithUpdate<AppState[StoreKey.Access]>,
};

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(<T extends keyof AppState>(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<T extends { lastUpdateTime?: number }>(
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 };
}
}
aW1wb3J0IHsKICBDaGF0U2Vzc2lvbiwKICB1c2VBY2Nlc3NTdG9yZSwKICB1c2VBcHBDb25maWcsCiAgdXNlQ2hhdFN0b3JlLAp9IGZyb20gIi4uL3N0b3JlIjsKaW1wb3J0IHsgdXNlTWFza1N0b3JlIH0gZnJvbSAiLi4vc3RvcmUvbWFzayI7CmltcG9ydCB7IHVzZVByb21wdFN0b3JlIH0gZnJvbSAiLi4vc3RvcmUvcHJvbXB0IjsKaW1wb3J0IHsgU3RvcmVLZXkgfSBmcm9tICIuLi9jb25zdGFudCI7CmltcG9ydCB7IG1lcmdlIH0gZnJvbSAiLi9tZXJnZSI7Cgp0eXBlIE5vbkZ1bmN0aW9uS2V5czxUPiA9IHsKICBbSyBpbiBrZXlvZiBUXTogVFtLXSBleHRlbmRzICguLi5hcmdzOiBhbnlbXSkgPT4gYW55ID8gbmV2ZXIgOiBLOwp9W2tleW9mIFRdOwp0eXBlIE5vbkZ1bmN0aW9uRmllbGRzPFQ+ID0gUGljazxULCBOb25GdW5jdGlvbktleXM8VD4+OwoKZXhwb3J0IGZ1bmN0aW9uIGdldE5vbkZ1bmN0aW9uRmlsZWRzPFQgZXh0ZW5kcyBvYmplY3Q+KG9iajogVCkgewogIGNvbnN0IHJldDogYW55ID0ge307CgogIE9iamVjdC5lbnRyaWVzKG9iaikubWFwKChbaywgdl0pID0+IHsKICAgIGlmICh0eXBlb2YgdiAhPT0gImZ1bmN0aW9uIikgewogICAgICByZXRba10gPSB2OwogICAgfQogIH0pOwoKICByZXR1cm4gcmV0IGFzIE5vbkZ1bmN0aW9uRmllbGRzPFQ+Owp9CgpleHBvcnQgdHlwZSBHZXRTdG9yZVN0YXRlPFQ+ID0gVCBleHRlbmRzIHsgZ2V0U3RhdGU6ICgpID0+IGluZmVyIFUgfQogID8gTm9uRnVuY3Rpb25GaWVsZHM8VT4KICA6IG5ldmVyOwoKY29uc3QgTG9jYWxTdGF0ZVNldHRlcnMgPSB7CiAgW1N0b3JlS2V5LkNoYXRdOiB1c2VDaGF0U3RvcmUuc2V0U3RhdGUsCiAgW1N0b3JlS2V5LkFjY2Vzc106IHVzZUFjY2Vzc1N0b3JlLnNldFN0YXRlLAogIFtTdG9yZUtleS5Db25maWddOiB1c2VBcHBDb25maWcuc2V0U3RhdGUsCiAgW1N0b3JlS2V5Lk1hc2tdOiB1c2VNYXNrU3RvcmUuc2V0U3RhdGUsCiAgW1N0b3JlS2V5LlByb21wdF06IHVzZVByb21wdFN0b3JlLnNldFN0YXRlLAp9IGFzIGNvbnN0OwoKY29uc3QgTG9jYWxTdGF0ZUdldHRlcnMgPSB7CiAgW1N0b3JlS2V5LkNoYXRdOiAoKSA9PiBnZXROb25GdW5jdGlvbkZpbGVkcyh1c2VDaGF0U3RvcmUuZ2V0U3RhdGUoKSksCiAgW1N0b3JlS2V5LkFjY2Vzc106ICgpID0+IGdldE5vbkZ1bmN0aW9uRmlsZWRzKHVzZUFjY2Vzc1N0b3JlLmdldFN0YXRlKCkpLAogIFtTdG9yZUtleS5Db25maWddOiAoKSA9PiBnZXROb25GdW5jdGlvbkZpbGVkcyh1c2VBcHBDb25maWcuZ2V0U3RhdGUoKSksCiAgW1N0b3JlS2V5Lk1hc2tdOiAoKSA9PiBnZXROb25GdW5jdGlvbkZpbGVkcyh1c2VNYXNrU3RvcmUuZ2V0U3RhdGUoKSksCiAgW1N0b3JlS2V5LlByb21wdF06ICgpID0+IGdldE5vbkZ1bmN0aW9uRmlsZWRzKHVzZVByb21wdFN0b3JlLmdldFN0YXRlKCkpLAp9IGFzIGNvbnN0OwoKZXhwb3J0IHR5cGUgQXBwU3RhdGUgPSB7CiAgW2sgaW4ga2V5b2YgdHlwZW9mIExvY2FsU3RhdGVHZXR0ZXJzXTogUmV0dXJuVHlwZTwKICAgICh0eXBlb2YgTG9jYWxTdGF0ZUdldHRlcnMpW2tdCiAgPjsKfTsKCnR5cGUgTWVyZ2VyPFQgZXh0ZW5kcyBrZXlvZiBBcHBTdGF0ZSwgVSA9IEFwcFN0YXRlW1RdPiA9ICgKICBsb2NhbFN0YXRlOiBVLAogIHJlbW90ZVN0YXRlOiBVLAopID0+IFU7Cgp0eXBlIFN0YXRlTWVyZ2VyID0gewogIFtLIGluIGtleW9mIEFwcFN0YXRlXTogTWVyZ2VyPEs+Owp9OwoKLy8gd2UgbWVyZ2UgcmVtb3RlIHN0YXRlIHRvIGxvY2FsIHN0YXRlCmNvbnN0IE1lcmdlU3RhdGVzOiBTdGF0ZU1lcmdlciA9IHsKICBbU3RvcmVLZXkuQ2hhdF06IChsb2NhbFN0YXRlLCByZW1vdGVTdGF0ZSkgPT4gewogICAgLy8gbWVyZ2Ugc2Vzc2lvbnMKICAgIGNvbnN0IGxvY2FsU2Vzc2lvbnM6IFJlY29yZDxzdHJpbmcsIENoYXRTZXNzaW9uPiA9IHt9OwogICAgbG9jYWxTdGF0ZS5zZXNzaW9ucy5mb3JFYWNoKChzKSA9PiAobG9jYWxTZXNzaW9uc1tzLmlkXSA9IHMpKTsKCiAgICByZW1vdGVTdGF0ZS5zZXNzaW9ucy5mb3JFYWNoKChyZW1vdGVTZXNzaW9uKSA9PiB7CiAgICAgIC8vIHNraXAgZW1wdHkgY2hhdHMKICAgICAgaWYgKHJlbW90ZVNlc3Npb24ubWVzc2FnZXMubGVuZ3RoID09PSAwKSByZXR1cm47CgogICAgICBjb25zdCBsb2NhbFNlc3Npb24gPSBsb2NhbFNlc3Npb25zW3JlbW90ZVNlc3Npb24uaWRdOwogICAgICBpZiAoIWxvY2FsU2Vzc2lvbikgewogICAgICAgIC8vIGlmIHJlbW90ZSBzZXNzaW9uIGlzIG5ldywganVzdCBtZXJnZSBpdAogICAgICAgIGxvY2FsU3RhdGUuc2Vzc2lvbnMucHVzaChyZW1vdGVTZXNzaW9uKTsKICAgICAgfSBlbHNlIHsKICAgICAgICAvLyBpZiBib3RoIGhhdmUgdGhlIHNhbWUgc2Vzc2lvbiBpZCwgbWVyZ2UgdGhlIG1lc3NhZ2VzCiAgICAgICAgY29uc3QgbG9jYWxNZXNzYWdlSWRzID0gbmV3IFNldChsb2NhbFNlc3Npb24ubWVzc2FnZXMubWFwKCh2KSA9PiB2LmlkKSk7CiAgICAgICAgcmVtb3RlU2Vzc2lvbi5tZXNzYWdlcy5mb3JFYWNoKChtKSA9PiB7CiAgICAgICAgICBpZiAoIWxvY2FsTWVzc2FnZUlkcy5oYXMobS5pZCkpIHsKICAgICAgICAgICAgbG9jYWxTZXNzaW9uLm1lc3NhZ2VzLnB1c2gobSk7CiAgICAgICAgICB9CiAgICAgICAgfSk7CgogICAgICAgIC8vIHNvcnQgbG9jYWwgbWVzc2FnZXMgd2l0aCBkYXRlIGZpZWxkIGluIGFzYyBvcmRlcgogICAgICAgIGxvY2FsU2Vzc2lvbi5tZXNzYWdlcy5zb3J0KAogICAgICAgICAgKGEsIGIpID0+IG5ldyBEYXRlKGEuZGF0ZSkuZ2V0VGltZSgpIC0gbmV3IERhdGUoYi5kYXRlKS5nZXRUaW1lKCksCiAgICAgICAgKTsKICAgICAgfQogICAgfSk7CgogICAgLy8gc29ydCBsb2NhbCBzZXNzaW9ucyB3aXRoIGRhdGUgZmllbGQgaW4gZGVzYyBvcmRlcgogICAgbG9jYWxTdGF0ZS5zZXNzaW9ucy5zb3J0KAogICAgICAoYSwgYikgPT4KICAgICAgICBuZXcgRGF0ZShiLmxhc3RVcGRhdGUpLmdldFRpbWUoKSAtIG5ldyBEYXRlKGEubGFzdFVwZGF0ZSkuZ2V0VGltZSgpLAogICAgKTsKCiAgICByZXR1cm4gbG9jYWxTdGF0ZTsKICB9LAogIFtTdG9yZUtleS5Qcm9tcHRdOiAobG9jYWxTdGF0ZSwgcmVtb3RlU3RhdGUpID0+IHsKICAgIGxvY2FsU3RhdGUucHJvbXB0cyA9IHsKICAgICAgLi4ucmVtb3RlU3RhdGUucHJvbXB0cywKICAgICAgLi4ubG9jYWxTdGF0ZS5wcm9tcHRzLAogICAgfTsKICAgIHJldHVybiBsb2NhbFN0YXRlOwogIH0sCiAgW1N0b3JlS2V5Lk1hc2tdOiAobG9jYWxTdGF0ZSwgcmVtb3RlU3RhdGUpID0+IHsKICAgIGxvY2FsU3RhdGUubWFza3MgPSB7CiAgICAgIC4uLnJlbW90ZVN0YXRlLm1hc2tzLAogICAgICAuLi5sb2NhbFN0YXRlLm1hc2tzLAogICAgfTsKICAgIHJldHVybiBsb2NhbFN0YXRlOwogIH0sCiAgW1N0b3JlS2V5LkNvbmZpZ106IG1lcmdlV2l0aFVwZGF0ZTxBcHBTdGF0ZVtTdG9yZUtleS5Db25maWddPiwKICBbU3RvcmVLZXkuQWNjZXNzXTogbWVyZ2VXaXRoVXBkYXRlPEFwcFN0YXRlW1N0b3JlS2V5LkFjY2Vzc10+LAp9OwoKZXhwb3J0IGZ1bmN0aW9uIGdldExvY2FsQXBwU3RhdGUoKSB7CiAgY29uc3QgYXBwU3RhdGUgPSBPYmplY3QuZnJvbUVudHJpZXMoCiAgICBPYmplY3QuZW50cmllcyhMb2NhbFN0YXRlR2V0dGVycykubWFwKChba2V5LCBnZXR0ZXJdKSA9PiB7CiAgICAgIHJldHVybiBba2V5LCBnZXR0ZXIoKV07CiAgICB9KSwKICApIGFzIEFwcFN0YXRlOwoKICByZXR1cm4gYXBwU3RhdGU7Cn0KCmV4cG9ydCBmdW5jdGlvbiBzZXRMb2NhbEFwcFN0YXRlKGFwcFN0YXRlOiBBcHBTdGF0ZSkgewogIE9iamVjdC5lbnRyaWVzKExvY2FsU3RhdGVTZXR0ZXJzKS5mb3JFYWNoKChba2V5LCBzZXR0ZXJdKSA9PiB7CiAgICBzZXR0ZXIoYXBwU3RhdGVba2V5IGFzIGtleW9mIEFwcFN0YXRlXSk7CiAgfSk7Cn0KCmV4cG9ydCBmdW5jdGlvbiBtZXJnZUFwcFN0YXRlKGxvY2FsU3RhdGU6IEFwcFN0YXRlLCByZW1vdGVTdGF0ZTogQXBwU3RhdGUpIHsKICBPYmplY3Qua2V5cyhsb2NhbFN0YXRlKS5mb3JFYWNoKDxUIGV4dGVuZHMga2V5b2YgQXBwU3RhdGU+KGs6IHN0cmluZykgPT4gewogICAgY29uc3Qga2V5ID0gayBhcyBUOwogICAgY29uc3QgbG9jYWxTdG9yZVN0YXRlID0gbG9jYWxTdGF0ZVtrZXldOwogICAgY29uc3QgcmVtb3RlU3RvcmVTdGF0ZSA9IHJlbW90ZVN0YXRlW2tleV07CiAgICBNZXJnZVN0YXRlc1trZXldKGxvY2FsU3RvcmVTdGF0ZSwgcmVtb3RlU3RvcmVTdGF0ZSk7CiAgfSk7CgogIHJldHVybiBsb2NhbFN0YXRlOwp9CgovKioKICogTWVyZ2Ugc3RhdGUgd2l0aCBgbGFzdFVwZGF0ZVRpbWVgLCBuZXdlciBzdGF0ZSB3aWxsIG92ZXJyaWRlCiAqCiAqIOWQiOW5tuaWueWQke+8mui+g+aWsOiAhSBpbi1wbGFjZSBpbi1wbGFjZSDopobnm5YgbG9jYWxTdGF0Ze+8jOWboOS4uuWQjue7rSBzZXRMb2NhbEFwcFN0YXRlIOS4jgogKiBjbGllbnQuc2V0IOmDveS7pSBsb2NhbFN0YXRlIOS4uuWHhuS4iuS8oOS6keerr+OAggogKiAtIHJlbW90ZSDmm7TmlrDvvJpyZW1vdGUg6KaG55uWIGxvY2Fs77yI5YaZ5YWlIGxvY2FsU3RhdGXvvInvvIzmnKzlnLDmi7/liLDov5znq6/mlrDlgLzlubbkuIrkvKAKICogLSBsb2NhbCDmm7TmlrDmiJbnm7jnrYnvvJrkv53nlZkgbG9jYWzvvIzkuI3orqnov5znq6/ml6flgLzopobnm5bvvIzmnKzlnLDmlrDlgLzmraPluLjkuIrkvKAKICovCmV4cG9ydCBmdW5jdGlvbiBtZXJnZVdpdGhVcGRhdGU8VCBleHRlbmRzIHsgbGFzdFVwZGF0ZVRpbWU/OiBudW1iZXIgfT4oCiAgbG9jYWxTdGF0ZTogVCwKICByZW1vdGVTdGF0ZTogVCwKKSB7CiAgY29uc3QgbG9jYWxVcGRhdGVUaW1lID0gbG9jYWxTdGF0ZS5sYXN0VXBkYXRlVGltZSA/PyAwOwogIGNvbnN0IHJlbW90ZVVwZGF0ZVRpbWUgPSByZW1vdGVTdGF0ZS5sYXN0VXBkYXRlVGltZSA/PyAwOwoKICBpZiAobG9jYWxVcGRhdGVUaW1lIDwgcmVtb3RlVXBkYXRlVGltZSkgewogICAgLy8gcmVtb3RlIOi+g+aWsO+8muiuqSByZW1vdGUg6KaG55uWIGxvY2Fs77yIaW4tcGxhY2Ug5L+u5pS5IGxvY2FsU3RhdGXvvIkKICAgIG1lcmdlKGxvY2FsU3RhdGUsIHJlbW90ZVN0YXRlKTsKICB9CiAgLy8gbG9jYWwg6L6D5paw5oiW55u4562J77ya5L+d55WZIGxvY2Fs77yM5LiN5ZCI5bm2IHJlbW90Ze+8jOmBv+WFjei/nOerr+aXp+WAvOimhuebluacrOWcsOaWsOWAvAogIHJldHVybiB7IC4uLmxvY2FsU3RhdGUgfTsKfQo=
Loading