/** * Console event handling with lifecycle hooks */ import type { WebDriver } from 'selenium-webdriver'; import type { ConsoleMessage } from '../types.js'; import { logDebug } from '../../utils/logger.js'; // Memory protection constants const MAX_CONSOLE_MESSAGES = 1000; // Maximum number of messages to keep const CONSOLE_TTL_MS = 5 * 60 * 1000; // 5 minutes TTL for old messages export interface ConsoleEventsOptions { /** Callback triggered on navigation events (for auto-clear) */ onNavigate?: (contextId?: string) => void; /** Auto-clear console on navigation (default: false - changed to prevent losing logs) */ autoClearOnNavigate?: boolean; } export class ConsoleEvents { private consoleMessages: ConsoleMessage[] = []; private subscribed = false; private options: ConsoleEventsOptions; constructor( private driver: WebDriver, options: ConsoleEventsOptions = {} ) { this.options = { autoClearOnNavigate: false, // Changed default to false to preserve logs across tabs ...options, }; } /** * Subscribe to BiDi console events and navigation lifecycle */ async subscribe(contextId?: string): Promise { if (this.subscribed) { return; } const bidi = await this.driver.getBidi(); await bidi.subscribe('log.entryAdded', contextId ? [contextId] : undefined); // Subscribe to navigation events for lifecycle hooks try { await bidi.subscribe('browsingContext.load', contextId ? [contextId] : undefined); await bidi.subscribe('browsingContext.domContentLoaded', contextId ? [contextId] : undefined); } catch { logDebug( 'Navigation events subscription skipped (may not be available in this Firefox version)' ); } const ws: any = bidi.socket; ws.on('message', (data: any) => { try { const payload = JSON.parse(data.toString()); // Handle console messages if (payload?.method === 'log.entryAdded') { const entry = payload.params; // The tab a message came from is what lets a reply carry the console // output of the page it acted on rather than every page at once. const message: ConsoleMessage & { context?: string } = { level: (entry.level as ConsoleMessage['level']) || 'info', text: entry.text || (entry.args ? JSON.stringify(entry.args) : ''), timestamp: entry.timestamp || Date.now(), source: entry.source?.realm, args: entry.args, context: entry.source?.context, }; this.consoleMessages.push(message); logDebug(`Console [${message.level}]: ${message.text}`); } // Handle navigation lifecycle events if ( payload?.method === 'browsingContext.load' || payload?.method === 'browsingContext.domContentLoaded' ) { if (this.options.autoClearOnNavigate) { this.clearMessages(); } if (this.options.onNavigate) { // Which page navigated decides whose page state is now wrong; without // it every tab has to be treated as having navigated. this.options.onNavigate(payload.params?.context); } } } catch { // ignore parse errors } }); this.subscribed = true; logDebug('Console listener active with lifecycle hooks'); } /** * Get all collected console messages */ getMessages(): ConsoleMessage[] { this.cleanupOldMessages(); return [...this.consoleMessages]; } /** * Drop console messages and report how many went. * * The predicate exists so one agent can clear its own tab without wiping the * buffer every other agent in the shared browser is reading from. */ clearMessages(shouldClear?: (message: ConsoleMessage) => boolean): number { if (!shouldClear) { const cleared = this.consoleMessages.length; this.consoleMessages = []; return cleared; } const before = this.consoleMessages.length; this.consoleMessages = this.consoleMessages.filter((message) => !shouldClear(message)); return before - this.consoleMessages.length; } /** * Remove old messages based on TTL and buffer size limit */ private cleanupOldMessages(): void { const now = Date.now(); const cutoffTime = now - CONSOLE_TTL_MS; // Remove messages older than TTL this.consoleMessages = this.consoleMessages.filter( (msg) => msg.timestamp && msg.timestamp >= cutoffTime ); // Enforce max buffer size (keep most recent messages) if (this.consoleMessages.length > MAX_CONSOLE_MESSAGES) { const excess = this.consoleMessages.length - MAX_CONSOLE_MESSAGES; this.consoleMessages.splice(0, excess); logDebug( `Console buffer limit reached: removed ${excess} oldest message(s) (max: ${MAX_CONSOLE_MESSAGES})` ); } } }