import { Podkop } from './podkop/types'; 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 class Store> { private value: T; private readonly initial: T; private listeners = new Set>(); private lastHash = ''; constructor(initial: T) { this.value = initial; this.initial = structuredClone(initial); this.lastHash = jsonStableStringify(initial); } get(): T { return this.value; } set(next: Partial): void { const prev = this.value; const merged = { ...prev, ...next }; if (jsonEqual(prev, merged)) return; this.value = merged; this.lastHash = jsonStableStringify(merged); const diff: Partial = {}; for (const key in merged) { if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key]; } this.listeners.forEach((cb) => cb(this.value, prev, diff)); } reset(): void { const prev = this.value; const next = structuredClone(this.initial); if (jsonEqual(prev, next)) return; this.value = next; this.lastHash = jsonStableStringify(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 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; podkop: number }; }; sectionsWidget: { loading: boolean; failed: boolean; data: Podkop.OutboundGroup[]; latencyFetching: boolean; }; } 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, podkop: 0 }, }, sectionsWidget: { loading: true, failed: false, latencyFetching: false, data: [], }, }; export const store = new Store(initialStore);