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
+39
View File
@@ -0,0 +1,39 @@
import { useEffect, useRef } from "react";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
/**
* 在 useEffect 中监听 Tauri 事件,自动管理异步注册和卸载清理。
* 避免每次使用时重复编写 active flag + async setup 样板代码。
*/
export function useTauriEvent<P>(
eventName: string,
handler: (payload: P) => void | Promise<void>,
): void {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
let disposed = false;
let unlisten: UnlistenFn | undefined;
void (async () => {
try {
const off = await listen<P>(eventName, (event) => {
void handlerRef.current(event.payload);
});
if (disposed) {
off();
} else {
unlisten = off;
}
} catch (error) {
console.error(`Failed to subscribe ${eventName} event`, error);
}
})();
return () => {
disposed = true;
unlisten?.();
};
}, [eventName]);
}
+13 -36
View File
@@ -1,11 +1,10 @@
import { useEffect } from "react";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { useQueryClient } from "@tanstack/react-query";
import type { AppId } from "@/lib/api/types";
import type { UsageResult } from "@/types";
import type { SubscriptionQuota } from "@/types/subscription";
import { usageKeys } from "@/lib/query/usage";
import { subscriptionKeys } from "@/lib/query/subscription";
import { useTauriEvent } from "./useTauriEvent";
type UsageCacheUpdatedPayload =
| {
@@ -28,39 +27,17 @@ type UsageCacheUpdatedPayload =
export function useUsageCacheBridge() {
const queryClient = useQueryClient();
useEffect(() => {
let unlisten: UnlistenFn | undefined;
let disposed = false;
(async () => {
const off = await listen<UsageCacheUpdatedPayload>(
"usage-cache-updated",
(event) => {
const payload = event.payload;
if (payload.kind === "script") {
queryClient.setQueryData<UsageResult>(
usageKeys.script(payload.providerId, payload.appType),
payload.data,
);
} else if (payload.kind === "subscription") {
queryClient.setQueryData<SubscriptionQuota>(
subscriptionKeys.quota(payload.appType),
payload.data,
);
}
},
useTauriEvent<UsageCacheUpdatedPayload>("usage-cache-updated", (payload) => {
if (payload.kind === "script") {
queryClient.setQueryData<UsageResult>(
usageKeys.script(payload.providerId, payload.appType),
payload.data,
);
if (disposed) {
off();
} else {
unlisten = off;
}
})();
return () => {
disposed = true;
unlisten?.();
};
}, [queryClient]);
} else if (payload.kind === "subscription") {
queryClient.setQueryData<SubscriptionQuota>(
subscriptionKeys.quota(payload.appType),
payload.data,
);
}
});
}