/** * State - 轻量级响应式状态管理 */ import type { StateKey, ChatSession } from '../types.js'; import type { ChatDB } from '../db/chat-db.js'; import type { OllamaAPI } from '../api/ollama.js'; function shallowEqual(a: unknown, b: unknown): boolean { if (a === b) return true; if (a == null || b == null) return a === b; if (typeof a !== 'object' || typeof b !== 'object') return false; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } if (Array.isArray(a) !== Array.isArray(b)) return false; const keysA = Object.keys(a as object); const keysB = Object.keys(b as object); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!Object.prototype.hasOwnProperty.call(b, key) || (a as Record)[key] !== (b as Record)[key]) return false; } return true; } type Listener = (value: unknown, old: unknown) => void; class AppState { private _state: Record = {}; private _listeners = new Map>(); private _batching = false; private _batchQueue: Set | null = null; set(key: string, value: unknown): void { const old = this._state[key]; this._state[key] = value; if (!shallowEqual(old, value)) { if (this._batching) { this._batchQueue!.add(key); } else { this._notify(key, value, old); } } } update(key: string, updater: (old: any) => any): void { const old = this._state[key]; const value = updater(old); this._state[key] = value; if (!shallowEqual(old, value)) { if (this._batching) { this._batchQueue!.add(key); } else { this._notify(key, value, old); } } } setIn(key: string, path: (string | number)[], value: unknown): void { if (!path || path.length === 0) { this.set(key, value); return; } const old = this._state[key]; if (old == null || typeof old !== 'object') { return; } const newRoot = Array.isArray(old) ? [...(old as unknown[])] : { ...(old as Record) }; let current: Record = newRoot as Record; for (let i = 0; i < path.length - 1; i++) { const segment = path[i]; const next = current[segment as string]; if (next == null || typeof next !== 'object') { const isIndex = typeof path[i + 1] === 'number'; current[segment as string] = isIndex ? [] : {}; } else { current[segment as string] = Array.isArray(next) ? [...next] : { ...(next as Record) }; } current = current[segment as string] as Record; } current[path[path.length - 1] as string] = value; this._state[key] = newRoot; if (!shallowEqual(old, newRoot)) { if (this._batching) { this._batchQueue!.add(key); } else { this._notify(key, newRoot, old); } } } batch(fn: () => void): void { this._batching = true; this._batchQueue = new Set(); try { fn(); } finally { const keys = this._batchQueue; this._batching = false; this._batchQueue = null; for (const k of keys) { this._notify(k, this._state[k], undefined); } } } private _notify(key: string, value: unknown, old: unknown): void { if (!this._listeners.has(key)) return; for (const cb of this._listeners.get(key)!) { try { cb(value, old); } catch { /* 回调异常不影响其他监听器 */ } } } get(key: string, defaultValue?: T): T { return (key in this._state ? this._state[key] : defaultValue) as T; } on(key: string, callback: Listener): () => void { if (!this._listeners.has(key)) { this._listeners.set(key, new Set()); } this._listeners.get(key)!.add(callback); return () => this._listeners.get(key)?.delete(callback); } init(initial: Record): void { Object.assign(this._state, initial); } } export const state = new AppState(); export const KEYS = { DB: 'db', API: 'api', CURRENT_SESSION: 'currentSession', IS_STREAMING: 'isStreaming', IS_HISTORY_VIEW: 'isHistoryView', PENDING_IMAGES: 'pendingImages', ABORT_CONTROLLER: 'abortController', NUM_CTX: 'numCtx', HISTORY_PAGE: 'historyPage', HISTORY_SEARCH: 'historySearchQuery', } as const;