diff --git a/CHANGELOG.md b/CHANGELOG.md index 809b2420e2..a57fd8e2dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### 🐞 Fixed +- Remove the main thread requirement from the `DataStore` [#3541](https://github.com/GetStream/stream-chat-swift/pull/3541) ### ⚡ Performance - Improve performance of accessing database model properties [#3534](https://github.com/GetStream/stream-chat-swift/pull/3534) - Improve performance of model conversions with large extra data [#3534](https://github.com/GetStream/stream-chat-swift/pull/3534) diff --git a/Sources/StreamChat/Database/DataStore.swift b/Sources/StreamChat/Database/DataStore.swift index e8bb9718f4..1a0e98cc29 100644 --- a/Sources/StreamChat/Database/DataStore.swift +++ b/Sources/StreamChat/Database/DataStore.swift @@ -34,63 +34,53 @@ public struct DataStore { /// - Returns: If there's a user object in the locally cached data matching the provided `id`, returns the matching /// model object. If a user object doesn't exist locally, returns `nil`. /// - /// **Warning**: Should be called on the `main` thread only. - /// /// - Parameter id: An id of a user. public func user(id: UserId) -> ChatUser? { - try? database.viewContext.user(id: id)?.asModel() + try? database.readAndWait { try? $0.user(id: id)?.asModel() } } /// Loads a current user model with a matching `id` from the **local data store**. /// /// If the data doesn't exist locally, it's recommended to use controllers to fetch data from remote servers. /// - /// **Warning**: Should be called on the `main` thread only. - /// /// - Returns: If there's a current user object in the locally cached data, returns the matching /// model object. If a user object doesn't exist locally, returns `nil`. public func currentUser() -> CurrentChatUser? { - try? database.viewContext.currentUser?.asModel() + try? database.readAndWait { try? $0.currentUser?.asModel() } } /// Loads a channel model with a matching `cid` from the **local data store**. /// /// If the data doesn't exist locally, it's recommended to use controllers to fetch data from remote servers. /// - /// **Warning**: Should be called on the `main` thread only. - /// /// - Returns: If there's a channel object in the locally cached data matching the provided `cid`, returns the matching /// model object. If a channel object doesn't exist locally, returns `nil`. /// /// - Parameter cid: An cid of a channel. public func channel(cid: ChannelId) -> ChatChannel? { - try? database.viewContext.channel(cid: cid)?.asModel() + try? database.readAndWait { try? $0.channel(cid: cid)?.asModel() } } /// Loads a message model with a matching `id` from the **local data store**. /// /// If the data doesn't exist locally, it's recommended to use controllers to fetch data from remote servers. /// - /// **Warning**: Should be called on the `main` thread only. - /// /// - Returns: If there's a message object in the locally cached data matching the provided `id`, returns the matching /// model object. If a user object doesn't exist locally, returns `nil`. /// /// - Parameter id: An id of a message. public func message(id: MessageId) -> ChatMessage? { - try? database.viewContext.message(id: id)?.asModel() + try? database.readAndWait { try? $0.message(id: id)?.asModel() } } /// Loads a thread model with a matching `parentMessageId` from the **local data store**. /// /// If the thread doesn't exist locally, it's recommended to fetch it with `messageController.loadThread()`. /// - /// **Warning**: Should be called on the `main` thread only. - /// /// - Returns: Returns the Thread object. /// /// - Parameter parentMessageId: The message id which is the root of a trhead. public func thread(parentMessageId: MessageId) -> ChatThread? { - try? database.viewContext.thread(parentMessageId: parentMessageId, cache: nil)?.asModel() + try? database.readAndWait { try? $0.thread(parentMessageId: parentMessageId, cache: nil)?.asModel() } } } diff --git a/Sources/StreamChat/Database/DatabaseContainer.swift b/Sources/StreamChat/Database/DatabaseContainer.swift index 97e8648c38..fe6d83fc0e 100644 --- a/Sources/StreamChat/Database/DatabaseContainer.swift +++ b/Sources/StreamChat/Database/DatabaseContainer.swift @@ -282,6 +282,24 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { } } } + + func readAndWait(_ actions: (DatabaseSession) throws -> T) throws -> T { + let context = viewContext + var result: T? + var readError: Error? + context.performAndWait { + do { + result = try actions(context) + } catch { + readError = error + } + } + if let result { + return result + } else { + throw readError ?? ClientError.Unknown() + } + } /// Removes all data from the local storage. func removeAllData(completion: ((Error?) -> Void)? = nil) { diff --git a/Tests/StreamChatTests/Database/DataStore_Tests.swift b/Tests/StreamChatTests/Database/DataStore_Tests.swift index d27a0ca22c..4e0ae4cbf5 100644 --- a/Tests/StreamChatTests/Database/DataStore_Tests.swift +++ b/Tests/StreamChatTests/Database/DataStore_Tests.swift @@ -26,12 +26,20 @@ final class DataStore_Tests: XCTestCase { XCTAssertNil(dataStore.user(id: userId)) try _client.databaseContainer.createUser(id: userId) XCTAssertNotNil(dataStore.user(id: userId)) + + DispatchQueue.global().sync { + XCTAssertNotNil(dataStore.user(id: userId)) + } } func test_currentUserIsLoaded() throws { XCTAssertNil(dataStore.currentUser()) try _client.databaseContainer.createCurrentUser() XCTAssertNotNil(dataStore.currentUser) + + DispatchQueue.global().sync { + XCTAssertNotNil(dataStore.currentUser) + } } func test_channelIsLoaded() throws { @@ -39,6 +47,10 @@ final class DataStore_Tests: XCTestCase { XCTAssertNil(dataStore.channel(cid: cid)) try _client.databaseContainer.createChannel(cid: cid) XCTAssertNotNil(dataStore.channel(cid: cid)) + + DispatchQueue.global().sync { + XCTAssertNotNil(dataStore.channel(cid: cid)) + } } func test_messageIsLoaded() throws { @@ -46,6 +58,24 @@ final class DataStore_Tests: XCTestCase { XCTAssertNil(dataStore.message(id: id)) try _client.databaseContainer.createMessage(id: id) XCTAssertNotNil(dataStore.message(id: id)) + + DispatchQueue.global().sync { + XCTAssertNotNil(dataStore.message(id: id)) + } + } + + func test_threadIsLoaded() throws { + let parentMessageId: MessageId = .unique + XCTAssertNil(dataStore.message(id: parentMessageId)) + try _client.databaseContainer.writeSynchronously { session in + let payload = ThreadPayload.dummy(parentMessageId: parentMessageId) + try session.saveThread(payload: payload, cache: nil) + } + XCTAssertNotNil(dataStore.thread(parentMessageId: parentMessageId)) + + DispatchQueue.global().sync { + XCTAssertNotNil(dataStore.thread(parentMessageId: parentMessageId)) + } } }