Files
crawler-plugin/frontend-vue/src/shared/task-progress-config.ts
T
huangzd1997 9e224929fb task-30: getTaskPollBackoffMs 递增序列
getTaskPollBackoffMs 签名改为 attempt 可选(省略视为 0,向后兼容),
序列 500×factor^attempt、封顶 maxMs 行为由任务 29 的 factor 配置驱动。
新增 8 个测试:attempt0/1/2 值、序列单调、封顶、省略参数默认、
负 attempt 当 0、自定义 factor。
2026-08-31 20:16:30 +08:00

118 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 任务轮询配置(Task 86 扩展)。
*
* 把隐藏页面轮询间隔、前台恢复与退避策略统一收敛为可编程配置:
* - getTaskPollIntervalMs / getTaskProgressCacheTtlMs:按 document.visibilityState
* 自适应切换可见/隐藏间隔与缓存 TTL(历史行为不变);
* - getTaskPollBackoffMs(attempt):指数退避(base × 2^attempt,封顶 max),
* 供轮询失败/in-flight 重试使用,重试次数不再硬编码;
* - getTaskForegroundRefreshEnabled / getTaskForegroundRefreshDelayMs:控制
* 页面切回前台时是否立即拉取一轮及其延迟;
* - configureTaskPolling / resetTaskPollingConfig:运行时配置与恢复默认。
*
* 纯 TS 模块:配置校验失败时抛错且保持原值(零状态变更),document 缺失
* (Node/SSR)时按可见间隔降级;所有 getter 在任意配置下都有确定返回值。
*/
export const TASK_POLL_VISIBLE_INTERVAL_MS = 10000;
export const TASK_POLL_HIDDEN_INTERVAL_MS = 60000;
export const TASK_PROGRESS_VISIBLE_CACHE_MILLIS = 5000;
export const TASK_PROGRESS_HIDDEN_CACHE_MILLIS = 30000;
export const TASK_POLL_BACKOFF_BASE_MS = 500;
export const TASK_POLL_BACKOFF_MAX_MS = 10000;
export const TASK_POLL_BACKOFF_FACTOR = 2;
export const TASK_PROGRESS_TIMEOUT_MS = 10000;
export interface TaskPollingConfig {
/** 退避基数(毫秒),必须为正数 */
backoffBaseMs?: number;
/** 退避上限(毫秒),必须为正数 */
backoffMaxMs?: number;
/** 退避指数因子,<1 时钳制为 1 */
backoffFactor?: number;
/** 页面切回前台时是否立即拉取一轮,默认 true */
foregroundRefreshEnabled?: boolean;
/** 前台恢复延迟(毫秒),默认 0 */
foregroundRefreshDelayMs?: number;
/** 进度类请求超时(毫秒),默认 10s,必须为正数 */
progressTimeoutMs?: number;
}
const DEFAULTS: Required<TaskPollingConfig> = {
backoffBaseMs: TASK_POLL_BACKOFF_BASE_MS,
backoffMaxMs: TASK_POLL_BACKOFF_MAX_MS,
backoffFactor: TASK_POLL_BACKOFF_FACTOR,
foregroundRefreshEnabled: true,
foregroundRefreshDelayMs: 0,
progressTimeoutMs: TASK_PROGRESS_TIMEOUT_MS,
};
let config: Required<TaskPollingConfig> = { ...DEFAULTS };
function isVisible() {
return typeof document === "undefined" || document.visibilityState === "visible";
}
export function getTaskPollIntervalMs() {
if (!isVisible()) {
return TASK_POLL_HIDDEN_INTERVAL_MS;
}
return TASK_POLL_VISIBLE_INTERVAL_MS;
}
export function getTaskProgressCacheTtlMs() {
if (typeof document !== "undefined" && document.hidden) {
return TASK_PROGRESS_HIDDEN_CACHE_MILLIS;
}
return TASK_PROGRESS_VISIBLE_CACHE_MILLIS;
}
/** 第 attempt 次退避延迟:base × factor^attempt,封顶 maxattempt 省略/非法时按首次(0)处理 */
export function getTaskPollBackoffMs(attempt?: number) {
const safe = Number.isFinite(attempt) && (attempt as number) > 0 ? Math.floor(attempt as number) : 0;
const factor = getTaskPollBackoffFactor();
const scaled = config.backoffBaseMs * Math.pow(factor, safe);
return scaled > config.backoffMaxMs ? config.backoffMaxMs : scaled;
}
export function getTaskPollBackoffMaxMs() {
return config.backoffMaxMs;
}
/** 退避指数因子,<1 时钳制为 1 */
export function getTaskPollBackoffFactor() {
return config.backoffFactor < 1 ? 1 : config.backoffFactor;
}
export function getTaskForegroundRefreshEnabled() {
return config.foregroundRefreshEnabled;
}
export function getTaskForegroundRefreshDelayMs() {
return config.foregroundRefreshDelayMs;
}
export function getTaskProgressTimeoutMs() {
return config.progressTimeoutMs;
}
export function configureTaskPolling(partial: TaskPollingConfig) {
const next = { ...config, ...partial };
if (!(next.backoffBaseMs > 0)) {
throw new Error("退避基数必须为正数: " + next.backoffBaseMs);
}
if (!(next.backoffMaxMs > 0)) {
throw new Error("退避上限必须为正数: " + next.backoffMaxMs);
}
if (!(next.foregroundRefreshDelayMs >= 0)) {
throw new Error("恢复延迟不能为负数: " + next.foregroundRefreshDelayMs);
}
if (!(next.progressTimeoutMs > 0)) {
throw new Error("progressTimeoutMs 必须为正数: " + next.progressTimeoutMs);
}
config = next;
}
export function resetTaskPollingConfig() {
config = { ...DEFAULTS };
}