174 lines
6.1 KiB
Swift
174 lines
6.1 KiB
Swift
|
|
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]] ?? []
|
|||
|
|
}
|
|||
|
|
}
|