refactor(品牌工具页): 历史轮询抽为 useHistoryPolling
QueryAsin / Withdraw / PatrolDelete 三页逐字相同的 startHistoryPolling / stopHistoryPolling 收敛为 shared/composables/useHistoryPolling:定时器走各页 categorized-timers(category 固定 history-poll),间隔默认主轮询的 2 倍。 categorized-timers 补 CategorizedTimers 类型导出;补注入式假定时器单测 5 例。 净减约 40 行;vue-tsc 构建与 695 个前端单测通过。
This commit is contained in:
@@ -273,6 +273,7 @@ import {
|
||||
sanitizeCountryCodes,
|
||||
} from "@/shared/country-options";
|
||||
import { formatDateTime } from '@/shared/utils/datetime'
|
||||
import { useHistoryPolling } from '@/shared/composables/useHistoryPolling'
|
||||
|
||||
const MAX_TRANSIENT_ERRORS = 30;
|
||||
/** 任务终态后等待结果文件(Java 侧异步生成)的最大轮次,12 × 10s ≈ 2 分钟 */
|
||||
@@ -302,7 +303,6 @@ const queuePushResult = ref("");
|
||||
const queuePayloadText = ref("");
|
||||
const activeTaskId = ref<number | null>(null);
|
||||
const queueWorkerRunning = ref(false);
|
||||
let historyPollTimer: number | null = null;
|
||||
const timers = createCategorizedTimers("patrol-delete");
|
||||
|
||||
const matchedRunnableItems = computed(() =>
|
||||
@@ -399,6 +399,13 @@ function taskGroupKey(item: PatrolDeleteHistoryItem) {
|
||||
|
||||
let disposed = false;
|
||||
|
||||
const historyPolling = useHistoryPolling({
|
||||
timers,
|
||||
isDisposed: () => disposed,
|
||||
shouldPoll: () => hasQueueWork.value,
|
||||
refresh: () => refreshActiveTaskProgress(),
|
||||
})
|
||||
|
||||
function sleep(ms: number) {
|
||||
if (disposed) return Promise.resolve();
|
||||
return timers.sleep("queue-wait", ms);
|
||||
@@ -793,27 +800,6 @@ async function refreshActiveTaskProgress(taskIds?: number[]) {
|
||||
return batch.missingTaskIds || [];
|
||||
}
|
||||
|
||||
function startHistoryPolling() {
|
||||
stopHistoryPolling();
|
||||
if (disposed || !hasQueueWork.value) return;
|
||||
const run = async () => {
|
||||
historyPollTimer = null;
|
||||
if (disposed || !hasQueueWork.value) return;
|
||||
await refreshActiveTaskProgress();
|
||||
if (!disposed && hasQueueWork.value) {
|
||||
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||
}
|
||||
};
|
||||
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||
}
|
||||
|
||||
function stopHistoryPolling() {
|
||||
if (historyPollTimer) {
|
||||
timers.clearTimer("history-poll", historyPollTimer);
|
||||
historyPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChange(rows: PatrolDeleteCandidateVo[]) {
|
||||
selectedCandidates.value = rows;
|
||||
}
|
||||
@@ -1060,7 +1046,7 @@ async function processQueue() {
|
||||
if (disposed || queueWorkerRunning.value) return;
|
||||
queueWorkerRunning.value = true;
|
||||
pushing.value = true;
|
||||
startHistoryPolling();
|
||||
historyPolling.start();
|
||||
|
||||
try {
|
||||
const api = getPywebviewApi();
|
||||
@@ -1175,7 +1161,7 @@ async function processQueue() {
|
||||
} finally {
|
||||
queueWorkerRunning.value = false;
|
||||
pushing.value = false;
|
||||
stopHistoryPolling();
|
||||
historyPolling.stop();
|
||||
saveQueueState();
|
||||
}
|
||||
}
|
||||
@@ -1260,7 +1246,7 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
disposed = true;
|
||||
stopHistoryPolling();
|
||||
historyPolling.stop();
|
||||
clearSleepTimers();
|
||||
timers.clearScope();
|
||||
});
|
||||
|
||||
@@ -215,6 +215,7 @@ import { passGuard } from "@/shared/dispatch-guard-ui";
|
||||
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
|
||||
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
|
||||
import { formatDateTime } from '@/shared/utils/datetime'
|
||||
import { useHistoryPolling } from '@/shared/composables/useHistoryPolling'
|
||||
|
||||
const MAX_TRANSIENT_ERRORS = 30;
|
||||
const ziniaoVersion = useZiniaoVersion();
|
||||
@@ -241,7 +242,6 @@ const activeQueueItem = ref<QueryAsinShopQueueItem | null>(null);
|
||||
const queueWorkerRunning = ref(false);
|
||||
const autoQueueEnabled = ref(false);
|
||||
const taskStartTimes = ref<Record<number, string>>({});
|
||||
let historyPollTimer: number | null = null;
|
||||
const timers = createCategorizedTimers("query-asin");
|
||||
|
||||
const matchedRunnableItems = computed(() =>
|
||||
@@ -327,6 +327,13 @@ function historyItemKey(item: QueryAsinHistoryItem) {
|
||||
|
||||
let disposed = false;
|
||||
|
||||
const historyPolling = useHistoryPolling({
|
||||
timers,
|
||||
isDisposed: () => disposed,
|
||||
shouldPoll: () => hasQueueWork.value,
|
||||
refresh: () => refreshActiveTaskProgress(),
|
||||
})
|
||||
|
||||
function sleep(ms: number) {
|
||||
if (disposed) return Promise.resolve();
|
||||
return timers.sleep("queue-wait", ms);
|
||||
@@ -597,7 +604,7 @@ function resetQueueWorkerIfIdle() {
|
||||
queueWorkerRunning.value = false;
|
||||
pushing.value = false;
|
||||
autoQueueEnabled.value = false;
|
||||
stopHistoryPolling();
|
||||
historyPolling.stop();
|
||||
saveQueueState();
|
||||
}
|
||||
|
||||
@@ -710,27 +717,6 @@ async function refreshActiveTaskProgress(taskIds?: number[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function startHistoryPolling() {
|
||||
stopHistoryPolling();
|
||||
if (disposed || !hasQueueWork.value) return;
|
||||
const run = async () => {
|
||||
historyPollTimer = null;
|
||||
if (disposed || !hasQueueWork.value) return;
|
||||
await refreshActiveTaskProgress();
|
||||
if (!disposed && hasQueueWork.value) {
|
||||
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||
}
|
||||
};
|
||||
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||
}
|
||||
|
||||
function stopHistoryPolling() {
|
||||
if (historyPollTimer) {
|
||||
timers.clearTimer("history-poll", historyPollTimer);
|
||||
historyPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChange(rows: QueryAsinCandidateVo[]) {
|
||||
selectedCandidates.value = rows;
|
||||
}
|
||||
@@ -894,7 +880,7 @@ async function processQueue() {
|
||||
if (disposed || queueWorkerRunning.value) return;
|
||||
queueWorkerRunning.value = true;
|
||||
pushing.value = true;
|
||||
startHistoryPolling();
|
||||
historyPolling.start();
|
||||
|
||||
try {
|
||||
const api = getPywebviewApi();
|
||||
@@ -1004,7 +990,7 @@ async function processQueue() {
|
||||
} finally {
|
||||
queueWorkerRunning.value = false;
|
||||
pushing.value = false;
|
||||
stopHistoryPolling();
|
||||
historyPolling.stop();
|
||||
saveQueueState();
|
||||
}
|
||||
}
|
||||
@@ -1087,7 +1073,7 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
disposed = true;
|
||||
stopHistoryPolling();
|
||||
historyPolling.stop();
|
||||
clearSleepTimers();
|
||||
timers.clearScope();
|
||||
});
|
||||
|
||||
@@ -255,6 +255,7 @@ import { passGuard } from "@/shared/dispatch-guard-ui";
|
||||
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
|
||||
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
|
||||
import { formatDateTime } from '@/shared/utils/datetime'
|
||||
import { useHistoryPolling } from '@/shared/composables/useHistoryPolling'
|
||||
|
||||
const MAX_TRANSIENT_ERRORS = 30;
|
||||
const ziniaoVersion = useZiniaoVersion();
|
||||
@@ -292,7 +293,6 @@ const activeQueueItem = ref<WithdrawTaskBatchQueueItem | null>(null);
|
||||
const queueWorkerRunning = ref(false);
|
||||
const autoQueueEnabled = ref(false);
|
||||
const taskStartTimes = ref<Record<number, string>>({});
|
||||
let historyPollTimer: number | null = null;
|
||||
const timers = createCategorizedTimers("withdraw");
|
||||
|
||||
const matchedRunnableItems = computed(() =>
|
||||
@@ -458,6 +458,13 @@ function mergeQueueBatches(
|
||||
|
||||
let disposed = false;
|
||||
|
||||
const historyPolling = useHistoryPolling({
|
||||
timers,
|
||||
isDisposed: () => disposed,
|
||||
shouldPoll: () => hasQueueWork.value,
|
||||
refresh: () => refreshActiveTaskProgress(),
|
||||
})
|
||||
|
||||
function sleep(ms: number) {
|
||||
if (disposed) return Promise.resolve();
|
||||
return timers.sleep("queue-wait", ms);
|
||||
@@ -741,7 +748,7 @@ function resetQueueWorkerIfIdle() {
|
||||
queueWorkerRunning.value = false;
|
||||
pushing.value = false;
|
||||
autoQueueEnabled.value = false;
|
||||
stopHistoryPolling();
|
||||
historyPolling.stop();
|
||||
saveQueueState();
|
||||
}
|
||||
|
||||
@@ -838,27 +845,6 @@ async function refreshActiveTaskProgress(taskIds?: number[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function startHistoryPolling() {
|
||||
stopHistoryPolling();
|
||||
if (disposed || !hasQueueWork.value) return;
|
||||
const run = async () => {
|
||||
historyPollTimer = null;
|
||||
if (disposed || !hasQueueWork.value) return;
|
||||
await refreshActiveTaskProgress();
|
||||
if (!disposed && hasQueueWork.value) {
|
||||
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||
}
|
||||
};
|
||||
historyPollTimer = timers.setTimeout("history-poll", run, getTaskPollIntervalMs() * 2);
|
||||
}
|
||||
|
||||
function stopHistoryPolling() {
|
||||
if (historyPollTimer) {
|
||||
timers.clearTimer("history-poll", historyPollTimer);
|
||||
historyPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChange(rows: WithdrawCandidateVo[]) {
|
||||
selectedCandidates.value = rows;
|
||||
}
|
||||
@@ -1033,7 +1019,7 @@ async function processQueue() {
|
||||
if (disposed || queueWorkerRunning.value) return;
|
||||
queueWorkerRunning.value = true;
|
||||
pushing.value = true;
|
||||
startHistoryPolling();
|
||||
historyPolling.start();
|
||||
|
||||
try {
|
||||
const api = getPywebviewApi();
|
||||
@@ -1136,7 +1122,7 @@ async function processQueue() {
|
||||
} finally {
|
||||
queueWorkerRunning.value = false;
|
||||
pushing.value = false;
|
||||
stopHistoryPolling();
|
||||
historyPolling.stop();
|
||||
saveQueueState();
|
||||
}
|
||||
}
|
||||
@@ -1220,7 +1206,7 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
disposed = true;
|
||||
stopHistoryPolling();
|
||||
historyPolling.stop();
|
||||
clearSleepTimers();
|
||||
timers.clearScope();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { getTaskPollIntervalMs } from '../task-progress-config.ts'
|
||||
import type { CategorizedTimers } from '../utils/categorized-timers.ts'
|
||||
|
||||
export interface HistoryPollingOptions {
|
||||
/** 页面自己的 categorized-timers 实例(决定定时器清理 scope)。 */
|
||||
timers: CategorizedTimers
|
||||
/** 组件是否已卸载。 */
|
||||
isDisposed: () => boolean
|
||||
/** 是否还有队列工作需要跟踪。 */
|
||||
shouldPoll: () => boolean
|
||||
/** 每轮拉取活动任务进度(返回值被忽略,允许各页返回不同结果)。 */
|
||||
refresh: () => Promise<unknown>
|
||||
/** 轮询间隔;默认 getTaskPollIntervalMs() 的 2 倍(历史刷新不需要主任务那么勤)。 */
|
||||
intervalMs?: () => number
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史任务进度轮询:队列有工作时按较慢的固定间隔拉取活动任务进度。
|
||||
* 与主任务轮询(useTaskProgressLoop)并存,但节奏更慢、只管历史列表刷新。
|
||||
*
|
||||
* 定时器统一注册到传入的 categorized-timers(category 固定为 'history-poll'),
|
||||
* 由页面在卸载时 clearScope 兜底清理。
|
||||
*/
|
||||
export function useHistoryPolling(options: HistoryPollingOptions) {
|
||||
const intervalMs = options.intervalMs ?? (() => getTaskPollIntervalMs() * 2)
|
||||
let timer: number | null = null
|
||||
|
||||
function stop() {
|
||||
if (timer != null) {
|
||||
options.timers.clearTimer('history-poll', timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
stop()
|
||||
if (options.isDisposed() || !options.shouldPoll()) return
|
||||
const run = async () => {
|
||||
timer = null
|
||||
if (options.isDisposed() || !options.shouldPoll()) return
|
||||
await options.refresh()
|
||||
if (!options.isDisposed() && options.shouldPoll()) {
|
||||
timer = options.timers.setTimeout('history-poll', run, intervalMs())
|
||||
}
|
||||
}
|
||||
timer = options.timers.setTimeout('history-poll', run, intervalMs())
|
||||
}
|
||||
|
||||
return { start, stop }
|
||||
}
|
||||
@@ -105,6 +105,8 @@ export function createCategorizedTimers(scope: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export type CategorizedTimers = ReturnType<typeof createCategorizedTimers>
|
||||
|
||||
export function getCategorizedTimerStats() {
|
||||
return Array.from(timerBuckets.entries()).map(([category, bucket]) => ({
|
||||
category,
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { useHistoryPolling } from '../src/shared/composables/useHistoryPolling.ts'
|
||||
import type { CategorizedTimers } from '../src/shared/utils/categorized-timers.ts'
|
||||
|
||||
function fakeTimers() {
|
||||
const scheduled: Array<{ category: string; id: number; delay: number; run: () => void }> = []
|
||||
const cleared: number[] = []
|
||||
let seq = 0
|
||||
const timers = {
|
||||
setTimeout(category: string, handler: () => void, delayMs: number) {
|
||||
const id = ++seq
|
||||
scheduled.push({ category, id, delay: delayMs, run: handler })
|
||||
return id
|
||||
},
|
||||
clearTimer(_category: string, id: number | null | undefined) {
|
||||
if (id != null) cleared.push(id)
|
||||
},
|
||||
}
|
||||
return { timers: timers as unknown as CategorizedTimers, scheduled, cleared }
|
||||
}
|
||||
|
||||
test('shouldPoll 为 false 时不排定时器', () => {
|
||||
const { timers, scheduled } = fakeTimers()
|
||||
const p = useHistoryPolling({
|
||||
timers,
|
||||
isDisposed: () => false,
|
||||
shouldPoll: () => false,
|
||||
refresh: async () => {},
|
||||
intervalMs: () => 40,
|
||||
})
|
||||
|
||||
p.start()
|
||||
|
||||
assert.equal(scheduled.length, 0)
|
||||
})
|
||||
|
||||
test('按传入间隔排定 history-poll,触发后刷新并续排', async () => {
|
||||
const { timers, scheduled } = fakeTimers()
|
||||
let calls = 0
|
||||
const p = useHistoryPolling({
|
||||
timers,
|
||||
isDisposed: () => false,
|
||||
shouldPoll: () => true,
|
||||
refresh: async () => {
|
||||
calls += 1
|
||||
},
|
||||
intervalMs: () => 40,
|
||||
})
|
||||
|
||||
p.start()
|
||||
assert.equal(scheduled.length, 1)
|
||||
assert.equal(scheduled[0].category, 'history-poll')
|
||||
assert.equal(scheduled[0].delay, 40)
|
||||
|
||||
await scheduled[0].run()
|
||||
|
||||
assert.equal(calls, 1)
|
||||
assert.equal(scheduled.length, 2)
|
||||
assert.equal(scheduled[1].delay, 40)
|
||||
})
|
||||
|
||||
test('stop 清除已排定的定时器', () => {
|
||||
const { timers, scheduled, cleared } = fakeTimers()
|
||||
const p = useHistoryPolling({
|
||||
timers,
|
||||
isDisposed: () => false,
|
||||
shouldPoll: () => true,
|
||||
refresh: async () => {},
|
||||
intervalMs: () => 40,
|
||||
})
|
||||
|
||||
p.start()
|
||||
p.stop()
|
||||
|
||||
assert.equal(scheduled.length, 1)
|
||||
assert.deepEqual(cleared, [scheduled[0].id])
|
||||
})
|
||||
|
||||
test('已卸载时触发不再刷新也不续排', async () => {
|
||||
const { timers, scheduled } = fakeTimers()
|
||||
let disposed = false
|
||||
let calls = 0
|
||||
const p = useHistoryPolling({
|
||||
timers,
|
||||
isDisposed: () => disposed,
|
||||
shouldPoll: () => true,
|
||||
refresh: async () => {
|
||||
calls += 1
|
||||
},
|
||||
intervalMs: () => 40,
|
||||
})
|
||||
|
||||
p.start()
|
||||
disposed = true
|
||||
await scheduled[0].run()
|
||||
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(scheduled.length, 1)
|
||||
})
|
||||
|
||||
test('刷新后队列已空则不再续排', async () => {
|
||||
const { timers, scheduled } = fakeTimers()
|
||||
let work = true
|
||||
const p = useHistoryPolling({
|
||||
timers,
|
||||
isDisposed: () => false,
|
||||
shouldPoll: () => work,
|
||||
refresh: async () => {
|
||||
work = false
|
||||
},
|
||||
intervalMs: () => 40,
|
||||
})
|
||||
|
||||
p.start()
|
||||
await scheduled[0].run()
|
||||
|
||||
assert.equal(scheduled.length, 1)
|
||||
})
|
||||
Reference in New Issue
Block a user