Keep both: upstream RAM-usage calculator for dns_cache_size and the fork AdGuard Home DNS mode options (dns_main_mode / dns_adguard_failover_enabled).
385 lines
10 KiB
TypeScript
385 lines
10 KiB
TypeScript
import { Tachyon } from '../types';
|
|
import { initialDiagnosticStore } from '../tabs/diagnostic/diagnostic.store';
|
|
|
|
function jsonStableStringify<T, V>(obj: T): string {
|
|
return JSON.stringify(obj, (_, value) => {
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
return Object.keys(value)
|
|
.sort()
|
|
.reduce(
|
|
(acc, key) => {
|
|
acc[key] = value[key];
|
|
return acc;
|
|
},
|
|
{} as Record<string, V>,
|
|
);
|
|
}
|
|
return value;
|
|
});
|
|
}
|
|
|
|
function jsonEqual<A, B>(a: A, b: B): boolean {
|
|
try {
|
|
return jsonStableStringify(a) === jsonStableStringify(b);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
|
|
|
|
// eslint-disable-next-line
|
|
export class StoreService<T extends Record<string, any>> {
|
|
private value: T;
|
|
private readonly initial: T;
|
|
private listeners = new Set<Listener<T>>();
|
|
|
|
constructor(initial: T) {
|
|
this.value = initial;
|
|
this.initial = structuredClone(initial);
|
|
}
|
|
|
|
get(): T {
|
|
return this.value;
|
|
}
|
|
|
|
set(next: Partial<T>): void {
|
|
const prev = this.value;
|
|
const diff: Partial<T> = {};
|
|
|
|
for (const key in next) {
|
|
// Reference equality fast path: most poll ticks pass the same object
|
|
// identity or genuinely new objects; the sorted stringify below runs
|
|
// on every key of a multi-hundred-KB payload twice per second
|
|
// otherwise.
|
|
if (next[key] === prev[key]) continue;
|
|
if (!jsonEqual(next[key], prev[key])) diff[key] = next[key];
|
|
}
|
|
|
|
if (Object.keys(diff).length === 0) {
|
|
return;
|
|
}
|
|
|
|
const merged = { ...prev, ...next };
|
|
this.value = merged;
|
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
|
}
|
|
|
|
reset<K extends keyof T>(keys?: K[]): void {
|
|
const prev = this.value;
|
|
const next = structuredClone(this.value);
|
|
|
|
if (keys && keys.length > 0) {
|
|
keys.forEach((key) => {
|
|
next[key] = structuredClone(this.initial[key]);
|
|
});
|
|
} else {
|
|
Object.assign(next, structuredClone(this.initial));
|
|
}
|
|
|
|
if (jsonEqual(prev, next)) return;
|
|
|
|
this.value = next;
|
|
|
|
const diff: Partial<T> = {};
|
|
for (const key in next) {
|
|
if (!jsonEqual(next[key], prev[key])) diff[key] = next[key];
|
|
}
|
|
|
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
|
}
|
|
|
|
subscribe(cb: Listener<T>): () => void {
|
|
this.listeners.add(cb);
|
|
cb(this.value, this.value, {});
|
|
return () => this.listeners.delete(cb);
|
|
}
|
|
|
|
unsubscribe(cb: Listener<T>): void {
|
|
this.listeners.delete(cb);
|
|
}
|
|
|
|
patch<K extends keyof T>(key: K, value: T[K]): void {
|
|
this.set({ [key]: value } as unknown as Partial<T>);
|
|
}
|
|
|
|
getKey<K extends keyof T>(key: K): T[K] {
|
|
return this.value[key];
|
|
}
|
|
|
|
subscribeKey<K extends keyof T>(
|
|
key: K,
|
|
cb: (value: T[K]) => void,
|
|
): () => void {
|
|
let prev = this.value[key];
|
|
const wrapper: Listener<T> = (val) => {
|
|
if (!jsonEqual(val[key], prev)) {
|
|
prev = val[key];
|
|
cb(val[key]);
|
|
}
|
|
};
|
|
this.listeners.add(wrapper);
|
|
return () => this.listeners.delete(wrapper);
|
|
}
|
|
}
|
|
|
|
export interface IDiagnosticsChecksItem {
|
|
state: 'error' | 'warning' | 'success';
|
|
key: string;
|
|
value: string;
|
|
}
|
|
|
|
export interface IDiagnosticsChecksStoreItem {
|
|
order: number;
|
|
code: string;
|
|
title: string;
|
|
description: string;
|
|
state: 'loading' | 'warning' | 'success' | 'error' | 'skipped';
|
|
items: Array<IDiagnosticsChecksItem>;
|
|
}
|
|
|
|
export interface StoreType {
|
|
tabService: {
|
|
current: string;
|
|
all: string[];
|
|
};
|
|
bandwidthWidget: {
|
|
loading: boolean;
|
|
failed: boolean;
|
|
data: { up: number; down: number };
|
|
};
|
|
trafficTotalWidget: {
|
|
loading: boolean;
|
|
failed: boolean;
|
|
data: { downloadTotal: number; uploadTotal: number };
|
|
};
|
|
systemInfoWidget: {
|
|
loading: boolean;
|
|
failed: boolean;
|
|
data: { connections: number; memory: number };
|
|
};
|
|
servicesInfoWidget: {
|
|
loading: boolean;
|
|
failed: boolean;
|
|
data: {
|
|
singbox: number;
|
|
tachyonRunning: number;
|
|
tachyonEnabled: number;
|
|
tachyonStatus: string;
|
|
watchdogRunning: number;
|
|
singboxMemoryMb?: number;
|
|
zapret2Running?: number;
|
|
zapret2MemoryMb?: number;
|
|
dnsmasqRunning?: number;
|
|
dnsmasqMemoryMb?: number;
|
|
dnsmasqLocalCacheEnabled?: number;
|
|
dnsmasqCacheSize?: number;
|
|
};
|
|
};
|
|
tailscaleWidget: {
|
|
loading: boolean;
|
|
failed: boolean;
|
|
data: Tachyon.GetTailscalePeers | null;
|
|
};
|
|
sectionsWidget: {
|
|
loading: boolean;
|
|
failed: boolean;
|
|
data: Tachyon.OutboundGroup[];
|
|
latencyFetchingSections: Record<string, boolean>;
|
|
latencyProgressSections: Record<string, Tachyon.LatencyActionProgress>;
|
|
selectorSwitchingSections: Record<string, string>;
|
|
subscriptionUpdatingSections: Record<string, boolean>;
|
|
};
|
|
diagnosticsRunAction: {
|
|
loading: boolean;
|
|
};
|
|
diagnosticsChecks: Array<IDiagnosticsChecksStoreItem>;
|
|
diagnosticsActions: {
|
|
restart: { loading: boolean };
|
|
start: { loading: boolean };
|
|
stop: { loading: boolean };
|
|
enable: { loading: boolean };
|
|
disable: { loading: boolean };
|
|
globalCheck: { loading: boolean };
|
|
doctor: { loading: boolean };
|
|
aiDoctor: { loading: boolean };
|
|
viewLogs: { loading: boolean };
|
|
showSingBoxConfig: { loading: boolean };
|
|
generateBugReport: { loading: boolean };
|
|
checkServices: { loading: boolean };
|
|
};
|
|
diagnosticsSystemInfo: {
|
|
loading: boolean;
|
|
loaded: boolean;
|
|
providerInfoLoaded: boolean;
|
|
tachyon_version: string;
|
|
tachyon_commit_sha: string;
|
|
tachyon_latest_version: string;
|
|
luci_app_version: string;
|
|
sing_box_version: string;
|
|
sing_box_extended: number;
|
|
sing_box_tiny: number;
|
|
sing_box_compressed: number;
|
|
sing_box_lx: number;
|
|
sing_box_tailscale: number;
|
|
sing_box_repo_url?: string;
|
|
sing_box_backup_version?: string;
|
|
sing_box_backup_time?: number;
|
|
zapret_version: string;
|
|
zapret_installed: number;
|
|
zapret_backup_version?: string;
|
|
zapret_backup_time?: number;
|
|
zapret2_version: string;
|
|
zapret2_installed: number;
|
|
zapret2_backup_version?: string;
|
|
zapret2_backup_time?: number;
|
|
byedpi_version: string;
|
|
byedpi_installed: number;
|
|
byedpi_backup_version?: string;
|
|
byedpi_backup_time?: number;
|
|
wdtt_version: string;
|
|
wdtt_installed: number;
|
|
wdtt_backup_version?: string;
|
|
wdtt_backup_time?: number;
|
|
olcrtc_version: string;
|
|
olcrtc_installed: number;
|
|
olcrtc_backup_version?: string;
|
|
olcrtc_backup_time?: number;
|
|
tailscale_version: string;
|
|
tailscale_installed: number;
|
|
tailscale_backup_version?: string;
|
|
tailscale_backup_time?: number;
|
|
fptn_version: string;
|
|
fptn_installed: number;
|
|
fptn_backup_version?: string;
|
|
fptn_backup_time?: number;
|
|
adguard_version?: string;
|
|
adguard_installed?: number;
|
|
adguard_enabled?: number;
|
|
adguard_backup_version?: string;
|
|
adguard_backup_time?: number;
|
|
server_inbounds_enabled_count: number;
|
|
direct_bypass_enabled?: number;
|
|
direct_bypass_address?: string;
|
|
direct_bypass_port?: string;
|
|
torrserver_running?: number;
|
|
torrserver_direct_available?: number;
|
|
torrserver_direct_enabled?: number;
|
|
torrserver_direct_active?: number;
|
|
openwrt_version: string;
|
|
device_model: string;
|
|
};
|
|
updatesActions: {
|
|
tachyonCheck: { loading: boolean };
|
|
tachyonInstall: { loading: boolean };
|
|
tachyonReinstall: { loading: boolean };
|
|
tachyonRollback: { loading: boolean };
|
|
singBoxCheck: { loading: boolean };
|
|
singBoxInstall: { loading: boolean };
|
|
singBoxRollback: { loading: boolean };
|
|
singBoxInstallExtended: { loading: boolean };
|
|
singBoxInstallExtendedCompressed: { loading: boolean };
|
|
singBoxInstallLx: { loading: boolean };
|
|
singBoxInstallTiny: { loading: boolean };
|
|
singBoxInstallStable: { loading: boolean };
|
|
zapretCheck: { loading: boolean };
|
|
zapretInstall: { loading: boolean };
|
|
zapretRemove: { loading: boolean };
|
|
zapretRollback: { loading: boolean };
|
|
zapret2Check: { loading: boolean };
|
|
zapret2Install: { loading: boolean };
|
|
zapret2Remove: { loading: boolean };
|
|
zapret2Rollback: { loading: boolean };
|
|
byedpiCheck: { loading: boolean };
|
|
byedpiInstall: { loading: boolean };
|
|
byedpiRemove: { loading: boolean };
|
|
byedpiRollback: { loading: boolean };
|
|
wdttCheck: { loading: boolean };
|
|
wdttInstall: { loading: boolean };
|
|
wdttRemove: { loading: boolean };
|
|
wdttRollback: { loading: boolean };
|
|
olcrtcCheck: { loading: boolean };
|
|
olcrtcInstall: { loading: boolean };
|
|
olcrtcRemove: { loading: boolean };
|
|
olcrtcRollback: { loading: boolean };
|
|
fptnCheck: { loading: boolean };
|
|
fptnInstall: { loading: boolean };
|
|
fptnRemove: { loading: boolean };
|
|
fptnRollback: { loading: boolean };
|
|
tailscaleCheck: { loading: boolean };
|
|
tailscaleInstall: { loading: boolean };
|
|
tailscaleRemove: { loading: boolean };
|
|
tailscaleRollback: { loading: boolean };
|
|
adguardCheck: { loading: boolean };
|
|
adguardInstall: { loading: boolean };
|
|
adguardRemove: { loading: boolean };
|
|
adguardRollback: { loading: boolean };
|
|
adguardEnable: { loading: boolean };
|
|
adguardDisable: { loading: boolean };
|
|
directBypassEnable: { loading: boolean };
|
|
directBypassDisable: { loading: boolean };
|
|
torrserverDirectEnable: { loading: boolean };
|
|
torrserverDirectDisable: { loading: boolean };
|
|
};
|
|
updatesChecks: Record<
|
|
Tachyon.ComponentName,
|
|
{
|
|
status: 'latest' | 'outdated' | 'dev' | 'outdated_same_release' | null;
|
|
latest_version: string;
|
|
release_url: string;
|
|
current_sha?: string;
|
|
latest_sha?: string;
|
|
}
|
|
>;
|
|
}
|
|
|
|
const initialStore: StoreType = {
|
|
tabService: {
|
|
current: '',
|
|
all: [],
|
|
},
|
|
bandwidthWidget: {
|
|
loading: true,
|
|
failed: false,
|
|
data: { up: 0, down: 0 },
|
|
},
|
|
trafficTotalWidget: {
|
|
loading: true,
|
|
failed: false,
|
|
data: { downloadTotal: 0, uploadTotal: 0 },
|
|
},
|
|
systemInfoWidget: {
|
|
loading: true,
|
|
failed: false,
|
|
data: { connections: 0, memory: 0 },
|
|
},
|
|
servicesInfoWidget: {
|
|
loading: true,
|
|
failed: false,
|
|
data: {
|
|
singbox: 0,
|
|
tachyonRunning: 0,
|
|
tachyonEnabled: 0,
|
|
tachyonStatus: '',
|
|
watchdogRunning: 0,
|
|
},
|
|
},
|
|
tailscaleWidget: {
|
|
loading: true,
|
|
failed: false,
|
|
data: null,
|
|
},
|
|
sectionsWidget: {
|
|
loading: true,
|
|
failed: false,
|
|
latencyFetchingSections: {},
|
|
latencyProgressSections: {},
|
|
selectorSwitchingSections: {},
|
|
subscriptionUpdatingSections: {},
|
|
data: [],
|
|
},
|
|
...initialDiagnosticStore,
|
|
};
|
|
|
|
export const store = new StoreService<StoreType>(initialStore);
|