/** Slot 状态(REQ §6.1) */ export type SlotStatus = | "empty" | "reserved" | "downloading" | "verifying" | "satisfied" | "attention" | "upgrade_pending"; export interface SlotState { status: SlotStatus; /** 是否已有可用的当前资产(升级流程中旧资产仍在,部分覆盖的剧集也可能有资产) */ hasAsset: boolean; } export type SlotEvent = | { type: "reserve" } | { type: "release" } | { type: "download_started" } | { type: "download_completed" } | { type: "download_failed"; reason?: string } | { type: "verify_passed"; complete: boolean } | { type: "verify_failed"; reason?: string } | { type: "upgrade_found" } | { type: "upgrade_cancelled" } | { type: "attention_resolved"; hasAsset: boolean } | { type: "asset_missing_confirmed" }; export class SlotTransitionError extends Error { override name = "SlotTransitionError"; constructor( readonly from: SlotStatus, readonly event: SlotEvent["type"], ) { super(`非法的 Slot 状态迁移: ${from} --${event}-->`); } } /** * Slot 状态机(纯函数)。非法迁移抛 SlotTransitionError。 * 升级流程:satisfied → upgrade_pending → reserved → downloading → verifying → satisfied; * 新文件验证通过前旧资产一直保留(hasAsset=true)。 * 剧集部分覆盖:verify_passed(complete=false) 回到 empty 但 hasAsset=true,继续追更。 */ export function transition(state: SlotState, event: SlotEvent): SlotState { const { status, hasAsset } = state; const bad = (): never => { throw new SlotTransitionError(status, event.type); }; switch (event.type) { case "reserve": if (status === "empty" || status === "attention" || status === "upgrade_pending") return { status: "reserved", hasAsset }; return bad(); case "release": if (status === "reserved") return { status: hasAsset ? "satisfied" : "empty", hasAsset }; return bad(); case "download_started": if (status === "reserved") return { status: "downloading", hasAsset }; return bad(); case "download_completed": if (status === "downloading") return { status: "verifying", hasAsset }; return bad(); case "download_failed": if (status === "downloading" || status === "reserved") return { status: "attention", hasAsset }; return bad(); case "verify_passed": if (status === "verifying") return { status: event.complete ? "satisfied" : "empty", hasAsset: true }; return bad(); case "verify_failed": if (status === "verifying") return { status: "attention", hasAsset }; return bad(); case "upgrade_found": if (status === "satisfied") return { status: "upgrade_pending", hasAsset }; return bad(); case "upgrade_cancelled": if (status === "upgrade_pending") return { status: "satisfied", hasAsset }; return bad(); case "attention_resolved": if (status === "attention") return { status: event.hasAsset ? "satisfied" : "empty", hasAsset: event.hasAsset }; return bad(); case "asset_missing_confirmed": // 只接受"确认缺失"(对账多次观察后);单次观察失败不得触发(失败 ≠ 不存在) if (status === "satisfied" || status === "upgrade_pending") return { status: "attention", hasAsset: false }; return bad(); } } export function canTransition(state: SlotState, event: SlotEvent): boolean { try { transition(state, event); return true; } catch { return false; } }