Skip to content

Commit

Permalink
Merge branch 'develop' into misc/github-issue-report
Browse files Browse the repository at this point in the history
  • Loading branch information
nuno-vieira authored Dec 20, 2024
2 parents 6d8497d + c65255f commit a0a40c9
Show file tree
Hide file tree
Showing 14 changed files with 165 additions and 86 deletions.
9 changes: 2 additions & 7 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
### 🔗 Issue Links

_Provide all Jira tickets and/or Github issues related to this PR, if applicable._
_Provide all Linear and/or Github issues related to this PR, if applicable._

### 🎯 Goal

Expand Down Expand Up @@ -33,9 +33,4 @@ _Explain how this change can be tested manually, if applicable._
- [ ] Changelog is updated with client-facing changes
- [ ] Changelog is updated with new localization keys
- [ ] New code is covered by unit tests
- [ ] Comparison screenshots added for visual changes
- [ ] Affected documentation updated (docusaurus, tutorial, CMS)

### 🎁 Meme

_Provide a funny gif or image that relates to your work on this pull request. (Optional)_
- [ ] Documentation has been updated in the `docs-content` repo
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

### 🔄 Changed
### ⚡ Performance
- Improve performance of accessing database model properties [#3534](https://github.com/GetStream/stream-chat-swift/pull/3534)
- Improve performance of model conversions with large extra data [#3534](https://github.com/GetStream/stream-chat-swift/pull/3534)

# [4.69.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.69.0)
_December 18, 2024_
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChat/Database/DTOs/ChannelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ extension ChatChannel {

let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.default.decode([String: RawJSON].self, from: dto.extraData)
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: dto.extraData)
} catch {
log.error(
"Failed to decode extra data for Channel with cid: <\(dto.cid)>, using default value instead. "
Expand Down
16 changes: 6 additions & 10 deletions Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,16 +224,12 @@ extension CurrentChatUser {

let user = dto.user

var extraData = [String: RawJSON]()
if !dto.user.extraData.isEmpty {
do {
extraData = try JSONDecoder.default.decode([String: RawJSON].self, from: dto.user.extraData)
} catch {
log.error(
"Failed to decode extra data for user with id: <\(dto.user.id)>, using default value instead. "
+ "Error: \(error)"
)
}
let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: dto.user.extraData)
} catch {
log.error("Failed to decode extra data for user with id: <\(dto.user.id)>, using default value instead. Error: \(error)")
extraData = [:]
}

let mutedUsers: [ChatUser] = try dto.mutedUsers.map { try $0.asModel() }
Expand Down
18 changes: 10 additions & 8 deletions Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ extension ChatChannelMember {

let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.default.decode([String: RawJSON].self, from: dto.user.extraData)
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: dto.user.extraData)
} catch {
log.error(
"Failed to decode extra data for user with id: <\(dto.user.id)>, using default value instead. "
Expand All @@ -198,13 +198,15 @@ extension ChatChannelMember {
extraData = [:]
}

var memberExtraData: [String: RawJSON] = [:]
if let dtoMemberExtraData = dto.extraData {
do {
memberExtraData = try JSONDecoder.default.decode([String: RawJSON].self, from: dtoMemberExtraData)
} catch {
memberExtraData = [:]
}
let memberExtraData: [String: RawJSON]
do {
memberExtraData = try JSONDecoder.stream.decodeCachedRawJSON(from: dto.extraData)
} catch {
log.error(
"Failed to decode extra data for channel member with id: <\(dto.user.id)>, using default value instead. "
+ "Error: \(error)"
)
memberExtraData = [:]
}

let role = dto.channelRoleRaw.flatMap { MemberRole(rawValue: $0) } ?? .member
Expand Down
39 changes: 13 additions & 26 deletions Sources/StreamChat/Database/DTOs/MessageDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1275,20 +1275,12 @@ extension MessageDTO {

/// Snapshots the current state of `MessageDTO` and returns its representation for the use in API calls.
func asRequestBody() -> MessageRequestBody {
var decodedExtraData: [String: RawJSON]

if let extraData = self.extraData {
do {
decodedExtraData = try JSONDecoder.default.decode([String: RawJSON].self, from: extraData)
} catch {
log.assertionFailure(
"Failed decoding saved extra data with error: \(error). This should never happen because"
+ "the extra data must be a valid JSON to be saved."
)
decodedExtraData = [:]
}
} else {
decodedExtraData = [:]
let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: self.extraData)
} catch {
log.assertionFailure("Failed decoding saved extra data with error: \(error). This should never happen because the extra data must be a valid JSON to be saved.")
extraData = [:]
}

let uploadedAttachments: [MessageAttachmentPayload] = attachments
Expand All @@ -1315,7 +1307,7 @@ extension MessageDTO {
pinned: pinned,
pinExpires: pinExpires?.bridgeDate,
pollId: poll?.id,
extraData: decodedExtraData
extraData: extraData
)
}

Expand Down Expand Up @@ -1365,17 +1357,12 @@ private extension ChatMessage {
moderationDetails = dto.moderationDetails.map { MessageModerationDetails(fromDTO: $0) }
textUpdatedAt = dto.textUpdatedAt?.bridgeDate

if let extraData = dto.extraData, !extraData.isEmpty {
do {
self.extraData = try JSONDecoder.default.decode([String: RawJSON].self, from: extraData)
} catch {
log
.error(
"Failed to decode extra data for Message with id: <\(dto.id)>, using default value instead. Error: \(error)"
)
self.extraData = [:]
}
} else {
do {
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: dto.extraData)
} catch {
log.error(
"Failed to decode extra data for Message with id: <\(dto.id)>, using default value instead. Error: \(error)"
)
extraData = [:]
}

Expand Down
19 changes: 7 additions & 12 deletions Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,12 @@ extension MessageReactionDTO {
func asModel() throws -> ChatMessageReaction {
try isNotDeleted()

let decodedExtraData: [String: RawJSON]

if let extraData = self.extraData, !extraData.isEmpty {
do {
decodedExtraData = try JSONDecoder.default.decode([String: RawJSON].self, from: extraData)
} catch {
log.error("Failed decoding saved extra data with error: \(error)")
decodedExtraData = [:]
}
} else {
decodedExtraData = [:]
let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: self.extraData)
} catch {
log.error("Failed decoding saved extra data with error: \(error)")
extraData = [:]
}

return try .init(
Expand All @@ -227,7 +222,7 @@ extension MessageReactionDTO {
createdAt: createdAt?.bridgeDate ?? .init(),
updatedAt: updatedAt?.bridgeDate ?? .init(),
author: user.asModel(),
extraData: decodedExtraData
extraData: extraData
)
}
}
13 changes: 8 additions & 5 deletions Sources/StreamChat/Database/DTOs/PollDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,14 @@ extension PollDTO {
func asModel() throws -> Poll {
try isNotDeleted()

var extraData: [String: RawJSON] = [:]
if let custom,
!custom.isEmpty,
let decoded = try? JSONDecoder.default.decode([String: RawJSON].self, from: custom) {
extraData = decoded
let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: custom)
} catch {
log.error(
"Failed to decode extra data for poll with id: <\(id)>, using default value instead. Error: \(error)"
)
extraData = [:]
}

let optionsArray = (options.array as? [PollOptionDTO]) ?? []
Expand Down
13 changes: 8 additions & 5 deletions Sources/StreamChat/Database/DTOs/PollOptionDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,14 @@ extension PollOptionDTO {
func asModel() throws -> PollOption {
try isNotDeleted()

var extraData: [String: RawJSON] = [:]
if let custom,
!custom.isEmpty,
let decoded = try? JSONDecoder.default.decode([String: RawJSON].self, from: custom) {
extraData = decoded
let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: custom)
} catch {
log.error(
"Failed to decode extra data for poll option with id: <\(id)>, using default value instead. Error: \(error)"
)
extraData = [:]
}
return PollOption(
id: id,
Expand Down
5 changes: 4 additions & 1 deletion Sources/StreamChat/Database/DTOs/ThreadDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,11 @@ extension ThreadDTO {

let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.default.decode([String: RawJSON].self, from: self.extraData)
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: self.extraData)
} catch {
log.error(
"Failed to decode extra data for thread with id: <\(parentMessageId)>, using default value instead. Error: \(error)"
)
extraData = [:]
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/StreamChat/Database/DTOs/UserDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ extension UserDTO {
func asRequestBody() -> UserRequestBody {
let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.default.decode([String: RawJSON].self, from: self.extraData)
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: self.extraData)
} catch {
log.assertionFailure(
"Failed decoding saved extra data with error: \(error). This should never happen because"
Expand Down Expand Up @@ -235,7 +235,7 @@ extension ChatUser {

let extraData: [String: RawJSON]
do {
extraData = try JSONDecoder.default.decode([String: RawJSON].self, from: dto.extraData)
extraData = try JSONDecoder.stream.decodeCachedRawJSON(from: dto.extraData)
} catch {
log.error(
"Failed to decode extra data for user with id: <\(dto.id)>, using default value instead. "
Expand Down
5 changes: 0 additions & 5 deletions Sources/StreamChat/Database/DatabaseContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,6 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
try actions(self.writableContext)
FetchCache.clear()

// Refresh the state by merging persistent state and local state for avoiding optimistic locking failure
for object in self.writableContext.updatedObjects {
self.writableContext.refresh(object, mergeChanges: true)
}

if self.writableContext.hasChanges {
log.debug("Context has changes. Saving.", subsystems: .database)
try self.writableContext.save()
Expand Down
57 changes: 55 additions & 2 deletions Sources/StreamChat/Utils/Codable+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation
final class StreamJSONDecoder: JSONDecoder, @unchecked Sendable {
let iso8601formatter: ISO8601DateFormatter
let dateCache: NSCache<NSString, NSDate>
let rawJSONCache: RawJSONCache

override convenience init() {
let iso8601formatter = ISO8601DateFormatter()
Expand All @@ -17,12 +18,21 @@ final class StreamJSONDecoder: JSONDecoder, @unchecked Sendable {
let dateCache = NSCache<NSString, NSDate>()
dateCache.countLimit = 5000 // We cache at most 5000 dates, which gives good enough performance

self.init(dateFormatter: iso8601formatter, dateCache: dateCache)
self.init(
dateFormatter: iso8601formatter,
dateCache: dateCache,
rawJSONCache: RawJSONCache(countLimit: 500)
)
}

init(dateFormatter: ISO8601DateFormatter, dateCache: NSCache<NSString, NSDate>) {
init(
dateFormatter: ISO8601DateFormatter,
dateCache: NSCache<NSString, NSDate>,
rawJSONCache: RawJSONCache
) {
iso8601formatter = dateFormatter
self.dateCache = dateCache
self.rawJSONCache = rawJSONCache

super.init()

Expand Down Expand Up @@ -50,6 +60,49 @@ final class StreamJSONDecoder: JSONDecoder, @unchecked Sendable {
}
}

extension StreamJSONDecoder {
class RawJSONCache {
private let storage: NSCache<NSNumber, BoxedRawJSON>

init(countLimit: Int) {
storage = NSCache()
storage.countLimit = countLimit
}

func rawJSON(forKey key: Int) -> [String: RawJSON]? {
storage.object(forKey: key as NSNumber)?.value
}

func setRawJSON(_ value: [String: RawJSON], forKey key: Int) {
storage.setObject(BoxedRawJSON(value: value), forKey: key as NSNumber)
}

final class BoxedRawJSON {
let value: [String: RawJSON]

init(value: [String: RawJSON]) {
self.value = value
}
}
}

/// A convenience method returning decoded RawJSON dictionary with caching enabled.
///
/// Extra data stored in models can be large, what can significantly slow
/// down DTO to model conversions. This function is a convenient way for
/// caching some of the data in DTO to model conversions.
func decodeCachedRawJSON(from data: Data?) throws -> [String: RawJSON] {
guard let data, !data.isEmpty else { return [:] }
let key = data.hashValue
if let value = rawJSONCache.rawJSON(forKey: key) {
return value
}
let rawJSON = try decode([String: RawJSON].self, from: data)
rawJSONCache.setRawJSON(rawJSON, forKey: key)
return rawJSON
}
}

extension JSONDecoder {
/// A default `JSONDecoder`.
static let `default`: JSONDecoder = stream
Expand Down
Loading

0 comments on commit a0a40c9

Please sign in to comment.