fix(connection-table): patch DataGrid event handling to prevent Safari crash
- Ensure api.publishEvent is patched only once, retrying until the API is ready. - Normalize missing event objects for Safari to avoid crashes. - Restore the original handler and clear timers on unmount to keep the grid stable.
This commit is contained in:
@@ -48,6 +48,7 @@
|
||||
- 修复静默启动不加载完整 WebView 的问题
|
||||
- 修复 Linux WebKit 网络进程的崩溃
|
||||
- 修复实际导入成功但显示导入失败的问题
|
||||
- 修复 macOS 连接界面显示异常
|
||||
|
||||
## v2.4.2
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
|
||||
import {
|
||||
DataGrid,
|
||||
GridColDef,
|
||||
GridColumnResizeParams,
|
||||
useGridApiRef,
|
||||
} from "@mui/x-data-grid";
|
||||
import dayjs from "dayjs";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
import { t } from "i18next";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
@@ -14,6 +19,125 @@ interface Props {
|
||||
|
||||
export const ConnectionTable = (props: Props) => {
|
||||
const { connections, onShowDetail } = props;
|
||||
const apiRef = useGridApiRef();
|
||||
useEffect(() => {
|
||||
const PATCH_FLAG_KEY = "__clashPatchedPublishEvent" as const;
|
||||
const ORIGINAL_KEY = "__clashOriginalPublishEvent" as const;
|
||||
let isUnmounted = false;
|
||||
let retryHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
let cleanupOriginal: (() => void) | null = null;
|
||||
|
||||
const scheduleRetry = () => {
|
||||
if (isUnmounted || retryHandle !== null) return;
|
||||
retryHandle = setTimeout(() => {
|
||||
retryHandle = null;
|
||||
ensurePatched();
|
||||
}, 16);
|
||||
};
|
||||
|
||||
// Safari occasionally emits grid events without an event object,
|
||||
// and MUI expects `defaultMuiPrevented` to exist. Normalize here to avoid crashes.
|
||||
const createFallbackEvent = () => {
|
||||
const fallback = {
|
||||
defaultMuiPrevented: false,
|
||||
preventDefault() {
|
||||
fallback.defaultMuiPrevented = true;
|
||||
},
|
||||
};
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const ensureMuiEvent = (
|
||||
value: unknown,
|
||||
): {
|
||||
defaultMuiPrevented: boolean;
|
||||
preventDefault: () => void;
|
||||
[key: string]: unknown;
|
||||
} => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return createFallbackEvent();
|
||||
}
|
||||
|
||||
const eventObject = value as {
|
||||
defaultMuiPrevented?: unknown;
|
||||
preventDefault?: () => void;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
if (typeof eventObject.defaultMuiPrevented !== "boolean") {
|
||||
eventObject.defaultMuiPrevented = false;
|
||||
}
|
||||
|
||||
if (typeof eventObject.preventDefault !== "function") {
|
||||
eventObject.preventDefault = () => {
|
||||
eventObject.defaultMuiPrevented = true;
|
||||
};
|
||||
}
|
||||
|
||||
return eventObject as {
|
||||
defaultMuiPrevented: boolean;
|
||||
preventDefault: () => void;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
const ensurePatched = () => {
|
||||
if (isUnmounted) return;
|
||||
const api = apiRef.current;
|
||||
|
||||
if (!api?.publishEvent) {
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataApi = api as unknown as typeof api &
|
||||
Record<string, unknown>;
|
||||
if (metadataApi[PATCH_FLAG_KEY] === true) return;
|
||||
|
||||
const originalPublishEvent = api.publishEvent;
|
||||
|
||||
const patchedPublishEvent = ((...rawArgs: unknown[]) => {
|
||||
rawArgs[2] = ensureMuiEvent(rawArgs[2]);
|
||||
|
||||
return (
|
||||
originalPublishEvent as unknown as (...args: unknown[]) => void
|
||||
).apply(api, rawArgs);
|
||||
}) as typeof originalPublishEvent;
|
||||
|
||||
api.publishEvent = patchedPublishEvent;
|
||||
metadataApi[PATCH_FLAG_KEY] = true;
|
||||
metadataApi[ORIGINAL_KEY] = originalPublishEvent;
|
||||
|
||||
cleanupOriginal = () => {
|
||||
const storedOriginal = metadataApi[ORIGINAL_KEY] as
|
||||
| typeof originalPublishEvent
|
||||
| undefined;
|
||||
|
||||
api.publishEvent = (
|
||||
typeof storedOriginal === "function"
|
||||
? storedOriginal
|
||||
: originalPublishEvent
|
||||
) as typeof originalPublishEvent;
|
||||
|
||||
delete metadataApi[PATCH_FLAG_KEY];
|
||||
delete metadataApi[ORIGINAL_KEY];
|
||||
};
|
||||
};
|
||||
|
||||
ensurePatched();
|
||||
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
if (retryHandle !== null) {
|
||||
clearTimeout(retryHandle);
|
||||
retryHandle = null;
|
||||
}
|
||||
if (cleanupOriginal) {
|
||||
cleanupOriginal();
|
||||
cleanupOriginal = null;
|
||||
}
|
||||
};
|
||||
}, [apiRef]);
|
||||
|
||||
const [columnVisible, setColumnVisible] = useState<
|
||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||
@@ -156,6 +280,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
apiRef={apiRef}
|
||||
hideFooter
|
||||
rows={connRows}
|
||||
columns={columns}
|
||||
|
||||
Reference in New Issue
Block a user