mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-31 15:37:14 +00:00
fix(frontend): share one WebSocket connection across bridge and hooks
websocketBridge.ts and useWebSocket.ts each declared their own module-scoped sharedClient plus an identical getSharedClient, so the "shared" client was not shared between them: whenever a page using useWebSocket (Clients/Inbounds) mounted alongside the always-mounted bridge, the panel opened two sockets to /ws. The server then pushed every traffic/stats/nodes/inbounds snapshot to both, doubling WebSocket bandwidth and running two independent reconnect loops, and the hook's socket was never disconnected on unmount. Hoist a single getSharedWebSocketClient into api/websocket.ts and route both the bridge and the hook through it, so exactly one connection is opened.
This commit is contained in:
@@ -190,3 +190,12 @@ export class WebSocketClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sharedClient: WebSocketClient | null = null;
|
||||||
|
|
||||||
|
export function getSharedWebSocketClient(): WebSocketClient {
|
||||||
|
if (sharedClient) return sharedClient;
|
||||||
|
const basePath = (typeof window !== 'undefined' && window.X_UI_BASE_PATH) || '';
|
||||||
|
sharedClient = new WebSocketClient(basePath);
|
||||||
|
return sharedClient;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,34 +1,19 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { WebSocketClient } from '@/api/websocket';
|
import { getSharedWebSocketClient } from '@/api/websocket';
|
||||||
import { keys } from '@/api/queryKeys';
|
import { keys } from '@/api/queryKeys';
|
||||||
import { isRecentLocalInvalidate } from '@/api/invalidationTracker';
|
import { isRecentLocalInvalidate } from '@/api/invalidationTracker';
|
||||||
|
|
||||||
type Handler = (payload: unknown) => void;
|
type Handler = (payload: unknown) => void;
|
||||||
|
|
||||||
interface SharedClient {
|
|
||||||
connect(): void;
|
|
||||||
on(event: string, fn: Handler): void;
|
|
||||||
off(event: string, fn: Handler): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sharedClient: SharedClient | null = null;
|
|
||||||
|
|
||||||
function getSharedClient(): SharedClient {
|
|
||||||
if (sharedClient) return sharedClient;
|
|
||||||
const basePath = (typeof window !== 'undefined' && window.X_UI_BASE_PATH) || '';
|
|
||||||
sharedClient = new WebSocketClient(basePath) as SharedClient;
|
|
||||||
return sharedClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
let invalidateTimer: number | null = null;
|
let invalidateTimer: number | null = null;
|
||||||
|
|
||||||
export function useWebSocketBridge() {
|
export function useWebSocketBridge() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const client = getSharedClient();
|
const client = getSharedWebSocketClient();
|
||||||
|
|
||||||
const onInvalidate: Handler = (payload) => {
|
const onInvalidate: Handler = (payload) => {
|
||||||
const p = payload as { type?: string } | undefined;
|
const p = payload as { type?: string } | undefined;
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { WebSocketClient } from '@/api/websocket';
|
import { getSharedWebSocketClient } from '@/api/websocket';
|
||||||
|
|
||||||
type Handler = (payload: unknown) => void;
|
type Handler = (payload: unknown) => void;
|
||||||
|
|
||||||
interface SharedClient {
|
|
||||||
connect(): void;
|
|
||||||
on(event: string, fn: Handler): void;
|
|
||||||
off(event: string, fn: Handler): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sharedClient: SharedClient | null = null;
|
|
||||||
|
|
||||||
function getSharedClient(): SharedClient {
|
|
||||||
if (sharedClient) return sharedClient;
|
|
||||||
const basePath = (typeof window !== 'undefined' && window.X_UI_BASE_PATH) || '';
|
|
||||||
sharedClient = new WebSocketClient(basePath) as SharedClient;
|
|
||||||
return sharedClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWebSocket(handlers: Record<string, Handler>) {
|
export function useWebSocket(handlers: Record<string, Handler>) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const client = getSharedClient();
|
const client = getSharedWebSocketClient();
|
||||||
const entries = Object.entries(handlers);
|
const entries = Object.entries(handlers);
|
||||||
for (const [event, fn] of entries) client.on(event, fn);
|
for (const [event, fn] of entries) client.on(event, fn);
|
||||||
client.connect();
|
client.connect();
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { useWebSocket } from '@/hooks/useWebSocket';
|
||||||
|
import { useWebSocketBridge } from '@/api/websocketBridge';
|
||||||
|
|
||||||
|
describe('shared WebSocket connection', () => {
|
||||||
|
let socketCount = 0;
|
||||||
|
let originalWS: typeof WebSocket;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
socketCount = 0;
|
||||||
|
originalWS = globalThis.WebSocket;
|
||||||
|
class FakeWebSocket {
|
||||||
|
static readonly CONNECTING = 0;
|
||||||
|
static readonly OPEN = 1;
|
||||||
|
static readonly CLOSING = 2;
|
||||||
|
static readonly CLOSED = 3;
|
||||||
|
readyState = FakeWebSocket.CONNECTING;
|
||||||
|
constructor() {
|
||||||
|
socketCount += 1;
|
||||||
|
}
|
||||||
|
addEventListener() {}
|
||||||
|
removeEventListener() {}
|
||||||
|
close() {}
|
||||||
|
send() {}
|
||||||
|
}
|
||||||
|
globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.WebSocket = originalWS;
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens a single socket when the bridge and a page hook are both mounted', () => {
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
renderHook(() => useWebSocketBridge(), { wrapper });
|
||||||
|
renderHook(() => useWebSocket({ traffic: () => {} }));
|
||||||
|
|
||||||
|
expect(socketCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user