iOS 与传输修复:取消即时落终态 + 行操作统一 + 记录按用户分桶 + 发送端进度/完成对齐接收端
- 取消传输无效:web cancelTransfer 拆除本端 p2p/relay 后立即 completeTransfer(sessionId, "CANCELLED") 落终态,不再仅依赖服务端 transfer:state CANCELLED 回显——回显延迟、或因设备名变更致路由不中而未送达时,旧实现使记录永停“进行中”,即用户所见“取消无效”。completeTransfer 幂等,随后回显安全重入。 - 进行中/已完成传输行操作统一:进行中行新增滑动“取消”+ 长按菜单“取消/切中继”,与历史行滑动+长按“删除”对称。取消即时无确认、随 transferDone 移入 history,析构滑动动画与现实一致不抖。详情页取消/切中继入口保留。 - 传输历史/消息持久化:记录按登录用户 userId 分桶持久化(EngineController recordKey/activateRecords/persist*,含旧无桶文件一次性回退)。reset() 改为只清内存、不再 RecordsStore.clear 磁盘——会话失效(authExpired/被吊销)属凭证层事件,不应连用户数据一并销毁;同一账户重登经 activateRecords 即恢复,换账号各读各桶天然防跨账号泄露。CDropApp 在 onAppear 与 onChange(session.user.id) 激活记录。 - 发送端进度/完成对齐接收端:接收端经数据通道节流(200ms)+finalize 即时回传已收字节(ack 帧);发送端进度改用 ackedBytes(单调取大),取代仅反映本地 SCTP 缓冲抽干的 senderDeliveredBytes——避免 Windows→iOS 等慢接收端组合下发送端进度虚高领先、完成早十数秒(4MB)。streamFile 抽干 buffer 后再等 ack 追平总量才宣告完成;ackedBytes==0(旧版本接收端)走旧“抽干即完成”行为、30s 超时兜底、接收端权威 /done 经 SSE 独立完成。
This commit is contained in:
@@ -159,16 +159,65 @@ final class EngineController: NSObject
|
||||
private static let historyCap = 50
|
||||
private static let messagesCap = 200
|
||||
|
||||
// 当前记录归属用户:传输历史 / 消息 / 设备快照按登录用户分桶持久化(文件名带 userId,见 recordKey)。
|
||||
// 这样会话失效(authExpired / 被吊销)只清内存、不毁盘,同一用户重登即恢复;换账号登录各读各的桶,
|
||||
// 天然防跨账号泄露。空串=尚未激活会话(事件先于激活到达时跳过落盘,见 persist*)。
|
||||
private var currentRecordUser = ""
|
||||
|
||||
override init()
|
||||
{
|
||||
super.init()
|
||||
PushRegistry.shared.engine = self
|
||||
// 加载上次持久化的传输历史 / 消息记录,修「重启清空」。新事件经引擎桥增量追加。
|
||||
history = RecordsStore.load([TransferItem].self, "history") ?? []
|
||||
messages = RecordsStore.load([MessageItem].self, "messages") ?? []
|
||||
// 加载上次的设备 presence 快照:冷启 / 墓碑恢复时立即显示设备列表(而非空列表等 SSE 重连),
|
||||
// 缓解「长时间重连」的观感;SSE 一连上即被新鲜 presence 整组替换、自校正在线状态。
|
||||
devices = RecordsStore.load([DeviceItem].self, "devices") ?? []
|
||||
// 记录加载推迟到 activateRecords(会话激活时,userId 已知才知道读哪个桶)。init 阶段
|
||||
// auth 尚未注入、无从分桶,故此处不再加载——activateRecords 由 AppRoot 在登录 / 冷启已有
|
||||
// 会话时即时调用,加载延迟可忽略。
|
||||
}
|
||||
|
||||
// 会话激活:登录完成 / 冷启已有会话时按 userId 加载该用户的持久记录到内存。reset() 只清内存不动
|
||||
// 磁盘,故隔夜被 authExpired 清空视图后,同一用户重登经此重新读盘即恢复历史 / 消息。幂等:重复激活
|
||||
// 仅重读(内存与磁盘一致,无副作用)。legacy 回退:旧版本无分桶的 "history"/"messages"/"devices"
|
||||
// 文件首次激活时一并采纳(随后落盘即写入分桶键),避免升级丢失既有记录。
|
||||
func activateRecords(userId: String)
|
||||
{
|
||||
guard !userId.isEmpty else { return }
|
||||
currentRecordUser = userId
|
||||
history = RecordsStore.load([TransferItem].self, recordKey("history"))
|
||||
?? RecordsStore.load([TransferItem].self, "history") ?? []
|
||||
messages = RecordsStore.load([MessageItem].self, recordKey("messages"))
|
||||
?? RecordsStore.load([MessageItem].self, "messages") ?? []
|
||||
devices = RecordsStore.load([DeviceItem].self, recordKey("devices"))
|
||||
?? RecordsStore.load([DeviceItem].self, "devices") ?? []
|
||||
}
|
||||
|
||||
// 分桶文件键:userId(broker subject,通常即 UUID 式安全字符)拼到记录名前。保险起见把非
|
||||
// 字母数字 / - / _ 的字符替换为 _,确保落地为合法文件名。
|
||||
private func recordKey(_ name: String) -> String
|
||||
{
|
||||
let safe = String(currentRecordUser.map
|
||||
{ ch in
|
||||
(ch.isLetter || ch.isNumber || ch == "-" || ch == "_") ? ch : "_"
|
||||
})
|
||||
return "\(safe)-\(name)"
|
||||
}
|
||||
|
||||
// 落盘助手:仅在已激活会话(currentRecordUser 非空)时写当前用户的桶。未激活时跳过,避免把
|
||||
// 增量事件写进 "-name" 的无主桶。
|
||||
private func persistHistory()
|
||||
{
|
||||
guard !currentRecordUser.isEmpty else { return }
|
||||
RecordsStore.save(history, recordKey("history"))
|
||||
}
|
||||
|
||||
private func persistMessages()
|
||||
{
|
||||
guard !currentRecordUser.isEmpty else { return }
|
||||
RecordsStore.save(messages, recordKey("messages"))
|
||||
}
|
||||
|
||||
private func persistDevices()
|
||||
{
|
||||
guard !currentRecordUser.isEmpty else { return }
|
||||
RecordsStore.save(devices, recordKey("devices"))
|
||||
}
|
||||
|
||||
// makeWebView:构建离屏 WebView——注入 __CDROP_BOOT__(device_type:"ios")、注册消息
|
||||
@@ -232,10 +281,11 @@ final class EngineController: NSObject
|
||||
seenIncomingTransfers = []
|
||||
activeTab = "transfer"
|
||||
pendingLoginRequest = nil
|
||||
// 登出清空持久记录,不跨账号残留(重启保活仅针对同一登录态)。
|
||||
RecordsStore.clear("history")
|
||||
RecordsStore.clear("messages")
|
||||
RecordsStore.clear("devices")
|
||||
// 不再清磁盘记录:传输历史 / 消息是用户数据,会话失效(authExpired / 被吊销)属凭证层事件,
|
||||
// 不应连用户数据一起销毁——否则隔夜令牌过期一次就抹掉全部历史(#3 根因)。这里只断开会话归属
|
||||
// (置空 currentRecordUser),磁盘按 userId 分桶留存:同一用户重登经 activateRecords 即恢复,
|
||||
// 换账号登录各读各的桶不会串。要彻底清除走设置页登出后用记录页 / 消息页的「清空」(用户主动)。
|
||||
currentRecordUser = ""
|
||||
status = t("ios.engine.disconnected")
|
||||
deviceName = ""
|
||||
PushRegistry.shared.reset()
|
||||
@@ -251,26 +301,26 @@ final class EngineController: NSObject
|
||||
func deleteTransferRecord(_ sessionId: String)
|
||||
{
|
||||
history.removeAll { $0.sessionId == sessionId }
|
||||
RecordsStore.save(history, "history")
|
||||
persistHistory()
|
||||
}
|
||||
|
||||
func clearHistory()
|
||||
{
|
||||
history.removeAll()
|
||||
RecordsStore.clear("history")
|
||||
RecordsStore.clear(recordKey("history"))
|
||||
}
|
||||
|
||||
// 删除一条消息(H)/ 清空全部(持久化同步)。
|
||||
func deleteMessage(_ id: String)
|
||||
{
|
||||
messages.removeAll { $0.id == id }
|
||||
RecordsStore.save(messages, "messages")
|
||||
persistMessages()
|
||||
}
|
||||
|
||||
func clearMessages()
|
||||
{
|
||||
messages.removeAll()
|
||||
RecordsStore.clear("messages")
|
||||
RecordsStore.clear(recordKey("messages"))
|
||||
}
|
||||
|
||||
// ---- 瞬时通知(#2) ----
|
||||
@@ -655,7 +705,7 @@ extension EngineController: WKScriptMessageHandler
|
||||
if let p = payload as? [String: Any], let raw = p["devices"] as? [Any]
|
||||
{
|
||||
devices = raw.compactMap { ($0 as? [String: Any]).flatMap(Self.parseDevice) }
|
||||
RecordsStore.save(devices, "devices") // 持久化 presence 快照,供下次冷启即时显示
|
||||
persistDevices() // 持久化 presence 快照(按用户分桶),供下次冷启即时显示
|
||||
}
|
||||
case "transfers":
|
||||
if let p = payload as? [String: Any], let raw = p["active"] as? [Any]
|
||||
@@ -678,7 +728,7 @@ extension EngineController: WKScriptMessageHandler
|
||||
{
|
||||
messages.removeLast(messages.count - Self.messagesCap)
|
||||
}
|
||||
RecordsStore.save(messages, "messages")
|
||||
persistMessages()
|
||||
// 新到的对端消息:不在消息页时计未读红点(#5)。本机发出的乐观回显(outgoing)不计。
|
||||
if isNew, item.direction == "incoming", activeTab != "messages" { unreadMessages += 1 }
|
||||
}
|
||||
@@ -692,7 +742,7 @@ extension EngineController: WKScriptMessageHandler
|
||||
history.insert(item, at: 0)
|
||||
if history.count > Self.historyCap { history.removeLast(history.count - Self.historyCap) }
|
||||
syncBackgroundTask()
|
||||
RecordsStore.save(history, "history") // 持久化,跨重启存活
|
||||
persistHistory() // 持久化(按用户分桶),跨重启存活
|
||||
}
|
||||
case "sendStarted":
|
||||
// 随后的 transfers 事件会带出该活跃项,这里无需额外处理。
|
||||
|
||||
Reference in New Issue
Block a user