import { createConsoleState } from '@modrinth/ui' import { clear_log_buffer, get_live_log_buffer, get_logs, get_minecraft_latest_log_cursor, } from '@/helpers/logs' import { createLatestLogReader } from './latest-log-reader' type ConsoleState = ReturnType interface LogEntry { filename: string name?: string log_type: string output?: string | null age?: number live?: boolean } interface InstanceConsoleEntry { liveConsole: ConsoleState historicalConsole: ConsoleState historicalCache: Map logList: LogEntry[] | null liveHistoryHydration: Promise | null latestLog: ReturnType } const instances = new Map() function getOrCreate(instanceId: string): InstanceConsoleEntry { let entry = instances.get(instanceId) if (entry) return entry const liveConsole = createConsoleState() entry = { liveConsole, historicalConsole: createConsoleState(), historicalCache: new Map(), logList: null, liveHistoryHydration: null, latestLog: createLatestLogReader( (cursor) => get_minecraft_latest_log_cursor(instanceId, cursor), (output, replace) => { if (replace) liveConsole.clear() void liveConsole.addLegacyLog(output) }, ), } instances.set(instanceId, entry) return entry } async function hydrate(instanceId: string): Promise { const entry = getOrCreate(instanceId) if (entry.liveHistoryHydration) { return entry.liveHistoryHydration } const hydration = (async () => { await entry.latestLog.refresh().catch(console.warn) if (!entry.latestLog.active && entry.liveConsole.output.value.length === 0) { const buffer = await get_live_log_buffer(instanceId) if (!entry.latestLog.active && entry.liveConsole.output.value.length === 0) { await entry.liveConsole.addLegacyLog(buffer) } } })() entry.liveHistoryHydration = hydration try { await hydration } finally { if (entry.liveHistoryHydration === hydration) { entry.liveHistoryHydration = null } } } async function getHistoricalLogs(instanceId: string): Promise { const entry = getOrCreate(instanceId) if (entry.logList) return entry.logList const logs: LogEntry[] = await get_logs(instanceId, true) entry.logList = logs for (const log of logs) { if (log.output) { entry.historicalCache.set(log.filename, log.output) } } return logs } function getHistoricalContent(instanceId: string, filename: string): string | undefined { return instances.get(instanceId)?.historicalCache.get(filename) } function invalidate(instanceId: string): void { const entry = instances.get(instanceId) if (!entry) return entry.historicalCache.clear() entry.logList = null } async function clearLive(instanceId: string): Promise { const entry = getOrCreate(instanceId) entry.latestLog.clear() entry.liveConsole.clear() await clear_log_buffer(instanceId).catch(() => {}) } async function destroy(instanceId: string): Promise { instances.get(instanceId)?.latestLog.reset() instances.delete(instanceId) await clear_log_buffer(instanceId).catch(() => {}) } export function useInstanceConsole(instanceId: string) { const entry = getOrCreate(instanceId) return { liveConsole: entry.liveConsole, historicalConsole: entry.historicalConsole, hydrate: () => hydrate(instanceId), getHistoricalLogs: () => getHistoricalLogs(instanceId), getHistoricalContent: (filename: string) => getHistoricalContent(instanceId, filename), invalidate: () => invalidate(instanceId), clearLive: () => clearLive(instanceId), refreshLive: () => entry.latestLog.refresh(), flushLive: () => entry.latestLog.flush(), acceptsProcessLogs: () => !entry.latestLog.active, resetLive: () => { entry.latestLog.reset() entry.liveConsole.clear() }, destroy: () => destroy(instanceId), } }