import Foundation /// 上传事件总线(session delegate → EventChannel,P3 计划 Task 8)。 /// /// 与 Android 侧 `UploadEventBus.kt` 同语义: /// - 事件带单调 seq;**终态**(completed / needsRenew / failed)在 Dart /// ack 前缓存(上限 [maxBuffered],超出丢最老),EventChannel 重新 /// listen 时重放——App 切后台恢复 / 引擎重建后不丢完成信号; /// - 进度事件瞬态,不缓存不重放; /// - 任务重新注册(renew 换 URL)或取消时 [purgeTask] 作废其缓存终态。 /// /// **进程死亡语义(iOS 增强并落盘)**:iOS 后台会话完成后系统可能在后台 /// 拉起 App(Dart 引擎未启动、无人监听事件)投递回调——终态事件除内存 /// 缓存外同步持久化到 Application Support plist(seq 计数器随行),下次 /// 进程内 EventChannel onListen 时一并重放(技术方案 §6.2「传输完成后 /// 持久记录原生回调」)。Android 侧等价场景由 WorkManager 自持任务、 /// 进程重启后 worker 重跑再发事件,无需落盘。 /// /// 事件负载为 [String: Any] 字典:null 值(etag/status 等)以 NSNull 表达 /// ——Flutter 标准编解码两侧均映射为 Dart null,与 Kotlin 显式 null 等价。 /// /// 线程模型:emit 可来自 session delegate 的后台队列;sink 由插件层包装 /// 为主线程投递(EventChannel 契约)。投递在锁内进行——sink 实现为非 /// 阻塞 DispatchQueue.main.async,锁内投递保证 seq 顺序。 final class UploadEventBus { static let shared = UploadEventBus() /// 缓存终态上限(防无限增长;超量丢最老——极端下 Dart 用 queryTask 校准)。 static let maxBuffered = 200 private let lock = NSLock() private var sink: (([String: Any]) -> Void)? private var seqGen = 0 private var unacked: [[String: Any]] = [] private let fileURL: URL init(directory: URL? = nil) { let dir = directory ?? FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] .appendingPathComponent("PhotoLiveUploads", isDirectory: true) try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) fileURL = dir.appendingPathComponent("upload-unacked-events.plist") loadPersisted() } // ── 事件出口(镜像 Kotlin UploadEventBus) ─────────────────────── func emitProgress(taskId: String, sent: Int64, total: Int64) { emit( [ "event": UploadBridge.eventProgress, "taskId": taskId, "sent": NSNumber(value: sent), "total": NSNumber(value: total), ], terminal: false ) } func emitCompleted(taskId: String, etag: String?) { emit( [ "event": UploadBridge.eventCompleted, "taskId": taskId, "etag": etag ?? NSNull(), ], terminal: true ) } func emitNeedsRenew(taskId: String) { emit( [ "event": UploadBridge.eventNeedsRenew, "taskId": taskId, ], terminal: true ) } func emitFailed(taskId: String, code: String, status: Int? = nil) { emit( [ "event": UploadBridge.eventFailed, "taskId": taskId, "code": code, "status": status.map { NSNumber(value: $0) } ?? NSNull(), ], terminal: true ) } private func emit(_ event: [String: Any], terminal: Bool) { lock.lock() seqGen += 1 var stamped = event stamped["seq"] = NSNumber(value: seqGen) if terminal { unacked.append(stamped) while unacked.count > UploadEventBus.maxBuffered { unacked.removeFirst() } persistLocked() } if let sink = sink { sink(stamped) } lock.unlock() } /// EventChannel onListen:接管 sink 并重放缓存的终态事件。 func attach(_ sink: @escaping ([String: Any]) -> Void) { lock.lock() self.sink = sink for event in unacked { sink(event) } lock.unlock() } /// EventChannel onCancel。 func detach() { lock.lock() sink = nil lock.unlock() } /// Dart ack:丢弃 seq ≤ 给定值的缓存终态。 func ack(seq: Int) { lock.lock() unacked.removeAll { (($0["seq"] as? NSNumber)?.intValue ?? 0) <= seq } persistLocked() lock.unlock() } /// 任务重新注册/取消:其缓存终态作废。 func purgeTask(taskId: String) { lock.lock() unacked.removeAll { $0["taskId"] as? String == taskId } persistLocked() lock.unlock() } // ── 终态落盘 / 恢复(进程死亡重放语义) ────────────────────────── /// plist 不支持 null 值——持久化前剔除 NSNull(Dart 侧以缺失等同 null)。 private func persistLocked() { let storable = unacked.map { event -> [String: Any] in var out: [String: Any] = [:] for (key, value) in event { if let number = value as? NSNumber { out[key] = number } else if let string = value as? String { out[key] = string } // NSNull(null 值)跳过。 } return out } let raw: [String: Any] = ["seq": NSNumber(value: seqGen), "unacked": storable] (raw as NSDictionary).write(to: fileURL, atomically: true) } private func loadPersisted() { guard let raw = NSDictionary(contentsOf: fileURL) as? [String: Any] else { return } seqGen = (raw["seq"] as? NSNumber)?.intValue ?? 0 unacked = raw["unacked"] as? [[String: Any]] ?? [] } }