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
+37 -112
View File
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion";
import { toast } from "sonner";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useQueryClient } from "@tanstack/react-query";
import {
Plus,
@@ -46,9 +45,11 @@ import { hermesApi } from "@/lib/api/hermes";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import { useAutoCompact } from "@/hooks/useAutoCompact";
import { useUsageCacheBridge } from "@/hooks/useUsageCacheBridge";
import { useTauriEvent } from "@/hooks/useTauriEvent";
import { useLastValidValue } from "@/hooks/useLastValidValue";
import { extractErrorMessage } from "@/utils/errorUtils";
import { isTextEditableTarget } from "@/utils/domUtils";
import { deepClone } from "@/utils/deepClone";
import { cn } from "@/lib/utils";
import {
isWindows,
@@ -361,117 +362,43 @@ function App() {
};
}, [activeApp, refetch]);
useEffect(() => {
let unsubscribe: (() => void) | undefined;
let active = true;
useTauriEvent("universal-provider-synced", async () => {
await queryClient.invalidateQueries({ queryKey: ["providers"] });
try {
await providersApi.updateTrayMenu();
} catch (error) {
console.error("[App] Failed to update tray menu", error);
}
});
const setupListener = async () => {
try {
const { listen } = await import("@tauri-apps/api/event");
const off = await listen("universal-provider-synced", async () => {
await queryClient.invalidateQueries({ queryKey: ["providers"] });
try {
await providersApi.updateTrayMenu();
} catch (error) {
console.error("[App] Failed to update tray menu", error);
}
});
if (!active) {
off();
return;
}
unsubscribe = off;
} catch (error) {
console.error(
"[App] Failed to subscribe universal-provider-synced event",
error,
);
}
};
void setupListener();
return () => {
active = false;
unsubscribe?.();
};
}, [queryClient]);
useEffect(() => {
let unsubscribe: (() => void) | undefined;
let active = true;
const setupListener = async () => {
try {
const off = await listen(
"webdav-sync-status-updated",
async (event) => {
const payload = (event.payload ??
{}) as WebDavSyncStatusUpdatedPayload;
await queryClient.invalidateQueries({ queryKey: ["settings"] });
if (payload.source !== "auto" || payload.status !== "error") {
return;
}
toast.error(
t("settings.webdavSync.autoSyncFailedToast", {
error: payload.error || t("common.unknown"),
}),
);
},
);
if (!active) {
off();
return;
}
unsubscribe = off;
} catch (error) {
console.error(
"[App] Failed to subscribe webdav-sync-status-updated event",
error,
);
}
};
void setupListener();
return () => {
active = false;
unsubscribe?.();
};
}, [queryClient, t]);
// Listen for proxy-official-warning: warn when takeover is enabled with an official provider
useEffect(() => {
let unsubscribe: (() => void) | undefined;
let active = true;
const setup = async () => {
const off = await listen("proxy-official-warning", (event) => {
const { providerName } = event.payload as {
appType: string;
providerName: string;
};
toast.warning(
t("notifications.proxyOfficialWarning", {
name: providerName,
defaultValue: `当前供应商 ${providerName} 是官方供应商,建议切换到第三方供应商后再使用代理接管`,
}),
{ duration: 8000 },
);
});
if (!active) {
off();
useTauriEvent<WebDavSyncStatusUpdatedPayload | null | undefined>(
"webdav-sync-status-updated",
async (payload) => {
const statusPayload = payload ?? {};
await queryClient.invalidateQueries({ queryKey: ["settings"] });
if (statusPayload.source !== "auto" || statusPayload.status !== "error") {
return;
}
unsubscribe = off;
};
toast.error(
t("settings.webdavSync.autoSyncFailedToast", {
error: statusPayload.error || t("common.unknown"),
}),
);
},
);
void setup();
return () => {
active = false;
unsubscribe?.();
};
}, [t]);
useTauriEvent<{ appType: string; providerName: string }>(
"proxy-official-warning",
(payload) => {
toast.warning(
t("notifications.proxyOfficialWarning", {
name: payload.providerName,
defaultValue: `当前供应商 ${payload.providerName} 是官方供应商,建议切换到第三方供应商后再使用代理接管`,
}),
{ duration: 8000 },
);
},
);
useEffect(() => {
let active = true;
@@ -744,13 +671,11 @@ function App() {
addToLive?: boolean;
} = {
name: `${provider.name} copy`,
settingsConfig: JSON.parse(JSON.stringify(provider.settingsConfig)), // 深拷贝
settingsConfig: deepClone(provider.settingsConfig),
websiteUrl: provider.websiteUrl,
category: provider.category,
sortIndex: newSortIndex, // 复制原 sortIndex + 1
meta: provider.meta
? JSON.parse(JSON.stringify(provider.meta))
: undefined, // 深拷贝
meta: provider.meta ? deepClone(provider.meta) : undefined,
icon: provider.icon,
iconColor: provider.iconColor,
};