Files
photo-live/apps/mobile/ios/Runner/UploadEventBus.swift
T

174 lines
6.1 KiB
Swift
Raw Normal View History

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]] ?? []
}
}