Фикс: детерминированный роутинг PacketSearch на iOS без подмешивания фоновых результатов
This commit is contained in:
@@ -66,6 +66,43 @@ final class SearchParityTests: XCTestCase {
|
||||
XCTAssertEqual(merged[0].title, "Server Name")
|
||||
XCTAssertEqual(merged[1].publicKey, localOnlyKey)
|
||||
}
|
||||
|
||||
func testActiveUiSearchIgnoresBackgroundUserInfoPackets() async {
|
||||
let bus = ChannelSearchBus()
|
||||
let uiChannel = SearchPacketChannel.ui(UUID())
|
||||
let uiDispatcher = ChannelBusSearchDispatcher(bus: bus, channel: uiChannel)
|
||||
let userInfoDispatcher = ChannelBusSearchDispatcher(bus: bus, channel: .userInfo)
|
||||
let vm = SearchViewModel(searchDispatcher: uiDispatcher)
|
||||
|
||||
var backgroundRequest = PacketSearch()
|
||||
backgroundRequest.search = "02aa"
|
||||
userInfoDispatcher.sendSearchPacket(backgroundRequest)
|
||||
|
||||
vm.setSearchQuery("alice")
|
||||
try? await Task.sleep(for: .milliseconds(1200))
|
||||
XCTAssertEqual(uiDispatcher.sentQueries, ["alice"])
|
||||
|
||||
var backgroundPacket = PacketSearch()
|
||||
backgroundPacket.users = [
|
||||
SearchUser(username: "random_user", title: "Random User", publicKey: "02aa", verified: 0, online: 0),
|
||||
]
|
||||
userInfoDispatcher.emit(backgroundPacket)
|
||||
try? await Task.sleep(for: .milliseconds(80))
|
||||
|
||||
XCTAssertTrue(vm.searchResults.isEmpty)
|
||||
XCTAssertTrue(vm.isSearching)
|
||||
|
||||
var uiPacket = PacketSearch()
|
||||
uiPacket.users = [
|
||||
SearchUser(username: "alice_user", title: "Alice", publicKey: "02bb", verified: 1, online: 0),
|
||||
]
|
||||
uiDispatcher.emit(uiPacket)
|
||||
try? await Task.sleep(for: .milliseconds(80))
|
||||
|
||||
XCTAssertEqual(vm.searchResults.count, 1)
|
||||
XCTAssertEqual(vm.searchResults.first?.username, "alice_user")
|
||||
XCTAssertFalse(vm.isSearching)
|
||||
}
|
||||
}
|
||||
|
||||
private extension SearchParityTests {
|
||||
@@ -124,3 +161,74 @@ private final class MockSearchDispatcher: SearchResultDispatching {
|
||||
handlers.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class ChannelBusSearchDispatcher: SearchResultDispatching {
|
||||
let connectionState: ConnectionState = .authenticated
|
||||
let privateHash: String? = "mock-private-hash"
|
||||
private(set) var sentQueries: [String] = []
|
||||
|
||||
private let bus: ChannelSearchBus
|
||||
private let channel: SearchPacketChannel
|
||||
|
||||
init(bus: ChannelSearchBus, channel: SearchPacketChannel) {
|
||||
self.bus = bus
|
||||
self.channel = channel
|
||||
}
|
||||
|
||||
func sendSearchPacket(_ packet: PacketSearch) {
|
||||
sentQueries.append(packet.search)
|
||||
bus.enqueue(channel: channel)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addSearchResultHandler(_ handler: @escaping (PacketSearch) -> Void) -> UUID {
|
||||
bus.addHandler(channel: channel, handler)
|
||||
}
|
||||
|
||||
func removeSearchResultHandler(_ id: UUID) {
|
||||
bus.removeHandler(id)
|
||||
}
|
||||
|
||||
func emit(_ packet: PacketSearch) {
|
||||
bus.dispatch(packet)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChannelSearchBus {
|
||||
private struct HandlerEntry {
|
||||
let channel: SearchPacketChannel
|
||||
let handler: (PacketSearch) -> Void
|
||||
}
|
||||
|
||||
private var handlers: [UUID: HandlerEntry] = [:]
|
||||
private var pendingChannels: [SearchPacketChannel] = []
|
||||
|
||||
@discardableResult
|
||||
func addHandler(
|
||||
channel: SearchPacketChannel,
|
||||
_ handler: @escaping (PacketSearch) -> Void
|
||||
) -> UUID {
|
||||
let id = UUID()
|
||||
handlers[id] = HandlerEntry(channel: channel, handler: handler)
|
||||
return id
|
||||
}
|
||||
|
||||
func removeHandler(_ id: UUID) {
|
||||
handlers.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
func enqueue(channel: SearchPacketChannel) {
|
||||
pendingChannels.append(channel)
|
||||
}
|
||||
|
||||
func dispatch(_ packet: PacketSearch) {
|
||||
let routed = pendingChannels.isEmpty ? SearchPacketChannel.unscoped : pendingChannels.removeFirst()
|
||||
let callbacks = handlers.values
|
||||
.filter { $0.channel == .unscoped || $0.channel == routed }
|
||||
.map(\.handler)
|
||||
for callback in callbacks {
|
||||
callback(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user