← 中文资源

简体中文版本

Promise 与任务取消

取消是协作式的:信号通知操作停止,操作必须在安全点检查信号——否则强拆会泄漏资源并让完成路径竞态。

阅读英文原文

概念定义

异步工作需要一个可组合的关断开关:用户放弃的请求、被首个成功者取代的扇出、超期的截止时间。取消令牌就是这个开关;操作通过检查信号并确定性展开来配合。

工作机制

控制器升起信号,操作侧观察它:被等待的原语以“取消专用错误”拒绝、循环在迭代边界检查、清理放进 finally。组合规则是子令牌随父令牌取消、link 合并多信号、截止时间是定时器驱动的令牌。

使用场景

一切可能活得比用处更久的操作:HTTP 请求、数据库查询、批任务、扇出作用域。SDK 签名应暴露取消参数,让调用方贯穿每一层传递令牌。

常见误区

丢弃引用不等于取消——游离 promise 照跑到底照耗资源。超时包装器若只与定时器竞速而不传信号,底层请求在超时后仍在暗跑。取消是控制流不是失败:把它当异常塞进通用错误处理,会对用户已放弃的工作发起幽灵重试。库必须在取消路径释放锁、连接与部分写入,并用测试覆盖;长命控制器上的取消监听泄漏则是慢性内存漏。

代码示例

interface RetryableError extends Error {
  retryable?: boolean;
}

class PermanentError extends Error {
  retryable = false;
}

function classify(err: unknown): RetryableError {
  if (err instanceof PermanentError) return err;

  const message = err instanceof Error ? err.message : String(err);
  const transient =
    /timeout|econnreset|socket hang up|503|429/i.test(message);

  return Object.assign(
    err instanceof Error ? err : new Error(message),
    { retryable: transient },
  );
}

async function withRetry<T>(
  fn: () => Promise<T>,
  { attempts = 3, baseMs = 200 } = {},
): Promise<T> {
  let lastError: RetryableError;

  for (let i = 1; i <= attempts; i++) {
    try {
      return await fn();
    } catch (raw) {
      lastError = classify(raw);

      if (!lastError.retryable || i === attempts) {
        throw lastError;
      }
      await new Promise((r) => setTimeout(r, baseMs * 2 ** (i - 1)));
    }
  }
  throw lastError!;
}

const result = await withRetry(async () => {
  if (Math.random() < 0.7) {
    const err = new Error('upstream 503 unavailable');
    throw err;
  }
  return 'settled';
});
console.log('outcome:', result);
export function mapPool(items, mapper, concurrency = 4) {
  if (!Array.isArray(items)) throw new TypeError("items must be an array");
  if (typeof mapper !== "function") throw new TypeError("mapper must be a function");
  if (!Number.isInteger(concurrency) || concurrency < 1) {
    throw new RangeError("concurrency must be a positive integer");
  }

  const results = new Array(items.length);
  let nextIndex = 0;

  async function worker() {
    while (nextIndex < items.length) {
      const index = nextIndex;
      nextIndex += 1;
      results[index] = await mapper(items[index], index);
    }
  }

  const workerCount = Math.min(concurrency, items.length);
  const workers = Array.from({ length: workerCount }, () => worker());
  return Promise.all(workers).then(() => results);
}