Files
crawler-plugin/frontend-vue/src/shared/page-e2e-core-path.ts
T

100 lines
3.6 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.
/**
* 页面 E2E 核心路径执行器(Task 93)。
*
* 把 Similar ASIN / 店铺抓取 / 采集数据三个页面的 E2E 核心路径收敛为
* 可注入依赖的确定性流程:解析文件(parse)→ 建任务(createTask)→
* 轮询到终态(poll)→ 刷新历史(refreshHistory),各步由 deps 提供实现,
* 页面接入时传真实接口即可复用同一套语义。
*
* 流程语义:
* - parse 返回 0 表示无可处理数据:安全跳过后续步骤,不创建无效资源;
* - poll 返回非终态时按最大次数轮询,超过 maxPollAttempts 降级返回当前
* 状态(不抛错、不无限轮询、不刷新历史);
* - 任一步抛错:执行立即终止,cleanup 必被调用(失败与成功路径都执行),
* 调用方可重新执行同一 deps 恢复;
* - 输入校验 fail-fastdeps 缺函数、parse 返回非法 taskId、maxPollAttempts
* 非法均抛错且零状态变更。
*/
export interface PageE2EDeps {
/** 解析文件/参数,返回 taskId;返回 0 表示无可处理数据 */
parse: () => Promise<number>
/** 创建任务 */
createTask: (taskId: number) => Promise<void>
/** 单次轮询任务进度,返回状态字符串 */
poll: (taskId: number) => Promise<string>
/** 任务到达终态后刷新历史列表(可选) */
refreshHistory?: () => Promise<void>
/** 释放临时资源(URL、控制器、计时器等),失败与成功路径都会调用(可选) */
cleanup?: () => Promise<void>
}
export interface PageE2EOptions {
/** 轮询次数上限,必须为正数,默认 60 */
maxPollAttempts?: number
/** 判定终态,默认 SUCCESS/FAILED */
isTerminal?: (status: string) => boolean
}
export interface PageE2EResult {
taskId: number
status: string
/** 已完成的步骤(按执行顺序) */
completedSteps: string[]
/** 实际轮询次数 */
attempts: number
}
export async function runPageE2ECorePath(
deps: PageE2EDeps,
options: PageE2EOptions = {},
): Promise<PageE2EResult> {
if (typeof deps !== 'object' || deps == null) {
throw new Error('deps 必须是对象')
}
if (typeof deps.parse !== 'function') {
throw new Error('parse 必须是函数')
}
if (typeof deps.createTask !== 'function') {
throw new Error('createTask 必须是函数')
}
if (typeof deps.poll !== 'function') {
throw new Error('poll 必须是函数')
}
const maxPollAttempts = options.maxPollAttempts ?? 60
if (!(maxPollAttempts > 0)) {
throw new Error('maxPollAttempts 必须为正数: ' + maxPollAttempts)
}
const isTerminal = options.isTerminal ?? ((status: string) => status === 'SUCCESS' || status === 'FAILED')
const completedSteps: string[] = []
let status = ''
let attempts = 0
try {
const taskId = await deps.parse()
if (taskId === 0) {
completedSteps.push('parse')
return { taskId: 0, status: '', completedSteps, attempts: 0 }
}
if (typeof taskId !== 'number' || !Number.isFinite(taskId) || taskId <= 0) {
throw new Error('parse 必须返回正整数 taskId: ' + taskId)
}
completedSteps.push('parse')
await deps.createTask(taskId)
completedSteps.push('create-task')
while (attempts < maxPollAttempts) {
attempts += 1
status = await deps.poll(taskId)
completedSteps.push('poll')
if (isTerminal(status)) {
await deps.refreshHistory?.()
completedSteps.push('refresh-history')
return { taskId, status, completedSteps, attempts }
}
}
return { taskId, status, completedSteps, attempts }
} finally {
await deps.cleanup?.()
}
}