All files / promise/src memoizePromise.ts

68% Statements 17/25
66.66% Branches 2/3
50% Functions 3/6
72.72% Lines 16/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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99                                                                                                                          2x         2x 2x 2x   2x 4x   4x 2x     2x             2x 2x 2x 2x 2x 2x 2x                
import { RejectFn, ResolveFn } from "./_types";
 
/**
 * Create a {@link Promise}-returning {@link Function} that returns the
 * memoized results of a {@link Promise}. The key for the lookup cache is
 * determined by the `keyExtractor` {@link Function}.
 *
 * An optional {@link Map} instance can be provided to be used as cache. This
 * allows for example to prepopulate the cache, or to force recomputation of
 * some values by removing their corresponding keys. If no instance is provided
 * an empty {@link Map} will be created.
 *
 * @remarks
 * If the {@link Promise} throws an error, no result will be cached and the
 * {@link Promise} will be run again next time.
 *
 * @example
 * Memoize the `fetch` function
 * ```ts
 * const keyExtractor = (url) => url;
 *
 * const memoizedFetch = memoizePromise(fetch, keyExtractor);
 *
 * const result1 = memoizedFetch("https://...");
 * const result2 = memoizedFetch("https://...");
 * // fetch will only be called once
 * ```
 *
 * @example
 * Provide an initial cache restored from database
 * ```ts
 * const restoredCacheFromDatabase = new Map([["https://...", 1]]);
 *
 * const keyExtractor = (url) => url;
 *
 * const memoizedFetch = memoizePromise(fetch, keyExtractor, restoredCacheFromDatabase);
 *
 * const result = memoizedFetch("https://...");
 * // result will be directly returned from cache
 * ```
 *
 * @example
 * Clear cache after 5 seconds
 * ```ts
 * const cache = new Map();
 * const keyExtractor = (url) => url;
 *
 * const memoizedFetch = memoizePromise(fetch, keyExtractor, cache);
 *
 * // ... fill the cache
 *
 * setTimeout(() => {
 *   cache.clear();
 * }, 5000);
 * ```
 *
 * @param fn - the {@link Function} to memoize
 * @param keyExtractor - the {@link Function} to extract the key for the cache
 * @param cache - an optional, possibly prefilled {@link Map} instance to use as cache
 * @returns the memoized result
 */
export const memoizePromise = <TArgs extends unknown[], R>(
  fn: (...args: TArgs) => PromiseLike<R>,
  keyExtractor: (...args: TArgs) => unknown,
  cache = new Map<unknown, R>()
): ((...args: TArgs) => Promise<R>) => {
  const pending = new Set<unknown>();
  const resolves: ResolveFn<R>[] = [];
  const rejects: RejectFn[] = [];
 
  return async (...args) => {
    const key = keyExtractor(...args);
 
    if (cache.has(key)) {
      return cache.get(key) as R;
    }
 
    Iif (pending.has(key)) {
      return new Promise((resolve, reject) => {
        resolves.push(resolve);
        rejects.push(reject);
      });
    }
 
    try {
      pending.add(key);
      const value = await fn(...args);
      pending.delete(key);
      cache.set(key, value);
      resolves.forEach((resolve) => resolve(value));
      return value;
    } catch (err) {
      pending.delete(key);
      rejects.forEach((reject) => reject(err));
      throw err;
    }
  };
};