Files
3x-ui/frontend/src/test/inbounds-ws-identity.test.tsx
T
Sanaei 7fc86f87de perf(inbounds): keep unchanged rows and online sets across websocket pushes
Every client_stats push carries the totals of all inbounds, and
applyClientStatsEvent rebuilt each row it listed, so every push replaced
all rows, re-ran the client rollup (a JSON parse of every inbound's
settings) and re-rendered the whole table even when no number moved. Every
traffic push also built new online and active maps, re-running the same
rollup.

Rows are now rebuilt only when their totals or a client's numbers change,
and the previous maps are kept when a push repeats the same sets. Measured
in jsdom with 450 inbounds of 50 clients each: an unchanged client_stats
push went from 7.9ms to 0.6ms with no row rebuilt, and a repeated traffic
push from 13.5ms to 8.3ms without the rollup.
2026-09-15 21:06:32 +02:00

108 lines
3.7 KiB
TypeScript

import type { ReactNode } from 'react';
import { act, renderHook, waitFor } from '@testing-library/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { describe, expect, it } from 'vitest';
import { keys } from '@/api/queryKeys';
import { useInbounds } from '@/pages/inbounds/useInbounds';
import { makeTestQueryClient } from './test-utils';
function seedInbounds() {
const rows = [1, 2].map((id) => ({
id,
protocol: 'vless',
tag: `in-${id}`,
enable: true,
up: 10,
down: 20,
total: 0,
expiryTime: 0,
settings: JSON.stringify({ clients: [{ email: `c${id}@x`, enable: true }] }),
clientStats: [
{ email: `c${id}@x`, up: 1, down: 2, total: 0, expiryTime: 0, enable: true, inboundId: id },
],
}));
const queryClient = makeTestQueryClient();
queryClient.setQueryData(keys.inbounds.slim(), rows);
queryClient.setQueryData(keys.clients.onlines(), []);
queryClient.setQueryData(keys.clients.onlinesByGuid(), {});
queryClient.setQueryData(keys.clients.activeInbounds(), {});
queryClient.setQueryData(keys.clients.lastOnline(), {});
queryClient.setQueryData(keys.settings.defaults(), {});
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
return { rows, wrapper };
}
async function renderInbounds() {
const { rows, wrapper } = seedInbounds();
const hook = renderHook(() => useInbounds(), { wrapper });
await waitFor(() => expect(hook.result.current.dbInbounds).toHaveLength(2));
return { rows, result: hook.result };
}
// Every client_stats push carries all inbounds' totals, so rebuilding a row whether or
// not its numbers moved re-ran the client rollup and the whole table on each push.
describe('inbound websocket merges keep unchanged state', () => {
it('keeps rows and the client rollup when a client_stats push changes nothing', async () => {
const { rows, result } = await renderInbounds();
const before = result.current.dbInbounds;
const rollup = result.current.clientCount;
act(() =>
result.current.applyClientStatsEvent({
inbounds: rows.map((r) => ({
id: r.id,
up: r.up,
down: r.down,
total: r.total,
enable: r.enable,
})),
clients: [{ email: 'c1@x', up: 1, down: 2, total: 0, expiryTime: 0, enable: true }],
}),
);
expect(result.current.dbInbounds).toBe(before);
expect(result.current.clientCount).toBe(rollup);
});
it('still rebuilds exactly the rows whose numbers moved', async () => {
const { result } = await renderInbounds();
const before = result.current.dbInbounds;
act(() =>
result.current.applyClientStatsEvent({
inbounds: [
{ id: 1, up: 99, down: 20, total: 0, enable: true },
{ id: 2, up: 10, down: 20, total: 0, enable: true },
],
clients: [{ email: 'c2@x', up: 5, down: 2, total: 0, expiryTime: 0, enable: true }],
}),
);
const [first, second] = result.current.dbInbounds;
expect(first).not.toBe(before[0]);
expect(first.up).toBe(99);
expect(second).not.toBe(before[1]);
expect(second.clientStats?.[0]?.up).toBe(5);
});
it('keeps the client rollup when a traffic push repeats the same online sets', async () => {
const { result } = await renderInbounds();
const push = () =>
result.current.applyTrafficEvent({
onlineClients: ['c1@x'],
onlineByGuid: { 'node:1': ['c1@x'] },
activeInbounds: { 'node:1': ['in-1'] },
});
act(push);
const rollup = result.current.clientCount;
act(push);
expect(result.current.clientCount).toBe(rollup);
});
});