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:
zcb
2026-05-26 23:39:16 +08:00
committed by GitHub
parent 05ba28016c
commit 8cdaf90d8d
12 changed files with 178 additions and 201 deletions
+5
View File
@@ -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
View File
@@ -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[]
>;
+30
View File
@@ -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);
});
});