Фикс: детерминированный роутинг PacketSearch на iOS без подмешивания фоновых результатов

This commit is contained in:
2026-03-28 21:07:05 +05:00
parent 5af28b68a8
commit e49d224e6a
13 changed files with 613 additions and 62 deletions

View File

@@ -81,10 +81,9 @@ final class ProtocolManager: @unchecked Sendable {
syncBatchLock.unlock()
return val
}
private let searchHandlersLock = NSLock()
private let resultHandlersLock = NSLock()
private let packetQueueLock = NSLock()
private var searchResultHandlers: [UUID: (PacketSearch) -> Void] = [:]
private let searchRouter = SearchPacketRouter()
private var resultHandlers: [UUID: (PacketResult) -> Void] = [:]
// Saved credentials for auto-reconnect
@@ -145,6 +144,7 @@ final class ProtocolManager: @unchecked Sendable {
handshakeComplete = false
clearPacketQueue()
clearResultHandlers()
searchRouter.resetPending()
syncBatchLock.lock()
_syncBatchActive = false
syncBatchLock.unlock()
@@ -184,6 +184,7 @@ final class ProtocolManager: @unchecked Sendable {
pingTimeoutTask = nil
handshakeComplete = false
heartbeatTask?.cancel()
searchRouter.resetPending()
connectionState = .connecting
client.forceReconnect()
}
@@ -208,6 +209,7 @@ final class ProtocolManager: @unchecked Sendable {
Self.logger.info("⚡ Fast reconnect — state=\(self.connectionState.rawValue)")
handshakeComplete = false
heartbeatTask?.cancel()
searchRouter.resetPending()
connectionState = .connecting
client.forceReconnect()
}
@@ -254,6 +256,7 @@ final class ProtocolManager: @unchecked Sendable {
pingTimeoutTask = nil
handshakeComplete = false
heartbeatTask?.cancel()
searchRouter.resetPending()
Task { @MainActor in
// Guard: only downgrade to .connecting if reconnect hasn't already progressed.
// forceReconnect() is called synchronously below if it completes fast,
@@ -268,6 +271,14 @@ final class ProtocolManager: @unchecked Sendable {
// MARK: - Sending
func sendSearchPacket(
_ packet: PacketSearch,
channel: SearchPacketChannel = .unscoped
) {
searchRouter.enqueueOutgoingRequest(channel: channel)
sendPacket(packet)
}
func sendPacket(_ packet: any Packet) {
PerformanceLogger.shared.track("protocol.sendPacket")
let id = String(type(of: packet).packetId, radix: 16)
@@ -285,18 +296,15 @@ final class ProtocolManager: @unchecked Sendable {
// MARK: - Search Handlers (Android-like wait/unwait)
@discardableResult
func addSearchResultHandler(_ handler: @escaping (PacketSearch) -> Void) -> UUID {
let id = UUID()
searchHandlersLock.lock()
searchResultHandlers[id] = handler
searchHandlersLock.unlock()
return id
func addSearchResultHandler(
channel: SearchPacketChannel = .unscoped,
_ handler: @escaping (PacketSearch) -> Void
) -> UUID {
searchRouter.addHandler(channel: channel, handler)
}
func removeSearchResultHandler(_ id: UUID) {
searchHandlersLock.lock()
searchResultHandlers.removeValue(forKey: id)
searchHandlersLock.unlock()
searchRouter.removeHandler(id)
}
// MARK: - Result Handlers (Android parity: waitPacket(0x02))
@@ -347,6 +355,7 @@ final class ProtocolManager: @unchecked Sendable {
pingVerificationInProgress = false
pingTimeoutTask?.cancel()
pingTimeoutTask = nil
self.searchRouter.resetPending()
Task { @MainActor in
self.connectionState = .disconnected
@@ -465,9 +474,11 @@ final class ProtocolManager: @unchecked Sendable {
}
case 0x03:
if let p = packet as? PacketSearch {
Self.logger.debug("📥 Search result: \(p.users.count) users, callback=\(self.onSearchResult != nil)")
let routedChannel = routeIncomingSearchPacket(p)
Self.logger.debug(
"📥 Search result: \(p.users.count) users, callback=\(self.onSearchResult != nil), routed=\(String(describing: routedChannel))"
)
onSearchResult?(p)
notifySearchResultHandlers(p)
}
case 0x05:
if let p = packet as? PacketOnlineState {
@@ -557,14 +568,8 @@ final class ProtocolManager: @unchecked Sendable {
}
}
private func notifySearchResultHandlers(_ packet: PacketSearch) {
searchHandlersLock.lock()
let handlers = Array(searchResultHandlers.values)
searchHandlersLock.unlock()
for handler in handlers {
handler(packet)
}
private func routeIncomingSearchPacket(_ packet: PacketSearch) -> SearchPacketChannel {
searchRouter.dispatchIncomingResponse(packet)
}
private func notifyResultHandlers(_ packet: PacketResult) {
@@ -609,6 +614,7 @@ final class ProtocolManager: @unchecked Sendable {
case .needDeviceVerification:
handshakeComplete = false
Self.logger.info("Server requires device verification — approve this device from your other Rosetta app")
searchRouter.resetPending()
Task { @MainActor in
self.connectionState = .deviceVerificationRequired