mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
refactor: replace JSON deep copy with deepClone helper and extract useTauriEvent hook (#3140)
* refactor: replace JSON.parse(JSON.stringify()) with structuredClone and extract useTauriEvent hook Replace all `JSON.parse(JSON.stringify())` deep copy patterns with native `structuredClone()` across production source (9 occurrences), tests (11 occurrences), and a hand-rolled `deepClone` utility in providerConfigUtils.ts. Add "ES2022" to tsconfig lib for type support. Extract a `useTauriEvent` hook to eliminate the repeated Tauri event listener boilerplate (`useEffect` + `active/disposed` flag + async `listen`) that was duplicated across App.tsx (3 listeners) and useUsageCacheBridge.ts. The hook handles async registration, race-condition guards, and cleanup automatically. * fix: add compatible deepClone helper - Add a shared deepClone helper with a structuredClone runtime guard and fallback. - Route clone call sites through the helper. - Preserve universal-provider-synced listener ordering and drop the dead-directory diff. * fix: harden Tauri event handling - Guard WebDAV sync status events against missing payloads. - Preserve settings query invalidation ordering before showing auto-sync errors. - Simplify useTauriEvent subscriptions to avoid dependency-driven re-listens. --------- Co-authored-by: zcb <zhangchongbiao@qiyuanlab.com> Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -230,6 +230,11 @@ describe("App integration with MSW", () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
emitTauriEvent("webdav-sync-status-updated", null);
|
||||
}).not.toThrow();
|
||||
expect(toastErrorMock).not.toHaveBeenCalled();
|
||||
|
||||
emitTauriEvent("webdav-sync-status-updated", {
|
||||
source: "auto",
|
||||
status: "error",
|
||||
|
||||
+23
-31
@@ -6,11 +6,15 @@ import type {
|
||||
SessionMeta,
|
||||
Settings,
|
||||
} from "@/types";
|
||||
import { deepClone } from "@/utils/deepClone";
|
||||
|
||||
type ProvidersByApp = Record<AppId, Record<string, Provider>>;
|
||||
type CurrentProviderState = Record<AppId, string>;
|
||||
type McpConfigState = Record<AppId, Record<string, McpServer>>;
|
||||
type LiveProviderIdsByApp = Record<"opencode" | "openclaw" | "hermes", string[]>;
|
||||
type LiveProviderIdsByApp = Record<
|
||||
"opencode" | "openclaw" | "hermes",
|
||||
string[]
|
||||
>;
|
||||
|
||||
const createDefaultProviders = (): ProvidersByApp => ({
|
||||
claude: {
|
||||
@@ -193,7 +197,7 @@ let mcpConfigs: McpConfigState = {
|
||||
};
|
||||
|
||||
const cloneProviders = (value: ProvidersByApp) =>
|
||||
JSON.parse(JSON.stringify(value)) as ProvidersByApp;
|
||||
deepClone(value) as ProvidersByApp;
|
||||
|
||||
export const resetProviderState = () => {
|
||||
providers = createDefaultProviders();
|
||||
@@ -266,9 +270,9 @@ export const getProviders = (appType: AppId) =>
|
||||
|
||||
export const getCurrentProviderId = (appType: AppId) => current[appType] ?? "";
|
||||
|
||||
export const getLiveProviderIds = (appType: "opencode" | "openclaw" | "hermes") => [
|
||||
...liveProviderIds[appType],
|
||||
];
|
||||
export const getLiveProviderIds = (
|
||||
appType: "opencode" | "openclaw" | "hermes",
|
||||
) => [...liveProviderIds[appType]];
|
||||
|
||||
export const setLiveProviderIds = (
|
||||
appType: "opencode" | "openclaw" | "hermes",
|
||||
@@ -294,10 +298,7 @@ export const setProviders = (
|
||||
appType: AppId,
|
||||
data: Record<string, Provider>,
|
||||
) => {
|
||||
providers[appType] = JSON.parse(JSON.stringify(data)) as Record<
|
||||
string,
|
||||
Provider
|
||||
>;
|
||||
providers[appType] = deepClone(data) as Record<string, Provider>;
|
||||
};
|
||||
|
||||
export const addProvider = (appType: AppId, provider: Provider) => {
|
||||
@@ -336,13 +337,9 @@ export const updateSortOrder = (
|
||||
};
|
||||
|
||||
export const listProviders = (appType: AppId) =>
|
||||
JSON.parse(JSON.stringify(providers[appType] ?? {})) as Record<
|
||||
string,
|
||||
Provider
|
||||
>;
|
||||
deepClone(providers[appType] ?? {}) as Record<string, Provider>;
|
||||
|
||||
export const getSettings = () =>
|
||||
JSON.parse(JSON.stringify(settingsState)) as Settings;
|
||||
export const getSettings = () => deepClone(settingsState) as Settings;
|
||||
|
||||
export const setSettings = (data: Partial<Settings>) => {
|
||||
settingsState = { ...settingsState, ...data };
|
||||
@@ -355,9 +352,10 @@ export const setAppConfigDirOverrideState = (value: string | null) => {
|
||||
};
|
||||
|
||||
export const getMcpConfig = (appType: AppId) => {
|
||||
const servers = JSON.parse(
|
||||
JSON.stringify(mcpConfigs[appType] ?? {}),
|
||||
) as Record<string, McpServer>;
|
||||
const servers = deepClone(mcpConfigs[appType] ?? {}) as Record<
|
||||
string,
|
||||
McpServer
|
||||
>;
|
||||
return {
|
||||
configPath: `/mock/${appType}.mcp.json`,
|
||||
servers,
|
||||
@@ -368,10 +366,7 @@ export const setMcpConfig = (
|
||||
appType: AppId,
|
||||
value: Record<string, McpServer>,
|
||||
) => {
|
||||
mcpConfigs[appType] = JSON.parse(JSON.stringify(value)) as Record<
|
||||
string,
|
||||
McpServer
|
||||
>;
|
||||
mcpConfigs[appType] = deepClone(value) as Record<string, McpServer>;
|
||||
};
|
||||
|
||||
export const setMcpServerEnabled = (
|
||||
@@ -394,7 +389,7 @@ export const upsertMcpServer = (
|
||||
if (!mcpConfigs[appType]) {
|
||||
mcpConfigs[appType] = {};
|
||||
}
|
||||
mcpConfigs[appType][id] = JSON.parse(JSON.stringify(server)) as McpServer;
|
||||
mcpConfigs[appType][id] = deepClone(server) as McpServer;
|
||||
};
|
||||
|
||||
export const deleteMcpServer = (appType: AppId, id: string) => {
|
||||
@@ -402,14 +397,11 @@ export const deleteMcpServer = (appType: AppId, id: string) => {
|
||||
delete mcpConfigs[appType][id];
|
||||
};
|
||||
|
||||
export const listSessions = () =>
|
||||
JSON.parse(JSON.stringify(sessionsState)) as SessionMeta[];
|
||||
export const listSessions = () => deepClone(sessionsState) as SessionMeta[];
|
||||
|
||||
export const getSessionMessages = (providerId: string, sourcePath: string) =>
|
||||
JSON.parse(
|
||||
JSON.stringify(
|
||||
sessionMessagesState[sessionMessageKey(providerId, sourcePath)] ?? [],
|
||||
),
|
||||
deepClone(
|
||||
sessionMessagesState[sessionMessageKey(providerId, sourcePath)] ?? [],
|
||||
) as SessionMessage[];
|
||||
|
||||
export const deleteSession = (
|
||||
@@ -433,8 +425,8 @@ export const setSessionFixtures = (
|
||||
sessions: SessionMeta[],
|
||||
messages: Record<string, SessionMessage[]>,
|
||||
) => {
|
||||
sessionsState = JSON.parse(JSON.stringify(sessions)) as SessionMeta[];
|
||||
sessionMessagesState = JSON.parse(JSON.stringify(messages)) as Record<
|
||||
sessionsState = deepClone(sessions) as SessionMeta[];
|
||||
sessionMessagesState = deepClone(messages) as Record<
|
||||
string,
|
||||
SessionMessage[]
|
||||
>;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { deepClone } from "@/utils/deepClone";
|
||||
|
||||
describe("deepClone", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("falls back when structuredClone is unavailable", () => {
|
||||
vi.stubGlobal("structuredClone", undefined);
|
||||
|
||||
const source = {
|
||||
nested: { value: "original" },
|
||||
list: [{ enabled: true }],
|
||||
createdAt: new Date("2026-01-30T00:00:00.000Z"),
|
||||
};
|
||||
|
||||
const cloned = deepClone(source);
|
||||
cloned.nested.value = "changed";
|
||||
cloned.list[0].enabled = false;
|
||||
|
||||
expect(cloned).not.toBe(source);
|
||||
expect(cloned.nested).not.toBe(source.nested);
|
||||
expect(cloned.list).not.toBe(source.list);
|
||||
expect(cloned.createdAt).not.toBe(source.createdAt);
|
||||
expect(cloned.createdAt.getTime()).toBe(source.createdAt.getTime());
|
||||
expect(source.nested.value).toBe("original");
|
||||
expect(source.list[0].enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user