Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Sources/StreamChat/APIClient/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class APIClient: @unchecked Sendable {
/// - Parameters:
/// - endpoint: The `Endpoint` used to create the network request.
/// - completion: Called when the networking request is finished.
func request<Response: Decodable>(
func request<Response: Decodable & Sendable>(
endpoint: Endpoint<Response>,
completion: @escaping @Sendable (Result<Response, Error>) -> Void
) {
Expand All @@ -95,7 +95,7 @@ class APIClient: @unchecked Sendable {
/// - Parameters:
/// - endpoint: The `Endpoint` used to create the network request.
/// - completion: Called when the networking request is finished.
func recoveryRequest<Response: Decodable>(
func recoveryRequest<Response: Decodable & Sendable>(
endpoint: Endpoint<Response>,
completion: @escaping @Sendable (Result<Response, Error>) -> Void
) {
Expand All @@ -113,7 +113,7 @@ class APIClient: @unchecked Sendable {
/// - Parameters:
/// - endpoint: The `Endpoint` used to create the network request.
/// - completion: Called when the networking request is finished.
func unmanagedRequest<Response: Decodable>(
func unmanagedRequest<Response: Decodable & Sendable>(
endpoint: Endpoint<Response>,
completion: @escaping @Sendable (Result<Response, Error>) -> Void
) {
Expand All @@ -122,7 +122,7 @@ class APIClient: @unchecked Sendable {
)
}

private func operation<Response: Decodable>(
private func operation<Response: Decodable & Sendable>(
endpoint: Endpoint<Response>,
isRecoveryOperation: Bool,
completion: @escaping @Sendable (Result<Response, Error>) -> Void
Expand Down Expand Up @@ -192,7 +192,7 @@ class APIClient: @unchecked Sendable {
}
}

private func unmanagedOperation<Response: Decodable>(
private func unmanagedOperation<Response: Decodable & Sendable>(
endpoint: Endpoint<Response>,
completion: @escaping @Sendable (Result<Response, Error>) -> Void
) -> AsyncOperation {
Expand Down Expand Up @@ -222,7 +222,7 @@ class APIClient: @unchecked Sendable {
/// - Parameters:
/// - endpoint: The `Endpoint` used to create the network request.
/// - completion: Called when the networking request is finished.
private func executeRequest<Response: Decodable>(
private func executeRequest<Response: Decodable & Sendable>(
endpoint: Endpoint<Response>,
completion: @escaping @Sendable (Result<Response, Error>) -> Void
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,16 @@ extension IdentifiablePayload {
guard let modelClass = modelClass, let keyPath = modelClass.idKeyPath else { continue }

let values = Array(identifiableValues)
var results: [NSManagedObject]?
nonisolated(unsafe) var modelMapping: [DatabaseId: NSManagedObjectID] = [:]
context.performAndWait {
results = modelClass.batchFetch(keyPath: keyPath, equalTo: values, context: context)
}
guard let results = results else { continue }

var modelMapping: [DatabaseId: NSManagedObjectID] = [:]
results.forEach {
if let id = modelClass.id(for: $0) {
modelMapping[id] = $0.objectID
let results = modelClass.batchFetch(keyPath: keyPath, equalTo: values, context: context)
results.forEach {
if let id = modelClass.id(for: $0) {
modelMapping[id] = $0.objectID
}
Comment on lines -61 to +67
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was interesting because DTOs were leaking outside of context's queue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... Could this be causing this crash: #3905?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to fix this on v4 as well

}
}

cache[modelClass.className] = modelMapping
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChat/APIClient/RequestEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class DefaultRequestEncoder: RequestEncoder, @unchecked Sendable {
private let waiterTimeout: TimeInterval = 10
weak var connectionDetailsProviderDelegate: ConnectionDetailsProviderDelegate?

func encodeRequest<ResponsePayload: Decodable>(
func encodeRequest<ResponsePayload: Decodable & Sendable>(
for endpoint: Endpoint<ResponsePayload>,
completion: @escaping @Sendable (Result<URLRequest, Error>) -> Void
) {
Expand Down
18 changes: 12 additions & 6 deletions Sources/StreamChat/Audio/AudioSessionConfiguring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring, @unchecked S
mode: .spokenAudio,
policy: .default,
options: [
// It is deprecated, but for now we need to use it,
// since the newer ones are not available in Xcode 15.
.allowBluetooth
.allowBluetoothDevice
]
)
try activateSession()
Expand All @@ -100,9 +98,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring, @unchecked S
policy: .default,
options: [
.defaultToSpeaker,
// It is deprecated, but for now we need to use it,
// since the newer ones are not available in Xcode 15.
.allowBluetooth
.allowBluetoothDevice
]
)
try activateSession()
Expand Down Expand Up @@ -177,3 +173,13 @@ final class AudioSessionConfiguratorError: ClientError, @unchecked Sendable {
.init("No available audio inputs found.", file, line)
}
}

// MARK: -

extension AVAudioSession.CategoryOptions {
#if compiler(>=6.2)
static let allowBluetoothDevice: AVAudioSession.CategoryOptions = .allowBluetoothHFP
#else
static let allowBluetoothDevice: AVAudioSession.CategoryOptions = .allowBluetooth
#endif
}
2 changes: 1 addition & 1 deletion Sources/StreamChat/ChatClient+Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ extension ChatClient {
}
}

var timerType: TimerScheduling.Type = DefaultTimer.self
nonisolated(unsafe) var timerType: TimerScheduling.Type = DefaultTimer.self

var tokenExpirationRetryStrategy: RetryStrategy = DefaultRetryStrategy()

Expand Down
24 changes: 0 additions & 24 deletions Sources/StreamChat/Config/ChatClientConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,30 +115,6 @@ public struct ChatClientConfig: Sendable {
/// A component that can be used to change an attachment which was successfully uploaded.
public var uploadedAttachmentPostProcessor: UploadedAttachmentPostProcessor?

/// Returns max possible attachment size in bytes.
/// By default the value is taken from `CDNClient.maxAttachmentSize` type.
/// But it can be overridden by setting a value here.
@available(*, deprecated, message: "The max attachment size can now be set from the Stream's Dashboard App Settings. It supports setting a size limit per attachment type.")
public var maxAttachmentSize: Int64 {
// TODO: For v5 the maxAttachmentSize should be responsibility of the UI SDK.
// Since this is not even used in the StreamChat LLC SDK.
get {
if let overrideMaxAttachmentSize = self.overrideMaxAttachmentSize {
return overrideMaxAttachmentSize
} else if let customCDNClient = customCDNClient {
return type(of: customCDNClient).maxAttachmentSize
} else {
return StreamCDNClient.maxAttachmentSize
}
}
set {
overrideMaxAttachmentSize = newValue
}
}

/// Used to override the maxAttachmentSize, by setting the value in the config instead of relying on `CDNClient`.
private var overrideMaxAttachmentSize: Int64?
Comment on lines -118 to -140
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome 🗑️


/// Returns max number of attachments that can be attached to a message.
///
/// The current limit on the backend is `30`. You can only configure a value below `30`.
Expand Down
11 changes: 6 additions & 5 deletions Sources/StreamChat/Database/DatabaseContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,10 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
}
}

func readAndWait<T>(_ actions: (DatabaseSession) throws -> T) throws -> T {
func readAndWait<T>(_ actions: @Sendable (DatabaseSession) throws -> T) throws -> T where T: Sendable {
let context = backgroundReadOnlyContext
var result: T?
var readError: Error?
nonisolated(unsafe) var result: T?
nonisolated(unsafe) var readError: Error?
context.performAndWait {
do {
result = try actions(context)
Expand All @@ -289,7 +289,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
}

/// Removes all data from the local storage.
func removeAllData(completion: ((Error?) -> Void)? = nil) {
func removeAllData(completion: (@Sendable (Error?) -> Void)? = nil) {
let entityNames = managedObjectModel.entities.compactMap(\.name)
writableContext.perform { [weak self] in
let requests = entityNames
Expand Down Expand Up @@ -487,8 +487,9 @@ extension NSManagedObjectContext {
queue: nil
) { [weak self] notification in
guard let self else { return }
nonisolated(unsafe) let unsafeNotification = notification
self.performAndWait {
self.mergeChanges(fromContextDidSave: notification)
self.mergeChanges(fromContextDidSave: unsafeNotification)
// Keep the state clean after merging changes
guard self.hasChanges else { return }
self.perform {
Expand Down
3 changes: 0 additions & 3 deletions Sources/StreamChat/Models/ChatMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,6 @@ public extension ChatMessage {
}

extension ChatMessage: Hashable {
// swiftlint:disable cyclomatic_complexity
public static func == (lhs: Self, rhs: Self) -> Bool {
guard lhs.id == rhs.id else { return false }
guard lhs.localState == rhs.localState else { return false }
Expand Down Expand Up @@ -629,8 +628,6 @@ extension ChatMessage: Hashable {
return true
}

// swiftlint:enable cyclomatic_complexity

public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
Expand Down
20 changes: 20 additions & 0 deletions Sources/StreamChat/Models/Thread.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,23 @@ public struct ChatThread: Identifiable, Sendable {
/// The custom data of the thread.
public let extraData: [String: RawJSON]
}

extension ChatThread: Hashable {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it from UI

public static func == (lhs: ChatThread, rhs: ChatThread) -> Bool {
lhs.parentMessageId == rhs.parentMessageId &&
lhs.updatedAt == rhs.updatedAt &&
lhs.title == rhs.title &&
lhs.reads == rhs.reads &&
lhs.latestReplies == rhs.latestReplies &&
lhs.lastMessageAt == rhs.lastMessageAt &&
lhs.channel == rhs.channel &&
lhs.participantCount == rhs.participantCount &&
lhs.replyCount == rhs.replyCount &&
lhs.threadParticipants == rhs.threadParticipants &&
lhs.extraData == rhs.extraData
}

public func hash(into hasher: inout Hasher) {
hasher.combine(parentMessageId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ListResult: DatabaseObserverType {}
/// A CoreData store observer which immediately reports changes as soon as the store has been changed.
///
/// - Note: Requires the ``DatabaseContainer/stateLayerContext`` which is immediately synchronized.
final class StateLayerDatabaseObserver<ResultType: DatabaseObserverType, Item, DTO: NSManagedObject> {
final class StateLayerDatabaseObserver<ResultType: DatabaseObserverType, Item, DTO: NSManagedObject>: @unchecked Sendable {
private let changeAggregator: ListChangeAggregator<DTO, Item>
private let frc: NSFetchedResultsController<DTO>
let itemCreator: (DTO) throws -> Item
Expand Down Expand Up @@ -67,7 +67,7 @@ extension StateLayerDatabaseObserver where ResultType == EntityResult {
}

var item: Item? {
var item: Item?
nonisolated(unsafe) var item: Item?
context.performAndWait {
item = Self.makeEntity(
frc: frc,
Expand Down Expand Up @@ -154,7 +154,7 @@ extension StateLayerDatabaseObserver where ResultType == ListResult {
}

var items: [Item] {
var collection: [Item]!
nonisolated(unsafe) var collection: [Item]!
context.performAndWait {
// When we already have loaded items, reuse them, otherwise refetch all
let items = reuseItems ?? updateItems(nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Foundation
class AttachmentQueueUploader: Worker, @unchecked Sendable {
@Atomic private var pendingAttachmentIDs: Set<AttachmentId> = []

private let observer: StateLayerDatabaseObserver<ListResult, AttachmentDTO, AttachmentDTO>
private let observer: StateLayerDatabaseObserver<ListResult, AttachmentId, AttachmentDTO>
private let attachmentPostProcessor: UploadedAttachmentPostProcessor?
private let attachmentUpdater = AnyAttachmentUpdater()
private let attachmentStorage = AttachmentStorage()
Expand All @@ -35,8 +35,10 @@ class AttachmentQueueUploader: Worker, @unchecked Sendable {

init(database: DatabaseContainer, apiClient: APIClient, attachmentPostProcessor: UploadedAttachmentPostProcessor?) {
observer = StateLayerDatabaseObserver(
context: database.backgroundReadOnlyContext,
fetchRequest: AttachmentDTO.pendingUploadFetchRequest()
database: database,
fetchRequest: AttachmentDTO.pendingUploadFetchRequest(),
itemCreator: { $0.attachmentID ?? AttachmentId(cid: ChannelId(type: .messaging, id: ""), messageId: "", index: -1) },
itemReuseKeyPaths: nil
)

self.attachmentPostProcessor = attachmentPostProcessor
Expand Down Expand Up @@ -67,7 +69,7 @@ class AttachmentQueueUploader: Worker, @unchecked Sendable {
}
}

private func handleChanges(changes: [ListChange<AttachmentDTO>]) {
private func handleChanges(changes: [ListChange<AttachmentId>]) {
guard !changes.isEmpty else { return }

// Only start uploading attachment when inserted and it is present in pendingAttachmentIds
Expand Down Expand Up @@ -282,12 +284,12 @@ class AttachmentQueueUploader: Worker, @unchecked Sendable {
}
}

private extension Array where Element == ListChange<AttachmentDTO> {
private extension Array where Element == ListChange<AttachmentId> {
var attachmentIDs: [AttachmentId] {
compactMap {
switch $0 {
case let .insert(dto, _), let .update(dto, _):
return dto.attachmentID
case let .insert(id, _), let .update(id, _):
return id.messageId.isEmpty ? nil : id
case .move, .remove:
return nil
}
Expand Down
44 changes: 24 additions & 20 deletions Sources/StreamChat/Workers/Background/MessageSender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,23 @@ class MessageSender: Worker, @unchecked Sendable {
@Atomic private var sendingQueueByCid: [ChannelId: MessageSendingQueue] = [:]
private var continuations = [MessageId: CheckedContinuation<ChatMessage, Error>]()

private lazy var observer = StateLayerDatabaseObserver<ListResult, MessageDTO, MessageDTO>(
context: self.database.backgroundReadOnlyContext,
fetchRequest: MessageDTO
.messagesPendingSendFetchRequest()
private lazy var observer = StateLayerDatabaseObserver<ListResult, MessageSendingQueue.SendRequest, MessageDTO>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to convert earlier, also makes sure DTOs don't escape from context's thread

database: database,
fetchRequest: MessageDTO.messagesPendingSendFetchRequest(),
itemCreator: { dto in
let cid: ChannelId = {
if let rawValue = dto.channel?.cid, let cid = try? ChannelId(cid: rawValue) {
return cid
}
return ChannelId(type: .messaging, id: "")
}()
return .init(
messageId: dto.id,
cid: cid,
createdLocallyAt: (dto.locallyCreatedAt ?? dto.createdAt).bridgeDate
)
},
itemReuseKeyPaths: nil
)

private let sendingDispatchQueue: DispatchQueue = .init(
Expand Down Expand Up @@ -71,26 +84,17 @@ class MessageSender: Worker, @unchecked Sendable {
}
}

func handleChanges(changes: [ListChange<MessageDTO>]) {
private func handleChanges(changes: [ListChange<MessageSendingQueue.SendRequest>]) {
// Convert changes to a dictionary of requests by their cid
nonisolated(unsafe) var newRequests: [ChannelId: [MessageSendingQueue.SendRequest]] = [:]
changes.forEach { change in
switch change {
case .insert(let dto, index: _), .update(let dto, index: _):
database.backgroundReadOnlyContext.performAndWait {
guard let cid = dto.channel.map({ try? ChannelId(cid: $0.cid) }) else {
log.error("Skipping sending of the message \(dto.id) because the channel info is missing.")
return
}
// Create the array if it didn't exist
guard let cid = cid else { return }
newRequests[cid] = newRequests[cid] ?? []
newRequests[cid]!.append(.init(
messageId: dto.id,
cid: cid,
createdLocallyAt: (dto.locallyCreatedAt ?? dto.createdAt).bridgeDate
))
}
case .insert(let request, index: _), .update(let request, index: _):
// Create the array if it didn't exist
let cid = request.cid
guard !cid.id.isEmpty else { return }
newRequests[cid] = newRequests[cid] ?? []
newRequests[cid]!.append(request)
case .move, .remove:
break
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/StreamChat/Workers/ChannelListLinker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ final class ChannelListLinker: Sendable {
transform: { $0 as? ChannelVisibleEvent },
callback: { [weak self, databaseContainer] event in
let context = databaseContainer.backgroundReadOnlyContext
context.perform {
context.perform { [self] in
guard let channel = try? context.channel(cid: event.cid)?.asModel() else { return }
self?.linkChannelIfNeeded(channel)
}
Expand All @@ -74,7 +74,7 @@ final class ChannelListLinker: Sendable {

private func isInChannelList(
_ channel: ChatChannel,
completion: @escaping (_ isPresent: Bool, _ belongsToOtherQuery: Bool) -> Void
completion: @escaping @Sendable (_ isPresent: Bool, _ belongsToOtherQuery: Bool) -> Void
) {
let context = databaseContainer.backgroundReadOnlyContext
context.performAndWait { [weak self] in
Expand Down
Loading
Loading