File size: 6,150 Bytes
391c43e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | /**
* Debug Events State Management
* Handles debug events persistence and state management with IndexedDB
*/
import { logger } from '@/lib/utils';
export interface DebugEvent {
id: string;
timestamp: number;
event: string;
data: any;
}
// Ephemeral events are only kept in memory for live viewing, not persisted to IndexedDB
// This prevents O(N²) memory growth from streaming token snapshots
const EPHEMERAL_EVENTS = new Set([
'assistant_delta',
'tool_param_delta',
'reasoning_delta',
'llm_request',
'stream_raw_chunk'
]);
interface StoredDebugEventsState {
id: string;
projectId: string;
events: DebugEvent[];
lastUpdated: string;
}
export class DebugEventsStateManager {
private eventsCache: Map<string, DebugEvent[]> = new Map();
private storeName = 'debugEvents';
private isInitialized = false;
/**
* Initialize by ensuring VFS database is ready
*/
private async initDB(): Promise<void> {
if (this.isInitialized) return;
// Import vfs dynamically to avoid circular dependency
const { vfs } = await import('@/lib/vfs');
await vfs.init();
this.isInitialized = true;
}
/**
* Get shared database connection from VFS
*/
private async getDB(): Promise<IDBDatabase> {
const { vfs } = await import('@/lib/vfs');
return vfs.getDatabase();
}
/**
* Get debug events ID for a project
*/
private getDebugEventsId(projectId: string): string {
return `debug_events_${projectId}`;
}
/**
* Load debug events from IndexedDB
*/
async loadEvents(projectId: string): Promise<DebugEvent[]> {
await this.initDB();
const db = await this.getDB();
const eventsId = this.getDebugEventsId(projectId);
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(eventsId);
request.onsuccess = () => {
const stored = request.result as StoredDebugEventsState;
if (stored && stored.events) {
this.eventsCache.set(projectId, stored.events);
resolve(stored.events);
} else {
resolve([]);
}
};
request.onerror = () => {
logger.error('Failed to load debug events from DB');
reject(request.error);
};
});
}
/**
* Save debug events to IndexedDB
*/
async saveEvents(projectId: string, events: DebugEvent[]): Promise<void> {
await this.initDB();
const db = await this.getDB();
const storedState: StoredDebugEventsState = {
id: this.getDebugEventsId(projectId),
projectId,
events,
lastUpdated: new Date().toISOString()
};
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put(storedState);
request.onsuccess = () => {
this.eventsCache.set(projectId, events);
resolve();
};
request.onerror = () => {
logger.error('Failed to save debug events to DB');
reject(request.error);
};
});
}
/**
* Append a new debug event
* Ephemeral events (deltas) are only kept in memory, not persisted to IndexedDB
*/
async appendEvent(projectId: string, event: DebugEvent): Promise<void> {
let events = this.eventsCache.get(projectId);
if (!events) {
events = await this.loadEvents(projectId);
}
events.push(event);
this.eventsCache.set(projectId, events);
// Skip persisting ephemeral streaming events - they're only useful during live viewing
// This prevents massive IndexedDB growth from streaming tokens
if (EPHEMERAL_EVENTS.has(event.event)) {
return;
}
// Save meaningful events to DB - await to prevent race conditions with rapid event appends
await this.saveEvents(projectId, events);
}
/**
* Clear all debug events for a project
*/
async clearEvents(projectId: string): Promise<void> {
this.eventsCache.delete(projectId);
await this.saveEvents(projectId, []);
logger.debug(`[DebugEventsState] Cleared debug events for project ${projectId}`);
}
/**
* Truncate debug events to a specific set (used for retry)
*/
async truncateEvents(projectId: string, events: DebugEvent[]): Promise<void> {
this.eventsCache.set(projectId, events);
await this.saveEvents(projectId, events);
logger.debug(`[DebugEventsState] Truncated debug events for project ${projectId} to ${events.length} events`);
}
/**
* Get debug events for a project (from cache or DB)
*/
async getEvents(projectId: string): Promise<DebugEvent[]> {
let events = this.eventsCache.get(projectId);
if (!events) {
events = await this.loadEvents(projectId);
}
return events;
}
/**
* Delete all debug events for a project
*/
async deleteProject(projectId: string): Promise<void> {
await this.initDB();
const db = await this.getDB();
const eventsId = this.getDebugEventsId(projectId);
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(eventsId);
request.onsuccess = () => {
this.eventsCache.delete(projectId);
resolve();
};
request.onerror = () => {
logger.error('Failed to delete debug events from DB');
reject(request.error);
};
});
}
/**
* Unload debug events for a project from memory cache
* Data remains in IndexedDB and can be reloaded on demand
* This is called when leaving a project to free up memory
*/
unloadProject(projectId: string): void {
const hadCache = this.eventsCache.has(projectId);
this.eventsCache.delete(projectId);
if (hadCache) {
logger.debug(`[DebugEventsState] Unloaded debug events cache for project ${projectId}`);
}
}
}
export const debugEventsState = new DebugEventsStateManager();
|