185632c053
- Add version-cache.ts with file-based persistence - 150 connections = 1 request (not 150) via request deduplication - Cache survives server restarts via .baileys-version-cache.json - Export cache utilities: getCachedVersion, refreshVersionCache, clearVersionCache - Update makeWASocketAutoVersion to use shared cache
225 lines
5.4 KiB
TypeScript
225 lines
5.4 KiB
TypeScript
import { promises as fs } from 'fs'
|
|
import { join } from 'path'
|
|
import { fetchLatestWaWebVersion } from './generics'
|
|
import type { WAVersion } from '../Types'
|
|
|
|
/**
|
|
* Version cache entry stored in file
|
|
*/
|
|
interface VersionCacheEntry {
|
|
version: WAVersion
|
|
fetchedAt: number // timestamp in ms
|
|
source: 'online' | 'fallback'
|
|
}
|
|
|
|
/**
|
|
* In-memory cache to avoid file reads on every connection
|
|
*/
|
|
let memoryCache: VersionCacheEntry | null = null
|
|
|
|
/**
|
|
* Promise to prevent concurrent fetches (deduplication)
|
|
*/
|
|
let fetchInProgress: Promise<VersionCacheEntry> | null = null
|
|
|
|
/**
|
|
* Default cache TTL: 6 hours
|
|
*/
|
|
const DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1000
|
|
|
|
/**
|
|
* Default cache file path
|
|
*/
|
|
const DEFAULT_CACHE_FILE = join(process.cwd(), '.baileys-version-cache.json')
|
|
|
|
/**
|
|
* Configuration for version cache
|
|
*/
|
|
export interface VersionCacheConfig {
|
|
/** Cache TTL in milliseconds (default: 6 hours) */
|
|
cacheTtlMs?: number
|
|
/** Path to cache file (default: .baileys-version-cache.json in cwd) */
|
|
cacheFilePath?: string
|
|
/** Logger instance */
|
|
logger?: { info: Function; debug: Function; warn: Function }
|
|
}
|
|
|
|
/**
|
|
* Reads the cache from file
|
|
*/
|
|
async function readCacheFile(filePath: string): Promise<VersionCacheEntry | null> {
|
|
try {
|
|
const data = await fs.readFile(filePath, 'utf-8')
|
|
return JSON.parse(data) as VersionCacheEntry
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes the cache to file
|
|
*/
|
|
async function writeCacheFile(filePath: string, entry: VersionCacheEntry): Promise<void> {
|
|
try {
|
|
await fs.writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8')
|
|
} catch {
|
|
// Ignore write errors - cache is optional
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if cache entry is still valid
|
|
*/
|
|
function isCacheValid(entry: VersionCacheEntry | null, ttlMs: number): boolean {
|
|
if (!entry) return false
|
|
const age = Date.now() - entry.fetchedAt
|
|
return age < ttlMs
|
|
}
|
|
|
|
/**
|
|
* Fetches version with deduplication (prevents 150 parallel requests)
|
|
*/
|
|
async function fetchVersionOnce(
|
|
cacheFilePath: string,
|
|
logger?: VersionCacheConfig['logger']
|
|
): Promise<VersionCacheEntry> {
|
|
logger?.info('Fetching WhatsApp Web version (shared for all connections)...')
|
|
|
|
const result = await fetchLatestWaWebVersion()
|
|
|
|
const entry: VersionCacheEntry = {
|
|
version: result.version,
|
|
fetchedAt: Date.now(),
|
|
source: result.isLatest ? 'online' : 'fallback'
|
|
}
|
|
|
|
// Update memory cache
|
|
memoryCache = entry
|
|
|
|
// Persist to file (async, don't wait)
|
|
writeCacheFile(cacheFilePath, entry).catch(() => {})
|
|
|
|
logger?.info(
|
|
{ version: entry.version, source: entry.source },
|
|
'Version fetched and cached'
|
|
)
|
|
|
|
return entry
|
|
}
|
|
|
|
/**
|
|
* Gets the cached WhatsApp version, fetching if necessary.
|
|
*
|
|
* Features:
|
|
* - File-based persistence (survives restarts)
|
|
* - In-memory cache (fast access)
|
|
* - Request deduplication (150 connections = 1 request)
|
|
* - Configurable TTL
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // All 150 connections share the same cached version
|
|
* const { version } = await getCachedVersion()
|
|
* const sock = makeWASocket({ version, auth })
|
|
* ```
|
|
*/
|
|
export async function getCachedVersion(
|
|
config: VersionCacheConfig = {}
|
|
): Promise<{ version: WAVersion; fromCache: boolean; age: number }> {
|
|
const {
|
|
cacheTtlMs = DEFAULT_CACHE_TTL_MS,
|
|
cacheFilePath = DEFAULT_CACHE_FILE,
|
|
logger
|
|
} = config
|
|
|
|
// 1. Check memory cache first (fastest)
|
|
if (isCacheValid(memoryCache, cacheTtlMs)) {
|
|
const age = Date.now() - memoryCache!.fetchedAt
|
|
logger?.debug({ age: Math.round(age / 1000) + 's' }, 'Using memory cached version')
|
|
return { version: memoryCache!.version, fromCache: true, age }
|
|
}
|
|
|
|
// 2. Check file cache (survives restarts)
|
|
const fileCache = await readCacheFile(cacheFilePath)
|
|
if (isCacheValid(fileCache, cacheTtlMs)) {
|
|
memoryCache = fileCache // Update memory cache
|
|
const age = Date.now() - fileCache!.fetchedAt
|
|
logger?.debug({ age: Math.round(age / 1000) + 's' }, 'Using file cached version')
|
|
return { version: fileCache!.version, fromCache: true, age }
|
|
}
|
|
|
|
// 3. Need to fetch - but deduplicate concurrent requests
|
|
// If 150 connections start at once, only 1 fetch happens
|
|
if (!fetchInProgress) {
|
|
fetchInProgress = fetchVersionOnce(cacheFilePath, logger)
|
|
.finally(() => { fetchInProgress = null })
|
|
}
|
|
|
|
const entry = await fetchInProgress
|
|
return { version: entry.version, fromCache: false, age: 0 }
|
|
}
|
|
|
|
/**
|
|
* Clears the version cache (memory and file)
|
|
*/
|
|
export async function clearVersionCache(
|
|
cacheFilePath: string = DEFAULT_CACHE_FILE
|
|
): Promise<void> {
|
|
memoryCache = null
|
|
try {
|
|
await fs.unlink(cacheFilePath)
|
|
} catch {
|
|
// Ignore if file doesn't exist
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Forces a refresh of the cached version
|
|
*/
|
|
export async function refreshVersionCache(
|
|
config: VersionCacheConfig = {}
|
|
): Promise<WAVersion> {
|
|
const { cacheFilePath = DEFAULT_CACHE_FILE, logger } = config
|
|
|
|
// Clear existing cache
|
|
memoryCache = null
|
|
|
|
// Fetch fresh
|
|
const entry = await fetchVersionOnce(cacheFilePath, logger)
|
|
return entry.version
|
|
}
|
|
|
|
/**
|
|
* Gets cache status information
|
|
*/
|
|
export function getVersionCacheStatus(
|
|
cacheTtlMs: number = DEFAULT_CACHE_TTL_MS
|
|
): {
|
|
hasCache: boolean
|
|
version: WAVersion | null
|
|
age: number | null
|
|
isExpired: boolean
|
|
expiresIn: number | null
|
|
} {
|
|
if (!memoryCache) {
|
|
return {
|
|
hasCache: false,
|
|
version: null,
|
|
age: null,
|
|
isExpired: true,
|
|
expiresIn: null
|
|
}
|
|
}
|
|
|
|
const age = Date.now() - memoryCache.fetchedAt
|
|
const isExpired = age >= cacheTtlMs
|
|
|
|
return {
|
|
hasCache: true,
|
|
version: memoryCache.version,
|
|
age,
|
|
isExpired,
|
|
expiresIn: isExpired ? 0 : cacheTtlMs - age
|
|
}
|
|
}
|