import { Tachyon } from '../types'; import { initialDiagnosticStore } from '../tabs/diagnostic/diagnostic.store'; function jsonStableStringify(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, ); } return value; }); } function jsonEqual(a: A, b: B): boolean { try { return jsonStableStringify(a) === jsonStableStringify(b); } catch { return false; } } type Listener = (next: T, prev: T, diff: Partial) => void; // eslint-disable-next-line export class StoreService> { private value: T; private readonly initial: T; private listeners = new Set>(); constructor(initial: T) { this.value = initial; this.initial = structuredClone(initial); } get(): T { return this.value; } set(next: Partial): void { const prev = this.value; const diff: Partial = {}; for (const key in next) { 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(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 = {}; 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): () => void { this.listeners.add(cb); cb(this.value, this.value, {}); return () => this.listeners.delete(cb); } unsubscribe(cb: Listener): void { this.listeners.delete(cb); } patch(key: K, value: T[K]): void { this.set({ [key]: value } as unknown as Partial); } getKey(key: K): T[K] { return this.value[key]; } subscribeKey( key: K, cb: (value: T[K]) => void, ): () => void { let prev = this.value[key]; const wrapper: Listener = (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; } 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; }; }; sectionsWidget: { loading: boolean; failed: boolean; data: Tachyon.OutboundGroup[]; latencyFetchingSections: Record; latencyProgressSections: Record; selectorSwitchingSections: Record; subscriptionUpdatingSections: Record; }; diagnosticsRunAction: { loading: boolean; }; diagnosticsChecks: Array; 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 }; }; 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; zapret_version: string; zapret_installed: number; zapret2_version: string; zapret2_installed: number; byedpi_version: string; byedpi_installed: number; server_inbounds_enabled_count: number; openwrt_version: string; device_model: string; }; updatesActions: { tachyonCheck: { loading: boolean }; tachyonInstall: { loading: boolean }; tachyonReinstall: { loading: boolean }; singBoxCheck: { loading: boolean }; singBoxInstall: { 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 }; zapret2Check: { loading: boolean }; zapret2Install: { loading: boolean }; zapret2Remove: { loading: boolean }; byedpiCheck: { loading: boolean }; byedpiInstall: { loading: boolean }; byedpiRemove: { 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, }, }, sectionsWidget: { loading: true, failed: false, latencyFetchingSections: {}, latencyProgressSections: {}, selectorSwitchingSections: {}, subscriptionUpdatingSections: {}, data: [], }, ...initialDiagnosticStore, }; export const store = new StoreService(initialStore);