diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..d0794db --- /dev/null +++ b/src/state.ts @@ -0,0 +1,218 @@ +import type { ChatSession, GroupMessage, BucketEntry, LogEntry, ModelInfo, TgUser, ExecutionContext } from './types'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let cfg: any = null as any; +let API_URL = ""; +let BOT_INFO: TgUser | null = null; +let BOT_SELF_ID: number | null = null; +let isInitialized = false; +let maintenanceCache: { value: boolean; ts: number } | null = null; +let requestOrigin = ""; +let globalCtx: ExecutionContext | null = null; + +const runtimeStatus = { + startedAt: Date.now(), + scheduler: { + running: false, + lastRunAt: null as number | null, + lastSuccessAt: null as number | null, + lastErrorAt: null as number | null, + lastError: null as string | null, + }, + polling: { + running: false, + lastPollAt: null as number | null, + lastUpdateAt: null as number | null, + lastUpdateId: null as number | null, + lastErrorAt: null as number | null, + lastError: null as string | null, + }, +}; + +const activeRequests = new Map>(); +const callbackRateLimits = new Map(); +const sessionLoadLocks = new Map>(); +const groupContextCache = new Map(); +const modelListStates = new Map(); +const adminPanelStates = new Map(); +const broadcastStates = new Map(); + +let globalDisabledKeys: Record = {}; +let _saveDisabledKeysTimer: ReturnType | null = null; +let lastDisabledKeysFetch = 0; + +const activeProgressMessages = new Map(); +const lastTypingSent = new Map(); + +// ───────────────────────────────────────────── +// SECTION: LOGGER +// ───────────────────────────────────────────── + +const MAX_LOGS = 100; +const recentLogs: LogEntry[] = []; + +function log(level: LogEntry["level"], message: string, context?: unknown) { + if (level === "info") { + const entry: LogEntry = { timestamp: Date.now(), level, message, context }; + recentLogs.push(entry); + if (recentLogs.length > MAX_LOGS) recentLogs.shift(); + return; + } + + const entry: LogEntry = { timestamp: Date.now(), level, message, context }; + const fn = level === "error" ? console.error : console.warn; + fn(`[${level.toUpperCase()}] ${message}`, context ?? ""); + recentLogs.push(entry); + if (recentLogs.length > MAX_LOGS) recentLogs.shift(); +} + +const logger = { + info: (msg: string, ctx?: unknown) => log("info", msg, ctx), + warn: (msg: string, ctx?: unknown) => log("warn", msg, ctx), + error: (msg: string, ctx?: unknown) => log("error", msg, ctx), +}; + +// ───────────────────────────────────────────── +// SECTION: CACHE (LRU eviction) +// ───────────────────────────────────────────── + +class CacheLayer { + private store = new Map(); + constructor(private maxSize = 500, private defaultTTL = 5 * 60 * 1000) {} + + set(key: string, value: T, ttl?: number): void { + if (this.store.size >= this.maxSize) { + let lruKey = ""; + let lruTime = Infinity; + for (const [k, v] of this.store) { + if (v.lastAccess < lruTime) { lruTime = v.lastAccess; lruKey = k; } + } + if (lruKey) this.store.delete(lruKey); + } + this.store.set(key, { data: value, expires: Date.now() + (ttl ?? this.defaultTTL), lastAccess: Date.now() }); + } + + get(key: string): T | null { + const e = this.store.get(key); + if (!e) return null; + if (Date.now() > e.expires) { this.store.delete(key); return null; } + e.lastAccess = Date.now(); + return e.data; + } + + delete(key: string): void { this.store.delete(key); } + clear(): void { this.store.clear(); } + size(): number { return this.store.size; } +} + +class TokenBucket { + private tokens: number; + private lastRefill: number; + constructor(private capacity: number, private refillRate: number) { + this.tokens = capacity; + this.lastRefill = Date.now(); + } + tryConsume(n = 1): boolean { + const elapsed = (Date.now() - this.lastRefill) / 1000; + this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate); + this.lastRefill = Date.now(); + if (this.tokens >= n) { this.tokens -= n; return true; } + return false; + } + availableTokens(): number { + const elapsed = (Date.now() - this.lastRefill) / 1000; + return Math.min(this.capacity, Math.floor(this.tokens + elapsed * this.refillRate)); + } +} + +const sessionCache = new CacheLayer(200, 3 * 60 * 1000); +const modelCache = new CacheLayer(10, 30 * 60 * 1000); + +const userBucketStore = new Map(); + +let elevenLabsKeyIndex = 0; +let cfAccountIndex = 0; +const MODEL_CACHE_TTL = 12 * 60 * 60 * 1000; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let _cachedBotConfig: any | null = null; +let _configCacheTs = 0; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const TOOLS_CACHE = { user: null as any, admin: null as any }; + +const COAXYS_OWNER_ID = 7826271898; + +function setCfg(val: typeof cfg) { cfg = val; } +function setApiUrl(val: string) { API_URL = val; } +function setBotInfo(val: TgUser | null) { BOT_INFO = val; } +function setBotSelfId(val: number | null) { BOT_SELF_ID = val; } +function setIsInitialized(val: boolean) { isInitialized = val; } +function setMaintenanceCache(val: { value: boolean; ts: number } | null) { maintenanceCache = val; } +function setRequestOrigin(val: string) { requestOrigin = val; } +function setGlobalCtx(val: ExecutionContext | null) { globalCtx = val; } +function setGlobalDisabledKeys(val: Record) { globalDisabledKeys = val; } +function setSaveDisabledKeysTimer(val: ReturnType | null) { _saveDisabledKeysTimer = val; } +function setLastDisabledKeysFetch(val: number) { lastDisabledKeysFetch = val; } +function setCachedBotConfig(val: any | null) { _cachedBotConfig = val; } +function setConfigCacheTs(val: number) { _configCacheTs = val; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setToolsCache(key: 'user' | 'admin', val: any) { TOOLS_CACHE[key] = val; } +function setElevenLabsKeyIndex(val: number) { elevenLabsKeyIndex = val; } +function setCfAccountIndex(val: number) { cfAccountIndex = val; } + +export { + cfg, + API_URL, + BOT_INFO, + BOT_SELF_ID, + isInitialized, + maintenanceCache, + requestOrigin, + globalCtx, + runtimeStatus, + activeRequests, + callbackRateLimits, + sessionLoadLocks, + groupContextCache, + modelListStates, + adminPanelStates, + broadcastStates, + globalDisabledKeys, + _saveDisabledKeysTimer, + lastDisabledKeysFetch, + activeProgressMessages, + lastTypingSent, + MAX_LOGS, + recentLogs, + log, + logger, + CacheLayer, + TokenBucket, + sessionCache, + modelCache, + userBucketStore, + elevenLabsKeyIndex, + cfAccountIndex, + MODEL_CACHE_TTL, + _cachedBotConfig, + _configCacheTs, + TOOLS_CACHE, + COAXYS_OWNER_ID, + setCfg, + setApiUrl, + setBotInfo, + setBotSelfId, + setIsInitialized, + setMaintenanceCache, + setRequestOrigin, + setGlobalCtx, + setGlobalDisabledKeys, + setSaveDisabledKeysTimer, + setLastDisabledKeysFetch, + setCachedBotConfig, + setConfigCacheTs, + setToolsCache, + setElevenLabsKeyIndex, + setCfAccountIndex, +};