feat: add persistent shared version cache for multiple connections
feat: add persistent shared version cache for multiple connections
This commit is contained in:
+78
-26
@@ -1,8 +1,21 @@
|
|||||||
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
||||||
import type { UserFacingSocketConfig, WAVersion } from '../Types'
|
import type { UserFacingSocketConfig, WAVersion } from '../Types'
|
||||||
import { fetchLatestWaWebVersion } from '../Utils/generics'
|
import { getCachedVersion, refreshVersionCache, clearVersionCache, getVersionCacheStatus } from '../Utils/version-cache'
|
||||||
|
import type { VersionCacheLogger } from '../Utils/version-cache'
|
||||||
import { makeCommunitiesSocket } from './communities'
|
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
|
* Compares two WhatsApp versions
|
||||||
* @returns true if versions are different
|
* @returns true if versions are different
|
||||||
@@ -41,13 +54,16 @@ const makeWASocket = (config: UserFacingSocketConfig) => {
|
|||||||
* and periodic version checks (soft update - transparent to user).
|
* and periodic version checks (soft update - transparent to user).
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Fetches latest version on connect
|
* - **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)
|
* - Checks for new versions every 6 hours (configurable)
|
||||||
* - Updates version on next natural reconnection (transparent)
|
* - Updates version on next natural reconnection (transparent)
|
||||||
* - Emits 'version.update' event when new version is detected
|
* - Emits 'version.update' event when new version is detected
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
|
* // All connections share the same cached version
|
||||||
* const sock = await makeWASocketAutoVersion({
|
* const sock = await makeWASocketAutoVersion({
|
||||||
* auth: state,
|
* auth: state,
|
||||||
* versionCheckIntervalMs: 6 * 60 * 60 * 1000 // 6 hours (default)
|
* versionCheckIntervalMs: 6 * 60 * 60 * 1000 // 6 hours (default)
|
||||||
@@ -67,6 +83,8 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logger = mergedConfig.logger
|
const logger = mergedConfig.logger
|
||||||
|
const cacheLogger = createCacheLogger(logger)
|
||||||
|
const checkIntervalMs = mergedConfig.versionCheckIntervalMs
|
||||||
|
|
||||||
// Track version separately to avoid mutating config (Fix #7)
|
// Track version separately to avoid mutating config (Fix #7)
|
||||||
let trackedVersion: WAVersion = [...mergedConfig.version] as WAVersion
|
let trackedVersion: WAVersion = [...mergedConfig.version] as WAVersion
|
||||||
@@ -84,25 +102,31 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch latest version before connecting
|
// Fetch latest version using SHARED CACHE
|
||||||
|
// 150 connections starting = 1 request (deduplication)
|
||||||
try {
|
try {
|
||||||
logger?.info('Fetching latest WhatsApp Web version...')
|
const { version, fromCache, age } = await getCachedVersion({
|
||||||
const result = await fetchLatestWaWebVersion()
|
cacheTtlMs: checkIntervalMs,
|
||||||
|
logger: cacheLogger
|
||||||
|
})
|
||||||
|
|
||||||
if (result.isLatest) {
|
logger?.info(
|
||||||
logger?.info({ version: result.version }, 'Using latest WhatsApp Web version')
|
{
|
||||||
mergedConfig.version = result.version
|
version,
|
||||||
trackedVersion = [...result.version] as WAVersion
|
fromCache,
|
||||||
} else {
|
ageMinutes: fromCache ? Math.round(age / 60000) : 0
|
||||||
logger?.warn(
|
},
|
||||||
{ error: result.error, fallbackVersion: mergedConfig.version },
|
fromCache
|
||||||
'Failed to fetch latest version, using bundled version'
|
? 'Using cached WhatsApp Web version'
|
||||||
)
|
: 'Fetched fresh WhatsApp Web version'
|
||||||
}
|
)
|
||||||
|
|
||||||
|
mergedConfig.version = version
|
||||||
|
trackedVersion = [...version] as WAVersion
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger?.warn(
|
logger?.warn(
|
||||||
{ error, fallbackVersion: mergedConfig.version },
|
{ error, fallbackVersion: mergedConfig.version },
|
||||||
'Error fetching latest version, using bundled version'
|
'Error fetching version, using bundled version'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +145,6 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Setup periodic version check if interval > 0
|
// Setup periodic version check if interval > 0
|
||||||
const checkIntervalMs = mergedConfig.versionCheckIntervalMs
|
|
||||||
if (checkIntervalMs > 0) {
|
if (checkIntervalMs > 0) {
|
||||||
logger?.info(
|
logger?.info(
|
||||||
{ intervalHours: checkIntervalMs / (60 * 60 * 1000) },
|
{ intervalHours: checkIntervalMs / (60 * 60 * 1000) },
|
||||||
@@ -137,7 +160,35 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger?.debug('Checking for WhatsApp Web version update...')
|
logger?.debug('Checking for WhatsApp Web version update...')
|
||||||
const result = await fetchLatestWaWebVersion()
|
|
||||||
|
// 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)
|
// Double-check socket is still open after async operation (Fix #8)
|
||||||
if (isSocketClosed) {
|
if (isSocketClosed) {
|
||||||
@@ -145,34 +196,32 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.isLatest && versionsAreDifferent(trackedVersion, result.version)) {
|
if (versionsAreDifferent(trackedVersion, newVersion)) {
|
||||||
const isCritical = isCriticalVersionChange(trackedVersion, result.version)
|
const isCritical = isCriticalVersionChange(trackedVersion, newVersion)
|
||||||
const previousVersion = trackedVersion
|
const previousVersion = trackedVersion
|
||||||
|
|
||||||
logger?.info(
|
logger?.info(
|
||||||
{
|
{
|
||||||
currentVersion: previousVersion,
|
currentVersion: previousVersion,
|
||||||
newVersion: result.version,
|
newVersion: newVersion,
|
||||||
isCritical
|
isCritical
|
||||||
},
|
},
|
||||||
'New WhatsApp Web version detected! Will use on next reconnection.'
|
'New WhatsApp Web version detected! Will use on next reconnection.'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update tracked version for next reconnection (Fix #7)
|
// Update tracked version for next reconnection (Fix #7)
|
||||||
trackedVersion = [...result.version] as WAVersion
|
trackedVersion = [...newVersion] as WAVersion
|
||||||
|
|
||||||
// Emit event for user to handle (only if socket still open)
|
// Emit event for user to handle (only if socket still open)
|
||||||
if (!isSocketClosed) {
|
if (!isSocketClosed) {
|
||||||
sock.ev.emit('version.update', {
|
sock.ev.emit('version.update', {
|
||||||
currentVersion: previousVersion,
|
currentVersion: previousVersion,
|
||||||
newVersion: result.version,
|
newVersion: newVersion,
|
||||||
isCritical
|
isCritical
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (result.isLatest) {
|
|
||||||
logger?.debug({ version: trackedVersion }, 'Version is up to date')
|
|
||||||
} else {
|
} else {
|
||||||
logger?.warn({ error: result.error }, 'Failed to check for version update')
|
logger?.debug({ version: trackedVersion }, 'Version is up to date')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger?.warn({ error }, 'Error checking for version update')
|
logger?.warn({ error }, 'Error checking for version update')
|
||||||
@@ -183,4 +232,7 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
|||||||
return sock
|
return sock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export cache utilities for manual control
|
||||||
|
export { getCachedVersion, refreshVersionCache, clearVersionCache, getVersionCacheStatus }
|
||||||
|
|
||||||
export default makeWASocket
|
export default makeWASocket
|
||||||
|
|||||||
@@ -33,5 +33,8 @@ export * from './cache-utils'
|
|||||||
export * from './circuit-breaker'
|
export * from './circuit-breaker'
|
||||||
export * from './retry-utils'
|
export * from './retry-utils'
|
||||||
|
|
||||||
|
// Version management
|
||||||
|
export * from './version-cache'
|
||||||
|
|
||||||
// Event streaming
|
// Event streaming
|
||||||
export * from './baileys-event-stream'
|
export * from './baileys-event-stream'
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger interface for version cache operations
|
||||||
|
*/
|
||||||
|
export interface VersionCacheLogger {
|
||||||
|
info: (obj: unknown, msg?: string) => void
|
||||||
|
debug: (obj: unknown, msg?: string) => void
|
||||||
|
warn: (obj: unknown, msg?: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* NOTE: Uses process.cwd() which may not be writable in some environments
|
||||||
|
* (containers, serverless, etc). In such cases, specify a custom `cacheFilePath`
|
||||||
|
* in the config pointing to a writable directory like `/tmp`.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
*
|
||||||
|
* NOTE: If running in a container or serverless environment where cwd
|
||||||
|
* is not writable, specify a writable path like '/tmp/.baileys-version-cache.json'
|
||||||
|
*/
|
||||||
|
cacheFilePath?: string
|
||||||
|
/** Logger instance */
|
||||||
|
logger?: VersionCacheLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result from refreshVersionCache with success status
|
||||||
|
*/
|
||||||
|
export interface RefreshVersionResult {
|
||||||
|
version: WAVersion
|
||||||
|
success: boolean
|
||||||
|
source: 'online' | 'fallback'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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?: VersionCacheLogger
|
||||||
|
): 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; source: 'online' | 'fallback' | 'memory' | 'file' }> {
|
||||||
|
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, source: 'memory' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, source: 'file' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, source: entry.source }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the version cache (memory and file).
|
||||||
|
* Also cancels any in-progress fetch to prevent it from restoring the cache.
|
||||||
|
*/
|
||||||
|
export async function clearVersionCache(
|
||||||
|
cacheFilePath: string = DEFAULT_CACHE_FILE
|
||||||
|
): Promise<void> {
|
||||||
|
// Wait for any in-progress fetch to complete before clearing
|
||||||
|
// This prevents the fetch from restoring the cache after we clear it
|
||||||
|
if (fetchInProgress) {
|
||||||
|
try {
|
||||||
|
await fetchInProgress
|
||||||
|
} catch {
|
||||||
|
// Ignore fetch errors during clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryCache = null
|
||||||
|
fetchInProgress = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.unlink(cacheFilePath)
|
||||||
|
} catch {
|
||||||
|
// Ignore if file doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces a refresh of the cached version.
|
||||||
|
* Returns success status to indicate if online fetch succeeded or fell back to bundled version.
|
||||||
|
*
|
||||||
|
* @returns Object with version, success status, and source
|
||||||
|
*/
|
||||||
|
export async function refreshVersionCache(
|
||||||
|
config: VersionCacheConfig = {}
|
||||||
|
): Promise<RefreshVersionResult> {
|
||||||
|
const { cacheFilePath = DEFAULT_CACHE_FILE, logger } = config
|
||||||
|
|
||||||
|
// Wait for any existing fetch to complete first (deduplication)
|
||||||
|
if (fetchInProgress) {
|
||||||
|
try {
|
||||||
|
const existing = await fetchInProgress
|
||||||
|
// If there's already a fresh fetch in progress, return its result
|
||||||
|
return {
|
||||||
|
version: existing.version,
|
||||||
|
success: existing.source === 'online',
|
||||||
|
source: existing.source
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore and proceed with new fetch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing cache
|
||||||
|
memoryCache = null
|
||||||
|
|
||||||
|
// Fetch fresh
|
||||||
|
const entry = await fetchVersionOnce(cacheFilePath, logger)
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: entry.version,
|
||||||
|
success: entry.source === 'online',
|
||||||
|
source: entry.source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
source: 'online' | 'fallback' | null
|
||||||
|
} {
|
||||||
|
if (!memoryCache) {
|
||||||
|
return {
|
||||||
|
hasCache: false,
|
||||||
|
version: null,
|
||||||
|
age: null,
|
||||||
|
isExpired: true,
|
||||||
|
expiresIn: null,
|
||||||
|
source: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const age = Date.now() - memoryCache.fetchedAt
|
||||||
|
const isExpired = age >= cacheTtlMs
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasCache: true,
|
||||||
|
version: memoryCache.version,
|
||||||
|
age,
|
||||||
|
isExpired,
|
||||||
|
expiresIn: isExpired ? 0 : cacheTtlMs - age,
|
||||||
|
source: memoryCache.source
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user