From 6ed5843b1a0784d7ec458c9f9f7017ac97cc7a69 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 19 Nov 2024 17:08:25 +0000 Subject: [PATCH 1/3] Resolve swift warning (#3499) --- Sources/StreamChat/Database/DTOs/MemberModelDTO.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift index 0a643eeaee..c4b6220b6d 100644 --- a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift +++ b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift @@ -131,7 +131,7 @@ extension NSManagedObjectContext { dto.notificationsMuted = payload.notificationsMuted if let extraData = payload.extraData { - dto.extraData = try? JSONEncoder.default.encode(payload.extraData) + dto.extraData = try? JSONEncoder.default.encode(extraData) } if let query = query { From 2b1c0c5fb37c20f5c9dc89f8ebc0869358dd0415 Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Wed, 20 Nov 2024 13:22:33 +0100 Subject: [PATCH 2/3] Remove docusaurus xcodeproj reference (#3501) --- StreamChat.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index 0e1952ae50..813505464d 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -3174,7 +3174,6 @@ 43F4750B26F4E4FF0009487D /* ChatMessageReactionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageReactionItemView.swift; sourceTree = ""; }; 43F4750D26FB247C0009487D /* ChatReactionPickerReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatReactionPickerReactionsView.swift; sourceTree = ""; }; 4A4E184528D06CA30062378D /* Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Documentation.docc; sourceTree = ""; }; - 4A51230029D3170C005CEA9B /* docusaurus */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docusaurus; sourceTree = ""; }; 4F05C0702C8832C40085B4B7 /* URLRequest+cURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+cURL.swift"; sourceTree = ""; }; 4F05ECB72B6CCA4900641820 /* DifferenceKit+Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Stream.swift"; sourceTree = ""; }; 4F072F022BC008D9006A66CA /* StateLayerDatabaseObserver_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateLayerDatabaseObserver_Tests.swift; sourceTree = ""; }; @@ -6559,7 +6558,6 @@ 8AD5EC8522E9A3E8005CFAC9 = { isa = PBXGroup; children = ( - 4A51230029D3170C005CEA9B /* docusaurus */, 4A4E184528D06CA30062378D /* Documentation.docc */, AD9BE32526680E4200A6D284 /* Stream.playground */, 792E3D6A25C97D920040B0C2 /* Package.swift */, From b5fa385e7c7d52620b5175a9fd08eaa431d7752a Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Wed, 20 Nov 2024 19:47:52 +0200 Subject: [PATCH 3/3] Use Core Data prefetching when StreamRuntimeCheck._isDatabasePrefetchingEnabled is set (#3495) --- .../Config/StreamRuntimeCheck.swift | 5 +++ .../StreamChat/Database/DTOs/ChannelDTO.swift | 17 ++++++++++ .../Database/DTOs/ChannelMuteDTO.swift | 10 ++++++ .../Database/DTOs/ChannelReadDTO.swift | 8 +++++ .../Database/DTOs/CurrentUserDTO.swift | 17 ++++++++++ .../Database/DTOs/MemberModelDTO.swift | 8 +++++ .../StreamChat/Database/DTOs/MessageDTO.swift | 31 +++++++++++++++++++ .../Database/DTOs/MessageReactionDTO.swift | 9 ++++++ .../StreamChat/Database/DTOs/PollDTO.swift | 11 +++++++ .../Database/DTOs/PollOptionDTO.swift | 7 +++++ .../Database/DTOs/PollVoteDTO.swift | 9 ++++++ .../Database/DTOs/PollVoteListQueryDTO.swift | 6 ++++ .../Database/DTOs/QueuedRequestDTO.swift | 2 ++ .../Database/DTOs/ReactionListQueryDTO.swift | 6 ++++ .../StreamChat/Database/DTOs/ThreadDTO.swift | 15 +++++++++ .../Database/DTOs/ThreadParticipantDTO.swift | 7 +++++ .../Database/DTOs/ThreadReadDTO.swift | 8 +++++ .../StreamChat/Database/DTOs/UserDTO.swift | 10 ++---- .../Database/DTOs/UserListQueryDTO.swift | 13 ++++---- .../Database/NSManagedObject+Extensions.swift | 27 ++++++++++++++++ 20 files changed, 212 insertions(+), 14 deletions(-) diff --git a/Sources/StreamChat/Config/StreamRuntimeCheck.swift b/Sources/StreamChat/Config/StreamRuntimeCheck.swift index bc7ea38dee..9fcbd42961 100644 --- a/Sources/StreamChat/Config/StreamRuntimeCheck.swift +++ b/Sources/StreamChat/Config/StreamRuntimeCheck.swift @@ -36,4 +36,9 @@ public enum StreamRuntimeCheck { /// /// Uses version 2 for offline state sync. public static var _isSyncV2Enabled = true + + /// For *internal use* only + /// + /// Core Data prefetches data used for creating immutable model objects (faulting is disabled). + public static var _isDatabasePrefetchingEnabled = false } diff --git a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift index 1dba83dd0b..fff9d1fb59 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift @@ -126,6 +126,7 @@ class ChannelDTO: NSManagedObject { static func fetchRequest(for cid: ChannelId) -> NSFetchRequest { let request = NSFetchRequest(entityName: ChannelDTO.entityName) + ChannelDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \ChannelDTO.updatedAt, ascending: false)] request.predicate = NSPredicate(format: "cid == %@", cid.rawValue) return request @@ -139,6 +140,7 @@ class ChannelDTO: NSManagedObject { static func load(cids: [ChannelId], context: NSManagedObjectContext) -> [ChannelDTO] { guard !cids.isEmpty else { return [] } let request = NSFetchRequest(entityName: ChannelDTO.entityName) + ChannelDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "cid IN %@", cids) return load(by: request, context: context) } @@ -159,6 +161,19 @@ class ChannelDTO: NSManagedObject { } } +extension ChannelDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [ + KeyPath.string(\ChannelDTO.currentlyTypingUsers), + KeyPath.string(\ChannelDTO.pinnedMessages), + KeyPath.string(\ChannelDTO.messages), + KeyPath.string(\ChannelDTO.members), + KeyPath.string(\ChannelDTO.reads), + KeyPath.string(\ChannelDTO.watchers) + ] + } +} + // MARK: - Reset Ephemeral Values extension ChannelDTO: EphemeralValuesContainer { @@ -382,6 +397,7 @@ extension ChannelDTO { chatClientConfig: ChatClientConfig ) -> NSFetchRequest { let request = NSFetchRequest(entityName: ChannelDTO.entityName) + ChannelDTO.applyPrefetchingState(to: request) // Fetch results controller requires at least one sorting descriptor. var sortDescriptors = query.sort.compactMap { $0.key.sortDescriptor(isAscending: $0.isAscending) } @@ -420,6 +436,7 @@ extension ChannelDTO { static func directMessageChannel(participantId: UserId, context: NSManagedObjectContext) -> ChannelDTO? { let request = NSFetchRequest(entityName: ChannelDTO.entityName) + ChannelDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \ChannelDTO.updatedAt, ascending: false)] request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "cid CONTAINS ':!members'"), diff --git a/Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift index 7422422fe6..f2d2d709c4 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift @@ -15,6 +15,7 @@ final class ChannelMuteDTO: NSManagedObject { static func fetchRequest(for cid: ChannelId) -> NSFetchRequest { let request = NSFetchRequest(entityName: ChannelMuteDTO.entityName) + ChannelMuteDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "channel.cid == %@", cid.rawValue) return request } @@ -37,6 +38,15 @@ final class ChannelMuteDTO: NSManagedObject { } } +extension ChannelMuteDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [ + KeyPath.string(\ChannelMuteDTO.channel), + KeyPath.string(\ChannelMuteDTO.currentUser) + ] + } +} + extension NSManagedObjectContext { @discardableResult func saveChannelMute(payload: MutedChannelPayload) throws -> ChannelMuteDTO { diff --git a/Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift index ab9d0bbc16..278623ade5 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift @@ -28,12 +28,14 @@ class ChannelReadDTO: NSManagedObject { static func fetchRequest(userId: String) -> NSFetchRequest { let request = NSFetchRequest(entityName: ChannelReadDTO.entityName) + ChannelReadDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "user.id == %@", userId) return request } static func fetchRequest(for cid: ChannelId, userId: String) -> NSFetchRequest { let request = NSFetchRequest(entityName: ChannelReadDTO.entityName) + ChannelReadDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "channel.cid == %@ && user.id == %@", cid.rawValue, userId) return request } @@ -191,6 +193,12 @@ extension NSManagedObjectContext { } } +extension ChannelReadDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\ChannelReadDTO.user)] + } +} + extension ChatChannelRead { fileprivate static func create(fromDTO dto: ChannelReadDTO) throws -> ChatChannelRead { try .init( diff --git a/Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift b/Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift index cf7b0bdadf..09e5e2f06f 100644 --- a/Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift +++ b/Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift @@ -33,6 +33,7 @@ class CurrentUserDTO: NSManagedObject { /// Returns a default fetch request for the current user. static var defaultFetchRequest: NSFetchRequest { let request = NSFetchRequest(entityName: CurrentUserDTO.entityName) + CurrentUserDTO.applyPrefetchingState(to: request) // Sorting doesn't matter here as soon as we have a single current-user in a database. // It's here to make the request safe for FRC request.sortDescriptors = [.init(keyPath: \CurrentUserDTO.unreadMessagesCount, ascending: true)] @@ -46,6 +47,7 @@ extension CurrentUserDTO { /// - Parameter context: The context used to fetch `CurrentUserDTO` fileprivate static func load(context: NSManagedObjectContext) -> CurrentUserDTO? { let request = NSFetchRequest(entityName: CurrentUserDTO.entityName) + CurrentUserDTO.applyPrefetchingState(to: request) let result = load(by: request, context: context) log.assert( @@ -61,6 +63,7 @@ extension CurrentUserDTO { /// - Parameter context: The context used to fetch/create `CurrentUserDTO` fileprivate static func loadOrCreate(context: NSManagedObjectContext) -> CurrentUserDTO { let request = NSFetchRequest(entityName: CurrentUserDTO.entityName) + CurrentUserDTO.applyPrefetchingState(to: request) let result = load(by: request, context: context) log.assert( result.count <= 1, @@ -194,6 +197,20 @@ extension NSManagedObjectContext: CurrentUserDatabaseSession { } } +extension CurrentUserDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [ + KeyPath.string(\CurrentUserDTO.channelMutes), + KeyPath.string(\CurrentUserDTO.currentDevice), + KeyPath.string(\CurrentUserDTO.devices), + KeyPath.string(\CurrentUserDTO.flaggedMessages), + KeyPath.string(\CurrentUserDTO.flaggedUsers), + KeyPath.string(\CurrentUserDTO.mutedUsers), + KeyPath.string(\CurrentUserDTO.user) + ] + } +} + extension CurrentUserDTO { /// Snapshots the current state of `CurrentUserDTO` and returns an immutable model object from it. func asModel() throws -> CurrentChatUser { try .create(fromDTO: self) } diff --git a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift index c4b6220b6d..2e6cdb8d19 100644 --- a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift +++ b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift @@ -43,6 +43,7 @@ extension MemberDTO { /// Returns a fetch request for the dto with the provided `userId`. static func member(_ userId: UserId, in cid: ChannelId) -> NSFetchRequest { let request = NSFetchRequest(entityName: MemberDTO.entityName) + MemberDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \MemberDTO.memberCreatedAt, ascending: false)] request.predicate = NSPredicate(format: "id == %@", Self.createId(userId: userId, channeldId: cid)) return request @@ -51,6 +52,7 @@ extension MemberDTO { /// Returns a fetch request for the DTOs matching the provided `query`. static func members(matching query: ChannelMemberListQuery) -> NSFetchRequest { let request = NSFetchRequest(entityName: MemberDTO.entityName) + MemberDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "ANY queries.queryHash == %@", query.queryHash) var sortDescriptors = query.sortDescriptors // For consistent order we need to have a sort descriptor which breaks ties @@ -166,6 +168,12 @@ extension NSManagedObjectContext { } } +extension MemberDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\MemberDTO.user)] + } +} + extension MemberDTO { func asModel() throws -> ChatChannelMember { try .create(fromDTO: self) } } diff --git a/Sources/StreamChat/Database/DTOs/MessageDTO.swift b/Sources/StreamChat/Database/DTOs/MessageDTO.swift index 4c0cb51e9e..8fff575ee3 100644 --- a/Sources/StreamChat/Database/DTOs/MessageDTO.swift +++ b/Sources/StreamChat/Database/DTOs/MessageDTO.swift @@ -354,6 +354,7 @@ class MessageDTO: NSManagedObject { shouldShowShadowedMessages: Bool ) -> NSFetchRequest { let request = NSFetchRequest(entityName: MessageDTO.entityName) + MessageDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: sortAscending)] request.predicate = channelMessagesPredicate( for: cid.rawValue, @@ -374,6 +375,7 @@ class MessageDTO: NSManagedObject { shouldShowShadowedMessages: Bool ) -> NSFetchRequest { let request = NSFetchRequest(entityName: MessageDTO.entityName) + MessageDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: sortAscending)] request.predicate = threadRepliesPredicate( for: messageId, @@ -387,6 +389,7 @@ class MessageDTO: NSManagedObject { static func messagesFetchRequest(for query: MessageSearchQuery) -> NSFetchRequest { let request = NSFetchRequest(entityName: MessageDTO.entityName) + MessageDTO.applyPrefetchingState(to: request) request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "ANY searches.filterHash == %@", query.filterHash), NSPredicate(format: "isHardDeleted == NO") @@ -399,6 +402,7 @@ class MessageDTO: NSManagedObject { /// Returns a fetch request for the dto with a specific `messageId`. static func message(withID messageId: MessageId) -> NSFetchRequest { let request = NSFetchRequest(entityName: MessageDTO.entityName) + MessageDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: false)] request.predicate = NSPredicate(format: "id == %@", messageId) return request @@ -413,6 +417,7 @@ class MessageDTO: NSManagedObject { context: NSManagedObjectContext ) -> [MessageDTO] { let request = NSFetchRequest(entityName: entityName) + MessageDTO.applyPrefetchingState(to: request) request.predicate = channelMessagesPredicate( for: cid, deletedMessagesVisibility: deletedMessagesVisibility, @@ -426,6 +431,7 @@ class MessageDTO: NSManagedObject { static func preview(for cid: String, context: NSManagedObjectContext) -> MessageDTO? { let request = NSFetchRequest(entityName: entityName) + MessageDTO.applyPrefetchingState(to: request) request.predicate = previewMessagePredicate( cid: cid, includeShadowedMessages: context.shouldShowShadowedMessages ?? false @@ -466,6 +472,7 @@ class MessageDTO: NSManagedObject { context: NSManagedObjectContext ) -> [MessageDTO] { let request = NSFetchRequest(entityName: entityName) + MessageDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "parentMessageId == %@", messageId) request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.createdAt, ascending: false)] request.fetchLimit = limit @@ -487,6 +494,7 @@ class MessageDTO: NSManagedObject { ] let request = NSFetchRequest(entityName: MessageDTO.entityName) + MessageDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: false)] request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates) @@ -534,6 +542,7 @@ class MessageDTO: NSManagedObject { guard let message = load(id: id, context: context) else { return nil } let request = NSFetchRequest(entityName: entityName) + MessageDTO.applyPrefetchingState(to: request) request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ channelMessagesPredicate(for: cid, deletedMessagesVisibility: deletedMessagesVisibility, shouldShowShadowedMessages: shouldShowShadowedMessages), .init(format: "id != %@", id), @@ -554,6 +563,7 @@ class MessageDTO: NSManagedObject { context: NSManagedObjectContext ) throws -> [MessageDTO] { let request = NSFetchRequest(entityName: MessageDTO.entityName) + MessageDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: sortAscending)] request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ channelMessagesPredicate( @@ -577,6 +587,7 @@ class MessageDTO: NSManagedObject { context: NSManagedObjectContext ) throws -> [MessageDTO] { let request = NSFetchRequest(entityName: MessageDTO.entityName) + MessageDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: sortAscending)] request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ threadRepliesPredicate( @@ -1227,6 +1238,26 @@ extension NSManagedObjectContext: MessageDatabaseSession { } } +extension MessageDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [ + KeyPath.string(\MessageDTO.attachments), + KeyPath.string(\MessageDTO.flaggedBy), + KeyPath.string(\MessageDTO.mentionedUsers), + KeyPath.string(\MessageDTO.moderationDetails), + KeyPath.string(\MessageDTO.pinnedBy), + KeyPath.string(\MessageDTO.poll), + KeyPath.string(\MessageDTO.quotedBy), + KeyPath.string(\MessageDTO.quotedMessage), + KeyPath.string(\MessageDTO.reactionGroups), + KeyPath.string(\MessageDTO.reads), + KeyPath.string(\MessageDTO.replies), + KeyPath.string(\MessageDTO.threadParticipants), + KeyPath.string(\MessageDTO.user) + ] + } +} + extension MessageDTO { /// Snapshots the current state of `MessageDTO` and returns an immutable model object from it. func asModel() throws -> ChatMessage { try .init(fromDTO: self, depth: 0) } diff --git a/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift b/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift index 24a4ceb105..5afb797f21 100644 --- a/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift +++ b/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift @@ -35,6 +35,7 @@ final class MessageReactionDTO: NSManagedObject { extension MessageReactionDTO { static func reactionListFetchRequest(query: ReactionListQuery) -> NSFetchRequest { let request = NSFetchRequest(entityName: MessageReactionDTO.entityName) + MessageReactionDTO.applyPrefetchingState(to: request) // Fetch results controller requires at least one sorting descriptor. // At the moment, we do not allow changing the query sorting. @@ -83,6 +84,7 @@ extension MessageReactionDTO { } let request = NSFetchRequest(entityName: MessageReactionDTO.entityName) + MessageReactionDTO.applyPrefetchingState(to: request) request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "id IN %@", ids), Self.notLocallyDeletedPredicates @@ -120,6 +122,7 @@ extension MessageReactionDTO { sort: [NSSortDescriptor] ) -> NSFetchRequest { let request = NSFetchRequest(entityName: MessageReactionDTO.entityName) + MessageReactionDTO.applyPrefetchingState(to: request) request.sortDescriptors = sort request.predicate = NSPredicate(format: "message.id == %@", messageId) request.fetchBatchSize = 30 @@ -184,6 +187,12 @@ extension NSManagedObjectContext { } } +extension MessageReactionDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\MessageReactionDTO.user)] + } +} + extension MessageReactionDTO { var localState: LocalReactionState? { get { diff --git a/Sources/StreamChat/Database/DTOs/PollDTO.swift b/Sources/StreamChat/Database/DTOs/PollDTO.swift index 292c1c98cb..4299343976 100644 --- a/Sources/StreamChat/Database/DTOs/PollDTO.swift +++ b/Sources/StreamChat/Database/DTOs/PollDTO.swift @@ -63,12 +63,23 @@ class PollDTO: NSManagedObject { static func fetchRequest(for pollId: String) -> NSFetchRequest { let request = NSFetchRequest(entityName: PollDTO.entityName) + PollDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \PollDTO.updatedAt, ascending: false)] request.predicate = NSPredicate(format: "id == %@", pollId) return request } } +extension PollDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [ + KeyPath.string(\PollDTO.createdBy), + KeyPath.string(\PollDTO.latestVotes), + KeyPath.string(\PollDTO.latestVotesByOption) + ] + } +} + extension PollDTO { func asModel() throws -> Poll { var extraData: [String: RawJSON] = [:] diff --git a/Sources/StreamChat/Database/DTOs/PollOptionDTO.swift b/Sources/StreamChat/Database/DTOs/PollOptionDTO.swift index fb6f48279a..4ee1331f80 100644 --- a/Sources/StreamChat/Database/DTOs/PollOptionDTO.swift +++ b/Sources/StreamChat/Database/DTOs/PollOptionDTO.swift @@ -44,11 +44,18 @@ class PollOptionDTO: NSManagedObject { static func fetchRequest(for optionId: String) -> NSFetchRequest { let request = NSFetchRequest(entityName: PollOptionDTO.entityName) + PollOptionDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "id == %@", optionId) return request } } +extension PollOptionDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\PollOptionDTO.latestVotes)] + } +} + extension PollOptionDTO { func asModel() throws -> PollOption { var extraData: [String: RawJSON] = [:] diff --git a/Sources/StreamChat/Database/DTOs/PollVoteDTO.swift b/Sources/StreamChat/Database/DTOs/PollVoteDTO.swift index cc2d2371e0..55155929cd 100644 --- a/Sources/StreamChat/Database/DTOs/PollVoteDTO.swift +++ b/Sources/StreamChat/Database/DTOs/PollVoteDTO.swift @@ -61,11 +61,18 @@ class PollVoteDTO: NSManagedObject { static func fetchRequest(for voteId: String, pollId: String) -> NSFetchRequest { let request = NSFetchRequest(entityName: PollVoteDTO.entityName) + PollVoteDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "id == %@ && pollId == %@", voteId, pollId) return request } } +extension PollVoteDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\PollVoteDTO.user)] + } +} + extension PollVoteDTO { func asModel() throws -> PollVote { try PollVote( @@ -201,6 +208,7 @@ extension NSManagedObjectContext { func pollVotes(for userId: String, pollId: String) throws -> [PollVoteDTO] { let request = NSFetchRequest(entityName: PollVoteDTO.entityName) + PollVoteDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "user.id == %@ && pollId == %@", userId, pollId) return PollVoteDTO.load(by: request, context: self) } @@ -235,6 +243,7 @@ extension NSManagedObjectContext { extension PollVoteDTO { static func pollVoteListFetchRequest(query: PollVoteListQuery) -> NSFetchRequest { let request = NSFetchRequest(entityName: PollVoteDTO.entityName) + PollVoteDTO.applyPrefetchingState(to: request) request.sortDescriptors = [.init(key: #keyPath(PollVoteDTO.createdAt), ascending: false)] request.predicate = NSPredicate(format: "ANY queries.filterHash == %@", query.queryHash) return request diff --git a/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift index f471d77f4c..f45141c71e 100644 --- a/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift +++ b/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift @@ -39,6 +39,12 @@ class PollVoteListQueryDTO: NSManagedObject { } } +extension PollVoteListQueryDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\PollVoteListQueryDTO.votes)] + } +} + extension NSManagedObjectContext { func linkVote(with id: String, in pollId: String, to filterHash: String?) throws { guard let filterHash else { throw ClientError.Unexpected() } diff --git a/Sources/StreamChat/Database/DTOs/QueuedRequestDTO.swift b/Sources/StreamChat/Database/DTOs/QueuedRequestDTO.swift index 1c6fe0dc21..d37380be39 100644 --- a/Sources/StreamChat/Database/DTOs/QueuedRequestDTO.swift +++ b/Sources/StreamChat/Database/DTOs/QueuedRequestDTO.swift @@ -27,12 +27,14 @@ class QueuedRequestDTO: NSManagedObject { static func loadAllPendingRequests(context: NSManagedObjectContext) -> [QueuedRequestDTO] { let request = NSFetchRequest(entityName: QueuedRequestDTO.entityName) + QueuedRequestDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \QueuedRequestDTO.date, ascending: true)] return load(by: request, context: context) } static func load(id: String, context: NSManagedObjectContext) -> QueuedRequestDTO? { let request = NSFetchRequest(entityName: entityName) + QueuedRequestDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "id == %@", id) return load(by: request, context: context).first } diff --git a/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift index a2293ce94d..e44036c84e 100644 --- a/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift @@ -39,6 +39,12 @@ class ReactionListQueryDTO: NSManagedObject { } } +extension ReactionListQueryDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\ReactionListQueryDTO.reactions)] + } +} + extension NSManagedObjectContext { func reactionListQuery(filterHash: String) -> ReactionListQueryDTO? { ReactionListQueryDTO.load(filterHash: filterHash, context: self) diff --git a/Sources/StreamChat/Database/DTOs/ThreadDTO.swift b/Sources/StreamChat/Database/DTOs/ThreadDTO.swift index 1e7a648c5e..e528545710 100644 --- a/Sources/StreamChat/Database/DTOs/ThreadDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ThreadDTO.swift @@ -59,6 +59,7 @@ class ThreadDTO: NSManagedObject { static func fetchRequest(for parentMessageId: MessageId) -> NSFetchRequest { let request = NSFetchRequest(entityName: ThreadDTO.entityName) + ThreadDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \ThreadDTO.updatedAt, ascending: false)] request.predicate = NSPredicate(format: "parentMessageId == %@", parentMessageId) return request @@ -66,6 +67,7 @@ class ThreadDTO: NSManagedObject { static func threadListFetchRequest() -> NSFetchRequest { let request = NSFetchRequest(entityName: ThreadDTO.entityName) + ThreadDTO.applyPrefetchingState(to: request) // By default threads are sorted by unread + updatedAt and // at the moment this is not customisable. @@ -126,6 +128,19 @@ class ThreadDTO: NSManagedObject { } } +extension ThreadDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [ + KeyPath.string(\ThreadDTO.channel), + KeyPath.string(\ThreadDTO.createdBy), + KeyPath.string(\ThreadDTO.latestReplies), + KeyPath.string(\ThreadDTO.parentMessage), + KeyPath.string(\ThreadDTO.read), + KeyPath.string(\ThreadDTO.threadParticipants) + ] + } +} + extension ThreadDTO { func asModel() throws -> ChatThread { let extraData: [String: RawJSON] diff --git a/Sources/StreamChat/Database/DTOs/ThreadParticipantDTO.swift b/Sources/StreamChat/Database/DTOs/ThreadParticipantDTO.swift index 84f43538c3..4629b53cd6 100644 --- a/Sources/StreamChat/Database/DTOs/ThreadParticipantDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ThreadParticipantDTO.swift @@ -15,6 +15,7 @@ class ThreadParticipantDTO: NSManagedObject { static func fetchRequest(for threadId: String, userId: String) -> NSFetchRequest { let request = NSFetchRequest(entityName: ThreadParticipantDTO.entityName) + ThreadParticipantDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "threadId == %@ && user.id == %@", threadId, userId) return request } @@ -37,6 +38,12 @@ class ThreadParticipantDTO: NSManagedObject { } } +extension ThreadParticipantDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\ThreadParticipantDTO.user)] + } +} + extension ThreadParticipantDTO { func asModel() throws -> ThreadParticipant { try .init( diff --git a/Sources/StreamChat/Database/DTOs/ThreadReadDTO.swift b/Sources/StreamChat/Database/DTOs/ThreadReadDTO.swift index 91980432b5..b103c70a79 100644 --- a/Sources/StreamChat/Database/DTOs/ThreadReadDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ThreadReadDTO.swift @@ -49,17 +49,25 @@ class ThreadReadDTO: NSManagedObject { static func fetchRequest(userId: String) -> NSFetchRequest { let request = NSFetchRequest(entityName: ThreadReadDTO.entityName) + ThreadReadDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "user.id == %@", userId) return request } static func fetchRequest(for parentMessageId: MessageId, userId: String) -> NSFetchRequest { let request = NSFetchRequest(entityName: ThreadReadDTO.entityName) + ThreadReadDTO.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "thread.parentMessageId == %@ && user.id == %@", parentMessageId, userId) return request } } +extension ThreadReadDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\ThreadReadDTO.user)] + } +} + extension ThreadReadDTO { func asModel() throws -> ThreadRead { try .init( diff --git a/Sources/StreamChat/Database/DTOs/UserDTO.swift b/Sources/StreamChat/Database/DTOs/UserDTO.swift index 1ee3fc7f0b..1f316fe0cb 100644 --- a/Sources/StreamChat/Database/DTOs/UserDTO.swift +++ b/Sources/StreamChat/Database/DTOs/UserDTO.swift @@ -31,6 +31,7 @@ class UserDTO: NSManagedObject { /// Returns a fetch request for the dto with the provided `userId`. static func user(withID userId: UserId) -> NSFetchRequest { let request = NSFetchRequest(entityName: UserDTO.entityName) + UserDTO.applyPrefetchingState(to: request) request.sortDescriptors = [NSSortDescriptor(keyPath: \UserDTO.id, ascending: false)] request.predicate = NSPredicate(format: "id == %@", userId) return request @@ -201,6 +202,7 @@ extension UserDTO { extension UserDTO { static func userListFetchRequest(query: UserListQuery) -> NSFetchRequest { let request = NSFetchRequest(entityName: UserDTO.entityName) + UserDTO.applyPrefetchingState(to: request) // Fetch results controller requires at least one sorting descriptor. let sortDescriptors = query.sort.compactMap { $0.key.sortDescriptor(isAscending: $0.isAscending) } @@ -214,15 +216,9 @@ extension UserDTO { return request } - static var userWithoutQueryFetchRequest: NSFetchRequest { - let request = NSFetchRequest(entityName: UserDTO.entityName) - request.sortDescriptors = [UserListSortingKey.defaultSortDescriptor] - request.predicate = NSPredicate(format: "queries.@count == 0") - return request - } - static func watcherFetchRequest(cid: ChannelId) -> NSFetchRequest { let request = NSFetchRequest(entityName: UserDTO.entityName) + UserDTO.applyPrefetchingState(to: request) request.sortDescriptors = [UserListSortingKey.defaultSortDescriptor] request.predicate = NSPredicate(format: "ANY watchedChannels.cid == %@", cid.rawValue) return request diff --git a/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift index 0e558062c2..cf269b1cdd 100644 --- a/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift +++ b/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift @@ -20,13 +20,6 @@ class UserListQueryDTO: NSManagedObject { @NSManaged var users: Set - static func observedQueries() -> NSFetchRequest { - let fetchRequest = NSFetchRequest(entityName: UserListQueryDTO.entityName) - fetchRequest.predicate = NSPredicate(format: "shouldBeUpdatedInBackground == YES") - fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \UserListQueryDTO.filterHash, ascending: true)] - return fetchRequest - } - static func load(filterHash: String, context: NSManagedObjectContext) -> UserListQueryDTO? { load( keyPath: #keyPath(UserListQueryDTO.filterHash), @@ -36,6 +29,12 @@ class UserListQueryDTO: NSManagedObject { } } +extension UserListQueryDTO { + override class func prefetchedRelationshipKeyPaths() -> [String] { + [KeyPath.string(\UserListQueryDTO.users)] + } +} + extension NSManagedObjectContext { func userListQuery(filterHash: String) -> UserListQueryDTO? { UserListQueryDTO.load(filterHash: filterHash, context: self) diff --git a/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift b/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift index 95d142c106..40d41f7675 100644 --- a/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift +++ b/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift @@ -33,6 +33,7 @@ extension NSFetchRequestGettable where Self: NSManagedObject { static func fetchRequest(keyPath: String, equalTo value: String) -> NSFetchRequest { let request = NSFetchRequest(entityName: entityName) + Self.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "%K == %@", keyPath, value) return request } @@ -40,6 +41,31 @@ extension NSFetchRequestGettable where Self: NSManagedObject { extension NSManagedObject: NSFetchRequestGettable {} +// MARK: - Fetch Request Prefetching + +protocol NSFetchRequestPrefetching {} + +extension NSFetchRequestPrefetching where Self: NSManagedObject { + /// Turns off Core Data object faulting and sets prefetched relationship keypaths. + /// + /// Note: Reduces additional Core Data fetches when most of the data is accessed from fetched objects. + static func applyPrefetchingState(to request: NSFetchRequest) { + guard StreamRuntimeCheck._isDatabasePrefetchingEnabled else { return } + request.returnsObjectsAsFaults = false + request.relationshipKeyPathsForPrefetching = Self.prefetchedRelationshipKeyPaths() + } +} + +extension NSManagedObject: NSFetchRequestPrefetching {} + +extension NSManagedObject { + @objc class func prefetchedRelationshipKeyPaths() -> [String] { + [] + } +} + +// MARK: - Loading Objects + extension NSManagedObject { static func load(by id: String, context: NSManagedObjectContext) -> [T] { load(keyPath: idKey, equalTo: id, context: context) @@ -47,6 +73,7 @@ extension NSManagedObject { static func load(keyPath: String, equalTo value: String, context: NSManagedObjectContext) -> [T] { let request = NSFetchRequest(entityName: entityName) + T.applyPrefetchingState(to: request) request.predicate = NSPredicate(format: "%K == %@", keyPath, value) return load(by: request, context: context) }