import { DEFAULT_CONNECTION_CONFIG } from '../Defaults' import type { UserFacingSocketConfig, WAVersion } from '../Types' import type { VersionCacheLogger } from '../Utils/version-cache' import { clearVersionCache, getCachedVersion, getVersionCacheStatus, refreshVersionCache } from '../Utils/version-cache' import { makeCommunitiesSocket } from './communities' /** * Adapts Baileys logger to VersionCacheLogger interface */ const createCacheLogger = (logger: any): VersionCacheLogger | undefined => { if (!logger) return undefined return { info: (obj: unknown, msg?: string) => logger.info(obj, msg), debug: (obj: unknown, msg?: string) => logger.debug(obj, msg), warn: (obj: unknown, msg?: string) => logger.warn(obj, msg) } } /** * Compares two WhatsApp versions * @returns true if versions are different */ const versionsAreDifferent = (v1: WAVersion, v2: WAVersion): boolean => { return v1[0] !== v2[0] || v1[1] !== v2[1] || v1[2] !== v2[2] } /** * Checks if a version change is critical (major or minor version changed) */ const isCriticalVersionChange = (oldVersion: WAVersion, newVersion: WAVersion): boolean => { // Major version change (index 0) or minor version change (index 1) return oldVersion[0] !== newVersion[0] || oldVersion[1] !== newVersion[1] } // export the last socket layer const makeWASocket = (config: UserFacingSocketConfig) => { const newConfig = { ...DEFAULT_CONNECTION_CONFIG, ...config } return makeCommunitiesSocket(newConfig) } /** * Creates a WhatsApp socket connection with automatic version fetching * and periodic version checks (soft update - transparent to user). * * Features: * - **Shared cache**: 150 connections = 1 request (not 150) * - **Persistent cache**: Survives server restarts * - Fetches latest version on connect (uses cache if valid) * - Checks for new versions every 6 hours (configurable) * - Updates version on next natural reconnection (transparent) * - Emits 'version.update' event when new version is detected * * @example * ```typescript * // All connections share the same cached version * const sock = await makeWASocketAutoVersion({ * auth: state, * versionCheckIntervalMs: 6 * 60 * 60 * 1000 // 6 hours (default) * }) * * // Listen for version updates * sock.ev.on('version.update', ({ currentVersion, newVersion, isCritical }) => { * console.log(`New version detected: ${newVersion.join('.')}`) * // Version will be used on next reconnection automatically * }) * ``` */ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => { const mergedConfig = { ...DEFAULT_CONNECTION_CONFIG, ...config } const logger = mergedConfig.logger const cacheLogger = createCacheLogger(logger) const checkIntervalMs = mergedConfig.versionCheckIntervalMs // Track version separately to avoid mutating config (Fix #7) let trackedVersion: WAVersion = [...mergedConfig.version] as WAVersion let versionCheckInterval: ReturnType | null = null let isSocketClosed = false /** * Cleans up the version check interval */ const cleanupInterval = () => { if (versionCheckInterval) { clearInterval(versionCheckInterval) versionCheckInterval = null logger?.debug('Stopped periodic version check') } } // Fetch latest version using SHARED CACHE // 150 connections starting = 1 request (deduplication) try { const { version, fromCache, age } = await getCachedVersion({ cacheTtlMs: checkIntervalMs, logger: cacheLogger }) logger?.info( { version, fromCache, ageMinutes: fromCache ? Math.round(age / 60000) : 0 }, fromCache ? 'Using cached WhatsApp Web version' : 'Fetched fresh WhatsApp Web version' ) mergedConfig.version = version trackedVersion = [...version] as WAVersion } catch (error) { logger?.warn({ error, fallbackVersion: mergedConfig.version }, 'Error fetching version, using bundled version') } // Create the socket const sock = makeWASocket(mergedConfig) // Listen for connection close to cleanup interval (Fix #1, #6) // This handles both explicit sock.end() and internal disconnections sock.ev.on('connection.update', update => { if (update.connection === 'close') { isSocketClosed = true cleanupInterval() } else if (update.connection === 'open') { isSocketClosed = false } }) // Setup periodic version check if interval > 0 if (checkIntervalMs > 0) { logger?.info({ intervalHours: checkIntervalMs / (60 * 60 * 1000) }, 'Starting periodic version check') versionCheckInterval = setInterval(async () => { // Skip if socket is closed (Fix #8 - race condition) if (isSocketClosed) { cleanupInterval() return } try { logger?.debug('Checking for WhatsApp Web version update...') // Check cache status first const cacheStatus = getVersionCacheStatus(checkIntervalMs) // Only refresh if cache is expired (one socket refreshes, others use cache) let newVersion: WAVersion let fetchSuccess = true if (cacheStatus.isExpired) { logger?.debug('Cache expired, refreshing...') const result = await refreshVersionCache({ logger: cacheLogger }) newVersion = result.version fetchSuccess = result.success // Don't update to fallback version on transient network errors if (!fetchSuccess) { logger?.warn({ fallbackVersion: result.version }, 'Failed to fetch latest version, keeping current version') return // Skip version update on fetch failure } } else if (cacheStatus.version) { // Cache still valid, use cached version directly (no file I/O) newVersion = cacheStatus.version } else { // No cache available, skip this check return } // Double-check socket is still open after async operation (Fix #8) if (isSocketClosed) { cleanupInterval() return } if (versionsAreDifferent(trackedVersion, newVersion)) { const isCritical = isCriticalVersionChange(trackedVersion, newVersion) const previousVersion = trackedVersion logger?.info( { currentVersion: previousVersion, newVersion: newVersion, isCritical }, 'New WhatsApp Web version detected! Will use on next reconnection.' ) // Update tracked version for next reconnection (Fix #7) trackedVersion = [...newVersion] as WAVersion // Emit event for user to handle (only if socket still open) if (!isSocketClosed) { sock.ev.emit('version.update', { currentVersion: previousVersion, newVersion: newVersion, isCritical }) } } else { logger?.debug({ version: trackedVersion }, 'Version is up to date') } } catch (error) { logger?.warn({ error }, 'Error checking for version update') } }, checkIntervalMs) } return sock } // Export cache utilities for manual control export { getCachedVersion, refreshVersionCache, clearVersionCache, getVersionCacheStatus } export default makeWASocket