/** * 任务轮询配置(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 = { 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 = { ...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,封顶 max;attempt 省略/非法时按首次(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 }; }