Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make accessing ChatClientConfig in the Database Layer safer #3566

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Adds `ChatThreadVC.loadNextReplies()` + `ChatThreadVC.didFinishLoadingNextReplies(error:)`
### 🐞 Fixed
- Fix thread reply action shown when inside a Thread [#3561](https://github.com/GetStream/stream-chat-swift/pull/3561)
- Fix creating controllers from background threads leading to rare crashes [#3566](https://github.com/GetStream/stream-chat-swift/pull/3566)

# [4.70.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.70.0)
_January 14, 2025_
Expand Down
12 changes: 2 additions & 10 deletions Sources/StreamChat/ChatClient+Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,11 @@ extension ChatClient {

var databaseContainerBuilder: (
_ kind: DatabaseContainer.Kind,
_ shouldFlushOnStart: Bool,
_ shouldResetEphemeralValuesOnStart: Bool,
_ localCachingSettings: ChatClientConfig.LocalCaching?,
_ deletedMessageVisibility: ChatClientConfig.DeletedMessageVisibility?,
_ shouldShowShadowedMessages: Bool?
_ chatClientConfig: ChatClientConfig
) -> DatabaseContainer = {
DatabaseContainer(
kind: $0,
shouldFlushOnStart: $1,
shouldResetEphemeralValuesOnStart: $2,
localCachingSettings: $3,
deletedMessagesVisibility: $4,
shouldShowShadowedMessages: $5
chatClientConfig: $1
)
}

Expand Down
12 changes: 2 additions & 10 deletions Sources/StreamChat/ChatClientFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,7 @@ class ChatClientFactory {
let dbFileURL = storeURL.appendingPathComponent(config.apiKey.apiKeyString)
return environment.databaseContainerBuilder(
.onDisk(databaseFileURL: dbFileURL),
config.shouldFlushLocalStorageOnStart,
config.isClientInActiveMode, // Only reset Ephemeral values in active mode
config.localCaching,
config.deletedMessagesVisibility,
config.shouldShowShadowedMessages
config
)
}

Expand All @@ -111,11 +107,7 @@ class ChatClientFactory {

return environment.databaseContainerBuilder(
.inMemory,
config.shouldFlushLocalStorageOnStart,
config.isClientInActiveMode, // Only reset Ephemeral values in active mode
config.localCaching,
config.deletedMessagesVisibility,
config.shouldShowShadowedMessages
config
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -887,9 +887,8 @@ private extension ChatMessageController {

func setRepliesObserver() {
let sortAscending = listOrdering == .topToBottom ? false : true
let deletedMessageVisibility = client.databaseContainer.viewContext
.deletedMessagesVisibility ?? .visibleForCurrentUser
let shouldShowShadowedMessages = client.databaseContainer.viewContext.shouldShowShadowedMessages ?? false
let deletedMessageVisibility = client.config.deletedMessagesVisibility
let shouldShowShadowedMessages = client.config.shouldShowShadowedMessages

let pageSize: Int = repliesPageSize
let observer = environment.repliesObserverBuilder(
Expand Down
8 changes: 5 additions & 3 deletions Sources/StreamChat/Database/DTOs/ChannelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,8 @@ extension ChatChannel {
throw InvalidModel(dto)
}

let clientConfig = context.chatClientConfig

let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: dto.extraData)
Expand Down Expand Up @@ -500,7 +502,7 @@ extension ChatChannel {

let latestMessages: [ChatMessage] = {
var messages = sortedMessageDTOs
.prefix(dto.managedObjectContext?.localCachingSettings?.chatChannel.latestMessagesLimit ?? 25)
.prefix(clientConfig.localCaching.chatChannel.latestMessagesLimit)
.compactMap { try? $0.relationshipAsModel(depth: depth) }
if let oldest = dto.oldestMessageAt?.bridgeDate {
messages = messages.filter { $0.createdAt >= oldest }
Expand Down Expand Up @@ -531,7 +533,7 @@ extension ChatChannel {
}
return lhsActivity > rhsActivity
}
.prefix(context.localCachingSettings?.chatChannel.lastActiveWatchersLimit ?? 100)
.prefix(clientConfig.localCaching.chatChannel.lastActiveWatchersLimit)
.compactMap { try? $0.asModel() }

let members = dto.members
Expand All @@ -543,7 +545,7 @@ extension ChatChannel {
}
return lhsActivity > rhsActivity
}
.prefix(context.localCachingSettings?.chatChannel.lastActiveMembersLimit ?? 100)
.prefix(clientConfig.localCaching.chatChannel.lastActiveMembersLimit)
.compactMap { try? $0.asModel() }

let muteDetails: MuteDetails? = {
Expand Down
23 changes: 13 additions & 10 deletions Sources/StreamChat/Database/DTOs/MessageDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ class MessageDTO: NSManagedObject {
MessageDTO.applyPrefetchingState(to: request)
request.predicate = previewMessagePredicate(
cid: cid,
includeShadowedMessages: context.shouldShowShadowedMessages ?? false
includeShadowedMessages: context.chatClientConfig.shouldShowShadowedMessages
)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.createdAt, ascending: false)]
request.fetchOffset = 0
Expand Down Expand Up @@ -1202,11 +1202,12 @@ extension NSManagedObjectContext: MessageDatabaseSession {
before id: MessageId,
cid: String
) throws -> MessageDTO? {
try MessageDTO.loadMessage(
let clientConfig = chatClientConfig
return try MessageDTO.loadMessage(
before: id,
cid: cid,
deletedMessagesVisibility: deletedMessagesVisibility ?? .alwaysVisible,
shouldShowShadowedMessages: shouldShowShadowedMessages ?? true,
deletedMessagesVisibility: clientConfig.deletedMessagesVisibility,
shouldShowShadowedMessages: clientConfig.shouldShowShadowedMessages,
context: self
)
}
Expand All @@ -1217,13 +1218,14 @@ extension NSManagedObjectContext: MessageDatabaseSession {
in cid: ChannelId,
sortAscending: Bool
) throws -> [MessageDTO] {
try MessageDTO.loadMessages(
let clientConfig = chatClientConfig
return try MessageDTO.loadMessages(
from: fromIncludingDate,
to: toIncludingDate,
in: cid,
sortAscending: sortAscending,
deletedMessagesVisibility: deletedMessagesVisibility ?? .alwaysVisible,
shouldShowShadowedMessages: shouldShowShadowedMessages ?? true,
deletedMessagesVisibility: clientConfig.deletedMessagesVisibility,
shouldShowShadowedMessages: clientConfig.shouldShowShadowedMessages,
context: self
)
}
Expand All @@ -1234,13 +1236,14 @@ extension NSManagedObjectContext: MessageDatabaseSession {
in messageId: MessageId,
sortAscending: Bool
) throws -> [MessageDTO] {
try MessageDTO.loadReplies(
let clientConfig = chatClientConfig
return try MessageDTO.loadReplies(
from: fromIncludingDate,
to: toIncludingDate,
in: messageId,
sortAscending: sortAscending,
deletedMessagesVisibility: deletedMessagesVisibility ?? .alwaysVisible,
shouldShowShadowedMessages: shouldShowShadowedMessages ?? true,
deletedMessagesVisibility: clientConfig.deletedMessagesVisibility,
shouldShowShadowedMessages: clientConfig.shouldShowShadowedMessages,
context: self
)
}
Expand Down
51 changes: 11 additions & 40 deletions Sources/StreamChat/Database/DatabaseContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
let context = newBackgroundContext()
context.automaticallyMergesChangesFromParent = true
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.perform { [localCachingSettings, deletedMessageVisibility, shouldShowShadowedMessages] in
context.localCachingSettings = localCachingSettings
context.deletedMessagesVisibility = deletedMessageVisibility
context.shouldShowShadowedMessages = shouldShowShadowedMessages
}
context.setChatClientConfig(chatClientConfig)
return context
}()

Expand All @@ -43,11 +39,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
let context = newBackgroundContext()
context.automaticallyMergesChangesFromParent = true
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.perform { [localCachingSettings, deletedMessageVisibility, shouldShowShadowedMessages] in
context.localCachingSettings = localCachingSettings
context.deletedMessagesVisibility = deletedMessageVisibility
context.shouldShowShadowedMessages = shouldShowShadowedMessages
}
context.setChatClientConfig(chatClientConfig)
return context
}()

Expand All @@ -65,23 +57,18 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
// Context is merged manually since automatically is too slow for reacting to changes needed by the state layer
context.automaticallyMergesChangesFromParent = false
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.perform { [localCachingSettings, deletedMessageVisibility, shouldShowShadowedMessages] in
context.localCachingSettings = localCachingSettings
context.deletedMessagesVisibility = deletedMessageVisibility
context.shouldShowShadowedMessages = shouldShowShadowedMessages
}
stateLayerContextRefreshObservers = [
context.observeChanges(in: writableContext),
context.observeChanges(in: viewContext)
]
context.setChatClientConfig(chatClientConfig)
return context
}()

private var stateLayerContextRefreshObservers = [NSObjectProtocol]()
private var loggerNotificationObserver: NSObjectProtocol?
private let localCachingSettings: ChatClientConfig.LocalCaching?
private let deletedMessageVisibility: ChatClientConfig.DeletedMessageVisibility?
private let shouldShowShadowedMessages: Bool?

let chatClientConfig: ChatClientConfig

static var cachedModels = [String: NSManagedObjectModel]()

Expand All @@ -102,13 +89,9 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
///
init(
kind: Kind,
shouldFlushOnStart: Bool = false,
shouldResetEphemeralValuesOnStart: Bool = true,
modelName: String = "StreamChatModel",
bundle: Bundle? = .streamChat,
localCachingSettings: ChatClientConfig.LocalCaching? = nil,
deletedMessagesVisibility: ChatClientConfig.DeletedMessageVisibility? = nil,
shouldShowShadowedMessages: Bool? = nil
chatClientConfig: ChatClientConfig
) {
let managedObjectModel: NSManagedObjectModel
if let cachedModel = Self.cachedModels[modelName] {
Expand All @@ -122,9 +105,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
Self.cachedModels[modelName] = model
}

self.localCachingSettings = localCachingSettings
deletedMessageVisibility = deletedMessagesVisibility
self.shouldShowShadowedMessages = shouldShowShadowedMessages
self.chatClientConfig = chatClientConfig

super.init(name: modelName, managedObjectModel: managedObjectModel)

Expand All @@ -140,36 +121,26 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
"Failed to initialize the in-memory storage with error: \(error). This is a non-recoverable error."
)
}
if shouldResetEphemeralValuesOnStart {
if chatClientConfig.isClientInActiveMode {
self?.resetEphemeralValues()
}
}
return
}
if shouldResetEphemeralValuesOnStart {
if chatClientConfig.isClientInActiveMode {
self?.resetEphemeralValues()
}
}

if shouldFlushOnStart {
if chatClientConfig.shouldFlushLocalStorageOnStart {
recreatePersistentStore(completion: persistentStoreCreatedCompletion)
} else {
setupPersistentStore(completion: persistentStoreCreatedCompletion)
}

viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
viewContext.automaticallyMergesChangesFromParent = true
if Thread.current.isMainThread {
viewContext.localCachingSettings = localCachingSettings
viewContext.deletedMessagesVisibility = deletedMessagesVisibility
viewContext.shouldShowShadowedMessages = shouldShowShadowedMessages
} else {
viewContext.perform { [viewContext, localCachingSettings, deletedMessagesVisibility, shouldShowShadowedMessages] in
viewContext.localCachingSettings = localCachingSettings
viewContext.deletedMessagesVisibility = deletedMessagesVisibility
viewContext.shouldShowShadowedMessages = shouldShowShadowedMessages
}
}
viewContext.setChatClientConfig(chatClientConfig)

FetchCache.clear()

Expand Down
18 changes: 17 additions & 1 deletion Sources/StreamChat/Database/DatabaseSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,23 @@

import CoreData

extension NSManagedObjectContext: DatabaseSession {}
extension NSManagedObjectContext: DatabaseSession {
private static let chatClientConfigKey = "io.getStream.StreamChat.config.key"

var chatClientConfig: ChatClientConfig {
var config: ChatClientConfig?
performAndWait {
config = userInfo[Self.chatClientConfigKey] as? ChatClientConfig
}
Comment on lines +12 to +14
Copy link
Contributor

@laevandus laevandus Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does bring slight overhead (it is not huge, just tried sending 20 messages). Thinking if we want to do this since we fixed actual misuses already and this accessor is only used from asModel().
Screenshot 2025-01-21 at 09 59 07

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum I see, I'll do a couple more perf tests. But if there is a significant impact, maybe it is better we remove the performAndWait

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After running the benchmarking script on a real device, it looks like there is no significant impact:

image

For now, lets keep it this way to bulletproof, but if we find any issues with this in the future, we can remove the performAndWait 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's re-test this with this branch: #3564

For now we merge it as it

return config!
}

func setChatClientConfig(_ config: ChatClientConfig) {
performAndWait {
userInfo[Self.chatClientConfigKey] = config
}
}
}

protocol UserDatabaseSession {
/// Saves the provided payload to the DB. Return's the matching `UserDTO` if the save was successful. Throws an error
Expand Down
5 changes: 3 additions & 2 deletions Sources/StreamChat/Repositories/MessageRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,9 @@ class MessageRepository {
) {
let context = database.backgroundReadOnlyContext
context.perform {
let deletedMessagesVisibility = context.deletedMessagesVisibility ?? .alwaysVisible
let shouldShowShadowedMessages = context.shouldShowShadowedMessages ?? true
let clientConfig = context.chatClientConfig
let deletedMessagesVisibility = clientConfig.deletedMessagesVisibility
let shouldShowShadowedMessages = clientConfig.shouldShowShadowedMessages
do {
let resultId = try MessageDTO.loadMessage(
before: messageId,
Expand Down

This file was deleted.

Loading
Loading