Skip to content

Commit

Permalink
Read data using a background context in the DataStore (#3541)
Browse files Browse the repository at this point in the history
  • Loading branch information
laevandus authored Dec 23, 2024
1 parent f065955 commit cf2d8c3
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 5 additions & 15 deletions Sources/StreamChat/Database/DataStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
}
18 changes: 18 additions & 0 deletions Sources/StreamChat/Database/DatabaseContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,24 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
}
}
}

func readAndWait<T>(_ 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) {
Expand Down
30 changes: 30 additions & 0 deletions Tests/StreamChatTests/Database/DataStore_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,56 @@ 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 {
let cid: ChannelId = .unique
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 {
let id: MessageId = .unique
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))
}
}
}

Expand Down

0 comments on commit cf2d8c3

Please sign in to comment.