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

Use Core Data prefetching when StreamRuntimeCheck._isDatabasePrefetchingEnabled #3495

Merged
merged 7 commits into from
Nov 20, 2024
5 changes: 5 additions & 0 deletions Sources/StreamChat/Config/StreamRuntimeCheck.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
17 changes: 17 additions & 0 deletions Sources/StreamChat/Database/DTOs/ChannelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class ChannelDTO: NSManagedObject {

static func fetchRequest(for cid: ChannelId) -> NSFetchRequest<ChannelDTO> {
let request = NSFetchRequest<ChannelDTO>(entityName: ChannelDTO.entityName)
ChannelDTO.applyPrefetchingState(to: request)
request.sortDescriptors = [NSSortDescriptor(keyPath: \ChannelDTO.updatedAt, ascending: false)]
request.predicate = NSPredicate(format: "cid == %@", cid.rawValue)
return request
Expand All @@ -139,6 +140,7 @@ class ChannelDTO: NSManagedObject {
static func load(cids: [ChannelId], context: NSManagedObjectContext) -> [ChannelDTO] {
guard !cids.isEmpty else { return [] }
let request = NSFetchRequest<ChannelDTO>(entityName: ChannelDTO.entityName)
ChannelDTO.applyPrefetchingState(to: request)
request.predicate = NSPredicate(format: "cid IN %@", cids)
return load(by: request, context: context)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -382,6 +397,7 @@ extension ChannelDTO {
chatClientConfig: ChatClientConfig
) -> NSFetchRequest<ChannelDTO> {
let request = NSFetchRequest<ChannelDTO>(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) }
Expand Down Expand Up @@ -420,6 +436,7 @@ extension ChannelDTO {

static func directMessageChannel(participantId: UserId, context: NSManagedObjectContext) -> ChannelDTO? {
let request = NSFetchRequest<ChannelDTO>(entityName: ChannelDTO.entityName)
ChannelDTO.applyPrefetchingState(to: request)
request.sortDescriptors = [NSSortDescriptor(keyPath: \ChannelDTO.updatedAt, ascending: false)]
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "cid CONTAINS ':!members'"),
Expand Down
10 changes: 10 additions & 0 deletions Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class ChannelMuteDTO: NSManagedObject {

static func fetchRequest(for cid: ChannelId) -> NSFetchRequest<ChannelMuteDTO> {
let request = NSFetchRequest<ChannelMuteDTO>(entityName: ChannelMuteDTO.entityName)
ChannelMuteDTO.applyPrefetchingState(to: request)
request.predicate = NSPredicate(format: "channel.cid == %@", cid.rawValue)
return request
}
Expand All @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ class ChannelReadDTO: NSManagedObject {

static func fetchRequest(userId: String) -> NSFetchRequest<ChannelReadDTO> {
let request = NSFetchRequest<ChannelReadDTO>(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<ChannelReadDTO> {
let request = NSFetchRequest<ChannelReadDTO>(entityName: ChannelReadDTO.entityName)
ChannelReadDTO.applyPrefetchingState(to: request)
request.predicate = NSPredicate(format: "channel.cid == %@ && user.id == %@", cid.rawValue, userId)
return request
}
Expand Down Expand Up @@ -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(
Expand Down
17 changes: 17 additions & 0 deletions Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class CurrentUserDTO: NSManagedObject {
/// Returns a default fetch request for the current user.
static var defaultFetchRequest: NSFetchRequest<CurrentUserDTO> {
let request = NSFetchRequest<CurrentUserDTO>(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)]
Expand All @@ -46,6 +47,7 @@ extension CurrentUserDTO {
/// - Parameter context: The context used to fetch `CurrentUserDTO`
fileprivate static func load(context: NSManagedObjectContext) -> CurrentUserDTO? {
let request = NSFetchRequest<CurrentUserDTO>(entityName: CurrentUserDTO.entityName)
CurrentUserDTO.applyPrefetchingState(to: request)
let result = load(by: request, context: context)

log.assert(
Expand All @@ -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<CurrentUserDTO>(entityName: CurrentUserDTO.entityName)
CurrentUserDTO.applyPrefetchingState(to: request)
let result = load(by: request, context: context)
log.assert(
result.count <= 1,
Expand Down Expand Up @@ -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) }
Expand Down
8 changes: 8 additions & 0 deletions Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ extension MemberDTO {
/// Returns a fetch request for the dto with the provided `userId`.
static func member(_ userId: UserId, in cid: ChannelId) -> NSFetchRequest<MemberDTO> {
let request = NSFetchRequest<MemberDTO>(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
Expand All @@ -49,6 +50,7 @@ extension MemberDTO {
/// Returns a fetch request for the DTOs matching the provided `query`.
static func members(matching query: ChannelMemberListQuery) -> NSFetchRequest<MemberDTO> {
let request = NSFetchRequest<MemberDTO>(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
Expand Down Expand Up @@ -160,6 +162,12 @@ extension NSManagedObjectContext {
}
}

extension MemberDTO {
override class func prefetchedRelationshipKeyPaths() -> [String] {
[KeyPath.string(\MemberDTO.user)]
}
}

extension MemberDTO {
func asModel() throws -> ChatChannelMember { try .create(fromDTO: self) }
}
Expand Down
31 changes: 31 additions & 0 deletions Sources/StreamChat/Database/DTOs/MessageDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ class MessageDTO: NSManagedObject {
shouldShowShadowedMessages: Bool
) -> NSFetchRequest<MessageDTO> {
let request = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
MessageDTO.applyPrefetchingState(to: request)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: sortAscending)]
request.predicate = channelMessagesPredicate(
for: cid.rawValue,
Expand All @@ -374,6 +375,7 @@ class MessageDTO: NSManagedObject {
shouldShowShadowedMessages: Bool
) -> NSFetchRequest<MessageDTO> {
let request = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
MessageDTO.applyPrefetchingState(to: request)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: sortAscending)]
request.predicate = threadRepliesPredicate(
for: messageId,
Expand All @@ -387,6 +389,7 @@ class MessageDTO: NSManagedObject {

static func messagesFetchRequest(for query: MessageSearchQuery) -> NSFetchRequest<MessageDTO> {
let request = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
MessageDTO.applyPrefetchingState(to: request)
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "ANY searches.filterHash == %@", query.filterHash),
NSPredicate(format: "isHardDeleted == NO")
Expand All @@ -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<MessageDTO> {
let request = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
MessageDTO.applyPrefetchingState(to: request)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: false)]
request.predicate = NSPredicate(format: "id == %@", messageId)
return request
Expand All @@ -413,6 +417,7 @@ class MessageDTO: NSManagedObject {
context: NSManagedObjectContext
) -> [MessageDTO] {
let request = NSFetchRequest<MessageDTO>(entityName: entityName)
MessageDTO.applyPrefetchingState(to: request)
request.predicate = channelMessagesPredicate(
for: cid,
deletedMessagesVisibility: deletedMessagesVisibility,
Expand All @@ -426,6 +431,7 @@ class MessageDTO: NSManagedObject {

static func preview(for cid: String, context: NSManagedObjectContext) -> MessageDTO? {
let request = NSFetchRequest<MessageDTO>(entityName: entityName)
MessageDTO.applyPrefetchingState(to: request)
request.predicate = previewMessagePredicate(
cid: cid,
includeShadowedMessages: context.shouldShowShadowedMessages ?? false
Expand Down Expand Up @@ -466,6 +472,7 @@ class MessageDTO: NSManagedObject {
context: NSManagedObjectContext
) -> [MessageDTO] {
let request = NSFetchRequest<MessageDTO>(entityName: entityName)
MessageDTO.applyPrefetchingState(to: request)
request.predicate = NSPredicate(format: "parentMessageId == %@", messageId)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.createdAt, ascending: false)]
request.fetchLimit = limit
Expand All @@ -487,6 +494,7 @@ class MessageDTO: NSManagedObject {
]

let request = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
MessageDTO.applyPrefetchingState(to: request)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: false)]
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)

Expand Down Expand Up @@ -534,6 +542,7 @@ class MessageDTO: NSManagedObject {
guard let message = load(id: id, context: context) else { return nil }

let request = NSFetchRequest<MessageDTO>(entityName: entityName)
MessageDTO.applyPrefetchingState(to: request)
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
channelMessagesPredicate(for: cid, deletedMessagesVisibility: deletedMessagesVisibility, shouldShowShadowedMessages: shouldShowShadowedMessages),
.init(format: "id != %@", id),
Expand All @@ -554,6 +563,7 @@ class MessageDTO: NSManagedObject {
context: NSManagedObjectContext
) throws -> [MessageDTO] {
let request = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
MessageDTO.applyPrefetchingState(to: request)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: sortAscending)]
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
channelMessagesPredicate(
Expand All @@ -577,6 +587,7 @@ class MessageDTO: NSManagedObject {
context: NSManagedObjectContext
) throws -> [MessageDTO] {
let request = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
MessageDTO.applyPrefetchingState(to: request)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: sortAscending)]
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
threadRepliesPredicate(
Expand Down Expand Up @@ -1220,6 +1231,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) }
Expand Down
9 changes: 9 additions & 0 deletions Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ final class MessageReactionDTO: NSManagedObject {
extension MessageReactionDTO {
static func reactionListFetchRequest(query: ReactionListQuery) -> NSFetchRequest<MessageReactionDTO> {
let request = NSFetchRequest<MessageReactionDTO>(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.
Expand Down Expand Up @@ -83,6 +84,7 @@ extension MessageReactionDTO {
}

let request = NSFetchRequest<MessageReactionDTO>(entityName: MessageReactionDTO.entityName)
MessageReactionDTO.applyPrefetchingState(to: request)
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "id IN %@", ids),
Self.notLocallyDeletedPredicates
Expand Down Expand Up @@ -120,6 +122,7 @@ extension MessageReactionDTO {
sort: [NSSortDescriptor]
) -> NSFetchRequest<MessageReactionDTO> {
let request = NSFetchRequest<MessageReactionDTO>(entityName: MessageReactionDTO.entityName)
MessageReactionDTO.applyPrefetchingState(to: request)
request.sortDescriptors = sort
request.predicate = NSPredicate(format: "message.id == %@", messageId)
request.fetchBatchSize = 30
Expand Down Expand Up @@ -184,6 +187,12 @@ extension NSManagedObjectContext {
}
}

extension MessageReactionDTO {
override class func prefetchedRelationshipKeyPaths() -> [String] {
[KeyPath.string(\MessageReactionDTO.user)]
}
}

extension MessageReactionDTO {
var localState: LocalReactionState? {
get {
Expand Down
11 changes: 11 additions & 0 deletions Sources/StreamChat/Database/DTOs/PollDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,23 @@ class PollDTO: NSManagedObject {

static func fetchRequest(for pollId: String) -> NSFetchRequest<PollDTO> {
let request = NSFetchRequest<PollDTO>(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] = [:]
Expand Down
7 changes: 7 additions & 0 deletions Sources/StreamChat/Database/DTOs/PollOptionDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,18 @@ class PollOptionDTO: NSManagedObject {

static func fetchRequest(for optionId: String) -> NSFetchRequest<PollOptionDTO> {
let request = NSFetchRequest<PollOptionDTO>(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] = [:]
Expand Down
Loading
Loading