diff --git a/Sources/StreamChat/Database/DatabaseContainer.swift b/Sources/StreamChat/Database/DatabaseContainer.swift index 74db7fb3376..bc523d1f027 100644 --- a/Sources/StreamChat/Database/DatabaseContainer.swift +++ b/Sources/StreamChat/Database/DatabaseContainer.swift @@ -27,14 +27,17 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { return context }() - /// This is the same thing as `viewContext` only it doesn’t run on main thread. - /// It’s just an optimization for removing as much as possible from the main thread. + /// The read-only context used for all the background reads and database observers. + /// + /// The context is refreshed when a write happens, therefore database observers react to changes + /// immediately. For example, here the state.messages needs to react before loadMessages finishes. + /// ```swift + /// try await chat.loadMessages() + /// let messages = chat.state.messages + /// ``` /// /// Updating DTOs from this context will lead to issues. /// Use `writableContext` to mutate database entities. - /// - /// Use this context to observe non-time sensitive changes. - /// If you need a time sensitive context, use `viewContext` instead. lazy var backgroundReadOnlyContext: NSManagedObjectContext = { let context = newBackgroundContext() // Changes are merged manually (synchronously on save) instead of automatically. This keeps the @@ -52,30 +55,6 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { }() private var backgroundReadOnlyContextRefreshObservers = [NSObjectProtocol]() - - /// An immediately reacting NSManagedObjectContext for the chat state layer. - /// - /// Chat state layer requires that the context is refreshed when a write happens. Otherwise database observers are too slow to react. - /// - /// For example, here the state.messages needs to react before loadMessages finishes. - /// ```swift - /// try await chat.loadMessages() - /// let messages = chat.state.messages - /// ``` - private(set) lazy var stateLayerContext: NSManagedObjectContext = { - let context = newBackgroundContext() - // Context is merged manually since automatically is too slow for reacting to changes needed by the state layer - context.automaticallyMergesChangesFromParent = false - context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - stateLayerContextRefreshObservers = [ - context.observeChanges(in: writableContext), - context.observeChanges(in: viewContext) - ] - context.setChatClientConfig(chatClientConfig) - return context - }() - - private var stateLayerContextRefreshObservers = [NSObjectProtocol]() private var loggerNotificationObserver: NSObjectProtocol? let chatClientConfig: ChatClientConfig @@ -83,7 +62,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { static let cachedModels = AllocatedUnfairLock([String: NSManagedObjectModel]()) /// All `NSManagedObjectContext`s this container owns. - private(set) lazy var allContext: [NSManagedObjectContext] = [viewContext, backgroundReadOnlyContext, stateLayerContext, writableContext] + private(set) lazy var allContext: [NSManagedObjectContext] = [viewContext, backgroundReadOnlyContext, writableContext] /// Creates a new `DatabaseContainer` instance. /// @@ -158,9 +137,6 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { } deinit { - stateLayerContextRefreshObservers.forEach { observer in - NotificationCenter.default.removeObserver(observer) - } backgroundReadOnlyContextRefreshObservers.forEach { observer in NotificationCenter.default.removeObserver(observer) } @@ -252,11 +228,8 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { } } - private func read( - from context: NSManagedObjectContext, - _ actions: @escaping @Sendable (DatabaseSession) throws -> T, - completion: @escaping @Sendable (Result) -> Void - ) { + func read(_ actions: @escaping @Sendable (DatabaseSession) throws -> T, completion: @escaping @Sendable (Result) -> Void) { + let context = backgroundReadOnlyContext context.perform { do { let changeCounts = context.currentChangeCounts() @@ -271,13 +244,9 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { } } - func read(_ actions: @escaping @Sendable (DatabaseSession) throws -> T, completion: @escaping @Sendable (Result) -> Void) { - read(from: backgroundReadOnlyContext, actions, completion: completion) - } - func read(_ actions: @escaping @Sendable (DatabaseSession) throws -> T) async throws -> T where T: Sendable { try await withCheckedThrowingContinuation { continuation in - read(from: stateLayerContext, actions) { result in + read(actions) { result in continuation.resume(with: result) } } diff --git a/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift b/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift index da3bc57eef7..b162efa9aa9 100644 --- a/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift +++ b/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift @@ -15,7 +15,7 @@ class ListResult: DatabaseObserverType {} /// A CoreData store observer which immediately reports changes as soon as the store has been changed. /// -/// - Note: Requires the ``DatabaseContainer/stateLayerContext`` which is immediately synchronized. +/// - Note: Requires a synchronously merged context, like ``DatabaseContainer/backgroundReadOnlyContext``. final class StateLayerDatabaseObserver: @unchecked Sendable { private let changeAggregator: ListChangeAggregator private let frc: NSFetchedResultsController @@ -59,7 +59,7 @@ extension StateLayerDatabaseObserver where ResultType == EntityResult { entityItemReuseKeyPaths itemReuseKeyPaths: (item: KeyPath, dto: KeyPath)? = nil ) { self.init( - context: database.stateLayerContext, + context: database.backgroundReadOnlyContext, fetchRequest: fetchRequest, itemCreator: itemCreator, itemReuseKeyPaths: itemReuseKeyPaths, @@ -140,7 +140,7 @@ extension StateLayerDatabaseObserver where ResultType == ListResult { runtimeSorting: [SortValue] = [] ) { self.init( - context: database.stateLayerContext, + context: database.backgroundReadOnlyContext, fetchRequest: fetchRequest, itemCreator: itemCreator, itemReuseKeyPaths: itemReuseKeyPaths, diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift index 6415b9480e0..e30630ed1fd 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift @@ -87,7 +87,7 @@ final class IdentifiablePayload_Tests: XCTestCase { ) savePayload(payload: channelList, database: database) - let contexts = [database.writableContext, database.backgroundReadOnlyContext, database.stateLayerContext] + let contexts = [database.writableContext, database.backgroundReadOnlyContext] let iterations = 2000 var caches: [PreWarmedCache] = (0..