All files / utils asyncMemoize.ts

100% Statements 24/24
88.24% Branches 15/17
100% Functions 5/5
100% Lines 22/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80                                                                                      19x 6x   6x 23x 23x 23x 23x       7x   16x 16x 16x 16x       16x 4x 4x 4x 4x 4x 4x       16x   6x   6x    
export interface AsyncMemoizeOptions<
  T extends (...args: any[]) => Promise<any>,
> {
  /**
   * 缓存键。
   *
   * @default arg0 => arg0
   */
  cacheKey?: (...args: Parameters<T>) => any
 
  /**
   * 缓存时效(毫秒)。
   *
   * @default 0
   */
  cacheTTL?:
    | number
    | ((
        result: Awaited<ReturnType<T>>,
        ...args: Parameters<T>
      ) => number | void)
}
 
export type AsyncMemoizeCacheMap = Map<
  string,
  {
    value: any
    expiredAt: number
  }
>
 
/**
 * 异步函数执行缓存。
 *
 * @param asyncFn 异步函数
 * @param options 选项
 */
export function asyncMemoize<T extends (...args: any[]) => Promise<any>>(
  asyncFn: T,
  options: AsyncMemoizeOptions<T> = {},
): T & {
  cache: AsyncMemoizeCacheMap
} {
  const { cacheKey = (...args: any[]) => args[0], cacheTTL } = options
  const cache: AsyncMemoizeCacheMap = new Map()
 
  const call = (...args: any[]) => {
    const _cacheKey = cacheKey(...args)
    const cacheValue = cache.get(_cacheKey)
    const currentMs = Date.now()
    if (
      cacheValue &&
      (cacheValue.expiredAt === 0 || currentMs < cacheValue.expiredAt)
    ) {
      return cacheValue.value
    }
    const cacheValueNew = asyncFn(...args)
    cacheValueNew.catch(() => cache.delete(_cacheKey))
    const cacheTTLIsFunction = typeof cacheTTL === 'function'
    cache.set(_cacheKey, {
      value: cacheValueNew,
      expiredAt: cacheTTLIsFunction ? 0 : cacheTTL ? currentMs + cacheTTL : 0,
    })
    if (cacheTTLIsFunction) {
      cacheValueNew.then(result => {
        const ttl = cacheTTL(result, ...(args as any))
        const expiredAt = ttl ? Date.now() + ttl : 0
        const cached = cache.get(_cacheKey)
        if (cached) {
          cached.expiredAt = expiredAt
        }
      })
    }
    return cacheValueNew
  }
  call.cache = cache
 
  return call as any
}