/** * 页面三通道加载编排(任务 119):dashboard / history / progress 请求路径分离。 * * 约定页面初载只调三类轻量接口——统计(stats)、历史摘要(summary)、轻量进度 * (light);明细(detail)与结果(result)仅在用户展开时按需加载。本模块把 * 该约定固化为可测试的编排:注入的 loader 各自独立、互不调用,首开仅并发发起 * 三类轻量加载,展开时再按需取明细/结果,并返回请求日志供断言。 * * 页面接入示例: * const page = createPageSeparatedLoads({ * statsLoader: () => getSimilarAsinDashboard(), * summaryLoader: () => getSimilarAsinHistory(), * lightLoader: (ids) => getModuleProgressLight('similarAsin', ids), * detailLoader: (taskId) => getSimilarAsinTaskProgressBatch([taskId]), * resultLoader: (taskId) => getSimilarAsinResult(taskId), * }) * onMounted(() => page.initialLoad()) */ export type PageLoadPhase = 'stats' | 'summary' | 'light' | 'detail' | 'result' export interface PageRequestLogEntry { phase: PageLoadPhase taskIds: number[] at: number } export interface PageSeparatedLoadsDeps { statsLoader: () => Promise summaryLoader: () => Promise lightLoader: (taskIds: number[]) => Promise detailLoader: (taskId: number) => Promise resultLoader: (taskId: number) => Promise } export interface PageSeparatedLoadsOptions { /** 初载并发上限:三类轻量请求 + 每次按需展开 1 个明细/结果 */ maxConcurrentRequests?: number } export interface PageSeparatedLoadsApi { /** dashboard 通道:只调统计 */ loadDashboard: () => Promise /** history 通道:只调摘要 */ loadHistory: () => Promise /** progress 通道:只调轻量进度 */ loadProgress: () => Promise /** 初载:并发发起 stats/summary/light 三类轻量请求,不加载明细 */ initialLoad: () => Promise /** 展开明细:仅当轻量条目已就绪时按需加载该任务明细 */ openDetail: (taskId: number) => Promise /** 取结果:按需加载指定任务结果文件 */ openResult: (taskId: number) => Promise requestLog: () => PageRequestLogEntry[] } export function createPageSeparatedLoads( deps: PageSeparatedLoadsDeps, options: PageSeparatedLoadsOptions = {}, ): PageSeparatedLoadsApi { const maxConcurrentRequests = options.maxConcurrentRequests ?? 3 if (maxConcurrentRequests < 1) { throw new Error('maxConcurrentRequests 必须 ≥ 1') } if (typeof deps?.statsLoader !== 'function' || typeof deps?.summaryLoader !== 'function') { throw new Error('statsLoader / summaryLoader 必须是函数') } if (typeof deps?.lightLoader !== 'function' || typeof deps?.detailLoader !== 'function') { throw new Error('lightLoader / detailLoader 必须是函数') } if (typeof deps?.resultLoader !== 'function') { throw new Error('resultLoader 必须是函数') } let inflight = 0 const releaseWaiters: Array<() => void> = [] const log: PageRequestLogEntry[] = [] const loadedLight = new Set() async function guarded(task: () => Promise) { while (inflight >= maxConcurrentRequests) { await new Promise((resolve) => { releaseWaiters.push(resolve) }) } inflight += 1 try { await task() } finally { inflight -= 1 const next = releaseWaiters.shift() if (next) next() } } function record(phase: PageLoadPhase, taskIds: number[] = []) { log.push({ phase, taskIds: [...taskIds], at: log.length }) } return { async loadDashboard() { await guarded(async () => { record('stats') await deps.statsLoader() }) }, async loadHistory() { await guarded(async () => { record('summary') await deps.summaryLoader() }) }, async loadProgress() { await guarded(async () => { const ids = loadedLight.size ? [...loadedLight] : [] record('light', ids) const result = await deps.lightLoader(ids) for (const item of extractTaskIds(result) ?? []) { loadedLight.add(item) } }) }, async initialLoad() { await Promise.all([ this.loadDashboard(), this.loadHistory(), this.loadProgress(), ]) }, async openDetail(taskId: number) { if (!loadedLight.has(taskId)) return undefined let detail: unknown = undefined await guarded(async () => { record('detail', [taskId]) detail = await deps.detailLoader(taskId) }) return detail }, async openResult(taskId: number) { if (!loadedLight.has(taskId)) return undefined let result: unknown = undefined await guarded(async () => { record('result', [taskId]) result = await deps.resultLoader(taskId) }) return result }, requestLog: () => log, } } /** 从 light 响应中提取已就绪 taskId 列表(兼容 items[{taskId}] 结构,null 安全) */ function extractTaskIds(result: unknown): number[] | undefined { const items = (result as { items?: Array<{ taskId?: number }> } | null)?.items if (!Array.isArray(items)) return undefined const ids = items .map((item) => item?.taskId) .filter((id): id is number => typeof id === 'number' && id > 0) return ids.length ? ids : undefined }