Skip to content

Commit 0ecc1b4

Browse files
authored
Do not access NSManagedObject outside of NSManagedObjectContext in IdentifiablePayload (#3908)
1 parent 3fd7fe6 commit 0ecc1b4

File tree

3 files changed

+40
-10
lines changed

3 files changed

+40
-10
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6-
### 🔄 Changed
6+
## StreamChat
7+
### 🐞 Fixed
8+
- Possible fix for rare `thereIsNoSadnessLikeTheDeathOfOptimism` crash in CoreData [#3908](https://github.com/GetStream/stream-chat-swift/pull/3908)
79

810
# [4.95.1](https://github.com/GetStream/stream-chat-swift/releases/tag/4.95.1)
911
_December 18, 2025_

Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiablePayload.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,13 @@ extension IdentifiablePayload {
5858
guard let modelClass = modelClass, let keyPath = modelClass.idKeyPath else { continue }
5959

6060
let values = Array(identifiableValues)
61-
var results: [NSManagedObject]?
61+
nonisolated(unsafe) var modelMapping: [DatabaseId: NSManagedObjectID] = [:]
6262
context.performAndWait {
63-
results = modelClass.batchFetch(keyPath: keyPath, equalTo: values, context: context)
64-
}
65-
guard let results = results else { continue }
66-
67-
var modelMapping: [DatabaseId: NSManagedObjectID] = [:]
68-
results.forEach {
69-
if let id = modelClass.id(for: $0) {
70-
modelMapping[id] = $0.objectID
63+
let results = modelClass.batchFetch(keyPath: keyPath, equalTo: values, context: context)
64+
results.forEach {
65+
if let id = modelClass.id(for: $0) {
66+
modelMapping[id] = $0.objectID
67+
}
7168
}
7269
}
7370
cache[modelClass.className] = modelMapping

Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,37 @@ final class IdentifiablePayload_Tests: XCTestCase {
7474
XCTAssertEqual(cache["\(UserDTO.self)"]?.count, 50)
7575
XCTAssertEqual(cache["\(MessageReactionDTO.self)"]?.count, 1000)
7676
}
77+
78+
func test_concurrentPerform_getPayloadToModelIdMappings() {
79+
let database = DatabaseContainer_Spy()
80+
let channelList = createChannelList(
81+
channels: 5,
82+
users: 5,
83+
otherWatchers: 2,
84+
messagesPerChannel: 10,
85+
readCountsPerChannel: 2,
86+
messageReactionsPerChannel: 2
87+
)
88+
savePayload(payload: channelList, database: database)
89+
90+
let contexts = [database.writableContext, database.backgroundReadOnlyContext, database.stateLayerContext]
91+
let iterations = 2000
92+
var caches: [PreWarmedCache] = (0..<iterations).map { _ in [:] }
93+
DispatchQueue.concurrentPerform(iterations: iterations) { index in
94+
autoreleasepool {
95+
let context = contexts[index % contexts.count]
96+
caches[index] = channelList.getPayloadToModelIdMappings(context: context)
97+
}
98+
}
99+
100+
for cache in caches {
101+
XCTAssertEqual(cache.keys.count, 4)
102+
XCTAssertEqual(cache["\(ChannelDTO.self)"]?.count, 5)
103+
XCTAssertEqual(cache["\(MessageDTO.self)"]?.count, 50)
104+
XCTAssertEqual(cache["\(UserDTO.self)"]?.count, 7)
105+
XCTAssertEqual(cache["\(MessageReactionDTO.self)"]?.count, 200)
106+
}
107+
}
77108

78109
// Identifiable
79110

0 commit comments

Comments
 (0)