/// 本地上传任务状态机(P3 计划 Task 5):纯迁移表,无 drift 依赖。 /// /// 状态取值与合法迁移对齐技术实施方案 §5.5 与 P3 计划 Task 5/6: /// - 准备:preparing → queued(入队)/ preparationFailed(准备失败,可回 /// preparing 重试); /// - 传输主干:queued → uploading → uploadedConfirming → completed; /// - 网络等待与重试:waitingNetwork / retryableFailed 可回 queued(重新 /// 出队)或直接 uploading(续传/重试); /// - 终态:completed / cancelled,不接受任何后续迁移(completed 的源文件 /// 仅按保留策略清理,见 task_store.dart 的 cleanupCandidates 硬守护)。 /// /// 取消语义(方案 §5.5):未开始(preparing/preparationFailed/queued/ /// waitingNetwork/retryableFailed)→ cancelled 为确定语义;uploading 进行中 /// → cancelled 为"尽力"(写入标记,实际中断由引擎在下一个检查点兑现)。 library; /// 上传任务状态(drift 表 `upload_tasks.state` 的取值)。 /// /// 注意与服务端会话状态(UploadSessionState:pending/confirmed/...)区分: /// 本枚举是客户端本地任务生命周期,二者在 uploadedConfirming 阶段交汇。 enum UploadTaskState { /// 准备中:选图/拍摄后复制源文件到受管目录、空间检查(Task 6)。 preparing('preparing'), /// 准备失败(独立状态,可重试回 preparing):复制失败/磁盘不足等。 preparationFailed('preparation_failed'), /// 已入队,等待传输引擎出队。 queued('queued'), /// 传输中:POST /uploads → presign PUT → complete 的任一环节。 uploading('uploading'), /// 字节已传完,等待/重试完成核验(POST complete)。 uploadedConfirming('uploaded_confirming'), /// 终态:核验通过,服务端已受理(cleanup 仅针对此态与 cancelled)。 completed('completed'), /// 网络不可达,指数退避等待恢复(nextRetryAt 记录下次尝试时间)。 waitingNetwork('waiting_network'), /// 可重试失败(服务端 5xx/超时等),按退避策略回 queued 重试。 retryableFailed('retryable_failed'), /// 终态:用户取消(uploading 态下为尽力取消)。 cancelled('cancelled'); const UploadTaskState(this.wire); /// 落库(drift state 列)取值。 final String wire; /// 解析落库取值;未知值 fail-fast(与 models.dart 的枚举解析约定一致)。 static UploadTaskState fromWire(String value) => values.firstWhere( (v) => v.wire == value, orElse: () => throw FormatException('未知 UploadTaskState: $value'), ); /// 是否终态(不接受任何后续迁移)。 bool get isTerminal => this == completed || this == cancelled; } /// 合法迁移表(键=当前状态,值=可直接迁入的目标集合)。 const Map> kUploadTaskTransitions = { UploadTaskState.preparing: { UploadTaskState.queued, UploadTaskState.preparationFailed, UploadTaskState.cancelled, }, UploadTaskState.preparationFailed: { UploadTaskState.preparing, UploadTaskState.cancelled, }, UploadTaskState.queued: { UploadTaskState.uploading, UploadTaskState.waitingNetwork, UploadTaskState.cancelled, }, UploadTaskState.uploading: { UploadTaskState.uploadedConfirming, UploadTaskState.waitingNetwork, UploadTaskState.retryableFailed, // 尽力取消:见库级文档(进行中取消由引擎在检查点兑现)。 UploadTaskState.cancelled, }, UploadTaskState.uploadedConfirming: { UploadTaskState.completed, UploadTaskState.retryableFailed, UploadTaskState.waitingNetwork, }, UploadTaskState.waitingNetwork: { UploadTaskState.queued, // 网络恢复直接续传(重试)。 UploadTaskState.uploading, UploadTaskState.cancelled, }, UploadTaskState.retryableFailed: { UploadTaskState.queued, UploadTaskState.cancelled, }, // 终态:无出边(completed 的清理走 cleanedAt 标记,不是状态迁移)。 UploadTaskState.completed: {}, UploadTaskState.cancelled: {}, }; /// `from → to` 是否合法迁移(自迁移一律非法,状态未变无需写库)。 bool canTransition(UploadTaskState from, UploadTaskState to) => kUploadTaskTransitions[from]!.contains(to); /// 断言 `from → to` 合法,否则抛 [StateError](携带合法目标集,便于排障)。 void assertTransition(UploadTaskState from, UploadTaskState to) { if (!canTransition(from, to)) { final legal = kUploadTaskTransitions[from]!; final hint = legal.isEmpty ? '(终态,无合法后继)' : ',合法目标:$legal'; throw StateError('非法上传任务状态迁移:$from → $to$hint'); } }