( fnOrName: T | string, fnOrExpireInSec: number | T, expireInSecOrOptions?: number | CacheableOptions, maybeOptions?: CacheableOptions )
| 185 | |
| 186 | // Implementation for cacheable (Redis-only - async) |
| 187 | export function cacheable<T extends (...args: any) => any>( |
| 188 | fnOrName: T | string, |
| 189 | fnOrExpireInSec: number | T, |
| 190 | expireInSecOrOptions?: number | CacheableOptions, |
| 191 | maybeOptions?: CacheableOptions |
| 192 | ) { |
| 193 | const name = typeof fnOrName === 'string' ? fnOrName : fnOrName.name; |
| 194 | const fn = |
| 195 | typeof fnOrName === 'function' |
| 196 | ? fnOrName |
| 197 | : typeof fnOrExpireInSec === 'function' |
| 198 | ? fnOrExpireInSec |
| 199 | : null; |
| 200 | |
| 201 | let expireInSec: number | null = null; |
| 202 | let options: CacheableOptions = {}; |
| 203 | |
| 204 | // Parse parameters based on function signature |
| 205 | if (typeof fnOrName === 'function') { |
| 206 | // Overload 1: cacheable(fn, expireInSec, options?) |
| 207 | expireInSec = typeof fnOrExpireInSec === 'number' ? fnOrExpireInSec : null; |
| 208 | if (expireInSecOrOptions && typeof expireInSecOrOptions === 'object') { |
| 209 | options = expireInSecOrOptions; |
| 210 | } |
| 211 | } else { |
| 212 | // Overload 2: cacheable(name, fn, expireInSec, options?) |
| 213 | expireInSec = |
| 214 | typeof expireInSecOrOptions === 'number' ? expireInSecOrOptions : null; |
| 215 | if (maybeOptions) { |
| 216 | options = maybeOptions; |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | if (typeof fn !== 'function') { |
| 221 | throw new Error('fn is not a function'); |
| 222 | } |
| 223 | |
| 224 | if (typeof expireInSec !== 'number') { |
| 225 | throw new Error('expireInSec is not a number'); |
| 226 | } |
| 227 | |
| 228 | const cachePrefix = `cachable:${name}`; |
| 229 | const getKey = (...args: Parameters<T>) => |
| 230 | `${cachePrefix}:${stringify(args)}`.replaceAll(/\s/g, ''); |
| 231 | |
| 232 | const lruCache = new LRUCache<string, any>({ |
| 233 | max: CACHEABLE_LRU_MAX, |
| 234 | ttl: CACHEABLE_LRU_TTL_MS, |
| 235 | }); |
| 236 | |
| 237 | // L1 LRU (60s) + L2 Redis. clear() deletes Redis + local LRU; other nodes may serve stale from LRU for up to 60s. |
| 238 | const cachedFn = async ( |
| 239 | ...args: Parameters<T> |
| 240 | ): Promise<Awaited<ReturnType<T>>> => { |
| 241 | const key = getKey(...args); |
| 242 | |
| 243 | // L1: in-memory LRU first (offloads Redis on hot keys) |
| 244 | const lruHit = lruCache.get(key); |
no test coverage detected