From 846aa695c26cb692d987477d4aeff75408d9bbf0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 14 Mar 2025 10:14:40 +1100 Subject: [PATCH] Updated searching to use a publisher and cancel it (instead of db interrupt) --- .../Conversations/ConversationSearch.swift | 49 ++++----- Session/Conversations/ConversationVC.swift | 4 +- .../GlobalSearchViewController.swift | 100 ++++++++---------- .../Utilities/Database+Utilities.swift | 6 -- 4 files changed, 70 insertions(+), 89 deletions(-) diff --git a/Session/Conversations/ConversationSearch.swift b/Session/Conversations/ConversationSearch.swift index 1e2e59c11..ca6515ca8 100644 --- a/Session/Conversations/ConversationSearch.swift +++ b/Session/Conversations/ConversationSearch.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import Combine import GRDB import SignalUtilitiesKit import SessionUIKit @@ -85,31 +86,25 @@ extension ConversationSearchController: UISearchResultsUpdating { } let threadId: String = self.threadId - - DispatchQueue.global(qos: .default).async { [weak self] in - let results: [Interaction.TimestampInfo]? = dependencies[singleton: .storage].read { db -> [Interaction.TimestampInfo] in - self?.resultsBar.willStartSearching(readConnection: db) - - return try Interaction.idsForTermWithin( + let searchCancellable: AnyCancellable = dependencies[singleton: .storage] + .readPublisher { db -> [Interaction.TimestampInfo] in + try Interaction.idsForTermWithin( threadId: threadId, pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText) ) .fetchAll(db) } - - // If we didn't get results back then we most likely interrupted the query so - // should ignore the results (if there are no results we would succeed and get - // an empty array back) - guard let results: [Interaction.TimestampInfo] = results else { return } - - DispatchQueue.main.async { - guard let strongSelf = self else { return } - - self?.resultsBar.stopLoading() - self?.resultsBar.updateResults(results: results, visibleItemIds: self?.delegate?.currentVisibleIds()) - self?.delegate?.conversationSearchController(strongSelf, didUpdateSearchResults: results, searchText: searchText) - } - } + .subscribe(on: DispatchQueue.global(qos: .default), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] results in + self?.resultsBar.stopLoading() + self?.resultsBar.updateResults(results: results, visibleItemIds: self?.delegate?.currentVisibleIds()) + self?.delegate?.conversationSearchController(self, didUpdateSearchResults: results, searchText: searchText) + } + ) + self.resultsBar.willStartSearching(searchCancellable: searchCancellable) } } @@ -138,7 +133,7 @@ protocol SearchResultsBarDelegate: AnyObject { public final class SearchResultsBar: UIView { @ThreadSafe private var hasResults: Bool = false @ThreadSafeObject private var results: [Interaction.TimestampInfo] = [] - @ThreadSafeObject private var readConnection: Database? = nil + @ThreadSafeObject private var currentSearchCancellable: AnyCancellable? = nil var currentIndex: Int? weak var resultsBarDelegate: SearchResultsBarDelegate? @@ -275,8 +270,7 @@ public final class SearchResultsBar: UIView { // MARK: - Content - /// This method will be called within a DB read block - func willStartSearching(readConnection: Database) { + func willStartSearching(searchCancellable: AnyCancellable) { let hasNoExistingResults: Bool = hasResults DispatchQueue.main.async { [weak self] in @@ -287,8 +281,8 @@ public final class SearchResultsBar: UIView { self?.startLoading() } - self.readConnection?.interrupt() - self._readConnection.set(to: readConnection) + currentSearchCancellable?.cancel() + _currentSearchCancellable.set(to: searchCancellable) } func updateResults(results: [Interaction.TimestampInfo]?, visibleItemIds: [Int64]?) { @@ -311,7 +305,6 @@ public final class SearchResultsBar: UIView { return 0 }() - self._readConnection.set(to: nil) self._results.performUpdate { _ in (results ?? []) } self.hasResults = (results != nil) @@ -366,6 +359,6 @@ public final class SearchResultsBar: UIView { public protocol ConversationSearchControllerDelegate: UISearchControllerDelegate { func conversationSearchControllerDependencies() -> Dependencies func currentVisibleIds() -> [Int64] - func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) - func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo: Interaction.TimestampInfo) + func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) + func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didSelectInteractionInfo: Interaction.TimestampInfo) } diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 2f303b22f..54c77a844 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1968,12 +1968,12 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa func conversationSearchControllerDependencies() -> Dependencies { return viewModel.dependencies } func currentVisibleIds() -> [Int64] { return (fullyVisibleCellViewModels() ?? []).map { $0.id } } - func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) { + func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) { viewModel.lastSearchedText = searchText tableView.reloadRows(at: tableView.indexPathsForVisibleRows ?? [], with: UITableView.RowAnimation.none) } - func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo interactionInfo: Interaction.TimestampInfo) { + func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didSelectInteractionInfo interactionInfo: Interaction.TimestampInfo) { scrollToInteractionIfNeeded(with: interactionInfo, focusBehaviour: .highlight) } diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index 7b8fe65fa..91163761c 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import Combine import GRDB import DifferenceKit import SessionUIKit @@ -106,7 +107,7 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI ) }() - @ThreadSafeObject private var readConnection: Database? = nil + @ThreadSafeObject private var currentSearchCancellable: AnyCancellable? = nil private lazy var searchResultSet: SearchResultData = defaultSearchResults private var termForCurrentSearchResultSet: String = "" private var lastSearchText: String? @@ -256,61 +257,54 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI guard force || lastSearchText != searchText else { return } lastSearchText = searchText - - DispatchQueue.global(qos: .default).async { [weak self, dependencies] in - self?.readConnection?.interrupt() - - let result: Result<[SectionModel], Error>? = dependencies[singleton: .storage].read { db -> Result<[SectionModel], Error> in - self?._readConnection.set(to: db) + currentSearchCancellable?.cancel() + + _currentSearchCancellable.set(to: dependencies[singleton: .storage] + .readPublisher { [dependencies] db -> [SectionModel] in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let contactsAndGroupsResults: [SessionThreadViewModel] = try SessionThreadViewModel + .contactsAndGroupsQuery( + userSessionId: userSessionId, + pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText), + searchTerm: searchText + ) + .fetchAll(db) + Thread.sleep(forTimeInterval: 1) + let messageResults: [SessionThreadViewModel] = try SessionThreadViewModel + .messagesQuery( + userSessionId: userSessionId, + pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText) + ) + .fetchAll(db) - do { - let userSessionId: SessionId = dependencies[cache: .general].sessionId - let contactsAndGroupsResults: [SessionThreadViewModel] = try SessionThreadViewModel - .contactsAndGroupsQuery( - userSessionId: userSessionId, - pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText), - searchTerm: searchText - ) - .fetchAll(db) - let messageResults: [SessionThreadViewModel] = try SessionThreadViewModel - .messagesQuery( - userSessionId: userSessionId, - pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText) - ) - .fetchAll(db) - - return .success([ - ArraySection(model: .contactsAndGroups, elements: contactsAndGroupsResults), - ArraySection(model: .messages, elements: messageResults) - ]) - } - catch { - // Don't log the 'interrupt' error as that's just the user typing too fast - if (error as? DatabaseError)?.resultCode != DatabaseError.SQLITE_INTERRUPT { - SNLog("[GlobalSearch] Failed to find results due to error: \(error)") - } - - return .failure(error) - } + return [ + ArraySection(model: .contactsAndGroups, elements: contactsAndGroupsResults), + ArraySection(model: .messages, elements: messageResults) + ] } - self?._readConnection.set(to: nil) - - DispatchQueue.main.async { - switch result { - case .success(let sections): - self?.termForCurrentSearchResultSet = searchText - self?.searchResultSet = SearchResultData( - state: (sections.map { $0.elements.count }.reduce(0, +) > 0) ? .results : .none, - data: sections - ) - self?.isLoading = false - self?.tableView.reloadData() - self?.refreshTimer = nil - - default: break + .subscribe(on: DispatchQueue.global(qos: .default), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sink( + receiveCompletion: { result in + /// Cancelling the search results in `receiveCompletion` not getting called so we can just log any + /// errors we get without needing to filter out "cancelled search" cases + switch result { + case .finished: break + case .failure(let error): + SNLog("[GlobalSearch] Failed to find results due to error: \(error)") + } + }, + receiveValue: { [weak self] sections in + self?.termForCurrentSearchResultSet = searchText + self?.searchResultSet = SearchResultData( + state: (sections.map { $0.elements.count }.reduce(0, +) > 0) ? .results : .none, + data: sections + ) + self?.isLoading = false + self?.tableView.reloadData() + self?.refreshTimer = nil } - } - } + )) } @objc func cancel() { diff --git a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift index 1c9a606e1..add7923f4 100644 --- a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift @@ -21,12 +21,6 @@ public extension Database { return try makeFTS5Pattern(rawPattern: rawPattern, forTable: table.databaseTableName) } - func interrupt() { - guard sqliteConnection != nil else { return } - - sqlite3_interrupt(sqliteConnection) - } - /// This is a custom implementation of the `afterNextTransaction` method which executes the closures within their own /// transactions to allow for nesting of 'afterNextTransaction' actions ///