¥
立即购买

JavaScript异步任务承诺创建器

12 浏览
1 试用
0 购买
Dec 8, 2025更新

本提示词专门用于生成高质量的JavaScript Promise代码,帮助开发者处理各种异步任务场景。通过清晰的代码结构、完善的错误处理机制和详细的注释说明,确保生成的Promise代码符合JavaScript最佳实践,具备良好的可读性和可维护性。适用于网络请求、文件操作、定时任务等多种异步编程场景,能够有效提升开发效率和代码质量。

Promise代码实现

/**
 * 基于 Promise 的“同会话仅执行一次”的用户资料获取任务
 * - GET /api/profile?with=settings,附带 Bearer 令牌
 * - 5000ms 超时,支持 AbortController 取消
 * - 最多重试 3 次(指数退避 + 抖动)
 * - 成功后写入本地(sessionStorage)缓存
 * - 全程进度回传
 * - 任务完成后释放控制器与监听器
 *
 * 使用环境:浏览器(如需 Node.js,请自定义存储与网络可用性检测)
 */

(function () {
  const PROFILE_CACHE_KEY = 'profile:cache:v1';
  const PROFILE_DONE_KEY = 'profile:done:v1';
  const PROFILE_INFLIGHT_KEY = 'profile:inflight:v1'; // 可用于诊断(不参与逻辑判定)

  // 单页面内的并发去重:同一时刻,复用同一个 Promise/控制器
  let sharedInFlight = null;

  /**
   * 启动一次“获取资料”的任务(同会话仅执行一次的保护内置)
   * 返回 { promise, abort },方便在外部取消。
   *
   * @param {Object} options
   * @param {string} options.token - 必填,Bearer 令牌
   * @param {string} [options.url='/api/profile?with=settings'] - 请求地址
   * @param {number} [options.timeoutMs=5000] - 每次尝试的超时时间(毫秒)
   * @param {number} [options.retries=3] - 最大重试次数
   * @param {(progress: ProgressEventDetail) => void} [options.onProgress] - 进度回调
   * @param {AbortSignal} [options.signal] - 可选,外部传入的取消信号
   * @returns {{ promise: Promise<FetchProfileResult>, abort: () => void }}
   */
  function startProfileFetch({
    token,
    url = '/api/profile?with=settings',
    timeoutMs = 5000,
    retries = 3,
    onProgress,
    signal: externalSignal,
  } = {}) {
    if (!isBrowserEnv()) {
      const error = new Error('This implementation requires a browser environment.');
      error.code = 'ENV_UNSUPPORTED';
      return {
        promise: Promise.reject(error),
        abort() {},
      };
    }

    // 触发条件:网络可用
    if (!isNetworkAvailable()) {
      const err = new Error('Network unavailable');
      err.code = 'NETWORK_UNAVAILABLE';
      return {
        promise: Promise.reject(err),
        abort() {},
      };
    }

    // 触发条件:进入个人中心时调用该函数(由使用方控制);此处只做“同会话仅执行一次”的保护
    // 如果本会话已经成功执行过,直接返回缓存
    const cached = readCache();
    const sessionDone = sessionStorage.getItem(PROFILE_DONE_KEY) === '1';
    if (sessionDone && cached) {
      const result = {
        data: cached.data,
        fromCache: true,
        attempts: 0,
        durationMs: 0,
        cachedAt: cached.cachedAt,
      };
      onProgressSafe(onProgress, { phase: 'skipped', progress: 100, detail: 'already_done_in_session' });
      return {
        promise: Promise.resolve(result),
        abort() {},
      };
    }

    // 单页面内并发去重:已有在途任务时复用
    if (sharedInFlight) {
      onProgressSafe(onProgress, { phase: 'attached_to_inflight', progress: 0 });
      return {
        promise: sharedInFlight.promise,
        abort: sharedInFlight.abort,
      };
    }

    // 创建主控制器(用于整条任务),支持外部 signal
    const mainController = new AbortController();
    const { signal: mainSignal } = mainController;
    const abort = () => {
      if (!mainSignal.aborted) mainController.abort(new DOMException('Aborted', 'AbortError'));
    };

    // 将外部 signal 与内部 mainController 关联
    let externalAbortCleanup = null;
    if (externalSignal) {
      const onExternalAbort = () => {
        if (!mainSignal.aborted) {
          mainController.abort(externalSignal.reason || new DOMException('Aborted', 'AbortError'));
        }
      };
      externalSignal.addEventListener('abort', onExternalAbort, { once: true });
      externalAbortCleanup = () => externalSignal.removeEventListener('abort', onExternalAbort);
    }

    // 构建在途对象,供并发复用
    const inflight = {};
    sharedInFlight = inflight;

    const startedAt = performance.now();
    sessionStorage.setItem(PROFILE_INFLIGHT_KEY, '1');

    const promise = (async () => {
      onProgressSafe(onProgress, { phase: 'started', progress: 5 });

      // 入参校验
      if (!token || typeof token !== 'string') {
        throw createNonRetryableError('TOKEN_MISSING', 'Bearer token is required.');
      }

      // 如果有缓存且尚未标记 session done,允许进一步请求,但可以先上报已存在本地缓存
      if (cached) {
        onProgressSafe(onProgress, { phase: 'local_cache_present', progress: 10, detail: 'stale_or_preload_cache' });
      }

      // 重试策略
      const maxRetries = Math.max(0, retries | 0);
      const baseDelay = 200; // ms
      let attempt = 0;

      while (true) {
        attempt += 1;
        const attemptLabel = `attempt_${attempt}`;
        onProgressSafe(onProgress, {
          phase: 'request',
          progress: 15,
          attempt,
          totalRetries: maxRetries,
        });

        // 为每次尝试创建独立控制器(用于 per-attempt 超时与取消合并)
        checkAbort(mainSignal); // 主任务是否已取消
        const attemptController = new AbortController();
        const attemptSignal = attemptController.signal;

        // 关联 mainSignal -> attemptSignal
        const unlink = linkAbort(mainSignal, attemptController);

        // 超时定时器(仅终止本次尝试,不会终止主任务)
        const timeoutId = setTimeout(() => {
          if (!attemptSignal.aborted) {
            attemptController.abort(new DOMException('Request timed out', 'TimeoutError'));
          }
        }, timeoutMs);

        try {
          const res = await fetch(url, {
            method: 'GET',
            headers: {
              'Accept': 'application/json',
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${token}`,
            },
            // 为了明确每次都请求最新数据,这里不依赖 HTTP 缓存
            cache: 'no-store',
            signal: attemptSignal,
          });

          onProgressSafe(onProgress, { phase: 'response', progress: 30, attempt, status: res.status });

          // 非 2xx
          if (!res.ok) {
            const err = new Error(`HTTP ${res.status}`);
            err.name = 'HTTPError';
            err.status = res.status;
            // 429/5xx/408 可重试,其它视为不可重试
            if (!isRetryableStatus(res.status)) {
              throw markNonRetryable(err, 'HTTP_NON_RETRYABLE');
            }
            throw err; // 进入重试流程
          }

          // 解析 JSON
          let data;
          try {
            data = await res.json();
          } catch (e) {
            throw markNonRetryable(e, 'JSON_PARSE_ERROR');
          }

          onProgressSafe(onProgress, { phase: 'parse', progress: 50, attempt });

          // 校验必需字段
          validateProfilePayload(data);
          onProgressSafe(onProgress, { phase: 'validate', progress: 70, attempt });

          // 写入本地缓存(同会话缓存)
          const cacheObj = { data, cachedAt: Date.now() };
          writeCache(cacheObj);
          sessionStorage.setItem(PROFILE_DONE_KEY, '1'); // 标记本会话已完成
          onProgressSafe(onProgress, { phase: 'cache', progress: 85 });

          // 成功完成
          const durationMs = Math.round(performance.now() - startedAt);
          onProgressSafe(onProgress, { phase: 'done', progress: 100, durationMs });

          return {
            data,
            fromCache: false,
            attempts: attempt,
            durationMs,
            cachedAt: cacheObj.cachedAt,
          };
        } catch (err) {
          // 归一化错误
          const normalized = normalizeError(err);

          // 若为取消
          if (isAbortError(normalized)) {
            onProgressSafe(onProgress, { phase: 'aborted', progress: 100, reason: normalized.name });
            throw normalized;
          }

          // 超时或可重试错误,进入重试;不可重试则直接抛出
          const canRetry = shouldRetry(normalized);
          const hasMore = attempt < maxRetries;

          if (canRetry && hasMore) {
            const backoff = computeBackoff(baseDelay, attempt);
            onProgressSafe(onProgress, {
              phase: 'retry',
              progress: 25,
              attempt,
              nextDelayMs: backoff,
              error: safeErrorForProgress(normalized),
            });

            // 等待退避时间,同时尊重主任务取消
            await delay(backoff, mainSignal);
            // 下一轮继续
          } else {
            // 无法重试或已达上限
            normalized.code ||= canRetry ? 'RETRY_EXHAUSTED' : normalized.code;
            onProgressSafe(onProgress, {
              phase: 'error',
              progress: 100,
              attempt,
              error: safeErrorForProgress(normalized),
            });
            throw normalized;
          }
        } finally {
          // 清理 per-attempt 资源
          clearTimeout(timeoutId);
          unlink();
        }
      }
    })()
      .finally(() => {
        // 任务最终收尾:释放控制器与监听器、在途标记
        if (externalAbortCleanup) {
          try { externalAbortCleanup(); } catch {}
          externalAbortCleanup = null;
        }
        sharedInFlight = null;
        try { sessionStorage.removeItem(PROFILE_INFLIGHT_KEY); } catch {}
        // 释放主控制器引用(帮助 GC)
        // 注意:不手动调用 abort,这里只是解除引用
      });

    inflight.promise = promise;
    inflight.abort = abort;

    return { promise, abort };
  }

  // ---------- 工具与内部函数 ----------

  function isBrowserEnv() {
    return typeof window !== 'undefined' && typeof document !== 'undefined';
  }

  function isNetworkAvailable() {
    // navigator.onLine 为启发式;如需更精确可在外层做探测
    return typeof navigator === 'undefined' ? true : navigator.onLine !== false;
  }

  function readCache() {
    try {
      const raw = sessionStorage.getItem(PROFILE_CACHE_KEY);
      if (!raw) return null;
      return JSON.parse(raw);
    } catch {
      return null;
    }
  }

  function writeCache(obj) {
    try {
      sessionStorage.setItem(PROFILE_CACHE_KEY, JSON.stringify(obj));
    } catch {
      // 忽略存储失败(配额/隐私模式),不影响主流程
    }
  }

  function onProgressSafe(cb, payload) {
    try {
      if (typeof cb === 'function') cb(payload);
    } catch {
      // 避免进度回调异常影响主流程
    }
  }

  function linkAbort(sourceSignal, targetController) {
    if (!sourceSignal) return () => {};
    const onAbort = () => {
      if (!targetController.signal.aborted) {
        targetController.abort(sourceSignal.reason || new DOMException('Aborted', 'AbortError'));
      }
    };
    sourceSignal.addEventListener('abort', onAbort, { once: true });
    return () => sourceSignal.removeEventListener('abort', onAbort);
  }

  function checkAbort(signal) {
    if (signal && signal.aborted) {
      throw signal.reason || new DOMException('Aborted', 'AbortError');
    }
  }

  function delay(ms, abortSignal) {
    if (ms <= 0) return Promise.resolve();
    return new Promise((resolve, reject) => {
      const id = setTimeout(() => {
        cleanup();
        resolve();
      }, ms);
      const onAbort = () => {
        cleanup();
        reject(abortSignal.reason || new DOMException('Aborted', 'AbortError'));
      };
      const cleanup = () => {
        clearTimeout(id);
        if (abortSignal) abortSignal.removeEventListener('abort', onAbort);
      };
      if (abortSignal) {
        if (abortSignal.aborted) {
          cleanup();
          reject(abortSignal.reason || new DOMException('Aborted', 'AbortError'));
          return;
        }
        abortSignal.addEventListener('abort', onAbort, { once: true });
      }
    });
  }

  function computeBackoff(base, attempt) {
    // 指数退避:base * 2^(attempt-1) + 抖动(0~250ms),并做上限裁剪
    const jitter = Math.floor(Math.random() * 250);
    const exp = base * Math.pow(2, Math.max(0, attempt - 1));
    const backoff = Math.min(2000, exp) + jitter; // cap at ~2s + jitter
    return backoff;
  }

  function isRetryableStatus(status) {
    return status === 408 || status === 429 || (status >= 500 && status <= 599);
  }

  function isAbortError(err) {
    return err && (err.name === 'AbortError' || err.name === 'TimeoutError');
  }

  function markNonRetryable(err, code) {
    err.code = code || err.code || 'NON_RETRYABLE';
    err.nonRetryable = true;
    return err;
  }

  function normalizeError(err) {
    if (!err) return new Error('Unknown error');
    // 保留 name/message/code/status,避免包含敏感信息(不记录 token)
    return err;
  }

  function safeErrorForProgress(err) {
    return {
      name: err.name,
      message: err.message,
      code: err.code,
      status: err.status,
    };
  }

  function shouldRetry(err) {
    if (!err) return false;
    if (isAbortError(err)) return false;
    if (err.nonRetryable) return false;
    if (err.name === 'HTTPError') {
      return isRetryableStatus(err.status);
    }
    // fetch 网络错误、超时等可重试
    if (err.name === 'TypeError') return true;
    if (err.name === 'TimeoutError') return true;
    return false;
  }

  function createNonRetryableError(code, message) {
    const err = new Error(message);
    err.code = code;
    err.nonRetryable = true;
    return err;
  }

  function validateProfilePayload(obj) {
    // 按需求校验必需字段,可根据实际接口调整
    // 要求:id(string),name(string),settings(object)
    if (!obj || typeof obj !== 'object') {
      throw markNonRetryable(new Error('Invalid payload: not an object'), 'INVALID_PAYLOAD');
    }
    if (typeof obj.id !== 'string' || obj.id.length === 0) {
      throw markNonRetryable(new Error('Missing or invalid "id"'), 'INVALID_ID');
    }
    if (typeof obj.name !== 'string' || obj.name.length === 0) {
      throw markNonRetryable(new Error('Missing or invalid "name"'), 'INVALID_NAME');
    }
    if (typeof obj.settings !== 'object' || obj.settings === null) {
      throw markNonRetryable(new Error('Missing or invalid "settings"'), 'INVALID_SETTINGS');
    }
  }

  // 暴露到全局(按需调整命名/导出方式)
  window.startProfileFetch = startProfileFetch;
})();

/**
 * @typedef {Object} FetchProfileResult
 * @property {any} data - 解析与校验后的资料对象
 * @property {boolean} fromCache - 是否来自本地缓存(sessionStorage)
 * @property {number} attempts - 实际尝试次数
 * @property {number} durationMs - 完整任务耗时(毫秒)
 * @property {number} cachedAt - 缓存写入时间戳(毫秒)
 */

/**
 * @typedef {Object} ProgressEventDetail
 * @property {string} phase - 进度阶段:started/request/response/parse/validate/cache/done/retry/aborted/error/skipped/local_cache_present/attached_to_inflight
 * @property {number} progress - 0~100 的大致进度
 * @property {number} [attempt]
 * @property {number} [totalRetries]
 * @property {number} [status]
 * @property {number} [nextDelayMs]
 * @property {string} [detail]
 * @property {any} [error] - 简化后的错误对象(无敏感信息)
 * @property {number} [durationMs]
 */

代码说明

  • 功能描述:

    • 发送携带 Bearer 令牌的 GET /api/profile?with=settings 请求;
    • 单次尝试 5000ms 超时,支持 AbortController 取消;
    • 最多重试 3 次,采用指数退避(含抖动);
    • 对响应 JSON 进行必需字段校验(id/name/settings);
    • 成功后将结果写入 sessionStorage 缓存,并在本会话标记为已完成;
    • 同一会话内再次调用会直接返回缓存(不再发起网络请求);
    • 单页面内并发去重:同一时刻复用同一个在途 Promise;
    • 全流程提供进度回调;
    • 任务结束后释放控制器与事件监听,避免内存泄漏。
  • 参数说明:

    • token: 必填,后端认证用的 Bearer 令牌字符串
    • url: 可选,请求地址,默认 /api/profile?with=settings
    • timeoutMs: 可选,每次请求尝试的超时时间,默认 5000ms
    • retries: 可选,最大重试次数,默认 3
    • onProgress: 可选,进度回调函数,接收 ProgressEventDetail 对象
    • signal: 可选,外部 AbortSignal,用于外部取消整个任务
  • 返回值:

    • 返回对象 { promise, abort }
      • promise: Promise
        • resolve:
          • data: 经过验证的资料对象
          • fromCache: 是否来自本地缓存
          • attempts: 实际尝试次数
          • durationMs: 总耗时(毫秒)
          • cachedAt: 缓存写入时间戳
        • reject:
          • 取消:name 为 AbortError
          • 超时:name 为 TimeoutError
          • HTTP 非重试错误:name 为 HTTPError,附 status
          • 解析/校验错误:附带 code(如 INVALID_PAYLOAD/INVALID_ID 等)
          • 重试耗尽:code 为 RETRY_EXHAUSTED
      • abort(): 取消整个任务(相当于调用内部的 AbortController.abort)

使用示例

// 假设:用户进入“个人中心”路由时触发
function onEnterProfilePage() {
  const token = getAuthTokenSomehow(); // 由你的应用获取
  if (!token) {
    console.warn('No token; skip profile fetch.');
    return;
  }

  const { promise, abort } = window.startProfileFetch({
    token,
    url: '/api/profile?with=settings',
    timeoutMs: 5000,
    retries: 3,
    onProgress: (p) => {
      // 可用于 UI 显示进度、诊断日志(注意不要打印 token)
      // console.log('[profile progress]', p);
      if (p.phase === 'retry') {
        // 可通知用户正在重试
      }
    },
  });

  promise
    .then((res) => {
      // 使用 profile 数据
      // console.log('profile loaded:', res.data);
      renderProfile(res.data);
    })
    .catch((err) => {
      if (err.name === 'AbortError') {
        // 用户离开页面或主动取消
        // console.log('Profile fetch aborted.');
        return;
      }
      // 其它错误:可提示用户或记录日志
      console.error('Profile fetch failed:', err);
      showToast('加载个人资料失败,请稍后重试');
    });

  // 若用户在数据未到达前离开页面,可取消任务:
  // onRouteLeave(() => abort());
}

// 若同会话内多次进入个人中心,startProfileFetch 会直接返回缓存结果,或复用在途任务

注意事项

  • 安全性

    • 不在日志或错误对象中暴露令牌;本实现未输出 token 到控制台。
    • 使用 Authorization: Bearer 头时,确保页面采用 HTTPS。
  • 超时与取消

    • 每次网络尝试均有独立的 5000ms 超时,不会影响重试逻辑;
    • 调用 abort() 或外部 signal.abort() 会取消整个任务(包括后续重试)。
  • 重试策略

    • 仅对超时、网络错误、状态码 408/429/5xx 进行重试;
    • JSON 解析与字段校验错误、4xx(除 408/429)视为不可重试;
    • 指数退避带随机抖动,最大等待约 2s + 抖动。
  • 会话与缓存

    • 仅在成功获取并校验通过后,才会将结果写入 sessionStorage,并标记本会话“已完成”;
    • 本会话内再次调用会直接返回缓存数据,不再访问网络;
    • 如需跨会话持久化,可改为 localStorage,但需考虑过期机制。
  • 诊断与调试

    • 通过 onProgress 可观察阶段与重试信息;
    • 若需进一步排查,在 devtools network 面板查看请求详情;
    • 如遇存储异常(隐私模式/配额),不会中断主流程,仅影响缓存。
  • 可扩展性

    • 若需要 ETag/If-None-Match 等条件请求,可在 headers 中扩展;
    • 若运行在 Node.js 环境,请将存储读写与网络可用性检测抽象为适配器,并替换 fetch 实现。
  • 性能与内存

    • 任务完成后会移除所有监听器并释放控制器引用,避免内存泄漏;
    • 单页面内并发去重避免重复请求,减轻网络开销。

Promise代码实现

'use strict';

/**
 * reportTask.js
 * 需求点覆盖:
 * - 读取 ./data/report.json(不存在则创建默认文件)
 * - 解析后追加生成时间并写回(原子写入)
 * - 全程使用流(createReadStream / createWriteStream + pipeline)
 * - 提供 cancel 信号(AbortController/AbortSignal)
 * - 10000ms 默认超时(可配置)
 * - 捕获 fs 相关错误并以 ReportFileError 抛出
 * - 关闭文件句柄与临时目录清理(finally 中执行)
 * - 支持每日 02:00 定时调度与手动触发
 */

const fs = require('fs');
const fsp = require('fs/promises');
const path = require('path');
const { Readable, Transform } = require('stream');
const { pipeline } = require('stream/promises');

class ReportFileError extends Error {
  constructor(message, cause) {
    super(message);
    this.name = 'ReportFileError';
    if (cause) this.cause = cause;
  }
  static from(cause, message = 'Report file operation failed') {
    return new ReportFileError(message, cause);
  }
}

class OperationCanceledError extends Error {
  constructor(message = 'Operation canceled') {
    super(message);
    this.name = 'OperationCanceledError';
  }
}

class OperationTimeoutError extends Error {
  constructor(message = 'Operation timed out') {
    super(message);
    this.name = 'OperationTimeoutError';
  }
}

function nowISO() {
  return new Date().toISOString();
}

async function ensureDir(dirPath) {
  try {
    await fsp.mkdir(dirPath, { recursive: true });
  } catch (err) {
    throw ReportFileError.from(err, `Failed to ensure directory: ${dirPath}`);
  }
}

async function fileExists(filePath) {
  try {
    await fsp.access(filePath, fs.constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

/**
 * 创建一个可超时的 AbortSignal。若 parentSignal 触发或超时,都会触发返回的 signal。
 * 返回 { signal, dispose() },调用 dispose 清理监听与计时器。
 */
function withTimeoutSignal(parentSignal, timeoutMs) {
  const controller = new AbortController();
  const onParentAbort = () => {
    // Node 18+ 会带 reason;为兼容性,这里兜底一个错误对象
    controller.abort(parentSignal.reason ?? new OperationCanceledError());
  };

  if (parentSignal) {
    if (parentSignal.aborted) {
      onParentAbort();
    } else {
      parentSignal.addEventListener('abort', onParentAbort, { once: true });
    }
  }

  const timeoutId =
    Number.isFinite(timeoutMs) && timeoutMs > 0
      ? setTimeout(() => controller.abort(new OperationTimeoutError(`Timed out after ${timeoutMs} ms`)), timeoutMs)
      : null;

  function dispose() {
    if (timeoutId) clearTimeout(timeoutId);
    if (parentSignal) parentSignal.removeEventListener('abort', onParentAbort);
  }

  return { signal: controller.signal, dispose, controller };
}

/**
 * 以流的方式读取 JSON 文件,返回解析后的对象。
 */
async function readJsonStream(filePath, { signal }) {
  const chunks = [];
  const readStream = fs.createReadStream(filePath, {
    encoding: 'utf8',
    signal,
  });

  const collector = new Transform({
    transform(chunk, _enc, cb) {
      chunks.push(chunk);
      cb();
    },
  });

  try {
    await pipeline(readStream, collector, { signal });
  } catch (err) {
    if (err?.name === 'AbortError') {
      throw new OperationCanceledError('Read stream aborted');
    }
    throw ReportFileError.from(err, `Failed to read file: ${filePath}`);
  }

  try {
    return JSON.parse(chunks.join(''));
  } catch (err) {
    throw ReportFileError.from(err, `Invalid JSON format in ${filePath}`);
  }
}

/**
 * 原子写入 JSON(同目录创建临时文件 -> 写入 -> rename 覆盖)
 * pipeline 确保写入流句柄正确关闭;finally 中清理临时目录。
 */
async function writeJsonAtomically(filePath, data, { signal }) {
  const dir = path.dirname(filePath);
  const tmpPrefix = path.join(dir, '.tmp-report-');

  let tmpDirPath = null;
  try {
    tmpDirPath = await fsp.mkdtemp(tmpPrefix);
  } catch (err) {
    throw ReportFileError.from(err, `Failed to create temp directory in ${dir}`);
  }

  const tmpFilePath = path.join(tmpDirPath, `report-${Date.now()}.json`);
  const json = JSON.stringify(data, null, 2);
  const readable = Readable.from([json], { objectMode: false });
  const writeStream = fs.createWriteStream(tmpFilePath, {
    encoding: 'utf8',
    mode: 0o600, // 最小必要权限
    flags: 'w',
    signal,
  });

  try {
    await pipeline(readable, writeStream, { signal });
    await fsp.rename(tmpFilePath, filePath); // 同文件系统内原子替换
  } catch (err) {
    if (err?.name === 'AbortError') {
      throw new OperationCanceledError('Write stream aborted');
    }
    throw ReportFileError.from(err, `Failed to write file: ${filePath}`);
  } finally {
    // 清理临时目录(即使失败也不阻断主流程)
    try {
      await fsp.rm(tmpDirPath, { recursive: true, force: true });
    } catch {
      /* ignore */
    }
  }
}

/**
 * 核心任务:读取 -> (必要时创建默认)-> 更新生成时间 -> 原子写回
 * 返回 { filePath, generatedAt }
 */
async function updateReport({
  filePath = path.resolve('./data/report.json'),
  timeout = 10_000,
  signal: externalSignal,
} = {}) {
  const { signal, dispose } = withTimeoutSignal(externalSignal ?? new AbortController().signal, timeout);

  try {
    await ensureDir(path.dirname(filePath));

    let reportObj;
    if (!(await fileExists(filePath))) {
      // 若不存在则创建默认对象
      reportObj = {
        items: [],
        createdAt: nowISO(),
      };
    } else {
      reportObj = await readJsonStream(filePath, { signal });
    }

    const generatedAt = nowISO();
    // 追加/更新生成时间:优先记录历史,否则写入单值
    if (Array.isArray(reportObj.history)) {
      reportObj.history.push({ generatedAt });
    } else if (reportObj && typeof reportObj === 'object') {
      reportObj.generatedAt = generatedAt;
    } else {
      // 非对象时兜底
      reportObj = { generatedAt };
    }

    await writeJsonAtomically(filePath, reportObj, { signal });

    return { filePath, generatedAt };
  } catch (err) {
    // 归一化错误类型
    if (err instanceof OperationCanceledError) throw err;
    if (err instanceof OperationTimeoutError) throw err;
    if (err?.name === 'AbortError') throw new OperationCanceledError();

    if (err instanceof ReportFileError) throw err;
    throw ReportFileError.from(err);
  } finally {
    dispose();
  }
}

/**
 * 便捷方法:返回 { promise, cancel } 用于手动取消
 */
function startUpdateWithCancel(options = {}) {
  const controller = new AbortController();
  const promise = updateReport({ ...options, signal: controller.signal });
  return {
    promise,
    cancel: (reason = new OperationCanceledError('Canceled by user')) => controller.abort(reason),
    signal: controller.signal,
  };
}

/**
 * 计算距离下一次指定时间(本地时区)的毫秒数
 */
function msUntilNextTime(hour = 2, minute = 0) {
  const now = new Date();
  const next = new Date(now);
  next.setHours(hour, minute, 0, 0);
  if (next <= now) next.setDate(next.getDate() + 1);
  return next.getTime() - now.getTime();
}

/**
 * 每日定时任务(默认 02:00)
 * 返回 { cancel },可停止调度
 */
function scheduleDailyAt(hour = 2, minute = 0, taskFn) {
  if (typeof taskFn !== 'function') {
    throw new TypeError('scheduleDailyAt requires a task function');
  }

  let timer = null;
  let stopped = false;

  const scheduleNext = () => {
    const delay = msUntilNextTime(hour, minute);
    timer = setTimeout(async function run() {
      if (stopped) return;
      try {
        await taskFn();
      } catch (err) {
        // 业务方可接管日志;此处保守输出
        console.error('[scheduleDailyAt] task error:', err);
      }
      if (!stopped) scheduleNext();
    }, delay);
    if (typeof timer.unref === 'function') timer.unref(); // 不阻止进程退出
  };

  scheduleNext();

  return {
    cancel() {
      stopped = true;
      if (timer) clearTimeout(timer);
    },
  };
}

module.exports = {
  updateReport,
  startUpdateWithCancel,
  scheduleDailyAt,
  ReportFileError,
  OperationCanceledError,
  OperationTimeoutError,
};

/**
 * CLI 示例(可选):node reportTask.js --run-now 或 --schedule
 */
if (require.main === module) {
  const [, , arg] = process.argv;

  if (arg === '--run-now') {
    updateReport()
      .then((res) => {
        console.log('[run-now] success:', res);
        process.exitCode = 0;
      })
      .catch((err) => {
        console.error('[run-now] failed:', err);
        process.exitCode = 1;
      });
  } else if (arg === '--schedule') {
    console.log('[schedule] job scheduled at 02:00 local time, press Ctrl+C to quit.');
    const job = scheduleDailyAt(2, 0, () => updateReport());
    const cleanExit = () => {
      job.cancel();
      console.log('\n[schedule] canceled');
      process.exit(0);
    };
    process.on('SIGINT', cleanExit);
    process.on('SIGTERM', cleanExit);
  }
}

代码说明

  • 功能描述:

    • updateReport:读取 ./data/report.json(如果不存在则创建默认结构),将当前时间追加为生成时间(history 数组存在则 push,否则写入 generatedAt 字段),再以原子方式写回文件。全程使用流与 pipeline,避免同步阻塞,并在 finally 中清理临时目录、关闭句柄。
    • startUpdateWithCancel:提供手动取消能力,返回 { promise, cancel, signal }。
    • scheduleDailyAt:本地时间每日固定时刻(默认 02:00)执行指定任务,返回 { cancel } 用于停止调度。
    • 自定义错误类型:
      • ReportFileError:封装 fs/JSON 相关错误。
      • OperationCanceledError:取消时抛出。
      • OperationTimeoutError:超时时抛出。
  • 参数说明:

    • updateReport(options)
      • filePath: string,目标文件路径,默认 ./data/report.json
      • timeout: number,超时毫秒数,默认 10000
      • signal: AbortSignal,可选,外部取消信号
    • startUpdateWithCancel(options)
      • 与 updateReport 相同,额外返回 cancel() 方法
    • scheduleDailyAt(hour, minute, taskFn)
      • hour: number,小时(0-23),默认 2
      • minute: number,分钟(0-59),默认 0
      • taskFn: Function,要执行的异步任务(可返回 Promise)
  • 返回值:

    • updateReport resolve:
      • { filePath: string, generatedAt: string(ISO) }
    • updateReport reject:
      • ReportFileError | OperationCanceledError | OperationTimeoutError
    • scheduleDailyAt:
      • { cancel: Function },调用后停止后续调度
    • startUpdateWithCancel:
      • { promise: Promise, cancel: Function, signal: AbortSignal }

使用示例

// 1) 手动触发一次(默认 10s 超时)
const { updateReport, startUpdateWithCancel, scheduleDailyAt } = require('./reportTask');

(async () => {
  try {
    const res = await updateReport({
      filePath: './data/report.json',
      timeout: 10_000,
    });
    console.log('Updated:', res);
  } catch (err) {
    if (err.name === 'OperationTimeoutError') {
      console.error('Timeout:', err.message);
    } else if (err.name === 'OperationCanceledError') {
      console.error('Canceled:', err.message);
    } else if (err.name === 'ReportFileError') {
      console.error('FS error:', err.message, 'cause:', err.cause);
    } else {
      console.error('Unknown error:', err);
    }
  }
})();

// 2) 带取消信号的手动触发
const run = startUpdateWithCancel({ timeout: 10_000 });
setTimeout(() => {
  // 例如用户操作取消
  run.cancel();
}, 1000);

run.promise
  .then((res) => console.log('Updated:', res))
  .catch((err) => console.error('Canceled or failed:', err));

// 3) 每日 02:00 定时执行
const job = scheduleDailyAt(2, 0, () => updateReport({ timeout: 10_000 }));
// 某些情况下可取消定时
// job.cancel();

注意事项

  • Node.js 版本建议:
    • 推荐 Node.js v16.14+(支持 stream/promises.pipeline 和 fs 流的 signal 选项)。较低版本可能无法传入 AbortSignal。
  • 流与 JSON:
    • JSON.parse 必须一次性解析完整文本,因此仍会在内存中保留字符串副本;如文件极大且需要真正的“流式解析”,建议使用增量 JSON 解析库(例如 JSONStream),但这超出当前实现范围。
  • 原子写入与临时目录:
    • 临时目录创建在目标文件同一目录下,确保 rename 的原子性与同文件系统;写入完成后无论成功与否都会清理临时目录。
  • 安全与权限:
    • 写入文件权限设置为 0o600(仅当前用户可读写),减少不必要的权限暴露。
  • 取消与超时:
    • 取消或超时会尽快中断流并释放句柄;外部传入的 AbortSignal 与内部超时会被合并。
    • 若传入的 signal 已经 aborted,会立即以取消错误拒绝。
  • 错误处理:
    • 所有 fs/JSON 相关错误统一封装为 ReportFileError 抛出;取消与超时分别抛出 OperationCanceledError 与 OperationTimeoutError,便于上层区分处理。
  • 调度器:
    • scheduleDailyAt 使用 setTimeout 精确调度到下一次目标时间,执行后再调度下一次;使用 unref() 避免单独定时器阻止进程退出。
    • 调度函数内部捕获任务异常并打印日志,不会中断后续调度;如需更强的可观测性,建议在 taskFn 内部上报日志/指标。
  • 路径与跨盘:
    • 若 data 目录与最终文件不在同一文件系统(极端情况,例如符号链接跨盘),rename 的原子性无法保证。建议确保 report.json 与临时目录在同一物理卷上。
  • 调试建议:
    • 可在 readJsonStream / writeJsonAtomically 中加入更多日志(例如读写字节数)以定位性能瓶颈。
    • 将 timeout 调小进行超时路径测试,将 cancel 提前触发测试取消路径和资源清理情况。

Promise代码实现

/**
 * 订单状态轮询器(支持暂停/恢复/取消、超时、降级到本地缓存、状态变化回调)
 * - 每 5 秒轮询一次 GET /api/order/{id}/status
 * - 最多轮询 12 次
 * - 每次请求 3000ms 超时
 * - 状态变化通过 onStatusChange 回调上报
 * - 若接口一直不可用则降级显示本地缓存状态并记录日志
 * - 用户提交订单后自动开始;页面离开或取消即停止
 *
 * 使用说明见下方“使用示例”与“注意事项”
 */

/**
 * 自定义取消错误类型
 */
class CancellationError extends Error {
  constructor(message = 'Polling was cancelled') {
    super(message);
    this.name = 'CancellationError';
    this.code = 'ERR_POLLING_CANCELLED';
  }
}

/**
 * 对 fetch 增加超时与外部 AbortSignal 支持,返回 Promise<Response>
 * - 确保超时定时器与事件监听在完成后清理,避免内存泄漏
 * @param {string} url
 * @param {object} options
 * @param {number} options.timeoutMs
 * @param {AbortSignal} [options.externalSignal]
 * @param {RequestInit} [options.fetchOptions]
 */
function fetchWithTimeout(url, { timeoutMs, externalSignal, fetchOptions = {} }) {
  const controller = new AbortController();
  const { signal } = controller;

  let onExternalAbort = null;
  if (externalSignal) {
    if (externalSignal.aborted) {
      controller.abort(externalSignal.reason);
    } else {
      onExternalAbort = () => controller.abort(externalSignal.reason);
      externalSignal.addEventListener('abort', onExternalAbort, { once: true });
    }
  }

  const timeoutId = setTimeout(() => {
    // 标准化超时原因,方便上层判断
    controller.abort(new DOMException('Request timed out', 'TimeoutError'));
  }, timeoutMs);

  // 合并 signal 到 fetch
  const finalFetchOptions = { ...fetchOptions, signal };

  return fetch(url, finalFetchOptions)
    .finally(() => {
      clearTimeout(timeoutId);
      if (onExternalAbort) {
        externalSignal.removeEventListener('abort', onExternalAbort);
      }
    });
}

/**
 * 创建订单状态轮询器
 * @param {string|number} orderId
 * @param {object} [options]
 * @param {string} [options.baseUrl='/api'] - API 基础路径
 * @param {number} [options.intervalMs=5000] - 轮询间隔
 * @param {number} [options.timeoutMs=3000] - 单次请求超时
 * @param {number} [options.maxAttempts=12] - 最大轮询次数
 * @param {(status: any, meta: {attempt: number, from: 'server'|'cache', timestamp: number}) => void} [options.onStatusChange]
 * @param {(state: string, meta?: any) => void} [options.onStateChange]
 * @param {(status: any) => boolean} [options.isTerminal] - 判断终态的函数
 * @param {() => (Promise<any> | any)} [options.getCachedStatus] - 获取本地缓存状态
 * @param {{info?:Function, warn?:Function, error?:Function}} [options.logger]
 * @param {boolean} [options.stopOnPageLeave=true] - 页面离开自动停止
 * @param {RequestInit} [options.fetchOptions] - 透传给 fetch 的配置(如 credentials、headers)
 */
function createOrderStatusPoller(orderId, options = {}) {
  if (orderId == null || orderId === '') {
    throw new Error('orderId is required');
  }

  const {
    baseUrl = '/api',
    intervalMs = 5000,
    timeoutMs = 3000,
    maxAttempts = 12,
    onStatusChange,
    onStateChange,
    isTerminal = (status) => {
      // 默认认为这些状态为终态,可按业务自定义
      const t = String(status || '').toLowerCase();
      return t === 'completed' || t === 'success' || t === 'failed' || t === 'canceled' || t === 'cancelled';
    },
    getCachedStatus = () => null,
    logger = console,
    stopOnPageLeave = true,
    fetchOptions,
  } = options;

  const url = `${baseUrl.replace(/\/+$/, '')}/order/${encodeURIComponent(orderId)}/status`;

  // 内部状态
  let attempts = 0;
  let timerId = null;
  let state = 'idle'; // idle -> running -> paused|cancelled|completed|degraded|exhausted
  let lastStatus = undefined;
  let lastServerStatus = undefined;
  let hadAnySuccess = false; // 是否至少成功访问过接口
  let inFlight = false;
  let settled = false;

  // 控制器
  const runController = new AbortController();

  // start Promise 的控制
  let resolveStart, rejectStart;
  const startPromise = new Promise((res, rej) => {
    resolveStart = res;
    rejectStart = rej;
  });

  // 页面离开自动取消
  const pageLeaveHandlers = [];
  const attachPageLeave = () => {
    if (!stopOnPageLeave || typeof window === 'undefined') return;
    const onPageHide = () => api.cancel(new CancellationError('Page left'));
    const onBeforeUnload = () => api.cancel(new CancellationError('Page is unloading'));
    window.addEventListener('pagehide', onPageHide, { once: true });
    window.addEventListener('beforeunload', onBeforeUnload, { once: true });
    pageLeaveHandlers.push(['pagehide', onPageHide], ['beforeunload', onBeforeUnload]);
  };

  const detachPageLeave = () => {
    if (typeof window === 'undefined') return;
    for (const [evt, handler] of pageLeaveHandlers) {
      window.removeEventListener(evt, handler);
    }
    pageLeaveHandlers.length = 0;
  };

  const setState = (next, meta) => {
    if (state !== next) {
      state = next;
      onStateChange && safeCall(() => onStateChange(next, meta));
    }
  };

  const safeCall = (fn) => {
    try {
      return fn();
    } catch (err) {
      // 回调不影响主流程,记录日志即可
      logger?.error?.('Callback error:', err);
    }
  };

  const clearTimer = () => {
    if (timerId != null) {
      clearTimeout(timerId);
      timerId = null;
    }
  };

  const scheduleNext = (delay = intervalMs) => {
    clearTimer();
    if (settled || state !== 'running') return;
    timerId = setTimeout(tick, delay);
  };

  const finalize = (result) => {
    if (settled) return;
    settled = true;
    clearTimer();
    detachPageLeave();
    runController.abort(); // 确保中断可能的后续动作
    resolveStart(result);
  };

  const fail = (error) => {
    if (settled) return;
    settled = true;
    clearTimer();
    detachPageLeave();
    runController.abort();
    rejectStart(error);
  };

  const notifyStatusChange = (status, from) => {
    if (status !== lastStatus) {
      lastStatus = status;
      onStatusChange &&
        safeCall(() =>
          onStatusChange(status, {
            attempt: attempts,
            from,
            timestamp: Date.now(),
          })
        );
    }
  };

  async function requestOnce() {
    const resp = await fetchWithTimeout(url, {
      timeoutMs,
      externalSignal: runController.signal,
      fetchOptions: { method: 'GET', ...fetchOptions },
    });

    // HTTP 错误仍作为“可用 but error”的情况处理;但只将 2xx 视为成功访问
    if (!resp.ok) {
      const err = new Error(`HTTP ${resp.status} ${resp.statusText}`);
      err.name = 'HttpError';
      err.status = resp.status;
      throw err;
    }

    // 尝试解析 JSON
    let data;
    try {
      data = await resp.json();
    } catch (e) {
      const err = new Error('Invalid JSON response');
      err.name = 'ParseError';
      throw err;
    }
    return data;
  }

  async function handleExhausted() {
    // 所有尝试结束
    if (!hadAnySuccess) {
      // 接口一直不可用 => 降级到缓存
      setState('degraded');
      logger?.warn?.(`[poll] API unavailable for order ${orderId}, falling back to cache`);
      let cached;
      try {
        cached = await Promise.resolve().then(() => getCachedStatus());
      } catch (e) {
        logger?.error?.('[poll] getCachedStatus failed:', e);
        cached = null;
      }
      notifyStatusChange(cached, 'cache');
      finalize({
        orderId,
        state: 'degraded',
        attempts,
        lastStatus: cached,
        source: 'cache',
        completedAt: Date.now(),
      });
    } else {
      // 接口可用但未到终态 => 返回最后的服务端状态
      setState('exhausted');
      logger?.warn?.(
        `[poll] Max attempts reached for order ${orderId}; last server status: ${JSON.stringify(lastServerStatus)}`
      );
      finalize({
        orderId,
        state: 'exhausted',
        attempts,
        lastStatus: lastServerStatus,
        source: 'server',
        completedAt: Date.now(),
      });
    }
  }

  async function tick() {
    if (settled || state !== 'running' || inFlight) return;

    if (attempts >= maxAttempts) {
      await handleExhausted();
      return;
    }

    attempts += 1;
    inFlight = true;

    try {
      const data = await requestOnce();
      hadAnySuccess = true;

      // 规范化服务端返回:假设 { status: '...' },若不同可在此调整
      const status = data && (data.status ?? data.orderStatus ?? data.state);
      lastServerStatus = status;

      notifyStatusChange(status, 'server');

      if (isTerminal(status)) {
        setState('completed');
        finalize({
          orderId,
          state: 'completed',
          attempts,
          lastStatus: status,
          source: 'server',
          completedAt: Date.now(),
        });
      } else {
        // 非终态,继续下一次
        scheduleNext(intervalMs);
      }
    } catch (err) {
      // 统一错误处理(网络错误、超时、HTTP 非 2xx、解析错误)
      if (err?.name === 'AbortError' || err instanceof DOMException && err.name === 'AbortError') {
        // 可能是取消/暂停触发的中断,区分状态
        if (state === 'paused' || state === 'cancelled') {
          // 暂停/取消时不再继续
        } else if (err.name === 'AbortError' && err.message === 'Request timed out' || err.name === 'TimeoutError') {
          logger?.warn?.(`[poll] timeout ${timeoutMs}ms (attempt ${attempts}/${maxAttempts}) for order ${orderId}`);
          scheduleNext(intervalMs);
        } else {
          // 其它 abort,按取消处理
          logger?.info?.('[poll] aborted by controller');
        }
      } else {
        // 对于 HTTP 错误、解析错误或网络错误:记录并继续重试
        logger?.warn?.(
          `[poll] attempt ${attempts}/${maxAttempts} failed for order ${orderId}: ${err?.name || 'Error'} - ${err?.message}`
        );
        scheduleNext(intervalMs);
      }
    } finally {
      inFlight = false;
    }
  }

  const api = {
    /**
     * 启动轮询(返回一个 Promise,当进入终态/降级/耗尽时 resolve;取消时 reject)
     * 多次调用将返回同一个 Promise,避免重复启动。
     * @returns {Promise<{orderId:any, state:string, attempts:number, lastStatus:any, source:'server'|'cache', completedAt:number}>}
     */
    start() {
      if (state === 'idle') {
        setState('running');
        attachPageLeave();
        // 立即开始首轮,而不是等待 interval
        scheduleNext(0);
      }
      return startPromise;
    },

    /**
     * 暂停轮询:会中断在途请求并停止调度;resume() 后继续
     */
    pause() {
      if (settled || state !== 'running') return;
      setState('paused', { attempts });
      clearTimer();
      // 中止在途 fetch
      runController.abort(new DOMException('Paused', 'AbortError'));
      // 重置 controller(后续 resume 需要新的 signal)
      resetController();
    },

    /**
     * 恢复轮询:从暂停处继续,立即触发下一次尝试
     */
    resume() {
      if (settled || state !== 'paused') return;
      setState('running', { attempts });
      scheduleNext(0);
    },

    /**
     * 取消轮询:reject start() 返回的 Promise
     * @param {Error} [reason]
     */
    cancel(reason = new CancellationError()) {
      if (settled || state === 'cancelled') return;
      setState('cancelled');
      clearTimer();
      detachPageLeave();
      runController.abort(reason);
      fail(reason);
    },

    /**
     * 获取当前状态
     */
    getState() {
      return {
        orderId,
        state,
        attempts,
        lastStatus,
        lastServerStatus,
        hadAnySuccess,
        inFlight,
      };
    },
  };

  // 重建 AbortController(用于 pause 后 resume)
  function resetController() {
    // 不能直接替换 runController 引用,因此这里重建内部信号的方式是重新绑定 fetchWithTimeout 的 externalSignal。
    // 为了简单起见,这里通过闭包使用同一个 runController,但每次 pause 实际会取消当前 in-flight,后续 fetchWithTimeout 会读取新的 signal。
    // 若希望彻底重建,可将 runController 改为可变引用,但需要更复杂的结构。这里保持简洁与安全。
    // 这里实际无需做任何事,因为 runController 仅用于在当前上下文中 abort in-flight 请求;
    // 下一次 tick 会重新创建 fetch 调用并使用 runController.signal(其状态已为非 aborted,因为我们不重用此前 aborted 状态)。
    // 然而 AbortController 一旦 abort 不能复用,因此采用如下策略:
    // - 使用一个可变引用保存当前 controller
  }

  // 为了真正可在 pause 后恢复,需要使用可变的 currentController
  // 我们替换上面的 runController 实现为可变版本
  // 由于上面代码已引用 runController.signal,这里进行适配:
  // 重写 runController 为代理对象,内部持有 currentController
  (function patchAbortControllerForResume() {
    let current = new AbortController();
    // 代理对象:暴露统一的 signal 和 abort,但内部可替换
    const proxy = {
      get signal() {
        return current.signal;
      },
      abort(reason) {
        try {
          current.abort(reason);
        } catch (_) {}
      },
      // 提供重置方法
      _reset() {
        current = new AbortController();
      },
    };
    // 将 runController 的属性重定向到 proxy
    // 这里使用 Object.assign 替换方法,保持对原引用的使用方可见
    runController.signal = proxy.signal;
    runController.abort = proxy.abort;
    resetController = () => proxy._reset();
  })();

  return api;
}

代码说明

  • 功能描述:

    • 轮询接口 GET /api/order/{id}/status,默认每5秒一次,最多12次,每次请求超时3000ms。
    • 支持暂停、恢复与取消:
      • pause() 会中断在途请求并停止后续调度;
      • resume() 会立即继续下一次尝试;
      • cancel() 会终止任务并使 start() 的 Promise 拒绝。
    • 状态变化通过 onStatusChange 回调上报,仅在状态从上次不同才触发。
    • 若在所有尝试中接口始终不可用(无任何一次 2xx 成功),则降级使用 getCachedStatus() 的本地状态,并记录日志。
    • 若接口可用但在最大次数内未到终态,返回最后一次的服务端状态,并标记为 exhausted。
    • 页面离开(pagehide/beforeunload)或手动取消会停止任务。
  • 参数说明:

    • orderId: 订单ID(必填)。
    • options:
      • baseUrl: API基础路径,默认 '/api'。
      • intervalMs: 轮询间隔,默认 5000。
      • timeoutMs: 单次请求超时,默认 3000。
      • maxAttempts: 最大轮询次数,默认 12。
      • onStatusChange(status, meta): 状态变化回调,meta 包含 attempt、from('server'|'cache')、timestamp。
      • onStateChange(state, meta): 轮询器状态变化回调,state 包括 idle|running|paused|cancelled|completed|degraded|exhausted。
      • isTerminal(status): 判断终态的函数,默认识别 completed/success/failed/canceled(lled)。
      • getCachedStatus(): 获取本地缓存状态的函数,支持同步或 Promise。
      • logger: 日志对象,默认 console,需包含 info/warn/error 方法之一即可。
      • stopOnPageLeave: 页面离开自动停止,默认 true。
      • fetchOptions: 透传给 fetch 的附加配置(如 headers、credentials 等)。
  • 返回值:

    • createOrderStatusPoller() 返回一个控制器对象:
      • start(): Promise,当任务完成/降级/耗尽时 resolve,取消时 reject。
      • pause(): void
      • resume(): void
      • cancel(reason?): void
      • getState(): 返回当前内部状态快照
    • ResolveResult 结构:
      • { orderId, state, attempts, lastStatus, source, completedAt }
        • state: 'completed' | 'degraded' | 'exhausted'
        • lastStatus: 最终状态(来自服务端或缓存)
        • source: 'server' | 'cache'
    • Reject:
      • 取消时抛出 CancellationError,code 为 'ERR_POLLING_CANCELLED'。
      • 其它异常均在内部吞掉并重试,正常不会向外抛出。

使用示例

// 假设在用户提交订单后自动开始
const orderId = '123456';

// 模拟本地缓存读取
function getCachedOrderStatus() {
  try {
    const raw = localStorage.getItem(`order:${orderId}:status`);
    return raw ? JSON.parse(raw) : 'unknown';
  } catch {
    return 'unknown';
  }
}

// 创建轮询器
const poller = createOrderStatusPoller(orderId, {
  baseUrl: '/api',
  intervalMs: 5000,
  timeoutMs: 3000,
  maxAttempts: 12,
  getCachedStatus: getCachedOrderStatus,
  onStatusChange: (status, meta) => {
    console.log('[onStatusChange]', status, meta);
    // 将最新状态写入本地缓存,便于降级显示
    try {
      localStorage.setItem(`order:${orderId}:status`, JSON.stringify(status));
    } catch {}
    // 根据业务更新 UI
    // updateUI(status);
  },
  onStateChange: (state) => {
    console.log('[onStateChange]', state);
  },
  isTerminal: (status) => {
    // 自定义终态判定(例如:completed/failed/cancelled)
    const t = String(status || '').toLowerCase();
    return ['completed', 'failed', 'canceled', 'cancelled'].includes(t);
  },
  fetchOptions: {
    // 例如:跨域需携带 cookie
    // credentials: 'include',
    // headers: { 'X-Requested-With': 'XMLHttpRequest' }
  },
});

// 用户提交订单后自动启动
poller.start()
  .then((result) => {
    console.log('Polling finished:', result);
    // 根据 result.state 做不同处理:
    // - completed: 订单已到达终态
    // - degraded: 接口不可用,已显示本地缓存状态
    // - exhausted: 接口可用但未到终态,已返回最后服务端状态
  })
  .catch((err) => {
    if (err?.code === 'ERR_POLLING_CANCELLED') {
      console.warn('Polling cancelled by user or page leave');
    } else {
      console.error('Unexpected polling error:', err);
    }
  });

// 在页面提供暂停/恢复/取消的交互
document.getElementById('pauseBtn')?.addEventListener('click', () => poller.pause());
document.getElementById('resumeBtn')?.addEventListener('click', () => poller.resume());
document.getElementById('cancelBtn')?.addEventListener('click', () => poller.cancel());

// 也可在路由切换/页面离开时取消(若未使用 stopOnPageLeave)
/*
window.addEventListener('pagehide', () => poller.cancel());
*/

// 读取当前内部状态快照
console.log('Current state:', poller.getState());

注意事项

  • 超时与中断:
    • 单次请求超时为 3000ms,超时会自动重试,直至达到最大次数或被暂停/取消。
    • pause() 会中断在途请求并停止后续调度;resume() 会立即继续下一次尝试。
  • 降级策略:
    • 若在所有尝试中从未成功拿到 2xx 响应,则视为“接口不可用”,降级到本地缓存状态,并通过 logger 记录。
    • 若接口可用但未达终态,返回 exhausted,并带上最后服务端状态,便于 UI 做相应提示。
  • 终态判定:
    • 默认识别 completed/success/failed/canceled(lled),可通过 isTerminal 自定义。
    • 仅在状态确实变化时才触发 onStatusChange,避免无意义的重复通知。
  • 页面生命周期:
    • 默认在 pagehide/beforeunload 自动取消,确保无多余网络请求和事件残留。若在单页应用路由切换时保留轮询,请将 stopOnPageLeave 设为 false 并自行管理 cancel()。
  • 性能与内存:
    • 所有定时器、事件监听、请求中断均在任务结束时清理,避免内存泄漏。
    • onStatusChange/onStateChange 中的异常会被捕获并记录,不会影响轮询流程。
  • 安全与健壮性:
    • 使用 AbortController 和超时控制,避免僵尸请求。
    • 不依赖已废弃 API;不使用过度复杂的嵌套结构,便于维护。
  • 调试建议:
    • 通过 logger 输出的 warn/error 定位网络错误、超时与解析问题。
    • 使用 getState() 查看当前 attempts、lastStatus、inFlight 等内部状态。
    • 在开发环境可缩短 intervalMs 和 maxAttempts 加速验证逻辑。
  • 其他环境:
    • Node.js 环境无页面事件,stopOnPageLeave 无效;其余逻辑可复用。需要 Node v18+ 或引入 fetch polyfill。

示例详情

解决的问题

面向前端与 Node.js 团队,帮助把“异步需求描述”一键转成可直接落地的 Promise/async 方案。通过标准化的生成结果与清晰注释,快速覆盖错误处理、超时与取消、重试与限流、状态回传与资源清理等关键环节,显著减少返工与线上隐患。适配常见场景(网络请求、文件读写、定时/轮询、事件处理、任务编排),让新人 10 分钟上手、老手 3 倍提效;推动团队形成统一的异步编码规范、提高评审通过率与交付速度。试用阶段聚焦“省时省心可直接用”,进阶版支持复杂任务编排、并发治理与可插拔日志策略,促进从体验到付费的自然转化。

适用用户

前端开发工程师

为接口请求、用户交互与事件处理一键生成带超时与重试的异步代码,统一风格,快速联调上线,显著减少线上报错。

Node.js后端工程师

将数据库访问、文件读写与定时任务封装为可复用模块,自动注入错误兜底和超时保护,加速服务稳定迭代。

全栈/独立开发者

复用参数化模板,在多端项目中快速切换场景配置,几分钟完成可落地实现与说明,减少查文档与踩坑时间。

特征总结

一键生成符合最佳实践的异步代码,覆盖请求、文件与定时等高频场景需求
自动注入错误捕获与超时控制,显著降低线上故障率与排障成本与风险
按输入条件定制流程与触发点,复杂链式任务也能清晰可控地编排全局
自动生成详细注释与使用指引,新成员也能快速上手并安全改动代码无顾虑
内置结构与命名优化建议,提升可读性,减少后续维护沟通与返工成本压力
提供可直接粘贴的示例与注意事项,搭配调试建议,落地更高效从零到上线
预设安全边界与规范清单,避免不当写法与潜在隐患,守住上线底线稳定性
支持超时、重试与回退策略,一键生成可靠方案,应对不稳定环境及异常波动
可复用的参数化模板,按业务场景快速切换配置,团队协作更统一与规范性
从需求分析到验证交付的流程化指引,让每次生成都可追溯、可复查更放心

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

AI 提示词价格
¥20.00元
先用后买,用好了再付款,超安全!

您购买后可以获得什么

获得完整提示词模板
- 共 701 tokens
- 4 个可调节参数
{ 异步任务 } { 执行条件 } { 错误处理 } { 超时设置 }
获得社区贡献内容的使用权
- 精选社区优质案例,助您快速上手提示词
使用提示词兑换券,低至 ¥ 9.9
了解兑换券 →
限时半价

不要错过!

半价获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59