Files
l-ui/frontend/src/lib/xray/balancer-helpers.ts
T
Muse 277838d7ca ITEM6 B4: frontend reshaping — xray page retired, stats switches to settings, template read paths closed
- Delete the xray settings page (routes/menu/title), geodata UI/service,
  XraySettingController/Service and routing-sync write paths; the stored
  xrayTemplateConfig row loses its last readers (GetXrayConfigTemplate now
  returns the embedded factory template; B1 already deleted the row).
- Settings GeneralTab gains the four trafficStatsSwitches switches
  (settings.trafficStatsSwitches, B1) with the dual warning copy; entity
  AllSetting carries the key end to end (TestTrafficStatsSwitchesPlumbing).
- Tag-directory consumers (panelOutbound, mtproto egress, node/host
  outbound pickers) move from POST /xray/ to GET /server/outboundDirectory.
- Shared Mux/Sockopt forms and balancer helpers relocated to lib/xray;
  converter preserves sniffing ips/domains exclusions into the panel
  sidecar instead of dropping them (B2 gap closed by retention).
- Freedom final-rules DB fix seeders retired; getDefaultJsonConfig
  endpoint removed (no in-repo consumer); locales pruned to referenced
  keys (pages.xray 331 -> 68) plus the five new settings keys.
2026-10-22 23:40:54 +00:00

96 lines
3.9 KiB
TypeScript

import type { BalancerObject } from '@/schemas/routing';
// The settings object these helpers read/mutate: only the routing
// balancers and the two observatory sections are touched. (This was
// XraySettingsValue from the retired xray settings page's hook; the
// helpers outlived the page because the subscription balancers tab
// still consumes the defaults.)
export interface BalancerSettingsLike {
routing?: { balancers?: BalancerObject[] } | null;
observatory?: unknown;
burstObservatory?: unknown;
}
export const DEFAULT_OBSERVATORY = Object.freeze({
subjectSelector: [] as string[],
probeURL: 'https://www.google.com/generate_204',
probeInterval: '1m',
enableConcurrency: true,
});
export const DEFAULT_BURST_OBSERVATORY = Object.freeze({
subjectSelector: [] as string[],
pingConfig: {
destination: 'https://www.google.com/generate_204',
interval: '1m',
connectivity: '',
timeout: '5s',
sampling: 2,
httpMethod: 'HEAD',
},
});
export function collectSelectors(list: BalancerObject[]): string[] {
const out = new Set<string>();
list.forEach((b) => (b.selector || []).forEach((s) => s && out.add(s)));
return [...out];
}
export function balancerRequiresBurstObservatory(b: BalancerObject): boolean {
const type = b.strategy?.type || 'random';
return (
type === 'leastLoad' ||
((type === 'random' || type === 'roundRobin') && (b.fallbackTag ?? '').length > 0)
);
}
export function settingsRequireBurstObservatory(t: BalancerSettingsLike | null): boolean {
const balancers = (t?.routing?.balancers || []) as BalancerObject[];
return balancers.some(balancerRequiresBurstObservatory);
}
// syncObservatories keeps the (burst)observatory sections aligned with the
// balancer strategies that actually require them. Observatories have no runtime
// reload API in xray-core, so creating OR removing one forces a full process
// restart — that's why an observer-less balancer never gets one and stays a
// live, routing-only change applied through the core API.
//
// xray-core binds the Observatory feature to a Random/RoundRobinStrategy only
// when its fallbackTag is set (issue #5605): with a fallbackTag the strategy
// calls RequireFeatures(Observatory) and the core aborts startup with "not all
// dependencies are resolved" if none exists; without a fallbackTag it never even
// consults an observatory. leastLoad needs the burst observer, while leastPing
// can use any extension.Observatory result with Alive/Delay. When a burst
// observer is required, keep all observer-backed balancers on burstObservatory
// to avoid xray-core resolving the earlier regular observatory feature instead.
//
// So each observer lives exactly as long as something requires it, and is
// dropped the moment nothing does — clearing the last fallbackTag (or deleting
// the last leastLoad) removes the burst observer again. A no-fallback
// Random/RoundRobin balancer never expands the observer either, because those
// strategies do not consume observer data.
export function syncObservatories(t: BalancerSettingsLike) {
const balancers = (t.routing?.balancers || []) as BalancerObject[];
const leastPings = balancers.filter((b) => b.strategy?.type === 'leastPing');
const required = balancers.filter(balancerRequiresBurstObservatory);
if (required.length > 0) {
delete t.observatory;
if (!t.burstObservatory)
t.burstObservatory = JSON.parse(JSON.stringify(DEFAULT_BURST_OBSERVATORY));
(t.burstObservatory as { subjectSelector: string[] }).subjectSelector = collectSelectors([
...required,
...leastPings,
]);
} else {
delete t.burstObservatory;
if (leastPings.length > 0) {
if (!t.observatory) t.observatory = JSON.parse(JSON.stringify(DEFAULT_OBSERVATORY));
(t.observatory as { subjectSelector: string[] }).subjectSelector =
collectSelectors(leastPings);
} else {
delete t.observatory;
}
}
}