Improve channel list and model conversion performance#4165
Conversation
Add a composite id (cid/userId) to ChannelReadDTO so reads can be resolved through the prewarmed cache and a single id-based fetch instead of a compound predicate query per read. Falls back to the legacy lookup and backfills the id for existing entries, enabling a lightweight Core Data migration.
Eliminates per-copy refcount overhead (30-47 bumps per ChatChannel copy, similar for ChatMessage) by making both types reference types. Since they were already immutable snapshots with value-based Equatable/Hashable, the semantic change is minimal while the performance gain is significant for hot paths like channel list diffing and background DTO-to-model mapping. Key changes: - ChatChannel: struct → final class, membership becomes let, changing() membership parameter uses double-optional to distinguish "keep existing" from "explicitly set to nil" - ChatMessage: struct → final class, init(fromDTO:) converted to static factory method, DraftMessage convenience init delegates to designated init - LivestreamChatHandler: membership mutations folded into changing() calls
…indow channelMessageDTOs is sorted descending by createdAt, so once a message falls outside the oldestMessageAt/truncatedAt window, every older message after it does too. Filtering the DTOs with prefix(while:) before building models, instead of filtering already-built ChatMessage models twice afterward, avoids constructing messages that get discarded and removes two redundant array copies.
BackgroundEntityDatabaseObserver never passed itemReuseKeyPaths to the underlying converter, so every content-change notification decoded the item once via the change aggregator and then again from scratch when refreshing the cached snapshot. This doubled the CoreData decode cost for CurrentChatUser (including its muted channels), single-channel, member, message, poll, and user group observers on every update.
decodeRawJSON ran a full Decodable pass through JSONDecoder for every
entity's extra data, even though most users, members, messages, and
channels have none and are persisted with the literal {} bytes. Short
circuit that common case to avoid the decode overhead on every model
conversion.
chatClientConfig is written once when a context is created and never mutated again, yet every read went through performAndWait even though callers (DTO -> model conversion) are already running on the context's queue. This getter is invoked once per message/member/channel created, so the extra confinement check added up to measurable overhead when decoding channels with many nested models.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR adds observer item reuse key paths, introduces composed identifiers for channel reads, converts channel and message models to classes with related membership updates, refactors message snapshot construction, and updates DTO, database, and JSON decoding paths with tests. ChangesObserver Reuse and Controller Wiring
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Generated by 🚫 Danger |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/StreamChat/Database/DTOs/MessageDTO.swift (1)
1813-1819: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winIncrement depth when materializing
latestReplies
latestRepliescallsChatMessage.create(fromDTO:depth:)directly, so nested replies reuse the same depth while other relationship fields go throughrelationshipAsModel(depth:). That bypasses the recursion guard for reply chains; switch this torelationshipAsModel(depth:)or increment the depth here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/StreamChat/Database/DTOs/MessageDTO.swift` around lines 1813 - 1819, The latestReplies materialization in ChatMessage.create(fromDTO:depth:) is bypassing the usual depth increment, so nested replies keep reusing the same recursion level. Update the latestReplies mapping to go through relationshipAsModel(depth:) or otherwise pass depth + 1 when creating each ChatMessage, matching the behavior used for other relationship fields and ensuring the recursion guard works correctly.
🧹 Nitpick comments (3)
Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (1)
68-96: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDouble fetch on cache-miss for brand-new reads.
For a genuinely new read (not in
cache, no row with matchingid, no legacy row), this now does two sequential Core Data fetches (load(id:)at Line 79 thenload(cid:userId:)at Line 85) before inserting, versus one fetch previously. This is exactly the "many members / long history" first-sync scenario the PR targets, so the extra round-trip per new read partially offsets the intended CPU win.Consider merging the id-lookup and legacy-lookup into a single fetch with a combined predicate, backfilling
idonly when it's found nil:♻️ Proposed refactor to merge the two fallback fetches
- if let existing = load(id: readId, context: context) { - return existing - } - - // Fallback for reads persisted before the `id` attribute existed: look them up by the - // compound predicate and backfill the identifier so subsequent lookups can use the id. - if let legacy = load(cid: cid, userId: userId, context: context) { - legacy.id = readId - return legacy - } + // Single fetch covering both the fast id-based path and the legacy compound-predicate + // fallback, instead of two separate round-trips. + let combinedRequest = NSFetchRequest<ChannelReadDTO>(entityName: ChannelReadDTO.entityName) + combinedRequest.predicate = NSPredicate( + format: "id == %@ OR (channel.cid == %@ && user.id == %@)", + readId, cid.rawValue, userId + ) + if let existing = load(by: combinedRequest, context: context).first { + if existing.id == nil { + existing.id = readId + } + return existing + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift` around lines 68 - 96, The loadOrCreate flow in ChannelReadDTO performs two Core Data fetches on a cache miss for a brand-new read, first via load(id:context:) and then via load(cid:userId:context:), which adds avoidable overhead. Refactor the fallback path to use a single lookup that can match either the new id or the legacy cid/userId identity, then backfill legacy records by setting id only when needed. Keep the existing cache check, existing load(id:) fast path, and the insertNewObject branch intact while consolidating the legacy fallback logic inside loadOrCreate.Sources/StreamChat/Models/ChatMessage.swift (1)
12-12: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueConsider documenting why
@unchecked Sendableis safe here.All stored properties are
let, but_quotedMessage: BoxedAny?type-erases its wrapped value, so the compiler can't verify Sendability automatically. A short comment noting the invariant (fully immutable, and boxed values are only read viagetValue()) would help future maintainers scope/justify this exception.As per coding guidelines, "
**/*.swift: ... UseSendableconformances where needed for cross-isolation transfers and avoid data races."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/StreamChat/Models/ChatMessage.swift` at line 12, Add a brief comment near the ChatMessage declaration explaining why the `@unchecked` Sendable conformance is safe: the type is fully immutable because all stored properties are let, and the type-erased _quotedMessage: BoxedAny? is only ever read through getValue(), so the compiler cannot prove Sendability but the runtime invariant keeps cross-isolation use safe.Source: Coding guidelines
Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents (1)
152-152: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winConsider adding a uniqueness constraint on the new
ChannelReadDTO.id.The new composed
id(cid + userId) is added with a fetch index but nouniquenessConstraints, unlike other entities that use anid-style identifier (e.g.MessageDTO,MemberDTO,UserDTOall constrain onid). SinceloadOrCreatenow relies onid-based lookup as the primary path, a DB-level constraint would provide defense-in-depth against duplicate rows for the same channel/user pair, complementing the existing cache/backfill logic inChannelReadDTO.loadOrCreate.♻️ Proposed addition
<entity name="ChannelReadDTO" representedClassName="ChannelReadDTO" syncable="YES"> <attribute name="id" optional="YES" attributeType="String"/> ... <fetchIndex name="id"> <fetchIndexElement property="id" type="Binary" order="ascending"/> </fetchIndex> + <uniquenessConstraints> + <uniquenessConstraint> + <constraint value="id"/> + </uniquenessConstraint> + </uniquenessConstraints> </entity>Also applies to: 161-163
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents` at line 152, Add a uniqueness constraint for ChannelReadDTO.id in the Core Data model, since loadOrCreate now depends on the composed cid+userId identifier as the primary lookup path. Update the StreamChatModel entity definition for ChannelReadDTO so its id field is constrained like MessageDTO, MemberDTO, and UserDTO, and ensure the same constraint is applied consistently where the model is defined. Keep the existing fetch index, but make the id constraint the DB-level guard against duplicate channel/user rows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@Sources/StreamChat/Database/DTOs/MessageDTO.swift`:
- Around line 1813-1819: The latestReplies materialization in
ChatMessage.create(fromDTO:depth:) is bypassing the usual depth increment, so
nested replies keep reusing the same recursion level. Update the latestReplies
mapping to go through relationshipAsModel(depth:) or otherwise pass depth + 1
when creating each ChatMessage, matching the behavior used for other
relationship fields and ensuring the recursion guard works correctly.
---
Nitpick comments:
In `@Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift`:
- Around line 68-96: The loadOrCreate flow in ChannelReadDTO performs two Core
Data fetches on a cache miss for a brand-new read, first via load(id:context:)
and then via load(cid:userId:context:), which adds avoidable overhead. Refactor
the fallback path to use a single lookup that can match either the new id or the
legacy cid/userId identity, then backfill legacy records by setting id only when
needed. Keep the existing cache check, existing load(id:) fast path, and the
insertNewObject branch intact while consolidating the legacy fallback logic
inside loadOrCreate.
In
`@Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents`:
- Line 152: Add a uniqueness constraint for ChannelReadDTO.id in the Core Data
model, since loadOrCreate now depends on the composed cid+userId identifier as
the primary lookup path. Update the StreamChatModel entity definition for
ChannelReadDTO so its id field is constrained like MessageDTO, MemberDTO, and
UserDTO, and ensure the same constraint is applied consistently where the model
is defined. Keep the existing fetch index, but make the id constraint the
DB-level guard against duplicate channel/user rows.
In `@Sources/StreamChat/Models/ChatMessage.swift`:
- Line 12: Add a brief comment near the ChatMessage declaration explaining why
the `@unchecked` Sendable conformance is safe: the type is fully immutable because
all stored properties are let, and the type-erased _quotedMessage: BoxedAny? is
only ever read through getValue(), so the compiler cannot prove Sendability but
the runtime invariant keeps cross-isolation use safe.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 56434f31-d2da-4479-a4e7-a99f493d38eb
📒 Files selected for processing (27)
Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiableModel.swiftSources/StreamChat/APIClient/Endpoints/Payloads/IdentifiablePayload.swiftSources/StreamChat/Controllers/ChannelController/ChannelController.swiftSources/StreamChat/Controllers/ChannelController/LivestreamChatHandler.swiftSources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swiftSources/StreamChat/Controllers/DatabaseObserver/BackgroundEntityDatabaseObserver.swiftSources/StreamChat/Controllers/MemberController/MemberController.swiftSources/StreamChat/Controllers/MessageController/MessageController.swiftSources/StreamChat/Controllers/PollController/PollController.swiftSources/StreamChat/Controllers/PollController/PollVoteListController.swiftSources/StreamChat/Controllers/UserController/UserController.swiftSources/StreamChat/Controllers/UserGroupController/UserGroupController.swiftSources/StreamChat/Database/DTOs/ChannelDTO.swiftSources/StreamChat/Database/DTOs/ChannelReadDTO.swiftSources/StreamChat/Database/DTOs/MessageDTO.swiftSources/StreamChat/Database/DatabaseSession.swiftSources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contentsSources/StreamChat/Models/Channel.swiftSources/StreamChat/Models/ChatMessage.swiftSources/StreamChat/Models/DraftMessage.swiftSources/StreamChat/Utils/Codable+Extensions.swiftStreamChat.xcodeproj/project.pbxprojTests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiableModel_Tests.swiftTests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swiftTests/StreamChatTests/BackgroundEntityDatabaseObserver_Tests.swiftTests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swiftTests/StreamChatTests/Utils/Codable+Extensions_Tests.swift
Hoist the coalesced values in ChatChannel.changing and ChatMessage.changing into explicit locals so the type-checker does not have to resolve all of them inside a single large initializer.
Public Interface+ public final class ChatMessage: Identifiable, @unchecked Sendable
+
+ public let id: MessageId
+ public let cid: ChannelId?
+ public let text: String
+ public let type: MessageType
+ public let command: String?
+ public let createdAt: Date
+ public let locallyCreatedAt: Date?
+ public let updatedAt: Date
+ public let deletedAt: Date?
+ public let textUpdatedAt: Date?
+ public let arguments: String?
+ public let parentMessageId: MessageId?
+ public let showReplyInChannel: Bool
+ public let replyCount: Int
+ public let extraData: [String: RawJSON]
+ public var quotedMessage: ChatMessage?
+ public let draftReply: DraftMessage?
+ public let reminder: MessageReminderInfo?
+ public let isBounced: Bool
+ public let isSilent: Bool
+ public let isShadowed: Bool
+ public let deletedForMe: Bool
+ public let reactionScores: [MessageReactionType: Int]
+ public let reactionCounts: [MessageReactionType: Int]
+ public let reactionGroups: [MessageReactionType: ChatMessageReactionGroup]
+ public let author: ChatUser
+ public let mentionedUsers: Set<ChatUser>
+ public let mentionedHere: Bool
+ public let mentionedChannel: Bool
+ public let mentionedGroups: Set<UserGroupMention>
+ public let mentionedRoles: [String]
+ public let threadParticipants: [ChatUser]
+ public var threadParticipantsCount: Int
+ public var attachmentCounts: [AttachmentType: Int]
+ public let latestReplies: [ChatMessage]
+ public let localState: LocalMessageState?
+ public let isFlaggedByCurrentUser: Bool
+ public let latestReactions: Set<ChatMessageReaction>
+ public let currentUserReactions: Set<ChatMessageReaction>
+ public var currentUserReactionsCount: Int
+ public let isSentByCurrentUser: Bool
+ public let pinDetails: MessagePinDetails?
+ public let translations: [TranslationLanguage: String]?
+ public let originalLanguage: TranslationLanguage?
+ public let moderationDetails: MessageModerationDetails?
+ public let readBy: Set<ChatUser>
+ public var readByCount: Int
+ public let poll: Poll?
+ public let sharedLocation: SharedLocation?
+ public let channelRole: MemberRole?
+
+
+ public func translatedText(for language: TranslationLanguage)-> String?
+ public func changing(text: String? = nil,type: MessageType? = nil,state: LocalMessageState? = nil,command: String? = nil,arguments: String? = nil,attachments: [AnyChatMessageAttachment]? = nil,translations: [TranslationLanguage: String]? = nil,originalLanguage: TranslationLanguage? = nil,moderationDetails: MessageModerationDetails? = nil,readBy: Set<ChatUser>? = nil,deletedAt: Date? = nil,extraData: [String: RawJSON]? = nil)-> ChatMessage
+ public func replacing(text: String?,extraData: [String: RawJSON]?,attachments: [AnyChatMessageAttachment]?)-> ChatMessage
+ public func replacing(text: String?,type: MessageType,state: LocalMessageState?,command: String?,arguments: String?,attachments: [AnyChatMessageAttachment]?,translations: [TranslationLanguage: String]?,originalLanguage: TranslationLanguage?,moderationDetails: MessageModerationDetails?,extraData: [String: RawJSON]?)-> ChatMessage
+ public final class ChatChannel: @unchecked Sendable
+
+ public let cid: ChannelId
+ public let name: String?
+ public let imageURL: URL?
+ public let lastMessageAt: Date?
+ public let createdAt: Date
+ public let updatedAt: Date
+ public let deletedAt: Date?
+ public let truncatedAt: Date?
+ public let isHidden: Bool
+ public let createdBy: ChatUser?
+ public let config: ChannelConfig
+ public let filterTags: Set<String>
+ public let ownCapabilities: Set<ChannelCapability>
+ public let isFrozen: Bool
+ public let isDisabled: Bool
+ public let isBlocked: Bool
+ public let memberCount: Int
+ public let messageCount: Int?
+ public let lastActiveMembers: [ChatChannelMember]
+ public let currentlyTypingUsers: Set<ChatUser>
+ public let membership: ChatChannelMember?
+ public var isArchived: Bool
+ public var isPinned: Bool
+ public let lastActiveWatchers: [ChatUser]
+ public let watcherCount: Int
+ public let team: TeamId?
+ public let unreadCount: ChannelUnreadCount
+ public let latestMessages: [ChatMessage]
+ public let lastMessageFromCurrentUser: ChatMessage?
+ public let pinnedMessages: [ChatMessage]
+ public let pendingMessages: [ChatMessage]
+ public let reads: [ChatChannelRead]
+ public let muteDetails: MuteDetails?
+ public var isMuted: Bool
+ public let cooldownDuration: Int
+ public let extraData: [String: RawJSON]
+ public let draftMessage: DraftMessage?
+ public let activeLiveLocations: [SharedLocation]
+ public let pushPreference: PushPreference?
+
+
+ public func replacing(name: String?,imageURL: URL?,extraData: [String: RawJSON]?)-> ChatChannel
+ public func changing(name: String? = nil,imageURL: URL? = nil,lastMessageAt: Date? = nil,createdAt: Date? = nil,deletedAt: Date? = nil,updatedAt: Date? = nil,truncatedAt: Date? = nil,isHidden: Bool? = nil,createdBy: ChatUser? = nil,config: ChannelConfig? = nil,filterTags: Set<String>? = nil,ownCapabilities: Set<ChannelCapability>? = nil,isFrozen: Bool? = nil,isDisabled: Bool? = nil,isBlocked: Bool? = nil,reads: [ChatChannelRead]? = nil,members: [ChatChannelMember]? = nil,membership: ChatChannelMember?? = nil,memberCount: Int? = nil,watchers: [ChatUser]? = nil,watcherCount: Int? = nil,team: TeamId? = nil,cooldownDuration: Int? = nil,pinnedMessages: [ChatMessage]? = nil,pushPreference: PushPreference? = nil,currentlyTypingUsers: Set<ChatUser>? = nil,extraData: [String: RawJSON]? = nil)-> ChatChannel
- public struct ChatMessage: Identifiable, Sendable
-
- public let id: MessageId
- public let cid: ChannelId?
- public let text: String
- public let type: MessageType
- public let command: String?
- public let createdAt: Date
- public let locallyCreatedAt: Date?
- public let updatedAt: Date
- public let deletedAt: Date?
- public let textUpdatedAt: Date?
- public let arguments: String?
- public let parentMessageId: MessageId?
- public let showReplyInChannel: Bool
- public let replyCount: Int
- public let extraData: [String: RawJSON]
- public var quotedMessage: ChatMessage?
- public let draftReply: DraftMessage?
- public let reminder: MessageReminderInfo?
- public let isBounced: Bool
- public let isSilent: Bool
- public let isShadowed: Bool
- public let deletedForMe: Bool
- public let reactionScores: [MessageReactionType: Int]
- public let reactionCounts: [MessageReactionType: Int]
- public let reactionGroups: [MessageReactionType: ChatMessageReactionGroup]
- public let author: ChatUser
- public let mentionedUsers: Set<ChatUser>
- public let mentionedHere: Bool
- public let mentionedChannel: Bool
- public let mentionedGroups: Set<UserGroupMention>
- public let mentionedRoles: [String]
- public let threadParticipants: [ChatUser]
- public var threadParticipantsCount: Int
- public var attachmentCounts: [AttachmentType: Int]
- public let latestReplies: [ChatMessage]
- public let localState: LocalMessageState?
- public let isFlaggedByCurrentUser: Bool
- public let latestReactions: Set<ChatMessageReaction>
- public let currentUserReactions: Set<ChatMessageReaction>
- public var currentUserReactionsCount: Int
- public let isSentByCurrentUser: Bool
- public let pinDetails: MessagePinDetails?
- public let translations: [TranslationLanguage: String]?
- public let originalLanguage: TranslationLanguage?
- public let moderationDetails: MessageModerationDetails?
- public let readBy: Set<ChatUser>
- public var readByCount: Int
- public let poll: Poll?
- public let sharedLocation: SharedLocation?
- public let channelRole: MemberRole?
-
-
- public func translatedText(for language: TranslationLanguage)-> String?
- public func changing(text: String? = nil,type: MessageType? = nil,state: LocalMessageState? = nil,command: String? = nil,arguments: String? = nil,attachments: [AnyChatMessageAttachment]? = nil,translations: [TranslationLanguage: String]? = nil,originalLanguage: TranslationLanguage? = nil,moderationDetails: MessageModerationDetails? = nil,readBy: Set<ChatUser>? = nil,deletedAt: Date? = nil,extraData: [String: RawJSON]? = nil)-> ChatMessage
- public func replacing(text: String?,extraData: [String: RawJSON]?,attachments: [AnyChatMessageAttachment]?)-> ChatMessage
- public func replacing(text: String?,type: MessageType,state: LocalMessageState?,command: String?,arguments: String?,attachments: [AnyChatMessageAttachment]?,translations: [TranslationLanguage: String]?,originalLanguage: TranslationLanguage?,moderationDetails: MessageModerationDetails?,extraData: [String: RawJSON]?)-> ChatMessage
- public struct ChatChannel: Sendable
+ public struct ChatChannel: Equatable, Sendable
- public let cid: ChannelId
+ public var lastActiveWatchersLimit
- public let name: String?
+ public var lastActiveMembersLimit
- public let imageURL: URL?
+ public var latestMessagesLimit
- public let lastMessageAt: Date?
- public let createdAt: Date
- public let updatedAt: Date
- public let deletedAt: Date?
- public let truncatedAt: Date?
- public let isHidden: Bool
- public let createdBy: ChatUser?
- public let config: ChannelConfig
- public let filterTags: Set<String>
- public let ownCapabilities: Set<ChannelCapability>
- public let isFrozen: Bool
- public let isDisabled: Bool
- public let isBlocked: Bool
- public let memberCount: Int
- public let messageCount: Int?
- public let lastActiveMembers: [ChatChannelMember]
- public let currentlyTypingUsers: Set<ChatUser>
- public internal var membership: ChatChannelMember?
- public var isArchived: Bool
- public var isPinned: Bool
- public let lastActiveWatchers: [ChatUser]
- public let watcherCount: Int
- public let team: TeamId?
- public let unreadCount: ChannelUnreadCount
- public let latestMessages: [ChatMessage]
- public let lastMessageFromCurrentUser: ChatMessage?
- public let pinnedMessages: [ChatMessage]
- public let pendingMessages: [ChatMessage]
- public let reads: [ChatChannelRead]
- public let muteDetails: MuteDetails?
- public var isMuted: Bool
- public let cooldownDuration: Int
- public let extraData: [String: RawJSON]
- public let draftMessage: DraftMessage?
- public let activeLiveLocations: [SharedLocation]
- public let pushPreference: PushPreference?
-
-
- public func replacing(name: String?,imageURL: URL?,extraData: [String: RawJSON]?)-> ChatChannel
- public func changing(name: String? = nil,imageURL: URL? = nil,lastMessageAt: Date? = nil,createdAt: Date? = nil,deletedAt: Date? = nil,updatedAt: Date? = nil,truncatedAt: Date? = nil,isHidden: Bool? = nil,createdBy: ChatUser? = nil,config: ChannelConfig? = nil,filterTags: Set<String>? = nil,ownCapabilities: Set<ChannelCapability>? = nil,isFrozen: Bool? = nil,isDisabled: Bool? = nil,isBlocked: Bool? = nil,reads: [ChatChannelRead]? = nil,members: [ChatChannelMember]? = nil,membership: ChatChannelMember? = nil,memberCount: Int? = nil,watchers: [ChatUser]? = nil,watcherCount: Int? = nil,team: TeamId? = nil,cooldownDuration: Int? = nil,pinnedMessages: [ChatMessage]? = nil,pushPreference: PushPreference? = nil,currentlyTypingUsers: Set<ChatUser>? = nil,extraData: [String: RawJSON]? = nil)-> ChatChannel |
SDK Size
|
StreamChat XCSize
Show 96 more objects
|
StreamChatUI XCSize
Show 53 more objects
|
|



🔗 Issue Links
🎯 Goal
Reduce the CPU cost of loading and updating the channel list, especially for channels with many members and long message histories. The work targets the database-to-model conversion path and repeated main-thread work observed while profiling the channel list.
📝 Summary
ChatChannelandChatMessageto final, immutable reference types to avoid repeated value copies during conversions.chatClientConfig.🛠 Implementation
The main change is making
ChatChannel/ChatMessagefinal classes, which removes a class of redundant copying in the DB observers and conversion helpers. The remaining commits each remove specific redundant work on the conversion / background-observer path (double model rebuilds, unnecessary message materialization past the truncation window, empty extra-data decoding, and a queue hop).🎨 Showcase
N/A — no user-facing UI changes.
🧪 Manual Testing Notes
Load a channel list where channels have many members and long histories, and confirm channel list synchronize/scroll no longer hangs the main thread. Behavior should be unchanged.
☑️ Contributor Checklist
docs-contentrepoSummary by CodeRabbit