mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-22 10:57:14 +00:00
chore(frontend): update dependencies and adapt to oxlint 1.79
npm install was failing with ERESOLVE: the lockfile pinned storybook 10.5.7 and vitest 4.1.10 as peers while package.json asked for ^10.5.9 and ^4.1.11, and npm would not move either. Neither npm update, a targeted install, nor --package-lock-only broke the cycle, so node_modules and package-lock.json were regenerated from scratch (601 packages, 0 vulnerabilities). oxlint 1.79.0 then promoted five React Compiler rules into the correctness category, flagging 101 pre-existing sites. 1.78.0 exits 0 on the same tree, so nothing in our code changed - the rule set grew. They are fixed rather than suppressed: - refs (31): latest-value ref writes moved out of render into an effect. onlineClientsRef turned out to be write-only and is gone; expireDiffRef and trafficDiffRef were replaced by reading the values directly. - set-state-in-effect (55): reset-on-open modals now adjust state during render; where an effect mixed a synchronous reset with an async fetch, the reset moved to render and the effect kept only the request. useMediaQuery became useSyncExternalStore. - preserve-manual-memoization (11): optional-chained deps the compiler cannot match, hoisted to locals or dropped where the memo wrapped a string concat. - purity (3): Date.now() in render replaced by a state-backed clock, which also refreshes the expiry tag every 60s instead of freezing it until the next unrelated re-render. - immutability (1): applyClientStatsEvent merged websocket traffic into DBInbound rows in place; it now rebuilds only the rows it touches. Two things fell out of that. clientCount is derived with useMemo instead of an imperative rebuildClientCount() called from five sites, which also fixes a staleness bug where changing the expiry or traffic threshold left the counts alone until some later rebuild. statsVersion existed only to force a re-render after an in-place mutation, is meaningless now that rows are replaced, and nothing read it, so it is removed. Also adds a lint:fix script - oxlint --fix was previously only reachable through the lint-staged hook.
This commit is contained in:
@@ -700,7 +700,9 @@ export function useClients(options: UseClientsOptions = {}) {
|
||||
// WS-driven in-place merges. Page wires these via useWebSocket; the bridge
|
||||
// covers coarse 'invalidate' and 'inbounds' events centrally.
|
||||
const queryRef = useRef(query);
|
||||
queryRef.current = query;
|
||||
useEffect(() => {
|
||||
queryRef.current = query;
|
||||
});
|
||||
|
||||
const applyTrafficEvent = useCallback(
|
||||
(payload: unknown) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useSyncExternalStore } from 'react';
|
||||
|
||||
export const MOBILE_BREAKPOINT_PX = 768;
|
||||
|
||||
@@ -11,17 +11,21 @@ export const MOBILE_BREAKPOINT_PX = 768;
|
||||
*/
|
||||
export function useMediaQuery(breakpoint: number = MOBILE_BREAKPOINT_PX) {
|
||||
const query = `(max-width: ${breakpoint}px)`;
|
||||
const [isMobile, setIsMobile] = useState<boolean>(() =>
|
||||
typeof window !== 'undefined' ? window.matchMedia(query).matches : false,
|
||||
|
||||
const subscribe = useCallback(
|
||||
(onStoreChange: () => void) => {
|
||||
const mql = window.matchMedia(query);
|
||||
mql.addEventListener('change', onStoreChange);
|
||||
return () => mql.removeEventListener('change', onStoreChange);
|
||||
},
|
||||
[query],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(query);
|
||||
const onChange = (e: MediaQueryListEvent) => setIsMobile(e.matches);
|
||||
mql.addEventListener('change', onChange);
|
||||
setIsMobile(mql.matches);
|
||||
return () => mql.removeEventListener('change', onChange);
|
||||
}, [query]);
|
||||
const isMobile = useSyncExternalStore(
|
||||
subscribe,
|
||||
() => window.matchMedia(query).matches,
|
||||
() => false,
|
||||
);
|
||||
|
||||
return { isMobile };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function useServerDraft<T>(
|
||||
server: T | undefined,
|
||||
@@ -6,37 +6,30 @@ export function useServerDraft<T>(
|
||||
equals: (left: T, right: T) => boolean,
|
||||
) {
|
||||
const cloneRef = useRef(clone);
|
||||
const equalsRef = useRef(equals);
|
||||
cloneRef.current = clone;
|
||||
equalsRef.current = equals;
|
||||
useEffect(() => {
|
||||
cloneRef.current = clone;
|
||||
});
|
||||
|
||||
const [draft, setDraft] = useState<T | undefined>();
|
||||
const [baseline, setBaseline] = useState<T | undefined>();
|
||||
const draftRef = useRef(draft);
|
||||
const baselineRef = useRef(baseline);
|
||||
draftRef.current = draft;
|
||||
baselineRef.current = baseline;
|
||||
const [syncedServer, setSyncedServer] = useState<T | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (server === undefined) return;
|
||||
const currentDraft = draftRef.current;
|
||||
const currentBaseline = baselineRef.current;
|
||||
const isDirty =
|
||||
currentDraft !== undefined &&
|
||||
(currentBaseline === undefined || !equalsRef.current(currentDraft, currentBaseline));
|
||||
setBaseline(server);
|
||||
if (isDirty && !equalsRef.current(currentDraft, server)) return;
|
||||
setDraft(cloneRef.current(server));
|
||||
}, [server]);
|
||||
const isDirty = draft !== undefined && (baseline === undefined || !equals(draft, baseline));
|
||||
|
||||
// Adopting the server value during render (not in an effect) keeps the
|
||||
// returned draft and isDirty consistent within the very first render.
|
||||
if (server !== syncedServer) {
|
||||
setSyncedServer(server);
|
||||
if (server !== undefined) {
|
||||
setBaseline(server);
|
||||
const keepLocalEdits = isDirty && !equals(draft as T, server);
|
||||
if (!keepLocalEdits) setDraft(clone(server));
|
||||
}
|
||||
}
|
||||
|
||||
const markSaved = useCallback((value: T) => {
|
||||
setBaseline(cloneRef.current(value));
|
||||
}, []);
|
||||
|
||||
const isDirty = useMemo(
|
||||
() => draft !== undefined && (baseline === undefined || !equalsRef.current(draft, baseline)),
|
||||
[baseline, draft],
|
||||
);
|
||||
|
||||
return { draft, setDraft, isDirty, markSaved };
|
||||
}
|
||||
|
||||
@@ -139,10 +139,14 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
const [outboundTestUrl, setOutboundTestUrlState] = useState(DEFAULT_TEST_URL);
|
||||
const [savedXraySetting, setSavedXraySetting] = useState('');
|
||||
const [savedOutboundTestUrl, setSavedOutboundTestUrl] = useState(DEFAULT_TEST_URL);
|
||||
const [inboundTags, setInboundTags] = useState<string[]>([]);
|
||||
const [clientReverseTags, setClientReverseTags] = useState<string[]>([]);
|
||||
const [subscriptionOutbounds, setSubscriptionOutbounds] = useState<unknown[]>([]);
|
||||
const [subscriptionOutboundTags, setSubscriptionOutboundTags] = useState<string[]>([]);
|
||||
const config = configQuery.data;
|
||||
const inboundTags = useMemo(() => config?.inboundTags || [], [config]);
|
||||
const clientReverseTags = useMemo(() => config?.clientReverseTags || [], [config]);
|
||||
const subscriptionOutbounds = useMemo<unknown[]>(
|
||||
() => config?.subscriptionOutbounds || [],
|
||||
[config],
|
||||
);
|
||||
const subscriptionOutboundTags = useMemo(() => config?.subscriptionOutboundTags || [], [config]);
|
||||
const [outboundTestStates, setOutboundTestStates] = useState<Record<number, OutboundTestState>>(
|
||||
{},
|
||||
);
|
||||
@@ -161,34 +165,34 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
const templateSettingsRef = useRef<XraySettingsValue | null>(null);
|
||||
const subscriptionOutboundsRef = useRef<unknown[]>([]);
|
||||
|
||||
xraySettingRef.current = xraySetting;
|
||||
outboundTestUrlRef.current = outboundTestUrl;
|
||||
savedXraySettingRef.current = savedXraySetting;
|
||||
savedOutboundTestUrlRef.current = savedOutboundTestUrl;
|
||||
templateSettingsRef.current = templateSettings;
|
||||
subscriptionOutboundsRef.current = subscriptionOutbounds;
|
||||
const [syncedConfig, setSyncedConfig] = useState<XrayConfigPayload | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!configQuery.data) return;
|
||||
const obj = configQuery.data;
|
||||
const pretty = JSON.stringify(obj.xraySetting, null, 2);
|
||||
const nextUrl = normalizeOutboundTestUrl(obj.outboundTestUrl || '');
|
||||
setInboundTags(obj.inboundTags || []);
|
||||
setClientReverseTags(obj.clientReverseTags || []);
|
||||
setSubscriptionOutbounds(obj.subscriptionOutbounds || []);
|
||||
setSubscriptionOutboundTags(obj.subscriptionOutboundTags || []);
|
||||
xraySettingRef.current = xraySetting;
|
||||
outboundTestUrlRef.current = outboundTestUrl;
|
||||
savedXraySettingRef.current = savedXraySetting;
|
||||
savedOutboundTestUrlRef.current = savedOutboundTestUrl;
|
||||
templateSettingsRef.current = templateSettings;
|
||||
subscriptionOutboundsRef.current = subscriptionOutbounds;
|
||||
});
|
||||
|
||||
// Adopt a fetched config during render, so the editor never paints one frame
|
||||
// of the previous config after a refetch. Local edits win over the refetch.
|
||||
if (config && config !== syncedConfig) {
|
||||
setSyncedConfig(config);
|
||||
const isDirty =
|
||||
savedXraySettingRef.current !== xraySettingRef.current ||
|
||||
savedOutboundTestUrlRef.current !== normalizeOutboundTestUrl(outboundTestUrlRef.current);
|
||||
if (isDirty) return;
|
||||
syncingRef.current = true;
|
||||
setXraySettingState(pretty);
|
||||
setTemplateSettingsState(obj.xraySetting);
|
||||
setSavedXraySetting(pretty);
|
||||
syncingRef.current = false;
|
||||
setOutboundTestUrlState(nextUrl);
|
||||
setSavedOutboundTestUrl(nextUrl);
|
||||
}, [configQuery.data]);
|
||||
savedXraySetting !== xraySetting ||
|
||||
savedOutboundTestUrl !== normalizeOutboundTestUrl(outboundTestUrl);
|
||||
if (!isDirty) {
|
||||
const pretty = JSON.stringify(config.xraySetting, null, 2);
|
||||
const nextUrl = normalizeOutboundTestUrl(config.outboundTestUrl || '');
|
||||
setXraySettingState(pretty);
|
||||
setTemplateSettingsState(config.xraySetting);
|
||||
setSavedXraySetting(pretty);
|
||||
setOutboundTestUrlState(nextUrl);
|
||||
setSavedOutboundTestUrl(nextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const fetched = configQuery.data !== undefined || configQuery.isError;
|
||||
const fetchError = configQuery.error ? (configQuery.error as Error).message : '';
|
||||
|
||||
Reference in New Issue
Block a user