diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 29cdfe84..332569d4 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -7,7 +7,7 @@ type JavaApiResponse, unwrapJavaResponse, } from "@/shared/api/http"; -import { getTaskProgressCacheTtlMs } from "@/shared/task-progress-config"; +import { getTaskProgressCacheTtlMs, getTaskProgressTimeoutMs } from "@/shared/task-progress-config"; import { createTaskProgressRequestCache } from "@/shared/task-progress-request-cache"; const JAVA_API_PREFIX = "/newApi/api"; @@ -71,7 +71,9 @@ async function postTaskProgressBatch( } const requestPromise = unwrapJavaResponse( - post, { taskIds: number[] }>(path, { taskIds: normalizedTaskIds }), + post, { taskIds: number[] }>(path, { taskIds: normalizedTaskIds }, { + timeout: getTaskProgressTimeoutMs(), + }), ) .then((data) => { taskProgressResponseCache.set(cacheKey, data); diff --git a/frontend-vue/src/shared/task-progress-config.ts b/frontend-vue/src/shared/task-progress-config.ts index a317c4e8..c8ddb83d 100644 --- a/frontend-vue/src/shared/task-progress-config.ts +++ b/frontend-vue/src/shared/task-progress-config.ts @@ -19,6 +19,7 @@ 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 = 5000; +export const TASK_PROGRESS_TIMEOUT_MS = 10000; export interface TaskPollingConfig { /** 退避基数(毫秒),必须为正数 */ @@ -29,6 +30,8 @@ export interface TaskPollingConfig { foregroundRefreshEnabled?: boolean; /** 前台恢复延迟(毫秒),默认 0 */ foregroundRefreshDelayMs?: number; + /** 进度类请求超时(毫秒),默认 10s,必须为正数 */ + progressTimeoutMs?: number; } const DEFAULTS: Required = { @@ -36,6 +39,7 @@ const DEFAULTS: Required = { backoffMaxMs: TASK_POLL_BACKOFF_MAX_MS, foregroundRefreshEnabled: true, foregroundRefreshDelayMs: 0, + progressTimeoutMs: TASK_PROGRESS_TIMEOUT_MS, }; let config: Required = { ...DEFAULTS }; @@ -73,6 +77,10 @@ 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)) { @@ -84,6 +92,9 @@ export function configureTaskPolling(partial: TaskPollingConfig) { if (!(next.foregroundRefreshDelayMs >= 0)) { throw new Error("恢复延迟不能为负数: " + next.foregroundRefreshDelayMs); } + if (!(next.progressTimeoutMs > 0)) { + throw new Error("progressTimeoutMs 必须为正数: " + next.progressTimeoutMs); + } config = next; } diff --git a/frontend-vue/tests/progress-timeout.test.ts b/frontend-vue/tests/progress-timeout.test.ts new file mode 100644 index 00000000..eb6bd8f8 --- /dev/null +++ b/frontend-vue/tests/progress-timeout.test.ts @@ -0,0 +1,64 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { + TASK_PROGRESS_TIMEOUT_MS, + getTaskProgressTimeoutMs, + configureTaskPolling, + resetTaskPollingConfig, +} from '../src/shared/task-progress-config.ts' + +test('test_progress_timeout_default', () => { + resetTaskPollingConfig() + assert.equal(getTaskProgressTimeoutMs(), 10000, '进度请求默认 10s') +}) + +test('test_progress_timeout_override', () => { + resetTaskPollingConfig() + configureTaskPolling({ progressTimeoutMs: 15000 }) + assert.equal(getTaskProgressTimeoutMs(), 15000) + resetTaskPollingConfig() +}) + +test('test_non_progress_timeout_30s', () => { + assert.equal(TASK_PROGRESS_TIMEOUT_MS, 10000, '进度超时常量应为 10s') + // 非进度请求默认超时由 http.ts 的 30000 保持,此处断言常量存在 + assert.ok(TASK_PROGRESS_TIMEOUT_MS < 30000) +}) + +test('test_timeout_config_name', () => { + resetTaskPollingConfig() + configureTaskPolling({ progressTimeoutMs: 8000 }) + assert.equal(getTaskProgressTimeoutMs(), 8000, '配置键 progressTimeoutMs 生效') + resetTaskPollingConfig() +}) + +test('test_progress_timeout_minimum', () => { + resetTaskPollingConfig() + assert.throws( + () => configureTaskPolling({ progressTimeoutMs: -1 }), + /超时.*正数|不能为负|progressTimeoutMs/, + '非法配置应抛错并保持原值', + ) + assert.equal(getTaskProgressTimeoutMs(), 10000, '非法配置后保持默认值') + resetTaskPollingConfig() +}) + +test('test_override_via_options', () => { + resetTaskPollingConfig() + configureTaskPolling({ progressTimeoutMs: 20000 }) + assert.equal(getTaskProgressTimeoutMs(), 20000, '函数级配置覆盖生效') + resetTaskPollingConfig() +}) + +test('test_timeout_zero', () => { + resetTaskPollingConfig() + assert.throws(() => configureTaskPolling({ progressTimeoutMs: 0 }), /正数|不能为负|progressTimeoutMs/) + assert.equal(getTaskProgressTimeoutMs(), 10000, '0 应被拒并保持默认') + resetTaskPollingConfig() +}) + +test('test_default_config_freeze', () => { + resetTaskPollingConfig() + const snapshots = [getTaskProgressTimeoutMs(), getTaskProgressTimeoutMs()] + assert.deepEqual(snapshots, [10000, 10000], '默认值快照稳定') +})