const { Client } = require('ssh2'); const mysql = require('mysql2/promise'); const fs = require('fs'); // Connection pooling and cache configuration const connectionCache = { ssh: null, dbConnection: null, lastUsed: 0, isConnecting: false, connectionPromise: null, // Cache expiration time in milliseconds (5 minutes) expirationTime: 5 * 60 * 1000, // Cache for query results (key: query string, value: {data, timestamp}) queryCache: new Map(), // Cache duration for different query types in milliseconds cacheDuration: { 'field-options': 30 * 60 * 1000, // 30 minutes for field options 'product-lines': 10 * 60 * 1000, // 10 minutes for product lines 'sublines': 10 * 60 * 1000, // 10 minutes for sublines 'taxonomy': 30 * 60 * 1000, // 30 minutes for taxonomy data 'default': 60 * 1000 // 1 minute default } }; /** * Get a database connection with connection pooling * @returns {Promise<{ssh: object, connection: object}>} The SSH and database connection */ async function getDbConnection() { const now = Date.now(); // Check if we need to refresh the connection due to inactivity const needsRefresh = !connectionCache.ssh || !connectionCache.dbConnection || (now - connectionCache.lastUsed > connectionCache.expirationTime); // If connection is still valid, update last used time and return existing connection if (!needsRefresh) { connectionCache.lastUsed = now; return { ssh: connectionCache.ssh, connection: connectionCache.dbConnection }; } // If another request is already establishing a connection, wait for that promise if (connectionCache.isConnecting && connectionCache.connectionPromise) { try { await connectionCache.connectionPromise; return { ssh: connectionCache.ssh, connection: connectionCache.dbConnection }; } catch (error) { // If that connection attempt failed, we'll try again below console.error('Error waiting for existing connection:', error); } } // Close existing connections if they exist if (connectionCache.dbConnection) { try { await connectionCache.dbConnection.end(); } catch (error) { console.error('Error closing existing database connection:', error); } } if (connectionCache.ssh) { try { connectionCache.ssh.end(); } catch (error) { console.error('Error closing existing SSH connection:', error); } } // Mark that we're establishing a new connection connectionCache.isConnecting = true; // Create a new promise for this connection attempt connectionCache.connectionPromise = setupSshTunnel().then(tunnel => { const { ssh, stream, dbConfig } = tunnel; return mysql.createConnection({ ...dbConfig, stream }).then(connection => { // Store the new connections connectionCache.ssh = ssh; connectionCache.dbConnection = connection; connectionCache.lastUsed = Date.now(); connectionCache.isConnecting = false; return { ssh, connection }; }); }).catch(error => { connectionCache.isConnecting = false; throw error; }); // Wait for the connection to be established return connectionCache.connectionPromise; } /** * Get cached query results or execute query if not cached * @param {string} cacheKey - Unique key to identify the query * @param {string} queryType - Type of query (field-options, product-lines, etc.) * @param {Function} queryFn - Function to execute if cache miss * @returns {Promise} The query result */ async function getCachedQuery(cacheKey, queryType, queryFn) { // Get cache duration based on query type const cacheDuration = connectionCache.cacheDuration[queryType] || connectionCache.cacheDuration.default; // Check if we have a valid cached result const cachedResult = connectionCache.queryCache.get(cacheKey); const now = Date.now(); if (cachedResult && (now - cachedResult.timestamp < cacheDuration)) { console.log(`Cache hit for ${queryType} query: ${cacheKey}`); return cachedResult.data; } // No valid cache found, execute the query console.log(`Cache miss for ${queryType} query: ${cacheKey}`); const result = await queryFn(); // Cache the result connectionCache.queryCache.set(cacheKey, { data: result, timestamp: now }); return result; } /** * Setup SSH tunnel to production database * @private - Should only be used by getDbConnection * @returns {Promise<{ssh: object, stream: object, dbConfig: object}>} */ async function setupSshTunnel() { const sshConfig = { host: process.env.PROD_SSH_HOST, port: process.env.PROD_SSH_PORT || 22, username: process.env.PROD_SSH_USER, privateKey: process.env.PROD_SSH_KEY_PATH ? fs.readFileSync(process.env.PROD_SSH_KEY_PATH) : undefined, compress: true }; const dbConfig = { host: process.env.PROD_DB_HOST || 'localhost', user: process.env.PROD_DB_USER, password: process.env.PROD_DB_PASSWORD, database: process.env.PROD_DB_NAME, port: process.env.PROD_DB_PORT || 3306, timezone: 'Z' }; return new Promise((resolve, reject) => { const ssh = new Client(); ssh.on('error', (err) => { console.error('SSH connection error:', err); reject(err); }); ssh.on('ready', () => { ssh.forwardOut( '127.0.0.1', 0, dbConfig.host, dbConfig.port, (err, stream) => { if (err) reject(err); resolve({ ssh, stream, dbConfig }); } ); }).connect(sshConfig); }); } /** * Clear cached query results * @param {string} [cacheKey] - Specific cache key to clear (clears all if not provided) */ function clearQueryCache(cacheKey) { if (cacheKey) { connectionCache.queryCache.delete(cacheKey); console.log(`Cleared cache for key: ${cacheKey}`); } else { connectionCache.queryCache.clear(); console.log('Cleared all query cache'); } } /** * Force close all active connections * Useful for server shutdown or manual connection reset */ async function closeAllConnections() { if (connectionCache.dbConnection) { try { await connectionCache.dbConnection.end(); console.log('Closed database connection'); } catch (error) { console.error('Error closing database connection:', error); } connectionCache.dbConnection = null; } if (connectionCache.ssh) { try { connectionCache.ssh.end(); console.log('Closed SSH connection'); } catch (error) { console.error('Error closing SSH connection:', error); } connectionCache.ssh = null; } connectionCache.lastUsed = 0; connectionCache.isConnecting = false; connectionCache.connectionPromise = null; } module.exports = { getDbConnection, getCachedQuery, clearQueryCache, closeAllConnections };