热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
本提示词专门用于生成高质量的JavaScript Promise代码,帮助开发者处理各种异步任务场景。通过清晰的代码结构、完善的错误处理机制和详细的注释说明,确保生成的Promise代码符合JavaScript最佳实践,具备良好的可读性和可维护性。适用于网络请求、文件操作、定时任务等多种异步编程场景,能够有效提升开发效率和代码质量。
/**
* 基于 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]
*/
功能描述:
参数说明:
返回值:
// 假设:用户进入“个人中心”路由时触发
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 会直接返回缓存结果,或复用在途任务
安全性
超时与取消
重试策略
会话与缓存
诊断与调试
可扩展性
性能与内存
'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);
}
}
功能描述:
参数说明:
返回值:
// 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();
/**
* 订单状态轮询器(支持暂停/恢复/取消、超时、降级到本地缓存、状态变化回调)
* - 每 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;
}
功能描述:
参数说明:
返回值:
// 假设在用户提交订单后自动开始
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());
面向前端与 Node.js 团队,帮助把“异步需求描述”一键转成可直接落地的 Promise/async 方案。通过标准化的生成结果与清晰注释,快速覆盖错误处理、超时与取消、重试与限流、状态回传与资源清理等关键环节,显著减少返工与线上隐患。适配常见场景(网络请求、文件读写、定时/轮询、事件处理、任务编排),让新人 10 分钟上手、老手 3 倍提效;推动团队形成统一的异步编码规范、提高评审通过率与交付速度。试用阶段聚焦“省时省心可直接用”,进阶版支持复杂任务编排、并发治理与可插拔日志策略,促进从体验到付费的自然转化。
为接口请求、用户交互与事件处理一键生成带超时与重试的异步代码,统一风格,快速联调上线,显著减少线上报错。
将数据库访问、文件读写与定时任务封装为可复用模块,自动注入错误兜底和超时保护,加速服务稳定迭代。
复用参数化模板,在多端项目中快速切换场景配置,几分钟完成可落地实现与说明,减少查文档与踩坑时间。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
半价获取高级提示词-优惠即将到期