真实世界中的各种 Promise 包装函数

版权声明:原创内容,转载请标明作者和出处。

可取消的 Promise CancellablePromise

export class CancellationError extends Error {
  constructor() {
    super("Promise was cancelled");
    this.name = "CancellationError";
  }
}

/**
 * 创建一个可取消的 Promise。返回一个包含 Promise 和取消函数的对象。
 *
 * @param {Promise<T> | T} promise - 需要变为可取消的 Promise,或者是一个同步的值。
 * @returns {{ promise: Promise<T>; cancel: () => void }} - 包含可取消的 Promise 和取消函数的对象。
 *
 * @author zilin
 *
 * @example
 *
 * // 创建一个可取消的 Promise
 * const { promise, cancel } = cancellablePromise(
 *   new Promise(resolve => setTimeout(() => resolve('resolved'), 1000))
 * );
 *
 * // 500 毫秒后取消 Promise
 * setTimeout(() => {
 *   cancel();
 * }, 500);
 *
 * // Promise 将被取消,所以这将抛出一个 CancellationError
 * promise.then(console.log).catch(console.error);
 */
export function cancellablePromise<T>(promise: Promise<T> | T): {
  promise: Promise<T>;
  cancel: () => void;
} {
  let isCancelled = false;
  const wrappedPromise = new Promise<T>((resolve, reject) => {
    Promise.resolve(promise).then(
      (val) => (isCancelled ? reject(new CancellationError()) : resolve(val)),
      (error) => {
        // Check if the promise is cancelled
        if (isCancelled) {
          reject(new CancellationError());
        } else {
          // If not, throw the original error
          reject(error);
        }
      }
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      isCancelled = true;
    },
  };
}

多次调用取最后一次结果 TakeLatestPromise

import { cancellablePromise, CancellationError } from "./cancellablePromise";

export type AsyncFunc<T, U extends unknown[]> = (...args: U) => Promise<T>;

export interface TakeLatestPromiseOptions {
  /** 一个 promise 被取消,是否抛出错误。默认为 `false` */
  throwOnCancellation?: boolean;
}

type TakeLatestPromiseReturn<T, Options> = Options extends {
  throwOnCancellation: true;
}
  ? T
  : T | undefined;

export function takeLatestPromise<T, U extends unknown[]>(
  asyncFunction: AsyncFunc<T, U>,
  options: { throwOnCancellation: true }
): AsyncFunc<T, U>;

export function takeLatestPromise<T, U extends unknown[]>(
  asyncFunction: AsyncFunc<T, U>,
  options?: TakeLatestPromiseOptions | undefined
): AsyncFunc<T | undefined, U>;

/**
 * 包装一个异步函数,返回一个新函数,该函数确保只有最新的调用会被完成。
 * 如果在上一个调用完成之前进行了新的调用,那么上一个调用将被取消。
 *
 * @param {AsyncFunc<T, U>} asyncFunction - 需要包装的异步函数。
 * @param {Options} [options] - 可选的配置对象。
 * @param {boolean} [options.throwOnCancellation = false] - 如果一个 promise 被取消,是否抛出错误。默认为 `false`。
 * @returns {AsyncFunc<T | undefined, U>} - 一个新函数,返回一个 promise,该 promise 解析为最新调用的结果,或者如果 promise 被取消,则为 `undefined`。
 *
 * @author zilin
 *
 * @example
 *
 * const fetchData = async (id) => {
 *   const response = await fetch(`/api/data/${id}`);
 *   const data = await response.json();
 *   return data;
 * };
 *
 * const enhancedFetchData = takeLatestPromise(fetchData, { throwOnCancellation: true });
 *
 * // 只有最新调用的结果会被打印出来。
 * enhancedFetchData(1).then(console.log);
 * enhancedFetchData(2).then(console.log);
 * enhancedFetchData(3).then(console.log);
 *
 * // 如果一个 promise 被取消,将会抛出一个错误。
 * try {
 *   await enhancedFetchData(1);
 * } catch (error) {
 *   console.error(error); // 输出: "Error: Promise was cancelled"
 * }
 */
export function takeLatestPromise<T, U extends unknown[]>(
  asyncFunction: AsyncFunc<T, U>,
  options?: TakeLatestPromiseOptions | undefined
) {
  let lastTask: ReturnType<typeof cancellablePromise> | null = null;
  return async function (
    ...args: U
  ): Promise<TakeLatestPromiseReturn<T, typeof options>> {
    if (lastTask) {
      lastTask.cancel();
    }
    lastTask = cancellablePromise(asyncFunction(...args));
    try {
      const result = await lastTask.promise;
      return result as T;
    } catch (error) {
      if (error instanceof CancellationError) {
        if (options?.throwOnCancellation) {
          throw error;
        } else {
          return undefined;
        }
      } else {
        throw error;
      }
    }
  };
}

重试 Promise RetryPromise

export class RetryError extends Error {
  constructor() {
    super("Maximum retry attempts exceeded");
    this.name = "RetryError";
  }
}

/**
 * 创建一个可重试的 Promise。如果 Promise 被拒绝,将尝试重新运行,直到 Promise 被解析或达到最大重试次数。
 *
 * @param {() => Promise<T>} promiseFunc - 需要重试的异步操作。
 * @param {number} maxAttempts - 最大重试次数。
 * @returns {Promise<T>} - 一个新的 Promise,如果操作成功或达到最大重试次数,则解析为原始值,否则抛出一个错误。
 *
 * @author zilin
 *
 * @example
 *
 * // 创建一个可重试的 Promise
 * const promise = retryPromise(
 *   () => new Promise((resolve, reject) => Math.random() > 0.5 ? resolve('Success') : reject('Failure')),
 *   5
 * );
 *
 * // 这个 Promise 会尝试运行异步操作,直到操作成功或达到 5 次重试
 * promise.then(console.log).catch(console.error);
 */
export function retryPromise<T>(
  promiseFunc: () => Promise<T>,
  maxAttempts: number
): Promise<T> {
  return new Promise<T>(async (resolve, reject) => {
    for (let i = 0; i < maxAttempts; i++) {
      try {
        const result = await promiseFunc();
        return resolve(result);
      } catch (error) {
        if (i === maxAttempts - 1) {
          return reject(new RetryError());
        }
      }
    }
  });
}

超时 Promise TimeoutPromise

export class TimeoutError extends Error {
  constructor() {
    super("Promise timed out");
    this.name = "TimeoutError";
  }
}

/**
 * 创建一个可超时的 Promise。如果在指定的时间内 Promise 未能完成,将抛出一个错误。
 *
 * @param {Promise<T> | T} promise - 需要变为可超时的 Promise,或者是一个同步的值。
 * @param {number} timeout - 超时时间,单位为毫秒。
 * @returns {Promise<T>} - 一个新的 Promise,如果在指定的时间内完成,则解析为原始值,否则抛出一个错误。
 *
 * @example
 *
 * // 创建一个可超时的 Promise
 * const promise = timeoutPromise(
 *   new Promise(resolve => setTimeout(() => resolve('resolved'), 2000)),
 *   1000
 * );
 *
 * // 由于原始 Promise 将在 2 秒后完成,而超时时间设置为 1 秒,所以这将抛出一个 TimeoutError
 * promise.then(console.log).catch(console.error);
 */
export function timeoutPromise<T>(
  promise: Promise<T> | T,
  timeout: number
): Promise<T> {
  /** 兼容 Node.js 和浏览器环境 */
  let timer: ReturnType<typeof setTimeout>;
  const timeoutPromise = new Promise<T>((_, reject) => {
    timer = setTimeout(() => {
      reject(new TimeoutError());
    }, timeout);
  });
  return Promise.race([promise, timeoutPromise]).finally(() => {
    clearTimeout(timer);
  });
}

可重用的 ReusePromise

/**
 * 创建一个可复用的 Promise 实例。如果第二次调用是前一个Promise还在执行,则返回前一个 Promise 实例
 */
/**
 * 自带缓存能力的 Promise 封装,如果参数保持一致,则复用之前的请求体,不会重新触发请求
 *
 * @param api 真正的请求函数
 * @param hash hash 函数
 */
export function reusePromise<Params, Result>(
  api: (params: Params) => Promise<Result>,
  generateHash: (params: Params) => string
): (params: Params) => Promise<Result> {
  /**
   * 基于 hash 来做请求状态的复用
   */
  const duringAPIPromiseHashMappings: Record<
    string,
    Promise<Result> | undefined
  > = {};

  return (params) => {
    const currentHash = generateHash(params);

    const existPromise = duringAPIPromiseHashMappings[currentHash];
    // 如果存在请求中的数据结构
    if (existPromise) {
      return existPromise; // 等待 Promise 结束
    }

    // 等待这次流程处理结束,将 hash 的缓存删除
    const res = (duringAPIPromiseHashMappings[currentHash] = api(
      params
    ).finally(() => {
      delete duringAPIPromiseHashMappings[currentHash];
    }));

    return res;
  };
}