task-93: Similar ASIN、店铺抓取和采集数据页面 E2E 核心路径执行器
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 页面 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?.()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user