mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-24 20:52:15 +08:00
WebSocket hooks (use-connection-data, use-log-data) - Single-pass merge for connection snapshots; reuse previous row reference when upload/download are unchanged so downstream memos can bail out - Single-allocation cap on closedConnections (no double concat+slice) - Single-allocation clamp in appendLogs - Cache dayjs().format result within each 50ms log flush window Connection table (connection-table.tsx) - Decouple 5s relative-time tick from columnDefs via shared module-level tickStore + memoised RelativeTimeCell so the tick no longer rebuilds the table - RowComponent memo with custom comparator (relies on stable row reference) - Lift sx objects to module scope - Stable onShowDetail via ref App data provider (app-data-provider, app-data-context) - Split monolithic context into six sub-contexts (Proxies/Rules/ClashConfig/ System/Uptime/Refreshers); 3s uptime ticks no longer invalidate unrelated consumers - useAppData adapter aggregates sub-contexts; consumer files unchanged - useStableFn helper stabilises all refresh* callback identities - Extract useCtx helper to remove repeated null-check boilerplate Canvas traffic graph (enhanced-canvas-traffic-graph, use-traffic-monitor) - Lift Y-axis scale (max/min) to drawGraph layer; calculateY allocations per frame: ~600 -> 1 - Document visibility gate stops RAF and the setNow interval when the window is hidden; appendData unaffected - Reuse the existing visibilitychange listener instead of adding a second one; isDocumentVisibleRef guards animate() inside the frame - Convert forEach arrow shorthands to block bodies - Drop unnecessary state-setter deps from a useCallback Proxy groups (proxy-groups, proxy-render, use-render-list) - Narrow proxyColItemsMemo dep: headState -> headState.showType - Per-group fingerprint cache + prevListRef in use-render-list; steady-state refetch ticks become O(0) and the list reference stays stable when nothing changed - useStableCallback wraps handleCheckAll / handleLocation
130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import { useQueryClient } from '@tanstack/react-query'
|
|
import { MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
|
|
|
import { useMihomoWsSubscription } from './use-mihomo-ws-subscription'
|
|
|
|
const MAX_CLOSED_CONNS_NUM = 500
|
|
|
|
export const initConnData: ConnectionMonitorData = {
|
|
uploadTotal: 0,
|
|
downloadTotal: 0,
|
|
activeConnections: [],
|
|
closedConnections: [],
|
|
}
|
|
|
|
export interface ConnectionMonitorData {
|
|
uploadTotal: number
|
|
downloadTotal: number
|
|
activeConnections: IConnectionsItem[]
|
|
closedConnections: IConnectionsItem[]
|
|
}
|
|
|
|
const mergeConnectionSnapshot = (
|
|
payload: IConnections,
|
|
previous: ConnectionMonitorData = initConnData,
|
|
): ConnectionMonitorData => {
|
|
const nextConnections = payload.connections ?? []
|
|
const previousActive = previous.activeConnections ?? []
|
|
const previousClosed = previous.closedConnections ?? []
|
|
|
|
const nextById = new Map<string, IConnectionsItem>()
|
|
for (let i = 0; i < nextConnections.length; i++) {
|
|
nextById.set(nextConnections[i].id, nextConnections[i])
|
|
}
|
|
|
|
const carried: IConnectionsItem[] = []
|
|
const dropped: IConnectionsItem[] = []
|
|
|
|
for (let i = 0; i < previousActive.length; i++) {
|
|
const prev = previousActive[i]
|
|
const next = nextById.get(prev.id)
|
|
if (next !== undefined) {
|
|
nextById.delete(prev.id)
|
|
if (prev.upload === next.upload && prev.download === next.download) {
|
|
// Reuse prev reference: row identity stability is the contract Stage 2 memo relies on.
|
|
carried.push(prev)
|
|
} else {
|
|
carried.push({
|
|
...next,
|
|
curUpload: next.upload - prev.upload,
|
|
curDownload: next.download - prev.download,
|
|
})
|
|
}
|
|
} else {
|
|
dropped.push(prev)
|
|
}
|
|
}
|
|
|
|
const activeConnections: IConnectionsItem[] = carried
|
|
for (let i = 0; i < nextConnections.length; i++) {
|
|
const conn = nextConnections[i]
|
|
if (nextById.has(conn.id)) {
|
|
activeConnections.push({
|
|
...conn,
|
|
curUpload: 0,
|
|
curDownload: 0,
|
|
})
|
|
}
|
|
}
|
|
|
|
const rawClosedLen = previousClosed.length + dropped.length
|
|
let closedConnections: IConnectionsItem[]
|
|
if (rawClosedLen <= MAX_CLOSED_CONNS_NUM) {
|
|
closedConnections = previousClosed.concat(dropped)
|
|
} else {
|
|
const skipPrev = rawClosedLen - MAX_CLOSED_CONNS_NUM
|
|
closedConnections =
|
|
skipPrev >= previousClosed.length
|
|
? dropped.slice(skipPrev - previousClosed.length)
|
|
: previousClosed.slice(skipPrev).concat(dropped)
|
|
}
|
|
|
|
return {
|
|
uploadTotal: payload.uploadTotal ?? 0,
|
|
downloadTotal: payload.downloadTotal ?? 0,
|
|
activeConnections,
|
|
closedConnections,
|
|
}
|
|
}
|
|
|
|
export const useConnectionData = () => {
|
|
const queryClient = useQueryClient()
|
|
const { response, refresh, subscriptionCacheKey } =
|
|
useMihomoWsSubscription<ConnectionMonitorData>({
|
|
storageKey: 'mihomo_connection_date',
|
|
buildSubscriptKey: (date) => `getClashConnection-${date}`,
|
|
fallbackData: initConnData,
|
|
connect: () => MihomoWebSocket.connect_connections(),
|
|
throttleMs: 16,
|
|
setupHandlers: ({ next, scheduleReconnect }) => ({
|
|
handleMessage: (data) => {
|
|
if (data.startsWith('Websocket error')) {
|
|
next(data)
|
|
void scheduleReconnect()
|
|
return
|
|
}
|
|
|
|
next(null, (old = initConnData) =>
|
|
mergeConnectionSnapshot(JSON.parse(data) as IConnections, old),
|
|
)
|
|
},
|
|
}),
|
|
})
|
|
|
|
const clearClosedConnections = () => {
|
|
if (!subscriptionCacheKey) return
|
|
queryClient.setQueryData<ConnectionMonitorData>([subscriptionCacheKey], {
|
|
uploadTotal: response.data?.uploadTotal ?? 0,
|
|
downloadTotal: response.data?.downloadTotal ?? 0,
|
|
activeConnections: response.data?.activeConnections ?? [],
|
|
closedConnections: [],
|
|
})
|
|
}
|
|
|
|
return {
|
|
response,
|
|
refreshGetClashConnection: refresh,
|
|
clearClosedConnections,
|
|
}
|
|
}
|