diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1be07ae1..c49740f5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,6 +6,9 @@ import Layout from './components/Layout'; import LoadingState from './components/LoadingState'; import { BrowserRouter as Router } from 'react-router-dom'; import { comm } from './utils/websocket'; +import { createLogger } from './lib/logger'; + +const logger = createLogger('App'); const App = () => { const [components, setComponents] = useState({ rows: [] }); @@ -17,7 +20,7 @@ const App = () => { useEffect(() => { comm.connect(); - console.log('[App] Connected to comm'); + logger.debug('Connected to comm'); const unsubscribe = comm.subscribe(handleMessage); return () => { @@ -40,7 +43,7 @@ const App = () => { }, [config]); const handleMessage = (message) => { - console.log('[App] Received message:', message); + logger.debug('Received message:', message); switch (message.type) { case 'components': @@ -74,7 +77,7 @@ const App = () => { case 'initial_state': // Handle initial state with bulk processing - console.log('[App] Received initial state:', message); + logger.debug('Received initial state:', message); if (message.states) { handleBulkStateUpdate(message.states); } @@ -84,15 +87,15 @@ const App = () => { const handleBulkStateUpdate = (stateUpdates) => { const startTime = performance.now(); - + try { if (!stateUpdates || typeof stateUpdates !== 'object') { - console.warn('[App] Invalid bulk state updates received:', stateUpdates); + logger.warn('Invalid bulk state updates received:', stateUpdates); return; } const updateCount = Object.keys(stateUpdates).length; - console.log(`[App] Processing bulk state update: ${updateCount} components`); + logger.debug(`Processing bulk state update: ${updateCount} components`); // Apply bulk state updates to current components setComponents((prevState) => { @@ -119,13 +122,13 @@ const App = () => { ); const processingTime = performance.now() - startTime; - console.log(`[App] Bulk state update applied: ${updateCount} components in ${processingTime.toFixed(2)}ms`); + logger.debug(`Bulk state update applied: ${updateCount} components in ${processingTime.toFixed(2)}ms`); return { rows: updatedRows }; }); } catch (error) { - console.error('[App] Error processing bulk state update:', error); + logger.error('Error processing bulk state update:', error); setError('Error processing bulk state update'); } }; @@ -133,7 +136,7 @@ const App = () => { const refreshComponentsList = async (components) => { if (!components || !components.rows) { setAreComponentsLoading(false); - console.warn('[App] Invalid components data received:', components); + logger.warn('Invalid components data received:', components); setComponents({ rows: [] }); return; } @@ -178,7 +181,7 @@ const App = () => { const updatedRows = components.rows.map((row) => row.map((component) => { if (!component || !component.id) { - console.warn('[App] Invalid component found during component refresh:', component); + logger.warn('Invalid component found during component refresh:', component); return component; } @@ -192,7 +195,7 @@ const App = () => { ); const processingTime = performance.now() - startTime; - + // Enhanced performance metrics for production monitoring const metrics = { componentCount: componentIds.length, @@ -201,14 +204,14 @@ const App = () => { batchCount: Math.ceil(componentIds.length / batchSize), timestamp: new Date().toISOString() }; - - console.debug(`[App] Enhanced bulk component processing completed: ${componentIds.length} components in ${processingTime.toFixed(2)}ms`, { metrics }); - + + logger.debug(`Enhanced bulk component processing completed: ${componentIds.length} components in ${processingTime.toFixed(2)}ms`, { metrics }); + setAreComponentsLoading(false); setComponents({ rows: updatedRows }); setError(null); } catch (error) { - console.error('[App] Error processing components:', error); + logger.error('Error processing components:', error); setAreComponentsLoading(false); setError('Error processing components data'); setComponents({ rows: [] }); @@ -216,7 +219,7 @@ const App = () => { }; const handleError = (errorContent) => { - console.error('[App] Received error:', errorContent); + logger.error('Received error:', errorContent); setAreComponentsLoading(false); setError(errorContent.message); @@ -238,7 +241,7 @@ const App = () => { }; const handleTransformErrors = (errorContents, components = null) => { - console.error('[App] Received transform errors:', {errorContents, components}); + logger.error('Received transform errors:', {errorContents, components}); setAreComponentsLoading(false); setTransformErrors(errorContents || []); if (components) { @@ -250,7 +253,7 @@ const App = () => { try { comm.updateComponentState(componentId, value); } catch (error) { - console.error('[App] Error updating component state:', error); + logger.error('Error updating component state:', error); setComponents((prevState) => { if (!prevState || !prevState.rows) return { rows: [] }; @@ -267,21 +270,21 @@ const App = () => { const processBulkComponentUpdates = async (updates) => { const startTime = performance.now(); - + try { if (!updates || typeof updates !== 'object') { - console.warn('[App] Invalid bulk component updates:', updates); + logger.warn('Invalid bulk component updates:', updates); return; } const updateCount = Object.keys(updates).length; - console.log(`[App] Processing bulk component update: ${updateCount} components`); + logger.debug(`Processing bulk component update: ${updateCount} components`); // Use the communication layer's bulk update capability const result = await comm.bulkStateUpdate(updates); - + const processingTime = performance.now() - startTime; - console.log(`[App] Bulk component update completed in ${processingTime.toFixed(2)}ms:`, { + logger.debug(`Bulk component update completed in ${processingTime.toFixed(2)}ms:`, { totalProcessed: result.totalProcessed, successCount: result.successCount, localChanges: result.localChanges, @@ -295,7 +298,7 @@ const App = () => { const updatedRows = prevState.rows.map((row) => row.map((component) => { if (!component || !component.id) { - console.warn('[App] Invalid component found during bulk component update:', component); + logger.warn('Invalid component found during bulk component update:', component); return component; } @@ -316,13 +319,13 @@ const App = () => { }); } catch (error) { - console.error('[App] Error processing bulk component update:', error); + logger.error('Error processing bulk component update:', error); setError('Error processing bulk component update'); } }; const updateConnectionStatus = (message) => { - console.log('[App] Updating connection status:', message); + logger.debug('Updating connection status:', message); setIsConnected(message.connected); setError(message.connected ? null : 'Lost connection. Attempting to reconnect...'); }; diff --git a/frontend/src/backend/service.js b/frontend/src/backend/service.js index ddf3f498..2771dca3 100644 --- a/frontend/src/backend/service.js +++ b/frontend/src/backend/service.js @@ -1,4 +1,7 @@ import * as Comlink from 'comlink'; +import { createLogger } from '../lib/logger'; + +const logger = createLogger('Service'); // import PreswaldWorker from './worker.js?worker&inline'; // ← change @@ -7,18 +10,18 @@ let workerInstance = null; export function createWorker() { // If we're already initialized, return the existing worker if (workerInstance) { - console.log('[Service] Reusing existing worker instance'); + logger.debug('[Service] Reusing existing worker instance'); return workerInstance; } - console.log('[Service] Starting new worker initialization'); + logger.debug('[Service] Starting new worker initialization'); try { const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }); // const worker = new PreswaldWorker(); // ← no URL needed workerInstance = Comlink.wrap(worker); return workerInstance; } catch (error) { - console.error('[Service] Worker initialization failed:', error); + logger.error('[Service] Worker initialization failed:', error); workerInstance = null; throw error; } diff --git a/frontend/src/lib/logger.js b/frontend/src/lib/logger.js new file mode 100644 index 00000000..8e089e83 --- /dev/null +++ b/frontend/src/lib/logger.js @@ -0,0 +1,106 @@ +/** + * Production-ready logging utility with configurable log levels + * Automatically disables debug logs in production builds + */ + +const LogLevel = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + NONE: 4 +}; + +// Determine log level based on environment +const getDefaultLogLevel = () => { + if (typeof process !== 'undefined' && process.env) { + // Node.js environment + if (process.env.NODE_ENV === 'production') { + return LogLevel.WARN; + } + if (process.env.NODE_ENV === 'test') { + return LogLevel.ERROR; + } + } + + // Browser environment - check for debug flag + if (typeof window !== 'undefined') { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has('debug')) { + return LogLevel.DEBUG; + } + // Check for production indicators + if (window.location.hostname !== 'localhost' && + !window.location.hostname.includes('127.0.0.1') && + !window.location.hostname.includes('dev.')) { + return LogLevel.WARN; + } + } + + return LogLevel.DEBUG; +}; + +class Logger { + constructor(namespace = 'App', level = null) { + this.namespace = namespace; + this.level = level !== null ? level : getDefaultLogLevel(); + } + + _shouldLog(level) { + return level >= this.level; + } + + _formatMessage(level, message) { + return `[${this.namespace}] ${message}`; + } + + debug(message, ...args) { + if (this._shouldLog(LogLevel.DEBUG)) { + console.debug(this._formatMessage('DEBUG', message), ...args); + } + } + + info(message, ...args) { + if (this._shouldLog(LogLevel.INFO)) { + console.info(this._formatMessage('INFO', message), ...args); + } + } + + warn(message, ...args) { + if (this._shouldLog(LogLevel.WARN)) { + console.warn(this._formatMessage('WARN', message), ...args); + } + } + + error(message, ...args) { + if (this._shouldLog(LogLevel.ERROR)) { + console.error(this._formatMessage('ERROR', message), ...args); + } + } + + // Set log level dynamically + setLevel(level) { + this.level = level; + } +} + +// Create default logger instance +const defaultLogger = new Logger('App'); + +// Factory function for creating namespaced loggers +export const createLogger = (namespace) => { + return new Logger(namespace); +}; + +// Export default logger methods +export const logger = { + debug: (...args) => defaultLogger.debug(...args), + info: (...args) => defaultLogger.info(...args), + warn: (...args) => defaultLogger.warn(...args), + error: (...args) => defaultLogger.error(...args), + setLevel: (level) => defaultLogger.setLevel(level), + LogLevel +}; + +export { LogLevel, Logger }; +export default logger; diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index 6613d768..b0c8685b 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -1,4 +1,7 @@ import { createWorker } from '../backend/service'; +import { createLogger } from '../lib/logger'; + +const logger = createLogger('WebSocket'); import { decode } from '@msgpack/msgpack'; /** @@ -55,7 +58,7 @@ class ServerUrlResolver { const startTime = performance.now(); try { - console.log('[ServerUrlResolver] Starting server URL resolution...'); + logger.debug('[ServerUrlResolver] Starting server URL resolution...'); // Priority 1: URL Parameters (if enabled and not in production build) if (enableUrlParams) { @@ -63,7 +66,7 @@ class ServerUrlResolver { if (urlServerUrl) { const validatedUrl = await this._validateAndNormalizeUrl(urlServerUrl); if (validatedUrl) { - console.log(`[ServerUrlResolver] Using URL parameter: ${validatedUrl}`); + logger.debug(`[ServerUrlResolver] Using URL parameter: ${validatedUrl}`); this._saveToStorage(validatedUrl); return validatedUrl; } @@ -76,10 +79,10 @@ class ServerUrlResolver { if (storedUrl) { const validatedUrl = await this._validateAndNormalizeUrl(storedUrl); if (validatedUrl) { - console.log(`[ServerUrlResolver] Using stored URL: ${validatedUrl}`); + logger.debug(`[ServerUrlResolver] Using stored URL: ${validatedUrl}`); return validatedUrl; } else { - console.warn('[ServerUrlResolver] Stored URL is invalid, clearing storage'); + logger.warn('[ServerUrlResolver] Stored URL is invalid, clearing storage'); this._clearStorage(); } } @@ -90,7 +93,7 @@ class ServerUrlResolver { if (envUrl) { const validatedUrl = await this._validateAndNormalizeUrl(envUrl); if (validatedUrl) { - console.log(`[ServerUrlResolver] Using environment variable: ${validatedUrl}`); + logger.debug(`[ServerUrlResolver] Using environment variable: ${validatedUrl}`); this._saveToStorage(validatedUrl); return validatedUrl; } @@ -102,7 +105,7 @@ class ServerUrlResolver { if (sessionUrl) { const validatedUrl = await this._validateAndNormalizeUrl(sessionUrl); if (validatedUrl) { - console.log(`[ServerUrlResolver] Using session storage: ${validatedUrl}`); + logger.debug(`[ServerUrlResolver] Using session storage: ${validatedUrl}`); return validatedUrl; } } @@ -111,24 +114,24 @@ class ServerUrlResolver { // Priority 5: Auto-detect localhost server const autoDetectedUrl = await this._autoDetectLocalhost(); if (autoDetectedUrl) { - console.log(`[ServerUrlResolver] Auto-detected localhost: ${autoDetectedUrl}`); + logger.debug(`[ServerUrlResolver] Auto-detected localhost: ${autoDetectedUrl}`); this._saveToStorage(autoDetectedUrl); return autoDetectedUrl; } // Final fallback: Default localhost configuration const fallbackUrl = `${window.location.protocol}//${fallbackHost}:${fallbackPort}`; - console.log(`[ServerUrlResolver] Using final fallback: ${fallbackUrl}`); + logger.debug(`[ServerUrlResolver] Using final fallback: ${fallbackUrl}`); const resolveTime = performance.now() - startTime; - console.log(`[ServerUrlResolver] Resolution completed in ${resolveTime.toFixed(2)}ms`); + logger.debug(`[ServerUrlResolver] Resolution completed in ${resolveTime.toFixed(2)}ms`); return fallbackUrl; } catch (error) { - console.error('[ServerUrlResolver] Error during resolution:', error); + logger.error('[ServerUrlResolver] Error during resolution:', error); const errorFallback = `${window.location.protocol}//${fallbackHost}:${fallbackPort}`; - console.log(`[ServerUrlResolver] Using error fallback: ${errorFallback}`); + logger.debug(`[ServerUrlResolver] Using error fallback: ${errorFallback}`); return errorFallback; } } @@ -140,7 +143,7 @@ class ServerUrlResolver { for (const paramName of this.URL_PARAM_NAMES) { const value = urlParams.get(paramName); if (value) { - console.log(`[ServerUrlResolver] Found URL parameter '${paramName}': ${value}`); + logger.debug(`[ServerUrlResolver] Found URL parameter '${paramName}': ${value}`); return value.trim(); } } @@ -151,7 +154,7 @@ class ServerUrlResolver { for (const paramName of this.URL_PARAM_NAMES) { const value = hashParams.get(paramName); if (value) { - console.log(`[ServerUrlResolver] Found hash parameter '${paramName}': ${value}`); + logger.debug(`[ServerUrlResolver] Found hash parameter '${paramName}': ${value}`); return value.trim(); } } @@ -159,7 +162,7 @@ class ServerUrlResolver { return null; } catch (error) { - console.error('[ServerUrlResolver] Error parsing URL parameters:', error); + logger.error('[ServerUrlResolver] Error parsing URL parameters:', error); return null; } } @@ -168,12 +171,12 @@ class ServerUrlResolver { try { const stored = localStorage.getItem(this.STORAGE_KEY); if (stored) { - console.log(`[ServerUrlResolver] Found in localStorage: ${stored}`); + logger.debug(`[ServerUrlResolver] Found in localStorage: ${stored}`); return stored.trim(); } return null; } catch (error) { - console.error('[ServerUrlResolver] Error accessing localStorage:', error); + logger.error('[ServerUrlResolver] Error accessing localStorage:', error); return null; } } @@ -182,12 +185,12 @@ class ServerUrlResolver { try { const stored = sessionStorage.getItem(this.STORAGE_KEY); if (stored) { - console.log(`[ServerUrlResolver] Found in sessionStorage: ${stored}`); + logger.debug(`[ServerUrlResolver] Found in sessionStorage: ${stored}`); return stored.trim(); } return null; } catch (error) { - console.error('[ServerUrlResolver] Error accessing sessionStorage:', error); + logger.error('[ServerUrlResolver] Error accessing sessionStorage:', error); return null; } } @@ -198,26 +201,26 @@ class ServerUrlResolver { if (metaTag) { const content = metaTag.getAttribute('content'); if (content) { - console.log(`[ServerUrlResolver] Found in meta tag: ${content}`); + logger.debug(`[ServerUrlResolver] Found in meta tag: ${content}`); return content.trim(); } } // Check for global environment variable if (window.PRESWALD_SERVER_URL) { - console.log(`[ServerUrlResolver] Found in global variable: ${window.PRESWALD_SERVER_URL}`); + logger.debug(`[ServerUrlResolver] Found in global variable: ${window.PRESWALD_SERVER_URL}`); return window.PRESWALD_SERVER_URL.trim(); } return null; } catch (error) { - console.error('[ServerUrlResolver] Error accessing environment:', error); + logger.error('[ServerUrlResolver] Error accessing environment:', error); return null; } } static async _autoDetectLocalhost() { - console.log('[ServerUrlResolver] Starting localhost auto-detection...'); + logger.debug('[ServerUrlResolver] Starting localhost auto-detection...'); const protocol = window.location.protocol; const detectionPromises = this.DEFAULT_LOCALHOST_PORTS.map(async (port) => { @@ -238,7 +241,7 @@ class ServerUrlResolver { clearTimeout(timeoutId); if (response.ok) { - console.log(`[ServerUrlResolver] Localhost server detected at port ${port}`); + logger.debug(`[ServerUrlResolver] Localhost server detected at port ${port}`); return testUrl; } } catch (error) { @@ -256,7 +259,7 @@ class ServerUrlResolver { clearTimeout(timeoutId); if (response.ok) { - console.log(`[ServerUrlResolver] Localhost server detected at port ${port} (root endpoint)`); + logger.debug(`[ServerUrlResolver] Localhost server detected at port ${port} (root endpoint)`); return testUrl; } } catch (rootError) { @@ -277,10 +280,10 @@ class ServerUrlResolver { return successfulUrl; } - console.log('[ServerUrlResolver] No localhost server auto-detected'); + logger.debug('[ServerUrlResolver] No localhost server auto-detected'); return null; } catch (error) { - console.error('[ServerUrlResolver] Error during localhost auto-detection:', error); + logger.error('[ServerUrlResolver] Error during localhost auto-detection:', error); return null; } } @@ -312,7 +315,7 @@ class ServerUrlResolver { // Remove trailing slash for consistency const cleanUrl = normalizedUrl.replace(/\/$/, ''); - console.log(`[ServerUrlResolver] Validated and normalized URL: ${cleanUrl}`); + logger.debug(`[ServerUrlResolver] Validated and normalized URL: ${cleanUrl}`); return cleanUrl; } catch (error) { @@ -324,9 +327,9 @@ class ServerUrlResolver { static _saveToStorage(url) { try { localStorage.setItem(this.STORAGE_KEY, url); - console.log(`[ServerUrlResolver] Saved to localStorage: ${url}`); + logger.debug(`[ServerUrlResolver] Saved to localStorage: ${url}`); } catch (error) { - console.error('[ServerUrlResolver] Error saving to localStorage:', error); + logger.error('[ServerUrlResolver] Error saving to localStorage:', error); } } @@ -334,9 +337,9 @@ class ServerUrlResolver { try { localStorage.removeItem(this.STORAGE_KEY); sessionStorage.removeItem(this.STORAGE_KEY); - console.log('[ServerUrlResolver] Cleared stored server URLs'); + logger.debug('[ServerUrlResolver] Cleared stored server URLs'); } catch (error) { - console.error('[ServerUrlResolver] Error clearing storage:', error); + logger.error('[ServerUrlResolver] Error clearing storage:', error); } } @@ -347,13 +350,13 @@ class ServerUrlResolver { static setServerUrl(url) { if (url && typeof url === 'string') { this._saveToStorage(url.trim()); - console.log(`[ServerUrlResolver] Manually set server URL: ${url}`); + logger.debug(`[ServerUrlResolver] Manually set server URL: ${url}`); } } static resetServerUrl() { this._clearStorage(); - console.log('[ServerUrlResolver] Reset server URL configuration'); + logger.debug('[ServerUrlResolver] Reset server URL configuration'); } } @@ -515,7 +518,7 @@ class MessageEncoder { try { return this.COMPRESSION_PREFIX + btoa(jsonString); } catch (error) { - console.warn('[MessageEncoder] Compression failed, using uncompressed:', error); + logger.warn('[MessageEncoder] Compression failed, using uncompressed:', error); return jsonString; } } @@ -602,7 +605,7 @@ class ComponentStateManager { */ getState(componentId) { if (!componentId || typeof componentId !== 'string') { - console.warn('[ComponentStateManager] Invalid componentId:', componentId); + logger.warn('[ComponentStateManager] Invalid componentId:', componentId); return undefined; } @@ -723,7 +726,7 @@ class ComponentStateManager { this.metrics.lastBulkDuration = duration; } - console.log(`[ComponentStateManager] Bulk update completed: ${changedCount}/${updateMap.size} changed in ${duration.toFixed(2)}ms`); + logger.debug(`[ComponentStateManager] Bulk update completed: ${changedCount}/${updateMap.size} changed in ${duration.toFixed(2)}ms`); return { success: true, @@ -734,7 +737,7 @@ class ComponentStateManager { }; } catch (error) { - console.error('[ComponentStateManager] Bulk update failed:', error); + logger.error('[ComponentStateManager] Bulk update failed:', error); throw new Error(`Bulk state update failed: ${error.message}`); } } @@ -913,7 +916,7 @@ class ComponentStateManager { try { callback(componentId, newValue, oldValue); } catch (error) { - console.error('[ComponentStateManager] Global subscriber error:', error); + logger.error('[ComponentStateManager] Global subscriber error:', error); } } } @@ -967,7 +970,7 @@ class ComponentStateManager { callback(notif.componentId, notif.newValue, notif.oldValue); } } catch (error) { - console.error('[ComponentStateManager] Global batch subscriber error:', error); + logger.error('[ComponentStateManager] Global batch subscriber error:', error); } } } @@ -1093,11 +1096,11 @@ class BaseCommunicationClient extends IPreswaldCommunicator { } this.callbacks.add(callback); - console.log(`[${this.constructor.name}] Subscriber added, total: ${this.callbacks.size}`); + logger.debug(`[${this.constructor.name}] Subscriber added, total: ${this.callbacks.size}`); return () => { this.callbacks.delete(callback); - console.log(`[${this.constructor.name}] Subscriber removed, total: ${this.callbacks.size}`); + logger.debug(`[${this.constructor.name}] Subscriber removed, total: ${this.callbacks.size}`); }; } @@ -1149,7 +1152,7 @@ class BaseCommunicationClient extends IPreswaldCommunicator { const stateResult = this.stateManager.bulkSetState(updates); if (stateResult.changedCount === 0) { - console.log(`[${this.constructor.name}] No state changes detected in bulk update`); + logger.debug(`[${this.constructor.name}] No state changes detected in bulk update`); return { results: [], totalProcessed: stateResult.totalCount, @@ -1192,7 +1195,7 @@ class BaseCommunicationClient extends IPreswaldCommunicator { } const duration = performance.now() - startTime; - console.log(`[${this.constructor.name}] Bulk update completed: ${successCount}/${changedUpdates.size} network updates (${stateResult.changedCount}/${stateResult.totalCount} local changes) in ${duration.toFixed(2)}ms`); + logger.debug(`[${this.constructor.name}] Bulk update completed: ${successCount}/${changedUpdates.size} network updates (${stateResult.changedCount}/${stateResult.totalCount} local changes) in ${duration.toFixed(2)}ms`); return { results, @@ -1235,7 +1238,7 @@ class BaseCommunicationClient extends IPreswaldCommunicator { } const duration = performance.now() - startTime; - console.log(`[${this.constructor.name}] Fallback bulk update completed: ${successCount}/${results.length} in ${duration.toFixed(2)}ms`); + logger.debug(`[${this.constructor.name}] Fallback bulk update completed: ${successCount}/${results.length} in ${duration.toFixed(2)}ms`); return { results, @@ -1336,9 +1339,9 @@ class BaseCommunicationClient extends IPreswaldCommunicator { if (connected && !wasConnected) { this.connectTime = performance.now(); - console.log(`[${this.constructor.name}] Connection established`); + logger.debug(`[${this.constructor.name}] Connection established`); } else if (!connected && wasConnected) { - console.log(`[${this.constructor.name}] Connection lost`); + logger.debug(`[${this.constructor.name}] Connection lost`); } this._notifySubscribers({ @@ -1379,12 +1382,12 @@ class WebSocketClient extends BaseCommunicationClient { async connect(config = {}) { if (this.isConnecting || (this.socket && this.socket.readyState === WebSocket.OPEN)) { - console.log('[WebSocket] Already connected or connecting'); + logger.debug('[WebSocket] Already connected or connecting'); return { success: true, message: 'Already connected' }; } this.isConnecting = true; - console.log('[WebSocket] Connecting...'); + logger.debug('[WebSocket] Connecting...'); try { const serverUrl = await ServerUrlResolver.resolveServerUrl({ @@ -1398,7 +1401,7 @@ class WebSocketClient extends BaseCommunicationClient { const serverHost = serverUrl.replace(/^https?:\/\//, ''); const wsUrl = `${wsProtocol}//${serverHost}/ws/${this.clientId}`; - console.log(`[WebSocket] Connecting to: ${wsUrl} (resolved from: ${serverUrl})`); + logger.debug(`[WebSocket] Connecting to: ${wsUrl} (resolved from: ${serverUrl})`); this.socket = new WebSocket(wsUrl); @@ -1413,7 +1416,7 @@ class WebSocketClient extends BaseCommunicationClient { this.socket.onopen = () => { clearTimeout(timeout); - console.log('[WebSocket] Connected successfully'); + logger.debug('[WebSocket] Connected successfully'); this.isConnecting = false; this.reconnectAttempts = 0; this.reconnectDelay = 1000; @@ -1430,7 +1433,7 @@ class WebSocketClient extends BaseCommunicationClient { this.socket.onclose = (event) => { clearTimeout(timeout); - console.log('[WebSocket] Connection closed:', event); + logger.debug('[WebSocket] Connection closed:', event); this.isConnecting = false; this.socket = null; this._setConnected(false); @@ -1442,7 +1445,7 @@ class WebSocketClient extends BaseCommunicationClient { this.socket.onerror = (error) => { clearTimeout(timeout); - console.error('[WebSocket] Error:', error); + logger.error('[WebSocket] Error:', error); this.isConnecting = false; this._handleError(error, 'Connection error'); reject(new Error('WebSocket connection error')); @@ -1461,11 +1464,11 @@ class WebSocketClient extends BaseCommunicationClient { } } catch (decodeError) { // Fallback to legacy JSON parsing for backwards compatibility - console.warn('[WebSocket] Using legacy JSON parsing:', decodeError.message); + logger.warn('[WebSocket] Using legacy JSON parsing:', decodeError.message); data = JSON.parse(event.data); } - console.log('[WebSocket] Message received:', { + logger.debug('[WebSocket] Message received:', { ...data, timestamp: new Date().toISOString(), }); @@ -1475,7 +1478,7 @@ class WebSocketClient extends BaseCommunicationClient { // Use ComponentStateManager for bulk initial state loading if (data.states) { this.stateManager.bulkSetState(data.states); - console.log('[WebSocket] Initial states loaded via ComponentStateManager:', Object.keys(data.states).length, 'components'); + logger.debug('[WebSocket] Initial states loaded via ComponentStateManager:', Object.keys(data.states).length, 'components'); } // Legacy compatibility this.componentStates = { ...data.states }; @@ -1485,7 +1488,7 @@ class WebSocketClient extends BaseCommunicationClient { if (data.component_id) { // Use ComponentStateManager for individual updates this.stateManager.setState(data.component_id, data.value); - console.log('[WebSocket] Component state updated:', { + logger.debug('[WebSocket] Component state updated:', { componentId: data.component_id, value: data.value, }); @@ -1496,7 +1499,7 @@ class WebSocketClient extends BaseCommunicationClient { if (data.states) { // Handle bulk updates efficiently const bulkResult = this.stateManager.bulkSetState(data.states); - console.log('[WebSocket] Bulk state update processed:', { + logger.debug('[WebSocket] Bulk state update processed:', { totalCount: bulkResult.totalCount, changedCount: bulkResult.changedCount, duration: bulkResult.duration @@ -1506,7 +1509,7 @@ class WebSocketClient extends BaseCommunicationClient { case 'bulk_update_ack': // Handle server acknowledgment of bulk updates - console.log('[WebSocket] Bulk update acknowledged by server:', { + logger.debug('[WebSocket] Bulk update acknowledged by server:', { totalCount: data.total_count, changedCount: data.changed_count, processingTime: data.processing_time, @@ -1543,7 +1546,7 @@ class WebSocketClient extends BaseCommunicationClient { if (stateUpdates.size > 0) { const bulkResult = this.stateManager.bulkSetState(stateUpdates); - console.log('[WebSocket] Component states bulk updated:', { + logger.debug('[WebSocket] Component states bulk updated:', { totalCount: bulkResult.totalCount, changedCount: bulkResult.changedCount, duration: bulkResult.duration @@ -1554,7 +1557,7 @@ class WebSocketClient extends BaseCommunicationClient { case 'connections_update': this.connections = data.connections || []; - console.log('[WebSocket] Connections updated:', this.connections); + logger.debug('[WebSocket] Connections updated:', this.connections); break; } @@ -1583,19 +1586,19 @@ class WebSocketClient extends BaseCommunicationClient { label, }); } else { - console.warn('[WebSocket] Unknown binary message format:', decoded); + logger.warn('[WebSocket] Unknown binary message format:', decoded); } } else { - console.warn('[WebSocket] Unrecognized message format:', event.data); + logger.warn('[WebSocket] Unrecognized message format:', event.data); } } catch (error) { - console.error('[WebSocket] Error processing message:', error); + logger.error('[WebSocket] Error processing message:', error); this._handleError(error, 'Message processing'); } }; }); } catch (error) { - console.error('[WebSocket] Error creating connection:', error); + logger.error('[WebSocket] Error creating connection:', error); this.isConnecting = false; this._handleError(error, 'Connection creation'); return { success: false, message: error.message }; @@ -1604,7 +1607,7 @@ class WebSocketClient extends BaseCommunicationClient { async disconnect() { if (this.socket) { - console.log('[WebSocket] Disconnecting...'); + logger.debug('[WebSocket] Disconnecting...'); // Process any pending` batched messages before disconnecting if (this.batchTimeout) { @@ -1624,7 +1627,7 @@ class WebSocketClient extends BaseCommunicationClient { _handleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { - console.log('[WebSocket] Max reconnection attempts reached'); + logger.debug('[WebSocket] Max reconnection attempts reached'); this._notifySubscribers({ type: 'error', content: { message: 'Failed to reconnect after multiple attempts' }, @@ -1640,7 +1643,7 @@ class WebSocketClient extends BaseCommunicationClient { setTimeout(() => { if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { - console.log('[WebSocket] Attempting reconnection...'); + logger.debug('[WebSocket] Attempting reconnection...'); this.connect(); } }, delay); @@ -1709,14 +1712,14 @@ class WebSocketClient extends BaseCommunicationClient { this.componentStates[componentId] = value; }); - console.log(`[WebSocket] Sent batched update: ${Object.keys(stateUpdates).length} components from ${this.messageQueue.length} queued messages`); + logger.debug(`[WebSocket] Sent batched update: ${Object.keys(stateUpdates).length} components from ${this.messageQueue.length} queued messages`); // Clear the queue this.messageQueue = []; this.batchTimeout = null; } catch (error) { - console.error('[WebSocket] Error processing batched messages:', error); + logger.error('[WebSocket] Error processing batched messages:', error); // Fallback to individual sends this.messageQueue.forEach(({ componentId, value }) => { const message = { type: 'component_update', states: { [componentId]: value } }; @@ -1752,7 +1755,7 @@ class WebSocketClient extends BaseCommunicationClient { } } catch (encodeError) { - console.warn('[WebSocket] Using legacy JSON encoding:', encodeError.message); + logger.warn('[WebSocket] Using legacy JSON encoding:', encodeError.message); encodedMessage = JSON.stringify(message); compressedSize = encodedMessage.length; } @@ -1766,13 +1769,13 @@ class WebSocketClient extends BaseCommunicationClient { // Enhanced logging with compression info if (originalSize && compressedSize < originalSize) { const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1); - console.log(`[WebSocket] Sent compressed message: ${originalSize}B → ${compressedSize}B (${compressionRatio}% reduction)`); + logger.debug(`[WebSocket] Sent compressed message: ${originalSize}B → ${compressedSize}B (${compressionRatio}% reduction)`); } else { - console.log('[WebSocket] Sent message:', message.type, compressedSize ? `${compressedSize}B` : ''); + logger.debug('[WebSocket] Sent message:', message.type, compressedSize ? `${compressedSize}B` : ''); } } catch (error) { - console.error('[WebSocket] Error sending message:', error); + logger.error('[WebSocket] Error sending message:', error); throw error; } } @@ -1792,10 +1795,10 @@ class WebSocketClient extends BaseCommunicationClient { this.socket.send(encodedMessage); this.metrics.messagesSent++; - console.log(`[WebSocket] Sent bulk update for ${updates instanceof Map ? updates.size : Object.keys(updates).length} components`); + logger.debug(`[WebSocket] Sent bulk update for ${updates instanceof Map ? updates.size : Object.keys(updates).length} components`); return { success: true }; } catch (error) { - console.error('[WebSocket] Error sending bulk update:', error); + logger.error('[WebSocket] Error sending bulk update:', error); throw error; } } @@ -1829,12 +1832,12 @@ class PostMessageClient extends BaseCommunicationClient { } async connect(config = {}) { - console.log('[PostMessage] Setting up listener...'); + logger.debug('[PostMessage] Setting up listener...'); window.addEventListener('message', this._handleMessage.bind(this)); // Assume connected in browser context this._setConnected(true); - console.log('[PostMessage] Connected successfully'); + logger.debug('[PostMessage] Connected successfully'); // Send pending updates Object.entries(this.pendingUpdates).forEach(([componentId, value]) => { @@ -1846,7 +1849,7 @@ class PostMessageClient extends BaseCommunicationClient { } async disconnect() { - console.log('[PostMessage] Disconnecting...'); + logger.debug('[PostMessage] Disconnecting...'); // Process any pending batched messages before disconnecting if (this.batchTimeout) { @@ -1877,24 +1880,24 @@ class PostMessageClient extends BaseCommunicationClient { } } catch (decodeError) { // Fallback to legacy JSON parsing - console.warn('[PostMessage] Using legacy JSON parsing:', decodeError.message); + logger.warn('[PostMessage] Using legacy JSON parsing:', decodeError.message); data = JSON.parse(event.data); } } else { data = event.data; } } catch (error) { - console.error('[PostMessage] Error parsing message:', error); + logger.error('[PostMessage] Error parsing message:', error); return; } - console.log('[PostMessage] Message received:', { + logger.debug('[PostMessage] Message received:', { ...data, timestamp: new Date().toISOString(), }); switch (data.type) { case 'connection_status': this.isConnected = data.connected; - console.log('[PostMessage] Connection status:', this.isConnected); + logger.debug('[PostMessage] Connection status:', this.isConnected); this._notifySubscribers(data); break; @@ -1902,7 +1905,7 @@ class PostMessageClient extends BaseCommunicationClient { // Use ComponentStateManager for bulk initial state loading if (data.states) { this.stateManager.bulkSetState(data.states); - console.log('[PostMessage] Initial states loaded via ComponentStateManager:', Object.keys(data.states).length, 'components'); + logger.debug('[PostMessage] Initial states loaded via ComponentStateManager:', Object.keys(data.states).length, 'components'); } // Legacy compatibility this.componentStates = { ...data.states }; @@ -1913,7 +1916,7 @@ class PostMessageClient extends BaseCommunicationClient { if (data.component_id) { // Use ComponentStateManager for individual updates this.stateManager.setState(data.component_id, data.value); - console.log('[PostMessage] Component state updated:', { + logger.debug('[PostMessage] Component state updated:', { componentId: data.component_id, value: data.value, }); @@ -1925,7 +1928,7 @@ class PostMessageClient extends BaseCommunicationClient { if (data.states) { // Handle bulk updates efficiently const bulkResult = this.stateManager.bulkSetState(data.states); - console.log('[PostMessage] Bulk state update processed:', { + logger.debug('[PostMessage] Bulk state update processed:', { totalCount: bulkResult.totalCount, changedCount: bulkResult.changedCount, duration: bulkResult.duration @@ -1936,7 +1939,7 @@ class PostMessageClient extends BaseCommunicationClient { case 'bulk_update_ack': // Handle server acknowledgment of bulk updates - console.log('[PostMessage] Bulk update acknowledged by server:', { + logger.debug('[PostMessage] Bulk update acknowledged by server:', { totalCount: data.total_count, changedCount: data.changed_count, processingTime: data.processing_time, @@ -1974,7 +1977,7 @@ class PostMessageClient extends BaseCommunicationClient { if (stateUpdates.size > 0) { const bulkResult = this.stateManager.bulkSetState(stateUpdates); - console.log('[PostMessage] Component states bulk updated:', { + logger.debug('[PostMessage] Component states bulk updated:', { totalCount: bulkResult.totalCount, changedCount: bulkResult.changedCount, duration: bulkResult.duration @@ -2003,7 +2006,7 @@ class PostMessageClient extends BaseCommunicationClient { _sendComponentUpdate(componentId, value) { if (!window.parent) { - console.warn('[PostMessage] No parent window to send update'); + logger.warn('[PostMessage] No parent window to send update'); return; } @@ -2062,14 +2065,14 @@ class PostMessageClient extends BaseCommunicationClient { this.componentStates[componentId] = value; }); - console.log(`[PostMessage] Sent batched update: ${Object.keys(stateUpdates).length} components from ${this.messageQueue.length} queued messages`); + logger.debug(`[PostMessage] Sent batched update: ${Object.keys(stateUpdates).length} components from ${this.messageQueue.length} queued messages`); // Clear the queue this.messageQueue = []; this.batchTimeout = null; } catch (error) { - console.error('[PostMessage] Error processing batched messages:', error); + logger.error('[PostMessage] Error processing batched messages:', error); // Fallback to individual sends this.messageQueue.forEach(({ componentId, value }) => { const message = { @@ -2112,7 +2115,7 @@ class PostMessageClient extends BaseCommunicationClient { } } catch (encodeError) { - console.warn('[PostMessage] Using legacy format:', encodeError.message); + logger.warn('[PostMessage] Using legacy format:', encodeError.message); encodedMessage = message; optimizedSize = JSON.stringify(message).length; } @@ -2126,13 +2129,13 @@ class PostMessageClient extends BaseCommunicationClient { // Enhanced logging with optimization info if (originalSize && optimizedSize < originalSize) { const reductionRatio = ((originalSize - optimizedSize) / originalSize * 100).toFixed(1); - console.log(`[PostMessage] Sent optimized message: ${originalSize}B → ${optimizedSize}B (${reductionRatio}% reduction)`); + logger.debug(`[PostMessage] Sent optimized message: ${originalSize}B → ${optimizedSize}B (${reductionRatio}% reduction)`); } else { - console.log('[PostMessage] Sent message:', message.type, optimizedSize ? `${optimizedSize}B` : ''); + logger.debug('[PostMessage] Sent message:', message.type, optimizedSize ? `${optimizedSize}B` : ''); } } catch (error) { - console.error('[PostMessage] Error sending message:', error); + logger.error('[PostMessage] Error sending message:', error); throw error; } } @@ -2195,7 +2198,7 @@ class PostMessageClient extends BaseCommunicationClient { class ComlinkClient extends BaseCommunicationClient { constructor(config = {}) { super(); - console.log('[Client] Initializing ComlinkClient'); + logger.debug('[Client] Initializing ComlinkClient'); this.worker = null; this.messageQueue = []; @@ -2210,28 +2213,28 @@ class ComlinkClient extends BaseCommunicationClient { } async connect() { - console.log('[Client] Starting connection'); + logger.debug('[Client] Starting connection'); try { if (this.isConnected) { - console.log('[Client] Already connected'); + logger.debug('[Client] Already connected'); return; } - console.log('[Client] About to create worker'); + logger.debug('[Client] About to create worker'); this.worker = createWorker(); - console.log('[Client] Worker created'); + logger.debug('[Client] Worker created'); - console.log('[Client] About to initialize Pyodide'); + logger.debug('[Client] About to initialize Pyodide'); const result = await this.worker.initializePyodide(); if (!result.success) { throw new Error('Failed to initialize Pyodide'); } this.isConnected = true; - console.log('[Client] Connection established'); + logger.debug('[Client] Connection established'); this._notifySubscribers({ type: 'connection_status', connected: true }); - console.log('[Client] Loading project fs'); + logger.debug('[Client] Loading project fs'); const resp = await fetch('project_fs.json', { cache: 'no-cache' }); const raw = await resp.json(); @@ -2246,7 +2249,7 @@ class ComlinkClient extends BaseCommunicationClient { } await this.worker.loadFilesToFS(files); - console.log('[Client] Project fs loaded'); + logger.debug('[Client] Project fs loaded'); const scriptResult = await this.worker.runScript( '/project/' + (raw.__entrypoint__ || 'hello.py') @@ -2261,14 +2264,14 @@ class ComlinkClient extends BaseCommunicationClient { // Process any pending updates const pendingCount = Object.keys(this.pendingUpdates).length; if (pendingCount > 0) { - console.log('[Client] Processing pending updates:', pendingCount); + logger.debug('[Client] Processing pending updates:', pendingCount); for (const [componentId, value] of Object.entries(this.pendingUpdates)) { await this._sendComponentUpdate(componentId, value); } this.pendingUpdates = {}; } } catch (error) { - console.error('[Client] Connection error:', error); + logger.error('[Client] Connection error:', error); this.isConnected = false; this._notifySubscribers({ type: 'error', @@ -2279,7 +2282,7 @@ class ComlinkClient extends BaseCommunicationClient { } _handleComponentUpdate(components) { - console.log('[Client] Handling component update:', components); + logger.debug('[Client] Handling component update:', components); if (components?.rows) { // Extract state updates from component data for bulk processing const stateUpdates = new Map(); @@ -2293,7 +2296,7 @@ class ComlinkClient extends BaseCommunicationClient { if (stateUpdates.size > 0) { const bulkResult = this.stateManager.bulkSetState(stateUpdates); - console.log('[Client] Component states bulk updated:', { + logger.debug('[Client] Component states bulk updated:', { totalCount: bulkResult.totalCount, changedCount: bulkResult.changedCount, duration: bulkResult.duration @@ -2308,21 +2311,21 @@ class ComlinkClient extends BaseCommunicationClient { } async disconnect() { - console.log('[Client] Disconnecting'); + logger.debug('[Client] Disconnecting'); if (this.worker) { this.worker.shutdown(); this.worker = null; this._setConnected(false); - console.log('[Client] Disconnected'); + logger.debug('[Client] Disconnected'); } } // subscribe, _notifySubscribers, getComponentState are inherited from BaseCommunicationClient async updateComponentState(componentId, value) { - console.log(`[Client] Updating state for component ${componentId}:`, value); + logger.debug(`[Client] Updating state for component ${componentId}:`, value); if (!this.isConnected || !this.worker) { - console.log('[Client] Not connected, queueing update'); + logger.debug('[Client] Not connected, queueing update'); this.pendingUpdates[componentId] = value; throw new Error('Connection not ready'); } @@ -2330,7 +2333,7 @@ class ComlinkClient extends BaseCommunicationClient { } async _sendComponentUpdate(componentId, value) { - console.log(`[Client] Sending component update - ${componentId}:`, value); + logger.debug(`[Client] Sending component update - ${componentId}:`, value); try { const result = await this.worker.updateComponent(componentId, value); if (!result.success) { @@ -2339,7 +2342,7 @@ class ComlinkClient extends BaseCommunicationClient { this._handleComponentUpdate(result.components); return true; } catch (error) { - console.error('[Client] Error updating component:', error); + logger.error('[Client] Error updating component:', error); this._notifySubscribers({ type: 'error', content: { message: error.message }, @@ -2349,7 +2352,7 @@ class ComlinkClient extends BaseCommunicationClient { } async loadFilesToFS(files) { - console.log('[Client] loadFilesToFS', files); + logger.debug('[Client] loadFilesToFS', files); if (!this.isConnected || !this.worker) { throw new Error('Connection not ready'); } @@ -2357,7 +2360,7 @@ class ComlinkClient extends BaseCommunicationClient { } async listFilesInDirectory(directoryPath) { - console.log('[Client] listFilesInDirectory', directoryPath); + logger.debug('[Client] listFilesInDirectory', directoryPath); if (!this.isConnected || !this.worker) { throw new Error('Connection not ready'); } @@ -2366,7 +2369,7 @@ class ComlinkClient extends BaseCommunicationClient { // 2. run an arbitrary python script ------------ async runScript(scriptPath) { - console.log('[Client] runScript', scriptPath); + logger.debug('[Client] runScript', scriptPath); if (!this.isConnected || !this.worker) { throw new Error('Connection not ready'); } @@ -2429,14 +2432,14 @@ class ConnectionPoolManager { lastHealthCheck: 0 }; - console.log('[ConnectionPool] Initialized with config:', this.config); + logger.debug('[ConnectionPool] Initialized with config:', this.config); } /** * Initialize the connection pool */ async initialize(transportType, transportConfig = {}) { - console.log(`[ConnectionPool] Initializing pool with ${this.config.minPoolSize} ${transportType} connections`); + logger.debug(`[ConnectionPool] Initializing pool with ${this.config.minPoolSize} ${transportType} connections`); const initPromises = []; for (let i = 0; i < this.config.minPoolSize; i++) { @@ -2451,7 +2454,7 @@ class ConnectionPoolManager { throw new Error('Failed to initialize any connections in the pool'); } - console.log(`[ConnectionPool] Initialized ${successfulConnections}/${this.config.minPoolSize} connections`); + logger.debug(`[ConnectionPool] Initialized ${successfulConnections}/${this.config.minPoolSize} connections`); // Start health monitoring this._startHealthMonitoring(); @@ -2499,7 +2502,7 @@ class ConnectionPoolManager { */ async addConnection(transportType, transportConfig = {}) { if (this.connectionPool.size >= this.config.maxPoolSize) { - console.warn('[ConnectionPool] Pool is at maximum capacity'); + logger.warn('[ConnectionPool] Pool is at maximum capacity'); return null; } @@ -2517,7 +2520,7 @@ class ConnectionPoolManager { return; } - console.log(`[ConnectionPool] Removing connection ${connectionId}`); + logger.debug(`[ConnectionPool] Removing connection ${connectionId}`); try { await connection.client.disconnect(); @@ -2552,7 +2555,7 @@ class ConnectionPoolManager { * Shutdown the connection pool */ async shutdown() { - console.log('[ConnectionPool] Shutting down connection pool'); + logger.debug('[ConnectionPool] Shutting down connection pool'); this.isShuttingDown = true; if (this.healthCheckTimer) { @@ -2574,7 +2577,7 @@ class ConnectionPoolManager { this.connectionMetrics.clear(); this.activeConnections.clear(); - console.log('[ConnectionPool] Shutdown complete'); + logger.debug('[ConnectionPool] Shutdown complete'); } /** @@ -2582,7 +2585,7 @@ class ConnectionPoolManager { * @private */ async _createConnection(connectionId, transportType, transportConfig) { - console.log(`[ConnectionPool] Creating connection ${connectionId}`); + logger.debug(`[ConnectionPool] Creating connection ${connectionId}`); try { const client = createTransportClient(transportType, transportConfig); @@ -2613,7 +2616,7 @@ class ConnectionPoolManager { isHealthy: true }); - console.log(`[ConnectionPool] Connection ${connectionId} created successfully in ${connectionTime.toFixed(2)}ms`); + logger.debug(`[ConnectionPool] Connection ${connectionId} created successfully in ${connectionTime.toFixed(2)}ms`); return connection; } catch (error) { @@ -2732,7 +2735,7 @@ class ConnectionPoolManager { this._performHealthCheck(); }, this.config.healthCheckInterval); - console.log(`[ConnectionPool] Health monitoring started (interval: ${this.config.healthCheckInterval}ms)`); + logger.debug(`[ConnectionPool] Health monitoring started (interval: ${this.config.healthCheckInterval}ms)`); } /** @@ -2742,7 +2745,7 @@ class ConnectionPoolManager { async _performHealthCheck() { if (this.isShuttingDown) return; - console.log('[ConnectionPool] Performing health check'); + logger.debug('[ConnectionPool] Performing health check'); const startTime = performance.now(); const healthCheckPromises = Array.from(this.connectionPool.entries()).map(async ([connectionId, connection]) => { @@ -2764,7 +2767,7 @@ class ConnectionPoolManager { try { await connection.client.connect(); this.activeConnections.add(connection); - console.log(`[ConnectionPool] Connection ${connectionId} reconnected successfully`); + logger.debug(`[ConnectionPool] Connection ${connectionId} reconnected successfully`); } catch (error) { console.error(`[ConnectionPool] Failed to reconnect ${connectionId}:`, error); } @@ -2785,7 +2788,7 @@ class ConnectionPoolManager { this.poolMetrics.lastHealthCheck = Date.now(); this.poolMetrics.activeConnections = this.activeConnections.size; - console.log(`[ConnectionPool] Health check completed in ${healthCheckTime.toFixed(2)}ms - ${this.activeConnections.size}/${this.connectionPool.size} connections healthy`); + logger.debug(`[ConnectionPool] Health check completed in ${healthCheckTime.toFixed(2)}ms - ${this.activeConnections.size}/${this.connectionPool.size} connections healthy`); } } @@ -2802,8 +2805,8 @@ class TransportSelector { const environment = this.detectEnvironment(); const optimalTransport = this.selectForEnvironment(environment, config); - console.log('[TransportSelector] Environment detected:', environment); - console.log('[TransportSelector] Selected transport:', optimalTransport); + logger.debug('[TransportSelector] Environment detected:', environment); + logger.debug('[TransportSelector] Selected transport:', optimalTransport); return optimalTransport; } @@ -2851,7 +2854,7 @@ class TransportSelector { (window.location.pathname.endsWith('.html') || window.location.pathname.endsWith('/')) ); } catch (error) { - console.warn('[TransportSelector] Error detecting HTML export environment:', error); + logger.warn('[TransportSelector] Error detecting HTML export environment:', error); return false; } } @@ -2870,7 +2873,7 @@ class TransportSelector { static selectForEnvironment(environment, config) { // Priority 1: HTML export environment should always use Comlink if (environment.isHtmlExport && environment.hasWorkers && config.enableWorkers !== false) { - console.log('[TransportSelector] HTML export environment detected, using Comlink transport'); + logger.debug('[TransportSelector] HTML export environment detected, using Comlink transport'); return TransportType.COMLINK; } @@ -2895,7 +2898,7 @@ class TransportSelector { } // Last resort fallback (should rarely be reached) - console.warn('[TransportSelector] No optimal transport found, defaulting to WebSocket'); + logger.warn('[TransportSelector] No optimal transport found, defaulting to WebSocket'); return TransportType.WEBSOCKET; } @@ -2948,11 +2951,11 @@ class PooledCommunicationClient extends IPreswaldCommunicator { async connect(config = {}) { if (this.isInitialized) { - console.log('[PooledClient] Already initialized'); + logger.debug('[PooledClient] Already initialized'); return { success: true, message: 'Already connected' }; } - console.log('[PooledClient] Initializing connection pool'); + logger.debug('[PooledClient] Initializing connection pool'); try { const poolConfig = { @@ -2969,18 +2972,18 @@ class PooledCommunicationClient extends IPreswaldCommunicator { this.isInitialized = true; this.isConnected = true; - console.log(`[PooledClient] Initialized with ${connectionsCreated} connections`); + logger.debug(`[PooledClient] Initialized with ${connectionsCreated} connections`); return { success: true, message: `Connected with ${connectionsCreated} pooled connections` }; } catch (error) { - console.error('[PooledClient] Failed to initialize connection pool:', error); + logger.error('[PooledClient] Failed to initialize connection pool:', error); throw error; } } async disconnect() { if (this.connectionPool) { - console.log('[PooledClient] Shutting down connection pool'); + logger.debug('[PooledClient] Shutting down connection pool'); await this.connectionPool.shutdown(); this.connectionPool = null; } @@ -2997,7 +3000,7 @@ class PooledCommunicationClient extends IPreswaldCommunicator { const connection = this.connectionPool.getConnection(); return connection.getComponentState(componentId); } catch (error) { - console.error('[PooledClient] Error getting component state:', error); + logger.error('[PooledClient] Error getting component state:', error); throw error; } } @@ -3019,7 +3022,7 @@ class PooledCommunicationClient extends IPreswaldCommunicator { return result; } catch (error) { this._updateAggregateMetrics('error', performance.now() - startTime); - console.error('[PooledClient] Error updating component state:', error); + logger.error('[PooledClient] Error updating component state:', error); throw error; } } @@ -3041,7 +3044,7 @@ class PooledCommunicationClient extends IPreswaldCommunicator { return result; } catch (error) { this._updateAggregateMetrics('error', performance.now() - startTime); - console.error('[PooledClient] Error in bulk state update:', error); + logger.error('[PooledClient] Error in bulk state update:', error); throw error; } } @@ -3059,7 +3062,7 @@ class PooledCommunicationClient extends IPreswaldCommunicator { const unsubscribe = connection.client.subscribe(callback); unsubscribeFunctions.push(unsubscribe); } catch (error) { - console.error('[PooledClient] Error subscribing to connection:', error); + logger.error('[PooledClient] Error subscribing to connection:', error); } } @@ -3069,7 +3072,7 @@ class PooledCommunicationClient extends IPreswaldCommunicator { try { unsubscribe(); } catch (error) { - console.error('[PooledClient] Error unsubscribing:', error); + logger.error('[PooledClient] Error unsubscribing:', error); } }); }; @@ -3161,17 +3164,17 @@ export const createCommunicationLayer = (config = {}) => { ...config }; - console.log('[CommunicationFactory] Creating communicator with config:', enhancedConfig); + logger.debug('[CommunicationFactory] Creating communicator with config:', enhancedConfig); // Select optimal transport const selectedTransport = TransportSelector.selectOptimalTransport(enhancedConfig); - console.log('[CommunicationFactory] Selected transport:', selectedTransport); + logger.debug('[CommunicationFactory] Selected transport:', selectedTransport); let client; // Create pooled or single client based on configuration if (enhancedConfig.enableConnectionPooling && selectedTransport === TransportType.WEBSOCKET) { - console.log('[CommunicationFactory] Creating pooled communication client'); + logger.debug('[CommunicationFactory] Creating pooled communication client'); client = new PooledCommunicationClient(selectedTransport, enhancedConfig); } else { if (enhancedConfig.enableConnectionPooling) { @@ -3192,21 +3195,21 @@ export const createCommunicationLayer = (config = {}) => { } const creationTime = performance.now() - startTime; - console.log(`[CommunicationFactory] Created ${client.constructor.name} in ${creationTime.toFixed(2)}ms`); + logger.debug(`[CommunicationFactory] Created ${client.constructor.name} in ${creationTime.toFixed(2)}ms`); return client; } catch (error) { - console.error('[CommunicationFactory] Failed to create communication layer:', error); + logger.error('[CommunicationFactory] Failed to create communication layer:', error); // Attempt fallback to basic WebSocket client try { - console.log('[CommunicationFactory] Attempting fallback to WebSocket...'); + logger.debug('[CommunicationFactory] Attempting fallback to WebSocket...'); const fallbackClient = new WebSocketClient(); - console.warn('[CommunicationFactory] Using fallback WebSocket client'); + logger.warn('[CommunicationFactory] Using fallback WebSocket client'); return fallbackClient; } catch (fallbackError) { - console.error('[CommunicationFactory] Fallback also failed:', fallbackError); + logger.error('[CommunicationFactory] Fallback also failed:', fallbackError); throw new Error(`Failed to create communication layer: ${error.message}`); } } @@ -3281,7 +3284,7 @@ export const createProductionCommunicationLayer = (config = {}) => { ...config }; - console.log('[ProductionFactory] Creating production communication layer with pooling'); + logger.debug('[ProductionFactory] Creating production communication layer with pooling'); return createCommunicationLayer(productionConfig); }; @@ -3299,7 +3302,7 @@ const initializeServerConfiguration = async () => { try { // Pre-resolve server URL to cache it and validate configuration const serverUrl = await ServerUrlResolver.resolveServerUrl(); - console.log(`[WebSocket Module] Pre-resolved server URL: ${serverUrl}`); + logger.debug(`[WebSocket Module] Pre-resolved server URL: ${serverUrl}`); // Make server URL available globally for debugging if (typeof window !== 'undefined') { @@ -3308,7 +3311,7 @@ const initializeServerConfiguration = async () => { return serverUrl; } catch (error) { - console.error('[WebSocket Module] Error pre-resolving server URL:', error); + logger.error('[WebSocket Module] Error pre-resolving server URL:', error); return null; } }; @@ -3335,7 +3338,7 @@ function getOrCreateCommunicationLayer() { const isHtmlExport = detectHtmlExportEnvironment(); const clientType = window.__PRESWALD_CLIENT_TYPE; - console.log(`[WebSocket Module] Environment analysis:`, { + logger.debug(`[WebSocket Module] Environment analysis:`, { isHtmlExport, clientType, pathname: window.location.pathname, @@ -3346,7 +3349,7 @@ function getOrCreateCommunicationLayer() { // Priority 1: Explicit client type 'comlink' always uses Comlink if (clientType === 'comlink' || clientType === TransportType.COMLINK) { - console.log(`[WebSocket Module] Using Comlink transport due to explicit client type: ${clientType}`); + logger.debug(`[WebSocket Module] Using Comlink transport due to explicit client type: ${clientType}`); _globalCommInstance = createCommunicationLayer({ transport: TransportType.COMLINK, enableUrlParams: false, @@ -3355,18 +3358,18 @@ function getOrCreateCommunicationLayer() { } // Priority 2: Check if we have a server available (even in HTML export environment) else if (isHtmlExport) { - console.log(`[WebSocket Module] HTML export environment detected, checking server availability...`); + logger.debug(`[WebSocket Module] HTML export environment detected, checking server availability...`); // Check if we can reach a server (quick check) const hasServerConnection = checkServerAvailability(); if (hasServerConnection) { - console.log(`[WebSocket Module] Server available in HTML export environment, using WebSocket transport`); + logger.debug(`[WebSocket Module] Server available in HTML export environment, using WebSocket transport`); _globalCommInstance = createCommunicationLayer({ transport: TransportType.WEBSOCKET }); } else { - console.log(`[WebSocket Module] No server available in HTML export environment, using Comlink transport`); + logger.debug(`[WebSocket Module] No server available in HTML export environment, using Comlink transport`); _globalCommInstance = createCommunicationLayer({ transport: TransportType.COMLINK, enableUrlParams: false, @@ -3376,18 +3379,18 @@ function getOrCreateCommunicationLayer() { } // Priority 3: Other explicit client types else if (clientType && clientType !== TransportType.AUTO) { - console.log(`[WebSocket Module] Explicit client type specified: ${clientType}`); + logger.debug(`[WebSocket Module] Explicit client type specified: ${clientType}`); _globalCommInstance = createCommunicationLayer({ transport: clientType }); } // Priority 4: Default auto-detection else { - console.log(`[WebSocket Module] Using auto-detection for transport selection`); + logger.debug(`[WebSocket Module] Using auto-detection for transport selection`); _globalCommInstance = createCommunicationLayer(); } return _globalCommInstance; } catch (error) { - console.error('[WebSocket Module] Error creating communication layer:', error); + logger.error('[WebSocket Module] Error creating communication layer:', error); // Fallback to basic creation _globalCommInstance = createCommunicationLayer(); return _globalCommInstance; @@ -3414,7 +3417,7 @@ function checkServerAvailability() { // 4. Check if project_fs.json is NOT available locally (indicates server environment) const hasLocalProjectFs = checkForProjectFsSync(); - console.log(`[WebSocket Module] Server availability check:`, { + logger.debug(`[WebSocket Module] Server availability check:`, { isServerPort, storedServerUrl: !!storedServerUrl, hasServerEndpoints, @@ -3426,7 +3429,7 @@ function checkServerAvailability() { return hasServerConnection; } catch (error) { - console.warn('[WebSocket Module] Error checking server availability:', error); + logger.warn('[WebSocket Module] Error checking server availability:', error); return false; } } @@ -3435,14 +3438,14 @@ function detectHtmlExportEnvironment() { try { // Primary detection: Check for explicit client type first if (window.__PRESWALD_CLIENT_TYPE === 'comlink') { - console.log('[WebSocket Module] HTML export detected via explicit client type: comlink'); + logger.debug('[WebSocket Module] HTML export detected via explicit client type: comlink'); return true; } // Secondary detection: Check for project_fs.json existence (synchronous check) const hasProjectFs = checkForProjectFsSync(); if (hasProjectFs) { - console.log('[WebSocket Module] HTML export detected via project_fs.json presence'); + logger.debug('[WebSocket Module] HTML export detected via project_fs.json presence'); return true; } @@ -3479,7 +3482,7 @@ function detectHtmlExportEnvironment() { const isHtmlExport = positiveIndicators >= 2; if (isHtmlExport) { - console.log(`[WebSocket Module] HTML export detected with ${positiveIndicators} indicators:`, { + logger.debug(`[WebSocket Module] HTML export detected with ${positiveIndicators} indicators:`, { clientType: window.__PRESWALD_CLIENT_TYPE, pathname: window.location.pathname, protocol: window.location.protocol, @@ -3493,7 +3496,7 @@ function detectHtmlExportEnvironment() { return isHtmlExport; } catch (error) { - console.warn('[WebSocket Module] Error detecting HTML export environment:', error); + logger.warn('[WebSocket Module] Error detecting HTML export environment:', error); return false; } } @@ -3554,7 +3557,7 @@ const createCommunicationLayerWithServerUrl = (serverUrl, additionalConfig = {}) */ const reconnectWithServerUrl = async (newServerUrl = null) => { try { - console.log(`[WebSocket Module] Reconnecting with server URL: ${newServerUrl || 'auto-resolve'}`); + logger.debug(`[WebSocket Module] Reconnecting with server URL: ${newServerUrl || 'auto-resolve'}`); if (newServerUrl) { ServerUrlResolver.setServerUrl(newServerUrl); @@ -3570,13 +3573,13 @@ const reconnectWithServerUrl = async (newServerUrl = null) => { // Reconnect with new configuration if (comm && typeof comm.connect === 'function') { const result = await comm.connect({ forceReconnect: true }); - console.log('[WebSocket Module] Reconnection result:', result); + logger.debug('[WebSocket Module] Reconnection result:', result); return result.success || false; } return false; } catch (error) { - console.error('[WebSocket Module] Error during reconnection:', error); + logger.error('[WebSocket Module] Error during reconnection:', error); return false; } }; @@ -3605,7 +3608,7 @@ const getServerConfiguration = async () => { } }; } catch (error) { - console.error('[WebSocket Module] Error getting server configuration:', error); + logger.error('[WebSocket Module] Error getting server configuration:', error); return { currentServerUrl: 'unknown', isConnected: false, diff --git a/preswald/engine/base_service.py b/preswald/engine/base_service.py index b633dc3f..bf873c57 100644 --- a/preswald/engine/base_service.py +++ b/preswald/engine/base_service.py @@ -30,6 +30,8 @@ class BasePreswaldService: Manages component states, diffing, and render buffer. """ + _instance = None + _instance_lock = Lock() _not_initialized_msg = "Base service not initialized." def __init__(self): @@ -84,11 +86,12 @@ def get_instance(cls): @classmethod def initialize(cls, script_path=None): - if cls._instance is None: - cls._instance = cls() - if script_path: - cls._instance._script_path = os.path.abspath(script_path) - cls._instance._initialize_data_manager(script_path) + with cls._instance_lock: + if cls._instance is None: + cls._instance = cls() + if script_path: + cls._instance._script_path = os.path.abspath(script_path) + cls._instance._initialize_data_manager(script_path) return cls._instance @property diff --git a/preswald/engine/managers/data.py b/preswald/engine/managers/data.py index 87a57d63..77066985 100644 --- a/preswald/engine/managers/data.py +++ b/preswald/engine/managers/data.py @@ -276,7 +276,7 @@ def __del__(self): try: # Clean up the CHSQL connection self._duckdb.execute("CALL chsql_cleanup();") - except: # noqa: E722 + except Exception: # noqa: S110 pass # Ignore cleanup errors on destruction diff --git a/preswald/engine/transformers/reactive_runtime.py b/preswald/engine/transformers/reactive_runtime.py index d872a21d..fdb0d0e7 100644 --- a/preswald/engine/transformers/reactive_runtime.py +++ b/preswald/engine/transformers/reactive_runtime.py @@ -283,7 +283,7 @@ def _generate_component_metadata(self, body: list[ast.stmt]) -> dict[int, tuple[ for stmt in body: # Skip function bodies entirely if isinstance(stmt, ast.FunctionDef): - logger.debug(f'[DEBUG] skipping _generate_component_metadata for {stmt.name=}') + logger.debug(f'skipping _generate_component_metadata for {stmt.name=}') continue call_node = stmt.value if isinstance(stmt, ast.Expr) else getattr(stmt, "value", None) @@ -937,7 +937,7 @@ def _lift_blackbox_function_call( ) return - logger.debug(f'[DEBUG] _lift_blackbox_function_call {component_id=}; {atom_name=}; {dep_names=}') + logger.debug(f'_lift_blackbox_function_call {component_id=}; {atom_name=}; {dep_names=}') self._finalize_and_register_atom( atom_name, component_id, @@ -1264,11 +1264,11 @@ def _try_lift_display_renderer( Returns: True if lifting succeeded and the atom was registered, False otherwise. """ - logger.debug(f"[DEBUG] Attempting to lift display renderer: {candidate=}, {component_id=}, {dependencies=}") + logger.debug(f"Attempting to lift display renderer: {candidate=}, {component_id=}, {dependencies=}") renderer_fn = get_display_renderers().get(candidate) if not renderer_fn: - logger.warning(f"[DEBUG] No renderer function registered for: {candidate}") + logger.debug(f"No renderer function registered for: {candidate}") return False self._used_display_renderer_fns.add(renderer_fn.__name__) @@ -1278,11 +1278,11 @@ def _try_lift_display_renderer( else: atom_name = generate_stable_atom_name_from_component_id(component_id) - logger.debug(f'[DEBUG] in _try_lift_display_renderer component id and atom name generated for {renderer_fn.__name__=} {component_id=} {atom_name=}') + logger.debug(f'in _try_lift_display_renderer component id and atom name generated for {renderer_fn.__name__=} {component_id=} {atom_name=}') call_node = stmt.value if isinstance(stmt, ast.Expr) else stmt.value if isinstance(stmt, ast.Assign) else None if not isinstance(call_node, ast.Call): - logger.warning(f"[DEBUG] Statement does not contain a valid call: {stmt}") + logger.debug(f"Statement does not contain a valid call: {stmt}") return False # Inspect the renderer function to determine parameter names @@ -1343,14 +1343,14 @@ def _try_lift_display_renderer( self._finalize_and_register_atom(atom_name, component_id, callsite_deps, renderer_call, callsite_node=stmt) - #logger.debug(f"[DEBUG] Replacing .show call with call to: {renderer_fn.__name__}({object_arg=}, {component_id=})") + #logger.debug(f"Replacing .show call with call to: {renderer_fn.__name__}({object_arg=}, {component_id=})") return True def _maybe_lift_display_renderer_from_expr(self, stmt: ast.Expr, call_node: ast.Call) -> bool: - logger.debug(f'[DEBUG] _maybe_lift_display_renderer_from_expr - {stmt=}; {call_node=}') + logger.debug(f'_maybe_lift_display_renderer_from_expr - {stmt=}; {call_node=}') if not isinstance(call_node.func, ast.Attribute): - logger.debug('[DEBUG] _maybe_lift_display_renderer_from_expr - returning because call_node.func is not an instance of attribute') + logger.debug(f'_maybe_lift_display_renderer_from_expr - returning because call_node.func is not an instance of attribute') return False attr = call_node.func.attr @@ -1362,11 +1362,11 @@ def _maybe_lift_display_renderer_from_expr(self, stmt: ast.Expr, call_node: ast. elif isinstance(receiver, ast.Subscript) and isinstance(receiver.value, ast.Name): varname = receiver.value.id else: - logger.debug(f'[DEBUG] _maybe_lift_display_renderer_from_expr - returning receiver is not a Name or Subscript {receiver=}') + logger.debug(f'_maybe_lift_display_renderer_from_expr - returning receiver is not a Name or Subscript {receiver=}') return False - logger.debug(f'[DEBUG] _maybe_lift_display_renderer_from_expr - {receiver=}; {attr=}; {varname=}') + logger.debug(f'_maybe_lift_display_renderer_from_expr - {receiver=}; {attr=}; {varname=}') atom_name = self._current_frame.variable_to_atom.get(varname) return_type = self._resolve_display_return_type(atom_name, varname) @@ -1391,15 +1391,15 @@ def _maybe_lift_display_renderer_from_expr(self, stmt: ast.Expr, call_node: ast. # check detectors for detector in get_display_detectors(): - logger.debug(f'[DEBUG] _maybe_lift_display_renderer_from_expr - applying detector to {call_node=}') + logger.debug(f'_maybe_lift_display_renderer_from_expr - applying detector to {call_node=}') if detector(call_node): candidate = f"{self.import_aliases.get(varname, varname)}.{attr}" resolver = get_display_dependency_resolvers().get(candidate) deps = resolver(self._current_frame) if resolver else [] - logger.debug(f'[DEBUG] _maybe_lift_display_renderer_from_expr - detected candidate {resolver=}; {candidate=}; {stmt=}; {deps=}') + logger.debug(f'_maybe_lift_display_renderer_from_expr - detected candidate {resolver=}; {candidate=}; {stmt=}; {deps=}') return self._try_lift_display_renderer(candidate=candidate, stmt=stmt, dependencies=deps) - logger.debug(f'[DEBUG] _maybe_lift_display_renderer_from_expr - nothing handled, returning False {candidate=}') + logger.debug(f'_maybe_lift_display_renderer_from_expr - nothing handled, returning False {candidate=}') return False @@ -1426,8 +1426,8 @@ def _lift_statements( # noqa: C901 Returns: A list of top level statements that are not lifted, to include in the rewritten module. """ - logger.debug(f"[DEBUG] Lifting statements inside function: {self.current_function.name if self.current_function else ''}") - logger.debug(f"[DEBUG] _lift_statements in {self.current_function.name if self.current_function else ''}") + logger.debug(f"Lifting statements inside function: {self.current_function.name if self.current_function else ''}") + logger.debug(f"_lift_statements in {self.current_function.name if self.current_function else ''}") component_metadata = component_metadata or {} return_renderers = {} if self._in_function_body else get_return_renderers() @@ -1436,7 +1436,7 @@ def _lift_statements( # noqa: C901 stmt_variable_maps, _ = self._generate_stmt_variable_maps(body, component_metadata) - logger.debug(f'[DEBUG] {return_renderers=} {output_stream_calls}') + logger.debug(f'{return_renderers=} {output_stream_calls}') new_body = [] pending_assignments = [] @@ -1449,7 +1449,7 @@ def _lift_statements( # noqa: C901 # skip user defined functions unless they are explicitly decorated or contain reactive calls if isinstance(stmt, ast.FunctionDef): if self._is_undecorated(stmt) and self._is_user_defined_blackbox_function(stmt): - logger.debug(f"[DEBUG] Skipping non-reactive user function: {stmt.name}") + logger.debug(f"Skipping non-reactive user function: {stmt.name}") new_body.append(stmt) continue @@ -1461,8 +1461,8 @@ def _lift_statements( # noqa: C901 self._lift_blackbox_function_call(stmt, stmt.value.func.id, scoped_map, variable_map) continue - logger.debug(f"[DEBUG] variable_map for stmt: {stmt} -> {stmt_variable_maps.get(stmt)}") - logger.debug(f"[DEBUG] Examining stmt: {ast.dump(stmt)}") + logger.debug(f"variable_map for stmt: {stmt} -> {stmt_variable_maps.get(stmt)}") + logger.debug(f"Examining stmt: {ast.dump(stmt)}") # Handle in script resolver registrations, such as register_display_dependency_resolver if ( @@ -1494,7 +1494,7 @@ def _lift_statements( # noqa: C901 logger.warning("[AST] register_display_dependency_resolver: expected lambda as second argument") continue - logger.info('[DEBUG] inside register_display_dependency_resolver gaurd') + logger.debug(f'inside register_display_dependency_resolver guard') try: func_name = func_name_node.value # e.g. "matplotlib.pyplot.show" @@ -1530,7 +1530,7 @@ def _lift_statements( # noqa: C901 logger.debug(f'handing known compnent calls {display_methods.items()=}') if self._is_known_component_call(call_node): full_func_name = self._get_call_func_name(call_node) - logger.debug(f"[DEBUG] Attempting to lift known component call '{full_func_name}' inside {self.current_function.name if self.current_function else ''}") + logger.debug(f"Attempting to lift known component call '{full_func_name}' inside {self.current_function.name if self.current_function else ''}") component_id, atom_name = component_metadata.get(id(call_node), (None, None)) if not atom_name: @@ -1628,7 +1628,7 @@ def _lift_statements( # noqa: C901 and isinstance(stmt.value.func.value, ast.Name) and stmt.value.func.value.id in self._current_frame.variable_to_atom ): - logger.debug('[DEBUG] going to call _lift_side_effect_stmt for %s', stmt.value.func.value.id) + logger.debug(f'going to call _lift_side_effect_stmt for %s', stmt.value.func.value.id) self._lift_side_effect_stmt(stmt) continue @@ -2282,7 +2282,7 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: # noqa: N if self._is_top_level(node): if self._is_undecorated(node) and self._is_user_defined_blackbox_function(node): - logger.debug(f"[DEBUG] visit_FunctionDef: Skipping top level user function: {node.name}") + logger.debug(f"visit_FunctionDef: Skipping top level user function: {node.name}") return node # Attach atom decorator @@ -2306,7 +2306,7 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: # noqa: N node.body = self._lift_statements(node.body, component_metadata=component_metadata) for atom in self._current_frame.generated_atoms: - logger.info(f"[DEBUG] Atom lifted inside function {node.name}: {atom.name}") + logger.debug(f"Atom lifted inside function {node.name}: {atom.name}") finally: self._module_frame.generated_atoms.extend(self._current_frame.generated_atoms) @@ -2382,7 +2382,7 @@ def generic_visit(self, node): super().generic_visit(node) finder = Finder() - #logger.debug(f"[DEBUG] AST node for dependency scan: {ast.dump(node, indent=2)}") + #logger.debug(f"AST node for dependency scan: {ast.dump(node, indent=2)}") finder.visit(node) return deps, dep_names diff --git a/preswald/interfaces/data.py b/preswald/interfaces/data.py index 5b2f38be..c8803bdc 100644 --- a/preswald/interfaces/data.py +++ b/preswald/interfaces/data.py @@ -17,10 +17,10 @@ def connect(): service = PreswaldService.get_instance() source_names, duckdb_conn = service.data_manager.connect() logger.info(f"Successfully connected to data sources: {source_names}") - # TODO: bug - getting duplicated if there are multiple clients return duckdb_conn except Exception as e: logger.error(f"Error connecting to datasources: {e}") + return None def query(sql: str, source_name: str) -> pd.DataFrame: @@ -34,6 +34,7 @@ def query(sql: str, source_name: str) -> pd.DataFrame: return df_result except Exception as e: logger.error(f"Error querying data source: {e}") + return pd.DataFrame() def get_df(source_name: str, table_name: str | None = None) -> pd.DataFrame: @@ -48,3 +49,4 @@ def get_df(source_name: str, table_name: str | None = None) -> pd.DataFrame: return df_result except Exception as e: logger.error(f"Error getting a dataframe from data source: {e}") + return pd.DataFrame() diff --git a/preswald/interfaces/render/registry.py b/preswald/interfaces/render/registry.py index 40deefc4..412e8d2b 100644 --- a/preswald/interfaces/render/registry.py +++ b/preswald/interfaces/render/registry.py @@ -253,7 +253,7 @@ def display_matplotlib_show(component_id: str): identifiers.append(identifier) plt.close('all') - logger.debug(f'[DEBUG] display_matplotlib_show - returning {len(components)} with {identifiers=}') + logger.debug(f'display_matplotlib_show - returning {len(components)} with {identifiers=}') return tuple(components) def display_plotly_figure_show(fig, component_id=None): @@ -336,7 +336,7 @@ def get_plotly_submodules(): t0 = time.perf_counter() try: - logger.info('[DEBUG] pre-registering display methods') + logger.debug('pre-registering display methods') # --- Matplotlib Registration --- register_display_method(MatplotlibFigure, "show") diff --git a/preswald/interfaces/workflow.py b/preswald/interfaces/workflow.py index 5eed74e0..af8586ac 100644 --- a/preswald/interfaces/workflow.py +++ b/preswald/interfaces/workflow.py @@ -570,12 +570,12 @@ def _execute_atom_inner(self, atom: Atom, dependency_values: dict[str, Any], inp if self._service: if isinstance(result, ComponentReturn): - logger.info('[DEBUG] - register_component_producer from workflow _execute_inner') + logger.debug('register_component_producer from workflow _execute_inner') self.register_component_producer(result.component_id, atom.name) elif isinstance(result, tuple): for item in result: if isinstance(item, ComponentReturn): - logger.info('[DEBUG] - register_component_producer from workflow _execute_inner. result is tuple.') + logger.debug('register_component_producer from workflow _execute_inner. result is tuple.') self.register_component_producer(item.component_id, atom.name) end_time = time.time() @@ -737,7 +737,7 @@ def get_critical_path(self) -> list[str]: return max(path_weights, key=lambda x: x[0])[1] except nx.NetworkXException as e: - print(f"Error finding critical path: {e}") + logger.error(f"Error finding critical path: {e}") return [] def get_parallel_groups(self) -> list[set[str]]: @@ -750,7 +750,7 @@ def get_parallel_groups(self) -> list[set[str]]: try: return list(nx.topological_generations(self.graph)) except nx.NetworkXException as e: - print(f"Error finding parallel groups: {e}") + logger.error(f"Error finding parallel groups: {e}") return [] def visualize( diff --git a/preswald/utils.py b/preswald/utils.py index 52da82fb..af0e10d1 100644 --- a/preswald/utils.py +++ b/preswald/utils.py @@ -52,7 +52,8 @@ def read_port_from_config(config_path: str, port: int): port = config["project"]["port"] return port except Exception as e: - print(f"Warning: Could not load port config from {config_path}: {e}") + logger.warning(f"Could not load port config from {config_path}: {e}") + return port def configure_logging(config_path: str | None = None, level: str | None = None): diff --git a/tests/test_data_interface.py b/tests/test_data_interface.py new file mode 100644 index 00000000..9e618679 --- /dev/null +++ b/tests/test_data_interface.py @@ -0,0 +1,80 @@ +""" +Tests for data interface bug fixes +""" +import unittest +from unittest.mock import Mock, patch +import pandas as pd + + +class TestDataInterfaceReturnValues(unittest.TestCase): + """Test that data interface functions return proper values in all paths.""" + + def test_query_exception_returns_empty_dataframe(self): + """Test that query() returns empty DataFrame on exception.""" + from preswald.interfaces.data import query + + # Mock PreswaldService to raise exception + with patch('preswald.interfaces.data.PreswaldService') as mock_service: + mock_instance = Mock() + mock_instance.data_manager.query.side_effect = Exception("Connection error") + mock_service.get_instance.return_value = mock_instance + + # Call query and verify it returns empty DataFrame on exception + result = query("SELECT * FROM test", "test_source") + + # Should return empty DataFrame, not None + self.assertIsInstance(result, pd.DataFrame) + self.assertTrue(result.empty) + + def test_get_df_exception_returns_empty_dataframe(self): + """Test that get_df() returns empty DataFrame on exception.""" + from preswald.interfaces.data import get_df + + # Mock PreswaldService to raise exception + with patch('preswald.interfaces.data.PreswaldService') as mock_service: + mock_instance = Mock() + mock_instance.data_manager.get_df.side_effect = Exception("Connection error") + mock_service.get_instance.return_value = mock_instance + + # Call get_df and verify it returns empty DataFrame on exception + result = get_df("test_source") + + # Should return empty DataFrame, not None + self.assertIsInstance(result, pd.DataFrame) + self.assertTrue(result.empty) + + def test_query_success_returns_dataframe(self): + """Test that query() returns DataFrame on success.""" + from preswald.interfaces.data import query + + expected_df = pd.DataFrame({'col1': [1, 2], 'col2': ['a', 'b']}) + + with patch('preswald.interfaces.data.PreswaldService') as mock_service: + mock_instance = Mock() + mock_instance.data_manager.query.return_value = expected_df + mock_service.get_instance.return_value = mock_instance + + result = query("SELECT * FROM test", "test_source") + + self.assertIsInstance(result, pd.DataFrame) + pd.testing.assert_frame_equal(result, expected_df) + + def test_get_df_success_returns_dataframe(self): + """Test that get_df() returns DataFrame on success.""" + from preswald.interfaces.data import get_df + + expected_df = pd.DataFrame({'col1': [1, 2], 'col2': ['a', 'b']}) + + with patch('preswald.interfaces.data.PreswaldService') as mock_service: + mock_instance = Mock() + mock_instance.data_manager.get_df.return_value = expected_df + mock_service.get_instance.return_value = mock_instance + + result = get_df("test_source") + + self.assertIsInstance(result, pd.DataFrame) + pd.testing.assert_frame_equal(result, expected_df) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_exception_handling.py b/tests/test_exception_handling.py new file mode 100644 index 00000000..d725a7c9 --- /dev/null +++ b/tests/test_exception_handling.py @@ -0,0 +1,61 @@ +""" +Tests for exception handling improvements +""" +import unittest +from unittest.mock import Mock, patch +import pandas as pd + + +class TestExceptionHandling(unittest.TestCase): + """Test that exceptions are handled properly.""" + + def test_connect_exception_returns_none(self): + """Test that connect() returns None on exception.""" + from preswald.interfaces.data import connect + + # Mock PreswaldService to raise exception + with patch('preswald.interfaces.data.PreswaldService') as mock_service: + mock_instance = Mock() + mock_instance.data_manager.connect.side_effect = Exception("Connection error") + mock_service.get_instance.return_value = mock_instance + + # Call connect and verify it returns None on exception + result = connect() + + # Should return None, not raise + self.assertIsNone(result) + + def test_data_manager_cleanup_exception_handling(self): + """Test that DataManager cleanup handles exceptions gracefully.""" + from preswald.engine.managers.data import ClickhouseSource + + # Create a mock duckdb connection that raises on execute + mock_duckdb = Mock() + mock_duckdb.execute.side_effect = Exception("Cleanup error") + + # Create source and test cleanup + source = ClickhouseSource.__new__(ClickhouseSource) + source._duckdb = mock_duckdb + + # This should not raise + try: + source.__del__() + except Exception: + self.fail("__del__ should not raise exceptions") + + +class TestLoggerConsistency(unittest.TestCase): + """Test that loggers are used consistently.""" + + def test_workflow_uses_logger_for_errors(self): + """Test that workflow uses logger instead of print for errors.""" + from preswald.interfaces.workflow import Workflow + import logging + + # Check that the module has a logger + import preswald.interfaces.workflow as workflow_module + self.assertTrue(hasattr(workflow_module, 'logger')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_singleton_thread_safety.py b/tests/test_singleton_thread_safety.py new file mode 100644 index 00000000..ee2612b9 --- /dev/null +++ b/tests/test_singleton_thread_safety.py @@ -0,0 +1,84 @@ +""" +Tests for singleton thread safety +""" +import unittest +import threading +import time +from unittest.mock import patch + + +class TestSingletonThreadSafety(unittest.TestCase): + """Test that singleton pattern is thread-safe.""" + + def test_base_service_singleton_lock_exists(self): + """Test that BasePreswaldService has _instance_lock.""" + from preswald.engine.base_service import BasePreswaldService + + self.assertTrue(hasattr(BasePreswaldService, '_instance_lock')) + self.assertIsNotNone(BasePreswaldService._instance_lock) + + def test_singleton_instance_is_shared(self): + """Test that singleton instance is shared across calls.""" + from preswald.engine.base_service import BasePreswaldService + + # Reset instance for test + BasePreswaldService._instance = None + + # Create two instances + instance1 = BasePreswaldService.initialize() + instance2 = BasePreswaldService.get_instance() + + # They should be the same object + self.assertIs(instance1, instance2) + + # Clean up + BasePreswaldService._instance = None + + +class TestErrorRegistrySingleton(unittest.TestCase): + """Test ErrorRegistry singleton pattern.""" + + def test_error_registry_is_singleton(self): + """Test that ErrorRegistry is a singleton.""" + from preswald.interfaces.render.error_registry import ErrorRegistry + + # Get two instances + instance1 = ErrorRegistry.get_instance() + instance2 = ErrorRegistry.get_instance() + + # They should be the same object + self.assertIs(instance1, instance2) + + def test_error_registry_thread_safety(self): + """Test that ErrorRegistry get_instance is thread-safe.""" + from preswald.interfaces.render.error_registry import ErrorRegistry + + instances = [] + errors = [] + + def get_instance(): + try: + instance = ErrorRegistry.get_instance() + instances.append(instance) + except Exception as e: + errors.append(e) + + # Create multiple threads + threads = [threading.Thread(target=get_instance) for _ in range(10)] + + # Start all threads + for t in threads: + t.start() + + # Wait for all to complete + for t in threads: + t.join() + + # All instances should be the same + self.assertEqual(len(instances), 10) + self.assertTrue(all(i is instances[0] for i in instances)) + self.assertEqual(len(errors), 0) + + +if __name__ == '__main__': + unittest.main()