100 lines
3.6 KiB
TypeScript
100 lines
3.6 KiB
TypeScript
/**
|
||
* 页面 E2E 核心路径执行器(Task 93)。
|
||
*
|
||
* 把 Similar ASIN / 店铺抓取 / 采集数据三个页面的 E2E 核心路径收敛为
|
||
* 可注入依赖的确定性流程:解析文件(parse)→ 建任务(createTask)→
|
||
* 轮询到终态(poll)→ 刷新历史(refreshHistory),各步由 deps 提供实现,
|
||
* 页面接入时传真实接口即可复用同一套语义。
|
||
*
|
||
* 流程语义:
|
||
* - parse 返回 0 表示无可处理数据:安全跳过后续步骤,不创建无效资源;
|
||
* - poll 返回非终态时按最大次数轮询,超过 maxPollAttempts 降级返回当前
|
||
* 状态(不抛错、不无限轮询、不刷新历史);
|
||
* - 任一步抛错:执行立即终止,cleanup 必被调用(失败与成功路径都执行),
|
||
* 调用方可重新执行同一 deps 恢复;
|
||
* - 输入校验 fail-fast:deps 缺函数、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?.()
|
||
}
|
||
}
|